Merge "List manageable volumes and snapshots"
This commit is contained in:
commit
b4b8222445
56
cinder/api/contrib/resource_common_manage.py
Normal file
56
cinder/api/contrib/resource_common_manage.py
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
# Copyright (c) 2016 Stratoscale, Ltd.
|
||||||
|
#
|
||||||
|
# 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 cinder.api import common
|
||||||
|
from cinder import exception
|
||||||
|
from cinder.i18n import _
|
||||||
|
|
||||||
|
|
||||||
|
def get_manageable_resources(req, is_detail, function_get_manageable,
|
||||||
|
view_builder):
|
||||||
|
context = req.environ['cinder.context']
|
||||||
|
params = req.params.copy()
|
||||||
|
host = params.get('host')
|
||||||
|
if host is None:
|
||||||
|
raise exception.InvalidHost(
|
||||||
|
reason=_("Host must be specified in query parameters"))
|
||||||
|
|
||||||
|
marker, limit, offset = common.get_pagination_params(params)
|
||||||
|
sort_keys, sort_dirs = common.get_sort_params(params,
|
||||||
|
default_key='reference')
|
||||||
|
|
||||||
|
# These parameters are generally validated at the DB layer, but in this
|
||||||
|
# case sorting is not done by the DB
|
||||||
|
valid_sort_keys = ('reference', 'size')
|
||||||
|
invalid_keys = [key for key in sort_keys if key not in valid_sort_keys]
|
||||||
|
if invalid_keys:
|
||||||
|
msg = _("Invalid sort keys passed: %s") % ', '.join(invalid_keys)
|
||||||
|
raise exception.InvalidParameterValue(err=msg)
|
||||||
|
valid_sort_dirs = ('asc', 'desc')
|
||||||
|
invalid_dirs = [d for d in sort_dirs if d not in valid_sort_dirs]
|
||||||
|
if invalid_dirs:
|
||||||
|
msg = _("Invalid sort dirs passed: %s") % ', '.join(invalid_dirs)
|
||||||
|
raise exception.InvalidParameterValue(err=msg)
|
||||||
|
|
||||||
|
resources = function_get_manageable(context, host, marker=marker,
|
||||||
|
limit=limit, offset=offset,
|
||||||
|
sort_keys=sort_keys,
|
||||||
|
sort_dirs=sort_dirs)
|
||||||
|
resource_count = len(resources)
|
||||||
|
|
||||||
|
if is_detail:
|
||||||
|
resources = view_builder.detail_list(req, resources, resource_count)
|
||||||
|
else:
|
||||||
|
resources = view_builder.summary_list(req, resources, resource_count)
|
||||||
|
return resources
|
@ -16,8 +16,10 @@ from oslo_config import cfg
|
|||||||
from oslo_log import log as logging
|
from oslo_log import log as logging
|
||||||
from webob import exc
|
from webob import exc
|
||||||
|
|
||||||
|
from cinder.api.contrib import resource_common_manage
|
||||||
from cinder.api import extensions
|
from cinder.api import extensions
|
||||||
from cinder.api.openstack import wsgi
|
from cinder.api.openstack import wsgi
|
||||||
|
from cinder.api.views import manageable_snapshots as list_manageable_view
|
||||||
from cinder.api.views import snapshots as snapshot_views
|
from cinder.api.views import snapshots as snapshot_views
|
||||||
from cinder import exception
|
from cinder import exception
|
||||||
from cinder.i18n import _
|
from cinder.i18n import _
|
||||||
@ -25,7 +27,10 @@ from cinder import volume as cinder_volume
|
|||||||
|
|
||||||
LOG = logging.getLogger(__name__)
|
LOG = logging.getLogger(__name__)
|
||||||
CONF = cfg.CONF
|
CONF = cfg.CONF
|
||||||
authorize = extensions.extension_authorizer('snapshot', 'snapshot_manage')
|
authorize_manage = extensions.extension_authorizer('snapshot',
|
||||||
|
'snapshot_manage')
|
||||||
|
authorize_list_manageable = extensions.extension_authorizer('snapshot',
|
||||||
|
'list_manageable')
|
||||||
|
|
||||||
|
|
||||||
class SnapshotManageController(wsgi.Controller):
|
class SnapshotManageController(wsgi.Controller):
|
||||||
@ -36,6 +41,7 @@ class SnapshotManageController(wsgi.Controller):
|
|||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super(SnapshotManageController, self).__init__(*args, **kwargs)
|
super(SnapshotManageController, self).__init__(*args, **kwargs)
|
||||||
self.volume_api = cinder_volume.API()
|
self.volume_api = cinder_volume.API()
|
||||||
|
self._list_manageable_view = list_manageable_view.ViewBuilder()
|
||||||
|
|
||||||
@wsgi.response(202)
|
@wsgi.response(202)
|
||||||
def create(self, req, body):
|
def create(self, req, body):
|
||||||
@ -81,7 +87,7 @@ class SnapshotManageController(wsgi.Controller):
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
context = req.environ['cinder.context']
|
context = req.environ['cinder.context']
|
||||||
authorize(context)
|
authorize_manage(context)
|
||||||
|
|
||||||
if not self.is_valid_body(body, 'snapshot'):
|
if not self.is_valid_body(body, 'snapshot'):
|
||||||
msg = _("Missing required element snapshot in request body.")
|
msg = _("Missing required element snapshot in request body.")
|
||||||
@ -130,6 +136,24 @@ class SnapshotManageController(wsgi.Controller):
|
|||||||
|
|
||||||
return self._view_builder.detail(req, new_snapshot)
|
return self._view_builder.detail(req, new_snapshot)
|
||||||
|
|
||||||
|
@wsgi.extends
|
||||||
|
def index(self, req):
|
||||||
|
"""Returns a summary list of snapshots available to manage."""
|
||||||
|
context = req.environ['cinder.context']
|
||||||
|
authorize_list_manageable(context)
|
||||||
|
return resource_common_manage.get_manageable_resources(
|
||||||
|
req, False, self.volume_api.get_manageable_snapshots,
|
||||||
|
self._list_manageable_view)
|
||||||
|
|
||||||
|
@wsgi.extends
|
||||||
|
def detail(self, req):
|
||||||
|
"""Returns a detailed list of snapshots available to manage."""
|
||||||
|
context = req.environ['cinder.context']
|
||||||
|
authorize_list_manageable(context)
|
||||||
|
return resource_common_manage.get_manageable_resources(
|
||||||
|
req, True, self.volume_api.get_manageable_snapshots,
|
||||||
|
self._list_manageable_view)
|
||||||
|
|
||||||
|
|
||||||
class Snapshot_manage(extensions.ExtensionDescriptor):
|
class Snapshot_manage(extensions.ExtensionDescriptor):
|
||||||
"""Allows existing backend storage to be 'managed' by Cinder."""
|
"""Allows existing backend storage to be 'managed' by Cinder."""
|
||||||
@ -141,4 +165,6 @@ class Snapshot_manage(extensions.ExtensionDescriptor):
|
|||||||
def get_resources(self):
|
def get_resources(self):
|
||||||
controller = SnapshotManageController()
|
controller = SnapshotManageController()
|
||||||
return [extensions.ResourceExtension(Snapshot_manage.alias,
|
return [extensions.ResourceExtension(Snapshot_manage.alias,
|
||||||
controller)]
|
controller,
|
||||||
|
collection_actions=
|
||||||
|
{'detail': 'GET'})]
|
||||||
|
@ -16,9 +16,11 @@ from oslo_log import log as logging
|
|||||||
from oslo_utils import uuidutils
|
from oslo_utils import uuidutils
|
||||||
from webob import exc
|
from webob import exc
|
||||||
|
|
||||||
|
from cinder.api.contrib import resource_common_manage
|
||||||
from cinder.api import extensions
|
from cinder.api import extensions
|
||||||
from cinder.api.openstack import wsgi
|
from cinder.api.openstack import wsgi
|
||||||
from cinder.api.v2.views import volumes as volume_views
|
from cinder.api.v2.views import volumes as volume_views
|
||||||
|
from cinder.api.views import manageable_volumes as list_manageable_view
|
||||||
from cinder import exception
|
from cinder import exception
|
||||||
from cinder.i18n import _
|
from cinder.i18n import _
|
||||||
from cinder import utils
|
from cinder import utils
|
||||||
@ -26,7 +28,9 @@ from cinder import volume as cinder_volume
|
|||||||
from cinder.volume import volume_types
|
from cinder.volume import volume_types
|
||||||
|
|
||||||
LOG = logging.getLogger(__name__)
|
LOG = logging.getLogger(__name__)
|
||||||
authorize = extensions.extension_authorizer('volume', 'volume_manage')
|
authorize_manage = extensions.extension_authorizer('volume', 'volume_manage')
|
||||||
|
authorize_list_manageable = extensions.extension_authorizer('volume',
|
||||||
|
'list_manageable')
|
||||||
|
|
||||||
|
|
||||||
class VolumeManageController(wsgi.Controller):
|
class VolumeManageController(wsgi.Controller):
|
||||||
@ -37,6 +41,7 @@ class VolumeManageController(wsgi.Controller):
|
|||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super(VolumeManageController, self).__init__(*args, **kwargs)
|
super(VolumeManageController, self).__init__(*args, **kwargs)
|
||||||
self.volume_api = cinder_volume.API()
|
self.volume_api = cinder_volume.API()
|
||||||
|
self._list_manageable_view = list_manageable_view.ViewBuilder()
|
||||||
|
|
||||||
@wsgi.response(202)
|
@wsgi.response(202)
|
||||||
def create(self, req, body):
|
def create(self, req, body):
|
||||||
@ -93,7 +98,7 @@ class VolumeManageController(wsgi.Controller):
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
context = req.environ['cinder.context']
|
context = req.environ['cinder.context']
|
||||||
authorize(context)
|
authorize_manage(context)
|
||||||
|
|
||||||
self.assert_valid_body(body, 'volume')
|
self.assert_valid_body(body, 'volume')
|
||||||
|
|
||||||
@ -145,6 +150,24 @@ class VolumeManageController(wsgi.Controller):
|
|||||||
|
|
||||||
return self._view_builder.detail(req, new_volume)
|
return self._view_builder.detail(req, new_volume)
|
||||||
|
|
||||||
|
@wsgi.extends
|
||||||
|
def index(self, req):
|
||||||
|
"""Returns a summary list of volumes available to manage."""
|
||||||
|
context = req.environ['cinder.context']
|
||||||
|
authorize_list_manageable(context)
|
||||||
|
return resource_common_manage.get_manageable_resources(
|
||||||
|
req, False, self.volume_api.get_manageable_volumes,
|
||||||
|
self._list_manageable_view)
|
||||||
|
|
||||||
|
@wsgi.extends
|
||||||
|
def detail(self, req):
|
||||||
|
"""Returns a detailed list of volumes available to manage."""
|
||||||
|
context = req.environ['cinder.context']
|
||||||
|
authorize_list_manageable(context)
|
||||||
|
return resource_common_manage.get_manageable_resources(
|
||||||
|
req, True, self.volume_api.get_manageable_volumes,
|
||||||
|
self._list_manageable_view)
|
||||||
|
|
||||||
|
|
||||||
class Volume_manage(extensions.ExtensionDescriptor):
|
class Volume_manage(extensions.ExtensionDescriptor):
|
||||||
"""Allows existing backend storage to be 'managed' by Cinder."""
|
"""Allows existing backend storage to be 'managed' by Cinder."""
|
||||||
@ -156,5 +179,7 @@ class Volume_manage(extensions.ExtensionDescriptor):
|
|||||||
def get_resources(self):
|
def get_resources(self):
|
||||||
controller = VolumeManageController()
|
controller = VolumeManageController()
|
||||||
res = extensions.ResourceExtension(Volume_manage.alias,
|
res = extensions.ResourceExtension(Volume_manage.alias,
|
||||||
controller)
|
controller,
|
||||||
|
collection_actions=
|
||||||
|
{'detail': 'GET'})
|
||||||
return [res]
|
return [res]
|
||||||
|
60
cinder/api/views/manageable_snapshots.py
Normal file
60
cinder/api/views/manageable_snapshots.py
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
# Copyright (c) 2016 Stratoscale, Ltd.
|
||||||
|
#
|
||||||
|
# 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_log import log as logging
|
||||||
|
|
||||||
|
from cinder.api import common
|
||||||
|
|
||||||
|
|
||||||
|
LOG = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class ViewBuilder(common.ViewBuilder):
|
||||||
|
"""Model manageable snapshot responses as a python dictionary."""
|
||||||
|
|
||||||
|
_collection_name = "os-snapshot-manage"
|
||||||
|
|
||||||
|
def summary_list(self, request, snapshots, count):
|
||||||
|
"""Show a list of manageable snapshots without many details."""
|
||||||
|
return self._list_view(self.summary, request, snapshots, count)
|
||||||
|
|
||||||
|
def detail_list(self, request, snapshots, count):
|
||||||
|
"""Detailed view of a list of manageable snapshots."""
|
||||||
|
return self._list_view(self.detail, request, snapshots, count)
|
||||||
|
|
||||||
|
def summary(self, request, snapshot):
|
||||||
|
"""Generic, non-detailed view of a manageable snapshot description."""
|
||||||
|
return {
|
||||||
|
'reference': snapshot['reference'],
|
||||||
|
'size': snapshot['size'],
|
||||||
|
'safe_to_manage': snapshot['safe_to_manage'],
|
||||||
|
'source_reference': snapshot['source_reference']
|
||||||
|
}
|
||||||
|
|
||||||
|
def detail(self, request, snapshot):
|
||||||
|
"""Detailed view of a manageable snapshot description."""
|
||||||
|
return {
|
||||||
|
'reference': snapshot['reference'],
|
||||||
|
'size': snapshot['size'],
|
||||||
|
'safe_to_manage': snapshot['safe_to_manage'],
|
||||||
|
'reason_not_safe': snapshot['reason_not_safe'],
|
||||||
|
'extra_info': snapshot['extra_info'],
|
||||||
|
'cinder_id': snapshot['cinder_id'],
|
||||||
|
'source_reference': snapshot['source_reference']
|
||||||
|
}
|
||||||
|
|
||||||
|
def _list_view(self, func, request, snapshots, count):
|
||||||
|
"""Provide a view for a list of manageable snapshots."""
|
||||||
|
snap_list = [func(request, snapshot) for snapshot in snapshots]
|
||||||
|
return {"manageable-snapshots": snap_list}
|
58
cinder/api/views/manageable_volumes.py
Normal file
58
cinder/api/views/manageable_volumes.py
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
# Copyright (c) 2016 Stratoscale, Ltd.
|
||||||
|
#
|
||||||
|
# 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_log import log as logging
|
||||||
|
|
||||||
|
from cinder.api import common
|
||||||
|
|
||||||
|
|
||||||
|
LOG = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class ViewBuilder(common.ViewBuilder):
|
||||||
|
"""Model manageable volume responses as a python dictionary."""
|
||||||
|
|
||||||
|
_collection_name = "os-volume-manage"
|
||||||
|
|
||||||
|
def summary_list(self, request, volumes, count):
|
||||||
|
"""Show a list of manageable volumes without many details."""
|
||||||
|
return self._list_view(self.summary, request, volumes, count)
|
||||||
|
|
||||||
|
def detail_list(self, request, volumes, count):
|
||||||
|
"""Detailed view of a list of manageable volumes."""
|
||||||
|
return self._list_view(self.detail, request, volumes, count)
|
||||||
|
|
||||||
|
def summary(self, request, volume):
|
||||||
|
"""Generic, non-detailed view of a manageable volume description."""
|
||||||
|
return {
|
||||||
|
'reference': volume['reference'],
|
||||||
|
'size': volume['size'],
|
||||||
|
'safe_to_manage': volume['safe_to_manage']
|
||||||
|
}
|
||||||
|
|
||||||
|
def detail(self, request, volume):
|
||||||
|
"""Detailed view of a manageable volume description."""
|
||||||
|
return {
|
||||||
|
'reference': volume['reference'],
|
||||||
|
'size': volume['size'],
|
||||||
|
'safe_to_manage': volume['safe_to_manage'],
|
||||||
|
'reason_not_safe': volume['reason_not_safe'],
|
||||||
|
'cinder_id': volume['cinder_id'],
|
||||||
|
'extra_info': volume['extra_info']
|
||||||
|
}
|
||||||
|
|
||||||
|
def _list_view(self, func, request, volumes, count):
|
||||||
|
"""Provide a view for a list of manageable volumes."""
|
||||||
|
vol_list = [func(request, volume) for volume in volumes]
|
||||||
|
return {"manageable-volumes": vol_list}
|
@ -1,4 +1,5 @@
|
|||||||
# Copyright (c) 2015 Huawei Technologies Co., Ltd.
|
# Copyright (c) 2015 Huawei Technologies Co., Ltd.
|
||||||
|
# Copyright (c) 2016 Stratoscale, Ltd.
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
# 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
|
# not use this file except in compliance with the License. You may obtain
|
||||||
@ -13,7 +14,12 @@
|
|||||||
# under the License.
|
# under the License.
|
||||||
|
|
||||||
import mock
|
import mock
|
||||||
|
from oslo_config import cfg
|
||||||
from oslo_serialization import jsonutils
|
from oslo_serialization import jsonutils
|
||||||
|
try:
|
||||||
|
from urllib import urlencode
|
||||||
|
except ImportError:
|
||||||
|
from urllib.parse import urlencode
|
||||||
import webob
|
import webob
|
||||||
|
|
||||||
from cinder import context
|
from cinder import context
|
||||||
@ -23,6 +29,8 @@ 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 fake_service
|
from cinder.tests.unit import fake_service
|
||||||
|
|
||||||
|
CONF = cfg.CONF
|
||||||
|
|
||||||
|
|
||||||
def app():
|
def app():
|
||||||
# no auth, just let environ['cinder.context'] pass through
|
# no auth, just let environ['cinder.context'] pass through
|
||||||
@ -39,6 +47,28 @@ def volume_get(self, context, volume_id, viewable_admin_meta=False):
|
|||||||
raise exception.VolumeNotFound(volume_id=volume_id)
|
raise exception.VolumeNotFound(volume_id=volume_id)
|
||||||
|
|
||||||
|
|
||||||
|
def api_get_manageable_snapshots(*args, **kwargs):
|
||||||
|
"""Replacement for cinder.volume.api.API.get_manageable_snapshots."""
|
||||||
|
snap_id = 'ffffffff-0000-ffff-0000-ffffffffffff'
|
||||||
|
snaps = [
|
||||||
|
{'reference': {'source-name': 'snapshot-%s' % snap_id},
|
||||||
|
'size': 4,
|
||||||
|
'extra_info': 'qos_setting:high',
|
||||||
|
'safe_to_manage': False,
|
||||||
|
'reason_not_safe': 'snapshot in use',
|
||||||
|
'cinder_id': snap_id,
|
||||||
|
'source_reference': {'source-name':
|
||||||
|
'volume-00000000-ffff-0000-ffff-000000'}},
|
||||||
|
{'reference': {'source-name': 'mysnap'},
|
||||||
|
'size': 5,
|
||||||
|
'extra_info': 'qos_setting:low',
|
||||||
|
'safe_to_manage': True,
|
||||||
|
'reason_not_safe': None,
|
||||||
|
'cinder_id': None,
|
||||||
|
'source_reference': {'source-name': 'myvol'}}]
|
||||||
|
return snaps
|
||||||
|
|
||||||
|
|
||||||
@mock.patch('cinder.volume.api.API.get', volume_get)
|
@mock.patch('cinder.volume.api.API.get', volume_get)
|
||||||
class SnapshotManageTest(test.TestCase):
|
class SnapshotManageTest(test.TestCase):
|
||||||
"""Test cases for cinder/api/contrib/snapshot_manage.py
|
"""Test cases for cinder/api/contrib/snapshot_manage.py
|
||||||
@ -55,15 +85,22 @@ class SnapshotManageTest(test.TestCase):
|
|||||||
with the correct arguments.
|
with the correct arguments.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def _get_resp(self, body):
|
def setUp(self):
|
||||||
|
super(SnapshotManageTest, self).setUp()
|
||||||
|
self._admin_ctxt = context.RequestContext(fake.USER_ID,
|
||||||
|
fake.PROJECT_ID,
|
||||||
|
is_admin=True)
|
||||||
|
self._non_admin_ctxt = context.RequestContext(fake.USER_ID,
|
||||||
|
fake.PROJECT_ID,
|
||||||
|
is_admin=False)
|
||||||
|
|
||||||
|
def _get_resp_post(self, body):
|
||||||
"""Helper to execute an os-snapshot-manage API call."""
|
"""Helper to execute an os-snapshot-manage API call."""
|
||||||
req = webob.Request.blank('/v2/%s/os-snapshot-manage' %
|
req = webob.Request.blank('/v2/%s/os-snapshot-manage' %
|
||||||
fake.PROJECT_ID)
|
fake.PROJECT_ID)
|
||||||
req.method = 'POST'
|
req.method = 'POST'
|
||||||
req.headers['Content-Type'] = 'application/json'
|
req.headers['Content-Type'] = 'application/json'
|
||||||
req.environ['cinder.context'] = context.RequestContext(fake.USER_ID,
|
req.environ['cinder.context'] = self._admin_ctxt
|
||||||
fake.PROJECT_ID,
|
|
||||||
True)
|
|
||||||
req.body = jsonutils.dump_as_bytes(body)
|
req.body = jsonutils.dump_as_bytes(body)
|
||||||
res = req.get_response(app())
|
res = req.get_response(app())
|
||||||
return res
|
return res
|
||||||
@ -80,13 +117,11 @@ class SnapshotManageTest(test.TestCase):
|
|||||||
called with the correct arguments, and that we return the correct HTTP
|
called with the correct arguments, and that we return the correct HTTP
|
||||||
code to the caller.
|
code to the caller.
|
||||||
"""
|
"""
|
||||||
ctxt = context.RequestContext(fake.USER_ID, fake.PROJECT_ID, True)
|
|
||||||
mock_db.return_value = fake_service.fake_service_obj(
|
mock_db.return_value = fake_service.fake_service_obj(
|
||||||
ctxt,
|
self._admin_ctxt,
|
||||||
binary='cinder-volume')
|
binary='cinder-volume')
|
||||||
body = {'snapshot': {'volume_id': fake.VOLUME_ID, 'ref': 'fake_ref'}}
|
body = {'snapshot': {'volume_id': fake.VOLUME_ID, 'ref': 'fake_ref'}}
|
||||||
|
res = self._get_resp_post(body)
|
||||||
res = self._get_resp(body)
|
|
||||||
self.assertEqual(202, res.status_int, res)
|
self.assertEqual(202, res.status_int, res)
|
||||||
|
|
||||||
# Check the db.service_get_by_host_and_topic was called with correct
|
# Check the db.service_get_by_host_and_topic was called with correct
|
||||||
@ -112,24 +147,96 @@ class SnapshotManageTest(test.TestCase):
|
|||||||
def test_manage_snapshot_missing_volume_id(self):
|
def test_manage_snapshot_missing_volume_id(self):
|
||||||
"""Test correct failure when volume_id is not specified."""
|
"""Test correct failure when volume_id is not specified."""
|
||||||
body = {'snapshot': {'ref': 'fake_ref'}}
|
body = {'snapshot': {'ref': 'fake_ref'}}
|
||||||
res = self._get_resp(body)
|
res = self._get_resp_post(body)
|
||||||
self.assertEqual(400, res.status_int)
|
self.assertEqual(400, res.status_int)
|
||||||
|
|
||||||
def test_manage_snapshot_missing_ref(self):
|
def test_manage_snapshot_missing_ref(self):
|
||||||
"""Test correct failure when the ref is not specified."""
|
"""Test correct failure when the ref is not specified."""
|
||||||
body = {'snapshot': {'volume_id': fake.VOLUME_ID}}
|
body = {'snapshot': {'volume_id': fake.VOLUME_ID}}
|
||||||
res = self._get_resp(body)
|
res = self._get_resp_post(body)
|
||||||
self.assertEqual(400, res.status_int)
|
self.assertEqual(400, res.status_int)
|
||||||
|
|
||||||
def test_manage_snapshot_error_body(self):
|
def test_manage_snapshot_error_body(self):
|
||||||
"""Test correct failure when body is invaild."""
|
"""Test correct failure when body is invaild."""
|
||||||
body = {'error_snapshot': {'volume_id': fake.VOLUME_ID}}
|
body = {'error_snapshot': {'volume_id': fake.VOLUME_ID}}
|
||||||
res = self._get_resp(body)
|
res = self._get_resp_post(body)
|
||||||
self.assertEqual(400, res.status_int)
|
self.assertEqual(400, res.status_int)
|
||||||
|
|
||||||
def test_manage_snapshot_error_volume_id(self):
|
def test_manage_snapshot_error_volume_id(self):
|
||||||
"""Test correct failure when volume can't be found."""
|
"""Test correct failure when volume can't be found."""
|
||||||
body = {'snapshot': {'volume_id': 'error_volume_id',
|
body = {'snapshot': {'volume_id': 'error_volume_id',
|
||||||
'ref': 'fake_ref'}}
|
'ref': 'fake_ref'}}
|
||||||
res = self._get_resp(body)
|
res = self._get_resp_post(body)
|
||||||
self.assertEqual(404, res.status_int)
|
self.assertEqual(404, res.status_int)
|
||||||
|
|
||||||
|
def _get_resp_get(self, host, detailed, paging, admin=True):
|
||||||
|
"""Helper to execute a GET os-snapshot-manage API call."""
|
||||||
|
params = {'host': host}
|
||||||
|
if paging:
|
||||||
|
params.update({'marker': '1234', 'limit': 10,
|
||||||
|
'offset': 4, 'sort': 'reference:asc'})
|
||||||
|
query_string = "?%s" % urlencode(params)
|
||||||
|
detail = ""
|
||||||
|
if detailed:
|
||||||
|
detail = "/detail"
|
||||||
|
url = "/v2/%s/os-snapshot-manage%s%s" % (fake.PROJECT_ID, detail,
|
||||||
|
query_string)
|
||||||
|
req = webob.Request.blank(url)
|
||||||
|
req.method = 'GET'
|
||||||
|
req.headers['Content-Type'] = 'application/json'
|
||||||
|
req.environ['cinder.context'] = (self._admin_ctxt if admin
|
||||||
|
else self._non_admin_ctxt)
|
||||||
|
res = req.get_response(app())
|
||||||
|
return res
|
||||||
|
|
||||||
|
@mock.patch('cinder.volume.api.API.get_manageable_snapshots',
|
||||||
|
wraps=api_get_manageable_snapshots)
|
||||||
|
def test_get_manageable_snapshots_non_admin(self, mock_api_manageable):
|
||||||
|
res = self._get_resp_get('fakehost', False, False, admin=False)
|
||||||
|
self.assertEqual(403, res.status_int)
|
||||||
|
self.assertEqual(False, mock_api_manageable.called)
|
||||||
|
res = self._get_resp_get('fakehost', True, False, admin=False)
|
||||||
|
self.assertEqual(403, res.status_int)
|
||||||
|
self.assertEqual(False, mock_api_manageable.called)
|
||||||
|
|
||||||
|
@mock.patch('cinder.volume.api.API.get_manageable_snapshots',
|
||||||
|
wraps=api_get_manageable_snapshots)
|
||||||
|
def test_get_manageable_snapshots_ok(self, mock_api_manageable):
|
||||||
|
res = self._get_resp_get('fakehost', False, False)
|
||||||
|
snap_name = 'snapshot-ffffffff-0000-ffff-0000-ffffffffffff'
|
||||||
|
exp = {'manageable-snapshots':
|
||||||
|
[{'reference': {'source-name': snap_name}, 'size': 4,
|
||||||
|
'safe_to_manage': False,
|
||||||
|
'source_reference':
|
||||||
|
{'source-name': 'volume-00000000-ffff-0000-ffff-000000'}},
|
||||||
|
{'reference': {'source-name': 'mysnap'}, 'size': 5,
|
||||||
|
'safe_to_manage': True,
|
||||||
|
'source_reference': {'source-name': 'myvol'}}]}
|
||||||
|
self.assertEqual(200, res.status_int)
|
||||||
|
self.assertEqual(jsonutils.loads(res.body), exp)
|
||||||
|
mock_api_manageable.assert_called_once_with(
|
||||||
|
self._admin_ctxt, 'fakehost', limit=CONF.osapi_max_limit,
|
||||||
|
marker=None, offset=0, sort_dirs=['desc'],
|
||||||
|
sort_keys=['reference'])
|
||||||
|
|
||||||
|
@mock.patch('cinder.volume.api.API.get_manageable_snapshots',
|
||||||
|
wraps=api_get_manageable_snapshots)
|
||||||
|
def test_get_manageable_snapshots_detailed_ok(self, mock_api_manageable):
|
||||||
|
res = self._get_resp_get('fakehost', True, True)
|
||||||
|
snap_id = 'ffffffff-0000-ffff-0000-ffffffffffff'
|
||||||
|
exp = {'manageable-snapshots':
|
||||||
|
[{'reference': {'source-name': 'snapshot-%s' % snap_id},
|
||||||
|
'size': 4, 'safe_to_manage': False, 'cinder_id': snap_id,
|
||||||
|
'reason_not_safe': 'snapshot in use',
|
||||||
|
'extra_info': 'qos_setting:high',
|
||||||
|
'source_reference':
|
||||||
|
{'source-name': 'volume-00000000-ffff-0000-ffff-000000'}},
|
||||||
|
{'reference': {'source-name': 'mysnap'}, 'size': 5,
|
||||||
|
'cinder_id': None, 'safe_to_manage': True,
|
||||||
|
'reason_not_safe': None, 'extra_info': 'qos_setting:low',
|
||||||
|
'source_reference': {'source-name': 'myvol'}}]}
|
||||||
|
self.assertEqual(200, res.status_int)
|
||||||
|
self.assertEqual(jsonutils.loads(res.body), exp)
|
||||||
|
mock_api_manageable.assert_called_once_with(
|
||||||
|
self._admin_ctxt, 'fakehost', limit=10, marker='1234', offset=4,
|
||||||
|
sort_dirs=['asc'], sort_keys=['reference'])
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
# Copyright 2014 IBM Corp.
|
# Copyright 2014 IBM Corp.
|
||||||
|
# Copyright (c) 2016 Stratoscale, Ltd.
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
# 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
|
# not use this file except in compliance with the License. You may obtain
|
||||||
@ -13,7 +14,12 @@
|
|||||||
# under the License.
|
# under the License.
|
||||||
|
|
||||||
import mock
|
import mock
|
||||||
|
from oslo_config import cfg
|
||||||
from oslo_serialization import jsonutils
|
from oslo_serialization import jsonutils
|
||||||
|
try:
|
||||||
|
from urllib import urlencode
|
||||||
|
except ImportError:
|
||||||
|
from urllib.parse import urlencode
|
||||||
import webob
|
import webob
|
||||||
|
|
||||||
from cinder import context
|
from cinder import context
|
||||||
@ -23,6 +29,8 @@ 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 fake_volume
|
from cinder.tests.unit import fake_volume
|
||||||
|
|
||||||
|
CONF = cfg.CONF
|
||||||
|
|
||||||
|
|
||||||
def app():
|
def app():
|
||||||
# no auth, just let environ['cinder.context'] pass through
|
# no auth, just let environ['cinder.context'] pass through
|
||||||
@ -100,6 +108,25 @@ def api_manage(*args, **kwargs):
|
|||||||
return fake_volume.fake_volume_obj(ctx, **vol)
|
return fake_volume.fake_volume_obj(ctx, **vol)
|
||||||
|
|
||||||
|
|
||||||
|
def api_get_manageable_volumes(*args, **kwargs):
|
||||||
|
"""Replacement for cinder.volume.api.API.get_manageable_volumes."""
|
||||||
|
vol_id = 'ffffffff-0000-ffff-0000-ffffffffffff'
|
||||||
|
vols = [
|
||||||
|
{'reference': {'source-name': 'volume-%s' % vol_id},
|
||||||
|
'size': 4,
|
||||||
|
'extra_info': 'qos_setting:high',
|
||||||
|
'safe_to_manage': False,
|
||||||
|
'cinder_id': vol_id,
|
||||||
|
'reason_not_safe': 'volume in use'},
|
||||||
|
{'reference': {'source-name': 'myvol'},
|
||||||
|
'size': 5,
|
||||||
|
'extra_info': 'qos_setting:low',
|
||||||
|
'safe_to_manage': True,
|
||||||
|
'cinder_id': None,
|
||||||
|
'reason_not_safe': None}]
|
||||||
|
return vols
|
||||||
|
|
||||||
|
|
||||||
@mock.patch('cinder.db.service_get_by_host_and_topic',
|
@mock.patch('cinder.db.service_get_by_host_and_topic',
|
||||||
db_service_get_by_host_and_topic)
|
db_service_get_by_host_and_topic)
|
||||||
@mock.patch('cinder.volume.volume_types.get_volume_type_by_name',
|
@mock.patch('cinder.volume.volume_types.get_volume_type_by_name',
|
||||||
@ -122,15 +149,19 @@ class VolumeManageTest(test.TestCase):
|
|||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
super(VolumeManageTest, self).setUp()
|
super(VolumeManageTest, self).setUp()
|
||||||
|
self._admin_ctxt = context.RequestContext(fake.USER_ID,
|
||||||
|
fake.PROJECT_ID,
|
||||||
|
is_admin=True)
|
||||||
|
self._non_admin_ctxt = context.RequestContext(fake.USER_ID,
|
||||||
|
fake.PROJECT_ID,
|
||||||
|
is_admin=False)
|
||||||
|
|
||||||
def _get_resp(self, body):
|
def _get_resp_post(self, body):
|
||||||
"""Helper to execute an os-volume-manage API call."""
|
"""Helper to execute a POST os-volume-manage API call."""
|
||||||
req = webob.Request.blank('/v2/%s/os-volume-manage' % fake.PROJECT_ID)
|
req = webob.Request.blank('/v2/%s/os-volume-manage' % fake.PROJECT_ID)
|
||||||
req.method = 'POST'
|
req.method = 'POST'
|
||||||
req.headers['Content-Type'] = 'application/json'
|
req.headers['Content-Type'] = 'application/json'
|
||||||
req.environ['cinder.context'] = context.RequestContext(fake.USER_ID,
|
req.environ['cinder.context'] = self._admin_ctxt
|
||||||
fake.PROJECT_ID,
|
|
||||||
True)
|
|
||||||
req.body = jsonutils.dump_as_bytes(body)
|
req.body = jsonutils.dump_as_bytes(body)
|
||||||
res = req.get_response(app())
|
res = req.get_response(app())
|
||||||
return res
|
return res
|
||||||
@ -148,7 +179,7 @@ class VolumeManageTest(test.TestCase):
|
|||||||
"""
|
"""
|
||||||
body = {'volume': {'host': 'host_ok',
|
body = {'volume': {'host': 'host_ok',
|
||||||
'ref': 'fake_ref'}}
|
'ref': 'fake_ref'}}
|
||||||
res = self._get_resp(body)
|
res = self._get_resp_post(body)
|
||||||
self.assertEqual(202, res.status_int, res)
|
self.assertEqual(202, res.status_int, res)
|
||||||
|
|
||||||
# Check that the manage API was called with the correct arguments.
|
# Check that the manage API was called with the correct arguments.
|
||||||
@ -161,13 +192,13 @@ class VolumeManageTest(test.TestCase):
|
|||||||
def test_manage_volume_missing_host(self):
|
def test_manage_volume_missing_host(self):
|
||||||
"""Test correct failure when host is not specified."""
|
"""Test correct failure when host is not specified."""
|
||||||
body = {'volume': {'ref': 'fake_ref'}}
|
body = {'volume': {'ref': 'fake_ref'}}
|
||||||
res = self._get_resp(body)
|
res = self._get_resp_post(body)
|
||||||
self.assertEqual(400, res.status_int)
|
self.assertEqual(400, res.status_int)
|
||||||
|
|
||||||
def test_manage_volume_missing_ref(self):
|
def test_manage_volume_missing_ref(self):
|
||||||
"""Test correct failure when the ref is not specified."""
|
"""Test correct failure when the ref is not specified."""
|
||||||
body = {'volume': {'host': 'host_ok'}}
|
body = {'volume': {'host': 'host_ok'}}
|
||||||
res = self._get_resp(body)
|
res = self._get_resp_post(body)
|
||||||
self.assertEqual(400, res.status_int)
|
self.assertEqual(400, res.status_int)
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@ -183,7 +214,7 @@ class VolumeManageTest(test.TestCase):
|
|||||||
body = {'volume': {'host': 'host_ok',
|
body = {'volume': {'host': 'host_ok',
|
||||||
'ref': 'fake_ref',
|
'ref': 'fake_ref',
|
||||||
'volume_type': fake.VOLUME_TYPE_ID}}
|
'volume_type': fake.VOLUME_TYPE_ID}}
|
||||||
res = self._get_resp(body)
|
res = self._get_resp_post(body)
|
||||||
self.assertEqual(202, res.status_int, res)
|
self.assertEqual(202, res.status_int, res)
|
||||||
self.assertTrue(mock_validate.called)
|
self.assertTrue(mock_validate.called)
|
||||||
pass
|
pass
|
||||||
@ -200,7 +231,7 @@ class VolumeManageTest(test.TestCase):
|
|||||||
body = {'volume': {'host': 'host_ok',
|
body = {'volume': {'host': 'host_ok',
|
||||||
'ref': 'fake_ref',
|
'ref': 'fake_ref',
|
||||||
'volume_type': 'good_fakevt'}}
|
'volume_type': 'good_fakevt'}}
|
||||||
res = self._get_resp(body)
|
res = self._get_resp_post(body)
|
||||||
self.assertEqual(202, res.status_int, res)
|
self.assertEqual(202, res.status_int, res)
|
||||||
self.assertTrue(mock_validate.called)
|
self.assertTrue(mock_validate.called)
|
||||||
pass
|
pass
|
||||||
@ -210,7 +241,7 @@ class VolumeManageTest(test.TestCase):
|
|||||||
body = {'volume': {'host': 'host_ok',
|
body = {'volume': {'host': 'host_ok',
|
||||||
'ref': 'fake_ref',
|
'ref': 'fake_ref',
|
||||||
'volume_type': fake.WILL_NOT_BE_FOUND_ID}}
|
'volume_type': fake.WILL_NOT_BE_FOUND_ID}}
|
||||||
res = self._get_resp(body)
|
res = self._get_resp_post(body)
|
||||||
self.assertEqual(404, res.status_int, res)
|
self.assertEqual(404, res.status_int, res)
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@ -219,6 +250,73 @@ class VolumeManageTest(test.TestCase):
|
|||||||
body = {'volume': {'host': 'host_ok',
|
body = {'volume': {'host': 'host_ok',
|
||||||
'ref': 'fake_ref',
|
'ref': 'fake_ref',
|
||||||
'volume_type': 'bad_fakevt'}}
|
'volume_type': 'bad_fakevt'}}
|
||||||
res = self._get_resp(body)
|
res = self._get_resp_post(body)
|
||||||
self.assertEqual(404, res.status_int, res)
|
self.assertEqual(404, res.status_int, res)
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
def _get_resp_get(self, host, detailed, paging, admin=True):
|
||||||
|
"""Helper to execute a GET os-volume-manage API call."""
|
||||||
|
params = {'host': host}
|
||||||
|
if paging:
|
||||||
|
params.update({'marker': '1234', 'limit': 10,
|
||||||
|
'offset': 4, 'sort': 'reference:asc'})
|
||||||
|
query_string = "?%s" % urlencode(params)
|
||||||
|
detail = ""
|
||||||
|
if detailed:
|
||||||
|
detail = "/detail"
|
||||||
|
url = "/v2/%s/os-volume-manage%s%s" % (fake.PROJECT_ID, detail,
|
||||||
|
query_string)
|
||||||
|
req = webob.Request.blank(url)
|
||||||
|
req.method = 'GET'
|
||||||
|
req.headers['Content-Type'] = 'application/json'
|
||||||
|
req.environ['cinder.context'] = (self._admin_ctxt if admin
|
||||||
|
else self._non_admin_ctxt)
|
||||||
|
res = req.get_response(app())
|
||||||
|
return res
|
||||||
|
|
||||||
|
@mock.patch('cinder.volume.api.API.get_manageable_volumes',
|
||||||
|
wraps=api_get_manageable_volumes)
|
||||||
|
def test_get_manageable_volumes_non_admin(self, mock_api_manageable):
|
||||||
|
res = self._get_resp_get('fakehost', False, False, admin=False)
|
||||||
|
self.assertEqual(403, res.status_int)
|
||||||
|
self.assertEqual(False, mock_api_manageable.called)
|
||||||
|
res = self._get_resp_get('fakehost', True, False, admin=False)
|
||||||
|
self.assertEqual(403, res.status_int)
|
||||||
|
self.assertEqual(False, mock_api_manageable.called)
|
||||||
|
|
||||||
|
@mock.patch('cinder.volume.api.API.get_manageable_volumes',
|
||||||
|
wraps=api_get_manageable_volumes)
|
||||||
|
def test_get_manageable_volumes_ok(self, mock_api_manageable):
|
||||||
|
res = self._get_resp_get('fakehost', False, True)
|
||||||
|
exp = {'manageable-volumes':
|
||||||
|
[{'reference':
|
||||||
|
{'source-name':
|
||||||
|
'volume-ffffffff-0000-ffff-0000-ffffffffffff'},
|
||||||
|
'size': 4, 'safe_to_manage': False},
|
||||||
|
{'reference': {'source-name': 'myvol'},
|
||||||
|
'size': 5, 'safe_to_manage': True}]}
|
||||||
|
self.assertEqual(200, res.status_int)
|
||||||
|
self.assertEqual(jsonutils.loads(res.body), exp)
|
||||||
|
mock_api_manageable.assert_called_once_with(
|
||||||
|
self._admin_ctxt, 'fakehost', limit=10, marker='1234', offset=4,
|
||||||
|
sort_dirs=['asc'], sort_keys=['reference'])
|
||||||
|
|
||||||
|
@mock.patch('cinder.volume.api.API.get_manageable_volumes',
|
||||||
|
wraps=api_get_manageable_volumes)
|
||||||
|
def test_get_manageable_volumes_detailed_ok(self, mock_api_manageable):
|
||||||
|
res = self._get_resp_get('fakehost', True, False)
|
||||||
|
vol_id = 'ffffffff-0000-ffff-0000-ffffffffffff'
|
||||||
|
exp = {'manageable-volumes':
|
||||||
|
[{'reference': {'source-name': 'volume-%s' % vol_id},
|
||||||
|
'size': 4, 'reason_not_safe': 'volume in use',
|
||||||
|
'cinder_id': vol_id, 'safe_to_manage': False,
|
||||||
|
'extra_info': 'qos_setting:high'},
|
||||||
|
{'reference': {'source-name': 'myvol'}, 'cinder_id': None,
|
||||||
|
'size': 5, 'reason_not_safe': None, 'safe_to_manage': True,
|
||||||
|
'extra_info': 'qos_setting:low'}]}
|
||||||
|
self.assertEqual(200, res.status_int)
|
||||||
|
self.assertEqual(jsonutils.loads(res.body), exp)
|
||||||
|
mock_api_manageable.assert_called_once_with(
|
||||||
|
self._admin_ctxt, 'fakehost', limit=CONF.osapi_max_limit,
|
||||||
|
marker=None, offset=0, sort_dirs=['desc'],
|
||||||
|
sort_keys=['reference'])
|
||||||
|
@ -74,6 +74,7 @@
|
|||||||
"volume_extension:services:update" : "rule:admin_api",
|
"volume_extension:services:update" : "rule:admin_api",
|
||||||
"volume_extension:volume_manage": "rule:admin_api",
|
"volume_extension:volume_manage": "rule:admin_api",
|
||||||
"volume_extension:volume_unmanage": "rule:admin_api",
|
"volume_extension:volume_unmanage": "rule:admin_api",
|
||||||
|
"volume_extension:list_manageable": "rule:admin_api",
|
||||||
"volume_extension:capabilities": "rule:admin_api",
|
"volume_extension:capabilities": "rule:admin_api",
|
||||||
|
|
||||||
"limits_extension:used_limits": "",
|
"limits_extension:used_limits": "",
|
||||||
@ -81,6 +82,7 @@
|
|||||||
"snapshot_extension:snapshot_actions:update_snapshot_status": "",
|
"snapshot_extension:snapshot_actions:update_snapshot_status": "",
|
||||||
"snapshot_extension:snapshot_manage": "rule:admin_api",
|
"snapshot_extension:snapshot_manage": "rule:admin_api",
|
||||||
"snapshot_extension:snapshot_unmanage": "rule:admin_api",
|
"snapshot_extension:snapshot_unmanage": "rule:admin_api",
|
||||||
|
"snapshot_extension:list_manageable": "rule:admin_api",
|
||||||
|
|
||||||
"volume:create_transfer": "",
|
"volume:create_transfer": "",
|
||||||
"volume:accept_transfer": "",
|
"volume:accept_transfer": "",
|
||||||
|
@ -1506,14 +1506,7 @@ class API(base.Base):
|
|||||||
LOG.info(_LI("Retype volume request issued successfully."),
|
LOG.info(_LI("Retype volume request issued successfully."),
|
||||||
resource=volume)
|
resource=volume)
|
||||||
|
|
||||||
def manage_existing(self, context, host, ref, name=None, description=None,
|
def _get_service_by_host(self, context, host):
|
||||||
volume_type=None, metadata=None,
|
|
||||||
availability_zone=None, bootable=False):
|
|
||||||
if volume_type and 'extra_specs' not in volume_type:
|
|
||||||
extra_specs = volume_types.get_volume_type_extra_specs(
|
|
||||||
volume_type['id'])
|
|
||||||
volume_type['extra_specs'] = extra_specs
|
|
||||||
|
|
||||||
elevated = context.elevated()
|
elevated = context.elevated()
|
||||||
try:
|
try:
|
||||||
svc_host = volume_utils.extract_host(host, 'backend')
|
svc_host = volume_utils.extract_host(host, 'backend')
|
||||||
@ -1530,6 +1523,18 @@ class API(base.Base):
|
|||||||
'service.'))
|
'service.'))
|
||||||
raise exception.ServiceUnavailable()
|
raise exception.ServiceUnavailable()
|
||||||
|
|
||||||
|
return service
|
||||||
|
|
||||||
|
def manage_existing(self, context, host, ref, name=None, description=None,
|
||||||
|
volume_type=None, metadata=None,
|
||||||
|
availability_zone=None, bootable=False):
|
||||||
|
if volume_type and 'extra_specs' not in volume_type:
|
||||||
|
extra_specs = volume_types.get_volume_type_extra_specs(
|
||||||
|
volume_type['id'])
|
||||||
|
volume_type['extra_specs'] = extra_specs
|
||||||
|
|
||||||
|
service = self._get_service_by_host(context, host)
|
||||||
|
|
||||||
if availability_zone is None:
|
if availability_zone is None:
|
||||||
availability_zone = service.get('availability_zone')
|
availability_zone = service.get('availability_zone')
|
||||||
|
|
||||||
@ -1564,6 +1569,14 @@ class API(base.Base):
|
|||||||
resource=vol_ref)
|
resource=vol_ref)
|
||||||
return vol_ref
|
return vol_ref
|
||||||
|
|
||||||
|
def get_manageable_volumes(self, context, host, marker=None, limit=None,
|
||||||
|
offset=None, sort_keys=None, sort_dirs=None):
|
||||||
|
self._get_service_by_host(context, host)
|
||||||
|
return self.volume_rpcapi.get_manageable_volumes(context, host,
|
||||||
|
marker, limit,
|
||||||
|
offset, sort_keys,
|
||||||
|
sort_dirs)
|
||||||
|
|
||||||
def manage_existing_snapshot(self, context, ref, volume,
|
def manage_existing_snapshot(self, context, ref, volume,
|
||||||
name=None, description=None,
|
name=None, description=None,
|
||||||
metadata=None):
|
metadata=None):
|
||||||
@ -1591,6 +1604,14 @@ class API(base.Base):
|
|||||||
ref, host)
|
ref, host)
|
||||||
return snapshot_object
|
return snapshot_object
|
||||||
|
|
||||||
|
def get_manageable_snapshots(self, context, host, marker=None, limit=None,
|
||||||
|
offset=None, sort_keys=None, sort_dirs=None):
|
||||||
|
self._get_service_by_host(context, host)
|
||||||
|
return self.volume_rpcapi.get_manageable_snapshots(context, host,
|
||||||
|
marker, limit,
|
||||||
|
offset, sort_keys,
|
||||||
|
sort_dirs)
|
||||||
|
|
||||||
# FIXME(jdg): Move these Cheesecake methods (freeze, thaw and failover)
|
# FIXME(jdg): Move these Cheesecake methods (freeze, thaw and failover)
|
||||||
# to a services API because that's what they are
|
# to a services API because that's what they are
|
||||||
def failover_host(self,
|
def failover_host(self,
|
||||||
|
@ -1819,6 +1819,37 @@ class ManageableVD(object):
|
|||||||
"""
|
"""
|
||||||
return
|
return
|
||||||
|
|
||||||
|
def get_manageable_volumes(self, cinder_volumes, marker, limit, offset,
|
||||||
|
sort_keys, sort_dirs):
|
||||||
|
"""List volumes on the backend available for management by Cinder.
|
||||||
|
|
||||||
|
Returns a list of dictionaries, each specifying a volume in the host,
|
||||||
|
with the following keys:
|
||||||
|
- reference (dictionary): The reference for a volume, which can be
|
||||||
|
passed to "manage_existing".
|
||||||
|
- size (int): The size of the volume according to the storage
|
||||||
|
backend, rounded up to the nearest GB.
|
||||||
|
- safe_to_manage (boolean): Whether or not this volume is safe to
|
||||||
|
manage according to the storage backend. For example, is the volume
|
||||||
|
in use or invalid for any reason.
|
||||||
|
- reason_not_safe (string): If safe_to_manage is False, the reason why.
|
||||||
|
- cinder_id (string): If already managed, provide the Cinder ID.
|
||||||
|
- extra_info (string): Any extra information to return to the user
|
||||||
|
|
||||||
|
:param cinder_volumes: A list of volumes in this host that Cinder
|
||||||
|
currently manages, used to determine if
|
||||||
|
a volume is manageable or not.
|
||||||
|
:param marker: The last item of the previous page; we return the
|
||||||
|
next results after this value (after sorting)
|
||||||
|
:param limit: Maximum number of items to return
|
||||||
|
:param offset: Number of items to skip after marker
|
||||||
|
:param sort_keys: List of keys to sort results by (valid keys are
|
||||||
|
'identifier' and 'size')
|
||||||
|
:param sort_dirs: List of directions to sort by, corresponding to
|
||||||
|
sort_keys (valid directions are 'asc' and 'desc')
|
||||||
|
"""
|
||||||
|
return []
|
||||||
|
|
||||||
@abc.abstractmethod
|
@abc.abstractmethod
|
||||||
def unmanage(self, volume):
|
def unmanage(self, volume):
|
||||||
"""Removes the specified volume from Cinder management.
|
"""Removes the specified volume from Cinder management.
|
||||||
@ -1871,6 +1902,40 @@ class ManageableSnapshotsVD(object):
|
|||||||
"""
|
"""
|
||||||
return
|
return
|
||||||
|
|
||||||
|
def get_manageable_snapshots(self, cinder_snapshots, marker, limit, offset,
|
||||||
|
sort_keys, sort_dirs):
|
||||||
|
"""List snapshots on the backend available for management by Cinder.
|
||||||
|
|
||||||
|
Returns a list of dictionaries, each specifying a snapshot in the host,
|
||||||
|
with the following keys:
|
||||||
|
- reference (dictionary): The reference for a snapshot, which can be
|
||||||
|
passed to "manage_existing_snapshot".
|
||||||
|
- size (int): The size of the snapshot according to the storage
|
||||||
|
backend, rounded up to the nearest GB.
|
||||||
|
- safe_to_manage (boolean): Whether or not this snapshot is safe to
|
||||||
|
manage according to the storage backend. For example, is the snapshot
|
||||||
|
in use or invalid for any reason.
|
||||||
|
- reason_not_safe (string): If safe_to_manage is False, the reason why.
|
||||||
|
- cinder_id (string): If already managed, provide the Cinder ID.
|
||||||
|
- extra_info (string): Any extra information to return to the user
|
||||||
|
- source_reference (string): Similar to "reference", but for the
|
||||||
|
snapshot's source volume.
|
||||||
|
|
||||||
|
:param cinder_snapshots: A list of snapshots in this host that Cinder
|
||||||
|
currently manages, used to determine if
|
||||||
|
a snapshot is manageable or not.
|
||||||
|
:param marker: The last item of the previous page; we return the
|
||||||
|
next results after this value (after sorting)
|
||||||
|
:param limit: Maximum number of items to return
|
||||||
|
:param offset: Number of items to skip after marker
|
||||||
|
:param sort_keys: List of keys to sort results by (valid keys are
|
||||||
|
'identifier' and 'size')
|
||||||
|
:param sort_dirs: List of directions to sort by, corresponding to
|
||||||
|
sort_keys (valid directions are 'asc' and 'desc')
|
||||||
|
|
||||||
|
"""
|
||||||
|
return []
|
||||||
|
|
||||||
# NOTE: Can't use abstractmethod before all drivers implement it
|
# NOTE: Can't use abstractmethod before all drivers implement it
|
||||||
def unmanage_snapshot(self, snapshot):
|
def unmanage_snapshot(self, snapshot):
|
||||||
"""Removes the specified snapshot from Cinder management.
|
"""Removes the specified snapshot from Cinder management.
|
||||||
@ -2025,6 +2090,11 @@ class VolumeDriver(ConsistencyGroupVD, TransferVD, ManageableVD, ExtendVD,
|
|||||||
msg = _("Manage existing volume not implemented.")
|
msg = _("Manage existing volume not implemented.")
|
||||||
raise NotImplementedError(msg)
|
raise NotImplementedError(msg)
|
||||||
|
|
||||||
|
def get_manageable_volumes(self, cinder_volumes, marker, limit, offset,
|
||||||
|
sort_keys, sort_dirs):
|
||||||
|
msg = _("Get manageable volumes not implemented.")
|
||||||
|
raise NotImplementedError(msg)
|
||||||
|
|
||||||
def unmanage(self, volume):
|
def unmanage(self, volume):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@ -2036,6 +2106,11 @@ class VolumeDriver(ConsistencyGroupVD, TransferVD, ManageableVD, ExtendVD,
|
|||||||
msg = _("Manage existing snapshot not implemented.")
|
msg = _("Manage existing snapshot not implemented.")
|
||||||
raise NotImplementedError(msg)
|
raise NotImplementedError(msg)
|
||||||
|
|
||||||
|
def get_manageable_snapshots(self, cinder_snapshots, marker, limit, offset,
|
||||||
|
sort_keys, sort_dirs):
|
||||||
|
msg = _("Get manageable snapshots not implemented.")
|
||||||
|
raise NotImplementedError(msg)
|
||||||
|
|
||||||
def unmanage_snapshot(self, snapshot):
|
def unmanage_snapshot(self, snapshot):
|
||||||
"""Unmanage the specified snapshot from Cinder management."""
|
"""Unmanage the specified snapshot from Cinder management."""
|
||||||
|
|
||||||
|
@ -36,6 +36,7 @@ intact.
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
import time
|
import time
|
||||||
|
|
||||||
@ -217,7 +218,7 @@ def locked_snapshot_operation(f):
|
|||||||
class VolumeManager(manager.SchedulerDependentManager):
|
class VolumeManager(manager.SchedulerDependentManager):
|
||||||
"""Manages attachable block storage devices."""
|
"""Manages attachable block storage devices."""
|
||||||
|
|
||||||
RPC_API_VERSION = '2.0'
|
RPC_API_VERSION = '2.1'
|
||||||
|
|
||||||
target = messaging.Target(version=RPC_API_VERSION)
|
target = messaging.Target(version=RPC_API_VERSION)
|
||||||
|
|
||||||
@ -2321,6 +2322,25 @@ class VolumeManager(manager.SchedulerDependentManager):
|
|||||||
resource=vol_ref)
|
resource=vol_ref)
|
||||||
return vol_ref['id']
|
return vol_ref['id']
|
||||||
|
|
||||||
|
def get_manageable_volumes(self, ctxt, marker, limit, offset, sort_keys,
|
||||||
|
sort_dirs):
|
||||||
|
try:
|
||||||
|
utils.require_driver_initialized(self.driver)
|
||||||
|
except exception.DriverNotInitialized:
|
||||||
|
with excutils.save_and_reraise_exception():
|
||||||
|
LOG.exception(_LE("Listing manageable volumes failed, due "
|
||||||
|
"to uninitialized driver."))
|
||||||
|
|
||||||
|
cinder_volumes = objects.VolumeList.get_all_by_host(ctxt, self.host)
|
||||||
|
try:
|
||||||
|
driver_entries = self.driver.get_manageable_volumes(
|
||||||
|
cinder_volumes, marker, limit, offset, sort_keys, sort_dirs)
|
||||||
|
except Exception:
|
||||||
|
with excutils.save_and_reraise_exception():
|
||||||
|
LOG.exception(_LE("Listing manageable volumes failed, due "
|
||||||
|
"to driver error."))
|
||||||
|
return driver_entries
|
||||||
|
|
||||||
def promote_replica(self, ctxt, volume_id):
|
def promote_replica(self, ctxt, volume_id):
|
||||||
"""Promote volume replica secondary to be the primary volume."""
|
"""Promote volume replica secondary to be the primary volume."""
|
||||||
volume = self.db.volume_get(ctxt, volume_id)
|
volume = self.db.volume_get(ctxt, volume_id)
|
||||||
@ -3411,6 +3431,25 @@ class VolumeManager(manager.SchedulerDependentManager):
|
|||||||
flow_engine.run()
|
flow_engine.run()
|
||||||
return snapshot.id
|
return snapshot.id
|
||||||
|
|
||||||
|
def get_manageable_snapshots(self, ctxt, marker, limit, offset,
|
||||||
|
sort_keys, sort_dirs):
|
||||||
|
try:
|
||||||
|
utils.require_driver_initialized(self.driver)
|
||||||
|
except exception.DriverNotInitialized:
|
||||||
|
with excutils.save_and_reraise_exception():
|
||||||
|
LOG.exception(_LE("Listing manageable snapshots failed, due "
|
||||||
|
"to uninitialized driver."))
|
||||||
|
|
||||||
|
cinder_snapshots = self.db.snapshot_get_by_host(ctxt, self.host)
|
||||||
|
try:
|
||||||
|
driver_entries = self.driver.get_manageable_snapshots(
|
||||||
|
cinder_snapshots, marker, limit, offset, sort_keys, sort_dirs)
|
||||||
|
except Exception:
|
||||||
|
with excutils.save_and_reraise_exception():
|
||||||
|
LOG.exception(_LE("Listing manageable snapshots failed, due "
|
||||||
|
"to driver error."))
|
||||||
|
return driver_entries
|
||||||
|
|
||||||
def get_capabilities(self, context, discover):
|
def get_capabilities(self, context, discover):
|
||||||
"""Get capabilities of backend storage."""
|
"""Get capabilities of backend storage."""
|
||||||
if discover:
|
if discover:
|
||||||
|
@ -99,9 +99,10 @@ class VolumeAPI(rpc.RPCAPI):
|
|||||||
the version_cap being set to 1.40.
|
the version_cap being set to 1.40.
|
||||||
|
|
||||||
2.0 - Remove 1.x compatibility
|
2.0 - Remove 1.x compatibility
|
||||||
|
2.1 - Add get_manageable_volumes() and get_manageable_snapshots().
|
||||||
"""
|
"""
|
||||||
|
|
||||||
RPC_API_VERSION = '2.0'
|
RPC_API_VERSION = '2.1'
|
||||||
TOPIC = CONF.volume_topic
|
TOPIC = CONF.volume_topic
|
||||||
BINARY = 'cinder-volume'
|
BINARY = 'cinder-volume'
|
||||||
|
|
||||||
@ -296,3 +297,17 @@ class VolumeAPI(rpc.RPCAPI):
|
|||||||
cctxt = self._get_cctxt(volume.host, '2.0')
|
cctxt = self._get_cctxt(volume.host, '2.0')
|
||||||
return cctxt.call(ctxt, 'secure_file_operations_enabled',
|
return cctxt.call(ctxt, 'secure_file_operations_enabled',
|
||||||
volume=volume)
|
volume=volume)
|
||||||
|
|
||||||
|
def get_manageable_volumes(self, ctxt, host, marker, limit, offset,
|
||||||
|
sort_keys, sort_dirs):
|
||||||
|
cctxt = self._get_cctxt(host, '2.1')
|
||||||
|
return cctxt.call(ctxt, 'get_manageable_volumes', marker=marker,
|
||||||
|
limit=limit, offset=offset, sort_keys=sort_keys,
|
||||||
|
sort_dirs=sort_dirs)
|
||||||
|
|
||||||
|
def get_manageable_snapshots(self, ctxt, host, marker, limit, offset,
|
||||||
|
sort_keys, sort_dirs):
|
||||||
|
cctxt = self._get_cctxt(host, '2.1')
|
||||||
|
return cctxt.call(ctxt, 'get_manageable_snapshots', marker=marker,
|
||||||
|
limit=limit, offset=offset, sort_keys=sort_keys,
|
||||||
|
sort_dirs=sort_dirs)
|
||||||
|
@ -67,6 +67,7 @@
|
|||||||
|
|
||||||
"volume_extension:volume_manage": "rule:admin_api",
|
"volume_extension:volume_manage": "rule:admin_api",
|
||||||
"volume_extension:volume_unmanage": "rule:admin_api",
|
"volume_extension:volume_unmanage": "rule:admin_api",
|
||||||
|
"volume_extension:list_manageable": "rule:admin_api",
|
||||||
|
|
||||||
"volume_extension:capabilities": "rule:admin_api",
|
"volume_extension:capabilities": "rule:admin_api",
|
||||||
|
|
||||||
@ -94,6 +95,7 @@
|
|||||||
"snapshot_extension:snapshot_actions:update_snapshot_status": "",
|
"snapshot_extension:snapshot_actions:update_snapshot_status": "",
|
||||||
"snapshot_extension:snapshot_manage": "rule:admin_api",
|
"snapshot_extension:snapshot_manage": "rule:admin_api",
|
||||||
"snapshot_extension:snapshot_unmanage": "rule:admin_api",
|
"snapshot_extension:snapshot_unmanage": "rule:admin_api",
|
||||||
|
"snapshot_extension:list_manageable": "rule:admin_api",
|
||||||
|
|
||||||
"consistencygroup:create" : "group:nobody",
|
"consistencygroup:create" : "group:nobody",
|
||||||
"consistencygroup:delete": "group:nobody",
|
"consistencygroup:delete": "group:nobody",
|
||||||
|
5
releasenotes/notes/list-manageable-86c77fc39c5b2cc9.yaml
Normal file
5
releasenotes/notes/list-manageable-86c77fc39c5b2cc9.yaml
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
features:
|
||||||
|
- Added the ability to list manageable volumes and snapshots via GET
|
||||||
|
operation on the /v2/<project_id>/os-volume-manage and
|
||||||
|
/v2/<project_id>/os-snapshot-manage URLs, respectively.
|
Loading…
Reference in New Issue
Block a user