diff --git a/ironic_python_agent/inspector.py b/ironic_python_agent/inspector.py index ac4a8ac99..49ddfc375 100644 --- a/ironic_python_agent/inspector.py +++ b/ironic_python_agent/inspector.py @@ -13,7 +13,11 @@ # See the License for the specific language governing permissions and # limitations under the License. +import base64 +import io +import json import logging +import tarfile import netaddr from oslo_concurrency import processutils @@ -251,3 +255,67 @@ def collect_default(data, failures): # dropped after inspector is ready (probably in Mitaka cycle). discover_network_properties(inventory, data, failures) discover_scheduling_properties(inventory, data, root_disk) + + +def collect_logs(data, failures): + """Collect journald logs from the ramdisk. + + As inspection runs before any nodes details are known, it's handy to have + logs returned with data. This collector sends logs to inspector in format + expected by the 'ramdisk_error' plugin: base64 encoded tar.gz. + + This collector should be installed last in the collector chain, otherwise + it won't collect enough logs. + + This collector does not report failures. + + :param data: mutable data that we'll send to inspector + :param failures: AccumulatedFailures object + """ + try: + out, _e = utils.execute('journalctl', '--full', '--no-pager', '-b', + '-n', '10000') + except (processutils.ProcessExecutionError, OSError): + LOG.warn('failed to get system journal') + return + + journal = io.BytesIO(out.encode('utf-8')) + with io.BytesIO() as fp: + with tarfile.open(fileobj=fp, mode='w:gz') as tar: + tarinfo = tarfile.TarInfo('journal') + tarinfo.size = len(out) + tar.addfile(tarinfo, journal) + + fp.seek(0) + data['logs'] = base64.b64encode(fp.getvalue()) + + +def collect_extra_hardware(data, failures): + """Collect detailed inventory using 'hardware-detect' utility. + + Recognizes ipa-inspection-benchmarks with list of benchmarks (possible + values are cpu, disk, mem) to run. No benchmarks are run by default, as + they're pretty time-consuming. + + Puts collected data as JSON under 'data' key. + Requires 'hardware' python package to be installed on the ramdisk in + addition to the packages in requirements.txt. + + :param data: mutable data that we'll send to inspector + :param failures: AccumulatedFailures object + """ + benchmarks = utils.get_agent_params().get('ipa-inspection-benchmarks', []) + if benchmarks: + benchmarks = ['--benchmark'] + benchmarks.split(',') + + try: + out, err = utils.execute('hardware-detect', *benchmarks) + except (processutils.ProcessExecutionError, OSError) as exc: + failures.add('failed to run hardware-detect utility: %s', exc) + return + + try: + data['data'] = json.loads(out) + except ValueError as exc: + msg = 'JSON returned from hardware-detect cannot be decoded: %s' + failures.add(msg, exc) diff --git a/ironic_python_agent/tests/unit/test_inspector.py b/ironic_python_agent/tests/unit/test_inspector.py index d2fa0891e..0e6c18b01 100644 --- a/ironic_python_agent/tests/unit/test_inspector.py +++ b/ironic_python_agent/tests/unit/test_inspector.py @@ -13,8 +13,11 @@ # See the License for the specific language governing permissions and # limitations under the License. +import base64 import collections import copy +import io +import tarfile import unittest import mock @@ -363,3 +366,74 @@ class TestCollectDefault(BaseDiscoverTest): self.failures) mock_discover_sched.assert_called_once_with( self.inventory, self.data, root_disk=None) + + +@mock.patch.object(utils, 'execute', autospec=True) +class TestCollectLogs(unittest.TestCase): + def test(self, mock_execute): + contents = 'journal contents' + mock_execute.return_value = (contents, '') + + data = {} + inspector.collect_logs(data, None) + res = io.BytesIO(base64.b64decode(data['logs'])) + + with tarfile.open(fileobj=res) as tar: + members = [(m.name, m.size) for m in tar] + self.assertEqual([('journal', len(contents))], members) + + member = tar.extractfile('journal') + self.assertEqual(contents, member.read().decode('utf-8')) + + def test_no_journal(self, mock_execute): + mock_execute.side_effect = OSError() + + data = {} + inspector.collect_logs(data, None) + self.assertFalse(data) + + +@mock.patch.object(utils, 'execute', autospec=True) +class TestCollectExtraHardware(unittest.TestCase): + def setUp(self): + super(TestCollectExtraHardware, self).setUp() + self.data = {} + self.failures = utils.AccumulatedFailures() + + def test_no_benchmarks(self, mock_execute): + mock_execute.return_value = ("[1, 2, 3]", "") + + inspector.collect_extra_hardware(self.data, None) + + self.assertEqual({'data': [1, 2, 3]}, self.data) + mock_execute.assert_called_once_with('hardware-detect') + + @mock.patch.object(utils, 'get_agent_params', autospec=True) + def test_benchmarks(self, mock_params, mock_execute): + mock_params.return_value = {'ipa-inspection-benchmarks': 'cpu,mem'} + mock_execute.return_value = ("[1, 2, 3]", "") + + inspector.collect_extra_hardware(self.data, None) + + self.assertEqual({'data': [1, 2, 3]}, self.data) + mock_execute.assert_called_once_with('hardware-detect', + '--benchmark', + 'cpu', 'mem') + + def test_execute_failed(self, mock_execute): + mock_execute.side_effect = processutils.ProcessExecutionError() + + inspector.collect_extra_hardware(self.data, self.failures) + + self.assertNotIn('data', self.data) + self.assertTrue(self.failures) + mock_execute.assert_called_once_with('hardware-detect') + + def test_parsing_failed(self, mock_execute): + mock_execute.return_value = ("foobar", "") + + inspector.collect_extra_hardware(self.data, self.failures) + + self.assertNotIn('data', self.data) + self.assertTrue(self.failures) + mock_execute.assert_called_once_with('hardware-detect') diff --git a/plugin-requirements.txt b/plugin-requirements.txt new file mode 100644 index 000000000..543823f6b --- /dev/null +++ b/plugin-requirements.txt @@ -0,0 +1,2 @@ +# Required for 'extra-hardware' inspection collector +hardware>=0.9 diff --git a/setup.cfg b/setup.cfg index f6150dfd9..c58b26d50 100644 --- a/setup.cfg +++ b/setup.cfg @@ -32,6 +32,8 @@ ironic_python_agent.hardware_managers = ironic_python_agent.inspector.collectors = default = ironic_python_agent.inspector:collect_default + logs = ironic_python_agent.inspector:collect_logs + extra-hardware = ironic_python_agent.inspector:collect_extra_hardware [pbr] autodoc_index_modules = True