Skip to content

Commit 680c5a2

Browse files
new: Add support for Metadata-related fields and endpoints (#289)
## 📝 Description This change adds preemptive support for endpoints and fields introduced as a part of the Linode Metadata Service. ## ✔️ How to Test ``` tox ```
1 parent 41dbadb commit 680c5a2

File tree

10 files changed

+141
-9
lines changed

10 files changed

+141
-9
lines changed

linode_api4/groups/image.py

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ def __call__(self, *filters):
2929
"""
3030
return self.client._get_and_filter(Image, *filters)
3131

32-
def create(self, disk, label=None, description=None):
32+
def create(self, disk, label=None, description=None, cloud_init=False):
3333
"""
3434
Creates a new Image from a disk you own.
3535
@@ -42,6 +42,8 @@ def create(self, disk, label=None, description=None):
4242
:type label: str
4343
:param description: The description for the new Image.
4444
:type description: str
45+
:param cloud_init: Whether this Image supports cloud-init.
46+
:type cloud_init: bool
4547
4648
:returns: The new Image.
4749
:rtype: Image
@@ -56,6 +58,9 @@ def create(self, disk, label=None, description=None):
5658
if description is not None:
5759
params["description"] = description
5860

61+
if cloud_init:
62+
params["cloud_init"] = cloud_init
63+
5964
result = self.client.post("/images", data=params)
6065

6166
if not "id" in result:
@@ -68,7 +73,11 @@ def create(self, disk, label=None, description=None):
6873
return Image(self.client, result["id"], result)
6974

7075
def create_upload(
71-
self, label: str, region: str, description: str = None
76+
self,
77+
label: str,
78+
region: str,
79+
description: str = None,
80+
cloud_init: bool = False,
7281
) -> Tuple[Image, str]:
7382
"""
7483
Creates a new Image and returns the corresponding upload URL.
@@ -81,12 +90,17 @@ def create_upload(
8190
:type region: str
8291
:param description: The description for the new Image.
8392
:type description: str
93+
:param cloud_init: Whether this Image supports cloud-init.
94+
:type cloud_init: bool
8495
8596
:returns: A tuple containing the new image and the image upload URL.
8697
:rtype: (Image, str)
8798
"""
8899
params = {"label": label, "region": region, "description": description}
89100

101+
if cloud_init:
102+
params["cloud_init"] = cloud_init
103+
90104
result = self.client.post("/images/upload", data=drop_null_keys(params))
91105

92106
if "image" not in result:

linode_api4/groups/linode.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import base64
12
import os
23

34
from linode_api4 import Profile
@@ -245,6 +246,10 @@ def instance_create(
245246
:param private_ip: Whether the new Instance should have private networking
246247
enabled and assigned a private IPv4 address.
247248
:type private_ip: bool
249+
:param metadata: Metadata-related fields to use when creating the new Instance.
250+
The contents of this field can be built using the
251+
:any:`build_instance_metadata` method.
252+
:type metadata: dict
248253
249254
:returns: A new Instance object, or a tuple containing the new Instance and
250255
the generated password.
@@ -301,6 +306,40 @@ def instance_create(
301306
return l
302307
return l, ret_pass
303308

309+
@staticmethod
310+
def build_instance_metadata(user_data=None, encode_user_data=True):
311+
"""
312+
Builds the contents of the ``metadata`` field to be passed into
313+
the :any:`instance_create` method. This helper can also be used
314+
when cloning and rebuilding Instances.
315+
**Creating an Instance with User Data**::
316+
new_linode, password = client.linode.instance_create(
317+
"g6-standard-2",
318+
"us-east",
319+
image="linode/ubuntu22.04",
320+
metadata=client.linode.build_instance_metadata(user_data="myuserdata")
321+
)
322+
:param user_data: User-defined data to provide to the Linode Instance through
323+
the Metadata service.
324+
:type user_data: str
325+
:param encode_user_data: If true, the provided user_data field will be automatically
326+
encoded to a valid base64 string. This field should only
327+
be set to false if the user_data param is already base64-encoded.
328+
:type encode_user_data: bool
329+
:returns: The built ``metadata`` structure.
330+
:rtype: dict
331+
"""
332+
result = {}
333+
334+
if user_data is not None:
335+
result["user_data"] = (
336+
base64.b64encode(user_data.encode()).decode()
337+
if encode_user_data
338+
else user_data
339+
)
340+
341+
return result
342+
304343
def stackscript_create(
305344
self, label, script, images, desc=None, public=False, **kwargs
306345
):

linode_api4/objects/image.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,4 +25,5 @@ class Image(Base):
2525
"vendor": Property(),
2626
"size": Property(),
2727
"deprecated": Property(),
28+
"capabilities": Property(),
2829
}

linode_api4/objects/linode.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -412,6 +412,7 @@ class Instance(Base):
412412
"tags": Property(mutable=True),
413413
"host_uuid": Property(),
414414
"watchdog_enabled": Property(mutable=True),
415+
"has_user_data": Property(),
415416
}
416417

417418
@property

test/fixtures/images.json

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@
1717
"vendor": "Debian",
1818
"eol": "2026-07-01T04:00:00",
1919
"expiry": "2026-08-01T04:00:00",
20-
"updated": "2020-07-01T04:00:00"
20+
"updated": "2020-07-01T04:00:00",
21+
"capabilities": []
2122
},
2223
{
2324
"created": "2017-01-01T00:01:01",
@@ -33,7 +34,8 @@
3334
"vendor": "Ubuntu",
3435
"eol": "2026-07-01T04:00:00",
3536
"expiry": "2026-08-01T04:00:00",
36-
"updated": "2020-07-01T04:00:00"
37+
"updated": "2020-07-01T04:00:00",
38+
"capabilities": []
3739
},
3840
{
3941
"created": "2017-01-01T00:01:01",
@@ -49,7 +51,8 @@
4951
"vendor": "Fedora",
5052
"eol": "2026-07-01T04:00:00",
5153
"expiry": "2026-08-01T04:00:00",
52-
"updated": "2020-07-01T04:00:00"
54+
"updated": "2020-07-01T04:00:00",
55+
"capabilities": []
5356
},
5457
{
5558
"created": "2017-08-20T14:01:01",
@@ -65,7 +68,8 @@
6568
"vendor": null,
6669
"eol": "2026-07-01T04:00:00",
6770
"expiry": "2026-08-01T04:00:00",
68-
"updated": "2020-07-01T04:00:00"
71+
"updated": "2020-07-01T04:00:00",
72+
"capabilities": ["cloud-init"]
6973
}
7074
]
71-
}
75+
}

test/fixtures/images_private_1337.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,5 +12,6 @@
1212
"status": "available",
1313
"type": "manual",
1414
"updated": "2021-08-14T22:44:02",
15-
"vendor": "Debian"
15+
"vendor": "Debian",
16+
"capabilities": ["cloud-init"]
1617
}

test/fixtures/images_upload.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@
1313
"status": "available",
1414
"type": "manual",
1515
"updated": "2021-08-14T22:44:02",
16-
"vendor": "Debian"
16+
"vendor": "Debian",
17+
"capabilities": ["cloud-init"]
1718
},
1819
"upload_to": "https://linode.com/"
1920
}

test/unit/linode_client_test.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,7 @@ def test_image_create(self):
129129

130130
self.assertIsNotNone(i)
131131
self.assertEqual(i.id, "private/123")
132+
self.assertEqual(i.capabilities[0], "cloud-init")
132133

133134
self.assertEqual(m.call_url, "/images")
134135

test/unit/objects/image_test.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ def test_image_create_upload(self):
7777
self.assertEqual(image.id, "private/1337")
7878
self.assertEqual(image.label, "Realest Image Upload")
7979
self.assertEqual(image.description, "very real image upload.")
80+
self.assertEqual(image.capabilities[0], "cloud-init")
8081

8182
self.assertEqual(url, "https://linode.com/")
8283

@@ -100,3 +101,33 @@ def put_mock(url: str, data: BinaryIO = None, **kwargs):
100101
self.assertEqual(image.id, "private/1337")
101102
self.assertEqual(image.label, "Realest Image Upload")
102103
self.assertEqual(image.description, "very real image upload.")
104+
105+
def test_image_create_cloud_init(self):
106+
"""
107+
Test that an image can be created successfully with cloud-init.
108+
"""
109+
110+
with self.mock_post("images/private/123") as m:
111+
self.client.images.create(
112+
"Test Image",
113+
"us-southeast",
114+
description="very real image upload.",
115+
cloud_init=True,
116+
)
117+
118+
self.assertTrue(m.call_data["cloud_init"])
119+
120+
def test_image_create_upload_cloud_init(self):
121+
"""
122+
Test that an image upload URL can be created successfully with cloud-init.
123+
"""
124+
125+
with self.mock_post("images/upload") as m:
126+
self.client.images.create_upload(
127+
"Test Image",
128+
"us-southeast",
129+
description="very real image upload.",
130+
cloud_init=True,
131+
)
132+
133+
self.assertTrue(m.call_data["cloud_init"])

test/unit/objects/linode_test.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -387,6 +387,45 @@ def test_create_disk(self):
387387

388388
assert disk.id == 12345
389389

390+
def test_instance_create_with_user_data(self):
391+
"""
392+
Tests that the metadata field is populated on Linode create.
393+
"""
394+
395+
with self.mock_post("linode/instances/123") as m:
396+
self.client.linode.instance_create(
397+
"g6-nanode-1",
398+
"us-southeast",
399+
metadata=self.client.linode.build_instance_metadata(
400+
user_data="cool"
401+
),
402+
)
403+
404+
self.assertEqual(
405+
m.call_data,
406+
{
407+
"region": "us-southeast",
408+
"type": "g6-nanode-1",
409+
"metadata": {"user_data": "Y29vbA=="},
410+
},
411+
)
412+
413+
def test_build_instance_metadata(self):
414+
"""
415+
Tests that the metadata field is built correctly.
416+
"""
417+
self.assertEqual(
418+
self.client.linode.build_instance_metadata(user_data="cool"),
419+
{"user_data": "Y29vbA=="},
420+
)
421+
422+
self.assertEqual(
423+
self.client.linode.build_instance_metadata(
424+
user_data="cool", encode_user_data=False
425+
),
426+
{"user_data": "cool"},
427+
)
428+
390429

391430
class DiskTest(ClientBaseCase):
392431
"""

0 commit comments

Comments
 (0)