Add Node BIOS support - REST API

Change-Id: Ie7570736498b4750eff2d9262f63ab0960b3b594
Partial-Bug: #1712032
Co-Authored-By: Yolanda Robla <yroblamo@redhat.com>
This commit is contained in:
zshi 2017-10-17 18:37:39 +08:00 committed by Yolanda Robla
parent 01ae88db37
commit 254d370331
11 changed files with 317 additions and 19 deletions

View File

@ -2,6 +2,17 @@
REST API Version History
========================
1.40 (Rocky, master)
---------------------
Added BIOS properties as sub resources of nodes:
* GET /v1/nodes/<node_ident>/bios
* GET /v1/nodes/<node_ident>/bios/<setting_name>
Added ``bios_interface`` field to the node object to allow getting and
setting the interface.
1.39 (Rocky, master)
--------------------

View File

@ -0,0 +1,127 @@
# Copyright 2018 Red Hat Inc.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from ironic_lib import metrics_utils
import pecan
from pecan import rest
import wsme
from wsme import types as wtypes
from ironic.api.controllers import base
from ironic.api.controllers import link
from ironic.api.controllers.v1 import types
from ironic.api.controllers.v1 import utils as api_utils
from ironic.api import expose
from ironic.common import exception
from ironic.common import policy
from ironic import objects
METRICS = metrics_utils.get_metrics_logger(__name__)
class BIOSSetting(base.APIBase):
"""API representation of a BIOS setting."""
name = wsme.wsattr(wtypes.text)
value = wsme.wsattr(wtypes.text)
links = wsme.wsattr([link.Link], readonly=True)
def __init__(self, **kwargs):
self.fields = []
fields = list(objects.BIOSSetting.fields)
for k in fields:
if hasattr(self, k):
self.fields.append(k)
value = kwargs.get(k, wtypes.Unset)
setattr(self, k, value)
@staticmethod
def _convert_with_links(bios, node_uuid, url):
"""Add links to the bios setting."""
name = bios.name
bios.links = [link.Link.make_link('self', url, 'nodes',
"%s/bios/%s" % (node_uuid, name)),
link.Link.make_link('bookmark', url, 'nodes',
"%s/bios/%s" % (node_uuid, name),
bookmark=True)]
return bios
@classmethod
def convert_with_links(cls, rpc_bios, node_uuid):
"""Add links to the bios setting."""
bios = BIOSSetting(**rpc_bios.as_dict())
return cls._convert_with_links(bios, node_uuid, pecan.request.host_url)
class BIOSSettingsCollection(wtypes.Base):
"""API representation of the bios settings for a node."""
bios = [BIOSSetting]
"""Node bios settings list"""
@staticmethod
def collection_from_list(node_ident, bios_settings):
col = BIOSSettingsCollection()
bios_list = []
for bios_setting in bios_settings:
bios_list.append(BIOSSetting.convert_with_links(bios_setting,
node_ident))
col.bios = bios_list
return col
class NodeBiosController(rest.RestController):
"""REST controller for bios."""
def __init__(self, node_ident=None):
super(NodeBiosController, self).__init__()
self.node_ident = node_ident
@METRICS.timer('NodeBiosController.get_all')
@expose.expose(BIOSSettingsCollection)
def get_all(self):
"""List node bios settings."""
cdict = pecan.request.context.to_policy_values()
policy.authorize('baremetal:node:bios:get', cdict, cdict)
node = api_utils.get_rpc_node(self.node_ident)
settings = objects.BIOSSettingList.get_by_node_id(
pecan.request.context, node.id)
return BIOSSettingsCollection.collection_from_list(self.node_ident,
settings)
@METRICS.timer('NodeBiosController.get_one')
@expose.expose({wtypes.text: BIOSSetting}, types.name)
def get_one(self, setting_name):
"""Retrieve information about the given bios setting.
:param setting_name: Logical name of the setting to retrieve.
"""
cdict = pecan.request.context.to_policy_values()
policy.authorize('baremetal:node:bios:get', cdict, cdict)
node = api_utils.get_rpc_node(self.node_ident)
try:
setting = objects.BIOSSetting.get(pecan.request.context, node.id,
setting_name)
except exception.BIOSSettingNotFound:
raise exception.BIOSSettingNotFound(node=node.uuid,
name=setting_name)
return {setting_name: BIOSSetting.convert_with_links(setting,
node.uuid)}

View File

@ -79,6 +79,10 @@ def hide_fields_in_newer_versions(obj):
obj.default_rescue_interface = wsme.Unset
obj.enabled_rescue_interfaces = wsme.Unset
if not api_utils.allow_bios_interface():
obj.default_bios_interface = wsme.Unset
obj.enabled_bios_interfaces = wsme.Unset
class Driver(base.APIBase):
"""API representation of a driver."""
@ -99,6 +103,7 @@ class Driver(base.APIBase):
"""A list containing links to driver properties"""
"""Default interface for a hardware type"""
default_bios_interface = wtypes.text
default_boot_interface = wtypes.text
default_console_interface = wtypes.text
default_deploy_interface = wtypes.text
@ -112,6 +117,7 @@ class Driver(base.APIBase):
default_vendor_interface = wtypes.text
"""A list of enabled interfaces for a hardware type"""
enabled_bios_interfaces = [wtypes.text]
enabled_boot_interfaces = [wtypes.text]
enabled_console_interfaces = [wtypes.text]
enabled_deploy_interfaces = [wtypes.text]

View File

@ -28,6 +28,7 @@ from wsme import types as wtypes
from ironic.api.controllers import base
from ironic.api.controllers import link
from ironic.api.controllers.v1 import bios
from ironic.api.controllers.v1 import collection
from ironic.api.controllers.v1 import notification_utils as notify
from ironic.api.controllers.v1 import port
@ -162,6 +163,9 @@ def hide_fields_in_newer_versions(obj):
if not api_utils.allow_rescue_interface():
obj.rescue_interface = wsme.Unset
if not api_utils.allow_bios_interface():
obj.bios_interface = wsme.Unset
def update_state_in_older_versions(obj):
"""Change provision state names for API backwards compatibility.
@ -1040,6 +1044,9 @@ class Node(base.APIBase):
traits = wtypes.ArrayType(str)
"""The traits associated with this node"""
bios_interface = wsme.wsattr(wtypes.text)
"""The bios interface to be used for this node"""
# NOTE(deva): "conductor_affinity" shouldn't be presented on the
# API because it's an internal value. Don't add it here.
@ -1194,7 +1201,8 @@ class Node(base.APIBase):
deploy_interface=None, inspect_interface=None,
management_interface=None, power_interface=None,
raid_interface=None, vendor_interface=None,
storage_interface=None, traits=[], rescue_interface=None)
storage_interface=None, traits=[], rescue_interface=None,
bios_interface=None)
# NOTE(matty_dubs): The chassis_uuid getter() is based on the
# _chassis_uuid variable:
sample._chassis_uuid = 'edcad704-b2da-41d5-96d9-afd580ecfa12'
@ -1463,6 +1471,7 @@ class NodesController(rest.RestController):
'vifs': NodeVIFController,
'volume': volume.VolumeController,
'traits': NodeTraitsController,
'bios': bios.NodeBiosController,
}
@pecan.expose()
@ -1476,7 +1485,9 @@ class NodesController(rest.RestController):
if ((remainder[0] == 'portgroups'
and not api_utils.allow_portgroups_subcontrollers())
or (remainder[0] == 'vifs'
and not api_utils.allow_vifs_subcontroller())):
and not api_utils.allow_vifs_subcontroller())
or (remainder[0] == 'bios' and
not api_utils.allow_bios_interface())):
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
@ -1827,6 +1838,10 @@ class NodesController(rest.RestController):
and node.storage_interface is not wtypes.Unset):
raise exception.NotAcceptable()
if (not api_utils.allow_bios_interface() and
node.bios_interface is not wtypes.Unset):
raise exception.NotAcceptable()
if node.traits is not wtypes.Unset:
msg = _("Cannot specify node traits on node creation. Traits must "
"be set via the node traits API.")
@ -1874,19 +1889,8 @@ class NodesController(rest.RestController):
chassis_uuid=api_node.chassis_uuid)
return api_node
@METRICS.timer('NodesController.patch')
@wsme.validate(types.uuid, [NodePatchType])
@expose.expose(Node, types.uuid_or_name, body=[NodePatchType])
def patch(self, node_ident, patch):
"""Update an existing node.
:param node_ident: UUID or logical name of a node.
:param patch: a json PATCH document to apply to this node.
"""
context = pecan.request.context
cdict = context.to_policy_values()
policy.authorize('baremetal:node:update', cdict, cdict)
# NOTE (yolanda): isolate validation to avoid patch too complex pep error
def _validate_patch(self, patch):
if self.from_chassis:
raise exception.OperationNotPermitted()
@ -1917,6 +1921,25 @@ class NodesController(rest.RestController):
if r_interface and not api_utils.allow_rescue_interface():
raise exception.NotAcceptable()
b_interface = api_utils.get_patch_values(patch, '/bios_interface')
if b_interface and not api_utils.allow_bios_interface():
raise exception.NotAcceptable()
@METRICS.timer('NodesController.patch')
@wsme.validate(types.uuid, [NodePatchType])
@expose.expose(Node, types.uuid_or_name, body=[NodePatchType])
def patch(self, node_ident, patch):
"""Update an existing node.
:param node_ident: UUID or logical name of a node.
:param patch: a json PATCH document to apply to this node.
"""
context = pecan.request.context
cdict = context.to_policy_values()
policy.authorize('baremetal:node:update', cdict, cdict)
self._validate_patch(patch)
rpc_node = api_utils.get_rpc_node_with_suffix(node_ident)
remove_inst_uuid_patch = [{'op': 'remove', 'path': '/instance_uuid'}]

View File

@ -362,6 +362,8 @@ def check_allowed_fields(fields):
"""
if fields is None:
return
if 'bios_interface' in fields and not allow_bios_interface():
raise exception.NotAcceptable()
if 'network_interface' in fields and not allow_network_interface():
raise exception.NotAcceptable()
if 'resource_class' in fields and not allow_resource_class():
@ -693,6 +695,14 @@ def allow_rescue_interface():
return pecan.request.version.minor >= versions.MINOR_38_RESCUE_INTERFACE
def allow_bios_interface():
"""Check if we should support bios interface.
Version 1.40 of the API added support for bios interface.
"""
return pecan.request.version.minor >= versions.MINOR_40_BIOS_INTERFACE
def get_controller_reserved_names(cls):
"""Get reserved names for a given controller.

View File

@ -76,6 +76,8 @@ BASE_VERSION = 1
# v1.37: Add node traits.
# v1.38: Add rescue and unrescue provision states
# v1.39: Add inspect wait provision state.
# v1.40: Add bios.properties.
# Add bios_interface to the node object.
MINOR_0_JUNO = 0
MINOR_1_INITIAL_VERSION = 1
@ -117,6 +119,7 @@ MINOR_36_AGENT_VERSION_HEARTBEAT = 36
MINOR_37_NODE_TRAITS = 37
MINOR_38_RESCUE_INTERFACE = 38
MINOR_39_INSPECT_WAIT = 39
MINOR_40_BIOS_INTERFACE = 40
# When adding another version, update:
# - MINOR_MAX_VERSION
@ -124,7 +127,7 @@ MINOR_39_INSPECT_WAIT = 39
# explanation of what changed in the new version
# - common/release_mappings.py, RELEASE_MAPPING['master']['api']
MINOR_MAX_VERSION = MINOR_39_INSPECT_WAIT
MINOR_MAX_VERSION = MINOR_40_BIOS_INTERFACE
# String representations of the minor and maximum versions
_MIN_VERSION_STRING = '{}.{}'.format(BASE_VERSION, MINOR_1_INITIAL_VERSION)

View File

@ -199,6 +199,13 @@ node_policies = [
[{'path': '/nodes/{node_ident}/traits', 'method': 'DELETE'},
{'path': '/nodes/{node_ident}/traits/{trait}',
'method': 'DELETE'}]),
policy.DocumentedRuleDefault(
'baremetal:node:bios:get',
'rule:is_admin or rule:is_observer',
'Retrieve Node BIOS information',
[{'path': '/nodes/{node_ident}/bios', 'method': 'GET'},
{'path': '/nodes/{node_ident}/bios/{setting}', 'method': 'GET'}])
]
port_policies = [

View File

@ -100,7 +100,7 @@ RELEASE_MAPPING = {
}
},
'master': {
'api': '1.39',
'api': '1.40',
'rpc': '1.44',
'objects': {
'Node': ['1.24'],

View File

@ -121,6 +121,7 @@ class TestListNodes(test_api_base.BaseApiTest):
self.assertNotIn('traits', data['nodes'][0])
# never expose the chassis_id
self.assertNotIn('chassis_id', data['nodes'][0])
self.assertNotIn('bios_interface', data['nodes'][0])
def test_get_one(self):
node = obj_utils.create_test_node(self.context,
@ -156,6 +157,7 @@ class TestListNodes(test_api_base.BaseApiTest):
self.assertIn('traits', data)
# never expose the chassis_id
self.assertNotIn('chassis_id', data)
self.assertIn('bios_interface', data)
def test_get_one_with_json(self):
# Test backward compatibility with guess_content_type_from_ext
@ -227,6 +229,13 @@ class TestListNodes(test_api_base.BaseApiTest):
headers={api_base.Version.string: '1.36'})
self.assertNotIn('traits', data)
def test_node_bios_hidden_in_lower_version(self):
node = obj_utils.create_test_node(self.context)
data = self.get_json(
'/nodes/%s' % node.uuid,
headers={api_base.Version.string: '1.39'})
self.assertNotIn('bios_interface', data)
def test_node_inspect_wait_state_between_api_versions(self):
node = obj_utils.create_test_node(self.context,
provision_state='inspect wait')
@ -2345,10 +2354,11 @@ class TestPost(test_api_base.BaseApiTest):
self.assertEqual('neutron', result['network_interface'])
def test_create_node_specify_interfaces(self):
headers = {api_base.Version.string: '1.38'}
headers = {api_base.Version.string: '1.40'}
all_interface_fields = api_utils.V31_FIELDS + ['network_interface',
'rescue_interface',
'storage_interface']
'storage_interface',
'bios_interface']
for field in all_interface_fields:
if field == 'network_interface':
cfg.CONF.set_override('enabled_%ss' % field, ['flat'])
@ -2841,6 +2851,14 @@ class TestPost(test_api_base.BaseApiTest):
self.assertEqual('application/json', response.content_type)
self.assertEqual(http_client.BAD_REQUEST, response.status_int)
def test_create_node_invalid_bios_interface(self):
ndict = test_api_utils.post_get_test_node(bios_interface='foo')
response = self.post_json('/nodes', ndict, expect_errors=True,
headers={api_base.Version.string:
str(api_v1.max_version())})
self.assertEqual('application/json', response.content_type)
self.assertEqual(http_client.BAD_REQUEST, response.status_int)
class TestDelete(test_api_base.BaseApiTest):
@ -4404,6 +4422,68 @@ class TestAttachDetachVif(test_api_base.BaseApiTest):
self.assertTrue(ret.json['error_message'])
class TestBIOS(test_api_base.BaseApiTest):
def setUp(self):
super(TestBIOS, self).setUp()
self.version = "1.40"
self.node = obj_utils.create_test_node(
self.context, id=1)
self.bios = obj_utils.create_test_bios_setting(self.context,
node_id=self.node.id)
def test_get_all_bios(self):
ret = self.get_json('/nodes/%s/bios' % self.node.uuid,
headers={api_base.Version.string: self.version})
expected_json = [
{u'created_at': ret['bios'][0]['created_at'],
u'updated_at': ret['bios'][0]['updated_at'],
u'links': [
{u'href': u'http://localhost/v1/nodes/' + self.node.uuid +
'/bios/virtualization', u'rel': u'self'},
{u'href': u'http://localhost/nodes/' + self.node.uuid +
'/bios/virtualization', u'rel': u'bookmark'}], u'name':
u'virtualization', u'value': u'on'}]
self.assertEqual({u'bios': expected_json}, ret)
def test_get_all_bios_fails_with_bad_version(self):
ret = self.get_json('/nodes/%s/bios' % self.node.uuid,
headers={api_base.Version.string: "1.39"},
expect_errors=True)
self.assertEqual(http_client.NOT_FOUND, ret.status_code)
def test_get_one_bios(self):
ret = self.get_json('/nodes/%s/bios/virtualization' % self.node.uuid,
headers={api_base.Version.string: self.version})
expected_json = {
u'virtualization': {
u'created_at': ret['virtualization']['created_at'],
u'updated_at': ret['virtualization']['updated_at'],
u'links': [
{u'href': u'http://localhost/v1/nodes/' + self.node.uuid +
'/bios/virtualization', u'rel': u'self'},
{u'href': u'http://localhost/nodes/' + self.node.uuid +
'/bios/virtualization', u'rel': u'bookmark'}],
u'name': u'virtualization', u'value': u'on'}}
self.assertEqual(expected_json, ret)
def test_get_one_bios_fails_with_bad_version(self):
ret = self.get_json('/nodes/%s/bios/virtualization' % self.node.uuid,
headers={api_base.Version.string: "1.39"},
expect_errors=True)
self.assertEqual(http_client.NOT_FOUND, ret.status_code)
def test_get_one_bios_fails_if_not_found(self):
ret = self.get_json('/nodes/%s/bios/fake_setting' % self.node.uuid,
headers={api_base.Version.string: self.version},
expect_errors=True)
self.assertEqual(http_client.NOT_FOUND, ret.status_code)
self.assertIn("fake_setting", ret.json['error_message'])
self.assertNotIn(self.node.id, ret.json['error_message'])
class TestTraits(test_api_base.BaseApiTest):
def setUp(self):

View File

@ -230,6 +230,31 @@ def create_test_volume_target(ctxt, **kw):
return volume_target
def get_test_bios_setting(ctxt, **kw):
"""Return a BiosSettingList object with appropriate attributes.
NOTE: The object leaves the attributes marked as changed, such
that a create() could be used to commit it to the DB.
"""
kw['object_type'] = 'bios'
db_bios_setting = db_utils.get_test_bios_setting(**kw)
bios_setting = objects.BIOSSetting(ctxt)
for key in db_bios_setting:
setattr(bios_setting, key, db_bios_setting[key])
return bios_setting
def create_test_bios_setting(ctxt, **kw):
"""Create and return a test bios setting list object.
Create a BIOS setting list in the DB and return a BIOSSettingList
object with appropriate attributes.
"""
bios_setting = get_test_bios_setting(ctxt, **kw)
bios_setting.create()
return bios_setting
def get_payloads_with_schemas(from_module):
"""Get the Payload classes with SCHEMAs defined.

View File

@ -0,0 +1,6 @@
---
features:
- Adds support for reading and changing the node's ``bios_interface`` field
and enables the GET endpoints to check BIOS settings, if they have already
been cached. This requires a compatible ``bios_interface`` to be set.
This feature is available starting with API version 1.40.