Add dynamic driver functionality to REST API

This adds API version 1.30, which adds dynamic driver parameters
and response fields to `GET /v1/drivers` and `GET /v1/drivers/<name>`.

Changes RAID APIs to work for dynamic drivers.

Also changes GET /v1/drivers/<name>/properties to work for dynamic
drivers. It uses the calculated default implementation for each
interface, when calculating the properties.

Last, changes node and driver vendor passthru to work correctly
for dynamic drivers. Similar to properties, driver vendor passthru
will use the calculated default vendor implementation.

Change-Id: If13e7e7fd368273e84d9a108be93b58150432fae
Partial-Bug: #1524745
This commit is contained in:
Jim Rollenhagen 2017-01-11 17:01:51 +00:00
parent 1dc154033f
commit e776757812
18 changed files with 802 additions and 69 deletions

View File

@ -2,6 +2,54 @@
REST API Version History
========================
**1.30** (Ocata)
Added dynamic driver APIs.
* GET /v1/drivers now accepts a ``type`` parameter (optional, one of
``classic`` or ``dynamic``), to limit the result to only classic drivers
or dynamic drivers (hardware types). Without this parameter, both
classic and dynamic drivers are returned.
* GET /v1/drivers now accepts a ``detail`` parameter (optional, one of
``True`` or ``False``), to show all fields for a driver. Defaults to
``False``.
* GET /v1/drivers now returns an additional ``type`` field to show if the
driver is classic or dynamic.
* GET /v1/drivers/<name> now returns an additional ``type`` field to show
if the driver is classic or dynamic.
* GET /v1/drivers/<name> now returns additional fields that are null for
classic drivers, and set as following for dynamic drivers:
* The value of the default_<interface-type>_interface is the entrypoint
name of the calculated default interface for that type:
* default_boot_interface
* default_console_interface
* default_deploy_interface
* default_inspect_interface
* default_management_interface
* default_network_interface
* default_power_interface
* default_raid_interface
* default_vendor_interface
* The value of the enabled_<interface-type>_interfaces is a list of
entrypoint names of the enabled interfaces for that type:
* enabled_boot_interfaces
* enabled_console_interfaces
* enabled_deploy_interfaces
* enabled_inspect_interfaces
* enabled_management_interfaces
* enabled_network_interfaces
* enabled_power_interfaces
* enabled_raid_interfaces
* enabled_vendor_interfaces
**1.29** (Ocata)
Add a new management API to support inject NMI,

View File

@ -27,6 +27,7 @@ from ironic.api.controllers.v1 import utils as api_utils
from ironic.api import expose
from ironic.common import exception
from ironic.common import policy
from ironic.drivers import base as driver_base
METRICS = metrics_utils.get_metrics_logger(__name__)
@ -71,14 +72,52 @@ class Driver(base.APIBase):
hosts = [wtypes.text]
"""A list of active conductors that support this driver"""
type = wtypes.text
"""Whether the driver is classic or dynamic (hardware type)"""
links = wsme.wsattr([link.Link], readonly=True)
"""A list containing self and bookmark links"""
properties = wsme.wsattr([link.Link], readonly=True)
"""A list containing links to driver properties"""
"""Default interface for a hardware type"""
default_boot_interface = wtypes.text
default_console_interface = wtypes.text
default_deploy_interface = wtypes.text
default_inspect_interface = wtypes.text
default_management_interface = wtypes.text
default_network_interface = wtypes.text
default_power_interface = wtypes.text
default_raid_interface = wtypes.text
default_vendor_interface = wtypes.text
"""A list of enabled interfaces for a hardware type"""
enabled_boot_interfaces = [wtypes.text]
enabled_console_interfaces = [wtypes.text]
enabled_deploy_interfaces = [wtypes.text]
enabled_inspect_interfaces = [wtypes.text]
enabled_management_interfaces = [wtypes.text]
enabled_network_interfaces = [wtypes.text]
enabled_power_interfaces = [wtypes.text]
enabled_raid_interfaces = [wtypes.text]
enabled_vendor_interfaces = [wtypes.text]
@staticmethod
def convert_with_links(name, hosts):
def convert_with_links(name, hosts, driver_type, detail=False,
interface_info=None):
"""Convert driver/hardware type info to an API-serializable object.
:param name: name of driver or hardware type.
:param hosts: list of conductor hostnames driver is active on.
:param driver_type: 'classic' for classic drivers, 'dynamic' for
hardware types.
:param detail: boolean, whether to include detailed info, such as
the 'type' field and default/enabled interfaces fields.
:param interface_info: optional list of dicts of hardware interface
info.
:returns: API-serializable driver object.
"""
driver = Driver()
driver.name = name
driver.hosts = hosts
@ -101,12 +140,51 @@ class Driver(base.APIBase):
'drivers', name + "/properties",
bookmark=True)
]
if api_utils.allow_dynamic_drivers():
driver.type = driver_type
if driver_type == 'dynamic' and detail:
if interface_info is None:
# TODO(jroll) objectify this
interface_info = (pecan.request.dbapi
.list_hardware_type_interfaces([name]))
for iface_type in driver_base.ALL_INTERFACES:
default = None
enabled = set()
for iface in interface_info:
if iface['interface_type'] == iface_type:
iface_name = iface['interface_name']
enabled.add(iface_name)
# NOTE(jroll) this assumes the default is the same
# on all conductors
if iface['default']:
default = iface_name
default_key = 'default_%s_interface' % iface_type
enabled_key = 'enabled_%s_interfaces' % iface_type
setattr(driver, default_key, default)
setattr(driver, enabled_key, list(enabled))
elif detail:
for iface_type in driver_base.ALL_INTERFACES:
# always return None for classic drivers
setattr(driver, 'default_%s_interface' % iface_type, None)
setattr(driver, 'enabled_%s_interfaces' % iface_type, None)
return driver
@classmethod
def sample(cls):
sample = cls(name="sample-driver",
hosts=["fake-host"])
attrs = {
'name': 'sample-driver',
'hosts': ['fake-host'],
'type': 'classic',
}
for iface_type in driver_base.ALL_INTERFACES:
attrs['default_%s_interface' % iface_type] = None
attrs['enabled_%s_interfaces' % iface_type] = None
sample = cls(**attrs)
return sample
@ -117,11 +195,40 @@ class DriverList(base.APIBase):
"""A list containing drivers objects"""
@staticmethod
def convert_with_links(drivers):
def convert_with_links(drivers, hardware_types, detail=False):
"""Convert drivers and hardware types to an API-serializable object.
:param drivers: dict mapping driver names to conductor hostnames.
:param hardware_types: dict mapping hardware type names to conductor
hostnames.
:param detail: boolean, whether to include detailed info, such as
the 'type' field and default/enabled interfaces fields.
:returns: an API-serializable driver collection object.
"""
collection = DriverList()
collection.drivers = [
Driver.convert_with_links(dname, list(drivers[dname]))
Driver.convert_with_links(dname, list(drivers[dname]), 'classic',
detail=detail)
for dname in drivers]
# NOTE(jroll) we return hardware types in all API versions,
# but restrict type/default/enabled fields to 1.30.
# This is checked in Driver.convert_with_links(), however also
# checking here can save us a DB query.
if api_utils.allow_dynamic_drivers() and detail:
iface_info = pecan.request.dbapi.list_hardware_type_interfaces(
list(hardware_types))
else:
iface_info = []
for htname in hardware_types:
interface_info = [i for i in iface_info
if i['hardware_type'] == htname]
collection.drivers.append(
Driver.convert_with_links(htname,
list(hardware_types[htname]),
'dynamic', detail=detail,
interface_info=interface_info))
return collection
@classmethod
@ -243,8 +350,8 @@ class DriversController(rest.RestController):
}
@METRICS.timer('DriversController.get_all')
@expose.expose(DriverList)
def get_all(self):
@expose.expose(DriverList, wtypes.text, types.boolean)
def get_all(self, type=None, detail=None):
"""Retrieve a list of drivers."""
# FIXME(deva): formatting of the auto-generated REST API docs
# will break from a single-line doc string.
@ -253,8 +360,21 @@ class DriversController(rest.RestController):
cdict = pecan.request.context.to_policy_values()
policy.authorize('baremetal:driver:get', cdict, cdict)
driver_list = pecan.request.dbapi.get_active_driver_dict()
return DriverList.convert_with_links(driver_list)
api_utils.check_allow_driver_detail(detail)
api_utils.check_allow_filter_driver_type(type)
if type not in (None, 'classic', 'dynamic'):
raise exception.Invalid(_(
'"type" filter must be one of "classic" or "dynamic", '
'if specified.'))
driver_list = {}
hw_type_dict = {}
if type is None or type == 'classic':
driver_list = pecan.request.dbapi.get_active_driver_dict()
if type is None or type == 'dynamic':
hw_type_dict = pecan.request.dbapi.get_active_hardware_type_dict()
return DriverList.convert_with_links(driver_list, hw_type_dict,
detail=detail)
@METRICS.timer('DriversController.get_one')
@expose.expose(Driver, wtypes.text)
@ -267,10 +387,20 @@ class DriversController(rest.RestController):
cdict = pecan.request.context.to_policy_values()
policy.authorize('baremetal:driver:get', cdict, cdict)
def _find_driver(driver_dict, driver_type):
for name, hosts in driver_dict.items():
if name == driver_name:
return Driver.convert_with_links(name, list(hosts),
driver_type, detail=True)
hw_type_dict = pecan.request.dbapi.get_active_hardware_type_dict()
driver = _find_driver(hw_type_dict, 'dynamic')
if driver:
return driver
driver_dict = pecan.request.dbapi.get_active_driver_dict()
for name, hosts in driver_dict.items():
if name == driver_name:
return Driver.convert_with_links(name, list(hosts))
driver = _find_driver(driver_dict, 'classic')
if driver:
return driver
raise exception.DriverNotFound(driver_name=driver_name)

View File

@ -350,6 +350,32 @@ def check_allow_specify_resource_class(resource_class):
'opr': versions.MINOR_21_RESOURCE_CLASS})
def check_allow_filter_driver_type(driver_type):
"""Check if filtering drivers by classic/dynamic is allowed.
Version 1.30 of the API allows this.
"""
if driver_type is not None and not allow_dynamic_drivers():
raise exception.NotAcceptable(_(
"Request not acceptable. The minimal required API version "
"should be %(base)s.%(opr)s") %
{'base': versions.BASE_VERSION,
'opr': versions.MINOR_30_DYNAMIC_DRIVERS})
def check_allow_driver_detail(detail):
"""Check if getting detailed driver info is allowed.
Version 1.30 of the API allows this.
"""
if detail is not None and not allow_dynamic_drivers():
raise exception.NotAcceptable(_(
"Request not acceptable. The minimal required API version "
"should be %(base)s.%(opr)s") %
{'base': versions.BASE_VERSION,
'opr': versions.MINOR_30_DYNAMIC_DRIVERS})
def initial_node_provision_state():
"""Return node state to use by default when creating new nodes.
@ -489,6 +515,16 @@ def allow_vifs_subcontroller():
versions.MINOR_28_VIFS_SUBCONTROLLER)
def allow_dynamic_drivers():
"""Check if dynamic driver API calls are allowed.
Version 1.30 of the API added support for all of the driver
composition related calls in the /v1/drivers API.
"""
return (pecan.request.version.minor >=
versions.MINOR_30_DYNAMIC_DRIVERS)
def get_controller_reserved_names(cls):
"""Get reserved names for a given controller.

View File

@ -60,6 +60,7 @@ BASE_VERSION = 1
# v1.27: Add soft reboot, soft power off and timeout.
# v1.28: Add vifs subcontroller to node
# v1.29: Add inject nmi.
# v1.30: Add dynamic driver interactions.
MINOR_0_JUNO = 0
MINOR_1_INITIAL_VERSION = 1
@ -91,11 +92,12 @@ MINOR_26_PORTGROUP_MODE_PROPERTIES = 26
MINOR_27_SOFT_POWER_OFF = 27
MINOR_28_VIFS_SUBCONTROLLER = 28
MINOR_29_INJECT_NMI = 29
MINOR_30_DYNAMIC_DRIVERS = 30
# When adding another version, update MINOR_MAX_VERSION and also update
# doc/source/dev/webapi-version-history.rst with a detailed explanation of
# what the version has changed.
MINOR_MAX_VERSION = MINOR_29_INJECT_NMI
MINOR_MAX_VERSION = MINOR_30_DYNAMIC_DRIVERS
# String representations of the minor and maximum versions
MIN_VERSION_STRING = '{}.{}'.format(BASE_VERSION, MINOR_1_INITIAL_VERSION)

View File

@ -95,11 +95,11 @@ def _attach_interfaces_to_driver(bare_driver, node, driver_or_hw_type):
for iface in dynamic_interfaces:
impl_name = getattr(node, '%s_interface' % iface)
impl = _get_interface(driver_or_hw_type, iface, impl_name)
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):
def get_interface(driver_or_hw_type, interface_type, interface_name):
"""Get interface implementation instance.
For hardware types also validates compatibility.
@ -170,7 +170,7 @@ def default_interface(driver_or_hw_type, interface_type):
if impl_name is not None:
# Check that the default is correct for this type
_get_interface(driver_or_hw_type, interface_type, impl_name)
get_interface(driver_or_hw_type, interface_type, impl_name)
elif is_hardware_type:
supported = getattr(driver_or_hw_type,
'supported_%s_interfaces' % interface_type)
@ -230,7 +230,7 @@ def check_and_update_node_interfaces(node, driver_or_hw_type=None):
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)
get_interface(driver_or_hw_type, iface, impl_name)
# Not changing the result, proceeding with the next interface
continue

View File

@ -67,6 +67,7 @@ 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.drivers import hardware_type
from ironic import objects
from ironic.objects import base as objects_base
from ironic.objects import fields
@ -332,7 +333,8 @@ class ConductorManager(base_manager.BaseConductorManager):
@messaging.expected_exceptions(exception.NoFreeConductorWorker,
exception.InvalidParameterValue,
exception.UnsupportedDriverExtension,
exception.DriverNotFound)
exception.DriverNotFound,
exception.InterfaceNotFoundInEntrypoint)
def driver_vendor_passthru(self, context, driver_name, driver_method,
http_method, info):
"""Handle top-level vendor actions.
@ -343,8 +345,11 @@ class ConductorManager(base_manager.BaseConductorManager):
async the conductor will start background worker to perform
vendor action.
For dynamic drivers, the calculated default vendor interface is used.
:param context: an admin context.
:param driver_name: name of the driver on which to call the method.
:param driver_name: name of the driver or hardware type on which to
call the method.
:param driver_method: name of the vendor method, for use by the driver.
:param http_method: the HTTP method used for the request.
:param info: user-supplied data to pass through to the driver.
@ -357,6 +362,8 @@ class ConductorManager(base_manager.BaseConductorManager):
:raises: DriverNotFound if the supplied driver is not loaded.
:raises: NoFreeConductorWorker when there is no free worker to start
async task.
:raises: InterfaceNotFoundInEntrypoint if the default interface for a
hardware type is invalid.
:returns: A dictionary containing:
:return: The response of the invoked vendor method
@ -371,14 +378,23 @@ class ConductorManager(base_manager.BaseConductorManager):
# Any locking in a top-level vendor action will need to be done by the
# implementation, as there is little we could reasonably lock on here.
LOG.debug("RPC driver_vendor_passthru for driver %s.", driver_name)
driver = driver_factory.get_driver(driver_name)
if not getattr(driver, 'vendor', None):
driver = driver_factory.get_driver_or_hardware_type(driver_name)
vendor = None
if isinstance(driver, hardware_type.AbstractHardwareType):
vendor_name = driver_factory.default_interface(driver, 'vendor')
if vendor_name is not None:
vendor = driver_factory.get_interface(driver, 'vendor',
vendor_name)
else:
vendor = getattr(driver, 'vendor', None)
if not vendor:
raise exception.UnsupportedDriverExtension(
driver=driver_name,
extension='vendor interface')
try:
vendor_opts = driver.vendor.driver_routes[driver_method]
vendor_opts = vendor.driver_routes[driver_method]
vendor_func = vendor_opts['func']
except KeyError:
raise exception.InvalidParameterValue(
@ -396,7 +412,7 @@ class ConductorManager(base_manager.BaseConductorManager):
# Invoke the vendor method accordingly with the mode
is_async = vendor_opts['async']
ret = None
driver.vendor.driver_validate(method=driver_method, **info)
vendor.driver_validate(method=driver_method, **info)
if is_async:
self._spawn_worker(vendor_func, context, **info)
@ -432,12 +448,20 @@ class ConductorManager(base_manager.BaseConductorManager):
@METRICS.timer('ConductorManager.get_driver_vendor_passthru_methods')
@messaging.expected_exceptions(exception.UnsupportedDriverExtension,
exception.DriverNotFound)
exception.DriverNotFound,
exception.InterfaceNotFoundInEntrypoint)
def get_driver_vendor_passthru_methods(self, context, driver_name):
"""Retrieve information about vendor methods of the given driver.
For dynamic drivers, the default vendor interface is used.
:param context: an admin context.
:param driver_name: name of the driver.
:param driver_name: name of the driver or hardware_type
:raises: UnsupportedDriverExtension if current driver does not have
vendor interface.
:raises: DriverNotFound if the supplied driver is not loaded.
:raises: InterfaceNotFoundInEntrypoint if the default interface for a
hardware type is invalid.
:returns: dictionary of <method name>:<method metadata> entries.
"""
@ -445,13 +469,22 @@ class ConductorManager(base_manager.BaseConductorManager):
# implementation, as there is little we could reasonably lock on here.
LOG.debug("RPC get_driver_vendor_passthru_methods for driver %s",
driver_name)
driver = driver_factory.get_driver(driver_name)
if not getattr(driver, 'vendor', None):
driver = driver_factory.get_driver_or_hardware_type(driver_name)
vendor = None
if isinstance(driver, hardware_type.AbstractHardwareType):
vendor_name = driver_factory.default_interface(driver, 'vendor')
if vendor_name is not None:
vendor = driver_factory.get_interface(driver, 'vendor',
vendor_name)
else:
vendor = getattr(driver, 'vendor', None)
if not vendor:
raise exception.UnsupportedDriverExtension(
driver=driver_name,
extension='vendor interface')
return get_vendor_passthru_metadata(driver.vendor.driver_routes)
return get_vendor_passthru_metadata(vendor.driver_routes)
@METRICS.timer('ConductorManager.do_node_deploy')
@messaging.expected_exceptions(exception.NoFreeConductorWorker,
@ -2018,7 +2051,7 @@ class ConductorManager(base_manager.BaseConductorManager):
"""
LOG.debug("RPC get_driver_properties called for driver %s.",
driver_name)
driver = driver_factory.get_driver(driver_name)
driver = driver_factory.get_driver_or_hardware_type(driver_name)
return driver.get_properties()
@METRICS.timer('ConductorManager._send_sensor_data')
@ -2346,29 +2379,42 @@ class ConductorManager(base_manager.BaseConductorManager):
node.save()
@METRICS.timer('ConductorManager.get_raid_logical_disk_properties')
@messaging.expected_exceptions(exception.UnsupportedDriverExtension)
@messaging.expected_exceptions(exception.UnsupportedDriverExtension,
exception.InterfaceNotFoundInEntrypoint)
def get_raid_logical_disk_properties(self, context, driver_name):
"""Get the logical disk properties for RAID configuration.
Gets the information about logical disk properties which can
be specified in the input RAID configuration.
be specified in the input RAID configuration. For dynamic drivers,
the default vendor interface is used.
:param context: request context.
:param driver_name: name of the driver
:raises: UnsupportedDriverExtension, if the driver doesn't
support RAID configuration.
:raises: InterfaceNotFoundInEntrypoint if the default interface for a
hardware type is invalid.
:returns: A dictionary containing the properties and a textual
description for them.
"""
LOG.debug("RPC get_raid_logical_disk_properties "
"called for driver %s", driver_name)
driver = driver_factory.get_driver(driver_name)
if not getattr(driver, 'raid', None):
driver = driver_factory.get_driver_or_hardware_type(driver_name)
raid_iface = None
if isinstance(driver, hardware_type.AbstractHardwareType):
raid_iface_name = driver_factory.default_interface(driver, 'raid')
if raid_iface_name is not None:
raid_iface = driver_factory.get_interface(driver, 'raid',
raid_iface_name)
else:
raid_iface = getattr(driver, 'raid', None)
if not raid_iface:
raise exception.UnsupportedDriverExtension(
driver=driver_name, extension='raid')
return driver.raid.get_logical_disk_properties()
return raid_iface.get_logical_disk_properties()
@METRICS.timer('ConductorManager.heartbeat')
@messaging.expected_exceptions(exception.NoFreeConductorWorker)

View File

@ -266,6 +266,8 @@ class ConductorAPI(object):
:raises: DriverNotFound if the supplied driver is not loaded.
:raises: NoFreeConductorWorker when there is no free worker to start
async task.
:raises: InterfaceNotFoundInEntrypoint if the default interface for a
hardware type is invalid.
:returns: A dictionary containing:
:return: The response of the invoked vendor method
@ -304,6 +306,11 @@ class ConductorAPI(object):
:param context: an admin context.
:param driver_name: name of the driver.
:param topic: RPC topic. Defaults to self.topic.
:raises: UnsupportedDriverExtension if current driver does not have
vendor interface.
:raises: DriverNotFound if the supplied driver is not loaded.
:raises: InterfaceNotFoundInEntrypoint if the default interface for a
hardware type is invalid.
:returns: dictionary of <method name>:<method metadata> entries.
"""
@ -666,6 +673,8 @@ class ConductorAPI(object):
:param topic: RPC topic. Defaults to self.topic.
:raises: UnsupportedDriverExtension if the driver doesn't
support RAID configuration.
:raises: InterfaceNotFoundInEntrypoint if the default interface for a
hardware type is invalid.
:returns: A dictionary containing the properties that can be mentioned
for logical disks and a textual description for them.
"""

View File

@ -558,6 +558,15 @@ class Connection(object):
:returns: List of ``ConductorHardwareInterfaces`` objects.
"""
@abc.abstractmethod
def list_hardware_type_interfaces(self, hardware_types):
"""List registered hardware interfaces for given hardware types.
This is restricted to only active conductors.
:param hardware_types: list of hardware types to filter by.
:returns: list of ``ConductorHardwareInterfaces`` objects.
"""
@abc.abstractmethod
def register_conductor_hardware_interfaces(self, conductor_id,
hardware_type, interface_type,

View File

@ -826,6 +826,14 @@ class Connection(api.Connection):
.filter_by(conductor_id=conductor_id))
return query.all()
def list_hardware_type_interfaces(self, hardware_types):
query = (model_query(models.ConductorHardwareInterfaces)
.filter(models.ConductorHardwareInterfaces.hardware_type
.in_(hardware_types)))
query = _filter_active_conductors(query)
return query.all()
def register_conductor_hardware_interfaces(self, conductor_id,
hardware_type, interface_type,
interfaces, default_interface):

View File

@ -84,3 +84,8 @@ class ManualManagementHardware(GenericHardware):
def supported_power_interfaces(self):
"""List of supported power interfaces."""
return [fake.FakePower]
@property
def supported_vendor_interfaces(self):
"""List of supported vendor interfaces."""
return [noop.NoVendor]

View File

@ -20,6 +20,7 @@ import abc
import six
from ironic.drivers import base as driver_base
from ironic.drivers.modules.network import noop as noop_net
from ironic.drivers.modules import noop
from ironic.drivers.modules.storage import noop as noop_storage
@ -90,3 +91,24 @@ class AbstractHardwareType(object):
def supported_vendor_interfaces(self):
"""List of supported vendor interfaces."""
return [noop.NoVendor]
def get_properties(self):
"""Get the properties of the hardware type.
Note that this returns properties for the default interface of each
type, for this hardware type. Since this is not node-aware,
interface overrides can't be detected.
:returns: dictionary of <property name>:<property description> entries.
"""
# NOTE(jroll) this avoids a circular import
from ironic.common import driver_factory
properties = {}
for iface_type in driver_base.ALL_INTERFACES:
default_iface = driver_factory.default_interface(self, iface_type)
if default_iface is not None:
iface = driver_factory.get_interface(self, iface_type,
default_iface)
properties.update(iface.get_properties())
return properties

View File

@ -65,3 +65,6 @@ class NoInspect(FailMixin, base.InspectInterface):
class NoRAID(FailMixin, base.RAIDInterface):
"""RAID interface implementation that raises errors on all requests."""
create_configuration = delete_configuration = _fail
def validate_raid_config(self, task, raid_config):
_fail(self, task)

View File

@ -24,65 +24,218 @@ from ironic.api.controllers import base as api_base
from ironic.api.controllers.v1 import driver
from ironic.common import exception
from ironic.conductor import rpcapi
from ironic.drivers import base as driver_base
from ironic.tests.unit.api import base
class TestListDrivers(base.BaseApiTest):
d1 = 'fake-driver1'
d2 = 'fake-driver2'
d3 = 'fake-hardware-type'
h1 = 'fake-host1'
h2 = 'fake-host2'
def register_fake_conductors(self):
self.dbapi.register_conductor({
c1 = self.dbapi.register_conductor({
'hostname': self.h1,
'drivers': [self.d1, self.d2],
})
self.dbapi.register_conductor({
c2 = self.dbapi.register_conductor({
'hostname': self.h2,
'drivers': [self.d2],
})
for c in (c1, c2):
self.dbapi.register_conductor_hardware_interfaces(
c.id, self.d3, 'deploy', ['iscsi', 'direct'], 'direct')
def test_drivers(self):
def _test_drivers(self, use_dynamic, detail=False):
self.register_fake_conductors()
expected = sorted([
{'name': self.d1, 'hosts': [self.h1]},
{'name': self.d2, 'hosts': [self.h1, self.h2]},
], key=lambda d: d['name'])
data = self.get_json('/drivers')
self.assertThat(data['drivers'], matchers.HasLength(2))
headers = {}
expected = [
{'name': self.d1, 'hosts': [self.h1], 'type': 'classic'},
{'name': self.d2, 'hosts': [self.h1, self.h2], 'type': 'classic'},
{'name': self.d3, 'hosts': [self.h1, self.h2], 'type': 'dynamic'},
]
expected = sorted(expected, key=lambda d: d['name'])
if use_dynamic:
headers[api_base.Version.string] = '1.30'
path = '/drivers'
if detail:
path += '?detail=True'
data = self.get_json(path, headers=headers)
self.assertEqual(len(expected), len(data['drivers']))
drivers = sorted(data['drivers'], key=lambda d: d['name'])
for i in range(len(expected)):
d = drivers[i]
self.assertEqual(expected[i]['name'], d['name'])
self.assertEqual(sorted(expected[i]['hosts']), sorted(d['hosts']))
e = expected[i]
self.assertEqual(e['name'], d['name'])
self.assertEqual(sorted(e['hosts']), sorted(d['hosts']))
self.validate_link(d['links'][0]['href'])
self.validate_link(d['links'][1]['href'])
if use_dynamic:
self.assertEqual(e['type'], d['type'])
# NOTE(jroll) we don't test detail=True with use_dynamic=False
# as this case can't actually happen.
if detail:
self.assertIn('default_deploy_interface', d)
else:
# ensure we don't spill these fields into driver listing
# one should be enough
self.assertNotIn('default_deploy_interface', d)
def test_drivers(self):
self._test_drivers(False)
def test_drivers_with_dynamic(self):
self._test_drivers(True)
def test_drivers_with_dynamic_detailed(self):
with mock.patch.object(self.dbapi, 'list_hardware_type_interfaces',
autospec=True) as mock_hw:
mock_hw.return_value = [
{
'hardware_type': self.d3,
'interface_type': 'deploy',
'interface_name': 'iscsi',
'default': False,
},
{
'hardware_type': self.d3,
'interface_type': 'deploy',
'interface_name': 'direct',
'default': True,
},
]
self._test_drivers(True, detail=True)
def _test_drivers_type_filter(self, requested_type):
self.register_fake_conductors()
headers = {api_base.Version.string: '1.30'}
data = self.get_json('/drivers?type=%s' % requested_type,
headers=headers)
for d in data['drivers']:
# just check it's the right type, other tests handle the rest
self.assertEqual(requested_type, d['type'])
def test_drivers_type_filter_classic(self):
self._test_drivers_type_filter('classic')
def test_drivers_type_filter_dynamic(self):
self._test_drivers_type_filter('dynamic')
def test_drivers_type_filter_bad_version(self):
headers = {api_base.Version.string: '1.29'}
data = self.get_json('/drivers?type=classic',
headers=headers,
expect_errors=True)
self.assertEqual(http_client.NOT_ACCEPTABLE, data.status_code)
def test_drivers_type_filter_bad_value(self):
headers = {api_base.Version.string: '1.30'}
data = self.get_json('/drivers?type=working',
headers=headers,
expect_errors=True)
self.assertEqual(http_client.BAD_REQUEST, data.status_code)
def test_drivers_detail_bad_version(self):
headers = {api_base.Version.string: '1.29'}
data = self.get_json('/drivers?detail=True',
headers=headers,
expect_errors=True)
self.assertEqual(http_client.NOT_ACCEPTABLE, data.status_code)
def test_drivers_detail_bad_version_false(self):
headers = {api_base.Version.string: '1.29'}
data = self.get_json('/drivers?detail=False',
headers=headers,
expect_errors=True)
self.assertEqual(http_client.NOT_ACCEPTABLE, data.status_code)
def test_drivers_no_active_conductor(self):
data = self.get_json('/drivers')
self.assertThat(data['drivers'], matchers.HasLength(0))
self.assertEqual([], data['drivers'])
@mock.patch.object(rpcapi.ConductorAPI, 'get_driver_properties')
def test_drivers_get_one_ok(self, mock_driver_properties):
def _test_drivers_get_one_ok(self, use_dynamic, mock_driver_properties):
# get_driver_properties mock is required by validate_link()
self.register_fake_conductors()
data = self.get_json('/drivers/%s' % self.d1,
headers={api_base.Version.string: '1.14'})
self.assertEqual(self.d1, data['name'])
self.assertEqual([self.h1], data['hosts'])
self.assertIn('properties', data.keys())
if use_dynamic:
driver = self.d3
driver_type = 'dynamic'
hosts = [self.h1, self.h2]
else:
driver = self.d1
driver_type = 'classic'
hosts = [self.h1]
data = self.get_json('/drivers/%s' % driver,
headers={api_base.Version.string: '1.30'})
self.assertEqual(driver, data['name'])
self.assertEqual(sorted(hosts), sorted(data['hosts']))
self.assertIn('properties', data)
self.assertEqual(driver_type, data['type'])
if use_dynamic:
for iface in driver_base.ALL_INTERFACES:
# NOTE(jroll) we don't expose storage interface yet
if iface != 'storage':
self.assertIn('default_%s_interface' % iface, data)
self.assertIn('enabled_%s_interfaces' % iface, data)
self.assertIsNotNone(data['default_deploy_interface'])
self.assertIsNotNone(data['enabled_deploy_interfaces'])
else:
self.assertIsNone(data['default_deploy_interface'])
self.assertIsNone(data['enabled_deploy_interfaces'])
self.validate_link(data['links'][0]['href'])
self.validate_link(data['links'][1]['href'])
self.validate_link(data['properties'][0]['href'])
self.validate_link(data['properties'][1]['href'])
def test_drivers_get_one_ok_classic(self):
self._test_drivers_get_one_ok(False)
def test_drivers_get_one_ok_dynamic(self):
with mock.patch.object(self.dbapi, 'list_hardware_type_interfaces',
autospec=True) as mock_hw:
mock_hw.return_value = [
{
'hardware_type': self.d3,
'interface_type': 'deploy',
'interface_name': 'iscsi',
'default': False,
},
{
'hardware_type': self.d3,
'interface_type': 'deploy',
'interface_name': 'direct',
'default': True,
},
]
self._test_drivers_get_one_ok(True)
mock_hw.assert_called_once_with([self.d3])
def test_driver_properties_hidden_in_lower_version(self):
self.register_fake_conductors()
data = self.get_json('/drivers/%s' % self.d1,
headers={api_base.Version.string: '1.8'})
self.assertNotIn('properties', data.keys())
self.assertNotIn('properties', data)
def test_driver_type_hidden_in_lower_version(self):
self.register_fake_conductors()
data = self.get_json('/drivers/%s' % self.d1,
headers={api_base.Version.string: '1.14'})
self.assertNotIn('type', data)
def test_drivers_get_one_not_found(self):
response = self.get_json('/drivers/%s' % self.d1, expect_errors=True)
@ -92,7 +245,7 @@ class TestListDrivers(base.BaseApiTest):
cfg.CONF.set_override('public_endpoint', public_url, 'api')
self.register_fake_conductors()
data = self.get_json('/drivers/%s' % self.d1)
self.assertIn('links', data.keys())
self.assertIn('links', data)
self.assertEqual(2, len(data['links']))
self.assertIn(self.d1, data['links'][0]['href'])
for l in data['links']:
@ -289,6 +442,25 @@ class TestDriverProperties(base.BaseApiTest):
self.assertEqual(mock_properties.return_value,
driver._DRIVER_PROPERTIES[driver_name])
def test_driver_properties_hw_type(self, mock_topic, mock_properties):
# Can get driver properties for manual-management hardware type
driver._DRIVER_PROPERTIES = {}
driver_name = 'manual-management'
mock_topic.return_value = 'fake_topic'
mock_properties.return_value = {'prop1': 'Property 1. Required.'}
with mock.patch.object(self.dbapi, 'get_active_hardware_type_dict',
autospec=True) as mock_hw_type:
mock_hw_type.return_value = {driver_name: 'fake_topic'}
data = self.get_json('/drivers/%s/properties' % driver_name)
self.assertEqual(mock_properties.return_value, data)
mock_topic.assert_called_once_with(driver_name)
mock_properties.assert_called_once_with(mock.ANY, driver_name,
topic=mock_topic.return_value)
self.assertEqual(mock_properties.return_value,
driver._DRIVER_PROPERTIES[driver_name])
def test_driver_properties_cached(self, mock_topic, mock_properties):
# only one RPC-conductor call will be made and the info cached
# for subsequent requests

View File

@ -221,6 +221,43 @@ class TestApiUtils(base.TestCase):
self.assertRaises(exception.NotAcceptable,
utils.check_allow_specify_resource_class, ['foo'])
@mock.patch.object(pecan, 'request', spec_set=['version'])
def test_check_allow_filter_driver_type(self, mock_request):
mock_request.version.minor = 30
self.assertIsNone(utils.check_allow_filter_driver_type('classic'))
@mock.patch.object(pecan, 'request', spec_set=['version'])
def test_check_allow_filter_driver_type_none(self, mock_request):
mock_request.version.minor = 29
self.assertIsNone(utils.check_allow_filter_driver_type(None))
@mock.patch.object(pecan, 'request', spec_set=['version'])
def test_check_allow_filter_driver_type_fail(self, mock_request):
mock_request.version.minor = 29
self.assertRaises(exception.NotAcceptable,
utils.check_allow_filter_driver_type, 'classic')
@mock.patch.object(pecan, 'request', spec_set=['version'])
def test_check_allow_driver_detail(self, mock_request):
mock_request.version.minor = 30
self.assertIsNone(utils.check_allow_driver_detail(True))
@mock.patch.object(pecan, 'request', spec_set=['version'])
def test_check_allow_driver_detail_false(self, mock_request):
mock_request.version.minor = 30
self.assertIsNone(utils.check_allow_driver_detail(False))
@mock.patch.object(pecan, 'request', spec_set=['version'])
def test_check_allow_driver_detail_none(self, mock_request):
mock_request.version.minor = 29
self.assertIsNone(utils.check_allow_driver_detail(None))
@mock.patch.object(pecan, 'request', spec_set=['version'])
def test_check_allow_driver_detail_fail(self, mock_request):
mock_request.version.minor = 29
self.assertRaises(exception.NotAcceptable,
utils.check_allow_driver_detail, True)
@mock.patch.object(pecan, 'request', spec_set=['version'])
def test_check_allow_manage_verbs(self, mock_request):
mock_request.version.minor = 4
@ -368,6 +405,13 @@ class TestApiUtils(base.TestCase):
mock_request.version.minor = 25
self.assertFalse(utils.allow_portgroup_mode_properties())
@mock.patch.object(pecan, 'request', spec_set=['version'])
def test_allow_dynamic_drivers(self, mock_request):
mock_request.version.minor = 30
self.assertTrue(utils.allow_dynamic_drivers())
mock_request.version.minor = 29
self.assertFalse(utils.allow_dynamic_drivers())
class TestNodeIdent(base.TestCase):

View File

@ -165,6 +165,18 @@ class ServiceSetUpMixin(object):
self.config(enabled_drivers=['fake'])
self.config(node_locked_retry_attempts=1, group='conductor')
self.config(node_locked_retry_interval=0, group='conductor')
self.config(enabled_hardware_types=['fake-hardware',
'manual-management'])
self.config(enabled_boot_interfaces=['fake', 'pxe'])
self.config(enabled_console_interfaces=['fake', 'no-console'])
self.config(enabled_deploy_interfaces=['fake', 'iscsi'])
self.config(enabled_inspect_interfaces=['fake', 'no-inspect'])
self.config(enabled_management_interfaces=['fake'])
self.config(enabled_power_interfaces=['fake'])
self.config(enabled_raid_interfaces=['fake', 'no-raid'])
self.config(enabled_vendor_interfaces=['fake', 'no-vendor'])
self.service = manager.ConductorManager(self.hostname, 'test-topic')
mock_the_extension_manager()
self.driver = driver_factory.get_driver("fake")

View File

@ -607,13 +607,15 @@ class VendorPassthruTestCase(mgr_utils.ServiceSetUpMixin,
@mock.patch.object(task_manager.TaskManager, 'upgrade_lock')
@mock.patch.object(task_manager.TaskManager, 'spawn_after')
def test_vendor_passthru_async(self, mock_spawn, mock_upgrade):
node = obj_utils.create_test_node(self.context, driver='fake')
def _test_vendor_passthru_async(self, driver, vendor_iface, mock_spawn,
mock_upgrade):
node = obj_utils.create_test_node(self.context, driver=driver,
vendor_interface=vendor_iface)
info = {'bar': 'baz'}
self._start_service()
response = self.service.vendor_passthru(self.context, node.uuid,
'first_method', 'POST',
'second_method', 'POST',
info)
# Waiting to make sure the below assertions are valid.
self._stop_service()
@ -631,6 +633,13 @@ class VendorPassthruTestCase(mgr_utils.ServiceSetUpMixin,
# Verify reservation has been cleared.
self.assertIsNone(node.reservation)
def test_vendor_passthru_async(self):
self._test_vendor_passthru_async('fake', None)
def test_vendor_passthru_async_hw_type(self):
self.config(enabled_vendor_interfaces=['fake'])
self._test_vendor_passthru_async('fake-hardware', 'fake')
@mock.patch.object(task_manager.TaskManager, 'upgrade_lock')
@mock.patch.object(task_manager.TaskManager, 'spawn_after')
def test_vendor_passthru_sync(self, mock_spawn, mock_upgrade):
@ -821,12 +830,20 @@ class VendorPassthruTestCase(mgr_utils.ServiceSetUpMixin,
self.assertEqual(exception.UnsupportedDriverExtension,
exc.exc_info[0])
@mock.patch.object(driver_factory, 'get_interface')
@mock.patch.object(manager.ConductorManager, '_spawn_worker')
def test_driver_vendor_passthru_sync(self, mock_spawn):
def _test_driver_vendor_passthru_sync(self, is_hw_type, mock_spawn,
mock_get_if):
expected = {'foo': 'bar'}
self.driver.vendor = mock.Mock(spec=drivers_base.VendorInterface)
vendor_mock = mock.Mock(spec=drivers_base.VendorInterface)
if is_hw_type:
mock_get_if.return_value = vendor_mock
driver_name = 'fake-hardware'
else:
self.driver.vendor = vendor_mock
driver_name = 'fake'
test_method = mock.MagicMock(return_value=expected)
self.driver.vendor.driver_routes = {
vendor_mock.driver_routes = {
'test_method': {'func': test_method,
'async': False,
'attach': False,
@ -834,19 +851,29 @@ class VendorPassthruTestCase(mgr_utils.ServiceSetUpMixin,
self.service.init_host()
# init_host() called _spawn_worker because of the heartbeat
mock_spawn.reset_mock()
# init_host() called get_interface during driver loading
mock_get_if.reset_mock()
vendor_args = {'test': 'arg'}
response = self.service.driver_vendor_passthru(
self.context, 'fake', 'test_method', 'POST', vendor_args)
self.context, driver_name, 'test_method', 'POST', vendor_args)
# Assert that the vendor interface has no custom
# driver_vendor_passthru()
self.assertFalse(hasattr(self.driver.vendor, 'driver_vendor_passthru'))
self.assertFalse(hasattr(vendor_mock, 'driver_vendor_passthru'))
self.assertEqual(expected, response['return'])
self.assertFalse(response['async'])
test_method.assert_called_once_with(self.context, **vendor_args)
# No worker was spawned
self.assertFalse(mock_spawn.called)
if is_hw_type:
mock_get_if.assert_called_once_with(mock.ANY, 'vendor', 'fake')
def test_driver_vendor_passthru_sync(self):
self._test_driver_vendor_passthru_sync(False)
def test_driver_vendor_passthru_sync_hw_type(self):
self._test_driver_vendor_passthru_sync(True)
@mock.patch.object(manager.ConductorManager, '_spawn_worker')
def test_driver_vendor_passthru_async(self, mock_spawn):
@ -920,21 +947,43 @@ class VendorPassthruTestCase(mgr_utils.ServiceSetUpMixin,
self.context, 'does_not_exist', 'test_method',
'POST', {})
def test_get_driver_vendor_passthru_methods(self):
self.driver.vendor = mock.Mock(spec=drivers_base.VendorInterface)
@mock.patch.object(driver_factory, 'get_interface')
def _test_get_driver_vendor_passthru_methods(self, is_hw_type,
mock_get_if):
vendor_mock = mock.Mock(spec=drivers_base.VendorInterface)
if is_hw_type:
mock_get_if.return_value = vendor_mock
driver_name = 'fake-hardware'
else:
self.driver.vendor = vendor_mock
driver_name = 'fake'
fake_routes = {'test_method': {'async': True,
'description': 'foo',
'http_methods': ['POST'],
'func': None}}
self.driver.vendor.driver_routes = fake_routes
vendor_mock.driver_routes = fake_routes
self.service.init_host()
# init_host() will call get_interface
mock_get_if.reset_mock()
data = self.service.get_driver_vendor_passthru_methods(self.context,
'fake')
driver_name)
# The function reference should not be returned
del fake_routes['test_method']['func']
self.assertEqual(fake_routes, data)
if is_hw_type:
mock_get_if.assert_called_once_with(mock.ANY, 'vendor', 'fake')
else:
mock_get_if.assert_not_called()
def test_get_driver_vendor_passthru_methods(self):
self._test_get_driver_vendor_passthru_methods(False)
def test_get_driver_vendor_passthru_methods_hw_type(self):
self._test_get_driver_vendor_passthru_methods(True)
def test_get_driver_vendor_passthru_methods_not_supported(self):
self.service.init_host()
self.driver.vendor = None
@ -3720,15 +3769,20 @@ class UpdatePortgroupTestCase(mgr_utils.ServiceSetUpMixin,
@mgr_utils.mock_record_keepalive
class RaidTestCases(mgr_utils.ServiceSetUpMixin, tests_db_base.DbTestCase):
driver_name = 'fake'
raid_interface = None
def setUp(self):
super(RaidTestCases, self).setUp()
self.node = obj_utils.create_test_node(
self.context, driver='fake', provision_state=states.MANAGEABLE)
self.context, driver=self.driver_name,
raid_interface=self.raid_interface,
provision_state=states.MANAGEABLE)
def test_get_raid_logical_disk_properties(self):
self._start_service()
properties = self.service.get_raid_logical_disk_properties(
self.context, 'fake')
self.context, self.driver_name)
self.assertIn('raid_level', properties)
self.assertIn('size_gb', properties)
@ -3737,7 +3791,7 @@ class RaidTestCases(mgr_utils.ServiceSetUpMixin, tests_db_base.DbTestCase):
self._start_service()
exc = self.assertRaises(messaging.rpc.ExpectedException,
self.service.get_raid_logical_disk_properties,
self.context, 'fake')
self.context, self.driver_name)
self.assertEqual(exception.UnsupportedDriverExtension, exc.exc_info[0])
def test_set_target_raid_config(self):
@ -3766,7 +3820,7 @@ class RaidTestCases(mgr_utils.ServiceSetUpMixin, tests_db_base.DbTestCase):
self.node.refresh()
self.assertEqual({}, self.node.target_raid_config)
self.assertEqual(exception.UnsupportedDriverExtension, exc.exc_info[0])
self.assertIn('fake', six.text_type(exc.exc_info[1]))
self.assertIn(self.driver_name, six.text_type(exc.exc_info[1]))
def test_set_target_raid_config_invalid_parameter_value(self):
# Missing raid_level in the below raid config.
@ -3784,6 +3838,42 @@ class RaidTestCases(mgr_utils.ServiceSetUpMixin, tests_db_base.DbTestCase):
self.assertEqual(exception.InvalidParameterValue, exc.exc_info[0])
@mgr_utils.mock_record_keepalive
class RaidHardwareTypeTestCases(RaidTestCases):
driver_name = 'fake-hardware'
raid_interface = 'fake'
def test_get_raid_logical_disk_properties_iface_not_supported(self):
self.config(enabled_raid_interfaces=[])
self._start_service()
exc = self.assertRaises(messaging.rpc.ExpectedException,
self.service.get_raid_logical_disk_properties,
self.context, self.driver_name)
self.assertEqual(exception.UnsupportedDriverExtension, exc.exc_info[0])
def test_set_target_raid_config_iface_not_supported(self):
# NOTE(jroll): it's impossible for a dynamic driver to have a null
# interface (e.g. node.driver.raid), so this instead tests that
# if validation fails, we blow up properly.
# need a different raid interface and a hardware type that supports it
self.node = obj_utils.create_test_node(
self.context, driver='manual-management',
raid_interface='no-raid',
uuid=uuidutils.generate_uuid(),
provision_state=states.MANAGEABLE)
raid_config = {'logical_disks': [{'size_gb': 100, 'raid_level': '1'}]}
exc = self.assertRaises(
messaging.rpc.ExpectedException,
self.service.set_target_raid_config,
self.context, self.node.uuid, raid_config)
self.node.refresh()
self.assertEqual({}, self.node.target_raid_config)
self.assertEqual(exception.UnsupportedDriverExtension, exc.exc_info[0])
self.assertIn('manual-management', six.text_type(exc.exc_info[1]))
@mock.patch.object(conductor_utils, 'node_power_action')
class ManagerDoSyncPowerStateTestCase(tests_db_base.DbTestCase):
def setUp(self):
@ -4662,6 +4752,27 @@ class ManagerTestProperties(tests_db_base.DbTestCase):
self.assertEqual(exception.DriverNotFound, exc.exc_info[0])
@mgr_utils.mock_record_keepalive
class ManagerTestHardwareTypeProperties(tests_db_base.DbTestCase):
def setUp(self):
super(ManagerTestHardwareTypeProperties, self).setUp()
self.service = manager.ConductorManager('test-host', 'test-topic')
def _check_hardware_type_properties(self, hardware_type, expected):
self.config(enabled_hardware_types=[hardware_type])
self.hardware_type = driver_factory.get_hardware_type(hardware_type)
self.service.init_host()
properties = self.service.get_driver_properties(self.context,
hardware_type)
self.assertEqual(sorted(expected), sorted(properties.keys()))
def test_hardware_type_properties_manual_management(self):
expected = ['deploy_kernel', 'deploy_ramdisk',
'deploy_forces_oob_reboot']
self._check_hardware_type_properties('manual-management', expected)
@mock.patch.object(task_manager, 'acquire')
@mock.patch.object(manager.ConductorManager, '_mapped_to_this_conductor')
@mock.patch.object(dbapi.IMPL, 'get_nodeinfo_list')

View File

@ -385,3 +385,59 @@ class DbConductorTestCase(base.DbTestCase):
# 61 seconds passed since last heartbeat, it's dead
mock_utcnow.return_value = time_ + datetime.timedelta(seconds=61)
self.assertEqual([c.hostname], self.dbapi.get_offline_conductors())
@mock.patch.object(timeutils, 'utcnow', autospec=True)
def test_list_hardware_type_interfaces(self, mock_utcnow):
self.config(heartbeat_timeout=60, group='conductor')
time_ = datetime.datetime(2000, 1, 1, 0, 0)
h = 'fake-host'
ht1 = 'hw-type-1'
ht2 = 'hw-type-2'
mock_utcnow.return_value = time_
self._create_test_cdr(hostname=h, hardware_types=[ht1, ht2])
expected = [
{
'hardware_type': ht1,
'interface_type': 'power',
'interface_name': 'ipmi',
'default': True,
},
{
'hardware_type': ht1,
'interface_type': 'power',
'interface_name': 'fake',
'default': False,
},
{
'hardware_type': ht2,
'interface_type': 'power',
'interface_name': 'ipmi',
'default': True,
},
{
'hardware_type': ht2,
'interface_type': 'power',
'interface_name': 'fake',
'default': False,
},
]
def _verify(expected, result):
for expected_row, row in zip(expected, result):
for k, v in expected_row.items():
self.assertEqual(v, getattr(row, k))
# with both hw types
result = self.dbapi.list_hardware_type_interfaces([ht1, ht2])
_verify(expected, result)
# with one hw type
result = self.dbapi.list_hardware_type_interfaces([ht1])
_verify(expected[:2], result)
# 61 seconds passed since last heartbeat, it's dead
mock_utcnow.return_value = time_ + datetime.timedelta(seconds=61)
result = self.dbapi.list_hardware_type_interfaces([ht1, ht2])
self.assertEqual([], result)

View File

@ -0,0 +1,20 @@
---
features:
- |
Provides support for dynamic drivers.
With REST API version 1.30, adds additional parameters and response
fields for GET /v1/drivers and GET /v1/drivers/<name>.
Also allows dynamic drivers to be used and returned in the following
API calls, in all versions of the REST API:
* GET /v1/drivers
* GET /v1/drivers/<name>
* GET /v1/drivers/<name>/properties
* GET /v1/drivers/<name>/vendor_passthru/methods
* GET/POST /v1/drivers/<name>/vendor_passthru
* GET/POST /v1/nodes/<id>/vendor_passthru
For more details, see the `REST API Version History documentation
<http://docs.openstack.org/developer/ironic/dev/webapi-version-history.html>`_.