From 9530f7ca6cce748499eef244722cfcd3a853a11a Mon Sep 17 00:00:00 2001 From: Stephen Mwangi Date: Wed, 24 Jan 2024 18:08:09 +0300 Subject: [PATCH] Add snapd device information to ComputerInfo message --- landscape/client/monitor/computerinfo.py | 33 ++++ .../client/monitor/tests/test_computerinfo.py | 43 +++++ landscape/client/snap_utils.py | 44 +++++ landscape/client/tests/test_snap_utils.py | 177 ++++++++++++++++++ landscape/message_schemas/server_bound.py | 11 ++ setup_client.py | 1 + snap-http | 2 +- 7 files changed, 310 insertions(+), 1 deletion(-) create mode 100644 landscape/client/snap_utils.py create mode 100644 landscape/client/tests/test_snap_utils.py diff --git a/landscape/client/monitor/computerinfo.py b/landscape/client/monitor/computerinfo.py index 3361c211a..1ceca6440 100644 --- a/landscape/client/monitor/computerinfo.py +++ b/landscape/client/monitor/computerinfo.py @@ -5,6 +5,7 @@ from twisted.internet.defer import returnValue from landscape.client.monitor.plugin import MonitorPlugin +from landscape.client.snap_utils import get_assertions from landscape.lib.cloud import fetch_ec2_meta_data from landscape.lib.fetch import fetch_async from landscape.lib.fs import read_text_file @@ -59,6 +60,11 @@ def register(self, registry): self.send_cloud_instance_metadata_message, True, ) + self.call_on_accepted( + "snap-info", + self.send_snap_message, + True, + ) def send_computer_message(self, urgent=False): message = self._create_computer_info_message() @@ -96,6 +102,17 @@ def send_cloud_instance_metadata_message(self, urgent=False): urgent=urgent, ) + def send_snap_message(self, urgent=False): + message = self._create_snap_info_message() + if message: + message["type"] = "snap-info" + logging.info("Queueing message with updated snap info.") + self.registry.broker.send_message( + message, + self._session_id, + urgent=urgent, + ) + def exchange(self, urgent=False): broker = self.registry.broker broker.call_if_accepted( @@ -113,6 +130,11 @@ def exchange(self, urgent=False): self.send_cloud_instance_metadata_message, urgent, ) + broker.call_if_accepted( + "snap-info", + self.send_snap_message, + urgent, + ) def _create_computer_info_message(self): message = {} @@ -195,3 +217,14 @@ def log_success(result): deferred.addCallback(log_success) deferred.addErrback(log_no_meta_data_found) return deferred + + def _create_snap_info_message(self): + """Create message with the snapd serial metadata.""" + message = {} + assertions = get_assertions("serial") + if assertions: + assertion = assertions[0] + self._add_if_new(message, "brand", assertion["brand-id"]) + self._add_if_new(message, "model", assertion["model"]) + self._add_if_new(message, "serial", assertion["serial"]) + return message diff --git a/landscape/client/monitor/tests/test_computerinfo.py b/landscape/client/monitor/tests/test_computerinfo.py index abc9cce59..4014ca7ee 100644 --- a/landscape/client/monitor/tests/test_computerinfo.py +++ b/landscape/client/monitor/tests/test_computerinfo.py @@ -559,3 +559,46 @@ def test_fetch_ec2_meta_data_bad_result_retry(self): }, result, ) + + @mock.patch("landscape.client.monitor.computerinfo.get_assertions") + def test_snap_info(self, mock_get_assertions): + """Test getting the snap info message.""" + mock_get_assertions.return_value = [ + { + "authority-id": "canonical", + "brand-id": "canonical", + "model": "pc-amd64", + "serial": "03961d5d-26e5-443f-838d-6db046126bea", + }, + ] + + self.mstore.set_accepted_types(["snap-info"]) + plugin = ComputerInfo(fetch_async=self.fetch_func) + self.monitor.add(plugin) + plugin.exchange() + messages = self.mstore.get_pending_messages() + self.assertEqual(len(messages), 1) + self.assertEqual(messages[0]["type"], "snap-info") + self.assertEqual(messages[0]["brand"], "canonical") + self.assertEqual(messages[0]["model"], "pc-amd64") + self.assertEqual( + messages[0]["serial"], + "03961d5d-26e5-443f-838d-6db046126bea", + ) + + @mock.patch("landscape.client.monitor.computerinfo.get_assertions") + def test_snap_info_no_results(self, mock_get_assertions): + """Test getting the snap info message when there are no results. + + No results can happen when: + - A SnapdHttpException occurs + - No serial assertion is found + """ + mock_get_assertions.return_value = None + + self.mstore.set_accepted_types(["snap-info"]) + plugin = ComputerInfo(fetch_async=self.fetch_func) + self.monitor.add(plugin) + plugin.exchange() + messages = self.mstore.get_pending_messages() + self.assertEqual(len(messages), 0) diff --git a/landscape/client/snap_utils.py b/landscape/client/snap_utils.py new file mode 100644 index 000000000..6e3e1668a --- /dev/null +++ b/landscape/client/snap_utils.py @@ -0,0 +1,44 @@ +import yaml + +from landscape.client import snap_http +from landscape.client.snap_http import SnapdHttpException + + +def parse_assertion(assertion: str): + """Parse an assertion into key-value pairs.""" + headers, signature = assertion.split("\n\n") + parsed = {} + parsed.update(yaml.safe_load(headers)) + parsed["signature"] = signature + return parsed + + +def get_assertions(assertion_type: str): + """Get and parse assertions.""" + try: + response = snap_http.get_assertions(assertion_type) + except SnapdHttpException: + return + + # the snapd API returns multiple assertions as a stream of + # bytes separated by double newlines, something like this: + # : + # : + # + # signature + # + # : + # : + # + # signature + + # extract the assertion headers + their signatures as separate assertions + sections = response.result.decode().split("\n\n") + raw_assertions = [ + "\n\n".join(sections[i : i + 2]) for i in range(0, len(sections), 2) + ] + return [ + parse_assertion(assertion) + for assertion in raw_assertions + if len(assertion) > 0 + ] diff --git a/landscape/client/tests/test_snap_utils.py b/landscape/client/tests/test_snap_utils.py new file mode 100644 index 000000000..98bf129a2 --- /dev/null +++ b/landscape/client/tests/test_snap_utils.py @@ -0,0 +1,177 @@ +from unittest import mock +from unittest import TestCase + +import yaml + +from landscape.client.snap_http import SnapdHttpException +from landscape.client.snap_http import SnapdResponse +from landscape.client.snap_utils import get_assertions +from landscape.client.snap_utils import parse_assertion + + +TEST_SERIAL_ASSERTION = """type: serial +authority-id: canonical +brand-id: canonical +model: pc-amd64 +serial: 03961d5d-26e5-443f-838d-6db046126bea +device-key: + AcbBTQRWhcGAARAA0y/BXkBJjPOl24qPKOZWy7H+6+piDPtyKIGfU9TDDrFjFnv3R8EMTz1WNW8 + 5nLR8gjDXNh3z7dLIbSPeC54bvQ7LlaO2VYICGdzHT5+68Rod9h5NYdTKgaWDyHdm2K1v2oOzmM + Z+MmL15TvP9lX1U8OIVkmHhCO7FeDGsPlsTX2Wz++SrOqG4PsvpYsaYUTHE+oZ+Eo8oySW/OxTm + rQIEUoDEWNbFR5/+33tHRDxKSjeErCVuVetZxlZW/gpCx5tmCyAcBgKoEKsPqrgzW4wUAONaSOG + Zuo35DxwqeGHOx3C118rYrGvqA2mCn3fFz/mqnciK3JzLemLjw4HyVd1DyaKUgGjR6VYBcadL72 + YN6gPiMMmlaAPtkdFIkqIp1OpvUFEEEHwNI88klM/N8+t3JE8cFpG6n4WBdHUAwtMmmVxXm5IsM + uNwrZdIBUu4WOAAgu2ZioeHLIQlDGw6dvVTaK+dTe0EXo5j+mH5DFnn0W1L7IAj6rX8HdiM5X5f + 4kwiezSfYXJgctdi0gizdGB7wcH0/JynaXA/tI3fEVDu45X7dA/XnCEzYkBxpidNfDkmXxSWt5N + NMuHZqqmNHNfLeKAo1yQ/SH702nth6vJYJaIX4Pgv5cVrX5L429U5SHV+8HaE0lPCfFo/rKRJa9 + rvnJ5OGR4TeRTLsAEQEAAQ== +device-key-sha3-384: _4U3nReiiIMIaHcl6zSdRzcu75Tz37FW8b7NHhxXjNaPaZzyGooMFqur0E +timestamp: 2016-11-08T18:16:12.977431Z +sign-key-sha3-384: BWDEoaqyr25nF5SNCvEv2v7QnM9QsfCc0PBMYD_i2NGSQ32EF2d4D0hqUel3 + +AcLBUgQAAQoABgUCWCIWcgAARegQAB4/UsBpzqLOYOpmR/j9BX5XNyEWxOWgFg5QLaY+0bIz/nbU +avFH4EwV7YKQxX5nGmt7vfFoUPsRrWO4E6RtXQ1x5kYr8sSltLIYEkUjHO7sqB6gzomQYkMnS2fI +xOZwJs9ev2sCnqr9pwPC8MDS5KW5iwXYvdBP1CIwNfQO48Ys8SC9MdYH0t3DbnuG/w+EceOIyI3o +ilkB427DiueGwlBpjNRSE4B8cvglXW9rcYW72bnNs1DSnCq8tNHHybBtOYm/Y/jmk7UGXwqYUGQQ +Iwu1W+SgloJdXLLgM80bPzLy+cYiIe1W1FSMzVdOforTkG5mVFHTL/0l4eceWequfcxU3DW9ggcN +YJx8MPW9ab5gPibx8FeVb6cMWEvm8S7wXIRSff/bkHMhpjAagp+A6dyYsuUwPXFxCvHSpT0vUwFS +CCPHkPUwj54GjKAGEkKMx+s0psQ3V+fcZgW5TBxk/+J83S/+6AiQ06W8rkabWCRyl2fX81vMBynQ +nu147uRGWTXfa31Mys9lAGNHMtEcMmA106f2XfATqNK99GlIIjOxqEe5zH3j51JtY+5kyJd9cqvl +Pb0rZnPySeGxnV4Q2403As67AJrIExRrcrK2yXZjEW3G2zTsFNzBSSZr0U8id1UJ/EZLB/em2EHw +D2FXTwfDiwGroHYUFAEu1DkHx7Sy +""" + +TEST_DECLARATION_ASSERTIONS = """type: snap-declaration +format: 1 +authority-id: canonical +revision: 6 +series: 16 +snap-id: ffnH0sJpX3NFAclH777M8BdXIWpo93af +plugs: + hardware-observe: + allow-auto-connection: true + mount-observe: + allow-auto-connection: true + network-observe: + allow-auto-connection: true + scsi-generic: + allow-auto-connection: true + shutdown: + allow-auto-connection: true + snapd-control: + allow-auto-connection: true + allow-installation: true + system-observe: + allow-auto-connection: true +publisher-id: 0N0rjFmfHsjIZMCjZ4IX5EtW2CsVd5Ky +snap-name: landscape-client +timestamp: 2023-11-08T07:46:21.082809Z +sign-key-sha3-384: BWDEoaqyr25nF5SNCvEv2v7QnM9QsfCc0PBMYD_i2NGSQ32EF2d4D0hqU + +AcLBUgQAAQoABgUCZUs80QAA5QQQALZTjK8YeaJIVjcDw4w7IbLyfO28hQEy95ocZOWLtPwnSYQk +OJBtSFknwTCGKvlaOFuy9PLIABG+MoObm+MOQ8QjqqgHZ+ESqCs5sHrpYk2U9nhI6h89iDshk6Sg +T9l0tqqyV5pEen9nH/zRJy6k8xm7iMR7y1DVl4PBFJVwmugyXn1/4/kG5XKwV1p4WdvIemPzk7nm +nNvVpx+T2IAbXCcZMnvXxmCIf+KqiIub3v4Cvpy9xyxNGqLdCHiCh9bsPoTz2lwHOpgki/rlS6gd +T+GZZf+0358Xom5CeLMMVlV/1jcZs3X0BTK5m2Tx5aW4f+4pHfSi7idtaV1lzqUagM+KO/UdGi7i +dxq1/6eP0YTDb/hHhjlhDQAgwBTCvxRDor8V/NE1TkdEsbMFMgEOT/70v6mkgpXXYY9gnEg0vlSW +osDXzqN6tchJzVQfYYPRjIplX3C+fF5axCUL/ly14Up558ge53zS6frImytG/Qxeh0Ga1RyJsqMu +mbmz+uHwQA2IuXU9MbubCzO6hvVP4zU8FKbSRLrXrSLOwxuFFFRU5aor/w+kbx3l/pgfzOkpSLgD +eEQQbEjwhN14cCTq+jt78MoIAVLMHj1cre567X6ei1HufpAYRdAEL5Igi3+8ua5AyI3QTp7mQIPm +UUvllJovZRye6mF0u7VK9YRQwaoY + +type: snap-declaration +authority-id: canonical +series: 16 +snap-id: XaUSoE9KNKazeLO5H02NMM2cTxX8E9IH +publisher-id: f22PSauKuNkwQTM9Wz67ZCjNACuSjjhN +snap-name: pocketses +timestamp: 2024-01-03T11:01:29.918417Z +sign-key-sha3-384: BWDEoaqyr25nF5SNCvEv2v7QnM9QsfCc0PBMYD_i2NGSQ32EF2d4D0hqU + +AcLBUgQAAQoABgUCZZU+iQAA8GsQADl85AbacovxCOrCb3zNDCCBulAakWiITOTd6lCPkibQq+2b +5Nj7PUl+DCqnVBr8tWx/o8Me/Q4yC+btcOZA/LIzCFHVXS/ExvKABSthgIjygkwM4BbXWpdm7Fua +CxY3GzsfBbOQncpOV/jN5UmoCPJ+/OBJleoxjboZGeMr7hEaKhMKed0+C1VXGVJnpL8MfdDjOgjz +WWSJX+KVfQhNIUZJVGMdJ6tl3/Dzs2wSnwfy8mftx0eXWB2Z9xnX17lWe/ewqG38DzYcoq2dbkNU +XItff1HdEJZANbmW1f2vRbeeShDUvW9mxHcYauI+VsBr5XiHDGO8h4l7kDrlh76j/th5y/WSOGyN +DEV/E5Q+JDUFy0k9NSgw6pCHY7EwOwyiM0zKe4wEUhuLUvIuO9n4lAUApIgxNvDgxx7ZymHggzHy +HQZEt8GwUBNgipqz1FI4u5eT2LhhqwVT6no2dmVwVTUxaPPRKxM3qApUzXzzzLJxNKf5XeM0/QFW +aYy6QoXO3X0Ze01DFq2aFqXsNi4flpONFDG3fsffXCoxyPRaXpB3bRXuRTN0oAun7IxF/m2KHZyn +Xl40RI/40APrJpUUgJu9QjXslspuX9X6JUNjdwUVs65qbfVvdjUMKcIkk/CX14YKquPXrIzwOaao +EJ67KJVjhdyWDv+AUE4FMC0hUGSh +""" + + +class AssertionTest(TestCase): + def setUp(self): + super().setUp() + + self.snap_http = mock.patch( + "landscape.client.snap_utils.snap_http", + ).start() + + def tearDown(self): + mock.patch.stopall() + + def test_parsing_a_valid_assertion(self): + assertion = parse_assertion(TEST_SERIAL_ASSERTION) + self.assertEqual(assertion["type"], "serial") + self.assertEqual( + assertion["serial"], + "03961d5d-26e5-443f-838d-6db046126bea", + ) + self.assertEqual(assertion["model"], "pc-amd64") + self.assertEqual( + assertion["sign-key-sha3-384"], + "BWDEoaqyr25nF5SNCvEv2v7QnM9QsfCc0PBMYD_i2NGSQ32EF2d4D0hqUel3", + ) + + def test_parsing_invalid_assertion(self): + with self.assertRaises(yaml.constructor.ConstructorError): + parse_assertion("assertion-header: {{ bad-val }}\n\nsignature") + + def test_get_assertions_one_result(self): + self.snap_http.get_assertions.return_value = SnapdResponse( + "sync", + 200, + "OK", + result=TEST_SERIAL_ASSERTION.encode(), + ) + + assertions = get_assertions("serial") + self.assertEqual(len(assertions), 1) + self.assertEqual(assertions[0]["type"], "serial") + self.assertEqual( + assertions[0]["serial"], + "03961d5d-26e5-443f-838d-6db046126bea", + ) + self.assertEqual(assertions[0]["model"], "pc-amd64") + + def test_get_assertions_multiple_results(self): + self.snap_http.get_assertions.return_value = SnapdResponse( + "sync", + 200, + "OK", + result=TEST_DECLARATION_ASSERTIONS.encode(), + ) + + assertions = get_assertions("snap-declaration") + self.assertEqual(len(assertions), 2) + self.assertEqual(assertions[0]["snap-name"], "landscape-client") + self.assertEqual(assertions[1]["snap-name"], "pocketses") + + def test_get_assertions_no_result(self): + self.snap_http.get_assertions.return_value = SnapdResponse( + "sync", + 200, + "OK", + result=b"", + ) + + assertions = get_assertions("serial-request") + self.assertEqual(assertions, []) + + def test_get_assertions_exception(self): + self.snap_http.get_assertions.side_effect = SnapdHttpException() + + assertions = get_assertions("unknown") + self.assertIsNone(assertions) diff --git a/landscape/message_schemas/server_bound.py b/landscape/message_schemas/server_bound.py index 89169945c..6153af10d 100644 --- a/landscape/message_schemas/server_bound.py +++ b/landscape/message_schemas/server_bound.py @@ -60,6 +60,7 @@ "LIVEPATCH", "UBUNTU_PRO_REBOOT_REQUIRED", "SNAPS", + "SNAP_INFO", ] @@ -163,6 +164,15 @@ }, ) +SNAP_INFO = Message( + "snap-info", + { + "brand": Unicode(), + "model": Unicode(), + "serial": Unicode(), + }, +) + hal_data = Dict( Unicode(), @@ -845,4 +855,5 @@ LIVEPATCH, UBUNTU_PRO_REBOOT_REQUIRED, SNAPS, + SNAP_INFO, ) diff --git a/setup_client.py b/setup_client.py index 0bbc7d3ca..fbc5ea3cf 100644 --- a/setup_client.py +++ b/setup_client.py @@ -25,6 +25,7 @@ "landscape.client.patch", "landscape.client.reactor", "landscape.client.service", + "landscape.client.snap_utils", "landscape.client.sysvconfig", "landscape.client.watchdog", ] diff --git a/snap-http b/snap-http index a79cdb541..b9e28022a 160000 --- a/snap-http +++ b/snap-http @@ -1 +1 @@ -Subproject commit a79cdb5417e83a02c9e3c19264272b0fbe2baea4 +Subproject commit b9e28022afe09f4e41dcb12d79bf7954fd76dfc4