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 @@ -4,6 +4,7 @@
from linode_api4.groups import Group
from linode_api4.objects import VPC, Region, VPCIPAddress, 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 @@ -36,6 +37,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,
**kwargs,
) -> VPC:
"""
Expand All @@ -53,6 +55,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]]

:returns: The new VPC object.
:rtype: VPC
Expand All @@ -63,6 +70,7 @@ def create(
"description": description,
"ipv6": ipv6,
"subnets": subnets,
"vpc_type": vpc_type,
}

if subnets is not None and len(subnets) > 0:
Expand Down
5 changes: 5 additions & 0 deletions linode_api4/objects/linode.py
Original file line number Diff line number Diff line change
Expand Up @@ -2115,6 +2115,11 @@ 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`.
Comment on lines +2118 to +2122
:returns: The newly created Linode Interface.
:rtype: LinodeInterface
"""
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 VPCIPv6RangeOptions(JSONObject):
"""
Expand Down Expand Up @@ -89,6 +98,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 @@ -110,6 +120,7 @@ class VPC(Base):
"region": Property(slug_relationship=Region),
"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",
"ipv6": [
{
"range": "fd71:1140:a9d0::/52"
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",
"ipv6": [
{
"range": "fd71:1140:a9d0::/52"
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
6 changes: 1 addition & 5 deletions test/integration/models/networking/test_networking.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,7 @@
import pytest
import requests

from linode_api4 import (
ApiError,
Instance,
LinodeClient,
)
from linode_api4 import ApiError, Instance, LinodeClient
from linode_api4.objects import (
Config,
ConfigInterfaceIPv4,
Expand Down
35 changes: 35 additions & 0 deletions test/unit/groups/linode_test.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from test.unit.base import ClientBaseCase
from test.unit.objects.linode_interface_test import (
build_interface_options_public,
build_interface_options_rdma_vpc,
build_interface_options_vlan,
build_interface_options_vpc,
)
Expand Down Expand Up @@ -128,6 +129,40 @@ def test_instance_create_with_interfaces_linode(self):
"interfaces": [iface._serialize() for iface in interfaces],
}

def test_instance_create_with_interfaces_linode_rdma(self):
"""
Tests that a Linode can be created with RDMA VPC LinodeInterfaces.
"""

interfaces = [
build_interface_options_rdma_vpc(),
]

with self.mock_post("linode/instances/124") as m:
self.client.linode.instance_create(
"g6-nanode-1",
"us-mia",
interface_generation=InterfaceGeneration.LINODE,
interfaces=interfaces,
)

assert m.call_data == {
"region": "us-mia",
"type": "g6-nanode-1",
"interface_generation": "linode",
"interfaces": [iface._serialize() for iface in interfaces],
}

assert m.call_data["interfaces"][0] == {
"firewall_id": -1,
"rdma_vpc": {
"subnet_id": 1234,
"ipv4": {
"addresses": [{"address": "auto", "primary": True}]
},
},
}

Comment on lines +156 to +165
def test_create_with_maintenance_policy(self):
"""
Tests that you can create a Linode with a maintenance policy
Expand Down
Loading