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:
Ilya Etingof 2019-04-10 08:14:22 +02:00 committed by Julia Kreger
parent 9f07ad1b6e
commit 263fd021b2
11 changed files with 743 additions and 8 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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