Skip to content

Enhanced Interfaces: Add support for Linode-related endpoints and fields #533

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 9 commits into
base: proj/enhanced-interfaces
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
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
56 changes: 45 additions & 11 deletions linode_api4/groups/linode.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
import base64
import os
from collections.abc import Iterable
from typing import Any, Dict, Optional, Union
from typing import Any, Dict, List, Optional, Union

from linode_api4.common import load_and_validate_keys
from linode_api4.errors import UnexpectedResponseError
from linode_api4.groups import Group
from linode_api4.objects import (
ConfigInterface,
Firewall,
Instance,
InstanceDiskEncryptionType,
Expand All @@ -21,8 +19,13 @@
from linode_api4.objects.linode import (
Backup,
InstancePlacementGroupAssignment,
InterfaceGeneration,
NetworkInterface,
_expand_placement_group_assignment,
)
from linode_api4.objects.linode_interfaces import (
LinodeInterfaceOptions,
)
from linode_api4.util import drop_null_keys


Expand Down Expand Up @@ -153,6 +156,13 @@ def instance_create(
int,
]
] = None,
interfaces: Optional[
List[
Union[LinodeInterfaceOptions, NetworkInterface, Dict[str, Any]],
]
] = None,
interface_generation: Optional[Union[InterfaceGeneration, str]] = None,
network_helper: Optional[bool] = None,
**kwargs,
):
"""
Expand Down Expand Up @@ -230,6 +240,30 @@ def instance_create(
"us-east",
backup=snapshot)
**Create an Instance with explicit interfaces:**
To create a new Instance with explicit interfaces, provide list of
LinodeInterfaceOptions objects or dicts to the "interfaces" field::
linode, password = client.linode.instance_create(
"g6-standard-1",
"us-mia",
image="linode/ubuntu24.04",
# This can be configured as an account-wide default
interface_generation=InterfaceGeneration.LINODE,
interfaces=[
LinodeInterfaceOptions(
default_route=LinodeInterfaceDefaultRouteOptions(
ipv4=True,
ipv6=True
),
public=LinodeInterfacePublicOptions
)
]
)
**Create an empty Instance**
If you want to create an empty Instance that you will configure manually,
Expand Down Expand Up @@ -293,9 +327,13 @@ def instance_create(
:type disk_encryption: InstanceDiskEncryptionType or str
:param interfaces: An array of Network Interfaces to add to this Linode’s Configuration Profile.
At least one and up to three Interface objects can exist in this array.
:type interfaces: list[ConfigInterface] or list[dict[str, Any]]
:type interfaces: List[LinodeInterfaceOptions], List[NetworkInterface], or List[dict[str, Any]]
:param placement_group: A Placement Group to create this Linode under.
:type placement_group: Union[InstancePlacementGroupAssignment, PlacementGroup, Dict[str, Any], int]
:param interface_generation: The generation of network interfaces this Linode uses.
:type interface_generation: InterfaceGeneration or str
:param network_helper: Whether this instance should have Network Helper enabled.
:type network_helper: bool
:returns: A new Instance object, or a tuple containing the new Instance and
the generated password.
Expand All @@ -311,13 +349,6 @@ def instance_create(
ret_pass = Instance.generate_root_password()
kwargs["root_pass"] = ret_pass

interfaces = kwargs.get("interfaces", None)
if interfaces is not None and isinstance(interfaces, Iterable):
kwargs["interfaces"] = [
i._serialize() if isinstance(i, ConfigInterface) else i
for i in interfaces
]
Comment on lines -314 to -319
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No longer necessary due to _flatten_request_body_recursive(drop_null_keys(params)) below


params = {
"type": ltype,
"region": region,
Expand All @@ -336,6 +367,9 @@ def instance_create(
if placement_group
else None
),
"interfaces": interfaces,
"interface_generation": interface_generation,
"network_helper": network_helper,
}

params.update(kwargs)
Expand Down
58 changes: 58 additions & 0 deletions linode_api4/groups/networking.py
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,64 @@ def ipv6_ranges(self, *filters):
"""
return self.client._get_and_filter(IPv6Range, *filters)

def ipv6_range_allocate(
self,
prefix_length: int,
route_target: Optional[str] = None,
linode: Optional[Union[Instance, int]] = None,
**kwargs,
) -> IPv6Range:
Comment on lines +183 to +189
Copy link
Contributor Author

@lgarber-akamai lgarber-akamai May 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This endpoint wasn't added as part of Enhanced Interfaces but it was oddly missing from the SDK. I added it here to unblock some of the IPv6 interface test cases below.

Let me know if this is worth splitting out into a separate PR 🙂

"""
Creates an IPv6 Range and assigns it based on the provided Linode or route target IPv6 SLAAC address.

API Documentation: https://techdocs.akamai.com/linode-api/reference/post-ipv6-range

Create an IPv6 range assigned to a Linode by ID::

range = client.networking.ipv6_range_allocate(64, linode_id=123)


Create an IPv6 range assigned to a Linode by SLAAC::

range = client.networking.ipv6_range_allocate(
64,
route_target=instance.ipv6.split("/")[0]
)

:param prefix_length: The prefix length of the IPv6 range.
:type prefix_length: int
:param route_target: The IPv6 SLAAC address to assign this range to. Required if linode is not specified.
:type route_target: str
:param linode: The ID of the Linode to assign this range to.
The SLAAC address for the provided Linode is used as the range's route_target.
Required if linode is not specified.
:type linode: Instance or int

:returns: The new IPAddress.
:rtype: IPAddress
"""

params = {
"prefix_length": prefix_length,
"route_target": route_target,
"linode_id": linode,
}

params.update(**kwargs)

result = self.client.post(
"/networking/ipv6/ranges",
data=drop_null_keys(_flatten_request_body_recursive(params)),
)

if not "range" in result:
raise UnexpectedResponseError(
"Unexpected response when allocating IPv6 range!", json=result
)

result = IPv6Range(self.client, result["range"], result)
return result

def ipv6_pools(self, *filters):
"""
Returns a list of IPv6 pools on this account.
Expand Down
1 change: 1 addition & 0 deletions linode_api4/objects/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from .region import Region
from .image import Image
from .linode import *
from .linode_interfaces import *
from .volume import *
from .domain import *
from .account import *
Expand Down
1 change: 1 addition & 0 deletions linode_api4/objects/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,7 @@ def __setattr__(self, name, value):
"""
Enforces allowing editing of only Properties defined as mutable
"""

if name in type(self).properties.keys():
if not type(self).properties[name].mutable:
raise AttributeError(
Expand Down
Loading