Skip to content

Commit df2d8c1

Browse files
new: Add support for Linode Disk Encryption (#413)
1 parent 90b43ab commit df2d8c1

14 files changed

+197
-24
lines changed

linode_api4/groups/linode.py

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import base64
22
import os
33
from collections.abc import Iterable
4+
from typing import Optional, Union
45

5-
from linode_api4 import Profile
6+
from linode_api4 import InstanceDiskEncryptionType, Profile
67
from linode_api4.common import SSH_KEY_TYPES, load_and_validate_keys
78
from linode_api4.errors import UnexpectedResponseError
89
from linode_api4.groups import Group
@@ -131,7 +132,15 @@ def kernels(self, *filters):
131132

132133
# create things
133134
def instance_create(
134-
self, ltype, region, image=None, authorized_keys=None, **kwargs
135+
self,
136+
ltype,
137+
region,
138+
image=None,
139+
authorized_keys=None,
140+
disk_encryption: Optional[
141+
Union[InstanceDiskEncryptionType, str]
142+
] = None,
143+
**kwargs,
135144
):
136145
"""
137146
Creates a new Linode Instance. This function has several modes of operation:
@@ -266,6 +275,8 @@ def instance_create(
266275
:type metadata: dict
267276
:param firewall: The firewall to attach this Linode to.
268277
:type firewall: int or Firewall
278+
:param disk_encryption: The disk encryption policy for this Linode.
279+
:type disk_encryption: InstanceDiskEncryptionType or str
269280
:param interfaces: An array of Network Interfaces to add to this Linode’s Configuration Profile.
270281
At least one and up to three Interface objects can exist in this array.
271282
:type interfaces: list[ConfigInterface] or list[dict[str, Any]]
@@ -326,6 +337,9 @@ def instance_create(
326337
"authorized_keys": authorized_keys,
327338
}
328339

340+
if disk_encryption is not None:
341+
params["disk_encryption"] = str(disk_encryption)
342+
329343
params.update(kwargs)
330344

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

linode_api4/objects/linode.py

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,25 @@
2222
from linode_api4.objects.base import MappedObject
2323
from linode_api4.objects.filtering import FilterableAttribute
2424
from linode_api4.objects.networking import IPAddress, IPv6Range, VPCIPAddress
25+
from linode_api4.objects.serializable import StrEnum
2526
from linode_api4.objects.vpc import VPC, VPCSubnet
2627
from linode_api4.paginated_list import PaginatedList
2728

2829
PASSWORD_CHARS = string.ascii_letters + string.digits + string.punctuation
2930

3031

32+
class InstanceDiskEncryptionType(StrEnum):
33+
"""
34+
InstanceDiskEncryptionType defines valid values for the
35+
Instance(...).disk_encryption field.
36+
37+
API Documentation: TODO
38+
"""
39+
40+
enabled = "enabled"
41+
disabled = "disabled"
42+
43+
3144
class Backup(DerivedBase):
3245
"""
3346
A Backup of a Linode Instance.
@@ -114,6 +127,7 @@ class Disk(DerivedBase):
114127
"filesystem": Property(),
115128
"updated": Property(is_datetime=True),
116129
"linode_id": Property(identifier=True),
130+
"disk_encryption": Property(),
117131
}
118132

119133
def duplicate(self):
@@ -650,6 +664,8 @@ class Instance(Base):
650664
"host_uuid": Property(),
651665
"watchdog_enabled": Property(mutable=True),
652666
"has_user_data": Property(),
667+
"disk_encryption": Property(),
668+
"lke_cluster_id": Property(),
653669
}
654670

655671
@property
@@ -1343,7 +1359,16 @@ def ip_allocate(self, public=False):
13431359
i = IPAddress(self._client, result["address"], result)
13441360
return i
13451361

1346-
def rebuild(self, image, root_pass=None, authorized_keys=None, **kwargs):
1362+
def rebuild(
1363+
self,
1364+
image,
1365+
root_pass=None,
1366+
authorized_keys=None,
1367+
disk_encryption: Optional[
1368+
Union[InstanceDiskEncryptionType, str]
1369+
] = None,
1370+
**kwargs,
1371+
):
13471372
"""
13481373
Rebuilding an Instance deletes all existing Disks and Configs and deploys
13491374
a new :any:`Image` to it. This can be used to reset an existing
@@ -1361,6 +1386,8 @@ def rebuild(self, image, root_pass=None, authorized_keys=None, **kwargs):
13611386
be a single key, or a path to a file containing
13621387
the key.
13631388
:type authorized_keys: list or str
1389+
:param disk_encryption: The disk encryption policy for this Linode.
1390+
:type disk_encryption: InstanceDiskEncryptionType or str
13641391
13651392
:returns: The newly generated password, if one was not provided
13661393
(otherwise True)
@@ -1378,6 +1405,10 @@ def rebuild(self, image, root_pass=None, authorized_keys=None, **kwargs):
13781405
"root_pass": root_pass,
13791406
"authorized_keys": authorized_keys,
13801407
}
1408+
1409+
if disk_encryption is not None:
1410+
params["disk_encryption"] = str(disk_encryption)
1411+
13811412
params.update(kwargs)
13821413

13831414
result = self._client.post(
@@ -1683,6 +1714,22 @@ def stats(self):
16831714
"{}/stats".format(Instance.api_endpoint), model=self
16841715
)
16851716

1717+
@property
1718+
def lke_cluster(self) -> Optional["LKECluster"]:
1719+
"""
1720+
Returns the LKE Cluster this Instance is a node of.
1721+
1722+
:returns: The LKE Cluster this Instance is a node of.
1723+
:rtype: Optional[LKECluster]
1724+
"""
1725+
1726+
# Local import to prevent circular dependency
1727+
from linode_api4.objects.lke import ( # pylint: disable=import-outside-toplevel
1728+
LKECluster,
1729+
)
1730+
1731+
return LKECluster(self._client, self.lke_cluster_id)
1732+
16861733
def stats_for(self, dt):
16871734
"""
16881735
Returns stats for the month containing the given datetime

linode_api4/objects/lke.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,7 @@ class LKENodePool(DerivedBase):
127127
"cluster_id": Property(identifier=True),
128128
"type": Property(slug_relationship=Type),
129129
"disks": Property(),
130+
"disk_encryption": Property(),
130131
"count": Property(mutable=True),
131132
"nodes": Property(
132133
volatile=True

linode_api4/objects/serializable.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import inspect
22
from dataclasses import dataclass
3+
from enum import Enum
34
from types import SimpleNamespace
45
from typing import (
56
Any,
@@ -223,3 +224,22 @@ def __delitem__(self, key):
223224

224225
def __len__(self):
225226
return len(vars(self))
227+
228+
229+
class StrEnum(str, Enum):
230+
"""
231+
Used for enums that are of type string, which is necessary
232+
for implicit JSON serialization.
233+
234+
NOTE: Replace this with StrEnum once Python 3.10 has been EOL'd.
235+
See: https://docs.python.org/3/library/enum.html#enum.StrEnum
236+
"""
237+
238+
def __new__(cls, *values):
239+
value = str(*values)
240+
member = str.__new__(cls, value)
241+
member._value_ = value
242+
return member
243+
244+
def __str__(self):
245+
return self._value_

test/fixtures/linode_instances.json

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,9 @@
4040
"image": "linode/ubuntu17.04",
4141
"tags": ["something"],
4242
"host_uuid": "3a3ddd59d9a78bb8de041391075df44de62bfec8",
43-
"watchdog_enabled": true
43+
"watchdog_enabled": true,
44+
"disk_encryption": "disabled",
45+
"lke_cluster_id": null
4446
},
4547
{
4648
"group": "test",
@@ -79,7 +81,9 @@
7981
"image": "linode/debian9",
8082
"tags": [],
8183
"host_uuid": "3a3ddd59d9a78bb8de041391075df44de62bfec8",
82-
"watchdog_enabled": false
84+
"watchdog_enabled": false,
85+
"disk_encryption": "enabled",
86+
"lke_cluster_id": 18881
8387
}
8488
]
8589
}

test/fixtures/linode_instances_123_disks.json

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@
1010
"id": 12345,
1111
"updated": "2017-01-01T00:00:00",
1212
"label": "Ubuntu 17.04 Disk",
13-
"created": "2017-01-01T00:00:00"
13+
"created": "2017-01-01T00:00:00",
14+
"disk_encryption": "disabled"
1415
},
1516
{
1617
"size": 512,
@@ -19,7 +20,8 @@
1920
"id": 12346,
2021
"updated": "2017-01-01T00:00:00",
2122
"label": "512 MB Swap Image",
22-
"created": "2017-01-01T00:00:00"
23+
"created": "2017-01-01T00:00:00",
24+
"disk_encryption": "disabled"
2325
}
2426
]
2527
}

test/fixtures/linode_instances_123_disks_12345_clone.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
"id": 12345,
66
"updated": "2017-01-01T00:00:00",
77
"label": "Ubuntu 17.04 Disk",
8-
"created": "2017-01-01T00:00:00"
8+
"created": "2017-01-01T00:00:00",
9+
"disk_encryption": "disabled"
910
}
1011

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
22
"id": "123456",
3-
"instance_id": 123458,
3+
"instance_id": 456,
44
"status": "ready"
55
}

test/fixtures/lke_clusters_18881_pools_456.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,5 +23,6 @@
2323
"example tag",
2424
"another example"
2525
],
26-
"type": "g6-standard-4"
26+
"type": "g6-standard-4",
27+
"disk_encryption": "enabled"
2728
}

test/integration/helpers.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -79,12 +79,14 @@ def wait_for_condition(
7979

8080

8181
# Retry function to help in case of requests sending too quickly before instance is ready
82-
def retry_sending_request(retries: int, condition: Callable, *args) -> object:
82+
def retry_sending_request(
83+
retries: int, condition: Callable, *args, **kwargs
84+
) -> object:
8385
curr_t = 0
8486
while curr_t < retries:
8587
try:
8688
curr_t += 1
87-
res = condition(*args)
89+
res = condition(*args, **kwargs)
8890
return res
8991
except ApiError:
9092
if curr_t >= retries:

test/integration/models/linode/test_linode.py

Lines changed: 46 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import time
2+
from test.integration.conftest import get_region
23
from test.integration.helpers import (
34
get_test_label,
45
retry_sending_request,
@@ -19,7 +20,7 @@
1920
Instance,
2021
Type,
2122
)
22-
from linode_api4.objects.linode import MigrationType
23+
from linode_api4.objects.linode import InstanceDiskEncryptionType, MigrationType
2324

2425

2526
@pytest.fixture(scope="session")
@@ -137,6 +138,30 @@ def create_linode_for_long_running_tests(test_linode_client):
137138
linode_instance.delete()
138139

139140

141+
@pytest.fixture(scope="function")
142+
def linode_with_disk_encryption(test_linode_client, request):
143+
client = test_linode_client
144+
145+
target_region = get_region(client, {"Disk Encryption"})
146+
timestamp = str(time.time_ns())
147+
label = "TestSDK-" + timestamp
148+
149+
disk_encryption = request.param
150+
151+
linode_instance, password = client.linode.instance_create(
152+
"g6-nanode-1",
153+
target_region,
154+
image="linode/ubuntu23.04",
155+
label=label,
156+
booted=False,
157+
disk_encryption=disk_encryption,
158+
)
159+
160+
yield linode_instance
161+
162+
linode_instance.delete()
163+
164+
140165
# Test helper
141166
def get_status(linode: Instance, status: str):
142167
return linode.status == status
@@ -165,8 +190,7 @@ def test_linode_transfer(test_linode_client, linode_with_volume_firewall):
165190

166191
def test_linode_rebuild(test_linode_client):
167192
client = test_linode_client
168-
available_regions = client.regions()
169-
chosen_region = available_regions[4]
193+
chosen_region = get_region(client, {"Disk Encryption"})
170194
label = get_test_label() + "_rebuild"
171195

172196
linode, password = client.linode.instance_create(
@@ -175,12 +199,18 @@ def test_linode_rebuild(test_linode_client):
175199

176200
wait_for_condition(10, 100, get_status, linode, "running")
177201

178-
retry_sending_request(3, linode.rebuild, "linode/debian10")
202+
retry_sending_request(
203+
3,
204+
linode.rebuild,
205+
"linode/debian10",
206+
disk_encryption=InstanceDiskEncryptionType.disabled,
207+
)
179208

180209
wait_for_condition(10, 100, get_status, linode, "rebuilding")
181210

182211
assert linode.status == "rebuilding"
183212
assert linode.image.id == "linode/debian10"
213+
assert linode.disk_encryption == InstanceDiskEncryptionType.disabled
184214

185215
wait_for_condition(10, 300, get_status, linode, "running")
186216

@@ -383,6 +413,18 @@ def test_linode_volumes(linode_with_volume_firewall):
383413
assert "test" in volumes[0].label
384414

385415

416+
@pytest.mark.parametrize(
417+
"linode_with_disk_encryption", ["disabled"], indirect=True
418+
)
419+
def test_linode_with_disk_encryption_disabled(linode_with_disk_encryption):
420+
linode = linode_with_disk_encryption
421+
422+
assert linode.disk_encryption == InstanceDiskEncryptionType.disabled
423+
assert (
424+
linode.disks[0].disk_encryption == InstanceDiskEncryptionType.disabled
425+
)
426+
427+
386428
def wait_for_disk_status(disk: Disk, timeout):
387429
start_time = time.time()
388430
while True:

0 commit comments

Comments
 (0)