Support defining and loading hardware types

This change introduces a new module ironic.drivers.hardware_types
with AbstractHardwareType class. It also updates driver_factory
code to support loading hardware types and creating dynamic drivers.
Interfaces validation code extended to cover hardware types.

This change also introduces the FakeHardware class for testing.
It is special-cased to bypass compatibility validation completely.

No hardware types are loaded on conductor start up yet, as hardware
types still do not participate in the hash ring. Thus, nodes with
hardware types cannot still be created via HTTP API.

Change-Id: If8e3342baf818a9e37aa82b43aec71898d48c29b
Partial-Bug: #1524745
This commit is contained in:
Dmitry Tantsur 2016-07-01 17:52:11 +02:00
parent b7f001f441
commit 901171194b
13 changed files with 953 additions and 92 deletions

View File

@ -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 = <None>
# 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 = <None>
# 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 = <None>
# 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 = <None>
# 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 = <None>
# 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 = <None>
# 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 = <None>
# 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 = <None>
# 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 = <None>
# 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

View File

@ -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.<interface type>).
: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']

View File

@ -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):

View File

@ -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)

View File

@ -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 = [

View File

@ -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]

View File

@ -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]

View File

@ -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://",

View File

@ -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

View File

@ -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()

View File

@ -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_<INTERFACE>_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)

View File

@ -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()

View File

@ -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