Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
bf13f2e
build(deps): bump actions/dependency-review-action from 4 to 5 (#695)
dependabot[bot] May 12, 2026
8985109
Change assignments type from dict to list (#687)
mawilk90 May 12, 2026
2f35543
TPT-4278: python-sdk: Implement support for Reserved IP for IPv4 (#672)
mgwoj Apr 28, 2026
8a07407
TPT-4278 python-sdk: Implement support for Reserved IP for IPv4
mgwoj Mar 26, 2026
1c63131
TPT-4278: python-sdk: Implement support for Reserved IP for IPv4
mgwoj Apr 15, 2026
ccd3020
Create int tests for Reserved IPs networking endpoints
mawilk90 Apr 24, 2026
799f113
Create int tests for Reserved IPs: types, allocate
mawilk90 Apr 24, 2026
3d70ecc
Create int tests for Reserved IPs: ephemeral
mawilk90 Apr 24, 2026
7da867a
Create int tests for Reserved IPs: linode instances
mawilk90 Apr 24, 2026
6ee4d06
Move fixtures for reserved IPs into conftest
mawilk90 Apr 27, 2026
5a98989
Create int tests for Reserved IPs: linode interfaces
mawilk90 Apr 27, 2026
a56bf1e
Create int tests for Reserved IPs: nodebalancers
mawilk90 Apr 27, 2026
3f0838a
Create int tests for Reserved IPs: tags
mawilk90 Apr 27, 2026
3930e05
Refactor
mawilk90 Apr 27, 2026
f24e2af
Update tests after API changes on DevCloud
mawilk90 Apr 28, 2026
f058877
Create int tests for Reserved IPs: tags #2
mawilk90 Apr 28, 2026
bb5fabe
Linter fix
mawilk90 Apr 28, 2026
09bf1d0
Linter fix
mawilk90 Apr 28, 2026
6a6afc9
Remove pytest.ini
mawilk90 Apr 28, 2026
5d57dc1
Remove unused assertions after API Team clarifications
mawilk90 Apr 28, 2026
f88bff4
Use reservedIP's region for NB
mawilk90 Apr 30, 2026
155d377
Refactor
mawilk90 Apr 30, 2026
42bd110
Address Copilot remarks
mawilk90 May 4, 2026
2a630ae
Linter fix
mawilk90 May 4, 2026
b52e666
Revert capabilities' changes in get_regions
mawilk90 May 4, 2026
b9ca4cf
Linter fixes
mawilk90 May 6, 2026
9e68fea
Merge branch 'proj/reserved-ips' into feature/TPT-4285-python-sdk-imp…
mawilk90 May 12, 2026
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
2 changes: 1 addition & 1 deletion .github/workflows/dependency-review.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,6 @@ jobs:
- name: 'Checkout repository'
uses: actions/checkout@v6
- name: 'Dependency Review'
uses: actions/dependency-review-action@v4
uses: actions/dependency-review-action@v5
with:
comment-summary-in-pr: on-failure
4 changes: 2 additions & 2 deletions linode_api4/groups/networking.py
Original file line number Diff line number Diff line change
Expand Up @@ -491,10 +491,10 @@ def ip_addresses_assign(self, assignments, region):
:param assignments: Any number of assignments to make. See
:any:`IPAddress.to` for details on how to construct
assignments.
:type assignments: dct
:type assignments: list
"""

for a in assignments["assignments"]:
for a in assignments:
if not "address" in a or not "linode_id" in a:
raise ValueError("Invalid assignment: {}".format(a))

Expand Down
45 changes: 45 additions & 0 deletions test/integration/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
PlacementGroupPolicy,
PlacementGroupType,
PostgreSQLDatabase,
ReservedIPAddress,
)
from linode_api4.errors import ApiError
from linode_api4.linode_client import LinodeClient, MonitorClient
Expand Down Expand Up @@ -727,3 +728,47 @@ def test_monitor_client(get_monitor_token_for_db_entities):
)

return client, entity_ids


@pytest.fixture
def create_reserved_ip(test_linode_client):
client = test_linode_client
region = get_region(client, {"Linodes", "Cloud Firewall"}, site_type="core")
reserved_ip = client.networking.reserved_ip_create(
region=region, tags=["test"]
)

yield reserved_ip

# Delete only if IP exists (some tests delete it earlier)
if client.networking.reserved_ips(
ReservedIPAddress.address == reserved_ip.address
):
reserved_ip.delete()


@pytest.fixture
def create_reserved_ip_assigned(test_linode_client, create_linode):
client = test_linode_client
linode = create_linode
reserved_ip = client.networking.reserved_ip_create(
region=linode.region,
tags=["test", "assigned"],
)

client.networking.ip_addresses_assign(
assignments=[{"address": reserved_ip.address, "linode_id": linode.id}],
Comment thread
mawilk90 marked this conversation as resolved.
region=linode.region,
)

reserved_ip = test_linode_client.load(
ReservedIPAddress, reserved_ip.address
)

yield linode, reserved_ip

# Delete only if IP exists (some tests delete it earlier)
if client.networking.reserved_ips(
ReservedIPAddress.address == reserved_ip.address
):
reserved_ip.delete()
78 changes: 78 additions & 0 deletions test/integration/models/linode/interfaces/test_interfaces.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
import copy
import ipaddress
from test.integration.helpers import get_test_label

import pytest

from linode_api4 import (
ApiError,
Instance,
InterfaceGeneration,
LinodeInterface,
LinodeInterfaceDefaultRouteOptions,
LinodeInterfaceOptions,
LinodeInterfacePublicIPv4AddressOptions,
LinodeInterfacePublicIPv4Options,
LinodeInterfacePublicIPv6Options,
Expand All @@ -18,9 +21,28 @@
LinodeInterfaceVPCIPv4Options,
LinodeInterfaceVPCIPv4RangeOptions,
LinodeInterfaceVPCOptions,
ReservedIPAddress,
)


def build_interface_public_ipv4(firewall, ip_address):
return LinodeInterfaceOptions(
firewall_id=firewall,
default_route=LinodeInterfaceDefaultRouteOptions(
ipv4=True,
),
public=LinodeInterfacePublicOptions(
ipv4=LinodeInterfacePublicIPv4Options(
addresses=[
LinodeInterfacePublicIPv4AddressOptions(
address=ip_address, primary=True
)
],
),
),
)


def test_linode_create_with_linode_interfaces(
create_vpc_with_subnet,
linode_with_linode_interfaces,
Expand Down Expand Up @@ -359,3 +381,59 @@ def test_linode_interface_firewalls(e2e_test_firewall, linode_interface_public):
firewall = firewalls[0]
assert firewall.id == e2e_test_firewall.id
assert firewall.label == e2e_test_firewall.label


@pytest.mark.parametrize(
"iface_type",
[InterfaceGeneration.LEGACY_CONFIG, InterfaceGeneration.LINODE],
)
def test_linode_interfaces_with_reserved_ips(
test_linode_client, e2e_test_firewall, create_reserved_ip, iface_type
):
client = test_linode_client
reserved_ip = create_reserved_ip
label = get_test_label(length=8)

if iface_type == InterfaceGeneration.LEGACY_CONFIG:
linode, _ = client.linode.instance_create(
"g6-nanode-1",
reserved_ip.region,
image="linode/debian12",
label=label,
firewall=e2e_test_firewall,
interface_generation=iface_type,
ipv4=[reserved_ip.address],
)
else:
interface = build_interface_public_ipv4(
e2e_test_firewall.id, reserved_ip.address
)
linode, _ = client.linode.instance_create(
"g6-nanode-1",
reserved_ip.region,
image="linode/debian12",
label=label,
interface_generation=iface_type,
interfaces=[interface],
)

linode_ips = linode.ips.ipv4.public
assert len(linode_ips) == 1
assert linode_ips[0].address == reserved_ip.address
assert linode_ips[0].reserved == True
assert linode_ips[0].linode_id == linode.id
assert linode_ips[0].assigned_entity.id == linode.id
assert linode_ips[0].assigned_entity.type == "linode"
assert linode_ips[0].assigned_entity.label == linode.label
assert (
linode_ips[0].assigned_entity.url == f"/v4/linode/instances/{linode.id}"
)

linode.delete()
reserved_ips_list = client.networking.reserved_ips(
ReservedIPAddress.address == reserved_ip.address
)
assert len(reserved_ips_list) == 1
assert reserved_ips_list[0].reserved == True
assert reserved_ips_list[0].linode_id is None
assert reserved_ips_list[0].assigned_entity is None
40 changes: 40 additions & 0 deletions test/integration/models/linode/test_linode.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
Instance,
InterfaceGeneration,
LinodeInterface,
ReservedIPAddress,
Type,
)
from linode_api4.objects.linode import InstanceDiskEncryptionType, MigrationType
Expand Down Expand Up @@ -1156,3 +1157,42 @@ def test_update_linode_maintenance_policy(create_linode, test_linode_client):
linode.invalidate()
assert result
assert linode.maintenance_policy_id == non_default_policy.slug


def test_update_linode_with_reserved_ip_in_address(
test_linode_client, e2e_test_firewall, create_reserved_ip
):
label = get_test_label(length=8)
client = test_linode_client
reserved_ip = create_reserved_ip

linode, _ = client.linode.instance_create(
"g6-nanode-1",
reserved_ip.region,
image="linode/debian12",
label=label,
firewall=e2e_test_firewall,
)

linode_ips = linode.ips.ipv4.public
assert len(linode_ips) == 1
assert linode_ips[0].address != reserved_ip.address

linode.ip_allocate(True, reserved_ip.address)
delattr(linode, "_ips")
linode_ips = linode.ips.ipv4.public
assert len(linode_ips) == 2
assert reserved_ip.address in [ip.address for ip in linode_ips]

reserved_ip = client.networking.reserved_ips(
ReservedIPAddress.address == reserved_ip.address
)[0]
assert reserved_ip.linode_id == linode.id
assert reserved_ip.assigned_entity.id == linode.id
assert reserved_ip.assigned_entity.type == "linode"
assert reserved_ip.assigned_entity.label == linode.label
assert (
reserved_ip.assigned_entity.url == f"/v4/linode/instances/{linode.id}"
)

linode.delete()
Loading