Merge "List manageable volumes and snapshots"

This commit is contained in:
Jenkins 2016-06-09 21:49:25 +00:00 committed by Gerrit Code Review
commit b4b8222445
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

@ -1506,14 +1506,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')
@ -1530,6 +1523,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')
@ -1564,6 +1569,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):
@ -1591,6 +1604,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)
@ -2321,6 +2322,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)
@ -3411,6 +3431,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.