Skip to content

Commit 05b9f8a

Browse files
authored
TPT-4278: python-sdk: Implement support for Reserved IP for IPv4 (#672)
1 parent c10fadc commit 05b9f8a

14 files changed

Lines changed: 866 additions & 23 deletions

conftest.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import os
2+
import sys
3+
4+
# Ensure the repo root is on sys.path so that `from test.unit.base import ...`
5+
# works regardless of which directory pytest is invoked from.
6+
sys.path.insert(0, os.path.dirname(__file__))

linode_api4/groups/linode.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,7 @@ def instance_create(
162162
interface_generation: Optional[Union[InterfaceGeneration, str]] = None,
163163
network_helper: Optional[bool] = None,
164164
maintenance_policy: Optional[str] = None,
165+
ipv4: Optional[List[str]] = None,
165166
**kwargs,
166167
):
167168
"""
@@ -336,6 +337,9 @@ def instance_create(
336337
:param maintenance_policy: The slug of the maintenance policy to apply during maintenance.
337338
If not provided, the default policy (linode/migrate) will be applied.
338339
:type maintenance_policy: str
340+
:param ipv4: A list of reserved IPv4 addresses to assign to this Instance.
341+
NOTE: Reserved IP feature may not currently be available to all users.
342+
:type ipv4: list[str]
339343
340344
:returns: A new Instance object, or a tuple containing the new Instance and
341345
the generated password.
@@ -373,6 +377,7 @@ def instance_create(
373377
"interfaces": interfaces,
374378
"interface_generation": interface_generation,
375379
"network_helper": network_helper,
380+
"ipv4": ipv4,
376381
}
377382

378383
params.update(kwargs)

linode_api4/groups/networking.py

Lines changed: 124 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from typing import Any, Dict, Optional, Union
1+
from typing import Any, Dict, List, Optional, Union
22

33
from linode_api4.errors import UnexpectedResponseError
44
from linode_api4.groups import Group
@@ -17,6 +17,8 @@
1717
Region,
1818
)
1919
from linode_api4.objects.base import _flatten_request_body_recursive
20+
from linode_api4.objects.networking import ReservedIPAddress, ReservedIPType
21+
from linode_api4.paginated_list import PaginatedList
2022
from linode_api4.util import drop_null_keys
2123

2224

@@ -328,29 +330,66 @@ def ips_assign(self, region, *assignments):
328330
},
329331
)
330332

331-
def ip_allocate(self, linode, public=True):
333+
def ip_allocate(
334+
self,
335+
linode: Optional[Union[Instance, int]] = None,
336+
public: bool = True,
337+
reserved: bool = False,
338+
region: Optional[Union[Region, str]] = None,
339+
) -> IPAddress:
332340
"""
333-
Allocates an IP to a Instance you own. Additional IPs must be requested
334-
by opening a support ticket first.
341+
Allocates an IP to an Instance you own, or reserves a new IP address.
342+
343+
When ``reserved`` is False (default), ``linode`` is required and an
344+
ephemeral IP is allocated and assigned to that Instance.
345+
346+
When ``reserved`` is True, either ``region`` or ``linode`` must be
347+
provided. Passing only ``region`` creates an unassigned reserved IP.
348+
Passing ``linode`` (with or without ``region``) creates a reserved IP
349+
in the Instance's region and assigns it to that Instance.
335350
336351
API Documentation: https://techdocs.akamai.com/linode-api/reference/post-allocate-ip
337352
338353
:param linode: The Instance to allocate the new IP for.
339354
:type linode: Instance or int
340355
:param public: If True, allocate a public IP address. Defaults to True.
341356
:type public: bool
357+
:param reserved: If True, reserve the new IP address.
358+
NOTE: Reserved IP feature may not currently be available to all users.
359+
:type reserved: bool
360+
:param region: The region for the reserved IP (required when reserved=True and linode is not set).
361+
NOTE: Reserved IP feature may not currently be available to all users.
362+
:type region: str or Region
342363
343364
:returns: The new IPAddress.
344365
:rtype: IPAddress
345366
"""
346-
result = self.client.post(
347-
"/networking/ips/",
348-
data={
349-
"linode_id": linode.id if isinstance(linode, Base) else linode,
350-
"type": "ipv4",
351-
"public": public,
352-
},
353-
)
367+
if not reserved and linode is None:
368+
raise ValueError("linode is required when reserved is False.")
369+
if reserved and linode is None and region is None:
370+
raise ValueError(
371+
"Either linode or region must be provided when reserved is True."
372+
)
373+
if not reserved and region is not None:
374+
raise ValueError("region is only valid when reserved is True.")
375+
376+
data = {
377+
"type": "ipv4",
378+
"public": public,
379+
}
380+
381+
if linode is not None:
382+
data["linode_id"] = (
383+
linode.id if isinstance(linode, Base) else linode
384+
)
385+
386+
if reserved:
387+
data["reserved"] = True
388+
389+
if region is not None:
390+
data["region"] = region.id if isinstance(region, Base) else region
391+
392+
result = self.client.post("/networking/ips/", data=data)
354393

355394
if not "address" in result:
356395
raise UnexpectedResponseError(
@@ -510,3 +549,76 @@ def delete_vlan(self, vlan, region):
510549
return False
511550

512551
return True
552+
553+
def reserved_ips(self, *filters) -> PaginatedList:
554+
"""
555+
Returns a list of reserved IPv4 addresses on your account.
556+
557+
NOTE: Reserved IP feature may not currently be available to all users.
558+
559+
API Documentation: https://techdocs.akamai.com/linode-api/reference/get-reserved-ips
560+
561+
:param filters: Any number of filters to apply to this query.
562+
See :doc:`Filtering Collections</linode_api4/objects/filtering>`
563+
for more details on filtering.
564+
565+
:returns: A list of reserved IP addresses on the account.
566+
:rtype: PaginatedList of ReservedIPAddress
567+
"""
568+
return self.client._get_and_filter(ReservedIPAddress, *filters)
569+
570+
def reserved_ip_create(
571+
self,
572+
region: Union[Region, str],
573+
tags: Optional[List[str]] = None,
574+
**kwargs,
575+
) -> ReservedIPAddress:
576+
"""
577+
Reserves a new IPv4 address in the given region.
578+
579+
NOTE: Reserved IP feature may not currently be available to all users.
580+
581+
API Documentation: https://techdocs.akamai.com/linode-api/reference/post-reserved-ip
582+
583+
:param region: The region in which to reserve the IP.
584+
:type region: str or Region
585+
:param tags: Tags to apply to the reserved IP.
586+
:type tags: list of str
587+
588+
:returns: The new reserved IP address.
589+
:rtype: ReservedIPAddress
590+
"""
591+
params = {
592+
"region": region.id if isinstance(region, Region) else region,
593+
}
594+
if tags is not None:
595+
params["tags"] = tags
596+
params.update(kwargs)
597+
598+
result = self.client.post("/networking/reserved/ips", data=params)
599+
600+
if "address" not in result:
601+
raise UnexpectedResponseError(
602+
"Unexpected response when reserving IP address!", json=result
603+
)
604+
605+
return ReservedIPAddress(self.client, result["address"], result)
606+
607+
def reserved_ip_types(self, *filters) -> PaginatedList:
608+
"""
609+
Returns a list of reserved IP types with pricing information.
610+
611+
NOTE: Reserved IP feature may not currently be available to all users.
612+
613+
API Documentation: https://techdocs.akamai.com/linode-api/reference/get-reserved-ip-types
614+
615+
:param filters: Any number of filters to apply to this query.
616+
See :doc:`Filtering Collections</linode_api4/objects/filtering>`
617+
for more details on filtering.
618+
619+
:returns: A list of reserved IP types.
620+
:rtype: PaginatedList of ReservedIPType
621+
"""
622+
return self.client._get_and_filter(
623+
ReservedIPType, *filters, endpoint="/networking/reserved/ips/types"
624+
)

linode_api4/groups/nodebalancer.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,20 +32,26 @@ def create(self, region, **kwargs):
3232
3333
:param region: The Region in which to create the NodeBalancer.
3434
:type region: Region or str
35+
:param ipv4: A reserved IPv4 address to assign to this NodeBalancer.
36+
NOTE: Reserved IP feature may not currently be available to all users.
37+
:type ipv4: str
3538
3639
:returns: The new NodeBalancer
3740
:rtype: NodeBalancer
3841
"""
42+
ipv4 = kwargs.pop("ipv4", None)
3943
params = {
4044
"region": region.id if isinstance(region, Base) else region,
4145
}
46+
if ipv4 is not None:
47+
params["ipv4"] = ipv4
4248
params.update(kwargs)
4349

4450
result = self.client.post("/nodebalancers", data=params)
4551

4652
if not "id" in result:
4753
raise UnexpectedResponseError(
48-
"Unexpected response when creating Nodebalaner!", json=result
54+
"Unexpected response when creating NodeBalancer!", json=result
4955
)
5056

5157
n = NodeBalancer(self.client, result["id"], result)

linode_api4/groups/tag.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,8 @@ def create(
3232
domains=None,
3333
nodebalancers=None,
3434
volumes=None,
35-
entities=[],
35+
entities=None,
36+
reserved_ipv4_addresses=None,
3637
):
3738
"""
3839
Creates a new Tag and optionally applies it to the given entities.
@@ -61,10 +62,14 @@ def create(
6162
:param volumes: A list of Volumes to apply this Tag to upon
6263
creation
6364
:type volumes: list of Volumes or list of int
65+
:param reserved_ipv4_addresses: A list of reserved IPv4 addresses to apply
66+
this Tag to upon creation.
67+
:type reserved_ipv4_addresses: list of str
6468
6569
:returns: The new Tag
6670
:rtype: Tag
6771
"""
72+
entities = entities or []
6873
linode_ids, nodebalancer_ids, domain_ids, volume_ids = [], [], [], []
6974

7075
# filter input into lists of ids
@@ -103,6 +108,7 @@ def create(
103108
"nodebalancers": nodebalancer_ids or None,
104109
"domains": domain_ids or None,
105110
"volumes": volume_ids or None,
111+
"reserved_ipv4_addresses": reserved_ipv4_addresses or None,
106112
}
107113

108114
result = self.client.post("/tags", data=params)

linode_api4/objects/linode.py

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1539,7 +1539,7 @@ def snapshot(self, label=None):
15391539
b = Backup(self._client, result["id"], self.id, result)
15401540
return b
15411541

1542-
def ip_allocate(self, public=False):
1542+
def ip_allocate(self, public=False, address=None):
15431543
"""
15441544
Allocates a new :any:`IPAddress` for this Instance. Additional public
15451545
IPs require justification, and you may need to open a :any:`SupportTicket`
@@ -1551,17 +1551,26 @@ def ip_allocate(self, public=False):
15511551
:param public: If the new IP should be public or private. Defaults to
15521552
private.
15531553
:type public: bool
1554+
:param address: A reserved IPv4 address to assign to this Instance instead
1555+
of allocating a new ephemeral IP. The address must be an
1556+
unassigned reserved IP owned by this account.
1557+
NOTE: Reserved IP feature may not currently be available to all users.
1558+
:type address: str
15541559
15551560
:returns: The new IPAddress
15561561
:rtype: IPAddress
15571562
"""
1563+
data = {
1564+
"type": "ipv4",
1565+
"public": public,
1566+
}
1567+
if address is not None:
1568+
data["address"] = address
1569+
15581570
result = self._client.post(
15591571
"{}/ips".format(Instance.api_endpoint),
15601572
model=self,
1561-
data={
1562-
"type": "ipv4",
1563-
"public": public,
1564-
},
1573+
data=data,
15651574
)
15661575

15671576
if not "address" in result:

linode_api4/objects/networking.py

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,20 @@ class InstanceIPNAT1To1(JSONObject):
5757
vpc_id: int = 0
5858

5959

60+
@dataclass
61+
class ReservedIPAssignedEntity(JSONObject):
62+
"""
63+
Represents the entity that a reserved IP is assigned to.
64+
65+
NOTE: Reserved IP feature may not currently be available to all users.
66+
"""
67+
68+
id: int = 0
69+
label: str = ""
70+
type: str = ""
71+
url: str = ""
72+
73+
6074
class IPAddress(Base):
6175
"""
6276
note:: This endpoint is in beta. This will only function if base_url is set to `https://api.linode.com/v4beta`.
@@ -90,6 +104,9 @@ class IPAddress(Base):
90104
"interface_id": Property(),
91105
"region": Property(slug_relationship=Region),
92106
"vpc_nat_1_1": Property(json_object=InstanceIPNAT1To1),
107+
"reserved": Property(mutable=True),
108+
"tags": Property(mutable=True, unordered=True),
109+
"assigned_entity": Property(json_object=ReservedIPAssignedEntity),
93110
}
94111

95112
@property
@@ -156,6 +173,40 @@ def delete(self):
156173
return True
157174

158175

176+
class ReservedIPAddress(Base):
177+
"""
178+
.. note:: This endpoint is in beta. This will only function if base_url is set to ``https://api.linode.com/v4beta``.
179+
180+
Represents a Linode Reserved IPv4 Address.
181+
182+
Update tags on a reserved IP by mutating the ``tags`` attribute and calling ``save()``.
183+
184+
NOTE: Reserved IP feature may not currently be available to all users.
185+
186+
API Documentation: https://techdocs.akamai.com/linode-api/reference/get-reserved-ip
187+
"""
188+
189+
api_endpoint = "/networking/reserved/ips/{address}"
190+
id_attribute = "address"
191+
192+
properties = {
193+
"address": Property(identifier=True),
194+
"gateway": Property(),
195+
"linode_id": Property(),
196+
"prefix": Property(),
197+
"public": Property(),
198+
"rdns": Property(),
199+
"region": Property(slug_relationship=Region),
200+
"reserved": Property(),
201+
"subnet_mask": Property(),
202+
"tags": Property(mutable=True, unordered=True),
203+
"type": Property(),
204+
"assigned_entity": Property(json_object=ReservedIPAssignedEntity),
205+
"interface_id": Property(),
206+
"vpc_nat_1_1": Property(json_object=InstanceIPNAT1To1),
207+
}
208+
209+
159210
@dataclass
160211
class VPCIPAddressIPv6(JSONObject):
161212
slaac_address: str = ""
@@ -424,3 +475,20 @@ class NetworkTransferPrice(Base):
424475
"region_prices": Property(json_object=RegionPrice),
425476
"transfer": Property(),
426477
}
478+
479+
480+
class ReservedIPType(Base):
481+
"""
482+
Represents a reserved IP type with pricing information.
483+
484+
NOTE: Reserved IP feature may not currently be available to all users.
485+
486+
API Documentation: https://techdocs.akamai.com/linode-api/reference/get-reserved-ip-types
487+
"""
488+
489+
properties = {
490+
"id": Property(identifier=True),
491+
"label": Property(),
492+
"price": Property(json_object=Price),
493+
"region_prices": Property(json_object=RegionPrice),
494+
}

0 commit comments

Comments
 (0)