From a98675890f3d404189e5a967d8f215628beb9ac7 Mon Sep 17 00:00:00 2001 From: Dmitry Tantsur Date: Fri, 17 Jun 2022 09:48:19 +0200 Subject: [PATCH] Collect udev properties in the ramdisk logs Change-Id: Ifcf3dfff00b604dec1e2f430369ab8053f50f137 --- ironic_python_agent/tests/unit/test_utils.py | 45 +++++++++++++++++-- ironic_python_agent/utils.py | 43 ++++++++++++++++++ .../notes/collect-udev-f6ada5163cf4a26c.yaml | 5 +++ 3 files changed, 89 insertions(+), 4 deletions(-) create mode 100644 releasenotes/notes/collect-udev-f6ada5163cf4a26c.yaml diff --git a/ironic_python_agent/tests/unit/test_utils.py b/ironic_python_agent/tests/unit/test_utils.py index c99f7dcaa..8adb78cc3 100644 --- a/ironic_python_agent/tests/unit/test_utils.py +++ b/ironic_python_agent/tests/unit/test_utils.py @@ -16,6 +16,7 @@ import errno import glob import io +import json import os import shutil import subprocess @@ -410,12 +411,14 @@ class TestUtils(ironic_agent_base.IronicAgentTest): mock_call.side_effect = os_error self.assertFalse(utils.is_journalctl_present()) + @mock.patch.object(utils, '_collect_udev', autospec=True) @mock.patch.object(utils, 'gzip_and_b64encode', autospec=True) @mock.patch.object(utils, 'is_journalctl_present', autospec=True) @mock.patch.object(utils, 'get_command_output', autospec=True) @mock.patch.object(utils, 'get_journalctl_output', autospec=True) def test_collect_system_logs_journald( - self, mock_logs, mock_outputs, mock_journalctl, mock_gzip_b64): + self, mock_logs, mock_outputs, mock_journalctl, mock_gzip_b64, + mock_udev): mock_journalctl.return_value = True ret = 'Patrick Star' mock_gzip_b64.return_value = ret @@ -435,13 +438,16 @@ class TestUtils(ironic_agent_base.IronicAgentTest): 'mount': mock.ANY, 'parted': mock.ANY, 'multipath': mock.ANY}, file_list=[]) + mock_udev.assert_called_once_with(mock.ANY) + @mock.patch.object(utils, '_collect_udev', autospec=True) @mock.patch.object(utils, 'gzip_and_b64encode', autospec=True) @mock.patch.object(utils, 'is_journalctl_present', autospec=True) @mock.patch.object(utils, 'get_command_output', autospec=True) @mock.patch.object(utils, 'get_journalctl_output', autospec=True) def test_collect_system_logs_journald_with_logfile( - self, mock_logs, mock_outputs, mock_journalctl, mock_gzip_b64): + self, mock_logs, mock_outputs, mock_journalctl, mock_gzip_b64, + mock_udev): tmp = tempfile.NamedTemporaryFile() self.addCleanup(lambda: tmp.close()) @@ -465,12 +471,15 @@ class TestUtils(ironic_agent_base.IronicAgentTest): 'mount': mock.ANY, 'parted': mock.ANY, 'multipath': mock.ANY}, file_list=[tmp.name]) + mock_udev.assert_called_once_with(mock.ANY) + @mock.patch.object(utils, '_collect_udev', autospec=True) @mock.patch.object(utils, 'gzip_and_b64encode', autospec=True) @mock.patch.object(utils, 'is_journalctl_present', autospec=True) @mock.patch.object(utils, 'get_command_output', autospec=True) def test_collect_system_logs_non_journald( - self, mock_outputs, mock_journalctl, mock_gzip_b64): + self, mock_outputs, mock_journalctl, mock_gzip_b64, + mock_udev): mock_journalctl.return_value = False ret = 'SpongeBob SquarePants' mock_gzip_b64.return_value = ret @@ -490,12 +499,15 @@ class TestUtils(ironic_agent_base.IronicAgentTest): 'mount': mock.ANY, 'parted': mock.ANY, 'multipath': mock.ANY}, file_list=['/var/log']) + mock_udev.assert_called_once_with(mock.ANY) + @mock.patch.object(utils, '_collect_udev', autospec=True) @mock.patch.object(utils, 'gzip_and_b64encode', autospec=True) @mock.patch.object(utils, 'is_journalctl_present', autospec=True) @mock.patch.object(utils, 'get_command_output', autospec=True) def test_collect_system_logs_non_journald_with_logfile( - self, mock_outputs, mock_journalctl, mock_gzip_b64): + self, mock_outputs, mock_journalctl, mock_gzip_b64, + mock_udev): tmp = tempfile.NamedTemporaryFile() self.addCleanup(lambda: tmp.close()) @@ -519,6 +531,31 @@ class TestUtils(ironic_agent_base.IronicAgentTest): 'mount': mock.ANY, 'parted': mock.ANY, 'multipath': mock.ANY}, file_list=['/var/log', tmp.name]) + mock_udev.assert_called_once_with(mock.ANY) + + @mock.patch('pyudev.Context', lambda: mock.sentinel.context) + @mock.patch('pyudev.Devices.from_device_file', autospec=True) + @mock.patch.object(ironic_utils, 'execute', autospec=True) + def test_collect_udev(self, mock_execute, mock_from_dev): + mock_execute.return_value = """ + fake0 + fake1 + fake42 + """, "" + mock_from_dev.side_effect = [ + mock.Mock(properties={'ID_UUID': '0'}), + RuntimeError('nope'), + {'ID_UUID': '42'} + ] + + result = {} + utils._collect_udev(result) + self.assertEqual({'udev/fake0', 'udev/fake42'}, set(result)) + for i in ('0', '42'): + buf = result[f'udev/fake{i}'] + # Avoiding getvalue on purpose - checking that the IO is not closed + val = json.loads(buf.read().decode('utf-8')) + self.assertEqual({'ID_UUID': i}, val) def test_get_ssl_client_options(self): # defaults diff --git a/ironic_python_agent/utils.py b/ironic_python_agent/utils.py index 66f6819fd..bcf9a0a43 100644 --- a/ironic_python_agent/utils.py +++ b/ironic_python_agent/utils.py @@ -18,6 +18,7 @@ import copy import errno import glob import io +import json import os import re import shutil @@ -33,6 +34,7 @@ from oslo_log import log as logging from oslo_serialization import base64 from oslo_serialization import jsonutils from oslo_utils import units +import pyudev import requests import tenacity @@ -530,6 +532,42 @@ def gzip_and_b64encode(io_dict=None, file_list=None): return base64.encode_as_text(fp.getvalue()) +def _collect_udev(io_dict): + """Collect device properties from udev.""" + try: + out, _e = ironic_utils.execute('lsblk', '-no', 'KNAME') + except processutils.ProcessExecutionError as exc: + LOG.warning('Could not list block devices: %s', exc) + return + + context = pyudev.Context() + + for kname in out.splitlines(): + kname = kname.strip() + if not kname: + continue + + name = os.path.join('/dev', kname) + + try: + udev = pyudev.Devices.from_device_file(context, name) + except Exception as e: + LOG.warning("Device %(dev)s is inaccessible, skipping... " + "Error: %(error)s", {'dev': name, 'error': e}) + continue + + try: + props = dict(udev.properties) + except AttributeError: # pyudev < 0.20 + props = dict(udev) + + fp = io.TextIOWrapper(io.BytesIO(), encoding='utf-8') + json.dump(props, fp) + buf = fp.detach() + buf.seek(0) + io_dict[f'udev/{kname}'] = buf + + def collect_system_logs(journald_max_lines=None): """Collect system logs. @@ -568,6 +606,11 @@ def collect_system_logs(journald_max_lines=None): for name, cmd in COLLECT_LOGS_COMMANDS.items(): try_get_command_output(io_dict, name, cmd) + try: + _collect_udev(io_dict) + except Exception: + LOG.exception('Unexpected error when collecting udev properties') + return gzip_and_b64encode(io_dict=io_dict, file_list=file_list) diff --git a/releasenotes/notes/collect-udev-f6ada5163cf4a26c.yaml b/releasenotes/notes/collect-udev-f6ada5163cf4a26c.yaml new file mode 100644 index 000000000..24437c3bf --- /dev/null +++ b/releasenotes/notes/collect-udev-f6ada5163cf4a26c.yaml @@ -0,0 +1,5 @@ +--- +other: + - | + Block devices properties reported by udev are now collected with the + ramdisk logs.