Merge "Add automated_clean field to the API"

This commit is contained in:
Zuul 2018-10-09 17:41:07 +00:00 committed by Gerrit Code Review
commit 1cdc13c61e
7 changed files with 127 additions and 3 deletions

View File

@ -2,6 +2,11 @@
REST API Version History
========================
1.47 (Stein, master)
--------------------
Added ``automated_clean`` field to the node object, enabling cleaning per node.
1.46 (Rocky, 11.1.0)
--------------------
Added ``conductor_group`` field to the node and the node response,

View File

@ -171,6 +171,9 @@ def _hide_fields_in_newer_versions_part_two(obj):
if not api_utils.allow_conductor_group():
obj.conductor_group = wsme.Unset
if not api_utils.allow_automated_clean():
obj.automated_clean = wsme.Unset
def hide_fields_in_newer_versions(obj):
"""This method hides fields that were added in newer API versions.
@ -1089,6 +1092,9 @@ class Node(base.APIBase):
conductor_group = wsme.wsattr(wtypes.text)
"""The conductor group to manage this node"""
automated_clean = types.boolean
"""Indicates whether the node will perform automated clean or not."""
# NOTE(deva): "conductor_affinity" shouldn't be presented on the
# API because it's an internal value. Don't add it here.
@ -1249,7 +1255,8 @@ class Node(base.APIBase):
management_interface=None, power_interface=None,
raid_interface=None, vendor_interface=None,
storage_interface=None, traits=[], rescue_interface=None,
bios_interface=None, conductor_group="")
bios_interface=None, conductor_group="",
automated_clean=None)
# NOTE(matty_dubs): The chassis_uuid getter() is based on the
# _chassis_uuid variable:
sample._chassis_uuid = 'edcad704-b2da-41d5-96d9-afd580ecfa12'
@ -1934,6 +1941,10 @@ class NodesController(rest.RestController):
and node.conductor_group != ""):
raise exception.NotAcceptable()
if (not api_utils.allow_automated_clean()
and node.automated_clean is not wtypes.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
@ -2018,6 +2029,10 @@ class NodesController(rest.RestController):
if conductor_group and not api_utils.allow_conductor_group():
raise exception.NotAcceptable()
automated_clean = api_utils.get_patch_values(patch, '/automated_clean')
if automated_clean and not api_utils.allow_automated_clean():
raise exception.NotAcceptable()
@METRICS.timer('NodesController.patch')
@wsme.validate(types.uuid, types.boolean, [NodePatchType])
@expose.expose(Node, types.uuid_or_name, types.boolean,

View File

@ -383,6 +383,8 @@ def check_allowed_fields(fields):
raise exception.NotAcceptable()
if 'conductor_group' in fields and not allow_conductor_group():
raise exception.NotAcceptable()
if 'automated_clean' in fields and not allow_automated_clean():
raise exception.NotAcceptable()
def check_allowed_portgroup_fields(fields):
@ -896,6 +898,15 @@ def allow_conductor_group():
versions.MINOR_46_NODE_CONDUCTOR_GROUP)
def allow_automated_clean():
"""Check if passing automated_clean for a node is allowed.
Version 1.47 exposes this field.
"""
return (pecan.request.version.minor >=
versions.MINOR_47_NODE_AUTOMATED_CLEAN)
def get_request_return_fields(fields, detail, default_fields):
"""Calculate fields to return from an API request

View File

@ -84,6 +84,7 @@ BASE_VERSION = 1
# v1.44: Add node deploy_step field
# v1.45: reset_interfaces parameter to node's PATCH
# v1.46: Add conductor_group to the node object.
# v1.47: Add automated_clean to the node object.
MINOR_0_JUNO = 0
MINOR_1_INITIAL_VERSION = 1
@ -132,6 +133,7 @@ MINOR_43_ENABLE_DETAIL_QUERY = 43
MINOR_44_NODE_DEPLOY_STEP = 44
MINOR_45_RESET_INTERFACES = 45
MINOR_46_NODE_CONDUCTOR_GROUP = 46
MINOR_47_NODE_AUTOMATED_CLEAN = 47
# When adding another version, update:
# - MINOR_MAX_VERSION
@ -139,7 +141,7 @@ MINOR_46_NODE_CONDUCTOR_GROUP = 46
# explanation of what changed in the new version
# - common/release_mappings.py, RELEASE_MAPPING['master']['api']
MINOR_MAX_VERSION = MINOR_46_NODE_CONDUCTOR_GROUP
MINOR_MAX_VERSION = MINOR_47_NODE_AUTOMATED_CLEAN
# String representations of the minor and maximum versions
_MIN_VERSION_STRING = '{}.{}'.format(BASE_VERSION, MINOR_1_INITIAL_VERSION)

View File

@ -131,7 +131,7 @@ RELEASE_MAPPING = {
}
},
'master': {
'api': '1.46',
'api': '1.47',
'rpc': '1.47',
'objects': {
'Node': ['1.28'],

View File

@ -124,6 +124,7 @@ class TestListNodes(test_api_base.BaseApiTest):
self.assertNotIn('bios_interface', data['nodes'][0])
self.assertNotIn('deploy_step', data['nodes'][0])
self.assertNotIn('conductor_group', data['nodes'][0])
self.assertNotIn('automated_clean', data['nodes'][0])
def test_get_one(self):
node = obj_utils.create_test_node(self.context,
@ -162,6 +163,7 @@ class TestListNodes(test_api_base.BaseApiTest):
self.assertIn('bios_interface', data)
self.assertIn('deploy_step', data)
self.assertIn('conductor_group', data)
self.assertIn('automated_clean', data)
def test_get_one_with_json(self):
# Test backward compatibility with guess_content_type_from_ext
@ -262,6 +264,28 @@ class TestListNodes(test_api_base.BaseApiTest):
self._test_node_field_hidden_in_lower_version('conductor_group',
'1.45', '1.46')
def test_node_automated_clean_hidden_in_lower_version(self):
self._test_node_field_hidden_in_lower_version('automated_clean',
'1.46', '1.47')
def test_node_automated_clean_null_field(self):
node = obj_utils.create_test_node(self.context, automated_clean=None)
data = self.get_json('/nodes/%s' % node.uuid,
headers={api_base.Version.string: '1.47'})
self.assertIsNone(data['automated_clean'])
def test_node_automated_clean_true_field(self):
node = obj_utils.create_test_node(self.context, automated_clean=True)
data = self.get_json('/nodes/%s' % node.uuid,
headers={api_base.Version.string: '1.47'})
self.assertEqual(data['automated_clean'], True)
def test_node_automated_clean_false_field(self):
node = obj_utils.create_test_node(self.context, automated_clean=False)
data = self.get_json('/nodes/%s' % node.uuid,
headers={api_base.Version.string: '1.47'})
self.assertEqual(data['automated_clean'], False)
def test_get_one_custom_fields(self):
node = obj_utils.create_test_node(self.context,
chassis_id=self.chassis.id)
@ -418,6 +442,14 @@ class TestListNodes(test_api_base.BaseApiTest):
headers={api_base.Version.string: '1.46'})
self.assertIn('conductor_group', response)
def test_get_automated_clean_fields(self):
node = obj_utils.create_test_node(self.context,
automated_clean=True)
fields = 'automated_clean'
response = self.get_json('/nodes/%s?fields=%s' % (node.uuid, fields),
headers={api_base.Version.string: '1.47'})
self.assertIn('automated_clean', response)
def test_detail(self):
node = obj_utils.create_test_node(self.context,
chassis_id=self.chassis.id)
@ -448,6 +480,7 @@ class TestListNodes(test_api_base.BaseApiTest):
self.assertIn('storage_interface', data['nodes'][0])
self.assertIn('traits', data['nodes'][0])
self.assertIn('conductor_group', data['nodes'][0])
self.assertIn('automated_clean', data['nodes'][0])
# never expose the chassis_id
self.assertNotIn('chassis_id', data['nodes'][0])
@ -477,6 +510,7 @@ class TestListNodes(test_api_base.BaseApiTest):
self.assertIn('network_interface', data['nodes'][0])
self.assertIn('resource_class', data['nodes'][0])
self.assertIn('conductor_group', data['nodes'][0])
self.assertIn('automated_clean', data['nodes'][0])
for field in api_utils.V31_FIELDS:
self.assertIn(field, data['nodes'][0])
# never expose the chassis_id
@ -2558,6 +2592,33 @@ class TestPatch(test_api_base.BaseApiTest):
self.assertEqual('application/json', response.content_type)
self.assertEqual(http_client.NOT_ACCEPTABLE, response.status_code)
def test_update_automated_clean(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.47'}
response = self.patch_json('/nodes/%s' % node.uuid,
[{'path': '/automated_clean',
'value': True,
'op': 'replace'}],
headers=headers)
self.assertEqual('application/json', response.content_type)
self.assertEqual(http_client.OK, response.status_code)
def test_update_automated_clean_old_api(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.46'}
response = self.patch_json('/nodes/%s' % node.uuid,
[{'path': '/automated_clean',
'value': True,
'op': 'replace'}],
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)
@ -3137,6 +3198,26 @@ 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_automated_clean(self):
ndict = test_api_utils.post_get_test_node(
automated_clean=True)
response = self.post_json('/nodes', ndict,
headers={api_base.Version.string:
str(api_v1.max_version())})
self.assertEqual(http_client.CREATED, response.status_int)
result = self.get_json('/nodes/%s' % ndict['uuid'],
headers={api_base.Version.string:
str(api_v1.max_version())})
self.assertEqual(True, result['automated_clean'])
def test_create_node_automated_clean_old_api_version(self):
headers = {api_base.Version.string: '1.32'}
ndict = test_api_utils.post_get_test_node(automated_clean=True)
response = self.post_json('/nodes', ndict, headers=headers,
expect_errors=True)
self.assertEqual('application/json', response.content_type)
self.assertEqual(http_client.NOT_ACCEPTABLE, response.status_int)
class TestDelete(test_api_base.BaseApiTest):

View File

@ -0,0 +1,10 @@
---
features:
- |
Allows enabling automated cleaning per node if it is disabled globally.
A new ``automated_clean`` field has been created on the node object,
allowing to control the individual automated cleaning of nodes.
When automated cleaning is disabled at global level, but enabled at node
level, the automated cleaning will be performed only on those nodes.
The new field is accessible starting with the API version 1.47.