Add node lessee field
This change adds a `lessee` field to nodes, and exposes it to policy. It also updates the non-admin node list API to match for both owner and lessee; and updates the allocation conductor to match owner allocations with nodes with the appropriate lessee. Change-Id: Ib31b49c7143ec8fd6cb486fc24038215b197c418 Story: 2006506 Task: 37930
This commit is contained in:
parent
b148cabdb2
commit
602a467a04
@ -101,6 +101,9 @@ supplied when the Node is created, or the resource may be updated later.
|
|||||||
.. versionadded:: 1.52
|
.. versionadded:: 1.52
|
||||||
Introduced the ``allocation_uuid`` field.
|
Introduced the ``allocation_uuid`` field.
|
||||||
|
|
||||||
|
.. versionadded:: 1.65
|
||||||
|
Introduced the ``lessee`` field.
|
||||||
|
|
||||||
Normal response codes: 201
|
Normal response codes: 201
|
||||||
|
|
||||||
Error codes: 400,403,406
|
Error codes: 400,403,406
|
||||||
@ -131,6 +134,7 @@ Request
|
|||||||
- vendor_interface: req_vendor_interface
|
- vendor_interface: req_vendor_interface
|
||||||
- owner: owner
|
- owner: owner
|
||||||
- description: n_description
|
- description: n_description
|
||||||
|
- lessee: lessee
|
||||||
|
|
||||||
**Example Node creation request with a dynamic driver:**
|
**Example Node creation request with a dynamic driver:**
|
||||||
|
|
||||||
@ -201,6 +205,7 @@ microversion 1.48.
|
|||||||
- protected_reason: protected_reason
|
- protected_reason: protected_reason
|
||||||
- conductor: conductor
|
- conductor: conductor
|
||||||
- owner: owner
|
- owner: owner
|
||||||
|
- lessee: lessee
|
||||||
- description: n_description
|
- description: n_description
|
||||||
- allocation_uuid: allocation_uuid
|
- allocation_uuid: allocation_uuid
|
||||||
|
|
||||||
@ -260,6 +265,9 @@ provision state, and maintenance setting for each Node.
|
|||||||
.. versionadded:: 1.51
|
.. versionadded:: 1.51
|
||||||
Introduced the ``description`` field.
|
Introduced the ``description`` field.
|
||||||
|
|
||||||
|
.. versionadded:: 1.65
|
||||||
|
Introduced the ``lessee`` field.
|
||||||
|
|
||||||
Normal response codes: 200
|
Normal response codes: 200
|
||||||
|
|
||||||
Error codes: 400,403,406
|
Error codes: 400,403,406
|
||||||
@ -279,6 +287,7 @@ Request
|
|||||||
- conductor: r_conductor
|
- conductor: r_conductor
|
||||||
- fault: r_fault
|
- fault: r_fault
|
||||||
- owner: owner
|
- owner: owner
|
||||||
|
- lessee: lessee
|
||||||
- description_contains: r_description_contains
|
- description_contains: r_description_contains
|
||||||
- fields: fields
|
- fields: fields
|
||||||
- limit: limit
|
- limit: limit
|
||||||
@ -347,6 +356,9 @@ Nova instance, eg. with a request to ``v1/nodes/detail?instance_uuid={NOVA INSTA
|
|||||||
.. versionadded:: 1.52
|
.. versionadded:: 1.52
|
||||||
Introduced the ``allocation_uuid`` field.
|
Introduced the ``allocation_uuid`` field.
|
||||||
|
|
||||||
|
.. versionadded:: 1.65
|
||||||
|
Introduced the ``lessee`` field.
|
||||||
|
|
||||||
Normal response codes: 200
|
Normal response codes: 200
|
||||||
|
|
||||||
Error codes: 400,403,406
|
Error codes: 400,403,406
|
||||||
@ -366,6 +378,7 @@ Request
|
|||||||
- conductor_group: r_conductor_group
|
- conductor_group: r_conductor_group
|
||||||
- conductor: r_conductor
|
- conductor: r_conductor
|
||||||
- owner: owner
|
- owner: owner
|
||||||
|
- lessee: lessee
|
||||||
- description_contains: r_description_contains
|
- description_contains: r_description_contains
|
||||||
- limit: limit
|
- limit: limit
|
||||||
- marker: marker
|
- marker: marker
|
||||||
@ -423,6 +436,7 @@ Response
|
|||||||
- protected: protected
|
- protected: protected
|
||||||
- protected_reason: protected_reason
|
- protected_reason: protected_reason
|
||||||
- owner: owner
|
- owner: owner
|
||||||
|
- lessee: lessee
|
||||||
- description: n_description
|
- description: n_description
|
||||||
- conductor: conductor
|
- conductor: conductor
|
||||||
- allocation_uuid: allocation_uuid
|
- allocation_uuid: allocation_uuid
|
||||||
@ -474,6 +488,9 @@ only the specified set.
|
|||||||
.. versionadded:: 1.61
|
.. versionadded:: 1.61
|
||||||
Introduced the ``retired`` and ``retired_reason`` fields.
|
Introduced the ``retired`` and ``retired_reason`` fields.
|
||||||
|
|
||||||
|
.. versionadded:: 1.65
|
||||||
|
Introduced the ``lessee`` field.
|
||||||
|
|
||||||
Normal response codes: 200
|
Normal response codes: 200
|
||||||
|
|
||||||
Error codes: 400,403,404,406
|
Error codes: 400,403,404,406
|
||||||
@ -537,6 +554,7 @@ Response
|
|||||||
- protected: protected
|
- protected: protected
|
||||||
- protected_reason: protected_reason
|
- protected_reason: protected_reason
|
||||||
- owner: owner
|
- owner: owner
|
||||||
|
- lessee: lessee
|
||||||
- description: n_description
|
- description: n_description
|
||||||
- conductor: conductor
|
- conductor: conductor
|
||||||
- allocation_uuid: allocation_uuid
|
- allocation_uuid: allocation_uuid
|
||||||
@ -632,6 +650,7 @@ Response
|
|||||||
- protected: protected
|
- protected: protected
|
||||||
- protected_reason: protected_reason
|
- protected_reason: protected_reason
|
||||||
- owner: owner
|
- owner: owner
|
||||||
|
- lessee: lessee
|
||||||
- description: n_description
|
- description: n_description
|
||||||
- conductor: conductor
|
- conductor: conductor
|
||||||
- allocation_uuid: allocation_uuid
|
- allocation_uuid: allocation_uuid
|
||||||
|
@ -910,6 +910,12 @@ last_error:
|
|||||||
in: body
|
in: body
|
||||||
required: true
|
required: true
|
||||||
type: string
|
type: string
|
||||||
|
lessee:
|
||||||
|
description: |
|
||||||
|
A string or UUID of the tenant who is leasing the object.
|
||||||
|
in: body
|
||||||
|
required: false
|
||||||
|
type: string
|
||||||
links:
|
links:
|
||||||
description: |
|
description: |
|
||||||
A list of relative links. Includes the self and
|
A list of relative links. Includes the self and
|
||||||
|
@ -23,6 +23,7 @@
|
|||||||
"instance_info": {},
|
"instance_info": {},
|
||||||
"instance_uuid": null,
|
"instance_uuid": null,
|
||||||
"last_error": null,
|
"last_error": null,
|
||||||
|
"lessee": null,
|
||||||
"links": [
|
"links": [
|
||||||
{
|
{
|
||||||
"href": "http://127.0.0.1:6385/v1/nodes/6d85703a-565d-469a-96ce-30b6de53079d",
|
"href": "http://127.0.0.1:6385/v1/nodes/6d85703a-565d-469a-96ce-30b6de53079d",
|
||||||
|
@ -26,6 +26,7 @@
|
|||||||
"instance_info": {},
|
"instance_info": {},
|
||||||
"instance_uuid": null,
|
"instance_uuid": null,
|
||||||
"last_error": null,
|
"last_error": null,
|
||||||
|
"lessee": null,
|
||||||
"links": [
|
"links": [
|
||||||
{
|
{
|
||||||
"href": "http://127.0.0.1:6385/v1/nodes/6d85703a-565d-469a-96ce-30b6de53079d",
|
"href": "http://127.0.0.1:6385/v1/nodes/6d85703a-565d-469a-96ce-30b6de53079d",
|
||||||
|
@ -27,6 +27,7 @@
|
|||||||
"instance_info": {},
|
"instance_info": {},
|
||||||
"instance_uuid": null,
|
"instance_uuid": null,
|
||||||
"last_error": null,
|
"last_error": null,
|
||||||
|
"lessee": null,
|
||||||
"links": [
|
"links": [
|
||||||
{
|
{
|
||||||
"href": "http://127.0.0.1:6385/v1/nodes/6d85703a-565d-469a-96ce-30b6de53079d",
|
"href": "http://127.0.0.1:6385/v1/nodes/6d85703a-565d-469a-96ce-30b6de53079d",
|
||||||
|
@ -28,6 +28,7 @@
|
|||||||
"instance_info": {},
|
"instance_info": {},
|
||||||
"instance_uuid": "5344a3e2-978a-444e-990a-cbf47c62ef88",
|
"instance_uuid": "5344a3e2-978a-444e-990a-cbf47c62ef88",
|
||||||
"last_error": null,
|
"last_error": null,
|
||||||
|
"lessee": null,
|
||||||
"links": [
|
"links": [
|
||||||
{
|
{
|
||||||
"href": "http://127.0.0.1:6385/v1/nodes/6d85703a-565d-469a-96ce-30b6de53079d",
|
"href": "http://127.0.0.1:6385/v1/nodes/6d85703a-565d-469a-96ce-30b6de53079d",
|
||||||
@ -132,6 +133,7 @@
|
|||||||
"instance_info": {},
|
"instance_info": {},
|
||||||
"instance_uuid": null,
|
"instance_uuid": null,
|
||||||
"last_error": null,
|
"last_error": null,
|
||||||
|
"lessee": null,
|
||||||
"links": [
|
"links": [
|
||||||
{
|
{
|
||||||
"href": "http://127.0.0.1:6385/v1/nodes/2b045129-a906-46af-bc1a-092b294b3428",
|
"href": "http://127.0.0.1:6385/v1/nodes/2b045129-a906-46af-bc1a-092b294b3428",
|
||||||
|
@ -2,6 +2,14 @@
|
|||||||
REST API Version History
|
REST API Version History
|
||||||
========================
|
========================
|
||||||
|
|
||||||
|
1.65 (Ussuri, master)
|
||||||
|
---------------------
|
||||||
|
|
||||||
|
Added ``lessee`` field to the node object. The field should match the
|
||||||
|
``project_id`` of the intended lessee. If an allocation has an owner,
|
||||||
|
then the allocation process will only match the allocation with a node
|
||||||
|
that has the same ``owner`` or ``lessee``.
|
||||||
|
|
||||||
1.64 (Ussuri, master)
|
1.64 (Ussuri, master)
|
||||||
---------------------
|
---------------------
|
||||||
|
|
||||||
|
@ -1250,7 +1250,10 @@ class Node(base.APIBase):
|
|||||||
owner = wsme.wsattr(str)
|
owner = wsme.wsattr(str)
|
||||||
"""Field for storage of physical node owner"""
|
"""Field for storage of physical node owner"""
|
||||||
|
|
||||||
description = wsme.wsattr(str)
|
lessee = wsme.wsattr(wtypes.text)
|
||||||
|
"""Field for storage of physical node lessee"""
|
||||||
|
|
||||||
|
description = wsme.wsattr(wtypes.text)
|
||||||
"""Field for node description"""
|
"""Field for node description"""
|
||||||
|
|
||||||
allocation_uuid = wsme.wsattr(types.uuid, readonly=True)
|
allocation_uuid = wsme.wsattr(types.uuid, readonly=True)
|
||||||
@ -1482,7 +1485,7 @@ class Node(base.APIBase):
|
|||||||
automated_clean=None, protected=False,
|
automated_clean=None, protected=False,
|
||||||
protected_reason=None, owner=None,
|
protected_reason=None, owner=None,
|
||||||
allocation_uuid='982ddb5b-bce5-4d23-8fb8-7f710f648cd5',
|
allocation_uuid='982ddb5b-bce5-4d23-8fb8-7f710f648cd5',
|
||||||
retired=False, retired_reason=None)
|
retired=False, retired_reason=None, lessee=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'
|
||||||
@ -1801,6 +1804,7 @@ class NodesController(rest.RestController):
|
|||||||
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, owner=None,
|
detail=None, conductor=None, owner=None,
|
||||||
|
lessee=None, project=None,
|
||||||
description_contains=None):
|
description_contains=None):
|
||||||
if self.from_chassis and not chassis_uuid:
|
if self.from_chassis and not chassis_uuid:
|
||||||
raise exception.MissingParameterValue(
|
raise exception.MissingParameterValue(
|
||||||
@ -1844,6 +1848,8 @@ class NodesController(rest.RestController):
|
|||||||
'fault': fault,
|
'fault': fault,
|
||||||
'conductor_group': conductor_group,
|
'conductor_group': conductor_group,
|
||||||
'owner': owner,
|
'owner': owner,
|
||||||
|
'lessee': lessee,
|
||||||
|
'project': project,
|
||||||
'description_contains': description_contains,
|
'description_contains': description_contains,
|
||||||
'retired': retired,
|
'retired': retired,
|
||||||
}
|
}
|
||||||
@ -1970,13 +1976,14 @@ class NodesController(rest.RestController):
|
|||||||
types.boolean, types.boolean, str, types.uuid, int, str,
|
types.boolean, types.boolean, str, types.uuid, int, str,
|
||||||
str, str, types.listtype, str,
|
str, str, types.listtype, str,
|
||||||
str, str, types.boolean, str,
|
str, str, types.boolean, str,
|
||||||
str, str)
|
str, str, str, str)
|
||||||
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, retired=None, provision_state=None,
|
maintenance=None, retired=None, provision_state=None,
|
||||||
marker=None, limit=None, sort_key='id', sort_dir='asc',
|
marker=None, limit=None, sort_key='id', sort_dir='asc',
|
||||||
driver=None, fields=None, resource_class=None, fault=None,
|
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, description_contains=None):
|
owner=None, description_contains=None, lessee=None,
|
||||||
|
project=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
|
||||||
@ -2010,6 +2017,10 @@ class NodesController(rest.RestController):
|
|||||||
that conductor.
|
that conductor.
|
||||||
:param owner: Optional string value that set the owner whose nodes
|
:param owner: Optional string value that set the owner whose nodes
|
||||||
are to be retrurned.
|
are to be retrurned.
|
||||||
|
:param lessee: Optional string value that set the lessee whose nodes
|
||||||
|
are to be returned.
|
||||||
|
:param project: Optional string value that set the project - lessee or
|
||||||
|
owner - whose nodes are to be returned.
|
||||||
: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.
|
||||||
@ -2017,7 +2028,7 @@ class NodesController(rest.RestController):
|
|||||||
with description field contains matching
|
with description field contains matching
|
||||||
value.
|
value.
|
||||||
"""
|
"""
|
||||||
owner = api_utils.check_list_policy('node', owner)
|
project = api_utils.check_list_policy('node', project)
|
||||||
|
|
||||||
api_utils.check_allow_specify_fields(fields)
|
api_utils.check_allow_specify_fields(fields)
|
||||||
api_utils.check_allowed_fields(fields)
|
api_utils.check_allowed_fields(fields)
|
||||||
@ -2029,6 +2040,7 @@ class NodesController(rest.RestController):
|
|||||||
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)
|
api_utils.check_allow_filter_by_owner(owner)
|
||||||
|
api_utils.check_allow_filter_by_lessee(lessee)
|
||||||
|
|
||||||
fields = api_utils.get_request_return_fields(fields, detail,
|
fields = api_utils.get_request_return_fields(fields, detail,
|
||||||
_DEFAULT_RETURN_FIELDS)
|
_DEFAULT_RETURN_FIELDS)
|
||||||
@ -2044,20 +2056,22 @@ class NodesController(rest.RestController):
|
|||||||
conductor_group=conductor_group,
|
conductor_group=conductor_group,
|
||||||
detail=detail,
|
detail=detail,
|
||||||
conductor=conductor,
|
conductor=conductor,
|
||||||
owner=owner,
|
owner=owner, lessee=lessee,
|
||||||
|
project=project,
|
||||||
**extra_args)
|
**extra_args)
|
||||||
|
|
||||||
@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, types.boolean, str, types.uuid, int, str,
|
types.boolean, types.boolean, str, types.uuid, int, str,
|
||||||
str, str, str, str,
|
str, str, str, str,
|
||||||
str, str, str, str)
|
str, str, str, str,
|
||||||
|
str, str)
|
||||||
def detail(self, chassis_uuid=None, instance_uuid=None, associated=None,
|
def detail(self, chassis_uuid=None, instance_uuid=None, associated=None,
|
||||||
maintenance=None, retired=None, provision_state=None,
|
maintenance=None, retired=None, provision_state=None,
|
||||||
marker=None, limit=None, sort_key='id', sort_dir='asc',
|
marker=None, limit=None, sort_key='id', sort_dir='asc',
|
||||||
driver=None, resource_class=None, fault=None,
|
driver=None, resource_class=None, fault=None,
|
||||||
conductor_group=None, conductor=None, owner=None,
|
conductor_group=None, conductor=None, owner=None,
|
||||||
description_contains=None):
|
description_contains=None, lessee=None, project=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
|
||||||
@ -2090,11 +2104,15 @@ class NodesController(rest.RestController):
|
|||||||
that conductor_group.
|
that conductor_group.
|
||||||
:param owner: Optional string value that set the owner whose nodes
|
:param owner: Optional string value that set the owner whose nodes
|
||||||
are to be retrurned.
|
are to be retrurned.
|
||||||
|
:param lessee: Optional string value that set the lessee whose nodes
|
||||||
|
are to be returned.
|
||||||
|
:param project: Optional string value that set the project - lessee or
|
||||||
|
owner - whose nodes are to be returned.
|
||||||
:param description_contains: Optional string value to get only nodes
|
:param description_contains: Optional string value to get only nodes
|
||||||
with description field contains matching
|
with description field contains matching
|
||||||
value.
|
value.
|
||||||
"""
|
"""
|
||||||
owner = api_utils.check_list_policy('node', owner)
|
project = api_utils.check_list_policy('node', project)
|
||||||
|
|
||||||
api_utils.check_for_invalid_state_and_allow_filter(provision_state)
|
api_utils.check_for_invalid_state_and_allow_filter(provision_state)
|
||||||
api_utils.check_allow_specify_driver(driver)
|
api_utils.check_allow_specify_driver(driver)
|
||||||
@ -2102,6 +2120,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_owner(owner)
|
api_utils.check_allow_filter_by_owner(owner)
|
||||||
|
api_utils.check_allow_filter_by_lessee(lessee)
|
||||||
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 = api.request.path.split('/')[:-1][-1]
|
parent = api.request.path.split('/')[:-1][-1]
|
||||||
@ -2122,7 +2141,8 @@ class NodesController(rest.RestController):
|
|||||||
fault=fault,
|
fault=fault,
|
||||||
conductor_group=conductor_group,
|
conductor_group=conductor_group,
|
||||||
conductor=conductor,
|
conductor=conductor,
|
||||||
owner=owner,
|
owner=owner, lessee=lessee,
|
||||||
|
project=project,
|
||||||
**extra_args)
|
**extra_args)
|
||||||
|
|
||||||
@METRICS.timer('NodesController.validate')
|
@METRICS.timer('NodesController.validate')
|
||||||
@ -2341,7 +2361,7 @@ class NodesController(rest.RestController):
|
|||||||
api_utils.check_owner_policy(
|
api_utils.check_owner_policy(
|
||||||
'node',
|
'node',
|
||||||
'baremetal:node:update_owner_provisioned',
|
'baremetal:node:update_owner_provisioned',
|
||||||
rpc_node['owner'])
|
rpc_node['owner'], rpc_node['lessee'])
|
||||||
except exception.HTTPForbidden:
|
except exception.HTTPForbidden:
|
||||||
msg = _('Cannot update owner of node "%(node)s" while it '
|
msg = _('Cannot update owner of node "%(node)s" while it '
|
||||||
'is in state "%(state)s".') % {
|
'is in state "%(state)s".') % {
|
||||||
|
@ -490,6 +490,7 @@ VERSIONED_FIELDS = {
|
|||||||
'events': versions.MINOR_54_EVENTS,
|
'events': versions.MINOR_54_EVENTS,
|
||||||
'retired': versions.MINOR_61_NODE_RETIRED,
|
'retired': versions.MINOR_61_NODE_RETIRED,
|
||||||
'retired_reason': versions.MINOR_61_NODE_RETIRED,
|
'retired_reason': versions.MINOR_61_NODE_RETIRED,
|
||||||
|
'lessee': versions.MINOR_65_NODE_LESSEE,
|
||||||
}
|
}
|
||||||
|
|
||||||
for field in V31_FIELDS:
|
for field in V31_FIELDS:
|
||||||
@ -717,6 +718,20 @@ def check_allow_filter_by_owner(owner):
|
|||||||
'opr': versions.MINOR_50_NODE_OWNER})
|
'opr': versions.MINOR_50_NODE_OWNER})
|
||||||
|
|
||||||
|
|
||||||
|
def check_allow_filter_by_lessee(lessee):
|
||||||
|
"""Check if filtering nodes by lessee is allowed.
|
||||||
|
|
||||||
|
Version 1.62 of the API allows filtering nodes by lessee.
|
||||||
|
"""
|
||||||
|
if (lessee is not None and api.request.version.minor
|
||||||
|
< versions.MINOR_65_NODE_LESSEE):
|
||||||
|
raise exception.NotAcceptable(_(
|
||||||
|
"Request not acceptable. The minimal required API version "
|
||||||
|
"should be %(base)s.%(opr)s") %
|
||||||
|
{'base': versions.BASE_VERSION,
|
||||||
|
'opr': versions.MINOR_65_NODE_LESSEE})
|
||||||
|
|
||||||
|
|
||||||
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.
|
||||||
|
|
||||||
@ -1165,12 +1180,13 @@ def check_policy(policy_name):
|
|||||||
policy.authorize(policy_name, cdict, cdict)
|
policy.authorize(policy_name, cdict, cdict)
|
||||||
|
|
||||||
|
|
||||||
def check_owner_policy(object_type, policy_name, owner):
|
def check_owner_policy(object_type, policy_name, owner, lessee=None):
|
||||||
"""Check if the policy authorizes this request on an object.
|
"""Check if the policy authorizes this request on an object.
|
||||||
|
|
||||||
:param: object_type: type of object being checked
|
:param: object_type: type of object being checked
|
||||||
:param: policy_name: Name of the policy to check.
|
:param: policy_name: Name of the policy to check.
|
||||||
:param: owner: the owner
|
:param: owner: the owner
|
||||||
|
:param: lessee: the lessee
|
||||||
|
|
||||||
:raises: HTTPForbidden if the policy forbids access.
|
:raises: HTTPForbidden if the policy forbids access.
|
||||||
"""
|
"""
|
||||||
@ -1178,6 +1194,8 @@ def check_owner_policy(object_type, policy_name, owner):
|
|||||||
|
|
||||||
target_dict = dict(cdict)
|
target_dict = dict(cdict)
|
||||||
target_dict[object_type + '.owner'] = owner
|
target_dict[object_type + '.owner'] = owner
|
||||||
|
if lessee:
|
||||||
|
target_dict[object_type + '.lessee'] = lessee
|
||||||
policy.authorize(policy_name, target_dict, cdict)
|
policy.authorize(policy_name, target_dict, cdict)
|
||||||
|
|
||||||
|
|
||||||
@ -1205,7 +1223,8 @@ def check_node_policy_and_retrieve(policy_name, node_ident,
|
|||||||
policy.authorize(policy_name, cdict, cdict)
|
policy.authorize(policy_name, cdict, cdict)
|
||||||
raise
|
raise
|
||||||
|
|
||||||
check_owner_policy('node', policy_name, rpc_node['owner'])
|
check_owner_policy('node', policy_name,
|
||||||
|
rpc_node['owner'], rpc_node['lessee'])
|
||||||
return rpc_node
|
return rpc_node
|
||||||
|
|
||||||
|
|
||||||
@ -1253,7 +1272,8 @@ def check_multiple_node_policies_and_retrieve(policy_names,
|
|||||||
node_ident,
|
node_ident,
|
||||||
with_suffix)
|
with_suffix)
|
||||||
else:
|
else:
|
||||||
check_owner_policy('node', policy_name, rpc_node['owner'])
|
check_owner_policy('node', policy_name,
|
||||||
|
rpc_node['owner'], rpc_node['lessee'])
|
||||||
return rpc_node
|
return rpc_node
|
||||||
|
|
||||||
|
|
||||||
@ -1303,6 +1323,7 @@ def check_port_policy_and_retrieve(policy_name, port_uuid):
|
|||||||
rpc_node = objects.Node.get_by_id(context, rpc_port.node_id)
|
rpc_node = objects.Node.get_by_id(context, rpc_port.node_id)
|
||||||
target_dict = dict(cdict)
|
target_dict = dict(cdict)
|
||||||
target_dict['node.owner'] = rpc_node['owner']
|
target_dict['node.owner'] = rpc_node['owner']
|
||||||
|
target_dict['node.lessee'] = rpc_node['lessee']
|
||||||
policy.authorize(policy_name, target_dict, cdict)
|
policy.authorize(policy_name, target_dict, cdict)
|
||||||
|
|
||||||
return rpc_port, rpc_node
|
return rpc_port, rpc_node
|
||||||
|
@ -102,6 +102,7 @@ BASE_VERSION = 1
|
|||||||
# v1.62: Add agent_token support for agent communication.
|
# v1.62: Add agent_token support for agent communication.
|
||||||
# v1.63: Add support for indicators
|
# v1.63: Add support for indicators
|
||||||
# v1.64: Add network_type to port.local_link_connection
|
# v1.64: Add network_type to port.local_link_connection
|
||||||
|
# v1.65: Add lessee to the node object.
|
||||||
|
|
||||||
MINOR_0_JUNO = 0
|
MINOR_0_JUNO = 0
|
||||||
MINOR_1_INITIAL_VERSION = 1
|
MINOR_1_INITIAL_VERSION = 1
|
||||||
@ -168,6 +169,7 @@ MINOR_61_NODE_RETIRED = 61
|
|||||||
MINOR_62_AGENT_TOKEN = 62
|
MINOR_62_AGENT_TOKEN = 62
|
||||||
MINOR_63_INDICATORS = 63
|
MINOR_63_INDICATORS = 63
|
||||||
MINOR_64_LOCAL_LINK_CONNECTION_NETWORK_TYPE = 64
|
MINOR_64_LOCAL_LINK_CONNECTION_NETWORK_TYPE = 64
|
||||||
|
MINOR_65_NODE_LESSEE = 65
|
||||||
|
|
||||||
# When adding another version, update:
|
# When adding another version, update:
|
||||||
# - MINOR_MAX_VERSION
|
# - MINOR_MAX_VERSION
|
||||||
@ -175,7 +177,7 @@ MINOR_64_LOCAL_LINK_CONNECTION_NETWORK_TYPE = 64
|
|||||||
# 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_64_LOCAL_LINK_CONNECTION_NETWORK_TYPE
|
MINOR_MAX_VERSION = MINOR_65_NODE_LESSEE
|
||||||
|
|
||||||
# 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)
|
||||||
|
@ -66,6 +66,9 @@ default_policies = [
|
|||||||
policy.RuleDefault('is_node_owner',
|
policy.RuleDefault('is_node_owner',
|
||||||
'project_id:%(node.owner)s',
|
'project_id:%(node.owner)s',
|
||||||
description='Owner of node'),
|
description='Owner of node'),
|
||||||
|
policy.RuleDefault('is_node_lessee',
|
||||||
|
'project_id:%(node.lessee)s',
|
||||||
|
description='Lessee of node'),
|
||||||
policy.RuleDefault('is_allocation_owner',
|
policy.RuleDefault('is_allocation_owner',
|
||||||
'project_id:%(allocation.owner)s',
|
'project_id:%(allocation.owner)s',
|
||||||
description='Owner of allocation'),
|
description='Owner of allocation'),
|
||||||
|
@ -214,11 +214,11 @@ RELEASE_MAPPING = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
'master': {
|
'master': {
|
||||||
'api': '1.64',
|
'api': '1.65',
|
||||||
'rpc': '1.50',
|
'rpc': '1.50',
|
||||||
'objects': {
|
'objects': {
|
||||||
'Allocation': ['1.1'],
|
'Allocation': ['1.1'],
|
||||||
'Node': ['1.33', '1.32'],
|
'Node': ['1.34', '1.33', '1.32'],
|
||||||
'Conductor': ['1.3'],
|
'Conductor': ['1.3'],
|
||||||
'Chassis': ['1.3'],
|
'Chassis': ['1.3'],
|
||||||
'DeployTemplate': ['1.1'],
|
'DeployTemplate': ['1.1'],
|
||||||
|
@ -113,7 +113,7 @@ def _candidate_nodes(context, allocation):
|
|||||||
# UUIDs on the API level.
|
# UUIDs on the API level.
|
||||||
filters['uuid_in'] = allocation.candidate_nodes
|
filters['uuid_in'] = allocation.candidate_nodes
|
||||||
if allocation.owner:
|
if allocation.owner:
|
||||||
filters['owner'] = allocation.owner
|
filters['project'] = allocation.owner
|
||||||
|
|
||||||
nodes = objects.Node.list(context, filters=filters)
|
nodes = objects.Node.list(context, filters=filters)
|
||||||
|
|
||||||
|
@ -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 lessee
|
||||||
|
|
||||||
|
Revision ID: b2ad35726bb0
|
||||||
|
Revises: ce6c4b3cf5a2
|
||||||
|
Create Date: 2020-01-07 20:49:50.851441
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = 'b2ad35726bb0'
|
||||||
|
down_revision = 'cd2c80feb331'
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
op.add_column('nodes', sa.Column('lessee', sa.String(255),
|
||||||
|
nullable=True))
|
@ -288,7 +288,7 @@ class Connection(api.Connection):
|
|||||||
_NODE_QUERY_FIELDS = {'console_enabled', 'maintenance', 'retired',
|
_NODE_QUERY_FIELDS = {'console_enabled', 'maintenance', 'retired',
|
||||||
'driver', 'resource_class', 'provision_state',
|
'driver', 'resource_class', 'provision_state',
|
||||||
'uuid', 'id', 'fault', 'conductor_group',
|
'uuid', 'id', 'fault', 'conductor_group',
|
||||||
'owner'}
|
'owner', 'lessee'}
|
||||||
_NODE_IN_QUERY_FIELDS = {'%s_in' % field: field
|
_NODE_IN_QUERY_FIELDS = {'%s_in' % field: field
|
||||||
for field in ('uuid', 'provision_state')}
|
for field in ('uuid', 'provision_state')}
|
||||||
_NODE_NON_NULL_FILTERS = {'associated': 'instance_uuid',
|
_NODE_NON_NULL_FILTERS = {'associated': 'instance_uuid',
|
||||||
@ -296,7 +296,7 @@ class Connection(api.Connection):
|
|||||||
'with_power_state': 'power_state'}
|
'with_power_state': 'power_state'}
|
||||||
_NODE_FILTERS = ({'chassis_uuid', 'reserved_by_any_of',
|
_NODE_FILTERS = ({'chassis_uuid', 'reserved_by_any_of',
|
||||||
'provisioned_before', 'inspection_started_before',
|
'provisioned_before', 'inspection_started_before',
|
||||||
'description_contains'}
|
'description_contains', 'project'}
|
||||||
| _NODE_QUERY_FIELDS
|
| _NODE_QUERY_FIELDS
|
||||||
| set(_NODE_IN_QUERY_FIELDS)
|
| set(_NODE_IN_QUERY_FIELDS)
|
||||||
| set(_NODE_NON_NULL_FILTERS))
|
| set(_NODE_NON_NULL_FILTERS))
|
||||||
@ -354,6 +354,10 @@ class Connection(api.Connection):
|
|||||||
if keyword is not None:
|
if keyword is not None:
|
||||||
query = query.filter(
|
query = query.filter(
|
||||||
models.Node.description.like(r'%{}%'.format(keyword)))
|
models.Node.description.like(r'%{}%'.format(keyword)))
|
||||||
|
if 'project' in filters:
|
||||||
|
project = filters['project']
|
||||||
|
query = query.filter((models.Node.owner == project)
|
||||||
|
| (models.Node.lessee == project))
|
||||||
|
|
||||||
return query
|
return query
|
||||||
|
|
||||||
|
@ -180,6 +180,7 @@ class Node(Base):
|
|||||||
server_default=false())
|
server_default=false())
|
||||||
protected_reason = Column(Text, nullable=True)
|
protected_reason = Column(Text, nullable=True)
|
||||||
owner = Column(String(255), nullable=True)
|
owner = Column(String(255), nullable=True)
|
||||||
|
lessee = Column(String(255), nullable=True)
|
||||||
allocation_id = Column(Integer, ForeignKey('allocations.id'),
|
allocation_id = Column(Integer, ForeignKey('allocations.id'),
|
||||||
nullable=True)
|
nullable=True)
|
||||||
description = Column(Text, nullable=True)
|
description = Column(Text, nullable=True)
|
||||||
|
@ -74,7 +74,8 @@ class Node(base.IronicObject, object_base.VersionedObjectDictCompat):
|
|||||||
# Version 1.31: Add allocation_id field
|
# Version 1.31: Add allocation_id field
|
||||||
# Version 1.32: Add description field
|
# Version 1.32: Add description field
|
||||||
# Version 1.33: Add retired and retired_reason fields
|
# Version 1.33: Add retired and retired_reason fields
|
||||||
VERSION = '1.33'
|
# Version 1.34: Add lessee field
|
||||||
|
VERSION = '1.34'
|
||||||
|
|
||||||
dbapi = db_api.get_instance()
|
dbapi = db_api.get_instance()
|
||||||
|
|
||||||
@ -159,6 +160,7 @@ class Node(base.IronicObject, object_base.VersionedObjectDictCompat):
|
|||||||
'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),
|
'owner': object_fields.StringField(nullable=True),
|
||||||
|
'lessee': object_fields.StringField(nullable=True),
|
||||||
'description': object_fields.StringField(nullable=True),
|
'description': object_fields.StringField(nullable=True),
|
||||||
'retired': objects.fields.BooleanField(nullable=True),
|
'retired': objects.fields.BooleanField(nullable=True),
|
||||||
'retired_reason': object_fields.StringField(nullable=True),
|
'retired_reason': object_fields.StringField(nullable=True),
|
||||||
@ -602,6 +604,8 @@ class Node(base.IronicObject, object_base.VersionedObjectDictCompat):
|
|||||||
should be set to None (or removed).
|
should be set to None (or removed).
|
||||||
Version 1.33: retired was added. For versions prior to this, it
|
Version 1.33: retired was added. For versions prior to this, it
|
||||||
should be set to False (or removed).
|
should be set to False (or removed).
|
||||||
|
Version 1.34: lessee 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
|
||||||
@ -616,7 +620,7 @@ class Node(base.IronicObject, object_base.VersionedObjectDictCompat):
|
|||||||
('bios_interface', 24), ('fault', 25),
|
('bios_interface', 24), ('fault', 25),
|
||||||
('automated_clean', 28), ('protected_reason', 29),
|
('automated_clean', 28), ('protected_reason', 29),
|
||||||
('owner', 30), ('allocation_id', 31), ('description', 32),
|
('owner', 30), ('allocation_id', 31), ('description', 32),
|
||||||
('retired_reason', 33)]
|
('retired_reason', 33), ('lessee', 34)]
|
||||||
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)
|
||||||
@ -675,6 +679,7 @@ class NodePayload(notification.NotificationPayloadBase):
|
|||||||
'storage_interface': ('node', 'storage_interface'),
|
'storage_interface': ('node', 'storage_interface'),
|
||||||
'vendor_interface': ('node', 'vendor_interface'),
|
'vendor_interface': ('node', 'vendor_interface'),
|
||||||
'owner': ('node', 'owner'),
|
'owner': ('node', 'owner'),
|
||||||
|
'lessee': ('node', 'lessee'),
|
||||||
'power_state': ('node', 'power_state'),
|
'power_state': ('node', 'power_state'),
|
||||||
'properties': ('node', 'properties'),
|
'properties': ('node', 'properties'),
|
||||||
'protected': ('node', 'protected'),
|
'protected': ('node', 'protected'),
|
||||||
@ -706,7 +711,8 @@ class NodePayload(notification.NotificationPayloadBase):
|
|||||||
# Version 1.12: Add node owner field.
|
# Version 1.12: Add node owner field.
|
||||||
# Version 1.13: Add description field.
|
# Version 1.13: Add description field.
|
||||||
# Version 1.14: Add retired and retired_reason fields exposed via API.
|
# Version 1.14: Add retired and retired_reason fields exposed via API.
|
||||||
VERSION = '1.14'
|
# Version 1.15: Add node lessee field.
|
||||||
|
VERSION = '1.15'
|
||||||
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),
|
||||||
@ -737,6 +743,7 @@ class NodePayload(notification.NotificationPayloadBase):
|
|||||||
'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),
|
'owner': object_fields.StringField(nullable=True),
|
||||||
|
'lessee': 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),
|
||||||
@ -793,7 +800,8 @@ class NodeSetPowerStatePayload(NodePayload):
|
|||||||
# Version 1.12: Parent NodePayload version 1.12
|
# Version 1.12: Parent NodePayload version 1.12
|
||||||
# Version 1.13: Parent NodePayload version 1.13
|
# Version 1.13: Parent NodePayload version 1.13
|
||||||
# Version 1.14: Parent NodePayload version 1.14
|
# Version 1.14: Parent NodePayload version 1.14
|
||||||
VERSION = '1.14'
|
# Version 1.15: Parent NodePayload version 1.15
|
||||||
|
VERSION = '1.15'
|
||||||
|
|
||||||
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
|
||||||
@ -848,7 +856,8 @@ class NodeCorrectedPowerStatePayload(NodePayload):
|
|||||||
# Version 1.12: Parent NodePayload version 1.12
|
# Version 1.12: Parent NodePayload version 1.12
|
||||||
# Version 1.13: Parent NodePayload version 1.13
|
# Version 1.13: Parent NodePayload version 1.13
|
||||||
# Version 1.14: Parent NodePayload version 1.14
|
# Version 1.14: Parent NodePayload version 1.14
|
||||||
VERSION = '1.14'
|
# Version 1.15: Parent NodePayload version 1.15
|
||||||
|
VERSION = '1.15'
|
||||||
|
|
||||||
fields = {
|
fields = {
|
||||||
'from_power': object_fields.StringField(nullable=True)
|
'from_power': object_fields.StringField(nullable=True)
|
||||||
@ -887,7 +896,8 @@ class NodeSetProvisionStatePayload(NodePayload):
|
|||||||
# Version 1.12: Parent NodePayload version 1.12
|
# Version 1.12: Parent NodePayload version 1.12
|
||||||
# Version 1.13: Parent NodePayload version 1.13
|
# Version 1.13: Parent NodePayload version 1.13
|
||||||
# Version 1.14: Parent NodePayload version 1.14
|
# Version 1.14: Parent NodePayload version 1.14
|
||||||
VERSION = '1.14'
|
# Version 1.15: Parent NodePayload version 1.15
|
||||||
|
VERSION = '1.15'
|
||||||
|
|
||||||
SCHEMA = dict(NodePayload.SCHEMA,
|
SCHEMA = dict(NodePayload.SCHEMA,
|
||||||
**{'instance_info': ('node', 'instance_info')})
|
**{'instance_info': ('node', 'instance_info')})
|
||||||
@ -933,7 +943,8 @@ class NodeCRUDPayload(NodePayload):
|
|||||||
# Version 1.10: Parent NodePayload version 1.12
|
# Version 1.10: Parent NodePayload version 1.12
|
||||||
# Version 1.11: Parent NodePayload version 1.13
|
# Version 1.11: Parent NodePayload version 1.13
|
||||||
# Version 1.12: Parent NodePayload version 1.14
|
# Version 1.12: Parent NodePayload version 1.14
|
||||||
VERSION = '1.12'
|
# Version 1.13: Parent NodePayload version 1.15
|
||||||
|
VERSION = '1.13'
|
||||||
|
|
||||||
SCHEMA = dict(NodePayload.SCHEMA,
|
SCHEMA = dict(NodePayload.SCHEMA,
|
||||||
**{'instance_info': ('node', 'instance_info'),
|
**{'instance_info': ('node', 'instance_info'),
|
||||||
|
@ -137,6 +137,7 @@ class TestListNodes(test_api_base.BaseApiTest):
|
|||||||
self.assertNotIn('owner', data['nodes'][0])
|
self.assertNotIn('owner', data['nodes'][0])
|
||||||
self.assertNotIn('retired', data['nodes'][0])
|
self.assertNotIn('retired', data['nodes'][0])
|
||||||
self.assertNotIn('retired_reason', data['nodes'][0])
|
self.assertNotIn('retired_reason', data['nodes'][0])
|
||||||
|
self.assertNotIn('lessee', 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,
|
||||||
@ -179,6 +180,7 @@ class TestListNodes(test_api_base.BaseApiTest):
|
|||||||
self.assertIn('protected', data)
|
self.assertIn('protected', data)
|
||||||
self.assertIn('protected_reason', data)
|
self.assertIn('protected_reason', data)
|
||||||
self.assertIn('owner', data)
|
self.assertIn('owner', data)
|
||||||
|
self.assertIn('lessee', data)
|
||||||
self.assertNotIn('allocation_id', data)
|
self.assertNotIn('allocation_id', data)
|
||||||
self.assertIn('allocation_uuid', data)
|
self.assertIn('allocation_uuid', data)
|
||||||
|
|
||||||
@ -384,6 +386,23 @@ class TestListNodes(test_api_base.BaseApiTest):
|
|||||||
self.assertTrue(data['retired'])
|
self.assertTrue(data['retired'])
|
||||||
self.assertEqual('warranty expired', data['retired_reason'])
|
self.assertEqual('warranty expired', data['retired_reason'])
|
||||||
|
|
||||||
|
def test_node_lessee_hidden_in_lower_version(self):
|
||||||
|
self._test_node_field_hidden_in_lower_version('lessee',
|
||||||
|
'1.64', '1.65')
|
||||||
|
|
||||||
|
def test_node_lessee_null_field(self):
|
||||||
|
node = obj_utils.create_test_node(self.context, lessee=None)
|
||||||
|
data = self.get_json('/nodes/%s' % node.uuid,
|
||||||
|
headers={api_base.Version.string: '1.65'})
|
||||||
|
self.assertIsNone(data['lessee'])
|
||||||
|
|
||||||
|
def test_node_lessee_present(self):
|
||||||
|
node = obj_utils.create_test_node(self.context,
|
||||||
|
lessee="some-lucky-project")
|
||||||
|
data = self.get_json('/nodes/%s' % node.uuid,
|
||||||
|
headers={api_base.Version.string: '1.65'})
|
||||||
|
self.assertEqual(data['lessee'], "some-lucky-project")
|
||||||
|
|
||||||
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)
|
||||||
@ -590,6 +609,14 @@ class TestListNodes(test_api_base.BaseApiTest):
|
|||||||
headers={api_base.Version.string: '1.51'})
|
headers={api_base.Version.string: '1.51'})
|
||||||
self.assertIn('description', response)
|
self.assertIn('description', response)
|
||||||
|
|
||||||
|
def test_get_lessee_field(self):
|
||||||
|
node = obj_utils.create_test_node(self.context,
|
||||||
|
lessee='some-lucky-project')
|
||||||
|
fields = 'lessee'
|
||||||
|
response = self.get_json('/nodes/%s?fields=%s' % (node.uuid, fields),
|
||||||
|
headers={api_base.Version.string: '1.65'})
|
||||||
|
self.assertIn('lessee', response)
|
||||||
|
|
||||||
def test_get_with_allocation(self):
|
def test_get_with_allocation(self):
|
||||||
allocation = obj_utils.create_test_allocation(self.context)
|
allocation = obj_utils.create_test_allocation(self.context)
|
||||||
node = obj_utils.create_test_node(self.context,
|
node = obj_utils.create_test_node(self.context,
|
||||||
@ -650,6 +677,7 @@ class TestListNodes(test_api_base.BaseApiTest):
|
|||||||
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])
|
self.assertIn('owner', data['nodes'][0])
|
||||||
|
self.assertIn('lessee', 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])
|
||||||
self.assertNotIn('allocation_id', data['nodes'][0])
|
self.assertNotIn('allocation_id', data['nodes'][0])
|
||||||
@ -687,6 +715,7 @@ class TestListNodes(test_api_base.BaseApiTest):
|
|||||||
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])
|
self.assertIn('owner', data['nodes'][0])
|
||||||
|
self.assertIn('lessee', 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
|
||||||
@ -764,14 +793,14 @@ class TestListNodes(test_api_base.BaseApiTest):
|
|||||||
self.assertEqual(http_client.FORBIDDEN, response.status_int)
|
self.assertEqual(http_client.FORBIDDEN, response.status_int)
|
||||||
|
|
||||||
@mock.patch.object(policy, 'authorize', spec=True)
|
@mock.patch.object(policy, 'authorize', spec=True)
|
||||||
def test_detail_list_all_forbid_owner_proj_mismatch(self, mock_authorize):
|
def test_detail_list_all_forbid_project_mismatch(self, mock_authorize):
|
||||||
def mock_authorize_function(rule, target, creds):
|
def mock_authorize_function(rule, target, creds):
|
||||||
if rule == 'baremetal:node:list_all':
|
if rule == 'baremetal:node:list_all':
|
||||||
raise exception.HTTPForbidden(resource='fake')
|
raise exception.HTTPForbidden(resource='fake')
|
||||||
return True
|
return True
|
||||||
mock_authorize.side_effect = mock_authorize_function
|
mock_authorize.side_effect = mock_authorize_function
|
||||||
|
|
||||||
response = self.get_json('/nodes/detail?owner=54321',
|
response = self.get_json('/nodes/detail?project=54321',
|
||||||
expect_errors=True,
|
expect_errors=True,
|
||||||
headers={
|
headers={
|
||||||
api_base.Version.string: '1.50',
|
api_base.Version.string: '1.50',
|
||||||
@ -788,17 +817,27 @@ class TestListNodes(test_api_base.BaseApiTest):
|
|||||||
mock_authorize.side_effect = mock_authorize_function
|
mock_authorize.side_effect = mock_authorize_function
|
||||||
|
|
||||||
nodes = []
|
nodes = []
|
||||||
for id in range(5):
|
for id in range(3):
|
||||||
node = obj_utils.create_test_node(self.context,
|
node = obj_utils.create_test_node(self.context,
|
||||||
uuid=uuidutils.generate_uuid(),
|
uuid=uuidutils.generate_uuid(),
|
||||||
owner='12345')
|
owner='12345')
|
||||||
nodes.append(node.uuid)
|
nodes.append(node.uuid)
|
||||||
|
for id in range(3):
|
||||||
|
node = obj_utils.create_test_node(self.context,
|
||||||
|
uuid=uuidutils.generate_uuid(),
|
||||||
|
lessee='12345')
|
||||||
|
nodes.append(node.uuid)
|
||||||
for id in range(2):
|
for id in range(2):
|
||||||
node = obj_utils.create_test_node(self.context,
|
node = obj_utils.create_test_node(self.context,
|
||||||
uuid=uuidutils.generate_uuid())
|
uuid=uuidutils.generate_uuid(),
|
||||||
|
owner='54321')
|
||||||
|
for id in range(2):
|
||||||
|
node = obj_utils.create_test_node(self.context,
|
||||||
|
uuid=uuidutils.generate_uuid(),
|
||||||
|
lessee='54321')
|
||||||
|
|
||||||
data = self.get_json('/nodes/detail', headers={
|
data = self.get_json('/nodes/detail', headers={
|
||||||
api_base.Version.string: '1.50',
|
api_base.Version.string: '1.65',
|
||||||
'X-Project-Id': '12345'})
|
'X-Project-Id': '12345'})
|
||||||
self.assertEqual(len(nodes), len(data['nodes']))
|
self.assertEqual(len(nodes), len(data['nodes']))
|
||||||
|
|
||||||
@ -1005,14 +1044,14 @@ class TestListNodes(test_api_base.BaseApiTest):
|
|||||||
self.assertEqual(http_client.FORBIDDEN, response.status_int)
|
self.assertEqual(http_client.FORBIDDEN, response.status_int)
|
||||||
|
|
||||||
@mock.patch.object(policy, 'authorize', spec=True)
|
@mock.patch.object(policy, 'authorize', spec=True)
|
||||||
def test_many_list_all_forbid_owner_proj_mismatch(self, mock_authorize):
|
def test_many_list_all_forbid_project_mismatch(self, mock_authorize):
|
||||||
def mock_authorize_function(rule, target, creds):
|
def mock_authorize_function(rule, target, creds):
|
||||||
if rule == 'baremetal:node:list_all':
|
if rule == 'baremetal:node:list_all':
|
||||||
raise exception.HTTPForbidden(resource='fake')
|
raise exception.HTTPForbidden(resource='fake')
|
||||||
return True
|
return True
|
||||||
mock_authorize.side_effect = mock_authorize_function
|
mock_authorize.side_effect = mock_authorize_function
|
||||||
|
|
||||||
response = self.get_json('/nodes?owner=54321',
|
response = self.get_json('/nodes?project=54321',
|
||||||
expect_errors=True,
|
expect_errors=True,
|
||||||
headers={
|
headers={
|
||||||
api_base.Version.string: '1.50',
|
api_base.Version.string: '1.50',
|
||||||
@ -1029,17 +1068,27 @@ class TestListNodes(test_api_base.BaseApiTest):
|
|||||||
mock_authorize.side_effect = mock_authorize_function
|
mock_authorize.side_effect = mock_authorize_function
|
||||||
|
|
||||||
nodes = []
|
nodes = []
|
||||||
for id in range(5):
|
for id in range(3):
|
||||||
node = obj_utils.create_test_node(self.context,
|
node = obj_utils.create_test_node(self.context,
|
||||||
uuid=uuidutils.generate_uuid(),
|
uuid=uuidutils.generate_uuid(),
|
||||||
owner='12345')
|
owner='12345')
|
||||||
nodes.append(node.uuid)
|
nodes.append(node.uuid)
|
||||||
|
for id in range(3):
|
||||||
|
node = obj_utils.create_test_node(self.context,
|
||||||
|
uuid=uuidutils.generate_uuid(),
|
||||||
|
lessee='12345')
|
||||||
|
nodes.append(node.uuid)
|
||||||
for id in range(2):
|
for id in range(2):
|
||||||
node = obj_utils.create_test_node(self.context,
|
node = obj_utils.create_test_node(self.context,
|
||||||
uuid=uuidutils.generate_uuid())
|
uuid=uuidutils.generate_uuid(),
|
||||||
|
owner='54321')
|
||||||
|
for id in range(2):
|
||||||
|
node = obj_utils.create_test_node(self.context,
|
||||||
|
uuid=uuidutils.generate_uuid(),
|
||||||
|
lessee='54321')
|
||||||
|
|
||||||
data = self.get_json('/nodes', headers={
|
data = self.get_json('/nodes', headers={
|
||||||
api_base.Version.string: '1.50',
|
api_base.Version.string: '1.65',
|
||||||
'X-Project-Id': '12345'})
|
'X-Project-Id': '12345'})
|
||||||
self.assertEqual(len(nodes), len(data['nodes']))
|
self.assertEqual(len(nodes), len(data['nodes']))
|
||||||
|
|
||||||
@ -1956,6 +2005,36 @@ class TestListNodes(test_api_base.BaseApiTest):
|
|||||||
self.assertIn(node2.uuid, uuids)
|
self.assertIn(node2.uuid, uuids)
|
||||||
self.assertNotIn(node1.uuid, uuids)
|
self.assertNotIn(node1.uuid, uuids)
|
||||||
|
|
||||||
|
def test_get_nodes_by_lessee(self):
|
||||||
|
node1 = obj_utils.create_test_node(self.context,
|
||||||
|
uuid=uuidutils.generate_uuid(),
|
||||||
|
lessee='project1')
|
||||||
|
node2 = obj_utils.create_test_node(self.context,
|
||||||
|
uuid=uuidutils.generate_uuid(),
|
||||||
|
lessee='project2')
|
||||||
|
|
||||||
|
for base_url in ('/nodes', '/nodes/detail'):
|
||||||
|
data = self.get_json(base_url + '?lessee=project1',
|
||||||
|
headers={api_base.Version.string: "1.65"})
|
||||||
|
uuids = [n['uuid'] for n in data['nodes']]
|
||||||
|
self.assertIn(node1.uuid, uuids)
|
||||||
|
self.assertNotIn(node2.uuid, uuids)
|
||||||
|
data = self.get_json(base_url + '?lessee=project2',
|
||||||
|
headers={api_base.Version.string: "1.65"})
|
||||||
|
uuids = [n['uuid'] for n in data['nodes']]
|
||||||
|
self.assertIn(node2.uuid, uuids)
|
||||||
|
self.assertNotIn(node1.uuid, uuids)
|
||||||
|
|
||||||
|
def test_get_nodes_by_lessee_not_allowed(self):
|
||||||
|
for url in ('/nodes?lessee=project1',
|
||||||
|
'/nodes/detail?lessee=project1'):
|
||||||
|
response = self.get_json(
|
||||||
|
url, headers={api_base.Version.string: "1.64"},
|
||||||
|
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'}
|
||||||
@ -3474,6 +3553,33 @@ class TestPatch(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_code)
|
self.assertEqual(http_client.BAD_REQUEST, response.status_code)
|
||||||
|
|
||||||
|
def test_update_lessee(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.65'}
|
||||||
|
response = self.patch_json('/nodes/%s' % node.uuid,
|
||||||
|
[{'path': '/lessee',
|
||||||
|
'value': 'new-project',
|
||||||
|
'op': 'replace'}],
|
||||||
|
headers=headers)
|
||||||
|
self.assertEqual('application/json', response.content_type)
|
||||||
|
self.assertEqual(http_client.OK, response.status_code)
|
||||||
|
|
||||||
|
def test_update_lessee_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.64'}
|
||||||
|
response = self.patch_json('/nodes/%s' % node.uuid,
|
||||||
|
[{'path': '/lessee',
|
||||||
|
'value': 'new-project',
|
||||||
|
'op': 'replace'}],
|
||||||
|
headers=headers,
|
||||||
|
expect_errors=True)
|
||||||
|
self.assertEqual('application/json', response.content_type)
|
||||||
|
self.assertEqual(http_client.NOT_ACCEPTABLE, response.status_code)
|
||||||
|
|
||||||
def test_patch_allocation_forbidden(self):
|
def test_patch_allocation_forbidden(self):
|
||||||
node = obj_utils.create_test_node(self.context,
|
node = obj_utils.create_test_node(self.context,
|
||||||
uuid=uuidutils.generate_uuid())
|
uuid=uuidutils.generate_uuid())
|
||||||
@ -4359,6 +4465,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_lessee(self):
|
||||||
|
ndict = test_api_utils.post_get_test_node(lessee='project')
|
||||||
|
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('project', result['lessee'])
|
||||||
|
|
||||||
|
def test_create_node_lessee_old_api_version(self):
|
||||||
|
headers = {api_base.Version.string: '1.64'}
|
||||||
|
ndict = test_api_utils.post_get_test_node(lessee='project')
|
||||||
|
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):
|
||||||
|
|
||||||
|
@ -803,6 +803,7 @@ class TestCheckOwnerPolicy(base.TestCase):
|
|||||||
self.valid_node_uuid = uuidutils.generate_uuid()
|
self.valid_node_uuid = uuidutils.generate_uuid()
|
||||||
self.node = test_api_utils.post_get_test_node()
|
self.node = test_api_utils.post_get_test_node()
|
||||||
self.node['owner'] = '12345'
|
self.node['owner'] = '12345'
|
||||||
|
self.node['lessee'] = '54321'
|
||||||
|
|
||||||
@mock.patch.object(api, 'request', spec_set=["context", "version"])
|
@mock.patch.object(api, 'request', spec_set=["context", "version"])
|
||||||
@mock.patch.object(policy, 'authorize', spec=True)
|
@mock.patch.object(policy, 'authorize', spec=True)
|
||||||
@ -813,10 +814,11 @@ class TestCheckOwnerPolicy(base.TestCase):
|
|||||||
mock_pr.context.to_policy_values.return_value = {}
|
mock_pr.context.to_policy_values.return_value = {}
|
||||||
|
|
||||||
utils.check_owner_policy(
|
utils.check_owner_policy(
|
||||||
'node', 'fake_policy', self.node['owner']
|
'node', 'fake_policy', self.node['owner'], self.node['lessee']
|
||||||
)
|
)
|
||||||
mock_authorize.assert_called_once_with(
|
mock_authorize.assert_called_once_with(
|
||||||
'fake_policy', {'node.owner': '12345'}, {})
|
'fake_policy',
|
||||||
|
{'node.owner': '12345', 'node.lessee': '54321'}, {})
|
||||||
|
|
||||||
@mock.patch.object(api, 'request', spec_set=["context", "version"])
|
@mock.patch.object(api, 'request', spec_set=["context", "version"])
|
||||||
@mock.patch.object(policy, 'authorize', spec=True)
|
@mock.patch.object(policy, 'authorize', spec=True)
|
||||||
@ -832,7 +834,7 @@ class TestCheckOwnerPolicy(base.TestCase):
|
|||||||
utils.check_owner_policy,
|
utils.check_owner_policy,
|
||||||
'node',
|
'node',
|
||||||
'fake-policy',
|
'fake-policy',
|
||||||
self.node['owner']
|
self.node
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -842,6 +844,7 @@ class TestCheckNodePolicyAndRetrieve(base.TestCase):
|
|||||||
self.valid_node_uuid = uuidutils.generate_uuid()
|
self.valid_node_uuid = uuidutils.generate_uuid()
|
||||||
self.node = test_api_utils.post_get_test_node()
|
self.node = test_api_utils.post_get_test_node()
|
||||||
self.node['owner'] = '12345'
|
self.node['owner'] = '12345'
|
||||||
|
self.node['lessee'] = '54321'
|
||||||
|
|
||||||
@mock.patch.object(api, 'request', spec_set=["context", "version"])
|
@mock.patch.object(api, 'request', spec_set=["context", "version"])
|
||||||
@mock.patch.object(policy, 'authorize', spec=True)
|
@mock.patch.object(policy, 'authorize', spec=True)
|
||||||
@ -860,7 +863,8 @@ class TestCheckNodePolicyAndRetrieve(base.TestCase):
|
|||||||
mock_grn.assert_called_once_with(self.valid_node_uuid)
|
mock_grn.assert_called_once_with(self.valid_node_uuid)
|
||||||
mock_grnws.assert_not_called()
|
mock_grnws.assert_not_called()
|
||||||
mock_authorize.assert_called_once_with(
|
mock_authorize.assert_called_once_with(
|
||||||
'fake_policy', {'node.owner': '12345'}, {})
|
'fake_policy',
|
||||||
|
{'node.owner': '12345', 'node.lessee': '54321'}, {})
|
||||||
self.assertEqual(self.node, rpc_node)
|
self.assertEqual(self.node, rpc_node)
|
||||||
|
|
||||||
@mock.patch.object(api, 'request', spec_set=["context", "version"])
|
@mock.patch.object(api, 'request', spec_set=["context", "version"])
|
||||||
@ -880,7 +884,8 @@ class TestCheckNodePolicyAndRetrieve(base.TestCase):
|
|||||||
mock_grn.assert_not_called()
|
mock_grn.assert_not_called()
|
||||||
mock_grnws.assert_called_once_with(self.valid_node_uuid)
|
mock_grnws.assert_called_once_with(self.valid_node_uuid)
|
||||||
mock_authorize.assert_called_once_with(
|
mock_authorize.assert_called_once_with(
|
||||||
'fake_policy', {'node.owner': '12345'}, {})
|
'fake_policy',
|
||||||
|
{'node.owner': '12345', 'node.lessee': '54321'}, {})
|
||||||
self.assertEqual(self.node, rpc_node)
|
self.assertEqual(self.node, rpc_node)
|
||||||
|
|
||||||
@mock.patch.object(api, 'request', spec_set=["context"])
|
@mock.patch.object(api, 'request', spec_set=["context"])
|
||||||
@ -1022,6 +1027,7 @@ class TestCheckMultipleNodePoliciesAndRetrieve(base.TestCase):
|
|||||||
self.valid_node_uuid = uuidutils.generate_uuid()
|
self.valid_node_uuid = uuidutils.generate_uuid()
|
||||||
self.node = test_api_utils.post_get_test_node()
|
self.node = test_api_utils.post_get_test_node()
|
||||||
self.node['owner'] = '12345'
|
self.node['owner'] = '12345'
|
||||||
|
self.node['lessee'] = '54321'
|
||||||
|
|
||||||
@mock.patch.object(utils, 'check_node_policy_and_retrieve')
|
@mock.patch.object(utils, 'check_node_policy_and_retrieve')
|
||||||
@mock.patch.object(utils, 'check_owner_policy')
|
@mock.patch.object(utils, 'check_owner_policy')
|
||||||
@ -1037,7 +1043,7 @@ class TestCheckMultipleNodePoliciesAndRetrieve(base.TestCase):
|
|||||||
mock_cnpar.assert_called_once_with('fake_policy_1',
|
mock_cnpar.assert_called_once_with('fake_policy_1',
|
||||||
self.valid_node_uuid, False)
|
self.valid_node_uuid, False)
|
||||||
mock_cop.assert_called_once_with(
|
mock_cop.assert_called_once_with(
|
||||||
'node', 'fake_policy_2', '12345')
|
'node', 'fake_policy_2', '12345', '54321')
|
||||||
self.assertEqual(self.node, rpc_node)
|
self.assertEqual(self.node, rpc_node)
|
||||||
|
|
||||||
@mock.patch.object(utils, 'check_node_policy_and_retrieve')
|
@mock.patch.object(utils, 'check_node_policy_and_retrieve')
|
||||||
@ -1075,7 +1081,7 @@ class TestCheckMultipleNodePoliciesAndRetrieve(base.TestCase):
|
|||||||
mock_cnpar.assert_called_once_with('fake_policy_1',
|
mock_cnpar.assert_called_once_with('fake_policy_1',
|
||||||
self.valid_node_uuid, False)
|
self.valid_node_uuid, False)
|
||||||
mock_cop.assert_called_once_with(
|
mock_cop.assert_called_once_with(
|
||||||
'node', 'fake_policy_2', '12345')
|
'node', 'fake_policy_2', '12345', '54321')
|
||||||
|
|
||||||
|
|
||||||
class TestCheckListPolicy(base.TestCase):
|
class TestCheckListPolicy(base.TestCase):
|
||||||
@ -1190,6 +1196,7 @@ class TestCheckPortPolicyAndRetrieve(base.TestCase):
|
|||||||
self.valid_port_uuid = uuidutils.generate_uuid()
|
self.valid_port_uuid = uuidutils.generate_uuid()
|
||||||
self.node = test_api_utils.post_get_test_node()
|
self.node = test_api_utils.post_get_test_node()
|
||||||
self.node['owner'] = '12345'
|
self.node['owner'] = '12345'
|
||||||
|
self.node['lessee'] = '54321'
|
||||||
self.port = objects.Port(self.context, node_id=42)
|
self.port = objects.Port(self.context, node_id=42)
|
||||||
|
|
||||||
@mock.patch.object(api, 'request', spec_set=["context", "version"])
|
@mock.patch.object(api, 'request', spec_set=["context", "version"])
|
||||||
@ -1211,7 +1218,9 @@ class TestCheckPortPolicyAndRetrieve(base.TestCase):
|
|||||||
self.valid_port_uuid)
|
self.valid_port_uuid)
|
||||||
mock_ngbi.assert_called_once_with(mock_pr.context, 42)
|
mock_ngbi.assert_called_once_with(mock_pr.context, 42)
|
||||||
mock_authorize.assert_called_once_with(
|
mock_authorize.assert_called_once_with(
|
||||||
'fake_policy', {'node.owner': '12345'}, {})
|
'fake_policy',
|
||||||
|
{'node.owner': '12345', 'node.lessee': '54321'},
|
||||||
|
{})
|
||||||
self.assertEqual(self.port, rpc_port)
|
self.assertEqual(self.port, rpc_port)
|
||||||
self.assertEqual(self.node, rpc_node)
|
self.assertEqual(self.node, rpc_node)
|
||||||
|
|
||||||
|
@ -69,6 +69,19 @@ class PolicyInCodeTestCase(base.TestCase):
|
|||||||
self.assertTrue(policy.check('is_node_owner', target, c1))
|
self.assertTrue(policy.check('is_node_owner', target, c1))
|
||||||
self.assertFalse(policy.check('is_node_owner', target, c2))
|
self.assertFalse(policy.check('is_node_owner', target, c2))
|
||||||
|
|
||||||
|
def test_is_node_lessee(self):
|
||||||
|
c1 = {'project_id': '1234',
|
||||||
|
'project_name': 'demo',
|
||||||
|
'project_domain_id': 'default'}
|
||||||
|
c2 = {'project_id': '5678',
|
||||||
|
'project_name': 'demo',
|
||||||
|
'project_domain_id': 'default'}
|
||||||
|
target = dict.copy(c1)
|
||||||
|
target['node.lessee'] = '1234'
|
||||||
|
|
||||||
|
self.assertTrue(policy.check('is_node_lessee', target, c1))
|
||||||
|
self.assertFalse(policy.check('is_node_lessee', target, c2))
|
||||||
|
|
||||||
def test_is_allocation_owner(self):
|
def test_is_allocation_owner(self):
|
||||||
c1 = {'project_id': '1234',
|
c1 = {'project_id': '1234',
|
||||||
'project_name': 'demo',
|
'project_name': 'demo',
|
||||||
|
@ -350,14 +350,20 @@ class DoAllocateTestCase(db_base.DbTestCase):
|
|||||||
|
|
||||||
@mock.patch.object(task_manager, 'acquire', autospec=True,
|
@mock.patch.object(task_manager, 'acquire', autospec=True,
|
||||||
side_effect=task_manager.acquire)
|
side_effect=task_manager.acquire)
|
||||||
def test_nodes_filtered_out_owner(self, mock_acquire):
|
def test_nodes_filtered_out_project(self, mock_acquire):
|
||||||
# Owner does not match
|
# Owner and lessee do not match
|
||||||
obj_utils.create_test_node(self.context,
|
obj_utils.create_test_node(self.context,
|
||||||
uuid=uuidutils.generate_uuid(),
|
uuid=uuidutils.generate_uuid(),
|
||||||
owner='54321',
|
owner='54321',
|
||||||
resource_class='x-large',
|
resource_class='x-large',
|
||||||
power_state='power off',
|
power_state='power off',
|
||||||
provision_state='available')
|
provision_state='available')
|
||||||
|
obj_utils.create_test_node(self.context,
|
||||||
|
uuid=uuidutils.generate_uuid(),
|
||||||
|
lessee='54321',
|
||||||
|
resource_class='x-large',
|
||||||
|
power_state='power off',
|
||||||
|
provision_state='available')
|
||||||
|
|
||||||
allocation = obj_utils.create_test_allocation(self.context,
|
allocation = obj_utils.create_test_allocation(self.context,
|
||||||
resource_class='x-large',
|
resource_class='x-large',
|
||||||
|
@ -990,6 +990,11 @@ class MigrationCheckersMixin(object):
|
|||||||
self.assertFalse(node['retired'])
|
self.assertFalse(node['retired'])
|
||||||
self.assertIsNone(node['retired_reason'])
|
self.assertIsNone(node['retired_reason'])
|
||||||
|
|
||||||
|
def _check_b2ad35726bb0(self, engine, data):
|
||||||
|
nodes = db_utils.get_table(engine, 'nodes')
|
||||||
|
col_names = [column.name for column in nodes.c]
|
||||||
|
self.assertIn('lessee', 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')
|
||||||
|
@ -399,6 +399,35 @@ class DbNodeTestCase(base.DbTestCase):
|
|||||||
self.dbapi.get_node_list,
|
self.dbapi.get_node_list,
|
||||||
filters=filters)
|
filters=filters)
|
||||||
|
|
||||||
|
def test_get_node_list_filter_by_project(self):
|
||||||
|
utils.create_test_node(uuid=uuidutils.generate_uuid())
|
||||||
|
node2 = utils.create_test_node(
|
||||||
|
uuid=uuidutils.generate_uuid(),
|
||||||
|
owner='project1',
|
||||||
|
lessee='project2',
|
||||||
|
)
|
||||||
|
node3 = utils.create_test_node(
|
||||||
|
uuid=uuidutils.generate_uuid(),
|
||||||
|
owner='project2',
|
||||||
|
)
|
||||||
|
node4 = utils.create_test_node(
|
||||||
|
uuid=uuidutils.generate_uuid(),
|
||||||
|
owner='project1',
|
||||||
|
lessee='project3',
|
||||||
|
)
|
||||||
|
|
||||||
|
res = self.dbapi.get_node_list(filters={'project': 'project1'})
|
||||||
|
self.assertEqual([node2.id, node4.id], [r.id for r in res])
|
||||||
|
|
||||||
|
res = self.dbapi.get_node_list(filters={'project': 'project2'})
|
||||||
|
self.assertEqual([node2.id, node3.id], [r.id for r in res])
|
||||||
|
|
||||||
|
res = self.dbapi.get_node_list(filters={'project': 'project3'})
|
||||||
|
self.assertEqual([node4.id], [r.id for r in res])
|
||||||
|
|
||||||
|
res = self.dbapi.get_node_list(filters={'project': 'flargle'})
|
||||||
|
self.assertEqual([], [r.id for r in res])
|
||||||
|
|
||||||
def test_get_node_list_description(self):
|
def test_get_node_list_description(self):
|
||||||
node1 = utils.create_test_node(uuid=uuidutils.generate_uuid(),
|
node1 = utils.create_test_node(uuid=uuidutils.generate_uuid(),
|
||||||
description='Hello')
|
description='Hello')
|
||||||
|
@ -226,7 +226,7 @@ def get_test_node(**kw):
|
|||||||
'description': kw.get('description'),
|
'description': kw.get('description'),
|
||||||
'retired': kw.get('retired', False),
|
'retired': kw.get('retired', False),
|
||||||
'retired_reason': kw.get('retired_reason', None),
|
'retired_reason': kw.get('retired_reason', None),
|
||||||
|
'lessee': kw.get('lessee', None),
|
||||||
}
|
}
|
||||||
|
|
||||||
for iface in drivers_base.ALL_INTERFACES:
|
for iface in drivers_base.ALL_INTERFACES:
|
||||||
|
@ -1134,6 +1134,68 @@ class TestConvertToVersion(db_base.DbTestCase):
|
|||||||
self.assertIsNone(node.description)
|
self.assertIsNone(node.description)
|
||||||
self.assertEqual({}, node.obj_get_changes())
|
self.assertEqual({}, node.obj_get_changes())
|
||||||
|
|
||||||
|
def test_lessee_supported_missing(self):
|
||||||
|
# lessee not set, should be set to default.
|
||||||
|
node = obj_utils.get_test_node(self.ctxt, **self.fake_node)
|
||||||
|
delattr(node, 'lessee')
|
||||||
|
node.obj_reset_changes()
|
||||||
|
node._convert_to_version("1.34")
|
||||||
|
self.assertIsNone(node.lessee)
|
||||||
|
self.assertEqual({'lessee': None},
|
||||||
|
node.obj_get_changes())
|
||||||
|
|
||||||
|
def test_lessee_supported_set(self):
|
||||||
|
# lessee set, no change required.
|
||||||
|
node = obj_utils.get_test_node(self.ctxt, **self.fake_node)
|
||||||
|
|
||||||
|
node.lessee = "some-lucky-project"
|
||||||
|
node.obj_reset_changes()
|
||||||
|
node._convert_to_version("1.34")
|
||||||
|
self.assertEqual("some-lucky-project",
|
||||||
|
node.lessee)
|
||||||
|
self.assertEqual({}, node.obj_get_changes())
|
||||||
|
|
||||||
|
def test_lessee_unsupported_missing(self):
|
||||||
|
# lessee not set, no change required.
|
||||||
|
node = obj_utils.get_test_node(self.ctxt, **self.fake_node)
|
||||||
|
|
||||||
|
delattr(node, 'lessee')
|
||||||
|
node.obj_reset_changes()
|
||||||
|
node._convert_to_version("1.33")
|
||||||
|
self.assertNotIn('lessee', node)
|
||||||
|
self.assertEqual({}, node.obj_get_changes())
|
||||||
|
|
||||||
|
def test_lessee_unsupported_set_remove(self):
|
||||||
|
# lessee set, should be removed.
|
||||||
|
node = obj_utils.get_test_node(self.ctxt, **self.fake_node)
|
||||||
|
|
||||||
|
node.lessee = "some-lucky-project"
|
||||||
|
node.obj_reset_changes()
|
||||||
|
node._convert_to_version("1.33")
|
||||||
|
self.assertNotIn('lessee', node)
|
||||||
|
self.assertEqual({}, node.obj_get_changes())
|
||||||
|
|
||||||
|
def test_lessee_unsupported_set_no_remove_non_default(self):
|
||||||
|
# lessee set, should be set to default.
|
||||||
|
node = obj_utils.get_test_node(self.ctxt, **self.fake_node)
|
||||||
|
|
||||||
|
node.lessee = "some-lucky-project"
|
||||||
|
node.obj_reset_changes()
|
||||||
|
node._convert_to_version("1.33", False)
|
||||||
|
self.assertIsNone(node.lessee)
|
||||||
|
self.assertEqual({'lessee': None},
|
||||||
|
node.obj_get_changes())
|
||||||
|
|
||||||
|
def test_lessee_unsupported_set_no_remove_default(self):
|
||||||
|
# lessee set, no change required.
|
||||||
|
node = obj_utils.get_test_node(self.ctxt, **self.fake_node)
|
||||||
|
|
||||||
|
node.lessee = None
|
||||||
|
node.obj_reset_changes()
|
||||||
|
node._convert_to_version("1.33", False)
|
||||||
|
self.assertIsNone(node.lessee)
|
||||||
|
self.assertEqual({}, node.obj_get_changes())
|
||||||
|
|
||||||
|
|
||||||
class TestNodePayloads(db_base.DbTestCase):
|
class TestNodePayloads(db_base.DbTestCase):
|
||||||
|
|
||||||
|
@ -676,7 +676,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.33-d6a8ba8dd3be3b2bbad0e0a5b9887aa8',
|
'Node': '1.34-ae873e627cf30bf28fe9f98a807b6200',
|
||||||
'MyObj': '1.5-9459d30d6954bffc7a9afd347a807ca6',
|
'MyObj': '1.5-9459d30d6954bffc7a9afd347a807ca6',
|
||||||
'Chassis': '1.3-d656e039fd8ae9f34efc232ab3980905',
|
'Chassis': '1.3-d656e039fd8ae9f34efc232ab3980905',
|
||||||
'Port': '1.9-0cb9202a4ec442e8c0d87a324155eaaf',
|
'Port': '1.9-0cb9202a4ec442e8c0d87a324155eaaf',
|
||||||
@ -684,21 +684,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.14-8b2dfc37d800f268d29a580ac034e2c6',
|
'NodePayload': '1.15-86ee30dbf374be4cf17c5b501d9e2e7b',
|
||||||
'NodeSetPowerStateNotification': '1.0-59acc533c11d306f149846f922739c15',
|
'NodeSetPowerStateNotification': '1.0-59acc533c11d306f149846f922739c15',
|
||||||
'NodeSetPowerStatePayload': '1.14-dcd4d7911717ba323ab4c3297b92c31c',
|
'NodeSetPowerStatePayload': '1.15-3c64b07a2b96c2661e7743b47ed43705',
|
||||||
'NodeCorrectedPowerStateNotification':
|
'NodeCorrectedPowerStateNotification':
|
||||||
'1.0-59acc533c11d306f149846f922739c15',
|
'1.0-59acc533c11d306f149846f922739c15',
|
||||||
'NodeCorrectedPowerStatePayload': '1.14-c7d20e953bbb9a1a4ce31ce22068e4bf',
|
'NodeCorrectedPowerStatePayload': '1.15-59a224a9191cdc9f1acc2e0dcd2d3adb',
|
||||||
'NodeSetProvisionStateNotification':
|
'NodeSetProvisionStateNotification':
|
||||||
'1.0-59acc533c11d306f149846f922739c15',
|
'1.0-59acc533c11d306f149846f922739c15',
|
||||||
'NodeSetProvisionStatePayload': '1.14-6d4145044a98c5cc80a40d69bbd98f61',
|
'NodeSetProvisionStatePayload': '1.15-488a3d62a0643d17e288ecf89ed5bbb4',
|
||||||
'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.12-3f63cdace5159785535049025ddf6a5c',
|
'NodeCRUDPayload': '1.13-8f673253ff8d7389897a6a80d224ac33',
|
||||||
'PortCRUDNotification': '1.0-59acc533c11d306f149846f922739c15',
|
'PortCRUDNotification': '1.0-59acc533c11d306f149846f922739c15',
|
||||||
'PortCRUDPayload': '1.3-21235916ed54a91b2a122f59571194e7',
|
'PortCRUDPayload': '1.3-21235916ed54a91b2a122f59571194e7',
|
||||||
'NodeMaintenanceNotification': '1.0-59acc533c11d306f149846f922739c15',
|
'NodeMaintenanceNotification': '1.0-59acc533c11d306f149846f922739c15',
|
||||||
|
5
releasenotes/notes/node-lessee-4fb320a597192742.yaml
Normal file
5
releasenotes/notes/node-lessee-4fb320a597192742.yaml
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
features:
|
||||||
|
- |
|
||||||
|
Adds a ``lessee`` field to nodes. This field is exposed to policy, so if
|
||||||
|
a policy file permits, a lessee will have access to specified node APIs.
|
Loading…
Reference in New Issue
Block a user