Skip to content

Commit 0e46856

Browse files
zliang-akamailgarber-akamaiykim-akamaiyec-akamaiamisiorek-akamai
authored
Project: Virtual Private Cloud (#345)
* Add support for VPCs * Drop debug statement * Clean up * Revert * oops * Add docs * Add reorder test * Add reorder test * make format * oops * Fix VPCSubnet docs * Address feedback * Use `asdict` to convert dataclass objects to dicts (#324) * test: additional integration tests for vpc (#335) ## 📝 Description - Extending test coverage for VPC ## ✔️ How to Test 1. Setup API token for alpha/beta environment and export it LINODE_TOKEN=mytoken 2. get cacert.pem (e.g. wget https://certurl.com/cacert.pem) 3. make slight modification to `def get_client()` in conftest.py e.g. `client = LinodeClient(token, base_url='https://api.dev.linode.com/v4beta', ca_path='/Users/ykim/linode/ykim/linode_api4-python/cacert.pem')` 4. run test `pytest test/integration/models/test_vpc.py` ## 📷 Preview **If applicable, include a screenshot or code snippet of this change. Otherwise, please remove this section.** --------- Co-authored-by: Zhiwei Liang <[email protected]> Co-authored-by: Zhiwei Liang <[email protected]> * Add `vpc_nat_1_1` to IPAddress (#342) ## 📝 Description This change is adding `vpc_nat_1_1` to the IPAddress for VPC. If a public IPv4 address is NAT 1:1 mapped to a private VPC IP, this field is returned VPC IP together with the VPC and subnet ids. Also trying to merge two commits from dev to proj/vpc to update the feature branch. The actual change is focusing on 3206ab8. ## ✔️ How to Test Build unit test to make sure that we can retrieve this field from IPAddress object: `tox` --------- Co-authored-by: Zhiwei Liang <[email protected]> * implementation * fixing tests * json_object * Support list deserialise in `JSONObject` * cleaning up code * make format * Replace `get_client` with `test_linode_client` (#346) * fix: Handle `null` values in `JSONObject` fields (#344) ## 📝 Description **What does this PR do and why is this change necessary?** Handle null values for `JSONObject` fields ## ✔️ How to Test **What are the steps to reproduce the issue or verify the changes?** `pytest test/integration/models/test_networking.py` --------- Co-authored-by: Lena Garber <[email protected]> Co-authored-by: Lena Garber <[email protected]> Co-authored-by: Youjung Kim <[email protected]> Co-authored-by: Ye Chen <[email protected]> Co-authored-by: Ania Misiorek <[email protected]> Co-authored-by: Ania Misiorek <[email protected]> Co-authored-by: Jacob Riddle <[email protected]>
1 parent 7ffe2bd commit 0e46856

30 files changed

+1618
-67
lines changed

linode_api4/groups/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,3 +18,4 @@
1818
from .support import *
1919
from .tag import *
2020
from .volume import *
21+
from .vpc import *

linode_api4/groups/linode.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
from linode_api4.objects import (
99
AuthorizedApp,
1010
Base,
11+
ConfigInterface,
12+
Firewall,
1113
Image,
1214
Instance,
1315
Kernel,
@@ -250,6 +252,8 @@ def instance_create(
250252
The contents of this field can be built using the
251253
:any:`build_instance_metadata` method.
252254
:type metadata: dict
255+
:param firewall: The firewall to attach this Linode to.
256+
:type firewall: int or Firewall
253257
254258
:returns: A new Instance object, or a tuple containing the new Instance and
255259
the generated password.
@@ -284,6 +288,10 @@ def instance_create(
284288
)
285289
del kwargs["backup"]
286290

291+
if "firewall" in kwargs:
292+
fw = kwargs.pop("firewall")
293+
kwargs["firewall_id"] = fw.id if isinstance(fw, Firewall) else fw
294+
287295
params = {
288296
"type": ltype.id if issubclass(type(ltype), Base) else ltype,
289297
"region": region.id if issubclass(type(region), Base) else region,
@@ -292,6 +300,7 @@ def instance_create(
292300
else None,
293301
"authorized_keys": authorized_keys,
294302
}
303+
295304
params.update(kwargs)
296305

297306
result = self.client.post("/linode/instances", data=params)

linode_api4/groups/vpc.py

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
from typing import Any, Dict, List, Optional, Union
2+
3+
from linode_api4 import VPCSubnet
4+
from linode_api4.errors import UnexpectedResponseError
5+
from linode_api4.groups import Group
6+
from linode_api4.objects import VPC, Base, Region
7+
from linode_api4.paginated_list import PaginatedList
8+
9+
10+
class VPCGroup(Group):
11+
def __call__(self, *filters) -> PaginatedList:
12+
"""
13+
Retrieves all of the VPCs the acting user has access to.
14+
15+
This is intended to be called off of the :any:`LinodeClient`
16+
class, like this::
17+
18+
vpcs = client.vpcs()
19+
20+
API Documentation: TODO
21+
22+
:param filters: Any number of filters to apply to this query.
23+
See :doc:`Filtering Collections</linode_api4/objects/filtering>`
24+
for more details on filtering.
25+
26+
:returns: A list of VPC the acting user can access.
27+
:rtype: PaginatedList of VPC
28+
"""
29+
return self.client._get_and_filter(VPC, *filters)
30+
31+
def create(
32+
self,
33+
label: str,
34+
region: Union[Region, str],
35+
description: Optional[str] = None,
36+
subnets: Optional[List[Dict[str, Any]]] = None,
37+
**kwargs,
38+
) -> VPC:
39+
"""
40+
Creates a new VPC under your Linode account.
41+
42+
API Documentation: TODO
43+
44+
:param label: The label of the newly created VPC.
45+
:type label: str
46+
:param region: The region of the newly created VPC.
47+
:type region: Union[Region, str]
48+
:param description: The user-defined description of this VPC.
49+
:type description: Optional[str]
50+
:param subnets: A list of subnets to create under this VPC.
51+
:type subnets: List[Dict[str, Any]]
52+
53+
:returns: The new VPC object.
54+
:rtype: VPC
55+
"""
56+
params = {
57+
"label": label,
58+
"region": region.id if isinstance(region, Region) else region,
59+
}
60+
61+
if description is not None:
62+
params["description"] = description
63+
64+
if subnets is not None and len(subnets) > 0:
65+
for subnet in subnets:
66+
if not isinstance(subnet, dict):
67+
raise ValueError(
68+
f"Unsupported type for subnet: {type(subnet)}"
69+
)
70+
71+
params["subnets"] = subnets
72+
73+
params.update(kwargs)
74+
75+
result = self.client.post("/vpcs", data=params)
76+
77+
if not "id" in result:
78+
raise UnexpectedResponseError(
79+
"Unexpected response when creating VPC", json=result
80+
)
81+
82+
d = VPC(self.client, result["id"], result)
83+
return d

linode_api4/linode_client.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
SupportGroup,
2929
TagGroup,
3030
VolumeGroup,
31+
VPCGroup,
3132
)
3233
from linode_api4.objects import Image, and_
3334
from linode_api4.objects.filtering import Filter
@@ -190,6 +191,9 @@ def __init__(
190191
#: Access methods related to Images - See :any:`ImageGroup` for more information.
191192
self.images = ImageGroup(self)
192193

194+
#: Access methods related to VPCs - See :any:`VPCGroup` for more information.
195+
self.vpcs = VPCGroup(self)
196+
193197
#: Access methods related to Event polling - See :any:`PollingGroup` for more information.
194198
self.polling = PollingGroup(self)
195199

linode_api4/objects/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# isort: skip_file
22
from .base import Base, Property, MappedObject, DATE_FORMAT, ExplicitNullValue
33
from .dbase import DerivedBase
4+
from .serializable import JSONObject
45
from .filtering import and_, or_
56
from .region import Region
67
from .image import Image
@@ -17,4 +18,5 @@
1718
from .object_storage import *
1819
from .lke import *
1920
from .database import *
21+
from .vpc import *
2022
from .beta import *

linode_api4/objects/base.py

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import time
22
from datetime import datetime, timedelta
33

4+
from linode_api4.objects.serializable import JSONObject
5+
46
from .filtering import FilterableMetaclass
57

68
DATE_FORMAT = "%Y-%m-%dT%H:%M:%S"
@@ -30,6 +32,7 @@ def __init__(
3032
id_relationship=False,
3133
slug_relationship=False,
3234
nullable=False,
35+
json_object=None,
3336
):
3437
"""
3538
A Property is an attribute returned from the API, and defines metadata
@@ -56,6 +59,8 @@ def __init__(
5659
self.is_datetime = is_datetime
5760
self.id_relationship = id_relationship
5861
self.slug_relationship = slug_relationship
62+
self.nullable = nullable
63+
self.json_class = json_object
5964

6065

6166
class MappedObject:
@@ -111,7 +116,7 @@ class Base(object, metaclass=FilterableMetaclass):
111116

112117
properties = {}
113118

114-
def __init__(self, client, id, json={}):
119+
def __init__(self, client: object, id: object, json: object = {}) -> object:
115120
self._set("_populated", False)
116121
self._set("_last_updated", datetime.min)
117122
self._set("_client", client)
@@ -123,8 +128,8 @@ def __init__(self, client, id, json={}):
123128
#: be updated on access.
124129
self._set("_raw_json", None)
125130

126-
for prop in type(self).properties:
127-
self._set(prop, None)
131+
for k in type(self).properties:
132+
self._set(k, None)
128133

129134
self._set("id", id)
130135
if hasattr(type(self), "id_attribute"):
@@ -289,7 +294,7 @@ def _serialize(self):
289294

290295
value = getattr(self, k)
291296

292-
if not value:
297+
if not v.nullable and (value is None or value == ""):
293298
continue
294299

295300
# Let's allow explicit null values as both classes and instances
@@ -305,7 +310,7 @@ def _serialize(self):
305310
for k, v in result.items():
306311
if isinstance(v, Base):
307312
result[k] = v.id
308-
elif isinstance(v, MappedObject):
313+
elif isinstance(v, MappedObject) or issubclass(type(v), JSONObject):
309314
result[k] = v.dict
310315

311316
return result
@@ -376,6 +381,18 @@ def _populate(self, json):
376381
.properties[key]
377382
.slug_relationship(self._client, json[key]),
378383
)
384+
elif type(self).properties[key].json_class:
385+
json_class = type(self).properties[key].json_class
386+
json_value = json[key]
387+
388+
# build JSON object
389+
if isinstance(json_value, list):
390+
# We need special handling for list responses
391+
value = [json_class.from_json(v) for v in json_value]
392+
else:
393+
value = json_class.from_json(json_value)
394+
395+
self._set(key, value)
379396
elif type(json[key]) is dict:
380397
self._set(key, MappedObject(**json[key]))
381398
elif type(json[key]) is list:

0 commit comments

Comments
 (0)