Merge "Add "owner" information field"
This commit is contained in:
commit
87b25d5447
@ -92,6 +92,9 @@ supplied when the Node is created, or the resource may be updated later.
|
||||
.. versionadded:: 1.46
|
||||
Introduced the ``conductor_group`` field.
|
||||
|
||||
.. versionadded: 1.50
|
||||
Introduced the ``owner`` field.
|
||||
|
||||
Normal response codes: 201
|
||||
|
||||
Error codes: 400,403,406
|
||||
@ -120,6 +123,7 @@ Request
|
||||
- storage_interface: req_storage_interface
|
||||
- uuid: req_uuid
|
||||
- vendor_interface: req_vendor_interface
|
||||
- owner: owner
|
||||
|
||||
**Example Node creation request with a dynamic driver:**
|
||||
|
||||
@ -188,6 +192,7 @@ microversion 1.48.
|
||||
- conductor_group: conductor_group
|
||||
- protected: protected
|
||||
- protected_reason: protected_reason
|
||||
- owner: owner
|
||||
|
||||
**Example JSON representation of a Node:**
|
||||
|
||||
@ -235,6 +240,9 @@ provision state, and maintenance setting for each Node.
|
||||
Introduced the ``conductor_group`` request parameter, to allow filtering the
|
||||
list of returned nodes by conductor group.
|
||||
|
||||
.. versionadded: 1.50
|
||||
Introduced the ``owner`` field.
|
||||
|
||||
Normal response codes: 200
|
||||
|
||||
Error codes: 400,403,406
|
||||
@ -258,6 +266,7 @@ Request
|
||||
- sort_dir: sort_dir
|
||||
- sort_key: sort_key
|
||||
- detail: detail
|
||||
- owner: owner
|
||||
|
||||
Response
|
||||
--------
|
||||
@ -307,6 +316,9 @@ Nova instance, eg. with a request to ``v1/nodes/detail?instance_uuid={NOVA INSTA
|
||||
.. versionadded:: 1.48
|
||||
Introduced the ``protected`` and ``protected_reason`` fields.
|
||||
|
||||
.. versionadded: 1.50
|
||||
Introduced the ``owner`` field.
|
||||
|
||||
Normal response codes: 200
|
||||
|
||||
Error codes: 400,403,406
|
||||
@ -324,6 +336,7 @@ Request
|
||||
- driver: r_driver
|
||||
- resource_class: r_resource_class
|
||||
- conductor_group: r_conductor_group
|
||||
- owner: owner
|
||||
- limit: limit
|
||||
- marker: marker
|
||||
- sort_dir: sort_dir
|
||||
|
@ -856,6 +856,12 @@ nodes:
|
||||
in: body
|
||||
required: true
|
||||
type: array
|
||||
owner:
|
||||
description: |
|
||||
A string or UUID of the tenant who owns the baremetal node.
|
||||
in: body
|
||||
required: false
|
||||
type: string
|
||||
passthru_async:
|
||||
description: |
|
||||
If True the passthru function is invoked asynchronously; if False,
|
||||
|
@ -36,6 +36,7 @@
|
||||
"management_interface": null,
|
||||
"name": "test_node_classic",
|
||||
"network_interface": "flat",
|
||||
"owner": null,
|
||||
"portgroups": [
|
||||
{
|
||||
"href": "http://127.0.0.1:6385/v1/nodes/6d85703a-565d-469a-96ce-30b6de53079d/portgroups",
|
||||
|
@ -38,6 +38,7 @@
|
||||
"management_interface": null,
|
||||
"name": "test_node_classic",
|
||||
"network_interface": "flat",
|
||||
"owner": null,
|
||||
"portgroups": [
|
||||
{
|
||||
"href": "http://127.0.0.1:6385/v1/nodes/6d85703a-565d-469a-96ce-30b6de53079d/portgroups",
|
||||
|
@ -40,6 +40,7 @@
|
||||
"management_interface": null,
|
||||
"name": "test_node_classic",
|
||||
"network_interface": "flat",
|
||||
"owner": null,
|
||||
"portgroups": [
|
||||
{
|
||||
"href": "http://127.0.0.1:6385/v1/nodes/6d85703a-565d-469a-96ce-30b6de53079d/portgroups",
|
||||
|
@ -40,6 +40,7 @@
|
||||
"management_interface": null,
|
||||
"name": "test_node_classic",
|
||||
"network_interface": "flat",
|
||||
"owner": "john doe",
|
||||
"portgroups": [
|
||||
{
|
||||
"href": "http://127.0.0.1:6385/v1/nodes/6d85703a-565d-469a-96ce-30b6de53079d/portgroups",
|
||||
@ -139,6 +140,7 @@
|
||||
"management_interface": "ipmitool",
|
||||
"name": "test_node_dynamic",
|
||||
"network_interface": "flat",
|
||||
"owner": "43e61ec9-8e42-4dcb-bc45-30d66aa93e5b",
|
||||
"portgroups": [
|
||||
{
|
||||
"href": "http://127.0.0.1:6385/v1/nodes/2b045129-a906-46af-bc1a-092b294b3428/portgroups",
|
||||
|
@ -2,6 +2,14 @@
|
||||
REST API Version History
|
||||
========================
|
||||
|
||||
1.50 (Stein, master)
|
||||
--------------------
|
||||
|
||||
Added ``owner`` field to the node object to enable operators to store
|
||||
information in relation to the owner of a node. The field is up to 255
|
||||
characters and MAY be used in a later point in time to allow designation
|
||||
and deligation of permissions.
|
||||
|
||||
1.49 (Stein, master)
|
||||
--------------------
|
||||
|
||||
|
@ -1075,6 +1075,9 @@ class Node(base.APIBase):
|
||||
conductor = wsme.wsattr(wtypes.text, readonly=True)
|
||||
"""Represent the conductor currently serving the node"""
|
||||
|
||||
owner = wsme.wsattr(wtypes.text)
|
||||
"""Field for storage of physical node owner"""
|
||||
|
||||
# NOTE(deva): "conductor_affinity" shouldn't be presented on the
|
||||
# API because it's an internal value. Don't add it here.
|
||||
|
||||
@ -1277,7 +1280,7 @@ class Node(base.APIBase):
|
||||
storage_interface=None, traits=[], rescue_interface=None,
|
||||
bios_interface=None, conductor_group="",
|
||||
automated_clean=None, protected=False,
|
||||
protected_reason=None)
|
||||
protected_reason=None, owner=None)
|
||||
# NOTE(matty_dubs): The chassis_uuid getter() is based on the
|
||||
# _chassis_uuid variable:
|
||||
sample._chassis_uuid = 'edcad704-b2da-41d5-96d9-afd580ecfa12'
|
||||
@ -1589,35 +1592,12 @@ class NodesController(rest.RestController):
|
||||
filtered_nodes.append(n)
|
||||
return filtered_nodes
|
||||
|
||||
def _create_node_filters(self, chassis_uuid=None, associated=None,
|
||||
maintenance=None, provision_state=None,
|
||||
driver=None, resource_class=None, fault=None,
|
||||
conductor_group=None):
|
||||
filters = {}
|
||||
if chassis_uuid:
|
||||
filters['chassis_uuid'] = chassis_uuid
|
||||
if associated is not None:
|
||||
filters['associated'] = associated
|
||||
if maintenance is not None:
|
||||
filters['maintenance'] = maintenance
|
||||
if provision_state:
|
||||
filters['provision_state'] = provision_state
|
||||
if driver:
|
||||
filters['driver'] = driver
|
||||
if resource_class is not None:
|
||||
filters['resource_class'] = resource_class
|
||||
if fault is not None:
|
||||
filters['fault'] = fault
|
||||
if conductor_group is not None:
|
||||
filters['conductor_group'] = conductor_group
|
||||
return filters
|
||||
|
||||
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, fault=None, conductor_group=None,
|
||||
detail=None, conductor=None):
|
||||
detail=None, conductor=None, owner=None):
|
||||
if self.from_chassis and not chassis_uuid:
|
||||
raise exception.MissingParameterValue(
|
||||
_("Chassis id not specified."))
|
||||
@ -1650,10 +1630,22 @@ class NodesController(rest.RestController):
|
||||
# be generated, which we don't want.
|
||||
limit = 0
|
||||
else:
|
||||
filters = self._create_node_filters(chassis_uuid, associated,
|
||||
maintenance, provision_state,
|
||||
driver, resource_class, fault,
|
||||
conductor_group)
|
||||
possible_filters = {
|
||||
'maintenance': maintenance,
|
||||
'chassis_uuid': chassis_uuid,
|
||||
'associated': associated,
|
||||
'provision_state': provision_state,
|
||||
'driver': driver,
|
||||
'resource_class': resource_class,
|
||||
'fault': fault,
|
||||
'conductor_group': conductor_group,
|
||||
'owner': owner,
|
||||
}
|
||||
filters = {}
|
||||
for key, value in possible_filters.items():
|
||||
if value is not None:
|
||||
filters[key] = value
|
||||
|
||||
nodes = objects.Node.list(pecan.request.context, limit, marker_obj,
|
||||
sort_key=sort_key, sort_dir=sort_dir,
|
||||
filters=filters)
|
||||
@ -1764,12 +1756,14 @@ class NodesController(rest.RestController):
|
||||
@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, wtypes.text, types.boolean, wtypes.text)
|
||||
wtypes.text, wtypes.text, types.boolean, 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):
|
||||
conductor_group=None, detail=None, conductor=None,
|
||||
owner=None):
|
||||
"""Retrieve a list of nodes.
|
||||
|
||||
:param chassis_uuid: Optional UUID of a chassis, to get only nodes for
|
||||
@ -1799,6 +1793,8 @@ class NodesController(rest.RestController):
|
||||
that conductor_group.
|
||||
:param conductor: Optional string value to get only nodes managed by
|
||||
that conductor.
|
||||
:param owner: Optional string value that set the owner whose nodes
|
||||
are to be retrurned.
|
||||
: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.
|
||||
@ -1815,6 +1811,7 @@ class NodesController(rest.RestController):
|
||||
api_utils.check_allow_filter_by_fault(fault)
|
||||
api_utils.check_allow_filter_by_conductor_group(conductor_group)
|
||||
api_utils.check_allow_filter_by_conductor(conductor)
|
||||
api_utils.check_allow_filter_by_owner(owner)
|
||||
|
||||
fields = api_utils.get_request_return_fields(fields, detail,
|
||||
_DEFAULT_RETURN_FIELDS)
|
||||
@ -1828,18 +1825,19 @@ class NodesController(rest.RestController):
|
||||
fields=fields, fault=fault,
|
||||
conductor_group=conductor_group,
|
||||
detail=detail,
|
||||
conductor=conductor)
|
||||
conductor=conductor,
|
||||
owner=owner)
|
||||
|
||||
@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)
|
||||
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):
|
||||
conductor=None, owner=None):
|
||||
"""Retrieve a list of nodes with detail.
|
||||
|
||||
:param chassis_uuid: Optional UUID of a chassis, to get only nodes for
|
||||
@ -1868,6 +1866,8 @@ class NodesController(rest.RestController):
|
||||
:param fault: Optional string value to get only nodes with that fault.
|
||||
:param conductor_group: Optional string value to get only nodes with
|
||||
that conductor_group.
|
||||
:param owner: Optional string value that set the owner whose nodes
|
||||
are to be retrurned.
|
||||
"""
|
||||
cdict = pecan.request.context.to_policy_values()
|
||||
policy.authorize('baremetal:node:get', cdict, cdict)
|
||||
@ -1877,6 +1877,7 @@ class NodesController(rest.RestController):
|
||||
api_utils.check_allow_specify_resource_class(resource_class)
|
||||
api_utils.check_allow_filter_by_fault(fault)
|
||||
api_utils.check_allow_filter_by_conductor_group(conductor_group)
|
||||
api_utils.check_allow_filter_by_owner(owner)
|
||||
api_utils.check_allowed_fields([sort_key])
|
||||
# /detail should only work against collections
|
||||
parent = pecan.request.path.split('/')[:-1][-1]
|
||||
@ -1895,7 +1896,8 @@ class NodesController(rest.RestController):
|
||||
resource_url=resource_url,
|
||||
fault=fault,
|
||||
conductor_group=conductor_group,
|
||||
conductor=conductor)
|
||||
conductor=conductor,
|
||||
owner=owner)
|
||||
|
||||
@METRICS.timer('NodesController.validate')
|
||||
@expose.expose(wtypes.text, types.uuid_or_name, types.uuid)
|
||||
|
@ -379,6 +379,7 @@ VERSIONED_FIELDS = {
|
||||
'protected': versions.MINOR_48_NODE_PROTECTED,
|
||||
'protected_reason': versions.MINOR_48_NODE_PROTECTED,
|
||||
'conductor': versions.MINOR_49_CONDUCTORS,
|
||||
'owner': versions.MINOR_50_NODE_OWNER,
|
||||
}
|
||||
|
||||
for field in V31_FIELDS:
|
||||
@ -544,6 +545,20 @@ def check_allow_filter_by_conductor_group(conductor_group):
|
||||
'opr': versions.MINOR_46_NODE_CONDUCTOR_GROUP})
|
||||
|
||||
|
||||
def check_allow_filter_by_owner(owner):
|
||||
"""Check if filtering nodes by owner is allowed.
|
||||
|
||||
Version 1.50 of the API allows filtering nodes by owner.
|
||||
"""
|
||||
if (owner is not None and pecan.request.version.minor
|
||||
< versions.MINOR_50_NODE_OWNER):
|
||||
raise exception.NotAcceptable(_(
|
||||
"Request not acceptable. The minimal required API version "
|
||||
"should be %(base)s.%(opr)s") %
|
||||
{'base': versions.BASE_VERSION,
|
||||
'opr': versions.MINOR_50_NODE_OWNER})
|
||||
|
||||
|
||||
def initial_node_provision_state():
|
||||
"""Return node state to use by default when creating new nodes.
|
||||
|
||||
@ -930,7 +945,7 @@ def get_request_return_fields(fields, detail, default_fields):
|
||||
def allow_expose_conductors():
|
||||
"""Check if accessing conductor endpoints is allowed.
|
||||
|
||||
Version 1.48 of the API exposed conductor endpoints and conductor field
|
||||
Version 1.49 of the API exposed conductor endpoints and conductor field
|
||||
for the node.
|
||||
"""
|
||||
return pecan.request.version.minor >= versions.MINOR_49_CONDUCTORS
|
||||
@ -939,7 +954,7 @@ def allow_expose_conductors():
|
||||
def check_allow_filter_by_conductor(conductor):
|
||||
"""Check if filtering nodes by conductor is allowed.
|
||||
|
||||
Version 1.48 of the API allows filtering nodes by conductor.
|
||||
Version 1.49 of the API allows filtering nodes by conductor.
|
||||
"""
|
||||
if conductor is not None and not allow_expose_conductors():
|
||||
raise exception.NotAcceptable(_(
|
||||
|
@ -86,6 +86,8 @@ BASE_VERSION = 1
|
||||
# v1.46: Add conductor_group to the node object.
|
||||
# v1.47: Add automated_clean to the node object.
|
||||
# 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.
|
||||
|
||||
MINOR_0_JUNO = 0
|
||||
MINOR_1_INITIAL_VERSION = 1
|
||||
@ -137,6 +139,7 @@ MINOR_46_NODE_CONDUCTOR_GROUP = 46
|
||||
MINOR_47_NODE_AUTOMATED_CLEAN = 47
|
||||
MINOR_48_NODE_PROTECTED = 48
|
||||
MINOR_49_CONDUCTORS = 49
|
||||
MINOR_50_NODE_OWNER = 50
|
||||
|
||||
# When adding another version, update:
|
||||
# - MINOR_MAX_VERSION
|
||||
@ -144,7 +147,7 @@ MINOR_49_CONDUCTORS = 49
|
||||
# explanation of what changed in the new version
|
||||
# - common/release_mappings.py, RELEASE_MAPPING['master']['api']
|
||||
|
||||
MINOR_MAX_VERSION = MINOR_49_CONDUCTORS
|
||||
MINOR_MAX_VERSION = MINOR_50_NODE_OWNER
|
||||
|
||||
# String representations of the minor and maximum versions
|
||||
_MIN_VERSION_STRING = '{}.{}'.format(BASE_VERSION, MINOR_1_INITIAL_VERSION)
|
||||
|
@ -131,10 +131,10 @@ RELEASE_MAPPING = {
|
||||
}
|
||||
},
|
||||
'master': {
|
||||
'api': '1.49',
|
||||
'api': '1.50',
|
||||
'rpc': '1.47',
|
||||
'objects': {
|
||||
'Node': ['1.29', '1.28'],
|
||||
'Node': ['1.30', '1.29', '1.28'],
|
||||
'Conductor': ['1.3'],
|
||||
'Chassis': ['1.3'],
|
||||
'Port': ['1.8'],
|
||||
|
@ -0,0 +1,32 @@
|
||||
# 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_node_owner
|
||||
|
||||
Revision ID: f190f9d00a11
|
||||
Revises: 93706939026c
|
||||
Create Date: 2018-11-12 00:33:58.575100
|
||||
|
||||
"""
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = 'f190f9d00a11'
|
||||
down_revision = '93706939026c'
|
||||
|
||||
|
||||
def upgrade():
|
||||
op.add_column('nodes', sa.Column('owner', sa.String(255),
|
||||
nullable=True))
|
@ -224,7 +224,7 @@ class Connection(api.Connection):
|
||||
'chassis_uuid', 'associated', 'reserved',
|
||||
'reserved_by_any_of', 'provisioned_before',
|
||||
'inspection_started_before', 'fault',
|
||||
'conductor_group'}
|
||||
'conductor_group', 'owner'}
|
||||
unsupported_filters = set(filters).difference(supported_filters)
|
||||
if unsupported_filters:
|
||||
msg = _("SqlAlchemy API does not support "
|
||||
@ -232,7 +232,7 @@ class Connection(api.Connection):
|
||||
raise ValueError(msg)
|
||||
for field in ['console_enabled', 'maintenance', 'driver',
|
||||
'resource_class', 'provision_state', 'uuid', 'id',
|
||||
'fault', 'conductor_group']:
|
||||
'fault', 'conductor_group', 'owner']:
|
||||
if field in filters:
|
||||
query = query.filter_by(**{field: filters[field]})
|
||||
if 'chassis_uuid' in filters:
|
||||
|
@ -179,7 +179,7 @@ class Node(Base):
|
||||
protected = Column(Boolean, nullable=False, default=False,
|
||||
server_default=false())
|
||||
protected_reason = Column(Text, nullable=True)
|
||||
|
||||
owner = Column(String(255), nullable=True)
|
||||
bios_interface = Column(String(255), nullable=True)
|
||||
boot_interface = Column(String(255), nullable=True)
|
||||
console_interface = Column(String(255), nullable=True)
|
||||
|
@ -66,7 +66,8 @@ class Node(base.IronicObject, object_base.VersionedObjectDictCompat):
|
||||
# Version 1.27: Add conductor_group field
|
||||
# Version 1.28: Add automated_clean field
|
||||
# Version 1.29: Add protected and protected_reason fields
|
||||
VERSION = '1.29'
|
||||
# Version 1.30: Add owner field
|
||||
VERSION = '1.30'
|
||||
|
||||
dbapi = db_api.get_instance()
|
||||
|
||||
@ -149,6 +150,7 @@ class Node(base.IronicObject, object_base.VersionedObjectDictCompat):
|
||||
'storage_interface': object_fields.StringField(nullable=True),
|
||||
'vendor_interface': object_fields.StringField(nullable=True),
|
||||
'traits': object_fields.ObjectField('TraitList', nullable=True),
|
||||
'owner': object_fields.StringField(nullable=True),
|
||||
}
|
||||
|
||||
def as_dict(self, secure=False):
|
||||
@ -581,6 +583,8 @@ class Node(base.IronicObject, object_base.VersionedObjectDictCompat):
|
||||
should be set to None (or removed).
|
||||
Version 1.29: protected was added. For versions prior to this, it
|
||||
should be set to False (or removed).
|
||||
Version 1.30: owner 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
|
||||
@ -593,7 +597,7 @@ class Node(base.IronicObject, object_base.VersionedObjectDictCompat):
|
||||
# Convert the different fields depending on version
|
||||
fields = [('rescue_interface', 22), ('traits', 23),
|
||||
('bios_interface', 24), ('automated_clean', 28),
|
||||
('protected_reason', 29)]
|
||||
('protected_reason', 29), ('owner', 30)]
|
||||
for name, minor in fields:
|
||||
self._adjust_field_to_version(name, None, target_version,
|
||||
1, minor, remove_unavailable_fields)
|
||||
@ -648,6 +652,7 @@ class NodePayload(notification.NotificationPayloadBase):
|
||||
'rescue_interface': ('node', 'rescue_interface'),
|
||||
'storage_interface': ('node', 'storage_interface'),
|
||||
'vendor_interface': ('node', 'vendor_interface'),
|
||||
'owner': ('node', 'owner'),
|
||||
'power_state': ('node', 'power_state'),
|
||||
'properties': ('node', 'properties'),
|
||||
'protected': ('node', 'protected'),
|
||||
@ -674,7 +679,8 @@ class NodePayload(notification.NotificationPayloadBase):
|
||||
# Version 1.9: Add deploy_step field exposed via API.
|
||||
# Version 1.10: Add conductor_group field exposed via API.
|
||||
# Version 1.11: Add protected and protected_reason fields exposed via API.
|
||||
VERSION = '1.11'
|
||||
# Version 1.12: Add node owner field.
|
||||
VERSION = '1.12'
|
||||
fields = {
|
||||
'clean_step': object_fields.FlexibleDictField(nullable=True),
|
||||
'conductor_group': object_fields.StringField(nullable=True),
|
||||
@ -703,6 +709,7 @@ class NodePayload(notification.NotificationPayloadBase):
|
||||
'storage_interface': object_fields.StringField(nullable=True),
|
||||
'vendor_interface': object_fields.StringField(nullable=True),
|
||||
'name': object_fields.StringField(nullable=True),
|
||||
'owner': object_fields.StringField(nullable=True),
|
||||
'power_state': object_fields.StringField(nullable=True),
|
||||
'properties': object_fields.FlexibleDictField(nullable=True),
|
||||
'protected': object_fields.BooleanField(nullable=True),
|
||||
@ -754,7 +761,8 @@ class NodeSetPowerStatePayload(NodePayload):
|
||||
# Version 1.9: Parent NodePayload version 1.9
|
||||
# Version 1.10: Parent NodePayload version 1.10
|
||||
# Version 1.11: Parent NodePayload version 1.11
|
||||
VERSION = '1.11'
|
||||
# Version 1.12: Parent NodePayload version 1.12
|
||||
VERSION = '1.12'
|
||||
|
||||
fields = {
|
||||
# "to_power" indicates the future target_power_state of the node. A
|
||||
@ -806,7 +814,8 @@ class NodeCorrectedPowerStatePayload(NodePayload):
|
||||
# Version 1.9: Parent NodePayload version 1.9
|
||||
# Version 1.10: Parent NodePayload version 1.10
|
||||
# Version 1.11: Parent NodePayload version 1.11
|
||||
VERSION = '1.11'
|
||||
# Version 1.12: Parent NodePayload version 1.12
|
||||
VERSION = '1.12'
|
||||
|
||||
fields = {
|
||||
'from_power': object_fields.StringField(nullable=True)
|
||||
@ -842,7 +851,8 @@ class NodeSetProvisionStatePayload(NodePayload):
|
||||
# Version 1.9: Parent NodePayload version 1.9
|
||||
# Version 1.10: Parent NodePayload version 1.10
|
||||
# Version 1.11: Parent NodePayload version 1.11
|
||||
VERSION = '1.11'
|
||||
# Version 1.12: Parent NodePayload version 1.12
|
||||
VERSION = '1.12'
|
||||
|
||||
SCHEMA = dict(NodePayload.SCHEMA,
|
||||
**{'instance_info': ('node', 'instance_info')})
|
||||
@ -885,7 +895,8 @@ class NodeCRUDPayload(NodePayload):
|
||||
# Version 1.7: Parent NodePayload version 1.9
|
||||
# Version 1.8: Parent NodePayload version 1.10
|
||||
# Version 1.9: Parent NodePayload version 1.11
|
||||
VERSION = '1.9'
|
||||
# Version 1.10: Parent NodePayload version 1.12
|
||||
VERSION = '1.10'
|
||||
|
||||
SCHEMA = dict(NodePayload.SCHEMA,
|
||||
**{'instance_info': ('node', 'instance_info'),
|
||||
|
@ -132,6 +132,7 @@ class TestListNodes(test_api_base.BaseApiTest):
|
||||
self.assertNotIn('automated_clean', data['nodes'][0])
|
||||
self.assertNotIn('protected', data['nodes'][0])
|
||||
self.assertNotIn('protected_reason', data['nodes'][0])
|
||||
self.assertNotIn('owner', data['nodes'][0])
|
||||
|
||||
def test_get_one(self):
|
||||
node = obj_utils.create_test_node(self.context,
|
||||
@ -173,6 +174,7 @@ class TestListNodes(test_api_base.BaseApiTest):
|
||||
self.assertIn('automated_clean', data)
|
||||
self.assertIn('protected', data)
|
||||
self.assertIn('protected_reason', data)
|
||||
self.assertIn('owner', data)
|
||||
|
||||
def test_get_one_with_json(self):
|
||||
# Test backward compatibility with guess_content_type_from_ext
|
||||
@ -326,6 +328,23 @@ class TestListNodes(test_api_base.BaseApiTest):
|
||||
self.assertTrue(data['protected'])
|
||||
self.assertEqual('reason!', data['protected_reason'])
|
||||
|
||||
def test_node_owner_hidden_in_lower_version(self):
|
||||
self._test_node_field_hidden_in_lower_version('owner',
|
||||
'1.49', '1.50')
|
||||
|
||||
def test_node_owner_null_field(self):
|
||||
node = obj_utils.create_test_node(self.context, owner=None)
|
||||
data = self.get_json('/nodes/%s' % node.uuid,
|
||||
headers={api_base.Version.string: '1.50'})
|
||||
self.assertIsNone(data['owner'])
|
||||
|
||||
def test_node_owner_present(self):
|
||||
node = obj_utils.create_test_node(self.context,
|
||||
owner="akindofmagic")
|
||||
data = self.get_json('/nodes/%s' % node.uuid,
|
||||
headers={api_base.Version.string: '1.50'})
|
||||
self.assertEqual(data['owner'], "akindofmagic")
|
||||
|
||||
def test_get_one_custom_fields(self):
|
||||
node = obj_utils.create_test_node(self.context,
|
||||
chassis_id=self.chassis.id)
|
||||
@ -517,6 +536,13 @@ class TestListNodes(test_api_base.BaseApiTest):
|
||||
headers={api_base.Version.string: '1.49'})
|
||||
self.assertIn('conductor', response)
|
||||
|
||||
def test_get_owner_fields(self):
|
||||
node = obj_utils.create_test_node(self.context, owner='fred')
|
||||
fields = 'owner'
|
||||
response = self.get_json('/nodes/%s?fields=%s' % (node.uuid, fields),
|
||||
headers={api_base.Version.string: '1.50'})
|
||||
self.assertIn('owner', response)
|
||||
|
||||
def test_detail(self):
|
||||
node = obj_utils.create_test_node(self.context,
|
||||
chassis_id=self.chassis.id)
|
||||
@ -550,6 +576,7 @@ class TestListNodes(test_api_base.BaseApiTest):
|
||||
self.assertIn('automated_clean', data['nodes'][0])
|
||||
self.assertIn('protected', data['nodes'][0])
|
||||
self.assertIn('protected_reason', data['nodes'][0])
|
||||
self.assertIn('owner', data['nodes'][0])
|
||||
# never expose the chassis_id
|
||||
self.assertNotIn('chassis_id', data['nodes'][0])
|
||||
|
||||
@ -582,6 +609,7 @@ class TestListNodes(test_api_base.BaseApiTest):
|
||||
self.assertIn('automated_clean', data['nodes'][0])
|
||||
self.assertIn('protected', data['nodes'][0])
|
||||
self.assertIn('protected_reason', data['nodes'][0])
|
||||
self.assertIn('owner', data['nodes'][0])
|
||||
for field in api_utils.V31_FIELDS:
|
||||
self.assertIn(field, data['nodes'][0])
|
||||
# never expose the chassis_id
|
||||
@ -1575,7 +1603,7 @@ class TestListNodes(test_api_base.BaseApiTest):
|
||||
|
||||
def test_get_nodes_by_conductor_not_allowed(self):
|
||||
response = self.get_json('/nodes?conductor=rocky.rocks',
|
||||
headers={api_base.Version.string: "1.47"},
|
||||
headers={api_base.Version.string: "1.48"},
|
||||
expect_errors=True)
|
||||
self.assertEqual('application/json', response.content_type)
|
||||
self.assertEqual(http_client.NOT_ACCEPTABLE, response.status_code)
|
||||
@ -1608,6 +1636,36 @@ class TestListNodes(test_api_base.BaseApiTest):
|
||||
self.assertNotIn(node1.uuid, uuids)
|
||||
self.assertIn(node2.uuid, uuids)
|
||||
|
||||
def test_get_nodes_by_owner(self):
|
||||
node1 = obj_utils.create_test_node(self.context,
|
||||
uuid=uuidutils.generate_uuid(),
|
||||
owner='fred')
|
||||
node2 = obj_utils.create_test_node(self.context,
|
||||
uuid=uuidutils.generate_uuid(),
|
||||
owner='bob')
|
||||
|
||||
for base_url in ('/nodes', '/nodes/detail'):
|
||||
data = self.get_json(base_url + '?owner=fred',
|
||||
headers={api_base.Version.string: "1.50"})
|
||||
uuids = [n['uuid'] for n in data['nodes']]
|
||||
self.assertIn(node1.uuid, uuids)
|
||||
self.assertNotIn(node2.uuid, uuids)
|
||||
data = self.get_json(base_url + '?owner=bob',
|
||||
headers={api_base.Version.string: "1.50"})
|
||||
uuids = [n['uuid'] for n in data['nodes']]
|
||||
self.assertIn(node2.uuid, uuids)
|
||||
self.assertNotIn(node1.uuid, uuids)
|
||||
|
||||
def test_get_nodes_by_owner_not_allowed(self):
|
||||
for url in ('/nodes?owner=fred',
|
||||
'/nodes/detail?owner=fred'):
|
||||
response = self.get_json(
|
||||
url, headers={api_base.Version.string: "1.48"},
|
||||
expect_errors=True)
|
||||
self.assertEqual('application/json', response.content_type)
|
||||
self.assertEqual(http_client.NOT_ACCEPTABLE, response.status_code)
|
||||
self.assertTrue(response.json['error_message'])
|
||||
|
||||
def test_get_console_information(self):
|
||||
node = obj_utils.create_test_node(self.context)
|
||||
expected_console_info = {'test': 'test-data'}
|
||||
@ -2788,6 +2846,19 @@ class TestPatch(test_api_base.BaseApiTest):
|
||||
self.assertEqual('application/json', response.content_type)
|
||||
self.assertEqual(http_client.OK, response.status_code)
|
||||
|
||||
def test_update_owner(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.50'}
|
||||
response = self.patch_json('/nodes/%s' % node.uuid,
|
||||
[{'path': '/owner',
|
||||
'value': 'meow',
|
||||
'op': 'replace'}],
|
||||
headers=headers)
|
||||
self.assertEqual('application/json', response.content_type)
|
||||
self.assertEqual(http_client.OK, response.status_code)
|
||||
|
||||
def test_update_protected_old_api(self):
|
||||
node = obj_utils.create_test_node(self.context,
|
||||
uuid=uuidutils.generate_uuid())
|
||||
@ -2815,6 +2886,20 @@ class TestPatch(test_api_base.BaseApiTest):
|
||||
self.assertEqual(http_client.BAD_REQUEST, response.status_code)
|
||||
self.assertTrue(response.json['error_message'])
|
||||
|
||||
def test_update_owner_old_api(self):
|
||||
node = obj_utils.create_test_node(self.context,
|
||||
uuid=uuidutils.generate_uuid())
|
||||
self.mock_update_node.return_value = node
|
||||
headers = {api_base.Version.string: '1.47'}
|
||||
response = self.patch_json('/nodes/%s' % node.uuid,
|
||||
[{'path': '/owner',
|
||||
'value': 'meow',
|
||||
'op': 'replace'}],
|
||||
headers=headers,
|
||||
expect_errors=True)
|
||||
self.assertEqual('application/json', response.content_type)
|
||||
self.assertEqual(http_client.NOT_ACCEPTABLE, response.status_code)
|
||||
|
||||
|
||||
def _create_node_locally(node):
|
||||
driver_factory.check_and_update_node_interfaces(node)
|
||||
@ -3422,6 +3507,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_owner(self):
|
||||
ndict = test_api_utils.post_get_test_node(owner='cowsay')
|
||||
response = self.post_json('/nodes', ndict,
|
||||
headers={api_base.Version.string:
|
||||
str(api_v1.max_version())})
|
||||
self.assertEqual(http_client.CREATED, response.status_int)
|
||||
result = self.get_json('/nodes/%s' % ndict['uuid'],
|
||||
headers={api_base.Version.string:
|
||||
str(api_v1.max_version())})
|
||||
self.assertEqual('cowsay', result['owner'])
|
||||
|
||||
def test_create_node_owner_old_api_version(self):
|
||||
headers = {api_base.Version.string: '1.32'}
|
||||
ndict = test_api_utils.post_get_test_node(owner='bob')
|
||||
response = self.post_json('/nodes', ndict, headers=headers,
|
||||
expect_errors=True)
|
||||
self.assertEqual('application/json', response.content_type)
|
||||
self.assertEqual(http_client.NOT_ACCEPTABLE, response.status_int)
|
||||
|
||||
|
||||
class TestDelete(test_api_base.BaseApiTest):
|
||||
|
||||
|
@ -786,6 +786,11 @@ class MigrationCheckersMixin(object):
|
||||
self.assertFalse(node['protected'])
|
||||
self.assertIsNone(node['protected_reason'])
|
||||
|
||||
def _check_f190f9d00a11(self, engine, data):
|
||||
nodes = db_utils.get_table(engine, 'nodes')
|
||||
col_names = [column.name for column in nodes.c]
|
||||
self.assertIn('owner', col_names)
|
||||
|
||||
def test_upgrade_and_version(self):
|
||||
with patch_with_engine(self.engine):
|
||||
self.migration_api.upgrade('head')
|
||||
|
@ -218,6 +218,7 @@ def get_test_node(**kw):
|
||||
'protected': kw.get('protected', False),
|
||||
'protected_reason': kw.get('protected_reason', None),
|
||||
'conductor': kw.get('conductor'),
|
||||
'owner': kw.get('owner', None),
|
||||
}
|
||||
|
||||
for iface in drivers_base.ALL_INTERFACES:
|
||||
|
@ -775,9 +775,7 @@ class TestConvertToVersion(db_base.DbTestCase):
|
||||
delattr(node, 'protected')
|
||||
delattr(node, 'protected_reason')
|
||||
node.obj_reset_changes()
|
||||
|
||||
node._convert_to_version("1.29")
|
||||
|
||||
self.assertFalse(node.protected)
|
||||
self.assertIsNone(node.protected_reason)
|
||||
self.assertEqual({'protected': False, 'protected_reason': None},
|
||||
@ -829,6 +827,67 @@ class TestConvertToVersion(db_base.DbTestCase):
|
||||
self.assertEqual({'protected': False, 'protected_reason': None},
|
||||
node.obj_get_changes())
|
||||
|
||||
def test_owner_supported_missing(self):
|
||||
# owner_interface not set, should be set to default.
|
||||
node = obj_utils.get_test_node(self.ctxt, **self.fake_node)
|
||||
delattr(node, 'owner')
|
||||
node.obj_reset_changes()
|
||||
node._convert_to_version("1.30")
|
||||
self.assertIsNone(node.owner)
|
||||
self.assertEqual({'owner': None},
|
||||
node.obj_get_changes())
|
||||
|
||||
def test_owner_supported_set(self):
|
||||
# owner set, no change required.
|
||||
node = obj_utils.get_test_node(self.ctxt, **self.fake_node)
|
||||
|
||||
node.owner = "Sure, there is an owner"
|
||||
node.obj_reset_changes()
|
||||
node._convert_to_version("1.30")
|
||||
self.assertEqual("Sure, there is an owner", node.owner)
|
||||
self.assertEqual({}, node.obj_get_changes())
|
||||
|
||||
def test_owner_unsupported_missing(self):
|
||||
# owner not set, no change required.
|
||||
node = obj_utils.get_test_node(self.ctxt, **self.fake_node)
|
||||
|
||||
delattr(node, 'owner')
|
||||
node.obj_reset_changes()
|
||||
node._convert_to_version("1.29")
|
||||
self.assertNotIn('owner', node)
|
||||
self.assertEqual({}, node.obj_get_changes())
|
||||
|
||||
def test_owner_unsupported_set_remove(self):
|
||||
# owner set, should be removed.
|
||||
node = obj_utils.get_test_node(self.ctxt, **self.fake_node)
|
||||
|
||||
node.owner = "magic"
|
||||
node.obj_reset_changes()
|
||||
node._convert_to_version("1.29")
|
||||
self.assertNotIn('owner', node)
|
||||
self.assertEqual({}, node.obj_get_changes())
|
||||
|
||||
def test_owner_unsupported_set_no_remove_non_default(self):
|
||||
# owner set, should be set to default.
|
||||
node = obj_utils.get_test_node(self.ctxt, **self.fake_node)
|
||||
|
||||
node.owner = "magic"
|
||||
node.obj_reset_changes()
|
||||
node._convert_to_version("1.29", False)
|
||||
self.assertIsNone(node.owner)
|
||||
self.assertEqual({'owner': None},
|
||||
node.obj_get_changes())
|
||||
|
||||
def test_owner_unsupported_set_no_remove_default(self):
|
||||
# owner set, no change required.
|
||||
node = obj_utils.get_test_node(self.ctxt, **self.fake_node)
|
||||
|
||||
node.owner = None
|
||||
node.obj_reset_changes()
|
||||
node._convert_to_version("1.29", False)
|
||||
self.assertIsNone(node.owner)
|
||||
self.assertEqual({}, node.obj_get_changes())
|
||||
|
||||
|
||||
class TestNodePayloads(db_base.DbTestCase):
|
||||
|
||||
@ -886,6 +945,7 @@ class TestNodePayloads(db_base.DbTestCase):
|
||||
self.assertEqual(self.node.traits.get_trait_names(), payload.traits)
|
||||
self.assertEqual(self.node.updated_at, payload.updated_at)
|
||||
self.assertEqual(self.node.uuid, payload.uuid)
|
||||
self.assertEqual(self.node.owner, payload.owner)
|
||||
|
||||
def test_node_payload(self):
|
||||
payload = objects.NodePayload(self.node)
|
||||
|
@ -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.29-7af860bb4017751104558139c52a1327',
|
||||
'Node': '1.30-8313460d6ea5457a527cd3d85e5ee3d8',
|
||||
'MyObj': '1.5-9459d30d6954bffc7a9afd347a807ca6',
|
||||
'Chassis': '1.3-d656e039fd8ae9f34efc232ab3980905',
|
||||
'Port': '1.8-898a47921f4a1f53fcdddd4eeb179e0b',
|
||||
@ -685,21 +685,21 @@ expected_object_fingerprints = {
|
||||
'Conductor': '1.3-d3f53e853b4d58cae5bfbd9a8341af4a',
|
||||
'EventType': '1.1-aa2ba1afd38553e3880c267404e8d370',
|
||||
'NotificationPublisher': '1.0-51a09397d6c0687771fb5be9a999605d',
|
||||
'NodePayload': '1.11-f323602c2e9c3edbf2a5567eca087ff5',
|
||||
'NodePayload': '1.12-7d650c2a024357275990681f020512e4',
|
||||
'NodeSetPowerStateNotification': '1.0-59acc533c11d306f149846f922739c15',
|
||||
'NodeSetPowerStatePayload': '1.11-b61e66ef9d100a2cc564d16b12810855',
|
||||
'NodeSetPowerStatePayload': '1.12-703d110d571cc95b2947bb6bd153fcb8',
|
||||
'NodeCorrectedPowerStateNotification':
|
||||
'1.0-59acc533c11d306f149846f922739c15',
|
||||
'NodeCorrectedPowerStatePayload': '1.11-e6e32a38ca655509802ac3c6d8bc17f6',
|
||||
'NodeCorrectedPowerStatePayload': '1.12-29cbb6b20a0aeea9e0ab9e17302e9e16',
|
||||
'NodeSetProvisionStateNotification':
|
||||
'1.0-59acc533c11d306f149846f922739c15',
|
||||
'NodeSetProvisionStatePayload': '1.11-d13cb3472eea163de5b0723a08e95d2c',
|
||||
'NodeSetProvisionStatePayload': '1.12-a302ce357ad39a0a4d1ca3c0ee44f0e0',
|
||||
'VolumeConnector': '1.0-3e0252c0ab6e6b9d158d09238a577d97',
|
||||
'VolumeTarget': '1.0-0b10d663d8dae675900b2c7548f76f5e',
|
||||
'ChassisCRUDNotification': '1.0-59acc533c11d306f149846f922739c15',
|
||||
'ChassisCRUDPayload': '1.0-dce63895d8186279a7dd577cffccb202',
|
||||
'NodeCRUDNotification': '1.0-59acc533c11d306f149846f922739c15',
|
||||
'NodeCRUDPayload': '1.9-c5e57432274371f7fe32f269519033cf',
|
||||
'NodeCRUDPayload': '1.10-49590dee863c5ed1193f5deae0a0a2f2',
|
||||
'PortCRUDNotification': '1.0-59acc533c11d306f149846f922739c15',
|
||||
'PortCRUDPayload': '1.2-233d259df442eb15cc584fae1fe81504',
|
||||
'NodeMaintenanceNotification': '1.0-59acc533c11d306f149846f922739c15',
|
||||
|
@ -0,0 +1,7 @@
|
||||
---
|
||||
features:
|
||||
- |
|
||||
Adds API version 1.50 which allows for the storage of an ``owner`` field
|
||||
on node objects. This is intended for either storage of human parsable
|
||||
information or the storage of a tenant UUID which could be leveraged
|
||||
in a future version of the Bare Metal as a Service API.
|
Loading…
Reference in New Issue
Block a user