Add owner to allocations and create relevant policies
Add an owner to allocations. Depending on policy, a non-admin can then create an allocation and have the owner set to their project. Allocation processing then respects the owner. Change-Id: I2965a4a601b9fa2c0212097da37b104a3e5514df Story: #2006506 Task: #37540
This commit is contained in:
parent
306aaccca6
commit
3fbb560af1
@ -47,6 +47,9 @@ parameters must be missing or match the provided node.
|
|||||||
.. versionadded:: 1.58
|
.. versionadded:: 1.58
|
||||||
Added support for backfilling allocations.
|
Added support for backfilling allocations.
|
||||||
|
|
||||||
|
.. versionadded:: 1.60
|
||||||
|
Introduced the ``owner`` field.
|
||||||
|
|
||||||
Normal response codes: 201
|
Normal response codes: 201
|
||||||
|
|
||||||
Error response codes: 400, 401, 403, 409, 503
|
Error response codes: 400, 401, 403, 409, 503
|
||||||
@ -63,6 +66,7 @@ Request
|
|||||||
- uuid: req_uuid
|
- uuid: req_uuid
|
||||||
- extra: req_extra
|
- extra: req_extra
|
||||||
- node: req_allocation_node
|
- node: req_allocation_node
|
||||||
|
- owner: owner
|
||||||
|
|
||||||
Request Example
|
Request Example
|
||||||
---------------
|
---------------
|
||||||
@ -83,6 +87,7 @@ Response Parameters
|
|||||||
- resource_class: allocation_resource_class
|
- resource_class: allocation_resource_class
|
||||||
- state: allocation_state
|
- state: allocation_state
|
||||||
- traits: allocation_traits
|
- traits: allocation_traits
|
||||||
|
- owner: owner
|
||||||
- extra: extra
|
- extra: extra
|
||||||
- created_at: created_at
|
- created_at: created_at
|
||||||
- updated_at: updated_at
|
- updated_at: updated_at
|
||||||
@ -104,6 +109,9 @@ Lists all Allocations.
|
|||||||
.. versionadded:: 1.52
|
.. versionadded:: 1.52
|
||||||
Allocation API was introduced.
|
Allocation API was introduced.
|
||||||
|
|
||||||
|
.. versionadded:: 1.60
|
||||||
|
Introduced the ``owner`` field.
|
||||||
|
|
||||||
Normal response codes: 200
|
Normal response codes: 200
|
||||||
|
|
||||||
Error response codes: 400, 401, 403, 404
|
Error response codes: 400, 401, 403, 404
|
||||||
@ -116,6 +124,7 @@ Request
|
|||||||
- node: r_allocation_node
|
- node: r_allocation_node
|
||||||
- resource_class: r_resource_class
|
- resource_class: r_resource_class
|
||||||
- state: r_allocation_state
|
- state: r_allocation_state
|
||||||
|
- owner: owner
|
||||||
- fields: fields
|
- fields: fields
|
||||||
- limit: limit
|
- limit: limit
|
||||||
- marker: marker
|
- marker: marker
|
||||||
@ -135,6 +144,7 @@ Response Parameters
|
|||||||
- resource_class: allocation_resource_class
|
- resource_class: allocation_resource_class
|
||||||
- state: allocation_state
|
- state: allocation_state
|
||||||
- traits: allocation_traits
|
- traits: allocation_traits
|
||||||
|
- owner: owner
|
||||||
- extra: extra
|
- extra: extra
|
||||||
- created_at: created_at
|
- created_at: created_at
|
||||||
- updated_at: updated_at
|
- updated_at: updated_at
|
||||||
@ -156,6 +166,9 @@ Shows details for an Allocation.
|
|||||||
.. versionadded:: 1.52
|
.. versionadded:: 1.52
|
||||||
Allocation API was introduced.
|
Allocation API was introduced.
|
||||||
|
|
||||||
|
.. versionadded:: 1.60
|
||||||
|
Introduced the ``owner`` field.
|
||||||
|
|
||||||
Normal response codes: 200
|
Normal response codes: 200
|
||||||
|
|
||||||
Error response codes: 400, 401, 403, 404
|
Error response codes: 400, 401, 403, 404
|
||||||
@ -181,6 +194,7 @@ Response Parameters
|
|||||||
- resource_class: allocation_resource_class
|
- resource_class: allocation_resource_class
|
||||||
- state: allocation_state
|
- state: allocation_state
|
||||||
- traits: allocation_traits
|
- traits: allocation_traits
|
||||||
|
- owner: owner
|
||||||
- extra: extra
|
- extra: extra
|
||||||
- created_at: created_at
|
- created_at: created_at
|
||||||
- updated_at: updated_at
|
- updated_at: updated_at
|
||||||
@ -237,6 +251,7 @@ Response Parameters
|
|||||||
- resource_class: allocation_resource_class
|
- resource_class: allocation_resource_class
|
||||||
- state: allocation_state
|
- state: allocation_state
|
||||||
- traits: allocation_traits
|
- traits: allocation_traits
|
||||||
|
- owner: owner
|
||||||
- extra: extra
|
- extra: extra
|
||||||
- created_at: created_at
|
- created_at: created_at
|
||||||
- updated_at: updated_at
|
- updated_at: updated_at
|
||||||
|
@ -1048,7 +1048,7 @@ nodes:
|
|||||||
type: array
|
type: array
|
||||||
owner:
|
owner:
|
||||||
description: |
|
description: |
|
||||||
A string or UUID of the tenant who owns the baremetal node.
|
A string or UUID of the tenant who owns the object.
|
||||||
in: body
|
in: body
|
||||||
required: false
|
required: false
|
||||||
type: string
|
type: string
|
||||||
|
@ -15,6 +15,7 @@
|
|||||||
],
|
],
|
||||||
"name": "allocation-1",
|
"name": "allocation-1",
|
||||||
"node_uuid": null,
|
"node_uuid": null,
|
||||||
|
"owner": null,
|
||||||
"resource_class": "bm-large",
|
"resource_class": "bm-large",
|
||||||
"state": "allocating",
|
"state": "allocating",
|
||||||
"traits": [],
|
"traits": [],
|
||||||
|
@ -15,6 +15,7 @@
|
|||||||
],
|
],
|
||||||
"name": "allocation-1",
|
"name": "allocation-1",
|
||||||
"node_uuid": "6d85703a-565d-469a-96ce-30b6de53079d",
|
"node_uuid": "6d85703a-565d-469a-96ce-30b6de53079d",
|
||||||
|
"owner": null,
|
||||||
"resource_class": "bm-large",
|
"resource_class": "bm-large",
|
||||||
"state": "active",
|
"state": "active",
|
||||||
"traits": [],
|
"traits": [],
|
||||||
|
@ -17,6 +17,7 @@
|
|||||||
},
|
},
|
||||||
"last_error": null,
|
"last_error": null,
|
||||||
"created_at": "2019-06-04T07:46:25+00:00",
|
"created_at": "2019-06-04T07:46:25+00:00",
|
||||||
|
"owner": null,
|
||||||
"resource_class": "CUSTOM_GOLD",
|
"resource_class": "CUSTOM_GOLD",
|
||||||
"updated_at": "2019-06-06T03:28:19.496960+00:00",
|
"updated_at": "2019-06-06T03:28:19.496960+00:00",
|
||||||
"traits": [],
|
"traits": [],
|
||||||
|
@ -17,6 +17,7 @@
|
|||||||
],
|
],
|
||||||
"name": "allocation-1",
|
"name": "allocation-1",
|
||||||
"node_uuid": "6d85703a-565d-469a-96ce-30b6de53079d",
|
"node_uuid": "6d85703a-565d-469a-96ce-30b6de53079d",
|
||||||
|
"owner": null,
|
||||||
"resource_class": "bm-large",
|
"resource_class": "bm-large",
|
||||||
"state": "active",
|
"state": "active",
|
||||||
"traits": [],
|
"traits": [],
|
||||||
@ -40,6 +41,7 @@
|
|||||||
],
|
],
|
||||||
"name": "allocation-2",
|
"name": "allocation-2",
|
||||||
"node_uuid": null,
|
"node_uuid": null,
|
||||||
|
"owner": null,
|
||||||
"resource_class": "bm-large",
|
"resource_class": "bm-large",
|
||||||
"state": "error",
|
"state": "error",
|
||||||
"traits": [
|
"traits": [
|
||||||
|
@ -2,7 +2,16 @@
|
|||||||
REST API Version History
|
REST API Version History
|
||||||
========================
|
========================
|
||||||
|
|
||||||
|
1.60 (Ussuri, master)
|
||||||
|
---------------------
|
||||||
|
|
||||||
|
Added ``owner`` field to the allocation object. The field should match the
|
||||||
|
``project_id`` of the intended owner. If the ``owner`` field is set, the
|
||||||
|
allocation process will only match the allocation with a node that has the
|
||||||
|
same ``owner`` field set.
|
||||||
|
|
||||||
1.59 (Ussuri, master)
|
1.59 (Ussuri, master)
|
||||||
|
---------------------
|
||||||
|
|
||||||
Added the ability to specify a ``vendor_data`` dictionary field in the
|
Added the ability to specify a ``vendor_data`` dictionary field in the
|
||||||
``configdrive`` parameter submitted with the deployment of a node. The value
|
``configdrive`` parameter submitted with the deployment of a node. The value
|
||||||
|
@ -37,6 +37,12 @@ from ironic import objects
|
|||||||
METRICS = metrics_utils.get_metrics_logger(__name__)
|
METRICS = metrics_utils.get_metrics_logger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def hide_fields_in_newer_versions(obj):
|
||||||
|
# if requested version is < 1.60, hide owner field
|
||||||
|
if not api_utils.allow_allocation_owner():
|
||||||
|
obj.owner = wsme.Unset
|
||||||
|
|
||||||
|
|
||||||
class Allocation(base.APIBase):
|
class Allocation(base.APIBase):
|
||||||
"""API representation of an allocation.
|
"""API representation of an allocation.
|
||||||
|
|
||||||
@ -72,6 +78,9 @@ class Allocation(base.APIBase):
|
|||||||
resource_class = wsme.wsattr(wtypes.StringType(max_length=80))
|
resource_class = wsme.wsattr(wtypes.StringType(max_length=80))
|
||||||
"""Requested resource class for this allocation"""
|
"""Requested resource class for this allocation"""
|
||||||
|
|
||||||
|
owner = wsme.wsattr(wtypes.text)
|
||||||
|
"""Owner of allocation"""
|
||||||
|
|
||||||
# NOTE(dtantsur): candidate_nodes is a list of UUIDs on the database level,
|
# NOTE(dtantsur): candidate_nodes is a list of UUIDs on the database level,
|
||||||
# but the API level also accept names, converting them on fly.
|
# but the API level also accept names, converting them on fly.
|
||||||
candidate_nodes = wsme.wsattr([wtypes.text])
|
candidate_nodes = wsme.wsattr([wtypes.text])
|
||||||
@ -149,6 +158,8 @@ class Allocation(base.APIBase):
|
|||||||
:type fields: list of str
|
:type fields: list of str
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
hide_fields_in_newer_versions(self)
|
||||||
|
|
||||||
if fields is not None:
|
if fields is not None:
|
||||||
self.unset_fields_except(fields)
|
self.unset_fields_except(fields)
|
||||||
|
|
||||||
@ -165,7 +176,8 @@ class Allocation(base.APIBase):
|
|||||||
candidate_nodes=[],
|
candidate_nodes=[],
|
||||||
extra={'foo': 'bar'},
|
extra={'foo': 'bar'},
|
||||||
created_at=datetime.datetime(2000, 1, 1, 12, 0, 0),
|
created_at=datetime.datetime(2000, 1, 1, 12, 0, 0),
|
||||||
updated_at=datetime.datetime(2000, 1, 1, 12, 0, 0))
|
updated_at=datetime.datetime(2000, 1, 1, 12, 0, 0),
|
||||||
|
owner=None)
|
||||||
return cls._convert_with_links(sample, 'http://localhost:6385')
|
return cls._convert_with_links(sample, 'http://localhost:6385')
|
||||||
|
|
||||||
|
|
||||||
@ -223,8 +235,8 @@ class AllocationsController(pecan.rest.RestController):
|
|||||||
return super(AllocationsController, self)._route(args, request)
|
return super(AllocationsController, self)._route(args, request)
|
||||||
|
|
||||||
def _get_allocations_collection(self, node_ident=None, resource_class=None,
|
def _get_allocations_collection(self, node_ident=None, resource_class=None,
|
||||||
state=None, marker=None, limit=None,
|
state=None, owner=None, marker=None,
|
||||||
sort_key='id', sort_dir='asc',
|
limit=None, sort_key='id', sort_dir='asc',
|
||||||
resource_url=None, fields=None):
|
resource_url=None, fields=None):
|
||||||
"""Return allocations collection.
|
"""Return allocations collection.
|
||||||
|
|
||||||
@ -236,6 +248,7 @@ class AllocationsController(pecan.rest.RestController):
|
|||||||
:param resource_url: Optional, URL to the allocation resource.
|
:param resource_url: Optional, URL to the allocation resource.
|
||||||
: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 owner: project_id of owner to filter by
|
||||||
"""
|
"""
|
||||||
limit = api_utils.validate_limit(limit)
|
limit = api_utils.validate_limit(limit)
|
||||||
sort_dir = api_utils.validate_sort_dir(sort_dir)
|
sort_dir = api_utils.validate_sort_dir(sort_dir)
|
||||||
@ -262,7 +275,8 @@ class AllocationsController(pecan.rest.RestController):
|
|||||||
possible_filters = {
|
possible_filters = {
|
||||||
'node_uuid': node_uuid,
|
'node_uuid': node_uuid,
|
||||||
'resource_class': resource_class,
|
'resource_class': resource_class,
|
||||||
'state': state
|
'state': state,
|
||||||
|
'owner': owner
|
||||||
}
|
}
|
||||||
|
|
||||||
filters = {}
|
filters = {}
|
||||||
@ -282,12 +296,27 @@ class AllocationsController(pecan.rest.RestController):
|
|||||||
sort_key=sort_key,
|
sort_key=sort_key,
|
||||||
sort_dir=sort_dir)
|
sort_dir=sort_dir)
|
||||||
|
|
||||||
|
def _check_allowed_allocation_fields(self, fields):
|
||||||
|
"""Check if fetching a particular field of an allocation is allowed.
|
||||||
|
|
||||||
|
Check if the required version is being requested for fields
|
||||||
|
that are only allowed to be fetched in a particular API version.
|
||||||
|
|
||||||
|
:param fields: list or set of fields to check
|
||||||
|
:raises: NotAcceptable if a field is not allowed
|
||||||
|
"""
|
||||||
|
if fields is None:
|
||||||
|
return
|
||||||
|
if 'owner' in fields and not api_utils.allow_allocation_owner():
|
||||||
|
raise exception.NotAcceptable()
|
||||||
|
|
||||||
@METRICS.timer('AllocationsController.get_all')
|
@METRICS.timer('AllocationsController.get_all')
|
||||||
@expose.expose(AllocationCollection, types.uuid_or_name, wtypes.text,
|
@expose.expose(AllocationCollection, types.uuid_or_name, wtypes.text,
|
||||||
wtypes.text, types.uuid, int, wtypes.text, wtypes.text,
|
wtypes.text, types.uuid, int, wtypes.text, wtypes.text,
|
||||||
types.listtype)
|
types.listtype, wtypes.text)
|
||||||
def get_all(self, node=None, resource_class=None, state=None, marker=None,
|
def get_all(self, node=None, resource_class=None, state=None, marker=None,
|
||||||
limit=None, sort_key='id', sort_dir='asc', fields=None):
|
limit=None, sort_key='id', sort_dir='asc', fields=None,
|
||||||
|
owner=None):
|
||||||
"""Retrieve a list of allocations.
|
"""Retrieve a list of allocations.
|
||||||
|
|
||||||
:param node: UUID or name of a node, to get only allocations for that
|
:param node: UUID or name of a node, to get only allocations for that
|
||||||
@ -303,12 +332,17 @@ class AllocationsController(pecan.rest.RestController):
|
|||||||
:param sort_dir: direction to sort. "asc" or "desc". Default: asc.
|
:param sort_dir: direction to sort. "asc" or "desc". Default: asc.
|
||||||
:param 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 owner: Filter by owner.
|
||||||
"""
|
"""
|
||||||
cdict = api.request.context.to_policy_values()
|
cdict = api.request.context.to_policy_values()
|
||||||
policy.authorize('baremetal:allocation:get', cdict, cdict)
|
policy.authorize('baremetal:allocation:get', cdict, cdict)
|
||||||
|
|
||||||
|
self._check_allowed_allocation_fields(fields)
|
||||||
|
if owner is not None and not api_utils.allow_allocation_owner():
|
||||||
|
raise exception.NotAcceptable()
|
||||||
|
|
||||||
return self._get_allocations_collection(node, resource_class, state,
|
return self._get_allocations_collection(node, resource_class, state,
|
||||||
marker, limit,
|
owner, marker, limit,
|
||||||
sort_key, sort_dir,
|
sort_key, sort_dir,
|
||||||
fields=fields)
|
fields=fields)
|
||||||
|
|
||||||
@ -324,6 +358,8 @@ class AllocationsController(pecan.rest.RestController):
|
|||||||
cdict = api.request.context.to_policy_values()
|
cdict = api.request.context.to_policy_values()
|
||||||
policy.authorize('baremetal:allocation:get', cdict, cdict)
|
policy.authorize('baremetal:allocation:get', cdict, cdict)
|
||||||
|
|
||||||
|
self._check_allowed_allocation_fields(fields)
|
||||||
|
|
||||||
rpc_allocation = api_utils.get_rpc_allocation_with_suffix(
|
rpc_allocation = api_utils.get_rpc_allocation_with_suffix(
|
||||||
allocation_ident)
|
allocation_ident)
|
||||||
return Allocation.convert_with_links(rpc_allocation, fields=fields)
|
return Allocation.convert_with_links(rpc_allocation, fields=fields)
|
||||||
@ -338,7 +374,18 @@ class AllocationsController(pecan.rest.RestController):
|
|||||||
"""
|
"""
|
||||||
context = api.request.context
|
context = api.request.context
|
||||||
cdict = context.to_policy_values()
|
cdict = context.to_policy_values()
|
||||||
policy.authorize('baremetal:allocation:create', cdict, cdict)
|
|
||||||
|
try:
|
||||||
|
policy.authorize('baremetal:allocation:create', cdict, cdict)
|
||||||
|
self._check_allowed_allocation_fields(allocation.as_dict())
|
||||||
|
except exception.HTTPForbidden:
|
||||||
|
owner = cdict.get('project_id')
|
||||||
|
if not owner or (allocation.owner and owner != allocation.owner):
|
||||||
|
raise
|
||||||
|
policy.authorize('baremetal:allocation:create_restricted',
|
||||||
|
cdict, cdict)
|
||||||
|
self._check_allowed_allocation_fields(allocation.as_dict())
|
||||||
|
allocation.owner = owner
|
||||||
|
|
||||||
if (allocation.name
|
if (allocation.name
|
||||||
and not api_utils.is_valid_logical_name(allocation.name)):
|
and not api_utils.is_valid_logical_name(allocation.name)):
|
||||||
@ -416,12 +463,15 @@ class AllocationsController(pecan.rest.RestController):
|
|||||||
|
|
||||||
def _validate_patch(self, patch):
|
def _validate_patch(self, patch):
|
||||||
allowed_fields = ['name', 'extra']
|
allowed_fields = ['name', 'extra']
|
||||||
|
fields = set()
|
||||||
for p in patch:
|
for p in patch:
|
||||||
path = p['path'].split('/')[1]
|
path = p['path'].split('/')[1]
|
||||||
if path not in allowed_fields:
|
if path not in allowed_fields:
|
||||||
msg = _("Cannot update %s in an allocation. Only 'name' and "
|
msg = _("Cannot update %s in an allocation. Only 'name' and "
|
||||||
"'extra' are allowed to be updated.")
|
"'extra' are allowed to be updated.")
|
||||||
raise exception.Invalid(msg % p['path'])
|
raise exception.Invalid(msg % p['path'])
|
||||||
|
fields.add(path)
|
||||||
|
self._check_allowed_allocation_fields(fields)
|
||||||
|
|
||||||
@METRICS.timer('AllocationsController.patch')
|
@METRICS.timer('AllocationsController.patch')
|
||||||
@wsme.validate(types.uuid, [AllocationPatchType])
|
@wsme.validate(types.uuid, [AllocationPatchType])
|
||||||
|
@ -1248,3 +1248,11 @@ def allow_allocation_backfill():
|
|||||||
Version 1.58 of the API added support for backfilling allocations.
|
Version 1.58 of the API added support for backfilling allocations.
|
||||||
"""
|
"""
|
||||||
return api.request.version.minor >= versions.MINOR_58_ALLOCATION_BACKFILL
|
return api.request.version.minor >= versions.MINOR_58_ALLOCATION_BACKFILL
|
||||||
|
|
||||||
|
|
||||||
|
def allow_allocation_owner():
|
||||||
|
"""Check if allocation owner field is allowed.
|
||||||
|
|
||||||
|
Version 1.60 of the API added the owner field to the allocation object.
|
||||||
|
"""
|
||||||
|
return api.request.version.minor >= versions.MINOR_60_ALLOCATION_OWNER
|
||||||
|
@ -97,6 +97,7 @@ BASE_VERSION = 1
|
|||||||
# v1.57: Add support for updating an exisiting allocation.
|
# v1.57: Add support for updating an exisiting allocation.
|
||||||
# v1.58: Add support for backfilling allocations.
|
# v1.58: Add support for backfilling allocations.
|
||||||
# v1.59: Add support vendor data in configdrives.
|
# v1.59: Add support vendor data in configdrives.
|
||||||
|
# v1.60: Add owner to the allocation object.
|
||||||
|
|
||||||
MINOR_0_JUNO = 0
|
MINOR_0_JUNO = 0
|
||||||
MINOR_1_INITIAL_VERSION = 1
|
MINOR_1_INITIAL_VERSION = 1
|
||||||
@ -158,6 +159,7 @@ MINOR_56_BUILD_CONFIGDRIVE = 56
|
|||||||
MINOR_57_ALLOCATION_UPDATE = 57
|
MINOR_57_ALLOCATION_UPDATE = 57
|
||||||
MINOR_58_ALLOCATION_BACKFILL = 58
|
MINOR_58_ALLOCATION_BACKFILL = 58
|
||||||
MINOR_59_CONFIGDRIVE_VENDOR_DATA = 59
|
MINOR_59_CONFIGDRIVE_VENDOR_DATA = 59
|
||||||
|
MINOR_60_ALLOCATION_OWNER = 60
|
||||||
|
|
||||||
# When adding another version, update:
|
# When adding another version, update:
|
||||||
# - MINOR_MAX_VERSION
|
# - MINOR_MAX_VERSION
|
||||||
@ -165,7 +167,7 @@ MINOR_59_CONFIGDRIVE_VENDOR_DATA = 59
|
|||||||
# 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_59_CONFIGDRIVE_VENDOR_DATA
|
MINOR_MAX_VERSION = MINOR_60_ALLOCATION_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)
|
||||||
|
@ -430,6 +430,11 @@ allocation_policies = [
|
|||||||
'rule:is_admin',
|
'rule:is_admin',
|
||||||
'Create Allocation records',
|
'Create Allocation records',
|
||||||
[{'path': '/allocations', 'method': 'POST'}]),
|
[{'path': '/allocations', 'method': 'POST'}]),
|
||||||
|
policy.DocumentedRuleDefault(
|
||||||
|
'baremetal:allocation:create_restricted',
|
||||||
|
'rule:baremetal:allocation:create',
|
||||||
|
'Create Allocation records that are restricted to an owner',
|
||||||
|
[{'path': '/allocations', 'method': 'POST'}]),
|
||||||
policy.DocumentedRuleDefault(
|
policy.DocumentedRuleDefault(
|
||||||
'baremetal:allocation:delete',
|
'baremetal:allocation:delete',
|
||||||
'rule:is_admin',
|
'rule:is_admin',
|
||||||
|
@ -197,10 +197,10 @@ RELEASE_MAPPING = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
'master': {
|
'master': {
|
||||||
'api': '1.59',
|
'api': '1.60',
|
||||||
'rpc': '1.48',
|
'rpc': '1.48',
|
||||||
'objects': {
|
'objects': {
|
||||||
'Allocation': ['1.0'],
|
'Allocation': ['1.1'],
|
||||||
'Node': ['1.32'],
|
'Node': ['1.32'],
|
||||||
'Conductor': ['1.3'],
|
'Conductor': ['1.3'],
|
||||||
'Chassis': ['1.3'],
|
'Chassis': ['1.3'],
|
||||||
|
@ -112,6 +112,8 @@ def _candidate_nodes(context, allocation):
|
|||||||
# NOTE(dtantsur): we assume that candidate_nodes were converted to
|
# NOTE(dtantsur): we assume that candidate_nodes were converted to
|
||||||
# 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:
|
||||||
|
filters['owner'] = 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 allocation owner
|
||||||
|
|
||||||
|
Revision ID: ce6c4b3cf5a2
|
||||||
|
Revises: 1e15e7122cc9
|
||||||
|
Create Date: 2019-11-21 20:46:09.106592
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = 'ce6c4b3cf5a2'
|
||||||
|
down_revision = '1e15e7122cc9'
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
op.add_column('allocations', sa.Column('owner', sa.String(255),
|
||||||
|
nullable=True))
|
@ -354,7 +354,7 @@ class Connection(api.Connection):
|
|||||||
if filters is None:
|
if filters is None:
|
||||||
filters = dict()
|
filters = dict()
|
||||||
supported_filters = {'state', 'resource_class', 'node_uuid',
|
supported_filters = {'state', 'resource_class', 'node_uuid',
|
||||||
'conductor_affinity'}
|
'conductor_affinity', '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 "
|
||||||
|
@ -340,6 +340,7 @@ class Allocation(Base):
|
|||||||
name = Column(String(255), nullable=True)
|
name = Column(String(255), nullable=True)
|
||||||
node_id = Column(Integer, ForeignKey('nodes.id'), nullable=True)
|
node_id = Column(Integer, ForeignKey('nodes.id'), nullable=True)
|
||||||
state = Column(String(15), nullable=False)
|
state = Column(String(15), nullable=False)
|
||||||
|
owner = Column(String(255), nullable=True)
|
||||||
last_error = Column(Text, nullable=True)
|
last_error = Column(Text, nullable=True)
|
||||||
resource_class = Column(String(80), nullable=True)
|
resource_class = Column(String(80), nullable=True)
|
||||||
traits = Column(db_types.JsonEncodedList)
|
traits = Column(db_types.JsonEncodedList)
|
||||||
|
@ -12,6 +12,7 @@
|
|||||||
|
|
||||||
from oslo_utils import strutils
|
from oslo_utils import strutils
|
||||||
from oslo_utils import uuidutils
|
from oslo_utils import uuidutils
|
||||||
|
from oslo_utils import versionutils
|
||||||
from oslo_versionedobjects import base as object_base
|
from oslo_versionedobjects import base as object_base
|
||||||
|
|
||||||
from ironic.common import exception
|
from ironic.common import exception
|
||||||
@ -25,7 +26,8 @@ from ironic.objects import notification
|
|||||||
@base.IronicObjectRegistry.register
|
@base.IronicObjectRegistry.register
|
||||||
class Allocation(base.IronicObject, object_base.VersionedObjectDictCompat):
|
class Allocation(base.IronicObject, object_base.VersionedObjectDictCompat):
|
||||||
# Version 1.0: Initial version
|
# Version 1.0: Initial version
|
||||||
VERSION = '1.0'
|
# Version 1.1: Add owner field
|
||||||
|
VERSION = '1.1'
|
||||||
|
|
||||||
dbapi = dbapi.get_instance()
|
dbapi = dbapi.get_instance()
|
||||||
|
|
||||||
@ -41,6 +43,7 @@ class Allocation(base.IronicObject, object_base.VersionedObjectDictCompat):
|
|||||||
'candidate_nodes': object_fields.ListOfStringsField(nullable=True),
|
'candidate_nodes': object_fields.ListOfStringsField(nullable=True),
|
||||||
'extra': object_fields.FlexibleDictField(nullable=True),
|
'extra': object_fields.FlexibleDictField(nullable=True),
|
||||||
'conductor_affinity': object_fields.IntegerField(nullable=True),
|
'conductor_affinity': object_fields.IntegerField(nullable=True),
|
||||||
|
'owner': object_fields.StringField(nullable=True),
|
||||||
}
|
}
|
||||||
|
|
||||||
def _convert_to_version(self, target_version,
|
def _convert_to_version(self, target_version,
|
||||||
@ -51,12 +54,30 @@ class Allocation(base.IronicObject, object_base.VersionedObjectDictCompat):
|
|||||||
the same, older, or newer than the version of the object. This is
|
the same, older, or newer than the version of the object. This is
|
||||||
used for DB interactions as well as for serialization/deserialization.
|
used for DB interactions as well as for serialization/deserialization.
|
||||||
|
|
||||||
|
Version 1.1: 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
|
||||||
unavailable in the target version; set this to True when
|
unavailable in the target version; set this to True when
|
||||||
(de)serializing. False to set the unavailable fields to appropriate
|
(de)serializing. False to set the unavailable fields to appropriate
|
||||||
values; set this to False for DB interactions.
|
values; set this to False for DB interactions.
|
||||||
"""
|
"""
|
||||||
|
target_version = versionutils.convert_version_to_tuple(target_version)
|
||||||
|
|
||||||
|
# Convert the owner field.
|
||||||
|
owner_is_set = self.obj_attr_is_set('owner')
|
||||||
|
if target_version >= (1, 1):
|
||||||
|
if not owner_is_set:
|
||||||
|
self.owner = None
|
||||||
|
elif owner_is_set:
|
||||||
|
# Target version does not support owner, and it is set.
|
||||||
|
if remove_unavailable_fields:
|
||||||
|
# (De)serialising: remove unavailable fields.
|
||||||
|
delattr(self, 'owner')
|
||||||
|
elif self.owner is not None:
|
||||||
|
# DB: set unavailable fields to their default.
|
||||||
|
self.owner = None
|
||||||
|
|
||||||
# NOTE(xek): We don't want to enable RPC on this call just yet. Remotable
|
# NOTE(xek): We don't want to enable RPC on this call just yet. Remotable
|
||||||
# methods can be used in the future to replace current explicit RPC calls.
|
# methods can be used in the future to replace current explicit RPC calls.
|
||||||
@ -266,7 +287,8 @@ class AllocationCRUDNotification(notification.NotificationBase):
|
|||||||
@base.IronicObjectRegistry.register
|
@base.IronicObjectRegistry.register
|
||||||
class AllocationCRUDPayload(notification.NotificationPayloadBase):
|
class AllocationCRUDPayload(notification.NotificationPayloadBase):
|
||||||
# Version 1.0: Initial version
|
# Version 1.0: Initial version
|
||||||
VERSION = '1.0'
|
# Version 1.1: Add allocation owner field.
|
||||||
|
VERSION = '1.1'
|
||||||
|
|
||||||
SCHEMA = {
|
SCHEMA = {
|
||||||
'candidate_nodes': ('allocation', 'candidate_nodes'),
|
'candidate_nodes': ('allocation', 'candidate_nodes'),
|
||||||
@ -274,6 +296,7 @@ class AllocationCRUDPayload(notification.NotificationPayloadBase):
|
|||||||
'extra': ('allocation', 'extra'),
|
'extra': ('allocation', 'extra'),
|
||||||
'last_error': ('allocation', 'last_error'),
|
'last_error': ('allocation', 'last_error'),
|
||||||
'name': ('allocation', 'name'),
|
'name': ('allocation', 'name'),
|
||||||
|
'owner': ('allocation', 'owner'),
|
||||||
'resource_class': ('allocation', 'resource_class'),
|
'resource_class': ('allocation', 'resource_class'),
|
||||||
'state': ('allocation', 'state'),
|
'state': ('allocation', 'state'),
|
||||||
'traits': ('allocation', 'traits'),
|
'traits': ('allocation', 'traits'),
|
||||||
@ -287,6 +310,7 @@ class AllocationCRUDPayload(notification.NotificationPayloadBase):
|
|||||||
'node_uuid': object_fields.StringField(nullable=True),
|
'node_uuid': object_fields.StringField(nullable=True),
|
||||||
'state': object_fields.StringField(nullable=True),
|
'state': object_fields.StringField(nullable=True),
|
||||||
'last_error': object_fields.StringField(nullable=True),
|
'last_error': object_fields.StringField(nullable=True),
|
||||||
|
'owner': object_fields.StringField(nullable=True),
|
||||||
'resource_class': object_fields.StringField(nullable=True),
|
'resource_class': object_fields.StringField(nullable=True),
|
||||||
'traits': object_fields.ListOfStringsField(nullable=True),
|
'traits': object_fields.ListOfStringsField(nullable=True),
|
||||||
'candidate_nodes': object_fields.ListOfStringsField(nullable=True),
|
'candidate_nodes': object_fields.ListOfStringsField(nullable=True),
|
||||||
|
@ -30,6 +30,7 @@ from ironic.api.controllers import v1 as api_v1
|
|||||||
from ironic.api.controllers.v1 import allocation as api_allocation
|
from ironic.api.controllers.v1 import allocation as api_allocation
|
||||||
from ironic.api.controllers.v1 import notification_utils
|
from ironic.api.controllers.v1 import notification_utils
|
||||||
from ironic.common import exception
|
from ironic.common import exception
|
||||||
|
from ironic.common import policy
|
||||||
from ironic.conductor import rpcapi
|
from ironic.conductor import rpcapi
|
||||||
from ironic import objects
|
from ironic import objects
|
||||||
from ironic.objects import fields as obj_fields
|
from ironic.objects import fields as obj_fields
|
||||||
@ -67,6 +68,7 @@ class TestListAllocations(test_api_base.BaseApiTest):
|
|||||||
self.assertEqual(allocation.name, data['allocations'][0]['name'])
|
self.assertEqual(allocation.name, data['allocations'][0]['name'])
|
||||||
self.assertEqual({}, data['allocations'][0]["extra"])
|
self.assertEqual({}, data['allocations'][0]["extra"])
|
||||||
self.assertEqual(self.node.uuid, data['allocations'][0]["node_uuid"])
|
self.assertEqual(self.node.uuid, data['allocations'][0]["node_uuid"])
|
||||||
|
self.assertEqual(allocation.owner, data['allocations'][0]["owner"])
|
||||||
# never expose the node_id
|
# never expose the node_id
|
||||||
self.assertNotIn('node_id', data['allocations'][0])
|
self.assertNotIn('node_id', data['allocations'][0])
|
||||||
|
|
||||||
@ -78,6 +80,7 @@ class TestListAllocations(test_api_base.BaseApiTest):
|
|||||||
self.assertEqual(allocation.uuid, data['uuid'])
|
self.assertEqual(allocation.uuid, data['uuid'])
|
||||||
self.assertEqual({}, data["extra"])
|
self.assertEqual({}, data["extra"])
|
||||||
self.assertEqual(self.node.uuid, data["node_uuid"])
|
self.assertEqual(self.node.uuid, data["node_uuid"])
|
||||||
|
self.assertEqual(allocation.owner, data["owner"])
|
||||||
# never expose the node_id
|
# never expose the node_id
|
||||||
self.assertNotIn('node_id', data)
|
self.assertNotIn('node_id', data)
|
||||||
|
|
||||||
@ -318,6 +321,29 @@ class TestListAllocations(test_api_base.BaseApiTest):
|
|||||||
headers=self.headers)
|
headers=self.headers)
|
||||||
self.assertEqual(3, len(data['allocations']))
|
self.assertEqual(3, len(data['allocations']))
|
||||||
|
|
||||||
|
def test_get_all_by_owner(self):
|
||||||
|
for i in range(5):
|
||||||
|
if i < 3:
|
||||||
|
owner = '12345'
|
||||||
|
else:
|
||||||
|
owner = '54321'
|
||||||
|
obj_utils.create_test_allocation(
|
||||||
|
self.context,
|
||||||
|
owner=owner,
|
||||||
|
uuid=uuidutils.generate_uuid(),
|
||||||
|
name='allocation%s' % i)
|
||||||
|
data = self.get_json("/allocations?owner=12345",
|
||||||
|
headers=self.headers)
|
||||||
|
self.assertEqual(3, len(data['allocations']))
|
||||||
|
|
||||||
|
def test_get_all_by_owner_not_allowed(self):
|
||||||
|
response = self.get_json("/allocations?owner=12345",
|
||||||
|
headers={api_base.Version.string: '1.59'},
|
||||||
|
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_all_by_node_name(self):
|
def test_get_all_by_node_name(self):
|
||||||
for i in range(5):
|
for i in range(5):
|
||||||
if i < 3:
|
if i < 3:
|
||||||
@ -393,6 +419,44 @@ class TestListAllocations(test_api_base.BaseApiTest):
|
|||||||
expect_errors=True, headers=self.headers)
|
expect_errors=True, headers=self.headers)
|
||||||
self.assertEqual(http_client.NOT_FOUND, res.status_code)
|
self.assertEqual(http_client.NOT_FOUND, res.status_code)
|
||||||
|
|
||||||
|
def test_allocation_owner_hidden_in_lower_version(self):
|
||||||
|
allocation = obj_utils.create_test_allocation(self.context,
|
||||||
|
node_id=self.node.id)
|
||||||
|
data = self.get_json(
|
||||||
|
'/allocations/%s' % allocation.uuid,
|
||||||
|
headers={api_base.Version.string: '1.59'})
|
||||||
|
self.assertNotIn('owner', data)
|
||||||
|
data = self.get_json(
|
||||||
|
'/allocations/%s' % allocation.uuid,
|
||||||
|
headers=self.headers)
|
||||||
|
self.assertIn('owner', data)
|
||||||
|
|
||||||
|
def test_allocation_owner_null_field(self):
|
||||||
|
allocation = obj_utils.create_test_allocation(self.context,
|
||||||
|
node_id=self.node.id,
|
||||||
|
owner=None)
|
||||||
|
data = self.get_json('/allocations/%s' % allocation.uuid,
|
||||||
|
headers=self.headers)
|
||||||
|
self.assertIsNone(data['owner'])
|
||||||
|
|
||||||
|
def test_allocation_owner_present(self):
|
||||||
|
allocation = obj_utils.create_test_allocation(self.context,
|
||||||
|
node_id=self.node.id,
|
||||||
|
owner='12345')
|
||||||
|
data = self.get_json('/allocations/%s' % allocation.uuid,
|
||||||
|
headers=self.headers)
|
||||||
|
self.assertEqual(data['owner'], '12345')
|
||||||
|
|
||||||
|
def test_get_owner_field(self):
|
||||||
|
allocation = obj_utils.create_test_allocation(self.context,
|
||||||
|
node_id=self.node.id,
|
||||||
|
owner='12345')
|
||||||
|
fields = 'owner'
|
||||||
|
response = self.get_json(
|
||||||
|
'/allocations/%s?fields=%s' % (allocation.uuid, fields),
|
||||||
|
headers=self.headers)
|
||||||
|
self.assertIn('owner', response)
|
||||||
|
|
||||||
|
|
||||||
class TestPatch(test_api_base.BaseApiTest):
|
class TestPatch(test_api_base.BaseApiTest):
|
||||||
headers = {api_base.Version.string: str(api_v1.max_version())}
|
headers = {api_base.Version.string: str(api_v1.max_version())}
|
||||||
@ -592,6 +656,18 @@ 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_not_acceptable(self):
|
||||||
|
allocation = obj_utils.create_test_allocation(
|
||||||
|
self.context, owner='12345', uuid=uuidutils.generate_uuid())
|
||||||
|
new_owner = '54321'
|
||||||
|
response = self.patch_json('/allocations/%s' % allocation.uuid,
|
||||||
|
[{'path': '/owner',
|
||||||
|
'value': new_owner,
|
||||||
|
'op': 'replace'}],
|
||||||
|
expect_errors=True, headers=self.headers)
|
||||||
|
self.assertEqual('application/json', response.content_type)
|
||||||
|
self.assertEqual(http_client.BAD_REQUEST, response.status_code)
|
||||||
|
|
||||||
|
|
||||||
def _create_locally(_api, _ctx, allocation, topic):
|
def _create_locally(_api, _ctx, allocation, topic):
|
||||||
if 'node_id' in allocation and allocation.node_id:
|
if 'node_id' in allocation and allocation.node_id:
|
||||||
@ -639,6 +715,7 @@ class TestPost(test_api_base.BaseApiTest):
|
|||||||
self.assertIsNone(result['node_uuid'])
|
self.assertIsNone(result['node_uuid'])
|
||||||
self.assertEqual([], result['candidate_nodes'])
|
self.assertEqual([], result['candidate_nodes'])
|
||||||
self.assertEqual([], result['traits'])
|
self.assertEqual([], result['traits'])
|
||||||
|
self.assertIsNone(None, result['owner'])
|
||||||
self.assertNotIn('node', result)
|
self.assertNotIn('node', result)
|
||||||
return_created_at = timeutils.parse_isotime(
|
return_created_at = timeutils.parse_isotime(
|
||||||
result['created_at']).replace(tzinfo=None)
|
result['created_at']).replace(tzinfo=None)
|
||||||
@ -837,6 +914,23 @@ class TestPost(test_api_base.BaseApiTest):
|
|||||||
self.assertEqual('application/json', response.content_type)
|
self.assertEqual('application/json', response.content_type)
|
||||||
self.assertTrue(response.json['error_message'])
|
self.assertTrue(response.json['error_message'])
|
||||||
|
|
||||||
|
def test_create_allocation_owner(self):
|
||||||
|
owner = '12345'
|
||||||
|
adict = apiutils.allocation_post_data(owner=owner)
|
||||||
|
self.post_json('/allocations', adict, headers=self.headers)
|
||||||
|
result = self.get_json('/allocations/%s' % adict['uuid'],
|
||||||
|
headers=self.headers)
|
||||||
|
self.assertEqual(owner, result['owner'])
|
||||||
|
|
||||||
|
def test_create_allocation_owner_not_allowed(self):
|
||||||
|
owner = '12345'
|
||||||
|
adict = apiutils.allocation_post_data(owner=owner)
|
||||||
|
response = self.post_json('/allocations', adict,
|
||||||
|
headers={api_base.Version.string: '1.59'},
|
||||||
|
expect_errors=True)
|
||||||
|
self.assertEqual('application/json', response.content_type)
|
||||||
|
self.assertEqual(http_client.NOT_ACCEPTABLE, response.status_int)
|
||||||
|
|
||||||
def test_backfill(self):
|
def test_backfill(self):
|
||||||
node = obj_utils.create_test_node(self.context)
|
node = obj_utils.create_test_node(self.context)
|
||||||
adict = apiutils.allocation_post_data(node=node.uuid)
|
adict = apiutils.allocation_post_data(node=node.uuid)
|
||||||
@ -901,13 +995,107 @@ class TestPost(test_api_base.BaseApiTest):
|
|||||||
def test_backfill_not_allowed(self):
|
def test_backfill_not_allowed(self):
|
||||||
node = obj_utils.create_test_node(self.context)
|
node = obj_utils.create_test_node(self.context)
|
||||||
headers = {api_base.Version.string: '1.57'}
|
headers = {api_base.Version.string: '1.57'}
|
||||||
adict = apiutils.allocation_post_data(node=node.uuid)
|
adict = {'node': node.uuid}
|
||||||
response = self.post_json('/allocations', adict, expect_errors=True,
|
response = self.post_json('/allocations', adict, expect_errors=True,
|
||||||
headers=headers)
|
headers=headers)
|
||||||
self.assertEqual(http_client.BAD_REQUEST, response.status_int)
|
self.assertEqual(http_client.BAD_REQUEST, response.status_int)
|
||||||
self.assertEqual('application/json', response.content_type)
|
self.assertEqual('application/json', response.content_type)
|
||||||
self.assertTrue(response.json['error_message'])
|
self.assertTrue(response.json['error_message'])
|
||||||
|
|
||||||
|
@mock.patch.object(policy, 'authorize', autospec=True)
|
||||||
|
def test_create_restricted_allocation(self, mock_authorize):
|
||||||
|
def mock_authorize_function(rule, target, creds):
|
||||||
|
if rule == 'baremetal:allocation:create':
|
||||||
|
raise exception.HTTPForbidden(resource='fake')
|
||||||
|
return True
|
||||||
|
mock_authorize.side_effect = mock_authorize_function
|
||||||
|
|
||||||
|
owner = '12345'
|
||||||
|
adict = apiutils.allocation_post_data()
|
||||||
|
headers = {api_base.Version.string: '1.60', 'X-Project-Id': owner}
|
||||||
|
response = self.post_json('/allocations', adict, headers=headers)
|
||||||
|
self.assertEqual('application/json', response.content_type)
|
||||||
|
self.assertEqual(http_client.CREATED, response.status_int)
|
||||||
|
self.assertEqual(owner, response.json['owner'])
|
||||||
|
result = self.get_json('/allocations/%s' % adict['uuid'],
|
||||||
|
headers=headers)
|
||||||
|
self.assertEqual(adict['uuid'], result['uuid'])
|
||||||
|
self.assertEqual(owner, result['owner'])
|
||||||
|
|
||||||
|
@mock.patch.object(policy, 'authorize', autospec=True)
|
||||||
|
def test_create_restricted_allocation_older_version(self, mock_authorize):
|
||||||
|
def mock_authorize_function(rule, target, creds):
|
||||||
|
if rule == 'baremetal:allocation:create':
|
||||||
|
raise exception.HTTPForbidden(resource='fake')
|
||||||
|
return True
|
||||||
|
mock_authorize.side_effect = mock_authorize_function
|
||||||
|
|
||||||
|
owner = '12345'
|
||||||
|
adict = apiutils.allocation_post_data()
|
||||||
|
del adict['owner']
|
||||||
|
headers = {api_base.Version.string: '1.59', 'X-Project-Id': owner}
|
||||||
|
response = self.post_json('/allocations', adict, headers=headers)
|
||||||
|
self.assertEqual('application/json', response.content_type)
|
||||||
|
self.assertEqual(http_client.CREATED, response.status_int)
|
||||||
|
result = self.get_json('/allocations/%s' % adict['uuid'],
|
||||||
|
headers=headers)
|
||||||
|
self.assertEqual(adict['uuid'], result['uuid'])
|
||||||
|
|
||||||
|
@mock.patch.object(policy, 'authorize', autospec=True)
|
||||||
|
def test_create_restricted_allocation_forbidden(self, mock_authorize):
|
||||||
|
def mock_authorize_function(rule, target, creds):
|
||||||
|
raise exception.HTTPForbidden(resource='fake')
|
||||||
|
mock_authorize.side_effect = mock_authorize_function
|
||||||
|
|
||||||
|
owner = '12345'
|
||||||
|
adict = apiutils.allocation_post_data()
|
||||||
|
headers = {api_base.Version.string: '1.60', 'X-Project-Id': owner}
|
||||||
|
response = self.post_json('/allocations', adict, expect_errors=True,
|
||||||
|
headers=headers)
|
||||||
|
self.assertEqual(http_client.FORBIDDEN, response.status_int)
|
||||||
|
self.assertEqual('application/json', response.content_type)
|
||||||
|
self.assertTrue(response.json['error_message'])
|
||||||
|
|
||||||
|
@mock.patch.object(policy, 'authorize', autospec=True)
|
||||||
|
def test_create_restricted_allocation_with_owner(self, mock_authorize):
|
||||||
|
def mock_authorize_function(rule, target, creds):
|
||||||
|
if rule == 'baremetal:allocation:create':
|
||||||
|
raise exception.HTTPForbidden(resource='fake')
|
||||||
|
return True
|
||||||
|
mock_authorize.side_effect = mock_authorize_function
|
||||||
|
|
||||||
|
owner = '12345'
|
||||||
|
adict = apiutils.allocation_post_data(owner=owner)
|
||||||
|
adict['owner'] = owner
|
||||||
|
headers = {api_base.Version.string: '1.60', 'X-Project-Id': owner}
|
||||||
|
response = self.post_json('/allocations', adict, headers=headers)
|
||||||
|
self.assertEqual('application/json', response.content_type)
|
||||||
|
self.assertEqual(http_client.CREATED, response.status_int)
|
||||||
|
self.assertEqual(owner, response.json['owner'])
|
||||||
|
result = self.get_json('/allocations/%s' % adict['uuid'],
|
||||||
|
headers=headers)
|
||||||
|
self.assertEqual(adict['uuid'], result['uuid'])
|
||||||
|
self.assertEqual(owner, result['owner'])
|
||||||
|
|
||||||
|
@mock.patch.object(policy, 'authorize', autospec=True)
|
||||||
|
def test_create_restricted_allocation_with_mismatch_owner(
|
||||||
|
self, mock_authorize):
|
||||||
|
def mock_authorize_function(rule, target, creds):
|
||||||
|
if rule == 'baremetal:allocation:create':
|
||||||
|
raise exception.HTTPForbidden(resource='fake')
|
||||||
|
return True
|
||||||
|
mock_authorize.side_effect = mock_authorize_function
|
||||||
|
|
||||||
|
owner = '12345'
|
||||||
|
adict = apiutils.allocation_post_data(owner=owner)
|
||||||
|
adict['owner'] = '54321'
|
||||||
|
headers = {api_base.Version.string: '1.60', 'X-Project-Id': owner}
|
||||||
|
response = self.post_json('/allocations', adict, expect_errors=True,
|
||||||
|
headers=headers)
|
||||||
|
self.assertEqual(http_client.FORBIDDEN, response.status_int)
|
||||||
|
self.assertEqual('application/json', response.content_type)
|
||||||
|
self.assertTrue(response.json['error_message'])
|
||||||
|
|
||||||
|
|
||||||
@mock.patch.object(rpcapi.ConductorAPI, 'destroy_allocation')
|
@mock.patch.object(rpcapi.ConductorAPI, 'destroy_allocation')
|
||||||
class TestDelete(test_api_base.BaseApiTest):
|
class TestDelete(test_api_base.BaseApiTest):
|
||||||
|
@ -190,7 +190,7 @@ def post_get_test_portgroup(**kw):
|
|||||||
|
|
||||||
_ALLOCATION_POST_FIELDS = {'resource_class', 'uuid', 'traits',
|
_ALLOCATION_POST_FIELDS = {'resource_class', 'uuid', 'traits',
|
||||||
'candidate_nodes', 'name', 'extra',
|
'candidate_nodes', 'name', 'extra',
|
||||||
'node'}
|
'node', 'owner'}
|
||||||
|
|
||||||
|
|
||||||
def allocation_post_data(node=None, **kw):
|
def allocation_post_data(node=None, **kw):
|
||||||
|
@ -348,6 +348,28 @@ class DoAllocateTestCase(db_base.DbTestCase):
|
|||||||
# All nodes are filtered out on the database level.
|
# All nodes are filtered out on the database level.
|
||||||
self.assertFalse(mock_acquire.called)
|
self.assertFalse(mock_acquire.called)
|
||||||
|
|
||||||
|
@mock.patch.object(task_manager, 'acquire', autospec=True,
|
||||||
|
side_effect=task_manager.acquire)
|
||||||
|
def test_nodes_filtered_out_owner(self, mock_acquire):
|
||||||
|
# Owner does not match
|
||||||
|
obj_utils.create_test_node(self.context,
|
||||||
|
uuid=uuidutils.generate_uuid(),
|
||||||
|
owner='54321',
|
||||||
|
resource_class='x-large',
|
||||||
|
power_state='power off',
|
||||||
|
provision_state='available')
|
||||||
|
|
||||||
|
allocation = obj_utils.create_test_allocation(self.context,
|
||||||
|
resource_class='x-large',
|
||||||
|
owner='12345')
|
||||||
|
allocations.do_allocate(self.context, allocation)
|
||||||
|
self.assertIn('no available nodes', allocation['last_error'])
|
||||||
|
self.assertIn('x-large', allocation['last_error'])
|
||||||
|
self.assertEqual('error', allocation['state'])
|
||||||
|
|
||||||
|
# All nodes are filtered out on the database level.
|
||||||
|
self.assertFalse(mock_acquire.called)
|
||||||
|
|
||||||
@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_locked(self, mock_acquire):
|
def test_nodes_locked(self, mock_acquire):
|
||||||
|
@ -964,6 +964,11 @@ class MigrationCheckersMixin(object):
|
|||||||
self.assertIsInstance(deploy_templates.c.extra.type,
|
self.assertIsInstance(deploy_templates.c.extra.type,
|
||||||
sqlalchemy.types.TEXT)
|
sqlalchemy.types.TEXT)
|
||||||
|
|
||||||
|
def _check_ce6c4b3cf5a2(self, engine, data):
|
||||||
|
allocations = db_utils.get_table(engine, 'allocations')
|
||||||
|
col_names = [column.name for column in allocations.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')
|
||||||
|
@ -595,6 +595,7 @@ def get_test_allocation(**kw):
|
|||||||
'updated_at': kw.get('updated_at'),
|
'updated_at': kw.get('updated_at'),
|
||||||
'uuid': kw.get('uuid', uuidutils.generate_uuid()),
|
'uuid': kw.get('uuid', uuidutils.generate_uuid()),
|
||||||
'version': kw.get('version', allocation.Allocation.VERSION),
|
'version': kw.get('version', allocation.Allocation.VERSION),
|
||||||
|
'owner': kw.get('owner', None),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -142,3 +142,64 @@ class TestAllocationObject(db_base.DbTestCase, obj_utils.SchemasTestMixIn):
|
|||||||
def test_payload_schemas(self):
|
def test_payload_schemas(self):
|
||||||
self._check_payload_schemas(objects.allocation,
|
self._check_payload_schemas(objects.allocation,
|
||||||
objects.Allocation.fields)
|
objects.Allocation.fields)
|
||||||
|
|
||||||
|
|
||||||
|
class TestConvertToVersion(db_base.DbTestCase):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super(TestConvertToVersion, self).setUp()
|
||||||
|
self.fake_allocation = db_utils.get_test_allocation()
|
||||||
|
|
||||||
|
def test_owner_supported_missing(self):
|
||||||
|
# Physical network not set, should be set to default.
|
||||||
|
allocation = objects.Allocation(self.context, **self.fake_allocation)
|
||||||
|
delattr(allocation, 'owner')
|
||||||
|
allocation.obj_reset_changes()
|
||||||
|
allocation._convert_to_version("1.1")
|
||||||
|
self.assertIsNone(allocation.owner)
|
||||||
|
self.assertEqual({'owner': None}, allocation.obj_get_changes())
|
||||||
|
|
||||||
|
def test_owner_supported_set(self):
|
||||||
|
# Physical network set, no change required.
|
||||||
|
allocation = objects.Allocation(self.context, **self.fake_allocation)
|
||||||
|
allocation.owner = 'owner1'
|
||||||
|
allocation.obj_reset_changes()
|
||||||
|
allocation._convert_to_version("1.1")
|
||||||
|
self.assertEqual('owner1', allocation.owner)
|
||||||
|
self.assertEqual({}, allocation.obj_get_changes())
|
||||||
|
|
||||||
|
def test_owner_unsupported_missing(self):
|
||||||
|
# Physical network not set, no change required.
|
||||||
|
allocation = objects.Allocation(self.context, **self.fake_allocation)
|
||||||
|
delattr(allocation, 'owner')
|
||||||
|
allocation.obj_reset_changes()
|
||||||
|
allocation._convert_to_version("1.0")
|
||||||
|
self.assertNotIn('owner', allocation)
|
||||||
|
self.assertEqual({}, allocation.obj_get_changes())
|
||||||
|
|
||||||
|
def test_owner_unsupported_set_remove(self):
|
||||||
|
# Physical network set, should be removed.
|
||||||
|
allocation = objects.Allocation(self.context, **self.fake_allocation)
|
||||||
|
allocation.owner = 'owner1'
|
||||||
|
allocation.obj_reset_changes()
|
||||||
|
allocation._convert_to_version("1.0")
|
||||||
|
self.assertNotIn('owner', allocation)
|
||||||
|
self.assertEqual({}, allocation.obj_get_changes())
|
||||||
|
|
||||||
|
def test_owner_unsupported_set_no_remove_non_default(self):
|
||||||
|
# Physical network set, should be set to default.
|
||||||
|
allocation = objects.Allocation(self.context, **self.fake_allocation)
|
||||||
|
allocation.owner = 'owner1'
|
||||||
|
allocation.obj_reset_changes()
|
||||||
|
allocation._convert_to_version("1.0", False)
|
||||||
|
self.assertIsNone(allocation.owner)
|
||||||
|
self.assertEqual({'owner': None}, allocation.obj_get_changes())
|
||||||
|
|
||||||
|
def test_owner_unsupported_set_no_remove_default(self):
|
||||||
|
# Physical network set, no change required.
|
||||||
|
allocation = objects.Allocation(self.context, **self.fake_allocation)
|
||||||
|
allocation.owner = None
|
||||||
|
allocation.obj_reset_changes()
|
||||||
|
allocation._convert_to_version("1.0", False)
|
||||||
|
self.assertIsNone(allocation.owner)
|
||||||
|
self.assertEqual({}, allocation.obj_get_changes())
|
||||||
|
@ -714,9 +714,9 @@ expected_object_fingerprints = {
|
|||||||
'TraitList': '1.0-33a2e1bb91ad4082f9f63429b77c1244',
|
'TraitList': '1.0-33a2e1bb91ad4082f9f63429b77c1244',
|
||||||
'BIOSSetting': '1.0-fd4a791dc2139a7cc21cefbbaedfd9e7',
|
'BIOSSetting': '1.0-fd4a791dc2139a7cc21cefbbaedfd9e7',
|
||||||
'BIOSSettingList': '1.0-33a2e1bb91ad4082f9f63429b77c1244',
|
'BIOSSettingList': '1.0-33a2e1bb91ad4082f9f63429b77c1244',
|
||||||
'Allocation': '1.0-25ebf609743cd3f332a4f80fcb818102',
|
'Allocation': '1.1-38937f2854722f1057ec667b12878708',
|
||||||
'AllocationCRUDNotification': '1.0-59acc533c11d306f149846f922739c15',
|
'AllocationCRUDNotification': '1.0-59acc533c11d306f149846f922739c15',
|
||||||
'AllocationCRUDPayload': '1.0-a82389d019f37cfe54b50049f73911b3',
|
'AllocationCRUDPayload': '1.1-3c8849932b80380bb96587ff62e8f087',
|
||||||
'DeployTemplate': '1.1-4e30c8e9098595e359bb907f095bf1a9',
|
'DeployTemplate': '1.1-4e30c8e9098595e359bb907f095bf1a9',
|
||||||
'DeployTemplateCRUDNotification': '1.0-59acc533c11d306f149846f922739c15',
|
'DeployTemplateCRUDNotification': '1.0-59acc533c11d306f149846f922739c15',
|
||||||
'DeployTemplateCRUDPayload': '1.0-200857e7e715f58a5b6d6b700ab73a3b',
|
'DeployTemplateCRUDPayload': '1.0-200857e7e715f58a5b6d6b700ab73a3b',
|
||||||
|
@ -0,0 +1,7 @@
|
|||||||
|
---
|
||||||
|
features:
|
||||||
|
- |
|
||||||
|
Adds an ``owner`` field to allocations. Depending on policy, a non-admin
|
||||||
|
can then create an allocation and have the owner set to their project.
|
||||||
|
Allocation processing will then ensure that only nodes with the same owner
|
||||||
|
are matched.
|
Loading…
Reference in New Issue
Block a user