Skip to content

Commit d7ddd4a

Browse files
Implement support for VPC Dual Stack
1 parent cf04ca6 commit d7ddd4a

19 files changed

+598
-218
lines changed

Diff for: linode_api4/groups/vpc.py

+13-7
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(

Diff for: linode_api4/objects/account.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -601,7 +601,7 @@ def entity(self):
601601
)
602602
return self.cls(self._client, self.id)
603603

604-
def _serialize(self):
604+
def _serialize(self, *args, **kwargs):
605605
"""
606606
Returns this grant in as JSON the api will accept. This is only relevant
607607
in the context of UserGrants.save
@@ -668,7 +668,7 @@ def _grants_dict(self):
668668

669669
return grants
670670

671-
def _serialize(self):
671+
def _serialize(self, *args, **kwargs):
672672
"""
673673
Returns the user grants in as JSON the api will accept.
674674
This is only relevant in the context of UserGrants.save

Diff for: linode_api4/objects/base.py

+16-10
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,9 @@ def _flatten_base_subclass(obj: "Base") -> Optional[Dict[str, Any]]:
114114

115115
@property
116116
def dict(self):
117+
return self._serialize()
118+
119+
def _serialize(self, is_put: bool = False) -> Dict[str, Any]:
117120
result = vars(self).copy()
118121
cls = type(self)
119122

@@ -123,7 +126,7 @@ def dict(self):
123126
elif isinstance(v, list):
124127
result[k] = [
125128
(
126-
item.dict
129+
item._serialize(is_put=is_put)
127130
if isinstance(item, (cls, JSONObject))
128131
else (
129132
self._flatten_base_subclass(item)
@@ -136,7 +139,7 @@ def dict(self):
136139
elif isinstance(v, Base):
137140
result[k] = self._flatten_base_subclass(v)
138141
elif isinstance(v, JSONObject):
139-
result[k] = v.dict
142+
result[k] = v._serialize(is_put=is_put)
140143

141144
return result
142145

@@ -278,9 +281,9 @@ def save(self, force=True) -> bool:
278281
data[key] = None
279282

280283
# Ensure we serialize any values that may not be already serialized
281-
data = _flatten_request_body_recursive(data)
284+
data = _flatten_request_body_recursive(data, is_put=True)
282285
else:
283-
data = self._serialize()
286+
data = self._serialize(is_put=True)
284287

285288
resp = self._client.put(type(self).api_endpoint, model=self, data=data)
286289

@@ -316,7 +319,7 @@ def invalidate(self):
316319

317320
self._set("_populated", False)
318321

319-
def _serialize(self):
322+
def _serialize(self, is_put: bool = False):
320323
"""
321324
A helper method to build a dict of all mutable Properties of
322325
this object
@@ -345,7 +348,7 @@ def _serialize(self):
345348

346349
# Resolve the underlying IDs of results
347350
for k, v in result.items():
348-
result[k] = _flatten_request_body_recursive(v)
351+
result[k] = _flatten_request_body_recursive(v, is_put=is_put)
349352

350353
return result
351354

@@ -503,7 +506,7 @@ def make_instance(cls, id, client, parent_id=None, json=None):
503506
return Base.make(id, client, cls, parent_id=parent_id, json=json)
504507

505508

506-
def _flatten_request_body_recursive(data: Any) -> Any:
509+
def _flatten_request_body_recursive(data: Any, is_put: bool = False) -> Any:
507510
"""
508511
This is a helper recursively flatten the given data for use in an API request body.
509512
@@ -515,15 +518,18 @@ def _flatten_request_body_recursive(data: Any) -> Any:
515518
"""
516519

517520
if isinstance(data, dict):
518-
return {k: _flatten_request_body_recursive(v) for k, v in data.items()}
521+
return {
522+
k: _flatten_request_body_recursive(v, is_put=is_put)
523+
for k, v in data.items()
524+
}
519525

520526
if isinstance(data, list):
521-
return [_flatten_request_body_recursive(v) for v in data]
527+
return [_flatten_request_body_recursive(v, is_put=is_put) for v in data]
522528

523529
if isinstance(data, Base):
524530
return data.id
525531

526532
if isinstance(data, MappedObject) or issubclass(type(data), JSONObject):
527-
return data.dict
533+
return data._serialize(is_put=is_put)
528534

529535
return data

Diff for: linode_api4/objects/linode.py

+104-27
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):
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):
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):
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):
@@ -510,16 +587,16 @@ def _populate(self, json):
510587

511588
self._set("devices", MappedObject(**devices))
512589

513-
def _serialize(self):
590+
def _serialize(self, *args, **kwargs):
514591
"""
515592
Overrides _serialize to transform interfaces into json
516593
"""
517-
partial = DerivedBase._serialize(self)
594+
partial = DerivedBase._serialize(self, *args, **kwargs)
518595
interfaces = []
519596

520597
for c in self.interfaces:
521598
if isinstance(c, ConfigInterface):
522-
interfaces.append(c._serialize())
599+
interfaces.append(c._serialize(*args, **kwargs))
523600
else:
524601
interfaces.append(c)
525602

@@ -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
"""
@@ -1927,8 +2004,8 @@ def _populate(self, json):
19272004
ndist = [Image(self._client, d) for d in self.images]
19282005
self._set("images", ndist)
19292006

1930-
def _serialize(self):
1931-
dct = Base._serialize(self)
2007+
def _serialize(self, *args, **kwargs):
2008+
dct = Base._serialize(self, *args, **kwargs)
19322009
dct["images"] = [d.id for d in self.images]
19332010
return dct
19342011

0 commit comments

Comments
 (0)