Skip to content

Commit

Permalink
wip: pro-lxd
Browse files Browse the repository at this point in the history
  • Loading branch information
orndorffgrant committed Aug 29, 2024
1 parent 99eddd0 commit 1d96455
Show file tree
Hide file tree
Showing 15 changed files with 301 additions and 56 deletions.
6 changes: 3 additions & 3 deletions debian/changelog
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
ubuntu-advantage-tools (1:1+devel) UNRELEASED; urgency=medium
ubuntu-advantage-tools (35~lxd-auto-attach-test) noble; urgency=medium

* wip
* testing pro lxd auto attach

-- Grant Orndorff <[email protected]> Fri, 16 Aug 2024 12:10:33 -0500
-- Grant Orndorff <[email protected]> Tue, 13 Aug 2024 16:28:22 -0500

ubuntu-advantage-tools (33.2) oracular; urgency=medium

Expand Down
66 changes: 66 additions & 0 deletions features/lxd.feature
Original file line number Diff line number Diff line change
Expand Up @@ -96,3 +96,69 @@ Feature: LXD Pro features
Examples:
| release | machine_type |
| jammy | lxd-container |

Scenario Outline: LXD guest auto-attach
Given a `<release>` `<machine_type>` machine with ubuntu-advantage-tools installed
# When I run `snap install lxd --channel latest/edge` with sudo
When I run `lxd init --minimal` with sudo
When I start `lxd-download` command `lxc image copy ubuntu-daily:noble local:` in the background
When I run `pro config show` with sudo
Then stdout matches regexp:
"""
lxd_guest_attach +off
"""
Then I verify that running `pro config set lxd_guest_attach=available` `with sudo` exits `1`
Then I will see the following on stderr:
"""
This machine is not attached to an Ubuntu Pro subscription.
See https://ubuntu.com/pro
"""
When I attach `contract_token` with sudo and options `--no-auto-enable`
When I run `pro config show` with sudo
Then stdout matches regexp:
"""
lxd_guest_attach +off
"""
# latest/edge needed for early testing for now
When I wait for the `lxd-download` command to complete
When I run `lxc launch ubuntu-daily:<guest_release> guest` with sudo
When I install ubuntu-advantage-tools on the `guest` lxd guest
Then I verify that running `lxc exec guest -- pro auto-attach` `with sudo` exits `1`
Then I will see the following on stderr:
"""
The LXD host does not allow guest auto attach
"""
When I run `pro config set lxd_guest_attach=available` with sudo
When I run `lxc exec guest -- pro auto-attach` with sudo
When I run `lxc exec guest -- pro api u.pro.status.is_attached.v1` with sudo
Then I will see the following on stdout:
"""
jq it to see its attached
"""
When I run `lxc exec guest -- pro detach --assume-yes` with sudo
When I run `pro config set lxd_guest_attach=off` with sudo
Then I verify that running `lxc exec guest -- pro auto-attach` `with sudo` exits `1`
Then I will see the following on stdout:
"""
The LXD host does not allow guest auto attach
"""
When I run `lxc delete --force guest` with sudo
When I run `pro config set lxd_guest_attach=on` with sudo
When I run `lxc restart guest` with sudo
When I run `lxc exec guest -- pro status --wait` with sudo
When I run `lxc exec guest -- pro api u.pro.status.is_attached.v1` with sudo
Then I will see the following on stdout:
"""
jq it to see its attached
"""
When I run `lxc exec guest -- pro detach --assume-yes` with sudo
When I run `pro config unset lxd_guest_attach` with sudo
Then I verify that running `lxc exec guest -- pro auto-attach` `with sudo` exits `1`
Then I will see the following on stdout:
"""
The LXD host does not allow guest auto attach
"""

Examples:
| release | machine_type | guest_release |
| jammy | lxd-vm | noble |
26 changes: 26 additions & 0 deletions features/steps/tasks.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import time

from behave import step

from features.steps.shell import when_i_run_command


@step("I start `{task}` command `{command}` in the background")
def start_task(context, task, command):
when_i_run_command(
context,
f"systemd-run --no-block --unit={task} --property=Type=oneshot --property=RemainAfterExit=yes {command}", # noqa: E501
"with sudo",
)


@step("I wait for the `{task}` command to complete")
def wait_for_task(context, task):
while True:
try:
when_i_run_command(
context, f"systemctl is-active {task}", "with sudo"
)
break
except AssertionError:
time.sleep(2)
156 changes: 107 additions & 49 deletions features/steps/ubuntu_advantage_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,64 +8,68 @@
from features.steps.shell import when_i_run_command, when_i_run_shell_command
from features.util import (
ALL_BINARY_PACKAGE_NAMES,
NORMAL_BINARY_PACKAGE_NAMES,
SUT,
InstallationSource,
build_debs,
get_debs_for_series,
)

SETUP_PRO_PACKAGE_SOURCES_SCRIPTS = {
InstallationSource.ARCHIVE: "sudo apt update",
InstallationSource.DAILY: """\
sudo add-apt-repository ppa:ua-client/daily
sudo apt update""",
InstallationSource.STAGING: """\
sudo add-apt-repository ppa:ua-client/staging
sudo apt update""",
InstallationSource.STABLE: """\
sudo add-apt-repository ppa:ua-client/stable
sudo apt update""",
InstallationSource.PROPOSED: """\
cat > /etc/apt/sources.list.d/proposed.list << EOF
deb http://archive.ubuntu.com/ubuntu/ {series}-proposed main
EOF
cat > /etc/apt/preferences.d/lower-proposed << EOF
Package: *
Pin: release a={series}-proposed
Pin-Priority: 400
EOF
cat > /etc/apt/preferences.d/upper-pro-posed << EOF
Package: {packages}
Pin: release a={series}-proposed
Pin-Priority: 1001
EOF
sudo apt update""",
InstallationSource.CUSTOM: """\
sudo add-apt-repository {ppa}
sudo apt update""",
}


def get_setup_pro_package_sources_script(context, series):
script = SETUP_PRO_PACKAGE_SOURCES_SCRIPTS.get(
context.pro_config.install_from, ""
)
script = script.format(
ppa=context.pro_config.custom_ppa,
series=series,
packages=" ".join(ALL_BINARY_PACKAGE_NAMES),
)
return script


def setup_pro_package_sources(context, machine_name=SUT):
instance = context.machines[machine_name].instance

if context.pro_config.install_from is InstallationSource.ARCHIVE:
instance.execute("sudo apt update")
elif context.pro_config.install_from is InstallationSource.DAILY:
instance.execute("sudo add-apt-repository ppa:ua-client/daily")
instance.execute("sudo apt update")
elif context.pro_config.install_from is InstallationSource.STAGING:
instance.execute("sudo add-apt-repository ppa:ua-client/staging")
instance.execute("sudo apt update")
elif context.pro_config.install_from is InstallationSource.STABLE:
instance.execute("sudo add-apt-repository ppa:ua-client/stable")
instance.execute("sudo apt update")
elif context.pro_config.install_from is InstallationSource.PROPOSED:
series = context.machines[machine_name].series
context.text = "deb http://archive.ubuntu.com/ubuntu/ {series}-proposed main\n".format( # noqa: E501
series=series
)
when_i_create_file_with_content(
context,
"/etc/apt/sources.list.d/uaclient-proposed.list",
machine_name=machine_name,
)

context.text = "Package: *\nPin: release a={series}-proposed\nPin-Priority: 400\n".format( # noqa: E501
series=series
)
when_i_create_file_with_content(
context,
"/etc/apt/preferences.d/lower-proposed",
machine_name=machine_name,
)

for package in ALL_BINARY_PACKAGE_NAMES:
context.text = "Package: {package}\nPin: release a={series}-proposed\nPin-Priority: 1001\n".format( # noqa: E501
package=package,
series=series,
)
when_i_create_file_with_content(
context,
"/etc/apt/preferences.d/{}-proposed".format(package),
machine_name=machine_name,
)

instance.execute("sudo apt update")
elif context.pro_config.install_from is InstallationSource.CUSTOM:
instance.execute(
"sudo add-apt-repository {}".format(context.pro_config.custom_ppa)
)
instance.execute("sudo apt update")
series = context.machines[machine_name].series
script = get_setup_pro_package_sources_script(context, series)
context.text = script
when_i_create_file_with_content(
context,
"/tmp/setup_pro.sh",
machine_name=machine_name,
)
instance.execute("sudo bash /tmp/setup_pro.sh")


@when("I install ubuntu-advantage-tools")
Expand Down Expand Up @@ -123,6 +127,60 @@ def when_i_install_uat(context, machine_name=SUT):
)


@when("I install ubuntu-advantage-tools on the `{guest_name}` lxd guest")
def when_i_install_uat_on_lxd_guest(context, guest_name):
# This function assumes "when_i_install_uat" was run on the SUT
if context.pro_config.install_from in {
InstallationSource.PREBUILT,
InstallationSource.LOCAL,
}:
to_install = []
for deb_name in NORMAL_BINARY_PACKAGE_NAMES:
deb_file_name = "behave_{}.deb".format(deb_name)
instance_tmp_path = "/tmp/{}".format(deb_file_name)
guest_path = "/root/{}".format(deb_file_name)
when_i_run_command(
context,
"lxc file push {tmp_path} {guest_name}{guest_path}".format(
tmp_path=instance_tmp_path,
guest_name=guest_name,
guest_path=guest_path,
),
"with sudo",
)
to_install.append(guest_path)
when_i_run_command(
context,
"lxc exec {guest_name} -- apt install -y {packages}".format(
guest_name=guest_name, packages=" ".join(to_install)
),
"with sudo",
)
else:
when_i_run_command(
context,
"lxc file push /tmp/setup_pro.sh {guest_name}/root/setup_pro.sh".format( # noqa: E501
guest_name=guest_name
),
"with sudo",
)
when_i_run_command(
context,
"lxc exec {guest_name} -- bash /root/setup_pro.sh".format(
guest_name=guest_name
),
"with sudo",
)
when_i_run_command(
context,
"lxc exec {guest_name} -- apt install -y {packages}".format(
guest_name=guest_name,
packages=" ".join(NORMAL_BINARY_PACKAGE_NAMES),
),
"with sudo",
)


@when("I ensure -proposed is not enabled anymore")
def when_i_ensure_proposed_not_enabled(context, machine_name=SUT):
if context.pro_config.install_from is InstallationSource.PROPOSED:
Expand Down
5 changes: 5 additions & 0 deletions features/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,11 @@
"ubuntu-advantage-tools",
"ubuntu-advantage-pro",
]
NORMAL_BINARY_PACKAGE_NAMES = [
"ubuntu-pro-client",
"ubuntu-pro-client-l10n",
"ubuntu-advantage-tools",
]


@dataclass
Expand Down
2 changes: 1 addition & 1 deletion lib/daemon.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ def main() -> int:
LOG.debug("checking for condition files")
is_correct_cloud = any(
os.path.exists("/run/cloud-init/cloud-id-{}".format(cloud))
for cloud in ("gce", "azure")
for cloud in ("gce", "azure", "lxd")
)
if is_correct_cloud and not os.path.exists(
retry_auto_attach.FLAG_FILE_PATH
Expand Down
1 change: 1 addition & 0 deletions systemd/ubuntu-advantage.service
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ ConditionPathExists=!/var/lib/ubuntu-advantage/private/machine-token.json
# The following conditions correspond to those two modes.
ConditionPathExists=|/run/cloud-init/cloud-id-gce
ConditionPathExists=|/run/cloud-init/cloud-id-azure
ConditionPathExists=|/run/cloud-init/cloud-id-lxd
ConditionPathExists=|/run/ubuntu-advantage/flags/auto-attach-failed

[Service]
Expand Down
3 changes: 2 additions & 1 deletion uaclient/clouds/identity.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,14 +67,15 @@ def cloud_instance_factory(
:raises CloudFactoryNonViableCloudError: if no cloud instance object can be
constructed because we explicitly do not support the cloud we're on
"""
from uaclient.clouds import aws, azure, gcp
from uaclient.clouds import aws, azure, gcp, lxd

cloud_instance_map = {
"aws": aws.AWSAutoAttachInstance,
"aws-china": aws.AWSAutoAttachInstance,
"aws-gov": aws.AWSAutoAttachInstance,
"azure": azure.AzureAutoAttachInstance,
"gce": gcp.GCPAutoAttachInstance,
"lxd": lxd.LXDAutoAttachInstance,
} # type: Dict[str, Type[clouds.AutoAttachInstance]]

if cloud_override is not None:
Expand Down
57 changes: 57 additions & 0 deletions uaclient/clouds/lxd.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import logging

from uaclient import config, exceptions, http, log, secret_manager, util
from uaclient.clouds import AutoAttachInstance

LOG = logging.getLogger(util.replace_top_level_logger_name(__name__))

LXD_INSTANCE_API_SOCKET_PATH = "/dev/lxd/sock"
LXD_INSTANCE_API_ENDPOINT_UBUNTU_PRO = "/1.0/ubuntu-pro"
LXD_INSTANCE_API_ENDPOINT_UBUNTU_PRO_GUEST_TOKEN = "/1.0/ubuntu-pro/token"


class LXDAutoAttachInstance(AutoAttachInstance):
@property
def is_viable(self) -> bool:
return True

def should_poll_for_pro_license(self) -> bool:
"""Yes, but only once - is_pro_license_present doesn't
support wait_for_change"""
return True

def is_pro_license_present(self, *, wait_for_change: bool) -> bool:
if wait_for_change:
# Unsupported
raise exceptions.CancelProLicensePolling()

resp = http.unix_socket_request(
LXD_INSTANCE_API_SOCKET_PATH,
"GET",
LXD_INSTANCE_API_ENDPOINT_UBUNTU_PRO,
)
if resp.code != 200:
LOG.warning(
"LXD instance API returned error for ubuntu-pro query",
extra=log.extra(code=resp.code, body=resp.body),
)
return False
return resp.json_dict.get("guest_attach", "off") in {"on", "available"}

def acquire_pro_token(self, cfg: config.UAConfig) -> str:
"""
Cloud-specific implementation of acquiring the pro token using whatever
method suits the platform
"""
resp = http.unix_socket_request(
LXD_INSTANCE_API_SOCKET_PATH,
"POST",
LXD_INSTANCE_API_ENDPOINT_UBUNTU_PRO_GUEST_TOKEN,
)
if resp.code == 404:
raise exceptions.LXDAutoAttachNotAvailable()
elif resp.code == 403:
raise exceptions.LXDAutoAttachNotAllowed()
guest_token = resp.json_dict.get("guest_token", "")
secret_manager.secrets.add_secret(guest_token)
return guest_token
Loading

0 comments on commit 1d96455

Please sign in to comment.