Skip to content

Commit eedfece

Browse files
Implement support for VPC Dual Stack (#524)
1 parent b2eff93 commit eedfece

16 files changed

+554
-198
lines changed

linode_api4/groups/vpc.py

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@
22

33
from linode_api4.errors import UnexpectedResponseError
44
from linode_api4.groups import Group
5-
from linode_api4.objects import VPC, Region, VPCIPAddress
5+
from linode_api4.objects import VPC, Region, VPCIPAddress, VPCIPv6RangeOptions
6+
from linode_api4.objects.base import _flatten_request_body_recursive
67
from linode_api4.paginated_list import PaginatedList
8+
from linode_api4.util import drop_null_keys
79

810

911
class VPCGroup(Group):
@@ -33,6 +35,7 @@ def create(
3335
region: Union[Region, str],
3436
description: Optional[str] = None,
3537
subnets: Optional[List[Dict[str, Any]]] = None,
38+
ipv6: Optional[List[Union[VPCIPv6RangeOptions, Dict[str, Any]]]] = None,
3639
**kwargs,
3740
) -> VPC:
3841
"""
@@ -48,30 +51,33 @@ def create(
4851
:type description: Optional[str]
4952
:param subnets: A list of subnets to create under this VPC.
5053
:type subnets: List[Dict[str, Any]]
54+
:param ipv6: The IPv6 address ranges for this VPC.
55+
:type ipv6: List[Union[VPCIPv6RangeOptions, Dict[str, Any]]]
5156
5257
:returns: The new VPC object.
5358
:rtype: VPC
5459
"""
5560
params = {
5661
"label": label,
5762
"region": region.id if isinstance(region, Region) else region,
63+
"description": description,
64+
"ipv6": ipv6,
65+
"subnets": subnets,
5866
}
5967

60-
if description is not None:
61-
params["description"] = description
62-
6368
if subnets is not None and len(subnets) > 0:
6469
for subnet in subnets:
6570
if not isinstance(subnet, dict):
6671
raise ValueError(
6772
f"Unsupported type for subnet: {type(subnet)}"
6873
)
6974

70-
params["subnets"] = subnets
71-
7275
params.update(kwargs)
7376

74-
result = self.client.post("/vpcs", data=params)
77+
result = self.client.post(
78+
"/vpcs",
79+
data=drop_null_keys(_flatten_request_body_recursive(params)),
80+
)
7581

7682
if not "id" in result:
7783
raise UnexpectedResponseError(

linode_api4/objects/linode.py

Lines changed: 99 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import string
22
import sys
3-
from dataclasses import dataclass
3+
from dataclasses import dataclass, field
44
from datetime import datetime
55
from enum import Enum
66
from os import urandom
@@ -291,10 +291,83 @@ def _populate(self, json):
291291

292292
@dataclass
293293
class ConfigInterfaceIPv4(JSONObject):
294+
"""
295+
ConfigInterfaceIPv4 represents the IPv4 configuration of a VPC interface.
296+
"""
297+
294298
vpc: str = ""
295299
nat_1_1: str = ""
296300

297301

302+
@dataclass
303+
class ConfigInterfaceIPv6SLAACOptions(JSONObject):
304+
"""
305+
ConfigInterfaceIPv6SLAACOptions is used to set a single IPv6 SLAAC configuration of a VPC interface.
306+
"""
307+
308+
range: str = ""
309+
310+
311+
@dataclass
312+
class ConfigInterfaceIPv6RangeOptions(JSONObject):
313+
"""
314+
ConfigInterfaceIPv6RangeOptions is used to set a single IPv6 range configuration of a VPC interface.
315+
"""
316+
317+
range: str = ""
318+
319+
320+
@dataclass
321+
class ConfigInterfaceIPv6Options(JSONObject):
322+
"""
323+
ConfigInterfaceIPv6Options is used to set the IPv6 configuration of a VPC interface.
324+
"""
325+
326+
slaac: List[ConfigInterfaceIPv6SLAACOptions] = field(
327+
default_factory=lambda: []
328+
)
329+
ranges: List[ConfigInterfaceIPv6RangeOptions] = field(
330+
default_factory=lambda: []
331+
)
332+
is_public: bool = False
333+
334+
335+
@dataclass
336+
class ConfigInterfaceIPv6SLAAC(JSONObject):
337+
"""
338+
ConfigInterfaceIPv6SLAAC represents a single SLAAC address under a VPC interface's IPv6 configuration.
339+
"""
340+
341+
put_class = ConfigInterfaceIPv6SLAACOptions
342+
343+
range: str = ""
344+
address: str = ""
345+
346+
347+
@dataclass
348+
class ConfigInterfaceIPv6Range(JSONObject):
349+
"""
350+
ConfigInterfaceIPv6Range represents a single IPv6 address under a VPC interface's IPv6 configuration.
351+
"""
352+
353+
put_class = ConfigInterfaceIPv6RangeOptions
354+
355+
range: str = ""
356+
357+
358+
@dataclass
359+
class ConfigInterfaceIPv6(JSONObject):
360+
"""
361+
ConfigInterfaceIPv6 represents the IPv6 configuration of a VPC interface.
362+
"""
363+
364+
put_class = ConfigInterfaceIPv6Options
365+
366+
slaac: List[ConfigInterfaceIPv6SLAAC] = field(default_factory=lambda: [])
367+
ranges: List[ConfigInterfaceIPv6Range] = field(default_factory=lambda: [])
368+
is_public: bool = False
369+
370+
298371
class NetworkInterface(DerivedBase):
299372
"""
300373
This class represents a Configuration Profile's network interface object.
@@ -320,6 +393,7 @@ class NetworkInterface(DerivedBase):
320393
"vpc_id": Property(id_relationship=VPC),
321394
"subnet_id": Property(),
322395
"ipv4": Property(mutable=True, json_object=ConfigInterfaceIPv4),
396+
"ipv6": Property(mutable=True, json_object=ConfigInterfaceIPv6),
323397
"ip_ranges": Property(mutable=True),
324398
}
325399

@@ -391,7 +465,10 @@ class ConfigInterface(JSONObject):
391465
# VPC-specific
392466
vpc_id: Optional[int] = None
393467
subnet_id: Optional[int] = None
468+
394469
ipv4: Optional[Union[ConfigInterfaceIPv4, Dict[str, Any]]] = None
470+
ipv6: Optional[Union[ConfigInterfaceIPv6, Dict[str, Any]]] = None
471+
395472
ip_ranges: Optional[List[str]] = None
396473

397474
# Computed
@@ -400,7 +477,7 @@ class ConfigInterface(JSONObject):
400477
def __repr__(self):
401478
return f"Interface: {self.purpose}"
402479

403-
def _serialize(self, *args, **kwargs):
480+
def _serialize(self, is_put: bool = False):
404481
purpose_formats = {
405482
"public": {"purpose": "public", "primary": self.primary},
406483
"vlan": {
@@ -412,11 +489,8 @@ def _serialize(self, *args, **kwargs):
412489
"purpose": "vpc",
413490
"primary": self.primary,
414491
"subnet_id": self.subnet_id,
415-
"ipv4": (
416-
self.ipv4.dict
417-
if isinstance(self.ipv4, ConfigInterfaceIPv4)
418-
else self.ipv4
419-
),
492+
"ipv4": self.ipv4,
493+
"ipv6": self.ipv6,
420494
"ip_ranges": self.ip_ranges,
421495
},
422496
}
@@ -426,11 +500,14 @@ def _serialize(self, *args, **kwargs):
426500
f"Unknown interface purpose: {self.purpose}",
427501
)
428502

429-
return {
430-
k: v
431-
for k, v in purpose_formats[self.purpose].items()
432-
if v is not None
433-
}
503+
return _flatten_request_body_recursive(
504+
{
505+
k: v
506+
for k, v in purpose_formats[self.purpose].items()
507+
if v is not None
508+
},
509+
is_put=is_put,
510+
)
434511

435512

436513
class Config(DerivedBase):
@@ -571,6 +648,7 @@ def interface_create_vpc(
571648
subnet: Union[int, VPCSubnet],
572649
primary=False,
573650
ipv4: Union[Dict[str, Any], ConfigInterfaceIPv4] = None,
651+
ipv6: Union[Dict[str, Any], ConfigInterfaceIPv6Options] = None,
574652
ip_ranges: Optional[List[str]] = None,
575653
) -> NetworkInterface:
576654
"""
@@ -584,6 +662,8 @@ def interface_create_vpc(
584662
:type primary: bool
585663
:param ipv4: The IPv4 configuration of the interface for the associated subnet.
586664
:type ipv4: Dict or ConfigInterfaceIPv4
665+
:param ipv6: The IPv6 configuration of the interface for the associated subnet.
666+
:type ipv6: Dict or ConfigInterfaceIPv6Options
587667
:param ip_ranges: A list of IPs or IP ranges in the VPC subnet.
588668
Packets to these CIDRs are routed through the
589669
VPC network interface.
@@ -594,19 +674,16 @@ def interface_create_vpc(
594674
"""
595675
params = {
596676
"purpose": "vpc",
597-
"subnet_id": subnet.id if isinstance(subnet, VPCSubnet) else subnet,
677+
"subnet_id": subnet,
598678
"primary": primary,
679+
"ipv4": ipv4,
680+
"ipv6": ipv6,
681+
"ip_ranges": ip_ranges,
599682
}
600683

601-
if ipv4 is not None:
602-
params["ipv4"] = (
603-
ipv4.dict if isinstance(ipv4, ConfigInterfaceIPv4) else ipv4
604-
)
605-
606-
if ip_ranges is not None:
607-
params["ip_ranges"] = ip_ranges
608-
609-
return self._interface_create(params)
684+
return self._interface_create(
685+
drop_null_keys(_flatten_request_body_recursive(params))
686+
)
610687

611688
def interface_reorder(self, interfaces: List[Union[int, NetworkInterface]]):
612689
"""

linode_api4/objects/networking.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
from dataclasses import dataclass
2-
from typing import Optional
2+
from typing import List, Optional
33

44
from linode_api4.common import Price, RegionPrice
55
from linode_api4.errors import UnexpectedResponseError
@@ -127,6 +127,11 @@ def delete(self):
127127
return True
128128

129129

130+
@dataclass
131+
class VPCIPAddressIPv6(JSONObject):
132+
slaac_address: str = ""
133+
134+
130135
@dataclass
131136
class VPCIPAddress(JSONObject):
132137
"""
@@ -152,6 +157,10 @@ class VPCIPAddress(JSONObject):
152157
address_range: Optional[str] = None
153158
nat_1_1: Optional[str] = None
154159

160+
ipv6_range: Optional[str] = None
161+
ipv6_is_public: Optional[bool] = None
162+
ipv6_addresses: Optional[List[VPCIPAddressIPv6]] = None
163+
155164

156165
class VLAN(Base):
157166
"""

linode_api4/objects/vpc.py

Lines changed: 54 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,54 @@
11
from dataclasses import dataclass
2-
from typing import List, Optional
2+
from typing import Any, Dict, List, Optional, Union
33

44
from linode_api4.errors import UnexpectedResponseError
55
from linode_api4.objects import Base, DerivedBase, Property, Region
6+
from linode_api4.objects.base import _flatten_request_body_recursive
67
from linode_api4.objects.networking import VPCIPAddress
78
from linode_api4.objects.serializable import JSONObject
89
from linode_api4.paginated_list import PaginatedList
10+
from linode_api4.util import drop_null_keys
11+
12+
13+
@dataclass
14+
class VPCIPv6RangeOptions(JSONObject):
15+
"""
16+
VPCIPv6RangeOptions is used to specify an IPv6 range when creating or updating a VPC.
17+
"""
18+
19+
range: str = ""
20+
allocation_class: Optional[str] = None
21+
22+
23+
@dataclass
24+
class VPCIPv6Range(JSONObject):
25+
"""
26+
VPCIPv6Range represents a single VPC IPv6 range.
27+
"""
28+
29+
put_class = VPCIPv6RangeOptions
30+
31+
range: str = ""
32+
33+
34+
@dataclass
35+
class VPCSubnetIPv6RangeOptions(JSONObject):
36+
"""
37+
VPCSubnetIPv6RangeOptions is used to specify an IPv6 range when creating or updating a VPC subnet.
38+
"""
39+
40+
range: str = ""
41+
42+
43+
@dataclass
44+
class VPCSubnetIPv6Range(JSONObject):
45+
"""
46+
VPCSubnetIPv6Range represents a single VPC subnet IPv6 range.
47+
"""
48+
49+
put_class = VPCSubnetIPv6RangeOptions
50+
51+
range: str = ""
952

1053

1154
@dataclass
@@ -35,6 +78,7 @@ class VPCSubnet(DerivedBase):
3578
"id": Property(identifier=True),
3679
"label": Property(mutable=True),
3780
"ipv4": Property(),
81+
"ipv6": Property(json_object=VPCSubnetIPv6Range, unordered=True),
3882
"linodes": Property(json_object=VPCSubnetLinode, unordered=True),
3983
"created": Property(is_datetime=True),
4084
"updated": Property(is_datetime=True),
@@ -55,6 +99,7 @@ class VPC(Base):
5599
"label": Property(mutable=True),
56100
"description": Property(mutable=True),
57101
"region": Property(slug_relationship=Region),
102+
"ipv6": Property(json_object=VPCIPv6Range, unordered=True),
58103
"subnets": Property(derived_class=VPCSubnet),
59104
"created": Property(is_datetime=True),
60105
"updated": Property(is_datetime=True),
@@ -64,6 +109,9 @@ def subnet_create(
64109
self,
65110
label: str,
66111
ipv4: Optional[str] = None,
112+
ipv6: Optional[
113+
List[Union[VPCSubnetIPv6RangeOptions, Dict[str, Any]]]
114+
] = None,
67115
**kwargs,
68116
) -> VPCSubnet:
69117
"""
@@ -76,19 +124,16 @@ def subnet_create(
76124
:param ipv4: The IPv4 range of this subnet in CIDR format.
77125
:type ipv4: str
78126
:param ipv6: The IPv6 range of this subnet in CIDR format.
79-
:type ipv6: str
127+
:type ipv6: List[Union[VPCSubnetIPv6RangeOptions, Dict[str, Any]]]
80128
"""
81-
params = {
82-
"label": label,
83-
}
84-
85-
if ipv4 is not None:
86-
params["ipv4"] = ipv4
129+
params = {"label": label, "ipv4": ipv4, "ipv6": ipv6}
87130

88131
params.update(kwargs)
89132

90133
result = self._client.post(
91-
"{}/subnets".format(VPC.api_endpoint), model=self, data=params
134+
"{}/subnets".format(VPC.api_endpoint),
135+
model=self,
136+
data=drop_null_keys(_flatten_request_body_recursive(params)),
92137
)
93138
self.invalidate()
94139

0 commit comments

Comments
 (0)