Merge "Add automated_clean field to the API"
This commit is contained in:
commit
1cdc13c61e
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
|
||||
|
@ -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)
|
||||
|
@ -131,7 +131,7 @@ RELEASE_MAPPING = {
|
||||
}
|
||||
},
|
||||
'master': {
|
||||
'api': '1.46',
|
||||
'api': '1.47',
|
||||
'rpc': '1.47',
|
||||
'objects': {
|
||||
'Node': ['1.28'],
|
||||
|
@ -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):
|
||||
|
||||
|
@ -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.
|
Loading…
x
Reference in New Issue
Block a user