Add dynamic interfaces fields to nodes API

This adds version 1.31 of the REST API, which adds dynamic interface
fields to the node object.

Change-Id: Ic8398a6093189a65a7c1ab5cf7e682577dde3257
Partial-Bug: #1524745
This commit is contained in:
Jim Rollenhagen 2017-01-24 16:09:12 +00:00
parent e776757812
commit 8570bee3d6
6 changed files with 193 additions and 2 deletions

View File

@ -2,6 +2,20 @@
REST API Version History
========================
**1.31** (Ocata)
Added the following fields to the node object, to allow getting and
setting interfaces for a dynamic driver:
* boot_interface
* console_interface
* deploy_interface
* inspect_interface
* management_interface
* power_interface
* raid_interface
* vendor_interface
**1.30** (Ocata)
Added dynamic driver APIs.

View File

@ -148,6 +148,10 @@ def hide_fields_in_newer_versions(obj):
if not api_utils.allow_resource_class():
obj.resource_class = wsme.Unset
if not api_utils.allow_dynamic_interfaces():
for field in api_utils.V31_FIELDS:
setattr(obj, field, wsme.Unset)
def update_state_in_older_versions(obj):
"""Change provision state names for API backwards compatibility.
@ -812,9 +816,33 @@ class Node(base.APIBase):
states = wsme.wsattr([link.Link], readonly=True)
"""Links to endpoint for retrieving and setting node states"""
boot_interface = wsme.wsattr(wtypes.text)
"""The boot interface to be used for this node"""
console_interface = wsme.wsattr(wtypes.text)
"""The console interface to be used for this node"""
deploy_interface = wsme.wsattr(wtypes.text)
"""The deploy interface to be used for this node"""
inspect_interface = wsme.wsattr(wtypes.text)
"""The inspect interface to be used for this node"""
management_interface = wsme.wsattr(wtypes.text)
"""The management interface to be used for this node"""
network_interface = wsme.wsattr(wtypes.text)
"""The network interface to be used for this node"""
power_interface = wsme.wsattr(wtypes.text)
"""The power interface to be used for this node"""
raid_interface = wsme.wsattr(wtypes.text)
"""The raid interface to be used for this node"""
vendor_interface = wsme.wsattr(wtypes.text)
"""The vendor 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.
@ -949,7 +977,11 @@ class Node(base.APIBase):
inspection_finished_at=None, inspection_started_at=time,
console_enabled=False, clean_step={},
raid_config=None, target_raid_config=None,
network_interface='flat', resource_class='baremetal-gold')
network_interface='flat', resource_class='baremetal-gold',
boot_interface=None, console_interface=None,
deploy_interface=None, inspect_interface=None,
management_interface=None, power_interface=None,
raid_interface=None, vendor_interface=None)
# NOTE(matty_dubs): The chassis_uuid getter() is based on the
# _chassis_uuid variable:
sample._chassis_uuid = 'edcad704-b2da-41d5-96d9-afd580ecfa12'
@ -1551,6 +1583,11 @@ class NodesController(rest.RestController):
n_interface is not wtypes.Unset):
raise exception.NotAcceptable()
if not api_utils.allow_dynamic_interfaces():
for field in api_utils.V31_FIELDS:
if getattr(node, field) is not wsme.Unset:
raise exception.NotAcceptable()
# NOTE(deva): get_topic_for checks if node.driver is in the hash ring
# and raises NoValidHost if it is not.
# We need to ensure that node has a UUID before it can
@ -1610,6 +1647,11 @@ class NodesController(rest.RestController):
if n_interfaces and not api_utils.allow_network_interface():
raise exception.NotAcceptable()
if not api_utils.allow_dynamic_interfaces():
for field in api_utils.V31_FIELDS:
if api_utils.get_patch_values(patch, '/%s' % field):
raise exception.NotAcceptable()
rpc_node = api_utils.get_rpc_node(node_ident)
remove_inst_uuid_patch = [{'op': 'remove', 'path': '/instance_uuid'}]

View File

@ -54,6 +54,17 @@ MIN_VERB_VERSIONS = {
states.VERBS['adopt']: versions.MINOR_17_ADOPT_VERB,
}
V31_FIELDS = [
'boot_interface',
'console_interface',
'deploy_interface',
'inspect_interface',
'management_interface',
'power_interface',
'raid_interface',
'vendor_interface',
]
def validate_limit(limit):
if limit is None:
@ -286,6 +297,9 @@ def check_allowed_fields(fields):
raise exception.NotAcceptable()
if 'resource_class' in fields and not allow_resource_class():
raise exception.NotAcceptable()
if not allow_dynamic_interfaces():
if set(V31_FIELDS).intersection(set(fields)):
raise exception.NotAcceptable()
def check_allowed_portgroup_fields(fields):
@ -525,6 +539,16 @@ def allow_dynamic_drivers():
versions.MINOR_30_DYNAMIC_DRIVERS)
def allow_dynamic_interfaces():
"""Check if dynamic interface fields are allowed.
Version 1.31 of the API added support for viewing and setting the fields
in ``V31_FIELDS`` on the node object.
"""
return (pecan.request.version.minor >=
versions.MINOR_31_DYNAMIC_INTERFACES)
def get_controller_reserved_names(cls):
"""Get reserved names for a given controller.

View File

@ -61,6 +61,7 @@ BASE_VERSION = 1
# v1.28: Add vifs subcontroller to node
# v1.29: Add inject nmi.
# v1.30: Add dynamic driver interactions.
# v1.31: Add dynamic interfaces fields to node.
MINOR_0_JUNO = 0
MINOR_1_INITIAL_VERSION = 1
@ -93,11 +94,12 @@ MINOR_27_SOFT_POWER_OFF = 27
MINOR_28_VIFS_SUBCONTROLLER = 28
MINOR_29_INJECT_NMI = 29
MINOR_30_DYNAMIC_DRIVERS = 30
MINOR_31_DYNAMIC_INTERFACES = 31
# When adding another version, update MINOR_MAX_VERSION and also update
# doc/source/dev/webapi-version-history.rst with a detailed explanation of
# what the version has changed.
MINOR_MAX_VERSION = MINOR_30_DYNAMIC_DRIVERS
MINOR_MAX_VERSION = MINOR_31_DYNAMIC_INTERFACES
# String representations of the minor and maximum versions
MIN_VERSION_STRING = '{}.{}'.format(BASE_VERSION, MINOR_1_INITIAL_VERSION)

View File

@ -116,6 +116,8 @@ class TestListNodes(test_api_base.BaseApiTest):
self.assertNotIn('target_raid_config', data['nodes'][0])
self.assertNotIn('network_interface', data['nodes'][0])
self.assertNotIn('resource_class', data['nodes'][0])
for field in api_utils.V31_FIELDS:
self.assertNotIn(field, data['nodes'][0])
# never expose the chassis_id
self.assertNotIn('chassis_id', data['nodes'][0])
@ -147,6 +149,8 @@ class TestListNodes(test_api_base.BaseApiTest):
self.assertIn('states', data)
self.assertIn('network_interface', data)
self.assertIn('resource_class', data)
for field in api_utils.V31_FIELDS:
self.assertIn(field, data)
# never expose the chassis_id
self.assertNotIn('chassis_id', data)
@ -158,6 +162,14 @@ class TestListNodes(test_api_base.BaseApiTest):
headers={api_base.Version.string: '1.8'})
self.assertNotIn('states', data)
def test_node_interface_fields_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.30'})
for field in api_utils.V31_FIELDS:
self.assertNotIn(field, data)
def test_get_one_custom_fields(self):
node = obj_utils.create_test_node(self.context,
chassis_id=self.chassis.id)
@ -237,6 +249,26 @@ class TestListNodes(test_api_base.BaseApiTest):
headers={api_base.Version.string: str(api_v1.MAX_VER)})
self.assertIn('network_interface', response)
def test_get_all_interface_fields_invalid_api_version(self):
node = obj_utils.create_test_node(self.context,
chassis_id=self.chassis.id)
fields_arg = ','.join(api_utils.V31_FIELDS)
response = self.get_json(
'/nodes/%s?fields=%s' % (node.uuid, fields_arg),
headers={api_base.Version.string: str(api_v1.MIN_VER)},
expect_errors=True)
self.assertEqual(http_client.NOT_ACCEPTABLE, response.status_int)
def test_get_all_interface_fields(self):
node = obj_utils.create_test_node(self.context,
chassis_id=self.chassis.id)
fields_arg = ','.join(api_utils.V31_FIELDS)
response = self.get_json(
'/nodes/%s?fields=%s' % (node.uuid, fields_arg),
headers={api_base.Version.string: str(api_v1.MAX_VER)})
for field in api_utils.V31_FIELDS:
self.assertIn(field, response)
def test_detail(self):
node = obj_utils.create_test_node(self.context,
chassis_id=self.chassis.id)
@ -261,6 +293,9 @@ class TestListNodes(test_api_base.BaseApiTest):
self.assertIn('raid_config', data['nodes'][0])
self.assertIn('target_raid_config', data['nodes'][0])
self.assertIn('network_interface', data['nodes'][0])
self.assertIn('resource_class', data['nodes'][0])
for field in api_utils.V31_FIELDS:
self.assertIn(field, data['nodes'][0])
# never expose the chassis_id
self.assertNotIn('chassis_id', data['nodes'][0])
@ -357,6 +392,18 @@ class TestListNodes(test_api_base.BaseApiTest):
self.assertEqual(node.resource_class,
new_data['nodes'][0]["resource_class"])
def test_hide_fields_in_newer_versions_interface_fields(self):
node = obj_utils.create_test_node(self.context)
data = self.get_json(
'/nodes/detail', headers={api_base.Version.string: '1.30'})
for field in api_utils.V31_FIELDS:
self.assertNotIn(field, data['nodes'][0])
new_data = self.get_json(
'/nodes/detail', headers={api_base.Version.string: '1.31'})
for field in api_utils.V31_FIELDS:
self.assertEqual(getattr(node, field),
new_data['nodes'][0][field])
def test_many(self):
nodes = []
for id in range(5):
@ -1739,6 +1786,35 @@ class TestPatch(test_api_base.BaseApiTest):
self.assertEqual('application/json', response.content_type)
self.assertEqual(http_client.BAD_REQUEST, response.status_code)
def test_update_interface_fields(self):
node = obj_utils.create_test_node(self.context,
uuid=uuidutils.generate_uuid())
self.mock_update_node.return_value = node
headers = {api_base.Version.string: str(api_v1.MAX_VER)}
for field in api_utils.V31_FIELDS:
response = self.patch_json('/nodes/%s' % node.uuid,
[{'path': '/%s' % field,
'value': 'fake',
'op': 'add'}],
headers=headers)
self.assertEqual('application/json', response.content_type)
self.assertEqual(http_client.OK, response.status_code)
def test_update_interface_fields_bad_version(self):
node = obj_utils.create_test_node(self.context,
uuid=uuidutils.generate_uuid())
self.mock_update_node.return_value = node
headers = {api_base.Version.string: '1.30'}
for field in api_utils.V31_FIELDS:
response = self.patch_json('/nodes/%s' % node.uuid,
[{'path': '/%s' % field,
'value': 'fake',
'op': 'add'}],
headers=headers,
expect_errors=True)
self.assertEqual('application/json', response.content_type)
self.assertEqual(http_client.NOT_ACCEPTABLE, response.status_code)
def _create_node_locally(node):
driver_factory.check_and_update_node_interfaces(node)
@ -1812,6 +1888,24 @@ class TestPost(test_api_base.BaseApiTest):
network_interface='neutron')
self.assertEqual('neutron', result['network_interface'])
def test_create_node_specify_interfaces(self):
headers = {api_base.Version.string: '1.31'}
for field in api_utils.V31_FIELDS:
node = {
'uuid': uuidutils.generate_uuid(),
field: 'fake'
}
result = self._test_create_node(headers=headers, **node)
self.assertEqual('fake', result[field])
def test_create_node_specify_interfaces_bad_version(self):
headers = {api_base.Version.string: '1.30'}
for field in api_utils.V31_FIELDS:
ndict = test_api_utils.post_get_test_node(**{field: 'fake'})
response = self.post_json('/nodes', ndict, headers=headers,
expect_errors=True)
self.assertEqual(http_client.NOT_ACCEPTABLE, response.status_int)
def test_create_node_name_empty_invalid(self):
ndict = test_api_utils.post_get_test_node(name='')
response = self.post_json('/nodes', ndict,

View File

@ -0,0 +1,15 @@
---
features:
- |
Adds version 1.31 of the REST API, which exposes the following fields on
the node resource, to allow getting and setting interfaces for a dynamic
driver:
* boot_interface
* console_interface
* deploy_interface
* inspect_interface
* management_interface
* power_interface
* raid_interface
* vendor_interface