Skip to content

Commit

Permalink
Merge pull request #436 from linode/proj/disk-encryption
Browse files Browse the repository at this point in the history
project: Linode Disk Encryption (re-apply)
  • Loading branch information
lgarber-akamai authored Jul 23, 2024
2 parents 72481ad + 9486d67 commit c786ecf
Show file tree
Hide file tree
Showing 13 changed files with 179 additions and 20 deletions.
18 changes: 17 additions & 1 deletion linode_api4/groups/linode.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import base64
import os
from collections.abc import Iterable
from typing import Optional, Union

from linode_api4 import InstanceDiskEncryptionType
from linode_api4.common import load_and_validate_keys
from linode_api4.errors import UnexpectedResponseError
from linode_api4.groups import Group
Expand Down Expand Up @@ -128,7 +130,15 @@ def kernels(self, *filters):

# create things
def instance_create(
self, ltype, region, image=None, authorized_keys=None, **kwargs
self,
ltype,
region,
image=None,
authorized_keys=None,
disk_encryption: Optional[
Union[InstanceDiskEncryptionType, str]
] = None,
**kwargs,
):
"""
Creates a new Linode Instance. This function has several modes of operation:
Expand Down Expand Up @@ -263,6 +273,9 @@ def instance_create(
:type metadata: dict
:param firewall: The firewall to attach this Linode to.
:type firewall: int or Firewall
:param disk_encryption: The disk encryption policy for this Linode.
NOTE: Disk encryption may not currently be available to all users.
:type disk_encryption: InstanceDiskEncryptionType or str
:param interfaces: An array of Network Interfaces to add to this Linode’s Configuration Profile.
At least one and up to three Interface objects can exist in this array.
:type interfaces: list[ConfigInterface] or list[dict[str, Any]]
Expand Down Expand Up @@ -330,6 +343,9 @@ def instance_create(
"authorized_keys": authorized_keys,
}

if disk_encryption is not None:
params["disk_encryption"] = str(disk_encryption)

params.update(kwargs)

result = self.client.post("/linode/instances", data=params)
Expand Down
50 changes: 49 additions & 1 deletion linode_api4/objects/linode.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,25 @@
from linode_api4.objects.base import MappedObject
from linode_api4.objects.filtering import FilterableAttribute
from linode_api4.objects.networking import IPAddress, IPv6Range, VPCIPAddress
from linode_api4.objects.serializable import StrEnum
from linode_api4.objects.vpc import VPC, VPCSubnet
from linode_api4.paginated_list import PaginatedList

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


class InstanceDiskEncryptionType(StrEnum):
"""
InstanceDiskEncryptionType defines valid values for the
Instance(...).disk_encryption field.
API Documentation: TODO
"""

enabled = "enabled"
disabled = "disabled"


class Backup(DerivedBase):
"""
A Backup of a Linode Instance.
Expand Down Expand Up @@ -114,6 +127,7 @@ class Disk(DerivedBase):
"filesystem": Property(),
"updated": Property(is_datetime=True),
"linode_id": Property(identifier=True),
"disk_encryption": Property(),
}

def duplicate(self):
Expand Down Expand Up @@ -662,6 +676,8 @@ class Instance(Base):
"host_uuid": Property(),
"watchdog_enabled": Property(mutable=True),
"has_user_data": Property(),
"disk_encryption": Property(),
"lke_cluster_id": Property(),
}

@property
Expand Down Expand Up @@ -1391,7 +1407,16 @@ def ip_allocate(self, public=False):
i = IPAddress(self._client, result["address"], result)
return i

def rebuild(self, image, root_pass=None, authorized_keys=None, **kwargs):
def rebuild(
self,
image,
root_pass=None,
authorized_keys=None,
disk_encryption: Optional[
Union[InstanceDiskEncryptionType, str]
] = None,
**kwargs,
):
"""
Rebuilding an Instance deletes all existing Disks and Configs and deploys
a new :any:`Image` to it. This can be used to reset an existing
Expand All @@ -1409,6 +1434,9 @@ def rebuild(self, image, root_pass=None, authorized_keys=None, **kwargs):
be a single key, or a path to a file containing
the key.
:type authorized_keys: list or str
:param disk_encryption: The disk encryption policy for this Linode.
NOTE: Disk encryption may not currently be available to all users.
:type disk_encryption: InstanceDiskEncryptionType or str
:returns: The newly generated password, if one was not provided
(otherwise True)
Expand All @@ -1426,6 +1454,10 @@ def rebuild(self, image, root_pass=None, authorized_keys=None, **kwargs):
"root_pass": root_pass,
"authorized_keys": authorized_keys,
}

if disk_encryption is not None:
params["disk_encryption"] = str(disk_encryption)

params.update(kwargs)

result = self._client.post(
Expand Down Expand Up @@ -1755,6 +1787,22 @@ def stats(self):
"{}/stats".format(Instance.api_endpoint), model=self
)

@property
def lke_cluster(self) -> Optional["LKECluster"]:
"""
Returns the LKE Cluster this Instance is a node of.
:returns: The LKE Cluster this Instance is a node of.
:rtype: Optional[LKECluster]
"""

# Local import to prevent circular dependency
from linode_api4.objects.lke import ( # pylint: disable=import-outside-toplevel
LKECluster,
)

return LKECluster(self._client, self.lke_cluster_id)

def stats_for(self, dt):
"""
Returns stats for the month containing the given datetime
Expand Down
1 change: 1 addition & 0 deletions linode_api4/objects/lke.py
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@ class LKENodePool(DerivedBase):
"cluster_id": Property(identifier=True),
"type": Property(slug_relationship=Type),
"disks": Property(),
"disk_encryption": Property(),
"count": Property(mutable=True),
"nodes": Property(
volatile=True
Expand Down
4 changes: 4 additions & 0 deletions test/fixtures/linode_instances.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@
"tags": ["something"],
"host_uuid": "3a3ddd59d9a78bb8de041391075df44de62bfec8",
"watchdog_enabled": true,
"disk_encryption": "disabled",
"lke_cluster_id": null,
"placement_group": {
"id": 123,
"label": "test",
Expand Down Expand Up @@ -86,6 +88,8 @@
"tags": [],
"host_uuid": "3a3ddd59d9a78bb8de041391075df44de62bfec8",
"watchdog_enabled": false,
"disk_encryption": "enabled",
"lke_cluster_id": 18881,
"placement_group": null
}
]
Expand Down
6 changes: 4 additions & 2 deletions test/fixtures/linode_instances_123_disks.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@
"id": 12345,
"updated": "2017-01-01T00:00:00",
"label": "Ubuntu 17.04 Disk",
"created": "2017-01-01T00:00:00"
"created": "2017-01-01T00:00:00",
"disk_encryption": "disabled"
},
{
"size": 512,
Expand All @@ -19,7 +20,8 @@
"id": 12346,
"updated": "2017-01-01T00:00:00",
"label": "512 MB Swap Image",
"created": "2017-01-01T00:00:00"
"created": "2017-01-01T00:00:00",
"disk_encryption": "disabled"
}
]
}
3 changes: 2 additions & 1 deletion test/fixtures/linode_instances_123_disks_12345_clone.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
"id": 12345,
"updated": "2017-01-01T00:00:00",
"label": "Ubuntu 17.04 Disk",
"created": "2017-01-01T00:00:00"
"created": "2017-01-01T00:00:00",
"disk_encryption": "disabled"
}

2 changes: 1 addition & 1 deletion test/fixtures/lke_clusters_18881_nodes_123456.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"id": "123456",
"instance_id": 123458,
"instance_id": 456,
"status": "ready"
}
3 changes: 2 additions & 1 deletion test/fixtures/lke_clusters_18881_pools_456.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,5 +23,6 @@
"example tag",
"another example"
],
"type": "g6-standard-4"
"type": "g6-standard-4",
"disk_encryption": "enabled"
}
6 changes: 4 additions & 2 deletions test/integration/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,12 +79,14 @@ def wait_for_condition(


# Retry function to help in case of requests sending too quickly before instance is ready
def retry_sending_request(retries: int, condition: Callable, *args) -> object:
def retry_sending_request(
retries: int, condition: Callable, *args, **kwargs
) -> object:
curr_t = 0
while curr_t < retries:
try:
curr_t += 1
res = condition(*args)
res = condition(*args, **kwargs)
return res
except ApiError:
if curr_t >= retries:
Expand Down
50 changes: 46 additions & 4 deletions test/integration/models/linode/test_linode.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import time
from test.integration.conftest import get_region
from test.integration.helpers import (
get_test_label,
retry_sending_request,
Expand All @@ -18,7 +19,7 @@
Instance,
Type,
)
from linode_api4.objects.linode import MigrationType
from linode_api4.objects.linode import InstanceDiskEncryptionType, MigrationType


@pytest.fixture(scope="session")
Expand Down Expand Up @@ -142,6 +143,30 @@ def create_linode_for_long_running_tests(test_linode_client, e2e_test_firewall):
linode_instance.delete()


@pytest.fixture(scope="function")
def linode_with_disk_encryption(test_linode_client, request):
client = test_linode_client

target_region = get_region(client, {"Disk Encryption"})
timestamp = str(time.time_ns())
label = "TestSDK-" + timestamp

disk_encryption = request.param

linode_instance, password = client.linode.instance_create(
"g6-nanode-1",
target_region,
image="linode/ubuntu23.04",
label=label,
booted=False,
disk_encryption=disk_encryption,
)

yield linode_instance

linode_instance.delete()


# Test helper
def get_status(linode: Instance, status: str):
return linode.status == status
Expand Down Expand Up @@ -170,8 +195,7 @@ def test_linode_transfer(test_linode_client, linode_with_volume_firewall):

def test_linode_rebuild(test_linode_client):
client = test_linode_client
available_regions = client.regions()
chosen_region = available_regions[4]
chosen_region = get_region(client, {"Disk Encryption"})
label = get_test_label() + "_rebuild"

linode, password = client.linode.instance_create(
Expand All @@ -180,12 +204,18 @@ def test_linode_rebuild(test_linode_client):

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

retry_sending_request(3, linode.rebuild, "linode/debian10")
retry_sending_request(
3,
linode.rebuild,
"linode/debian10",
disk_encryption=InstanceDiskEncryptionType.disabled,
)

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

assert linode.status == "rebuilding"
assert linode.image.id == "linode/debian10"
assert linode.disk_encryption == InstanceDiskEncryptionType.disabled

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

Expand Down Expand Up @@ -388,6 +418,18 @@ def test_linode_volumes(linode_with_volume_firewall):
assert "test" in volumes[0].label


@pytest.mark.parametrize(
"linode_with_disk_encryption", ["disabled"], indirect=True
)
def test_linode_with_disk_encryption_disabled(linode_with_disk_encryption):
linode = linode_with_disk_encryption

assert linode.disk_encryption == InstanceDiskEncryptionType.disabled
assert (
linode.disks[0].disk_encryption == InstanceDiskEncryptionType.disabled
)


def wait_for_disk_status(disk: Disk, timeout):
start_time = time.time()
while True:
Expand Down
21 changes: 18 additions & 3 deletions test/integration/models/lke/test_lke.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
import base64
import re
from test.integration.conftest import get_region
from test.integration.helpers import (
get_test_label,
send_request_when_resource_available,
wait_for_condition,
)
from typing import Any, Dict

import pytest

from linode_api4 import (
InstanceDiskEncryptionType,
LKEClusterControlPlaneACLAddressesOptions,
LKEClusterControlPlaneACLOptions,
LKEClusterControlPlaneOptions,
Expand All @@ -21,7 +24,7 @@
def lke_cluster(test_linode_client):
node_type = test_linode_client.linode.types()[1] # g6-standard-1
version = test_linode_client.lke.versions()[0]
region = test_linode_client.regions().first()
region = get_region(test_linode_client, {"Disk Encryption", "Kubernetes"})
node_pools = test_linode_client.lke.node_pool(node_type, 3)
label = get_test_label() + "_cluster"

Expand All @@ -38,7 +41,7 @@ def lke_cluster(test_linode_client):
def lke_cluster_with_acl(test_linode_client):
node_type = test_linode_client.linode.types()[1] # g6-standard-1
version = test_linode_client.lke.versions()[0]
region = test_linode_client.regions().first()
region = get_region(test_linode_client, {"Kubernetes"})
node_pools = test_linode_client.lke.node_pool(node_type, 1)
label = get_test_label() + "_cluster"

Expand Down Expand Up @@ -81,9 +84,21 @@ def test_get_lke_clusters(test_linode_client, lke_cluster):
def test_get_lke_pool(test_linode_client, lke_cluster):
cluster = lke_cluster

wait_for_condition(
10,
500,
get_node_status,
cluster,
"ready",
)

pool = test_linode_client.load(LKENodePool, cluster.pools[0].id, cluster.id)

assert cluster.pools[0].id == pool.id
def _to_comparable(p: LKENodePool) -> Dict[str, Any]:
return {k: v for k, v in p._raw_json.items() if k not in {"nodes"}}

assert _to_comparable(cluster.pools[0]) == _to_comparable(pool)
assert pool.disk_encryption == InstanceDiskEncryptionType.enabled


def test_cluster_dashboard_url_view(lke_cluster):
Expand Down
Loading

0 comments on commit c786ecf

Please sign in to comment.