Skip to content

Commit

Permalink
Merge pull request #273 from abehnia/lndeng-1550-create-cloud-init-me…
Browse files Browse the repository at this point in the history
…ssage

feat: add cloud-init message
  • Loading branch information
abehnia authored Oct 4, 2024
2 parents eb8610d + 61c86ea commit 4f2f0db
Show file tree
Hide file tree
Showing 7 changed files with 191 additions and 5 deletions.
7 changes: 4 additions & 3 deletions example.conf
Original file line number Diff line number Diff line change
Expand Up @@ -93,10 +93,8 @@ ignore_sigusr1 = False
# SwiftUsage - Swift cluster usage
# CephUsage - Ceph usage information
# ComputerTags - changes in computer tags
# UbuntuProInfo - Ubuntu Pro registration information
# LivePatch - Livepath status information
# UbuntuProRebootRequired - informs if the system needs to be rebooted
# SnapServicesMonitor - manage snap services
# CloudInit - Relevant information for cloud init
#
# The special value "ALL" is an alias for the full list of plugins.
monitor_plugins = ALL
Expand Down Expand Up @@ -180,6 +178,9 @@ cloud = True
# KeystoneToken
# SnapManager
# SnapServicesManager
# UbuntuProInfo - Ubuntu Pro registration information
# LivePatch - Livepath status information
# UbuntuProRebootRequired - informs if the system needs to be rebooted
#
# The special value "ALL" is an alias for the entire list of plugins above and is the default.
manager_plugins = ALL
Expand Down
2 changes: 1 addition & 1 deletion landscape/client/manager/tests/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ def test_plugin_factories(self):
"SnapServicesManager",
"UbuntuProInfo",
"LivePatch",
"UbuntuProRebootRequired"
"UbuntuProRebootRequired",
],
ALL_PLUGINS,
)
Expand Down
69 changes: 69 additions & 0 deletions landscape/client/monitor/cloudinit.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import json
import subprocess

from landscape.client.monitor.plugin import DataWatcher


class CloudInit(DataWatcher):

message_type = "cloud-init"
message_key = message_type
scope = message_type
persist_name = message_type
run_immediately = True
run_interval = 3600 * 24 # 24h

def get_data(self) -> str:
return json.dumps(get_cloud_init(), sort_keys=True)

def register(self, monitor):
super().register(monitor)
self.call_on_accepted("cloud-init", self.exchange, True)


def get_cloud_init():
"""
cloud-init returns all the information the instance has been initialized
with, in JSON format. This function takes the the output and parses it
into a python dictionary and sticks it in "output" along with error and
return code information.
"""

data = {}
output = {}

try:
completed_process = subprocess.run(
["cloud-init", "query", "-a"],
encoding="utf8",
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
)
except FileNotFoundError as exc:
data["return_code"] = -1
data["error"] = str(exc)
data["output"] = output
except Exception as exc:
data["return_code"] = -2
data["error"] = str(exc)
data["output"] = output
else:
string_output = completed_process.stdout.strip()
try:
# INFO: We don't want to parse an empty string.
if string_output:
json_output = json.loads(string_output)
# INFO: Only return relevant information from cloud init.
output["availability_zone"] = json_output.get(
"availability_zone",
"",
) or json_output.get("availability-zone", "")
data["return_code"] = completed_process.returncode
data["error"] = completed_process.stderr
data["output"] = output
except json.decoder.JSONDecodeError as exc:
data["return_code"] = completed_process.returncode
data["error"] = str(exc)
data["output"] = output

return data
1 change: 1 addition & 0 deletions landscape/client/monitor/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
"CephUsage",
"ComputerTags",
"SnapServicesMonitor",
"CloudInit",
]


Expand Down
108 changes: 108 additions & 0 deletions landscape/client/monitor/tests/test_cloud_init.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import json
from unittest import mock

from landscape.client.monitor.cloudinit import CloudInit
from landscape.client.tests.helpers import LandscapeTest
from landscape.client.tests.helpers import MonitorHelper


def subprocess_cloud_init_mock(*args, **kwargs):
"""Mock a cloud-init subprocess output."""
data = {"availability_zone": "us-east-1"}
output = json.dumps(data)
return mock.Mock(stdout=output, stderr="", returncode=0)


class CloudInitTest(LandscapeTest):
"""Cloud init plugin tests."""

helpers = [MonitorHelper]

def setUp(self):
super(CloudInitTest, self).setUp()
self.mstore.set_accepted_types(["cloud-init"])

def test_cloud_init(self):
"""Test calling cloud-init."""
plugin = CloudInit()

with mock.patch("subprocess.run") as run_mock:
run_mock.side_effect = subprocess_cloud_init_mock
self.monitor.add(plugin)
plugin.exchange()

messages = self.mstore.get_pending_messages()
self.assertTrue(len(messages) > 0)
message = json.loads(messages[0]["cloud-init"])
self.assertEqual(message["output"]["availability_zone"], "us-east-1")
self.assertEqual(message["return_code"], 0)
self.assertFalse(message["error"])

def test_cloud_init_when_not_installed(self):
"""Tests calling cloud-init when it is not installed."""
plugin = CloudInit()

with mock.patch("subprocess.run") as run_mock:
run_mock.side_effect = FileNotFoundError("Not found!")
self.monitor.add(plugin)
plugin.exchange()

messages = self.mstore.get_pending_messages()
message = json.loads(messages[0]["cloud-init"])
self.assertTrue(len(messages) > 0)
self.assertTrue(message["error"])
self.assertEqual(message["return_code"], -1)
self.assertEqual({}, message["output"])

def test_undefined_exception(self):
"""Test calling cloud-init when a random exception occurs."""
plugin = CloudInit()

with mock.patch("subprocess.run") as run_mock:
run_mock.side_effect = ValueError("Not found!")
self.monitor.add(plugin)
plugin.exchange()

messages = self.mstore.get_pending_messages()
message = json.loads(messages[0]["cloud-init"])
self.assertTrue(len(messages) > 0)
self.assertTrue(message["error"])
self.assertEqual(message["return_code"], -2)
self.assertEqual({}, message["output"])

def test_json_parse_error(self):
"""
If a Json parsing error occurs, show the exception and unparsed data.
"""
plugin = CloudInit()

with mock.patch("subprocess.run") as run_mock:
run_mock.return_value = mock.Mock(stdout="'")
run_mock.return_value.returncode = 0
self.monitor.add(plugin)
plugin.exchange()

messages = self.mstore.get_pending_messages()
message = json.loads(messages[0]["cloud-init"])
self.assertTrue(len(messages) > 0)
self.assertTrue(message["error"])
self.assertEqual({}, message["output"])

def test_empty_string(self):
"""
If cloud-init is disabled, stdout is an empty string.
"""
plugin = CloudInit()

with mock.patch("subprocess.run") as run_mock:
run_mock.return_value = mock.Mock(stdout="", stderr="Error")
run_mock.return_value.returncode = 1
self.monitor.add(plugin)
plugin.exchange()

messages = self.mstore.get_pending_messages()
message = json.loads(messages[0]["cloud-init"])
self.assertTrue(len(messages) > 0)
self.assertTrue(message["error"])
self.assertEqual(message["return_code"], 1)
self.assertEqual({}, message["output"])
7 changes: 7 additions & 0 deletions landscape/message_schemas/server_bound.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
"ACTIVE_PROCESS_INFO",
"COMPUTER_UPTIME",
"CLIENT_UPTIME",
"CLOUD_INIT",
"OPERATION_RESULT",
"COMPUTER_INFO",
"DISTRIBUTION_INFO",
Expand Down Expand Up @@ -768,6 +769,11 @@
{"ubuntu-pro-reboot-required": Unicode()},
)

CLOUD_INIT = Message(
"cloud-init",
{"cloud-init": Unicode()},
)

SNAPS = Message(
"snaps",
{
Expand Down Expand Up @@ -849,6 +855,7 @@
ACTIVE_PROCESS_INFO,
COMPUTER_UPTIME,
CLIENT_UPTIME,
CLOUD_INIT,
OPERATION_RESULT,
COMPUTER_INFO,
DISTRIBUTION_INFO,
Expand Down
2 changes: 1 addition & 1 deletion snap/hooks/default-configure
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ if [ -z "$_manager_plugins" ]; then
fi

if [ -z "$_monitor_plugins" ]; then
_monitor_plugins="ActiveProcessInfo,ComputerInfo,LoadAverage,MemoryInfo,MountInfo,ProcessorInfo,Temperature,UserMonitor,RebootRequired,NetworkActivity,NetworkDevice,CPUUsage,SwiftUsage,CephUsage,ComputerTags,SnapServicesMonitor"
_monitor_plugins="ActiveProcessInfo,ComputerInfo,LoadAverage,MemoryInfo,MountInfo,ProcessorInfo,Temperature,UserMonitor,RebootRequired,NetworkActivity,NetworkDevice,CPUUsage,SwiftUsage,CephUsage,ComputerTags,SnapServicesMonitor,CloudInit"
fi

cat > "$CLIENT_CONF" << EOF
Expand Down

0 comments on commit 4f2f0db

Please sign in to comment.