From cc9e05da504693af6ceafff64527200bcbe4bb7d Mon Sep 17 00:00:00 2001 From: Jaganathan Palanisamy Date: Thu, 9 Feb 2017 07:04:33 -0500 Subject: [PATCH] NUMA-topology collector Implement the optional collector for fetching the NUMA topology details. Collects RAM, CPU Cores, thread siblings and NICS data for each NUMA node and stored under "numa_topology" key. Closes-bug: #1635253 Co-Authored-By: Jaganathan Palanisamy Change-Id: I5a546c009d95f39b7af4d89cf785be8acb8ebc67 Signed-off-by: karthik s --- ironic_python_agent/errors.py | 6 + ironic_python_agent/numa_inspector.py | 255 +++++++++++++ .../tests/unit/test_numa_inspector.py | 350 ++++++++++++++++++ ...d-numa-topology-info-8c253fd9e56169f1.yaml | 4 + setup.cfg | 1 + 5 files changed, 616 insertions(+) create mode 100644 ironic_python_agent/numa_inspector.py create mode 100644 ironic_python_agent/tests/unit/test_numa_inspector.py create mode 100644 releasenotes/notes/add-numa-topology-info-8c253fd9e56169f1.yaml diff --git a/ironic_python_agent/errors.py b/ironic_python_agent/errors.py index 2c49ffc88..11f769f04 100644 --- a/ironic_python_agent/errors.py +++ b/ironic_python_agent/errors.py @@ -293,6 +293,12 @@ class ISCSIError(RESTError): super(ISCSIError, self).__init__(details) +class IncompatibleNumaFormatError(RESTError): + """Error raised when unexpected format data in NUMA node.""" + + message = 'Error in NUMA node data format' + + class ISCSICommandError(ISCSIError): """Error executing TGT command.""" diff --git a/ironic_python_agent/numa_inspector.py b/ironic_python_agent/numa_inspector.py new file mode 100644 index 000000000..4111217f7 --- /dev/null +++ b/ironic_python_agent/numa_inspector.py @@ -0,0 +1,255 @@ +# Copyright 2017 Red Hat, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os + +from oslo_log import log +import pint + +from ironic_python_agent import errors + +LOG = log.getLogger(__name__) + +UNIT_CONVERTER = pint.UnitRegistry(filename=None) +UNIT_CONVERTER.define('kB = []') +UNIT_CONVERTER.define('KB = []') +UNIT_CONVERTER.define('MB = 1024 KB') +UNIT_CONVERTER.define('GB = 1048576 KB') + + +def get_numa_node_id(numa_node_dir): + """Provides the NUMA node id from NUMA node directory + + :param numa_node_dir: NUMA node directory + :raises: IncompatibleNumaFormatError: when unexpected format data + in NUMA node dir + + :return: NUMA node id + """ + try: + return int(os.path.basename(numa_node_dir)[4:]) + except (IOError, ValueError, IndexError) as exc: + msg = ('Failed to get NUMA node id for %(node)s: ' + '%(error)s' % {'node': numa_node_dir, 'error': exc}) + raise errors.IncompatibleNumaFormatError(msg) + + +def get_nodes_memory_info(numa_node_dirs): + """Collect the NUMA nodes memory information. + + "ram": [{"numa_node": , "size_kb": }, ...] + + :param numa_node_dirs: A list of NUMA node directories + :raises: IncompatibleNumaFormatError: when unexpected format data + in NUMA node + + :return: A list of memory information with NUMA node id + """ + ram = [] + for numa_node_dir in numa_node_dirs: + numa_node_memory = {} + numa_node_id = get_numa_node_id(numa_node_dir) + try: + with open(os.path.join(numa_node_dir, + 'meminfo')) as meminfo_file: + for line in meminfo_file: + if 'MemTotal' in line: + break + else: + msg = ('Memory information is not available for ' + '%(node)s' % {'node': numa_node_dir}) + raise errors.IncompatibleNumaFormatError(msg) + except IOError as exc: + msg = ('Failed to get memory information ' + 'for %(node)s: %(error)s' % + {'node': numa_node_dir, 'error': exc}) + raise errors.IncompatibleNumaFormatError(msg) + try: + # To get memory size with unit from memory info line + # Memory info sample line format 'Node 0 MemTotal: 1560000 kB' + value = line.split(":")[1].strip() + memory_kb = int(UNIT_CONVERTER(value).to_base_units()) + except (ValueError, IndexError, pint.errors.UndefinedUnitError) as exc: + msg = ('Failed to get memory information for %(node)s: ' + '%(error)s' % {'node': numa_node_dir, 'error': exc}) + raise errors.IncompatibleNumaFormatError(msg) + numa_node_memory['numa_node'] = numa_node_id + numa_node_memory['size_kb'] = memory_kb + LOG.debug('Found memory available %d KB in NUMA node %d', + memory_kb, numa_node_id) + ram.append(numa_node_memory) + return ram + + +def get_nodes_cores_info(numa_node_dirs): + """Collect the NUMA nodes cpu's and thread's information. + + "cpus": [ + { + "cpu": , "numa_node": , + "thread_siblings": [] + }, + ..., + ] + NUMA nodes path: /sys/devices/system/node/node + + Thread dirs path: /sys/devices/system/node/node/cpu + + CPU id file path: /sys/devices/system/node/node/cpu/ + topology/core_id + + :param numa_node_dirs: A list of NUMA node directories + :raises: IncompatibleNumaFormatError: when unexpected format data + in NUMA node + + :return: A list of cpu information with NUMA node id and thread siblings + """ + dict_cpus = {} + for numa_node_dir in numa_node_dirs: + numa_node_id = get_numa_node_id(numa_node_dir) + try: + thread_dirs = os.listdir(numa_node_dir) + except OSError as exc: + msg = ('Failed to get list of threads for %(node)s: ' + '%(error)s' % {'node': numa_node_dir, 'error': exc}) + raise errors.IncompatibleNumaFormatError(msg) + for thread_dir in thread_dirs: + if (not os.path.isdir(os.path.join(numa_node_dir, thread_dir)) + or not thread_dir.startswith("cpu")): + continue + try: + thread_id = int(thread_dir[3:]) + except (ValueError, IndexError) as exc: + msg = ('Failed to get cores information for ' + '%(node)s: %(error)s' % + {'node': numa_node_dir, 'error': exc}) + raise errors.IncompatibleNumaFormatError(msg) + try: + with open(os.path.join(numa_node_dir, thread_dir, 'topology', + 'core_id')) as core_id_file: + cpu_id = int(core_id_file.read().strip()) + except (IOError, ValueError) as exc: + msg = ('Failed to gather cpu_id for thread' + '%(thread)s NUMA node %(node)s: %(error)s' % + {'thread': thread_dir, 'node': numa_node_dir, + 'error': exc}) + raise errors.IncompatibleNumaFormatError(msg) + # CPU and NUMA node together forms a unique value, as cpu_id is + # specific to a NUMA node + # NUMA node id and cpu id tuple is used for unique key + dict_key = numa_node_id, cpu_id + if dict_key in dict_cpus: + if thread_id not in dict_cpus[dict_key]['thread_siblings']: + dict_cpus[dict_key]['thread_siblings'].append(thread_id) + else: + cpu_item = {} + cpu_item['thread_siblings'] = [thread_id] + cpu_item['cpu'] = cpu_id + cpu_item['numa_node'] = numa_node_id + dict_cpus[dict_key] = cpu_item + LOG.debug('Found a thread sibling %d for CPU %d in NUMA node %d', + thread_id, cpu_id, numa_node_id) + return list(dict_cpus.values()) + + +def get_nodes_nics_info(nic_device_path): + """Collect the NUMA nodes nics information. + + "nics": [ + {"name": "", "numa_node": }, + ..., + ] + + :param nic_device_path: nic device directory path + :raises: IncompatibleNumaFormatError: when unexpected format data + in NUMA node + + :return: A list of nics information with NUMA node id + """ + nics = [] + if not os.path.isdir(nic_device_path): + msg = ('Failed to get list of NIC\'s, NIC device path ' + 'does not exist: %(nic_device_path)s' % + {'nic_device_path': nic_device_path}) + raise errors.IncompatibleNumaFormatError(msg) + for nic_dir in os.listdir(nic_device_path): + if not os.path.isdir(os.path.join(nic_device_path, nic_dir, 'device')): + continue + try: + with open(os.path.join(nic_device_path, nic_dir, 'device', + 'numa_node')) as nicsinfo_file: + numa_node_id = int(nicsinfo_file.read().strip()) + except (IOError, ValueError) as exc: + msg = ('Failed to gather NIC\'s for NUMA node %(node)s: ' + '%(error)s' % {'node': nic_dir, 'error': exc}) + raise errors.IncompatibleNumaFormatError(msg) + numa_node_nics = {} + numa_node_nics['name'] = nic_dir + numa_node_nics['numa_node'] = numa_node_id + LOG.debug('Found a NIC %s in NUMA node %d', nic_dir, + numa_node_id) + nics.append(numa_node_nics) + return nics + + +def collect_numa_topology_info(data, failures): + """Collect the NUMA topology information. + + { + "numa_topology": { + "ram": [{"numa_node": , "size_kb": }, ...], + "cpus": [ + { + "cpu": , "numa_node": , + "thread_siblings": [] + }, + ..., + ], + "nics": [ + {"name": "", "numa_node": }, + ..., + ] + } + } + + The data is gathered from /sys/devices/system/node/node and + /sys/class/net/ directories. + + :param data: mutable data that we'll send to inspector + :param failures: AccumulatedFailures object + + :return: None + """ + numa_node_path = '/sys/devices/system/node/' + nic_device_path = '/sys/class/net/' + numa_info = {} + numa_node_dirs = [] + if not os.path.isdir(numa_node_path): + LOG.warning('Failed to get list of NUMA nodes, NUMA node path ' + 'does not exist: %s', numa_node_path) + return + for numa_node_dir in os.listdir(numa_node_path): + numa_node_dir_path = os.path.join(numa_node_path, numa_node_dir) + if (os.path.isdir(numa_node_dir_path) + and numa_node_dir.startswith("node")): + numa_node_dirs.append(numa_node_dir_path) + try: + numa_info['ram'] = get_nodes_memory_info(numa_node_dirs) + numa_info['cpus'] = get_nodes_cores_info(numa_node_dirs) + numa_info['nics'] = get_nodes_nics_info(nic_device_path) + except errors.IncompatibleNumaFormatError as exc: + LOG.warning('Failed to get some NUMA information (%s)', exc) + return + data['numa_topology'] = numa_info diff --git a/ironic_python_agent/tests/unit/test_numa_inspector.py b/ironic_python_agent/tests/unit/test_numa_inspector.py new file mode 100644 index 000000000..501853f5d --- /dev/null +++ b/ironic_python_agent/tests/unit/test_numa_inspector.py @@ -0,0 +1,350 @@ +# Copyright 2017 Red Hat, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os + +import mock +from oslotest import base as test_base + +from ironic_python_agent import errors +from ironic_python_agent import numa_inspector as numa_insp +from ironic_python_agent import utils + + +class TestCollectNumaTopologyInfo(test_base.BaseTestCase): + def setUp(self): + super(TestCollectNumaTopologyInfo, self).setUp() + self.data = {} + self.failures = utils.AccumulatedFailures() + + @mock.patch.object(numa_insp, 'get_nodes_nics_info', autospec=True) + @mock.patch.object(numa_insp, 'get_nodes_cores_info', autospec=True) + @mock.patch.object(numa_insp, 'get_nodes_memory_info', autospec=True) + @mock.patch.object(os.path, 'isdir', autospec=True) + @mock.patch.object(os, 'listdir', autospec=True) + def test_collect_success(self, mock_listdir, mock_isdir, mock_memory_info, + mock_cores_info, mock_nics_info): + numa_node_dirs = ['node0', 'node1'] + mock_listdir.return_value = numa_node_dirs + mock_isdir.return_value = True + mock_memory_info.return_value = [{'numa_node': 0, 'size_kb': 1560000}, + {'numa_node': 1, 'size_kb': 1200000}] + mock_cores_info.return_value = [{'cpu': 0, 'numa_node': 0, + 'thread_siblings': [0, 1, 2, 3]}, + {'cpu': 1, 'numa_node': 0, + 'thread_siblings': [4, 5, 6]}, + {'cpu': 0, 'numa_node': 1, + 'thread_siblings': [16, 17]}, + {'cpu': 1, 'numa_node': 1, + 'thread_siblings': [18, 19]}] + mock_nics_info.return_value = [{'name': 'enp0s01', 'numa_node': 0}, + {'name': 'enp0s02', 'numa_node': 1}] + expected_numa_info = {"ram": [{'numa_node': 0, 'size_kb': 1560000}, + {'numa_node': 1, 'size_kb': 1200000}], + "cpus": [{'cpu': 0, 'numa_node': 0, + 'thread_siblings': [0, 1, 2, 3]}, + {'cpu': 1, 'numa_node': 0, + 'thread_siblings': [4, 5, 6]}, + {'cpu': 0, 'numa_node': 1, + 'thread_siblings': [16, 17]}, + {'cpu': 1, 'numa_node': 1, + 'thread_siblings': [18, 19]}], + "nics": [{'name': 'enp0s01', 'numa_node': 0}, + {'name': 'enp0s02', 'numa_node': 1}]} + mock_open = mock.mock_open() + with mock.patch('six.moves.builtins.open', mock_open): + numa_insp.collect_numa_topology_info(self.data, self.failures) + self.assertEqual(expected_numa_info, self.data["numa_topology"]) + + @mock.patch.object(os.path, 'isdir', autospec=True) + def test_collect_no_numa_dirs(self, mock_isdir): + mock_isdir.return_value = False + numa_insp.collect_numa_topology_info(self.data, self.failures) + self.assertNotIn("numa_topology", self.data) + + @mock.patch.object(numa_insp, 'get_nodes_nics_info', autospec=True) + @mock.patch.object(numa_insp, 'get_nodes_cores_info', autospec=True) + @mock.patch.object(numa_insp, 'get_nodes_memory_info', autospec=True) + @mock.patch.object(os.path, 'isdir', autospec=True) + @mock.patch.object(os, 'listdir', autospec=True) + def test_collect_no_nics_dirs(self, mock_listdir, mock_isdir, + mock_memory_info, mock_cores_info, + mock_nics_info): + numa_node_dirs = ['node0', 'node1'] + mock_listdir.return_value = numa_node_dirs + mock_isdir.return_value = True + mock_memory_info.return_value = [{'numa_node': 0, 'size_kb': 1560000}, + {'numa_node': 1, 'size_kb': 1200000}] + mock_cores_info.return_value = [{'cpu': 0, 'numa_node': 0, + 'thread_siblings': [0, 1, 2, 3]}, + {'cpu': 1, 'numa_node': 1, + 'thread_siblings': [4, 5, 6]}] + mock_nics_info.side_effect = errors.IncompatibleNumaFormatError("") + mock_open = mock.mock_open() + with mock.patch('six.moves.builtins.open', mock_open): + numa_insp.collect_numa_topology_info(self.data, self.failures) + self.assertNotIn("numa_topology", self.data) + + @mock.patch.object(numa_insp, 'get_nodes_nics_info', autospec=True) + @mock.patch.object(numa_insp, 'get_nodes_cores_info', autospec=True) + @mock.patch.object(numa_insp, 'get_nodes_memory_info', autospec=True) + @mock.patch.object(os.path, 'isdir', autospec=True) + @mock.patch.object(os, 'listdir', autospec=True) + def test_collect_failure(self, mock_listdir, mock_isdir, mock_memory_info, + mock_cores_info, mock_nics_info): + numa_node_dirs = ['node0', 'node1'] + mock_listdir.return_value = numa_node_dirs + mock_isdir.return_value = True + mock_memory_info.side_effect = errors.IncompatibleNumaFormatError("") + mock_cores_info.side_effect = errors.IncompatibleNumaFormatError("") + mock_nics_info.side_effect = errors.IncompatibleNumaFormatError("") + numa_insp.collect_numa_topology_info(self.data, self.failures) + self.assertNotIn("numa_topology", self.data) + self.assertFalse(self.failures) + + +class TestGetNumaTopologyInfo(test_base.BaseTestCase): + def setUp(self): + super(TestGetNumaTopologyInfo, self).setUp() + self.data = {} + self.failures = utils.AccumulatedFailures() + + def test_get_numa_node_id_valid_format(self): + numa_node_dir = '/sys/devices/system/node/node0' + expected_numa_node_id = 0 + numa_node_id = numa_insp.get_numa_node_id(numa_node_dir) + self.assertEqual(expected_numa_node_id, numa_node_id) + + def test_get_numa_node_id_invalid_format(self): + self.assertRaises(errors.IncompatibleNumaFormatError, + numa_insp.get_numa_node_id, + '/sys/devices/system/node/node-*0') + self.assertRaises(errors.IncompatibleNumaFormatError, + numa_insp.get_numa_node_id, + '/sys/devices/system/node/nod') + self.assertRaises(errors.IncompatibleNumaFormatError, + numa_insp.get_numa_node_id, + '') + + @mock.patch.object(numa_insp, 'get_numa_node_id', autospec=True) + def test_get_nodes_memory_info(self, mock_node_id): + numa_node_dirs = ['/sys/devices/system/node/node0', + '/sys/devices/system/node/node1'] + mock_node_id.side_effect = [0, 1] + reads = [['Node 0 Ignored Line', + 'Node 0 MemTotal: 1560000 kB'], + ['Node 1 MemTotal: 1200000 kB', + 'Node 1 Ignored Line']] + expected_meminfo = [{'numa_node': 0, 'size_kb': 1560000}, + {'numa_node': 1, 'size_kb': 1200000}] + mock_open = mock.mock_open() + with mock.patch('six.moves.builtins.open', mock_open): + mock_meminfo_file = mock.MagicMock() + mock_meminfo_file.__enter__.side_effect = reads + mock_open.return_value = mock_meminfo_file + ram = numa_insp.get_nodes_memory_info(numa_node_dirs) + self.assertListEqual(expected_meminfo, ram) + + @mock.patch.object(numa_insp, 'get_numa_node_id', autospec=True) + def test_bad_nodes_memory_info(self, mock_node_id): + numa_node_dirs = ['/sys/devices/system/node/node0', + '/sys/devices/system/node/node1'] + mock_node_id.side_effect = [0, 1] + reads = [['Node 0 MemTotal: 1560000 kB'], IOError] + mock_open = mock.mock_open() + with mock.patch('six.moves.builtins.open', mock_open): + mock_meminfo_file = mock.MagicMock() + mock_meminfo_file.__enter__.side_effect = reads + mock_open.return_value = mock_meminfo_file + self.assertRaises(errors.IncompatibleNumaFormatError, + numa_insp.get_nodes_memory_info, + numa_node_dirs) + + @mock.patch.object(numa_insp, 'get_numa_node_id', autospec=True) + def test_nodes_invalid_numa_format_memory_info(self, mock_node_id): + numa_node_dirs = ['/sys/devices/system/node/node0', + '/sys/devices/system/node/node1'] + mock_node_id.side_effect = [0, 1] + reads = [['Node 0: MemTotal: 1560000 kB'], + ['Node 1 MemTotal: 1200000 kB']] + mock_open = mock.mock_open() + with mock.patch('six.moves.builtins.open', mock_open): + mock_meminfo_file = mock.MagicMock() + mock_meminfo_file.__enter__.side_effect = reads + mock_open.return_value = mock_meminfo_file + self.assertRaises(errors.IncompatibleNumaFormatError, + numa_insp.get_nodes_memory_info, + numa_node_dirs) + + @mock.patch.object(numa_insp, 'get_numa_node_id', autospec=True) + def test_nodes_invalid_memory_unit(self, mock_node_id): + numa_node_dirs = ['/sys/devices/system/node/node0', + '/sys/devices/system/node/node1'] + mock_node_id.side_effect = [0, 1] + reads = [['Node 0 MemTotal: 1560000 TB'], + ['Node 1 MemTotal: 1200000 kB']] + mock_open = mock.mock_open() + with mock.patch('six.moves.builtins.open', mock_open): + mock_meminfo_file = mock.MagicMock() + mock_meminfo_file.__enter__.side_effect = reads + mock_open.return_value = mock_meminfo_file + self.assertRaises(errors.IncompatibleNumaFormatError, + numa_insp.get_nodes_memory_info, + numa_node_dirs) + + @mock.patch.object(numa_insp, 'get_numa_node_id', autospec=True) + def test_get_numa_node_id_invalid_format_memory_info(self, + mock_node_id): + numa_node_dirs = ['/sys/devices/system/node/node-*0', + '/sys/devices/system/node/node1'] + mock_node_id.side_effect = errors.IncompatibleNumaFormatError + self.assertRaises(errors.IncompatibleNumaFormatError, + numa_insp.get_nodes_memory_info, + numa_node_dirs) + + @mock.patch.object(os.path, 'isdir', autospec=True) + @mock.patch.object(os, 'listdir', autospec=True) + @mock.patch.object(numa_insp, 'get_numa_node_id', autospec=True) + def test_get_nodes_cores_info(self, mock_node_id, + mock_listdir, mock_isdir): + numa_node_dirs = ['/sys/devices/system/node/node0', + '/sys/devices/system/node/node1'] + mock_node_id.side_effect = [0, 1] + mock_listdir.side_effect = [['cpu0', 'cpu1', 'cpu2', + 'cpu3', 'cpu4'], + ['cpu5', 'cpu6', 'cpu7']] + mock_isdir.return_value = True + reads = ['0', '0', '1', '1', '1', '0', '0', '0'] + expected_cores_info = [{'cpu': 0, 'numa_node': 0, + 'thread_siblings': [0, 1]}, + {'cpu': 1, 'numa_node': 0, + 'thread_siblings': [2, 3, 4]}, + {'cpu': 0, 'numa_node': 1, + 'thread_siblings': [5, 6, 7]}] + mock_open = mock.mock_open() + with mock.patch('six.moves.builtins.open', mock_open): + mock_core_id_file = mock_open.return_value.read + mock_core_id_file.side_effect = reads + cpus = numa_insp.get_nodes_cores_info(numa_node_dirs) + self.assertEqual(len(cpus), len(expected_cores_info)) + for cpu in cpus: + self.assertIn(cpu, expected_cores_info) + + @mock.patch.object(os.path, 'isdir', autospec=True) + @mock.patch.object(os, 'listdir', autospec=True) + @mock.patch.object(numa_insp, 'get_numa_node_id', autospec=True) + def test_bad_nodes_cores_info(self, mock_node_id, + mock_listdir, mock_isdir): + numa_node_dirs = ['/sys/devices/system/node/node0'] + mock_node_id.return_value = 0 + thread_dirs = ['cpu0', 'cpu1', 'cpu2', 'cpu3', 'cpu4', 'cpu5'] + mock_listdir.return_value = thread_dirs + mock_isdir.return_value = True + reads = ['0', '0', '1', '1', '1', IOError] + mock_open = mock.mock_open() + with mock.patch('six.moves.builtins.open', mock_open): + mock_core_id_file = mock_open.return_value.read + mock_core_id_file.side_effect = reads + self.assertRaises(errors.IncompatibleNumaFormatError, + numa_insp.get_nodes_cores_info, + numa_node_dirs) + + @mock.patch.object(numa_insp, 'get_numa_node_id', autospec=True) + def test_get_numa_node_id_invalid_format_cores_info(self, + mock_node_id): + numa_node_dirs = ['/sys/devices/system/node/nodeid0'] + mock_node_id.side_effect = errors.IncompatibleNumaFormatError + self.assertRaises(errors.IncompatibleNumaFormatError, + numa_insp.get_nodes_cores_info, + numa_node_dirs) + + @mock.patch.object(os.path, 'isdir', autospec=True) + @mock.patch.object(os, 'listdir', autospec=True) + @mock.patch.object(numa_insp, 'get_numa_node_id', autospec=True) + def test_nodes_invalid_threaddir_format_cores_info(self, mock_node_id, + mock_listdir, + mock_isdir): + numa_node_dirs = ['/sys/devices/system/node/node0'] + mock_node_id.return_value = 0 + thread_dirs = ['cpuid0', 'cpu1', 'cpu2', 'cpu3', 'cpu4', 'cpu5'] + mock_listdir.return_value = thread_dirs + mock_isdir.return_value = True + reads = ['0', '0', '1', '1', '1', '2'] + mock_open = mock.mock_open() + with mock.patch('six.moves.builtins.open', mock_open): + mock_core_id_file = mock_open.return_value.read + mock_core_id_file.side_effect = reads + self.assertRaises(errors.IncompatibleNumaFormatError, + numa_insp.get_nodes_cores_info, + numa_node_dirs) + + @mock.patch.object(os.path, 'isdir', autospec=True) + @mock.patch.object(os, 'listdir', autospec=True) + @mock.patch.object(numa_insp, 'get_numa_node_id', autospec=True) + def test_bad_nodes_thread_dirs(self, mock_node_id, + mock_listdir, mock_isdir): + numa_node_dirs = ['/sys/devices/system/node/node0'] + mock_node_id.return_value = 0 + mock_listdir.side_effect = errors.IncompatibleNumaFormatError("") + mock_isdir.return_value = True + self.assertRaises(errors.IncompatibleNumaFormatError, + numa_insp.get_nodes_cores_info, + numa_node_dirs) + + @mock.patch.object(os.path, 'isdir', autospec=True) + @mock.patch.object(os, 'listdir', autospec=True) + def test_get_nodes_nics_info(self, mock_listdir, mock_isdir): + nic_dirs = ['enp0s01', 'enp0s02'] + mock_listdir.return_value = nic_dirs + mock_isdir.return_value = True + reads = ['0', '1'] + expected_nicsinfo = [{'name': 'enp0s01', 'numa_node': 0}, + {'name': 'enp0s02', 'numa_node': 1}] + mock_open = mock.mock_open() + with mock.patch('six.moves.builtins.open', mock_open): + mock_nicsinfo_file = mock_open.return_value.read + mock_nicsinfo_file.side_effect = reads + nics = numa_insp.get_nodes_nics_info('/sys/class/net/') + self.assertListEqual(expected_nicsinfo, nics) + + @mock.patch.object(os.path, 'isdir', autospec=True) + @mock.patch.object(os, 'listdir', autospec=True) + def test_bad_nodes_nics_info(self, mock_listdir, mock_isdir): + nic_dirs = ['enp0s01', 'enp0s02'] + mock_listdir.return_value = nic_dirs + mock_isdir.return_value = True + reads = ['0', IOError] + mock_open = mock.mock_open() + with mock.patch('six.moves.builtins.open', mock_open): + mock_nicsinfo_file = mock_open.return_value.read + mock_nicsinfo_file.side_effect = reads + self.assertRaises(errors.IncompatibleNumaFormatError, + numa_insp.get_nodes_nics_info, + '/sys/class/net/') + + @mock.patch.object(os, 'listdir', autospec=True) + @mock.patch.object(os.path, 'isdir', autospec=True) + def test_no_nics_dir(self, mock_isdir, mock_listdir): + mock_isdir.return_value = False + nic_dirs = ['enp0s01', 'enp0s02'] + mock_listdir.return_value = nic_dirs + reads = ['0', '1'] + mock_open = mock.mock_open() + with mock.patch('six.moves.builtins.open', mock_open): + mock_nicsinfo_file = mock_open.return_value.read + mock_nicsinfo_file.side_effect = reads + self.assertRaises(errors.IncompatibleNumaFormatError, + numa_insp.get_nodes_nics_info, + '/sys/class/net/') diff --git a/releasenotes/notes/add-numa-topology-info-8c253fd9e56169f1.yaml b/releasenotes/notes/add-numa-topology-info-8c253fd9e56169f1.yaml new file mode 100644 index 000000000..7f3b155c4 --- /dev/null +++ b/releasenotes/notes/add-numa-topology-info-8c253fd9e56169f1.yaml @@ -0,0 +1,4 @@ +--- +features: + - Adds new optional NUMA topology collector named ``numa_topology``. + It collects NUMA specific RAM, CPU and NIC information. diff --git a/setup.cfg b/setup.cfg index 102433100..7c41fc9e6 100644 --- a/setup.cfg +++ b/setup.cfg @@ -40,6 +40,7 @@ ironic_python_agent.inspector.collectors = logs = ironic_python_agent.inspector:collect_logs extra-hardware = ironic_python_agent.inspector:collect_extra_hardware pci-devices = ironic_python_agent.inspector:collect_pci_devices_info + numa-topology = ironic_python_agent.numa_inspector:collect_numa_topology_info [pbr] autodoc_index_modules = True