Skip to content

Commit 062d9e0

Browse files
committed
TPT-4278 python-sdk: Implement support for Reserved IP for IPv4
1 parent c10fadc commit 062d9e0

File tree

14 files changed

+806
-21
lines changed

14 files changed

+806
-21
lines changed

conftest.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import sys
2+
import os
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: 104 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
Region,
1818
)
1919
from linode_api4.objects.base import _flatten_request_body_recursive
20+
from linode_api4.objects.networking import ReservedIPAddress, ReservedIPType
2021
from linode_api4.util import drop_null_keys
2122

2223

@@ -328,29 +329,53 @@ def ips_assign(self, region, *assignments):
328329
},
329330
)
330331

331-
def ip_allocate(self, linode, public=True):
332+
def ip_allocate(
333+
self, linode=None, public=True, reserved=False, region=None
334+
):
332335
"""
333-
Allocates an IP to a Instance you own. Additional IPs must be requested
334-
by opening a support ticket first.
336+
Allocates an IP to a Instance you own, or reserves a new IP address.
337+
338+
When ``reserved`` is False (default), ``linode`` is required and an
339+
ephemeral IP is allocated and assigned to that Instance.
340+
341+
When ``reserved`` is True, either ``region`` or ``linode`` must be
342+
provided. Passing only ``region`` creates an unassigned reserved IP.
343+
Passing ``linode`` (with or without ``region``) creates a reserved IP
344+
in the Instance's region and assigns it to that Instance.
335345
336346
API Documentation: https://techdocs.akamai.com/linode-api/reference/post-allocate-ip
337347
338348
:param linode: The Instance to allocate the new IP for.
339349
:type linode: Instance or int
340350
:param public: If True, allocate a public IP address. Defaults to True.
341351
:type public: bool
352+
:param reserved: If True, reserve the new IP address.
353+
NOTE: Reserved IP feature may not currently be available to all users.
354+
:type reserved: bool
355+
:param region: The region for the reserved IP (required when reserved=True and linode is not set).
356+
NOTE: Reserved IP feature may not currently be available to all users.
357+
:type region: str or Region
342358
343359
:returns: The new IPAddress.
344360
:rtype: IPAddress
345361
"""
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-
)
362+
data = {
363+
"type": "ipv4",
364+
"public": public,
365+
}
366+
367+
if linode is not None:
368+
data["linode_id"] = (
369+
linode.id if isinstance(linode, Base) else linode
370+
)
371+
372+
if reserved:
373+
data["reserved"] = True
374+
375+
if region is not None:
376+
data["region"] = region.id if isinstance(region, Base) else region
377+
378+
result = self.client.post("/networking/ips/", data=data)
354379

355380
if not "address" in result:
356381
raise UnexpectedResponseError(
@@ -510,3 +535,71 @@ def delete_vlan(self, vlan, region):
510535
return False
511536

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

linode_api4/groups/nodebalancer.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,21 +24,26 @@ def __call__(self, *filters):
2424
"""
2525
return self.client._get_and_filter(NodeBalancer, *filters)
2626

27-
def create(self, region, **kwargs):
27+
def create(self, region, ipv4=None, **kwargs):
2828
"""
2929
Creates a new NodeBalancer in the given Region.
3030
3131
API Documentation: https://techdocs.akamai.com/linode-api/reference/post-node-balancer
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
"""
3942
params = {
4043
"region": region.id if isinstance(region, Base) else region,
4144
}
45+
if ipv4 is not None:
46+
params["ipv4"] = ipv4
4247
params.update(kwargs)
4348

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

linode_api4/groups/tag.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ def create(
3232
domains=None,
3333
nodebalancers=None,
3434
volumes=None,
35+
reserved_ipv4_addresses=None,
3536
entities=[],
3637
):
3738
"""
@@ -61,6 +62,9 @@ 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
@@ -103,6 +107,7 @@ def create(
103107
"nodebalancers": nodebalancer_ids or None,
104108
"domains": domain_ids or None,
105109
"volumes": volume_ids or None,
110+
"reserved_ipv4_addresses": reserved_ipv4_addresses or None,
106111
}
107112

108113
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: 66 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,38 @@ 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+
}
206+
207+
159208
@dataclass
160209
class VPCIPAddressIPv6(JSONObject):
161210
slaac_address: str = ""
@@ -424,3 +473,20 @@ class NetworkTransferPrice(Base):
424473
"region_prices": Property(json_object=RegionPrice),
425474
"transfer": Property(),
426475
}
476+
477+
478+
class ReservedIPType(Base):
479+
"""
480+
Represents a reserved IP type with pricing information.
481+
482+
NOTE: Reserved IP feature may not currently be available to all users.
483+
484+
API Documentation: https://techdocs.akamai.com/linode-api/reference/get-reserved-iptype
485+
"""
486+
487+
properties = {
488+
"id": Property(identifier=True),
489+
"label": Property(),
490+
"price": Property(json_object=Price),
491+
"region_prices": Property(json_object=RegionPrice),
492+
}

linode_api4/objects/tag.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,15 @@
66
Property,
77
Volume,
88
)
9+
from linode_api4.objects.networking import ReservedIPAddress
910
from linode_api4.paginated_list import PaginatedList
1011

1112
CLASS_MAP = {
1213
"linode": Instance,
1314
"domain": Domain,
1415
"nodebalancer": NodeBalancer,
1516
"volume": Volume,
17+
"reserved_ipv4_address": ReservedIPAddress,
1618
}
1719

1820

@@ -124,7 +126,8 @@ def make_instance(cls, id, client, parent_id=None, json=None):
124126

125127
# discard the envelope
126128
real_json = json["data"]
127-
real_id = real_json["id"]
129+
id_attr = getattr(make_cls, "id_attribute", "id")
130+
real_id = real_json[id_attr]
128131

129132
# make the real object type
130133
return Base.make(

pytest.ini

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
[pytest]
2+
testpaths = test
3+
markers =
4+
smoke: mark a test as a smoke test
5+
flaky: mark a test as a flaky test for rerun
6+
python_files = *_test.py test_*.py

0 commit comments

Comments
 (0)