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

feat: install Ubuntu Pro Client in LXD instance #664

Merged
Show file tree
Hide file tree
Changes from 5 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
163 changes: 142 additions & 21 deletions craft_providers/lxd/lxc.py
Original file line number Diff line number Diff line change
Expand Up @@ -1197,17 +1197,26 @@
]

try:
proc = self._run_lxc(command, capture_output=True, project=project)
data = json.loads(proc.stdout)
if data.get("result") == "success":
if data.get("data", {}).get("attributes", {}).get("is_attached"):
proc = self._run_lxc(
command, capture_output=True, check=False, project=project
)
if proc.returncode == 0:
data = json.loads(proc.stdout)
if data.get("result") == "success" and data.get("data", {}).get(
"attributes", {}
).get("is_attached"):
logger.debug("Managed instance is Pro enabled.")
return True
logger.debug("Managed instance is not Pro enabled.")
return False
if data.get("result") == "success":
logger.debug("Managed instance is not Pro enabled.")
return False

Check warning on line 1212 in craft_providers/lxd/lxc.py

View check run for this annotation

Codecov / codecov/patch

craft_providers/lxd/lxc.py#L1211-L1212

Added lines #L1211 - L1212 were not covered by tests
linostar marked this conversation as resolved.
Show resolved Hide resolved

raise LXDError(
brief=f"Failed to get a successful response from `pro` command on {instance_name!r}.",
)

raise LXDError(
brief=f"Failed to get a successful response from `pro` command on {instance_name!r}.",
brief=f"Ubuntu Pro Client is not installed on {instance_name!r}."
)
except json.JSONDecodeError as error:
raise LXDError(
Expand Down Expand Up @@ -1249,19 +1258,30 @@
try:
payload = json.dumps({"token": pro_token, "auto_enable_services": False})

self._run_lxc(
proc = self._run_lxc(
command,
capture_output=True,
check=False,
project=project,
input=payload.encode(),
)

# 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(
"Managed instance successfully attached to a Pro subscription."
)
if proc.returncode == 0:
logger.debug(
"Managed instance successfully attached to a Pro subscription."
)
elif proc.returncode == 1:
raise LXDError(
brief=f"Invalid token used to attach {instance_name!r} to a Pro subscription."
)
elif proc.returncode == 2:
logger.debug(

Check warning on line 1278 in craft_providers/lxd/lxc.py

View check run for this annotation

Codecov / codecov/patch

craft_providers/lxd/lxc.py#L1278

Added line #L1278 was not covered by tests
"Managed instance is already attached to a Pro subscription."
)
else:
raise LXDError(
brief=f"Ubuntu Pro Client is not installed on {instance_name!r}."
)
except json.JSONDecodeError as error:
raise LXDError(
brief=f"Failed to parse JSON response of `pro` command on {instance_name!r}.",
Expand Down Expand Up @@ -1301,18 +1321,25 @@
json.dumps({"service": service}),
]
try:
self._run_lxc(
proc = self._run_lxc(
command,
capture_output=True,
check=False,
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."
)
if proc.returncode == 0:
logger.debug(
f"Pro service {service!r} successfully enabled on instance."
)
elif proc.returncode == 1:
raise LXDError(
brief=f"Failed to enable Pro service {service!r} on unattached instance {instance_name!r}.",
)
else:
raise LXDError(
brief=f"Ubuntu Pro Client is not installed on {instance_name!r}."
)
except json.JSONDecodeError as error:
raise LXDError(
brief=f"Failed to parse JSON response of `pro` command on {instance_name!r}.",
Expand All @@ -1322,3 +1349,97 @@
brief=f"Failed to enable Pro service {service!r} on instance {instance_name!r}.",
details=errors.details_from_called_process_error(error),
) from error

def is_pro_installed(
self,
*,
instance_name: str,
project: str = "default",
remote: str = "local",
) -> bool:
"""Check whether Ubuntu Pro Client is installed in the instance.

:param instance_name: Name of instance.
:param project: Name of LXD project.
:param remote: Name of LXD remote.

:raises LXDError: on unexpected error.
"""
command = [
"exec",
f"{remote}:{instance_name}",
"pro",
"version",
]
try:
self._run_lxc(
command,
capture_output=True,
project=project,
)

logger.debug("Ubuntu Pro Client is installed in managed instance.")
return True # noqa: TRY300
except subprocess.CalledProcessError:
logger.debug(f"Ubuntu Pro Client is not installed on {instance_name!r}.")
return False

def install_pro_client(
self,
*,
instance_name: str,
project: str = "default",
remote: str = "local",
) -> None:
"""Install Ubuntu Pro Client in the instance.

:param instance_name: Name of instance.
:param project: Name of LXD project.
:param remote: Name of LXD remote.

:raises LXDError: on unexpected error.
"""
command = [
"exec",
f"{remote}:{instance_name}",
"--",
"apt",
"install",
"-y",
"ubuntu-advantage-tools",
]
try:
self._run_lxc(
command,
capture_output=True,
project=project,
)

if not self.is_pro_installed(
instance_name=instance_name,
project=project,
remote=remote,
):
command = [
"exec",
f"{remote}:{instance_name}",
"--",
"apt",
"install",
"-y",
"ubuntu-advantage-tools=27.11.2~$(lsb_release -rs).1",
linostar marked this conversation as resolved.
Show resolved Hide resolved
]
self._run_lxc(
command,
capture_output=True,
project=project,
)

logger.debug(
"Ubuntu Pro Client successfully installed in managed instance."
)
except subprocess.CalledProcessError as error:
raise LXDError(

Check warning on line 1442 in craft_providers/lxd/lxc.py

View check run for this annotation

Codecov / codecov/patch

craft_providers/lxd/lxc.py#L1441-L1442

Added lines #L1441 - L1442 were not covered by tests
brief=f"Failed to install Ubuntu Pro Client in instance {instance_name!r}.",
details=errors.details_from_called_process_error(error),
) from error
11 changes: 11 additions & 0 deletions craft_providers/lxd/lxd_instance.py
Original file line number Diff line number Diff line change
Expand Up @@ -684,3 +684,14 @@ def enable_pro_service(self, services: Iterable[str]) -> None:
project=self.project,
remote=self.remote,
)

def install_pro_client(self) -> None:
"""Install Ubuntu Pro Client in the instance.

:raises: LXDError: On unexpected error.
"""
self.lxc.install_pro_client(
instance_name=self.instance_name,
project=self.project,
remote=self.remote,
)
61 changes: 53 additions & 8 deletions tests/integration/lxd/test_lxc.py
Original file line number Diff line number Diff line change
Expand Up @@ -284,7 +284,7 @@ def test_info(instance, lxc, session_project):
assert data["Name"] == instance


def test_is_pro_enabled_ubuntu(instance, lxc, session_project):
def test_is_pro_enabled_ubuntu_success(instance, lxc, session_project):
"""Test the scenario where Pro client is installed."""
result = lxc.is_pro_enabled(
instance_name=instance,
Expand All @@ -295,18 +295,20 @@ def test_is_pro_enabled_ubuntu(instance, lxc, session_project):
assert result is False


def test_is_pro_enabled_alma(instance_alma, lxc, session_project):
def test_is_pro_enabled_alma_failure(instance_alma, lxc, session_project):
"""Test the scenario where Pro client is not installed."""
with pytest.raises(LXDError) as raised:
lxc.is_pro_enabled(
instance_name=instance_alma,
project=session_project,
)

assert raised.value.brief == (f"Failed to run `pro` command on {instance_alma!r}.")
assert raised.value.brief == (
f"Ubuntu Pro Client is not installed on {instance_alma!r}."
)


def test_attach_pro_subscription(instance, lxc, session_project):
def test_attach_pro_subscription_success(instance, lxc, session_project):
"""Test the attachment scenario with a fake Pro token."""
with pytest.raises(LXDError) as raised:
lxc.attach_pro_subscription(
Expand All @@ -316,12 +318,26 @@ def test_attach_pro_subscription(instance, lxc, session_project):
)

assert raised.value.brief == (
f"Failed to attach {instance!r} to a Pro subscription."
f"Invalid token used 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."""
def test_attach_pro_subscription_failure(instance_alma, lxc, session_project):
"""Test the attachment scenario with a fake Pro token."""
with pytest.raises(LXDError) as raised:
lxc.attach_pro_subscription(
instance_name=instance_alma,
pro_token="random", # noqa: S106
project=session_project,
)

assert raised.value.brief == (
f"Ubuntu Pro Client is not installed on {instance_alma!r}."
)


def test_enable_pro_service_success(instance, lxc, session_project):
"""Test the scenario to enable a Pro service."""
with pytest.raises(LXDError) as raised:
lxc.enable_pro_service(
instance_name=instance,
Expand All @@ -330,5 +346,34 @@ def test_enable_pro_service(instance, lxc, session_project):
)

assert raised.value.brief == (
f"Failed to enable Pro service 'esm-infra' on instance {instance!r}."
f"Failed to enable Pro service 'esm-infra' on unattached instance {instance!r}."
)


def test_enable_pro_service_failure(instance_alma, lxc, session_project):
"""Test the scenario to enable a Pro service."""
with pytest.raises(LXDError) as raised:
lxc.enable_pro_service(
instance_name=instance_alma,
services=["esm-infra"],
project=session_project,
)

assert raised.value.brief == (
f"Ubuntu Pro Client is not installed on {instance_alma!r}."
)


def test_install_pro_client(instance, lxc, session_project):
"""Test the scenario of installing the Pro Client."""
lxc.install_pro_client(
instance_name=instance,
project=session_project,
)

assert (
lxc.is_pro_installed(
instance_name=instance,
project=session_project,
)
) is True
Loading
Loading