Skip to content
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
8 changes: 8 additions & 0 deletions linode_api4/groups/vpc.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
VPCIPv6RangeOptions,
)
from linode_api4.objects.base import _flatten_request_body_recursive
from linode_api4.objects.vpc import VPCType
from linode_api4.paginated_list import PaginatedList
from linode_api4.util import drop_null_keys

Expand Down Expand Up @@ -43,6 +44,7 @@ def create(
description: Optional[str] = None,
subnets: Optional[List[Dict[str, Any]]] = None,
ipv6: Optional[List[Union[VPCIPv6RangeOptions, Dict[str, Any]]]] = None,
vpc_type: Optional[Union[VPCType, str]] = None,
ipv4: Optional[List[Union[VPCIPv4RangeOptions, Dict[str, Any]]]] = None,
**kwargs,
) -> VPC:
Expand All @@ -61,6 +63,11 @@ def create(
:type subnets: List[Dict[str, Any]]
:param ipv6: The IPv6 address ranges for this VPC.
:type ipv6: List[Union[VPCIPv6RangeOptions, Dict[str, Any]]]
:param vpc_type: The type of VPC to create. Defaults to ``regular`` on
the API side. Set to ``rdma`` to create a GPUDirect
RDMA VPC (requires the ``GPUDirect RDMA`` account
capability).
:type vpc_type: Optional[Union[VPCType, str]]
:param ipv4: The IPv4 address ranges for this VPC. Note that IPv4 VPCs may not currently be available to all users.
:type ipv4: List[Union[VPCIPv4RangeOptions, Dict[str, Any]]]

Expand All @@ -74,6 +81,7 @@ def create(
"ipv4": ipv4,
"ipv6": ipv6,
"subnets": subnets,
"vpc_type": vpc_type,
}

if subnets is not None and len(subnets) > 0:
Expand Down
12 changes: 12 additions & 0 deletions linode_api4/objects/linode.py
Original file line number Diff line number Diff line change
Expand Up @@ -2115,10 +2115,22 @@ def interface_create(
:param vpc: The VPC-specific configuration of the new interface.
If set, the new instance will be a VPC interface.

.. note::
RDMA VPC interfaces (``rdma_vpc``) cannot be added via this
endpoint. They may only be specified at instance creation time via
:func:`linode_api4.LinodeGroup.instance_create`.

:returns: The newly created Linode Interface.
:rtype: LinodeInterface
"""

if kwargs.get("rdma_vpc") is not None:
raise ValueError(
"RDMA VPC interfaces (rdma_vpc) cannot be added via "
"interface_create(). They may only be specified at instance "
"creation time via LinodeGroup.instance_create()."
)

params = {
"firewall_id": firewall,
"default_route": default_route,
Expand Down
82 changes: 82 additions & 0 deletions linode_api4/objects/linode_interfaces.py
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,47 @@ class LinodeInterfaceVLANOptions(JSONObject):
ipam_address: Optional[str] = None


@dataclass
class LinodeInterfaceRDMAVPCIPv4AddressOptions(JSONObject):
"""
Options accepted for a single address when creating or updating the IPv4
configuration of an RDMA VPC Linode Interface.

Only one address is supported per RDMA VPC interface, and it must be
marked as primary.
"""

address: Optional[str] = None
primary: Optional[bool] = None


@dataclass
class LinodeInterfaceRDMAVPCIPv4Options(JSONObject):
"""
Options accepted when creating or updating the IPv4 configuration of an
RDMA VPC Linode Interface.

The ``addresses`` list MUST contain exactly one element. If omitted, the
API defaults to a single primary ``auto`` address.
"""

addresses: Optional[List[LinodeInterfaceRDMAVPCIPv4AddressOptions]] = None


@dataclass
class LinodeInterfaceRDMAVPCOptions(JSONObject):
"""
RDMA-VPC-exclusive options accepted when creating or updating a Linode
Interface.

Used for GPUDirect RDMA interfaces. Default routes and NAT 1:1 addresses
are not supported on RDMA VPC interfaces.
"""

subnet_id: int = 0
ipv4: Optional[LinodeInterfaceRDMAVPCIPv4Options] = None


@dataclass
class LinodeInterfaceOptions(JSONObject):
"""
Expand All @@ -204,6 +245,7 @@ class LinodeInterfaceOptions(JSONObject):
vpc: Optional[LinodeInterfaceVPCOptions] = None
public: Optional[LinodeInterfacePublicOptions] = None
vlan: Optional[LinodeInterfaceVLANOptions] = None
rdma_vpc: Optional[LinodeInterfaceRDMAVPCOptions] = None


# Interface GET Response
Expand Down Expand Up @@ -409,6 +451,45 @@ class LinodeInterfaceVLAN(JSONObject):
ipam_address: Optional[str] = None


@dataclass
class LinodeInterfaceRDMAVPCIPv4Address(JSONObject):
"""
A single address under the IPv4 configuration of an RDMA VPC Linode Interface.
"""

put_class = LinodeInterfaceRDMAVPCIPv4AddressOptions

address: str = ""
primary: bool = False


@dataclass
class LinodeInterfaceRDMAVPCIPv4(JSONObject):
"""
The IPv4 configuration of an RDMA VPC Linode Interface.
"""

put_class = LinodeInterfaceRDMAVPCIPv4Options

addresses: List[LinodeInterfaceRDMAVPCIPv4Address] = field(
default_factory=list
)


@dataclass
class LinodeInterfaceRDMAVPC(JSONObject):
"""
RDMA VPC-specific configuration field for a Linode Interface.
"""

put_class = LinodeInterfaceRDMAVPCOptions

vpc_id: int = 0
subnet_id: int = 0

ipv4: Optional[LinodeInterfaceRDMAVPCIPv4] = None


class LinodeInterface(DerivedBase):
"""
A Linode's network interface.
Expand Down Expand Up @@ -449,6 +530,7 @@ class LinodeInterface(DerivedBase):
"public": Property(mutable=True, json_object=LinodeInterfacePublic),
"vlan": Property(mutable=True, json_object=LinodeInterfaceVLAN),
"vpc": Property(mutable=True, json_object=LinodeInterfaceVPC),
"rdma_vpc": Property(mutable=True, json_object=LinodeInterfaceRDMAVPC),
}

def firewalls(self, *filters) -> List[Firewall]:
Expand Down
1 change: 1 addition & 0 deletions linode_api4/objects/region.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ class Capability(StrEnum):
ruleset = "Cloud Firewall Rule Set"
prefixlists = "Cloud Firewall Prefix Lists"
current_prefixlists = "Cloud Firewall Prefix List Current References"
gpudirect_rdma = "GPUDirect RDMA"


@dataclass
Expand Down
13 changes: 12 additions & 1 deletion linode_api4/objects/vpc.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,20 @@
from linode_api4.objects import Base, DerivedBase, Property, Region
from linode_api4.objects.base import _flatten_request_body_recursive
from linode_api4.objects.networking import VPCIPAddress
from linode_api4.objects.serializable import JSONObject
from linode_api4.objects.serializable import JSONObject, StrEnum
from linode_api4.paginated_list import PaginatedList
from linode_api4.util import drop_null_keys


class VPCType(StrEnum):
"""
VPCType represents the supported VPC types.
"""

regular = "regular"
rdma = "rdma"


@dataclass
class VPCIPv4DefaultRange(JSONObject):
"""
Expand Down Expand Up @@ -119,6 +128,7 @@ class VPCSubnet(DerivedBase):
"ipv6": Property(json_object=VPCSubnetIPv6Range, unordered=True),
"linodes": Property(json_object=VPCSubnetLinode, unordered=True),
"databases": Property(json_object=VPCSubnetDatabase, unordered=True),
"vpc_type": Property(),
"created": Property(is_datetime=True),
"updated": Property(is_datetime=True),
}
Expand All @@ -144,6 +154,7 @@ class VPC(Base):
),
"ipv6": Property(json_object=VPCIPv6Range, unordered=True),
"subnets": Property(derived_class=VPCSubnet),
"vpc_type": Property(),
"created": Property(is_datetime=True),
"updated": Property(is_datetime=True),
}
Expand Down
27 changes: 27 additions & 0 deletions test/fixtures/linode_instances_124_interfaces_999.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
{
"id": 999,
"mac_address": "22:00:f2:9e:d3:48",
"created": "2026-03-12T09:54:34",
"updated": "2026-03-12T09:54:35",
"default_route": {
"ipv4": false,
"ipv6": false
},
"version": 1,
"public": null,
"vpc": null,
"vlan": null,
"rdma_vpc": {
"vpc_id": 7,
"subnet_id": 8,
"ipv4": {
"addresses": [
{
"address": "10.0.0.2",
"primary": true
}
]
}
}
}

1 change: 1 addition & 0 deletions test/fixtures/vpcs.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
"id": 123456,
"description": "A very real VPC.",
"region": "us-southeast",
"vpc_type": "regular",
"ipv4": [
{
"range": "10.0.0.0/8"
Expand Down
1 change: 1 addition & 0 deletions test/fixtures/vpcs_123456.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
"id": 123456,
"description": "A very real VPC.",
"region": "us-southeast",
"vpc_type": "regular",
"ipv4": [
{
"range": "10.0.0.0/8"
Expand Down
1 change: 1 addition & 0 deletions test/fixtures/vpcs_123456_subnets.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
]
}
],
"vpc_type": "regular",
"created": "2018-01-01T00:01:01",
"updated": "2018-01-01T00:01:01"
}
Expand Down
1 change: 1 addition & 0 deletions test/fixtures/vpcs_123456_subnets_789.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
"range": "fd71:1140:a9d0::/52"
}
],
"vpc_type": "regular",
"linodes": [
{
"id": 12345,
Expand Down
50 changes: 49 additions & 1 deletion test/integration/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
from requests.exceptions import ConnectionError, RequestException

from linode_api4 import (
Capability,
Instance,
InterfaceGeneration,
IPAddress,
Expand All @@ -41,6 +42,8 @@
RUN_LONG_TESTS = "RUN_LONG_TESTS"
SKIP_E2E_FIREWALL = "SKIP_E2E_FIREWALL"

TEST_VPC_REGION = None

ALL_ACCOUNT_AVAILABILITIES = {
"Linodes",
"NodeBalancers",
Expand Down Expand Up @@ -468,9 +471,13 @@ def test_oauth_client(test_linode_client):
@pytest.fixture(scope="session")
def create_vpc(test_linode_client):
client = test_linode_client

label = get_test_label(length=10)

global TEST_VPC_REGION
TEST_VPC_REGION = get_region(
test_linode_client, {"VPCs", "VPC IPv6 Stack", "Linode Interfaces"}
)

vpc = client.vpcs.create(
label=label,
region=get_region(
Expand All @@ -490,6 +497,32 @@ def create_vpc(test_linode_client):
vpc.delete()


@pytest.fixture
def create_vpc_with_rdma_type(test_linode_client):
client = test_linode_client
label = get_test_label(length=10)

if TEST_VPC_REGION:
region = TEST_VPC_REGION
else:
region = get_region(
# GPUDirect RDMA capability not available for now
# test_linode_client, {Capability.vpcs, Capability.gpudirect_rdma}
test_linode_client,
{Capability.vpcs},
)

vpc = client.vpcs.create(
label=label,
region=region,
description="test description",
vpc_type="rdma",
)
yield vpc

vpc.delete()


@pytest.fixture(scope="session")
def create_vpc_with_subnet(test_linode_client, create_vpc):
subnet = create_vpc.subnet_create(
Expand Down Expand Up @@ -525,6 +558,21 @@ def create_vpc_with_subnet_and_linode(
instance.delete()


@pytest.fixture
def create_vpc_with_subnet_and_rdma_type(create_vpc_with_rdma_type):
vpc_rdma = create_vpc_with_rdma_type
label = get_test_label(length=10)

subnet_rdma = vpc_rdma.subnet_create(
label=label,
ipv4="10.0.0.0/24",
)

yield vpc_rdma, subnet_rdma

subnet_rdma.delete()


@pytest.fixture
def create_vpc_with_ipv4(test_linode_client):
client = test_linode_client
Expand Down
Loading