Skip to content

Commit

Permalink
snap: install snapd as a snap before installing snaps if necessary
Browse files Browse the repository at this point in the history
  • Loading branch information
orndorffgrant committed Oct 25, 2023
1 parent 00bf3dc commit 2aca7df
Show file tree
Hide file tree
Showing 7 changed files with 209 additions and 1 deletion.
38 changes: 38 additions & 0 deletions features/livepatch.feature
Original file line number Diff line number Diff line change
Expand Up @@ -197,3 +197,41 @@ Feature: Livepatch
Examples: ubuntu release
| release | machine_type | release_num |
| jammy | lxd-vm | 22.04 |

@series.xenial
@uses.config.machine_type.any
@uses.config.machine_type.lxd.vm
Scenario Outline: snapd installed as a snap if necessary
Given a `<release>` `<machine_type>` machine with ubuntu-advantage-tools installed
When I run `snap list` with sudo
Then stdout does not contain substring:
"""
snapd
"""
When I set the machine token overlay to the following yaml
"""
machineTokenInfo:
contractInfo:
resourceEntitlements:
- type: livepatch
directives:
requiredSnaps:
- name: core22
"""
When I attach `contract_token` with sudo
Then stdout contains substring:
"""
Installing snapd snap
"""
When I run `snap list` with sudo
Then stdout contains substring:
"""
snapd
"""
And stdout contains substring:
"""
core22
"""
Examples: ubuntu release
| release | machine_type |
| xenial | lxd-vm |
14 changes: 14 additions & 0 deletions uaclient/entitlements/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -495,6 +495,20 @@ def handle_required_snaps(self) -> bool:
event.info(messages.INSTALLING_PACKAGES.format(packages="snapd"))
snap.install_snapd()

if not snap.is_snapd_installed_as_a_snap():
event.info(
messages.INSTALLING_PACKAGES.format(packages="snapd snap")
)
try:
snap.install_snap("snapd")
except exceptions.ProcessExecutionError as e:
LOG.warning("Failed to install snapd as a snap", exc_info=e)
event.info(
messages.EXECUTING_COMMAND_FAILED.format(
command="snap install snapd"
)
)

snap.run_snapd_wait_cmd()

http_proxy = http.validate_proxy(
Expand Down
14 changes: 14 additions & 0 deletions uaclient/entitlements/livepatch.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,20 @@ def _perform_enable(self, silent: bool = False) -> bool:
event.info(messages.INSTALLING_PACKAGES.format(packages="snapd"))
snap.install_snapd()

if not snap.is_snapd_installed_as_a_snap():
event.info(
messages.INSTALLING_PACKAGES.format(packages="snapd snap")
)
try:
snap.install_snap("snapd")
except exceptions.ProcessExecutionError as e:
LOG.warning("Failed to install snapd as a snap", exc_info=e)
event.info(
messages.EXECUTING_COMMAND_FAILED.format(
command="snap install snapd"
)
)

snap.run_snapd_wait_cmd()

http_proxy = http.validate_proxy(
Expand Down
6 changes: 6 additions & 0 deletions uaclient/entitlements/tests/test_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -1275,6 +1275,9 @@ class TestHandleRequiredSnaps:
@mock.patch(
"uaclient.entitlements.base.UAEntitlement._base_entitlement_cfg"
)
@mock.patch(
"uaclient.snap.is_snapd_installed_as_a_snap", return_value=True
)
@mock.patch("uaclient.snap.is_snapd_installed", return_value=True)
@mock.patch("uaclient.snap.run_snapd_wait_cmd")
@mock.patch("uaclient.snap.get_snap_info")
Expand All @@ -1289,6 +1292,7 @@ def test_handle_required_snaps(
m_get_snap_info,
m_run_snapd_wait_cmd,
m_is_snapd_installed,
m_is_snapd_installed_as_a_snap,
m_base_ent_cfg,
entitlement_cfg,
concrete_entitlement_factory,
Expand All @@ -1305,11 +1309,13 @@ def test_handle_required_snaps(

if not entitlement_cfg:
assert 0 == m_is_snapd_installed.call_count
assert 0 == m_is_snapd_installed_as_a_snap.call_count
assert 0 == m_run_snapd_wait_cmd.call_count
assert 0 == m_validate_proxy.call_count
assert 0 == m_configure_snap_proxy.call_count
else:
assert 1 == m_is_snapd_installed.call_count
assert 1 == m_is_snapd_installed_as_a_snap.call_count
assert 1 == m_run_snapd_wait_cmd.call_count
assert 2 == m_validate_proxy.call_count
assert 1 == m_configure_snap_proxy.call_count
Expand Down
90 changes: 89 additions & 1 deletion uaclient/entitlements/tests/test_livepatch.py
Original file line number Diff line number Diff line change
Expand Up @@ -372,6 +372,7 @@ def test_livepatch_disable_and_setup_performed_when_resource_token_changes(
assert setup_calls == m_setup_livepatch_config.call_args_list


@mock.patch(M_PATH + "snap.is_snapd_installed_as_a_snap")
@mock.patch(M_PATH + "snap.is_snapd_installed")
@mock.patch("uaclient.http.validate_proxy", side_effect=lambda x, y, z: y)
@mock.patch("uaclient.snap.configure_snap_proxy")
Expand All @@ -385,6 +386,13 @@ class TestLivepatchEntitlementEnable:
retry_sleeps=apt.APT_RETRIES,
)
]
mocks_snapd_install_as_a_snap = [
mock.call(
["/usr/bin/snap", "install", "snapd"],
capture=True,
retry_sleeps=[0.5, 1, 5],
)
]
mocks_snap_wait_seed = [
mock.call(
["/usr/bin/snap", "wait", "system", "seed.loaded"], capture=True
Expand All @@ -398,7 +406,10 @@ class TestLivepatchEntitlementEnable:
)
]
mocks_install = (
mocks_snapd_install + mocks_snap_wait_seed + mocks_livepatch_install
mocks_snapd_install
+ mocks_snapd_install_as_a_snap
+ mocks_snap_wait_seed
+ mocks_livepatch_install
)
mocks_config = [
mock.call(
Expand Down Expand Up @@ -444,6 +455,7 @@ def test_enable_installs_snapd_and_livepatch_snap_when_absent(
m_snap_proxy,
m_validate_proxy,
m_is_snapd_installed,
m_is_snapd_installed_as_a_snap,
capsys,
caplog_text,
event,
Expand All @@ -454,6 +466,7 @@ def test_enable_installs_snapd_and_livepatch_snap_when_absent(
application_status = ApplicationStatus.ENABLED
m_app_status.return_value = application_status, "enabled"
m_is_snapd_installed.return_value = False
m_is_snapd_installed_as_a_snap.return_value = False

def fake_update_sources_list(sources_list):
if apt_update_success:
Expand All @@ -469,6 +482,7 @@ def fake_update_sources_list(sources_list):
msg = (
"Installing snapd\n"
"Updating standard Ubuntu package lists\n"
"Installing snapd snap\n"
"Installing canonical-livepatch snap\n"
"Disabling Livepatch prior to re-attach with new token\n"
"Canonical Livepatch enabled\n"
Expand All @@ -487,6 +501,73 @@ def fake_update_sources_list(sources_list):
assert m_snap_proxy.call_count == 1
assert m_livepatch_proxy.call_count == 1

@pytest.mark.parametrize("caplog_text", [logging.DEBUG], indirect=True)
@mock.patch("uaclient.system.get_release_info")
@mock.patch("uaclient.system.subp")
@mock.patch("uaclient.contract.apply_contract_overrides")
@mock.patch("uaclient.apt.update_sources_list")
@mock.patch("uaclient.apt.run_apt_install_command")
@mock.patch("uaclient.apt.run_apt_update_command")
@mock.patch("uaclient.system.which", return_value=None)
@mock.patch(M_PATH + "LivepatchEntitlement.application_status")
@mock.patch(
M_PATH + "LivepatchEntitlement.can_enable", return_value=(True, None)
)
def test_enable_continues_when_snap_install_snapd_fails(
self,
m_can_enable,
m_app_status,
m_which,
m_run_apt_update,
m_run_apt_install,
m_update_sources_list,
_m_contract_overrides,
m_subp,
_m_get_release_info,
m_livepatch_proxy,
m_snap_proxy,
m_validate_proxy,
m_is_snapd_installed,
m_is_snapd_installed_as_a_snap,
capsys,
caplog_text,
event,
entitlement,
):
"""Install snapd and canonical-livepatch snap when not on system."""
application_status = ApplicationStatus.ENABLED
m_app_status.return_value = application_status, "enabled"
m_is_snapd_installed.return_value = False
m_is_snapd_installed_as_a_snap.return_value = False
m_subp.side_effect = [
None,
exceptions.ProcessExecutionError("test"),
None,
None,
None,
None,
None,
]

assert entitlement.enable()
assert self.mocks_install + self.mocks_config in m_subp.call_args_list
assert self.mocks_apt_update == m_run_apt_update.call_args_list
assert 1 == m_update_sources_list.call_count
msg = (
"Installing snapd\n"
"Updating standard Ubuntu package lists\n"
"Installing snapd snap\n"
"Executing `snap install snapd` failed.\n"
"Installing canonical-livepatch snap\n"
"Disabling Livepatch prior to re-attach with new token\n"
"Canonical Livepatch enabled\n"
)
assert (msg, "") == capsys.readouterr()
assert [mock.call(livepatch.LIVEPATCH_CMD)] == m_which.call_args_list
assert m_validate_proxy.call_count == 2
assert m_snap_proxy.call_count == 1
assert m_livepatch_proxy.call_count == 1

@mock.patch("uaclient.system.get_release_info")
@mock.patch("uaclient.system.subp")
@mock.patch("uaclient.contract.apply_contract_overrides")
Expand All @@ -507,6 +588,7 @@ def test_enable_installs_only_livepatch_snap_when_absent_but_snapd_present(
m_snap_proxy,
m_validate_proxy,
m_is_snapd_installed,
m_is_snapd_installed_as_a_snap,
capsys,
event,
entitlement,
Expand Down Expand Up @@ -556,6 +638,7 @@ def test_enable_does_not_install_livepatch_snap_when_present(
m_snap_proxy,
m_validate_proxy,
m_is_snapd_installed,
m_is_snapd_installed_as_a_snap,
capsys,
event,
entitlement,
Expand Down Expand Up @@ -616,6 +699,7 @@ def test_enable_does_not_disable_inactive_livepatch_snap_when_present(
m_snap_proxy,
m_validate_proxy,
m_is_snapd_installed,
m_is_snapd_installed_as_a_snap,
capsys,
entitlement,
):
Expand Down Expand Up @@ -660,6 +744,7 @@ def test_enable_fails_when_blocking_service_is_enabled(
m_snap_proxy,
m_validate_proxy,
_m_is_snapd_installed,
m_is_snapd_installed_as_a_snap,
cls_name,
cls_title,
entitlement,
Expand Down Expand Up @@ -696,6 +781,7 @@ def test_enable_alerts_user_that_snapd_does_not_wait_command(
m_snap_proxy,
m_validate_proxy,
m_is_snapd_installed,
m_is_snapd_installed_as_a_snap,
entitlement,
capsys,
caplog_text,
Expand Down Expand Up @@ -759,6 +845,7 @@ def test_enable_raise_exception_when_snapd_cant_be_installed(
m_snap_proxy,
m_validate_proxy,
m_is_snapd_installed,
m_is_snapd_installed_as_a_snap,
entitlement,
):
m_is_snapd_installed.return_value = False
Expand Down Expand Up @@ -794,6 +881,7 @@ def test_enable_raise_exception_for_unexpected_error_on_snapd_wait(
m_snap_proxy,
m_validate_proxy,
m_is_snapd_installed,
m_is_snapd_installed_as_a_snap,
entitlement,
):
m_is_snapd_installed.return_value = True
Expand Down
5 changes: 5 additions & 0 deletions uaclient/snap.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,11 @@ def is_snapd_installed() -> bool:
return "snapd" in apt.get_installed_packages_names()


def is_snapd_installed_as_a_snap() -> bool:
"""Returns whether or not snapd is installed as a snap"""
return any((snap.name == "snapd" for snap in get_installed_snaps()))


def configure_snap_proxy(
http_proxy: Optional[str] = None,
https_proxy: Optional[str] = None,
Expand Down
43 changes: 43 additions & 0 deletions uaclient/tests/test_snap.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,53 @@
get_config_option_value,
get_installed_snaps,
get_snap_info,
is_snapd_installed_as_a_snap,
unconfigure_snap_proxy,
)


class TestIsSnapdInstalledAsASnap:
@pytest.mark.parametrize(
["installed_snaps", "expected"],
[
([], False),
(
[
SnapPackage(
"one", "123", "456", "oldest/unstable", "someone"
)
],
False,
),
(
[
SnapPackage(
"snapd", "123", "456", "oldest/unstable", "someone"
)
],
True,
),
(
[
SnapPackage(
"one", "123", "456", "oldest/unstable", "someone"
),
SnapPackage(
"snapd", "123", "456", "oldest/unstable", "someone"
),
],
True,
),
],
)
@mock.patch("uaclient.snap.get_installed_snaps")
def test_is_snapd_installed_as_a_snap(
self, m_get_installed_snaps, installed_snaps, expected
):
m_get_installed_snaps.return_value = installed_snaps
assert expected == is_snapd_installed_as_a_snap()


class TestConfigureSnapProxy:
@pytest.mark.parametrize("http_proxy", ("http_proxy", "", None))
@pytest.mark.parametrize("https_proxy", ("https_proxy", "", None))
Expand Down

0 comments on commit 2aca7df

Please sign in to comment.