Merge "Add description field to node"

This commit is contained in:
Zuul 2019-02-07 03:52:45 +00:00 committed by Gerrit Code Review
commit 1e93a48ef6
16 changed files with 311 additions and 26 deletions

View File

@ -2,6 +2,12 @@
REST API Version History
========================
1.51 (Stein, master)
--------------------
Added ``description`` field to the node object to enable operators to store
any information relates to the node. The field is up to 4096 characters.
1.50 (Stein, master)
--------------------

View File

@ -110,6 +110,8 @@ ALLOWED_TARGET_POWER_STATES = (ir_states.POWER_ON,
ir_states.SOFT_REBOOT,
ir_states.SOFT_POWER_OFF)
_NODE_DESCRIPTION_MAX_LENGTH = 4096
def get_nodes_controller_reserved_names():
global _NODES_CONTROLLER_RESERVED_WORDS
@ -1078,6 +1080,9 @@ class Node(base.APIBase):
owner = wsme.wsattr(wtypes.text)
"""Field for storage of physical node owner"""
description = wsme.wsattr(wtypes.text)
"""Field for node description"""
# NOTE(deva): "conductor_affinity" shouldn't be presented on the
# API because it's an internal value. Don't add it here.
@ -1603,7 +1608,8 @@ class NodesController(rest.RestController):
sort_key, sort_dir, driver=None,
resource_class=None, resource_url=None,
fields=None, fault=None, conductor_group=None,
detail=None, conductor=None, owner=None):
detail=None, conductor=None, owner=None,
description_contains=None):
if self.from_chassis and not chassis_uuid:
raise exception.MissingParameterValue(
_("Chassis id not specified."))
@ -1646,6 +1652,7 @@ class NodesController(rest.RestController):
'fault': fault,
'conductor_group': conductor_group,
'owner': owner,
'description_contains': description_contains,
}
filters = {}
for key, value in possible_filters.items():
@ -1763,13 +1770,13 @@ class NodesController(rest.RestController):
types.boolean, wtypes.text, types.uuid, int, wtypes.text,
wtypes.text, wtypes.text, types.listtype, wtypes.text,
wtypes.text, wtypes.text, types.boolean, wtypes.text,
wtypes.text)
wtypes.text, 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, resource_class=None, fault=None,
conductor_group=None, detail=None, conductor=None,
owner=None):
owner=None, description_contains=None):
"""Retrieve a list of nodes.
:param chassis_uuid: Optional UUID of a chassis, to get only nodes for
@ -1804,6 +1811,9 @@ class NodesController(rest.RestController):
:param fields: Optional, a list with a specified set of fields
of the resource to be returned.
:param fault: Optional string value to get only nodes with that fault.
:param description_contains: Optional string value to get only nodes
with description field contains matching
value.
"""
cdict = pecan.request.context.to_policy_values()
policy.authorize('baremetal:node:get', cdict, cdict)
@ -1822,6 +1832,7 @@ class NodesController(rest.RestController):
fields = api_utils.get_request_return_fields(fields, detail,
_DEFAULT_RETURN_FIELDS)
extra_args = {'description_contains': description_contains}
return self._get_nodes_collection(chassis_uuid, instance_uuid,
associated, maintenance,
provision_state, marker,
@ -1832,18 +1843,19 @@ class NodesController(rest.RestController):
conductor_group=conductor_group,
detail=detail,
conductor=conductor,
owner=owner)
owner=owner,
**extra_args)
@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, 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,
resource_class=None, fault=None, conductor_group=None,
conductor=None, owner=None):
conductor=None, owner=None, description_contains=None):
"""Retrieve a list of nodes with detail.
:param chassis_uuid: Optional UUID of a chassis, to get only nodes for
@ -1874,6 +1886,9 @@ class NodesController(rest.RestController):
that conductor_group.
:param owner: Optional string value that set the owner whose nodes
are to be retrurned.
:param description_contains: Optional string value to get only nodes
with description field contains matching
value.
"""
cdict = pecan.request.context.to_policy_values()
policy.authorize('baremetal:node:get', cdict, cdict)
@ -1893,6 +1908,7 @@ class NodesController(rest.RestController):
api_utils.check_allow_filter_by_conductor(conductor)
resource_url = '/'.join(['nodes', 'detail'])
extra_args = {'description_contains': description_contains}
return self._get_nodes_collection(chassis_uuid, instance_uuid,
associated, maintenance,
provision_state, marker,
@ -1903,7 +1919,8 @@ class NodesController(rest.RestController):
fault=fault,
conductor_group=conductor_group,
conductor=conductor,
owner=owner)
owner=owner,
**extra_args)
@METRICS.timer('NodesController.validate')
@expose.expose(wtypes.text, types.uuid_or_name, types.uuid)
@ -1984,6 +2001,12 @@ class NodesController(rest.RestController):
"creation. These fields can only be set for active nodes")
raise exception.Invalid(msg)
if (node.description is not wtypes.Unset and
len(node.description) > _NODE_DESCRIPTION_MAX_LENGTH):
msg = _("Cannot create node with description exceeds %s "
"characters") % _NODE_DESCRIPTION_MAX_LENGTH
raise exception.Invalid(msg)
# NOTE(deva): get_topic_for checks if node.driver is in the hash ring
# and raises NoValidHost if it is not.
# We need to ensure that node has a UUID before it can
@ -2040,6 +2063,12 @@ class NodesController(rest.RestController):
"changing the node's driver.")
raise exception.Invalid(msg)
description = api_utils.get_patch_values(patch, '/description')
if description and len(description[0]) > _NODE_DESCRIPTION_MAX_LENGTH:
msg = _("Cannot create node with description exceeds %s "
"characters") % _NODE_DESCRIPTION_MAX_LENGTH
raise exception.Invalid(msg)
@METRICS.timer('NodesController.patch')
@wsme.validate(types.uuid, types.boolean, [NodePatchType])
@expose.expose(Node, types.uuid_or_name, types.boolean,

View File

@ -380,6 +380,7 @@ VERSIONED_FIELDS = {
'protected_reason': versions.MINOR_48_NODE_PROTECTED,
'conductor': versions.MINOR_49_CONDUCTORS,
'owner': versions.MINOR_50_NODE_OWNER,
'description': versions.MINOR_51_NODE_DESCRIPTION,
}
for field in V31_FIELDS:

View File

@ -88,6 +88,7 @@ BASE_VERSION = 1
# v1.48: Add protected to the node object.
# v1.49: Exposes current conductor on the node object.
# v1.50: Add owner to the node object.
# v1.51: Add description to the node object.
MINOR_0_JUNO = 0
MINOR_1_INITIAL_VERSION = 1
@ -140,6 +141,7 @@ MINOR_47_NODE_AUTOMATED_CLEAN = 47
MINOR_48_NODE_PROTECTED = 48
MINOR_49_CONDUCTORS = 49
MINOR_50_NODE_OWNER = 50
MINOR_51_NODE_DESCRIPTION = 51
# When adding another version, update:
# - MINOR_MAX_VERSION
@ -147,7 +149,7 @@ MINOR_50_NODE_OWNER = 50
# explanation of what changed in the new version
# - common/release_mappings.py, RELEASE_MAPPING['master']['api']
MINOR_MAX_VERSION = MINOR_50_NODE_OWNER
MINOR_MAX_VERSION = MINOR_51_NODE_DESCRIPTION
# String representations of the minor and maximum versions
_MIN_VERSION_STRING = '{}.{}'.format(BASE_VERSION, MINOR_1_INITIAL_VERSION)

View File

@ -131,11 +131,11 @@ RELEASE_MAPPING = {
}
},
'master': {
'api': '1.50',
'api': '1.51',
'rpc': '1.48',
'objects': {
'Allocation': ['1.0'],
'Node': ['1.31', '1.30', '1.29', '1.28'],
'Node': ['1.32', '1.31', '1.30', '1.29', '1.28'],
'Conductor': ['1.3'],
'Chassis': ['1.3'],
'Port': ['1.9'],

View File

@ -0,0 +1,31 @@
# 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 node description
Revision ID: 28c44432c9c3
Revises: dd67b91a1981
Create Date: 2019-01-23 13:54:08.850421
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '28c44432c9c3'
down_revision = '9cbeefa3763f'
def upgrade():
op.add_column('nodes', sa.Column('description', sa.Text(),
nullable=True))

View File

@ -225,7 +225,7 @@ class Connection(api.Connection):
def __init__(self):
pass
def _add_nodes_filters(self, query, filters):
def _validate_nodes_filters(self, filters):
if filters is None:
filters = dict()
supported_filters = {'console_enabled', 'maintenance', 'driver',
@ -233,13 +233,17 @@ class Connection(api.Connection):
'chassis_uuid', 'associated', 'reserved',
'reserved_by_any_of', 'provisioned_before',
'inspection_started_before', 'fault',
'conductor_group', 'owner',
'uuid_in', 'with_power_state'}
'conductor_group', 'owner', 'uuid_in',
'with_power_state', 'description_contains'}
unsupported_filters = set(filters).difference(supported_filters)
if unsupported_filters:
msg = _("SqlAlchemy API does not support "
"filtering by %s") % ', '.join(unsupported_filters)
raise ValueError(msg)
return filters
def _add_nodes_filters(self, query, filters):
filters = self._validate_nodes_filters(filters)
for field in ['console_enabled', 'maintenance', 'driver',
'resource_class', 'provision_state', 'uuid', 'id',
'fault', 'conductor_group', 'owner']:
@ -280,6 +284,11 @@ class Connection(api.Connection):
query = query.filter(models.Node.power_state != sql.null())
else:
query = query.filter(models.Node.power_state == sql.null())
if 'description_contains' in filters:
keyword = filters['description_contains']
if keyword is not None:
query = query.filter(
models.Node.description.like(r'%{}%'.format(keyword)))
return query

View File

@ -182,6 +182,7 @@ class Node(Base):
owner = Column(String(255), nullable=True)
allocation_id = Column(Integer, ForeignKey('allocations.id'),
nullable=True)
description = Column(Text, nullable=True)
bios_interface = Column(String(255), nullable=True)
boot_interface = Column(String(255), nullable=True)

View File

@ -68,7 +68,8 @@ class Node(base.IronicObject, object_base.VersionedObjectDictCompat):
# Version 1.29: Add protected and protected_reason fields
# Version 1.30: Add owner field
# Version 1.31: Add allocation_id field
VERSION = '1.31'
# Version 1.32: Add description field
VERSION = '1.32'
dbapi = db_api.get_instance()
@ -153,6 +154,7 @@ class Node(base.IronicObject, object_base.VersionedObjectDictCompat):
'vendor_interface': object_fields.StringField(nullable=True),
'traits': object_fields.ObjectField('TraitList', nullable=True),
'owner': object_fields.StringField(nullable=True),
'description': object_fields.StringField(nullable=True),
}
def as_dict(self, secure=False):
@ -577,6 +579,8 @@ class Node(base.IronicObject, object_base.VersionedObjectDictCompat):
set to None or removed.
Version 1.31: allocation_id was added. For versions prior to this, it
should be set to None (or removed).
Version 1.32: description was added. For versions prior to this, it
should be set to None (or removed).
:param target_version: the desired version of the object
:param remove_unavailable_fields: True to remove fields that are
@ -590,7 +594,7 @@ class Node(base.IronicObject, object_base.VersionedObjectDictCompat):
fields = [('rescue_interface', 22), ('traits', 23),
('bios_interface', 24), ('fault', 25),
('automated_clean', 28), ('protected_reason', 29),
('owner', 30), ('allocation_id', 31)]
('owner', 30), ('allocation_id', 31), ('description', 32)]
for name, minor in fields:
self._adjust_field_to_version(name, None, target_version,
1, minor, remove_unavailable_fields)
@ -622,6 +626,7 @@ class NodePayload(notification.NotificationPayloadBase):
'console_enabled': ('node', 'console_enabled'),
'created_at': ('node', 'created_at'),
'deploy_step': ('node', 'deploy_step'),
'description': ('node', 'description'),
'driver': ('node', 'driver'),
'extra': ('node', 'extra'),
'inspection_finished_at': ('node', 'inspection_finished_at'),
@ -672,13 +677,15 @@ class NodePayload(notification.NotificationPayloadBase):
# Version 1.10: Add conductor_group field exposed via API.
# Version 1.11: Add protected and protected_reason fields exposed via API.
# Version 1.12: Add node owner field.
VERSION = '1.12'
# Version 1.13: Add description field.
VERSION = '1.13'
fields = {
'clean_step': object_fields.FlexibleDictField(nullable=True),
'conductor_group': object_fields.StringField(nullable=True),
'console_enabled': object_fields.BooleanField(nullable=True),
'created_at': object_fields.DateTimeField(nullable=True),
'deploy_step': object_fields.FlexibleDictField(nullable=True),
'description': object_fields.StringField(nullable=True),
'driver': object_fields.StringField(nullable=True),
'extra': object_fields.FlexibleDictField(nullable=True),
'inspection_finished_at': object_fields.DateTimeField(nullable=True),
@ -754,7 +761,8 @@ class NodeSetPowerStatePayload(NodePayload):
# Version 1.10: Parent NodePayload version 1.10
# Version 1.11: Parent NodePayload version 1.11
# Version 1.12: Parent NodePayload version 1.12
VERSION = '1.12'
# Version 1.13: Parent NodePayload version 1.13
VERSION = '1.13'
fields = {
# "to_power" indicates the future target_power_state of the node. A
@ -807,7 +815,8 @@ class NodeCorrectedPowerStatePayload(NodePayload):
# Version 1.10: Parent NodePayload version 1.10
# Version 1.11: Parent NodePayload version 1.11
# Version 1.12: Parent NodePayload version 1.12
VERSION = '1.12'
# Version 1.13: Parent NodePayload version 1.13
VERSION = '1.13'
fields = {
'from_power': object_fields.StringField(nullable=True)
@ -844,7 +853,8 @@ class NodeSetProvisionStatePayload(NodePayload):
# Version 1.10: Parent NodePayload version 1.10
# Version 1.11: Parent NodePayload version 1.11
# Version 1.12: Parent NodePayload version 1.12
VERSION = '1.12'
# Version 1.13: Parent NodePayload version 1.13
VERSION = '1.13'
SCHEMA = dict(NodePayload.SCHEMA,
**{'instance_info': ('node', 'instance_info')})
@ -888,7 +898,8 @@ class NodeCRUDPayload(NodePayload):
# Version 1.8: Parent NodePayload version 1.10
# Version 1.9: Parent NodePayload version 1.11
# Version 1.10: Parent NodePayload version 1.12
VERSION = '1.10'
# Version 1.11: Parent NodePayload version 1.13
VERSION = '1.11'
SCHEMA = dict(NodePayload.SCHEMA,
**{'instance_info': ('node', 'instance_info'),

View File

@ -345,6 +345,12 @@ class TestListNodes(test_api_base.BaseApiTest):
headers={api_base.Version.string: '1.50'})
self.assertEqual(data['owner'], "akindofmagic")
def test_node_description_null_field(self):
node = obj_utils.create_test_node(self.context, description=None)
data = self.get_json('/nodes/%s' % node.uuid,
headers={api_base.Version.string: '1.51'})
self.assertIsNone(data['description'])
def test_get_one_custom_fields(self):
node = obj_utils.create_test_node(self.context,
chassis_id=self.chassis.id)
@ -543,6 +549,14 @@ class TestListNodes(test_api_base.BaseApiTest):
headers={api_base.Version.string: '1.50'})
self.assertIn('owner', response)
def test_get_description_field(self):
node = obj_utils.create_test_node(self.context,
description='useful piece')
fields = 'description'
response = self.get_json('/nodes/%s?fields=%s' % (node.uuid, fields),
headers={api_base.Version.string: '1.51'})
self.assertIn('description', response)
def test_detail(self):
node = obj_utils.create_test_node(self.context,
chassis_id=self.chassis.id)
@ -790,6 +804,17 @@ class TestListNodes(test_api_base.BaseApiTest):
'/nodes/detail', headers={api_base.Version.string: '1.37'})
self.assertEqual(['CUSTOM_1'], new_data['nodes'][0]["traits"])
def test_hide_fields_in_newer_versions_description(self):
node = obj_utils.create_test_node(self.context,
description="useful piece")
data = self.get_json('/nodes/%s' % node.uuid,
headers={api_base.Version.string: "1.50"})
self.assertNotIn('description', data)
data = self.get_json('/nodes/%s' % node.uuid,
headers={api_base.Version.string: "1.51"})
self.assertEqual('useful piece', data['description'])
def test_many(self):
nodes = []
for id in range(5):
@ -1690,6 +1715,25 @@ 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_description(self):
node1 = obj_utils.create_test_node(self.context,
uuid=uuidutils.generate_uuid(),
description='some cats here')
node2 = obj_utils.create_test_node(self.context,
uuid=uuidutils.generate_uuid(),
description='some dogs there')
data = self.get_json('/nodes?description_contains=cat',
headers={api_base.Version.string: '1.51'})
uuids = [n['uuid'] for n in data['nodes']]
self.assertIn(node1.uuid, uuids)
self.assertNotIn(node2.uuid, uuids)
data = self.get_json('/nodes?description_contains=dog',
headers={api_base.Version.string: '1.51'})
uuids = [n['uuid'] for n in data['nodes']]
self.assertIn(node2.uuid, uuids)
self.assertNotIn(node1.uuid, uuids)
def test_get_console_information(self):
node = obj_utils.create_test_node(self.context)
expected_console_info = {'test': 'test-data'}
@ -2924,6 +2968,34 @@ 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_description(self):
node = obj_utils.create_test_node(self.context,
uuid=uuidutils.generate_uuid())
self.mock_update_node.return_value = node
headers = {api_base.Version.string: '1.51'}
response = self.patch_json('/nodes/%s' % node.uuid,
[{'path': '/description',
'value': 'meow',
'op': 'replace'}],
headers=headers)
self.assertEqual('application/json', response.content_type)
self.assertEqual(http_client.OK, response.status_code)
def test_update_description_oversize(self):
node = obj_utils.create_test_node(self.context,
uuid=uuidutils.generate_uuid())
desc = '12345678' * 512 + 'last weed'
self.mock_update_node.return_value = node
headers = {api_base.Version.string: '1.51'}
response = self.patch_json('/nodes/%s' % node.uuid,
[{'path': '/description',
'value': desc,
'op': 'replace'}],
headers=headers,
expect_errors=True)
self.assertEqual('application/json', response.content_type)
self.assertEqual(http_client.BAD_REQUEST, response.status_code)
def _create_node_locally(node):
driver_factory.check_and_update_node_interfaces(node)
@ -3550,6 +3622,27 @@ class TestPost(test_api_base.BaseApiTest):
self.assertEqual('application/json', response.content_type)
self.assertEqual(http_client.NOT_ACCEPTABLE, response.status_int)
def test_create_node_description(self):
node = test_api_utils.post_get_test_node(description='useful stuff')
response = self.post_json('/nodes', node,
headers={api_base.Version.string:
str(api_v1.max_version())})
self.assertEqual(http_client.CREATED, response.status_int)
result = self.get_json('/nodes/%s' % node['uuid'],
headers={api_base.Version.string:
str(api_v1.max_version())})
self.assertEqual('useful stuff', result['description'])
def test_create_node_description_oversize(self):
desc = '12345678' * 512 + 'last weed'
node = test_api_utils.post_get_test_node(description=desc)
response = self.post_json('/nodes', node,
headers={api_base.Version.string:
str(api_v1.max_version())},
expect_errors=True)
self.assertEqual('application/json', response.content_type)
self.assertEqual(http_client.BAD_REQUEST, response.status_int)
class TestDelete(test_api_base.BaseApiTest):

View File

@ -851,6 +851,13 @@ class MigrationCheckersMixin(object):
(sqlalchemy.types.Boolean,
sqlalchemy.types.Integer))
def _check_28c44432c9c3(self, engine, data):
nodes_tbl = db_utils.get_table(engine, 'nodes')
col_names = [column.name for column in nodes_tbl.c]
self.assertIn('description', col_names)
self.assertIsInstance(nodes_tbl.c.description.type,
sqlalchemy.types.TEXT)
def test_upgrade_and_version(self):
with patch_with_engine(self.engine):
self.migration_api.upgrade('head')

View File

@ -273,6 +273,19 @@ class DbNodeTestCase(base.DbTestCase):
states.INSPECTING})
self.assertEqual([node2.id], [r[0] for r in res])
def test_get_nodeinfo_list_description(self):
node1 = utils.create_test_node(uuid=uuidutils.generate_uuid(),
description='Hello')
node2 = utils.create_test_node(uuid=uuidutils.generate_uuid(),
description='World!')
res = self.dbapi.get_nodeinfo_list(
filters={'description_contains': 'Hello'})
self.assertEqual([node1.id], [r[0] for r in res])
res = self.dbapi.get_nodeinfo_list(filters={'description_contains':
'World!'})
self.assertEqual([node2.id], [r[0] for r in res])
def test_get_node_list(self):
uuids = []
for i in range(1, 6):
@ -382,6 +395,19 @@ class DbNodeTestCase(base.DbTestCase):
self.dbapi.get_node_list,
filters=filters)
def test_get_node_list_description(self):
node1 = utils.create_test_node(uuid=uuidutils.generate_uuid(),
description='Hello')
node2 = utils.create_test_node(uuid=uuidutils.generate_uuid(),
description='World!')
res = self.dbapi.get_node_list(filters={
'description_contains': 'Hello'})
self.assertEqual([node1.id], [r.id for r in res])
res = self.dbapi.get_node_list(filters={
'description_contains': 'World!'})
self.assertEqual([node2.id], [r.id for r in res])
def test_get_node_list_chassis_not_found(self):
self.assertRaises(exception.ChassisNotFound,
self.dbapi.get_node_list,

View File

@ -222,6 +222,7 @@ def get_test_node(**kw):
'conductor': kw.get('conductor'),
'owner': kw.get('owner', None),
'allocation_id': kw.get('allocation_id'),
'description': kw.get('description'),
}
for iface in drivers_base.ALL_INTERFACES:

View File

@ -949,6 +949,68 @@ class TestConvertToVersion(db_base.DbTestCase):
self.assertIsNone(node.allocation_id)
self.assertEqual({}, node.obj_get_changes())
def test_description_supported_missing(self):
# description not set, should be set to default.
node = obj_utils.get_test_node(self.ctxt, **self.fake_node)
delattr(node, 'description')
node.obj_reset_changes()
node._convert_to_version("1.32")
self.assertIsNone(node.description)
self.assertEqual({'description': None},
node.obj_get_changes())
def test_description_supported_set(self):
# description set, no change required.
node = obj_utils.get_test_node(self.ctxt, **self.fake_node)
node.description = "Useful information relates to this node"
node.obj_reset_changes()
node._convert_to_version("1.32")
self.assertEqual("Useful information relates to this node",
node.description)
self.assertEqual({}, node.obj_get_changes())
def test_description_unsupported_missing(self):
# description not set, no change required.
node = obj_utils.get_test_node(self.ctxt, **self.fake_node)
delattr(node, 'description')
node.obj_reset_changes()
node._convert_to_version("1.31")
self.assertNotIn('description', node)
self.assertEqual({}, node.obj_get_changes())
def test_description_unsupported_set_remove(self):
# description set, should be removed.
node = obj_utils.get_test_node(self.ctxt, **self.fake_node)
node.description = "Useful piece"
node.obj_reset_changes()
node._convert_to_version("1.31")
self.assertNotIn('description', node)
self.assertEqual({}, node.obj_get_changes())
def test_description_unsupported_set_no_remove_non_default(self):
# description set, should be set to default.
node = obj_utils.get_test_node(self.ctxt, **self.fake_node)
node.description = "Useful piece"
node.obj_reset_changes()
node._convert_to_version("1.31", False)
self.assertIsNone(node.description)
self.assertEqual({'description': None},
node.obj_get_changes())
def test_description_unsupported_set_no_remove_default(self):
# description set, no change required.
node = obj_utils.get_test_node(self.ctxt, **self.fake_node)
node.description = None
node.obj_reset_changes()
node._convert_to_version("1.31", False)
self.assertIsNone(node.description)
self.assertEqual({}, node.obj_get_changes())
class TestNodePayloads(db_base.DbTestCase):

View File

@ -677,7 +677,7 @@ class TestObject(_LocalTest, _TestObject):
# version bump. It is an MD5 hash of the object fields and remotable methods.
# The fingerprint values should only be changed if there is a version bump.
expected_object_fingerprints = {
'Node': '1.31-1b77c11e94f971a71c76f5f44fb5b3f4',
'Node': '1.32-525750e76f07b62142ed5297334b7832',
'MyObj': '1.5-9459d30d6954bffc7a9afd347a807ca6',
'Chassis': '1.3-d656e039fd8ae9f34efc232ab3980905',
'Port': '1.9-0cb9202a4ec442e8c0d87a324155eaaf',
@ -685,21 +685,21 @@ expected_object_fingerprints = {
'Conductor': '1.3-d3f53e853b4d58cae5bfbd9a8341af4a',
'EventType': '1.1-aa2ba1afd38553e3880c267404e8d370',
'NotificationPublisher': '1.0-51a09397d6c0687771fb5be9a999605d',
'NodePayload': '1.12-7d650c2a024357275990681f020512e4',
'NodePayload': '1.13-18a34d461ef7d5dbc1c3e5a55fcb867a',
'NodeSetPowerStateNotification': '1.0-59acc533c11d306f149846f922739c15',
'NodeSetPowerStatePayload': '1.12-703d110d571cc95b2947bb6bd153fcb8',
'NodeSetPowerStatePayload': '1.13-4f96e52568e058e3fd6ffc9b0cf15764',
'NodeCorrectedPowerStateNotification':
'1.0-59acc533c11d306f149846f922739c15',
'NodeCorrectedPowerStatePayload': '1.12-29cbb6b20a0aeea9e0ab9e17302e9e16',
'NodeCorrectedPowerStatePayload': '1.13-929af354e7c3474520ce6162ee794717',
'NodeSetProvisionStateNotification':
'1.0-59acc533c11d306f149846f922739c15',
'NodeSetProvisionStatePayload': '1.12-a302ce357ad39a0a4d1ca3c0ee44f0e0',
'NodeSetProvisionStatePayload': '1.13-fa15d2954961d8edcaba9d737a1cad91',
'VolumeConnector': '1.0-3e0252c0ab6e6b9d158d09238a577d97',
'VolumeTarget': '1.0-0b10d663d8dae675900b2c7548f76f5e',
'ChassisCRUDNotification': '1.0-59acc533c11d306f149846f922739c15',
'ChassisCRUDPayload': '1.0-dce63895d8186279a7dd577cffccb202',
'NodeCRUDNotification': '1.0-59acc533c11d306f149846f922739c15',
'NodeCRUDPayload': '1.10-49590dee863c5ed1193f5deae0a0a2f2',
'NodeCRUDPayload': '1.11-f1c6a6b099e8e28f55378c448c033de0',
'PortCRUDNotification': '1.0-59acc533c11d306f149846f922739c15',
'PortCRUDPayload': '1.3-21235916ed54a91b2a122f59571194e7',
'NodeMaintenanceNotification': '1.0-59acc533c11d306f149846f922739c15',

View File

@ -0,0 +1,6 @@
---
features:
- |
Adds a ``description`` field to the node object to enable operators to
store any information relates to the node. The field is up to 4096
characters.