Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
82a2772
Enhanced Interfaces: Add support for Firewall templates (#529)
lgarber-akamai Apr 18, 2025
f42c79f
Enhanced Interfaces: Add account-related fields (#525)
lgarber-akamai Apr 22, 2025
9ce77ef
Enhanced Interfaces: Implement endpoints & fields related to VPCs and…
lgarber-akamai Apr 25, 2025
c8fa2d5
Merge branch 'dev' of github.com:linode/linode_api4-python into proj/…
lgarber-akamai Apr 28, 2025
5727f10
Merge branch 'dev' into proj/enhanced-interfaces
zliang-akamai Apr 30, 2025
6e9a204
Enhanced Interfaces: Add support for Linode-related endpoints and fie…
lgarber-akamai May 20, 2025
24dd9e4
Merge branch 'dev' into proj/enhanced-interfaces
zliang-akamai May 21, 2025
c6456f0
Merge branch 'dev' into proj/enhanced-interfaces
zliang-akamai May 28, 2025
06f8a5f
Enable `include_none_values` in FirewallSettingsDefaultFirewallIDs (#…
zliang-akamai May 28, 2025
221afdc
Linode Interfaces: Allow specifying ExplicitNullValue for LinodeInter…
lgarber-akamai Jun 25, 2025
139e6b5
Merge branch 'dev' into proj/enhanced-interfaces
zliang-akamai Jul 8, 2025
ee5d84f
Merge branch 'dev' into proj/enhanced-interfaces
zliang-akamai Aug 21, 2025
9a746ff
Merge branch 'dev' into proj/enhanced-interfaces
zliang-akamai Sep 16, 2025
f7356f4
Fix
zliang-akamai Sep 16, 2025
18a5afa
Merge branch 'dev' into proj/enhanced-interfaces
zliang-akamai Sep 16, 2025
9cb0d68
Update test/fixtures/linode_instances.json
zliang-akamai Sep 16, 2025
77d438c
Update test/unit/objects/linode_test.py
zliang-akamai Sep 16, 2025
a4053dc
Update test/integration/conftest.py
zliang-akamai Sep 16, 2025
6bd29cf
Update linode_api4/objects/linode.py
zliang-akamai Sep 16, 2025
919fd1f
More fixes
zliang-akamai Sep 16, 2025
ee045c4
lint
zliang-akamai Sep 16, 2025
f821acd
Apply suggestion from @Copilot
zliang-akamai Sep 16, 2025
3fa5926
Apply suggestion from @Copilot
zliang-akamai Sep 17, 2025
bc8ad19
Remove unnecessary local imports
zliang-akamai Sep 17, 2025
9591877
Merge branch 'dev' into proj/enhanced-interfaces
zliang-akamai Sep 17, 2025
1bd6d70
Fix IPv6 addresses
zliang-akamai Sep 17, 2025
2aa2846
Apply suggestion from @Copilot
zliang-akamai Sep 17, 2025
c096474
Update test/unit/objects/networking_test.py
zliang-akamai Sep 17, 2025
498fa3c
Update test/unit/objects/linode_test.py
zliang-akamai Sep 17, 2025
5c3c7ce
Merge branch 'dev' into proj/enhanced-interfaces
zliang-akamai Sep 22, 2025
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,
maintenance_policy: Optional[str] = None,
**kwargs,
):
Expand Down Expand Up @@ -231,6 +241,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 @@ -294,9 +328,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
:param maintenance_policy: The slug of the maintenance policy to apply during maintenance.
If not provided, the default policy (linode/migrate) will be applied.
NOTE: This field is in beta and may only
Expand All @@ -317,13 +355,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
]

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

params.update(kwargs)
Expand Down
120 changes: 118 additions & 2 deletions linode_api4/groups/networking.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,23 @@
from typing import Any, Dict, Optional, Union

from linode_api4.errors import UnexpectedResponseError
from linode_api4.groups import Group
from linode_api4.objects import (
VLAN,
Base,
Firewall,
FirewallCreateDevicesOptions,
FirewallSettings,
FirewallTemplate,
Instance,
IPAddress,
IPv6Pool,
IPv6Range,
NetworkTransferPrice,
Region,
)
from linode_api4.objects.base import _flatten_request_body_recursive
from linode_api4.util import drop_null_keys


class NetworkingGroup(Group):
Expand All @@ -33,7 +40,15 @@ def firewalls(self, *filters):
"""
return self.client._get_and_filter(Firewall, *filters)

def firewall_create(self, label, rules, **kwargs):
def firewall_create(
self,
label: str,
rules: Dict[str, Any],
devices: Optional[
Union[FirewallCreateDevicesOptions, Dict[str, Any]]
] = None,
**kwargs,
):
"""
Creates a new Firewall, either in the given Region or
attached to the given Instance.
Expand All @@ -44,6 +59,8 @@ def firewall_create(self, label, rules, **kwargs):
:type label: str
:param rules: The rules to apply to the new Firewall. For more information on Firewall rules, see our `Firewalls Documentation`_.
:type rules: dict
:param devices: Represents devices to create created alongside a Linode Firewall.
:type devices: Optional[Union[FirewallCreateDevicesOptions, Dict[str, Any]]]

:returns: The new Firewall.
:rtype: Firewall
Expand Down Expand Up @@ -81,10 +98,14 @@ def firewall_create(self, label, rules, **kwargs):
params = {
"label": label,
"rules": rules,
"devices": devices,
}
params.update(kwargs)

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

if not "id" in result:
raise UnexpectedResponseError(
Expand All @@ -94,6 +115,43 @@ def firewall_create(self, label, rules, **kwargs):
f = Firewall(self.client, result["id"], result)
return f

def firewall_templates(self, *filters):
"""
Returns a list of Firewall Templates available to the current user.

API Documentation: https://techdocs.akamai.com/linode-api/reference/get-firewall-templates

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

: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 Firewall Templates available to the current user.
:rtype: PaginatedList of FirewallTemplate
"""
return self.client._get_and_filter(FirewallTemplate, *filters)

def firewall_settings(self) -> FirewallSettings:
"""
Returns an object representing the Linode Firewall settings for the current user.

API Documentation: https://techdocs.akamai.com/linode-api/reference/get-firewall-settings

NOTE: This feature may not currently be available to all users.
:returns: An object representing the Linode Firewall settings for the current user.
:rtype: FirewallSettings
"""
result = self.client.get("/networking/firewalls/settings")

if "default_firewall_ids" not in result:
raise UnexpectedResponseError(
"Unexpected response when getting firewall settings!",
json=result,
)

return FirewallSettings(self.client, None, result)

def ips(self, *filters):
"""
Returns a list of IP addresses on this account, excluding private addresses.
Expand Down Expand Up @@ -124,6 +182,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:
"""
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
20 changes: 20 additions & 0 deletions linode_api4/objects/account.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
from linode_api4.objects.networking import Firewall
from linode_api4.objects.nodebalancer import NodeBalancer
from linode_api4.objects.profile import PersonalAccessToken
from linode_api4.objects.serializable import StrEnum
from linode_api4.objects.support import SupportTicket
from linode_api4.objects.volume import Volume
from linode_api4.objects.vpc import VPC
Expand Down Expand Up @@ -180,6 +181,24 @@ class Login(Base):
}


class AccountSettingsInterfacesForNewLinodes(StrEnum):
"""
A string enum corresponding to valid values
for the AccountSettings(...).interfaces_for_new_linodes field.

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

legacy_config_only = "legacy_config_only"
legacy_config_default_but_linode_allowed = (
"legacy_config_default_but_linode_allowed"
)
linode_default_but_legacy_config_allowed = (
"linode_default_but_legacy_config_allowed"
)
linode_only = "linode_only"


class AccountSettings(Base):
"""
Information related to your Account settings.
Expand All @@ -198,6 +217,7 @@ class AccountSettings(Base):
),
"object_storage": Property(),
"backups_enabled": Property(mutable=True),
"interfaces_for_new_linodes": Property(mutable=True),
"maintenance_policy": Property(
mutable=True
), # Note: This field is only available when using v4beta.
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