Skip to content

Implement support for VPC Dual Stack #524

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: proj/vpc-dual-stack
Choose a base branch
from
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
20 changes: 13 additions & 7 deletions linode_api4/groups/vpc.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@

from linode_api4.errors import UnexpectedResponseError
from linode_api4.groups import Group
from linode_api4.objects import VPC, Region, VPCIPAddress
from linode_api4.objects import VPC, Region, VPCIPAddress, VPCIPv6RangeOptions
from linode_api4.objects.base import _flatten_request_body_recursive
from linode_api4.paginated_list import PaginatedList
from linode_api4.util import drop_null_keys


class VPCGroup(Group):
Expand Down Expand Up @@ -33,6 +35,7 @@ def create(
region: Union[Region, str],
description: Optional[str] = None,
subnets: Optional[List[Dict[str, Any]]] = None,
ipv6: Optional[List[Union[VPCIPv6RangeOptions, Dict[str, Any]]]] = None,
**kwargs,
) -> VPC:
"""
Expand All @@ -48,30 +51,33 @@ def create(
:type description: Optional[str]
:param subnets: A list of subnets to create under this VPC.
:type subnets: List[Dict[str, Any]]
:param ipv6: The IPv6 address ranges for this VPC.
:type ipv6: List[Union[VPCIPv6RangeOptions, Dict[str, Any]]]

:returns: The new VPC object.
:rtype: VPC
"""
params = {
"label": label,
"region": region.id if isinstance(region, Region) else region,
"description": description,
"ipv6": ipv6,
"subnets": subnets,
}

if description is not None:
params["description"] = description

if subnets is not None and len(subnets) > 0:
for subnet in subnets:
if not isinstance(subnet, dict):
raise ValueError(
f"Unsupported type for subnet: {type(subnet)}"
)

params["subnets"] = subnets

params.update(kwargs)

result = self.client.post("/vpcs", data=params)
result = self.client.post(
"/vpcs",
data=drop_null_keys(_flatten_request_body_recursive(params)),
)

if not "id" in result:
raise UnexpectedResponseError(
Expand Down
4 changes: 2 additions & 2 deletions linode_api4/objects/account.py
Original file line number Diff line number Diff line change
Expand Up @@ -601,7 +601,7 @@ def entity(self):
)
return self.cls(self._client, self.id)

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

return grants

def _serialize(self):
def _serialize(self, *args, **kwargs):
"""
Returns the user grants in as JSON the api will accept.
This is only relevant in the context of UserGrants.save
Expand Down
26 changes: 16 additions & 10 deletions linode_api4/objects/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,9 @@ def _flatten_base_subclass(obj: "Base") -> Optional[Dict[str, Any]]:

@property
def dict(self):
return self._serialize()

def _serialize(self, is_put: bool = False) -> Dict[str, Any]:
result = vars(self).copy()
cls = type(self)

Expand All @@ -123,7 +126,7 @@ def dict(self):
elif isinstance(v, list):
result[k] = [
(
item.dict
item._serialize(is_put=is_put)
if isinstance(item, (cls, JSONObject))
else (
self._flatten_base_subclass(item)
Expand All @@ -136,7 +139,7 @@ def dict(self):
elif isinstance(v, Base):
result[k] = self._flatten_base_subclass(v)
elif isinstance(v, JSONObject):
result[k] = v.dict
result[k] = v._serialize(is_put=is_put)

return result

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

# Ensure we serialize any values that may not be already serialized
data = _flatten_request_body_recursive(data)
data = _flatten_request_body_recursive(data, is_put=True)
else:
data = self._serialize()
data = self._serialize(is_put=True)

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

Expand Down Expand Up @@ -316,7 +319,7 @@ def invalidate(self):

self._set("_populated", False)

def _serialize(self):
def _serialize(self, is_put: bool = False):
"""
A helper method to build a dict of all mutable Properties of
this object
Expand Down Expand Up @@ -345,7 +348,7 @@ def _serialize(self):

# Resolve the underlying IDs of results
for k, v in result.items():
result[k] = _flatten_request_body_recursive(v)
result[k] = _flatten_request_body_recursive(v, is_put=is_put)

return result

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


def _flatten_request_body_recursive(data: Any) -> Any:
def _flatten_request_body_recursive(data: Any, is_put: bool = False) -> Any:
"""
This is a helper recursively flatten the given data for use in an API request body.

Expand All @@ -515,15 +518,18 @@ def _flatten_request_body_recursive(data: Any) -> Any:
"""

if isinstance(data, dict):
return {k: _flatten_request_body_recursive(v) for k, v in data.items()}
return {
k: _flatten_request_body_recursive(v, is_put=is_put)
for k, v in data.items()
}

if isinstance(data, list):
return [_flatten_request_body_recursive(v) for v in data]
return [_flatten_request_body_recursive(v, is_put=is_put) for v in data]

if isinstance(data, Base):
return data.id

if isinstance(data, MappedObject) or issubclass(type(data), JSONObject):
return data.dict
return data._serialize(is_put=is_put)

return data
131 changes: 104 additions & 27 deletions linode_api4/objects/linode.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import string
import sys
from dataclasses import dataclass
from dataclasses import dataclass, field
from datetime import datetime
from enum import Enum
from os import urandom
Expand Down Expand Up @@ -291,10 +291,83 @@ def _populate(self, json):

@dataclass
class ConfigInterfaceIPv4(JSONObject):
"""
ConfigInterfaceIPv4 represents the IPv4 configuration of a VPC interface.
"""

vpc: str = ""
nat_1_1: str = ""


@dataclass
class ConfigInterfaceIPv6SLAACOptions(JSONObject):
"""
ConfigInterfaceIPv6SLAACOptions is used to set a single IPv6 SLAAC configuration of a VPC interface.
"""

range: str = ""


@dataclass
class ConfigInterfaceIPv6RangeOptions(JSONObject):
"""
ConfigInterfaceIPv6RangeOptions is used to set a single IPv6 range configuration of a VPC interface.
"""

range: str = ""


@dataclass
class ConfigInterfaceIPv6Options(JSONObject):
"""
ConfigInterfaceIPv6Options is used to set the IPv6 configuration of a VPC interface.
"""

slaac: List[ConfigInterfaceIPv6SLAACOptions] = field(
default_factory=lambda: []
)
ranges: List[ConfigInterfaceIPv6RangeOptions] = field(
default_factory=lambda: []
)
is_public: bool = False


@dataclass
class ConfigInterfaceIPv6SLAAC(JSONObject):
"""
ConfigInterfaceIPv6SLAAC represents a single SLAAC address under a VPC interface's IPv6 configuration.
"""

put_class = ConfigInterfaceIPv6SLAACOptions

range: str = ""
address: str = ""


@dataclass
class ConfigInterfaceIPv6Range(JSONObject):
"""
ConfigInterfaceIPv6Range represents a single IPv6 address under a VPC interface's IPv6 configuration.
"""

put_class = ConfigInterfaceIPv6RangeOptions

range: str = ""


@dataclass
class ConfigInterfaceIPv6(JSONObject):
"""
ConfigInterfaceIPv6 represents the IPv6 configuration of a VPC interface.
"""

put_class = ConfigInterfaceIPv6Options

slaac: List[ConfigInterfaceIPv6SLAAC] = field(default_factory=lambda: [])
ranges: List[ConfigInterfaceIPv6Range] = field(default_factory=lambda: [])
is_public: bool = False


class NetworkInterface(DerivedBase):
"""
This class represents a Configuration Profile's network interface object.
Expand All @@ -320,6 +393,7 @@ class NetworkInterface(DerivedBase):
"vpc_id": Property(id_relationship=VPC),
"subnet_id": Property(),
"ipv4": Property(mutable=True, json_object=ConfigInterfaceIPv4),
"ipv6": Property(mutable=True, json_object=ConfigInterfaceIPv6),
"ip_ranges": Property(mutable=True),
}

Expand Down Expand Up @@ -391,7 +465,10 @@ class ConfigInterface(JSONObject):
# VPC-specific
vpc_id: Optional[int] = None
subnet_id: Optional[int] = None

ipv4: Optional[Union[ConfigInterfaceIPv4, Dict[str, Any]]] = None
ipv6: Optional[Union[ConfigInterfaceIPv6, Dict[str, Any]]] = None

ip_ranges: Optional[List[str]] = None

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

def _serialize(self):
def _serialize(self, is_put: bool = False):
purpose_formats = {
"public": {"purpose": "public", "primary": self.primary},
"vlan": {
Expand All @@ -412,11 +489,8 @@ def _serialize(self):
"purpose": "vpc",
"primary": self.primary,
"subnet_id": self.subnet_id,
"ipv4": (
self.ipv4.dict
if isinstance(self.ipv4, ConfigInterfaceIPv4)
else self.ipv4
),
"ipv4": self.ipv4,
"ipv6": self.ipv6,
"ip_ranges": self.ip_ranges,
},
}
Expand All @@ -426,11 +500,14 @@ def _serialize(self):
f"Unknown interface purpose: {self.purpose}",
)

return {
k: v
for k, v in purpose_formats[self.purpose].items()
if v is not None
}
return _flatten_request_body_recursive(
{
k: v
for k, v in purpose_formats[self.purpose].items()
if v is not None
},
is_put=is_put,
)


class Config(DerivedBase):
Expand Down Expand Up @@ -510,16 +587,16 @@ def _populate(self, json):

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

def _serialize(self):
def _serialize(self, *args, **kwargs):
"""
Overrides _serialize to transform interfaces into json
"""
partial = DerivedBase._serialize(self)
partial = DerivedBase._serialize(self, *args, **kwargs)
interfaces = []

for c in self.interfaces:
if isinstance(c, ConfigInterface):
interfaces.append(c._serialize())
interfaces.append(c._serialize(*args, **kwargs))
else:
interfaces.append(c)

Expand Down Expand Up @@ -571,6 +648,7 @@ def interface_create_vpc(
subnet: Union[int, VPCSubnet],
primary=False,
ipv4: Union[Dict[str, Any], ConfigInterfaceIPv4] = None,
ipv6: Union[Dict[str, Any], ConfigInterfaceIPv6Options] = None,
ip_ranges: Optional[List[str]] = None,
) -> NetworkInterface:
"""
Expand All @@ -584,6 +662,8 @@ def interface_create_vpc(
:type primary: bool
:param ipv4: The IPv4 configuration of the interface for the associated subnet.
:type ipv4: Dict or ConfigInterfaceIPv4
:param ipv6: The IPv6 configuration of the interface for the associated subnet.
:type ipv6: Dict or ConfigInterfaceIPv6Options
:param ip_ranges: A list of IPs or IP ranges in the VPC subnet.
Packets to these CIDRs are routed through the
VPC network interface.
Expand All @@ -594,19 +674,16 @@ def interface_create_vpc(
"""
params = {
"purpose": "vpc",
"subnet_id": subnet.id if isinstance(subnet, VPCSubnet) else subnet,
"subnet_id": subnet,
"primary": primary,
"ipv4": ipv4,
"ipv6": ipv6,
"ip_ranges": ip_ranges,
}

if ipv4 is not None:
params["ipv4"] = (
ipv4.dict if isinstance(ipv4, ConfigInterfaceIPv4) else ipv4
)

if ip_ranges is not None:
params["ip_ranges"] = ip_ranges

return self._interface_create(params)
return self._interface_create(
drop_null_keys(_flatten_request_body_recursive(params))
)

def interface_reorder(self, interfaces: List[Union[int, NetworkInterface]]):
"""
Expand Down Expand Up @@ -1927,8 +2004,8 @@ def _populate(self, json):
ndist = [Image(self._client, d) for d in self.images]
self._set("images", ndist)

def _serialize(self):
dct = Base._serialize(self)
def _serialize(self, *args, **kwargs):
dct = Base._serialize(self, *args, **kwargs)
dct["images"] = [d.id for d in self.images]
return dct

Expand Down
Loading