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:
Jim Rollenhagen 2016-07-20 14:20:45 -07:00
parent 246e886dde
commit f16c6570bf
16 changed files with 348 additions and 30 deletions

View File

@ -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.

View File

@ -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()

View File

@ -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.

View File

@ -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)

View File

@ -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))

View File

@ -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:

View File

@ -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)

View File

@ -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),

View File

@ -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)

View File

@ -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):

View File

@ -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):

View File

@ -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')

View File

@ -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']})

View File

@ -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'),
}

View File

@ -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',

View File

@ -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.