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
|
API Versions History
|
||||||
--------------------
|
--------------------
|
||||||
|
|
||||||
|
**1.21**
|
||||||
|
|
||||||
|
Add node ``resource_class`` field.
|
||||||
|
|
||||||
**1.20**
|
**1.20**
|
||||||
|
|
||||||
Add node ``network_interface`` field.
|
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:
|
if pecan.request.version.minor < versions.MINOR_20_NETWORK_INTERFACE:
|
||||||
obj.network_interface = wsme.Unset
|
obj.network_interface = wsme.Unset
|
||||||
|
|
||||||
|
if not api_utils.allow_resource_class():
|
||||||
|
obj.resource_class = wsme.Unset
|
||||||
|
|
||||||
|
|
||||||
def update_state_in_older_versions(obj):
|
def update_state_in_older_versions(obj):
|
||||||
"""Change provision state names for API backwards compatability.
|
"""Change provision state names for API backwards compatability.
|
||||||
@ -699,6 +702,11 @@ class Node(base.APIBase):
|
|||||||
extra = {wtypes.text: types.jsontype}
|
extra = {wtypes.text: types.jsontype}
|
||||||
"""This node's meta data"""
|
"""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
|
# NOTE: properties should use a class to enforce required properties
|
||||||
# current list: arch, cpus, disk, ram, image
|
# current list: arch, cpus, disk, ram, image
|
||||||
properties = {wtypes.text: types.jsontype}
|
properties = {wtypes.text: types.jsontype}
|
||||||
@ -819,7 +827,7 @@ class Node(base.APIBase):
|
|||||||
inspection_finished_at=None, inspection_started_at=time,
|
inspection_finished_at=None, inspection_started_at=time,
|
||||||
console_enabled=False, clean_step={},
|
console_enabled=False, clean_step={},
|
||||||
raid_config=None, target_raid_config=None,
|
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
|
# NOTE(matty_dubs): The chassis_uuid getter() is based on the
|
||||||
# _chassis_uuid variable:
|
# _chassis_uuid variable:
|
||||||
sample._chassis_uuid = 'edcad704-b2da-41d5-96d9-afd580ecfa12'
|
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,
|
def _get_nodes_collection(self, chassis_uuid, instance_uuid, associated,
|
||||||
maintenance, provision_state, marker, limit,
|
maintenance, provision_state, marker, limit,
|
||||||
sort_key, sort_dir, driver=None,
|
sort_key, sort_dir, driver=None,
|
||||||
|
resource_class=None,
|
||||||
resource_url=None, fields=None):
|
resource_url=None, fields=None):
|
||||||
if self.from_chassis and not chassis_uuid:
|
if self.from_chassis and not chassis_uuid:
|
||||||
raise exception.MissingParameterValue(
|
raise exception.MissingParameterValue(
|
||||||
@ -1038,6 +1047,8 @@ class NodesController(rest.RestController):
|
|||||||
filters['provision_state'] = provision_state
|
filters['provision_state'] = provision_state
|
||||||
if driver:
|
if driver:
|
||||||
filters['driver'] = 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,
|
nodes = objects.Node.list(pecan.request.context, limit, marker_obj,
|
||||||
sort_key=sort_key, sort_dir=sort_dir,
|
sort_key=sort_key, sort_dir=sort_dir,
|
||||||
@ -1128,11 +1139,11 @@ class NodesController(rest.RestController):
|
|||||||
@METRICS.timer('NodesController.get_all')
|
@METRICS.timer('NodesController.get_all')
|
||||||
@expose.expose(NodeCollection, types.uuid, types.uuid, types.boolean,
|
@expose.expose(NodeCollection, types.uuid, types.uuid, types.boolean,
|
||||||
types.boolean, wtypes.text, types.uuid, int, wtypes.text,
|
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,
|
def get_all(self, chassis_uuid=None, instance_uuid=None, associated=None,
|
||||||
maintenance=None, provision_state=None, marker=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,
|
||||||
fields=None):
|
fields=None, resource_class=None):
|
||||||
"""Retrieve a list of nodes.
|
"""Retrieve a list of nodes.
|
||||||
|
|
||||||
:param chassis_uuid: Optional UUID of a chassis, to get only nodes for
|
: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 sort_dir: direction to sort. "asc" or "desc". Default: asc.
|
||||||
:param driver: Optional string value to get only nodes using that
|
:param driver: Optional string value to get only nodes using that
|
||||||
driver.
|
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
|
:param fields: Optional, a list with a specified set of fields
|
||||||
of the resource to be returned.
|
of the resource to be returned.
|
||||||
"""
|
"""
|
||||||
api_utils.check_allow_specify_fields(fields)
|
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_for_invalid_state_and_allow_filter(provision_state)
|
||||||
api_utils.check_allow_specify_driver(driver)
|
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:
|
if fields is None:
|
||||||
fields = _DEFAULT_RETURN_FIELDS
|
fields = _DEFAULT_RETURN_FIELDS
|
||||||
return self._get_nodes_collection(chassis_uuid, instance_uuid,
|
return self._get_nodes_collection(chassis_uuid, instance_uuid,
|
||||||
associated, maintenance,
|
associated, maintenance,
|
||||||
provision_state, marker,
|
provision_state, marker,
|
||||||
limit, sort_key, sort_dir,
|
limit, sort_key, sort_dir,
|
||||||
driver, fields=fields)
|
driver=driver,
|
||||||
|
resource_class=resource_class,
|
||||||
|
fields=fields)
|
||||||
|
|
||||||
@METRICS.timer('NodesController.detail')
|
@METRICS.timer('NodesController.detail')
|
||||||
@expose.expose(NodeCollection, types.uuid, types.uuid, types.boolean,
|
@expose.expose(NodeCollection, types.uuid, types.uuid, types.boolean,
|
||||||
types.boolean, wtypes.text, types.uuid, int, wtypes.text,
|
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,
|
def detail(self, chassis_uuid=None, instance_uuid=None, associated=None,
|
||||||
maintenance=None, provision_state=None, marker=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.
|
"""Retrieve a list of nodes with detail.
|
||||||
|
|
||||||
:param chassis_uuid: Optional UUID of a chassis, to get only nodes for
|
: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 sort_dir: direction to sort. "asc" or "desc". Default: asc.
|
||||||
:param driver: Optional string value to get only nodes using that
|
:param driver: Optional string value to get only nodes using that
|
||||||
driver.
|
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_for_invalid_state_and_allow_filter(provision_state)
|
||||||
api_utils.check_allow_specify_driver(driver)
|
api_utils.check_allow_specify_driver(driver)
|
||||||
|
api_utils.check_allow_specify_resource_class(resource_class)
|
||||||
# /detail should only work against collections
|
# /detail should only work against collections
|
||||||
parent = pecan.request.path.split('/')[:-1][-1]
|
parent = pecan.request.path.split('/')[:-1][-1]
|
||||||
if parent != "nodes":
|
if parent != "nodes":
|
||||||
@ -1208,7 +1228,9 @@ class NodesController(rest.RestController):
|
|||||||
associated, maintenance,
|
associated, maintenance,
|
||||||
provision_state, marker,
|
provision_state, marker,
|
||||||
limit, sort_key, sort_dir,
|
limit, sort_key, sort_dir,
|
||||||
driver, resource_url)
|
driver=driver,
|
||||||
|
resource_class=resource_class,
|
||||||
|
resource_url=resource_url)
|
||||||
|
|
||||||
@METRICS.timer('NodesController.validate')
|
@METRICS.timer('NodesController.validate')
|
||||||
@expose.expose(wtypes.text, types.uuid_or_name, types.uuid)
|
@expose.expose(wtypes.text, types.uuid_or_name, types.uuid)
|
||||||
@ -1247,7 +1269,7 @@ class NodesController(rest.RestController):
|
|||||||
raise exception.OperationNotPermitted()
|
raise exception.OperationNotPermitted()
|
||||||
|
|
||||||
api_utils.check_allow_specify_fields(fields)
|
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)
|
rpc_node = api_utils.get_rpc_node(node_ident)
|
||||||
return Node.convert_with_links(rpc_node, fields=fields)
|
return Node.convert_with_links(rpc_node, fields=fields)
|
||||||
@ -1262,6 +1284,10 @@ class NodesController(rest.RestController):
|
|||||||
if self.from_chassis:
|
if self.from_chassis:
|
||||||
raise exception.OperationNotPermitted()
|
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
|
n_interface = node.network_interface
|
||||||
if (not api_utils.allow_network_interface() and
|
if (not api_utils.allow_network_interface() and
|
||||||
n_interface is not wtypes.Unset):
|
n_interface is not wtypes.Unset):
|
||||||
@ -1322,6 +1348,10 @@ class NodesController(rest.RestController):
|
|||||||
if self.from_chassis:
|
if self.from_chassis:
|
||||||
raise exception.OperationNotPermitted()
|
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')
|
n_interfaces = api_utils.get_patch_values(patch, '/network_interface')
|
||||||
if n_interfaces and not api_utils.allow_network_interface():
|
if n_interfaces and not api_utils.allow_network_interface():
|
||||||
raise exception.NotAcceptable()
|
raise exception.NotAcceptable()
|
||||||
|
@ -240,16 +240,17 @@ def check_allow_specify_fields(fields):
|
|||||||
raise exception.NotAcceptable()
|
raise exception.NotAcceptable()
|
||||||
|
|
||||||
|
|
||||||
def check_allow_specify_network_interface_in_fields(fields):
|
def check_allowed_fields(fields):
|
||||||
"""Check if fetching a network_interface attribute is allowed.
|
"""Check if fetching a particular field is allowed.
|
||||||
|
|
||||||
Version 1.20 of the API allows to fetching a network_interface
|
This method checks if the required version is being requested for fields
|
||||||
attribute. This method check if the required version is being
|
that are only allowed to be fetched in a particular API version.
|
||||||
requested.
|
|
||||||
"""
|
"""
|
||||||
if (fields is not None
|
if fields is None:
|
||||||
and 'network_interface' in fields
|
return
|
||||||
and not allow_network_interface()):
|
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()
|
raise exception.NotAcceptable()
|
||||||
|
|
||||||
|
|
||||||
@ -303,6 +304,20 @@ def check_allow_specify_driver(driver):
|
|||||||
'opr': versions.MINOR_16_DRIVER_FILTER})
|
'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():
|
def initial_node_provision_state():
|
||||||
"""Return node state to use by default when creating new nodes.
|
"""Return node state to use by default when creating new nodes.
|
||||||
|
|
||||||
@ -359,6 +374,15 @@ def allow_network_interface():
|
|||||||
versions.MINOR_20_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):
|
def get_controller_reserved_names(cls):
|
||||||
"""Get reserved names for a given controller.
|
"""Get reserved names for a given controller.
|
||||||
|
|
||||||
|
@ -50,6 +50,7 @@ BASE_VERSION = 1
|
|||||||
# v1.18: Add port.internal_info.
|
# v1.18: Add port.internal_info.
|
||||||
# v1.19: Add port.local_link_connection and port.pxe_enabled.
|
# v1.19: Add port.local_link_connection and port.pxe_enabled.
|
||||||
# v1.20: Add node.network_interface
|
# v1.20: Add node.network_interface
|
||||||
|
# v1.21: Add node.resource_class
|
||||||
|
|
||||||
MINOR_0_JUNO = 0
|
MINOR_0_JUNO = 0
|
||||||
MINOR_1_INITIAL_VERSION = 1
|
MINOR_1_INITIAL_VERSION = 1
|
||||||
@ -72,11 +73,12 @@ MINOR_17_ADOPT_VERB = 17
|
|||||||
MINOR_18_PORT_INTERNAL_INFO = 18
|
MINOR_18_PORT_INTERNAL_INFO = 18
|
||||||
MINOR_19_PORT_ADVANCED_NET_FIELDS = 19
|
MINOR_19_PORT_ADVANCED_NET_FIELDS = 19
|
||||||
MINOR_20_NETWORK_INTERFACE = 20
|
MINOR_20_NETWORK_INTERFACE = 20
|
||||||
|
MINOR_21_RESOURCE_CLASS = 21
|
||||||
|
|
||||||
# When adding another version, update MINOR_MAX_VERSION and also update
|
# 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
|
# doc/source/webapi/v1.rst with a detailed explanation of what the version has
|
||||||
# changed.
|
# changed.
|
||||||
MINOR_MAX_VERSION = MINOR_20_NETWORK_INTERFACE
|
MINOR_MAX_VERSION = MINOR_21_RESOURCE_CLASS
|
||||||
|
|
||||||
# String representations of the minor and maximum versions
|
# String representations of the minor and maximum versions
|
||||||
MIN_VERSION_STRING = '{}.{}'.format(BASE_VERSION, MINOR_1_INITIAL_VERSION)
|
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'])
|
query = query.filter_by(maintenance=filters['maintenance'])
|
||||||
if 'driver' in filters:
|
if 'driver' in filters:
|
||||||
query = query.filter_by(driver=filters['driver'])
|
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:
|
if 'provision_state' in filters:
|
||||||
query = query.filter_by(provision_state=filters['provision_state'])
|
query = query.filter_by(provision_state=filters['provision_state'])
|
||||||
if 'provisioned_before' in filters:
|
if 'provisioned_before' in filters:
|
||||||
|
@ -118,6 +118,7 @@ class Node(Base):
|
|||||||
driver_info = Column(db_types.JsonEncodedDict)
|
driver_info = Column(db_types.JsonEncodedDict)
|
||||||
driver_internal_info = Column(db_types.JsonEncodedDict)
|
driver_internal_info = Column(db_types.JsonEncodedDict)
|
||||||
clean_step = Column(db_types.JsonEncodedDict)
|
clean_step = Column(db_types.JsonEncodedDict)
|
||||||
|
resource_class = Column(String(80), nullable=True)
|
||||||
|
|
||||||
raid_config = Column(db_types.JsonEncodedDict)
|
raid_config = Column(db_types.JsonEncodedDict)
|
||||||
target_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.
|
# and save() validate the input of property values.
|
||||||
# Version 1.15: Add get_by_port_addresses
|
# Version 1.15: Add get_by_port_addresses
|
||||||
# Version 1.16: Add network_interface field
|
# 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()
|
dbapi = db_api.get_instance()
|
||||||
|
|
||||||
@ -99,6 +100,9 @@ class Node(base.IronicObject, object_base.VersionedObjectDictCompat):
|
|||||||
# that started but failed to finish.
|
# that started but failed to finish.
|
||||||
'last_error': object_fields.StringField(nullable=True),
|
'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_finished_at': object_fields.DateTimeField(nullable=True),
|
||||||
'inspection_started_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('conductor_affinity')
|
||||||
node.pop('chassis_id')
|
node.pop('chassis_id')
|
||||||
node.pop('tags')
|
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
|
# NOTE(jroll): pop out fields that were introduced in later API versions,
|
||||||
# versions don't fail
|
# unless explicitly requested. Otherwise, these will cause tests using
|
||||||
|
# older API versions to fail.
|
||||||
if 'network_interface' not in kw:
|
if 'network_interface' not in kw:
|
||||||
node.pop('network_interface')
|
node.pop('network_interface')
|
||||||
|
if 'resource_class' not in kw:
|
||||||
|
node.pop('resource_class')
|
||||||
|
|
||||||
internal = node_controller.NodePatchType.internal_attrs()
|
internal = node_controller.NodePatchType.internal_attrs()
|
||||||
return remove_internal(node, internal)
|
return remove_internal(node, internal)
|
||||||
|
|
||||||
|
@ -111,6 +111,7 @@ class TestListNodes(test_api_base.BaseApiTest):
|
|||||||
self.assertNotIn('raid_config', data['nodes'][0])
|
self.assertNotIn('raid_config', data['nodes'][0])
|
||||||
self.assertNotIn('target_raid_config', data['nodes'][0])
|
self.assertNotIn('target_raid_config', data['nodes'][0])
|
||||||
self.assertNotIn('network_interface', data['nodes'][0])
|
self.assertNotIn('network_interface', data['nodes'][0])
|
||||||
|
self.assertNotIn('resource_class', data['nodes'][0])
|
||||||
# never expose the chassis_id
|
# never expose the chassis_id
|
||||||
self.assertNotIn('chassis_id', data['nodes'][0])
|
self.assertNotIn('chassis_id', data['nodes'][0])
|
||||||
|
|
||||||
@ -137,6 +138,7 @@ class TestListNodes(test_api_base.BaseApiTest):
|
|||||||
self.assertIn('clean_step', data)
|
self.assertIn('clean_step', data)
|
||||||
self.assertIn('states', data)
|
self.assertIn('states', data)
|
||||||
self.assertIn('network_interface', data)
|
self.assertIn('network_interface', data)
|
||||||
|
self.assertIn('resource_class', data)
|
||||||
# never expose the chassis_id
|
# never expose the chassis_id
|
||||||
self.assertNotIn('chassis_id', data)
|
self.assertNotIn('chassis_id', data)
|
||||||
|
|
||||||
@ -336,6 +338,17 @@ class TestListNodes(test_api_base.BaseApiTest):
|
|||||||
self.assertEqual(node.network_interface,
|
self.assertEqual(node.network_interface,
|
||||||
new_data['nodes'][0]["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):
|
def test_many(self):
|
||||||
nodes = []
|
nodes = []
|
||||||
for id in range(5):
|
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.assertEqual(http_client.NOT_ACCEPTABLE, response.status_code)
|
||||||
self.assertTrue(response.json['error_message'])
|
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):
|
def test_get_console_information(self):
|
||||||
node = obj_utils.create_test_node(self.context)
|
node = obj_utils.create_test_node(self.context)
|
||||||
expected_console_info = {'test': 'test-data'}
|
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('application/json', response.content_type)
|
||||||
self.assertEqual(http_client.NOT_ACCEPTABLE, response.status_code)
|
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):
|
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('application/json', response.content_type)
|
||||||
self.assertEqual(http_client.BAD_REQUEST, response.status_int)
|
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):
|
class TestDelete(test_api_base.BaseApiTest):
|
||||||
|
|
||||||
|
@ -131,21 +131,33 @@ class TestApiUtils(base.TestCase):
|
|||||||
utils.check_allow_specify_fields, ['foo'])
|
utils.check_allow_specify_fields, ['foo'])
|
||||||
|
|
||||||
@mock.patch.object(pecan, 'request', spec_set=['version'])
|
@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
|
mock_request.version.minor = 20
|
||||||
self.assertIsNone(
|
self.assertIsNone(
|
||||||
utils.check_allow_specify_network_interface_in_fields(
|
utils.check_allowed_fields(['network_interface']))
|
||||||
['network_interface']))
|
|
||||||
|
|
||||||
@mock.patch.object(pecan, 'request', spec_set=['version'])
|
@mock.patch.object(pecan, 'request', spec_set=['version'])
|
||||||
def test_check_allow_specify_network_interface_in_fields_fail(
|
def test_check_allowed_fields_network_interface_fail(self, mock_request):
|
||||||
self, mock_request):
|
|
||||||
mock_request.version.minor = 19
|
mock_request.version.minor = 19
|
||||||
self.assertRaises(
|
self.assertRaises(
|
||||||
exception.NotAcceptable,
|
exception.NotAcceptable,
|
||||||
utils.check_allow_specify_network_interface_in_fields,
|
utils.check_allowed_fields,
|
||||||
['network_interface'])
|
['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'])
|
@mock.patch.object(pecan, 'request', spec_set=['version'])
|
||||||
def test_check_allow_specify_driver(self, mock_request):
|
def test_check_allow_specify_driver(self, mock_request):
|
||||||
mock_request.version.minor = 16
|
mock_request.version.minor = 16
|
||||||
@ -157,6 +169,17 @@ class TestApiUtils(base.TestCase):
|
|||||||
self.assertRaises(exception.NotAcceptable,
|
self.assertRaises(exception.NotAcceptable,
|
||||||
utils.check_allow_specify_driver, ['fake'])
|
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'])
|
@mock.patch.object(pecan, 'request', spec_set=['version'])
|
||||||
def test_check_allow_manage_verbs(self, mock_request):
|
def test_check_allow_manage_verbs(self, mock_request):
|
||||||
mock_request.version.minor = 4
|
mock_request.version.minor = 4
|
||||||
@ -255,6 +278,13 @@ class TestApiUtils(base.TestCase):
|
|||||||
mock_request.version.minor = 19
|
mock_request.version.minor = 19
|
||||||
self.assertFalse(utils.allow_network_interface())
|
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):
|
class TestNodeIdent(base.TestCase):
|
||||||
|
|
||||||
|
@ -433,6 +433,13 @@ class MigrationCheckersMixin(object):
|
|||||||
self.assertIsInstance(portgroups.c.internal_info.type,
|
self.assertIsInstance(portgroups.c.internal_info.type,
|
||||||
sqlalchemy.types.TEXT)
|
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):
|
def test_upgrade_and_version(self):
|
||||||
with patch_with_engine(self.engine):
|
with patch_with_engine(self.engine):
|
||||||
self.migration_api.upgrade('head')
|
self.migration_api.upgrade('head')
|
||||||
|
@ -123,7 +123,8 @@ class DbNodeTestCase(base.DbTestCase):
|
|||||||
node2 = utils.create_test_node(
|
node2 = utils.create_test_node(
|
||||||
driver='driver-two',
|
driver='driver-two',
|
||||||
uuid=uuidutils.generate_uuid(),
|
uuid=uuidutils.generate_uuid(),
|
||||||
maintenance=True)
|
maintenance=True,
|
||||||
|
resource_class='foo')
|
||||||
node3 = utils.create_test_node(
|
node3 = utils.create_test_node(
|
||||||
driver='driver-one',
|
driver='driver-one',
|
||||||
uuid=uuidutils.generate_uuid(),
|
uuid=uuidutils.generate_uuid(),
|
||||||
@ -157,6 +158,9 @@ class DbNodeTestCase(base.DbTestCase):
|
|||||||
self.assertEqual(sorted([node1.id, node3.id]),
|
self.assertEqual(sorted([node1.id, node3.id]),
|
||||||
sorted([r.id for r in res]))
|
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(
|
res = self.dbapi.get_node_list(
|
||||||
filters={'reserved_by_any_of': ['fake-host',
|
filters={'reserved_by_any_of': ['fake-host',
|
||||||
'another-fake-host']})
|
'another-fake-host']})
|
||||||
|
@ -226,6 +226,7 @@ def get_test_node(**kw):
|
|||||||
'raid_config': kw.get('raid_config'),
|
'raid_config': kw.get('raid_config'),
|
||||||
'target_raid_config': kw.get('target_raid_config'),
|
'target_raid_config': kw.get('target_raid_config'),
|
||||||
'tags': kw.get('tags', []),
|
'tags': kw.get('tags', []),
|
||||||
|
'resource_class': kw.get('resource_class'),
|
||||||
'network_interface': kw.get('network_interface'),
|
'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.
|
# 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.
|
# The fingerprint values should only be changed if there is a version bump.
|
||||||
expected_object_fingerprints = {
|
expected_object_fingerprints = {
|
||||||
'Node': '1.16-2a6646627cb937f083f428f5d54e6458',
|
'Node': '1.17-ed09e704576dc1b5a74abcbb727bf722',
|
||||||
'MyObj': '1.5-4f5efe8f0fcaf182bbe1c7fe3ba858db',
|
'MyObj': '1.5-4f5efe8f0fcaf182bbe1c7fe3ba858db',
|
||||||
'Chassis': '1.3-d656e039fd8ae9f34efc232ab3980905',
|
'Chassis': '1.3-d656e039fd8ae9f34efc232ab3980905',
|
||||||
'Port': '1.6-609504503d68982a10f495659990084b',
|
'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