Add indicators REST API endpoints
Added REST API endpoints for indicator management: * GET /v1/nodes/<node_ident>/management/indicators` to list all available indicators names for each of the hardware component. * GET /v1/nodes/<node_ident>/management/indicators/<indicator_ident> to retrieve the state of given indicator. * PUT /v1/nodes/<node_ident>/management/indicators/<indicator_ident>` change state of the desired indicator. This implementation slightly deviates from the original spec in part of having component name in the URL - this implementation flattens component out. The spec: https://review.opendev.org/#/c/655685/7/specs/approved/expose-hardware-indicators.rst Change-Id: I3a36f58b12487e18a6898aef6b077d4221f8a5b8 Story: 2005342 Task: 30291
This commit is contained in:
parent
9f07ad1b6e
commit
263fd021b2
@ -2,6 +2,20 @@
|
||||
REST API Version History
|
||||
========================
|
||||
|
||||
1.63 (Ussuri, master)
|
||||
---------------------
|
||||
|
||||
Added the following new endpoints for indicator management:
|
||||
|
||||
* ``GET /v1/nodes/<node_ident>/management/indicators`` to list all
|
||||
available indicators names for each of the hardware component.
|
||||
Currently known components are: ``chassis``, ``system``, ``disk``, ``power``
|
||||
and ``nic``.
|
||||
* ``GET /v1/nodes/<node_ident>/management/indicators/<component>/<indicator_ident>``
|
||||
to retrieve all indicators and their states for the hardware component.
|
||||
* ``PUT /v1/nodes/<node_ident>/management/indicators/<component>/<indicator_ident>``
|
||||
change state of the desired indicators of the component.
|
||||
|
||||
1.62 (Ussuri, master)
|
||||
---------------------
|
||||
|
||||
|
@ -266,6 +266,184 @@ class BootDeviceController(rest.RestController):
|
||||
return {'supported_boot_devices': boot_devices}
|
||||
|
||||
|
||||
class IndicatorAtComponent(object):
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
name = kwargs.get('name')
|
||||
component = kwargs.get('component')
|
||||
unique_name = kwargs.get('unique_name')
|
||||
|
||||
if name and component:
|
||||
self.unique_name = name + '@' + component
|
||||
self.name = name
|
||||
self.component = component
|
||||
|
||||
elif unique_name:
|
||||
try:
|
||||
index = unique_name.index('@')
|
||||
|
||||
except ValueError:
|
||||
raise exception.InvalidParameterValue(
|
||||
_('Malformed indicator name "%s"') % unique_name)
|
||||
|
||||
self.component = unique_name[index + 1:]
|
||||
self.name = unique_name[:index]
|
||||
self.unique_name = unique_name
|
||||
|
||||
else:
|
||||
raise exception.MissingParameterValue(
|
||||
_('Missing indicator name "%s"'))
|
||||
|
||||
|
||||
class IndicatorState(base.APIBase):
|
||||
"""API representation of indicator state."""
|
||||
|
||||
state = wsme.wsattr(wtypes.text)
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
self.state = kwargs.get('state')
|
||||
|
||||
|
||||
class Indicator(base.APIBase):
|
||||
"""API representation of an indicator."""
|
||||
|
||||
name = wsme.wsattr(wtypes.text)
|
||||
|
||||
component = wsme.wsattr(wtypes.text)
|
||||
|
||||
readonly = types.BooleanType()
|
||||
|
||||
states = wtypes.ArrayType(str)
|
||||
|
||||
links = wsme.wsattr([link.Link], readonly=True)
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
self.name = kwargs.get('name')
|
||||
self.component = kwargs.get('component')
|
||||
self.readonly = kwargs.get('readonly', True)
|
||||
self.states = kwargs.get('states', [])
|
||||
|
||||
@staticmethod
|
||||
def _convert_with_links(node_uuid, indicator, url):
|
||||
"""Add links to the indicator."""
|
||||
indicator.links = [
|
||||
link.Link.make_link(
|
||||
'self', url, 'nodes',
|
||||
'%s/management/indicators/%s' % (
|
||||
node_uuid, indicator.name)),
|
||||
link.Link.make_link(
|
||||
'bookmark', url, 'nodes',
|
||||
'%s/management/indicators/%s' % (
|
||||
node_uuid, indicator.name),
|
||||
bookmark=True)]
|
||||
return indicator
|
||||
|
||||
@classmethod
|
||||
def convert_with_links(cls, node_uuid, rpc_component, rpc_name,
|
||||
**rpc_fields):
|
||||
"""Add links to the indicator."""
|
||||
indicator = Indicator(
|
||||
component=rpc_component, name=rpc_name, **rpc_fields)
|
||||
return cls._convert_with_links(
|
||||
node_uuid, indicator, pecan.request.host_url)
|
||||
|
||||
|
||||
class IndicatorsCollection(wtypes.Base):
|
||||
"""API representation of the indicators for a node."""
|
||||
|
||||
indicators = [Indicator]
|
||||
"""Node indicators list"""
|
||||
|
||||
@staticmethod
|
||||
def collection_from_dict(node_ident, indicators):
|
||||
col = IndicatorsCollection()
|
||||
|
||||
indicator_list = []
|
||||
for component, names in indicators.items():
|
||||
for name, fields in names.items():
|
||||
indicator_at_component = IndicatorAtComponent(
|
||||
component=component, name=name)
|
||||
indicator = Indicator.convert_with_links(
|
||||
node_ident, component, indicator_at_component.unique_name,
|
||||
**fields)
|
||||
indicator_list.append(indicator)
|
||||
col.indicators = indicator_list
|
||||
return col
|
||||
|
||||
|
||||
class IndicatorController(rest.RestController):
|
||||
|
||||
@METRICS.timer('IndicatorController.put')
|
||||
@expose.expose(None, types.uuid_or_name, wtypes.text, wtypes.text,
|
||||
status_code=http_client.NO_CONTENT)
|
||||
def put(self, node_ident, indicator, state):
|
||||
"""Set node hardware component indicator to the desired state.
|
||||
|
||||
:param node_ident: the UUID or logical name of a node.
|
||||
:param indicator: Indicator ID (as reported by
|
||||
`get_supported_indicators`).
|
||||
:param state: Indicator state, one of
|
||||
mod:`ironic.common.indicator_states`.
|
||||
|
||||
"""
|
||||
cdict = pecan.request.context.to_policy_values()
|
||||
policy.authorize('baremetal:node:set_indicator_state', cdict, cdict)
|
||||
|
||||
rpc_node = api_utils.get_rpc_node(node_ident)
|
||||
topic = pecan.request.rpcapi.get_topic_for(rpc_node)
|
||||
indicator_at_component = IndicatorAtComponent(unique_name=indicator)
|
||||
pecan.request.rpcapi.set_indicator_state(
|
||||
pecan.request.context, rpc_node.uuid,
|
||||
indicator_at_component.component, indicator_at_component.name,
|
||||
state, topic=topic)
|
||||
|
||||
@METRICS.timer('IndicatorController.get_one')
|
||||
@expose.expose(IndicatorState, types.uuid_or_name, wtypes.text)
|
||||
def get_one(self, node_ident, indicator):
|
||||
"""Get node hardware component indicator and its state.
|
||||
|
||||
:param node_ident: the UUID or logical name of a node.
|
||||
:param indicator: Indicator ID (as reported by
|
||||
`get_supported_indicators`).
|
||||
:returns: a dict with the "state" key and one of
|
||||
mod:`ironic.common.indicator_states` as a value.
|
||||
"""
|
||||
cdict = pecan.request.context.to_policy_values()
|
||||
policy.authorize('baremetal:node:get_indicator_state', cdict, cdict)
|
||||
|
||||
rpc_node = api_utils.get_rpc_node(node_ident)
|
||||
topic = pecan.request.rpcapi.get_topic_for(rpc_node)
|
||||
indicator_at_component = IndicatorAtComponent(unique_name=indicator)
|
||||
state = pecan.request.rpcapi.get_indicator_state(
|
||||
pecan.request.context, rpc_node.uuid,
|
||||
indicator_at_component.component, indicator_at_component.name,
|
||||
topic=topic)
|
||||
return IndicatorState(state=state)
|
||||
|
||||
@METRICS.timer('IndicatorController.get_all')
|
||||
@expose.expose(IndicatorsCollection, types.uuid_or_name, wtypes.text,
|
||||
ignore_extra_args=True)
|
||||
def get_all(self, node_ident):
|
||||
"""Get node hardware components and their indicators.
|
||||
|
||||
:param node_ident: the UUID or logical name of a node.
|
||||
:returns: A json object of hardware components
|
||||
(:mod:`ironic.common.components`) as keys with indicator IDs
|
||||
(from `get_supported_indicators`) as values.
|
||||
|
||||
"""
|
||||
cdict = pecan.request.context.to_policy_values()
|
||||
policy.authorize('baremetal:node:get_indicator_state', cdict, cdict)
|
||||
|
||||
rpc_node = api_utils.get_rpc_node(node_ident)
|
||||
topic = pecan.request.rpcapi.get_topic_for(rpc_node)
|
||||
indicators = pecan.request.rpcapi.get_supported_indicators(
|
||||
pecan.request.context, rpc_node.uuid, topic=topic)
|
||||
|
||||
return IndicatorsCollection.collection_from_dict(
|
||||
node_ident, indicators)
|
||||
|
||||
|
||||
class InjectNmiController(rest.RestController):
|
||||
|
||||
@METRICS.timer('InjectNmiController.put')
|
||||
@ -308,6 +486,9 @@ class NodeManagementController(rest.RestController):
|
||||
inject_nmi = InjectNmiController()
|
||||
"""Expose inject_nmi as a sub-element of management"""
|
||||
|
||||
indicators = IndicatorController()
|
||||
"""Expose indicators as a sub-element of management"""
|
||||
|
||||
|
||||
class ConsoleInfo(base.Base):
|
||||
"""API representation of the console information for a node."""
|
||||
|
@ -23,8 +23,8 @@ CONF = cfg.CONF
|
||||
BASE_VERSION = 1
|
||||
|
||||
# Here goes a short log of changes in every version.
|
||||
# Refer to doc/source/dev/webapi-version-history.rst for a detailed explanation
|
||||
# of what each version contains.
|
||||
# Refer to doc/source/contributor/webapi-version-history.rst for a detailed
|
||||
# explanation of what each version contains.
|
||||
#
|
||||
# v1.0: corresponds to Juno API, not supported since Kilo
|
||||
# v1.1: API at the point in time when versioning support was added,
|
||||
@ -100,6 +100,7 @@ BASE_VERSION = 1
|
||||
# v1.60: Add owner to the allocation object.
|
||||
# v1.61: Add retired and retired_reason to the node object.
|
||||
# v1.62: Add agent_token support for agent communication.
|
||||
# v1.63: Add support for indicators
|
||||
|
||||
MINOR_0_JUNO = 0
|
||||
MINOR_1_INITIAL_VERSION = 1
|
||||
@ -164,6 +165,7 @@ MINOR_59_CONFIGDRIVE_VENDOR_DATA = 59
|
||||
MINOR_60_ALLOCATION_OWNER = 60
|
||||
MINOR_61_NODE_RETIRED = 61
|
||||
MINOR_62_AGENT_TOKEN = 62
|
||||
MINOR_63_INDICATORS = 63
|
||||
|
||||
# When adding another version, update:
|
||||
# - MINOR_MAX_VERSION
|
||||
@ -171,7 +173,7 @@ MINOR_62_AGENT_TOKEN = 62
|
||||
# explanation of what changed in the new version
|
||||
# - common/release_mappings.py, RELEASE_MAPPING['master']['api']
|
||||
|
||||
MINOR_MAX_VERSION = MINOR_62_AGENT_TOKEN
|
||||
MINOR_MAX_VERSION = MINOR_63_INDICATORS
|
||||
|
||||
# String representations of the minor and maximum versions
|
||||
_MIN_VERSION_STRING = '{}.{}'.format(BASE_VERSION, MINOR_1_INITIAL_VERSION)
|
||||
|
@ -157,6 +157,23 @@ node_policies = [
|
||||
[{'path': '/nodes/{node_ident}/management/boot_device',
|
||||
'method': 'PUT'}]),
|
||||
|
||||
policy.DocumentedRuleDefault(
|
||||
'baremetal:node:get_indicator_state',
|
||||
'rule:is_admin or rule:is_observer',
|
||||
'Retrieve Node indicators and their states',
|
||||
[{'path': '/nodes/{node_ident}/management/indicators/'
|
||||
'{component}/{indicator}',
|
||||
'method': 'GET'},
|
||||
{'path': '/nodes/{node_ident}/management/indicators',
|
||||
'method': 'GET'}]),
|
||||
policy.DocumentedRuleDefault(
|
||||
'baremetal:node:set_indicator_state',
|
||||
'rule:is_admin',
|
||||
'Change Node indicator state',
|
||||
[{'path': '/nodes/{node_ident}/management/indicators/'
|
||||
'{component}/{indicator}',
|
||||
'method': 'PUT'}]),
|
||||
|
||||
policy.DocumentedRuleDefault(
|
||||
'baremetal:node:inject_nmi',
|
||||
'rule:is_admin',
|
||||
|
@ -214,8 +214,8 @@ RELEASE_MAPPING = {
|
||||
}
|
||||
},
|
||||
'master': {
|
||||
'api': '1.62',
|
||||
'rpc': '1.49',
|
||||
'api': '1.63',
|
||||
'rpc': '1.50',
|
||||
'objects': {
|
||||
'Allocation': ['1.1'],
|
||||
'Node': ['1.33', '1.32'],
|
||||
|
@ -90,7 +90,7 @@ class ConductorManager(base_manager.BaseConductorManager):
|
||||
# NOTE(rloo): This must be in sync with rpcapi.ConductorAPI's.
|
||||
# NOTE(pas-ha): This also must be in sync with
|
||||
# ironic.common.release_mappings.RELEASE_MAPPING['master']
|
||||
RPC_API_VERSION = '1.49'
|
||||
RPC_API_VERSION = '1.50'
|
||||
|
||||
target = messaging.Target(version=RPC_API_VERSION)
|
||||
|
||||
@ -2784,6 +2784,132 @@ class ConductorManager(base_manager.BaseConductorManager):
|
||||
purpose=lock_purpose) as task:
|
||||
return task.driver.management.get_supported_boot_devices(task)
|
||||
|
||||
@METRICS.timer('ConductorManager.set_indicator_state')
|
||||
@messaging.expected_exceptions(exception.NodeLocked,
|
||||
exception.UnsupportedDriverExtension,
|
||||
exception.InvalidParameterValue)
|
||||
def set_indicator_state(self, context, node_id, component,
|
||||
indicator, state):
|
||||
"""Set node hardware components indicator to the desired state.
|
||||
|
||||
:param context: request context.
|
||||
:param node_id: node id or uuid.
|
||||
:param component: The hardware component, one of
|
||||
:mod:`ironic.common.components`.
|
||||
:param indicator: Indicator IDs, as
|
||||
reported by `get_supported_indicators`)
|
||||
:param state: Indicator state, one of
|
||||
mod:`ironic.common.indicator_states`.
|
||||
:raises: NodeLocked if node is locked by another conductor.
|
||||
:raises: UnsupportedDriverExtension if the node's driver doesn't
|
||||
support management.
|
||||
:raises: InvalidParameterValue when the wrong driver info is
|
||||
specified or an invalid boot device is specified.
|
||||
:raises: MissingParameterValue if missing supplied info.
|
||||
|
||||
"""
|
||||
LOG.debug('RPC set_indicator_state called for node %(node)s with '
|
||||
'component %(component)s, indicator %(indicator)s and state '
|
||||
'%(state)s', {'node': node_id, 'component': component,
|
||||
'indicator': indicator, 'state': state})
|
||||
with task_manager.acquire(context, node_id,
|
||||
purpose='setting indicator state') as task:
|
||||
task.driver.management.validate(task)
|
||||
task.driver.management.set_indicator_state(
|
||||
task, component, indicator, state)
|
||||
|
||||
@METRICS.timer('ConductorManager.get_indicator_states')
|
||||
@messaging.expected_exceptions(exception.NodeLocked,
|
||||
exception.UnsupportedDriverExtension,
|
||||
exception.InvalidParameterValue)
|
||||
def get_indicator_state(self, context, node_id, component, indicator):
|
||||
"""Get node hardware component indicator state.
|
||||
|
||||
:param context: request context.
|
||||
:param node_id: node id or uuid.
|
||||
:param component: The hardware component, one of
|
||||
:mod:`ironic.common.components`.
|
||||
:param indicator: Indicator IDs, as
|
||||
reported by `get_supported_indicators`)
|
||||
:raises: NodeLocked if node is locked by another conductor.
|
||||
:raises: UnsupportedDriverExtension if the node's driver doesn't
|
||||
support management.
|
||||
:raises: InvalidParameterValue when the wrong driver info is
|
||||
specified.
|
||||
:raises: MissingParameterValue if missing supplied info.
|
||||
:returns: Indicator state, one of
|
||||
mod:`ironic.common.indicator_states`.
|
||||
|
||||
"""
|
||||
LOG.debug('RPC get_indicator_states called for node %s', node_id)
|
||||
with task_manager.acquire(context, node_id,
|
||||
purpose='getting indicators states') as task:
|
||||
task.driver.management.validate(task)
|
||||
return task.driver.management.get_indicator_state(
|
||||
task, component, indicator)
|
||||
|
||||
@METRICS.timer('ConductorManager.get_supported_indicators')
|
||||
@messaging.expected_exceptions(exception.NodeLocked,
|
||||
exception.UnsupportedDriverExtension,
|
||||
exception.InvalidParameterValue)
|
||||
def get_supported_indicators(self, context, node_id, component=None):
|
||||
"""Get node hardware components and their indicators.
|
||||
|
||||
:param context: request context.
|
||||
:param node_id: node id or uuid.
|
||||
:param component: If not `None`, return indicator information
|
||||
for just this component, otherwise return indicators for
|
||||
all existing components.
|
||||
:raises: NodeLocked if node is locked by another conductor.
|
||||
:raises: UnsupportedDriverExtension if the node's driver doesn't
|
||||
support management.
|
||||
:raises: InvalidParameterValue when the wrong driver info is
|
||||
specified.
|
||||
:raises: MissingParameterValue if missing supplied info.
|
||||
:returns: A dictionary of hardware components
|
||||
(:mod:`ironic.common.components`) as keys with indicator IDs
|
||||
as values.
|
||||
|
||||
::
|
||||
|
||||
{
|
||||
'chassis': {
|
||||
'enclosure-0': {
|
||||
"readonly": true,
|
||||
"states": [
|
||||
"OFF",
|
||||
"ON"
|
||||
]
|
||||
}
|
||||
},
|
||||
'system':
|
||||
'blade-A': {
|
||||
"readonly": true,
|
||||
"states": [
|
||||
"OFF",
|
||||
"ON"
|
||||
]
|
||||
}
|
||||
},
|
||||
'drive':
|
||||
'ssd0': {
|
||||
"readonly": true,
|
||||
"states": [
|
||||
"OFF",
|
||||
"ON"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
"""
|
||||
LOG.debug('RPC get_supported_indicators called for node %s', node_id)
|
||||
lock_purpose = 'getting supported indicators'
|
||||
with task_manager.acquire(context, node_id, shared=True,
|
||||
purpose=lock_purpose) as task:
|
||||
return task.driver.management.get_supported_indicators(
|
||||
task, component)
|
||||
|
||||
@METRICS.timer('ConductorManager.inspect_hardware')
|
||||
@messaging.expected_exceptions(exception.NoFreeConductorWorker,
|
||||
exception.NodeLocked,
|
||||
|
@ -101,13 +101,15 @@ class ConductorAPI(object):
|
||||
| 1.48 - Added allocation API
|
||||
| 1.49 - Added get_node_with_token and agent_token argument to
|
||||
heartbeat
|
||||
| 1.50 - Added set_indicator_state, get_indicator_state and
|
||||
| get_supported_indicators.
|
||||
|
||||
"""
|
||||
|
||||
# NOTE(rloo): This must be in sync with manager.ConductorManager's.
|
||||
# NOTE(pas-ha): This also must be in sync with
|
||||
# ironic.common.release_mappings.RELEASE_MAPPING['master']
|
||||
RPC_API_VERSION = '1.49'
|
||||
RPC_API_VERSION = '1.50'
|
||||
|
||||
def __init__(self, topic=None):
|
||||
super(ConductorAPI, self).__init__()
|
||||
@ -713,6 +715,89 @@ class ConductorAPI(object):
|
||||
return cctxt.call(context, 'get_supported_boot_devices',
|
||||
node_id=node_id)
|
||||
|
||||
def set_indicator_state(self, context, node_id, component,
|
||||
indicator, state, topic=None):
|
||||
"""Set node hardware components indicator to the desired state.
|
||||
|
||||
:param context: request context.
|
||||
:param node_id: node id or uuid.
|
||||
:param component: The hardware component, one of
|
||||
:mod:`ironic.common.components`.
|
||||
:param indicator: Indicator IDs, as
|
||||
reported by `get_supported_indicators`)
|
||||
:param state: Indicator state, one of
|
||||
mod:`ironic.common.indicator_states`.
|
||||
:param topic: RPC topic. Defaults to self.topic.
|
||||
:raises: NodeLocked if node is locked by another conductor.
|
||||
:raises: UnsupportedDriverExtension if the node's driver doesn't
|
||||
support management.
|
||||
:raises: InvalidParameterValue when the wrong driver info is
|
||||
specified or an invalid boot device is specified.
|
||||
:raises: MissingParameterValue if missing supplied info.
|
||||
|
||||
"""
|
||||
cctxt = self.client.prepare(topic=topic or self.topic, version='1.50')
|
||||
return cctxt.call(context, 'set_indicator_state', node_id=node_id,
|
||||
component=component, indicator=indicator,
|
||||
state=state)
|
||||
|
||||
def get_indicator_state(self, context, node_id, component, indicator,
|
||||
topic=None):
|
||||
"""Get node hardware component indicator state.
|
||||
|
||||
:param context: request context.
|
||||
:param node_id: node id or uuid.
|
||||
:param component: The hardware component, one of
|
||||
:mod:`ironic.common.components`.
|
||||
:param indicator: Indicator IDs, as
|
||||
reported by `get_supported_indicators`)
|
||||
:param topic: RPC topic. Defaults to self.topic.
|
||||
:raises: NodeLocked if node is locked by another conductor.
|
||||
:raises: UnsupportedDriverExtension if the node's driver doesn't
|
||||
support management.
|
||||
:raises: InvalidParameterValue when the wrong driver info is
|
||||
specified.
|
||||
:raises: MissingParameterValue if missing supplied info.
|
||||
:returns: Indicator state, one of
|
||||
mod:`ironic.common.indicator_states`.
|
||||
|
||||
"""
|
||||
cctxt = self.client.prepare(topic=topic or self.topic, version='1.50')
|
||||
return cctxt.call(context, 'get_indicator_state', node_id=node_id,
|
||||
component=component, indicator=indicator)
|
||||
|
||||
def get_supported_indicators(self, context, node_id,
|
||||
component=None, topic=None):
|
||||
"""Get node hardware components and their indicators.
|
||||
|
||||
:param context: request context.
|
||||
:param node_id: node id or uuid.
|
||||
:param component: The hardware component, one of
|
||||
:mod:`ironic.common.components`.
|
||||
:param topic: RPC topic. Defaults to self.topic.
|
||||
:raises: NodeLocked if node is locked by another conductor.
|
||||
:raises: UnsupportedDriverExtension if the node's driver doesn't
|
||||
support management.
|
||||
:raises: InvalidParameterValue when the wrong driver info is
|
||||
specified.
|
||||
:raises: MissingParameterValue if missing supplied info.
|
||||
:returns: A dictionary of hardware components
|
||||
(:mod:`ironic.common.components`) as keys with indicator IDs
|
||||
as values.
|
||||
|
||||
::
|
||||
|
||||
{
|
||||
'chassis': ['enclosure-0'],
|
||||
'system': ['blade-A']
|
||||
'drive': ['ssd0']
|
||||
}
|
||||
|
||||
"""
|
||||
cctxt = self.client.prepare(topic=topic or self.topic, version='1.50')
|
||||
return cctxt.call(context, 'get_supported_indicators', node_id=node_id,
|
||||
component=component)
|
||||
|
||||
def inspect_hardware(self, context, node_id, topic=None):
|
||||
"""Signals the conductor service to perform hardware introspection.
|
||||
|
||||
|
@ -33,8 +33,10 @@ from ironic.api.controllers.v1 import notification_utils
|
||||
from ironic.api.controllers.v1 import utils as api_utils
|
||||
from ironic.api.controllers.v1 import versions
|
||||
from ironic.common import boot_devices
|
||||
from ironic.common import components
|
||||
from ironic.common import driver_factory
|
||||
from ironic.common import exception
|
||||
from ironic.common import indicator_states
|
||||
from ironic.common import policy
|
||||
from ironic.common import states
|
||||
from ironic.conductor import rpcapi
|
||||
@ -2113,6 +2115,152 @@ class TestListNodes(test_api_base.BaseApiTest):
|
||||
self.assertEqual("******", data["driver_info"]["ssh_password"])
|
||||
self.assertEqual("******", data["driver_info"]["ssh_key_contents"])
|
||||
|
||||
@mock.patch.object(rpcapi.ConductorAPI, 'get_indicator_state')
|
||||
def test_get_indicator_state(self, mock_gis):
|
||||
node = obj_utils.create_test_node(self.context)
|
||||
expected_data = {
|
||||
'state': indicator_states.ON
|
||||
}
|
||||
mock_gis.return_value = indicator_states.ON
|
||||
component = components.SYSTEM
|
||||
indicator_id = 'led'
|
||||
indicator_name = indicator_id + '@' + component
|
||||
data = self.get_json(
|
||||
'/nodes/%s/management/indicators'
|
||||
'/%s' % (node.uuid, indicator_name))
|
||||
self.assertEqual(expected_data, data)
|
||||
mock_gis.assert_called_once_with(
|
||||
mock.ANY, node.uuid, component, indicator_id,
|
||||
topic='test-topic')
|
||||
|
||||
@mock.patch.object(rpcapi.ConductorAPI, 'get_indicator_state')
|
||||
def test_get_indicator_state_versioning(self, mock_gis):
|
||||
node = obj_utils.create_test_node(self.context, name='spam')
|
||||
expected_data = {
|
||||
'state': indicator_states.ON
|
||||
}
|
||||
mock_gis.return_value = indicator_states.ON
|
||||
component = components.SYSTEM
|
||||
indicator_id = 'led'
|
||||
indicator_name = indicator_id + '@' + component
|
||||
data = self.get_json(
|
||||
'/nodes/%s/management/indicators'
|
||||
'/%s' % (node.uuid, indicator_name),
|
||||
headers={api_base.Version.string: "1.63"})
|
||||
self.assertEqual(expected_data, data)
|
||||
mock_gis.assert_called_once_with(
|
||||
mock.ANY, node.uuid, component, indicator_id,
|
||||
topic='test-topic')
|
||||
|
||||
@mock.patch.object(rpcapi.ConductorAPI, 'get_indicator_state')
|
||||
def test_get_indicator_state_iface_not_supported(self, mock_gis):
|
||||
node = obj_utils.create_test_node(self.context)
|
||||
mock_gis.side_effect = exception.UnsupportedDriverExtension(
|
||||
extension='management', driver='test-driver')
|
||||
component = components.SYSTEM
|
||||
indicator_id = 'led'
|
||||
indicator_name = indicator_id + '@' + component
|
||||
ret = self.get_json(
|
||||
'/nodes/%s/management/indicators'
|
||||
'/%s' % (node.uuid, indicator_name),
|
||||
expect_errors=True)
|
||||
self.assertEqual(http_client.BAD_REQUEST, ret.status_code)
|
||||
self.assertTrue(ret.json['error_message'])
|
||||
mock_gis.assert_called_once_with(
|
||||
mock.ANY, node.uuid, component, indicator_id,
|
||||
topic='test-topic')
|
||||
|
||||
@mock.patch.object(rpcapi.ConductorAPI, 'get_supported_indicators')
|
||||
def test_get_supported_indicators(self, mock_gsi):
|
||||
mock_gsi.return_value = {
|
||||
components.CHASSIS: {
|
||||
'led': {
|
||||
'readonly': True,
|
||||
'states': [
|
||||
'OFF',
|
||||
'ON'
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
node = obj_utils.create_test_node(self.context)
|
||||
|
||||
expected_data = {
|
||||
'indicators': [
|
||||
{'component': 'chassis',
|
||||
'name': 'led@chassis',
|
||||
'readonly': True,
|
||||
'states': ['OFF', 'ON'],
|
||||
'links': [
|
||||
{'href': 'http://localhost/v1/nodes/1be26c0b-03f2-4d2e'
|
||||
'-ae87-c02d7f33c123/management/indicators/'
|
||||
'led@chassis',
|
||||
'rel': 'self'},
|
||||
{'href': 'http://localhost/nodes/1be26c0b-03f2-4d2e-ae'
|
||||
'87-c02d7f33c123/management/indicators/'
|
||||
'led@chassis',
|
||||
'rel': 'bookmark'}]}
|
||||
]
|
||||
}
|
||||
|
||||
data = self.get_json('/nodes/%s/management/indicators'
|
||||
% node.uuid)
|
||||
self.assertEqual(expected_data, data)
|
||||
mock_gsi.assert_called_once_with(
|
||||
mock.ANY, node.uuid, topic='test-topic')
|
||||
|
||||
@mock.patch.object(rpcapi.ConductorAPI, 'get_supported_indicators')
|
||||
def test_get_supported_indicators_versioning(self, mock_gsi):
|
||||
mock_gsi.return_value = {
|
||||
components.CHASSIS: {
|
||||
'led': {
|
||||
'readonly': True,
|
||||
'states': [
|
||||
'OFF',
|
||||
'ON'
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
node = obj_utils.create_test_node(self.context)
|
||||
|
||||
expected_data = {
|
||||
'indicators': [
|
||||
{'component': 'chassis',
|
||||
'name': 'led@chassis',
|
||||
'readonly': True,
|
||||
'states': ['OFF', 'ON'],
|
||||
'links': [
|
||||
{'href': 'http://localhost/v1/nodes/1be26c0b-03f2-4d2e'
|
||||
'-ae87-c02d7f33c123/management/indicators/'
|
||||
'led@chassis',
|
||||
'rel': 'self'},
|
||||
{'href': 'http://localhost/nodes/1be26c0b-03f2-4d2e-ae'
|
||||
'87-c02d7f33c123/management/indicators/'
|
||||
'led@chassis',
|
||||
'rel': 'bookmark'}]}
|
||||
]
|
||||
}
|
||||
|
||||
data = self.get_json('/nodes/%s/management/indicators'
|
||||
% node.uuid,
|
||||
headers={api_base.Version.string: "1.63"})
|
||||
self.assertEqual(expected_data, data)
|
||||
mock_gsi.assert_called_once_with(
|
||||
mock.ANY, node.uuid, topic='test-topic')
|
||||
|
||||
@mock.patch.object(rpcapi.ConductorAPI, 'get_supported_indicators')
|
||||
def test_get_supported_indicators_iface_not_supported(self, mock_gsi):
|
||||
node = obj_utils.create_test_node(self.context)
|
||||
mock_gsi.side_effect = exception.UnsupportedDriverExtension(
|
||||
extension='management', driver='test-driver')
|
||||
ret = self.get_json('/nodes/%s/management/indicators' %
|
||||
node.uuid, expect_errors=True)
|
||||
self.assertEqual(http_client.BAD_REQUEST, ret.status_code)
|
||||
self.assertTrue(ret.json['error_message'])
|
||||
mock_gsi.assert_called_once_with(
|
||||
mock.ANY, node.uuid, topic='test-topic')
|
||||
|
||||
|
||||
class TestPatch(test_api_base.BaseApiTest):
|
||||
|
||||
@ -4657,7 +4805,7 @@ class TestPut(test_api_base.BaseApiTest):
|
||||
ret = self.put_json('/nodes/%s/states/provision' % self.node.uuid,
|
||||
{'target': states.ACTIVE,
|
||||
'configdrive': fake_cd},
|
||||
headers={api_base.Version.string: '1.59'})
|
||||
headers={api_base.Version.string: '1.60'})
|
||||
self.assertEqual(http_client.ACCEPTED, ret.status_code)
|
||||
self.assertEqual(b'', ret.body)
|
||||
self.mock_dnd.assert_called_once_with(context=mock.ANY,
|
||||
@ -5535,6 +5683,85 @@ class TestPut(test_api_base.BaseApiTest):
|
||||
headers={api_base.Version.string: "1.41"})
|
||||
self.assertEqual(http_client.ACCEPTED, ret.status_code)
|
||||
|
||||
@mock.patch.object(rpcapi.ConductorAPI, 'set_indicator_state')
|
||||
def test_set_indicator_state(self, mock_sis):
|
||||
component = components.SYSTEM
|
||||
indicator_id = 'led'
|
||||
indicator_name = indicator_id + '@' + component
|
||||
state = indicator_states.ON
|
||||
ret = self.put_json(
|
||||
'/nodes/%s/management/indicators'
|
||||
'/%s' % (self.node.uuid, indicator_name),
|
||||
{'state': state})
|
||||
self.assertEqual(http_client.NO_CONTENT, ret.status_code)
|
||||
self.assertEqual(b'', ret.body)
|
||||
mock_sis.assert_called_once_with(
|
||||
mock.ANY, self.node.uuid, component, indicator_id, state,
|
||||
topic='test-topic')
|
||||
|
||||
@mock.patch.object(rpcapi.ConductorAPI, 'set_indicator_state')
|
||||
def test_set_indicator_state_versioning(self, mock_sis):
|
||||
component = components.SYSTEM
|
||||
indicator_id = 'led'
|
||||
indicator_name = indicator_id + '@' + component
|
||||
state = indicator_states.ON
|
||||
ret = self.put_json(
|
||||
'/nodes/%s/management/indicators'
|
||||
'/%s' % (self.node.uuid, indicator_name),
|
||||
{'state': state}, headers={api_base.Version.string: "1.63"})
|
||||
|
||||
self.assertEqual(http_client.NO_CONTENT, ret.status_code)
|
||||
self.assertEqual(b'', ret.body)
|
||||
mock_sis.assert_called_once_with(
|
||||
mock.ANY, self.node.uuid, component, indicator_id, state,
|
||||
topic='test-topic')
|
||||
|
||||
@mock.patch.object(rpcapi.ConductorAPI, 'set_indicator_state')
|
||||
def test_set_indicator_state_not_supported(self, mock_sis):
|
||||
mock_sis.side_effect = exception.UnsupportedDriverExtension(
|
||||
extension='management', driver='test-driver')
|
||||
component = components.SYSTEM
|
||||
indicator_id = 'led'
|
||||
indicator_name = indicator_id + '@' + component
|
||||
state = indicator_states.ON
|
||||
ret = self.put_json(
|
||||
'/nodes/%s/management/indicators'
|
||||
'/%s' % (self.node.uuid, indicator_name),
|
||||
{'state': state}, expect_errors=True)
|
||||
self.assertEqual(http_client.BAD_REQUEST, ret.status_code)
|
||||
self.assertTrue(ret.json['error_message'])
|
||||
mock_sis.assert_called_once_with(
|
||||
mock.ANY, self.node.uuid, component, indicator_id, state,
|
||||
topic='test-topic')
|
||||
|
||||
@mock.patch.object(rpcapi.ConductorAPI, 'set_indicator_state')
|
||||
def test_set_indicator_state_qs(self, mock_sis):
|
||||
component = components.SYSTEM
|
||||
indicator_id = 'led'
|
||||
indicator_name = indicator_id + '@' + component
|
||||
state = indicator_states.ON
|
||||
ret = self.put_json(
|
||||
'/nodes/%s/management/indicators/%s?'
|
||||
'state=%s' % (self.node.uuid, indicator_name, state), {})
|
||||
self.assertEqual(http_client.NO_CONTENT, ret.status_code)
|
||||
self.assertEqual(b'', ret.body)
|
||||
mock_sis.assert_called_once_with(
|
||||
mock.ANY, self.node.uuid, component, indicator_id, state,
|
||||
topic='test-topic')
|
||||
|
||||
@mock.patch.object(rpcapi.ConductorAPI, 'set_indicator_state')
|
||||
def test_set_indicator_state_invalid_value(self, mock_sis):
|
||||
mock_sis.side_effect = exception.InvalidParameterValue('error')
|
||||
component = components.SYSTEM
|
||||
indicator_id = 'led'
|
||||
indicator_name = indicator_id + '@' + component
|
||||
ret = self.put_json(
|
||||
'/nodes/%s/management/indicators/%s?'
|
||||
'state=glow' % (self.node.uuid, indicator_name), {},
|
||||
expect_errors=True)
|
||||
self.assertEqual('application/json', ret.content_type)
|
||||
self.assertEqual(http_client.BAD_REQUEST, ret.status_code)
|
||||
|
||||
|
||||
class TestCheckCleanSteps(base.TestCase):
|
||||
def test__check_clean_steps_not_list(self):
|
||||
|
@ -33,9 +33,11 @@ from oslo_versionedobjects import base as ovo_base
|
||||
from oslo_versionedobjects import fields
|
||||
|
||||
from ironic.common import boot_devices
|
||||
from ironic.common import components
|
||||
from ironic.common import driver_factory
|
||||
from ironic.common import exception
|
||||
from ironic.common import images
|
||||
from ironic.common import indicator_states
|
||||
from ironic.common import nova
|
||||
from ironic.common import states
|
||||
from ironic.conductor import cleaning
|
||||
@ -4068,6 +4070,56 @@ class BootDeviceTestCase(mgr_utils.ServiceSetUpMixin, db_base.DbTestCase):
|
||||
self.assertEqual([boot_devices.PXE], bootdevs)
|
||||
|
||||
|
||||
@mgr_utils.mock_record_keepalive
|
||||
class IndicatorsTestCase(mgr_utils.ServiceSetUpMixin, db_base.DbTestCase):
|
||||
|
||||
@mock.patch.object(fake.FakeManagement, 'set_indicator_state',
|
||||
autospec=True)
|
||||
@mock.patch.object(fake.FakeManagement, 'validate', autospec=True)
|
||||
def test_set_indicator_state(self, mock_val, mock_sbd):
|
||||
node = obj_utils.create_test_node(self.context, driver='fake-hardware')
|
||||
self.service.set_indicator_state(
|
||||
self.context, node.uuid, components.CHASSIS,
|
||||
'led', indicator_states.ON)
|
||||
mock_val.assert_called_once_with(mock.ANY, mock.ANY)
|
||||
mock_sbd.assert_called_once_with(
|
||||
mock.ANY, mock.ANY, components.CHASSIS, 'led', indicator_states.ON)
|
||||
|
||||
def test_get_indicator_state(self):
|
||||
node = obj_utils.create_test_node(self.context, driver='fake-hardware')
|
||||
state = self.service.get_indicator_state(
|
||||
self.context, node.uuid, components.CHASSIS, 'led-0')
|
||||
expected = indicator_states.ON
|
||||
self.assertEqual(expected, state)
|
||||
|
||||
def test_get_supported_indicators(self):
|
||||
node = obj_utils.create_test_node(self.context, driver='fake-hardware')
|
||||
indicators = self.service.get_supported_indicators(
|
||||
self.context, node.uuid)
|
||||
expected = {
|
||||
'chassis': {
|
||||
'led-0': {
|
||||
'readonly': True,
|
||||
'states': [
|
||||
indicator_states.OFF,
|
||||
indicator_states.ON
|
||||
]
|
||||
}
|
||||
},
|
||||
'system': {
|
||||
'led': {
|
||||
'readonly': False,
|
||||
'states': [
|
||||
indicator_states.BLINKING,
|
||||
indicator_states.OFF,
|
||||
indicator_states.ON
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
self.assertEqual(expected, indicators)
|
||||
|
||||
|
||||
@mgr_utils.mock_record_keepalive
|
||||
class NmiTestCase(mgr_utils.ServiceSetUpMixin, db_base.DbTestCase):
|
||||
|
||||
|
@ -26,7 +26,9 @@ import oslo_messaging as messaging
|
||||
from oslo_messaging import _utils as messaging_utils
|
||||
|
||||
from ironic.common import boot_devices
|
||||
from ironic.common import components
|
||||
from ironic.common import exception
|
||||
from ironic.common import indicator_states
|
||||
from ironic.common import release_mappings
|
||||
from ironic.common import states
|
||||
from ironic.conductor import manager as conductor_manager
|
||||
@ -362,6 +364,29 @@ class RPCAPITestCase(db_base.DbTestCase):
|
||||
version='1.17',
|
||||
node_id=self.fake_node['uuid'])
|
||||
|
||||
def test_set_indicator_state(self):
|
||||
self._test_rpcapi('set_indicator_state',
|
||||
'call',
|
||||
version='1.50',
|
||||
node_id=self.fake_node['uuid'],
|
||||
component=components.CHASSIS,
|
||||
indicator='led',
|
||||
state=indicator_states.ON)
|
||||
|
||||
def test_get_indicator_state(self):
|
||||
self._test_rpcapi('get_indicator_state',
|
||||
'call',
|
||||
version='1.50',
|
||||
node_id=self.fake_node['uuid'],
|
||||
component=components.CHASSIS,
|
||||
indicator='led')
|
||||
|
||||
def test_get_supported_indicators(self):
|
||||
self._test_rpcapi('get_supported_indicators',
|
||||
'call',
|
||||
version='1.50',
|
||||
node_id=self.fake_node['uuid'])
|
||||
|
||||
def test_get_node_vendor_passthru_methods(self):
|
||||
self._test_rpcapi('get_node_vendor_passthru_methods',
|
||||
'call',
|
||||
|
@ -0,0 +1,6 @@
|
||||
---
|
||||
features:
|
||||
- |
|
||||
Adds REST API endpoints for indicator management. Three new endpoints, for
|
||||
listing, reading and setting the indicators, reside under the
|
||||
``/v1/nodes/<node_ident>/management/indicators`` location.
|
Loading…
Reference in New Issue
Block a user