Merge "Add Node BIOS support - REST API"
This commit is contained in:
commit
089ea08629
@ -2,6 +2,17 @@
|
|||||||
REST API Version History
|
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)
|
1.39 (Rocky, master)
|
||||||
--------------------
|
--------------------
|
||||||
|
|
||||||
|
127
ironic/api/controllers/v1/bios.py
Normal file
127
ironic/api/controllers/v1/bios.py
Normal 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)}
|
@ -79,6 +79,10 @@ def hide_fields_in_newer_versions(obj):
|
|||||||
obj.default_rescue_interface = wsme.Unset
|
obj.default_rescue_interface = wsme.Unset
|
||||||
obj.enabled_rescue_interfaces = 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):
|
class Driver(base.APIBase):
|
||||||
"""API representation of a driver."""
|
"""API representation of a driver."""
|
||||||
@ -99,6 +103,7 @@ class Driver(base.APIBase):
|
|||||||
"""A list containing links to driver properties"""
|
"""A list containing links to driver properties"""
|
||||||
|
|
||||||
"""Default interface for a hardware type"""
|
"""Default interface for a hardware type"""
|
||||||
|
default_bios_interface = wtypes.text
|
||||||
default_boot_interface = wtypes.text
|
default_boot_interface = wtypes.text
|
||||||
default_console_interface = wtypes.text
|
default_console_interface = wtypes.text
|
||||||
default_deploy_interface = wtypes.text
|
default_deploy_interface = wtypes.text
|
||||||
@ -112,6 +117,7 @@ class Driver(base.APIBase):
|
|||||||
default_vendor_interface = wtypes.text
|
default_vendor_interface = wtypes.text
|
||||||
|
|
||||||
"""A list of enabled interfaces for a hardware type"""
|
"""A list of enabled interfaces for a hardware type"""
|
||||||
|
enabled_bios_interfaces = [wtypes.text]
|
||||||
enabled_boot_interfaces = [wtypes.text]
|
enabled_boot_interfaces = [wtypes.text]
|
||||||
enabled_console_interfaces = [wtypes.text]
|
enabled_console_interfaces = [wtypes.text]
|
||||||
enabled_deploy_interfaces = [wtypes.text]
|
enabled_deploy_interfaces = [wtypes.text]
|
||||||
|
@ -28,6 +28,7 @@ from wsme import types as wtypes
|
|||||||
|
|
||||||
from ironic.api.controllers import base
|
from ironic.api.controllers import base
|
||||||
from ironic.api.controllers import link
|
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 collection
|
||||||
from ironic.api.controllers.v1 import notification_utils as notify
|
from ironic.api.controllers.v1 import notification_utils as notify
|
||||||
from ironic.api.controllers.v1 import port
|
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():
|
if not api_utils.allow_rescue_interface():
|
||||||
obj.rescue_interface = wsme.Unset
|
obj.rescue_interface = wsme.Unset
|
||||||
|
|
||||||
|
if not api_utils.allow_bios_interface():
|
||||||
|
obj.bios_interface = wsme.Unset
|
||||||
|
|
||||||
|
|
||||||
def update_state_in_older_versions(obj):
|
def update_state_in_older_versions(obj):
|
||||||
"""Change provision state names for API backwards compatibility.
|
"""Change provision state names for API backwards compatibility.
|
||||||
@ -1040,6 +1044,9 @@ class Node(base.APIBase):
|
|||||||
traits = wtypes.ArrayType(str)
|
traits = wtypes.ArrayType(str)
|
||||||
"""The traits associated with this node"""
|
"""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
|
# NOTE(deva): "conductor_affinity" shouldn't be presented on the
|
||||||
# API because it's an internal value. Don't add it here.
|
# 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,
|
deploy_interface=None, inspect_interface=None,
|
||||||
management_interface=None, power_interface=None,
|
management_interface=None, power_interface=None,
|
||||||
raid_interface=None, vendor_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
|
# NOTE(matty_dubs): The chassis_uuid getter() is based on the
|
||||||
# _chassis_uuid variable:
|
# _chassis_uuid variable:
|
||||||
sample._chassis_uuid = 'edcad704-b2da-41d5-96d9-afd580ecfa12'
|
sample._chassis_uuid = 'edcad704-b2da-41d5-96d9-afd580ecfa12'
|
||||||
@ -1463,6 +1471,7 @@ class NodesController(rest.RestController):
|
|||||||
'vifs': NodeVIFController,
|
'vifs': NodeVIFController,
|
||||||
'volume': volume.VolumeController,
|
'volume': volume.VolumeController,
|
||||||
'traits': NodeTraitsController,
|
'traits': NodeTraitsController,
|
||||||
|
'bios': bios.NodeBiosController,
|
||||||
}
|
}
|
||||||
|
|
||||||
@pecan.expose()
|
@pecan.expose()
|
||||||
@ -1476,7 +1485,9 @@ class NodesController(rest.RestController):
|
|||||||
if ((remainder[0] == 'portgroups'
|
if ((remainder[0] == 'portgroups'
|
||||||
and not api_utils.allow_portgroups_subcontrollers())
|
and not api_utils.allow_portgroups_subcontrollers())
|
||||||
or (remainder[0] == 'vifs'
|
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)
|
pecan.abort(http_client.NOT_FOUND)
|
||||||
if remainder[0] == 'traits' and not api_utils.allow_traits():
|
if remainder[0] == 'traits' and not api_utils.allow_traits():
|
||||||
# NOTE(mgoddard): Returning here will ensure we exhibit the
|
# 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):
|
and node.storage_interface is not wtypes.Unset):
|
||||||
raise exception.NotAcceptable()
|
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:
|
if node.traits is not wtypes.Unset:
|
||||||
msg = _("Cannot specify node traits on node creation. Traits must "
|
msg = _("Cannot specify node traits on node creation. Traits must "
|
||||||
"be set via the node traits API.")
|
"be set via the node traits API.")
|
||||||
@ -1874,19 +1889,8 @@ class NodesController(rest.RestController):
|
|||||||
chassis_uuid=api_node.chassis_uuid)
|
chassis_uuid=api_node.chassis_uuid)
|
||||||
return api_node
|
return api_node
|
||||||
|
|
||||||
@METRICS.timer('NodesController.patch')
|
# NOTE (yolanda): isolate validation to avoid patch too complex pep error
|
||||||
@wsme.validate(types.uuid, [NodePatchType])
|
def _validate_patch(self, patch):
|
||||||
@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)
|
|
||||||
|
|
||||||
if self.from_chassis:
|
if self.from_chassis:
|
||||||
raise exception.OperationNotPermitted()
|
raise exception.OperationNotPermitted()
|
||||||
|
|
||||||
@ -1917,6 +1921,25 @@ class NodesController(rest.RestController):
|
|||||||
if r_interface and not api_utils.allow_rescue_interface():
|
if r_interface and not api_utils.allow_rescue_interface():
|
||||||
raise exception.NotAcceptable()
|
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)
|
rpc_node = api_utils.get_rpc_node_with_suffix(node_ident)
|
||||||
|
|
||||||
remove_inst_uuid_patch = [{'op': 'remove', 'path': '/instance_uuid'}]
|
remove_inst_uuid_patch = [{'op': 'remove', 'path': '/instance_uuid'}]
|
||||||
|
@ -362,6 +362,8 @@ def check_allowed_fields(fields):
|
|||||||
"""
|
"""
|
||||||
if fields is None:
|
if fields is None:
|
||||||
return
|
return
|
||||||
|
if 'bios_interface' in fields and not allow_bios_interface():
|
||||||
|
raise exception.NotAcceptable()
|
||||||
if 'network_interface' in fields and not allow_network_interface():
|
if 'network_interface' in fields and not allow_network_interface():
|
||||||
raise exception.NotAcceptable()
|
raise exception.NotAcceptable()
|
||||||
if 'resource_class' in fields and not allow_resource_class():
|
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
|
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):
|
def get_controller_reserved_names(cls):
|
||||||
"""Get reserved names for a given controller.
|
"""Get reserved names for a given controller.
|
||||||
|
|
||||||
|
@ -76,6 +76,8 @@ BASE_VERSION = 1
|
|||||||
# v1.37: Add node traits.
|
# v1.37: Add node traits.
|
||||||
# v1.38: Add rescue and unrescue provision states
|
# v1.38: Add rescue and unrescue provision states
|
||||||
# v1.39: Add inspect wait provision state.
|
# v1.39: Add inspect wait provision state.
|
||||||
|
# v1.40: Add bios.properties.
|
||||||
|
# Add bios_interface to the node object.
|
||||||
|
|
||||||
MINOR_0_JUNO = 0
|
MINOR_0_JUNO = 0
|
||||||
MINOR_1_INITIAL_VERSION = 1
|
MINOR_1_INITIAL_VERSION = 1
|
||||||
@ -117,6 +119,7 @@ MINOR_36_AGENT_VERSION_HEARTBEAT = 36
|
|||||||
MINOR_37_NODE_TRAITS = 37
|
MINOR_37_NODE_TRAITS = 37
|
||||||
MINOR_38_RESCUE_INTERFACE = 38
|
MINOR_38_RESCUE_INTERFACE = 38
|
||||||
MINOR_39_INSPECT_WAIT = 39
|
MINOR_39_INSPECT_WAIT = 39
|
||||||
|
MINOR_40_BIOS_INTERFACE = 40
|
||||||
|
|
||||||
# When adding another version, update:
|
# When adding another version, update:
|
||||||
# - MINOR_MAX_VERSION
|
# - MINOR_MAX_VERSION
|
||||||
@ -124,7 +127,7 @@ MINOR_39_INSPECT_WAIT = 39
|
|||||||
# 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_39_INSPECT_WAIT
|
MINOR_MAX_VERSION = MINOR_40_BIOS_INTERFACE
|
||||||
|
|
||||||
# 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)
|
||||||
|
@ -199,6 +199,13 @@ node_policies = [
|
|||||||
[{'path': '/nodes/{node_ident}/traits', 'method': 'DELETE'},
|
[{'path': '/nodes/{node_ident}/traits', 'method': 'DELETE'},
|
||||||
{'path': '/nodes/{node_ident}/traits/{trait}',
|
{'path': '/nodes/{node_ident}/traits/{trait}',
|
||||||
'method': 'DELETE'}]),
|
'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 = [
|
port_policies = [
|
||||||
|
@ -100,7 +100,7 @@ RELEASE_MAPPING = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
'master': {
|
'master': {
|
||||||
'api': '1.39',
|
'api': '1.40',
|
||||||
'rpc': '1.44',
|
'rpc': '1.44',
|
||||||
'objects': {
|
'objects': {
|
||||||
'Node': ['1.25'],
|
'Node': ['1.25'],
|
||||||
|
@ -121,6 +121,7 @@ class TestListNodes(test_api_base.BaseApiTest):
|
|||||||
self.assertNotIn('traits', data['nodes'][0])
|
self.assertNotIn('traits', data['nodes'][0])
|
||||||
# never expose the chassis_id
|
# never expose the chassis_id
|
||||||
self.assertNotIn('chassis_id', data['nodes'][0])
|
self.assertNotIn('chassis_id', data['nodes'][0])
|
||||||
|
self.assertNotIn('bios_interface', data['nodes'][0])
|
||||||
|
|
||||||
def test_get_one(self):
|
def test_get_one(self):
|
||||||
node = obj_utils.create_test_node(self.context,
|
node = obj_utils.create_test_node(self.context,
|
||||||
@ -156,6 +157,7 @@ class TestListNodes(test_api_base.BaseApiTest):
|
|||||||
self.assertIn('traits', data)
|
self.assertIn('traits', data)
|
||||||
# never expose the chassis_id
|
# never expose the chassis_id
|
||||||
self.assertNotIn('chassis_id', data)
|
self.assertNotIn('chassis_id', data)
|
||||||
|
self.assertIn('bios_interface', data)
|
||||||
|
|
||||||
def test_get_one_with_json(self):
|
def test_get_one_with_json(self):
|
||||||
# Test backward compatibility with guess_content_type_from_ext
|
# 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'})
|
headers={api_base.Version.string: '1.36'})
|
||||||
self.assertNotIn('traits', data)
|
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):
|
def test_node_inspect_wait_state_between_api_versions(self):
|
||||||
node = obj_utils.create_test_node(self.context,
|
node = obj_utils.create_test_node(self.context,
|
||||||
provision_state='inspect wait')
|
provision_state='inspect wait')
|
||||||
@ -2345,10 +2354,11 @@ class TestPost(test_api_base.BaseApiTest):
|
|||||||
self.assertEqual('neutron', result['network_interface'])
|
self.assertEqual('neutron', result['network_interface'])
|
||||||
|
|
||||||
def test_create_node_specify_interfaces(self):
|
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',
|
all_interface_fields = api_utils.V31_FIELDS + ['network_interface',
|
||||||
'rescue_interface',
|
'rescue_interface',
|
||||||
'storage_interface']
|
'storage_interface',
|
||||||
|
'bios_interface']
|
||||||
for field in all_interface_fields:
|
for field in all_interface_fields:
|
||||||
if field == 'network_interface':
|
if field == 'network_interface':
|
||||||
cfg.CONF.set_override('enabled_%ss' % field, ['flat'])
|
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('application/json', response.content_type)
|
||||||
self.assertEqual(http_client.BAD_REQUEST, response.status_int)
|
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):
|
class TestDelete(test_api_base.BaseApiTest):
|
||||||
|
|
||||||
@ -4404,6 +4422,68 @@ class TestAttachDetachVif(test_api_base.BaseApiTest):
|
|||||||
self.assertTrue(ret.json['error_message'])
|
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):
|
class TestTraits(test_api_base.BaseApiTest):
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
|
@ -230,6 +230,31 @@ def create_test_volume_target(ctxt, **kw):
|
|||||||
return volume_target
|
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):
|
def get_payloads_with_schemas(from_module):
|
||||||
"""Get the Payload classes with SCHEMAs defined.
|
"""Get the Payload classes with SCHEMAs defined.
|
||||||
|
|
||||||
|
6
releasenotes/notes/add-node-bios-9c1c3d442e8acdac.yaml
Normal file
6
releasenotes/notes/add-node-bios-9c1c3d442e8acdac.yaml
Normal 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.
|
Loading…
x
Reference in New Issue
Block a user