Default type overrides
This patch adds a feature by which we allow setting default volume types for projects. The following changes are made to achieve the feature: 1) Add 4 set of APIs, set, get, get_all, unset volume type 2) All policies (except get_all) default to system/domain/project admin 3) Preference order: project default, conf default 4) Logic to not allow deletion of default type We validate set, get and unset APIs with keystone to verify a valid project id is passed in the request and user has proper authorization rights to show the project. The policies are system/domain/project admin by default except get_all policy which defaults to system admin. Implements: Blueprint multiple-default-volume-types Change-Id: Idcc949ed6adbaea0c2337fac83014998b81ff1f8
This commit is contained in:
parent
63a8f2e1bf
commit
e63cb8548a
167
api-ref/source/v3/default-types.inc
Normal file
167
api-ref/source/v3/default-types.inc
Normal file
@ -0,0 +1,167 @@
|
|||||||
|
.. -*- rst -*-
|
||||||
|
|
||||||
|
Default Volume Types (default-types)
|
||||||
|
====================================
|
||||||
|
|
||||||
|
Manage a default volume type for individual projects.
|
||||||
|
|
||||||
|
By default, a volume-create request that does not specify a volume-type
|
||||||
|
will assign the configured system default volume type to the volume.
|
||||||
|
You can override this behavior on a per-project basis by setting a different
|
||||||
|
default volume type for any project.
|
||||||
|
|
||||||
|
Available in microversion 3.62 or higher.
|
||||||
|
|
||||||
|
NOTE: The default policy for list API is system admin so you would require
|
||||||
|
a system scoped token to access it.
|
||||||
|
To get a system scoped token, you need to run the following command:
|
||||||
|
|
||||||
|
openstack --os-system-scope all --os-project-name='' token issue
|
||||||
|
|
||||||
|
Create or update a default volume type
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
.. rest_method:: PUT /v3/default-types/{project-id}
|
||||||
|
|
||||||
|
Create or update the default volume type for a project
|
||||||
|
|
||||||
|
Response codes
|
||||||
|
--------------
|
||||||
|
|
||||||
|
.. rest_status_code:: success ../status.yaml
|
||||||
|
|
||||||
|
- 200
|
||||||
|
|
||||||
|
.. rest_status_code:: error ../status.yaml
|
||||||
|
|
||||||
|
- 400
|
||||||
|
- 404
|
||||||
|
|
||||||
|
Request Parameters
|
||||||
|
------------------
|
||||||
|
|
||||||
|
.. rest_parameters:: parameters.yaml
|
||||||
|
|
||||||
|
- project_id: project_id_path
|
||||||
|
- volume_type: volume_type_name_or_id
|
||||||
|
|
||||||
|
Request Example
|
||||||
|
---------------
|
||||||
|
|
||||||
|
.. literalinclude:: ./samples/set-default-type-request.json
|
||||||
|
:language: javascript
|
||||||
|
|
||||||
|
|
||||||
|
Response Parameters
|
||||||
|
-------------------
|
||||||
|
|
||||||
|
.. rest_parameters:: parameters.yaml
|
||||||
|
|
||||||
|
- project_id: project_id
|
||||||
|
- volume_type_id: volume_type_id
|
||||||
|
|
||||||
|
Response Example
|
||||||
|
----------------
|
||||||
|
|
||||||
|
.. literalinclude:: ./samples/set-default-type-response.json
|
||||||
|
:language: javascript
|
||||||
|
|
||||||
|
Show a default volume type
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
.. rest_method:: GET /v3/default-types/{project-id}
|
||||||
|
|
||||||
|
Show the default volume type for a project
|
||||||
|
|
||||||
|
Response codes
|
||||||
|
--------------
|
||||||
|
|
||||||
|
.. rest_status_code:: success ../status.yaml
|
||||||
|
|
||||||
|
- 200
|
||||||
|
|
||||||
|
.. rest_status_code:: error ../status.yaml
|
||||||
|
|
||||||
|
- 404
|
||||||
|
|
||||||
|
Request Parameters
|
||||||
|
------------------
|
||||||
|
|
||||||
|
.. rest_parameters:: parameters.yaml
|
||||||
|
|
||||||
|
- project_id: project_id_path
|
||||||
|
|
||||||
|
Response Parameters
|
||||||
|
-------------------
|
||||||
|
|
||||||
|
.. rest_parameters:: parameters.yaml
|
||||||
|
|
||||||
|
- project_id: project_id
|
||||||
|
- volume_type_id: volume_type_id
|
||||||
|
|
||||||
|
Response Example
|
||||||
|
----------------
|
||||||
|
|
||||||
|
.. literalinclude:: ./samples/get-default-type-response.json
|
||||||
|
:language: javascript
|
||||||
|
|
||||||
|
List default volume types
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
.. rest_method:: GET /v3/default-types/
|
||||||
|
|
||||||
|
Get a list of all default volume types
|
||||||
|
|
||||||
|
Response codes
|
||||||
|
--------------
|
||||||
|
|
||||||
|
.. rest_status_code:: success ../status.yaml
|
||||||
|
|
||||||
|
- 200
|
||||||
|
|
||||||
|
.. rest_status_code:: error ../status.yaml
|
||||||
|
|
||||||
|
- 404
|
||||||
|
|
||||||
|
Response Parameters
|
||||||
|
-------------------
|
||||||
|
|
||||||
|
.. rest_parameters:: parameters.yaml
|
||||||
|
|
||||||
|
- project_id: project_id
|
||||||
|
- volume_type_id: volume_type_id
|
||||||
|
|
||||||
|
Response Example
|
||||||
|
----------------
|
||||||
|
|
||||||
|
.. literalinclude:: ./samples/get-default-types-response.json
|
||||||
|
:language: javascript
|
||||||
|
|
||||||
|
Delete a default volume type
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
.. rest_method:: DELETE /v3/default-types/{project-id}
|
||||||
|
|
||||||
|
Unset the default volume type for a project.
|
||||||
|
|
||||||
|
This operation does not do anything to the volume type itself.
|
||||||
|
It simply removes the volume type from being the default volume type for
|
||||||
|
the specified project.
|
||||||
|
|
||||||
|
Response codes
|
||||||
|
--------------
|
||||||
|
|
||||||
|
.. rest_status_code:: success ../status.yaml
|
||||||
|
|
||||||
|
- 204
|
||||||
|
|
||||||
|
.. rest_status_code:: error ../status.yaml
|
||||||
|
|
||||||
|
- 404
|
||||||
|
|
||||||
|
Request Parameters
|
||||||
|
------------------
|
||||||
|
|
||||||
|
.. rest_parameters:: parameters.yaml
|
||||||
|
|
||||||
|
- project_id: project_id_path
|
@ -16,6 +16,7 @@ Block Storage API V3 (CURRENT)
|
|||||||
.. To create a volume, I might need a volume type, so list those next.
|
.. To create a volume, I might need a volume type, so list those next.
|
||||||
.. include:: volumes-v3-types.inc
|
.. include:: volumes-v3-types.inc
|
||||||
.. include:: volume-type-access.inc
|
.. include:: volume-type-access.inc
|
||||||
|
.. include:: default-types.inc
|
||||||
|
|
||||||
.. Now my primary focus is on volumes and what I can do with them.
|
.. Now my primary focus is on volumes and what I can do with them.
|
||||||
.. include:: volumes-v3-volumes.inc
|
.. include:: volumes-v3-volumes.inc
|
||||||
|
@ -168,6 +168,12 @@ volume_type_id:
|
|||||||
in: path
|
in: path
|
||||||
required: true
|
required: true
|
||||||
type: string
|
type: string
|
||||||
|
volume_type_name_or_id:
|
||||||
|
description: |
|
||||||
|
The name or UUID for an existing volume type.
|
||||||
|
in: path
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
|
||||||
# variables in query
|
# variables in query
|
||||||
action:
|
action:
|
||||||
|
6
api-ref/source/v3/samples/get-default-type-response.json
Normal file
6
api-ref/source/v3/samples/get-default-type-response.json
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"default_type": {
|
||||||
|
"project_id": "6685584b-1eac-4da6-b5c3-555430cf68ff",
|
||||||
|
"volume_type_id": "40ec6e5e-c9bd-4170-8740-c1cd42d7eabb"
|
||||||
|
}
|
||||||
|
}
|
12
api-ref/source/v3/samples/get-default-types-response.json
Normal file
12
api-ref/source/v3/samples/get-default-types-response.json
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"default_types": [
|
||||||
|
{
|
||||||
|
"project_id": "6685584b-1eac-4da6-b5c3-555430cf68ff",
|
||||||
|
"volume_type_id": "40ec6e5e-c9bd-4170-8740-c1cd42d7eabb"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"project_id": "dd46ea3e-6f3f-4e50-85fa-40c182e25d12",
|
||||||
|
"volume_type_id": "9fb51b63-3cd4-493f-9380-53d8f0a04bd4"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
5
api-ref/source/v3/samples/set-default-type-request.json
Normal file
5
api-ref/source/v3/samples/set-default-type-request.json
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"default_type": {
|
||||||
|
"volume_type": "lvm_backend"
|
||||||
|
}
|
||||||
|
}
|
6
api-ref/source/v3/samples/set-default-type-response.json
Normal file
6
api-ref/source/v3/samples/set-default-type-response.json
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"default_type": {
|
||||||
|
"project_id": "6685584b-1eac-4da6-b5c3-555430cf68ff",
|
||||||
|
"volume_type_id": "40ec6e5e-c9bd-4170-8740-c1cd42d7eabb"
|
||||||
|
}
|
||||||
|
}
|
@ -22,7 +22,7 @@
|
|||||||
"min_version": "3.0",
|
"min_version": "3.0",
|
||||||
"status": "CURRENT",
|
"status": "CURRENT",
|
||||||
"updated": "2018-07-17T00:00:00Z",
|
"updated": "2018-07-17T00:00:00Z",
|
||||||
"version": "3.61"
|
"version": "3.62"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@ -46,7 +46,7 @@
|
|||||||
"min_version": "3.0",
|
"min_version": "3.0",
|
||||||
"status": "CURRENT",
|
"status": "CURRENT",
|
||||||
"updated": "2018-07-17T00:00:00Z",
|
"updated": "2018-07-17T00:00:00Z",
|
||||||
"version": "3.61"
|
"version": "3.62"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@ -163,6 +163,8 @@ VOLUME_TIME_COMPARISON_FILTER = '3.60'
|
|||||||
|
|
||||||
VOLUME_CLUSTER_NAME = '3.61'
|
VOLUME_CLUSTER_NAME = '3.61'
|
||||||
|
|
||||||
|
DEFAULT_TYPE_OVERRIDES = '3.62'
|
||||||
|
|
||||||
|
|
||||||
def get_mv_header(version):
|
def get_mv_header(version):
|
||||||
"""Gets a formatted HTTP microversion header.
|
"""Gets a formatted HTTP microversion header.
|
||||||
|
@ -141,6 +141,7 @@ REST_API_VERSION_HISTORY = """
|
|||||||
("GET /v3/{project_id}/volumes/detail") requests.
|
("GET /v3/{project_id}/volumes/detail") requests.
|
||||||
* 3.61 - Add ``cluster_name`` attribute to response body of volume details
|
* 3.61 - Add ``cluster_name`` attribute to response body of volume details
|
||||||
for admin.
|
for admin.
|
||||||
|
* 3.62 - Default volume type overrides
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# The minimum and maximum versions of the API supported
|
# The minimum and maximum versions of the API supported
|
||||||
@ -148,9 +149,9 @@ REST_API_VERSION_HISTORY = """
|
|||||||
# minimum version of the API supported.
|
# minimum version of the API supported.
|
||||||
# Explicitly using /v2 endpoints will still work
|
# Explicitly using /v2 endpoints will still work
|
||||||
_MIN_API_VERSION = "3.0"
|
_MIN_API_VERSION = "3.0"
|
||||||
_MAX_API_VERSION = "3.61"
|
_MAX_API_VERSION = "3.62"
|
||||||
_LEGACY_API_VERSION2 = "2.0"
|
_LEGACY_API_VERSION2 = "2.0"
|
||||||
UPDATED = "2018-07-17T00:00:00Z"
|
UPDATED = "2020-10-14T00:00:00Z"
|
||||||
|
|
||||||
|
|
||||||
# NOTE(cyeoh): min and max versions declared as functions so we can
|
# NOTE(cyeoh): min and max versions declared as functions so we can
|
||||||
|
@ -473,3 +473,9 @@ Time must be expressed in ISO 8601 format.
|
|||||||
----
|
----
|
||||||
Add ``cluster_name`` attribute to response body of volume details for admin in
|
Add ``cluster_name`` attribute to response body of volume details for admin in
|
||||||
Active/Active HA mode.
|
Active/Active HA mode.
|
||||||
|
|
||||||
|
3.62
|
||||||
|
----
|
||||||
|
Add support for set, get, and unset a default volume type for a specific
|
||||||
|
project. Setting this default overrides the configured default_volume_type
|
||||||
|
value.
|
||||||
|
34
cinder/api/schemas/default_types.py
Normal file
34
cinder/api/schemas/default_types.py
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
# Copyright 2020 Red Hat, Inc.
|
||||||
|
# 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.
|
||||||
|
"""
|
||||||
|
Schema for V3 Default types API.
|
||||||
|
|
||||||
|
"""
|
||||||
|
from cinder.api.validation import parameter_types
|
||||||
|
|
||||||
|
|
||||||
|
create_or_update = {
|
||||||
|
'type': 'object',
|
||||||
|
'properties': {
|
||||||
|
'default_type': {
|
||||||
|
'type': 'object',
|
||||||
|
'properties': {
|
||||||
|
'volume_type': parameter_types.name,
|
||||||
|
},
|
||||||
|
'required': ['volume_type'],
|
||||||
|
'additionalProperties': False,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
@ -52,7 +52,7 @@ class VolumeTypesController(wsgi.Controller):
|
|||||||
|
|
||||||
# get default volume type
|
# get default volume type
|
||||||
if id is not None and id == 'default':
|
if id is not None and id == 'default':
|
||||||
vol_type = volume_types.get_default_volume_type()
|
vol_type = volume_types.get_default_volume_type(context)
|
||||||
if not vol_type:
|
if not vol_type:
|
||||||
msg = _("Default volume type can not be found.")
|
msg = _("Default volume type can not be found.")
|
||||||
raise exception.VolumeTypeNotFound(message=msg)
|
raise exception.VolumeTypeNotFound(message=msg)
|
||||||
|
127
cinder/api/v3/default_types.py
Normal file
127
cinder/api/v3/default_types.py
Normal file
@ -0,0 +1,127 @@
|
|||||||
|
# Copyright 2020 Red Hat, Inc.
|
||||||
|
# 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.
|
||||||
|
|
||||||
|
"""The resource filters api."""
|
||||||
|
|
||||||
|
from keystoneauth1 import exceptions as ks_exc
|
||||||
|
from six.moves import http_client
|
||||||
|
from webob import exc
|
||||||
|
|
||||||
|
from cinder.api import microversions as mv
|
||||||
|
from cinder.api.openstack import wsgi
|
||||||
|
from cinder.api.schemas import default_types as default_types
|
||||||
|
from cinder.api.v3.views import default_types as default_types_view
|
||||||
|
from cinder.api import validation
|
||||||
|
from cinder import db
|
||||||
|
from cinder import exception
|
||||||
|
from cinder.i18n import _
|
||||||
|
from cinder import objects
|
||||||
|
from cinder.policies import default_types as policy
|
||||||
|
from cinder import quota_utils
|
||||||
|
|
||||||
|
|
||||||
|
class DefaultTypesController(wsgi.Controller):
|
||||||
|
"""The Default types API controller for the OpenStack API."""
|
||||||
|
|
||||||
|
_view_builder_class = default_types_view.ViewBuilder
|
||||||
|
|
||||||
|
def _validate_project_and_authorize(self, context, project_id,
|
||||||
|
policy_check):
|
||||||
|
try:
|
||||||
|
target_project = quota_utils.get_project_hierarchy(context,
|
||||||
|
project_id)
|
||||||
|
target_project = {'project_id': target_project.id,
|
||||||
|
'domain_id': target_project.domain_id}
|
||||||
|
context.authorize(policy_check, target=target_project)
|
||||||
|
except ks_exc.http.NotFound:
|
||||||
|
explanation = _("Project with id %s not found." % project_id)
|
||||||
|
raise exc.HTTPNotFound(explanation=explanation)
|
||||||
|
except exception.NotAuthorized:
|
||||||
|
explanation = _("You are not authorized to perform this "
|
||||||
|
"operation.")
|
||||||
|
raise exc.HTTPForbidden(explanation=explanation)
|
||||||
|
|
||||||
|
@wsgi.response(http_client.OK)
|
||||||
|
@wsgi.Controller.api_version(mv.DEFAULT_TYPE_OVERRIDES)
|
||||||
|
@validation.schema(default_types.create_or_update)
|
||||||
|
def create_update(self, req, id, body):
|
||||||
|
"""Set a default volume type for the specified project."""
|
||||||
|
context = req.environ['cinder.context']
|
||||||
|
|
||||||
|
project_id = id
|
||||||
|
volume_type_id = body['default_type']['volume_type']
|
||||||
|
|
||||||
|
self._validate_project_and_authorize(context, project_id,
|
||||||
|
policy.CREATE_UPDATE_POLICY)
|
||||||
|
try:
|
||||||
|
volume_type_id = objects.VolumeType.get_by_name_or_id(
|
||||||
|
context, volume_type_id).id
|
||||||
|
|
||||||
|
except exception.VolumeTypeNotFound as e:
|
||||||
|
raise exc.HTTPBadRequest(explanation=e.msg)
|
||||||
|
|
||||||
|
default_type = db.project_default_volume_type_set(
|
||||||
|
context, volume_type_id, project_id)
|
||||||
|
|
||||||
|
return self._view_builder.create(default_type)
|
||||||
|
|
||||||
|
@wsgi.response(http_client.OK)
|
||||||
|
@wsgi.Controller.api_version(mv.DEFAULT_TYPE_OVERRIDES)
|
||||||
|
def detail(self, req, id):
|
||||||
|
"""Return detail of a default type."""
|
||||||
|
|
||||||
|
context = req.environ['cinder.context']
|
||||||
|
|
||||||
|
project_id = id
|
||||||
|
self._validate_project_and_authorize(context, project_id,
|
||||||
|
policy.GET_POLICY)
|
||||||
|
default_type = db.project_default_volume_type_get(context, project_id)
|
||||||
|
if not default_type:
|
||||||
|
raise exception.VolumeTypeProjectDefaultNotFound(
|
||||||
|
project_id=project_id)
|
||||||
|
return self._view_builder.detail(default_type)
|
||||||
|
|
||||||
|
@wsgi.response(http_client.OK)
|
||||||
|
@wsgi.Controller.api_version(mv.DEFAULT_TYPE_OVERRIDES)
|
||||||
|
def index(self, req):
|
||||||
|
"""Return a list of default types."""
|
||||||
|
|
||||||
|
context = req.environ['cinder.context']
|
||||||
|
try:
|
||||||
|
context.authorize(policy.GET_ALL_POLICY)
|
||||||
|
except exception.NotAuthorized:
|
||||||
|
explanation = _("You are not authorized to perform this "
|
||||||
|
"operation.")
|
||||||
|
raise exc.HTTPForbidden(explanation=explanation)
|
||||||
|
|
||||||
|
default_types = db.project_default_volume_type_get(context)
|
||||||
|
return self._view_builder.index(default_types)
|
||||||
|
|
||||||
|
@wsgi.response(http_client.NO_CONTENT)
|
||||||
|
@wsgi.Controller.api_version(mv.DEFAULT_TYPE_OVERRIDES)
|
||||||
|
def delete(self, req, id):
|
||||||
|
"""Unset a default volume type for a project."""
|
||||||
|
|
||||||
|
context = req.environ['cinder.context']
|
||||||
|
|
||||||
|
project_id = id
|
||||||
|
self._validate_project_and_authorize(context, project_id,
|
||||||
|
policy.DELETE_POLICY)
|
||||||
|
db.project_default_volume_type_unset(context, id)
|
||||||
|
|
||||||
|
|
||||||
|
def create_resource():
|
||||||
|
"""Create the wsgi resource for this controller."""
|
||||||
|
return wsgi.Resource(DefaultTypesController())
|
@ -27,6 +27,7 @@ from cinder.api.v3 import attachments
|
|||||||
from cinder.api.v3 import backups
|
from cinder.api.v3 import backups
|
||||||
from cinder.api.v3 import clusters
|
from cinder.api.v3 import clusters
|
||||||
from cinder.api.v3 import consistencygroups
|
from cinder.api.v3 import consistencygroups
|
||||||
|
from cinder.api.v3 import default_types
|
||||||
from cinder.api.v3 import group_snapshots
|
from cinder.api.v3 import group_snapshots
|
||||||
from cinder.api.v3 import group_specs
|
from cinder.api.v3 import group_specs
|
||||||
from cinder.api.v3 import group_types
|
from cinder.api.v3 import group_types
|
||||||
@ -199,3 +200,24 @@ class APIRouter(cinder.api.openstack.APIRouter):
|
|||||||
controller=self.resources['volume_transfers'],
|
controller=self.resources['volume_transfers'],
|
||||||
collection={'detail': 'GET'},
|
collection={'detail': 'GET'},
|
||||||
member={'accept': 'POST'})
|
member={'accept': 'POST'})
|
||||||
|
|
||||||
|
self.resources['default_types'] = default_types.create_resource()
|
||||||
|
mapper.connect("default-types", "/default-types/{id}",
|
||||||
|
controller=self.resources['default_types'],
|
||||||
|
action='create_update',
|
||||||
|
conditions={"method": ['PUT']})
|
||||||
|
|
||||||
|
mapper.connect("default-types", "/default-types",
|
||||||
|
controller=self.resources['default_types'],
|
||||||
|
action='index',
|
||||||
|
conditions={"method": ['GET']})
|
||||||
|
|
||||||
|
mapper.connect("default-types", "/default-types/{id}",
|
||||||
|
controller=self.resources['default_types'],
|
||||||
|
action='detail',
|
||||||
|
conditions={"method": ['GET']})
|
||||||
|
|
||||||
|
mapper.connect("default-types", "/default-types/{id}",
|
||||||
|
controller=self.resources['default_types'],
|
||||||
|
action='delete',
|
||||||
|
conditions={"method": ['DELETE']})
|
||||||
|
68
cinder/api/v3/views/default_types.py
Normal file
68
cinder/api/v3/views/default_types.py
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
# Copyright 2020 Red Hat, Inc.
|
||||||
|
# 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.
|
||||||
|
|
||||||
|
|
||||||
|
class ViewBuilder(object):
|
||||||
|
"""Model default type API response as a python dictionary."""
|
||||||
|
|
||||||
|
_collection_name = "default_types"
|
||||||
|
|
||||||
|
def _convert_to_dict(self, default):
|
||||||
|
return {'project_id': default.project_id,
|
||||||
|
'volume_type_id': default.volume_type_id}
|
||||||
|
|
||||||
|
def create(self, default_type):
|
||||||
|
"""Detailed view of a default type when set."""
|
||||||
|
|
||||||
|
return {'default_type': self._convert_to_dict(default_type)}
|
||||||
|
|
||||||
|
def index(self, default_types):
|
||||||
|
"""Build a view of a list of default types.
|
||||||
|
|
||||||
|
.. code-block:: json
|
||||||
|
|
||||||
|
{"default_types":
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"project_id": "248592b4-a6da-4c4c-abe0-9d8dbe0b74b4",
|
||||||
|
"volume_type_id": "7152eb1e-aef0-4bcd-a3ab-46b7ef17e2e6"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"project_id": "1234567-4c4c-abcd-abe0-1a2b3c4d5e6ff",
|
||||||
|
"volume_type_id": "5e3b298a-f1fc-4d32-9828-0d720da81ddd"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
default_types_view = []
|
||||||
|
for default_type in default_types:
|
||||||
|
default_types_view.append(self._convert_to_dict(default_type))
|
||||||
|
|
||||||
|
return {'default_types': default_types_view}
|
||||||
|
|
||||||
|
def detail(self, default_type):
|
||||||
|
"""Build a view of a default type.
|
||||||
|
|
||||||
|
.. code-block:: json
|
||||||
|
|
||||||
|
{"default_type":
|
||||||
|
{
|
||||||
|
"project_id": "248592b4-a6da-4c4c-abe0-9d8dbe0b74b4",
|
||||||
|
"volume_type_id": "6bd1de9a-b8b5-4c43-a597-00170ab06b50"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
return {'default_type': self._convert_to_dict(default_type)}
|
@ -718,6 +718,27 @@ def volume_type_access_remove(context, type_id, project_id):
|
|||||||
return IMPL.volume_type_access_remove(context, type_id, project_id)
|
return IMPL.volume_type_access_remove(context, type_id, project_id)
|
||||||
|
|
||||||
|
|
||||||
|
def project_default_volume_type_set(context, volume_type_id, project_id):
|
||||||
|
"""Set default volume type for a project"""
|
||||||
|
return IMPL.project_default_volume_type_set(context, volume_type_id,
|
||||||
|
project_id)
|
||||||
|
|
||||||
|
|
||||||
|
def project_default_volume_type_get(context, project_id=None):
|
||||||
|
"""Get default volume type for a project"""
|
||||||
|
return IMPL.project_default_volume_type_get(context, project_id)
|
||||||
|
|
||||||
|
|
||||||
|
def project_default_volume_type_unset(context, project_id):
|
||||||
|
"""Unset default volume type for a project (hard delete)"""
|
||||||
|
return IMPL.project_default_volume_type_unset(context, project_id)
|
||||||
|
|
||||||
|
|
||||||
|
def get_all_projects_with_default_type(context, volume_type_id):
|
||||||
|
"""Get all the projects associated with a default type"""
|
||||||
|
return IMPL.get_all_projects_with_default_type(context, volume_type_id)
|
||||||
|
|
||||||
|
|
||||||
####################
|
####################
|
||||||
|
|
||||||
|
|
||||||
|
@ -20,6 +20,7 @@
|
|||||||
|
|
||||||
import collections
|
import collections
|
||||||
from collections import abc
|
from collections import abc
|
||||||
|
import contextlib
|
||||||
import datetime as dt
|
import datetime as dt
|
||||||
import functools
|
import functools
|
||||||
import itertools
|
import itertools
|
||||||
@ -4355,6 +4356,65 @@ def volume_type_access_remove(context, type_id, project_id):
|
|||||||
volume_type_id=type_id, project_id=project_id)
|
volume_type_id=type_id, project_id=project_id)
|
||||||
|
|
||||||
|
|
||||||
|
def project_default_volume_type_set(context, volume_type_id, project_id):
|
||||||
|
"""Set default volume type for a project"""
|
||||||
|
|
||||||
|
session = get_session()
|
||||||
|
with session.begin():
|
||||||
|
update_default = project_default_volume_type_get(context, project_id,
|
||||||
|
session=session)
|
||||||
|
if update_default:
|
||||||
|
LOG.info("Updating default type for project %s", project_id)
|
||||||
|
update_default.volume_type_id = volume_type_id
|
||||||
|
return update_default
|
||||||
|
|
||||||
|
access_ref = models.DefaultVolumeTypes(volume_type_id=volume_type_id,
|
||||||
|
project_id=project_id)
|
||||||
|
access_ref.save(session=session)
|
||||||
|
return access_ref
|
||||||
|
|
||||||
|
|
||||||
|
def project_default_volume_type_get(context, project_id=None, session=None):
|
||||||
|
"""Get default volume type(s) for a project(s)
|
||||||
|
|
||||||
|
If a project id is passed, it returns default type for that particular
|
||||||
|
project else returns default volume types for all projects
|
||||||
|
"""
|
||||||
|
if session:
|
||||||
|
# This is requested by set method.
|
||||||
|
# To avoid race condition, we use the same session here
|
||||||
|
session_ctxt = contextlib.suppress()
|
||||||
|
else:
|
||||||
|
session = get_session()
|
||||||
|
session_ctxt = session.begin()
|
||||||
|
with session_ctxt:
|
||||||
|
if project_id:
|
||||||
|
return model_query(context, models.DefaultVolumeTypes,
|
||||||
|
session=session).\
|
||||||
|
filter_by(project_id=project_id).first()
|
||||||
|
return model_query(context, models.DefaultVolumeTypes,
|
||||||
|
session=session).all()
|
||||||
|
|
||||||
|
|
||||||
|
def get_all_projects_with_default_type(context, volume_type_id):
|
||||||
|
"""Get all projects with volume_type_id as their default type"""
|
||||||
|
session = get_session()
|
||||||
|
with session.begin():
|
||||||
|
return model_query(context, models.DefaultVolumeTypes,
|
||||||
|
session=session).\
|
||||||
|
filter_by(volume_type_id=volume_type_id).all()
|
||||||
|
|
||||||
|
|
||||||
|
def project_default_volume_type_unset(context, project_id):
|
||||||
|
"""Unset default volume type for a project (hard delete)"""
|
||||||
|
|
||||||
|
session = get_session()
|
||||||
|
with session.begin():
|
||||||
|
(model_query(context, models.DefaultVolumeTypes,
|
||||||
|
session=session).
|
||||||
|
filter_by(project_id=project_id).delete())
|
||||||
|
|
||||||
|
|
||||||
@require_admin_context
|
@require_admin_context
|
||||||
def group_type_access_remove(context, type_id, project_id):
|
def group_type_access_remove(context, type_id, project_id):
|
||||||
"""Remove given tenant from the group type access list."""
|
"""Remove given tenant from the group type access list."""
|
||||||
|
@ -0,0 +1,45 @@
|
|||||||
|
# Copyright 2020 Red Hat, Inc.
|
||||||
|
# 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.
|
||||||
|
|
||||||
|
from sqlalchemy import Boolean, Column, DateTime
|
||||||
|
from sqlalchemy import MetaData, String, Table, ForeignKey
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade(migrate_engine):
|
||||||
|
meta = MetaData()
|
||||||
|
meta.bind = migrate_engine
|
||||||
|
|
||||||
|
# This is required to establish foreign key dependency between
|
||||||
|
# volume_type_id and volume_types.id columns. See L#34-35
|
||||||
|
Table('volume_types', meta, autoload=True)
|
||||||
|
|
||||||
|
default_volume_types = Table(
|
||||||
|
'default_volume_types', meta,
|
||||||
|
Column('created_at', DateTime),
|
||||||
|
Column('updated_at', DateTime),
|
||||||
|
Column('deleted_at', DateTime),
|
||||||
|
Column('volume_type_id', String(36),
|
||||||
|
ForeignKey('volume_types.id'), index=True),
|
||||||
|
Column('project_id', String(length=255), unique=True,
|
||||||
|
primary_key=True, nullable=False),
|
||||||
|
Column('deleted', Boolean(create_constraint=True, name=None)),
|
||||||
|
mysql_engine='InnoDB',
|
||||||
|
mysql_charset='utf8'
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
default_volume_types.create()
|
||||||
|
except Exception:
|
||||||
|
raise
|
@ -513,6 +513,18 @@ class GroupTypeSpecs(BASE, CinderBase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class DefaultVolumeTypes(BASE, CinderBase):
|
||||||
|
"""Represent projects associated volume_types."""
|
||||||
|
__tablename__ = "default_volume_types"
|
||||||
|
volume_type_id = Column(String, ForeignKey('volume_types.id'),
|
||||||
|
nullable=False, index=True)
|
||||||
|
project_id = Column(String(255), unique=True, primary_key=True)
|
||||||
|
volume_type = relationship(
|
||||||
|
VolumeType,
|
||||||
|
foreign_keys=volume_type_id,
|
||||||
|
primaryjoin='DefaultVolumeTypes.volume_type_id == VolumeType.id')
|
||||||
|
|
||||||
|
|
||||||
class QualityOfServiceSpecs(BASE, CinderBase):
|
class QualityOfServiceSpecs(BASE, CinderBase):
|
||||||
"""Represents QoS specs as key/value pairs.
|
"""Represents QoS specs as key/value pairs.
|
||||||
|
|
||||||
|
@ -392,7 +392,7 @@ class VolumeTypeDeletionError(Invalid):
|
|||||||
|
|
||||||
|
|
||||||
class VolumeTypeDefaultDeletionError(Invalid):
|
class VolumeTypeDefaultDeletionError(Invalid):
|
||||||
message = _("The volume type %(volume_type_id)s is the default volume "
|
message = _("The volume type %(volume_type_id)s is a default volume "
|
||||||
"type and cannot be deleted.")
|
"type and cannot be deleted.")
|
||||||
|
|
||||||
|
|
||||||
@ -401,6 +401,10 @@ class VolumeTypeDefaultMisconfiguredError(CinderException):
|
|||||||
"%(volume_type_name)s cannot be found.")
|
"%(volume_type_name)s cannot be found.")
|
||||||
|
|
||||||
|
|
||||||
|
class VolumeTypeProjectDefaultNotFound(NotFound):
|
||||||
|
message = _("Default type for project %(project_id)s not found.")
|
||||||
|
|
||||||
|
|
||||||
class GroupTypeNotFound(NotFound):
|
class GroupTypeNotFound(NotFound):
|
||||||
message = _("Group type %(group_type_id)s could not be found.")
|
message = _("Group type %(group_type_id)s could not be found.")
|
||||||
|
|
||||||
|
@ -21,6 +21,7 @@ from cinder.policies import backups
|
|||||||
from cinder.policies import base
|
from cinder.policies import base
|
||||||
from cinder.policies import capabilities
|
from cinder.policies import capabilities
|
||||||
from cinder.policies import clusters
|
from cinder.policies import clusters
|
||||||
|
from cinder.policies import default_types
|
||||||
from cinder.policies import group_actions
|
from cinder.policies import group_actions
|
||||||
from cinder.policies import group_snapshot_actions
|
from cinder.policies import group_snapshot_actions
|
||||||
from cinder.policies import group_snapshots
|
from cinder.policies import group_snapshots
|
||||||
@ -83,4 +84,5 @@ def list_rules():
|
|||||||
volume_metadata.list_rules(),
|
volume_metadata.list_rules(),
|
||||||
type_extra_specs.list_rules(),
|
type_extra_specs.list_rules(),
|
||||||
volumes.list_rules(),
|
volumes.list_rules(),
|
||||||
|
default_types.list_rules(),
|
||||||
)
|
)
|
||||||
|
@ -18,6 +18,10 @@ from oslo_policy import policy
|
|||||||
RULE_ADMIN_OR_OWNER = 'rule:admin_or_owner'
|
RULE_ADMIN_OR_OWNER = 'rule:admin_or_owner'
|
||||||
RULE_ADMIN_API = 'rule:admin_api'
|
RULE_ADMIN_API = 'rule:admin_api'
|
||||||
|
|
||||||
|
SYSTEM_ADMIN = 'role:admin and system_scope:all'
|
||||||
|
|
||||||
|
SYSTEM_OR_DOMAIN_OR_PROJECT_ADMIN = 'rule:system_or_domain_or_project_admin'
|
||||||
|
|
||||||
rules = [
|
rules = [
|
||||||
policy.RuleDefault('context_is_admin', 'role:admin',
|
policy.RuleDefault('context_is_admin', 'role:admin',
|
||||||
description="Decides what is required for the "
|
description="Decides what is required for the "
|
||||||
@ -30,6 +34,12 @@ rules = [
|
|||||||
'is_admin:True or (role:admin and '
|
'is_admin:True or (role:admin and '
|
||||||
'is_admin_project:True)',
|
'is_admin_project:True)',
|
||||||
description="Default rule for most Admin APIs."),
|
description="Default rule for most Admin APIs."),
|
||||||
|
policy.RuleDefault('system_or_domain_or_project_admin',
|
||||||
|
'(role:admin and system_scope:all) or '
|
||||||
|
'(role:admin and domain_id:%(domain_id)s) or '
|
||||||
|
'(role:admin and project_id:%(project_id)s)',
|
||||||
|
description="Default rule for admins of cloud, domain "
|
||||||
|
"or a project."),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
76
cinder/policies/default_types.py
Normal file
76
cinder/policies/default_types.py
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
# Copyright 2020 Red Hat, Inc.
|
||||||
|
# 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.
|
||||||
|
|
||||||
|
from oslo_policy import policy
|
||||||
|
|
||||||
|
from cinder.policies import base
|
||||||
|
|
||||||
|
CREATE_UPDATE_POLICY = "volume_extension:default_set_or_update"
|
||||||
|
GET_POLICY = "volume_extension:default_get"
|
||||||
|
GET_ALL_POLICY = "volume_extension:default_get_all"
|
||||||
|
DELETE_POLICY = "volume_extension:default_unset"
|
||||||
|
|
||||||
|
default_type_policies = [
|
||||||
|
policy.DocumentedRuleDefault(
|
||||||
|
name=CREATE_UPDATE_POLICY,
|
||||||
|
check_str=base.SYSTEM_OR_DOMAIN_OR_PROJECT_ADMIN,
|
||||||
|
scope_types=['system'],
|
||||||
|
description="Set or update default volume type.",
|
||||||
|
operations=[
|
||||||
|
{
|
||||||
|
'method': 'PUT',
|
||||||
|
'path': '/default-types'
|
||||||
|
}
|
||||||
|
]),
|
||||||
|
policy.DocumentedRuleDefault(
|
||||||
|
name=GET_POLICY,
|
||||||
|
check_str=base.SYSTEM_OR_DOMAIN_OR_PROJECT_ADMIN,
|
||||||
|
scope_types=['system'],
|
||||||
|
description="Get default types.",
|
||||||
|
operations=[
|
||||||
|
{
|
||||||
|
'method': 'GET',
|
||||||
|
'path': '/default-types/{project-id}'
|
||||||
|
}
|
||||||
|
]),
|
||||||
|
policy.DocumentedRuleDefault(
|
||||||
|
name=GET_ALL_POLICY,
|
||||||
|
check_str=base.SYSTEM_ADMIN,
|
||||||
|
scope_types=['system'],
|
||||||
|
description="Get all default types. "
|
||||||
|
"WARNING: Changing this might open up too much "
|
||||||
|
"information regarding cloud deployment.",
|
||||||
|
operations=[
|
||||||
|
{
|
||||||
|
'method': 'GET',
|
||||||
|
'path': '/default-types/'
|
||||||
|
}
|
||||||
|
]),
|
||||||
|
policy.DocumentedRuleDefault(
|
||||||
|
name=DELETE_POLICY,
|
||||||
|
check_str=base.SYSTEM_OR_DOMAIN_OR_PROJECT_ADMIN,
|
||||||
|
scope_types=['system'],
|
||||||
|
description="Unset default type.",
|
||||||
|
operations=[
|
||||||
|
{
|
||||||
|
'method': 'DELETE',
|
||||||
|
'path': '/default-types/{project-id}'
|
||||||
|
}
|
||||||
|
]),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def list_rules():
|
||||||
|
return default_type_policies
|
@ -155,20 +155,20 @@ def authorize(context, action, target, do_raise=True, exc=None):
|
|||||||
do_raise is False.
|
do_raise is False.
|
||||||
"""
|
"""
|
||||||
init()
|
init()
|
||||||
credentials = context.to_policy_values()
|
|
||||||
if not exc:
|
if not exc:
|
||||||
exc = exception.PolicyNotAuthorized
|
exc = exception.PolicyNotAuthorized
|
||||||
try:
|
try:
|
||||||
result = _ENFORCER.authorize(action, target, credentials,
|
result = _ENFORCER.authorize(action, target, context,
|
||||||
do_raise=do_raise, exc=exc, action=action)
|
do_raise=do_raise, exc=exc, action=action)
|
||||||
except policy.PolicyNotRegistered:
|
except policy.PolicyNotRegistered:
|
||||||
with excutils.save_and_reraise_exception():
|
with excutils.save_and_reraise_exception():
|
||||||
LOG.exception('Policy not registered')
|
LOG.exception('Policy not registered')
|
||||||
except Exception:
|
except Exception:
|
||||||
with excutils.save_and_reraise_exception():
|
with excutils.save_and_reraise_exception():
|
||||||
LOG.error('Policy check for %(action)s failed with credentials '
|
LOG.error('Policy check for %(action)s failed with context '
|
||||||
'%(credentials)s',
|
'%(credentials)s',
|
||||||
{'action': action, 'credentials': credentials})
|
{'action': action,
|
||||||
|
'credentials': context.to_policy_values()})
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
@ -36,8 +36,10 @@ class GenericProjectInfo(object):
|
|||||||
project_parent_id=None,
|
project_parent_id=None,
|
||||||
project_subtree=None,
|
project_subtree=None,
|
||||||
project_parent_tree=None,
|
project_parent_tree=None,
|
||||||
is_admin_project=False):
|
is_admin_project=False,
|
||||||
|
domain_id=None):
|
||||||
self.id = project_id
|
self.id = project_id
|
||||||
|
self.domain_id = domain_id
|
||||||
self.keystone_api_version = project_keystone_api_version
|
self.keystone_api_version = project_keystone_api_version
|
||||||
self.parent_id = project_parent_id
|
self.parent_id = project_parent_id
|
||||||
self.subtree = project_subtree
|
self.subtree = project_subtree
|
||||||
@ -106,6 +108,7 @@ def get_project_hierarchy(context, project_id, subtree_as_ids=False,
|
|||||||
parents_as_ids=parents_as_ids)
|
parents_as_ids=parents_as_ids)
|
||||||
|
|
||||||
generic_project.parent_id = None
|
generic_project.parent_id = None
|
||||||
|
generic_project.domain_id = project.domain_id
|
||||||
if project.parent_id != project.domain_id:
|
if project.parent_id != project.domain_id:
|
||||||
generic_project.parent_id = project.parent_id
|
generic_project.parent_id = project.parent_id
|
||||||
|
|
||||||
|
@ -56,6 +56,10 @@ class OpenStackApiException400(OpenStackApiException):
|
|||||||
message = _("400 Bad Request")
|
message = _("400 Bad Request")
|
||||||
|
|
||||||
|
|
||||||
|
class OpenStackApiException403(OpenStackApiException):
|
||||||
|
message = _("403 Forbidden")
|
||||||
|
|
||||||
|
|
||||||
class OpenStackApiException500(OpenStackApiException):
|
class OpenStackApiException500(OpenStackApiException):
|
||||||
message = _("500 Internal Server Error")
|
message = _("500 Internal Server Error")
|
||||||
|
|
||||||
@ -129,11 +133,15 @@ class TestOpenStackClient(object):
|
|||||||
self._authenticate(True)
|
self._authenticate(True)
|
||||||
|
|
||||||
def api_request(self, relative_uri, check_response_status=None,
|
def api_request(self, relative_uri, check_response_status=None,
|
||||||
strip_version=False, **kwargs):
|
strip_version=False, base_url=True, **kwargs):
|
||||||
auth_result = self._authenticate()
|
auth_result = self._authenticate()
|
||||||
|
|
||||||
# NOTE(justinsb): httplib 'helpfully' converts headers to lower case
|
if base_url:
|
||||||
|
# NOTE(justinsb): httplib 'helpfully' converts headers to lower
|
||||||
|
# case
|
||||||
base_uri = auth_result['x-server-management-url']
|
base_uri = auth_result['x-server-management-url']
|
||||||
|
else:
|
||||||
|
base_uri = self.auth_uri
|
||||||
|
|
||||||
if strip_version:
|
if strip_version:
|
||||||
# cut out version number and tenant_id
|
# cut out version number and tenant_id
|
||||||
@ -169,12 +177,12 @@ class TestOpenStackClient(object):
|
|||||||
else:
|
else:
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
def api_get(self, relative_uri, **kwargs):
|
def api_get(self, relative_uri, base_url=True, **kwargs):
|
||||||
kwargs.setdefault('check_response_status', [http_client.OK])
|
kwargs.setdefault('check_response_status', [http_client.OK])
|
||||||
response = self.api_request(relative_uri, **kwargs)
|
response = self.api_request(relative_uri, base_url=base_url, **kwargs)
|
||||||
return self._decode_json(response)
|
return self._decode_json(response)
|
||||||
|
|
||||||
def api_post(self, relative_uri, body, **kwargs):
|
def api_post(self, relative_uri, body, base_url=True, **kwargs):
|
||||||
kwargs['method'] = 'POST'
|
kwargs['method'] = 'POST'
|
||||||
if body:
|
if body:
|
||||||
headers = kwargs.setdefault('headers', {})
|
headers = kwargs.setdefault('headers', {})
|
||||||
@ -183,10 +191,10 @@ class TestOpenStackClient(object):
|
|||||||
|
|
||||||
kwargs.setdefault('check_response_status', [http_client.OK,
|
kwargs.setdefault('check_response_status', [http_client.OK,
|
||||||
http_client.ACCEPTED])
|
http_client.ACCEPTED])
|
||||||
response = self.api_request(relative_uri, **kwargs)
|
response = self.api_request(relative_uri, base_url=base_url, **kwargs)
|
||||||
return self._decode_json(response)
|
return self._decode_json(response)
|
||||||
|
|
||||||
def api_put(self, relative_uri, body, **kwargs):
|
def api_put(self, relative_uri, body, base_url=True, **kwargs):
|
||||||
kwargs['method'] = 'PUT'
|
kwargs['method'] = 'PUT'
|
||||||
if body:
|
if body:
|
||||||
headers = kwargs.setdefault('headers', {})
|
headers = kwargs.setdefault('headers', {})
|
||||||
@ -196,15 +204,15 @@ class TestOpenStackClient(object):
|
|||||||
kwargs.setdefault('check_response_status', [http_client.OK,
|
kwargs.setdefault('check_response_status', [http_client.OK,
|
||||||
http_client.ACCEPTED,
|
http_client.ACCEPTED,
|
||||||
http_client.NO_CONTENT])
|
http_client.NO_CONTENT])
|
||||||
response = self.api_request(relative_uri, **kwargs)
|
response = self.api_request(relative_uri, base_url=base_url, **kwargs)
|
||||||
return self._decode_json(response)
|
return self._decode_json(response)
|
||||||
|
|
||||||
def api_delete(self, relative_uri, **kwargs):
|
def api_delete(self, relative_uri, base_url=True, **kwargs):
|
||||||
kwargs['method'] = 'DELETE'
|
kwargs['method'] = 'DELETE'
|
||||||
kwargs.setdefault('check_response_status', [http_client.OK,
|
kwargs.setdefault('check_response_status', [http_client.OK,
|
||||||
http_client.ACCEPTED,
|
http_client.ACCEPTED,
|
||||||
http_client.NO_CONTENT])
|
http_client.NO_CONTENT])
|
||||||
return self.api_request(relative_uri, **kwargs)
|
return self.api_request(relative_uri, base_url=base_url, **kwargs)
|
||||||
|
|
||||||
def get_volume(self, volume_id):
|
def get_volume(self, volume_id):
|
||||||
return self.api_get('/volumes/%s' % volume_id)['volume']
|
return self.api_get('/volumes/%s' % volume_id)['volume']
|
||||||
@ -329,3 +337,19 @@ class TestOpenStackClient(object):
|
|||||||
|
|
||||||
def list_group_replication_targets(self, group_id, params):
|
def list_group_replication_targets(self, group_id, params):
|
||||||
return self.api_post('/groups/%s/action' % group_id, params)
|
return self.api_post('/groups/%s/action' % group_id, params)
|
||||||
|
|
||||||
|
def set_default_type(self, project_id, params):
|
||||||
|
body = {"default_type": params}
|
||||||
|
return self.api_put('default-types/%s' % project_id, body,
|
||||||
|
base_url=False)['default_type']
|
||||||
|
|
||||||
|
def get_default_type(self, project_id=None):
|
||||||
|
if project_id:
|
||||||
|
return self.api_get('default-types/%s' % project_id,
|
||||||
|
base_url=False)['default_type']
|
||||||
|
return self.api_get('default-types',
|
||||||
|
base_url=False)['default_types']
|
||||||
|
|
||||||
|
def unset_default_type(self, project_id):
|
||||||
|
self.api_delete('default-types/%s' % project_id,
|
||||||
|
base_url=False)
|
||||||
|
124
cinder/tests/functional/test_default_types.py
Normal file
124
cinder/tests/functional/test_default_types.py
Normal file
@ -0,0 +1,124 @@
|
|||||||
|
# Copyright 2020 Red Hat, Inc.
|
||||||
|
# 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.
|
||||||
|
|
||||||
|
from unittest import mock
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
from cinder import context
|
||||||
|
from cinder.tests.functional.api import client
|
||||||
|
from cinder.tests.functional import functional_helpers
|
||||||
|
|
||||||
|
|
||||||
|
class DefaultVolumeTypesTest(functional_helpers._FunctionalTestBase):
|
||||||
|
_vol_type_name = 'functional_test_type'
|
||||||
|
osapi_version_minor = '62'
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super(DefaultVolumeTypesTest, self).setUp()
|
||||||
|
self.volume_type = self.api.create_type(self._vol_type_name)
|
||||||
|
self.project = self.FakeProject()
|
||||||
|
# Need to mock out Keystone so the functional tests don't require other
|
||||||
|
# services
|
||||||
|
_keystone_client = mock.MagicMock()
|
||||||
|
_keystone_client.version = 'v3'
|
||||||
|
_keystone_client.projects.get.side_effect = self._get_project
|
||||||
|
_keystone_client_get = mock.patch(
|
||||||
|
'cinder.quota_utils._keystone_client',
|
||||||
|
lambda *args, **kwargs: _keystone_client)
|
||||||
|
_keystone_client_get.start()
|
||||||
|
self.addCleanup(_keystone_client_get.stop)
|
||||||
|
|
||||||
|
def _get_project(self, project_id, *args, **kwargs):
|
||||||
|
return self.project
|
||||||
|
|
||||||
|
class FakeProject(object):
|
||||||
|
_dom_id = uuid.uuid4().hex
|
||||||
|
|
||||||
|
def __init__(self, parent_id=None):
|
||||||
|
self.id = uuid.uuid4().hex
|
||||||
|
self.parent_id = parent_id
|
||||||
|
self.domain_id = self._dom_id
|
||||||
|
self.subtree = None
|
||||||
|
self.parents = None
|
||||||
|
|
||||||
|
@mock.patch.object(context.RequestContext, 'authorize')
|
||||||
|
def test_default_type_set(self, mock_authorize):
|
||||||
|
default_type = self.api.set_default_type(
|
||||||
|
self.project.id, {'volume_type': self._vol_type_name})
|
||||||
|
self.assertEqual(self.project.id, default_type['project_id'])
|
||||||
|
self.assertEqual(self.volume_type['id'],
|
||||||
|
default_type['volume_type_id'])
|
||||||
|
|
||||||
|
@mock.patch.object(context.RequestContext, 'authorize')
|
||||||
|
def test_default_type_get(self, mock_authorize):
|
||||||
|
self.api.set_default_type(self.project.id,
|
||||||
|
{'volume_type': self._vol_type_name})
|
||||||
|
default_type = self.api.get_default_type(project_id=self.project.id)
|
||||||
|
|
||||||
|
self.assertEqual(self.project.id, default_type['project_id'])
|
||||||
|
self.assertEqual(self.volume_type['id'],
|
||||||
|
default_type['volume_type_id'])
|
||||||
|
|
||||||
|
@mock.patch.object(context.RequestContext, 'authorize')
|
||||||
|
def test_default_type_get_all(self, mock_authorize):
|
||||||
|
self.api.set_default_type(self.project.id,
|
||||||
|
{'volume_type': self._vol_type_name})
|
||||||
|
default_types = self.api.get_default_type()
|
||||||
|
|
||||||
|
self.assertEqual(1, len(default_types))
|
||||||
|
self.assertEqual(self.project.id, default_types[0]['project_id'])
|
||||||
|
self.assertEqual(self.volume_type['id'],
|
||||||
|
default_types[0]['volume_type_id'])
|
||||||
|
|
||||||
|
@mock.patch.object(context.RequestContext, 'authorize')
|
||||||
|
def test_default_type_unset(self, mock_authorize):
|
||||||
|
self.api.set_default_type(self.project.id,
|
||||||
|
{'volume_type': self._vol_type_name})
|
||||||
|
|
||||||
|
default_types = self.api.get_default_type()
|
||||||
|
self.assertEqual(1, len(default_types))
|
||||||
|
self.api.unset_default_type(self.project.id)
|
||||||
|
default_types = self.api.get_default_type()
|
||||||
|
self.assertEqual(0, len(default_types))
|
||||||
|
|
||||||
|
def test_default_type_set_not_authorized(self):
|
||||||
|
self.assertRaises(client.OpenStackApiException403,
|
||||||
|
self.api.set_default_type,
|
||||||
|
self.project.id,
|
||||||
|
{'volume_type': self._vol_type_name})
|
||||||
|
|
||||||
|
@mock.patch.object(context.RequestContext, 'authorize')
|
||||||
|
def test_default_type_set_volume_type_not_found(self, mock_authorize):
|
||||||
|
self.assertRaises(client.OpenStackApiException400,
|
||||||
|
self.api.set_default_type,
|
||||||
|
self.project.id,
|
||||||
|
{'volume_type': 'fake_type'})
|
||||||
|
|
||||||
|
def test_default_type_get_not_authorized(self):
|
||||||
|
self.assertRaises(client.OpenStackApiException403,
|
||||||
|
self.api.get_default_type)
|
||||||
|
|
||||||
|
def test_default_type_unset_not_authorized(self):
|
||||||
|
self.assertRaises(client.OpenStackApiException403,
|
||||||
|
self.api.unset_default_type,
|
||||||
|
self.project.id)
|
||||||
|
|
||||||
|
@mock.patch.object(context.RequestContext, 'authorize')
|
||||||
|
def test_cannot_delete_project_default_type(self, mock_authorize):
|
||||||
|
default_type = self.api.set_default_type(
|
||||||
|
self.project.id, {'volume_type': self._vol_type_name})
|
||||||
|
self.assertRaises(client.OpenStackApiException400,
|
||||||
|
self.api.delete_type,
|
||||||
|
default_type['volume_type_id'])
|
@ -129,11 +129,13 @@ class HTTPRequest(webob.Request):
|
|||||||
kwargs['base_url'] = 'http://localhost/v3'
|
kwargs['base_url'] = 'http://localhost/v3'
|
||||||
use_admin_context = kwargs.pop('use_admin_context', False)
|
use_admin_context = kwargs.pop('use_admin_context', False)
|
||||||
version = kwargs.pop('version', api_version._MIN_API_VERSION)
|
version = kwargs.pop('version', api_version._MIN_API_VERSION)
|
||||||
|
system_scope = kwargs.pop('system_scope', None)
|
||||||
out = os_wsgi.Request.blank(*args, **kwargs)
|
out = os_wsgi.Request.blank(*args, **kwargs)
|
||||||
out.environ['cinder.context'] = FakeRequestContext(
|
out.environ['cinder.context'] = FakeRequestContext(
|
||||||
fake.USER_ID,
|
fake.USER_ID,
|
||||||
fake.PROJECT_ID,
|
fake.PROJECT_ID,
|
||||||
is_admin=use_admin_context)
|
is_admin=use_admin_context,
|
||||||
|
system_scope=system_scope)
|
||||||
out.api_version_request = api_version.APIVersionRequest(version)
|
out.api_version_request = api_version.APIVersionRequest(version)
|
||||||
return out
|
return out
|
||||||
|
|
||||||
|
@ -74,7 +74,7 @@ def return_volume_types_get_volume_type(context, id):
|
|||||||
return fake_volume_type(id)
|
return fake_volume_type(id)
|
||||||
|
|
||||||
|
|
||||||
def return_volume_types_get_default():
|
def return_volume_types_get_default(context):
|
||||||
return fake_volume_type(1)
|
return fake_volume_type(1)
|
||||||
|
|
||||||
|
|
||||||
|
227
cinder/tests/unit/api/v3/test_default_types.py
Normal file
227
cinder/tests/unit/api/v3/test_default_types.py
Normal file
@ -0,0 +1,227 @@
|
|||||||
|
# Copyright 2020 Red Hat, Inc.
|
||||||
|
# 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.
|
||||||
|
|
||||||
|
from unittest import mock
|
||||||
|
|
||||||
|
import webob
|
||||||
|
|
||||||
|
from cinder.api import microversions as mv
|
||||||
|
from cinder.api.v3 import default_types
|
||||||
|
from cinder import context
|
||||||
|
from cinder import exception
|
||||||
|
from cinder import objects
|
||||||
|
from cinder.tests.unit.api import fakes
|
||||||
|
from cinder.tests.unit import fake_constants as fake
|
||||||
|
from cinder.tests.unit import test
|
||||||
|
|
||||||
|
|
||||||
|
class DefaultVolumeTypesApiTest(test.TestCase):
|
||||||
|
|
||||||
|
def _create_volume_type(self, ctxt, volume_type_name, extra_specs=None,
|
||||||
|
is_public=True, projects=None):
|
||||||
|
vol_type = objects.VolumeType(ctxt,
|
||||||
|
name=volume_type_name,
|
||||||
|
is_public=is_public,
|
||||||
|
description='',
|
||||||
|
extra_specs=extra_specs,
|
||||||
|
projects=projects)
|
||||||
|
vol_type.create()
|
||||||
|
return vol_type
|
||||||
|
|
||||||
|
def _set_default_type_system_scope(self, project_id=fake.PROJECT_ID,
|
||||||
|
volume_type='volume_type1'):
|
||||||
|
body = {
|
||||||
|
'default_type':
|
||||||
|
{'volume_type': volume_type}
|
||||||
|
}
|
||||||
|
req = fakes.HTTPRequest.blank('/v3/default-types/%s' % project_id,
|
||||||
|
use_admin_context=True,
|
||||||
|
version=mv.DEFAULT_TYPE_OVERRIDES,
|
||||||
|
system_scope='all')
|
||||||
|
res_dict = self.controller.create_update(req, id=project_id,
|
||||||
|
body=body)
|
||||||
|
return res_dict
|
||||||
|
|
||||||
|
def _set_default_type_project_scope(self, project_id=fake.PROJECT_ID,
|
||||||
|
volume_type='volume_type1'):
|
||||||
|
body = {
|
||||||
|
'default_type':
|
||||||
|
{'volume_type': volume_type}
|
||||||
|
}
|
||||||
|
req = fakes.HTTPRequest.blank('/v3/default-types/%s' % project_id,
|
||||||
|
use_admin_context=True,
|
||||||
|
version=mv.DEFAULT_TYPE_OVERRIDES)
|
||||||
|
res_dict = self.controller.create_update(req, id=project_id,
|
||||||
|
body=body)
|
||||||
|
return res_dict
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super(DefaultVolumeTypesApiTest, self).setUp()
|
||||||
|
self.controller = default_types.DefaultTypesController()
|
||||||
|
self.ctxt = context.RequestContext(user_id=fake.USER_ID,
|
||||||
|
project_id=fake.PROJECT_ID,
|
||||||
|
is_admin=True,
|
||||||
|
system_scope='all')
|
||||||
|
self.type1 = self._create_volume_type(
|
||||||
|
self.ctxt, 'volume_type1')
|
||||||
|
self.type2 = self._create_volume_type(
|
||||||
|
self.ctxt, 'volume_type2')
|
||||||
|
|
||||||
|
get_patcher = mock.patch('cinder.quota_utils.get_project_hierarchy',
|
||||||
|
self._get_project)
|
||||||
|
get_patcher.start()
|
||||||
|
self.addCleanup(get_patcher.stop)
|
||||||
|
|
||||||
|
class FakeProject(object):
|
||||||
|
|
||||||
|
def __init__(self, id=fake.PROJECT_ID, domain_id=fake.DOMAIN_ID,
|
||||||
|
parent_id=None, is_admin_project=False):
|
||||||
|
self.id = id
|
||||||
|
self.domain_id = domain_id
|
||||||
|
|
||||||
|
def _get_project(self, context, id, subtree_as_ids=False,
|
||||||
|
parents_as_ids=False, is_admin_project=False):
|
||||||
|
return self.FakeProject(id)
|
||||||
|
|
||||||
|
def test_default_volume_types_create_update_system_admin(self):
|
||||||
|
res_dict = self._set_default_type_system_scope()
|
||||||
|
self.assertEqual(fake.PROJECT_ID,
|
||||||
|
res_dict['default_type']['project_id'])
|
||||||
|
self.assertEqual(self.type1.id,
|
||||||
|
res_dict['default_type']['volume_type_id'])
|
||||||
|
|
||||||
|
def test_default_volume_types_create_update_project_admin(self):
|
||||||
|
res_dict = self._set_default_type_project_scope()
|
||||||
|
self.assertEqual(fake.PROJECT_ID,
|
||||||
|
res_dict['default_type']['project_id'])
|
||||||
|
self.assertEqual(self.type1.id,
|
||||||
|
res_dict['default_type']['volume_type_id'])
|
||||||
|
|
||||||
|
def test_default_volume_types_detail_system_admin(self):
|
||||||
|
self._set_default_type_system_scope()
|
||||||
|
req = fakes.HTTPRequest.blank('/v3/default-types/%s' % fake.PROJECT_ID,
|
||||||
|
use_admin_context=True,
|
||||||
|
version=mv.DEFAULT_TYPE_OVERRIDES,
|
||||||
|
system_scope='all')
|
||||||
|
res_dict = self.controller.detail(req, fake.PROJECT_ID)
|
||||||
|
|
||||||
|
self.assertEqual(fake.PROJECT_ID,
|
||||||
|
res_dict['default_type']['project_id'])
|
||||||
|
self.assertEqual(self.type1.id,
|
||||||
|
res_dict['default_type']['volume_type_id'])
|
||||||
|
|
||||||
|
def test_default_volume_types_detail_project_admin(self):
|
||||||
|
self._set_default_type_project_scope()
|
||||||
|
req = fakes.HTTPRequest.blank('/v3/default-types/%s' % fake.PROJECT_ID,
|
||||||
|
use_admin_context=True,
|
||||||
|
version=mv.DEFAULT_TYPE_OVERRIDES)
|
||||||
|
res_dict = self.controller.detail(req, fake.PROJECT_ID)
|
||||||
|
|
||||||
|
self.assertEqual(fake.PROJECT_ID,
|
||||||
|
res_dict['default_type']['project_id'])
|
||||||
|
self.assertEqual(self.type1.id,
|
||||||
|
res_dict['default_type']['volume_type_id'])
|
||||||
|
|
||||||
|
def test_default_volume_types_detail_no_default_found(self):
|
||||||
|
req = fakes.HTTPRequest.blank('/v3/default-types/%s' % fake.PROJECT_ID,
|
||||||
|
use_admin_context=True,
|
||||||
|
version=mv.DEFAULT_TYPE_OVERRIDES,
|
||||||
|
system_scope='all')
|
||||||
|
self.assertRaises(exception.VolumeTypeProjectDefaultNotFound,
|
||||||
|
self.controller.detail, req, fake.PROJECT_ID)
|
||||||
|
|
||||||
|
def test_default_volume_types_list(self):
|
||||||
|
req = fakes.HTTPRequest.blank('/v3/default-types/',
|
||||||
|
use_admin_context=True,
|
||||||
|
version=mv.DEFAULT_TYPE_OVERRIDES,
|
||||||
|
system_scope='all')
|
||||||
|
# Confirm this returns an empty list when no default types are set
|
||||||
|
res_dict = self.controller.index(req)
|
||||||
|
self.assertEqual(0, len(res_dict['default_types']))
|
||||||
|
|
||||||
|
self._set_default_type_system_scope()
|
||||||
|
self._set_default_type_system_scope(project_id=fake.PROJECT2_ID,
|
||||||
|
volume_type='volume_type2')
|
||||||
|
res_dict = self.controller.index(req)
|
||||||
|
|
||||||
|
self.assertEqual(2, len(res_dict['default_types']))
|
||||||
|
self.assertEqual(fake.PROJECT_ID,
|
||||||
|
res_dict['default_types'][0]['project_id'])
|
||||||
|
self.assertEqual(fake.PROJECT2_ID,
|
||||||
|
res_dict['default_types'][1]['project_id'])
|
||||||
|
|
||||||
|
def test_default_volume_types_delete_system_admin(self):
|
||||||
|
self._set_default_type_system_scope()
|
||||||
|
req = fakes.HTTPRequest.blank('/v3/default-types/',
|
||||||
|
use_admin_context=True,
|
||||||
|
version=mv.DEFAULT_TYPE_OVERRIDES,
|
||||||
|
system_scope='all')
|
||||||
|
res_dict = self.controller.index(req)
|
||||||
|
self.assertEqual(1, len(res_dict['default_types']))
|
||||||
|
|
||||||
|
self.controller.delete(req, fake.PROJECT_ID)
|
||||||
|
res_dict_new = self.controller.index(req)
|
||||||
|
self.assertEqual(0, len(res_dict_new['default_types']))
|
||||||
|
|
||||||
|
def test_default_volume_types_delete_project_admin(self):
|
||||||
|
self._set_default_type_project_scope()
|
||||||
|
req = fakes.HTTPRequest.blank('/v3/default-types/',
|
||||||
|
use_admin_context=True,
|
||||||
|
version=mv.DEFAULT_TYPE_OVERRIDES)
|
||||||
|
req_admin = fakes.HTTPRequest.blank('/v3/default-types/',
|
||||||
|
use_admin_context=True,
|
||||||
|
version=mv.DEFAULT_TYPE_OVERRIDES,
|
||||||
|
system_scope='all')
|
||||||
|
res_dict = self.controller.index(req_admin)
|
||||||
|
self.assertEqual(1, len(res_dict['default_types']))
|
||||||
|
|
||||||
|
self.controller.delete(req, fake.PROJECT_ID)
|
||||||
|
res_dict_new = self.controller.index(req_admin)
|
||||||
|
self.assertEqual(0, len(res_dict_new['default_types']))
|
||||||
|
|
||||||
|
def test_default_volume_types_create_update_other_project(self):
|
||||||
|
body = {
|
||||||
|
'default_type':
|
||||||
|
{'volume_type': 'volume_type1'}
|
||||||
|
}
|
||||||
|
req = fakes.HTTPRequest.blank('/v3/default-types/%s' %
|
||||||
|
fake.PROJECT_ID,
|
||||||
|
use_admin_context=True,
|
||||||
|
version=mv.DEFAULT_TYPE_OVERRIDES)
|
||||||
|
self.assertRaises(webob.exc.HTTPForbidden,
|
||||||
|
self.controller.create_update, req,
|
||||||
|
id=fake.PROJECT2_ID, body=body)
|
||||||
|
|
||||||
|
def test_default_volume_types_detail_other_project(self):
|
||||||
|
req = fakes.HTTPRequest.blank('/v3/default-types/%s' % fake.PROJECT_ID,
|
||||||
|
use_admin_context=True,
|
||||||
|
version=mv.DEFAULT_TYPE_OVERRIDES)
|
||||||
|
self.assertRaises(webob.exc.HTTPForbidden, self.controller.detail, req,
|
||||||
|
fake.PROJECT2_ID)
|
||||||
|
|
||||||
|
def test_default_volume_types_index_no_system_scope(self):
|
||||||
|
self._set_default_type_system_scope(project_id=fake.PROJECT2_ID,
|
||||||
|
volume_type='volume_type2')
|
||||||
|
req = fakes.HTTPRequest.blank('/v3/default-types/',
|
||||||
|
use_admin_context=True,
|
||||||
|
version=mv.DEFAULT_TYPE_OVERRIDES)
|
||||||
|
self.assertRaises(webob.exc.HTTPForbidden, self.controller.index, req)
|
||||||
|
|
||||||
|
def test_default_volume_types_delete_other_project(self):
|
||||||
|
req = fakes.HTTPRequest.blank('/v3/default-types/',
|
||||||
|
use_admin_context=True,
|
||||||
|
version=mv.DEFAULT_TYPE_OVERRIDES)
|
||||||
|
self.assertRaises(webob.exc.HTTPForbidden, self.controller.delete, req,
|
||||||
|
fake.PROJECT2_ID)
|
@ -13,10 +13,13 @@
|
|||||||
from cinder.api import microversions as mv
|
from cinder.api import microversions as mv
|
||||||
from cinder.api.v2 import types
|
from cinder.api.v2 import types
|
||||||
from cinder import context
|
from cinder import context
|
||||||
|
from cinder import db
|
||||||
|
from cinder import exception
|
||||||
from cinder import objects
|
from cinder import objects
|
||||||
from cinder.tests.unit.api import fakes
|
from cinder.tests.unit.api import fakes
|
||||||
from cinder.tests.unit import fake_constants as fake
|
from cinder.tests.unit import fake_constants as fake
|
||||||
from cinder.tests.unit import test
|
from cinder.tests.unit import test
|
||||||
|
from cinder.volume import volume_types
|
||||||
|
|
||||||
|
|
||||||
class VolumeTypesApiTest(test.TestCase):
|
class VolumeTypesApiTest(test.TestCase):
|
||||||
@ -89,3 +92,19 @@ class VolumeTypesApiTest(test.TestCase):
|
|||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
['volume_type1', 'volume_type2'],
|
['volume_type1', 'volume_type2'],
|
||||||
sorted([az['name'] for az in res_dict['volume_types']]))
|
sorted([az['name'] for az in res_dict['volume_types']]))
|
||||||
|
|
||||||
|
def test_delete_non_project_default_type(self):
|
||||||
|
type = self._create_volume_type(self.ctxt, 'type1')
|
||||||
|
db.project_default_volume_type_set(
|
||||||
|
self.ctxt, fake.VOLUME_TYPE_ID, fake.PROJECT_ID)
|
||||||
|
volume_types.destroy(self.ctxt, type.id)
|
||||||
|
self.assertRaises(exception.VolumeTypeNotFound,
|
||||||
|
volume_types.get_by_name_or_id,
|
||||||
|
self.ctxt, type.id)
|
||||||
|
|
||||||
|
def test_cannot_delete_project_default_type(self):
|
||||||
|
default_type = db.project_default_volume_type_set(
|
||||||
|
self.ctxt, fake.VOLUME_TYPE_ID, fake.PROJECT_ID)
|
||||||
|
self.assertRaises(exception.VolumeTypeDefaultDeletionError,
|
||||||
|
volume_types.destroy,
|
||||||
|
self.ctxt, default_type['volume_type_id'])
|
||||||
|
112
cinder/tests/unit/db/test_default_types.py
Normal file
112
cinder/tests/unit/db/test_default_types.py
Normal file
@ -0,0 +1,112 @@
|
|||||||
|
# Copyright 2020 Red Hat, Inc.
|
||||||
|
# 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.
|
||||||
|
|
||||||
|
"""Tests for default volume types."""
|
||||||
|
|
||||||
|
from cinder import context
|
||||||
|
from cinder import db
|
||||||
|
from cinder.tests.unit import fake_constants as fake
|
||||||
|
from cinder.tests.unit import test
|
||||||
|
|
||||||
|
|
||||||
|
class DefaultVolumeTypesTestCase(test.TestCase):
|
||||||
|
"""DB tests for default volume types."""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super(DefaultVolumeTypesTestCase, self).setUp()
|
||||||
|
self.ctxt = context.RequestContext(user_id=fake.USER_ID,
|
||||||
|
project_id=fake.PROJECT_ID,
|
||||||
|
is_admin=True)
|
||||||
|
|
||||||
|
def test_default_type_set(self):
|
||||||
|
default_type = db.project_default_volume_type_set(
|
||||||
|
self.ctxt, fake.VOLUME_TYPE_ID, fake.PROJECT_ID)
|
||||||
|
self.assertEqual(fake.PROJECT_ID, default_type.project_id)
|
||||||
|
self.assertEqual(fake.VOLUME_TYPE_ID, default_type.volume_type_id)
|
||||||
|
db.project_default_volume_type_unset(self.ctxt,
|
||||||
|
default_type.project_id)
|
||||||
|
|
||||||
|
def test_default_type_get(self):
|
||||||
|
db.project_default_volume_type_set(self.ctxt, fake.VOLUME_TYPE_ID,
|
||||||
|
fake.PROJECT_ID)
|
||||||
|
default_type = db.project_default_volume_type_get(
|
||||||
|
self.ctxt, project_id=fake.PROJECT_ID)
|
||||||
|
self.assertEqual(fake.PROJECT_ID, default_type.project_id)
|
||||||
|
self.assertEqual(fake.VOLUME_TYPE_ID, default_type.volume_type_id)
|
||||||
|
db.project_default_volume_type_unset(self.ctxt,
|
||||||
|
default_type.project_id)
|
||||||
|
|
||||||
|
def test_get_all_projects_by_default_type(self):
|
||||||
|
db.project_default_volume_type_set(self.ctxt, fake.VOLUME_TYPE_ID,
|
||||||
|
fake.PROJECT_ID)
|
||||||
|
default_type = db.get_all_projects_with_default_type(
|
||||||
|
self.ctxt, volume_type_id=fake.VOLUME_TYPE_ID)
|
||||||
|
self.assertEqual(1, len(default_type))
|
||||||
|
self.assertEqual(fake.PROJECT_ID, default_type[0].project_id)
|
||||||
|
|
||||||
|
def test_default_type_get_all(self):
|
||||||
|
db.project_default_volume_type_set(self.ctxt, fake.VOLUME_TYPE_ID,
|
||||||
|
fake.PROJECT_ID)
|
||||||
|
db.project_default_volume_type_set(self.ctxt, fake.VOLUME_TYPE2_ID,
|
||||||
|
fake.PROJECT2_ID)
|
||||||
|
default_types = db.project_default_volume_type_get(self.ctxt)
|
||||||
|
self.assertEqual(2, len(default_types))
|
||||||
|
db.project_default_volume_type_unset(self.ctxt,
|
||||||
|
default_types[0].project_id)
|
||||||
|
db.project_default_volume_type_unset(self.ctxt,
|
||||||
|
default_types[1].project_id)
|
||||||
|
|
||||||
|
def test_default_type_delete(self):
|
||||||
|
db.project_default_volume_type_set(self.ctxt, fake.VOLUME_TYPE_ID,
|
||||||
|
fake.PROJECT_ID)
|
||||||
|
default_types = db.project_default_volume_type_get(self.ctxt)
|
||||||
|
self.assertEqual(1, len(default_types))
|
||||||
|
db.project_default_volume_type_unset(self.ctxt,
|
||||||
|
default_types[0].project_id)
|
||||||
|
default_types = db.project_default_volume_type_get(self.ctxt)
|
||||||
|
self.assertEqual(0, len(default_types))
|
||||||
|
|
||||||
|
def test_default_type_update(self):
|
||||||
|
default_type = db.project_default_volume_type_set(
|
||||||
|
self.ctxt, fake.VOLUME_TYPE_ID, fake.PROJECT_ID)
|
||||||
|
self.assertEqual(fake.PROJECT_ID, default_type.project_id)
|
||||||
|
self.assertEqual(fake.VOLUME_TYPE_ID, default_type.volume_type_id)
|
||||||
|
|
||||||
|
# update to type 2
|
||||||
|
db.project_default_volume_type_set(self.ctxt, fake.VOLUME_TYPE2_ID,
|
||||||
|
fake.PROJECT_ID)
|
||||||
|
default_type = db.project_default_volume_type_get(
|
||||||
|
self.ctxt, project_id=fake.PROJECT_ID)
|
||||||
|
self.assertEqual(fake.PROJECT_ID, default_type.project_id)
|
||||||
|
self.assertEqual(fake.VOLUME_TYPE2_ID, default_type.volume_type_id)
|
||||||
|
|
||||||
|
# update to type 3
|
||||||
|
db.project_default_volume_type_set(self.ctxt, fake.VOLUME_TYPE3_ID,
|
||||||
|
fake.PROJECT_ID)
|
||||||
|
default_type = db.project_default_volume_type_get(
|
||||||
|
self.ctxt, project_id=fake.PROJECT_ID)
|
||||||
|
self.assertEqual(fake.PROJECT_ID, default_type.project_id)
|
||||||
|
self.assertEqual(fake.VOLUME_TYPE3_ID, default_type.volume_type_id)
|
||||||
|
|
||||||
|
# back to original
|
||||||
|
db.project_default_volume_type_set(self.ctxt, fake.VOLUME_TYPE_ID,
|
||||||
|
fake.PROJECT_ID)
|
||||||
|
default_type = db.project_default_volume_type_get(
|
||||||
|
self.ctxt, project_id=fake.PROJECT_ID)
|
||||||
|
self.assertEqual(fake.PROJECT_ID, default_type.project_id)
|
||||||
|
self.assertEqual(fake.VOLUME_TYPE_ID, default_type.volume_type_id)
|
||||||
|
|
||||||
|
db.project_default_volume_type_unset(self.ctxt,
|
||||||
|
default_type.project_id)
|
@ -42,6 +42,7 @@ OBJECT3_ID = '7bf5ffa9-18a2-4b64-aab4-0798b53ee4e7'
|
|||||||
PROJECT_ID = '89afd400-b646-4bbc-b12b-c0a4d63e5bd3'
|
PROJECT_ID = '89afd400-b646-4bbc-b12b-c0a4d63e5bd3'
|
||||||
PROJECT2_ID = '452ebfbc-55d9-402a-87af-65061916c24b'
|
PROJECT2_ID = '452ebfbc-55d9-402a-87af-65061916c24b'
|
||||||
PROJECT3_ID = 'f6c912d7-bf30-4b12-af81-a9e0b2f85f85'
|
PROJECT3_ID = 'f6c912d7-bf30-4b12-af81-a9e0b2f85f85'
|
||||||
|
DOMAIN_ID = 'e747b880-4565-4d18-b8e2-310bdec83759'
|
||||||
PROVIDER_ID = '60087173-e899-470a-9e3a-ba4cffa3e3e3'
|
PROVIDER_ID = '60087173-e899-470a-9e3a-ba4cffa3e3e3'
|
||||||
PROVIDER2_ID = '1060eccd-64bb-4ed2-86ce-aeaf135a97b8'
|
PROVIDER2_ID = '1060eccd-64bb-4ed2-86ce-aeaf135a97b8'
|
||||||
PROVIDER3_ID = '63736819-1c95-440e-a873-b9d685afede5'
|
PROVIDER3_ID = '63736819-1c95-440e-a873-b9d685afede5'
|
||||||
|
@ -34,6 +34,10 @@ class CinderPolicyTests(test.TestCase):
|
|||||||
user_id=fake_constants.USER_ID, project_id=self.project_id,
|
user_id=fake_constants.USER_ID, project_id=self.project_id,
|
||||||
roles=['admin']
|
roles=['admin']
|
||||||
)
|
)
|
||||||
|
self.other_admin_context = cinder_context.RequestContext(
|
||||||
|
user_id=fake_constants.USER_ID, project_id=self.other_project_id,
|
||||||
|
roles=['admin']
|
||||||
|
)
|
||||||
self.user_context = cinder_context.RequestContext(
|
self.user_context = cinder_context.RequestContext(
|
||||||
user_id=fake_constants.USER2_ID, project_id=self.project_id,
|
user_id=fake_constants.USER2_ID, project_id=self.project_id,
|
||||||
roles=['non-admin']
|
roles=['non-admin']
|
||||||
@ -42,6 +46,9 @@ class CinderPolicyTests(test.TestCase):
|
|||||||
user_id=fake_constants.USER3_ID, project_id=self.other_project_id,
|
user_id=fake_constants.USER3_ID, project_id=self.other_project_id,
|
||||||
roles=['non-admin']
|
roles=['non-admin']
|
||||||
)
|
)
|
||||||
|
self.system_admin_context = cinder_context.RequestContext(
|
||||||
|
user_id=fake_constants.USER_ID, project_id=self.project_id,
|
||||||
|
roles=['admin'], system_scope='all')
|
||||||
fake_image.mock_image_service(self)
|
fake_image.mock_image_service(self)
|
||||||
|
|
||||||
def _get_request_response(self, context, path, method, body=None,
|
def _get_request_response(self, context, path, method, body=None,
|
||||||
|
203
cinder/tests/unit/policies/test_default_volume_types.py
Normal file
203
cinder/tests/unit/policies/test_default_volume_types.py
Normal file
@ -0,0 +1,203 @@
|
|||||||
|
# Copyright 2020 Red Hat, Inc.
|
||||||
|
# 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.
|
||||||
|
|
||||||
|
from unittest import mock
|
||||||
|
|
||||||
|
from six.moves import http_client
|
||||||
|
|
||||||
|
from cinder.api import microversions as mv
|
||||||
|
from cinder import db
|
||||||
|
from cinder.tests.unit import fake_constants
|
||||||
|
from cinder.tests.unit.policies import test_base
|
||||||
|
|
||||||
|
|
||||||
|
class DefaultVolumeTypesPolicyTests(test_base.CinderPolicyTests):
|
||||||
|
|
||||||
|
class FakeDefaultType:
|
||||||
|
project_id = fake_constants.PROJECT_ID
|
||||||
|
volume_type_id = fake_constants.VOLUME_TYPE_ID
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super(DefaultVolumeTypesPolicyTests, self).setUp()
|
||||||
|
self.volume_type = self._create_fake_type(self.admin_context)
|
||||||
|
self.project = self.FakeProject()
|
||||||
|
# Need to mock out Keystone so the functional tests don't require other
|
||||||
|
# services
|
||||||
|
_keystone_client = mock.MagicMock()
|
||||||
|
_keystone_client.version = 'v3'
|
||||||
|
_keystone_client.projects.get.side_effect = self._get_project
|
||||||
|
_keystone_client_get = mock.patch(
|
||||||
|
'cinder.quota_utils._keystone_client',
|
||||||
|
lambda *args, **kwargs: _keystone_client)
|
||||||
|
_keystone_client_get.start()
|
||||||
|
self.addCleanup(_keystone_client_get.stop)
|
||||||
|
|
||||||
|
def _get_project(self, project_id, *args, **kwargs):
|
||||||
|
return self.project
|
||||||
|
|
||||||
|
class FakeProject(object):
|
||||||
|
_dom_id = fake_constants.DOMAIN_ID
|
||||||
|
|
||||||
|
def __init__(self, parent_id=None):
|
||||||
|
self.id = fake_constants.PROJECT_ID
|
||||||
|
self.parent_id = parent_id
|
||||||
|
self.domain_id = self._dom_id
|
||||||
|
self.subtree = None
|
||||||
|
self.parents = None
|
||||||
|
|
||||||
|
def test_system_admin_can_set_default(self):
|
||||||
|
system_admin_context = self.system_admin_context
|
||||||
|
|
||||||
|
path = '/v3/default-types/%s' % system_admin_context.project_id
|
||||||
|
body = {
|
||||||
|
'default_type':
|
||||||
|
{"volume_type": self.volume_type.id}
|
||||||
|
}
|
||||||
|
response = self._get_request_response(system_admin_context,
|
||||||
|
path, 'PUT', body=body,
|
||||||
|
microversion=
|
||||||
|
mv.DEFAULT_TYPE_OVERRIDES)
|
||||||
|
|
||||||
|
self.assertEqual(http_client.OK, response.status_int)
|
||||||
|
|
||||||
|
def test_project_admin_can_set_default(self):
|
||||||
|
admin_context = self.admin_context
|
||||||
|
|
||||||
|
path = '/v3/default-types/%s' % admin_context.project_id
|
||||||
|
body = {
|
||||||
|
'default_type':
|
||||||
|
{"volume_type": self.volume_type.id}
|
||||||
|
}
|
||||||
|
response = self._get_request_response(admin_context,
|
||||||
|
path, 'PUT', body=body,
|
||||||
|
microversion=
|
||||||
|
mv.DEFAULT_TYPE_OVERRIDES)
|
||||||
|
|
||||||
|
self.assertEqual(http_client.OK, response.status_int)
|
||||||
|
|
||||||
|
def test_project_admin_cannot_set_default_for_other_project(self):
|
||||||
|
admin_context = self.admin_context
|
||||||
|
|
||||||
|
path = '/v3/default-types/%s' % admin_context.project_id
|
||||||
|
body = {
|
||||||
|
'default_type':
|
||||||
|
{"volume_type": self.volume_type.id}
|
||||||
|
}
|
||||||
|
response = self._get_request_response(self.other_admin_context,
|
||||||
|
path, 'PUT', body=body,
|
||||||
|
microversion=
|
||||||
|
mv.DEFAULT_TYPE_OVERRIDES)
|
||||||
|
|
||||||
|
self.assertEqual(http_client.FORBIDDEN, response.status_int)
|
||||||
|
|
||||||
|
@mock.patch.object(db, 'project_default_volume_type_get',
|
||||||
|
return_value=FakeDefaultType())
|
||||||
|
def test_system_admin_can_get_default(self, mock_default_get):
|
||||||
|
system_admin_context = self.system_admin_context
|
||||||
|
|
||||||
|
path = '/v3/default-types/%s' % system_admin_context.project_id
|
||||||
|
response = self._get_request_response(system_admin_context,
|
||||||
|
path, 'GET',
|
||||||
|
microversion=
|
||||||
|
mv.DEFAULT_TYPE_OVERRIDES)
|
||||||
|
|
||||||
|
self.assertEqual(http_client.OK, response.status_int)
|
||||||
|
|
||||||
|
def test_project_admin_can_get_default(self):
|
||||||
|
admin_context = self.admin_context
|
||||||
|
|
||||||
|
path = '/v3/default-types/%s' % admin_context.project_id
|
||||||
|
body = {
|
||||||
|
'default_type':
|
||||||
|
{"volume_type": self.volume_type.id}
|
||||||
|
}
|
||||||
|
self._get_request_response(admin_context,
|
||||||
|
path, 'PUT', body=body,
|
||||||
|
microversion=
|
||||||
|
mv.DEFAULT_TYPE_OVERRIDES)
|
||||||
|
|
||||||
|
path = '/v3/default-types/%s' % admin_context.project_id
|
||||||
|
response = self._get_request_response(admin_context,
|
||||||
|
path, 'GET',
|
||||||
|
microversion=
|
||||||
|
mv.DEFAULT_TYPE_OVERRIDES)
|
||||||
|
|
||||||
|
self.assertEqual(http_client.OK, response.status_int)
|
||||||
|
|
||||||
|
def test_project_admin_cannot_get_default_for_other_project(self):
|
||||||
|
admin_context = self.admin_context
|
||||||
|
|
||||||
|
path = '/v3/default-types/%s' % admin_context.project_id
|
||||||
|
response = self._get_request_response(self.other_admin_context,
|
||||||
|
path, 'GET',
|
||||||
|
microversion=
|
||||||
|
mv.DEFAULT_TYPE_OVERRIDES)
|
||||||
|
|
||||||
|
self.assertEqual(http_client.FORBIDDEN, response.status_int)
|
||||||
|
|
||||||
|
def test_system_admin_can_get_all_default(self):
|
||||||
|
system_admin_context = self.system_admin_context
|
||||||
|
|
||||||
|
path = '/v3/default-types'
|
||||||
|
response = self._get_request_response(system_admin_context,
|
||||||
|
path, 'GET',
|
||||||
|
microversion=
|
||||||
|
mv.DEFAULT_TYPE_OVERRIDES)
|
||||||
|
|
||||||
|
self.assertEqual(http_client.OK, response.status_int)
|
||||||
|
|
||||||
|
def test_project_admin_cannot_get_all_default(self):
|
||||||
|
admin_context = self.admin_context
|
||||||
|
|
||||||
|
path = '/v3/default-types'
|
||||||
|
response = self._get_request_response(admin_context,
|
||||||
|
path, 'GET',
|
||||||
|
microversion=
|
||||||
|
mv.DEFAULT_TYPE_OVERRIDES)
|
||||||
|
|
||||||
|
self.assertEqual(http_client.FORBIDDEN, response.status_int)
|
||||||
|
|
||||||
|
def test_system_admin_can_unset_default(self):
|
||||||
|
system_admin_context = self.system_admin_context
|
||||||
|
|
||||||
|
path = '/v3/default-types/%s' % system_admin_context.project_id
|
||||||
|
response = self._get_request_response(system_admin_context,
|
||||||
|
path, 'DELETE',
|
||||||
|
microversion=
|
||||||
|
mv.DEFAULT_TYPE_OVERRIDES)
|
||||||
|
|
||||||
|
self.assertEqual(http_client.NO_CONTENT, response.status_int)
|
||||||
|
|
||||||
|
def test_project_admin_can_unset_default(self):
|
||||||
|
admin_context = self.admin_context
|
||||||
|
|
||||||
|
path = '/v3/default-types/%s' % admin_context.project_id
|
||||||
|
response = self._get_request_response(admin_context,
|
||||||
|
path, 'DELETE',
|
||||||
|
microversion=
|
||||||
|
mv.DEFAULT_TYPE_OVERRIDES)
|
||||||
|
|
||||||
|
self.assertEqual(http_client.NO_CONTENT, response.status_int)
|
||||||
|
|
||||||
|
def test_project_admin_cannot_unset_default_for_other_project(self):
|
||||||
|
admin_context = self.admin_context
|
||||||
|
|
||||||
|
path = '/v3/default-types/%s' % admin_context.project_id
|
||||||
|
response = self._get_request_response(self.other_admin_context,
|
||||||
|
path, 'DELETE',
|
||||||
|
microversion=
|
||||||
|
mv.DEFAULT_TYPE_OVERRIDES)
|
||||||
|
|
||||||
|
self.assertEqual(http_client.FORBIDDEN, response.status_int)
|
@ -72,7 +72,7 @@ class QuotaUtilsTest(test.TestCase):
|
|||||||
del returned_project.subtree
|
del returned_project.subtree
|
||||||
keystoneclient.projects.get.return_value = returned_project
|
keystoneclient.projects.get.return_value = returned_project
|
||||||
expected_project = quota_utils.GenericProjectInfo(
|
expected_project = quota_utils.GenericProjectInfo(
|
||||||
self.context.project_id, 'v3', 'bar')
|
self.context.project_id, 'v3', 'bar', domain_id='default')
|
||||||
project = quota_utils.get_project_hierarchy(
|
project = quota_utils.get_project_hierarchy(
|
||||||
self.context, self.context.project_id)
|
self.context, self.context.project_id)
|
||||||
self.assertEqual(expected_project.__dict__, project.__dict__)
|
self.assertEqual(expected_project.__dict__, project.__dict__)
|
||||||
@ -86,7 +86,8 @@ class QuotaUtilsTest(test.TestCase):
|
|||||||
returned_project.subtree = subtree_dict
|
returned_project.subtree = subtree_dict
|
||||||
keystoneclient.projects.get.return_value = returned_project
|
keystoneclient.projects.get.return_value = returned_project
|
||||||
expected_project = quota_utils.GenericProjectInfo(
|
expected_project = quota_utils.GenericProjectInfo(
|
||||||
self.context.project_id, 'v3', 'bar', subtree_dict)
|
self.context.project_id, 'v3', 'bar', subtree_dict,
|
||||||
|
domain_id='default')
|
||||||
project = quota_utils.get_project_hierarchy(
|
project = quota_utils.get_project_hierarchy(
|
||||||
self.context, self.context.project_id, subtree_as_ids=True)
|
self.context, self.context.project_id, subtree_as_ids=True)
|
||||||
keystoneclient.projects.get.assert_called_once_with(
|
keystoneclient.projects.get.assert_called_once_with(
|
||||||
|
@ -379,7 +379,7 @@ class ExtractVolumeRequestTask(flow_utils.CinderTask):
|
|||||||
raise
|
raise
|
||||||
|
|
||||||
# otherwise, use the default volume type
|
# otherwise, use the default volume type
|
||||||
return volume_types.get_default_volume_type()
|
return volume_types.get_default_volume_type(context)
|
||||||
|
|
||||||
def execute(self, context, size, snapshot, image_id, source_volume,
|
def execute(self, context, size, snapshot, image_id, source_volume,
|
||||||
availability_zone, volume_type, metadata, key_manager,
|
availability_zone, volume_type, metadata, key_manager,
|
||||||
|
@ -116,17 +116,28 @@ def destroy(context, id):
|
|||||||
if id is None:
|
if id is None:
|
||||||
msg = _("id cannot be None")
|
msg = _("id cannot be None")
|
||||||
raise exception.InvalidVolumeType(reason=msg)
|
raise exception.InvalidVolumeType(reason=msg)
|
||||||
elevated = context if context.is_admin else context.elevated()
|
|
||||||
|
projects_with_default_type = db.get_all_projects_with_default_type(
|
||||||
|
context.elevated(), id)
|
||||||
|
if len(projects_with_default_type) > 0:
|
||||||
|
# don't allow delete if the type requested is a project default
|
||||||
|
project_list = [p.project_id for p in projects_with_default_type]
|
||||||
|
LOG.exception('Default type with %(volume_type_id)s is associated '
|
||||||
|
'with projects %(projects)s',
|
||||||
|
{'volume_type_id': id,
|
||||||
|
'projects': project_list})
|
||||||
|
raise exception.VolumeTypeDefaultDeletionError(volume_type_id=id)
|
||||||
|
|
||||||
# Default type *must* be set in order to delete any volume type.
|
# Default type *must* be set in order to delete any volume type.
|
||||||
# If the default isn't set, the following call will raise
|
# If the default isn't set, the following call will raise
|
||||||
# VolumeTypeDefaultMisconfiguredError exception which will error out the
|
# VolumeTypeDefaultMisconfiguredError exception which will error out the
|
||||||
# delete operation.
|
# delete operation.
|
||||||
default_type = get_default_volume_type()
|
default_type = get_default_volume_type()
|
||||||
# don't allow delete if the type requested is the default type
|
# don't allow delete if the type requested is the conf default type
|
||||||
if id == default_type.get('id'):
|
if id == default_type.get('id'):
|
||||||
raise exception.VolumeTypeDefaultDeletionError(volume_type_id=id)
|
raise exception.VolumeTypeDefaultDeletionError(volume_type_id=id)
|
||||||
|
|
||||||
|
elevated = context if context.is_admin else context.elevated()
|
||||||
return db.volume_type_destroy(elevated, id)
|
return db.volume_type_destroy(elevated, id)
|
||||||
|
|
||||||
|
|
||||||
@ -189,12 +200,18 @@ def get_volume_type_by_name(context, name):
|
|||||||
return db.volume_type_get_by_name(context, name)
|
return db.volume_type_get_by_name(context, name)
|
||||||
|
|
||||||
|
|
||||||
def get_default_volume_type():
|
def get_default_volume_type(contxt=None):
|
||||||
"""Get the default volume type.
|
"""Get the default volume type.
|
||||||
|
|
||||||
:raises VolumeTypeDefaultMisconfiguredError: when the configured default
|
:raises VolumeTypeDefaultMisconfiguredError: when the configured default
|
||||||
is not found
|
is not found
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
if contxt:
|
||||||
|
project_default = db.project_default_volume_type_get(
|
||||||
|
contxt, contxt.project_id)
|
||||||
|
if project_default:
|
||||||
|
return get_volume_type(contxt, project_default.volume_type_id)
|
||||||
name = CONF.default_volume_type
|
name = CONF.default_volume_type
|
||||||
ctxt = context.get_admin_context()
|
ctxt = context.get_admin_context()
|
||||||
vol_type = {}
|
vol_type = {}
|
||||||
|
@ -99,11 +99,11 @@ volume creation.
|
|||||||
|
|
||||||
#. volume_type
|
#. volume_type
|
||||||
#. cinder_img_volume_type (via glance image metadata)
|
#. cinder_img_volume_type (via glance image metadata)
|
||||||
#. default_volume_type (via cinder.conf)
|
#. default volume type (via project defaults or cinder.conf)
|
||||||
|
|
||||||
|
|
||||||
volume-type
|
volume-type
|
||||||
+++++++++++
|
^^^^^^^^^^^
|
||||||
|
|
||||||
User can specify `volume type` when creating a volume.
|
User can specify `volume type` when creating a volume.
|
||||||
|
|
||||||
@ -122,7 +122,7 @@ User can specify `volume type` when creating a volume.
|
|||||||
|
|
||||||
|
|
||||||
cinder_img_volume_type
|
cinder_img_volume_type
|
||||||
++++++++++++++++++++++
|
^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
If glance image has ``cinder_img_volume_type`` property, Cinder uses this
|
If glance image has ``cinder_img_volume_type`` property, Cinder uses this
|
||||||
parameter to specify ``volume type`` when creating a volume.
|
parameter to specify ``volume type`` when creating a volume.
|
||||||
@ -198,11 +198,37 @@ a volume from the image.
|
|||||||
| user_id | 33fdc37314914796883706b33e587d51 |
|
| user_id | 33fdc37314914796883706b33e587d51 |
|
||||||
+---------------------+--------------------------------------+
|
+---------------------+--------------------------------------+
|
||||||
|
|
||||||
default_volume_type
|
default volume type
|
||||||
+++++++++++++++++++
|
^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
If above parameters are not set, Cinder uses default_volume_type which is
|
If above parameters are not set, cinder uses default volume type during
|
||||||
defined in cinder.conf during volume creation.
|
volume creation.
|
||||||
|
|
||||||
|
The effective default volume type (whether it be project default or
|
||||||
|
default_volume_type) can be checked with the following command:
|
||||||
|
|
||||||
|
.. code-block:: console
|
||||||
|
|
||||||
|
$ cinder type-default
|
||||||
|
|
||||||
|
There are 2 ways to set the default volume type:
|
||||||
|
|
||||||
|
1) Project specific defaults
|
||||||
|
2) default_volume_type defined in cinder.conf
|
||||||
|
|
||||||
|
Project specific defaults (available since mv 3.62 or higher)
|
||||||
|
"""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""
|
||||||
|
|
||||||
|
Project specific defaults can be managed using the `Default Volume Types API
|
||||||
|
<https://docs.openstack.org/api-ref/block-storage/v3/#default-volume-types-default-types>`_
|
||||||
|
It is set on a per project basis and has a higher priority over
|
||||||
|
default_volume_type defined in cinder.conf
|
||||||
|
|
||||||
|
default_volume_type
|
||||||
|
"""""""""""""""""""
|
||||||
|
|
||||||
|
If the project specific default is not set then default_volume_type
|
||||||
|
configured in cinder.conf is used to create volumes.
|
||||||
|
|
||||||
Example cinder.conf file configuration.
|
Example cinder.conf file configuration.
|
||||||
|
|
||||||
|
@ -0,0 +1,9 @@
|
|||||||
|
---
|
||||||
|
features:
|
||||||
|
- |
|
||||||
|
Added support for project specific default volume types.
|
||||||
|
Microversion 3.62 of the Block Storage API introduces new
|
||||||
|
calls to set, get, and unset a default volume type for a
|
||||||
|
specific project.
|
||||||
|
Project specific defaults have higher priority than
|
||||||
|
the default_volume_type option in cinder.conf
|
Loading…
x
Reference in New Issue
Block a user