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 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'})]
|
||||
|
@ -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]
|
||||
|
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) 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'])
|
||||
|
@ -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'])
|
||||
|
@ -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": "",
|
||||
|
@ -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,
|
||||
|
@ -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."""
|
||||
|
||||
|
@ -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:
|
||||
|
@ -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)
|
||||
|
@ -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",
|
||||
|
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