From 708d1668b7f3f4fee243c80b0b1fefedcdc6467c Mon Sep 17 00:00:00 2001 From: Kevin Nasto Date: Tue, 6 Aug 2024 15:56:41 -0500 Subject: [PATCH] Initial commit --- landscape/client/manager/config.py | 1 + .../client/{monitor => manager}/livepatch.py | 5 +- landscape/client/manager/plugin.py | 72 +++++++++++++++++++ landscape/client/manager/tests/test_config.py | 1 + .../tests/test_livepatch.py | 26 +++---- landscape/client/manager/tests/test_plugin.py | 36 +++++++++- landscape/client/manager/ubuntuproinfo.py | 41 ++--------- landscape/client/monitor/config.py | 1 - 8 files changed, 129 insertions(+), 54 deletions(-) rename landscape/client/{monitor => manager}/livepatch.py (95%) rename landscape/client/{monitor => manager}/tests/test_livepatch.py (91%) diff --git a/landscape/client/manager/config.py b/landscape/client/manager/config.py index f89ebbc99..fc423654d 100644 --- a/landscape/client/manager/config.py +++ b/landscape/client/manager/config.py @@ -15,6 +15,7 @@ "SnapManager", "SnapServicesManager", "UbuntuProInfo", + "LivePatch", ] diff --git a/landscape/client/monitor/livepatch.py b/landscape/client/manager/livepatch.py similarity index 95% rename from landscape/client/monitor/livepatch.py rename to landscape/client/manager/livepatch.py index 61735087e..0a7d5235f 100644 --- a/landscape/client/monitor/livepatch.py +++ b/landscape/client/manager/livepatch.py @@ -2,10 +2,11 @@ import subprocess import yaml -from landscape.client.monitor.plugin import DataWatcher +from landscape.client.manager.plugin import DataWatcherManager -class LivePatch(DataWatcher): + +class LivePatch(DataWatcherManager): """ Plugin that captures and reports Livepatch status information information. diff --git a/landscape/client/manager/plugin.py b/landscape/client/manager/plugin.py index ea20b8c74..f4ab16218 100644 --- a/landscape/client/manager/plugin.py +++ b/landscape/client/manager/plugin.py @@ -1,8 +1,12 @@ +import logging +from pathlib import Path + from twisted.internet.defer import maybeDeferred from landscape.client.broker.client import BrokerClientPlugin from landscape.lib.format import format_object from landscape.lib.log import log_failure +from landscape.lib.persist import Persist # Protocol messages! Same constants are defined in the server. FAILED = 5 @@ -66,3 +70,71 @@ def send(args): deferred.addCallback(send) return deferred + + +class DataWatcherManager(ManagerPlugin): + """ + A utility for plugins which send data to the Landscape server + which does not constantly change. New messages will only be sent + when the result of get_data() has changed since the last time it + was called. Note this is the same as the DataWatcher plugin but + for Manager plugins instead of Monitor.Subclasses should provide + a get_data method + """ + + message_type = None + + def __init__(self): + super().__init__() + self._persist = None + + def register(self, registry): + super().register(registry) + self._persist_filename = Path( + self.registry.config.data_path, + self.message_type + '.manager.bpkl', + ) + self._persist = Persist(filename=self._persist_filename) + self.call_on_accepted(self.message_type, self.send_message) + + def run(self): + return self.registry.broker.call_if_accepted( + self.message_type, + self.send_message, + ) + + def send_message(self): + """Send a message to the broker if the data has changed since the last + call""" + result = self.get_new_data() + if not result: + logging.debug("{} unchanged so not sending".format( + self.message_type)) + return + logging.debug("Sending new {} data!".format(self.message_type)) + message = {"type": self.message_type, self.message_type: result} + return self.registry.broker.send_message(message, self._session_id) + + def get_new_data(self): + """Returns the data only if it has changed""" + data = self.get_data() + if self._persist is None: # Persist not initialized yet + return data + elif self._persist.get("data") != data: + self._persist.set("data", data) + return data + else: # Data not changed + return None + + def get_data(self): + """ + The result of this will be cached and subclasses must implement this + and return the correct return type defined in the server bound message + schema + """ + raise NotImplementedError("Subclasses must implement get_data()") + + def _reset(self): + """Reset the persist.""" + if self._persist: + self._persist.remove("data") diff --git a/landscape/client/manager/tests/test_config.py b/landscape/client/manager/tests/test_config.py index 085bd09fe..63db8ae48 100644 --- a/landscape/client/manager/tests/test_config.py +++ b/landscape/client/manager/tests/test_config.py @@ -23,6 +23,7 @@ def test_plugin_factories(self): "SnapManager", "SnapServicesManager", "UbuntuProInfo", + "LivePatch" ], ALL_PLUGINS, ) diff --git a/landscape/client/monitor/tests/test_livepatch.py b/landscape/client/manager/tests/test_livepatch.py similarity index 91% rename from landscape/client/monitor/tests/test_livepatch.py rename to landscape/client/manager/tests/test_livepatch.py index 85cbc772c..a1253f8d8 100644 --- a/landscape/client/monitor/tests/test_livepatch.py +++ b/landscape/client/manager/tests/test_livepatch.py @@ -2,7 +2,7 @@ import yaml from unittest import mock -from landscape.client.monitor.livepatch import LivePatch +from landscape.client.manager.livepatch import LivePatch from landscape.client.tests.helpers import LandscapeTest, MonitorHelper @@ -28,11 +28,11 @@ def setUp(self): def test_livepatch(self): """Tests calling livepatch status.""" plugin = LivePatch() - self.monitor.add(plugin) with mock.patch("subprocess.run") as run_mock: run_mock.side_effect = subprocess_livepatch_mock - plugin.exchange() + self.monitor.add(plugin) + plugin.run() messages = self.mstore.get_pending_messages() self.assertTrue(len(messages) > 0) @@ -47,11 +47,11 @@ def test_livepatch(self): def test_livepatch_when_not_installed(self): """Tests calling livepatch when it is not installed.""" plugin = LivePatch() - self.monitor.add(plugin) with mock.patch("subprocess.run") as run_mock: run_mock.side_effect = FileNotFoundError("Not found!") - plugin.exchange() + self.monitor.add(plugin) + plugin.run() messages = self.mstore.get_pending_messages() message = json.loads(messages[0]["livepatch"]) @@ -64,11 +64,11 @@ def test_livepatch_when_not_installed(self): def test_undefined_exception(self): """Tests calling livepatch when random exception occurs""" plugin = LivePatch() - self.monitor.add(plugin) with mock.patch("subprocess.run") as run_mock: run_mock.side_effect = ValueError("Not found!") - plugin.exchange() + self.monitor.add(plugin) + plugin.run() messages = self.mstore.get_pending_messages() message = json.loads(messages[0]["livepatch"]) @@ -83,13 +83,13 @@ def test_yaml_json_parse_error(self): If json or yaml parsing error than show exception and unparsed data """ plugin = LivePatch() - self.monitor.add(plugin) invalid_data = "'" with mock.patch("subprocess.run") as run_mock: run_mock.return_value = mock.Mock(stdout=invalid_data) run_mock.return_value.returncode = 0 - plugin.exchange() + self.monitor.add(plugin) + plugin.run() messages = self.mstore.get_pending_messages() message = json.loads(messages[0]["livepatch"]) @@ -104,14 +104,14 @@ def test_empty_string(self): If livepatch is disabled, stdout is empty string """ plugin = LivePatch() - self.monitor.add(plugin) invalid_data = "" with mock.patch("subprocess.run") as run_mock: run_mock.return_value = mock.Mock(stdout=invalid_data, stderr='Error') run_mock.return_value.returncode = 1 - plugin.exchange() + self.monitor.add(plugin) + plugin.run() messages = self.mstore.get_pending_messages() message = json.loads(messages[0]["livepatch"]) @@ -126,11 +126,11 @@ def test_timestamped_fields_deleted(self): """This is so data doesn't keep getting sent if not changed""" plugin = LivePatch() - self.monitor.add(plugin) with mock.patch("subprocess.run") as run_mock: run_mock.side_effect = subprocess_livepatch_mock - plugin.exchange() + self.monitor.add(plugin) + plugin.run() messages = self.mstore.get_pending_messages() self.assertTrue(len(messages) > 0) diff --git a/landscape/client/manager/tests/test_plugin.py b/landscape/client/manager/tests/test_plugin.py index 1aadd0acc..2bd8d08fd 100644 --- a/landscape/client/manager/tests/test_plugin.py +++ b/landscape/client/manager/tests/test_plugin.py @@ -1,7 +1,7 @@ from twisted.internet.defer import Deferred from landscape.client.manager.plugin import FAILED -from landscape.client.manager.plugin import ManagerPlugin +from landscape.client.manager.plugin import ManagerPlugin, DataWatcherManager from landscape.client.manager.plugin import SUCCEEDED from landscape.client.tests.helpers import LandscapeTest from landscape.client.tests.helpers import ManagerHelper @@ -126,3 +126,37 @@ def assert_messages(ignored): result.addCallback(assert_messages) deferred.callback("blah") return result + + +class StubDataWatchingPlugin(DataWatcherManager): + + message_type = "wubble" + + def __init__(self, data=None): + self.data = data + + def get_data(self): + return self.data + + +class DataWatcherManagerTest(LandscapeTest): + + helpers = [ManagerHelper] + + def setUp(self): + LandscapeTest.setUp(self) + self.plugin = StubDataWatchingPlugin("hello world") + self.plugin.register(self.manager) + + def test_get_message(self): + self.assertEqual( + self.plugin.get_new_data(), + "hello world", + ) + + def test_get_message_unchanging(self): + self.assertEqual( + self.plugin.get_new_data(), + "hello world", + ) + self.assertEqual(self.plugin.get_new_data(), None) diff --git a/landscape/client/manager/ubuntuproinfo.py b/landscape/client/manager/ubuntuproinfo.py index e5c054a61..08a77bd74 100644 --- a/landscape/client/manager/ubuntuproinfo.py +++ b/landscape/client/manager/ubuntuproinfo.py @@ -3,15 +3,13 @@ from datetime import datetime from datetime import timedelta from datetime import timezone -from pathlib import Path from landscape.client import IS_CORE from landscape.client import IS_SNAP -from landscape.client.manager.plugin import ManagerPlugin -from landscape.lib.persist import Persist +from landscape.client.manager.plugin import DataWatcherManager -class UbuntuProInfo(ManagerPlugin): +class UbuntuProInfo(DataWatcherManager): """ Plugin that captures and reports Ubuntu Pro registration information. @@ -25,41 +23,10 @@ class UbuntuProInfo(ManagerPlugin): message_type = "ubuntu-pro-info" run_interval = 900 # 15 minutes - def register(self, registry): - super().register(registry) - self._persist_filename = Path( - self.registry.config.data_path, - "ubuntu-pro-info.bpickle", - ) - self._persist = Persist(filename=self._persist_filename) - self.call_on_accepted(self.message_type, self.send_message) - - def run(self): - return self.registry.broker.call_if_accepted( - self.message_type, - self.send_message, - ) - - def send_message(self): - """Send a message to the broker if the data has changed since the last - call""" - result = self.get_data() - if not result: - return - message = {"type": self.message_type, "ubuntu-pro-info": result} - return self.registry.broker.send_message(message, self._session_id) - def get_data(self): - """Persist data to avoid sending messages if result hasn't changed""" ubuntu_pro_info = get_ubuntu_pro_info() - - if self._persist.get("data") != ubuntu_pro_info: - self._persist.set("data", ubuntu_pro_info) - return json.dumps(ubuntu_pro_info, separators=(",", ":")) - - def _reset(self): - """Reset the persist.""" - self._persist.remove("data") + return json.dumps(ubuntu_pro_info, separators=(",", ":"), + sort_keys=True) def get_ubuntu_pro_info() -> dict: diff --git a/landscape/client/monitor/config.py b/landscape/client/monitor/config.py index 4fb351e78..298d6b5c0 100644 --- a/landscape/client/monitor/config.py +++ b/landscape/client/monitor/config.py @@ -20,7 +20,6 @@ "SwiftUsage", "CephUsage", "ComputerTags", - "LivePatch", "UbuntuProRebootRequired", "SnapServicesMonitor", ]