Skip to content

Commit dcb43b1

Browse files
Merge pull request #602 from linode/dev
v5.36.0
2 parents 8ab9ca6 + 93056c0 commit dcb43b1

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

56 files changed

+3222
-107
lines changed

.github/workflows/ci.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ jobs:
1717
uses: actions/checkout@v5
1818

1919
- name: setup python 3
20-
uses: actions/setup-python@v5
20+
uses: actions/setup-python@v6
2121
with:
2222
python-version: '3.x'
2323

@@ -34,7 +34,7 @@ jobs:
3434
python-version: ['3.9', '3.10', '3.11', '3.12', '3.13']
3535
steps:
3636
- uses: actions/checkout@v5
37-
- uses: actions/setup-python@v5
37+
- uses: actions/setup-python@v6
3838
with:
3939
python-version: ${{ matrix.python-version }}
4040
- name: Run tests

.github/workflows/e2e-test-pr.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ jobs:
8080
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
8181

8282
- name: Setup Python
83-
uses: actions/setup-python@v5
83+
uses: actions/setup-python@v6
8484
with:
8585
python-version: '3.x'
8686

@@ -115,7 +115,7 @@ jobs:
115115
LINODE_CLI_OBJ_ACCESS_KEY: ${{ secrets.LINODE_CLI_OBJ_ACCESS_KEY }}
116116
LINODE_CLI_OBJ_SECRET_KEY: ${{ secrets.LINODE_CLI_OBJ_SECRET_KEY }}
117117

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

177177
steps:
178178
- name: Set up Python
179-
uses: actions/setup-python@v5
179+
uses: actions/setup-python@v6
180180
with:
181181
python-version: '3.x'
182182

.github/workflows/e2e-test.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ jobs:
7171
submodules: 'recursive'
7272

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

@@ -141,7 +141,7 @@ jobs:
141141

142142
steps:
143143
- name: Set up Python
144-
uses: actions/setup-python@v5
144+
uses: actions/setup-python@v6
145145
with:
146146
python-version: '3.x'
147147

@@ -189,7 +189,7 @@ jobs:
189189
name: test-report-file
190190

191191
- name: Set up Python
192-
uses: actions/setup-python@v5
192+
uses: actions/setup-python@v6
193193
with:
194194
python-version: '3.x'
195195

.github/workflows/nightly-smoke-tests.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ jobs:
2424
ref: dev
2525

2626
- name: Set up Python
27-
uses: actions/setup-python@v5
27+
uses: actions/setup-python@v6
2828
with:
2929
python-version: '3.x'
3030

.github/workflows/publish-pypi.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ jobs:
1515
uses: actions/checkout@v5
1616

1717
- name: Setup Python
18-
uses: actions/setup-python@v5
18+
uses: actions/setup-python@v6
1919
with:
2020
python-version: '3.x'
2121

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

3030
- name: Publish the release artifacts to PyPI
31-
uses: pypa/gh-action-pypi-publish@76f52bc884231f62b9a034ebfe128415bbaabdfc # pin@release/v1.12.4
31+
uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # pin@release/v1.13.0

.github/workflows/release-cross-repo-test.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ jobs:
2525
run: sudo apt-get install -y build-essential
2626

2727
- name: Set up Python
28-
uses: actions/setup-python@v5
28+
uses: actions/setup-python@v6
2929
with:
3030
python-version: '3.10'
3131

linode_api4/groups/linode.py

Lines changed: 45 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,11 @@
11
import base64
22
import os
3-
from collections.abc import Iterable
4-
from typing import Any, Dict, Optional, Union
3+
from typing import Any, Dict, List, Optional, Union
54

65
from linode_api4.common import load_and_validate_keys
76
from linode_api4.errors import UnexpectedResponseError
87
from linode_api4.groups import Group
98
from linode_api4.objects import (
10-
ConfigInterface,
119
Firewall,
1210
Instance,
1311
InstanceDiskEncryptionType,
@@ -21,8 +19,13 @@
2119
from linode_api4.objects.linode import (
2220
Backup,
2321
InstancePlacementGroupAssignment,
22+
InterfaceGeneration,
23+
NetworkInterface,
2424
_expand_placement_group_assignment,
2525
)
26+
from linode_api4.objects.linode_interfaces import (
27+
LinodeInterfaceOptions,
28+
)
2629
from linode_api4.util import drop_null_keys
2730

2831

@@ -153,6 +156,13 @@ def instance_create(
153156
int,
154157
]
155158
] = None,
159+
interfaces: Optional[
160+
List[
161+
Union[LinodeInterfaceOptions, NetworkInterface, Dict[str, Any]],
162+
]
163+
] = None,
164+
interface_generation: Optional[Union[InterfaceGeneration, str]] = None,
165+
network_helper: Optional[bool] = None,
156166
maintenance_policy: Optional[str] = None,
157167
**kwargs,
158168
):
@@ -231,6 +241,30 @@ def instance_create(
231241
"us-east",
232242
backup=snapshot)
233243
244+
**Create an Instance with explicit interfaces:**
245+
246+
To create a new Instance with explicit interfaces, provide list of
247+
LinodeInterfaceOptions objects or dicts to the "interfaces" field::
248+
249+
linode, password = client.linode.instance_create(
250+
"g6-standard-1",
251+
"us-mia",
252+
image="linode/ubuntu24.04",
253+
254+
# This can be configured as an account-wide default
255+
interface_generation=InterfaceGeneration.LINODE,
256+
257+
interfaces=[
258+
LinodeInterfaceOptions(
259+
default_route=LinodeInterfaceDefaultRouteOptions(
260+
ipv4=True,
261+
ipv6=True
262+
),
263+
public=LinodeInterfacePublicOptions
264+
)
265+
]
266+
)
267+
234268
**Create an empty Instance**
235269
236270
If you want to create an empty Instance that you will configure manually,
@@ -294,9 +328,13 @@ def instance_create(
294328
:type disk_encryption: InstanceDiskEncryptionType or str
295329
:param interfaces: An array of Network Interfaces to add to this Linode’s Configuration Profile.
296330
At least one and up to three Interface objects can exist in this array.
297-
:type interfaces: list[ConfigInterface] or list[dict[str, Any]]
331+
:type interfaces: List[LinodeInterfaceOptions], List[NetworkInterface], or List[dict[str, Any]]
298332
:param placement_group: A Placement Group to create this Linode under.
299333
:type placement_group: Union[InstancePlacementGroupAssignment, PlacementGroup, Dict[str, Any], int]
334+
:param interface_generation: The generation of network interfaces this Linode uses.
335+
:type interface_generation: InterfaceGeneration or str
336+
:param network_helper: Whether this instance should have Network Helper enabled.
337+
:type network_helper: bool
300338
:param maintenance_policy: The slug of the maintenance policy to apply during maintenance.
301339
If not provided, the default policy (linode/migrate) will be applied.
302340
NOTE: This field is in beta and may only
@@ -317,13 +355,6 @@ def instance_create(
317355
ret_pass = Instance.generate_root_password()
318356
kwargs["root_pass"] = ret_pass
319357

320-
interfaces = kwargs.get("interfaces", None)
321-
if interfaces is not None and isinstance(interfaces, Iterable):
322-
kwargs["interfaces"] = [
323-
i._serialize() if isinstance(i, ConfigInterface) else i
324-
for i in interfaces
325-
]
326-
327358
params = {
328359
"type": ltype,
329360
"region": region,
@@ -343,6 +374,9 @@ def instance_create(
343374
if placement_group
344375
else None
345376
),
377+
"interfaces": interfaces,
378+
"interface_generation": interface_generation,
379+
"network_helper": network_helper,
346380
}
347381

348382
params.update(kwargs)

linode_api4/groups/networking.py

Lines changed: 118 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,23 @@
1+
from typing import Any, Dict, Optional, Union
2+
13
from linode_api4.errors import UnexpectedResponseError
24
from linode_api4.groups import Group
35
from linode_api4.objects import (
46
VLAN,
57
Base,
68
Firewall,
9+
FirewallCreateDevicesOptions,
10+
FirewallSettings,
11+
FirewallTemplate,
712
Instance,
813
IPAddress,
914
IPv6Pool,
1015
IPv6Range,
1116
NetworkTransferPrice,
1217
Region,
1318
)
19+
from linode_api4.objects.base import _flatten_request_body_recursive
20+
from linode_api4.util import drop_null_keys
1421

1522

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

36-
def firewall_create(self, label, rules, **kwargs):
43+
def firewall_create(
44+
self,
45+
label: str,
46+
rules: Dict[str, Any],
47+
devices: Optional[
48+
Union[FirewallCreateDevicesOptions, Dict[str, Any]]
49+
] = None,
50+
**kwargs,
51+
):
3752
"""
3853
Creates a new Firewall, either in the given Region or
3954
attached to the given Instance.
@@ -44,6 +59,8 @@ def firewall_create(self, label, rules, **kwargs):
4459
:type label: str
4560
:param rules: The rules to apply to the new Firewall. For more information on Firewall rules, see our `Firewalls Documentation`_.
4661
:type rules: dict
62+
:param devices: Represents devices to create created alongside a Linode Firewall.
63+
:type devices: Optional[Union[FirewallCreateDevicesOptions, Dict[str, Any]]]
4764
4865
:returns: The new Firewall.
4966
:rtype: Firewall
@@ -81,10 +98,14 @@ def firewall_create(self, label, rules, **kwargs):
8198
params = {
8299
"label": label,
83100
"rules": rules,
101+
"devices": devices,
84102
}
85103
params.update(kwargs)
86104

87-
result = self.client.post("/networking/firewalls", data=params)
105+
result = self.client.post(
106+
"/networking/firewalls",
107+
data=drop_null_keys(_flatten_request_body_recursive(params)),
108+
)
88109

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

118+
def firewall_templates(self, *filters):
119+
"""
120+
Returns a list of Firewall Templates available to the current user.
121+
122+
API Documentation: https://techdocs.akamai.com/linode-api/reference/get-firewall-templates
123+
124+
NOTE: This feature may not currently be available to all users.
125+
126+
:param filters: Any number of filters to apply to this query.
127+
See :doc:`Filtering Collections</linode_api4/objects/filtering>`
128+
for more details on filtering.
129+
130+
:returns: A list of Firewall Templates available to the current user.
131+
:rtype: PaginatedList of FirewallTemplate
132+
"""
133+
return self.client._get_and_filter(FirewallTemplate, *filters)
134+
135+
def firewall_settings(self) -> FirewallSettings:
136+
"""
137+
Returns an object representing the Linode Firewall settings for the current user.
138+
139+
API Documentation: https://techdocs.akamai.com/linode-api/reference/get-firewall-settings
140+
141+
NOTE: This feature may not currently be available to all users.
142+
:returns: An object representing the Linode Firewall settings for the current user.
143+
:rtype: FirewallSettings
144+
"""
145+
result = self.client.get("/networking/firewalls/settings")
146+
147+
if "default_firewall_ids" not in result:
148+
raise UnexpectedResponseError(
149+
"Unexpected response when getting firewall settings!",
150+
json=result,
151+
)
152+
153+
return FirewallSettings(self.client, None, result)
154+
97155
def ips(self, *filters):
98156
"""
99157
Returns a list of IP addresses on this account, excluding private addresses.
@@ -124,6 +182,64 @@ def ipv6_ranges(self, *filters):
124182
"""
125183
return self.client._get_and_filter(IPv6Range, *filters)
126184

185+
def ipv6_range_allocate(
186+
self,
187+
prefix_length: int,
188+
route_target: Optional[str] = None,
189+
linode: Optional[Union[Instance, int]] = None,
190+
**kwargs,
191+
) -> IPv6Range:
192+
"""
193+
Creates an IPv6 Range and assigns it based on the provided Linode or route target IPv6 SLAAC address.
194+
195+
API Documentation: https://techdocs.akamai.com/linode-api/reference/post-ipv6-range
196+
197+
Create an IPv6 range assigned to a Linode by ID::
198+
199+
range = client.networking.ipv6_range_allocate(64, linode_id=123)
200+
201+
202+
Create an IPv6 range assigned to a Linode by SLAAC::
203+
204+
range = client.networking.ipv6_range_allocate(
205+
64,
206+
route_target=instance.ipv6.split("/")[0]
207+
)
208+
209+
:param prefix_length: The prefix length of the IPv6 range.
210+
:type prefix_length: int
211+
:param route_target: The IPv6 SLAAC address to assign this range to. Required if linode is not specified.
212+
:type route_target: str
213+
:param linode: The ID of the Linode to assign this range to.
214+
The SLAAC address for the provided Linode is used as the range's route_target.
215+
Required if linode is not specified.
216+
:type linode: Instance or int
217+
218+
:returns: The new IPAddress.
219+
:rtype: IPAddress
220+
"""
221+
222+
params = {
223+
"prefix_length": prefix_length,
224+
"route_target": route_target,
225+
"linode_id": linode,
226+
}
227+
228+
params.update(**kwargs)
229+
230+
result = self.client.post(
231+
"/networking/ipv6/ranges",
232+
data=drop_null_keys(_flatten_request_body_recursive(params)),
233+
)
234+
235+
if not "range" in result:
236+
raise UnexpectedResponseError(
237+
"Unexpected response when allocating IPv6 range!", json=result
238+
)
239+
240+
result = IPv6Range(self.client, result["range"], result)
241+
return result
242+
127243
def ipv6_pools(self, *filters):
128244
"""
129245
Returns a list of IPv6 pools on this account.

0 commit comments

Comments
 (0)