Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import sys
import os
Comment thread
mgwoj marked this conversation as resolved.
Outdated

# Ensure the repo root is on sys.path so that `from test.unit.base import ...`
# works regardless of which directory pytest is invoked from.
sys.path.insert(0, os.path.dirname(__file__))
5 changes: 5 additions & 0 deletions linode_api4/groups/linode.py
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,7 @@ def instance_create(
interface_generation: Optional[Union[InterfaceGeneration, str]] = None,
network_helper: Optional[bool] = None,
maintenance_policy: Optional[str] = None,
ipv4: Optional[List[str]] = None,
**kwargs,
):
"""
Expand Down Expand Up @@ -336,6 +337,9 @@ def instance_create(
:param maintenance_policy: The slug of the maintenance policy to apply during maintenance.
If not provided, the default policy (linode/migrate) will be applied.
:type maintenance_policy: str
:param ipv4: A list of reserved IPv4 addresses to assign to this Instance.
NOTE: Reserved IP feature may not currently be available to all users.
:type ipv4: list[str]

:returns: A new Instance object, or a tuple containing the new Instance and
the generated password.
Expand Down Expand Up @@ -373,6 +377,7 @@ def instance_create(
"interfaces": interfaces,
"interface_generation": interface_generation,
"network_helper": network_helper,
"ipv4": ipv4,
}

params.update(kwargs)
Expand Down
115 changes: 104 additions & 11 deletions linode_api4/groups/networking.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
Region,
)
from linode_api4.objects.base import _flatten_request_body_recursive
from linode_api4.objects.networking import ReservedIPAddress, ReservedIPType
from linode_api4.util import drop_null_keys


Expand Down Expand Up @@ -328,29 +329,53 @@ def ips_assign(self, region, *assignments):
},
)

def ip_allocate(self, linode, public=True):
def ip_allocate(
Comment thread
yec-akamai marked this conversation as resolved.
self, linode=None, public=True, reserved=False, region=None
):
"""
Allocates an IP to a Instance you own. Additional IPs must be requested
by opening a support ticket first.
Allocates an IP to a Instance you own, or reserves a new IP address.
Comment thread
mgwoj marked this conversation as resolved.
Outdated

When ``reserved`` is False (default), ``linode`` is required and an
ephemeral IP is allocated and assigned to that Instance.

When ``reserved`` is True, either ``region`` or ``linode`` must be
provided. Passing only ``region`` creates an unassigned reserved IP.
Passing ``linode`` (with or without ``region``) creates a reserved IP
in the Instance's region and assigns it to that Instance.

API Documentation: https://techdocs.akamai.com/linode-api/reference/post-allocate-ip

:param linode: The Instance to allocate the new IP for.
:type linode: Instance or int
:param public: If True, allocate a public IP address. Defaults to True.
:type public: bool
:param reserved: If True, reserve the new IP address.
NOTE: Reserved IP feature may not currently be available to all users.
:type reserved: bool
:param region: The region for the reserved IP (required when reserved=True and linode is not set).
NOTE: Reserved IP feature may not currently be available to all users.
:type region: str or Region

:returns: The new IPAddress.
:rtype: IPAddress
"""
result = self.client.post(
"/networking/ips/",
data={
"linode_id": linode.id if isinstance(linode, Base) else linode,
"type": "ipv4",
"public": public,
},
)
data = {
"type": "ipv4",
"public": public,
}

Comment thread
mgwoj marked this conversation as resolved.
if linode is not None:
data["linode_id"] = (
linode.id if isinstance(linode, Base) else linode
)

if reserved:
data["reserved"] = True

if region is not None:
data["region"] = region.id if isinstance(region, Base) else region

result = self.client.post("/networking/ips/", data=data)

if not "address" in result:
raise UnexpectedResponseError(
Expand Down Expand Up @@ -510,3 +535,71 @@ def delete_vlan(self, vlan, region):
return False

return True

def reserved_ips(self, *filters):
"""
Returns a list of reserved IPv4 addresses on your account.

NOTE: Reserved IP feature may not currently be available to all users.

API Documentation: https://techdocs.akamai.com/linode-api/reference/get-reserved-ips

:param filters: Any number of filters to apply to this query.
See :doc:`Filtering Collections</linode_api4/objects/filtering>`
for more details on filtering.

:returns: A list of reserved IP addresses on the account.
:rtype: PaginatedList of ReservedIPAddress
"""
return self.client._get_and_filter(ReservedIPAddress, *filters)

def reserved_ip_create(self, region, tags=None, **kwargs):
"""
Reserves a new IPv4 address in the given region.

NOTE: Reserved IP feature may not currently be available to all users.

API Documentation: https://techdocs.akamai.com/linode-api/reference/post-reserve-ip
Comment thread
mgwoj marked this conversation as resolved.
Outdated

:param region: The region in which to reserve the IP.
:type region: str or Region
:param tags: Tags to apply to the reserved IP.
:type tags: list of str

:returns: The new reserved IP address.
:rtype: ReservedIPAddress
"""
params = {
"region": region.id if isinstance(region, Region) else region,
}
if tags is not None:
params["tags"] = tags
params.update(kwargs)

result = self.client.post("/networking/reserved/ips", data=params)

if "address" not in result:
raise UnexpectedResponseError(
"Unexpected response when reserving IP address!", json=result
)

return ReservedIPAddress(self.client, result["address"], result)

def reserved_ip_types(self, *filters):
"""
Returns a list of reserved IP types with pricing information.

NOTE: Reserved IP feature may not currently be available to all users.

API Documentation: https://techdocs.akamai.com/linode-api/reference/get-reserved-iptypes
Comment thread
mgwoj marked this conversation as resolved.
Outdated

:param filters: Any number of filters to apply to this query.
See :doc:`Filtering Collections</linode_api4/objects/filtering>`
for more details on filtering.

:returns: A list of reserved IP types.
:rtype: PaginatedList of ReservedIPType
"""
return self.client._get_and_filter(
ReservedIPType, *filters, endpoint="/networking/reserved/ips/types"
)
7 changes: 6 additions & 1 deletion linode_api4/groups/nodebalancer.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,21 +24,26 @@ def __call__(self, *filters):
"""
return self.client._get_and_filter(NodeBalancer, *filters)

def create(self, region, **kwargs):
def create(self, region, ipv4=None, **kwargs):
Comment thread
mgwoj marked this conversation as resolved.
Outdated
"""
Creates a new NodeBalancer in the given Region.

Comment thread
mgwoj marked this conversation as resolved.
Outdated
API Documentation: https://techdocs.akamai.com/linode-api/reference/post-node-balancer

:param region: The Region in which to create the NodeBalancer.
:type region: Region or str
:param ipv4: A reserved IPv4 address to assign to this NodeBalancer.
NOTE: Reserved IP feature may not currently be available to all users.
:type ipv4: str

:returns: The new NodeBalancer
:rtype: NodeBalancer
"""
params = {
"region": region.id if isinstance(region, Base) else region,
}
if ipv4 is not None:
params["ipv4"] = ipv4
params.update(kwargs)

result = self.client.post("/nodebalancers", data=params)
Expand Down
5 changes: 5 additions & 0 deletions linode_api4/groups/tag.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ def create(
domains=None,
nodebalancers=None,
volumes=None,
reserved_ipv4_addresses=None,
entities=[],
):
Comment thread
mgwoj marked this conversation as resolved.
Outdated
"""
Expand Down Expand Up @@ -61,6 +62,9 @@ def create(
:param volumes: A list of Volumes to apply this Tag to upon
creation
:type volumes: list of Volumes or list of int
:param reserved_ipv4_addresses: A list of reserved IPv4 addresses to apply
this Tag to upon creation.
:type reserved_ipv4_addresses: list of str

:returns: The new Tag
:rtype: Tag
Expand Down Expand Up @@ -103,6 +107,7 @@ def create(
"nodebalancers": nodebalancer_ids or None,
"domains": domain_ids or None,
"volumes": volume_ids or None,
"reserved_ipv4_addresses": reserved_ipv4_addresses or None,
}

result = self.client.post("/tags", data=params)
Expand Down
19 changes: 14 additions & 5 deletions linode_api4/objects/linode.py
Original file line number Diff line number Diff line change
Expand Up @@ -1539,7 +1539,7 @@ def snapshot(self, label=None):
b = Backup(self._client, result["id"], self.id, result)
return b

def ip_allocate(self, public=False):
def ip_allocate(self, public=False, address=None):
"""
Allocates a new :any:`IPAddress` for this Instance. Additional public
IPs require justification, and you may need to open a :any:`SupportTicket`
Expand All @@ -1551,17 +1551,26 @@ def ip_allocate(self, public=False):
:param public: If the new IP should be public or private. Defaults to
private.
:type public: bool
:param address: A reserved IPv4 address to assign to this Instance instead
of allocating a new ephemeral IP. The address must be an
unassigned reserved IP owned by this account.
NOTE: Reserved IP feature may not currently be available to all users.
:type address: str

:returns: The new IPAddress
:rtype: IPAddress
"""
data = {
"type": "ipv4",
"public": public,
}
if address is not None:
data["address"] = address

result = self._client.post(
"{}/ips".format(Instance.api_endpoint),
model=self,
data={
"type": "ipv4",
"public": public,
},
data=data,
)

if not "address" in result:
Expand Down
66 changes: 66 additions & 0 deletions linode_api4/objects/networking.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,20 @@ class InstanceIPNAT1To1(JSONObject):
vpc_id: int = 0


@dataclass
class ReservedIPAssignedEntity(JSONObject):
"""
Represents the entity that a reserved IP is assigned to.

NOTE: Reserved IP feature may not currently be available to all users.
"""

id: int = 0
label: str = ""
type: str = ""
url: str = ""


class IPAddress(Base):
"""
note:: This endpoint is in beta. This will only function if base_url is set to `https://api.linode.com/v4beta`.
Expand Down Expand Up @@ -90,6 +104,9 @@ class IPAddress(Base):
"interface_id": Property(),
"region": Property(slug_relationship=Region),
"vpc_nat_1_1": Property(json_object=InstanceIPNAT1To1),
"reserved": Property(mutable=True),
"tags": Property(mutable=True, unordered=True),
"assigned_entity": Property(json_object=ReservedIPAssignedEntity),
}

@property
Expand Down Expand Up @@ -156,6 +173,38 @@ def delete(self):
return True


class ReservedIPAddress(Base):
"""
.. note:: This endpoint is in beta. This will only function if base_url is set to ``https://api.linode.com/v4beta``.

Represents a Linode Reserved IPv4 Address.

Update tags on a reserved IP by mutating the ``tags`` attribute and calling ``save()``.

NOTE: Reserved IP feature may not currently be available to all users.

API Documentation: https://techdocs.akamai.com/linode-api/reference/get-reserved-ip
"""

api_endpoint = "/networking/reserved/ips/{address}"
id_attribute = "address"

properties = {
"address": Property(identifier=True),
"gateway": Property(),
"linode_id": Property(),
"prefix": Property(),
"public": Property(),
"rdns": Property(),
"region": Property(slug_relationship=Region),
"reserved": Property(),
"subnet_mask": Property(),
"tags": Property(mutable=True, unordered=True),
"type": Property(),
"assigned_entity": Property(json_object=ReservedIPAssignedEntity),
}
Comment thread
mgwoj marked this conversation as resolved.


@dataclass
class VPCIPAddressIPv6(JSONObject):
slaac_address: str = ""
Expand Down Expand Up @@ -424,3 +473,20 @@ class NetworkTransferPrice(Base):
"region_prices": Property(json_object=RegionPrice),
"transfer": Property(),
}


class ReservedIPType(Base):
"""
Represents a reserved IP type with pricing information.

NOTE: Reserved IP feature may not currently be available to all users.

API Documentation: https://techdocs.akamai.com/linode-api/reference/get-reserved-iptype
Comment thread
mgwoj marked this conversation as resolved.
Outdated
"""

properties = {
"id": Property(identifier=True),
"label": Property(),
"price": Property(json_object=Price),
"region_prices": Property(json_object=RegionPrice),
}
5 changes: 4 additions & 1 deletion linode_api4/objects/tag.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,15 @@
Property,
Volume,
)
from linode_api4.objects.networking import ReservedIPAddress
from linode_api4.paginated_list import PaginatedList

CLASS_MAP = {
"linode": Instance,
"domain": Domain,
"nodebalancer": NodeBalancer,
"volume": Volume,
"reserved_ipv4_address": ReservedIPAddress,
}


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

# discard the envelope
real_json = json["data"]
real_id = real_json["id"]
id_attr = getattr(make_cls, "id_attribute", "id")
real_id = real_json[id_attr]

# make the real object type
return Base.make(
Expand Down
6 changes: 6 additions & 0 deletions pytest.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
[pytest]
testpaths = test
markers =
smoke: mark a test as a smoke test
flaky: mark a test as a flaky test for rerun
python_files = *_test.py test_*.py
Comment thread
mgwoj marked this conversation as resolved.
Outdated
Loading
Loading