From 7f1be98aab6237c542a3437f624af1c8086e3b7e Mon Sep 17 00:00:00 2001 From: Stephen Mwangi Date: Mon, 22 Jan 2024 23:36:16 +0300 Subject: [PATCH] Add getting and setting snap config (#203) --- .github/workflows/ci.yml | 8 +- .github/workflows/codecov.yml | 2 + .gitmodules | 3 + landscape/client/manager/snapmanager.py | 40 +-- .../client/manager/tests/test_snapmanager.py | 160 ++++++++-- landscape/client/monitor/snapmonitor.py | 22 +- .../client/monitor/tests/test_snapmonitor.py | 64 +++- landscape/client/snap/__init__.py | 0 landscape/client/snap/http.py | 302 ------------------ landscape/client/snap/tests/__init__.py | 0 landscape/client/snap/tests/test_http.py | 51 --- landscape/client/snap_http | 1 + landscape/lib/tests/test_schema.py | 3 +- landscape/message_schemas/server_bound.py | 2 + setup_client.py | 2 +- snap-http | 1 + 16 files changed, 235 insertions(+), 426 deletions(-) create mode 100644 .gitmodules delete mode 100644 landscape/client/snap/__init__.py delete mode 100644 landscape/client/snap/http.py delete mode 100644 landscape/client/snap/tests/__init__.py delete mode 100644 landscape/client/snap/tests/test_http.py create mode 120000 landscape/client/snap_http create mode 160000 snap-http diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f515d777a..38efb55b2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -7,7 +7,9 @@ jobs: matrix: os: ["ubuntu-22.04", "ubuntu-20.04"] steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 + with: + submodules: true - run: | make depends # -common seems a catch-22, but this is just a shortcut to @@ -17,6 +19,8 @@ jobs: lint: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 + with: + submodules: true - run: make depends - run: make lint diff --git a/.github/workflows/codecov.yml b/.github/workflows/codecov.yml index b92d9780b..74ec22834 100644 --- a/.github/workflows/codecov.yml +++ b/.github/workflows/codecov.yml @@ -7,6 +7,8 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 + with: + submodules: true - run: | make depends # -common seems a catch-22, but this is just a shortcut to diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 000000000..ab50d1c59 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "snap-http"] + path = snap-http + url = https://github.com/Perfect5th/snap-http diff --git a/landscape/client/manager/snapmanager.py b/landscape/client/manager/snapmanager.py index d34bafa3d..c92434331 100644 --- a/landscape/client/manager/snapmanager.py +++ b/landscape/client/manager/snapmanager.py @@ -1,15 +1,16 @@ +import json import logging from collections import deque from twisted.internet import task +from landscape.client import snap_http from landscape.client.manager.plugin import FAILED from landscape.client.manager.plugin import ManagerPlugin from landscape.client.manager.plugin import SUCCEEDED -from landscape.client.snap.http import INCOMPLETE_STATUSES -from landscape.client.snap.http import SnapdHttpException -from landscape.client.snap.http import SnapHttp -from landscape.client.snap.http import SUCCESS_STATUSES +from landscape.client.snap_http import INCOMPLETE_STATUSES +from landscape.client.snap_http import SnapdHttpException +from landscape.client.snap_http import SUCCESS_STATUSES class SnapManager(ManagerPlugin): @@ -23,18 +24,18 @@ class SnapManager(ManagerPlugin): def __init__(self): super().__init__() - self._snap_http = SnapHttp() self.SNAP_METHODS = { - "install-snaps": self._snap_http.install_snap, - "install-snaps-batch": self._snap_http.install_snaps, - "remove-snaps": self._snap_http.remove_snap, - "remove-snaps-batch": self._snap_http.remove_snaps, - "refresh-snaps": self._snap_http.refresh_snap, - "refresh-snaps-batch": self._snap_http.refresh_snaps, - "hold-snaps": self._snap_http.hold_snap, - "hold-snaps-batch": self._snap_http.hold_snaps, - "unhold-snaps": self._snap_http.unhold_snap, - "unhold-snaps-batch": self._snap_http.unhold_snaps, + "install-snaps": snap_http.install, + "install-snaps-batch": snap_http.install_all, + "remove-snaps": snap_http.remove, + "remove-snaps-batch": snap_http.remove_all, + "refresh-snaps": snap_http.refresh, + "refresh-snaps-batch": snap_http.refresh_all, + "hold-snaps": snap_http.hold, + "hold-snaps-batch": snap_http.hold_all, + "unhold-snaps": snap_http.unhold, + "unhold-snaps-batch": snap_http.unhold_all, + "set-snap-config": snap_http.set_conf, } def register(self, registry): @@ -46,6 +47,7 @@ def register(self, registry): registry.register_message("refresh-snaps", self._handle_snap_task) registry.register_message("hold-snaps", self._handle_snap_task) registry.register_message("unhold-snaps", self._handle_snap_task) + registry.register_message("set-snap-config", self._handle_snap_task) def _handle_snap_task(self, message): """ @@ -86,7 +88,7 @@ def _handle_batch_snap_task(self, message): ) queue.append((response["change"], "BATCH")) except SnapdHttpException as e: - result = e.json["result"] + result = json.loads(e.args[0])["result"] logging.error( f"Error in {message_type}: {message}", ) @@ -130,7 +132,7 @@ def _handle_multiple_snap_tasks(self, message): ) queue.append((response["change"], name)) except SnapdHttpException as e: - result = e.json["result"] + result = json.loads(e.args[0])["result"] logging.error( f"Error in {message_type} for '{name}': {message}", ) @@ -161,7 +163,7 @@ def get_status(): logging.info("Polling snapd for status of pending snap changes") try: - result = self._snap_http.check_changes().get("result", []) + result = snap_http.check_changes().get("result", []) result_dict = {c["id"]: c for c in result} except SnapdHttpException as e: logging.error(f"Error checking status of snap changes: {e}") @@ -253,7 +255,7 @@ def _respond(self, snap_results, opid, errors): def _send_installed_snap_update(self): try: - installed_snaps = self._snap_http.get_snaps() + installed_snaps = snap_http.list().result except SnapdHttpException as e: logging.error( f"Unable to list installed snaps after snap change: {e}", diff --git a/landscape/client/manager/tests/test_snapmanager.py b/landscape/client/manager/tests/test_snapmanager.py index 1ce52f1ee..21795677b 100644 --- a/landscape/client/manager/tests/test_snapmanager.py +++ b/landscape/client/manager/tests/test_snapmanager.py @@ -3,8 +3,8 @@ from landscape.client.manager.manager import FAILED from landscape.client.manager.manager import SUCCEEDED from landscape.client.manager.snapmanager import SnapManager -from landscape.client.snap.http import SnapdHttpException -from landscape.client.snap.http import SnapHttp as OrigSnapHttp +from landscape.client.snap_http import SnapdHttpException +from landscape.client.snap_http import SnapdResponse from landscape.client.tests.helpers import LandscapeTest from landscape.client.tests.helpers import ManagerHelper @@ -15,13 +15,10 @@ class SnapManagerTest(LandscapeTest): def setUp(self): super().setUp() - self.snap_http = mock.Mock(spec_set=OrigSnapHttp) - self.SnapHttp = mock.patch( - "landscape.client.manager.snapmanager.SnapHttp", + self.snap_http = mock.patch( + "landscape.client.manager.snapmanager.snap_http", ).start() - self.SnapHttp.return_value = self.snap_http - self.broker_service.message_store.set_accepted_types( ["operation-result"], ) @@ -49,14 +46,19 @@ def install_snap(name, revision=None, channel=None, classic=False): return mock.DEFAULT - self.snap_http.install_snap.side_effect = install_snap + self.snap_http.install.side_effect = install_snap self.snap_http.check_changes.return_value = { "result": [ {"id": "1", "status": "Done"}, {"id": "2", "status": "Done"}, ], } - self.snap_http.get_snaps.return_value = {"installed": []} + self.snap_http.list.return_value = SnapdResponse( + "sync", + 200, + "OK", + {"installed": []}, + ) result = self.manager.dispatch_message( { @@ -90,23 +92,28 @@ def test_install_snaps_batch(self): When no channels or revisions are specified, snaps are installed via a single call to snapd. """ - self.snap_http.install_snaps.return_value = {"change": "1"} + self.snap_http.install_all.return_value = {"change": "1"} self.snap_http.check_changes.return_value = { "result": [{"id": "1", "status": "Done"}], } - self.snap_http.get_snaps.return_value = { - "installed": [ - { - "name": "hello", - "id": "test", - "confinement": "strict", - "tracking-channel": "latest/stable", - "revision": "100", - "publisher": {"validation": "yep", "username": "me"}, - "version": "1.2.3", - }, - ], - } + self.snap_http.list.return_value = SnapdResponse( + "sync", + 200, + "OK", + { + "installed": [ + { + "name": "hello", + "id": "test", + "confinement": "strict", + "tracking-channel": "latest/stable", + "revision": "100", + "publisher": {"validation": "yep", "username": "me"}, + "version": "1.2.3", + }, + ], + }, + ) result = self.manager.dispatch_message( { @@ -136,10 +143,15 @@ def got_result(r): return result.addCallback(got_result) def test_install_snap_immediate_error(self): - self.snap_http.install_snaps.side_effect = SnapdHttpException( + self.snap_http.install_all.side_effect = SnapdHttpException( b'{"result": "whoops"}', ) - self.snap_http.get_snaps.return_value = {"installed": []} + self.snap_http.list.return_value = SnapdResponse( + "sync", + 200, + "OK", + {"installed": []}, + ) result = self.manager.dispatch_message( { @@ -168,9 +180,11 @@ def got_result(r): return result.addCallback(got_result) def test_install_snap_no_status(self): - self.snap_http.install_snaps.return_value = {"change": "1"} + self.snap_http.install_all.return_value = {"change": "1"} self.snap_http.check_changes.return_value = {"result": []} - self.snap_http.get_snaps.return_value = {"installed": []} + self.snap_http.list.return_value = SnapdResponse( + "sync", 200, "OK", {"installed": []} + ) result = self.manager.dispatch_message( { @@ -197,9 +211,11 @@ def got_result(r): return result.addCallback(got_result) def test_install_snap_check_error(self): - self.snap_http.install_snaps.return_value = {"change": "1"} + self.snap_http.install_all.return_value = {"change": "1"} self.snap_http.check_changes.side_effect = SnapdHttpException("whoops") - self.snap_http.get_snaps.return_value = {"installed": []} + self.snap_http.list.return_value = SnapdResponse( + "sync", 200, "OK", {"installed": []} + ) result = self.manager.dispatch_message( { @@ -228,11 +244,13 @@ def got_result(r): return result.addCallback(got_result) def test_remove_snap(self): - self.snap_http.remove_snaps.return_value = {"change": "1"} + self.snap_http.remove_all.return_value = {"change": "1"} self.snap_http.check_changes.return_value = { "result": [{"id": "1", "status": "Done"}], } - self.snap_http.get_snaps.return_value = {"installed": []} + self.snap_http.list.return_value = SnapdResponse( + "sync", 200, "OK", {"installed": []} + ) result = self.manager.dispatch_message( { @@ -257,3 +275,83 @@ def got_result(r): ) return result.addCallback(got_result) + + def test_set_config(self): + self.snap_http.set_conf.return_value = {"change": "1"} + self.snap_http.check_changes.return_value = { + "result": [{"id": "1", "status": "Done"}], + } + self.snap_http.list.return_value = SnapdResponse( + "sync", 200, "OK", {"installed": []} + ) + + result = self.manager.dispatch_message( + { + "type": "set-snap-config", + "operation-id": 123, + "snaps": [ + { + "name": "hello", + "config": {"foo": {"bar": "qux", "baz": "quux"}}, + } + ], + } + ) + + def got_result(r): + self.assertMessages( + self.broker_service.message_store.get_pending_messages(), + [ + { + "type": "operation-result", + "status": SUCCEEDED, + "result-text": "{'completed': ['hello'], " + "'errored': [], 'errors': {}}", + "operation-id": 123, + }, + ], + ) + + return result.addCallback(got_result) + + def test_set_config_sync_error(self): + self.snap_http.set_conf.side_effect = SnapdHttpException( + b'{"result": "whoops"}', + ) + self.snap_http.check_changes.return_value = { + "result": [{"id": "1", "status": "Done"}], + } + self.snap_http.list.return_value = SnapdResponse( + "sync", 200, "OK", {"installed": []} + ) + + result = self.manager.dispatch_message( + { + "type": "set-snap-config", + "operation-id": 123, + "snaps": [ + { + "name": "hello", + "config": {"foo": {"bar": "qux", "baz": "quux"}}, + } + ], + } + ) + + def got_result(r): + self.assertMessages( + self.broker_service.message_store.get_pending_messages(), + [ + { + "type": "operation-result", + "status": FAILED, + "result-text": ( + "{'completed': [], 'errored': [], " + "'errors': {'hello': 'whoops'}}" + ), + "operation-id": 123, + }, + ], + ) + + return result.addCallback(got_result) diff --git a/landscape/client/monitor/snapmonitor.py b/landscape/client/monitor/snapmonitor.py index 6a55a6847..162bc52e7 100644 --- a/landscape/client/monitor/snapmonitor.py +++ b/landscape/client/monitor/snapmonitor.py @@ -1,8 +1,9 @@ +import json import logging +from landscape.client import snap_http from landscape.client.monitor.plugin import DataWatcher -from landscape.client.snap.http import SnapdHttpException -from landscape.client.snap.http import SnapHttp +from landscape.client.snap_http import SnapdHttpException from landscape.message_schemas.server_bound import SNAPS @@ -13,11 +14,6 @@ class SnapMonitor(DataWatcher): persist_name = message_type scope = "snaps" - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - - self._snap_http = SnapHttp() - def register(self, registry): self.config = registry.config # The default interval is 30 minutes. @@ -27,17 +23,25 @@ def register(self, registry): def get_data(self): try: - snaps = self._snap_http.get_snaps() + snaps = snap_http.list().result except SnapdHttpException as e: logging.error(f"Unable to list installed snaps: {e}") return + for i in range(len(snaps)): + try: + config = snap_http.get_conf(snaps[i]["name"]) + except SnapdHttpException: + config = {} + + snaps[i]["config"] = json.dumps(config) + # We get a lot of extra info from snapd. To avoid caching it all # or invalidating the cache on timestamp changes, we use Message # coercion to strip out the unnecessaries, then sort on the snap # IDs to order the list. data = SNAPS.coerce( - {"type": "snaps", "snaps": {"installed": snaps["result"]}}, + {"type": "snaps", "snaps": {"installed": snaps}}, ) data["snaps"]["installed"].sort(key=lambda x: x["id"]) diff --git a/landscape/client/monitor/tests/test_snapmonitor.py b/landscape/client/monitor/tests/test_snapmonitor.py index 086aec694..47883d5e2 100644 --- a/landscape/client/monitor/tests/test_snapmonitor.py +++ b/landscape/client/monitor/tests/test_snapmonitor.py @@ -1,8 +1,10 @@ -from unittest.mock import Mock +from unittest.mock import patch from landscape.client.monitor.snapmonitor import SnapMonitor -from landscape.client.snap.http import SnapdHttpException, SnapHttp -from landscape.client.tests.helpers import LandscapeTest, MonitorHelper +from landscape.client.snap_http import SnapdHttpException +from landscape.client.snap_http import SnapdResponse +from landscape.client.tests.helpers import LandscapeTest +from landscape.client.tests.helpers import MonitorHelper class SnapMonitorTest(LandscapeTest): @@ -30,15 +32,13 @@ def test_get_data_snapd_http_exception(self): """ Tests that we return no data if there is an error getting it. """ - snap_http_mock = Mock( - spec=SnapHttp, - get_snaps=Mock(side_effect=SnapdHttpException) - ) plugin = SnapMonitor() - plugin._snap_http = snap_http_mock self.monitor.add(plugin) - with self.assertLogs(level="ERROR") as cm: + with patch( + "landscape.client.monitor.snapmonitor.snap_http", + ) as snap_http_mock, self.assertLogs(level="ERROR") as cm: + snap_http_mock.list.side_effect = SnapdHttpException plugin.exchange() messages = self.mstore.get_pending_messages() @@ -46,5 +46,49 @@ def test_get_data_snapd_http_exception(self): self.assertEqual(len(messages), 0) self.assertEqual( cm.output, - ["ERROR:root:Unable to list installed snaps: "] + ["ERROR:root:Unable to list installed snaps: "], + ) + + @patch("landscape.client.monitor.snapmonitor.snap_http") + def test_get_snap_config(self, snap_http_mock): + """Tests that we can get and coerce snap config.""" + plugin = SnapMonitor() + self.monitor.add(plugin) + + snap_http_mock.list.return_value = SnapdResponse( + "sync", + 200, + "OK", + [ + { + "name": "test-snap", + "revision": "1", + "confinement": "strict", + "version": "v1.0", + "id": "123", + } + ], + ) + snap_http_mock.get_conf.return_value = { + "foo": {"baz": "default", "qux": [1, True, 2.0]}, + "bar": "enabled", + } + plugin.exchange() + + messages = self.mstore.get_pending_messages() + + self.assertTrue(len(messages) > 0) + self.assertDictEqual( + messages[0]["snaps"]["installed"][0], + { + "name": "test-snap", + "revision": "1", + "confinement": "strict", + "version": "v1.0", + "id": "123", + "config": ( + '{"foo": {"baz": "default", "qux": [1, true, 2.0]}, ' + '"bar": "enabled"}' + ), + }, ) diff --git a/landscape/client/snap/__init__.py b/landscape/client/snap/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/landscape/client/snap/http.py b/landscape/client/snap/http.py deleted file mode 100644 index d3d1ba17c..000000000 --- a/landscape/client/snap/http.py +++ /dev/null @@ -1,302 +0,0 @@ -""" -Functions for interacting with the snapd REST API. -See https://snapcraft.io/docs/snapd-api for documentation of the API. -""" -import json -from io import BytesIO - -import pycurl - -SNAPD_SOCKET = "/run/snapd.socket" -BASE_URL = "http://localhost/v2" - -# For the below, refer to https://snapcraft.io/docs/snapd-api#heading--changes -COMPLETE_STATUSES = {"Done", "Error", "Hold", "Abort"} -INCOMPLETE_STATUSES = {"Do", "Doing", "Undo", "Undoing"} -SUCCESS_STATUSES = {"Done"} -ERROR_STATUSES = {"Error", "Hold", "Unknown"} - - -class SnapdHttpException(Exception): - @property - def json(self): - """Attempts to parse the body of this exception as json.""" - body = self.args[0] - - return json.loads(body) - - -class SnapHttp: - def __init__(self, snap_url=BASE_URL, snap_socket=SNAPD_SOCKET): - self._snap_url = snap_url - self._snap_socket = snap_socket - - def check_change(self, cid): - """Check the status of snapd change with id `cid`.""" - return self._get("/changes/" + cid) - - def check_changes(self): - """Check the status of all snapd changes.""" - return self._get("/changes?select=all") - - def enable_snap(self, name): - """Enables a previously disabled snap by `name`.""" - return self._post("/snaps/" + name, {"action": "enable"}) - - def enable_snaps(self, snaps): - """See `self.enable_snap`.""" - return self._post( - "/snaps", - { - "action": "enable", - "snaps": snaps, - }, - ) - - def disable_snap(self, name): - """ - Disables a snap by `name`, making its binaries and services - unavailable. - """ - return self._post("/snaps/" + name, {"action": "disable"}) - - def disable_snaps(self, snaps): - """See `self.disable_snap`.""" - return self._post( - "/snaps", - { - "action": "enable", - "snaps": snaps, - }, - ) - - def get_snaps(self): - """GETs a list of installed snaps.""" - return self._get("/snaps") - - def hold_snap(self, name, hold_level="general", time="forever"): - """ - Holds a snap by `name` at `hold_level` until `time`. - - `hold_level` is "general" or "auto-refresh". - `time` is "forever" or an RFC3339 timestamp. - """ - body = _clean_dict( - { - "action": "hold", - "hold-level": hold_level, - "time": time, - }, - ) - - return self._post("/snaps/" + name, body) - - def hold_snaps(self, snaps, hold_level="general", time="forever"): - """ - Same as `self.hold_snap`, except for a batch of snaps. - """ - body = _clean_dict( - { - "action": "hold", - "snaps": snaps, - "hold-level": hold_level, - "time": time, - }, - ) - - return self._post("/snaps", body) - - def install_snap(self, name, revision=None, channel=None, classic=False): - """ - Installs a snap by `name` at `revision`, tracking `channel`. If - `classic`, then snap is installed in classic containment mode. - - If `revision` is not provided, latest will be used. - If `channel` is not provided, stable will be used. - """ - body = _clean_dict( - { - "action": "install", - "revision": revision, - "channel": channel, - "classic": classic, - }, - ) - - return self._post("/snaps/" + name, body) - - def install_snaps(self, snaps): - return self._post("/snaps", {"action": "install", "snaps": snaps}) - - def refresh_snap(self, name, revision=None, channel=None, classic=None): - """ - Refreshes a snap, switching to the given `revision` and `channel` if - provided. - - If `classic` is provided, snap will be changed to the classic - confinement if True, or out of classic confinement if False. - """ - body = _clean_dict( - { - "action": "refresh", - "revision": revision, - "channel": channel, - "classic": classic, - }, - ) - - return self._post("/snaps/" + name, body) - - def refresh_snaps(self, snaps=[]): - """ - Refreshes `snaps` to the latest revision. If `snaps` is empty, - all snaps are refreshed. - """ - body = {"action": "refresh"} - - if snaps: - body["snaps"] = snaps - - return self._post("/snaps", body) - - def revert_snap(self, name, revision=None, classic=None): - """ - Reverts a snap, switching to the given `revision` is provided. - Otherwise switches to the revision used prior to the last - refresh. - - If `classic` is provided, snap will be changed to classic - confinement if True, or out of classic confinement if False. - """ - body = _clean_dict( - { - "action": "revert", - "revision": revision, - "classic": classic, - }, - ) - - return self._post("/snaps/" + name, body) - - def revert_snaps(self, snaps): - """ - Reverts `snaps` to the revision used prior to the last refresh. - """ - return self._post( - "/snaps", - { - "action": "refresh", - "snaps": snaps, - }, - ) - - def remove_snap(self, name): - return self._post("/snaps/" + name, {"action": "remove"}) - - def remove_snaps(self, snaps): - return self._post( - "/snaps", - { - "action": "remove", - "snaps": snaps, - }, - ) - - def switch_snap(self, name, channel="stable"): - """Switches the channel that a snap is tracking.""" - return self._post( - "/snaps/" + name, - _clean_dict( - { - "action": "switch", - "channel": channel, - }, - ), - ) - - def switch_snaps(self, snaps, channel="stable"): - return self._post( - "/snaps", - _clean_dict( - { - "action": "switch", - "snaps": snaps, - "channel": channel, - }, - ), - ) - - def unhold_snap(self, name): - """ - Remove a hold on a snap, allowing it to refresh on it's usual - schedule. - """ - return self._post("/snaps/" + name, {"action": "unhold"}) - - def unhold_snaps(self, snaps): - """See `self.unhold_snap`.""" - return self._post( - "/snaps", - { - "action": "unhold", - "snaps": snaps, - }, - ) - - def _get(self, path): - """Perform a GET request of `path` to the snap REST API.""" - curl, buff = self._setup_curl(path) - - self._perform(curl, buff) - - return json.loads(buff.getvalue()) - - def _perform(self, curl, buff, raise_on_error=True): - """ - Performs a pycurl request, optionally raising on a pycurl or HTTP - error. - """ - try: - curl.perform() - except pycurl.error as e: - raise SnapdHttpException(e) - - response_code = curl.getinfo(curl.RESPONSE_CODE) - if response_code >= 400: - raise SnapdHttpException(buff.getvalue()) - - def _post(self, path, body): - """ - Perform a POST request of `path` to the snap REST API, with the - JSON-ified `body` - """ - curl, buff = self._setup_curl(path) - json_body = json.dumps(body) - - curl.setopt(curl.POSTFIELDS, json_body) - curl.setopt(curl.HTTPHEADER, ["Content-Type: application/json"]) - self._perform(curl, buff) - - return json.loads(buff.getvalue()) - - def _setup_curl(self, path): - """ - Prepares pycurl to communicate with the snap REST API at the given - `path`. - """ - curl = pycurl.Curl() - buff = BytesIO() - - curl.setopt(curl.UNIX_SOCKET_PATH, self._snap_socket) - curl.setopt(curl.URL, self._snap_url + path) - curl.setopt(curl.WRITEDATA, buff) - - return curl, buff - - -def _clean_dict(d): - """ - Only includes keys from `d` in the resulting dict if they are not - None. - """ - return {k: v for k, v in d.items() if v is not None} diff --git a/landscape/client/snap/tests/__init__.py b/landscape/client/snap/tests/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/landscape/client/snap/tests/test_http.py b/landscape/client/snap/tests/test_http.py deleted file mode 100644 index 965761597..000000000 --- a/landscape/client/snap/tests/test_http.py +++ /dev/null @@ -1,51 +0,0 @@ -from unittest import TestCase -from unittest.mock import Mock -from unittest.mock import patch - -from landscape.client.snap.http import SnapdHttpException -from landscape.client.snap.http import SnapHttp - - -class SnapHttpTestCase(TestCase): - """ - Most of the snapd REST API methods require root. I don't think it's - a good idea to be running unit tests as root, and mocking the HTTP - requests isn't a valuable way to test things, so this testsuite is - rather limited. Thankfully most of the methods of SnapHttp are very - simple - we just test that we are sending the right POST bodies for - the more complex cases. - """ - - def test_get_snaps(self): - """get_snaps() returns a dict with a list of installed snaps.""" - http = SnapHttp() - result = http.get_snaps()["result"] - - self.assertTrue(isinstance(result, list)) - self.assertGreater(len(result), 0) - - first = result[0] - for key in ("id", "name", "publisher"): - self.assertIn(key, first) - - def test_get_snaps_error_code(self): - """ - get_snaps raises a SnapdHttpException if the response code from - the snapd HTTP service is >= 400 - """ - http = SnapHttp() - - with patch("pycurl.Curl") as curl_mock: - getinfo_mock = Mock(return_value=400) - curl_mock.return_value = Mock(getinfo=getinfo_mock) - - self.assertRaises(SnapdHttpException, http.get_snaps) - - def test_get_snaps_couldnt_connect(self): - """ - get_snaps raises a SnapdHttpException if we cannot reach the - snapd HTTP service. - """ - http = SnapHttp(snap_socket="/run/garbage.socket") - - self.assertRaises(SnapdHttpException, http.get_snaps) diff --git a/landscape/client/snap_http b/landscape/client/snap_http new file mode 120000 index 000000000..baa417ac7 --- /dev/null +++ b/landscape/client/snap_http @@ -0,0 +1 @@ +../../snap-http/snap_http \ No newline at end of file diff --git a/landscape/lib/tests/test_schema.py b/landscape/lib/tests/test_schema.py index 8bda891fe..ea19a2ff9 100644 --- a/landscape/lib/tests/test_schema.py +++ b/landscape/lib/tests/test_schema.py @@ -171,7 +171,8 @@ def test_key_dict_unknown_key(self): def test_key_dict_unknown_key_not_strict(self): self.assertEqual( KeyDict({"foo": Int()}, strict=False).coerce({"foo": 1, "bar": 2}), - {"foo": 1}) + {"foo": 1}, + ) def test_key_dict_bad(self): self.assertRaises(InvalidError, KeyDict({}).coerce, object()) diff --git a/landscape/message_schemas/server_bound.py b/landscape/message_schemas/server_bound.py index f4a819e13..89169945c 100644 --- a/landscape/message_schemas/server_bound.py +++ b/landscape/message_schemas/server_bound.py @@ -781,6 +781,7 @@ ), "confinement": Unicode(), "summary": Unicode(), + "config": Unicode(), }, strict=False, optional=[ @@ -788,6 +789,7 @@ "summary", "publisher", "tracking-channel", + "config", ], ), ), diff --git a/setup_client.py b/setup_client.py index a77f27969..0bbc7d3ca 100644 --- a/setup_client.py +++ b/setup_client.py @@ -12,7 +12,7 @@ "landscape.client.manager", "landscape.client.monitor", "landscape.client.package", - "landscape.client.snap", + "landscape.client.snap_http", "landscape.client.upgraders", "landscape.client.user", ] diff --git a/snap-http b/snap-http new file mode 160000 index 000000000..a79cdb541 --- /dev/null +++ b/snap-http @@ -0,0 +1 @@ +Subproject commit a79cdb5417e83a02c9e3c19264272b0fbe2baea4