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:
parent
0378d68a8d
commit
1574ccf2d2
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": "",
|
||||
|
@ -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,
|
||||
|
@ -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)
|
||||
|
||||
@ -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:
|
||||
|
@ -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