Merge "Automatically migrate nodes to hardware types"
This commit is contained in:
commit
9e29d77988
@ -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'),
|
||||
)
|
||||
|
||||
|
||||
|
@ -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}
|
||||
|
@ -923,6 +923,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.
|
||||
|
@ -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.
|
||||
|
@ -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.
|
||||
|
@ -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."""
|
||||
|
@ -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')
|
||||
|
@ -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.
|
||||
|
@ -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())
|
||||
|
@ -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)
|
||||
|
@ -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))
|
||||
|
@ -0,0 +1,23 @@
|
||||
---
|
||||
upgrade:
|
||||
- |
|
||||
Adds new data migration ``migrate_to_hardware_types`` that will try to
|
||||
migrate nodes from classic drivers to hardware types on upgrade. Matching
|
||||
hardware types and interfaces have to be provided on classic drivers
|
||||
themselves. Nodes that cannot be migrated are skipped. This can primary
|
||||
happen for three reasons:
|
||||
|
||||
* migration is not implemented for the classic driver,
|
||||
* the matching hardware type is not enabled,
|
||||
* one or more matching hardware interfaces are not enabled.
|
||||
|
||||
In the latter case, the new migration command line option
|
||||
``reset_unsupported_interfaces`` can be used to reset optional interfaces
|
||||
(all except for ``boot``, ``deploy``, ``management`` and ``power``) to
|
||||
their no-op implementations (e.g. ``no-inspect``) if the matching
|
||||
implementation is not enabled. Use it like::
|
||||
|
||||
ironic-dbsync online_data_migrations --option migrate_to_hardware_types.reset_unsupported_interfaces=true
|
||||
|
||||
This migration can be repeated several times to migrate skipped nodes
|
||||
after the configuration is changed.
|
Loading…
Reference in New Issue
Block a user