Skip to content

Commit

Permalink
feat: enable pro services on managed LXD instance (#656)
Browse files Browse the repository at this point in the history
* add enable_pro_service function

* add unit tests for enable_pro_service

* add integration test for enable_pro_service

* change enable_pro_service signature so it accepts multiple services as arg
  • Loading branch information
linostar authored Sep 23, 2024
1 parent 2bd0ca6 commit c511987
Show file tree
Hide file tree
Showing 5 changed files with 212 additions and 2 deletions.
53 changes: 52 additions & 1 deletion craft_providers/lxd/lxc.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
import time
from collections import deque
from datetime import datetime, timezone
from typing import Any, Callable, Dict, List, Optional
from typing import Any, Callable, Dict, Iterable, List, Optional

import yaml

Expand Down Expand Up @@ -1271,3 +1271,54 @@ def attach_pro_subscription(
brief=f"Failed to attach {instance_name!r} to a Pro subscription.",
details=errors.details_from_called_process_error(error),
) from error

def enable_pro_service(
self,
*,
instance_name: str,
services: Iterable[str],
project: str = "default",
remote: str = "local",
) -> None:
"""Enable a Pro service on the instance.
:param instance_name: Name of instance.
:param services: Name of services to enable.
:param project: Name of LXD project.
:param remote: Name of LXD remote.
:raises LXDError: on unexpected error.
"""
for service in services:
command = [
"exec",
f"{remote}:{instance_name}",
"--",
"pro",
"api",
"u.pro.services.enable.v1",
"--data",
json.dumps({"service": service}),
]
try:
self._run_lxc(
command,
capture_output=True,
project=project,
)

# No need to parse the output here, as an output with
# "result": "failure" will also have a return code != 0
# hence triggering a CalledProcesssError exception
logger.debug(
f"Pro service {service!r} successfully enabled on instance."
)
except json.JSONDecodeError as error:
raise LXDError(
brief=f"Failed to parse JSON response of `pro` command on {instance_name!r}.",
) from error
except subprocess.CalledProcessError as error:
raise LXDError(
brief=f"Failed to enable Pro service {service!r} on instance {instance_name!r}.",
details=errors.details_from_called_process_error(error),
) from error
16 changes: 15 additions & 1 deletion craft_providers/lxd/lxd_instance.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
import shutil
import subprocess
import tempfile
from typing import Any, Dict, List, Optional
from typing import Any, Dict, Iterable, List, Optional

from craft_providers import pro
from craft_providers.const import TIMEOUT_SIMPLE
Expand Down Expand Up @@ -670,3 +670,17 @@ def attach_pro_subscription(self):
project=self.project,
remote=self.remote,
)

def enable_pro_service(self, services: Iterable[str]) -> None:
"""Enable a Pro service on the instance.
:param services: Pro services to enable.
:raises: LXDError: On unexpected error.
"""
self.lxc.enable_pro_service(
instance_name=self.instance_name,
services=services,
project=self.project,
remote=self.remote,
)
14 changes: 14 additions & 0 deletions tests/integration/lxd/test_lxc.py
Original file line number Diff line number Diff line change
Expand Up @@ -318,3 +318,17 @@ def test_attach_pro_subscription(instance, lxc, session_project):
assert raised.value.brief == (
f"Failed to attach {instance!r} to a Pro subscription."
)


def test_enable_pro_service(instance, lxc, session_project):
"""Test the scenario where Pro client is not installed."""
with pytest.raises(LXDError) as raised:
lxc.enable_pro_service(
instance_name=instance,
services=["esm-infra"],
project=session_project,
)

assert raised.value.brief == (
f"Failed to enable Pro service 'esm-infra' on instance {instance!r}."
)
118 changes: 118 additions & 0 deletions tests/unit/lxd/test_lxc.py
Original file line number Diff line number Diff line change
Expand Up @@ -2799,3 +2799,121 @@ def test_attach_pro_subscription_process_error(fake_process):
)

assert len(fake_process.calls) == 1


def test_enable_pro_service_success(fake_process):
fake_process.register_subprocess(
[
"lxc",
"--project",
"test-project",
"exec",
"test-remote:test-instance",
"--",
"pro",
"api",
"u.pro.services.enable.v1",
"--data",
'{"service": "esm-infra"}',
],
stdout=b"""{"_schema_version": "v1", "data": {"attributes": {"disabled": [], "enabled": ["esm-infra"], "messages": [], "reboot_required": false}, "meta": {"environment_vars": []}, "type": "EnableService"}, "errors": [], "result": "success", "version": "32.3.1~24.04", "warnings": []}""",
)
fake_process.register_subprocess(
[
"lxc",
"--project",
"test-project",
"exec",
"test-remote:test-instance",
"--",
"pro",
"api",
"u.pro.services.enable.v1",
"--data",
'{"service": "esm-apps"}',
],
stdout=b"""{"_schema_version": "v1", "data": {"attributes": {"disabled": [], "enabled": ["esm-apps"], "messages": [], "reboot_required": false}, "meta": {"environment_vars": []}, "type": "EnableService"}, "errors": [], "result": "success", "version": "32.3.1~24.04", "warnings": []}""",
)

assert (
LXC().enable_pro_service(
instance_name="test-instance",
services=["esm-infra", "esm-apps"],
project="test-project",
remote="test-remote",
)
is None
)

assert len(fake_process.calls) == 2


def test_enable_pro_service_failed(fake_process):
fake_process.register_subprocess(
[
"lxc",
"--project",
"test-project",
"exec",
"test-remote:test-instance",
"--",
"pro",
"api",
"u.pro.services.enable.v1",
"--data",
'{"service": "invalid"}',
],
stdout=b"""{"_schema_version": "v1", "data": {"meta": {"environment_vars": []}}, "errors": [{"code": "entitlement-not-found", "meta": {"entitlement_name": "invalid"}, "title": "could not find entitlement named \"invalid\""}], "result": "failure", "version": "32.3.1~24.04", "warnings": []}""",
returncode=1,
)

with pytest.raises(LXDError) as exc_info:
LXC().enable_pro_service(
instance_name="test-instance",
services=["invalid"],
project="test-project",
remote="test-remote",
)

assert (
exc_info.value.brief
== "Failed to enable Pro service 'invalid' on instance 'test-instance'."
)

assert len(fake_process.calls) == 1


def test_enable_pro_service_process_error(fake_process):
fake_process.register_subprocess(
[
"lxc",
"--project",
"test-project",
"exec",
"test-remote:test-instance",
"--",
"pro",
"api",
"u.pro.services.enable.v1",
"--data",
'{"service": "esm-infra"}',
],
returncode=127,
)

with pytest.raises(LXDError) as exc_info:
LXC().enable_pro_service(
instance_name="test-instance",
services=["esm-infra"],
project="test-project",
remote="test-remote",
)

assert exc_info.value == LXDError(
brief="Failed to enable Pro service 'esm-infra' on instance 'test-instance'.",
details=errors.details_from_called_process_error(
exc_info.value.__cause__ # type: ignore
),
)

assert len(fake_process.calls) == 1
13 changes: 13 additions & 0 deletions tests/unit/lxd/test_lxd_instance.py
Original file line number Diff line number Diff line change
Expand Up @@ -1062,3 +1062,16 @@ def test_is_pro_enabled(mock_lxc, instance):
remote=instance.remote,
)
]


def test_enable_pro_service(mock_lxc, instance):
instance.enable_pro_service(["esm-apps"])

assert mock_lxc.mock_calls == [
mock.call.enable_pro_service(
instance_name=instance.instance_name,
services=["esm-apps"],
project=instance.project,
remote=instance.remote,
)
]

0 comments on commit c511987

Please sign in to comment.