diff --git a/etc/ironic/ironic.conf.sample b/etc/ironic/ironic.conf.sample index ab0d663541..144112b72b 100644 --- a/etc/ironic/ironic.conf.sample +++ b/etc/ironic/ironic.conf.sample @@ -30,18 +30,152 @@ # developer documentation online. (list value) #enabled_drivers = pxe_ipmitool +# WARNING: This configuration option is part of the incomplete +# driver composition work, changing it's setting has no +# effect. Specify the list of hardware types to load during +# service initialization. Missing hardware types, or hardware +# types which fail to initialize, will prevent the conductor +# service from starting. No hardware types are enabled by +# default now, but in the future this option will default to a +# recommended set of production-oriented hardware types. A +# complete list of hardware types present on your system may +# be found by enumerating the "ironic.hardware.types" +# entrypoint. (list value) +#enabled_hardware_types = + +# WARNING: This configuration option is part of the incomplete +# driver composition work, changing it's setting has no +# effect. Specify the list of boot interfaces to load during +# service initialization. Missing boot interfaces, or boot +# interfaces which fail to initialize, will prevent the +# ironic-conductor service from starting. The default value is +# a recommended set of production-oriented boot interfaces. A +# complete list of boot interfaces present on your system may +# be found by enumerating the +# "ironic.hardware.interfaces.boot" entrypoint. When setting +# this value, please make sure that every enabled hardware +# type will have the same set of enabled boot interfaces on +# every ironic-conductor service. (list value) +#enabled_boot_interfaces = + +# WARNING: This configuration option is part of the incomplete +# driver composition work, changing it's setting has no +# effect. Default boot interface to be used for nodes that do +# not have boot_interface field set. A complete list of boot +# interfaces present on your system may be found by +# enumerating the "ironic.hardware.interfaces.boot" +# entrypoint. (string value) +#default_boot_interface = + +# WARNING: This configuration option is part of the incomplete +# driver composition work, changing it's setting has no +# effect. Specify the list of console interfaces to load +# during service initialization. Missing console interfaces, +# or console interfaces which fail to initialize, will prevent +# the ironic-conductor service from starting. The default +# value is a recommended set of production-oriented console +# interfaces. A complete list of console interfaces present on +# your system may be found by enumerating the +# "ironic.hardware.interfaces.console" entrypoint. When +# setting this value, please make sure that every enabled +# hardware type will have the same set of enabled console +# interfaces on every ironic-conductor service. (list value) +#enabled_console_interfaces = no-console + +# WARNING: This configuration option is part of the incomplete +# driver composition work, changing it's setting has no +# effect. Default console interface to be used for nodes that +# do not have console_interface field set. A complete list of +# console interfaces present on your system may be found by +# enumerating the "ironic.hardware.interfaces.console" +# entrypoint. (string value) +#default_console_interface = + +# WARNING: This configuration option is part of the incomplete +# driver composition work, changing it's setting has no +# effect. Specify the list of deploy interfaces to load during +# service initialization. Missing deploy interfaces, or deploy +# interfaces which fail to initialize, will prevent the +# ironic-conductor service from starting. The default value is +# a recommended set of production-oriented deploy interfaces. +# A complete list of deploy interfaces present on your system +# may be found by enumerating the +# "ironic.hardware.interfaces.deploy" entrypoint. When setting +# this value, please make sure that every enabled hardware +# type will have the same set of enabled deploy interfaces on +# every ironic-conductor service. (list value) +#enabled_deploy_interfaces = + +# WARNING: This configuration option is part of the incomplete +# driver composition work, changing it's setting has no +# effect. Default deploy interface to be used for nodes that +# do not have deploy_interface field set. A complete list of +# deploy interfaces present on your system may be found by +# enumerating the "ironic.hardware.interfaces.deploy" +# entrypoint. (string value) +#default_deploy_interface = + +# WARNING: This configuration option is part of the incomplete +# driver composition work, changing it's setting has no +# effect. Specify the list of inspect interfaces to load +# during service initialization. Missing inspect interfaces, +# or inspect interfaces which fail to initialize, will prevent +# the ironic-conductor service from starting. The default +# value is a recommended set of production-oriented inspect +# interfaces. A complete list of inspect interfaces present on +# your system may be found by enumerating the +# "ironic.hardware.interfaces.inspect" entrypoint. When +# setting this value, please make sure that every enabled +# hardware type will have the same set of enabled inspect +# interfaces on every ironic-conductor service. (list value) +#enabled_inspect_interfaces = no-inspect + +# WARNING: This configuration option is part of the incomplete +# driver composition work, changing it's setting has no +# effect. Default inspect interface to be used for nodes that +# do not have inspect_interface field set. A complete list of +# inspect interfaces present on your system may be found by +# enumerating the "ironic.hardware.interfaces.inspect" +# entrypoint. (string value) +#default_inspect_interface = + +# WARNING: This configuration option is part of the incomplete +# driver composition work, changing it's setting has no +# effect. Specify the list of management interfaces to load +# during service initialization. Missing management +# interfaces, or management interfaces which fail to +# initialize, will prevent the ironic-conductor service from +# starting. The default value is a recommended set of +# production-oriented management interfaces. A complete list +# of management interfaces present on your system may be found +# by enumerating the "ironic.hardware.interfaces.management" +# entrypoint. When setting this value, please make sure that +# every enabled hardware type will have the same set of +# enabled management interfaces on every ironic-conductor +# service. (list value) +#enabled_management_interfaces = + +# WARNING: This configuration option is part of the incomplete +# driver composition work, changing it's setting has no +# effect. Default management interface to be used for nodes +# that do not have management_interface field set. A complete +# list of management interfaces present on your system may be +# found by enumerating the +# "ironic.hardware.interfaces.management" entrypoint. (string +# value) +#default_management_interface = + # Specify the list of network interfaces to load during # service initialization. Missing network interfaces, or # network interfaces which fail to initialize, will prevent -# the conductor service from starting. The option default is a -# recommended set of production-oriented network interfaces. A -# complete list of network interfaces present on your system -# may be found by enumerating the -# "ironic.hardware.interfaces.network" entrypoint. This value -# must be the same on all ironic-conductor and ironic-api -# services, because it is used by ironic-api service to -# validate a new or updated node's network_interface value. -# (list value) +# the ironic-conductor service from starting. The default +# value is a recommended set of production-oriented network +# interfaces. A complete list of network interfaces present on +# your system may be found by enumerating the +# "ironic.hardware.interfaces.network" entrypoint. When +# setting this value, please make sure that every enabled +# hardware type will have the same set of enabled network +# interfaces on every ironic-conductor service. (list value) #enabled_network_interfaces = flat,noop # Default network interface to be used for nodes that do not @@ -51,6 +185,78 @@ # entrypoint. (string value) #default_network_interface = +# WARNING: This configuration option is part of the incomplete +# driver composition work, changing it's setting has no +# effect. Specify the list of power interfaces to load during +# service initialization. Missing power interfaces, or power +# interfaces which fail to initialize, will prevent the +# ironic-conductor service from starting. The default value is +# a recommended set of production-oriented power interfaces. A +# complete list of power interfaces present on your system may +# be found by enumerating the +# "ironic.hardware.interfaces.power" entrypoint. When setting +# this value, please make sure that every enabled hardware +# type will have the same set of enabled power interfaces on +# every ironic-conductor service. (list value) +#enabled_power_interfaces = + +# WARNING: This configuration option is part of the incomplete +# driver composition work, changing it's setting has no +# effect. Default power interface to be used for nodes that do +# not have power_interface field set. A complete list of power +# interfaces present on your system may be found by +# enumerating the "ironic.hardware.interfaces.power" +# entrypoint. (string value) +#default_power_interface = + +# WARNING: This configuration option is part of the incomplete +# driver composition work, changing it's setting has no +# effect. Specify the list of raid interfaces to load during +# service initialization. Missing raid interfaces, or raid +# interfaces which fail to initialize, will prevent the +# ironic-conductor service from starting. The default value is +# a recommended set of production-oriented raid interfaces. A +# complete list of raid interfaces present on your system may +# be found by enumerating the +# "ironic.hardware.interfaces.raid" entrypoint. When setting +# this value, please make sure that every enabled hardware +# type will have the same set of enabled raid interfaces on +# every ironic-conductor service. (list value) +#enabled_raid_interfaces = no-raid + +# WARNING: This configuration option is part of the incomplete +# driver composition work, changing it's setting has no +# effect. Default raid interface to be used for nodes that do +# not have raid_interface field set. A complete list of raid +# interfaces present on your system may be found by +# enumerating the "ironic.hardware.interfaces.raid" +# entrypoint. (string value) +#default_raid_interface = + +# WARNING: This configuration option is part of the incomplete +# driver composition work, changing it's setting has no +# effect. Specify the list of vendor interfaces to load during +# service initialization. Missing vendor interfaces, or vendor +# interfaces which fail to initialize, will prevent the +# ironic-conductor service from starting. The default value is +# a recommended set of production-oriented vendor interfaces. +# A complete list of vendor interfaces present on your system +# may be found by enumerating the +# "ironic.hardware.interfaces.vendor" entrypoint. When setting +# this value, please make sure that every enabled hardware +# type will have the same set of enabled vendor interfaces on +# every ironic-conductor service. (list value) +#enabled_vendor_interfaces = no-vendor + +# WARNING: This configuration option is part of the incomplete +# driver composition work, changing it's setting has no +# effect. Default vendor interface to be used for nodes that +# do not have vendor_interface field set. A complete list of +# vendor interfaces present on your system may be found by +# enumerating the "ironic.hardware.interfaces.vendor" +# entrypoint. (string value) +#default_vendor_interface = + # Used if there is a formatting error when generating an # exception message (a programming error). If True, raise an # exception; if False, use the unformatted message. (boolean diff --git a/ironic/common/driver_factory.py b/ironic/common/driver_factory.py index 64ffc377dc..fead4ef548 100644 --- a/ironic/common/driver_factory.py +++ b/ironic/common/driver_factory.py @@ -23,6 +23,8 @@ from ironic.common import exception from ironic.common.i18n import _LI, _LW from ironic.conf import CONF from ironic.drivers import base as driver_base +from ironic.drivers import fake_hardware +from ironic.drivers import hardware_type LOG = log.getLogger(__name__) @@ -34,71 +36,240 @@ def build_driver_for_task(task, driver_name=None): """Builds a composable driver for a given task. Starts with a `BareDriver` object, and attaches implementations of the - various driver interfaces to it. Currently these all come from the - monolithic driver singleton, but later will come from separate - driver factories and configurable via the database. + various driver interfaces to it. For classic drivers these all come from + the monolithic driver singleton, for hardware types - from separate + driver factories and are configurable via the database. :param task: The task containing the node to build a driver for. - :param driver_name: The name of the monolithic driver to use as a base, - if different than task.node.driver. + :param driver_name: The name of the classic driver or hardware type to use + as a base, if different than task.node.driver. :returns: A driver object for the task. - :raises: DriverNotFound if node.driver could not be - found in the "ironic.drivers" namespace. + :raises: DriverNotFound if node.driver could not be found in either + "ironic.drivers" or "ironic.hardware.types" namespaces. :raises: InterfaceNotFoundInEntrypoint if some node interfaces are set to invalid or unsupported values. + :raises: IncompatibleInterface if driver is a hardware type and + the requested implementation is not compatible with it. """ node = task.node - check_and_update_node_interfaces(node) - driver = driver_base.BareDriver() - _attach_interfaces_to_driver(driver, node, driver_name=driver_name) - return driver + driver_name = driver_name or node.driver + + driver_or_hw_type = get_driver_or_hardware_type(driver_name) + check_and_update_node_interfaces(node, driver_or_hw_type=driver_or_hw_type) + + bare_driver = driver_base.BareDriver() + _attach_interfaces_to_driver(bare_driver, node, driver_or_hw_type) + + return bare_driver -def _attach_interfaces_to_driver(driver, node, driver_name=None): - driver_singleton = get_driver(driver_name or node.driver) - for iface in driver_singleton.all_interfaces: - impl = getattr(driver_singleton, iface, None) - setattr(driver, iface, impl) +def _attach_interfaces_to_driver(bare_driver, node, driver_or_hw_type): + """Attach interface implementations to a bare driver object. - network_iface = node.network_interface - network_factory = NetworkInterfaceFactory() + For classic drivers, copies implementations from the singleton driver + object, then attaches the dynamic interfaces (network_interface for classic + drivers, all interfaces for dynamic drivers made of hardware types). + + For hardware types, load all interface implementations dynamically. + + :param bare_driver: BareDriver instance to attach interfaces to + :param node: Node object + :param driver_or_hw_type: classic driver or hardware type instance + :raises: InterfaceNotFoundInEntrypoint if the entry point was not found. + :raises: IncompatibleInterface if driver is a hardware type and + the requested implementation is not compatible with it. + """ + if isinstance(driver_or_hw_type, hardware_type.AbstractHardwareType): + # For hardware types all interfaces are dynamic + dynamic_interfaces = _INTERFACE_LOADERS + else: + # Copy implementations from the classic driver singleton + for iface in driver_or_hw_type.all_interfaces: + impl = getattr(driver_or_hw_type, iface, None) + setattr(bare_driver, iface, impl) + + # NOTE(dtantsur): only network interface is dynamic for classic + # drivers, thus it requires separate treatment. + dynamic_interfaces = ['network'] + + for iface in dynamic_interfaces: + impl_name = getattr(node, '%s_interface' % iface) + impl = _get_interface(driver_or_hw_type, iface, impl_name) + setattr(bare_driver, iface, impl) + + +def _get_interface(driver_or_hw_type, interface_type, interface_name): + """Get interface implementation instance. + + For hardware types also validates compatibility. + + :param driver_or_hw_type: a hardware type or classic driver instance. + :param interface_type: name of the interface type (e.g. 'boot'). + :param interface_name: name of the interface implementation from an + appropriate entry point + (ironic.hardware.interfaces.). + :returns: instance of the requested interface implementation. + :raises: InterfaceNotFoundInEntrypoint if the entry point was not found. + :raises: IncompatibleInterface if driver_or_hw_type is a hardware type and + the requested implementation is not compatible with it. + """ + factory = _INTERFACE_LOADERS[interface_type]() try: - net_driver = network_factory.get_driver(network_iface) + impl_instance = factory.get_driver(interface_name) except KeyError: raise exception.InterfaceNotFoundInEntrypoint( - iface=network_iface, - entrypoint=network_factory._entrypoint_name, - valid=network_factory.names) - driver.network = net_driver + iface=interface_name, + entrypoint=factory._entrypoint_name, + valid=factory.names) + + if not isinstance(driver_or_hw_type, hardware_type.AbstractHardwareType): + # NOTE(dtantsur): classic drivers do not have notion of compatibility + return impl_instance + + if isinstance(driver_or_hw_type, fake_hardware.FakeHardware): + # NOTE(dtantsur): special-case fake hardware type to allow testing with + # any combinations of interface implementations. + return impl_instance + + supported_impls = getattr(driver_or_hw_type, + 'supported_%s_interfaces' % interface_type) + if type(impl_instance) not in supported_impls: + raise exception.IncompatibleInterface( + interface_type=interface_type, interface_impl=impl_instance, + hardware_type=driver_or_hw_type.__class__.__name__) + + return impl_instance -def check_and_update_node_interfaces(node): +def _default_interface(hardware_type, interface_type, factory): + """Calculate and return the default interface implementation. + + Finds the first implementation that is supported by the hardware type + and is enabled in the configuration. + + :param hardware_type: hardware type instance. + :param interface_type: type of the interface (e.g. 'boot'). + :param factory: interface factory class to use for loading implementations. + :returns: an entrypoint name of the calculated default implementation + or None if no default implementation can be found. + :raises: InterfaceNotFoundInEntrypoint if the entry point was not found. + """ + supported = getattr(hardware_type, + 'supported_%s_interfaces' % interface_type) + # Mapping of classes to entry points + enabled = {obj.__class__: name for (name, obj) in factory().items()} + + # Order of the supported list matters + for impl_class in supported: + try: + return enabled[impl_class] + except KeyError: + pass + + +def check_and_update_node_interfaces(node, driver_or_hw_type=None): """Ensure that node interfaces (e.g. for creation or updating) are valid. - Updates interfaces with calculated defaults, if they are not provided. + Updates (but doesn't save to the database) hardware interfaces with + calculated defaults, if they are not provided. + + This function is run on node updating and creation, as well as each time + a driver instance is built for a node. :param node: node object to check and potentially update - :raises: InterfaceNotFoundInEntrypoint on validation failure + :param driver_or_hw_type: classic driver or hardware type instance object; + will be detected from node.driver if missing :returns: True if any changes were made to the node, otherwise False + :raises: InterfaceNotFoundInEntrypoint on validation failure + :raises: NoValidDefaultForInterface if the default value cannot be + calculated and is not provided in the configuration + :raises: DriverNotFound if the node's driver or hardware type is not found """ - # NOTE(dtantsur): objects raise NotImplementedError on accessing fields - # that are known, but missing from an object. Thus, we cannot just use - # getattr(node, 'network_interface', None) here. - if 'network_interface' in node and node.network_interface is not None: - if node.network_interface not in CONF.enabled_network_interfaces: - raise exception.InterfaceNotFoundInEntrypoint( - iface=node.network_interface, - entrypoint=NetworkInterfaceFactory._entrypoint_name, - valid=NetworkInterfaceFactory().names) + if driver_or_hw_type is None: + driver_or_hw_type = get_driver_or_hardware_type(node.driver) + is_hardware_type = isinstance(driver_or_hw_type, + hardware_type.AbstractHardwareType) + + # Legacy network interface defaults + additional_defaults = { + 'network': 'flat' if CONF.dhcp.dhcp_provider == 'neutron' else 'noop' + } + + if is_hardware_type: + factories = _INTERFACE_LOADERS else: - node.network_interface = ( - CONF.default_network_interface or - ('flat' if CONF.dhcp.dhcp_provider == 'neutron' else 'noop')) - return True + # Only network interface is dynamic for classic drivers + factories = {'network': _INTERFACE_LOADERS['network']} - return False + # Result - whether the node object was modified + result = False + + # Walk through all dynamic interfaces and check/update them + for iface, factory in factories.items(): + field_name = '%s_interface' % iface + # NOTE(dtantsur): objects raise NotImplementedError on accessing fields + # that are known, but missing from an object. Thus, we cannot just use + # getattr(node, field_name, None) here. + if field_name in node: + impl_name = getattr(node, field_name) + if impl_name is not None: + # Check that the provided value is correct for this type + _get_interface(driver_or_hw_type, iface, impl_name) + # Not changing the result, proceeding with the next interface + continue + + # The fallback default from the configuration + impl_name = getattr(CONF, 'default_%s_interface' % iface) + if impl_name is None: + impl_name = additional_defaults.get(iface) + + if impl_name is not None: + # Check that the default is correct for this type + _get_interface(driver_or_hw_type, iface, impl_name) + elif is_hardware_type: + impl_name = _default_interface(driver_or_hw_type, iface, factory) + + if impl_name is None: + raise exception.NoValidDefaultForInterface( + interface_type=iface, node=node.uuid, driver=node.driver) + + # Set the calculated default and set result to True + setattr(node, field_name, impl_name) + result = True + + return result +def get_driver_or_hardware_type(name): + """Get driver or hardware type by its entry point name. + + First, checks the hardware types namespace, then checks the classic + drivers namespace. The first object found is returned. + + :param name: entry point name. + :returns: An instance of a hardware type or a classic driver. + :raises: DriverNotFound if neither hardware type nor classic driver found. + """ + try: + return get_hardware_type(name) + except exception.DriverNotFound: + return get_driver(name) + + +def get_hardware_type(hardware_type): + """Get a hardware type instance by name. + + :param hardware_type: the name of the hardware type to find + :returns: An instance of ironic.drivers.hardware_type.AbstractHardwareType + :raises: DriverNotFound if requested hardware type cannot be found + """ + try: + return HardwareTypesFactory().get_driver(hardware_type) + except KeyError: + raise exception.DriverNotFound(driver_name=hardware_type) + + +# TODO(dtantsur): rename to get_classic_driver def get_driver(driver_name): """Simple method to get a ref to an instance of a driver. @@ -234,7 +405,7 @@ class BaseDriverFactory(object): # just in case more than one could not be found ... names = ', '.join(names) raise exception.DriverNotFoundInEntrypoint( - driver_name=names, entrypoint=cls._entrypoint_name) + names=names, entrypoint=cls._entrypoint_name) # warn for any untested/unsupported/deprecated drivers or interfaces cls._extension_manager.map(cls._extension_manager.names(), @@ -248,6 +419,10 @@ class BaseDriverFactory(object): """The list of driver names available.""" return self._extension_manager.names() + def items(self): + """Iterator over pairs (name, instance).""" + return ((ext.name, ext.obj) for ext in self._extension_manager) + def _warn_if_unsupported(ext): if not ext.obj.supported: @@ -260,6 +435,21 @@ class DriverFactory(BaseDriverFactory): _enabled_driver_list_config_option = 'enabled_drivers' -class NetworkInterfaceFactory(BaseDriverFactory): - _entrypoint_name = 'ironic.hardware.interfaces.network' - _enabled_driver_list_config_option = 'enabled_network_interfaces' +class HardwareTypesFactory(BaseDriverFactory): + _entrypoint_name = 'ironic.hardware.types' + _enabled_driver_list_config_option = 'enabled_hardware_types' + + +_INTERFACE_LOADERS = { + name: type('%sInterfaceFactory' % name.capitalize(), + (BaseDriverFactory,), + {'_entrypoint_name': 'ironic.hardware.interfaces.%s' % name, + '_enabled_driver_list_config_option': + 'enabled_%s_interfaces' % name}) + for name in driver_base.ALL_INTERFACES +} + + +# TODO(dtantsur): This factory is still used explicitly in many places, +# refactor them later to use _INTERFACE_LOADERS. +NetworkInterfaceFactory = _INTERFACE_LOADERS['network'] diff --git a/ironic/common/exception.py b/ironic/common/exception.py index b0e0c3d04b..cfdb6a40e7 100644 --- a/ironic/common/exception.py +++ b/ironic/common/exception.py @@ -301,13 +301,18 @@ class DHCPLoadError(IronicException): "reason: %(reason)s") +# TODO(dtantsur): word "driver" is overused in class names here, and generally +# means stevedore driver, not ironic driver. Rename them in the future. + + class DriverNotFound(NotFound): - _msg_fmt = _("Could not find the following driver(s): %(driver_name)s.") + _msg_fmt = _("Could not find the following driver(s) or hardware type(s): " + "%(driver_name)s.") class DriverNotFoundInEntrypoint(DriverNotFound): - _msg_fmt = _("Could not find the following driver(s) in the " - "'%(entrypoint)s' entrypoint: %(driver_name)s.") + _msg_fmt = _("Could not find the following items in the " + "'%(entrypoint)s' entrypoint: %(names)s.") class InterfaceNotFoundInEntrypoint(InvalidParameterValue): @@ -316,6 +321,17 @@ class InterfaceNotFoundInEntrypoint(InvalidParameterValue): "are %(valid)s.") +class IncompatibleInterface(InvalidParameterValue): + _msg_fmt = _("%(interface_type)s interface implementation " + "'%(interface_impl)s' is not supported by hardware type " + "%(hardware_type)s.") + + +class NoValidDefaultForInterface(InvalidParameterValue): + _msg_fmt = _("No default value found for %(interface_type)s interface " + "for node %(node)s with driver or hardware type %(driver)s.") + + class ImageNotFound(NotFound): _msg_fmt = _("Image %(image_id)s could not be found.") @@ -551,7 +567,8 @@ class ConfigInvalid(IronicException): class DriverLoadError(IronicException): - _msg_fmt = _("Driver %(driver)s could not be loaded. Reason: %(reason)s.") + _msg_fmt = _("Driver, hardware type or interface %(driver)s could not be " + "loaded. Reason: %(reason)s.") class ConsoleError(IronicException): diff --git a/ironic/conductor/manager.py b/ironic/conductor/manager.py index 81931727ef..d288de3cbc 100644 --- a/ironic/conductor/manager.py +++ b/ironic/conductor/manager.py @@ -66,6 +66,7 @@ from ironic.conductor import notification_utils as notify_utils from ironic.conductor import task_manager from ironic.conductor import utils from ironic.conf import CONF +from ironic.drivers import base as drivers_base from ironic import objects from ironic.objects import base as objects_base @@ -92,7 +93,10 @@ class ConductorManager(base_manager.BaseConductorManager): @METRICS.timer('ConductorManager.create_node') @messaging.expected_exceptions(exception.InvalidParameterValue, - exception.InterfaceNotFoundInEntrypoint) + exception.InterfaceNotFoundInEntrypoint, + exception.IncompatibleInterface, + exception.NoValidDefaultForInterface, + exception.DriverNotFound) def create_node(self, context, node_obj): """Create a node in database. @@ -101,7 +105,12 @@ class ConductorManager(base_manager.BaseConductorManager): :returns: created node object. :raises: InterfaceNotFoundInEntrypoint if validation fails for any dynamic interfaces (e.g. network_interface). + :raises: IncompatibleInterface if one or more of the requested + interfaces are not compatible with the hardware type. + :raises: NoValidDefaultForInterface if no default can be calculated + for some interfaces, and explicit values must be provided. :raises: InvalidParameterValue if some fields fail validation. + :raises: DriverNotFound if the driver or hardware type is not found. """ LOG.debug("RPC create_node called for node %s.", node_obj.uuid) driver_factory.check_and_update_node_interfaces(node_obj) @@ -112,7 +121,10 @@ class ConductorManager(base_manager.BaseConductorManager): @messaging.expected_exceptions(exception.InvalidParameterValue, exception.NodeLocked, exception.InvalidState, - exception.InterfaceNotFoundInEntrypoint) + exception.InterfaceNotFoundInEntrypoint, + exception.IncompatibleInterface, + exception.NoValidDefaultForInterface, + exception.DriverNotFound) def update_node(self, context, node_obj): """Update a node with the supplied data. @@ -134,17 +146,24 @@ class ConductorManager(base_manager.BaseConductorManager): if 'maintenance' in delta and not node_obj.maintenance: node_obj.maintenance_reason = None - if 'network_interface' in delta: - allowed_update_states = [states.ENROLL, states.INSPECTING, - states.MANAGEABLE] + # TODO(dtantsur): reconsider allowing changing some (but not all) + # interfaces for active nodes in the future. + allowed_update_states = [states.ENROLL, states.INSPECTING, + states.MANAGEABLE] + for iface in drivers_base.ALL_INTERFACES: + interface_field = '%s_interface' % iface + if interface_field not in delta: + continue + if not (node_obj.provision_state in allowed_update_states or node_obj.maintenance): - action = _("Node %(node)s can not have network_interface " + action = _("Node %(node)s can not have %(iface)s " "updated unless it is in one of allowed " "(%(allowed)s) states or in maintenance mode.") raise exception.InvalidState( action % {'node': node_obj.uuid, - 'allowed': ', '.join(allowed_update_states)}) + 'allowed': ', '.join(allowed_update_states), + 'iface': interface_field}) driver_factory.check_and_update_node_interfaces(node_obj) diff --git a/ironic/conf/default.py b/ironic/conf/default.py index b3f2f07ee7..6df090ad08 100644 --- a/ironic/conf/default.py +++ b/ironic/conf/default.py @@ -26,6 +26,42 @@ from oslo_utils import netutils from ironic.common.i18n import _ + +# TODO(dtantsur): remove the variants with warnings as soon as we support +# actually creating nodes with hardware types. + +_ENABLED_IFACE_HELP = _('Specify the list of {0} interfaces to load during ' + 'service initialization. Missing {0} interfaces, ' + 'or {0} interfaces which fail to initialize, will ' + 'prevent the ironic-conductor service from starting. ' + 'The default value is a recommended set of ' + 'production-oriented {0} interfaces. A complete ' + 'list of {0} interfaces present on your system may ' + 'be found by enumerating the ' + '"ironic.hardware.interfaces.{0}" entrypoint. ' + 'When setting this value, please make sure that ' + 'every enabled hardware type will have the same ' + 'set of enabled {0} interfaces on every ' + 'ironic-conductor service.') + +_ENABLED_IFACE_HELP_WITH_WARNING = ( + _('WARNING: This configuration option is part of the incomplete driver ' + 'composition work, changing it\'s setting has no effect. ') + + _ENABLED_IFACE_HELP +) + +_DEFAULT_IFACE_HELP = _('Default {0} interface to be used for nodes that ' + 'do not have {0}_interface field set. A complete ' + 'list of {0} interfaces present on your system may ' + 'be found by enumerating the ' + '"ironic.hardware.interfaces.{0}" entrypoint.') + +_DEFAULT_IFACE_HELP_WITH_WARNING = ( + _('WARNING: This configuration option is part of the incomplete driver ' + 'composition work, changing it\'s setting has no effect. ') + + _DEFAULT_IFACE_HELP +) + api_opts = [ cfg.StrOpt( 'auth_strategy', @@ -57,27 +93,67 @@ driver_opts = [ 'be found by enumerating the "ironic.drivers" ' 'entrypoint. An example may be found in the ' 'developer documentation online.')), + cfg.ListOpt('enabled_hardware_types', + default=[], + help=_('WARNING: This configuration option is part of the ' + 'incomplete driver composition work, changing it\'s ' + 'setting has no effect. ' + 'Specify the list of hardware types to load during ' + 'service initialization. Missing hardware types, or ' + 'hardware types which fail to initialize, will prevent ' + 'the conductor service from starting. No hardware ' + 'types are enabled by default now, but in the future ' + 'this option will default to a recommended set of ' + 'production-oriented hardware types. ' + 'A complete list of hardware types present on your ' + 'system may be found by enumerating the ' + '"ironic.hardware.types" entrypoint.')), + # TODO(dtantsur): populate with production-ready values + cfg.ListOpt('enabled_boot_interfaces', + default=[], + help=_ENABLED_IFACE_HELP_WITH_WARNING.format('boot')), + cfg.StrOpt('default_boot_interface', + help=_DEFAULT_IFACE_HELP_WITH_WARNING.format('boot')), + cfg.ListOpt('enabled_console_interfaces', + default=['no-console'], + help=_ENABLED_IFACE_HELP_WITH_WARNING.format('console')), + cfg.StrOpt('default_console_interface', + help=_DEFAULT_IFACE_HELP_WITH_WARNING.format('console')), + cfg.ListOpt('enabled_deploy_interfaces', + default=[], + help=_ENABLED_IFACE_HELP_WITH_WARNING.format('deploy')), + cfg.StrOpt('default_deploy_interface', + help=_DEFAULT_IFACE_HELP_WITH_WARNING.format('deploy')), + cfg.ListOpt('enabled_inspect_interfaces', + default=['no-inspect'], + help=_ENABLED_IFACE_HELP_WITH_WARNING.format('inspect')), + cfg.StrOpt('default_inspect_interface', + help=_DEFAULT_IFACE_HELP_WITH_WARNING.format('inspect')), + cfg.ListOpt('enabled_management_interfaces', + default=[], + help=_ENABLED_IFACE_HELP_WITH_WARNING.format('management')), + cfg.StrOpt('default_management_interface', + help=_DEFAULT_IFACE_HELP_WITH_WARNING.format('management')), cfg.ListOpt('enabled_network_interfaces', default=['flat', 'noop'], - help=_('Specify the list of network interfaces to load during ' - 'service initialization. Missing network interfaces, ' - 'or network interfaces which fail to initialize, will ' - 'prevent the conductor service from starting. The ' - 'option default is a recommended set of ' - 'production-oriented network interfaces. A complete ' - 'list of network interfaces present on your system may ' - 'be found by enumerating the ' - '"ironic.hardware.interfaces.network" entrypoint. ' - 'This value must be the same on all ironic-conductor ' - 'and ironic-api services, because it is used by ' - 'ironic-api service to validate a new or updated ' - 'node\'s network_interface value.')), + help=_ENABLED_IFACE_HELP.format('network')), cfg.StrOpt('default_network_interface', - help=_('Default network interface to be used for nodes that ' - 'do not have network_interface field set. A complete ' - 'list of network interfaces present on your system may ' - 'be found by enumerating the ' - '"ironic.hardware.interfaces.network" entrypoint.')) + help=_DEFAULT_IFACE_HELP.format('network')), + cfg.ListOpt('enabled_power_interfaces', + default=[], + help=_ENABLED_IFACE_HELP_WITH_WARNING.format('power')), + cfg.StrOpt('default_power_interface', + help=_DEFAULT_IFACE_HELP_WITH_WARNING.format('power')), + cfg.ListOpt('enabled_raid_interfaces', + default=['no-raid'], + help=_ENABLED_IFACE_HELP_WITH_WARNING.format('raid')), + cfg.StrOpt('default_raid_interface', + help=_DEFAULT_IFACE_HELP_WITH_WARNING.format('raid')), + cfg.ListOpt('enabled_vendor_interfaces', + default=['no-vendor'], + help=_ENABLED_IFACE_HELP_WITH_WARNING.format('vendor')), + cfg.StrOpt('default_vendor_interface', + help=_DEFAULT_IFACE_HELP_WITH_WARNING.format('vendor')), ] exc_log_opts = [ diff --git a/ironic/drivers/fake_hardware.py b/ironic/drivers/fake_hardware.py new file mode 100644 index 0000000000..769a6664d8 --- /dev/null +++ b/ironic/drivers/fake_hardware.py @@ -0,0 +1,72 @@ +# Copyright 2016 Red Hat, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +""" +Fake hardware type. +""" + +from ironic.drivers import hardware_type +from ironic.drivers.modules import fake + + +class FakeHardware(hardware_type.AbstractHardwareType): + """Fake hardware type. + + This hardware type is special-cased in the driver factory to bypass + compatibility verification. Thus, supported_* methods here are only + for calculating the defaults, not for actual check. + + All fake implementations are still expected to be enabled in the + configuration. + """ + + @property + def supported_boot_interfaces(self): + """List of classes of supported boot interfaces.""" + return [fake.FakeBoot] + + @property + def supported_console_interfaces(self): + """List of classes of supported console interfaces.""" + return [fake.FakeConsole] + + @property + def supported_deploy_interfaces(self): + """List of classes of supported deploy interfaces.""" + return [fake.FakeDeploy] + + @property + def supported_inspect_interfaces(self): + """List of classes of supported inspect interfaces.""" + return [fake.FakeInspect] + + @property + def supported_management_interfaces(self): + """List of classes of supported management interfaces.""" + return [fake.FakeManagement] + + @property + def supported_power_interfaces(self): + """List of classes of supported power interfaces.""" + return [fake.FakePower] + + @property + def supported_raid_interfaces(self): + """List of classes of supported raid interfaces.""" + return [fake.FakeRAID] + + @property + def supported_vendor_interfaces(self): + """List of classes of supported rescue interfaces.""" + return [fake.FakeVendorB, fake.FakeVendorA] diff --git a/ironic/drivers/hardware_type.py b/ironic/drivers/hardware_type.py new file mode 100644 index 0000000000..71287235ab --- /dev/null +++ b/ironic/drivers/hardware_type.py @@ -0,0 +1,86 @@ +# Copyright 2016 Red Hat, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +""" +Abstract base class for all hardware types. +""" + +import abc + +import six + +from ironic.drivers.modules.network import noop as noop_net +from ironic.drivers.modules import noop + + +@six.add_metaclass(abc.ABCMeta) +class AbstractHardwareType(object): + """Abstract base class for all hardware types. + + Hardware type is a family of hardware supporting the same set of interfaces + from the ironic standpoint. This can be as wide as all hardware supporting + the IPMI protocol or as narrow as several hardware models supporting some + specific interfaces. + + A hardware type defines an ordered list of supported implementations for + each driver interface (power, deploy, etc). + """ + + supported = True + """Whether hardware is supported by the community.""" + + # Required hardware interfaces + + @abc.abstractproperty + def supported_boot_interfaces(self): + """List of supported boot interfaces.""" + + @abc.abstractproperty + def supported_deploy_interfaces(self): + """List of supported deploy interfaces.""" + + @abc.abstractproperty + def supported_management_interfaces(self): + """List of supported management interfaces.""" + + @abc.abstractproperty + def supported_power_interfaces(self): + """List of supported power interfaces.""" + + # Optional hardware interfaces + + @property + def supported_console_interfaces(self): + """List of supported console interfaces.""" + return [noop.NoConsole] + + @property + def supported_inspect_interfaces(self): + """List of supported inspect interfaces.""" + return [noop.NoInspect] + + @property + def supported_network_interfaces(self): + """List of supported network interfaces.""" + return [noop_net.NoopNetwork] + + @property + def supported_raid_interfaces(self): + """List of supported raid interfaces.""" + return [noop.NoRAID] + + @property + def supported_vendor_interfaces(self): + """List of supported vendor interfaces.""" + return [noop.NoVendor] diff --git a/ironic/tests/base.py b/ironic/tests/base.py index f0afb5f207..3f5f2ed41a 100644 --- a/ironic/tests/base.py +++ b/ironic/tests/base.py @@ -40,6 +40,7 @@ from ironic.common import context as ironic_context from ironic.common import driver_factory from ironic.common import hash_ring from ironic.conf import CONF +from ironic.drivers import base as drivers_base from ironic.objects import base as objects_base from ironic.tests.unit import policy_fixture @@ -112,7 +113,9 @@ class TestCase(testtools.TestCase): self.policy = self.useFixture(policy_fixture.PolicyFixture()) driver_factory.DriverFactory._extension_manager = None - driver_factory.NetworkInterfaceFactory._extension_manager = None + driver_factory.HardwareTypesFactory._extension_manager = None + for factory in driver_factory._INTERFACE_LOADERS.values(): + factory._extension_manager = None def _set_config(self): self.cfg_fixture = self.useFixture(config_fixture.Config(CONF)) @@ -124,8 +127,10 @@ class TestCase(testtools.TestCase): self.config(provisioning_network=uuidutils.generate_uuid(), group='neutron') self.config(enabled_drivers=['fake']) - self.config(enabled_network_interfaces=['flat', 'noop', 'neutron'], - default_network_interface=None) + self.config(enabled_hardware_types=['fake-hardware']) + self.config(enabled_network_interfaces=['flat', 'noop', 'neutron']) + for iface in drivers_base.ALL_INTERFACES: + self.config(**{'default_%s_interface' % iface: None}) self.set_defaults(host='fake-mini', debug=True) self.set_defaults(connection="sqlite://", diff --git a/ironic/tests/unit/__init__.py b/ironic/tests/unit/__init__.py index dcc2f0d687..d1cd870f2b 100644 --- a/ironic/tests/unit/__init__.py +++ b/ironic/tests/unit/__init__.py @@ -34,3 +34,7 @@ eventlet.monkey_patch(os=False) # at module import time, because we may be using mock decorators in our # tests that run at import time. objects.register_all() + +# NOTE(dtantsur): this module creates mocks which may be used at random points +# of time, so it must be imported as early as possible. +from ironic.tests.unit.drivers import third_party_driver_mocks # noqa diff --git a/ironic/tests/unit/api/v1/test_nodes.py b/ironic/tests/unit/api/v1/test_nodes.py index c7a1fe71eb..513e0a5058 100644 --- a/ironic/tests/unit/api/v1/test_nodes.py +++ b/ironic/tests/unit/api/v1/test_nodes.py @@ -1732,6 +1732,7 @@ class TestPost(test_api_base.BaseApiTest): def setUp(self): super(TestPost, self).setUp() + self.config(enabled_drivers=['fake']) self.chassis = obj_utils.create_test_chassis(self.context) p = mock.patch.object(rpcapi.ConductorAPI, 'get_topic_for') self.mock_gtf = p.start() diff --git a/ironic/tests/unit/common/test_driver_factory.py b/ironic/tests/unit/common/test_driver_factory.py index b15f746462..5a0c23dd22 100644 --- a/ironic/tests/unit/common/test_driver_factory.py +++ b/ironic/tests/unit/common/test_driver_factory.py @@ -19,6 +19,10 @@ from ironic.common import driver_factory from ironic.common import exception from ironic.conductor import task_manager from ironic.drivers import base as drivers_base +from ironic.drivers import fake_hardware +from ironic.drivers import hardware_type +from ironic.drivers.modules import fake +from ironic.drivers.modules import noop from ironic.tests import base from ironic.tests.unit.db import base as db_base from ironic.tests.unit.objects import utils as obj_utils @@ -135,9 +139,10 @@ class NetworkInterfaceFactoryTestCase(db_base.DbTestCase): factory._entrypoint_name) self.assertEqual(['flat', 'neutron', 'noop'], sorted(factory._enabled_driver_list)) - # NOTE(jroll) 4 checks, one for the driver we're building and - # one for each of the 3 network interfaces - self.assertEqual(4, mock_warn.call_count) + # NOTE(jroll) 5 checks, one for the driver we're building and + # one for each of the 3 network interfaces, the last - for the fake + # hardware type. + self.assertEqual(5, mock_warn.call_count) def test_build_driver_for_task_default_is_none(self): # flat, neutron, and noop network interfaces are enabled in base test @@ -232,3 +237,164 @@ class CheckAndUpdateNodeInterfacesTestCase(db_base.DbTestCase): self.assertRaises(exception.InterfaceNotFoundInEntrypoint, driver_factory.check_and_update_node_interfaces, node) + + +class TestFakeHardware(hardware_type.AbstractHardwareType): + @property + def supported_boot_interfaces(self): + """List of supported boot interfaces.""" + return [fake.FakeBoot] + + @property + def supported_console_interfaces(self): + """List of supported console interfaces.""" + return [fake.FakeConsole] + + @property + def supported_deploy_interfaces(self): + """List of supported deploy interfaces.""" + return [fake.FakeDeploy] + + @property + def supported_inspect_interfaces(self): + """List of supported inspect interfaces.""" + return [fake.FakeInspect] + + @property + def supported_management_interfaces(self): + """List of supported management interfaces.""" + return [fake.FakeManagement] + + @property + def supported_power_interfaces(self): + """List of supported power interfaces.""" + return [fake.FakePower] + + @property + def supported_raid_interfaces(self): + """List of supported raid interfaces.""" + return [fake.FakeRAID] + + @property + def supported_vendor_interfaces(self): + """List of supported rescue interfaces.""" + return [fake.FakeVendorB, fake.FakeVendorA] + + +OPTIONAL_INTERFACES = set(drivers_base.BareDriver().standard_interfaces) - { + 'management', 'boot'} + + +class HardwareTypeLoadTestCase(db_base.DbTestCase): + + def setUp(self): + super(HardwareTypeLoadTestCase, self).setUp() + self.config(dhcp_provider=None, group='dhcp') + self.ifaces = {} + self.node_kwargs = {} + for iface in drivers_base.ALL_INTERFACES: + if iface == 'network': + self.ifaces[iface] = 'noop' + enabled = ['noop'] + else: + self.ifaces[iface] = 'fake' + enabled = ['fake'] + if iface in OPTIONAL_INTERFACES: + enabled.append('no-%s' % iface) + + self.config(**{'enabled_%s_interfaces' % iface: enabled}) + self.node_kwargs['%s_interface' % iface] = self.ifaces[iface] + + def test_get_hardware_type_existing(self): + hw_type = driver_factory.get_hardware_type('fake-hardware') + self.assertIsInstance(hw_type, fake_hardware.FakeHardware) + + def test_get_hardware_type_missing(self): + self.assertRaises(exception.DriverNotFound, + # "fake" is a classic driver + driver_factory.get_hardware_type, 'fake') + + def test_get_driver_or_hardware_type(self): + hw_type = driver_factory.get_driver_or_hardware_type('fake-hardware') + self.assertIsInstance(hw_type, fake_hardware.FakeHardware) + driver = driver_factory.get_driver_or_hardware_type('fake') + self.assertNotIsInstance(driver, fake_hardware.FakeHardware) + + def test_get_driver_or_hardware_type_missing(self): + self.assertRaises(exception.DriverNotFound, + driver_factory.get_driver_or_hardware_type, + 'banana') + + def test_build_driver_for_task(self): + node = obj_utils.create_test_node(self.context, driver='fake-hardware', + **self.node_kwargs) + with task_manager.acquire(self.context, node.id) as task: + for iface in drivers_base.ALL_INTERFACES: + impl = getattr(task.driver, iface) + self.assertIsNotNone(impl) + + def test_build_driver_for_task_incorrect(self): + self.node_kwargs['power_interface'] = 'foobar' + node = obj_utils.create_test_node(self.context, driver='fake-hardware', + **self.node_kwargs) + self.assertRaises(exception.InterfaceNotFoundInEntrypoint, + task_manager.acquire, self.context, node.id) + + def test_build_driver_for_task_fake(self): + # Checks that fake driver is compatible with any interfaces, even those + # which are not declared in supported__interfaces result. + self.node_kwargs['raid_interface'] = 'no-raid' + node = obj_utils.create_test_node(self.context, driver='fake-hardware', + **self.node_kwargs) + with task_manager.acquire(self.context, node.id) as task: + for iface in drivers_base.ALL_INTERFACES: + impl = getattr(task.driver, iface) + self.assertIsNotNone(impl) + self.assertIsInstance(task.driver.raid, noop.NoRAID) + + @mock.patch.object(driver_factory, 'get_hardware_type', autospec=True, + return_value=TestFakeHardware()) + def test_build_driver_for_task_not_fake(self, mock_get_hw_type): + # Checks that other hardware types do check compatibility. + self.node_kwargs['raid_interface'] = 'no-raid' + node = obj_utils.create_test_node(self.context, driver='fake-2', + **self.node_kwargs) + self.assertRaises(exception.IncompatibleInterface, + task_manager.acquire, self.context, node.id) + mock_get_hw_type.assert_called_once_with('fake-2') + + def test_build_driver_for_task_no_defaults(self): + self.config(dhcp_provider=None, group='dhcp') + for iface in drivers_base.ALL_INTERFACES: + if iface != 'network': + self.config(**{'enabled_%s_interfaces' % iface: []}) + self.config(**{'default_%s_interface' % iface: None}) + node = obj_utils.create_test_node(self.context, driver='fake-hardware') + self.assertRaises(exception.NoValidDefaultForInterface, + task_manager.acquire, self.context, node.id) + + def test_build_driver_for_task_calculated_defaults(self): + self.config(dhcp_provider=None, group='dhcp') + node = obj_utils.create_test_node(self.context, driver='fake-hardware') + with task_manager.acquire(self.context, node.id) as task: + for iface in drivers_base.ALL_INTERFACES: + impl = getattr(task.driver, iface) + self.assertIsNotNone(impl) + + def test_build_driver_for_task_configured_defaults(self): + for iface in drivers_base.ALL_INTERFACES: + self.config(**{'default_%s_interface' % iface: self.ifaces[iface]}) + + node = obj_utils.create_test_node(self.context, driver='fake-hardware') + with task_manager.acquire(self.context, node.id) as task: + for iface in drivers_base.ALL_INTERFACES: + impl = getattr(task.driver, iface) + self.assertIsNotNone(impl) + self.assertEqual(self.ifaces[iface], + getattr(task.node, '%s_interface' % iface)) + + def test_build_driver_for_task_bad_default(self): + self.config(default_power_interface='foobar') + node = obj_utils.create_test_node(self.context, driver='fake-hardware') + self.assertRaises(exception.InterfaceNotFoundInEntrypoint, + task_manager.acquire, self.context, node.id) diff --git a/ironic/tests/unit/conductor/test_manager.py b/ironic/tests/unit/conductor/test_manager.py index 5b6394801e..c3e2b75a7e 100644 --- a/ironic/tests/unit/conductor/test_manager.py +++ b/ironic/tests/unit/conductor/test_manager.py @@ -485,10 +485,10 @@ class UpdateNodeTestCase(mgr_utils.ServiceSetUpMixin, # check that it fails because driver not found node.driver = wrong_driver node.driver_info = {} - self.assertRaises(exception.DriverNotFound, - self.service.update_node, - self.context, - node) + exc = self.assertRaises(messaging.rpc.ExpectedException, + self.service.update_node, + self.context, node) + self.assertEqual(exception.DriverNotFound, exc.exc_info[0]) # verify change did not happen node.refresh() diff --git a/setup.cfg b/setup.cfg index 5369d86a41..085e419409 100644 --- a/setup.cfg +++ b/setup.cfg @@ -97,26 +97,45 @@ ironic.drivers = pxe_iscsi_cimc = ironic.drivers.pxe:PXEAndCIMCDriver pxe_agent_cimc = ironic.drivers.agent:AgentAndCIMCDriver +ironic.hardware.interfaces.boot = + fake = ironic.drivers.modules.fake:FakeBoot + ironic.hardware.interfaces.console = + fake = ironic.drivers.modules.fake:FakeConsole no-console = ironic.drivers.modules.noop:NoConsole +ironic.hardware.interfaces.deploy = + fake = ironic.drivers.modules.fake:FakeDeploy + ironic.hardware.interfaces.inspect = + fake = ironic.drivers.modules.fake:FakeInspect no-inspect = ironic.drivers.modules.noop:NoInspect +ironic.hardware.interfaces.management = + fake = ironic.drivers.modules.fake:FakeManagement + ironic.hardware.interfaces.network = flat = ironic.drivers.modules.network.flat:FlatNetwork noop = ironic.drivers.modules.network.noop:NoopNetwork neutron = ironic.drivers.modules.network.neutron:NeutronNetwork +ironic.hardware.interfaces.power = + fake = ironic.drivers.modules.fake:FakePower + ironic.hardware.interfaces.raid = + fake = ironic.drivers.modules.fake:FakeRAID no-raid = ironic.drivers.modules.noop:NoRAID ironic.hardware.interfaces.rescue = no-rescue = ironic.drivers.modules.noop:NoRescue ironic.hardware.interfaces.vendor = + fake = ironic.drivers.modules.fake:FakeVendorB no-vendor = ironic.drivers.modules.noop:NoVendor +ironic.hardware.types = + fake-hardware = ironic.drivers.fake_hardware:FakeHardware + ironic.database.migration_backend = sqlalchemy = ironic.db.sqlalchemy.migration