summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--examples/business-logic/example_business_logic.py7
-rw-r--r--ironic_python_agent/agent.py11
-rw-r--r--ironic_python_agent/extensions/image.py5
-rw-r--r--ironic_python_agent/extensions/standby.py34
-rw-r--r--ironic_python_agent/hardware.py72
-rw-r--r--ironic_python_agent/hardware_managers/cna.py7
-rw-r--r--ironic_python_agent/tests/unit/hardware_managers/test_cna.py4
-rw-r--r--ironic_python_agent/tests/unit/test_hardware.py63
-rw-r--r--ironic_python_agent/utils.py3
-rw-r--r--lower-constraints.txt26
-rw-r--r--releasenotes/notes/bmc-mac-introspection-e4c2e203d8529710.yaml6
-rw-r--r--tox.ini10
-rw-r--r--zuul.d/project.yaml5
13 files changed, 214 insertions, 39 deletions
diff --git a/examples/business-logic/example_business_logic.py b/examples/business-logic/example_business_logic.py
index 4fb7fc32..eac88a59 100644
--- a/examples/business-logic/example_business_logic.py
+++ b/examples/business-logic/example_business_logic.py
@@ -190,5 +190,8 @@ class ExampleBusinessLogicHardwareManager(hardware.HardwareManager):
# Make sure to provide default values for optional arguments.
def companyx_apply_something(self, node, ports, required_value,
optional_value=None):
- LOG.info('apply_something called with required_value={} and '
- 'optional_value={}'.format(required_value, optional_value))
+ LOG.info('apply_something called with '
+ 'required_value=%(required_value)s and '
+ 'optional_value=%(optional_value)s',
+ {'required_value': required_value,
+ 'optional_value': optional_value})
diff --git a/ironic_python_agent/agent.py b/ironic_python_agent/agent.py
index a9f8077f..c5b24560 100644
--- a/ironic_python_agent/agent.py
+++ b/ironic_python_agent/agent.py
@@ -125,17 +125,16 @@ class IronicPythonAgentHeartbeater(threading.Thread):
self.heartbeat_forced = False
self.previous_heartbeat = _time()
except errors.HeartbeatConflictError:
- LOG.warning('conflict error sending heartbeat to {}'.format(
- self.agent.api_url))
+ LOG.warning('conflict error sending heartbeat to %s',
+ self.agent.api_url)
except Exception:
- LOG.exception('error sending heartbeat to {}'.format(
- self.agent.api_url))
+ LOG.exception('error sending heartbeat to %s', self.agent.api_url)
finally:
interval_multiplier = random.uniform(self.min_jitter_multiplier,
self.max_jitter_multiplier)
self.interval = self.agent.heartbeat_timeout * interval_multiplier
- log_msg = 'sleeping before next heartbeat, interval: {0}'
- LOG.info(log_msg.format(self.interval))
+ LOG.info('sleeping before next heartbeat, interval: %s',
+ self.interval)
def force_heartbeat(self):
self.heartbeat_forced = True
diff --git a/ironic_python_agent/extensions/image.py b/ironic_python_agent/extensions/image.py
index b38742b3..1d439923 100644
--- a/ironic_python_agent/extensions/image.py
+++ b/ironic_python_agent/extensions/image.py
@@ -489,8 +489,9 @@ def _prepare_boot_partitions_for_softraid(device, holders, efi_part,
# RAID the ESPs, metadata=1.0 is mandatory to be able to boot
md_device = '/dev/md/esp'
- LOG.debug("Creating md device {} for the ESPs on {}".format(
- md_device, efi_partitions))
+ LOG.debug("Creating md device %(md_device)s for the ESPs "
+ "on %(efi_partitions)s",
+ {'md_device': md_device, 'efi_partitions': efi_partitions})
utils.execute('mdadm', '--create', md_device, '--force',
'--run', '--metadata=1.0', '--level', '1',
'--raid-devices', len(efi_partitions),
diff --git a/ironic_python_agent/extensions/standby.py b/ironic_python_agent/extensions/standby.py
index 970cf7de..e63a9572 100644
--- a/ironic_python_agent/extensions/standby.py
+++ b/ironic_python_agent/extensions/standby.py
@@ -202,7 +202,7 @@ def _write_whole_disk_image(image, image_info, device):
command = ['qemu-img', 'convert',
'-t', 'directsync', '-O', 'host_device', '-W',
image, device]
- LOG.info('Writing image with command: {}'.format(' '.join(command)))
+ LOG.info('Writing image with command: %s', ' '.join(command))
try:
disk_utils.convert_image(image, device, out_format='host_device',
cache='directsync', out_of_order=True)
@@ -232,8 +232,9 @@ def _write_image(image_info, device, configdrive=None):
else:
_write_whole_disk_image(image, image_info, device)
totaltime = time.time() - starttime
- LOG.info('Image {} written to device {} in {} seconds'.format(
- image, device, totaltime))
+ LOG.info('Image %(image)s written to device %(device)s in %(totaltime)s '
+ 'seconds', {'image': image, 'device': device,
+ 'totaltime': totaltime})
try:
disk_utils.fix_gpt_partition(device, node_uuid=None)
except exception.InstanceDeployFailure:
@@ -329,7 +330,7 @@ class ImageDownload(object):
details = []
for url in image_info['urls']:
try:
- LOG.info("Attempting to download image from {}".format(url))
+ LOG.info("Attempting to download image from %s", url)
self._request = _download_with_proxy(image_info, url,
image_info['id'])
except errors.ImageDownloadError as e:
@@ -386,12 +387,16 @@ class ImageDownload(object):
not match the checksum as reported by glance in image_info.
"""
checksum = self._hash_algo.hexdigest()
- LOG.debug('Verifying image at {} against {} checksum '
- '{}'.format(image_location, self._hash_algo.name, checksum))
+ LOG.debug('Verifying image at %(image_location)s against '
+ '%(algo_name)s checksum %(checksum)s',
+ {'image_location': image_location,
+ 'algo_name': self._hash_algo.name,
+ 'checksum': checksum})
if checksum != self._expected_hash_value:
- LOG.error(errors.ImageChecksumError.details_str.format(
+ error_msg = errors.ImageChecksumError.details_str.format(
image_location, self._image_info['id'],
- self._expected_hash_value, checksum))
+ self._expected_hash_value, checksum)
+ LOG.error(error_msg)
raise errors.ImageChecksumError(image_location,
self._image_info['id'],
self._expected_hash_value,
@@ -434,8 +439,10 @@ def _download_image(image_info):
break
totaltime = time.time() - starttime
- LOG.info("Image downloaded from {} in {} seconds".format(image_location,
- totaltime))
+ LOG.info("Image downloaded from %(image_location)s "
+ "in %(totaltime)s seconds",
+ {'image_location': image_location,
+ 'totaltime': totaltime})
image_download.verify_image(image_location)
@@ -597,8 +604,8 @@ class StandbyExtension(base.BaseAgentExtension):
break
totaltime = time.time() - starttime
- LOG.info("Image streamed onto device {} in {} "
- "seconds".format(device, totaltime))
+ LOG.info("Image streamed onto device %(device)s in %(totaltime)s "
+ "seconds", {'device': device, 'totaltime': totaltime})
# Verify if the checksum of the streamed image is correct
image_download.verify_image(device)
# Fix any gpt partition
@@ -609,7 +616,8 @@ class StandbyExtension(base.BaseAgentExtension):
pass
# Fix the root partition UUID
root_uuid = disk_utils.block_uuid(device)
- LOG.info("{} UUID is now {}".format(device, root_uuid))
+ LOG.info("%(device)s UUID is now %(root_uuid)s",
+ {'device': device, 'root_uuid': root_uuid})
self.partition_uuids['root uuid'] = root_uuid
def _fix_up_partition_uuids(self, image_info, device):
diff --git a/ironic_python_agent/hardware.py b/ironic_python_agent/hardware.py
index 04426419..47108405 100644
--- a/ironic_python_agent/hardware.py
+++ b/ironic_python_agent/hardware.py
@@ -445,13 +445,13 @@ def list_all_block_devices(block_type='disk',
# lvm, part, rom, loop
if devtype != block_type:
if devtype is None or ignore_raid:
- LOG.debug("Skipping: {!r}".format(line))
+ LOG.debug("Skipping: %s", line)
continue
elif ('raid' in devtype
and block_type in ['raid', 'disk']):
LOG.debug(
"TYPE detected to contain 'raid', signifying a "
- "RAID volume. Found: {!r}".format(line))
+ "RAID volume. Found: %s", line)
elif (devtype == 'md'
and (block_type == 'part'
or block_type == 'md')):
@@ -461,11 +461,11 @@ def list_all_block_devices(block_type='disk',
# more detail.
LOG.debug(
"TYPE detected to contain 'md', signifying a "
- "RAID partition. Found: {!r}".format(line))
+ "RAID partition. Found: %s", line)
else:
LOG.debug(
- "TYPE did not match. Wanted: {!r} but found: {!r}".format(
- block_type, line))
+ "TYPE did not match. Wanted: %(block_type)s but found: "
+ "%(line)s", {'block_type': block_type, 'line': line})
continue
# Ensure all required columns are at least present, even if blank
@@ -704,6 +704,9 @@ class HardwareManager(object, metaclass=abc.ABCMeta):
def get_bmc_address(self):
raise errors.IncompatibleHardwareMethodError()
+ def get_bmc_mac(self):
+ raise errors.IncompatibleHardwareMethodError()
+
def get_bmc_v6address(self):
raise errors.IncompatibleHardwareMethodError()
@@ -829,6 +832,14 @@ class HardwareManager(object, metaclass=abc.ABCMeta):
hardware_info['system_vendor'] = self.get_system_vendor_info()
hardware_info['boot'] = self.get_boot_info()
hardware_info['hostname'] = netutils.get_hostname()
+
+ try:
+ hardware_info['bmc_mac'] = self.get_bmc_mac()
+ except errors.IncompatibleHardwareMethodError:
+ # if the hardware manager does not support obtaining the BMC MAC,
+ # we simply don't expose it.
+ pass
+
LOG.info('Inventory collected in %.2f second(s)', time.time() - start)
return hardware_info
@@ -1790,6 +1801,57 @@ class GenericHardwareManager(HardwareManager):
return '0.0.0.0'
+ def get_bmc_mac(self):
+ """Attempt to detect BMC MAC address
+
+ :return: MAC address of the first LAN channel or 00:00:00:00:00:00 in
+ case none of them has one or is configured properly
+ """
+ # These modules are rarely loaded automatically
+ il_utils.try_execute('modprobe', 'ipmi_msghandler')
+ il_utils.try_execute('modprobe', 'ipmi_devintf')
+ il_utils.try_execute('modprobe', 'ipmi_si')
+
+ try:
+ # From all the channels 0-15, only 1-11 can be assigned to
+ # different types of communication media and protocols and
+ # effectively used
+ for channel in range(1, 12):
+ out, e = utils.execute(
+ "ipmitool lan print {} | awk '/(IP|MAC) Address[ \\t]*:/"
+ " {{print $4}}'".format(channel), shell=True)
+ if e.startswith("Invalid channel"):
+ continue
+
+ try:
+ ip, mac = out.strip().split("\n")
+ except ValueError:
+ LOG.warning('Invalid ipmitool output %(output)s',
+ {'output': out})
+ continue
+
+ if ip == "0.0.0.0":
+ # disabled, ignore
+ continue
+
+ if not re.match("^[0-9a-f]{2}(:[0-9a-f]{2}){5}$", mac, re.I):
+ LOG.warning('Invalid MAC address %(output)s',
+ {'output': mac})
+ continue
+
+ # In case we get 00:00:00:00:00:00 on a valid channel, we need
+ # to keep querying
+ if mac != '00:00:00:00:00:00':
+ return mac
+
+ except (processutils.ProcessExecutionError, OSError) as e:
+ # Not error, because it's normal in virtual environment
+ LOG.warning("Cannot get BMC MAC address: %s", e)
+ return
+
+ # no valid mac found, signal this clearly
+ raise errors.IncompatibleHardwareMethodError()
+
def get_bmc_v6address(self):
"""Attempt to detect BMC v6 address
diff --git a/ironic_python_agent/hardware_managers/cna.py b/ironic_python_agent/hardware_managers/cna.py
index 2d49d9e0..a4021c10 100644
--- a/ironic_python_agent/hardware_managers/cna.py
+++ b/ironic_python_agent/hardware_managers/cna.py
@@ -64,9 +64,10 @@ def _disable_embedded_lldp_agent_in_cna_card():
failed_dirs.append(inner_dir)
continue
if failed_dirs:
- LOG.warning('Failed to disable the embedded LLDP on Intel CNA network '
- 'card. Addresses of failed pci devices: {}'
- .format(str(failed_dirs).strip('[]')))
+ warning_msg = ('Failed to disable the embedded LLDP on Intel '
+ 'CNA network card. Addresses of failed pci '
+ 'devices: %s', str(failed_dirs).strip('[]'))
+ LOG.warning(warning_msg)
class IntelCnaHardwareManager(hardware.HardwareManager):
diff --git a/ironic_python_agent/tests/unit/hardware_managers/test_cna.py b/ironic_python_agent/tests/unit/hardware_managers/test_cna.py
index b38d6d20..29c456a7 100644
--- a/ironic_python_agent/tests/unit/hardware_managers/test_cna.py
+++ b/ironic_python_agent/tests/unit/hardware_managers/test_cna.py
@@ -112,8 +112,8 @@ class TestIntelCnaHardwareManager(base.IronicAgentTest):
cna._disable_embedded_lldp_agent_in_cna_card()
expected_log_message = ('Failed to disable the embedded LLDP on '
'Intel CNA network card. Addresses of '
- 'failed pci devices: {}'
- .format(str(listdir_dict).strip('[]')))
+ 'failed pci devices: %s',
+ str(listdir_dict).strip('[]'))
mock_log.warning.assert_called_once_with(expected_log_message)
@mock.patch.object(cna, 'LOG', autospec=True)
diff --git a/ironic_python_agent/tests/unit/test_hardware.py b/ironic_python_agent/tests/unit/test_hardware.py
index 61001260..44b2a87d 100644
--- a/ironic_python_agent/tests/unit/test_hardware.py
+++ b/ironic_python_agent/tests/unit/test_hardware.py
@@ -1165,6 +1165,7 @@ class TestGenericHardwareManager(base.IronicAgentTest):
current_boot_mode='bios', pxe_interface='boot:if')
self.hardware.get_bmc_address = mock.Mock()
+ self.hardware.get_bmc_mac = mock.Mock()
self.hardware.get_bmc_v6address = mock.Mock()
self.hardware.get_system_vendor_info = mock.Mock()
@@ -2302,6 +2303,68 @@ class TestGenericHardwareManager(base.IronicAgentTest):
@mock.patch.object(il_utils, 'try_execute', autospec=True)
@mock.patch.object(utils, 'execute', autospec=True)
+ def test_get_bmc_mac_not_available(self, mocked_execute, mte):
+ mocked_execute.return_value = '', ''
+ self.assertRaises(errors.IncompatibleHardwareMethodError,
+ self.hardware.get_bmc_mac)
+
+ @mock.patch.object(il_utils, 'try_execute', autospec=True)
+ @mock.patch.object(utils, 'execute', autospec=True)
+ def test_get_bmc_mac(self, mocked_execute, mte):
+ mocked_execute.return_value = '192.1.2.3\n01:02:03:04:05:06', ''
+ self.assertEqual('01:02:03:04:05:06', self.hardware.get_bmc_mac())
+
+ @mock.patch.object(il_utils, 'try_execute', autospec=True)
+ @mock.patch.object(utils, 'execute', autospec=True)
+ def test_get_bmc_mac_virt(self, mocked_execute, mte):
+ mocked_execute.side_effect = processutils.ProcessExecutionError()
+ self.assertIsNone(self.hardware.get_bmc_mac())
+
+ @mock.patch.object(il_utils, 'try_execute', autospec=True)
+ @mock.patch.object(utils, 'execute', autospec=True)
+ def test_get_bmc_mac_zeroed(self, mocked_execute, mte):
+ mocked_execute.return_value = '0.0.0.0\n00:00:00:00:00:00', ''
+ self.assertRaises(errors.IncompatibleHardwareMethodError,
+ self.hardware.get_bmc_mac)
+
+ @mock.patch.object(il_utils, 'try_execute', autospec=True)
+ @mock.patch.object(utils, 'execute', autospec=True)
+ def test_get_bmc_mac_invalid(self, mocked_execute, mte):
+ # In case of invalid lan channel, stdout is empty and the error
+ # on stderr is "Invalid channel"
+ mocked_execute.return_value = '\n', 'Invalid channel: 55'
+ self.assertRaises(errors.IncompatibleHardwareMethodError,
+ self.hardware.get_bmc_mac)
+
+ @mock.patch.object(il_utils, 'try_execute', autospec=True)
+ @mock.patch.object(utils, 'execute', autospec=True)
+ def test_get_bmc_mac_random_error(self, mocked_execute, mte):
+ mocked_execute.return_value = ('192.1.2.3\n00:00:00:00:00:02',
+ 'Random error message')
+ self.assertEqual('00:00:00:00:00:02', self.hardware.get_bmc_mac())
+
+ @mock.patch.object(il_utils, 'try_execute', autospec=True)
+ @mock.patch.object(utils, 'execute', autospec=True)
+ def test_get_bmc_mac_iterate_channels(self, mocked_execute, mte):
+ # For channel 1 we simulate unconfigured IP
+ # and for any other we return a correct IP address
+ def side_effect(*args, **kwargs):
+ if args[0].startswith("ipmitool lan print 1"):
+ return '', 'Invalid channel 1\n'
+ elif args[0].startswith("ipmitool lan print 2"):
+ return '0.0.0.0\n00:00:00:00:23:42', ''
+ elif args[0].startswith("ipmitool lan print 3"):
+ return 'meow', ''
+ elif args[0].startswith("ipmitool lan print 4"):
+ return '192.1.2.3\n01:02:03:04:05:06', ''
+ else:
+ # this should never happen because the previous one was good
+ raise AssertionError
+ mocked_execute.side_effect = side_effect
+ self.assertEqual('01:02:03:04:05:06', self.hardware.get_bmc_mac())
+
+ @mock.patch.object(il_utils, 'try_execute', autospec=True)
+ @mock.patch.object(utils, 'execute', autospec=True)
def test_get_bmc_v6address_not_enabled(self, mocked_execute, mte):
mocked_execute.side_effect = [('ipv4\n', '')] * 11
self.assertEqual('::/0', self.hardware.get_bmc_v6address())
diff --git a/ironic_python_agent/utils.py b/ironic_python_agent/utils.py
index 2fc60ff1..74ac328b 100644
--- a/ironic_python_agent/utils.py
+++ b/ironic_python_agent/utils.py
@@ -865,8 +865,7 @@ def create_partition_table(dev_name, partition_table_type):
:raises: CommandExecutionError if an error is encountered while
attempting to create the partition table.
"""
- LOG.info("Creating partition table on {}".format(
- dev_name))
+ LOG.info("Creating partition table on %s", dev_name)
try:
execute('parted', dev_name, '-s', '--',
'mklabel', partition_table_type)
diff --git a/lower-constraints.txt b/lower-constraints.txt
new file mode 100644
index 00000000..6e5fc9bc
--- /dev/null
+++ b/lower-constraints.txt
@@ -0,0 +1,26 @@
+Pint==0.5
+Werkzeug==1.0.1
+bandit==1.1.0
+coverage==4.0
+cryptography==2.3
+dogpile.cache==0.9.2
+eventlet==0.18.2
+importlib_metadata==1.7.0;python_version<'3.8'
+ironic-lib==4.7.1
+netifaces==0.10.4
+openstacksdk==0.49.0
+oslo.concurrency==3.26.0
+oslo.config==5.2.0
+oslo.log==3.36.0
+oslo.serialization==2.18.0
+oslo.service==1.24.0
+oslo.utils==3.33.0
+oslotest==3.2.0
+pbr==2.0.0
+psutil==3.2.2
+pyudev==0.18
+requests==2.14.2
+stestr==1.0.0
+stevedore==1.20.0
+tenacity==6.2.0
+testtools==2.2.0
diff --git a/releasenotes/notes/bmc-mac-introspection-e4c2e203d8529710.yaml b/releasenotes/notes/bmc-mac-introspection-e4c2e203d8529710.yaml
new file mode 100644
index 00000000..296cf15c
--- /dev/null
+++ b/releasenotes/notes/bmc-mac-introspection-e4c2e203d8529710.yaml
@@ -0,0 +1,6 @@
+---
+features:
+ - |
+ The introspection now includes the MAC address of the IPMI LAN channel
+ which has a valid IP address and MAC address assigned in the hardware
+ inventory data as ``bmc_mac``.
diff --git a/tox.ini b/tox.ini
index ede439ad..ccbe9fa5 100644
--- a/tox.ini
+++ b/tox.ini
@@ -35,10 +35,10 @@ commands = stestr run {posargs}
[testenv:pep8]
usedevelop = False
deps=
- hacking>=3.1.0,<4.0.0 # Apache-2.0
+ hacking>=4.1.0,<5.0.0 # Apache-2.0
bashate>=0.5.1 # Apache-2.0
flake8-import-order>=0.17.1 # LGPLv3
- pycodestyle>=2.0.0,<2.7.0 # MIT
+ pycodestyle>=2.0.0,<3.0.0 # MIT
doc8>=0.8.1 # Apache-2.0
allowlist_externals = bash
commands =
@@ -137,3 +137,9 @@ commands =
pip install -e {toxinidir}/examples/business-logic
pip install -e {toxinidir}/examples/vendor-device
python -c 'import example_business_logic; import example_device'
+
+[testenv:lower-constraints]
+deps =
+ -c{toxinidir}/lower-constraints.txt
+ -r{toxinidir}/test-requirements.txt
+ -r{toxinidir}/requirements.txt
diff --git a/zuul.d/project.yaml b/zuul.d/project.yaml
index ff5161d6..a995a8f3 100644
--- a/zuul.d/project.yaml
+++ b/zuul.d/project.yaml
@@ -1,9 +1,10 @@
- project:
templates:
- - openstack-python3-xena-jobs
+ - check-requirements
- openstack-cover-jobs
+ - openstack-lower-constraints-master-branch-jobs
+ - openstack-python3-xena-jobs
- publish-openstack-docs-pti
- - check-requirements
- release-notes-jobs-python3
check:
jobs: