List manageable volumes and snapshots

Cinder currently has the ability to take over the management of
existing volumes and snapshots ("manage existing") and to relinquish
management of volumes and snapshots ("unmanage"). The API to manage an
existing volume takes a reference, which is a driver-specific string
that is used to identify the volume on the storage backend.  This
patch adds APIs for listing volumes and snapshots available for
management to make this flow more user-friendly.

DocImpact
APIImpact
Change-Id: Iff19b5002e5bc037e28c91d104853f40eb4cb6ab
Implements: blueprint list-manage-existing
This commit is contained in:
Avishay Traeger 2016-02-26 08:22:18 +02:00
parent 0378d68a8d
commit 1574ccf2d2
14 changed files with 629 additions and 40 deletions

View 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

View File

@ -16,8 +16,10 @@ from oslo_config import cfg
from oslo_log import log as logging
from webob import exc
from cinder.api.contrib import resource_common_manage
from cinder.api import extensions
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 import exception
from cinder.i18n import _
@ -25,7 +27,10 @@ from cinder import volume as cinder_volume
LOG = logging.getLogger(__name__)
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):
@ -36,6 +41,7 @@ class SnapshotManageController(wsgi.Controller):
def __init__(self, *args, **kwargs):
super(SnapshotManageController, self).__init__(*args, **kwargs)
self.volume_api = cinder_volume.API()
self._list_manageable_view = list_manageable_view.ViewBuilder()
@wsgi.response(202)
def create(self, req, body):
@ -81,7 +87,7 @@ class SnapshotManageController(wsgi.Controller):
"""
context = req.environ['cinder.context']
authorize(context)
authorize_manage(context)
if not self.is_valid_body(body, 'snapshot'):
msg = _("Missing required element snapshot in request body.")
@ -130,6 +136,24 @@ class SnapshotManageController(wsgi.Controller):
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):
"""Allows existing backend storage to be 'managed' by Cinder."""
@ -141,4 +165,6 @@ class Snapshot_manage(extensions.ExtensionDescriptor):
def get_resources(self):
controller = SnapshotManageController()
return [extensions.ResourceExtension(Snapshot_manage.alias,
controller)]
controller,
collection_actions=
{'detail': 'GET'})]

View File

@ -16,9 +16,11 @@ from oslo_log import log as logging
from oslo_utils import uuidutils
from webob import exc
from cinder.api.contrib import resource_common_manage
from cinder.api import extensions
from cinder.api.openstack import wsgi
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.i18n import _
from cinder import utils
@ -26,7 +28,9 @@ from cinder import volume as cinder_volume
from cinder.volume import volume_types
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):
@ -37,6 +41,7 @@ class VolumeManageController(wsgi.Controller):
def __init__(self, *args, **kwargs):
super(VolumeManageController, self).__init__(*args, **kwargs)
self.volume_api = cinder_volume.API()
self._list_manageable_view = list_manageable_view.ViewBuilder()
@wsgi.response(202)
def create(self, req, body):
@ -93,7 +98,7 @@ class VolumeManageController(wsgi.Controller):
"""
context = req.environ['cinder.context']
authorize(context)
authorize_manage(context)
self.assert_valid_body(body, 'volume')
@ -145,6 +150,24 @@ class VolumeManageController(wsgi.Controller):
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):
"""Allows existing backend storage to be 'managed' by Cinder."""
@ -156,5 +179,7 @@ class Volume_manage(extensions.ExtensionDescriptor):
def get_resources(self):
controller = VolumeManageController()
res = extensions.ResourceExtension(Volume_manage.alias,
controller)
controller,
collection_actions=
{'detail': 'GET'})
return [res]

View 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}

View 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}

View File

@ -1,4 +1,5 @@
# Copyright (c) 2015 Huawei Technologies Co., Ltd.
# 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
@ -13,7 +14,12 @@
# under the License.
import mock
from oslo_config import cfg
from oslo_serialization import jsonutils
try:
from urllib import urlencode
except ImportError:
from urllib.parse import urlencode
import webob
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_service
CONF = cfg.CONF
def app():
# 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)
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)
class SnapshotManageTest(test.TestCase):
"""Test cases for cinder/api/contrib/snapshot_manage.py
@ -55,15 +85,22 @@ class SnapshotManageTest(test.TestCase):
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."""
req = webob.Request.blank('/v2/%s/os-snapshot-manage' %
fake.PROJECT_ID)
req.method = 'POST'
req.headers['Content-Type'] = 'application/json'
req.environ['cinder.context'] = context.RequestContext(fake.USER_ID,
fake.PROJECT_ID,
True)
req.environ['cinder.context'] = self._admin_ctxt
req.body = jsonutils.dump_as_bytes(body)
res = req.get_response(app())
return res
@ -80,13 +117,11 @@ class SnapshotManageTest(test.TestCase):
called with the correct arguments, and that we return the correct HTTP
code to the caller.
"""
ctxt = context.RequestContext(fake.USER_ID, fake.PROJECT_ID, True)
mock_db.return_value = fake_service.fake_service_obj(
ctxt,
self._admin_ctxt,
binary='cinder-volume')
body = {'snapshot': {'volume_id': fake.VOLUME_ID, 'ref': 'fake_ref'}}
res = self._get_resp(body)
res = self._get_resp_post(body)
self.assertEqual(202, res.status_int, res)
# 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):
"""Test correct failure when volume_id is not specified."""
body = {'snapshot': {'ref': 'fake_ref'}}
res = self._get_resp(body)
res = self._get_resp_post(body)
self.assertEqual(400, res.status_int)
def test_manage_snapshot_missing_ref(self):
"""Test correct failure when the ref is not specified."""
body = {'snapshot': {'volume_id': fake.VOLUME_ID}}
res = self._get_resp(body)
res = self._get_resp_post(body)
self.assertEqual(400, res.status_int)
def test_manage_snapshot_error_body(self):
"""Test correct failure when body is invaild."""
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)
def test_manage_snapshot_error_volume_id(self):
"""Test correct failure when volume can't be found."""
body = {'snapshot': {'volume_id': 'error_volume_id',
'ref': 'fake_ref'}}
res = self._get_resp(body)
res = self._get_resp_post(body)
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'])

View File

@ -1,4 +1,5 @@
# Copyright 2014 IBM Corp.
# 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
@ -13,7 +14,12 @@
# under the License.
import mock
from oslo_config import cfg
from oslo_serialization import jsonutils
try:
from urllib import urlencode
except ImportError:
from urllib.parse import urlencode
import webob
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_volume
CONF = cfg.CONF
def app():
# 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)
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',
db_service_get_by_host_and_topic)
@mock.patch('cinder.volume.volume_types.get_volume_type_by_name',
@ -122,15 +149,19 @@ class VolumeManageTest(test.TestCase):
def setUp(self):
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):
"""Helper to execute an os-volume-manage API call."""
def _get_resp_post(self, body):
"""Helper to execute a POST os-volume-manage API call."""
req = webob.Request.blank('/v2/%s/os-volume-manage' % fake.PROJECT_ID)
req.method = 'POST'
req.headers['Content-Type'] = 'application/json'
req.environ['cinder.context'] = context.RequestContext(fake.USER_ID,
fake.PROJECT_ID,
True)
req.environ['cinder.context'] = self._admin_ctxt
req.body = jsonutils.dump_as_bytes(body)
res = req.get_response(app())
return res
@ -148,7 +179,7 @@ class VolumeManageTest(test.TestCase):
"""
body = {'volume': {'host': 'host_ok',
'ref': 'fake_ref'}}
res = self._get_resp(body)
res = self._get_resp_post(body)
self.assertEqual(202, res.status_int, res)
# 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):
"""Test correct failure when host is not specified."""
body = {'volume': {'ref': 'fake_ref'}}
res = self._get_resp(body)
res = self._get_resp_post(body)
self.assertEqual(400, res.status_int)
def test_manage_volume_missing_ref(self):
"""Test correct failure when the ref is not specified."""
body = {'volume': {'host': 'host_ok'}}
res = self._get_resp(body)
res = self._get_resp_post(body)
self.assertEqual(400, res.status_int)
pass
@ -183,7 +214,7 @@ class VolumeManageTest(test.TestCase):
body = {'volume': {'host': 'host_ok',
'ref': 'fake_ref',
'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.assertTrue(mock_validate.called)
pass
@ -200,7 +231,7 @@ class VolumeManageTest(test.TestCase):
body = {'volume': {'host': 'host_ok',
'ref': 'fake_ref',
'volume_type': 'good_fakevt'}}
res = self._get_resp(body)
res = self._get_resp_post(body)
self.assertEqual(202, res.status_int, res)
self.assertTrue(mock_validate.called)
pass
@ -210,7 +241,7 @@ class VolumeManageTest(test.TestCase):
body = {'volume': {'host': 'host_ok',
'ref': 'fake_ref',
'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)
pass
@ -219,6 +250,73 @@ class VolumeManageTest(test.TestCase):
body = {'volume': {'host': 'host_ok',
'ref': 'fake_ref',
'volume_type': 'bad_fakevt'}}
res = self._get_resp(body)
res = self._get_resp_post(body)
self.assertEqual(404, res.status_int, res)
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'])

View File

@ -74,6 +74,7 @@
"volume_extension:services:update" : "rule:admin_api",
"volume_extension:volume_manage": "rule:admin_api",
"volume_extension:volume_unmanage": "rule:admin_api",
"volume_extension:list_manageable": "rule:admin_api",
"volume_extension:capabilities": "rule:admin_api",
"limits_extension:used_limits": "",
@ -81,6 +82,7 @@
"snapshot_extension:snapshot_actions:update_snapshot_status": "",
"snapshot_extension:snapshot_manage": "rule:admin_api",
"snapshot_extension:snapshot_unmanage": "rule:admin_api",
"snapshot_extension:list_manageable": "rule:admin_api",
"volume:create_transfer": "",
"volume:accept_transfer": "",

View File

@ -1562,14 +1562,7 @@ class API(base.Base):
LOG.info(_LI("Retype volume request issued successfully."),
resource=volume)
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
def _get_service_by_host(self, context, host):
elevated = context.elevated()
try:
svc_host = volume_utils.extract_host(host, 'backend')
@ -1586,6 +1579,18 @@ class API(base.Base):
'service.'))
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:
availability_zone = service.get('availability_zone')
@ -1620,6 +1625,14 @@ class API(base.Base):
resource=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,
name=None, description=None,
metadata=None):
@ -1647,6 +1660,14 @@ class API(base.Base):
ref, host)
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)
# to a services API because that's what they are
def failover_host(self,

View File

@ -1819,6 +1819,37 @@ class ManageableVD(object):
"""
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
def unmanage(self, volume):
"""Removes the specified volume from Cinder management.
@ -1871,6 +1902,40 @@ class ManageableSnapshotsVD(object):
"""
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
def unmanage_snapshot(self, snapshot):
"""Removes the specified snapshot from Cinder management.
@ -2025,6 +2090,11 @@ class VolumeDriver(ConsistencyGroupVD, TransferVD, ManageableVD, ExtendVD,
msg = _("Manage existing volume not implemented.")
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):
pass
@ -2036,6 +2106,11 @@ class VolumeDriver(ConsistencyGroupVD, TransferVD, ManageableVD, ExtendVD,
msg = _("Manage existing snapshot not implemented.")
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):
"""Unmanage the specified snapshot from Cinder management."""

View File

@ -36,6 +36,7 @@ intact.
"""
import requests
import time
@ -217,7 +218,7 @@ def locked_snapshot_operation(f):
class VolumeManager(manager.SchedulerDependentManager):
"""Manages attachable block storage devices."""
RPC_API_VERSION = '2.0'
RPC_API_VERSION = '2.1'
target = messaging.Target(version=RPC_API_VERSION)
@ -2323,6 +2324,25 @@ class VolumeManager(manager.SchedulerDependentManager):
resource=vol_ref)
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):
"""Promote volume replica secondary to be the primary volume."""
volume = self.db.volume_get(ctxt, volume_id)
@ -3413,6 +3433,25 @@ class VolumeManager(manager.SchedulerDependentManager):
flow_engine.run()
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):
"""Get capabilities of backend storage."""
if discover:

View File

@ -99,9 +99,10 @@ class VolumeAPI(rpc.RPCAPI):
the version_cap being set to 1.40.
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
BINARY = 'cinder-volume'
@ -296,3 +297,17 @@ class VolumeAPI(rpc.RPCAPI):
cctxt = self._get_cctxt(volume.host, '2.0')
return cctxt.call(ctxt, 'secure_file_operations_enabled',
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)

View File

@ -67,6 +67,7 @@
"volume_extension:volume_manage": "rule:admin_api",
"volume_extension:volume_unmanage": "rule:admin_api",
"volume_extension:list_manageable": "rule:admin_api",
"volume_extension:capabilities": "rule:admin_api",
@ -94,6 +95,7 @@
"snapshot_extension:snapshot_actions:update_snapshot_status": "",
"snapshot_extension:snapshot_manage": "rule:admin_api",
"snapshot_extension:snapshot_unmanage": "rule:admin_api",
"snapshot_extension:list_manageable": "rule:admin_api",
"consistencygroup:create" : "group:nobody",
"consistencygroup:delete": "group:nobody",

View 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.