--- /dev/null
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright IBM Corp. 2013 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 os
+import tempfile
+
+from oslo.config import cfg
+
+from cinder import context
+from cinder import db
+from cinder import exception
+from cinder.image import image_utils
+from cinder.openstack.common import importutils
+from cinder.openstack.common import log as logging
+from cinder import test
+from cinder import utils
+from cinder.volume import configuration as conf
+from cinder.volume.drivers.gpfs import GPFSDriver
+
+LOG = logging.getLogger(__name__)
+
+CONF = cfg.CONF
+
+
+class FakeImageService():
+ def update(self, context, image_id, path):
+ pass
+
+ def download(self, context, image_id, image_fd):
+ for b in range(256):
+ image_fd.write('some_image_data')
+ image_fd.close()
+
+
+class FakeQemuImgInfo(object):
+ def __init__(self):
+ self.file_format = None
+ self.backing_file = None
+
+
+class GPFSDriverTestCase(test.TestCase):
+ driver_name = "cinder.volume.drivers.gpfs.GPFSDriver"
+ context = context.get_admin_context()
+
+ def _execute_wrapper(self, cmd, *args, **kwargs):
+ try:
+ kwargs.pop('run_as_root')
+ except KeyError:
+ pass
+ return utils.execute(cmd, *args, **kwargs)
+
+ def setUp(self):
+ super(GPFSDriverTestCase, self).setUp()
+ self.volumes_path = tempfile.mkdtemp(prefix="gpfs_")
+ self.images_dir = '%s/images' % self.volumes_path
+
+ if not os.path.exists(self.volumes_path):
+ os.mkdir(self.volumes_path)
+ if not os.path.exists(self.images_dir):
+ os.mkdir(self.images_dir)
+ self.image_id = '70a599e0-31e7-49b7-b260-868f441e862b'
+ self.image_path = self.images_dir + "/" + self.image_id
+ utils.execute('truncate', '-s', 1024, self.image_path)
+
+ self.driver = GPFSDriver(configuration=conf.Configuration(None))
+ self.driver.set_execute(self._execute_wrapper)
+ self.flags(volume_driver=self.driver_name,
+ gpfs_mount_point_base=self.volumes_path)
+ self.volume = importutils.import_object(CONF.volume_manager)
+ self.volume.driver.set_execute(self._execute_wrapper)
+
+ self.stubs.Set(GPFSDriver, '_create_gpfs_snap',
+ self._fake_gpfs_snap)
+ self.stubs.Set(GPFSDriver, '_create_gpfs_copy',
+ self._fake_gpfs_copy)
+ self.stubs.Set(GPFSDriver, '_gpfs_redirect',
+ self._fake_gpfs_redirect)
+ self.stubs.Set(GPFSDriver, '_is_gpfs_parent_file',
+ self._fake_is_gpfs_parent)
+ self.stubs.Set(GPFSDriver, '_delete_gpfs_file',
+ self._fake_delete_gpfs_file)
+ self.stubs.Set(image_utils, 'qemu_img_info',
+ self._fake_qemu_qcow2_image_info)
+ self.stubs.Set(image_utils, 'convert_image',
+ self._fake_convert_image)
+
+ self.context = context.get_admin_context()
+
+ CONF.gpfs_images_dir = self.images_dir
+
+ def tearDown(self):
+ try:
+ os.remove(self.image_path)
+ os.rmdir(self.images_dir)
+ os.rmdir(self.volumes_path)
+ except OSError:
+ pass
+ super(GPFSDriverTestCase, self).tearDown()
+
+ def _create_volume(self, size=0, snapshot_id=None, image_id=None,
+ metadata=None):
+ """Create a volume object."""
+ vol = {}
+ vol['size'] = size
+ vol['snapshot_id'] = snapshot_id
+ vol['image_id'] = image_id
+ vol['user_id'] = 'fake'
+ vol['project_id'] = 'fake'
+ vol['availability_zone'] = CONF.storage_availability_zone
+ vol['status'] = "creating"
+ vol['attach_status'] = "detached"
+ vol['host'] = CONF.host
+ if metadata is not None:
+ vol['metadata'] = metadata
+ return db.volume_create(context.get_admin_context(), vol)
+
+ def test_create_delete_volume_full_backing_file(self):
+ """create and delete vol with full creation method"""
+ CONF.gpfs_sparse_volumes = False
+ vol = self._create_volume(size=1)
+ volume_id = vol['id']
+ self.assertTrue(os.path.exists(self.volumes_path))
+ self.volume.create_volume(self.context, volume_id)
+ path = self.volumes_path + '/' + vol['name']
+ self.assertTrue(os.path.exists(self.volumes_path))
+ self.assertTrue(os.path.exists(path))
+ self.volume.delete_volume(self.context, volume_id)
+ self.assertFalse(os.path.exists(path))
+ self.assertTrue(os.path.exists(self.volumes_path))
+
+ def test_create_delete_volume_sparse_backing_file(self):
+ """create and delete vol with default sparse creation method"""
+ CONF.gpfs_sparse_volumes = True
+ vol = self._create_volume(size=1)
+ volume_id = vol['id']
+ self.assertTrue(os.path.exists(self.volumes_path))
+ self.volume.create_volume(self.context, volume_id)
+ path = self.volumes_path + '/' + vol['name']
+ self.assertTrue(os.path.exists(self.volumes_path))
+ self.assertTrue(os.path.exists(path))
+ self.volume.delete_volume(self.context, volume_id)
+ self.assertFalse(os.path.exists(path))
+ self.assertTrue(os.path.exists(self.volumes_path))
+
+ def _create_snapshot(self, volume_id, size='0'):
+ """Create a snapshot object."""
+ snap = {}
+ snap['volume_size'] = size
+ snap['user_id'] = 'fake'
+ snap['project_id'] = 'fake'
+ snap['volume_id'] = volume_id
+ snap['status'] = "creating"
+ return db.snapshot_create(context.get_admin_context(), snap)
+
+ def test_create_delete_snapshot(self):
+ volume_src = self._create_volume()
+ self.volume.create_volume(self.context, volume_src['id'])
+ snapCount = len(db.snapshot_get_all_for_volume(self.context,
+ volume_src['id']))
+ self.assertTrue(snapCount == 0)
+ snapshot = self._create_snapshot(volume_src['id'])
+ snapshot_id = snapshot['id']
+ self.volume.create_snapshot(self.context, volume_src['id'],
+ snapshot_id)
+ self.assertTrue(os.path.exists(os.path.join(self.volumes_path,
+ snapshot['name'])))
+ snapCount = len(db.snapshot_get_all_for_volume(self.context,
+ volume_src['id']))
+ self.assertTrue(snapCount == 1)
+ self.volume.delete_volume(self.context, volume_src['id'])
+ self.volume.delete_snapshot(self.context, snapshot_id)
+ self.assertFalse(os.path.exists(os.path.join(self.volumes_path,
+ snapshot['name'])))
+ snapCount = len(db.snapshot_get_all_for_volume(self.context,
+ volume_src['id']))
+ self.assertTrue(snapCount == 0)
+
+ def test_create_volume_from_snapshot(self):
+ volume_src = self._create_volume()
+ self.volume.create_volume(self.context, volume_src['id'])
+ snapshot = self._create_snapshot(volume_src['id'])
+ snapshot_id = snapshot['id']
+ self.volume.create_snapshot(self.context, volume_src['id'],
+ snapshot_id)
+ self.assertTrue(os.path.exists(os.path.join(self.volumes_path,
+ snapshot['name'])))
+ volume_dst = self._create_volume(0, snapshot_id)
+ self.volume.create_volume(self.context, volume_dst['id'], snapshot_id)
+ self.assertEqual(volume_dst['id'], db.volume_get(
+ context.get_admin_context(),
+ volume_dst['id']).id)
+ self.assertEqual(snapshot_id, db.volume_get(
+ context.get_admin_context(),
+ volume_dst['id']).snapshot_id)
+ self.volume.delete_volume(self.context, volume_dst['id'])
+
+ self.volume.delete_volume(self.context, volume_src['id'])
+ self.volume.delete_snapshot(self.context, snapshot_id)
+
+ def test_create_cloned_volume(self):
+ volume_src = self._create_volume()
+ self.volume.create_volume(self.context, volume_src['id'])
+
+ volume_dst = self._create_volume()
+ volumepath = os.path.join(self.volumes_path, volume_dst['name'])
+ self.assertFalse(os.path.exists(volumepath))
+
+ self.driver.create_cloned_volume(volume_dst, volume_src)
+ self.assertEqual(volume_dst['id'], db.volume_get(
+ context.get_admin_context(),
+ volume_dst['id']).id)
+
+ self.assertTrue(os.path.exists(volumepath))
+
+ self.volume.delete_volume(self.context, volume_src['id'])
+ self.volume.delete_volume(self.context, volume_dst['id'])
+
+ def test_create_volume_from_snapshot_method(self):
+ volume_src = self._create_volume()
+ self.volume.create_volume(self.context, volume_src['id'])
+
+ snapshot = self._create_snapshot(volume_src['id'])
+ snapshot_id = snapshot['id']
+ self.volume.create_snapshot(self.context, volume_src['id'],
+ snapshot_id)
+ volume_dst = self._create_volume()
+ self.driver.create_volume_from_snapshot(volume_dst, snapshot)
+ self.assertEqual(volume_dst['id'], db.volume_get(
+ context.get_admin_context(),
+ volume_dst['id']).id)
+
+ volumepath = os.path.join(self.volumes_path, volume_dst['name'])
+ self.assertTrue(os.path.exists(volumepath))
+
+ self.volume.delete_volume(self.context, volume_dst['id'])
+ self.volume.delete_volume(self.context, volume_src['id'])
+ self.volume.delete_snapshot(self.context, snapshot_id)
+
+ def test_copy_image_to_volume_with_copy_on_write_mode(self):
+ """Test the function of copy_image_to_volume
+ focusing on the integretion of the image_util
+ using copy_on_write image sharing mode.
+ """
+
+ # specify image file format is raw
+ self.stubs.Set(image_utils, 'qemu_img_info',
+ self._fake_qemu_raw_image_info)
+
+ volume = self._create_volume()
+ volumepath = os.path.join(self.volumes_path, volume['name'])
+ CONF.gpfs_images_share_mode = 'copy_on_write'
+ self.driver.copy_image_to_volume(self.context,
+ volume,
+ FakeImageService(),
+ self.image_id)
+
+ self.assertTrue(os.path.exists(volumepath))
+ self.volume.delete_volume(self.context, volume['id'])
+ self.assertFalse(os.path.exists(volumepath))
+
+ def test_copy_image_to_volume_with_copy_mode(self):
+ """Test the function of copy_image_to_volume
+ focusing on the integretion of the image_util
+ using copy image sharing mode.
+ """
+
+ # specify image file format is raw
+ self.stubs.Set(image_utils, 'qemu_img_info',
+ self._fake_qemu_raw_image_info)
+
+ volume = self._create_volume()
+ volumepath = os.path.join(self.volumes_path, volume['name'])
+ CONF.gpfs_images_share_mode = 'copy'
+ self.driver.copy_image_to_volume(self.context,
+ volume,
+ FakeImageService(),
+ self.image_id)
+
+ self.assertTrue(os.path.exists(volumepath))
+ self.volume.delete_volume(self.context, volume['id'])
+
+ def test_copy_image_to_volume_with_non_gpfs_image_dir(self):
+ """Test the function of copy_image_to_volume
+ focusing on the integretion of the image_util
+ using a non gpfs glance images directory
+ """
+ # specify image file format is raw
+ self.stubs.Set(image_utils, 'qemu_img_info',
+ self._fake_qemu_raw_image_info)
+
+ for share_mode in ['copy_on_write', 'copy']:
+ volume = self._create_volume()
+ volumepath = os.path.join(self.volumes_path, volume['name'])
+ CONF.gpfs_images_share_mode = share_mode
+ CONF.gpfs_images_dir = None
+ self.driver.copy_image_to_volume(self.context,
+ volume,
+ FakeImageService(),
+ self.image_id)
+ self.assertTrue(os.path.exists(volumepath))
+ self.volume.delete_volume(self.context, volume['id'])
+
+ def test_copy_image_to_volume_with_illegal_image_format(self):
+ """Test the function of copy_image_to_volume
+ focusing on the integretion of the image_util
+ using an illegal image file format
+ """
+ # specify image file format is qcow2
+ self.stubs.Set(image_utils, 'qemu_img_info',
+ self._fake_qemu_qcow2_image_info)
+
+ volume = self._create_volume()
+ CONF.gpfs_images_share_mode = 'copy'
+ CONF.gpfs_images_dir = self.images_dir
+ self.assertRaises(exception.ImageUnacceptable,
+ self.driver.copy_image_to_volume,
+ self.context,
+ volume,
+ FakeImageService(),
+ self.image_id)
+
+ self.volume.delete_volume(self.context, volume['id'])
+
+ def test_get_volume_stats(self):
+ stats = self.driver.get_volume_stats()
+ self.assertEquals(stats['volume_backend_name'], 'GPFS')
+ self.assertEquals(stats['storage_protocol'], 'file')
+
+ def test_check_for_setup_error_ok(self):
+ self.stubs.Set(GPFSDriver, '_get_gpfs_state',
+ self._fake_gpfs_get_state_active)
+ self.stubs.Set(GPFSDriver, '_is_gpfs_path',
+ self._fake_is_gpfs_path)
+ self.driver.check_for_setup_error()
+
+ def test_check_for_setup_error_gpfs_not_active(self):
+ self.stubs.Set(GPFSDriver, '_get_gpfs_state',
+ self._fake_gpfs_get_state_not_active)
+ self.stubs.Set(GPFSDriver, '_is_gpfs_path',
+ self._fake_is_gpfs_path)
+ self.assertRaises(exception.VolumeBackendAPIException,
+ self.driver.check_for_setup_error)
+
+ def test_check_for_setup_error_not_gpfs_path(self):
+ self.stubs.Set(GPFSDriver, '_get_gpfs_state',
+ self._fake_gpfs_get_state_active)
+ self.stubs.Set(GPFSDriver, '_is_gpfs_path',
+ self._fake_is_not_gpfs_path)
+ self.assertRaises(exception.VolumeBackendAPIException,
+ self.driver.check_for_setup_error)
+
+ def _fake_gpfs_snap(self, src, dest=None, modebits='644'):
+ if dest is None:
+ dest = src
+ utils.execute('truncate', '-s', 0, dest)
+ utils.execute('chmod', '644', dest)
+
+ def _fake_gpfs_copy(self, src, dest):
+ utils.execute('truncate', '-s', 0, dest)
+ utils.execute('chmod', '666', dest)
+
+ def _fake_gpfs_redirect(self, src):
+ return True
+
+ def _fake_is_gpfs_parent(self, gpfs_file):
+ return False
+
+ def _fake_gpfs_get_state_active(self):
+ active_txt = ('mmgetstate::HEADER:version:reserved:reserved:'
+ 'nodeName:nodeNumber:state:quorum:nodesUp:totalNodes:'
+ 'remarks:cnfsState:\n'
+ 'mmgetstate::0:1:::hostname:1:active:1:1:'
+ '1:quorum node:(undefined):')
+ return active_txt
+
+ def _fake_gpfs_get_state_not_active(self):
+ inactive_txt = ('mmgetstate::HEADER:version:reserved:reserved:'
+ 'nodeName:nodeNumber:state:quorum:nodesUp:totalNodes:'
+ 'remarks:cnfsState:\n'
+ 'mmgetstate::0:1:::hostname:1:down:1:1:'
+ '1:quorum node:(undefined):')
+ return inactive_txt
+
+ def _fake_is_gpfs_path(self, path):
+ pass
+
+ def _fake_is_not_gpfs_path(self, path):
+ raise(exception.ProcessExecutionError('invalid gpfs path'))
+
+ def _fake_convert_image(self, source, dest, out_format):
+ utils.execute('cp', source, dest)
+
+ def _fake_qemu_qcow2_image_info(self, path):
+ data = FakeQemuImgInfo()
+ data.file_format = 'qcow2'
+ data.backing_file = None
+ data.virtual_size = 1024 * 1024 * 1024
+ return data
+
+ def _fake_qemu_raw_image_info(self, path):
+ data = FakeQemuImgInfo()
+ data.file_format = 'raw'
+ data.backing_file = None
+ data.virtual_size = 1024 * 1024 * 1024
+ return data
+
+ def _fake_delete_gpfs_file(self, fchild):
+ volume_path = fchild
+ vol_name = os.path.basename(fchild)
+ vol_id = vol_name.split('volume-').pop()
+ utils.execute('rm', '-f', volume_path)
+ utils.execute('rm', '-f', volume_path + '.snap')
+ all_snaps = db.snapshot_get_all_for_volume(self.context, vol_id)
+ for snap in all_snaps:
+ snap_path = self.volumes_path + '/' + snap['name']
+ utils.execute('rm', '-f', snap_path)
--- /dev/null
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright IBM Corp. 2013 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.
+
+"""
+GPFS Volume Driver.
+
+"""
+import math
+import os
+import re
+
+from oslo.config import cfg
+
+from cinder import exception
+from cinder.image import image_utils
+from cinder.openstack.common import fileutils
+from cinder.openstack.common import log as logging
+from cinder import units
+from cinder.volume import driver
+
+VERSION = 1.0
+LOG = logging.getLogger(__name__)
+
+gpfs_opts = [
+ cfg.StrOpt('gpfs_mount_point_base',
+ default=None,
+ help='Path to the directory on GPFS mount point where '
+ 'volumes are stored'),
+ cfg.StrOpt('gpfs_images_dir',
+ default=None,
+ help='Path to GPFS Glance repository as mounted on '
+ 'Nova nodes'),
+ cfg.StrOpt('gpfs_images_share_mode',
+ default=None,
+ help='Set this if Glance image repo is on GPFS as well '
+ 'so that the image bits can be transferred efficiently '
+ 'between Glance and Cinder. Valid values are copy or '
+ 'copy_on_write. copy performs a full copy of the image, '
+ 'copy_on_write efficiently shares unmodified blocks of '
+ 'the image.'),
+ cfg.IntOpt('gpfs_max_clone_depth',
+ default=0,
+ help='A lengthy chain of copy-on-write snapshots or clones '
+ 'could have impact on performance. This option limits '
+ 'the number of indirections required to reach a specific '
+ 'block. 0 indicates unlimited.'),
+ cfg.BoolOpt('gpfs_sparse_volumes',
+ default=True,
+ help=('Create volumes as sparse files which take no space. '
+ 'If set to False volume is created as regular file. '
+ 'In this case volume creation may take a significantly '
+ 'longer time.')),
+]
+CONF = cfg.CONF
+CONF.register_opts(gpfs_opts)
+
+
+class GPFSDriver(driver.VolumeDriver):
+
+ """Implements volume functions using GPFS primitives."""
+
+ def __init__(self, *args, **kwargs):
+ super(GPFSDriver, self).__init__(*args, **kwargs)
+ self.configuration.append_config_values(gpfs_opts)
+
+ def _get_gpfs_state(self):
+ (out, _) = self._execute('mmgetstate', '-Y', run_as_root=True)
+ return out
+
+ def _check_gpfs_state(self):
+ out = self._get_gpfs_state()
+ lines = out.splitlines()
+ state_token = lines[0].split(':').index('state')
+ gpfs_state = lines[1].split(':')[state_token]
+ if gpfs_state != 'active':
+ LOG.error(_('GPFS is not active. Detailed output: %s') % out)
+ exception_message = (_("GPFS is not running - state: %s") %
+ gpfs_state)
+ raise exception.VolumeBackendAPIException(data=exception_message)
+
+ def _is_gpfs_path(self, directory):
+ self._execute('mmlsattr', directory, run_as_root=True)
+
+ def _is_samefs(self, p1, p2):
+ if os.lstat(p1).st_dev == os.lstat(p2).st_dev:
+ return True
+ return False
+
+ def check_for_setup_error(self):
+ """Returns an error if prerequisites aren't met."""
+ self._check_gpfs_state()
+
+ if(self.configuration.gpfs_mount_point_base is None):
+ msg = _('Option gpfs_mount_point_base is not set correctly.')
+ LOG.error(msg)
+ raise exception.VolumeBackendAPIException(data=msg)
+
+ if(self.configuration.gpfs_images_share_mode and
+ self.configuration.gpfs_images_share_mode not in ['copy_on_write',
+ 'copy']):
+ msg = _('Option gpfs_images_share_mode is not set correctly.')
+ LOG.warn(msg)
+ raise exception.VolumeBackendAPIException(data=msg)
+
+ if(self.configuration.gpfs_images_share_mode and
+ self.configuration.gpfs_images_dir is None):
+ msg = _('Option gpfs_images_dir is not set correctly.')
+ LOG.warn(msg)
+ raise exception.VolumeBackendAPIException(data=msg)
+
+ if(self.configuration.gpfs_images_share_mode == 'copy_on_write' and
+ not self._is_samefs(self.configuration.gpfs_mount_point_base,
+ self.configuration.gpfs_images_dir)):
+ msg = (_('gpfs_images_share_mode is set to copy_on_write, but '
+ '%(vol)s and %(img)s belong to different file systems') %
+ {'vol': self.configuration.gpfs_mount_point_base,
+ 'img': self.configuration.gpfs_images_dir})
+ LOG.warn(msg)
+ raise exception.VolumeBackendAPIException(data=msg)
+
+ for directory in [self.configuration.gpfs_mount_point_base,
+ self.configuration.gpfs_images_dir]:
+ if directory is None:
+ continue
+
+ if not directory.startswith('/'):
+ msg = (_('%s must be an absolute path.') % directory)
+ LOG.error(msg)
+ raise exception.VolumeBackendAPIException(data=msg)
+
+ if not os.path.isdir(directory):
+ msg = (_('%s is not a directory.') % directory)
+ LOG.error(msg)
+ raise exception.VolumeBackendAPIException(data=msg)
+
+ try:
+ # check that configured directories are on GPFS
+ self._is_gpfs_path(directory)
+ except exception.ProcessExecutionError:
+ msg = (_('%s is not on GPFS. Perhaps GPFS not mounted.') %
+ directory)
+ LOG.error(msg)
+ raise exception.VolumeBackendAPIException(data=msg)
+
+ def _create_sparse_file(self, path, size):
+ """Creates file with 0 disk usage."""
+
+ sizestr = self._sizestr(size)
+ self._execute('truncate', '-s', sizestr, path, run_as_root=True)
+ self._execute('chmod', '666', path, run_as_root=True)
+
+ def _create_regular_file(self, path, size):
+ """Creates regular file of given size."""
+
+ block_size_mb = 1
+ block_count = size * units.GiB / (block_size_mb * units.MiB)
+
+ self._execute('dd', 'if=/dev/zero', 'of=%s' % path,
+ 'bs=%dM' % block_size_mb,
+ 'count=%d' % block_count,
+ run_as_root=True)
+ self._execute('chmod', '666', path, run_as_root=True)
+
+ def create_volume(self, volume):
+ """Creates a GPFS volume."""
+ volume_path = self.local_path(volume)
+ volume_size = volume['size']
+
+ if self.configuration.gpfs_sparse_volumes:
+ self._create_sparse_file(volume_path, volume_size)
+ else:
+ self._create_regular_file(volume_path, volume_size)
+
+ v_metadata = volume.get('volume_metadata')
+ fstype = None
+ fslabel = None
+ for item in v_metadata:
+ if item['key'] == 'fstype':
+ fstype = item['value']
+ elif item['key'] == 'fslabel':
+ fslabel = item['value']
+ if fstype:
+ self._mkfs(volume, fstype, fslabel)
+
+ def create_volume_from_snapshot(self, volume, snapshot):
+ """Creates a GPFS volume from a snapshot."""
+ volume_path = self.local_path(volume)
+ snapshot_path = self.local_path(snapshot)
+ self._create_gpfs_copy(src=snapshot_path, dest=volume_path)
+ self._gpfs_redirect(volume_path)
+ data = image_utils.qemu_img_info(volume_path)
+ return {'size': math.ceil(data.virtual_size / 1024.0 ** 3)}
+
+ def create_cloned_volume(self, volume, src_vref):
+ src = self.local_path(src_vref)
+ dest = self.local_path(volume)
+ self._create_gpfs_clone(src, dest)
+ data = image_utils.qemu_img_info(dest)
+ return {'size': math.ceil(data.virtual_size / 1024.0 ** 3)}
+
+ def _delete_gpfs_file(self, fchild):
+ if not os.path.exists(fchild):
+ return
+ (out, err) = self._execute('mmclone', 'show', fchild, run_as_root=True)
+ fparent = None
+ reInode = re.compile(
+ '.*\s+(?:yes|no)\s+\d+\s+(?P<inode>\d+)', re.M | re.S)
+ match = reInode.match(out)
+ if match:
+ inode = match.group('inode')
+ path = os.path.dirname(fchild)
+ (out, err) = self._execute('find', path, '-maxdepth', '1',
+ '-inum', inode, run_as_root=True)
+ if out:
+ fparent = out.split('\n', 1)[0]
+ self._execute(
+ 'rm', '-f', fchild, check_exit_code=False, run_as_root=True)
+
+ # There is no need to check for volume references on this snapshot
+ # because 'rm -f' itself serves as a simple and implicit check. If the
+ # parent is referenced by another volume, GPFS doesn't allow deleting
+ # it. 'rm -f' silently fails and the subsequent check on the path
+ # indicates whether there are any volumes derived from that snapshot.
+ # If there are such volumes, we quit recursion and let the other
+ # volumes delete the snapshot later. If there are no references, rm
+ # would succeed and the snapshot is deleted.
+ if not os.path.exists(fchild) and fparent:
+ fpbase = os.path.basename(fparent)
+ if (fpbase.startswith('snapshot-') or fpbase.endswith('.snap')):
+ self._delete_gpfs_file(fparent)
+
+ def delete_volume(self, volume):
+ """Deletes a logical volume."""
+ volume_path = self.local_path(volume)
+ self._delete_gpfs_file(volume_path)
+
+ def _gpfs_redirect(self, src):
+ """Removes the copy_on_write dependency between src and parent.
+
+ Remove the copy_on_write dependency between the src file and its
+ immediate parent such that the length of dependency chain is reduced
+ by 1.
+ """
+ max_depth = self.configuration.gpfs_max_clone_depth
+ if max_depth == 0:
+ return False
+ (out, err) = self._execute('mmclone', 'show', src, run_as_root=True)
+ reDepth = re.compile('.*\s+no\s+(?P<depth>\d+)', re.M | re.S)
+ match = reDepth.match(out)
+ if match:
+ depth = int(match.group('depth'))
+ if depth > max_depth:
+ self._execute('mmclone', 'redirect', src, run_as_root=True)
+ return True
+ return False
+
+ def _create_gpfs_clone(self, src, dest):
+ snap = dest + ".snap"
+ self._create_gpfs_snap(src, snap)
+ self._create_gpfs_copy(snap, dest)
+ if(self._gpfs_redirect(src) and self._gpfs_redirect(dest)):
+ self._execute('rm', '-f', snap, run_as_root=True)
+
+ def _create_gpfs_copy(self, src, dest, modebits='666'):
+ self._execute('mmclone', 'copy', src, dest, run_as_root=True)
+ self._execute('chmod', modebits, dest, run_as_root=True)
+
+ def _create_gpfs_snap(self, src, dest=None, modebits='644'):
+ if dest is None:
+ self._execute('mmclone', 'snap', src, run_as_root=True)
+ self._execute('chmod', modebits, src, run_as_root=True)
+ else:
+ self._execute('mmclone', 'snap', src, dest, run_as_root=True)
+ self._execute('chmod', modebits, dest, run_as_root=True)
+
+ def _is_gpfs_parent_file(self, gpfs_file):
+ out, _ = self._execute('mmclone', 'show', gpfs_file, run_as_root=True)
+ ptoken = out.splitlines().pop().split()[0]
+ return ptoken == 'yes'
+
+ def create_snapshot(self, snapshot):
+ """Creates a GPFS snapshot."""
+ snapshot_path = self.local_path(snapshot)
+ volume_path = os.path.join(self.configuration.gpfs_mount_point_base,
+ snapshot['volume_name'])
+ self._create_gpfs_snap(src=volume_path, dest=snapshot_path)
+
+ def delete_snapshot(self, snapshot):
+ """Deletes a GPFS snapshot."""
+ # A snapshot file is deleted as a part of delete_volume when
+ # all volumes derived from it are deleted.
+
+ def local_path(self, volume):
+ return os.path.join(self.configuration.gpfs_mount_point_base,
+ volume['name'])
+
+ def ensure_export(self, context, volume):
+ """Synchronously recreates an export for a logical volume."""
+ pass
+
+ def create_export(self, context, volume):
+ """Exports the volume."""
+ pass
+
+ def remove_export(self, context, volume):
+ """Removes an export for a logical volume."""
+ pass
+
+ def initialize_connection(self, volume, connector):
+ return {
+ 'driver_volume_type': 'local',
+ 'data': {
+ 'name': volume['name'],
+ 'device_path': self.local_path(volume),
+ }
+ }
+
+ def terminate_connection(self, volume, connector, **kwargs):
+ pass
+
+ def get_volume_stats(self, refresh=False):
+ """Get volume status.
+
+ If 'refresh' is True, or stats have never been updated, run update
+ the stats first.
+ """
+ if not self._stats or refresh:
+ self._update_volume_status()
+
+ return self._stats
+
+ def _update_volume_status(self):
+ """Retrieve status info from volume group."""
+
+ LOG.debug("Updating volume status")
+ data = {}
+ backend_name = self.configuration.safe_get('volume_backend_name')
+ data["volume_backend_name"] = backend_name or 'GPFS'
+ data["vendor_name"] = 'IBM'
+ data["driver_version"] = '1.0'
+ data["storage_protocol"] = 'file'
+ free, capacity = self._get_available_capacity(self.configuration.
+ gpfs_mount_point_base)
+ data['total_capacity_gb'] = math.ceil(capacity / 1024.0 ** 3)
+ data['free_capacity_gb'] = math.ceil(free / 1024.0 ** 3)
+ data['reserved_percentage'] = 0
+ data['QoS_support'] = False
+ self._stats = data
+
+ def _sizestr(self, size_in_g):
+ if int(size_in_g) == 0:
+ return '100M'
+ return '%sG' % size_in_g
+
+ def copy_image_to_volume(self, context, volume, image_service, image_id):
+ """Fetch the image from image_service and write it to the volume."""
+ return self._gpfs_fetch_to_raw(context, image_service, image_id,
+ self.local_path(volume))
+
+ def copy_volume_to_image(self, context, volume, image_service, image_meta):
+ """Copy the volume to the specified image."""
+ image_utils.upload_volume(context,
+ image_service,
+ image_meta,
+ self.local_path(volume))
+
+ def _mkfs(self, volume, fs, label=None):
+ if fs == 'swap':
+ cmd = ['mkswap']
+ else:
+ cmd = ['mkfs', '-t', fs]
+
+ if fs in ('ext3', 'ext4'):
+ cmd.append('-F')
+ if label:
+ if fs in ('msdos', 'vfat'):
+ label_opt = '-n'
+ else:
+ label_opt = '-L'
+ cmd.extend([label_opt, label])
+
+ path = self.local_path(volume)
+ cmd.append(path)
+ try:
+ self._execute(*cmd, run_as_root=True)
+ except exception.ProcessExecutionError as exc:
+ exception_message = (_("mkfs failed on volume %(vol)s, "
+ "error message was: %(err)s")
+ % {'vol': volume['name'], 'err': exc.stderr})
+ LOG.error(exception_message)
+ raise exception.VolumeBackendAPIException(
+ data=exception_message)
+
+ def _get_available_capacity(self, path):
+ """Calculate available space on path."""
+ out, _ = self._execute('df', '-P', '-B', '1', path,
+ run_as_root=True)
+ out = out.splitlines()[1]
+ size = int(out.split()[1])
+ available = int(out.split()[3])
+ return available, size
+
+ def _gpfs_fetch(self, context, image_service, image_id, path, _user_id,
+ _project_id):
+ if not (self.configuration.gpfs_images_share_mode and
+ self.configuration.gpfs_images_dir and
+ os.path.exists(
+ os.path.join(self.configuration.gpfs_images_dir,
+ image_id))):
+ with fileutils.remove_path_on_error(path):
+ with open(path, "wb") as image_file:
+ image_service.download(context, image_id, image_file)
+ else:
+ image_path = os.path.join(self.configuration.gpfs_images_dir,
+ image_id)
+ if self.configuration.gpfs_images_share_mode == 'copy_on_write':
+ # check if the image is a GPFS snap file
+ if not self._is_gpfs_parent_file(image_path):
+ self._create_gpfs_snap(image_path, modebits='666')
+ self._execute('ln', '-s', image_path, path, run_as_root=True)
+ else: # copy
+ self._execute('cp', image_path, path, run_as_root=True)
+ self._execute('chmod', '666', path, run_as_root=True)
+
+ def _gpfs_fetch_to_raw(self, context, image_service, image_id, dest,
+ user_id=None, project_id=None):
+ if (self.configuration.image_conversion_dir and not
+ os.path.exists(self.configuration.image_conversion_dir)):
+ os.makedirs(self.configuration.image_conversion_dir)
+
+ tmp = "%s.part" % dest
+ with fileutils.remove_path_on_error(tmp):
+ self._gpfs_fetch(context, image_service, image_id, tmp, user_id,
+ project_id)
+
+ data = image_utils.qemu_img_info(tmp)
+ fmt = data.file_format
+ if fmt is None:
+ msg = _("'qemu-img info' parsing failed.")
+ LOG.error(msg)
+ raise exception.ImageUnacceptable(
+ reason=msg,
+ image_id=image_id)
+
+ backing_file = data.backing_file
+ if backing_file is not None:
+ msg = (_("fmt = %(fmt)s backed by: %(backing_file)s") %
+ {'fmt': fmt, 'backing_file': backing_file})
+ LOG.error(msg)
+ raise exception.ImageUnacceptable(
+ image_id=image_id,
+ reason=msg)
+
+ LOG.debug("%s was %s, converting to raw" % (image_id, fmt))
+ image_utils.convert_image(tmp, dest, 'raw')
+
+ data = image_utils.qemu_img_info(dest)
+ if data.file_format != "raw":
+ msg = (_("Converted to raw, but format is now %s") %
+ data.file_format)
+ LOG.error(msg)
+ raise exception.ImageUnacceptable(
+ image_id=image_id,
+ reason=msg)
+ os.unlink(tmp)
+ return {'size': math.ceil(data.virtual_size / 1024.0 ** 3)}
# Driver to use for volume creation (string value)
#volume_driver=cinder.volume.drivers.lvm.LVMISCSIDriver
-# Total option count: 300
+#
+# Options defined in cinder.volume.drivers.gpfs
+#
+
+# Path to the directory on GPFS mount point where
+# volumes are stored (string value)
+# gpfs_mount_point_base=$state_path/mnt
+
+# Path to GPFS Glance repository as mounted on
+# Nova nodes (string value)
+# gpfs_images_dir=None
+
+# Set this if Glance image repo is on GPFS as well
+# so that the image bits can be transferred efficiently
+# between Glance and Cinder. Valid values are copy or
+# copy_on_write. copy performs a full copy of the image,
+# copy_on_write efficiently shares unmodified blocks of
+# the image. (string value)
+# gpfs_images_share_mode=None
+
+# A lengthy chain of copy-on-write snapshots or clones
+# could have impact on performance. This option limits
+# the number of indirections required to reach a specific
+# block. 0 indicates unlimited. (integer value)
+# gpfs_max_clone_depth=0
+
+# Create volumes as sparse files which take no space.
+# If set to False volume is created as regular file.
+# In this case volume creation may take a significantly
+# longer time. (boolean value)
+# gpfs_sparse_volumes=True
+
+# Total option count: 305
ls: CommandFilter, ls, root
tee: CommandFilter, tee, root
multipath: CommandFilter, multipath, root
+
+# cinder/volume/drivers/gpfs.py
+mmgetstate: CommandFilter, /usr/lpp/mmfs/bin/mmgetstate, root
+mmclone: CommandFilter, /usr/lpp/mmfs/bin/mmclone, root
+mmlsattr: CommandFilter, /usr/lpp/mmfs/bin/mmlsattr, root
+find: CommandFilter, find, root
+mkfs: CommandFilter, mkfs, root