Add node.resource_class field
This adds the "resource_class" field to the node table, object, and API, as well as a database migration to go with it. Change-Id: I936f2e7b2f4d26e01354e826e5595ff021c3a55c Partial-Bug: #1604916
This commit is contained in:
parent
246e886dde
commit
f16c6570bf
@ -32,6 +32,10 @@ always requests the newest supported API version.
|
||||
API Versions History
|
||||
--------------------
|
||||
|
||||
**1.21**
|
||||
|
||||
Add node ``resource_class`` field.
|
||||
|
||||
**1.20**
|
||||
|
||||
Add node ``network_interface`` field.
|
||||
|
@ -140,6 +140,9 @@ def hide_fields_in_newer_versions(obj):
|
||||
if pecan.request.version.minor < versions.MINOR_20_NETWORK_INTERFACE:
|
||||
obj.network_interface = wsme.Unset
|
||||
|
||||
if not api_utils.allow_resource_class():
|
||||
obj.resource_class = wsme.Unset
|
||||
|
||||
|
||||
def update_state_in_older_versions(obj):
|
||||
"""Change provision state names for API backwards compatability.
|
||||
@ -699,6 +702,11 @@ class Node(base.APIBase):
|
||||
extra = {wtypes.text: types.jsontype}
|
||||
"""This node's meta data"""
|
||||
|
||||
resource_class = wsme.wsattr(wtypes.StringType(max_length=80))
|
||||
"""The resource class for the node, useful for classifying or grouping
|
||||
nodes. Used, for example, to classify nodes in Nova's placement
|
||||
engine."""
|
||||
|
||||
# NOTE: properties should use a class to enforce required properties
|
||||
# current list: arch, cpus, disk, ram, image
|
||||
properties = {wtypes.text: types.jsontype}
|
||||
@ -819,7 +827,7 @@ 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')
|
||||
network_interface='flat', resource_class='baremetal-gold')
|
||||
# NOTE(matty_dubs): The chassis_uuid getter() is based on the
|
||||
# _chassis_uuid variable:
|
||||
sample._chassis_uuid = 'edcad704-b2da-41d5-96d9-afd580ecfa12'
|
||||
@ -1006,6 +1014,7 @@ class NodesController(rest.RestController):
|
||||
def _get_nodes_collection(self, chassis_uuid, instance_uuid, associated,
|
||||
maintenance, provision_state, marker, limit,
|
||||
sort_key, sort_dir, driver=None,
|
||||
resource_class=None,
|
||||
resource_url=None, fields=None):
|
||||
if self.from_chassis and not chassis_uuid:
|
||||
raise exception.MissingParameterValue(
|
||||
@ -1038,6 +1047,8 @@ class NodesController(rest.RestController):
|
||||
filters['provision_state'] = provision_state
|
||||
if driver:
|
||||
filters['driver'] = driver
|
||||
if resource_class is not None:
|
||||
filters['resource_class'] = resource_class
|
||||
|
||||
nodes = objects.Node.list(pecan.request.context, limit, marker_obj,
|
||||
sort_key=sort_key, sort_dir=sort_dir,
|
||||
@ -1128,11 +1139,11 @@ class NodesController(rest.RestController):
|
||||
@METRICS.timer('NodesController.get_all')
|
||||
@expose.expose(NodeCollection, types.uuid, types.uuid, types.boolean,
|
||||
types.boolean, wtypes.text, types.uuid, int, wtypes.text,
|
||||
wtypes.text, wtypes.text, types.listtype)
|
||||
wtypes.text, wtypes.text, types.listtype, wtypes.text)
|
||||
def get_all(self, chassis_uuid=None, instance_uuid=None, associated=None,
|
||||
maintenance=None, provision_state=None, marker=None,
|
||||
limit=None, sort_key='id', sort_dir='asc', driver=None,
|
||||
fields=None):
|
||||
fields=None, resource_class=None):
|
||||
"""Retrieve a list of nodes.
|
||||
|
||||
:param chassis_uuid: Optional UUID of a chassis, to get only nodes for
|
||||
@ -1153,28 +1164,34 @@ class NodesController(rest.RestController):
|
||||
:param sort_dir: direction to sort. "asc" or "desc". Default: asc.
|
||||
:param driver: Optional string value to get only nodes using that
|
||||
driver.
|
||||
:param resource_class: Optional string value to get only nodes with
|
||||
that resource_class.
|
||||
:param fields: Optional, a list with a specified set of fields
|
||||
of the resource to be returned.
|
||||
"""
|
||||
api_utils.check_allow_specify_fields(fields)
|
||||
api_utils.check_allowed_fields(fields)
|
||||
api_utils.check_for_invalid_state_and_allow_filter(provision_state)
|
||||
api_utils.check_allow_specify_driver(driver)
|
||||
api_utils.check_allow_specify_network_interface_in_fields(fields)
|
||||
api_utils.check_allow_specify_resource_class(resource_class)
|
||||
if fields is None:
|
||||
fields = _DEFAULT_RETURN_FIELDS
|
||||
return self._get_nodes_collection(chassis_uuid, instance_uuid,
|
||||
associated, maintenance,
|
||||
provision_state, marker,
|
||||
limit, sort_key, sort_dir,
|
||||
driver, fields=fields)
|
||||
driver=driver,
|
||||
resource_class=resource_class,
|
||||
fields=fields)
|
||||
|
||||
@METRICS.timer('NodesController.detail')
|
||||
@expose.expose(NodeCollection, types.uuid, types.uuid, types.boolean,
|
||||
types.boolean, wtypes.text, types.uuid, int, wtypes.text,
|
||||
wtypes.text, wtypes.text)
|
||||
wtypes.text, wtypes.text, wtypes.text)
|
||||
def detail(self, chassis_uuid=None, instance_uuid=None, associated=None,
|
||||
maintenance=None, provision_state=None, marker=None,
|
||||
limit=None, sort_key='id', sort_dir='asc', driver=None):
|
||||
limit=None, sort_key='id', sort_dir='asc', driver=None,
|
||||
resource_class=None):
|
||||
"""Retrieve a list of nodes with detail.
|
||||
|
||||
:param chassis_uuid: Optional UUID of a chassis, to get only nodes for
|
||||
@ -1195,9 +1212,12 @@ class NodesController(rest.RestController):
|
||||
:param sort_dir: direction to sort. "asc" or "desc". Default: asc.
|
||||
:param driver: Optional string value to get only nodes using that
|
||||
driver.
|
||||
:param resource_class: Optional string value to get only nodes with
|
||||
that resource_class.
|
||||
"""
|
||||
api_utils.check_for_invalid_state_and_allow_filter(provision_state)
|
||||
api_utils.check_allow_specify_driver(driver)
|
||||
api_utils.check_allow_specify_resource_class(resource_class)
|
||||
# /detail should only work against collections
|
||||
parent = pecan.request.path.split('/')[:-1][-1]
|
||||
if parent != "nodes":
|
||||
@ -1208,7 +1228,9 @@ class NodesController(rest.RestController):
|
||||
associated, maintenance,
|
||||
provision_state, marker,
|
||||
limit, sort_key, sort_dir,
|
||||
driver, resource_url)
|
||||
driver=driver,
|
||||
resource_class=resource_class,
|
||||
resource_url=resource_url)
|
||||
|
||||
@METRICS.timer('NodesController.validate')
|
||||
@expose.expose(wtypes.text, types.uuid_or_name, types.uuid)
|
||||
@ -1247,7 +1269,7 @@ class NodesController(rest.RestController):
|
||||
raise exception.OperationNotPermitted()
|
||||
|
||||
api_utils.check_allow_specify_fields(fields)
|
||||
api_utils.check_allow_specify_network_interface_in_fields(fields)
|
||||
api_utils.check_allowed_fields(fields)
|
||||
|
||||
rpc_node = api_utils.get_rpc_node(node_ident)
|
||||
return Node.convert_with_links(rpc_node, fields=fields)
|
||||
@ -1262,6 +1284,10 @@ class NodesController(rest.RestController):
|
||||
if self.from_chassis:
|
||||
raise exception.OperationNotPermitted()
|
||||
|
||||
if (not api_utils.allow_resource_class() and
|
||||
node.resource_class is not wtypes.Unset):
|
||||
raise exception.NotAcceptable()
|
||||
|
||||
n_interface = node.network_interface
|
||||
if (not api_utils.allow_network_interface() and
|
||||
n_interface is not wtypes.Unset):
|
||||
@ -1322,6 +1348,10 @@ class NodesController(rest.RestController):
|
||||
if self.from_chassis:
|
||||
raise exception.OperationNotPermitted()
|
||||
|
||||
resource_class = api_utils.get_patch_values(patch, '/resource_class')
|
||||
if resource_class and not api_utils.allow_resource_class():
|
||||
raise exception.NotAcceptable()
|
||||
|
||||
n_interfaces = api_utils.get_patch_values(patch, '/network_interface')
|
||||
if n_interfaces and not api_utils.allow_network_interface():
|
||||
raise exception.NotAcceptable()
|
||||
|
@ -240,16 +240,17 @@ def check_allow_specify_fields(fields):
|
||||
raise exception.NotAcceptable()
|
||||
|
||||
|
||||
def check_allow_specify_network_interface_in_fields(fields):
|
||||
"""Check if fetching a network_interface attribute is allowed.
|
||||
def check_allowed_fields(fields):
|
||||
"""Check if fetching a particular field is allowed.
|
||||
|
||||
Version 1.20 of the API allows to fetching a network_interface
|
||||
attribute. This method check if the required version is being
|
||||
requested.
|
||||
This method checks if the required version is being requested for fields
|
||||
that are only allowed to be fetched in a particular API version.
|
||||
"""
|
||||
if (fields is not None
|
||||
and 'network_interface' in fields
|
||||
and not allow_network_interface()):
|
||||
if fields is None:
|
||||
return
|
||||
if 'network_interface' in fields and not allow_network_interface():
|
||||
raise exception.NotAcceptable()
|
||||
if 'resource_class' in fields and not allow_resource_class():
|
||||
raise exception.NotAcceptable()
|
||||
|
||||
|
||||
@ -303,6 +304,20 @@ def check_allow_specify_driver(driver):
|
||||
'opr': versions.MINOR_16_DRIVER_FILTER})
|
||||
|
||||
|
||||
def check_allow_specify_resource_class(resource_class):
|
||||
"""Check if filtering nodes by resource_class is allowed.
|
||||
|
||||
Version 1.21 of the API allows filtering nodes by resource_class.
|
||||
"""
|
||||
if (resource_class is not None and pecan.request.version.minor <
|
||||
versions.MINOR_21_RESOURCE_CLASS):
|
||||
raise exception.NotAcceptable(_(
|
||||
"Request not acceptable. The minimal required API version "
|
||||
"should be %(base)s.%(opr)s") %
|
||||
{'base': versions.BASE_VERSION,
|
||||
'opr': versions.MINOR_21_RESOURCE_CLASS})
|
||||
|
||||
|
||||
def initial_node_provision_state():
|
||||
"""Return node state to use by default when creating new nodes.
|
||||
|
||||
@ -359,6 +374,15 @@ def allow_network_interface():
|
||||
versions.MINOR_20_NETWORK_INTERFACE)
|
||||
|
||||
|
||||
def allow_resource_class():
|
||||
"""Check if we should support resource_class node field.
|
||||
|
||||
Version 1.21 of the API added support for resource_class.
|
||||
"""
|
||||
return (pecan.request.version.minor >=
|
||||
versions.MINOR_21_RESOURCE_CLASS)
|
||||
|
||||
|
||||
def get_controller_reserved_names(cls):
|
||||
"""Get reserved names for a given controller.
|
||||
|
||||
|
@ -50,6 +50,7 @@ BASE_VERSION = 1
|
||||
# v1.18: Add port.internal_info.
|
||||
# v1.19: Add port.local_link_connection and port.pxe_enabled.
|
||||
# v1.20: Add node.network_interface
|
||||
# v1.21: Add node.resource_class
|
||||
|
||||
MINOR_0_JUNO = 0
|
||||
MINOR_1_INITIAL_VERSION = 1
|
||||
@ -72,11 +73,12 @@ MINOR_17_ADOPT_VERB = 17
|
||||
MINOR_18_PORT_INTERNAL_INFO = 18
|
||||
MINOR_19_PORT_ADVANCED_NET_FIELDS = 19
|
||||
MINOR_20_NETWORK_INTERFACE = 20
|
||||
MINOR_21_RESOURCE_CLASS = 21
|
||||
|
||||
# When adding another version, update MINOR_MAX_VERSION and also update
|
||||
# doc/source/webapi/v1.rst with a detailed explanation of what the version has
|
||||
# changed.
|
||||
MINOR_MAX_VERSION = MINOR_20_NETWORK_INTERFACE
|
||||
MINOR_MAX_VERSION = MINOR_21_RESOURCE_CLASS
|
||||
|
||||
# String representations of the minor and maximum versions
|
||||
MIN_VERSION_STRING = '{}.{}'.format(BASE_VERSION, MINOR_1_INITIAL_VERSION)
|
||||
|
@ -0,0 +1,33 @@
|
||||
# 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.
|
||||
|
||||
"""add resource_class to node
|
||||
|
||||
Revision ID: dd34e1f1303b
|
||||
Revises: 10b163d4481e
|
||||
Create Date: 2016-07-20 21:48:12.475320
|
||||
|
||||
"""
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = 'dd34e1f1303b'
|
||||
down_revision = '10b163d4481e'
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
def upgrade():
|
||||
op.add_column('nodes', sa.Column('resource_class', sa.String(80),
|
||||
nullable=True))
|
@ -214,6 +214,8 @@ class Connection(api.Connection):
|
||||
query = query.filter_by(maintenance=filters['maintenance'])
|
||||
if 'driver' in filters:
|
||||
query = query.filter_by(driver=filters['driver'])
|
||||
if 'resource_class' in filters:
|
||||
query = query.filter_by(resource_class=filters['resource_class'])
|
||||
if 'provision_state' in filters:
|
||||
query = query.filter_by(provision_state=filters['provision_state'])
|
||||
if 'provisioned_before' in filters:
|
||||
|
@ -118,6 +118,7 @@ class Node(Base):
|
||||
driver_info = Column(db_types.JsonEncodedDict)
|
||||
driver_internal_info = Column(db_types.JsonEncodedDict)
|
||||
clean_step = Column(db_types.JsonEncodedDict)
|
||||
resource_class = Column(String(80), nullable=True)
|
||||
|
||||
raid_config = Column(db_types.JsonEncodedDict)
|
||||
target_raid_config = Column(db_types.JsonEncodedDict)
|
||||
|
@ -47,7 +47,8 @@ class Node(base.IronicObject, object_base.VersionedObjectDictCompat):
|
||||
# and save() validate the input of property values.
|
||||
# Version 1.15: Add get_by_port_addresses
|
||||
# Version 1.16: Add network_interface field
|
||||
VERSION = '1.16'
|
||||
# Version 1.17: Add resource_class field
|
||||
VERSION = '1.17'
|
||||
|
||||
dbapi = db_api.get_instance()
|
||||
|
||||
@ -99,6 +100,9 @@ class Node(base.IronicObject, object_base.VersionedObjectDictCompat):
|
||||
# that started but failed to finish.
|
||||
'last_error': object_fields.StringField(nullable=True),
|
||||
|
||||
# Used by nova to relate the node to a flavor
|
||||
'resource_class': object_fields.StringField(nullable=True),
|
||||
|
||||
'inspection_finished_at': object_fields.DateTimeField(nullable=True),
|
||||
'inspection_started_at': object_fields.DateTimeField(nullable=True),
|
||||
|
||||
|
@ -94,11 +94,15 @@ def node_post_data(**kw):
|
||||
node.pop('conductor_affinity')
|
||||
node.pop('chassis_id')
|
||||
node.pop('tags')
|
||||
# NOTE(vdrok): network_interface was introduced in API version 1.20, return
|
||||
# it only if it was explicitly requested, so that tests using older API
|
||||
# versions don't fail
|
||||
|
||||
# NOTE(jroll): pop out fields that were introduced in later API versions,
|
||||
# unless explicitly requested. Otherwise, these will cause tests using
|
||||
# older API versions to fail.
|
||||
if 'network_interface' not in kw:
|
||||
node.pop('network_interface')
|
||||
if 'resource_class' not in kw:
|
||||
node.pop('resource_class')
|
||||
|
||||
internal = node_controller.NodePatchType.internal_attrs()
|
||||
return remove_internal(node, internal)
|
||||
|
||||
|
@ -111,6 +111,7 @@ class TestListNodes(test_api_base.BaseApiTest):
|
||||
self.assertNotIn('raid_config', data['nodes'][0])
|
||||
self.assertNotIn('target_raid_config', data['nodes'][0])
|
||||
self.assertNotIn('network_interface', data['nodes'][0])
|
||||
self.assertNotIn('resource_class', data['nodes'][0])
|
||||
# never expose the chassis_id
|
||||
self.assertNotIn('chassis_id', data['nodes'][0])
|
||||
|
||||
@ -137,6 +138,7 @@ class TestListNodes(test_api_base.BaseApiTest):
|
||||
self.assertIn('clean_step', data)
|
||||
self.assertIn('states', data)
|
||||
self.assertIn('network_interface', data)
|
||||
self.assertIn('resource_class', data)
|
||||
# never expose the chassis_id
|
||||
self.assertNotIn('chassis_id', data)
|
||||
|
||||
@ -336,6 +338,17 @@ class TestListNodes(test_api_base.BaseApiTest):
|
||||
self.assertEqual(node.network_interface,
|
||||
new_data['nodes'][0]["network_interface"])
|
||||
|
||||
def test_hide_fields_in_newer_versions_resource_class(self):
|
||||
node = obj_utils.create_test_node(self.context,
|
||||
resource_class='foo')
|
||||
data = self.get_json(
|
||||
'/nodes/detail', headers={api_base.Version.string: '1.20'})
|
||||
self.assertNotIn('resource_class', data['nodes'][0])
|
||||
new_data = self.get_json(
|
||||
'/nodes/detail', headers={api_base.Version.string: '1.21'})
|
||||
self.assertEqual(node.resource_class,
|
||||
new_data['nodes'][0]["resource_class"])
|
||||
|
||||
def test_many(self):
|
||||
nodes = []
|
||||
for id in range(5):
|
||||
@ -756,6 +769,75 @@ class TestListNodes(test_api_base.BaseApiTest):
|
||||
self.assertEqual(http_client.NOT_ACCEPTABLE, response.status_code)
|
||||
self.assertTrue(response.json['error_message'])
|
||||
|
||||
def _test_get_nodes_by_resource_class(self, detail=False):
|
||||
if detail:
|
||||
base_url = '/nodes/detail?resource_class=%s'
|
||||
else:
|
||||
base_url = '/nodes?resource_class=%s'
|
||||
|
||||
node = obj_utils.create_test_node(self.context,
|
||||
uuid=uuidutils.generate_uuid(),
|
||||
driver='fake',
|
||||
resource_class='foo')
|
||||
node1 = obj_utils.create_test_node(self.context,
|
||||
uuid=uuidutils.generate_uuid(),
|
||||
driver='fake',
|
||||
resource_class='bar')
|
||||
|
||||
data = self.get_json(base_url % 'foo',
|
||||
headers={api_base.Version.string: "1.21"})
|
||||
uuids = [n['uuid'] for n in data['nodes']]
|
||||
self.assertIn(node.uuid, uuids)
|
||||
self.assertNotIn(node1.uuid, uuids)
|
||||
data = self.get_json(base_url % 'bar',
|
||||
headers={api_base.Version.string: "1.21"})
|
||||
uuids = [n['uuid'] for n in data['nodes']]
|
||||
self.assertIn(node1.uuid, uuids)
|
||||
self.assertNotIn(node.uuid, uuids)
|
||||
|
||||
def test_get_nodes_by_resource_class(self):
|
||||
self._test_get_nodes_by_resource_class(detail=False)
|
||||
|
||||
def test_get_nodes_by_resource_class_detail(self):
|
||||
self._test_get_nodes_by_resource_class(detail=True)
|
||||
|
||||
def _test_get_nodes_by_invalid_resource_class(self, detail=False):
|
||||
if detail:
|
||||
base_url = '/nodes/detail?resource_class=%s'
|
||||
else:
|
||||
base_url = '/nodes?resource_class=%s'
|
||||
|
||||
data = self.get_json(base_url % 'test',
|
||||
headers={api_base.Version.string: "1.21"})
|
||||
self.assertEqual(0, len(data['nodes']))
|
||||
|
||||
def test_get_nodes_by_invalid_resource_class(self):
|
||||
self._test_get_nodes_by_invalid_resource_class(detail=False)
|
||||
|
||||
def test_get_nodes_by_invalid_resource_class_detail(self):
|
||||
self._test_get_nodes_by_invalid_resource_class(detail=True)
|
||||
|
||||
def _test_get_nodes_by_resource_class_invalid_api_version(self,
|
||||
detail=False):
|
||||
if detail:
|
||||
base_url = '/nodes/detail?resource_class=%s'
|
||||
else:
|
||||
base_url = '/nodes?resource_class=%s'
|
||||
|
||||
response = self.get_json(
|
||||
base_url % 'fake',
|
||||
headers={api_base.Version.string: str(api_v1.MIN_VER)},
|
||||
expect_errors=True)
|
||||
self.assertEqual(http_client.NOT_ACCEPTABLE, response.status_code)
|
||||
self.assertTrue(response.json['error_message'])
|
||||
|
||||
def test_get_nodes_by_resource_class_invalid_api_version(self):
|
||||
self._test_get_nodes_by_resource_class_invalid_api_version(
|
||||
detail=False)
|
||||
|
||||
def test_get_nodes_by_resource_class_invalid_api_version_detail(self):
|
||||
self._test_get_nodes_by_resource_class_invalid_api_version(detail=True)
|
||||
|
||||
def test_get_console_information(self):
|
||||
node = obj_utils.create_test_node(self.context)
|
||||
expected_console_info = {'test': 'test-data'}
|
||||
@ -1452,6 +1534,64 @@ 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_resource_class(self):
|
||||
node = obj_utils.create_test_node(self.context,
|
||||
uuid=uuidutils.generate_uuid())
|
||||
self.mock_update_node.return_value = node
|
||||
resource_class = 'foo'
|
||||
headers = {api_base.Version.string: '1.21'}
|
||||
response = self.patch_json('/nodes/%s' % node.uuid,
|
||||
[{'path': '/resource_class',
|
||||
'value': resource_class,
|
||||
'op': 'add'}],
|
||||
headers=headers)
|
||||
self.assertEqual('application/json', response.content_type)
|
||||
self.assertEqual(http_client.OK, response.status_code)
|
||||
|
||||
def test_update_resource_class_old_api(self):
|
||||
node = obj_utils.create_test_node(self.context,
|
||||
uuid=uuidutils.generate_uuid())
|
||||
self.mock_update_node.return_value = node
|
||||
resource_class = 'foo'
|
||||
headers = {api_base.Version.string: '1.20'}
|
||||
response = self.patch_json('/nodes/%s' % node.uuid,
|
||||
[{'path': '/resource_class',
|
||||
'value': resource_class,
|
||||
'op': 'add'}],
|
||||
headers=headers,
|
||||
expect_errors=True)
|
||||
self.assertEqual('application/json', response.content_type)
|
||||
self.assertEqual(http_client.NOT_ACCEPTABLE, response.status_code)
|
||||
|
||||
def test_update_resource_class_max_length(self):
|
||||
node = obj_utils.create_test_node(self.context,
|
||||
uuid=uuidutils.generate_uuid())
|
||||
self.mock_update_node.return_value = node
|
||||
resource_class = 'f' * 80
|
||||
headers = {api_base.Version.string: '1.21'}
|
||||
response = self.patch_json('/nodes/%s' % node.uuid,
|
||||
[{'path': '/resource_class',
|
||||
'value': resource_class,
|
||||
'op': 'add'}],
|
||||
headers=headers)
|
||||
self.assertEqual('application/json', response.content_type)
|
||||
self.assertEqual(http_client.OK, response.status_code)
|
||||
|
||||
def test_update_resource_class_too_long(self):
|
||||
node = obj_utils.create_test_node(self.context,
|
||||
uuid=uuidutils.generate_uuid())
|
||||
self.mock_update_node.return_value = node
|
||||
resource_class = 'f' * 81
|
||||
headers = {api_base.Version.string: '1.21'}
|
||||
response = self.patch_json('/nodes/%s' % node.uuid,
|
||||
[{'path': '/resource_class',
|
||||
'value': resource_class,
|
||||
'op': 'add'}],
|
||||
headers=headers,
|
||||
expect_errors=True)
|
||||
self.assertEqual('application/json', response.content_type)
|
||||
self.assertEqual(http_client.BAD_REQUEST, response.status_code)
|
||||
|
||||
|
||||
class TestPost(test_api_base.BaseApiTest):
|
||||
|
||||
@ -1793,6 +1933,25 @@ 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_resource_class(self):
|
||||
ndict = test_api_utils.post_get_test_node(
|
||||
resource_class='foo')
|
||||
response = self.post_json('/nodes', ndict,
|
||||
headers={api_base.Version.string:
|
||||
str(api_v1.MAX_VER)})
|
||||
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_VER)})
|
||||
self.assertEqual('foo', result['resource_class'])
|
||||
|
||||
def test_create_node_resource_class_old_api_version(self):
|
||||
ndict = test_api_utils.post_get_test_node(
|
||||
resource_class='foo')
|
||||
response = self.post_json('/nodes', ndict, 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):
|
||||
|
||||
|
@ -131,21 +131,33 @@ class TestApiUtils(base.TestCase):
|
||||
utils.check_allow_specify_fields, ['foo'])
|
||||
|
||||
@mock.patch.object(pecan, 'request', spec_set=['version'])
|
||||
def test_check_allow_specify_network_interface(self, mock_request):
|
||||
def test_check_allowed_fields_network_interface(self, mock_request):
|
||||
mock_request.version.minor = 20
|
||||
self.assertIsNone(
|
||||
utils.check_allow_specify_network_interface_in_fields(
|
||||
['network_interface']))
|
||||
utils.check_allowed_fields(['network_interface']))
|
||||
|
||||
@mock.patch.object(pecan, 'request', spec_set=['version'])
|
||||
def test_check_allow_specify_network_interface_in_fields_fail(
|
||||
self, mock_request):
|
||||
def test_check_allowed_fields_network_interface_fail(self, mock_request):
|
||||
mock_request.version.minor = 19
|
||||
self.assertRaises(
|
||||
exception.NotAcceptable,
|
||||
utils.check_allow_specify_network_interface_in_fields,
|
||||
utils.check_allowed_fields,
|
||||
['network_interface'])
|
||||
|
||||
@mock.patch.object(pecan, 'request', spec_set=['version'])
|
||||
def test_check_allowed_fields_resource_class(self, mock_request):
|
||||
mock_request.version.minor = 21
|
||||
self.assertIsNone(
|
||||
utils.check_allowed_fields(['resource_class']))
|
||||
|
||||
@mock.patch.object(pecan, 'request', spec_set=['version'])
|
||||
def test_check_allowed_fields_resource_class_fail(self, mock_request):
|
||||
mock_request.version.minor = 20
|
||||
self.assertRaises(
|
||||
exception.NotAcceptable,
|
||||
utils.check_allowed_fields,
|
||||
['resource_class'])
|
||||
|
||||
@mock.patch.object(pecan, 'request', spec_set=['version'])
|
||||
def test_check_allow_specify_driver(self, mock_request):
|
||||
mock_request.version.minor = 16
|
||||
@ -157,6 +169,17 @@ class TestApiUtils(base.TestCase):
|
||||
self.assertRaises(exception.NotAcceptable,
|
||||
utils.check_allow_specify_driver, ['fake'])
|
||||
|
||||
@mock.patch.object(pecan, 'request', spec_set=['version'])
|
||||
def test_check_allow_specify_resource_class(self, mock_request):
|
||||
mock_request.version.minor = 21
|
||||
self.assertIsNone(utils.check_allow_specify_resource_class(['foo']))
|
||||
|
||||
@mock.patch.object(pecan, 'request', spec_set=['version'])
|
||||
def test_check_allow_specify_resource_class_fail(self, mock_request):
|
||||
mock_request.version.minor = 20
|
||||
self.assertRaises(exception.NotAcceptable,
|
||||
utils.check_allow_specify_resource_class, ['foo'])
|
||||
|
||||
@mock.patch.object(pecan, 'request', spec_set=['version'])
|
||||
def test_check_allow_manage_verbs(self, mock_request):
|
||||
mock_request.version.minor = 4
|
||||
@ -255,6 +278,13 @@ class TestApiUtils(base.TestCase):
|
||||
mock_request.version.minor = 19
|
||||
self.assertFalse(utils.allow_network_interface())
|
||||
|
||||
@mock.patch.object(pecan, 'request', spec_set=['version'])
|
||||
def test_allow_resource_class(self, mock_request):
|
||||
mock_request.version.minor = 21
|
||||
self.assertTrue(utils.allow_resource_class())
|
||||
mock_request.version.minor = 20
|
||||
self.assertFalse(utils.allow_resource_class())
|
||||
|
||||
|
||||
class TestNodeIdent(base.TestCase):
|
||||
|
||||
|
@ -433,6 +433,13 @@ class MigrationCheckersMixin(object):
|
||||
self.assertIsInstance(portgroups.c.internal_info.type,
|
||||
sqlalchemy.types.TEXT)
|
||||
|
||||
def _check_dd34e1f1303b(self, engine, data):
|
||||
nodes = db_utils.get_table(engine, 'nodes')
|
||||
col_names = [column.name for column in nodes.c]
|
||||
self.assertIn('resource_class', col_names)
|
||||
self.assertIsInstance(nodes.c.resource_class.type,
|
||||
sqlalchemy.types.String)
|
||||
|
||||
def test_upgrade_and_version(self):
|
||||
with patch_with_engine(self.engine):
|
||||
self.migration_api.upgrade('head')
|
||||
|
@ -123,7 +123,8 @@ class DbNodeTestCase(base.DbTestCase):
|
||||
node2 = utils.create_test_node(
|
||||
driver='driver-two',
|
||||
uuid=uuidutils.generate_uuid(),
|
||||
maintenance=True)
|
||||
maintenance=True,
|
||||
resource_class='foo')
|
||||
node3 = utils.create_test_node(
|
||||
driver='driver-one',
|
||||
uuid=uuidutils.generate_uuid(),
|
||||
@ -157,6 +158,9 @@ class DbNodeTestCase(base.DbTestCase):
|
||||
self.assertEqual(sorted([node1.id, node3.id]),
|
||||
sorted([r.id for r in res]))
|
||||
|
||||
res = self.dbapi.get_node_list(filters={'resource_class': 'foo'})
|
||||
self.assertEqual([node2.id], [r.id for r in res])
|
||||
|
||||
res = self.dbapi.get_node_list(
|
||||
filters={'reserved_by_any_of': ['fake-host',
|
||||
'another-fake-host']})
|
||||
|
@ -226,6 +226,7 @@ def get_test_node(**kw):
|
||||
'raid_config': kw.get('raid_config'),
|
||||
'target_raid_config': kw.get('target_raid_config'),
|
||||
'tags': kw.get('tags', []),
|
||||
'resource_class': kw.get('resource_class'),
|
||||
'network_interface': kw.get('network_interface'),
|
||||
}
|
||||
|
||||
|
@ -404,7 +404,7 @@ class TestObject(_LocalTest, _TestObject):
|
||||
# version bump. It is md5 hash of object fields and remotable methods.
|
||||
# The fingerprint values should only be changed if there is a version bump.
|
||||
expected_object_fingerprints = {
|
||||
'Node': '1.16-2a6646627cb937f083f428f5d54e6458',
|
||||
'Node': '1.17-ed09e704576dc1b5a74abcbb727bf722',
|
||||
'MyObj': '1.5-4f5efe8f0fcaf182bbe1c7fe3ba858db',
|
||||
'Chassis': '1.3-d656e039fd8ae9f34efc232ab3980905',
|
||||
'Port': '1.6-609504503d68982a10f495659990084b',
|
||||
|
@ -0,0 +1,13 @@
|
||||
---
|
||||
features:
|
||||
- Adds a `resource_class` field to the node resource,
|
||||
which will be used by Nova to define which nodes may
|
||||
quantitatively match a Nova flavor. Operators should
|
||||
populate this accordingly before deploying the Ocata
|
||||
version of Nova.
|
||||
upgrade:
|
||||
- Adds a `resource_class` field to the node resource,
|
||||
which will be used by Nova to define which nodes may
|
||||
quantitatively match a Nova flavor. Operators should
|
||||
populate this accordingly before deploying the Ocata
|
||||
version of Nova.
|
Loading…
Reference in New Issue
Block a user