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

snap: install snapd as a snap before installing snaps if necessary #2811

Merged
merged 1 commit into from
Oct 27, 2023
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
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
Loading