API for node inventory
Add api to access node inventory Story: 2010275 Task: 46204 Change-Id: If50f665da5fbb16f7646f3d6195a6e14e7325b0a
This commit is contained in:
parent
320b1f0ca7
commit
2e80ea9099
40
api-ref/source/baremetal-api-v1-nodes-inventory.inc
Normal file
40
api-ref/source/baremetal-api-v1-nodes-inventory.inc
Normal file
@ -0,0 +1,40 @@
|
||||
.. -*- rst -*-
|
||||
|
||||
==============
|
||||
Node inventory
|
||||
==============
|
||||
|
||||
.. versionadded:: 1.81
|
||||
|
||||
Given a Node identifier, the API provides access to the introspection data
|
||||
associated to the Node via ``v1/nodes/{node_ident}/inventory`` endpoint.
|
||||
|
||||
Fetch node inventory
|
||||
===============================
|
||||
|
||||
.. rest_method:: GET /v1/nodes/{node_ident}/inventory
|
||||
|
||||
Normal response code: 200
|
||||
|
||||
Error codes:
|
||||
- 404 (NodeNotFound, InventoryNotRecorded)
|
||||
|
||||
Request
|
||||
-------
|
||||
|
||||
.. rest_parameters:: parameters.yaml
|
||||
|
||||
- node_ident: node_ident
|
||||
|
||||
Response
|
||||
--------
|
||||
|
||||
.. rest_parameters:: parameters.yaml
|
||||
|
||||
- inventory: n_inventory
|
||||
- plugin_data: n_plugin_data
|
||||
|
||||
**Example of inventory from a node:**
|
||||
|
||||
.. literalinclude:: samples/node-inventory-response.json
|
||||
:language: javascript
|
@ -1191,6 +1191,18 @@ n_indicators:
|
||||
in: body
|
||||
required: true
|
||||
type: array
|
||||
n_inventory:
|
||||
description: |
|
||||
Inventory of this node.
|
||||
in: body
|
||||
required: false
|
||||
type: JSON
|
||||
n_plugin_data:
|
||||
description: |
|
||||
Plugin data of this node.
|
||||
in: body
|
||||
required: false
|
||||
type: JSON
|
||||
n_portgroups:
|
||||
description: |
|
||||
Links to the collection of portgroups on this node.
|
||||
|
31
api-ref/source/samples/node-inventory-response.json
Normal file
31
api-ref/source/samples/node-inventory-response.json
Normal file
@ -0,0 +1,31 @@
|
||||
{
|
||||
"inventory": {
|
||||
"interfaces":[
|
||||
{
|
||||
"name":"eth0",
|
||||
"mac_address":"52:54:00:90:35:d6",
|
||||
"ipv4_address":"192.168.122.128",
|
||||
"ipv6_address":"fe80::5054:ff:fe90:35d6%eth0",
|
||||
"has_carrier":true,
|
||||
"lldp":null,
|
||||
"vendor":"0x1af4",
|
||||
"product":"0x0001"
|
||||
}
|
||||
],
|
||||
"cpu":{
|
||||
"model_name":"QEMU Virtual CPU version 2.5+",
|
||||
"frequency":null,
|
||||
"count":1,
|
||||
"architecture":"x86_64"
|
||||
}
|
||||
},
|
||||
"plugin_data":{
|
||||
"macs":[
|
||||
"52:54:00:90:35:d6"
|
||||
],
|
||||
"local_gb":10,
|
||||
"cpus":1,
|
||||
"cpu_arch":"x86_64",
|
||||
"memory_mb":2048
|
||||
}
|
||||
}
|
@ -2,6 +2,13 @@
|
||||
REST API Version History
|
||||
========================
|
||||
|
||||
1.81 (Antelope)
|
||||
----------------------
|
||||
|
||||
Add endpoint to retrieve introspection data for nodes via the REST API.
|
||||
|
||||
* ``GET /v1/nodes/{node_ident}/inventory/``
|
||||
|
||||
1.80 (Zed, 21.1)
|
||||
----------------------
|
||||
|
||||
|
@ -48,6 +48,7 @@ from ironic.common import states as ir_states
|
||||
from ironic.conductor import steps as conductor_steps
|
||||
import ironic.conf
|
||||
from ironic.drivers import base as driver_base
|
||||
from ironic.drivers.modules import inspector as inspector
|
||||
from ironic import objects
|
||||
|
||||
|
||||
@ -1944,6 +1945,39 @@ class NodeHistoryController(rest.RestController):
|
||||
node.uuid, event, detail=True)
|
||||
|
||||
|
||||
class NodeInventoryController(rest.RestController):
|
||||
|
||||
def __init__(self, node_ident):
|
||||
super(NodeInventoryController).__init__()
|
||||
self.node_ident = node_ident
|
||||
|
||||
def _node_inventory_convert(self, node_inventory):
|
||||
inventory_data = node_inventory['inventory_data']
|
||||
plugin_data = node_inventory['plugin_data']
|
||||
return {"inventory": inventory_data, "plugin_data": plugin_data}
|
||||
|
||||
@METRICS.timer('NodeInventoryController.get')
|
||||
@method.expose()
|
||||
@args.validate(node_ident=args.uuid_or_name)
|
||||
def get(self):
|
||||
"""Node inventory of the node.
|
||||
|
||||
:param node_ident: the UUID of a node.
|
||||
"""
|
||||
node = api_utils.check_node_policy_and_retrieve(
|
||||
'baremetal:node:inventory:get', self.node_ident)
|
||||
store_data = CONF.inspector.inventory_data_backend
|
||||
if store_data == 'none':
|
||||
raise exception.NotFound(
|
||||
(_("Cannot obtain node inventory because it was not stored")))
|
||||
if store_data == 'database':
|
||||
node_inventory = objects.NodeInventory.get_by_node_id(
|
||||
api.request.context, node.id)
|
||||
return self._node_inventory_convert(node_inventory)
|
||||
if store_data == 'swift':
|
||||
return inspector.get_introspection_data(node.uuid)
|
||||
|
||||
|
||||
class NodesController(rest.RestController):
|
||||
"""REST controller for Nodes."""
|
||||
|
||||
@ -1990,6 +2024,7 @@ class NodesController(rest.RestController):
|
||||
'bios': bios.NodeBiosController,
|
||||
'allocation': allocation.NodeAllocationController,
|
||||
'history': NodeHistoryController,
|
||||
'inventory': NodeInventoryController,
|
||||
}
|
||||
|
||||
@pecan.expose()
|
||||
@ -2013,7 +2048,9 @@ class NodesController(rest.RestController):
|
||||
or (remainder[0] == 'allocation'
|
||||
and not api_utils.allow_allocations())
|
||||
or (remainder[0] == 'history'
|
||||
and not api_utils.allow_node_history())):
|
||||
and not api_utils.allow_node_history())
|
||||
or (remainder[0] == 'inventory'
|
||||
and not api_utils.allow_node_inventory())):
|
||||
pecan.abort(http_client.NOT_FOUND)
|
||||
if remainder[0] == 'traits' and not api_utils.allow_traits():
|
||||
# NOTE(mgoddard): Returning here will ensure we exhibit the
|
||||
|
@ -1341,6 +1341,11 @@ def allow_node_history():
|
||||
return api.request.version.minor >= versions.MINOR_78_NODE_HISTORY
|
||||
|
||||
|
||||
def allow_node_inventory():
|
||||
"""Check if node inventory is allowed."""
|
||||
return api.request.version.minor >= versions.MINOR_81_NODE_INVENTORY
|
||||
|
||||
|
||||
def get_request_return_fields(fields, detail, default_fields,
|
||||
check_detail_version=allow_detail_query,
|
||||
check_fields_version=None):
|
||||
|
@ -118,6 +118,7 @@ BASE_VERSION = 1
|
||||
# v1.78: Add node history endpoint
|
||||
# v1.79: Change allocation behaviour to prefer node name match
|
||||
# v1.80: Marker to represent self service node creation/deletion
|
||||
# v1.81: Add node inventory
|
||||
MINOR_0_JUNO = 0
|
||||
MINOR_1_INITIAL_VERSION = 1
|
||||
MINOR_2_AVAILABLE_STATE = 2
|
||||
@ -199,6 +200,7 @@ MINOR_77_DRIVER_FIELDS_SELECTOR = 77
|
||||
MINOR_78_NODE_HISTORY = 78
|
||||
MINOR_79_ALLOCATION_NODE_NAME = 79
|
||||
MINOR_80_PROJECT_CREATE_DELETE_NODE = 80
|
||||
MINOR_81_NODE_INVENTORY = 81
|
||||
|
||||
# When adding another version, update:
|
||||
# - MINOR_MAX_VERSION
|
||||
@ -206,7 +208,7 @@ MINOR_80_PROJECT_CREATE_DELETE_NODE = 80
|
||||
# explanation of what changed in the new version
|
||||
# - common/release_mappings.py, RELEASE_MAPPING['master']['api']
|
||||
|
||||
MINOR_MAX_VERSION = MINOR_80_PROJECT_CREATE_DELETE_NODE
|
||||
MINOR_MAX_VERSION = MINOR_81_NODE_INVENTORY
|
||||
|
||||
# String representations of the minor and maximum versions
|
||||
_MIN_VERSION_STRING = '{}.{}'.format(BASE_VERSION, MINOR_1_INITIAL_VERSION)
|
||||
|
@ -954,6 +954,19 @@ node_policies = [
|
||||
# operating context.
|
||||
deprecated_rule=deprecated_node_get
|
||||
),
|
||||
policy.DocumentedRuleDefault(
|
||||
name='baremetal:node:inventory:get',
|
||||
check_str=SYSTEM_OR_OWNER_READER,
|
||||
scope_types=['system', 'project'],
|
||||
description='Retrieve introspection data for a node.',
|
||||
operations=[
|
||||
{'path': '/nodes/{node_ident}/inventory', 'method': 'GET'},
|
||||
],
|
||||
# This rule fallsback to deprecated_node_get in order to provide a
|
||||
# mechanism so the additional policies only engage in an updated
|
||||
# operating context.
|
||||
deprecated_rule=deprecated_node_get
|
||||
),
|
||||
|
||||
|
||||
]
|
||||
|
@ -511,7 +511,7 @@ RELEASE_MAPPING = {
|
||||
}
|
||||
},
|
||||
'master': {
|
||||
'api': '1.80',
|
||||
'api': '1.81',
|
||||
'rpc': '1.55',
|
||||
'objects': {
|
||||
'Allocation': ['1.1'],
|
||||
|
@ -168,6 +168,23 @@ class SwiftAPI(object):
|
||||
(parse_result.scheme, parse_result.netloc, url_path,
|
||||
None, None, None))
|
||||
|
||||
def get_object(self, object, container):
|
||||
"""Downloads a given object from Swift.
|
||||
|
||||
:param object: The name of the object in Swift
|
||||
:param container: The name of the container for the object.
|
||||
Defaults to the value set in the configuration options.
|
||||
:returns: Swift object
|
||||
:raises: utils.Error, if the Swift operation fails.
|
||||
"""
|
||||
try:
|
||||
obj = self.connection.download_object(object, container=container)
|
||||
except swift_exceptions.ClientException as e:
|
||||
operation = _("get object")
|
||||
raise exception.SwiftOperationError(operation=operation, error=e)
|
||||
|
||||
return obj
|
||||
|
||||
def delete_object(self, container, obj):
|
||||
"""Deletes the given Swift object.
|
||||
|
||||
|
@ -425,3 +425,20 @@ def store_introspection_data(node_uuid, inventory_data, plugin_data):
|
||||
plugin_data,
|
||||
container)
|
||||
return swift_object_name
|
||||
|
||||
|
||||
def get_introspection_data(node_uuid):
|
||||
"""Uploads introspection data to Swift.
|
||||
|
||||
:param data: data to store in Swift
|
||||
:param node_id: ID of the Ironic node that the data came from
|
||||
:returns: name of the Swift object that the data is stored in
|
||||
"""
|
||||
swift_api = swift.SwiftAPI()
|
||||
swift_object_name = '%s-%s' % (_OBJECT_NAME_PREFIX, node_uuid)
|
||||
container = CONF.inspector.swift_inventory_data_container
|
||||
inventory_data = swift_api.get_object(swift_object_name + '-inventory',
|
||||
container)
|
||||
plugin_data = swift_api.get_object(swift_object_name + '-plugin',
|
||||
container)
|
||||
return {"inventory": inventory_data, "plugin_data": plugin_data}
|
||||
|
@ -43,6 +43,7 @@ from ironic.common import indicator_states
|
||||
from ironic.common import policy
|
||||
from ironic.common import states
|
||||
from ironic.conductor import rpcapi
|
||||
from ironic.drivers.modules import inspector
|
||||
from ironic import objects
|
||||
from ironic.objects import fields as obj_fields
|
||||
from ironic import tests as tests_root
|
||||
@ -51,6 +52,7 @@ from ironic.tests.unit.api import base as test_api_base
|
||||
from ironic.tests.unit.api import utils as test_api_utils
|
||||
from ironic.tests.unit.objects import utils as obj_utils
|
||||
|
||||
CONF = inspector.CONF
|
||||
|
||||
with open(
|
||||
os.path.join(
|
||||
@ -7912,3 +7914,55 @@ class TestNodeHistory(test_api_base.BaseApiTest):
|
||||
self.assertIn('nodes/%s/history' % self.node.uuid, ret['next'])
|
||||
self.assertIn('limit=1', ret['next'])
|
||||
self.assertIn('marker=%s' % result_uuid, ret['next'])
|
||||
|
||||
|
||||
class TestNodeInventory(test_api_base.BaseApiTest):
|
||||
fake_inventory_data = {"cpu": "amd"}
|
||||
fake_plugin_data = {"disks": [{"name": "/dev/vda"}]}
|
||||
|
||||
def setUp(self):
|
||||
super(TestNodeInventory, self).setUp()
|
||||
self.version = "1.81"
|
||||
self.node = obj_utils.create_test_node(
|
||||
self.context,
|
||||
provision_state=states.AVAILABLE, name='node-81')
|
||||
self.node.save()
|
||||
self.node.obj_reset_changes()
|
||||
|
||||
def _add_inventory(self):
|
||||
self.inventory = objects.NodeInventory(
|
||||
node_id=self.node.id, inventory_data=self.fake_inventory_data,
|
||||
plugin_data=self.fake_plugin_data)
|
||||
self.inventory.create()
|
||||
|
||||
def test_get_old_version(self):
|
||||
ret = self.get_json('/nodes/%s/inventory' % self.node.uuid,
|
||||
headers={api_base.Version.string: "1.80"},
|
||||
expect_errors=True)
|
||||
self.assertEqual(http_client.NOT_FOUND, ret.status_code)
|
||||
|
||||
def test_get_inventory_no_inventory(self):
|
||||
ret = self.get_json('/nodes/%s/inventory' % self.node.uuid,
|
||||
headers={api_base.Version.string: self.version},
|
||||
expect_errors=True)
|
||||
self.assertEqual(http_client.NOT_FOUND, ret.status_code)
|
||||
|
||||
def test_get_inventory(self):
|
||||
self._add_inventory()
|
||||
CONF.set_override('inventory_data_backend', 'database',
|
||||
group='inspector')
|
||||
ret = self.get_json('/nodes/%s/inventory' % self.node.uuid,
|
||||
headers={api_base.Version.string: self.version})
|
||||
self.assertEqual({'inventory': self.fake_inventory_data,
|
||||
'plugin_data': self.fake_plugin_data}, ret)
|
||||
|
||||
@mock.patch.object(inspector, 'get_introspection_data', autospec=True)
|
||||
def test_get_inventory_swift(self, mock_get_data):
|
||||
CONF.set_override('inventory_data_backend', 'swift',
|
||||
group='inspector')
|
||||
mock_get_data.return_value = {"inventory": self.fake_inventory_data,
|
||||
"plugin_data": self.fake_plugin_data}
|
||||
ret = self.get_json('/nodes/%s/inventory' % self.node.uuid,
|
||||
headers={api_base.Version.string: self.version})
|
||||
self.assertEqual({'inventory': self.fake_inventory_data,
|
||||
'plugin_data': self.fake_plugin_data}, ret)
|
||||
|
@ -286,6 +286,8 @@ class TestRBACModelBeforeScopesBase(TestACLBase):
|
||||
db_utils.create_test_node_trait(
|
||||
node_id=fake_db_node['id'])
|
||||
fake_history = db_utils.create_test_history(node_id=fake_db_node.id)
|
||||
fake_inventory = db_utils.create_test_inventory(
|
||||
node_id=fake_db_node.id)
|
||||
# dedicated node for portgroup addition test to avoid
|
||||
# false positives with test runners.
|
||||
db_utils.create_test_node(
|
||||
@ -309,6 +311,7 @@ class TestRBACModelBeforeScopesBase(TestACLBase):
|
||||
'volume_target_ident': fake_db_volume_target['uuid'],
|
||||
'volume_connector_ident': fake_db_volume_connector['uuid'],
|
||||
'history_ident': fake_history['uuid'],
|
||||
'node_inventory': fake_inventory,
|
||||
})
|
||||
|
||||
|
||||
@ -415,6 +418,8 @@ class TestRBACProjectScoped(TestACLBase):
|
||||
resource_class="CUSTOM_TEST")
|
||||
owned_node_history = db_utils.create_test_history(
|
||||
node_id=owned_node.id)
|
||||
owned_node_inventory = db_utils.create_test_inventory(
|
||||
node_id=owned_node.id)
|
||||
|
||||
# Leased nodes
|
||||
leased_node = db_utils.create_test_node(
|
||||
@ -445,6 +450,8 @@ class TestRBACProjectScoped(TestACLBase):
|
||||
|
||||
leased_node_history = db_utils.create_test_history(
|
||||
node_id=leased_node.id)
|
||||
leased_node_inventory = db_utils.create_test_inventory(
|
||||
node_id=leased_node.id)
|
||||
|
||||
# Random objects that shouldn't be project visible
|
||||
other_node = db_utils.create_test_node(
|
||||
@ -480,7 +487,9 @@ class TestRBACProjectScoped(TestACLBase):
|
||||
'owner_allocation': fake_owner_allocation['uuid'],
|
||||
'lessee_allocation': fake_leased_allocation['uuid'],
|
||||
'owned_history_ident': owned_node_history['uuid'],
|
||||
'lessee_history_ident': leased_node_history['uuid']})
|
||||
'lessee_history_ident': leased_node_history['uuid'],
|
||||
'owned_inventory': owned_node_inventory,
|
||||
'leased_inventory': leased_node_inventory})
|
||||
|
||||
@ddt.file_data('test_rbac_project_scoped.yaml')
|
||||
@ddt.unpack
|
||||
|
@ -3402,3 +3402,41 @@ node_history_get_entry_admin:
|
||||
method: get
|
||||
headers: *third_party_admin_headers
|
||||
assert_status: 404
|
||||
|
||||
# Node inventory support
|
||||
|
||||
node_inventory_get_admin:
|
||||
path: '/v1/nodes/{owner_node_ident}/inventory'
|
||||
method: get
|
||||
headers: *owner_admin_headers
|
||||
assert_status: 200
|
||||
|
||||
node_inventory_get_member:
|
||||
path: '/v1/nodes/{owner_node_ident}/inventory'
|
||||
method: get
|
||||
headers: *owner_member_headers
|
||||
assert_status: 200
|
||||
|
||||
node_inventory_get_reader:
|
||||
path: '/v1/nodes/{owner_node_ident}/inventory'
|
||||
method: get
|
||||
headers: *owner_reader_headers
|
||||
assert_status: 200
|
||||
|
||||
lessee_node_inventory_get_admin:
|
||||
path: '/v1/nodes/{node_ident}/inventory'
|
||||
method: get
|
||||
headers: *lessee_admin_headers
|
||||
assert_status: 404
|
||||
|
||||
lessee_node_inventory_get_member:
|
||||
path: '/v1/nodes/{node_ident}/inventory'
|
||||
method: get
|
||||
headers: *lessee_member_headers
|
||||
assert_status: 404
|
||||
|
||||
lessee_node_inventory_get_reader:
|
||||
path: '/v1/nodes/{node_ident}/inventory'
|
||||
method: get
|
||||
headers: *lessee_reader_headers
|
||||
assert_status: 404
|
||||
|
@ -2123,3 +2123,17 @@ node_history_get_entry_reader:
|
||||
method: get
|
||||
headers: *reader_headers
|
||||
assert_status: 200
|
||||
|
||||
# Node inventory support
|
||||
|
||||
node_inventory_get_admin:
|
||||
path: '/v1/nodes/{node_ident}/inventory'
|
||||
method: get
|
||||
headers: *admin_headers
|
||||
assert_status: 200
|
||||
|
||||
node_inventory_get_reader:
|
||||
path: '/v1/nodes/{node_ident}/inventory'
|
||||
method: get
|
||||
headers: *reader_headers
|
||||
assert_status: 200
|
||||
|
@ -0,0 +1,5 @@
|
||||
---
|
||||
features:
|
||||
- |
|
||||
Adds API version ``1.81`` which enables fetching node inventory
|
||||
which might have been stored during introspection
|
Loading…
x
Reference in New Issue
Block a user