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.
|
||||
.. include:: volumes-v3-types.inc
|
||||
.. include:: volume-type-access.inc
|
||||
.. include:: default-types.inc
|
||||
|
||||
.. Now my primary focus is on volumes and what I can do with them.
|
||||
.. include:: volumes-v3-volumes.inc
|
||||
|
@ -168,6 +168,12 @@ volume_type_id:
|
||||
in: path
|
||||
required: true
|
||||
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
|
||||
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",
|
||||
"status": "CURRENT",
|
||||
"updated": "2018-07-17T00:00:00Z",
|
||||
"version": "3.61"
|
||||
"version": "3.62"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -46,7 +46,7 @@
|
||||
"min_version": "3.0",
|
||||
"status": "CURRENT",
|
||||
"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'
|
||||
|
||||
DEFAULT_TYPE_OVERRIDES = '3.62'
|
||||
|
||||
|
||||
def get_mv_header(version):
|
||||
"""Gets a formatted HTTP microversion header.
|
||||
|
@ -141,6 +141,7 @@ REST_API_VERSION_HISTORY = """
|
||||
("GET /v3/{project_id}/volumes/detail") requests.
|
||||
* 3.61 - Add ``cluster_name`` attribute to response body of volume details
|
||||
for admin.
|
||||
* 3.62 - Default volume type overrides
|
||||
"""
|
||||
|
||||
# The minimum and maximum versions of the API supported
|
||||
@ -148,9 +149,9 @@ REST_API_VERSION_HISTORY = """
|
||||
# minimum version of the API supported.
|
||||
# Explicitly using /v2 endpoints will still work
|
||||
_MIN_API_VERSION = "3.0"
|
||||
_MAX_API_VERSION = "3.61"
|
||||
_MAX_API_VERSION = "3.62"
|
||||
_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
|
||||
|
@ -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
|
||||
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
|
||||
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:
|
||||
msg = _("Default volume type can not be found.")
|
||||
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 clusters
|
||||
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_specs
|
||||
from cinder.api.v3 import group_types
|
||||
@ -199,3 +200,24 @@ class APIRouter(cinder.api.openstack.APIRouter):
|
||||
controller=self.resources['volume_transfers'],
|
||||
collection={'detail': 'GET'},
|
||||
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)
|
||||
|
||||
|
||||
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
|
||||
from collections import abc
|
||||
import contextlib
|
||||
import datetime as dt
|
||||
import functools
|
||||
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)
|
||||
|
||||
|
||||
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
|
||||
def group_type_access_remove(context, type_id, project_id):
|
||||
"""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):
|
||||
"""Represents QoS specs as key/value pairs.
|
||||
|
||||
|
@ -392,7 +392,7 @@ class VolumeTypeDeletionError(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.")
|
||||
|
||||
|
||||
@ -401,6 +401,10 @@ class VolumeTypeDefaultMisconfiguredError(CinderException):
|
||||
"%(volume_type_name)s cannot be found.")
|
||||
|
||||
|
||||
class VolumeTypeProjectDefaultNotFound(NotFound):
|
||||
message = _("Default type for project %(project_id)s not found.")
|
||||
|
||||
|
||||
class GroupTypeNotFound(NotFound):
|
||||
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 capabilities
|
||||
from cinder.policies import clusters
|
||||
from cinder.policies import default_types
|
||||
from cinder.policies import group_actions
|
||||
from cinder.policies import group_snapshot_actions
|
||||
from cinder.policies import group_snapshots
|
||||
@ -83,4 +84,5 @@ def list_rules():
|
||||
volume_metadata.list_rules(),
|
||||
type_extra_specs.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_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 = [
|
||||
policy.RuleDefault('context_is_admin', 'role:admin',
|
||||
description="Decides what is required for the "
|
||||
@ -30,6 +34,12 @@ rules = [
|
||||
'is_admin:True or (role:admin and '
|
||||
'is_admin_project:True)',
|
||||
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.
|
||||
"""
|
||||
init()
|
||||
credentials = context.to_policy_values()
|
||||
if not exc:
|
||||
exc = exception.PolicyNotAuthorized
|
||||
try:
|
||||
result = _ENFORCER.authorize(action, target, credentials,
|
||||
result = _ENFORCER.authorize(action, target, context,
|
||||
do_raise=do_raise, exc=exc, action=action)
|
||||
except policy.PolicyNotRegistered:
|
||||
with excutils.save_and_reraise_exception():
|
||||
LOG.exception('Policy not registered')
|
||||
except 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',
|
||||
{'action': action, 'credentials': credentials})
|
||||
{'action': action,
|
||||
'credentials': context.to_policy_values()})
|
||||
return result
|
||||
|
||||
|
||||
|
@ -36,8 +36,10 @@ class GenericProjectInfo(object):
|
||||
project_parent_id=None,
|
||||
project_subtree=None,
|
||||
project_parent_tree=None,
|
||||
is_admin_project=False):
|
||||
is_admin_project=False,
|
||||
domain_id=None):
|
||||
self.id = project_id
|
||||
self.domain_id = domain_id
|
||||
self.keystone_api_version = project_keystone_api_version
|
||||
self.parent_id = project_parent_id
|
||||
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)
|
||||
|
||||
generic_project.parent_id = None
|
||||
generic_project.domain_id = project.domain_id
|
||||
if project.parent_id != project.domain_id:
|
||||
generic_project.parent_id = project.parent_id
|
||||
|
||||
|
@ -56,6 +56,10 @@ class OpenStackApiException400(OpenStackApiException):
|
||||
message = _("400 Bad Request")
|
||||
|
||||
|
||||
class OpenStackApiException403(OpenStackApiException):
|
||||
message = _("403 Forbidden")
|
||||
|
||||
|
||||
class OpenStackApiException500(OpenStackApiException):
|
||||
message = _("500 Internal Server Error")
|
||||
|
||||
@ -129,11 +133,15 @@ class TestOpenStackClient(object):
|
||||
self._authenticate(True)
|
||||
|
||||
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()
|
||||
|
||||
# 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']
|
||||
else:
|
||||
base_uri = self.auth_uri
|
||||
|
||||
if strip_version:
|
||||
# cut out version number and tenant_id
|
||||
@ -169,12 +177,12 @@ class TestOpenStackClient(object):
|
||||
else:
|
||||
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])
|
||||
response = self.api_request(relative_uri, **kwargs)
|
||||
response = self.api_request(relative_uri, base_url=base_url, **kwargs)
|
||||
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'
|
||||
if body:
|
||||
headers = kwargs.setdefault('headers', {})
|
||||
@ -183,10 +191,10 @@ class TestOpenStackClient(object):
|
||||
|
||||
kwargs.setdefault('check_response_status', [http_client.OK,
|
||||
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)
|
||||
|
||||
def api_put(self, relative_uri, body, **kwargs):
|
||||
def api_put(self, relative_uri, body, base_url=True, **kwargs):
|
||||
kwargs['method'] = 'PUT'
|
||||
if body:
|
||||
headers = kwargs.setdefault('headers', {})
|
||||
@ -196,15 +204,15 @@ class TestOpenStackClient(object):
|
||||
kwargs.setdefault('check_response_status', [http_client.OK,
|
||||
http_client.ACCEPTED,
|
||||
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)
|
||||
|
||||
def api_delete(self, relative_uri, **kwargs):
|
||||
def api_delete(self, relative_uri, base_url=True, **kwargs):
|
||||
kwargs['method'] = 'DELETE'
|
||||
kwargs.setdefault('check_response_status', [http_client.OK,
|
||||
http_client.ACCEPTED,
|
||||
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):
|
||||
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):
|
||||
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'
|
||||
use_admin_context = kwargs.pop('use_admin_context', False)
|
||||
version = kwargs.pop('version', api_version._MIN_API_VERSION)
|
||||
system_scope = kwargs.pop('system_scope', None)
|
||||
out = os_wsgi.Request.blank(*args, **kwargs)
|
||||
out.environ['cinder.context'] = FakeRequestContext(
|
||||
fake.USER_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)
|
||||
return out
|
||||
|
||||
|
@ -74,7 +74,7 @@ def return_volume_types_get_volume_type(context, id):
|
||||
return fake_volume_type(id)
|
||||
|
||||
|
||||
def return_volume_types_get_default():
|
||||
def return_volume_types_get_default(context):
|
||||
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.v2 import types
|
||||
from cinder import context
|
||||
from cinder import db
|
||||
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
|
||||
from cinder.volume import volume_types
|
||||
|
||||
|
||||
class VolumeTypesApiTest(test.TestCase):
|
||||
@ -89,3 +92,19 @@ class VolumeTypesApiTest(test.TestCase):
|
||||
self.assertEqual(
|
||||
['volume_type1', 'volume_type2'],
|
||||
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'
|
||||
PROJECT2_ID = '452ebfbc-55d9-402a-87af-65061916c24b'
|
||||
PROJECT3_ID = 'f6c912d7-bf30-4b12-af81-a9e0b2f85f85'
|
||||
DOMAIN_ID = 'e747b880-4565-4d18-b8e2-310bdec83759'
|
||||
PROVIDER_ID = '60087173-e899-470a-9e3a-ba4cffa3e3e3'
|
||||
PROVIDER2_ID = '1060eccd-64bb-4ed2-86ce-aeaf135a97b8'
|
||||
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,
|
||||
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(
|
||||
user_id=fake_constants.USER2_ID, project_id=self.project_id,
|
||||
roles=['non-admin']
|
||||
@ -42,6 +46,9 @@ class CinderPolicyTests(test.TestCase):
|
||||
user_id=fake_constants.USER3_ID, project_id=self.other_project_id,
|
||||
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)
|
||||
|
||||
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
|
||||
keystoneclient.projects.get.return_value = returned_project
|
||||
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(
|
||||
self.context, self.context.project_id)
|
||||
self.assertEqual(expected_project.__dict__, project.__dict__)
|
||||
@ -86,7 +86,8 @@ class QuotaUtilsTest(test.TestCase):
|
||||
returned_project.subtree = subtree_dict
|
||||
keystoneclient.projects.get.return_value = returned_project
|
||||
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(
|
||||
self.context, self.context.project_id, subtree_as_ids=True)
|
||||
keystoneclient.projects.get.assert_called_once_with(
|
||||
|
@ -379,7 +379,7 @@ class ExtractVolumeRequestTask(flow_utils.CinderTask):
|
||||
raise
|
||||
|
||||
# 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,
|
||||
availability_zone, volume_type, metadata, key_manager,
|
||||
|
@ -116,17 +116,28 @@ def destroy(context, id):
|
||||
if id is None:
|
||||
msg = _("id cannot be None")
|
||||
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.
|
||||
# If the default isn't set, the following call will raise
|
||||
# VolumeTypeDefaultMisconfiguredError exception which will error out the
|
||||
# delete operation.
|
||||
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'):
|
||||
raise exception.VolumeTypeDefaultDeletionError(volume_type_id=id)
|
||||
|
||||
elevated = context if context.is_admin else context.elevated()
|
||||
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)
|
||||
|
||||
|
||||
def get_default_volume_type():
|
||||
def get_default_volume_type(contxt=None):
|
||||
"""Get the default volume type.
|
||||
|
||||
:raises VolumeTypeDefaultMisconfiguredError: when the configured default
|
||||
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
|
||||
ctxt = context.get_admin_context()
|
||||
vol_type = {}
|
||||
|
@ -99,11 +99,11 @@ volume creation.
|
||||
|
||||
#. volume_type
|
||||
#. 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
|
||||
+++++++++++
|
||||
^^^^^^^^^^^
|
||||
|
||||
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
|
||||
++++++++++++++++++++++
|
||||
^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
If glance image has ``cinder_img_volume_type`` property, Cinder uses this
|
||||
parameter to specify ``volume type`` when creating a volume.
|
||||
@ -198,11 +198,37 @@ a volume from the image.
|
||||
| user_id | 33fdc37314914796883706b33e587d51 |
|
||||
+---------------------+--------------------------------------+
|
||||
|
||||
default_volume_type
|
||||
+++++++++++++++++++
|
||||
default volume type
|
||||
^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
If above parameters are not set, Cinder uses default_volume_type which is
|
||||
defined in cinder.conf during volume creation.
|
||||
If above parameters are not set, cinder uses default volume type during
|
||||
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.
|
||||
|
||||
|
@ -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…
Reference in New Issue
Block a user