Browse Source

Use openstacksdk for ironic module

This patches removes the ironic-client dependency from the
ironic module in favor of openstacksdk.

Increase minimum required version of openstacksdk to use
recent added features.

Change-Id: I31964179835ad454e8205a59f483b419de4fb62d
changes/79/672179/59
Riccardo Pittau 3 years ago
parent
commit
3accdfbbc6
  1. 93
      ironic_inspector/common/ironic.py
  2. 5
      ironic_inspector/conductor/manager.py
  3. 21
      ironic_inspector/introspect.py
  4. 40
      ironic_inspector/node_cache.py
  5. 6
      ironic_inspector/plugins/discovery.py
  6. 4
      ironic_inspector/plugins/rules.py
  7. 6
      ironic_inspector/plugins/standard.py
  8. 4
      ironic_inspector/process.py
  9. 7
      ironic_inspector/pxe_filter/base.py
  10. 2
      ironic_inspector/pxe_filter/dnsmasq.py
  11. 2
      ironic_inspector/pxe_filter/iptables.py
  12. 8
      ironic_inspector/test/base.py
  13. 117
      ironic_inspector/test/functional.py
  14. 106
      ironic_inspector/test/unit/test_common_ironic.py
  15. 16
      ironic_inspector/test/unit/test_dnsmasq_pxe_filter.py
  16. 166
      ironic_inspector/test/unit/test_introspect.py
  17. 16
      ironic_inspector/test/unit/test_iptables.py
  18. 29
      ironic_inspector/test/unit/test_manager.py
  19. 129
      ironic_inspector/test/unit/test_node_cache.py
  20. 20
      ironic_inspector/test/unit/test_plugins_discovery.py
  21. 14
      ironic_inspector/test/unit/test_plugins_rules.py
  22. 6
      ironic_inspector/test/unit/test_plugins_standard.py
  23. 92
      ironic_inspector/test/unit/test_process.py
  24. 2
      lower-constraints.txt
  25. 2
      requirements.txt

93
ironic_inspector/common/ironic.py

@ -13,9 +13,9 @@
import socket
from ironicclient import client
from ironicclient import exceptions as ironic_exc
import netaddr
import openstack
from openstack import exceptions as os_exc
from oslo_config import cfg
import retrying
@ -26,23 +26,14 @@ from ironic_inspector import utils
CONF = cfg.CONF
LOG = utils.getProcessingLogger(__name__)
# See https://docs.openstack.org/ironic/latest/contributor/states.html # noqa
# See https://docs.openstack.org/ironic/latest/contributor/states.html
VALID_STATES = frozenset(['enroll', 'manageable', 'inspecting', 'inspect wait',
'inspect failed'])
# States where an instance is deployed and an admin may be doing something.
VALID_ACTIVE_STATES = frozenset(['active', 'rescue'])
# 1.38 is the latest API version in the Queens release series, 10.1.0.
# 1.46 is the latest API version in the Rocky release series, 11.1.0.
# 1.56 is the latest API version in the Stein release series, 12.1.0
# NOTE(mgoddard): This should be updated with each release to ensure that
# inspector is able to use the latest ironic API. In particular, this version
# is used when processing introspection rules, and is the default version used
# by processing plugins.
DEFAULT_IRONIC_API_VERSION = ['1.38', '1.46', '1.56']
IRONIC_SESSION = None
_IRONIC_SESSION = None
class NotFound(utils.Error):
@ -53,13 +44,34 @@ class NotFound(utils.Error):
super(NotFound, self).__init__(msg, code, *args, **kwargs)
def _get_ironic_session():
global _IRONIC_SESSION
if not _IRONIC_SESSION:
_IRONIC_SESSION = keystone.get_session('ironic')
return _IRONIC_SESSION
def get_client(token=None):
"""Get an ironic client connection."""
session = _get_ironic_session()
try:
return openstack.connection.Connection(
session=session, oslo_conf=CONF).baremetal
except Exception as exc:
LOG.error('Failed to establish a connection with ironic, '
'reason: %s', exc)
raise
def reset_ironic_session():
"""Reset the global session variable.
Mostly useful for unit tests.
"""
global IRONIC_SESSION
IRONIC_SESSION = None
global _IRONIC_SESSION
_IRONIC_SESSION = None
def get_ipmi_address(node):
@ -111,33 +123,6 @@ def get_ipmi_address(node):
return none_address
def get_client(token=None,
api_version=DEFAULT_IRONIC_API_VERSION): # pragma: no cover
"""Get Ironic client instance."""
global IRONIC_SESSION
if not IRONIC_SESSION:
IRONIC_SESSION = keystone.get_session('ironic')
args = {
'session': IRONIC_SESSION,
'os_ironic_api_version': api_version,
'max_retries': CONF.ironic.max_retries,
'retry_interval': CONF.ironic.retry_interval
}
if token is not None:
args['token'] = token
endpoint = keystone.get_adapter('ironic',
session=IRONIC_SESSION).get_endpoint()
if not endpoint:
raise utils.Error(
_('Cannot find the bare metal endpoint either in Keystone or '
'in the configuration'), code=500)
return client.get_client(1, endpoint=endpoint, **args)
def check_provision_state(node):
"""Sanity checks the provision state of the node.
@ -188,16 +173,18 @@ def get_node(node_id, ironic=None, **kwargs):
ironic = ironic if ironic is not None else get_client()
try:
return ironic.node.get(node_id, **kwargs)
except ironic_exc.NotFound:
node = ironic.get_node(node_id, **kwargs)
node.uuid = node.id
except os_exc.ResourceNotFound:
raise NotFound(node_id)
except ironic_exc.HttpError as exc:
except os_exc.BadRequestException as exc:
raise utils.Error(_("Cannot get node %(node)s: %(exc)s") %
{'node': node_id, 'exc': exc})
return node
@retrying.retry(
retry_on_exception=lambda exc: isinstance(exc, ironic_exc.ClientException),
retry_on_exception=lambda exc: isinstance(exc, os_exc.SDKException),
stop_max_attempt_number=5, wait_fixed=1000)
def call_with_retries(func, *args, **kwargs):
"""Call an ironic client function retrying all errors.
@ -217,15 +204,16 @@ def lookup_node_by_macs(macs, introspection_data=None,
nodes = set()
for mac in macs:
ports = ironic.port.list(address=mac, fields=["uuid", "node_uuid"])
ports = ironic.ports(address=mac, fields=["uuid", "node_uuid"])
ports = list(ports)
if not ports:
continue
elif fail:
raise utils.Error(
_('Port %(mac)s already exists, uuid: %(uuid)s') %
{'mac': mac, 'uuid': ports[0].uuid}, data=introspection_data)
{'mac': mac, 'uuid': ports[0].id}, data=introspection_data)
else:
nodes.update(p.node_uuid for p in ports)
nodes.update(p.node_id for p in ports)
if len(nodes) > 1:
raise utils.Error(_('MAC addresses %(macs)s correspond to more than '
@ -233,6 +221,7 @@ def lookup_node_by_macs(macs, introspection_data=None,
{'macs': ', '.join(macs),
'nodes': ', '.join(nodes)},
data=introspection_data)
elif nodes:
return nodes.pop()
@ -245,7 +234,7 @@ def lookup_node_by_bmc_addresses(addresses, introspection_data=None,
# FIXME(aarefiev): it's not effective to fetch all nodes, and may
# impact on performance on big clusters
nodes = ironic.node.list(fields=('uuid', 'driver_info'), limit=0)
nodes = ironic.nodes(fields=('id', 'driver_info'), limit=None)
found = set()
for node in nodes:
bmc_address, bmc_ipv4, bmc_ipv6 = get_ipmi_address(node)
@ -255,10 +244,10 @@ def lookup_node_by_bmc_addresses(addresses, introspection_data=None,
elif fail:
raise utils.Error(
_('Node %(uuid)s already has BMC address %(addr)s') %
{'addr': addr, 'uuid': node.uuid},
{'addr': addr, 'uuid': node.id},
data=introspection_data)
else:
found.add(node.uuid)
found.add(node.id)
if len(found) > 1:
raise utils.Error(_('BMC addresses %(addr)s correspond to more than '

5
ironic_inspector/conductor/manager.py

@ -90,6 +90,7 @@ class ConductorManager(object):
(periodic_clean_up_, None, None)],
executor_factory=periodics.ExistingExecutor(utils.executor()),
on_failure=self._periodics_watchdog)
utils.executor().submit(self._periodics_worker.start)
if CONF.enable_mdns:
@ -203,6 +204,6 @@ def periodic_clean_up(): # pragma: no cover
def sync_with_ironic():
ironic = ir_utils.get_client()
# TODO(yuikotakada): pagination
ironic_nodes = ironic.node.list(limit=0)
ironic_node_uuids = {node.uuid for node in ironic_nodes}
ironic_nodes = ironic.nodes(fields=["uuid"], limit=None)
ironic_node_uuids = {node.id for node in ironic_nodes}
node_cache.delete_nodes_not_in_list(ironic_node_uuids)

21
ironic_inspector/introspect.py

@ -16,6 +16,7 @@
import time
from eventlet import semaphore
from openstack import exceptions as os_exc
from oslo_config import cfg
from oslo_utils import strutils
@ -48,15 +49,15 @@ def introspect(node_id, manage_boot=True, token=None):
ir_utils.check_provision_state(node)
if manage_boot:
validation = ironic.node.validate(node.uuid)
if not validation.power['result']:
msg = _('Failed validation of power interface, reason: %s')
raise utils.Error(msg % validation.power['reason'],
node_info=node)
try:
ironic.validate_node(node.id, required='power')
except os_exc.ValidationException as exc:
msg = _('Failed validation of power interface: %s')
raise utils.Error(msg % exc, node_info=node)
bmc_address, bmc_ipv4, bmc_ipv6 = ir_utils.get_ipmi_address(node)
lookup_attrs = list(filter(None, [bmc_ipv4, bmc_ipv6]))
node_info = node_cache.start_introspection(node.uuid,
node_info = node_cache.start_introspection(node.id,
bmc_address=lookup_attrs,
manage_boot=manage_boot,
ironic=ironic)
@ -114,7 +115,7 @@ def _background_introspect_locked(node_info, ironic):
if node_info.manage_boot:
try:
ironic.node.set_boot_device(
ironic.set_node_boot_device(
node_info.uuid, 'pxe',
persistent=_persistent_ramdisk_boot(node_info.node()))
except Exception as exc:
@ -122,9 +123,9 @@ def _background_introspect_locked(node_info, ironic):
node_info=node_info)
try:
ironic.node.set_power_state(node_info.uuid, 'reboot')
ironic.set_node_power_state(node_info.uuid, 'rebooting')
except Exception as exc:
raise utils.Error(_('Failed to power on the node, check it\'s '
raise utils.Error(_('Failed to power on the node, check its '
'power management configuration: %s') % exc,
node_info=node_info)
LOG.info('Introspection started successfully',
@ -164,7 +165,7 @@ def _abort(node_info, ironic):
LOG.debug('Forcing power-off', node_info=node_info)
if node_info.manage_boot:
try:
ironic.node.set_power_state(node_info.uuid, 'off')
ironic.set_node_power_state(node_info.uuid, 'power off')
except Exception as exc:
LOG.warning('Failed to power off node: %s', exc,
node_info=node_info)

40
ironic_inspector/node_cache.py

@ -22,7 +22,7 @@ import json
import operator
from automaton import exceptions as automaton_errors
from ironicclient import exceptions
from openstack import exceptions as os_exc
from oslo_config import cfg
from oslo_db.sqlalchemy import utils as db_utils
from oslo_utils import excutils
@ -339,17 +339,17 @@ class NodeInfo(object):
for port in ports:
mac = port
extra = {}
pxe_enabled = True
is_pxe_enabled = True
if isinstance(port, dict):
mac = port['mac']
client_id = port.get('client_id')
if client_id:
extra = {'client-id': client_id}
pxe_enabled = port.get('pxe', True)
is_pxe_enabled = port.get('pxe', True)
if mac not in self.ports():
self._create_port(mac, ironic=ironic, extra=extra,
pxe_enabled=pxe_enabled)
is_pxe_enabled=is_pxe_enabled)
else:
existing_macs.append(mac)
@ -366,21 +366,21 @@ class NodeInfo(object):
"""
if self._ports is None:
ironic = ironic or self.ironic
port_list = ironic.node.list_ports(self.uuid, limit=0, detail=True)
port_list = ironic.ports(node=self.uuid, limit=None, details=True)
self._ports = {p.address: p for p in port_list}
return self._ports
def _create_port(self, mac, ironic=None, **kwargs):
ironic = ironic or self.ironic
try:
port = ironic.port.create(
port = ironic.create_port(
node_uuid=self.uuid, address=mac, **kwargs)
LOG.info('Port %(uuid)s was created successfully, MAC: %(mac)s,'
'attributes: %(attrs)s',
{'uuid': port.uuid, 'mac': port.address,
{'uuid': port.id, 'mac': port.address,
'attrs': kwargs},
node_info=self)
except exceptions.Conflict:
except os_exc.ConflictException:
LOG.warning('Port %s already exists, skipping',
mac, node_info=self)
# NOTE(dtantsur): we didn't get port object back, so we have to
@ -397,7 +397,7 @@ class NodeInfo(object):
:param patches: JSON patches to apply
:param ironic: Ironic client to use instead of self.ironic
:param kwargs: Arguments to pass to ironicclient.
:raises: ironicclient exceptions
:raises: openstacksdk exceptions
"""
ironic = ironic or self.ironic
# NOTE(aarefiev): support path w/o ahead forward slash
@ -407,7 +407,7 @@ class NodeInfo(object):
patch['path'] = '/' + patch['path']
LOG.debug('Updating node with patches %s', patches, node_info=self)
self._node = ironic.node.update(self.uuid, patches, **kwargs)
self._node = ironic.patch_node(self.uuid, patches, **kwargs)
def patch_port(self, port, patches, ironic=None):
"""Apply JSON patches to a port.
@ -424,7 +424,7 @@ class NodeInfo(object):
LOG.debug('Updating port %(mac)s with patches %(patches)s',
{'mac': port.address, 'patches': patches},
node_info=self)
new_port = ironic.port.update(port.uuid, patches)
new_port = ironic.patch_port(port.id, patches)
ports[port.address] = new_port
def update_properties(self, ironic=None, **props):
@ -458,7 +458,7 @@ class NodeInfo(object):
:param ironic: Ironic client to use instead of self.ironic
"""
ironic = ironic or self.ironic
ironic.node.add_trait(self.uuid, trait)
ironic.add_node_trait(self.uuid, trait)
def remove_trait(self, trait, ironic=None):
"""Remove a trait from the node.
@ -468,8 +468,8 @@ class NodeInfo(object):
"""
ironic = ironic or self.ironic
try:
ironic.node.remove_trait(self.uuid, trait)
except exceptions.NotFound:
ironic.remove_node_trait(self.uuid, trait)
except os_exc.NotFoundException:
LOG.debug('Trait %s is not set, cannot remove', trait,
node_info=self)
@ -484,7 +484,7 @@ class NodeInfo(object):
if isinstance(port, str):
port = ports[port]
ironic.port.delete(port.uuid)
ironic.delete_port(port.id)
del ports[port.address]
def get_by_path(self, path):
@ -919,12 +919,12 @@ def create_node(driver, ironic=None, **attributes):
if ironic is None:
ironic = ir_utils.get_client()
try:
node = ironic.node.create(driver=driver, **attributes)
except exceptions.InvalidAttribute as e:
node = ironic.create_node(driver=driver, **attributes)
except os_exc.SDKException as e:
LOG.error('Failed to create new node: %s', e)
else:
LOG.info('Node %s was created successfully', node.uuid)
return add_node(node.uuid, istate.States.enrolling, ironic=ironic)
LOG.info('Node %s was created successfully', node.id)
return add_node(node.id, istate.States.enrolling, ironic=ironic)
def record_node(ironic=None, bmc_addresses=None, macs=None):
@ -953,7 +953,7 @@ def record_node(ironic=None, bmc_addresses=None, macs=None):
"and BMC address(es) %(addr)s") %
{'macs': macs, 'addr': bmc_addresses})
node = ironic.node.get(node, fields=['uuid', 'provision_state'])
node = ironic.get_node(node, fields=['uuid', 'provision_state'])
# TODO(dtantsur): do we want to allow updates in all states?
if node.provision_state not in ir_utils.VALID_ACTIVE_STATES:
raise utils.Error(_("Node %(node)s is not active, its provision "

6
ironic_inspector/plugins/discovery.py

@ -68,9 +68,15 @@ def enroll_node_not_found_hook(introspection_data, **kwargs):
node_driver_info = _extract_node_driver_info(introspection_data)
node_attr['driver_info'] = node_driver_info
# NOTE(rpittau) by default, if the provision_state is None, it will
# be set to 'available' by openstacksdk, blocking the inspection of the
# node in this phase, as it's not a valid state for inspection.
node_attr['provision_state'] = 'enroll'
node_driver = CONF.discovery.enroll_node_driver
_check_existing_nodes(introspection_data, node_driver_info, ironic)
LOG.debug('Creating discovered node with driver %(driver)s and '
'attributes: %(attr)s',
{'driver': node_driver, 'attr': node_attr},

4
ironic_inspector/plugins/rules.py

@ -16,8 +16,8 @@
import operator
import re
from ironicclient import exceptions as ironic_exc
import netaddr
from openstack import exceptions as os_exc
from ironic_inspector.common.i18n import _
from ironic_inspector.plugins import base
@ -138,7 +138,7 @@ class SetAttributeAction(base.RuleActionPlugin):
try:
node_info.patch([{'op': 'add', 'path': params['path'],
'value': params['value']}], **kwargs)
except (TypeError, ironic_exc.NotAcceptable):
except (TypeError, os_exc.SDKException):
# TODO(dtantsur): remove support for old ironicclient and Queens
if 'reset_interfaces' in params:
# An explicit request, report an error.

6
ironic_inspector/plugins/standard.py

@ -296,7 +296,7 @@ class ValidateInterfacesHook(base.ProcessingHook):
node_info.delete_port(port)
if CONF.processing.overwrite_existing:
# Make sure pxe_enabled is up-to-date
# Make sure is_pxe_enabled is up-to-date
ports = node_info.ports()
for iface in introspection_data['interfaces'].values():
try:
@ -305,8 +305,8 @@ class ValidateInterfacesHook(base.ProcessingHook):
continue
real_pxe = iface.get('pxe', True)
if port.pxe_enabled != real_pxe:
LOG.info('Fixing pxe_enabled=%(val)s on port %(port)s '
if port.is_pxe_enabled != real_pxe:
LOG.info('Fixing is_pxe_enabled=%(val)s on port %(port)s '
'to match introspected data',
{'port': port.address, 'val': real_pxe},
node_info=node_info, data=introspection_data)

4
ironic_inspector/process.py

@ -283,7 +283,7 @@ def _process_node(node_info, node, introspection_data):
node_info.invalidate_cache()
rules.apply(node_info, introspection_data)
resp = {'uuid': node.uuid}
resp = {'uuid': node.id}
# determine how to handle power
if keep_power_on or not node_info.manage_boot:
@ -301,7 +301,7 @@ def _finish(node_info, ironic, introspection_data, power_off=True):
if power_off:
LOG.debug('Forcing power off of node %s', node_info.uuid)
try:
ironic.node.set_power_state(node_info.uuid, 'off')
ironic.set_node_power_state(node_info.uuid, 'power off')
except Exception as exc:
if node_info.node().provision_state == 'enroll':
LOG.info("Failed to power off the node in"

7
ironic_inspector/pxe_filter/base.py

@ -182,11 +182,14 @@ class BaseFilter(interface.FilterDriver):
:raises: periodics.NeverAgain
:returns: a periodic task to be run in the background.
"""
ironic = ir_utils.get_client()
_cached_client = None
def periodic_sync_task():
nonlocal _cached_client
if _cached_client is None:
_cached_client = ir_utils.get_client()
try:
self.sync(ironic)
self.sync(_cached_client)
except InvalidFilterDriverState as e:
LOG.warning('Filter driver %s disabling periodic sync '
'task because of an invalid state.', self)

2
ironic_inspector/pxe_filter/dnsmasq.py

@ -87,7 +87,7 @@ class DnsmasqFilter(pxe_filter.BaseFilter):
active_macs = node_cache.active_macs()
# ironic_macs are all the MACs know to ironic (all ironic ports)
ironic_macs = set(port.address for port in
ir_utils.call_with_retries(ironic.port.list, limit=0,
ir_utils.call_with_retries(ironic.ports, limit=None,
fields=['address']))
blacklist, whitelist = _get_black_white_lists()
# removedlist are the MACs that are in either blacklist or whitelist,

2
ironic_inspector/pxe_filter/iptables.py

@ -232,7 +232,7 @@ def _ib_mac_to_rmac_mapping(ports):
def _get_blacklist(ironic):
ports = [port for port in
ir_utils.call_with_retries(ironic.port.list, limit=0,
ir_utils.call_with_retries(ironic.ports, limit=None,
fields=['address', 'extra'])
if port.address not in node_cache.active_macs()]
_ib_mac_to_rmac_mapping(ports)

8
ironic_inspector/test/base.py

@ -171,17 +171,23 @@ class NodeTest(InventoryTest):
def setUp(self):
super(NodeTest, self).setUp()
self.uuid = uuidutils.generate_uuid()
fake_node = {
'driver': 'ipmi',
'driver_info': {'ipmi_address': self.bmc_address},
'properties': {'cpu_arch': 'i386', 'local_gb': 40},
'uuid': self.uuid,
'id': self.uuid,
'power_state': 'power on',
'provision_state': 'inspecting',
'extra': {},
'instance_uuid': None,
'maintenance': False
}
# TODO(rpittau) the correct value is id, not uuid, based on the
# openstacksdk schema. The code and the fake_node date are correct
# but all the tests still use uuid, so just making it equal to id
# until we find the courage to change it in all tests.
fake_node['uuid'] = fake_node['id']
mock_to_dict = mock.Mock(return_value=fake_node)
self.node = mock.Mock(**fake_node)

117
ironic_inspector/test/functional.py

@ -114,9 +114,9 @@ class Base(base.NodeTest):
self.cli_fixture = self.useFixture(
fixtures.MockPatchObject(ir_utils, 'get_client'))
self.cli = self.cli_fixture.mock.return_value
self.cli.node.get.return_value = self.node
self.cli.node.update.return_value = self.node
self.cli.node.list.return_value = [self.node]
self.cli.get_node.return_value = self.node
self.cli.patch_node.return_value = self.node
self.cli.nodes.return_value = [self.node]
self.patch = [
{'op': 'add', 'path': '/properties/cpus', 'value': '4'},
@ -249,8 +249,8 @@ class Test(Base):
def test_bmc(self):
self.call_introspect(self.uuid)
eventlet.greenthread.sleep(DEFAULT_SLEEP)
self.cli.node.set_power_state.assert_called_once_with(self.uuid,
'reboot')
self.cli.set_node_power_state.assert_called_once_with(self.uuid,
'rebooting')
status = self.call_get_status(self.uuid)
self.check_status(status, finished=False, state=istate.States.waiting)
@ -259,12 +259,12 @@ class Test(Base):
self.assertEqual({'uuid': self.uuid}, res)
eventlet.greenthread.sleep(DEFAULT_SLEEP)
self.cli.node.update.assert_called_with(self.uuid, mock.ANY)
self.assertCalledWithPatch(self.patch, self.cli.node.update)
self.cli.port.create.assert_called_once_with(
self.cli.patch_node.assert_called_with(self.uuid, mock.ANY)
self.assertCalledWithPatch(self.patch, self.cli.patch_node)
self.cli.create_port.assert_called_once_with(
node_uuid=self.uuid, address='11:22:33:44:55:66', extra={},
pxe_enabled=True)
self.assertTrue(self.cli.node.set_boot_device.called)
is_pxe_enabled=True)
self.assertTrue(self.cli.set_node_boot_device.called)
status = self.call_get_status(self.uuid)
self.check_status(status, finished=True, state=istate.States.finished)
@ -275,22 +275,22 @@ class Test(Base):
uuid_to_delete = uuidutils.generate_uuid()
uuid_to_update = uuidutils.generate_uuid()
# Two ports already exist: one with incorrect pxe_enabled, the other
# Two ports already exist: one with incorrect is_pxe_enabled, the other
# should be deleted.
self.cli.node.list_ports.return_value = [
mock.Mock(address=self.macs[1], uuid=uuid_to_update,
node_uuid=self.uuid, extra={}, pxe_enabled=True),
mock.Mock(address='foobar', uuid=uuid_to_delete,
node_uuid=self.uuid, extra={}, pxe_enabled=True),
self.cli.ports.return_value = [
mock.Mock(address=self.macs[1], id=uuid_to_update,
node_id=self.uuid, extra={}, is_pxe_enabled=True),
mock.Mock(address='foobar', id=uuid_to_delete,
node_id=self.uuid, extra={}, is_pxe_enabled=True),
]
# Two more ports are created, one with client_id. Make sure the
# returned object has the same properties as requested in create().
self.cli.port.create.side_effect = mock.Mock
self.cli.create_port.side_effect = mock.Mock
self.call_introspect(self.uuid)
eventlet.greenthread.sleep(DEFAULT_SLEEP)
self.cli.node.set_power_state.assert_called_once_with(self.uuid,
'reboot')
self.cli.set_node_power_state.assert_called_once_with(self.uuid,
'rebooting')
status = self.call_get_status(self.uuid)
self.check_status(status, finished=False, state=istate.States.waiting)
@ -299,17 +299,18 @@ class Test(Base):
self.assertEqual({'uuid': self.uuid}, res)
eventlet.greenthread.sleep(DEFAULT_SLEEP)
self.cli.node.update.assert_called_with(self.uuid, mock.ANY)
self.assertCalledWithPatch(self.patch, self.cli.node.update)
self.cli.patch_node.assert_called_with(self.uuid, mock.ANY)
self.assertCalledWithPatch(self.patch, self.cli.patch_node)
calls = [
mock.call(node_uuid=self.uuid, address=self.macs[0],
extra={}, pxe_enabled=True),
extra={}, is_pxe_enabled=True),
mock.call(node_uuid=self.uuid, address=self.macs[2],
extra={'client-id': self.client_id}, pxe_enabled=False),
extra={'client-id': self.client_id},
is_pxe_enabled=False),
]
self.cli.port.create.assert_has_calls(calls, any_order=True)
self.cli.port.delete.assert_called_once_with(uuid_to_delete)
self.cli.port.update.assert_called_once_with(
self.cli.create_port.assert_has_calls(calls, any_order=True)
self.cli.delete_port.assert_called_once_with(uuid_to_delete)
self.cli.patch_port.assert_called_once_with(
uuid_to_update,
[{'op': 'replace', 'path': '/pxe_enabled', 'value': False}])
@ -369,7 +370,7 @@ class Test(Base):
def test_manage_boot(self):
self.call_introspect(self.uuid, manage_boot=False)
eventlet.greenthread.sleep(DEFAULT_SLEEP)
self.assertFalse(self.cli.node.set_power_state.called)
self.assertFalse(self.cli.set_node_power_state.called)
status = self.call_get_status(self.uuid)
self.check_status(status, finished=False, state=istate.States.waiting)
@ -378,8 +379,8 @@ class Test(Base):
self.assertEqual({'uuid': self.uuid}, res)
eventlet.greenthread.sleep(DEFAULT_SLEEP)
self.cli.node.update.assert_called_with(self.uuid, mock.ANY)
self.assertFalse(self.cli.node.set_boot_device.called)
self.cli.patch_node.assert_called_with(self.uuid, mock.ANY)
self.assertFalse(self.cli.set_node_boot_device.called)
status = self.call_get_status(self.uuid)
self.check_status(status, finished=True, state=istate.States.finished)
@ -476,7 +477,7 @@ class Test(Base):
self.call_continue(self.data)
eventlet.greenthread.sleep(DEFAULT_SLEEP)
self.cli.node.update.assert_any_call(
self.cli.patch_node.assert_any_call(
self.uuid,
[{'op': 'add', 'path': '/extra/foo', 'value': 'bar'}])
@ -514,11 +515,11 @@ class Test(Base):
self.call_continue(self.data)
eventlet.greenthread.sleep(DEFAULT_SLEEP)
self.cli.node.update.assert_any_call(
self.cli.patch_node.assert_any_call(
self.uuid,
[{'op': 'add', 'path': '/extra/foo', 'value': 'bar'}])
self.cli.node.update.assert_any_call(
self.cli.patch_node.assert_any_call(
self.uuid,
[{'op': 'add', 'path': '/driver_info/ipmi_address',
'value': self.data['inventory']['bmc_address']}])
@ -528,8 +529,8 @@ class Test(Base):
self.call_introspect(self.uuid)
eventlet.greenthread.sleep(DEFAULT_SLEEP)
self.cli.node.set_power_state.assert_called_once_with(self.uuid,
'reboot')
self.cli.set_node_power_state.assert_called_once_with(self.uuid,
'rebooting')
status = self.call_get_status(self.uuid)
self.check_status(status, finished=False, state=istate.States.waiting)
@ -538,10 +539,10 @@ class Test(Base):
self.assertEqual({'uuid': self.uuid}, res)
eventlet.greenthread.sleep(DEFAULT_SLEEP)
self.assertCalledWithPatch(self.patch_root_hints, self.cli.node.update)
self.cli.port.create.assert_called_once_with(
self.assertCalledWithPatch(self.patch_root_hints, self.cli.patch_node)
self.cli.create_port.assert_called_once_with(
node_uuid=self.uuid, address='11:22:33:44:55:66', extra={},
pxe_enabled=True)
is_pxe_enabled=True)
status = self.call_get_status(self.uuid)
self.check_status(status, finished=True, state=istate.States.finished)
@ -549,8 +550,8 @@ class Test(Base):
def test_abort_introspection(self):
self.call_introspect(self.uuid)
eventlet.greenthread.sleep(DEFAULT_SLEEP)
self.cli.node.set_power_state.assert_called_once_with(self.uuid,
'reboot')
self.cli.set_node_power_state.assert_called_once_with(self.uuid,
'rebooting')
status = self.call_get_status(self.uuid)
self.check_status(status, finished=False, state=istate.States.waiting)
@ -573,8 +574,8 @@ class Test(Base):
def test_stored_data_processing(self):
self.call_introspect(self.uuid)
eventlet.greenthread.sleep(DEFAULT_SLEEP)
self.cli.node.set_power_state.assert_called_once_with(self.uuid,
'reboot')
self.cli.set_node_power_state.assert_called_once_with(self.uuid,
'rebooting')
res = self.call_continue(self.data)
self.assertEqual({'uuid': self.uuid}, res)
@ -681,8 +682,8 @@ class Test(Base):
self.call_introspect(self.uuid)
eventlet.greenthread.sleep(DEFAULT_SLEEP)
self.cli.node.set_power_state.assert_called_once_with(self.uuid,
'reboot')
self.cli.set_node_power_state.assert_called_once_with(self.uuid,
'rebooting')
status = self.call_get_status(self.uuid)
self.check_status(status, finished=False, state=istate.States.waiting)
@ -691,11 +692,11 @@ class Test(Base):
self.assertEqual({'uuid': self.uuid}, res)
eventlet.greenthread.sleep(DEFAULT_SLEEP)
self.cli.node.update.assert_called_with(self.uuid, mock.ANY)
self.assertCalledWithPatch(self.patch, self.cli.node.update)
self.cli.port.create.assert_called_once_with(
self.cli.patch_node.assert_called_with(self.uuid, mock.ANY)
self.assertCalledWithPatch(self.patch, self.cli.patch_node)
self.cli.create_port.assert_called_once_with(
node_uuid=self.uuid, extra={}, address='11:22:33:44:55:66',
pxe_enabled=True)
is_pxe_enabled=True)
status = self.call_get_status(self.uuid)
self.check_status(status, finished=True, state=istate.States.finished)
@ -703,8 +704,8 @@ class Test(Base):
def test_lldp_plugin(self):
self.call_introspect(self.uuid)
eventlet.greenthread.sleep(DEFAULT_SLEEP)
self.cli.node.set_power_state.assert_called_once_with(self.uuid,
'reboot')
self.cli.set_node_power_state.assert_called_once_with(self.uuid,
'rebooting')
status = self.call_get_status(self.uuid)
self.check_status(status, finished=False, state=istate.States.waiting)
@ -730,8 +731,8 @@ class Test(Base):
cfg.CONF.set_override('permit_active_introspection', True,
'processing')
self.node.provision_state = 'active'
self.cli.node.list_ports.return_value = [
mock.Mock(address='11:22:33:44:55:66', node_uuid=self.node.uuid)
self.cli.ports.return_value = [
mock.Mock(address='11:22:33:44:55:66', node_id=self.node.uuid)
]
# NOTE(dtantsur): we're not starting introspection in this test.
@ -739,10 +740,10 @@ class Test(Base):
self.assertEqual({'uuid': self.uuid}, res)
eventlet.greenthread.sleep(DEFAULT_SLEEP)
self.cli.node.update.assert_called_with(self.uuid, mock.ANY)
self.assertCalledWithPatch(self.patch, self.cli.node.update)
self.assertFalse(self.cli.port.create.called)
self.assertFalse(self.cli.node.set_boot_device.called)
self.cli.patch_node.assert_called_with(self.uuid, mock.ANY)
self.assertCalledWithPatch(self.patch, self.cli.patch_node)
self.assertFalse(self.cli.create_port.called)
self.assertFalse(self.cli.set_node_boot_device.called)
status = self.call_get_status(self.uuid)
self.check_status(status, finished=True, state=istate.States.finished)
@ -751,9 +752,9 @@ class Test(Base):
# Start with a normal introspection as a pre-requisite
self.test_bmc()
self.cli.node.update.reset_mock()
self.cli.node.set_boot_device.reset_mock()
self.cli.port.create.reset_mock()
self.cli.patch_node.reset_mock()
self.cli.set_node_boot_device.reset_mock()
self.cli.create_port.reset_mock()
# Provide some updates
self.data['inventory']['memory']['physical_mb'] = 16384
self.patch = [

106
ironic_inspector/test/unit/test_common_ironic.py

@ -14,10 +14,9 @@
import socket
import unittest
from ironicclient import client
from ironicclient import exc as ironic_exc
import mock
from oslo_config import cfg
import openstack
from openstack import exceptions as os_exc
from ironic_inspector.common import ironic as ir_utils
from ironic_inspector.common import keystone
@ -25,59 +24,16 @@ from ironic_inspector.test import base
from ironic_inspector import utils
CONF = cfg.CONF
class TestGetClientBase(object):
def test_get_client_with_auth_token(self, mock_client, mock_load,
mock_opts, mock_adapter):
fake_token = 'token'
mock_sess = mock.Mock()
mock_load.return_value = mock_sess
ir_utils.get_client(fake_token)
args = {'token': fake_token,
'session': mock_sess,
'os_ironic_api_version': ir_utils.DEFAULT_IRONIC_API_VERSION,
'max_retries': CONF.ironic.max_retries,
'retry_interval': CONF.ironic.retry_interval}
endpoint = mock_adapter.return_value.get_endpoint.return_value
mock_client.assert_called_once_with(1, endpoint=endpoint, **args)
def test_get_client_without_auth_token(self, mock_client, mock_load,
mock_opts, mock_adapter):
mock_sess = mock.Mock()
mock_load.return_value = mock_sess
ir_utils.get_client(None)
args = {'session': mock_sess,
'os_ironic_api_version': ir_utils.DEFAULT_IRONIC_API_VERSION,
'max_retries': CONF.ironic.max_retries,
'retry_interval': CONF.ironic.retry_interval}
endpoint = mock_adapter.return_value.get_endpoint.return_value
mock_client.assert_called_once_with(1, endpoint=endpoint, **args)
@mock.patch.object(keystone, 'get_adapter')
@mock.patch.object(keystone, 'register_auth_opts')
@mock.patch.object(keystone, 'get_session')
@mock.patch.object(client, 'get_client', autospec=True)
class TestGetClientAuth(TestGetClientBase, base.BaseTest):
@mock.patch.object(openstack.connection, 'Connection', autospec=True)
class TestGetClientBase(base.BaseTest):
def setUp(self):
super(TestGetClientAuth, self).setUp()
super(TestGetClientBase, self).setUp()
ir_utils.reset_ironic_session()
self.cfg.config(auth_strategy='keystone')
self.addCleanup(ir_utils.reset_ironic_session)
@mock.patch.object(keystone, 'get_adapter')
@mock.patch.object(keystone, 'register_auth_opts')
@mock.patch.object(keystone, 'get_session')
@mock.patch.object(client, 'get_client', autospec=True)
class TestGetClientNoAuth(TestGetClientBase, base.BaseTest):
def setUp(self):
super(TestGetClientNoAuth, self).setUp()
ir_utils.reset_ironic_session()
self.cfg.config(auth_strategy='noauth')
self.addCleanup(ir_utils.reset_ironic_session)
def test_get_client(self, mock_connection, mock_session):
ir_utils.get_client()
self.assertEqual(1, mock_session.call_count)
class TestGetIpmiAddress(base.BaseTest):
@ -178,7 +134,7 @@ class TestCallWithRetries(unittest.TestCase):
@mock.patch('time.sleep', lambda _x: None)
def test_retries_on_ironicclient_error(self):
self.call.side_effect = [
ironic_exc.ClientException('boom')
os_exc.SDKException('boom')
] * 3 + [mock.sentinel.result]
result = ir_utils.call_with_retries(self.call, 'meow', answer=42)
@ -188,8 +144,8 @@ class TestCallWithRetries(unittest.TestCase):
@mock.patch('time.sleep', lambda _x: None)
def test_retries_on_ironicclient_error_with_failure(self):
self.call.side_effect = ironic_exc.ClientException('boom')
self.assertRaisesRegexp(ironic_exc.ClientException, 'boom',
self.call.side_effect = os_exc.SDKException('boom')
self.assertRaisesRegexp(os_exc.SDKException, 'boom',
ir_utils.call_with_retries,
self.call, 'meow', answer=42)
self.call.assert_called_with('meow', answer=42)
@ -199,13 +155,13 @@ class TestCallWithRetries(unittest.TestCase):
class TestLookupNode(base.NodeTest):
def setUp(self):
super(TestLookupNode, self).setUp()
self.ironic = mock.Mock(spec=['node', 'port'],
node=mock.Mock(spec=['list']),
port=mock.Mock(spec=['list']))
self.ironic.node.list.return_value = [self.node]
self.ironic = mock.Mock(spec=['nodes', 'ports'],
nodes=mock.Mock(spec=['list']),
ports=mock.Mock(spec=['list']))
self.ironic.nodes.return_value = [self.node]
# Simulate only the PXE port enrolled
self.port = mock.Mock(address=self.pxe_mac, node_uuid=self.node.uuid)
self.ironic.port.list.side_effect = [
self.port = mock.Mock(address=self.pxe_mac, node_id=self.node.uuid)
self.ironic.ports.side_effect = [
[self.port]
] + [[]] * (len(self.macs) - 1)
@ -215,20 +171,20 @@ class TestLookupNode(base.NodeTest):
def test_lookup_by_mac_only(self):
uuid = ir_utils.lookup_node(macs=self.macs, ironic=self.ironic)
self.assertEqual(self.node.uuid, uuid)
self.ironic.port.list.assert_has_calls([
self.ironic.ports.assert_has_calls([
mock.call(address=mac,
fields=['uuid', 'node_uuid']) for mac in self.macs
])
def test_lookup_by_mac_duplicates(self):
self.ironic.port.list.side_effect = [
self.ironic.ports.side_effect = [
[self.port],
[mock.Mock(address=self.inactive_mac, node_uuid='another')]
[mock.Mock(address=self.inactive_mac, node_id='another')]
] + [[]] * (len(self.macs) - 1)
self.assertRaisesRegex(utils.Error, 'more than one node',
ir_utils.lookup_node,
macs=self.macs, ironic=self.ironic)
self.ironic.port.list.assert_has_calls([
self.ironic.ports.assert_has_calls([
mock.call(address=mac,
fields=['uuid', 'node_uuid']) for mac in self.macs
])
@ -238,12 +194,12 @@ class TestLookupNode(base.NodeTest):
'42.42.42.42'],
ironic=self.ironic)
self.assertEqual(self.node.uuid, uuid)
self.assertEqual(1, self.ironic.node.list.call_count)
self.assertEqual(1, self.ironic.nodes.call_count)
def test_lookup_by_bmc_duplicates(self):
self.ironic.node.list.return_value = [
self.ironic.nodes.return_value = [
self.node,
mock.Mock(uuid='another',
mock.Mock(id='another',
driver_info={'ipmi_address': '42.42.42.42'}),
]
self.assertRaisesRegex(utils.Error, 'more than one node',
@ -251,7 +207,7 @@ class TestLookupNode(base.NodeTest):
bmc_addresses=[self.bmc_address,
'42.42.42.42'],
ironic=self.ironic)
self.assertEqual(1, self.ironic.node.list.call_count)
self.assertEqual(1, self.ironic.nodes.call_count)
def test_lookup_by_both(self):
uuid = ir_utils.lookup_node(bmc_addresses=[self.bmc_address,
@ -259,15 +215,15 @@ class TestLookupNode(base.NodeTest):
macs=self.macs,
ironic=self.ironic)
self.assertEqual(self.node.uuid, uuid)
self.ironic.port.list.assert_has_calls([
self.ironic.ports.assert_has_calls([
mock.call(address=mac,
fields=['uuid', 'node_uuid']) for mac in self.macs
])
self.assertEqual(1, self.ironic.node.list.call_count)
self.assertEqual(1, self.ironic.nodes.call_count)
def test_lookup_by_both_duplicates(self):
self.ironic.port.list.side_effect = [
[mock.Mock(address=self.inactive_mac, node_uuid='another')]
self.ironic.ports.side_effect = [
[mock.Mock(address=self.inactive_mac, node_id='another')]
] + [[]] * (len(self.macs) - 1)
self.assertRaisesRegex(utils.Error, 'correspond to different nodes',
ir_utils.lookup_node,
@ -275,8 +231,8 @@ class TestLookupNode(base.NodeTest):
self.bmc_v6address],
macs=self.macs,
ironic=self.ironic)
self.ironic.port.list.assert_has_calls([
self.ironic.ports.assert_has_calls([
mock.call(address=mac,
fields=['uuid', 'node_uuid']) for mac in self.macs
])
self.assertEqual(1, self.ironic.node.list.call_count)
self.assertEqual(1, self.ironic.nodes.call_count)

16
ironic_inspector/test/unit/test_dnsmasq_pxe_filter.py

@ -20,8 +20,8 @@ import datetime
import os
import fixtures
from ironicclient import exc as ironic_exc
import mock
from openstack import exceptions as os_exc
from oslo_config import cfg
from ironic_inspector.common import ironic as ir_utils
@ -380,7 +380,7 @@ class TestSync(DnsmasqTestBase):
self.whitelist = {}
self.mock__get_black_white_lists.return_value = (self.blacklist,
self.whitelist)
self.mock_ironic.port.list.return_value = [
self.mock_ironic.ports.return_value = [
mock.Mock(address=address) for address in self.ironic_macs]
self.mock_active_macs.return_value = self.active_macs
self.mock_should_enable_unknown_hosts = self.useFixture(
@ -406,8 +406,8 @@ class TestSync(DnsmasqTestBase):
self.mock__whitelist_mac.assert_called_once_with('active_mac')
self.mock__blacklist_mac.assert_called_once_with('new_mac')
self.mock_ironic.port.list.assert_called_once_with(limit=0,
fields=['address'])
self.mock_ironic.ports.assert_called_once_with(
limit=None, fields=['address'])
self.mock_active_macs.assert_called_once_with()
self.mock__get_black_white_lists.assert_called_once_with()
self.mock__configure_unknown_hosts.assert_called_once_with()
@ -420,8 +420,8 @@ class TestSync(DnsmasqTestBase):
@mock.patch('time.sleep', lambda _x: None)
def test__sync_with_port_list_retries(self):
self.mock_ironic.port.list.side_effect = [
ironic_exc.ConnectionRefused('boom'),
self.mock_ironic.ports.side_effect = [
os_exc.SDKException('boom'),
[mock.Mock(address=address) for address in self.ironic_macs]
]
self.driver._sync(self.mock_ironic)
@ -429,8 +429,8 @@ class TestSync(DnsmasqTestBase):
self.mock__whitelist_mac.assert_called_once_with('active_mac')
self.mock__blacklist_mac.assert_called_once_with('new_mac')
self.mock_ironic.port.list.assert_called_with(limit=0,
fields=['address'])
self.mock_ironic.ports.assert_called_with(
limit=None, fields=['address'])
self.mock_active_macs.assert_called_once_with()
self.mock__get_black_white_lists.assert_called_once_with()
self.mock__configure_removedlist.assert_called_once_with({'gone_mac'})

166
ironic_inspector/test/unit/test_introspect.py

@ -15,8 +15,8 @@ import collections
import time
import fixtures
from ironicclient import exceptions
import mock
from openstack import exceptions as os_exc
from oslo_config import cfg
from ironic_inspector.common import ironic as ir_utils
@ -48,8 +48,8 @@ class BaseTest(test_base.NodeTest):
def _prepare(self, client_mock):
cli = client_mock.return_value
cli.node.get.return_value = self.node
cli.node.validate.return_value = mock.Mock(power={'result': True})
cli.get_node.return_value = self.node
cli.validate_node.return_value = mock.Mock(power={'result': True})
return cli
@ -62,22 +62,21 @@ class TestIntrospect(BaseTest):
introspect.introspect(self.node.uuid)
cli.node.get.assert_called_once_with(self.uuid)
cli.node.validate.assert_called_once_with(self.uuid)
cli.get_node.assert_called_once_with(self.uuid)
cli.validate_node.assert_called_once_with(self.uuid, required='power')
start_mock.assert_called_once_with(self.uuid,
bmc_address=[self.bmc_address],
manage_boot=True,
ironic=cli)
self.node_info.ports.assert_called_once_with()
self.node_info.add_attribute.assert_called_once_with('mac',
self.macs)
self.node_info.add_attribute.assert_called_once_with(
'mac', self.macs)
self.sync_filter_mock.assert_called_with(cli)
cli.node.set_boot_device.assert_called_once_with(self.uuid,
'pxe