summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorZuul <zuul@review.opendev.org>2023-01-16 14:24:45 +0000
committerGerrit Code Review <review@openstack.org>2023-01-16 14:24:45 +0000
commitb11067cfdcef0a370177803eddb86aa972b12b3d (patch)
treead5981dbe666d64f5239a6ea0226cc5f16843b48
parentff33a81de848ffbdb44ad334de026943c64f7edd (diff)
parent2e80ea9099cd913216c1a57f1af45ebd1152a235 (diff)
downloadironic-b11067cfdcef0a370177803eddb86aa972b12b3d.tar.gz
Merge "API for node inventory"
-rw-r--r--api-ref/source/baremetal-api-v1-nodes-inventory.inc40
-rw-r--r--api-ref/source/parameters.yaml12
-rw-r--r--api-ref/source/samples/node-inventory-response.json31
-rw-r--r--doc/source/contributor/webapi-version-history.rst7
-rw-r--r--ironic/api/controllers/v1/node.py39
-rw-r--r--ironic/api/controllers/v1/utils.py5
-rw-r--r--ironic/api/controllers/v1/versions.py4
-rw-r--r--ironic/common/policy.py13
-rw-r--r--ironic/common/release_mappings.py2
-rw-r--r--ironic/common/swift.py17
-rw-r--r--ironic/drivers/modules/inspector.py17
-rw-r--r--ironic/tests/unit/api/controllers/v1/test_node.py54
-rw-r--r--ironic/tests/unit/api/test_acl.py11
-rw-r--r--ironic/tests/unit/api/test_rbac_project_scoped.yaml38
-rw-r--r--ironic/tests/unit/api/test_rbac_system_scoped.yaml14
-rw-r--r--releasenotes/notes/add-node-inventory-7cde961b14caa11e.yaml5
16 files changed, 305 insertions, 4 deletions
diff --git a/api-ref/source/baremetal-api-v1-nodes-inventory.inc b/api-ref/source/baremetal-api-v1-nodes-inventory.inc
new file mode 100644
index 000000000..4c36e5aa2
--- /dev/null
+++ b/api-ref/source/baremetal-api-v1-nodes-inventory.inc
@@ -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
diff --git a/api-ref/source/parameters.yaml b/api-ref/source/parameters.yaml
index d0da64ec2..b55ef405f 100644
--- a/api-ref/source/parameters.yaml
+++ b/api-ref/source/parameters.yaml
@@ -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.
diff --git a/api-ref/source/samples/node-inventory-response.json b/api-ref/source/samples/node-inventory-response.json
new file mode 100644
index 000000000..7916f6717
--- /dev/null
+++ b/api-ref/source/samples/node-inventory-response.json
@@ -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
+ }
+}
diff --git a/doc/source/contributor/webapi-version-history.rst b/doc/source/contributor/webapi-version-history.rst
index 074106bc8..51b4a8d03 100644
--- a/doc/source/contributor/webapi-version-history.rst
+++ b/doc/source/contributor/webapi-version-history.rst
@@ -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)
----------------------
diff --git a/ironic/api/controllers/v1/node.py b/ironic/api/controllers/v1/node.py
index 59b166db4..fc6d70481 100644
--- a/ironic/api/controllers/v1/node.py
+++ b/ironic/api/controllers/v1/node.py
@@ -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
diff --git a/ironic/api/controllers/v1/utils.py b/ironic/api/controllers/v1/utils.py
index 8de2d156d..0494077cc 100644
--- a/ironic/api/controllers/v1/utils.py
+++ b/ironic/api/controllers/v1/utils.py
@@ -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):
diff --git a/ironic/api/controllers/v1/versions.py b/ironic/api/controllers/v1/versions.py
index 763d92389..4dcfd7fb8 100644
--- a/ironic/api/controllers/v1/versions.py
+++ b/ironic/api/controllers/v1/versions.py
@@ -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)
diff --git a/ironic/common/policy.py b/ironic/common/policy.py
index 7fdd398f9..afce51c77 100644
--- a/ironic/common/policy.py
+++ b/ironic/common/policy.py
@@ -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
+ ),
]
diff --git a/ironic/common/release_mappings.py b/ironic/common/release_mappings.py
index 76c40fc2f..79636e30e 100644
--- a/ironic/common/release_mappings.py
+++ b/ironic/common/release_mappings.py
@@ -511,7 +511,7 @@ RELEASE_MAPPING = {
}
},
'master': {
- 'api': '1.80',
+ 'api': '1.81',
'rpc': '1.55',
'objects': {
'Allocation': ['1.1'],
diff --git a/ironic/common/swift.py b/ironic/common/swift.py
index dde94fb18..87cda4fad 100644
--- a/ironic/common/swift.py
+++ b/ironic/common/swift.py
@@ -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.
diff --git a/ironic/drivers/modules/inspector.py b/ironic/drivers/modules/inspector.py
index 20911cbaa..a4c8c1091 100644
--- a/ironic/drivers/modules/inspector.py
+++ b/ironic/drivers/modules/inspector.py
@@ -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}
diff --git a/ironic/tests/unit/api/controllers/v1/test_node.py b/ironic/tests/unit/api/controllers/v1/test_node.py
index 6531f36e7..2f880db7d 100644
--- a/ironic/tests/unit/api/controllers/v1/test_node.py
+++ b/ironic/tests/unit/api/controllers/v1/test_node.py
@@ -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)
diff --git a/ironic/tests/unit/api/test_acl.py b/ironic/tests/unit/api/test_acl.py
index cdc20d477..f5cbe498d 100644
--- a/ironic/tests/unit/api/test_acl.py
+++ b/ironic/tests/unit/api/test_acl.py
@@ -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
diff --git a/ironic/tests/unit/api/test_rbac_project_scoped.yaml b/ironic/tests/unit/api/test_rbac_project_scoped.yaml
index b55439ad1..b57f7fc5c 100644
--- a/ironic/tests/unit/api/test_rbac_project_scoped.yaml
+++ b/ironic/tests/unit/api/test_rbac_project_scoped.yaml
@@ -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
diff --git a/ironic/tests/unit/api/test_rbac_system_scoped.yaml b/ironic/tests/unit/api/test_rbac_system_scoped.yaml
index d74a5fcae..533356217 100644
--- a/ironic/tests/unit/api/test_rbac_system_scoped.yaml
+++ b/ironic/tests/unit/api/test_rbac_system_scoped.yaml
@@ -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
diff --git a/releasenotes/notes/add-node-inventory-7cde961b14caa11e.yaml b/releasenotes/notes/add-node-inventory-7cde961b14caa11e.yaml
new file mode 100644
index 000000000..93751e7d9
--- /dev/null
+++ b/releasenotes/notes/add-node-inventory-7cde961b14caa11e.yaml
@@ -0,0 +1,5 @@
+---
+features:
+ - |
+ Adds API version ``1.81`` which enables fetching node inventory
+ which might have been stored during introspection