Skip to content
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

project: Linode Disk Encryption (re-apply) #436

Merged
merged 3 commits into from
Jul 23, 2024
Merged
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
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