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
|
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)
|
1.62 (Ussuri, master)
|
||||||
---------------------
|
---------------------
|
||||||
|
|
||||||
|
@ -266,6 +266,184 @@ class BootDeviceController(rest.RestController):
|
|||||||
return {'supported_boot_devices': boot_devices}
|
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):
|
class InjectNmiController(rest.RestController):
|
||||||
|
|
||||||
@METRICS.timer('InjectNmiController.put')
|
@METRICS.timer('InjectNmiController.put')
|
||||||
@ -308,6 +486,9 @@ class NodeManagementController(rest.RestController):
|
|||||||
inject_nmi = InjectNmiController()
|
inject_nmi = InjectNmiController()
|
||||||
"""Expose inject_nmi as a sub-element of management"""
|
"""Expose inject_nmi as a sub-element of management"""
|
||||||
|
|
||||||
|
indicators = IndicatorController()
|
||||||
|
"""Expose indicators as a sub-element of management"""
|
||||||
|
|
||||||
|
|
||||||
class ConsoleInfo(base.Base):
|
class ConsoleInfo(base.Base):
|
||||||
"""API representation of the console information for a node."""
|
"""API representation of the console information for a node."""
|
||||||
|
@ -23,8 +23,8 @@ CONF = cfg.CONF
|
|||||||
BASE_VERSION = 1
|
BASE_VERSION = 1
|
||||||
|
|
||||||
# Here goes a short log of changes in every version.
|
# Here goes a short log of changes in every version.
|
||||||
# Refer to doc/source/dev/webapi-version-history.rst for a detailed explanation
|
# Refer to doc/source/contributor/webapi-version-history.rst for a detailed
|
||||||
# of what each version contains.
|
# explanation of what each version contains.
|
||||||
#
|
#
|
||||||
# v1.0: corresponds to Juno API, not supported since Kilo
|
# v1.0: corresponds to Juno API, not supported since Kilo
|
||||||
# v1.1: API at the point in time when versioning support was added,
|
# 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.60: Add owner to the allocation object.
|
||||||
# v1.61: Add retired and retired_reason to the node object.
|
# v1.61: Add retired and retired_reason to the node object.
|
||||||
# v1.62: Add agent_token support for agent communication.
|
# v1.62: Add agent_token support for agent communication.
|
||||||
|
# v1.63: Add support for indicators
|
||||||
|
|
||||||
MINOR_0_JUNO = 0
|
MINOR_0_JUNO = 0
|
||||||
MINOR_1_INITIAL_VERSION = 1
|
MINOR_1_INITIAL_VERSION = 1
|
||||||
@ -164,6 +165,7 @@ MINOR_59_CONFIGDRIVE_VENDOR_DATA = 59
|
|||||||
MINOR_60_ALLOCATION_OWNER = 60
|
MINOR_60_ALLOCATION_OWNER = 60
|
||||||
MINOR_61_NODE_RETIRED = 61
|
MINOR_61_NODE_RETIRED = 61
|
||||||
MINOR_62_AGENT_TOKEN = 62
|
MINOR_62_AGENT_TOKEN = 62
|
||||||
|
MINOR_63_INDICATORS = 63
|
||||||
|
|
||||||
# When adding another version, update:
|
# When adding another version, update:
|
||||||
# - MINOR_MAX_VERSION
|
# - MINOR_MAX_VERSION
|
||||||
@ -171,7 +173,7 @@ MINOR_62_AGENT_TOKEN = 62
|
|||||||
# explanation of what changed in the new version
|
# explanation of what changed in the new version
|
||||||
# - common/release_mappings.py, RELEASE_MAPPING['master']['api']
|
# - 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
|
# String representations of the minor and maximum versions
|
||||||
_MIN_VERSION_STRING = '{}.{}'.format(BASE_VERSION, MINOR_1_INITIAL_VERSION)
|
_MIN_VERSION_STRING = '{}.{}'.format(BASE_VERSION, MINOR_1_INITIAL_VERSION)
|
||||||
|
@ -157,6 +157,23 @@ node_policies = [
|
|||||||
[{'path': '/nodes/{node_ident}/management/boot_device',
|
[{'path': '/nodes/{node_ident}/management/boot_device',
|
||||||
'method': 'PUT'}]),
|
'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(
|
policy.DocumentedRuleDefault(
|
||||||
'baremetal:node:inject_nmi',
|
'baremetal:node:inject_nmi',
|
||||||
'rule:is_admin',
|
'rule:is_admin',
|
||||||
|
@ -214,8 +214,8 @@ RELEASE_MAPPING = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
'master': {
|
'master': {
|
||||||
'api': '1.62',
|
'api': '1.63',
|
||||||
'rpc': '1.49',
|
'rpc': '1.50',
|
||||||
'objects': {
|
'objects': {
|
||||||
'Allocation': ['1.1'],
|
'Allocation': ['1.1'],
|
||||||
'Node': ['1.33', '1.32'],
|
'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(rloo): This must be in sync with rpcapi.ConductorAPI's.
|
||||||
# NOTE(pas-ha): This also must be in sync with
|
# NOTE(pas-ha): This also must be in sync with
|
||||||
# ironic.common.release_mappings.RELEASE_MAPPING['master']
|
# ironic.common.release_mappings.RELEASE_MAPPING['master']
|
||||||
RPC_API_VERSION = '1.49'
|
RPC_API_VERSION = '1.50'
|
||||||
|
|
||||||
target = messaging.Target(version=RPC_API_VERSION)
|
target = messaging.Target(version=RPC_API_VERSION)
|
||||||
|
|
||||||
@ -2784,6 +2784,132 @@ class ConductorManager(base_manager.BaseConductorManager):
|
|||||||
purpose=lock_purpose) as task:
|
purpose=lock_purpose) as task:
|
||||||
return task.driver.management.get_supported_boot_devices(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')
|
@METRICS.timer('ConductorManager.inspect_hardware')
|
||||||
@messaging.expected_exceptions(exception.NoFreeConductorWorker,
|
@messaging.expected_exceptions(exception.NoFreeConductorWorker,
|
||||||
exception.NodeLocked,
|
exception.NodeLocked,
|
||||||
|
@ -101,13 +101,15 @@ class ConductorAPI(object):
|
|||||||
| 1.48 - Added allocation API
|
| 1.48 - Added allocation API
|
||||||
| 1.49 - Added get_node_with_token and agent_token argument to
|
| 1.49 - Added get_node_with_token and agent_token argument to
|
||||||
heartbeat
|
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(rloo): This must be in sync with manager.ConductorManager's.
|
||||||
# NOTE(pas-ha): This also must be in sync with
|
# NOTE(pas-ha): This also must be in sync with
|
||||||
# ironic.common.release_mappings.RELEASE_MAPPING['master']
|
# ironic.common.release_mappings.RELEASE_MAPPING['master']
|
||||||
RPC_API_VERSION = '1.49'
|
RPC_API_VERSION = '1.50'
|
||||||
|
|
||||||
def __init__(self, topic=None):
|
def __init__(self, topic=None):
|
||||||
super(ConductorAPI, self).__init__()
|
super(ConductorAPI, self).__init__()
|
||||||
@ -713,6 +715,89 @@ class ConductorAPI(object):
|
|||||||
return cctxt.call(context, 'get_supported_boot_devices',
|
return cctxt.call(context, 'get_supported_boot_devices',
|
||||||
node_id=node_id)
|
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):
|
def inspect_hardware(self, context, node_id, topic=None):
|
||||||
"""Signals the conductor service to perform hardware introspection.
|
"""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 utils as api_utils
|
||||||
from ironic.api.controllers.v1 import versions
|
from ironic.api.controllers.v1 import versions
|
||||||
from ironic.common import boot_devices
|
from ironic.common import boot_devices
|
||||||
|
from ironic.common import components
|
||||||
from ironic.common import driver_factory
|
from ironic.common import driver_factory
|
||||||
from ironic.common import exception
|
from ironic.common import exception
|
||||||
|
from ironic.common import indicator_states
|
||||||
from ironic.common import policy
|
from ironic.common import policy
|
||||||
from ironic.common import states
|
from ironic.common import states
|
||||||
from ironic.conductor import rpcapi
|
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_password"])
|
||||||
self.assertEqual("******", data["driver_info"]["ssh_key_contents"])
|
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):
|
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,
|
ret = self.put_json('/nodes/%s/states/provision' % self.node.uuid,
|
||||||
{'target': states.ACTIVE,
|
{'target': states.ACTIVE,
|
||||||
'configdrive': fake_cd},
|
'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(http_client.ACCEPTED, ret.status_code)
|
||||||
self.assertEqual(b'', ret.body)
|
self.assertEqual(b'', ret.body)
|
||||||
self.mock_dnd.assert_called_once_with(context=mock.ANY,
|
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"})
|
headers={api_base.Version.string: "1.41"})
|
||||||
self.assertEqual(http_client.ACCEPTED, ret.status_code)
|
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):
|
class TestCheckCleanSteps(base.TestCase):
|
||||||
def test__check_clean_steps_not_list(self):
|
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 oslo_versionedobjects import fields
|
||||||
|
|
||||||
from ironic.common import boot_devices
|
from ironic.common import boot_devices
|
||||||
|
from ironic.common import components
|
||||||
from ironic.common import driver_factory
|
from ironic.common import driver_factory
|
||||||
from ironic.common import exception
|
from ironic.common import exception
|
||||||
from ironic.common import images
|
from ironic.common import images
|
||||||
|
from ironic.common import indicator_states
|
||||||
from ironic.common import nova
|
from ironic.common import nova
|
||||||
from ironic.common import states
|
from ironic.common import states
|
||||||
from ironic.conductor import cleaning
|
from ironic.conductor import cleaning
|
||||||
@ -4068,6 +4070,56 @@ class BootDeviceTestCase(mgr_utils.ServiceSetUpMixin, db_base.DbTestCase):
|
|||||||
self.assertEqual([boot_devices.PXE], bootdevs)
|
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
|
@mgr_utils.mock_record_keepalive
|
||||||
class NmiTestCase(mgr_utils.ServiceSetUpMixin, db_base.DbTestCase):
|
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 oslo_messaging import _utils as messaging_utils
|
||||||
|
|
||||||
from ironic.common import boot_devices
|
from ironic.common import boot_devices
|
||||||
|
from ironic.common import components
|
||||||
from ironic.common import exception
|
from ironic.common import exception
|
||||||
|
from ironic.common import indicator_states
|
||||||
from ironic.common import release_mappings
|
from ironic.common import release_mappings
|
||||||
from ironic.common import states
|
from ironic.common import states
|
||||||
from ironic.conductor import manager as conductor_manager
|
from ironic.conductor import manager as conductor_manager
|
||||||
@ -362,6 +364,29 @@ class RPCAPITestCase(db_base.DbTestCase):
|
|||||||
version='1.17',
|
version='1.17',
|
||||||
node_id=self.fake_node['uuid'])
|
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):
|
def test_get_node_vendor_passthru_methods(self):
|
||||||
self._test_rpcapi('get_node_vendor_passthru_methods',
|
self._test_rpcapi('get_node_vendor_passthru_methods',
|
||||||
'call',
|
'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