Skip to content

Commit

Permalink
Add snapd device information to ComputerInfo message
Browse files Browse the repository at this point in the history
  • Loading branch information
st3v3nmw committed Jan 30, 2024
1 parent 7f1be98 commit 9530f7c
Show file tree
Hide file tree
Showing 7 changed files with 310 additions and 1 deletion.
33 changes: 33 additions & 0 deletions landscape/client/monitor/computerinfo.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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(
Expand All @@ -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 = {}
Expand Down Expand Up @@ -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
43 changes: 43 additions & 0 deletions landscape/client/monitor/tests/test_computerinfo.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
44 changes: 44 additions & 0 deletions landscape/client/snap_utils.py
Original file line number Diff line number Diff line change
@@ -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:
# <assertion1-headers>: <value>
# <assertion1-headers>: <value>
#
# signature
#
# <assertion2-headers>: <value>
# <assertion2-headers>: <value>
#
# 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
]
177 changes: 177 additions & 0 deletions landscape/client/tests/test_snap_utils.py
Original file line number Diff line number Diff line change
@@ -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)
11 changes: 11 additions & 0 deletions landscape/message_schemas/server_bound.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@
"LIVEPATCH",
"UBUNTU_PRO_REBOOT_REQUIRED",
"SNAPS",
"SNAP_INFO",
]


Expand Down Expand Up @@ -163,6 +164,15 @@
},
)

SNAP_INFO = Message(
"snap-info",
{
"brand": Unicode(),
"model": Unicode(),
"serial": Unicode(),
},
)


hal_data = Dict(
Unicode(),
Expand Down Expand Up @@ -845,4 +855,5 @@
LIVEPATCH,
UBUNTU_PRO_REBOOT_REQUIRED,
SNAPS,
SNAP_INFO,
)
1 change: 1 addition & 0 deletions setup_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
"landscape.client.patch",
"landscape.client.reactor",
"landscape.client.service",
"landscape.client.snap_utils",
"landscape.client.sysvconfig",
"landscape.client.watchdog",
]
Expand Down
2 changes: 1 addition & 1 deletion snap-http

0 comments on commit 9530f7c

Please sign in to comment.