Skip to content
Merged
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
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ jobs:
uses: actions/checkout@v5

- name: setup python 3
uses: actions/setup-python@v5
uses: actions/setup-python@v6
with:
python-version: '3.x'

Expand All @@ -34,7 +34,7 @@ jobs:
python-version: ['3.9', '3.10', '3.11', '3.12', '3.13']
steps:
- uses: actions/checkout@v5
- uses: actions/setup-python@v5
- uses: actions/setup-python@v6
with:
python-version: ${{ matrix.python-version }}
- name: Run tests
Expand Down
6 changes: 3 additions & 3 deletions .github/workflows/e2e-test-pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

- name: Setup Python
uses: actions/setup-python@v5
uses: actions/setup-python@v6
with:
python-version: '3.x'

Expand Down Expand Up @@ -115,7 +115,7 @@ jobs:
LINODE_CLI_OBJ_ACCESS_KEY: ${{ secrets.LINODE_CLI_OBJ_ACCESS_KEY }}
LINODE_CLI_OBJ_SECRET_KEY: ${{ secrets.LINODE_CLI_OBJ_SECRET_KEY }}

- uses: actions/github-script@v7
- uses: actions/github-script@v8
id: update-check-run
if: ${{ inputs.pull_request_number != '' && fromJson(steps.commit-hash.outputs.data).repository.pullRequest.headRef.target.oid == inputs.sha }}
env:
Expand Down Expand Up @@ -176,7 +176,7 @@ jobs:

steps:
- name: Set up Python
uses: actions/setup-python@v5
uses: actions/setup-python@v6
with:
python-version: '3.x'

Expand Down
6 changes: 3 additions & 3 deletions .github/workflows/e2e-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ jobs:
submodules: 'recursive'

- name: Setup Python
uses: actions/setup-python@v5
uses: actions/setup-python@v6
with:
python-version: ${{ inputs.run-eol-python-version == 'true' && env.EOL_PYTHON_VERSION || inputs.python-version || env.DEFAULT_PYTHON_VERSION }}

Expand Down Expand Up @@ -141,7 +141,7 @@ jobs:

steps:
- name: Set up Python
uses: actions/setup-python@v5
uses: actions/setup-python@v6
with:
python-version: '3.x'

Expand Down Expand Up @@ -189,7 +189,7 @@ jobs:
name: test-report-file

- name: Set up Python
uses: actions/setup-python@v5
uses: actions/setup-python@v6
with:
python-version: '3.x'

Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/nightly-smoke-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ jobs:
ref: dev

- name: Set up Python
uses: actions/setup-python@v5
uses: actions/setup-python@v6
with:
python-version: '3.x'

Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/publish-pypi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ jobs:
uses: actions/checkout@v5

- name: Setup Python
uses: actions/setup-python@v5
uses: actions/setup-python@v6
with:
python-version: '3.x'

Expand All @@ -28,4 +28,4 @@ jobs:
LINODE_SDK_VERSION: ${{ github.event.release.tag_name }}

- name: Publish the release artifacts to PyPI
uses: pypa/gh-action-pypi-publish@76f52bc884231f62b9a034ebfe128415bbaabdfc # pin@release/v1.12.4
uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # pin@release/v1.13.0
2 changes: 1 addition & 1 deletion .github/workflows/release-cross-repo-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ jobs:
run: sudo apt-get install -y build-essential

- name: Set up Python
uses: actions/setup-python@v5
uses: actions/setup-python@v6
with:
python-version: '3.10'

Expand Down
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
Loading
Loading