From b2ce9c851cb6fecff945d286a5ca685cbaeca74e Mon Sep 17 00:00:00 2001 From: Stephen Mwangi Date: Wed, 7 Aug 2024 19:33:09 +0300 Subject: [PATCH] fix: get ubuntu pro info on snaps (#257) --- landscape/client/__init__.py | 6 +++ .../manager/tests/test_ubuntuproinfo.py | 43 +++++++++++++++--- landscape/client/manager/ubuntuproinfo.py | 45 +++++++++++++++++-- snap/snapcraft.yaml | 7 +++ 4 files changed, 91 insertions(+), 10 deletions(-) diff --git a/landscape/client/__init__.py b/landscape/client/__init__.py index ccb5857ce..74826b516 100644 --- a/landscape/client/__init__.py +++ b/landscape/client/__init__.py @@ -9,3 +9,9 @@ DEFAULT_CONFIG = ( "/etc/landscape-client.conf" if IS_SNAP else "/etc/landscape/client.conf" ) + +UA_DATA_DIR = ( + "/var/lib/snapd/hostfs/var/lib/ubuntu-advantage" + if IS_SNAP + else "/var/lib/ubuntu-advantage" +) diff --git a/landscape/client/manager/tests/test_ubuntuproinfo.py b/landscape/client/manager/tests/test_ubuntuproinfo.py index b5f99f3cd..9d5daa88a 100644 --- a/landscape/client/manager/tests/test_ubuntuproinfo.py +++ b/landscape/client/manager/tests/test_ubuntuproinfo.py @@ -1,3 +1,6 @@ +import json +import os +import tempfile from datetime import datetime from unittest import mock @@ -161,14 +164,40 @@ def test_persistence_reset(self): self.assertTrue("ubuntu-pro-info" in messages[1]) self.assertEqual(messages[1]["ubuntu-pro-info"], data) - @mock.patch("landscape.client.manager.ubuntuproinfo.IS_SNAP", new=True) - def test_pro_client_not_called_for_snap(self): - """ - The snap will not currently allow calls to the pro client. + @mock.patch.multiple( + "landscape.client.manager.ubuntuproinfo", + IS_SNAP=True, + UA_DATA_DIR=tempfile.gettempdir(), + ) + def test_pro_status_file_read_for_snap(self): + """The snap should read the status file instead of calling `pro`.""" + temp_file_path = os.path.join(tempfile.gettempdir(), "status.json") + with open(temp_file_path, "w") as fp: + mocked_info = { + "_schema_version": "0.1", + "account": { + "created_at": "2024-01-08T13:26:52+00:00", + "external_account_ids": [], + "id": "zYxWvU_sRqPoNmLkJiHgFeDcBa9876543210ZyXwVuTsRqPon", + "name": "jane.doe@example.com", + }, + "foo": "bar" + } + json.dump(mocked_info, fp) - Ensure that get_ubuntu_pro_info returns an empty dictionary instead of - calling the subprocess for pro. - """ + ubuntu_pro_info = get_ubuntu_pro_info() + del mocked_info["foo"] + self.assertEqual(mocked_info, ubuntu_pro_info) + + os.remove(temp_file_path) + + @mock.patch.multiple( + "landscape.client.manager.ubuntuproinfo", + IS_SNAP=True, + UA_DATA_DIR="/i/do/not/exist", + ) + def test_pro_status_file_not_found_for_snap(self): + """The snap will return {} if the status file is not found.""" ubuntu_pro_info = get_ubuntu_pro_info() self.assertEqual({}, ubuntu_pro_info) diff --git a/landscape/client/manager/ubuntuproinfo.py b/landscape/client/manager/ubuntuproinfo.py index e5c054a61..02348f692 100644 --- a/landscape/client/manager/ubuntuproinfo.py +++ b/landscape/client/manager/ubuntuproinfo.py @@ -7,6 +7,7 @@ from landscape.client import IS_CORE from landscape.client import IS_SNAP +from landscape.client import UA_DATA_DIR from landscape.client.manager.plugin import ManagerPlugin from landscape.lib.persist import Persist @@ -80,9 +81,47 @@ def get_ubuntu_pro_info() -> dict: ) if IS_SNAP: - # Snap does not support Ubuntu Pro Info and throws an error if `pro` is - # called. - return {} + # By default, Ubuntu Advantage / Pro stores the status information + # in /var/lib/ubuntu-advantage/status.json (we have a `system-files` + # plug for this). + # This `data_dir` can however be changed in + # /etc/ubuntu-advantage/uaclient.conf which would lead to + # permission errors since we don't have a plug for arbitrary + # folders on the host fs. + + try: + with open(f"{UA_DATA_DIR}/status.json") as fp: + pro_info = json.load(fp) + except (FileNotFoundError, PermissionError): + # Happens if the Ubuntu Pro client isn't installed, or + # if the `data_dir` folder setting was changed from the default + return {} + + # The status file has more information than `pro status` + keys_to_keep = [ + "_doc", + "_schema_version", + "account", + "attached", + "config", + "config_path", + "contract", + "effective", + "environment_vars", + "errors", + "execution_details", + "execution_status", + "expires", + "features", + "machine_id", + "notices", + "result", + "services", + "simulated", + "version", + "warnings", + ] + return {k: pro_info[k] for k in keys_to_keep if k in pro_info} try: completed_process = subprocess.run( diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml index 7b8b4a4eb..f305072a9 100644 --- a/snap/snapcraft.yaml +++ b/snap/snapcraft.yaml @@ -23,6 +23,12 @@ environment: LANDSCAPE_CLIENT_SNAP: 1 PYTHONPATH: $SNAP/usr/lib/python3/dist-packages:$SNAP/usr/lib/python3.10/site-packages:$PYTHONPATH +plugs: + var-lib-ubuntu-advantage-status: + interface: system-files + read: + - /var/lib/snapd/hostfs/var/lib/ubuntu-advantage/status.json + apps: landscape-client: daemon: simple @@ -42,6 +48,7 @@ apps: - process-control - network-control - network-manager + - var-lib-ubuntu-advantage-status config: command: usr/bin/landscape-config plugs: [network]