--- /dev/null
+# Copyright 2014 StorPool
+# All Rights Reserved.
+#
+# 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.
+
+
+import re
+import sys
+
+import mock
+from oslo.utils import units
+
+
+fakeStorPool = mock.Mock()
+fakeStorPool.spopenstack = mock.Mock()
+fakeStorPool.spapi = mock.Mock()
+fakeStorPool.sptypes = mock.Mock()
+sys.modules['storpool'] = fakeStorPool
+
+
+from cinder import exception
+from cinder import test
+from cinder.volume import configuration as conf
+from cinder.volume.drivers import storpool as driver
+
+
+volume_types = {
+ 1: {},
+ 2: {'storpool_template': 'ssd'},
+ 3: {'storpool_template': 'hdd'}
+}
+volumes = {}
+snapshots = {}
+
+
+def MockExtraSpecs(vtype):
+ return volume_types[vtype]
+
+
+def mock_volume_types(f):
+ def _types_inner_inner1(inst, *args, **kwargs):
+ @mock.patch('cinder.volume.volume_types.get_volume_type_extra_specs',
+ new=MockExtraSpecs)
+ def _types_inner_inner2():
+ return f(inst, *args, **kwargs)
+
+ return _types_inner_inner2()
+
+ return _types_inner_inner1
+
+
+def volumeName(vid):
+ return 'os--volume--{id}'.format(id=vid)
+
+
+def snapshotName(vtype, vid):
+ return 'os--snap--{t}--{id}'.format(t=vtype, id=vid)
+
+
+class MockDisk(object):
+ def __init__(self, diskId):
+ self.id = diskId
+ self.generationLeft = -1
+ self.agCount = 13
+ self.agFree = 12
+ self.agAllocated = 1
+
+
+class MockTemplate(object):
+ def __init__(self, name):
+ self.name = name
+
+
+class MockApiError(Exception):
+ def __init__(self, msg):
+ super(MockApiError, self).__init__(msg)
+
+
+class MockAPI(object):
+ def __init__(self):
+ self._disks = {diskId: MockDisk(diskId) for diskId in (1, 2, 3, 4)}
+ self._disks[3].generationLeft = 42
+
+ self._templates = [MockTemplate(name) for name in ('ssd', 'hdd')]
+
+ def setlog(self, log):
+ self._log = log
+
+ def disksList(self):
+ return self._disks
+
+ def snapshotCreate(self, vname, snap):
+ snapshots[snap['name']] = dict(volumes[vname])
+
+ def snapshotDelete(self, name):
+ del snapshots[name]
+
+ def volumeCreate(self, v):
+ if v['name'] in volumes:
+ raise MockApiError('volume already exists')
+ volumes[v['name']] = v
+
+ def volumeDelete(self, name):
+ del volumes[name]
+
+ def volumeTemplatesList(self):
+ return self._templates
+
+ def volumesReassign(self, json):
+ pass
+
+ def volumeUpdate(self, name, size):
+ volumes[name]['size'] = size['size']
+
+
+class MockAttachDB(object):
+ def __init__(self, log):
+ self._api = MockAPI()
+
+ def api(self):
+ return self._api
+
+ def volumeName(self, vid):
+ return volumeName(vid)
+
+ def snapshotName(self, vtype, vid):
+ return snapshotName(vtype, vid)
+
+
+def MockVolumeUpdateDesc(size):
+ return {'size': size}
+
+
+fakeStorPool.spapi.ApiError = MockApiError
+fakeStorPool.spopenstack.AttachDB = MockAttachDB
+fakeStorPool.sptypes.VolumeUpdateDesc = MockVolumeUpdateDesc
+
+
+class StorPoolTestCase(test.TestCase):
+
+ def setUp(self):
+ super(StorPoolTestCase, self).setUp()
+
+ self.cfg = mock.Mock(spec=conf.Configuration)
+ self.cfg.volume_backend_name = 'storpool_test'
+ self.cfg.storpool_template = None
+ self.cfg.storpool_replication = 3
+
+ mock_exec = mock.Mock()
+ mock_exec.return_value = ('', '')
+
+ self.driver = driver.StorPoolDriver(execute=mock_exec,
+ configuration=self.cfg)
+
+ def test_initialized(self):
+ self.driver.check_for_setup_error()
+ self.driver.validate_connector(None)
+ self.driver.validate_connector(5)
+ c = self.driver.initialize_connection(None, None)
+ self.assertEqual('storpool', c['driver_volume_type'])
+ self.assertDictEqual({}, c['data'])
+ self.driver.terminate_connection(None, None)
+ self.driver.create_export(None, None)
+ self.driver.remove_export(None, None)
+
+ def test_stats(self):
+ stats = self.driver.get_volume_stats(refresh=True)
+ self.assertEqual('StorPool', stats['vendor_name'])
+ self.assertEqual('storpool', stats['storage_protocol'])
+ self.assertListEqual(['default', 'template_hdd', 'template_ssd'],
+ sorted([p['pool_name'] for p in stats['pools']]))
+ r = re.compile('^template_([A-Za-z0-9_]+)$')
+ for pool in stats['pools']:
+ self.assertEqual(19, pool['total_capacity_gb'])
+ self.assertEqual(5, pool['free_capacity_gb'])
+ if pool['pool_name'] != 'default':
+ m = r.match(pool['pool_name'])
+ self.assertIsNotNone(m)
+ self.assertIsNotNone(m.group(1))
+ self.assertEqual(m.group(1), pool['storpool_template'])
+
+ def assertVolumeNames(self, names):
+ self.assertListEqual(sorted([volumeName(n) for n in names]),
+ sorted(volumes.keys()))
+
+ @mock_volume_types
+ def test_create_delete_volume(self):
+ self.assertVolumeNames([])
+ self.assertDictEqual({}, volumes)
+ self.assertDictEqual({}, snapshots)
+
+ self.driver.create_volume({'id': '1', 'name': 'v1', 'size': 1,
+ 'volume_type': None})
+ self.assertListEqual([volumeName('1')], volumes.keys())
+ self.assertVolumeNames(('1',))
+ v = volumes[volumeName('1')]
+ self.assertEqual(1 * units.Gi, v['size'])
+ self.assertNotIn('template', v.keys())
+ self.assertEqual(3, v['replication'])
+
+ caught = False
+ try:
+ self.driver.create_volume({'id': '1', 'name': 'v1', 'size': 0,
+ 'volume_type': None})
+ except exception.VolumeBackendAPIException:
+ caught = True
+ self.assertEqual(True, caught)
+
+ self.driver.delete_volume({'id': '1'})
+ self.assertVolumeNames([])
+ self.assertDictEqual({}, volumes)
+
+ self.driver.create_volume({'id': '1', 'name': 'v1', 'size': 2,
+ 'volume_type': None})
+ self.assertVolumeNames(('1',))
+ v = volumes[volumeName('1')]
+ self.assertEqual(2 * units.Gi, v['size'])
+ self.assertNotIn('template', v.keys())
+ self.assertEqual(3, v['replication'])
+
+ self.driver.create_volume({'id': '2', 'name': 'v2', 'size': 3,
+ 'volume_type': {'id': 1}})
+ self.assertVolumeNames(('1', '2'))
+ v = volumes[volumeName('2')]
+ self.assertEqual(3 * units.Gi, v['size'])
+ self.assertNotIn('template', v.keys())
+ self.assertEqual(3, v['replication'])
+
+ self.driver.create_volume({'id': '3', 'name': 'v2', 'size': 4,
+ 'volume_type': {'id': 2}})
+ self.assertVolumeNames(('1', '2', '3'))
+ v = volumes[volumeName('3')]
+ self.assertEqual(4 * units.Gi, v['size'])
+ self.assertEqual('ssd', v['template'])
+ self.assertNotIn('replication', v.keys())
+
+ self.driver.create_volume({'id': '4', 'name': 'v2', 'size': 5,
+ 'volume_type': {'id': 3}})
+ self.assertVolumeNames(('1', '2', '3', '4'))
+ v = volumes[volumeName('4')]
+ self.assertEqual(5 * units.Gi, v['size'])
+ self.assertEqual('hdd', v['template'])
+ self.assertNotIn('replication', v.keys())
+
+ # Make sure the dictionary is not corrupted somehow...
+ v = volumes[volumeName('1')]
+ self.assertEqual(2 * units.Gi, v['size'])
+ self.assertNotIn('template', v.keys())
+ self.assertEqual(3, v['replication'])
+
+ for vid in ('1', '2', '3', '4'):
+ self.driver.delete_volume({'id': vid})
+ self.assertVolumeNames([])
+ self.assertDictEqual({}, volumes)
+ self.assertDictEqual({}, snapshots)
+
+ def test_clone_extend_volume(self):
+ self.assertVolumeNames([])
+ self.assertDictEqual({}, volumes)
+ self.assertDictEqual({}, snapshots)
+
+ self.driver.create_volume({'id': '1', 'name': 'v1', 'size': 1,
+ 'volume_type': None})
+ self.assertVolumeNames(('1',))
+ self.driver.extend_volume({'id': '1'}, 2)
+ self.assertEqual(2 * units.Gi, volumes[volumeName('1')]['size'])
+
+ self.driver.create_cloned_volume({'id': '2', 'name': 'clo', 'size': 3},
+ {'id': 1})
+ self.assertVolumeNames(('1', '2'))
+ self.assertDictEqual({}, snapshots)
+ # Note: this would not be true in a real environment (the snapshot will
+ # have been deleted, the volume would have no parent), but with this
+ # fake implementation it helps us make sure that the second volume was
+ # created with the proper options.
+ self.assertEqual(volumes[volumeName('2')]['parent'],
+ snapshotName('clone', '2'))
+
+ self.driver.delete_volume({'id': 1})
+ self.driver.delete_volume({'id': 2})
+
+ self.assertDictEqual({}, volumes)
+ self.assertDictEqual({}, snapshots)
+
+ @mock_volume_types
+ def test_config_replication(self):
+ self.assertVolumeNames([])
+ self.assertDictEqual({}, volumes)
+ self.assertDictEqual({}, snapshots)
+
+ save_repl = self.driver.configuration.storpool_replication
+
+ self.driver.configuration.storpool_replication = 3
+ stats = self.driver.get_volume_stats(refresh=True)
+ pool = stats['pools'][0]
+ self.assertEqual(19, pool['total_capacity_gb'])
+ self.assertEqual(5, pool['free_capacity_gb'])
+
+ self.driver.create_volume({'id': 'cfgrepl1', 'name': 'v1', 'size': 1,
+ 'volume_type': None})
+ self.assertVolumeNames(('cfgrepl1',))
+ v = volumes[volumeName('cfgrepl1')]
+ self.assertEqual(3, v['replication'])
+ self.assertNotIn('template', v)
+ self.driver.delete_volume({'id': 'cfgrepl1'})
+
+ self.driver.configuration.storpool_replication = 2
+ stats = self.driver.get_volume_stats(refresh=True)
+ pool = stats['pools'][0]
+ self.assertEqual(19, pool['total_capacity_gb'])
+ self.assertEqual(8, pool['free_capacity_gb'])
+
+ self.driver.create_volume({'id': 'cfgrepl2', 'name': 'v1', 'size': 1,
+ 'volume_type': None})
+ self.assertVolumeNames(('cfgrepl2',))
+ v = volumes[volumeName('cfgrepl2')]
+ self.assertEqual(2, v['replication'])
+ self.assertNotIn('template', v)
+ self.driver.delete_volume({'id': 'cfgrepl2'})
+
+ self.driver.create_volume({'id': 'cfgrepl3', 'name': 'v1', 'size': 1,
+ 'volume_type': {'id': 2}})
+ self.assertVolumeNames(('cfgrepl3',))
+ v = volumes[volumeName('cfgrepl3')]
+ self.assertNotIn('replication', v)
+ self.assertEqual('ssd', v['template'])
+ self.driver.delete_volume({'id': 'cfgrepl3'})
+
+ self.driver.configuration.storpool_replication = save_repl
+
+ self.assertVolumeNames([])
+ self.assertDictEqual({}, volumes)
+ self.assertDictEqual({}, snapshots)
+
+ @mock_volume_types
+ def test_config_template(self):
+ self.assertVolumeNames([])
+ self.assertDictEqual({}, volumes)
+ self.assertDictEqual({}, snapshots)
+
+ save_template = self.driver.configuration.storpool_template
+
+ self.driver.configuration.storpool_template = None
+
+ self.driver.create_volume({'id': 'cfgtempl1', 'name': 'v1', 'size': 1,
+ 'volume_type': None})
+ self.assertVolumeNames(('cfgtempl1',))
+ v = volumes[volumeName('cfgtempl1')]
+ self.assertEqual(3, v['replication'])
+ self.assertNotIn('template', v)
+ self.driver.delete_volume({'id': 'cfgtempl1'})
+
+ self.driver.create_volume({'id': 'cfgtempl2', 'name': 'v1', 'size': 1,
+ 'volume_type': {'id': 2}})
+ self.assertVolumeNames(('cfgtempl2',))
+ v = volumes[volumeName('cfgtempl2')]
+ self.assertNotIn('replication', v)
+ self.assertEqual('ssd', v['template'])
+ self.driver.delete_volume({'id': 'cfgtempl2'})
+
+ self.driver.configuration.storpool_template = 'hdd'
+
+ self.driver.create_volume({'id': 'cfgtempl3', 'name': 'v1', 'size': 1,
+ 'volume_type': None})
+ self.assertVolumeNames(('cfgtempl3',))
+ v = volumes[volumeName('cfgtempl3')]
+ self.assertNotIn('replication', v)
+ self.assertEqual('hdd', v['template'])
+ self.driver.delete_volume({'id': 'cfgtempl3'})
+
+ self.driver.create_volume({'id': 'cfgtempl4', 'name': 'v1', 'size': 1,
+ 'volume_type': {'id': 2}})
+ self.assertVolumeNames(('cfgtempl4',))
+ v = volumes[volumeName('cfgtempl4')]
+ self.assertNotIn('replication', v)
+ self.assertEqual('ssd', v['template'])
+ self.driver.delete_volume({'id': 'cfgtempl4'})
+
+ self.driver.configuration.storpool_template = save_template
+
+ self.assertVolumeNames([])
+ self.assertDictEqual({}, volumes)
+ self.assertDictEqual({}, snapshots)
--- /dev/null
+# Copyright (c) 2014 StorPool
+# All Rights Reserved.
+#
+# 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.
+
+"""StorPool block device driver"""
+
+from __future__ import absolute_import
+
+from oslo.config import cfg
+from oslo.utils import units
+import six
+
+from cinder import exception
+from cinder.i18n import _LE
+from cinder.openstack.common import log as logging
+from cinder.volume import driver
+from cinder.volume import volume_types
+
+LOG = logging.getLogger(__name__)
+
+from storpool import spapi
+from storpool import spopenstack
+from storpool import sptypes
+
+
+storpool_opts = [
+ cfg.StrOpt('storpool_template',
+ default=None,
+ help='The StorPool template for volumes with no type.'),
+ cfg.IntOpt('storpool_replication',
+ default=3,
+ help='The default StorPool chain replication value. '
+ 'Used when creating a volume with no specified type if '
+ 'storpool_template is not set. Also used for calculating '
+ 'the apparent free space reported in the stats.')
+]
+
+
+class StorPoolDriver(driver.VolumeDriver):
+ """The StorPool block device driver using the StorPool API"""
+
+ VERSION = '0.1.0'
+
+ def __init__(self, *args, **kwargs):
+ super(StorPoolDriver, self).__init__(*args, **kwargs)
+ self.configuration.append_config_values(storpool_opts)
+ self._sp_config = None
+ self._ourId = None
+ self._ourIdInt = None
+ self._attach = spopenstack.AttachDB(log=LOG)
+
+ def _backendException(self, e):
+ return exception.VolumeBackendAPIException(data=six.text_type(e))
+
+ def _template_from_volume_type(self, vtype):
+ specs = volume_types.get_volume_type_extra_specs(vtype['id'])
+ if specs is None:
+ return None
+ return specs.get('storpool_template', None)
+
+ def create_volume(self, volume):
+ size = int(volume['size']) * units.Gi
+ name = self._attach.volumeName(volume['id'])
+ if volume['volume_type'] is not None:
+ template = self._template_from_volume_type(volume['volume_type'])
+ else:
+ template = self.configuration.storpool_template
+ try:
+ if template is None:
+ self._attach.api().volumeCreate({
+ 'name': name,
+ 'size': size,
+ 'replication': self.configuration.storpool_replication
+ })
+ else:
+ self._attach.api().volumeCreate({
+ 'name': name,
+ 'size': size,
+ 'template': template
+ })
+ except spapi.ApiError as e:
+ raise self._backendException(e)
+
+ def validate_connector(self, connector):
+ pass
+
+ def initialize_connection(self, volume, connector):
+ return {'driver_volume_type': 'storpool', 'data': {}}
+
+ def terminate_connection(self, volume, connector, **kwargs):
+ pass
+
+ def create_snapshot(self, snapshot):
+ volname = self._attach.volumeName(snapshot['volume_id'])
+ name = self._attach.snapshotName('snap', snapshot['id'])
+ try:
+ self._attach.api().snapshotCreate(volname, {'name': name})
+ except spapi.ApiError as e:
+ raise self._backendException(e)
+
+ def create_volume_from_snapshot(self, volume, snapshot):
+ size = int(volume['size']) * units.Gi
+ volname = self._attach.volumeName(volume['id'])
+ name = self._attach.snapshotName('snap', snapshot['id'])
+ try:
+ self._attach.api().volumeCreate({
+ 'name': volname,
+ 'size': size,
+ 'parent': name
+ })
+ except spapi.ApiError as e:
+ raise self._backendException(e)
+
+ def create_cloned_volume(self, volume, src_vref):
+ refname = self._attach.volumeName(src_vref['id'])
+ snapname = self._attach.snapshotName('clone', volume['id'])
+ try:
+ self._attach.api().snapshotCreate(refname, {'name': snapname})
+ except spapi.ApiError as e:
+ raise self._backendException(e)
+
+ size = int(volume['size']) * units.Gi
+ volname = self._attach.volumeName(volume['id'])
+ try:
+ self._attach.api().volumeCreate({
+ 'name': volname,
+ 'size': size,
+ 'parent': snapname
+ })
+ except spapi.ApiError as e:
+ raise self._backendException(e)
+ finally:
+ try:
+ self._attach.api().snapshotDelete(snapname)
+ except spapi.ApiError as e:
+ # ARGH!
+ LOG.error(_LE("Could not delete the temp snapshot {n}: {msg}").
+ format(n=snapname, msg=six.text_type(e)))
+
+ def create_export(self, context, volume):
+ pass
+
+ def remove_export(self, context, volume):
+ pass
+
+ def delete_volume(self, volume):
+ name = self._attach.volumeName(volume['id'])
+ try:
+ self._attach.api().volumesReassign(
+ json=[{"volume": name, "detach": "all"}])
+ self._attach.api().volumeDelete(name)
+ except spapi.ApiError as e:
+ if e.name == 'objectDoesNotExist':
+ pass
+ else:
+ raise self._backendException(e)
+
+ def delete_snapshot(self, snapshot):
+ name = self._attach.snapshotName('snap', snapshot['id'])
+ try:
+ self._attach.api().volumesReassign(
+ json=[{"snapshot": name, "detach": "all"}])
+ self._attach.api().snapshotDelete(name)
+ except spapi.ApiError as e:
+ if e.name == 'objectDoesNotExist':
+ pass
+ else:
+ raise self._backendException(e)
+
+ def check_for_setup_error(self):
+ try:
+ self._attach.api()
+ except Exception as e:
+ LOG.error(_LE("StorPoolDriver API initialization failed: {e}").
+ format(e=e))
+ raise
+
+ def get_volume_stats(self, refresh=False):
+ if refresh:
+ self._update_volume_stats()
+
+ return self._stats
+
+ def _update_volume_stats(self):
+ try:
+ dl = self._attach.api().disksList()
+ templates = self._attach.api().volumeTemplatesList()
+ except spapi.ApiError as e:
+ raise self._backendException(e)
+ total = 0
+ used = 0
+ free = 0
+ agSize = 512 * units.Mi
+ for (id, desc) in dl.iteritems():
+ if desc.generationLeft != -1:
+ continue
+ total += desc.agCount * agSize
+ used += desc.agAllocated * agSize
+ free += desc.agFree * agSize * 4096 / (4096 + 128)
+
+ # Report the free space as if all new volumes will be created
+ # with StorPool replication 3; anything else is rare.
+ free /= self.configuration.storpool_replication
+
+ space = {
+ 'total_capacity_gb': total / units.Gi,
+ 'free_capacity_gb': free / units.Gi,
+ 'reserved_percentage': 0,
+ 'QoS_support': False,
+ }
+
+ pools = [dict(space, pool_name='default')]
+
+ pools += [dict(space,
+ pool_name='template_' + t.name,
+ storpool_template=t.name
+ ) for t in templates]
+
+ self._stats = {
+ 'volume_backend_name': self.configuration.safe_get(
+ 'volume_backend_name') or 'storpool',
+ 'vendor_name': 'StorPool',
+ 'driver_version': self.VERSION,
+ 'storage_protocol': 'storpool',
+
+ 'pools': pools
+ }
+
+ def _attach_volume(self, context, volume, properties, remote=False):
+ req_id = context.request_id
+ req = self._attach.get()[req_id]
+ name = req['volume']
+ self._attach.sync(req_id, None)
+ return {'device': {'path': '/dev/storpool/{v}'.format(v=name)}}
+
+ def _detach_volume(self, context, attach_info, volume, properties,
+ force=False, remote=False):
+ req_id = context.request_id
+ req = self._attach.get()[req_id]
+ name = req['volume']
+ return self._attach.sync(req_id, name)
+
+ def backup_volume(self, context, backup, backup_service):
+ volume = self.db.volume_get(context, backup['volume_id'])
+ req_id = context.request_id
+ volname = self._attach.volumeName(volume['id'])
+ name = self._attach.volsnapName(volume['id'], req_id)
+ try:
+ self._attach.api().snapshotCreate(volname, {'name': name})
+ except spapi.ApiError as e:
+ raise self._backendException(e)
+ self._attach.add(req_id, {
+ 'volume': name,
+ 'type': 'backup',
+ 'id': req_id,
+ 'rights': 1,
+ 'volsnap': True
+ })
+ try:
+ return super(StorPoolDriver, self).backup_volume(
+ context, backup, backup_service)
+ finally:
+ self._attach.remove(req_id)
+ try:
+ self._attach.api().snapshotDelete(name)
+ except spapi.ApiError as e:
+ LOG.error(
+ _LE('Could not remove the temp snapshot {n} for {v}: {e}').
+ format(n=name, v=volname, e=six.text_type(e)))
+ pass
+
+ def copy_volume_to_image(self, context, volume, image_service, image_meta):
+ req_id = context.request_id
+ volname = self._attach.volumeName(volume['id'])
+ name = self._attach.volsnapName(volume['id'], req_id)
+ try:
+ self._attach.api().snapshotCreate(volname, {'name': name})
+ except spapi.ApiError as e:
+ raise self._backendException(e)
+ self._attach.add(req_id, {
+ 'volume': name,
+ 'type': 'copy-from',
+ 'id': req_id,
+ 'rights': 1,
+ 'volsnap': True
+ })
+ try:
+ return super(StorPoolDriver, self).copy_volume_to_image(
+ context, volume, image_service, image_meta)
+ finally:
+ self._attach.remove(req_id)
+ try:
+ self._attach.api().snapshotDelete(name)
+ except spapi.ApiError as e:
+ LOG.error(
+ _LE('Could not remove the temp snapshot {n} for {v}: {e}').
+ format(n=name, v=volname, e=six.text_type(e)))
+ pass
+
+ def copy_image_to_volume(self, context, volume, image_service, image_id):
+ req_id = context.request_id
+ name = self._attach.volumeName(volume['id'])
+ self._attach.add(req_id, {
+ 'volume': name,
+ 'type': 'copy-to',
+ 'id': req_id,
+ 'rights': 2
+ })
+ try:
+ return super(StorPoolDriver, self).copy_image_to_volume(
+ context, volume, image_service, image_id)
+ finally:
+ self._attach.remove(req_id)
+
+ def extend_volume(self, volume, new_size):
+ size = int(new_size) * units.Gi
+ name = self._attach.volumeName(volume['id'])
+ try:
+ upd = sptypes.VolumeUpdateDesc(size=size)
+ self._attach.api().volumeUpdate(name, upd)
+ except spapi.ApiError as e:
+ raise self._backendException(e)