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
|
.. versionadded:: 1.46
|
||||||
Introduced the ``conductor_group`` field.
|
Introduced the ``conductor_group`` field.
|
||||||
|
|
||||||
|
.. versionadded: 1.50
|
||||||
|
Introduced the ``owner`` field.
|
||||||
|
|
||||||
Normal response codes: 201
|
Normal response codes: 201
|
||||||
|
|
||||||
Error codes: 400,403,406
|
Error codes: 400,403,406
|
||||||
@ -120,6 +123,7 @@ Request
|
|||||||
- storage_interface: req_storage_interface
|
- storage_interface: req_storage_interface
|
||||||
- uuid: req_uuid
|
- uuid: req_uuid
|
||||||
- vendor_interface: req_vendor_interface
|
- vendor_interface: req_vendor_interface
|
||||||
|
- owner: owner
|
||||||
|
|
||||||
**Example Node creation request with a dynamic driver:**
|
**Example Node creation request with a dynamic driver:**
|
||||||
|
|
||||||
@ -188,6 +192,7 @@ microversion 1.48.
|
|||||||
- conductor_group: conductor_group
|
- conductor_group: conductor_group
|
||||||
- protected: protected
|
- protected: protected
|
||||||
- protected_reason: protected_reason
|
- protected_reason: protected_reason
|
||||||
|
- owner: owner
|
||||||
|
|
||||||
**Example JSON representation of a Node:**
|
**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
|
Introduced the ``conductor_group`` request parameter, to allow filtering the
|
||||||
list of returned nodes by conductor group.
|
list of returned nodes by conductor group.
|
||||||
|
|
||||||
|
.. versionadded: 1.50
|
||||||
|
Introduced the ``owner`` field.
|
||||||
|
|
||||||
Normal response codes: 200
|
Normal response codes: 200
|
||||||
|
|
||||||
Error codes: 400,403,406
|
Error codes: 400,403,406
|
||||||
@ -258,6 +266,7 @@ Request
|
|||||||
- sort_dir: sort_dir
|
- sort_dir: sort_dir
|
||||||
- sort_key: sort_key
|
- sort_key: sort_key
|
||||||
- detail: detail
|
- detail: detail
|
||||||
|
- owner: owner
|
||||||
|
|
||||||
Response
|
Response
|
||||||
--------
|
--------
|
||||||
@ -307,6 +316,9 @@ Nova instance, eg. with a request to ``v1/nodes/detail?instance_uuid={NOVA INSTA
|
|||||||
.. versionadded:: 1.48
|
.. versionadded:: 1.48
|
||||||
Introduced the ``protected`` and ``protected_reason`` fields.
|
Introduced the ``protected`` and ``protected_reason`` fields.
|
||||||
|
|
||||||
|
.. versionadded: 1.50
|
||||||
|
Introduced the ``owner`` field.
|
||||||
|
|
||||||
Normal response codes: 200
|
Normal response codes: 200
|
||||||
|
|
||||||
Error codes: 400,403,406
|
Error codes: 400,403,406
|
||||||
@ -324,6 +336,7 @@ Request
|
|||||||
- driver: r_driver
|
- driver: r_driver
|
||||||
- resource_class: r_resource_class
|
- resource_class: r_resource_class
|
||||||
- conductor_group: r_conductor_group
|
- conductor_group: r_conductor_group
|
||||||
|
- owner: owner
|
||||||
- limit: limit
|
- limit: limit
|
||||||
- marker: marker
|
- marker: marker
|
||||||
- sort_dir: sort_dir
|
- sort_dir: sort_dir
|
||||||
|
@ -856,6 +856,12 @@ nodes:
|
|||||||
in: body
|
in: body
|
||||||
required: true
|
required: true
|
||||||
type: array
|
type: array
|
||||||
|
owner:
|
||||||
|
description: |
|
||||||
|
A string or UUID of the tenant who owns the baremetal node.
|
||||||
|
in: body
|
||||||
|
required: false
|
||||||
|
type: string
|
||||||
passthru_async:
|
passthru_async:
|
||||||
description: |
|
description: |
|
||||||
If True the passthru function is invoked asynchronously; if False,
|
If True the passthru function is invoked asynchronously; if False,
|
||||||
|
@ -36,6 +36,7 @@
|
|||||||
"management_interface": null,
|
"management_interface": null,
|
||||||
"name": "test_node_classic",
|
"name": "test_node_classic",
|
||||||
"network_interface": "flat",
|
"network_interface": "flat",
|
||||||
|
"owner": null,
|
||||||
"portgroups": [
|
"portgroups": [
|
||||||
{
|
{
|
||||||
"href": "http://127.0.0.1:6385/v1/nodes/6d85703a-565d-469a-96ce-30b6de53079d/portgroups",
|
"href": "http://127.0.0.1:6385/v1/nodes/6d85703a-565d-469a-96ce-30b6de53079d/portgroups",
|
||||||
|
@ -38,6 +38,7 @@
|
|||||||
"management_interface": null,
|
"management_interface": null,
|
||||||
"name": "test_node_classic",
|
"name": "test_node_classic",
|
||||||
"network_interface": "flat",
|
"network_interface": "flat",
|
||||||
|
"owner": null,
|
||||||
"portgroups": [
|
"portgroups": [
|
||||||
{
|
{
|
||||||
"href": "http://127.0.0.1:6385/v1/nodes/6d85703a-565d-469a-96ce-30b6de53079d/portgroups",
|
"href": "http://127.0.0.1:6385/v1/nodes/6d85703a-565d-469a-96ce-30b6de53079d/portgroups",
|
||||||
|
@ -40,6 +40,7 @@
|
|||||||
"management_interface": null,
|
"management_interface": null,
|
||||||
"name": "test_node_classic",
|
"name": "test_node_classic",
|
||||||
"network_interface": "flat",
|
"network_interface": "flat",
|
||||||
|
"owner": null,
|
||||||
"portgroups": [
|
"portgroups": [
|
||||||
{
|
{
|
||||||
"href": "http://127.0.0.1:6385/v1/nodes/6d85703a-565d-469a-96ce-30b6de53079d/portgroups",
|
"href": "http://127.0.0.1:6385/v1/nodes/6d85703a-565d-469a-96ce-30b6de53079d/portgroups",
|
||||||
|
@ -40,6 +40,7 @@
|
|||||||
"management_interface": null,
|
"management_interface": null,
|
||||||
"name": "test_node_classic",
|
"name": "test_node_classic",
|
||||||
"network_interface": "flat",
|
"network_interface": "flat",
|
||||||
|
"owner": "john doe",
|
||||||
"portgroups": [
|
"portgroups": [
|
||||||
{
|
{
|
||||||
"href": "http://127.0.0.1:6385/v1/nodes/6d85703a-565d-469a-96ce-30b6de53079d/portgroups",
|
"href": "http://127.0.0.1:6385/v1/nodes/6d85703a-565d-469a-96ce-30b6de53079d/portgroups",
|
||||||
@ -139,6 +140,7 @@
|
|||||||
"management_interface": "ipmitool",
|
"management_interface": "ipmitool",
|
||||||
"name": "test_node_dynamic",
|
"name": "test_node_dynamic",
|
||||||
"network_interface": "flat",
|
"network_interface": "flat",
|
||||||
|
"owner": "43e61ec9-8e42-4dcb-bc45-30d66aa93e5b",
|
||||||
"portgroups": [
|
"portgroups": [
|
||||||
{
|
{
|
||||||
"href": "http://127.0.0.1:6385/v1/nodes/2b045129-a906-46af-bc1a-092b294b3428/portgroups",
|
"href": "http://127.0.0.1:6385/v1/nodes/2b045129-a906-46af-bc1a-092b294b3428/portgroups",
|
||||||
|
@ -2,6 +2,14 @@
|
|||||||
REST API Version History
|
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)
|
1.49 (Stein, master)
|
||||||
--------------------
|
--------------------
|
||||||
|
|
||||||
|
@ -1075,6 +1075,9 @@ class Node(base.APIBase):
|
|||||||
conductor = wsme.wsattr(wtypes.text, readonly=True)
|
conductor = wsme.wsattr(wtypes.text, readonly=True)
|
||||||
"""Represent the conductor currently serving the node"""
|
"""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
|
# NOTE(deva): "conductor_affinity" shouldn't be presented on the
|
||||||
# API because it's an internal value. Don't add it here.
|
# 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,
|
storage_interface=None, traits=[], rescue_interface=None,
|
||||||
bios_interface=None, conductor_group="",
|
bios_interface=None, conductor_group="",
|
||||||
automated_clean=None, protected=False,
|
automated_clean=None, protected=False,
|
||||||
protected_reason=None)
|
protected_reason=None, owner=None)
|
||||||
# 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'
|
||||||
@ -1589,35 +1592,12 @@ class NodesController(rest.RestController):
|
|||||||
filtered_nodes.append(n)
|
filtered_nodes.append(n)
|
||||||
return filtered_nodes
|
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,
|
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,
|
resource_class=None, resource_url=None,
|
||||||
fields=None, fault=None, conductor_group=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:
|
if self.from_chassis and not chassis_uuid:
|
||||||
raise exception.MissingParameterValue(
|
raise exception.MissingParameterValue(
|
||||||
_("Chassis id not specified."))
|
_("Chassis id not specified."))
|
||||||
@ -1650,10 +1630,22 @@ class NodesController(rest.RestController):
|
|||||||
# be generated, which we don't want.
|
# be generated, which we don't want.
|
||||||
limit = 0
|
limit = 0
|
||||||
else:
|
else:
|
||||||
filters = self._create_node_filters(chassis_uuid, associated,
|
possible_filters = {
|
||||||
maintenance, provision_state,
|
'maintenance': maintenance,
|
||||||
driver, resource_class, fault,
|
'chassis_uuid': chassis_uuid,
|
||||||
conductor_group)
|
'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,
|
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,
|
||||||
filters=filters)
|
filters=filters)
|
||||||
@ -1764,12 +1756,14 @@ class NodesController(rest.RestController):
|
|||||||
@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, 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,
|
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, resource_class=None, fault=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.
|
"""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
|
||||||
@ -1799,6 +1793,8 @@ class NodesController(rest.RestController):
|
|||||||
that conductor_group.
|
that conductor_group.
|
||||||
:param conductor: Optional string value to get only nodes managed by
|
:param conductor: Optional string value to get only nodes managed by
|
||||||
that conductor.
|
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
|
:param fields: Optional, a list with a specified set of fields
|
||||||
of the resource to be returned.
|
of the resource to be returned.
|
||||||
:param fault: Optional string value to get only nodes with that fault.
|
: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_fault(fault)
|
||||||
api_utils.check_allow_filter_by_conductor_group(conductor_group)
|
api_utils.check_allow_filter_by_conductor_group(conductor_group)
|
||||||
api_utils.check_allow_filter_by_conductor(conductor)
|
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,
|
fields = api_utils.get_request_return_fields(fields, detail,
|
||||||
_DEFAULT_RETURN_FIELDS)
|
_DEFAULT_RETURN_FIELDS)
|
||||||
@ -1828,18 +1825,19 @@ class NodesController(rest.RestController):
|
|||||||
fields=fields, fault=fault,
|
fields=fields, fault=fault,
|
||||||
conductor_group=conductor_group,
|
conductor_group=conductor_group,
|
||||||
detail=detail,
|
detail=detail,
|
||||||
conductor=conductor)
|
conductor=conductor,
|
||||||
|
owner=owner)
|
||||||
|
|
||||||
@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, 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,
|
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, fault=None, conductor_group=None,
|
resource_class=None, fault=None, conductor_group=None,
|
||||||
conductor=None):
|
conductor=None, owner=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
|
||||||
@ -1868,6 +1866,8 @@ class NodesController(rest.RestController):
|
|||||||
:param fault: Optional string value to get only nodes with that fault.
|
:param fault: Optional string value to get only nodes with that fault.
|
||||||
:param conductor_group: Optional string value to get only nodes with
|
:param conductor_group: Optional string value to get only nodes with
|
||||||
that conductor_group.
|
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()
|
cdict = pecan.request.context.to_policy_values()
|
||||||
policy.authorize('baremetal:node:get', cdict, cdict)
|
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_specify_resource_class(resource_class)
|
||||||
api_utils.check_allow_filter_by_fault(fault)
|
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_group(conductor_group)
|
||||||
|
api_utils.check_allow_filter_by_owner(owner)
|
||||||
api_utils.check_allowed_fields([sort_key])
|
api_utils.check_allowed_fields([sort_key])
|
||||||
# /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]
|
||||||
@ -1895,7 +1896,8 @@ class NodesController(rest.RestController):
|
|||||||
resource_url=resource_url,
|
resource_url=resource_url,
|
||||||
fault=fault,
|
fault=fault,
|
||||||
conductor_group=conductor_group,
|
conductor_group=conductor_group,
|
||||||
conductor=conductor)
|
conductor=conductor,
|
||||||
|
owner=owner)
|
||||||
|
|
||||||
@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)
|
||||||
|
@ -379,6 +379,7 @@ VERSIONED_FIELDS = {
|
|||||||
'protected': versions.MINOR_48_NODE_PROTECTED,
|
'protected': versions.MINOR_48_NODE_PROTECTED,
|
||||||
'protected_reason': versions.MINOR_48_NODE_PROTECTED,
|
'protected_reason': versions.MINOR_48_NODE_PROTECTED,
|
||||||
'conductor': versions.MINOR_49_CONDUCTORS,
|
'conductor': versions.MINOR_49_CONDUCTORS,
|
||||||
|
'owner': versions.MINOR_50_NODE_OWNER,
|
||||||
}
|
}
|
||||||
|
|
||||||
for field in V31_FIELDS:
|
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})
|
'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():
|
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.
|
||||||
|
|
||||||
@ -930,7 +945,7 @@ def get_request_return_fields(fields, detail, default_fields):
|
|||||||
def allow_expose_conductors():
|
def allow_expose_conductors():
|
||||||
"""Check if accessing conductor endpoints is allowed.
|
"""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.
|
for the node.
|
||||||
"""
|
"""
|
||||||
return pecan.request.version.minor >= versions.MINOR_49_CONDUCTORS
|
return pecan.request.version.minor >= versions.MINOR_49_CONDUCTORS
|
||||||
@ -939,7 +954,7 @@ def allow_expose_conductors():
|
|||||||
def check_allow_filter_by_conductor(conductor):
|
def check_allow_filter_by_conductor(conductor):
|
||||||
"""Check if filtering nodes by conductor is allowed.
|
"""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():
|
if conductor is not None and not allow_expose_conductors():
|
||||||
raise exception.NotAcceptable(_(
|
raise exception.NotAcceptable(_(
|
||||||
|
@ -86,6 +86,8 @@ BASE_VERSION = 1
|
|||||||
# v1.46: Add conductor_group to the node object.
|
# v1.46: Add conductor_group to the node object.
|
||||||
# v1.47: Add automated_clean to the node object.
|
# v1.47: Add automated_clean to the node object.
|
||||||
# v1.48: Add protected 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_0_JUNO = 0
|
||||||
MINOR_1_INITIAL_VERSION = 1
|
MINOR_1_INITIAL_VERSION = 1
|
||||||
@ -137,6 +139,7 @@ MINOR_46_NODE_CONDUCTOR_GROUP = 46
|
|||||||
MINOR_47_NODE_AUTOMATED_CLEAN = 47
|
MINOR_47_NODE_AUTOMATED_CLEAN = 47
|
||||||
MINOR_48_NODE_PROTECTED = 48
|
MINOR_48_NODE_PROTECTED = 48
|
||||||
MINOR_49_CONDUCTORS = 49
|
MINOR_49_CONDUCTORS = 49
|
||||||
|
MINOR_50_NODE_OWNER = 50
|
||||||
|
|
||||||
# When adding another version, update:
|
# When adding another version, update:
|
||||||
# - MINOR_MAX_VERSION
|
# - MINOR_MAX_VERSION
|
||||||
@ -144,7 +147,7 @@ MINOR_49_CONDUCTORS = 49
|
|||||||
# explanation of what changed in the new version
|
# explanation of what changed in the new version
|
||||||
# - common/release_mappings.py, RELEASE_MAPPING['master']['api']
|
# - 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
|
# 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)
|
||||||
|
@ -131,10 +131,10 @@ RELEASE_MAPPING = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
'master': {
|
'master': {
|
||||||
'api': '1.49',
|
'api': '1.50',
|
||||||
'rpc': '1.47',
|
'rpc': '1.47',
|
||||||
'objects': {
|
'objects': {
|
||||||
'Node': ['1.29', '1.28'],
|
'Node': ['1.30', '1.29', '1.28'],
|
||||||
'Conductor': ['1.3'],
|
'Conductor': ['1.3'],
|
||||||
'Chassis': ['1.3'],
|
'Chassis': ['1.3'],
|
||||||
'Port': ['1.8'],
|
'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',
|
'chassis_uuid', 'associated', 'reserved',
|
||||||
'reserved_by_any_of', 'provisioned_before',
|
'reserved_by_any_of', 'provisioned_before',
|
||||||
'inspection_started_before', 'fault',
|
'inspection_started_before', 'fault',
|
||||||
'conductor_group'}
|
'conductor_group', 'owner'}
|
||||||
unsupported_filters = set(filters).difference(supported_filters)
|
unsupported_filters = set(filters).difference(supported_filters)
|
||||||
if unsupported_filters:
|
if unsupported_filters:
|
||||||
msg = _("SqlAlchemy API does not support "
|
msg = _("SqlAlchemy API does not support "
|
||||||
@ -232,7 +232,7 @@ class Connection(api.Connection):
|
|||||||
raise ValueError(msg)
|
raise ValueError(msg)
|
||||||
for field in ['console_enabled', 'maintenance', 'driver',
|
for field in ['console_enabled', 'maintenance', 'driver',
|
||||||
'resource_class', 'provision_state', 'uuid', 'id',
|
'resource_class', 'provision_state', 'uuid', 'id',
|
||||||
'fault', 'conductor_group']:
|
'fault', 'conductor_group', 'owner']:
|
||||||
if field in filters:
|
if field in filters:
|
||||||
query = query.filter_by(**{field: filters[field]})
|
query = query.filter_by(**{field: filters[field]})
|
||||||
if 'chassis_uuid' in filters:
|
if 'chassis_uuid' in filters:
|
||||||
|
@ -179,7 +179,7 @@ class Node(Base):
|
|||||||
protected = Column(Boolean, nullable=False, default=False,
|
protected = Column(Boolean, nullable=False, default=False,
|
||||||
server_default=false())
|
server_default=false())
|
||||||
protected_reason = Column(Text, nullable=True)
|
protected_reason = Column(Text, nullable=True)
|
||||||
|
owner = Column(String(255), nullable=True)
|
||||||
bios_interface = Column(String(255), nullable=True)
|
bios_interface = Column(String(255), nullable=True)
|
||||||
boot_interface = Column(String(255), nullable=True)
|
boot_interface = Column(String(255), nullable=True)
|
||||||
console_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.27: Add conductor_group field
|
||||||
# Version 1.28: Add automated_clean field
|
# Version 1.28: Add automated_clean field
|
||||||
# Version 1.29: Add protected and protected_reason fields
|
# 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()
|
dbapi = db_api.get_instance()
|
||||||
|
|
||||||
@ -149,6 +150,7 @@ class Node(base.IronicObject, object_base.VersionedObjectDictCompat):
|
|||||||
'storage_interface': object_fields.StringField(nullable=True),
|
'storage_interface': object_fields.StringField(nullable=True),
|
||||||
'vendor_interface': object_fields.StringField(nullable=True),
|
'vendor_interface': object_fields.StringField(nullable=True),
|
||||||
'traits': object_fields.ObjectField('TraitList', nullable=True),
|
'traits': object_fields.ObjectField('TraitList', nullable=True),
|
||||||
|
'owner': object_fields.StringField(nullable=True),
|
||||||
}
|
}
|
||||||
|
|
||||||
def as_dict(self, secure=False):
|
def as_dict(self, secure=False):
|
||||||
@ -581,6 +583,8 @@ class Node(base.IronicObject, object_base.VersionedObjectDictCompat):
|
|||||||
should be set to None (or removed).
|
should be set to None (or removed).
|
||||||
Version 1.29: protected was added. For versions prior to this, it
|
Version 1.29: protected was added. For versions prior to this, it
|
||||||
should be set to False (or removed).
|
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 target_version: the desired version of the object
|
||||||
:param remove_unavailable_fields: True to remove fields that are
|
: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
|
# Convert the different fields depending on version
|
||||||
fields = [('rescue_interface', 22), ('traits', 23),
|
fields = [('rescue_interface', 22), ('traits', 23),
|
||||||
('bios_interface', 24), ('automated_clean', 28),
|
('bios_interface', 24), ('automated_clean', 28),
|
||||||
('protected_reason', 29)]
|
('protected_reason', 29), ('owner', 30)]
|
||||||
for name, minor in fields:
|
for name, minor in fields:
|
||||||
self._adjust_field_to_version(name, None, target_version,
|
self._adjust_field_to_version(name, None, target_version,
|
||||||
1, minor, remove_unavailable_fields)
|
1, minor, remove_unavailable_fields)
|
||||||
@ -648,6 +652,7 @@ class NodePayload(notification.NotificationPayloadBase):
|
|||||||
'rescue_interface': ('node', 'rescue_interface'),
|
'rescue_interface': ('node', 'rescue_interface'),
|
||||||
'storage_interface': ('node', 'storage_interface'),
|
'storage_interface': ('node', 'storage_interface'),
|
||||||
'vendor_interface': ('node', 'vendor_interface'),
|
'vendor_interface': ('node', 'vendor_interface'),
|
||||||
|
'owner': ('node', 'owner'),
|
||||||
'power_state': ('node', 'power_state'),
|
'power_state': ('node', 'power_state'),
|
||||||
'properties': ('node', 'properties'),
|
'properties': ('node', 'properties'),
|
||||||
'protected': ('node', 'protected'),
|
'protected': ('node', 'protected'),
|
||||||
@ -674,7 +679,8 @@ class NodePayload(notification.NotificationPayloadBase):
|
|||||||
# Version 1.9: Add deploy_step field exposed via API.
|
# Version 1.9: Add deploy_step field exposed via API.
|
||||||
# Version 1.10: Add conductor_group 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: Add protected and protected_reason fields exposed via API.
|
||||||
VERSION = '1.11'
|
# Version 1.12: Add node owner field.
|
||||||
|
VERSION = '1.12'
|
||||||
fields = {
|
fields = {
|
||||||
'clean_step': object_fields.FlexibleDictField(nullable=True),
|
'clean_step': object_fields.FlexibleDictField(nullable=True),
|
||||||
'conductor_group': object_fields.StringField(nullable=True),
|
'conductor_group': object_fields.StringField(nullable=True),
|
||||||
@ -703,6 +709,7 @@ class NodePayload(notification.NotificationPayloadBase):
|
|||||||
'storage_interface': object_fields.StringField(nullable=True),
|
'storage_interface': object_fields.StringField(nullable=True),
|
||||||
'vendor_interface': object_fields.StringField(nullable=True),
|
'vendor_interface': object_fields.StringField(nullable=True),
|
||||||
'name': object_fields.StringField(nullable=True),
|
'name': object_fields.StringField(nullable=True),
|
||||||
|
'owner': object_fields.StringField(nullable=True),
|
||||||
'power_state': object_fields.StringField(nullable=True),
|
'power_state': object_fields.StringField(nullable=True),
|
||||||
'properties': object_fields.FlexibleDictField(nullable=True),
|
'properties': object_fields.FlexibleDictField(nullable=True),
|
||||||
'protected': object_fields.BooleanField(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.9: Parent NodePayload version 1.9
|
||||||
# Version 1.10: Parent NodePayload version 1.10
|
# Version 1.10: Parent NodePayload version 1.10
|
||||||
# Version 1.11: Parent NodePayload version 1.11
|
# Version 1.11: Parent NodePayload version 1.11
|
||||||
VERSION = '1.11'
|
# Version 1.12: Parent NodePayload version 1.12
|
||||||
|
VERSION = '1.12'
|
||||||
|
|
||||||
fields = {
|
fields = {
|
||||||
# "to_power" indicates the future target_power_state of the node. A
|
# "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.9: Parent NodePayload version 1.9
|
||||||
# Version 1.10: Parent NodePayload version 1.10
|
# Version 1.10: Parent NodePayload version 1.10
|
||||||
# Version 1.11: Parent NodePayload version 1.11
|
# Version 1.11: Parent NodePayload version 1.11
|
||||||
VERSION = '1.11'
|
# Version 1.12: Parent NodePayload version 1.12
|
||||||
|
VERSION = '1.12'
|
||||||
|
|
||||||
fields = {
|
fields = {
|
||||||
'from_power': object_fields.StringField(nullable=True)
|
'from_power': object_fields.StringField(nullable=True)
|
||||||
@ -842,7 +851,8 @@ class NodeSetProvisionStatePayload(NodePayload):
|
|||||||
# Version 1.9: Parent NodePayload version 1.9
|
# Version 1.9: Parent NodePayload version 1.9
|
||||||
# Version 1.10: Parent NodePayload version 1.10
|
# Version 1.10: Parent NodePayload version 1.10
|
||||||
# Version 1.11: Parent NodePayload version 1.11
|
# 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,
|
SCHEMA = dict(NodePayload.SCHEMA,
|
||||||
**{'instance_info': ('node', 'instance_info')})
|
**{'instance_info': ('node', 'instance_info')})
|
||||||
@ -885,7 +895,8 @@ class NodeCRUDPayload(NodePayload):
|
|||||||
# Version 1.7: Parent NodePayload version 1.9
|
# Version 1.7: Parent NodePayload version 1.9
|
||||||
# Version 1.8: Parent NodePayload version 1.10
|
# Version 1.8: Parent NodePayload version 1.10
|
||||||
# Version 1.9: Parent NodePayload version 1.11
|
# 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,
|
SCHEMA = dict(NodePayload.SCHEMA,
|
||||||
**{'instance_info': ('node', 'instance_info'),
|
**{'instance_info': ('node', 'instance_info'),
|
||||||
|
@ -132,6 +132,7 @@ class TestListNodes(test_api_base.BaseApiTest):
|
|||||||
self.assertNotIn('automated_clean', data['nodes'][0])
|
self.assertNotIn('automated_clean', data['nodes'][0])
|
||||||
self.assertNotIn('protected', data['nodes'][0])
|
self.assertNotIn('protected', data['nodes'][0])
|
||||||
self.assertNotIn('protected_reason', data['nodes'][0])
|
self.assertNotIn('protected_reason', data['nodes'][0])
|
||||||
|
self.assertNotIn('owner', data['nodes'][0])
|
||||||
|
|
||||||
def test_get_one(self):
|
def test_get_one(self):
|
||||||
node = obj_utils.create_test_node(self.context,
|
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('automated_clean', data)
|
||||||
self.assertIn('protected', data)
|
self.assertIn('protected', data)
|
||||||
self.assertIn('protected_reason', data)
|
self.assertIn('protected_reason', data)
|
||||||
|
self.assertIn('owner', data)
|
||||||
|
|
||||||
def test_get_one_with_json(self):
|
def test_get_one_with_json(self):
|
||||||
# Test backward compatibility with guess_content_type_from_ext
|
# Test backward compatibility with guess_content_type_from_ext
|
||||||
@ -326,6 +328,23 @@ class TestListNodes(test_api_base.BaseApiTest):
|
|||||||
self.assertTrue(data['protected'])
|
self.assertTrue(data['protected'])
|
||||||
self.assertEqual('reason!', data['protected_reason'])
|
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):
|
def test_get_one_custom_fields(self):
|
||||||
node = obj_utils.create_test_node(self.context,
|
node = obj_utils.create_test_node(self.context,
|
||||||
chassis_id=self.chassis.id)
|
chassis_id=self.chassis.id)
|
||||||
@ -517,6 +536,13 @@ class TestListNodes(test_api_base.BaseApiTest):
|
|||||||
headers={api_base.Version.string: '1.49'})
|
headers={api_base.Version.string: '1.49'})
|
||||||
self.assertIn('conductor', response)
|
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):
|
def test_detail(self):
|
||||||
node = obj_utils.create_test_node(self.context,
|
node = obj_utils.create_test_node(self.context,
|
||||||
chassis_id=self.chassis.id)
|
chassis_id=self.chassis.id)
|
||||||
@ -550,6 +576,7 @@ class TestListNodes(test_api_base.BaseApiTest):
|
|||||||
self.assertIn('automated_clean', data['nodes'][0])
|
self.assertIn('automated_clean', data['nodes'][0])
|
||||||
self.assertIn('protected', data['nodes'][0])
|
self.assertIn('protected', data['nodes'][0])
|
||||||
self.assertIn('protected_reason', data['nodes'][0])
|
self.assertIn('protected_reason', data['nodes'][0])
|
||||||
|
self.assertIn('owner', 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])
|
||||||
|
|
||||||
@ -582,6 +609,7 @@ class TestListNodes(test_api_base.BaseApiTest):
|
|||||||
self.assertIn('automated_clean', data['nodes'][0])
|
self.assertIn('automated_clean', data['nodes'][0])
|
||||||
self.assertIn('protected', data['nodes'][0])
|
self.assertIn('protected', data['nodes'][0])
|
||||||
self.assertIn('protected_reason', data['nodes'][0])
|
self.assertIn('protected_reason', data['nodes'][0])
|
||||||
|
self.assertIn('owner', data['nodes'][0])
|
||||||
for field in api_utils.V31_FIELDS:
|
for field in api_utils.V31_FIELDS:
|
||||||
self.assertIn(field, data['nodes'][0])
|
self.assertIn(field, data['nodes'][0])
|
||||||
# never expose the chassis_id
|
# never expose the chassis_id
|
||||||
@ -1575,7 +1603,7 @@ class TestListNodes(test_api_base.BaseApiTest):
|
|||||||
|
|
||||||
def test_get_nodes_by_conductor_not_allowed(self):
|
def test_get_nodes_by_conductor_not_allowed(self):
|
||||||
response = self.get_json('/nodes?conductor=rocky.rocks',
|
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)
|
expect_errors=True)
|
||||||
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)
|
||||||
@ -1608,6 +1636,36 @@ class TestListNodes(test_api_base.BaseApiTest):
|
|||||||
self.assertNotIn(node1.uuid, uuids)
|
self.assertNotIn(node1.uuid, uuids)
|
||||||
self.assertIn(node2.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):
|
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'}
|
||||||
@ -2783,6 +2841,19 @@ class TestPatch(test_api_base.BaseApiTest):
|
|||||||
response = self.patch_json('/nodes/%s' % node.uuid,
|
response = self.patch_json('/nodes/%s' % node.uuid,
|
||||||
[{'path': '/protected_reason',
|
[{'path': '/protected_reason',
|
||||||
'value': 'reason!',
|
'value': 'reason!',
|
||||||
|
'op': 'replace'}],
|
||||||
|
headers=headers)
|
||||||
|
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'}],
|
'op': 'replace'}],
|
||||||
headers=headers)
|
headers=headers)
|
||||||
self.assertEqual('application/json', response.content_type)
|
self.assertEqual('application/json', response.content_type)
|
||||||
@ -2815,6 +2886,20 @@ class TestPatch(test_api_base.BaseApiTest):
|
|||||||
self.assertEqual(http_client.BAD_REQUEST, response.status_code)
|
self.assertEqual(http_client.BAD_REQUEST, response.status_code)
|
||||||
self.assertTrue(response.json['error_message'])
|
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):
|
def _create_node_locally(node):
|
||||||
driver_factory.check_and_update_node_interfaces(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('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_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):
|
class TestDelete(test_api_base.BaseApiTest):
|
||||||
|
|
||||||
|
@ -786,6 +786,11 @@ class MigrationCheckersMixin(object):
|
|||||||
self.assertFalse(node['protected'])
|
self.assertFalse(node['protected'])
|
||||||
self.assertIsNone(node['protected_reason'])
|
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):
|
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')
|
||||||
|
@ -218,6 +218,7 @@ def get_test_node(**kw):
|
|||||||
'protected': kw.get('protected', False),
|
'protected': kw.get('protected', False),
|
||||||
'protected_reason': kw.get('protected_reason', None),
|
'protected_reason': kw.get('protected_reason', None),
|
||||||
'conductor': kw.get('conductor'),
|
'conductor': kw.get('conductor'),
|
||||||
|
'owner': kw.get('owner', None),
|
||||||
}
|
}
|
||||||
|
|
||||||
for iface in drivers_base.ALL_INTERFACES:
|
for iface in drivers_base.ALL_INTERFACES:
|
||||||
|
@ -775,9 +775,7 @@ class TestConvertToVersion(db_base.DbTestCase):
|
|||||||
delattr(node, 'protected')
|
delattr(node, 'protected')
|
||||||
delattr(node, 'protected_reason')
|
delattr(node, 'protected_reason')
|
||||||
node.obj_reset_changes()
|
node.obj_reset_changes()
|
||||||
|
|
||||||
node._convert_to_version("1.29")
|
node._convert_to_version("1.29")
|
||||||
|
|
||||||
self.assertFalse(node.protected)
|
self.assertFalse(node.protected)
|
||||||
self.assertIsNone(node.protected_reason)
|
self.assertIsNone(node.protected_reason)
|
||||||
self.assertEqual({'protected': False, 'protected_reason': None},
|
self.assertEqual({'protected': False, 'protected_reason': None},
|
||||||
@ -829,6 +827,67 @@ class TestConvertToVersion(db_base.DbTestCase):
|
|||||||
self.assertEqual({'protected': False, 'protected_reason': None},
|
self.assertEqual({'protected': False, 'protected_reason': None},
|
||||||
node.obj_get_changes())
|
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):
|
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.traits.get_trait_names(), payload.traits)
|
||||||
self.assertEqual(self.node.updated_at, payload.updated_at)
|
self.assertEqual(self.node.updated_at, payload.updated_at)
|
||||||
self.assertEqual(self.node.uuid, payload.uuid)
|
self.assertEqual(self.node.uuid, payload.uuid)
|
||||||
|
self.assertEqual(self.node.owner, payload.owner)
|
||||||
|
|
||||||
def test_node_payload(self):
|
def test_node_payload(self):
|
||||||
payload = objects.NodePayload(self.node)
|
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.
|
# 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.
|
# The fingerprint values should only be changed if there is a version bump.
|
||||||
expected_object_fingerprints = {
|
expected_object_fingerprints = {
|
||||||
'Node': '1.29-7af860bb4017751104558139c52a1327',
|
'Node': '1.30-8313460d6ea5457a527cd3d85e5ee3d8',
|
||||||
'MyObj': '1.5-9459d30d6954bffc7a9afd347a807ca6',
|
'MyObj': '1.5-9459d30d6954bffc7a9afd347a807ca6',
|
||||||
'Chassis': '1.3-d656e039fd8ae9f34efc232ab3980905',
|
'Chassis': '1.3-d656e039fd8ae9f34efc232ab3980905',
|
||||||
'Port': '1.8-898a47921f4a1f53fcdddd4eeb179e0b',
|
'Port': '1.8-898a47921f4a1f53fcdddd4eeb179e0b',
|
||||||
@ -685,21 +685,21 @@ expected_object_fingerprints = {
|
|||||||
'Conductor': '1.3-d3f53e853b4d58cae5bfbd9a8341af4a',
|
'Conductor': '1.3-d3f53e853b4d58cae5bfbd9a8341af4a',
|
||||||
'EventType': '1.1-aa2ba1afd38553e3880c267404e8d370',
|
'EventType': '1.1-aa2ba1afd38553e3880c267404e8d370',
|
||||||
'NotificationPublisher': '1.0-51a09397d6c0687771fb5be9a999605d',
|
'NotificationPublisher': '1.0-51a09397d6c0687771fb5be9a999605d',
|
||||||
'NodePayload': '1.11-f323602c2e9c3edbf2a5567eca087ff5',
|
'NodePayload': '1.12-7d650c2a024357275990681f020512e4',
|
||||||
'NodeSetPowerStateNotification': '1.0-59acc533c11d306f149846f922739c15',
|
'NodeSetPowerStateNotification': '1.0-59acc533c11d306f149846f922739c15',
|
||||||
'NodeSetPowerStatePayload': '1.11-b61e66ef9d100a2cc564d16b12810855',
|
'NodeSetPowerStatePayload': '1.12-703d110d571cc95b2947bb6bd153fcb8',
|
||||||
'NodeCorrectedPowerStateNotification':
|
'NodeCorrectedPowerStateNotification':
|
||||||
'1.0-59acc533c11d306f149846f922739c15',
|
'1.0-59acc533c11d306f149846f922739c15',
|
||||||
'NodeCorrectedPowerStatePayload': '1.11-e6e32a38ca655509802ac3c6d8bc17f6',
|
'NodeCorrectedPowerStatePayload': '1.12-29cbb6b20a0aeea9e0ab9e17302e9e16',
|
||||||
'NodeSetProvisionStateNotification':
|
'NodeSetProvisionStateNotification':
|
||||||
'1.0-59acc533c11d306f149846f922739c15',
|
'1.0-59acc533c11d306f149846f922739c15',
|
||||||
'NodeSetProvisionStatePayload': '1.11-d13cb3472eea163de5b0723a08e95d2c',
|
'NodeSetProvisionStatePayload': '1.12-a302ce357ad39a0a4d1ca3c0ee44f0e0',
|
||||||
'VolumeConnector': '1.0-3e0252c0ab6e6b9d158d09238a577d97',
|
'VolumeConnector': '1.0-3e0252c0ab6e6b9d158d09238a577d97',
|
||||||
'VolumeTarget': '1.0-0b10d663d8dae675900b2c7548f76f5e',
|
'VolumeTarget': '1.0-0b10d663d8dae675900b2c7548f76f5e',
|
||||||
'ChassisCRUDNotification': '1.0-59acc533c11d306f149846f922739c15',
|
'ChassisCRUDNotification': '1.0-59acc533c11d306f149846f922739c15',
|
||||||
'ChassisCRUDPayload': '1.0-dce63895d8186279a7dd577cffccb202',
|
'ChassisCRUDPayload': '1.0-dce63895d8186279a7dd577cffccb202',
|
||||||
'NodeCRUDNotification': '1.0-59acc533c11d306f149846f922739c15',
|
'NodeCRUDNotification': '1.0-59acc533c11d306f149846f922739c15',
|
||||||
'NodeCRUDPayload': '1.9-c5e57432274371f7fe32f269519033cf',
|
'NodeCRUDPayload': '1.10-49590dee863c5ed1193f5deae0a0a2f2',
|
||||||
'PortCRUDNotification': '1.0-59acc533c11d306f149846f922739c15',
|
'PortCRUDNotification': '1.0-59acc533c11d306f149846f922739c15',
|
||||||
'PortCRUDPayload': '1.2-233d259df442eb15cc584fae1fe81504',
|
'PortCRUDPayload': '1.2-233d259df442eb15cc584fae1fe81504',
|
||||||
'NodeMaintenanceNotification': '1.0-59acc533c11d306f149846f922739c15',
|
'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