diff --git a/ironic_python_agent/hardware.py b/ironic_python_agent/hardware.py index 5de626613..65c9421a9 100644 --- a/ironic_python_agent/hardware.py +++ b/ironic_python_agent/hardware.py @@ -18,6 +18,7 @@ import abc import functools import os +import psutil import six import stevedore @@ -45,26 +46,19 @@ class HardwareType(object): MAC_ADDRESS = 'mac_address' -class HardwareInfo(encoding.Serializable): - def __init__(self, type, id): - self.type = type - self.id = id +class BlockDevice(encoding.Serializable): + def __init__(self, name, size): + self.name = name + self.size = size def serialize(self): return utils.get_ordereddict([ - ('type', self.type), - ('id', self.id), + ('name', self.name), + ('size', self.size), ]) -class BlockDevice(object): - def __init__(self, name, size, start_sector): - self.name = name - self.size = size - self.start_sector = start_sector - - -class NetworkInterface(object): +class NetworkInterface(encoding.Serializable): def __init__(self, name, mac_addr): self.name = name self.mac_address = mac_addr @@ -72,6 +66,38 @@ class NetworkInterface(object): self.switch_port_descr = None self.switch_chassis_descr = None + def serialize(self): + return utils.get_ordereddict([ + ('name', self.name), + ('mac_address', self.mac_address), + ('switch_port_descr', self.switch_port_descr), + ('switch_chassis_descr', self.switch_port_descr), + ]) + + +class CPU(encoding.Serializable): + def __init__(self, model_name, frequency, count): + self.model_name = model_name + self.frequency = frequency + self.count = count + + def serialize(self): + return utils.get_ordereddict([ + ('model_name', self.model_name), + ('frequency', self.frequency), + ('count', self.count), + ]) + + +class Memory(encoding.Serializable): + def __init__(self, total): + self.total = total + + def serialize(self): + return utils.get_ordereddict([ + ('total', self.total), + ]) + class HardwareManager(object): @abc.abstractmethod @@ -87,10 +113,11 @@ class HardwareManager(object): pass def list_hardware_info(self): - hardware_info = [] - for interface in self.list_network_interfaces(): - hardware_info.append(HardwareInfo(HardwareType.MAC_ADDRESS, - interface.mac_address)) + hardware_info = {} + hardware_info['interfaces'] = self.list_network_interfaces() + hardware_info['cpu'] = self.get_cpus() + hardware_info['disks'] = self.list_block_devices() + hardware_info['memory'] = self.get_memory() return hardware_info @@ -123,7 +150,26 @@ class GenericHardwareManager(HardwareManager): for name in iface_names if self._is_device(name)] - def _list_block_devices(self): + def get_cpus(self): + model = None + freq = None + with open('/proc/cpuinfo') as f: + lines = f.read() + for line in lines.split('\n'): + if model and freq: + break + if not model and line.startswith('model name'): + model = line.split(':')[1].strip() + if not freq and line.startswith('cpu MHz'): + freq = line.split(':')[1].strip() + + return CPU(model, freq, psutil.cpu_count()) + + def get_memory(self): + # psutil returns a long, force it to an int + return Memory(int(psutil.phymem_usage().total)) + + def list_block_devices(self): report = utils.execute('blockdev', '--report', check_exit_code=[0])[0] lines = report.split('\n') @@ -131,20 +177,17 @@ class GenericHardwareManager(HardwareManager): startsec_idx = lines[0].index('StartSec') device_idx = lines[0].index('Device') size_idx = lines[0].index('Size') + # If a device doesn't start at sector 0, assume it is a partition return [BlockDevice(line[device_idx], - int(line[size_idx]), - int(line[startsec_idx])) + int(line[size_idx])) for line - in lines[1:]] + in lines[1:] if int(line[startsec_idx]) == 0] def get_os_install_device(self): - # Assume anything with a start sector other than 0, is a partition - block_devices = [device for device in self._list_block_devices() - if device.start_sector == 0] - # Find the first device larger than 4GB, assume it is the OS disk # TODO(russellhaering): This isn't a valid assumption in all cases, # is there a more reasonable default behavior? + block_devices = self.list_block_devices() block_devices.sort(key=lambda device: device.size) for device in block_devices: if device.size >= (4 * pow(1024, 3)): diff --git a/ironic_python_agent/ironic_api_client.py b/ironic_python_agent/ironic_api_client.py index 52a860210..e73c31f90 100644 --- a/ironic_python_agent/ironic_api_client.py +++ b/ironic_python_agent/ironic_api_client.py @@ -30,7 +30,7 @@ LOG = log.getLogger(__name__) class APIClient(object): api_version = 'v1' - payload_version = '1' + payload_version = '2' def __init__(self, api_url): self.api_url = api_url.rstrip('/') diff --git a/ironic_python_agent/tests/hardware.py b/ironic_python_agent/tests/hardware.py index 82540199c..43caf496e 100644 --- a/ironic_python_agent/tests/hardware.py +++ b/ironic_python_agent/tests/hardware.py @@ -65,16 +65,100 @@ class TestGenericHardwareManager(test_base.BaseTestCase): '--report', check_exit_code=[0]) - def test_list_hardwre_info(self): + @mock.patch('psutil.cpu_count') + @mock.patch(OPEN_FUNCTION_NAME) + def test_get_cpus(self, mocked_open, mocked_cpucount): + mocked_open.return_value.__enter__ = lambda s: s + mocked_open.return_value.__exit__ = mock.Mock() + read_mock = mocked_open.return_value.read + read_mock.return_value = ( + 'processor : 0\n' + 'vendor_id : GenuineIntel\n' + 'cpu family : 6\n' + 'model : 58\n' + 'model name : Intel(R) Core(TM) i7-3720QM CPU @ 2.60GHz\n' + 'stepping : 9\n' + 'microcode : 0x15\n' + 'cpu MHz : 2594.685\n' + 'cache size : 6144 KB\n' + 'fpu : yes\n' + 'fpu_exception : yes\n' + 'cpuid level : 13\n' + 'wp : yes\n' + 'flags : fpu vme de pse tsc msr pae mce cx8 apic sep ' + 'mtrr pge mca cmov pat pse36 clflush dts mmx fxsr sse sse2 ss ' + 'syscall nx rdtscp lm constant_tsc arch_perfmon pebs bts nopl ' + 'xtopology tsc_reliable nonstop_tsc aperfmperf eagerfpu pni ' + 'pclmulqdq ssse3 cx16 pcid sse4_1 sse4_2 x2apic popcnt aes xsave ' + 'avx f16c rdrand hypervisor lahf_lm ida arat epb xsaveopt pln pts ' + 'dtherm fsgsbase smep\n' + 'bogomips : 5189.37\n' + 'clflush size : 64\n' + 'cache_alignment : 64\n' + 'address sizes : 40 bits physical, 48 bits virtual\n' + 'power management:\n' + '\n' + 'processor : 1\n' + 'vendor_id : GenuineIntel\n' + 'cpu family : 6\n' + 'model : 58\n' + 'model name : Intel(R) Core(TM) i7-3720QM CPU @ 2.60GHz\n' + 'stepping : 9\n' + 'microcode : 0x15\n' + 'cpu MHz : 2594.685\n' + 'cache size : 6144 KB\n' + 'fpu : yes\n' + 'fpu_exception : yes\n' + 'cpuid level : 13\n' + 'wp : yes\n' + 'flags : fpu vme de pse tsc msr pae mce cx8 apic sep ' + 'mtrr pge mca cmov pat pse36 clflush dts mmx fxsr sse sse2 ss ' + 'syscall nx rdtscp lm constant_tsc arch_perfmon pebs bts nopl ' + 'xtopology tsc_reliable nonstop_tsc aperfmperf eagerfpu pni ' + 'pclmulqdq ssse3 cx16 pcid sse4_1 sse4_2 x2apic popcnt aes xsave ' + 'avx f16c rdrand hypervisor lahf_lm ida arat epb xsaveopt pln pts ' + 'dtherm fsgsbase smep\n' + 'bogomips : 5189.37\n' + 'clflush size : 64\n' + 'cache_alignment : 64\n' + 'address sizes : 40 bits physical, 48 bits virtual\n' + 'power management:\n' + ) + + mocked_cpucount.return_value = 2 + + cpus = self.hardware.get_cpus() + self.assertEqual(cpus.model_name, + 'Intel(R) Core(TM) i7-3720QM CPU @ 2.60GHz') + self.assertEqual(cpus.frequency, '2594.685') + self.assertEqual(cpus.count, 2) + + def test_list_hardware_info(self): self.hardware.list_network_interfaces = mock.Mock() self.hardware.list_network_interfaces.return_value = [ hardware.NetworkInterface('eth0', '00:0c:29:8c:11:b1'), hardware.NetworkInterface('eth1', '00:0c:29:8c:11:b2'), ] + self.hardware.get_cpus = mock.Mock() + self.hardware.get_cpus.return_value = hardware.CPU( + 'Awesome CPU x14 9001', + 9001, + 14) + + self.hardware.get_memory = mock.Mock() + self.hardware.get_memory.return_value = hardware.Memory(1017012) + + self.hardware.list_block_devices = mock.Mock() + self.hardware.list_block_devices.return_value = [ + hardware.BlockDevice('/dev/sdj', 1073741824), + hardware.BlockDevice('/dev/hdaa', 65535), + ] + hardware_info = self.hardware.list_hardware_info() - self.assertEqual(len(hardware_info), 2) - self.assertEqual(hardware_info[0].type, 'mac_address') - self.assertEqual(hardware_info[1].type, 'mac_address') - self.assertEqual(hardware_info[0].id, '00:0c:29:8c:11:b1') - self.assertEqual(hardware_info[1].id, '00:0c:29:8c:11:b2') + self.assertEqual(hardware_info['memory'], self.hardware.get_memory()) + self.assertEqual(hardware_info['cpu'], self.hardware.get_cpus()) + self.assertEqual(hardware_info['disks'], + self.hardware.list_block_devices()) + self.assertEqual(hardware_info['interfaces'], + self.hardware.list_network_interfaces()) diff --git a/ironic_python_agent/tests/ironic_api_client.py b/ironic_python_agent/tests/ironic_api_client.py index b0dd2b2df..eeca401e3 100644 --- a/ironic_python_agent/tests/ironic_api_client.py +++ b/ironic_python_agent/tests/ironic_api_client.py @@ -41,12 +41,18 @@ class TestBaseIronicPythonAgent(test_base.BaseTestCase): def setUp(self): super(TestBaseIronicPythonAgent, self).setUp() self.api_client = ironic_api_client.APIClient(API_URL) - self.hardware_info = [ - hardware.HardwareInfo(hardware.HardwareType.MAC_ADDRESS, - 'aa:bb:cc:dd:ee:ff'), - hardware.HardwareInfo(hardware.HardwareType.MAC_ADDRESS, - 'ff:ee:dd:cc:bb:aa'), - ] + self.hardware_info = { + 'interfaces': [ + hardware.NetworkInterface('eth0', '00:0c:29:8c:11:b1'), + hardware.NetworkInterface('eth1', '00:0c:29:8c:11:b2'), + ], + 'cpu': hardware.CPU('Awesome Jay CPU x10 9001', '9001', '10'), + 'disks': [ + hardware.BlockDevice('/dev/sdj', '9001'), + hardware.BlockDevice('/dev/hdj', '9002'), + ], + 'memory': hardware.Memory('8675309'), + } def test_successful_heartbeat(self): expected_heartbeat_before = time.time() + 120 @@ -65,7 +71,7 @@ class TestBaseIronicPythonAgent(test_base.BaseTestCase): self.assertEqual(heartbeat_before, expected_heartbeat_before) heartbeat_path = 'v1/nodes/deadbeef-dabb-ad00-b105-f00d00bab10c/' \ - 'vendor_passthru/heartbeat' + 'vendor_passthru/heartbeat' request_args = self.api_client.session.request.call_args[0] self.assertEqual(request_args[0], 'POST') self.assertEqual(request_args[1], API_URL + heartbeat_path) @@ -162,16 +168,40 @@ class TestBaseIronicPythonAgent(test_base.BaseTestCase): data = self.api_client.session.request.call_args[1]['data'] content = json.loads(data) self.assertEqual(content['version'], self.api_client.payload_version) - self.assertEqual(content['inventory'], [ - { - 'type': 'mac_address', - 'id': 'aa:bb:cc:dd:ee:ff', + self.assertEqual(content['inventory'], { + u'interfaces': [ + { + u'mac_address': u'00:0c:29:8c:11:b1', + u'name': u'eth0', + u'switch_chassis_descr': None, + u'switch_port_descr': None + }, + { + u'mac_address': u'00:0c:29:8c:11:b2', + u'name': u'eth1', + u'switch_chassis_descr': None, + 'switch_port_descr': None + } + ], + u'cpu': { + u'model_name': u'Awesome Jay CPU x10 9001', + u'frequency': u'9001', + u'count': u'10', }, - { - 'type': 'mac_address', - 'id': 'ff:ee:dd:cc:bb:aa', + u'disks': [ + { + u'name': u'/dev/sdj', + u'size': u'9001', + }, + { + u'name': u'/dev/hdj', + u'size': u'9002', + }, + ], + u'memory': { + u'total': u'8675309', }, - ]) + }) def test_do_lookup_bad_response_code(self): response = FakeResponse(status_code=400, content={ diff --git a/requirements.txt b/requirements.txt index 53281e62a..1b1fb88c3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -9,3 +9,4 @@ oslo.config>=1.2.0 Babel>=1.3 iso8601>=0.1.9 oslotest==1.0 +psutil>=1.1.1