summaryrefslogtreecommitdiff
path: root/ironic
diff options
context:
space:
mode:
authorDmitry Tantsur <divius.inside@gmail.com>2018-01-16 17:27:17 +0100
committerDmitry Tantsur <divius.inside@gmail.com>2018-01-26 21:17:26 +0000
commitcc6f7bc73e2b1c9dad9624f3bc9c4c6ac3d103de (patch)
tree016aa468cdbc15a9f31b5c73b5450f16e736aba6 /ironic
parent98570dc6addb4dbdac8cf394fcf29e6c640f1fff (diff)
downloadironic-cc6f7bc73e2b1c9dad9624f3bc9c4c6ac3d103de.tar.gz
Automatically migrate nodes to hardware types
This change adds a new data migration: migrate_to_hardware_types. It works by walking through known classic drivers, detecting matching hardware types and interfaces and updates nodes accordingly. Nodes that cannot be updated (e.g. matching hardware type is not enabled) are skipped. A new migration option reset_unsupported_interfaces can be set to True to allow resetting optional interfaces to their no-op versions. The example implementation are provided for the community supported IPMI and SNMP drivers, as well as for fake drivers based on them. Change-Id: I732b44f2ab1ef73f56b352415ffd9cdd8a0e232b Partial-Bug: #1690185
Diffstat (limited to 'ironic')
-rw-r--r--ironic/cmd/dbsync.py2
-rw-r--r--ironic/common/driver_factory.py98
-rw-r--r--ironic/db/api.py19
-rw-r--r--ironic/db/sqlalchemy/api.py77
-rw-r--r--ironic/drivers/base.py12
-rw-r--r--ironic/drivers/fake.py69
-rw-r--r--ironic/drivers/ipmi.py45
-rw-r--r--ironic/drivers/pxe.py9
-rw-r--r--ironic/tests/unit/common/test_driver_factory.py76
-rw-r--r--ironic/tests/unit/db/test_api.py34
-rw-r--r--ironic/tests/unit/drivers/test_base.py55
11 files changed, 496 insertions, 0 deletions
diff --git a/ironic/cmd/dbsync.py b/ironic/cmd/dbsync.py
index 30b484df3..5de5828f1 100644
--- a/ironic/cmd/dbsync.py
+++ b/ironic/cmd/dbsync.py
@@ -65,6 +65,8 @@ ONLINE_MIGRATIONS = (
# Added in Pike, modified in Queens
# TODO(rloo): remove in Rocky
(dbapi, 'backfill_version_column'),
+ # TODO(dtantsur): remove when classic drivers are removed (Rocky?)
+ (dbapi, 'migrate_to_hardware_types'),
)
diff --git a/ironic/common/driver_factory.py b/ironic/common/driver_factory.py
index 531e23a29..a51e7bc54 100644
--- a/ironic/common/driver_factory.py
+++ b/ironic/common/driver_factory.py
@@ -17,6 +17,7 @@ import collections
from oslo_concurrency import lockutils
from oslo_log import log
+import stevedore
from stevedore import named
from ironic.common import exception
@@ -558,3 +559,100 @@ _INTERFACE_LOADERS = {
# refactor them later to use _INTERFACE_LOADERS.
NetworkInterfaceFactory = _INTERFACE_LOADERS['network']
StorageInterfaceFactory = _INTERFACE_LOADERS['storage']
+
+
+def calculate_migration_delta(driver_name, driver_class,
+ reset_unsupported_interfaces=False):
+ """Calculate an update for the given classic driver extension.
+
+ This function calculates a database update required to convert a node
+ with a classic driver to hardware types and interfaces.
+
+ This function is used in the data migrations and is not a part of the
+ public Python API.
+
+ :param driver_name: the entry point name of the driver
+ :param driver_class: class of classic driver.
+ :param reset_unsupported_interfaces: if set to True, target interfaces
+ that are not enabled will be replaced with a no-<interface name>,
+ if possible.
+ :returns: Node fields requiring update as a dict (field -> new value).
+ None if a migration is not possible.
+ """
+ # NOTE(dtantsur): provide defaults for optional interfaces
+ defaults = {'console': 'no-console',
+ 'inspect': 'no-inspect',
+ 'raid': 'no-raid',
+ 'rescue': 'no-rescue',
+ 'vendor': 'no-vendor'}
+ try:
+ hw_type, new_ifaces = driver_class.to_hardware_type()
+ except NotImplementedError:
+ LOG.warning('Skipping migrating nodes with driver %s, '
+ 'migration not supported', driver_name)
+ return None
+ else:
+ ifaces = dict(defaults, **new_ifaces)
+
+ if hw_type not in CONF.enabled_hardware_types:
+ LOG.warning('Skipping migrating nodes with driver %(drv)s: '
+ 'hardware type %(hw_type)s is not enabled',
+ {'drv': driver_name, 'hw_type': hw_type})
+ return None
+
+ not_enabled = []
+ delta = {'driver': hw_type}
+ for iface, value in ifaces.items():
+ conf = 'enabled_%s_interfaces' % iface
+ if value not in getattr(CONF, conf):
+ not_enabled.append((iface, value))
+ else:
+ delta['%s_interface' % iface] = value
+
+ if not_enabled and reset_unsupported_interfaces:
+ still_not_enabled = []
+ for iface, value in not_enabled:
+ try:
+ default = defaults[iface]
+ except KeyError:
+ still_not_enabled.append((iface, value))
+ else:
+ conf = 'enabled_%s_interfaces' % iface
+ if default not in getattr(CONF, conf):
+ still_not_enabled.append((iface, value))
+ else:
+ delta['%s_interface' % iface] = default
+
+ not_enabled = still_not_enabled
+
+ if not_enabled:
+ LOG.warning('Skipping migrating nodes with driver %(drv)s, '
+ 'the following interfaces are not supported: '
+ '%(ifaces)s',
+ {'drv': driver_name,
+ 'ifaces': ', '.join('%s_interface=%s' % tpl
+ for tpl in not_enabled)})
+ return None
+
+ return delta
+
+
+def classic_drivers_to_migrate():
+ """Get drivers requiring migration.
+
+ This function is used in the data migrations and is not a part of the
+ public Python API.
+
+ :returns: a dict mapping driver names to driver classes
+ """
+ def failure_callback(mgr, ep, exc):
+ LOG.warning('Unable to load classic driver %(drv)s: %(err)s',
+ {'drv': ep.name, 'err': exc})
+
+ extension_manager = (
+ stevedore.ExtensionManager(
+ 'ironic.drivers',
+ invoke_on_load=False,
+ on_load_failure_callback=failure_callback))
+
+ return {ext.name: ext.plugin for ext in extension_manager}
diff --git a/ironic/db/api.py b/ironic/db/api.py
index c52cc7bd4..41c8cbaa9 100644
--- a/ironic/db/api.py
+++ b/ironic/db/api.py
@@ -924,6 +924,25 @@ class Connection(object):
# TODO(rloo) Delete this in Rocky cycle.
@abc.abstractmethod
+ def migrate_to_hardware_types(self, context, max_count,
+ reset_unsupported_interfaces=False):
+ """Migrate nodes from classic drivers to hardware types.
+
+ Go through all nodes with a classic driver and try to migrate them to a
+ corresponding hardware type and a correct set of hardware interfaces.
+
+ :param context: the admin context
+ :param max_count: The maximum number of objects to migrate. Must be
+ >= 0. If zero, all the objects will be migrated.
+ :param reset_unsupported_interfaces: whether to reset unsupported
+ optional interfaces to their no-XXX versions.
+ :returns: A 2-tuple, 1. the total number of objects that need to be
+ migrated (at the beginning of this call) and 2. the number
+ of migrated objects.
+ """
+ # TODO(dtantsur) Delete this in Rocky cycle.
+
+ @abc.abstractmethod
def set_node_traits(self, node_id, traits, version):
"""Replace all of the node traits with specified list of traits.
diff --git a/ironic/db/sqlalchemy/api.py b/ironic/db/sqlalchemy/api.py
index 4fa528a06..1b78bf78a 100644
--- a/ironic/db/sqlalchemy/api.py
+++ b/ironic/db/sqlalchemy/api.py
@@ -33,6 +33,7 @@ from sqlalchemy.orm.exc import NoResultFound, MultipleResultsFound
from sqlalchemy.orm import joinedload
from sqlalchemy import sql
+from ironic.common import driver_factory
from ironic.common import exception
from ironic.common.i18n import _
from ironic.common import profiler
@@ -1294,6 +1295,82 @@ class Connection(api.Connection):
return total_to_migrate, total_migrated
+ @oslo_db_api.retry_on_deadlock
+ def migrate_to_hardware_types(self, context, max_count,
+ reset_unsupported_interfaces=False):
+ """Migrate nodes from classic drivers to hardware types.
+
+ Go through all nodes with a classic driver and try to migrate them to
+ a corresponding hardware type and a correct set of hardware interfaces.
+
+ If migration is not possible for any reason (e.g. the target hardware
+ type is not enabled), the nodes are skipped. An operator is expected to
+ correct the configuration and either rerun online_data_migration or
+ migrate the nodes manually.
+
+ :param context: the admin context (not used)
+ :param max_count: The maximum number of objects to migrate. Must be
+ >= 0. If zero, all the objects will be migrated.
+ :param reset_unsupported_interfaces: whether to reset unsupported
+ optional interfaces to their no-XXX versions.
+ :returns: A 2-tuple, 1. the total number of objects that need to be
+ migrated (at the beginning of this call) and 2. the number
+ of migrated objects.
+ """
+ reset_unsupported_interfaces = strutils.bool_from_string(
+ reset_unsupported_interfaces, strict=True)
+
+ drivers = driver_factory.classic_drivers_to_migrate()
+
+ total_to_migrate = (model_query(models.Node)
+ .filter(models.Node.driver.in_(list(drivers)))
+ .count())
+
+ total_migrated = 0
+ for driver, driver_cls in drivers.items():
+ if max_count and total_migrated >= max_count:
+ return total_to_migrate, total_migrated
+
+ # UPDATE with LIMIT seems to be a MySQL-only feature, so first
+ # fetch the required number of Node IDs, then update them.
+ query = model_query(models.Node.id).filter_by(driver=driver)
+ if max_count:
+ query = query.limit(max_count - total_migrated)
+ ids = [obj.id for obj in query]
+ if not ids:
+ continue
+
+ delta = driver_factory.calculate_migration_delta(
+ driver, driver_cls, reset_unsupported_interfaces)
+ if delta is None:
+ # NOTE(dtantsur): mark unsupported nodes as migrated. Otherwise
+ # calling online_data_migration without --max-count will result
+ # in a infinite loop.
+ total_migrated += len(ids)
+ continue
+
+ # UPDATE with LIMIT seems to be a MySQL-only feature, so first
+ # fetch the required number of Node IDs, then update them.
+ query = model_query(models.Node.id).filter_by(driver=driver)
+ if max_count:
+ query = query.limit(max_count - total_migrated)
+ ids = [obj.id for obj in query]
+ if not ids:
+ LOG.debug('No nodes with driver %s', driver)
+ continue
+
+ LOG.info('Migrating nodes with driver %(drv)s to %(delta)s',
+ {'drv': driver, 'delta': delta})
+
+ with _session_for_write():
+ num_migrated = (model_query(models.Node)
+ .filter_by(driver=driver)
+ .filter(models.Node.id.in_(ids))
+ .update(delta, synchronize_session=False))
+ total_migrated += num_migrated
+
+ return total_to_migrate, total_migrated
+
@staticmethod
def _verify_max_traits_per_node(node_id, num_traits):
"""Verify that an operation would not exceed the per-node trait limit.
diff --git a/ironic/drivers/base.py b/ironic/drivers/base.py
index 1c14cca72..0e7b2ffdf 100644
--- a/ironic/drivers/base.py
+++ b/ironic/drivers/base.py
@@ -147,6 +147,18 @@ class BaseDriver(object):
properties.update(iface.get_properties())
return properties
+ @classmethod
+ def to_hardware_type(cls):
+ """Return corresponding hardware type and hardware interfaces.
+
+ :returns: a tuple with two items:
+
+ * new driver field - the target hardware type
+ * dictionary containing interfaces to update, e.g.
+ {'deploy': 'iscsi', 'power': 'ipmitool'}
+ """
+ raise NotImplementedError()
+
class BareDriver(BaseDriver):
"""A bare driver object which will have interfaces attached later.
diff --git a/ironic/drivers/fake.py b/ironic/drivers/fake.py
index 28ddf1f7f..5834a1151 100644
--- a/ironic/drivers/fake.py
+++ b/ironic/drivers/fake.py
@@ -70,6 +70,14 @@ class FakeDriver(base.BaseDriver):
self.inspect = fake.FakeInspect()
self.raid = fake.FakeRAID()
+ @classmethod
+ def to_hardware_type(cls):
+ return 'fake-hardware', {
+ iface: 'fake'
+ for iface in ['boot', 'console', 'deploy', 'inspect',
+ 'management', 'power', 'raid', 'rescue', 'vendor']
+ }
+
class FakeSoftPowerDriver(FakeDriver):
"""Example implementation of a Driver."""
@@ -89,6 +97,17 @@ class FakeIPMIToolDriver(base.BaseDriver):
self.vendor = ipmitool.VendorPassthru()
self.management = ipmitool.IPMIManagement()
+ @classmethod
+ def to_hardware_type(cls):
+ return 'fake-hardware', {
+ 'boot': 'fake',
+ 'console': 'ipmitool-shellinabox',
+ 'deploy': 'fake',
+ 'management': 'ipmitool',
+ 'power': 'ipmitool',
+ 'vendor': 'ipmitool'
+ }
+
class FakeIPMIToolSocatDriver(base.BaseDriver):
"""Example implementation of a Driver."""
@@ -100,6 +119,17 @@ class FakeIPMIToolSocatDriver(base.BaseDriver):
self.vendor = ipmitool.VendorPassthru()
self.management = ipmitool.IPMIManagement()
+ @classmethod
+ def to_hardware_type(cls):
+ return 'fake-hardware', {
+ 'boot': 'fake',
+ 'console': 'ipmitool-socat',
+ 'deploy': 'fake',
+ 'management': 'ipmitool',
+ 'power': 'ipmitool',
+ 'vendor': 'ipmitool'
+ }
+
class FakePXEDriver(base.BaseDriver):
"""Example implementation of a Driver."""
@@ -109,6 +139,15 @@ class FakePXEDriver(base.BaseDriver):
self.boot = pxe.PXEBoot()
self.deploy = iscsi_deploy.ISCSIDeploy()
+ @classmethod
+ def to_hardware_type(cls):
+ return 'fake-hardware', {
+ 'boot': 'pxe',
+ 'deploy': 'iscsi',
+ 'management': 'fake',
+ 'power': 'fake',
+ }
+
class FakeAgentDriver(base.BaseDriver):
"""Example implementation of an AgentDriver."""
@@ -119,6 +158,16 @@ class FakeAgentDriver(base.BaseDriver):
self.deploy = agent.AgentDeploy()
self.raid = agent.AgentRAID()
+ @classmethod
+ def to_hardware_type(cls):
+ return 'fake-hardware', {
+ 'boot': 'pxe',
+ 'deploy': 'direct',
+ 'management': 'fake',
+ 'power': 'fake',
+ 'raid': 'agent'
+ }
+
class FakeIloDriver(base.BaseDriver):
"""Fake iLO driver, used in testing."""
@@ -162,6 +211,15 @@ class FakeSNMPDriver(base.BaseDriver):
self.power = snmp.SNMPPower()
self.deploy = fake.FakeDeploy()
+ @classmethod
+ def to_hardware_type(cls):
+ return 'snmp', {
+ 'boot': 'fake',
+ 'deploy': 'fake',
+ 'management': 'fake',
+ 'power': 'snmp',
+ }
+
class FakeIRMCDriver(base.BaseDriver):
"""Fake iRMC driver."""
@@ -191,6 +249,17 @@ class FakeIPMIToolInspectorDriver(base.BaseDriver):
# integration.
self.inspect = inspector.Inspector()
+ @classmethod
+ def to_hardware_type(cls):
+ return 'fake-hardware', {
+ 'boot': 'fake',
+ 'console': 'ipmitool-shellinabox',
+ 'deploy': 'fake',
+ 'inspect': 'inspector',
+ 'management': 'ipmitool',
+ 'power': 'ipmitool',
+ }
+
class FakeUcsDriver(base.BaseDriver):
"""Fake UCS driver."""
diff --git a/ironic/drivers/ipmi.py b/ironic/drivers/ipmi.py
index 3319f0eea..7bdbc341a 100644
--- a/ironic/drivers/ipmi.py
+++ b/ironic/drivers/ipmi.py
@@ -14,6 +14,8 @@
Hardware types and classic drivers for IPMI (using ipmitool).
"""
+from oslo_config import cfg
+
from ironic.drivers import base
from ironic.drivers import generic
from ironic.drivers.modules import agent
@@ -24,6 +26,9 @@ from ironic.drivers.modules import noop
from ironic.drivers.modules import pxe
+CONF = cfg.CONF
+
+
class IPMIHardware(generic.GenericHardware):
"""IPMI hardware type.
@@ -53,6 +58,22 @@ class IPMIHardware(generic.GenericHardware):
return [ipmitool.VendorPassthru, noop.NoVendor]
+def _to_hardware_type():
+ # NOTE(dtantsur): classic drivers are not affected by the
+ # enabled_inspect_interfaces configuration option.
+ if CONF.inspector.enabled:
+ inspect_interface = 'inspector'
+ else:
+ inspect_interface = 'no-inspect'
+
+ return {'boot': 'pxe',
+ 'inspect': inspect_interface,
+ 'management': 'ipmitool',
+ 'power': 'ipmitool',
+ 'raid': 'agent',
+ 'vendor': 'ipmitool'}
+
+
class PXEAndIPMIToolDriver(base.BaseDriver):
"""PXE + IPMITool driver.
@@ -74,6 +95,12 @@ class PXEAndIPMIToolDriver(base.BaseDriver):
self.vendor = ipmitool.VendorPassthru()
self.raid = agent.AgentRAID()
+ @classmethod
+ def to_hardware_type(cls):
+ return 'ipmi', dict(_to_hardware_type(),
+ console='ipmitool-shellinabox',
+ deploy='iscsi')
+
class PXEAndIPMIToolAndSocatDriver(PXEAndIPMIToolDriver):
"""PXE + IPMITool + socat driver.
@@ -93,6 +120,12 @@ class PXEAndIPMIToolAndSocatDriver(PXEAndIPMIToolDriver):
PXEAndIPMIToolDriver.__init__(self)
self.console = ipmitool.IPMISocatConsole()
+ @classmethod
+ def to_hardware_type(cls):
+ return 'ipmi', dict(_to_hardware_type(),
+ console='ipmitool-socat',
+ deploy='iscsi')
+
class AgentAndIPMIToolDriver(base.BaseDriver):
"""Agent + IPMITool driver.
@@ -116,6 +149,12 @@ class AgentAndIPMIToolDriver(base.BaseDriver):
self.inspect = inspector.Inspector.create_if_enabled(
'AgentAndIPMIToolDriver')
+ @classmethod
+ def to_hardware_type(cls):
+ return 'ipmi', dict(_to_hardware_type(),
+ console='ipmitool-shellinabox',
+ deploy='direct')
+
class AgentAndIPMIToolAndSocatDriver(AgentAndIPMIToolDriver):
"""Agent + IPMITool + socat driver.
@@ -134,3 +173,9 @@ class AgentAndIPMIToolAndSocatDriver(AgentAndIPMIToolDriver):
def __init__(self):
AgentAndIPMIToolDriver.__init__(self)
self.console = ipmitool.IPMISocatConsole()
+
+ @classmethod
+ def to_hardware_type(cls):
+ return 'ipmi', dict(_to_hardware_type(),
+ console='ipmitool-socat',
+ deploy='direct')
diff --git a/ironic/drivers/pxe.py b/ironic/drivers/pxe.py
index 3fff137e4..4cba28b7f 100644
--- a/ironic/drivers/pxe.py
+++ b/ironic/drivers/pxe.py
@@ -100,6 +100,15 @@ class PXEAndSNMPDriver(base.BaseDriver):
# Only PXE as a boot device is supported.
self.management = None
+ @classmethod
+ def to_hardware_type(cls):
+ return 'snmp', {
+ 'boot': 'pxe',
+ 'deploy': 'iscsi',
+ 'management': 'fake',
+ 'power': 'snmp',
+ }
+
class PXEAndIRMCDriver(base.BaseDriver):
"""PXE + iRMC driver using SCCI.
diff --git a/ironic/tests/unit/common/test_driver_factory.py b/ironic/tests/unit/common/test_driver_factory.py
index dc0a01144..12e7b2e67 100644
--- a/ironic/tests/unit/common/test_driver_factory.py
+++ b/ironic/tests/unit/common/test_driver_factory.py
@@ -14,6 +14,7 @@
import mock
from oslo_utils import uuidutils
+import stevedore
from stevedore import named
from ironic.common import driver_factory
@@ -796,3 +797,78 @@ class HardwareTypeLoadTestCase(db_base.DbTestCase):
def test_enabled_supported_interfaces_non_default(self):
self._test_enabled_supported_interfaces(True)
+
+
+class ClassicDriverMigrationTestCase(base.TestCase):
+
+ def setUp(self):
+ super(ClassicDriverMigrationTestCase, self).setUp()
+ self.driver_cls = mock.Mock(spec=['to_hardware_type'])
+ self.driver_cls2 = mock.Mock(spec=['to_hardware_type'])
+ self.new_ifaces = {
+ 'console': 'new-console',
+ 'inspect': 'new-inspect'
+ }
+
+ self.driver_cls.to_hardware_type.return_value = ('hw-type',
+ self.new_ifaces)
+ self.ext = mock.Mock(plugin=self.driver_cls)
+ self.ext.name = 'drv1'
+ self.ext2 = mock.Mock(plugin=self.driver_cls2)
+ self.ext2.name = 'drv2'
+ self.config(enabled_hardware_types=['hw-type'],
+ enabled_console_interfaces=['no-console', 'new-console'],
+ enabled_inspect_interfaces=['no-inspect', 'new-inspect'],
+ enabled_raid_interfaces=['no-raid'],
+ enabled_rescue_interfaces=['no-rescue'],
+ enabled_vendor_interfaces=['no-vendor'])
+
+ def test_calculate_migration_delta(self):
+ delta = driver_factory.calculate_migration_delta(
+ 'drv', self.driver_cls, False)
+ self.assertEqual({'driver': 'hw-type',
+ 'console_interface': 'new-console',
+ 'inspect_interface': 'new-inspect',
+ 'raid_interface': 'no-raid',
+ 'rescue_interface': 'no-rescue',
+ 'vendor_interface': 'no-vendor'},
+ delta)
+
+ def test_calculate_migration_delta_not_implemeted(self):
+ self.driver_cls.to_hardware_type.side_effect = NotImplementedError()
+ delta = driver_factory.calculate_migration_delta(
+ 'drv', self.driver_cls, False)
+ self.assertIsNone(delta)
+
+ def test_calculate_migration_delta_unsupported_hw_type(self):
+ self.driver_cls.to_hardware_type.return_value = ('hw-type2',
+ self.new_ifaces)
+ delta = driver_factory.calculate_migration_delta(
+ 'drv', self.driver_cls, False)
+ self.assertIsNone(delta)
+
+ def test__calculate_migration_delta_unsupported_interface(self):
+ self.new_ifaces['inspect'] = 'unsupported inspect'
+ delta = driver_factory.calculate_migration_delta(
+ 'drv', self.driver_cls, False)
+ self.assertIsNone(delta)
+
+ def test_calculate_migration_delta_unsupported_interface_reset(self):
+ self.new_ifaces['inspect'] = 'unsupported inspect'
+ delta = driver_factory.calculate_migration_delta(
+ 'drv', self.driver_cls, True)
+ self.assertEqual({'driver': 'hw-type',
+ 'console_interface': 'new-console',
+ 'inspect_interface': 'no-inspect',
+ 'raid_interface': 'no-raid',
+ 'rescue_interface': 'no-rescue',
+ 'vendor_interface': 'no-vendor'},
+ delta)
+
+ @mock.patch.object(stevedore, 'ExtensionManager', autospec=True)
+ def test_classic_drivers_to_migrate(self, mock_ext_mgr):
+ mock_ext_mgr.return_value.__iter__.return_value = iter([self.ext,
+ self.ext2])
+ self.assertEqual({'drv1': self.driver_cls,
+ 'drv2': self.driver_cls2},
+ driver_factory.classic_drivers_to_migrate())
diff --git a/ironic/tests/unit/db/test_api.py b/ironic/tests/unit/db/test_api.py
index 19cf9f70d..01686bd5d 100644
--- a/ironic/tests/unit/db/test_api.py
+++ b/ironic/tests/unit/db/test_api.py
@@ -10,9 +10,11 @@
# License for the specific language governing permissions and limitations
# under the License.
+import mock
from oslo_utils import uuidutils
from ironic.common import context
+from ironic.common import driver_factory
from ironic.common import release_mappings
from ironic.db import api as db_api
from ironic.tests.unit.db import base
@@ -137,3 +139,35 @@ class BackfillVersionTestCase(base.DbTestCase):
for hostname in conductors:
conductor = self.dbapi.get_conductor(hostname)
self.assertEqual(self.conductor_ver, conductor.version)
+
+
+@mock.patch.object(driver_factory, 'calculate_migration_delta', autospec=True)
+@mock.patch.object(driver_factory, 'classic_drivers_to_migrate', autospec=True)
+class MigrateToHardwareTypesTestCase(base.DbTestCase):
+
+ def setUp(self):
+ super(MigrateToHardwareTypesTestCase, self).setUp()
+ self.context = context.get_admin_context()
+ self.dbapi = db_api.get_instance()
+ self.node = utils.create_test_node(uuid=uuidutils.generate_uuid(),
+ driver='classic_driver')
+
+ def test_migrate(self, mock_drivers, mock_delta):
+ mock_drivers.return_value = {'classic_driver': mock.sentinel.drv1,
+ 'another_driver': mock.sentinel.drv2}
+ mock_delta.return_value = {'driver': 'new_driver',
+ 'inspect_interface': 'new_inspect'}
+ result = self.dbapi.migrate_to_hardware_types(self.context, 0)
+ self.assertEqual((1, 1), result)
+ node = self.dbapi.get_node_by_id(self.node.id)
+ self.assertEqual('new_driver', node.driver)
+ self.assertEqual('new_inspect', node.inspect_interface)
+
+ def test_migrate_unsupported(self, mock_drivers, mock_delta):
+ mock_drivers.return_value = {'classic_driver': mock.sentinel.drv1,
+ 'another_driver': mock.sentinel.drv2}
+ mock_delta.return_value = None
+ result = self.dbapi.migrate_to_hardware_types(self.context, 0)
+ self.assertEqual((1, 1), result)
+ node = self.dbapi.get_node_by_id(self.node.id)
+ self.assertEqual('classic_driver', node.driver)
diff --git a/ironic/tests/unit/drivers/test_base.py b/ironic/tests/unit/drivers/test_base.py
index 52642ec15..ebf08ccf4 100644
--- a/ironic/tests/unit/drivers/test_base.py
+++ b/ironic/tests/unit/drivers/test_base.py
@@ -16,7 +16,9 @@
import json
import mock
+import stevedore
+from ironic.common import driver_factory
from ironic.common import exception
from ironic.common import raid
from ironic.drivers import base as driver_base
@@ -451,3 +453,56 @@ class TestBareDriver(base.TestCase):
'rescue', 'storage'),
driver_base.BareDriver.standard_interfaces
)
+
+
+class TestToHardwareType(base.TestCase):
+ def setUp(self):
+ super(TestToHardwareType, self).setUp()
+ self.driver_classes = list(
+ driver_factory.classic_drivers_to_migrate().values())
+ self.existing_ifaces = {}
+ for iface in driver_base.ALL_INTERFACES:
+ self.existing_ifaces[iface] = stevedore.ExtensionManager(
+ 'ironic.hardware.interfaces.%s' % iface,
+ invoke_on_load=False).names()
+ self.hardware_types = stevedore.ExtensionManager(
+ 'ironic.hardware.types', invoke_on_load=False).names()
+ # These are the interfaces that don't have a no-op version
+ self.mandatory_interfaces = ['boot', 'deploy', 'management', 'power']
+
+ def test_to_hardware_type_returns_hardware_type(self):
+ for driver in self.driver_classes:
+ try:
+ hw_type = driver.to_hardware_type()[0]
+ except NotImplementedError:
+ continue
+ self.assertIn(hw_type, self.hardware_types,
+ '%s returns unknown hardware type %s' %
+ (driver, hw_type))
+
+ def test_to_hardware_type_returns_existing_interfaces(self):
+ # Check that all defined implementations of to_hardware_type
+ # contain only existing interface types
+ for driver in self.driver_classes:
+ try:
+ delta = driver.to_hardware_type()[1]
+ except NotImplementedError:
+ continue
+ for iface, value in delta.items():
+ self.assertIn(iface, self.existing_ifaces,
+ '%s returns unknown interface %s' %
+ (driver, iface))
+ self.assertIn(value, self.existing_ifaces[iface],
+ '%s returns unknown %s interface %s' %
+ (driver, iface, value))
+
+ def test_to_hardware_type_mandatory_interfaces(self):
+ for driver in self.driver_classes:
+ try:
+ delta = driver.to_hardware_type()[1]
+ except NotImplementedError:
+ continue
+ for iface in self.mandatory_interfaces:
+ self.assertIn(iface, delta,
+ '%s does not return mandatory interface %s' %
+ (driver, iface))