]> review.fuel-infra Code Review - openstack-build/cinder-build.git/commitdiff
Create RemoteFSSnapDriver class
authorEric Harney <eharney@redhat.com>
Wed, 9 Jul 2014 20:22:05 +0000 (16:22 -0400)
committerEric Harney <eharney@redhat.com>
Mon, 18 Aug 2014 14:59:19 +0000 (10:59 -0400)
Move RemoteFSDriver into its own file.

Refactor pieces needed for qcow2 snapshots into a class.

Also add self.SHARE_FORMAT_REGEX.

Implements bp: remotefs-snaps

Change-Id: I8f9eaed125f4b916b835a63b04239058e34492a9

cinder/exception.py
cinder/tests/test_nfs.py
cinder/volume/drivers/glusterfs.py
cinder/volume/drivers/ibm/ibmnas.py
cinder/volume/drivers/nfs.py
cinder/volume/drivers/remotefs.py [new file with mode: 0644]
etc/cinder/cinder.conf.sample

index b72d9d08bd0195b443fb8f511a0ac7a24155e7c2..474b202fb00bb9e6ac760678203f77392a2fe8cd 100644 (file)
@@ -460,6 +460,10 @@ class ExportFailure(Invalid):
     message = _("Failed to export for volume: %(reason)s")
 
 
+class RemoveExportException(VolumeDriverException):
+    message = _("Failed to remove export for volume %(volume)s: %(reason)s")
+
+
 class MetadataCreateFailure(Invalid):
     message = _("Failed to create metadata for volume: %(reason)s")
 
@@ -665,36 +669,45 @@ class Invalid3PARDomain(VolumeDriverException):
     message = _("Invalid 3PAR Domain: %(err)s")
 
 
+# RemoteFS drivers
+class RemoteFSException(VolumeDriverException):
+    message = _("Unknown RemoteFS exception")
+
+
+class RemoteFSNoSharesMounted(RemoteFSException):
+    message = _("No mounted shares found")
+
+
+class RemoteFSNoSuitableShareFound(RemoteFSException):
+    message = _("There is no share which can host %(volume_size)sG")
+
+
 # NFS driver
-class NfsException(VolumeDriverException):
+class NfsException(RemoteFSException):
     message = _("Unknown NFS exception")
 
 
-class NfsNoSharesMounted(VolumeDriverException):
+class NfsNoSharesMounted(RemoteFSNoSharesMounted):
     message = _("No mounted NFS shares found")
 
 
-class NfsNoSuitableShareFound(VolumeDriverException):
+class NfsNoSuitableShareFound(RemoteFSNoSuitableShareFound):
     message = _("There is no share which can host %(volume_size)sG")
 
 
 # Gluster driver
-class GlusterfsException(VolumeDriverException):
+class GlusterfsException(RemoteFSException):
     message = _("Unknown Gluster exception")
 
 
-class GlusterfsNoSharesMounted(VolumeDriverException):
+class GlusterfsNoSharesMounted(RemoteFSNoSharesMounted):
     message = _("No mounted Gluster shares found")
 
 
-class GlusterfsNoSuitableShareFound(VolumeDriverException):
+class GlusterfsNoSuitableShareFound(RemoteFSNoSuitableShareFound):
     message = _("There is no share which can host %(volume_size)sG")
 
 
-class RemoveExportException(VolumeDriverException):
-    message = _("Failed to remove export for volume %(volume)s: %(reason)s")
-
-
 # HP MSA
 class HPMSAVolumeDriverException(VolumeDriverException):
     message = _("HP MSA Volume Driver exception")
index d9c9961863652ed91a0d964c43312511e1de424e..d9f9ad8b59b5d112a2c1c2a141a6a5b65c727b56 100644 (file)
@@ -32,6 +32,7 @@ from cinder.openstack.common import units
 from cinder import test
 from cinder.volume import configuration as conf
 from cinder.volume.drivers import nfs
+from cinder.volume.drivers import remotefs
 
 
 class DumbVolume(object):
@@ -49,7 +50,7 @@ class RemoteFsDriverTestCase(test.TestCase):
 
     def setUp(self):
         super(RemoteFsDriverTestCase, self).setUp()
-        self._driver = nfs.RemoteFsDriver()
+        self._driver = remotefs.RemoteFSDriver()
         self._mox = mox_lib.Mox()
         self.addCleanup(self._mox.UnsetStubs)
 
index 0345d0546906752fe84d186daee5f0145ec686ef..d633f956dd12b41923db8a4f1d9c5b4a9cd64bb5 100644 (file)
 #    under the License.
 
 import errno
-import hashlib
-import json
 import os
 import stat
-import tempfile
 import time
 
 from oslo.config import cfg
 
-from cinder.brick.remotefs import remotefs
+from cinder.brick.remotefs import remotefs as remotefs_brick
 from cinder import compute
 from cinder import db
 from cinder import exception
@@ -34,7 +31,7 @@ from cinder.openstack.common import log as logging
 from cinder.openstack.common import processutils
 from cinder.openstack.common import units
 from cinder import utils
-from cinder.volume.drivers import nfs
+from cinder.volume.drivers import remotefs as remotefs_drv
 
 LOG = logging.getLogger(__name__)
 
@@ -60,7 +57,7 @@ CONF.register_opts(volume_opts)
 CONF.import_opt('volume_name_template', 'cinder.db')
 
 
-class GlusterfsDriver(nfs.RemoteFsDriver):
+class GlusterfsDriver(remotefs_drv.RemoteFSSnapDriver):
     """Gluster based cinder driver. Creates file on Gluster share for using it
     as block device on hypervisor.
 
@@ -72,7 +69,7 @@ class GlusterfsDriver(nfs.RemoteFsDriver):
     driver_volume_type = 'glusterfs'
     driver_prefix = 'glusterfs'
     volume_backend_name = 'GlusterFS'
-    VERSION = '1.1.1'
+    VERSION = '1.2.0'
 
     def __init__(self, execute=processutils.execute, *args, **kwargs):
         self._remotefsclient = None
@@ -82,7 +79,7 @@ class GlusterfsDriver(nfs.RemoteFsDriver):
         self.base = getattr(self.configuration,
                             'glusterfs_mount_point_base',
                             CONF.glusterfs_mount_point_base)
-        self._remotefsclient = remotefs.RemoteFsClient(
+        self._remotefsclient = remotefs_brick.RemoteFsClient(
             'glusterfs',
             execute,
             glusterfs_mount_point_base=self.base)
@@ -166,30 +163,6 @@ class GlusterfsDriver(nfs.RemoteFsDriver):
                           hashed)
         return path
 
-    def _local_path_volume(self, volume):
-        path_to_disk = '%s/%s' % (
-            self._local_volume_dir(volume),
-            volume['name'])
-
-        return path_to_disk
-
-    def _local_path_volume_info(self, volume):
-        return '%s%s' % (self._local_path_volume(volume), '.info')
-
-    def _qemu_img_info(self, path):
-        """Sanitize image_utils' qemu_img_info.
-
-        This code expects to deal only with relative filenames.
-        """
-
-        info = image_utils.qemu_img_info(path)
-        if info.image:
-            info.image = os.path.basename(info.image)
-        if info.backing_file:
-            info.backing_file = os.path.basename(info.backing_file)
-
-        return info
-
     def get_active_image_from_info(self, volume):
         """Returns filename of the active image from the info file."""
 
@@ -571,32 +544,6 @@ class GlusterfsDriver(nfs.RemoteFsDriver):
         snap_info[snapshot['id']] = os.path.basename(new_snap_path)
         self._write_info_file(info_path, snap_info)
 
-    def _read_file(self, filename):
-        """This method is to make it easier to stub out code for testing.
-
-        Returns a string representing the contents of the file.
-        """
-
-        with open(filename, 'r') as f:
-            return f.read()
-
-    def _read_info_file(self, info_path, empty_if_missing=False):
-        """Return dict of snapshot information."""
-
-        if not os.path.exists(info_path):
-            if empty_if_missing is True:
-                return {}
-
-        return json.loads(self._read_file(info_path))
-
-    def _write_info_file(self, info_path, snap_info):
-        if 'active' not in snap_info.keys():
-            msg = _("'active' must be present when writing snap_info.")
-            raise exception.GlusterfsException(msg)
-
-        with open(info_path, 'w') as f:
-            json.dump(snap_info, f, indent=1, sort_keys=True)
-
     def _get_matching_backing_file(self, backing_chain, snapshot_file):
         return next(f for f in backing_chain
                     if f.get('backing-filename', '') == snapshot_file)
@@ -909,45 +856,6 @@ class GlusterfsDriver(nfs.RemoteFsDriver):
             del(snap_info[snapshot['id']])
             self._write_info_file(info_path, snap_info)
 
-    def _get_backing_chain_for_path(self, volume, path):
-        """Returns list of dicts containing backing-chain information.
-
-        Includes 'filename', and 'backing-filename' for each
-        applicable entry.
-
-        Consider converting this to use --backing-chain and --output=json
-        when environment supports qemu-img 1.5.0.
-
-        :param volume: volume reference
-        :param path: path to image file at top of chain
-
-        """
-
-        output = []
-
-        info = self._qemu_img_info(path)
-        new_info = {}
-        new_info['filename'] = os.path.basename(path)
-        new_info['backing-filename'] = info.backing_file
-
-        output.append(new_info)
-
-        while new_info['backing-filename']:
-            filename = new_info['backing-filename']
-            path = os.path.join(self._local_volume_dir(volume), filename)
-            info = self._qemu_img_info(path)
-            backing_filename = info.backing_file
-            new_info = {}
-            new_info['filename'] = filename
-            new_info['backing-filename'] = backing_filename
-
-            output.append(new_info)
-
-        return output
-
-    def _qemu_img_commit(self, path):
-        return self._execute('qemu-img', 'commit', path, run_as_root=True)
-
     def ensure_export(self, ctx, volume):
         """Synchronously recreates an export for a logical volume."""
 
@@ -955,7 +863,6 @@ class GlusterfsDriver(nfs.RemoteFsDriver):
 
     def create_export(self, ctx, volume):
         """Exports the volume."""
-
         pass
 
     def remove_export(self, ctx, volume):
@@ -1105,26 +1012,6 @@ class GlusterfsDriver(nfs.RemoteFsDriver):
 
         LOG.debug('Available shares: %s' % self._mounted_shares)
 
-    def _ensure_share_writable(self, path):
-        """Ensure that the Cinder user can write to the share.
-
-        If not, raise an exception.
-
-        :param path: path to test
-        :raises: GlusterfsException
-        :returns: None
-        """
-
-        prefix = '.cinder-write-test-' + str(os.getpid()) + '-'
-
-        try:
-            tempfile.NamedTemporaryFile(prefix=prefix, dir=path)
-        except OSError:
-            msg = _('GlusterFS share at %(dir)s is not writable by the '
-                    'Cinder volume service. Snapshot operations will not be '
-                    'supported.') % {'dir': path}
-            raise exception.GlusterfsException(msg)
-
     def _ensure_share_mounted(self, glusterfs_share):
         """Mount GlusterFS share.
         :param glusterfs_share: string
@@ -1170,39 +1057,9 @@ class GlusterfsDriver(nfs.RemoteFsDriver):
                 volume_size=volume_size_for)
         return greatest_share
 
-    def _get_hash_str(self, base_str):
-        """Return a string that represents hash of base_str
-        (in a hex format).
-        """
-        return hashlib.md5(base_str).hexdigest()
-
-    def _get_mount_point_for_share(self, glusterfs_share):
-        """Return mount point for share.
-        :param glusterfs_share: example 172.18.194.100:/var/glusterfs
-        """
-        return self._remotefsclient.get_mount_point(glusterfs_share)
-
-    def _get_available_capacity(self, glusterfs_share):
-        """Calculate available space on the GlusterFS share.
-        :param glusterfs_share: example 172.18.194.100:/var/glusterfs
-        """
-        mount_point = self._get_mount_point_for_share(glusterfs_share)
-
-        out, _ = self._execute('df', '--portability', '--block-size', '1',
-                               mount_point, run_as_root=True)
-        out = out.splitlines()[1]
-
-        size = int(out.split()[1])
-        available = int(out.split()[3])
-
-        return available, size
-
-    def _get_capacity_info(self, glusterfs_share):
-        available, size = self._get_available_capacity(glusterfs_share)
-        return size, available, size - available
-
     def _mount_glusterfs(self, glusterfs_share, mount_path, ensure=False):
         """Mount GlusterFS share to mount path."""
+        # TODO(eharney): make this fs-agnostic and factor into remotefs
         self._execute('mkdir', '-p', mount_path)
 
         command = ['mount', '-t', 'glusterfs', glusterfs_share,
@@ -1212,9 +1069,6 @@ class GlusterfsDriver(nfs.RemoteFsDriver):
 
         self._do_mount(command, ensure, glusterfs_share)
 
-    def _get_mount_point_base(self):
-        return self.base
-
     def backup_volume(self, context, backup, backup_service):
         """Create a new backup from an existing volume.
 
index d1313ee0f9c5369c4835ddb7f550342dc283d559..6647ea2b017caae6c6bdb298a85459838aee7ad7 100644 (file)
@@ -40,7 +40,7 @@ from cinder.openstack.common import processutils
 from cinder.openstack.common import units
 from cinder import utils
 from cinder.volume.drivers import nfs
-from cinder.volume.drivers.nfs import nas_opts
+from cinder.volume.drivers.remotefs import nas_opts
 from cinder.volume.drivers.san import san
 
 VERSION = '1.0.0'
index 9c519aede0550a8a3d4e1b9f3528dbe416f4e587..d42ed83dca34f103db23b16dffe20a150574f971 100644 (file)
 
 import errno
 import os
-import re
 
 from oslo.config import cfg
 
-from cinder.brick.remotefs import remotefs
+from cinder.brick.remotefs import remotefs as remotefs_brick
 from cinder import exception
 from cinder.image import image_utils
 from cinder.openstack.common.gettextutils import _
@@ -27,7 +26,7 @@ from cinder.openstack.common import log as logging
 from cinder.openstack.common import processutils as putils
 from cinder.openstack.common import units
 from cinder import utils
-from cinder.volume import driver
+from cinder.volume.drivers import remotefs
 
 VERSION = '1.1.0'
 
@@ -61,338 +60,11 @@ volume_opts = [
                      'of the nfs man page for details.')),
 ]
 
-nas_opts = [
-    cfg.StrOpt('nas_ip',
-               default='',
-               help='IP address or Hostname of NAS system.'),
-    cfg.StrOpt('nas_login',
-               default='admin',
-               help='User name to connect to NAS system.'),
-    cfg.StrOpt('nas_password',
-               default='',
-               help='Password to connect to NAS system.',
-               secret=True),
-    cfg.IntOpt('nas_ssh_port',
-               default=22,
-               help='SSH port to use to connect to NAS system.'),
-    cfg.StrOpt('nas_private_key',
-               default='',
-               help='Filename of private key to use for SSH authentication.'),
-]
-
 CONF = cfg.CONF
 CONF.register_opts(volume_opts)
-CONF.register_opts(nas_opts)
-
-
-class RemoteFsDriver(driver.VolumeDriver):
-    """Common base for drivers that work like NFS."""
-
-    VERSION = "0.0.0"
-
-    def __init__(self, *args, **kwargs):
-        super(RemoteFsDriver, self).__init__(*args, **kwargs)
-        self.shares = {}
-        self._mounted_shares = []
-
-    def check_for_setup_error(self):
-        """Just to override parent behavior."""
-        pass
-
-    def initialize_connection(self, volume, connector):
-        """Allow connection to connector and return connection info.
-
-        :param volume: volume reference
-        :param connector: connector reference
-        """
-        data = {'export': volume['provider_location'],
-                'name': volume['name']}
-        if volume['provider_location'] in self.shares:
-            data['options'] = self.shares[volume['provider_location']]
-        return {
-            'driver_volume_type': self.driver_volume_type,
-            'data': data,
-            'mount_point_base': self._get_mount_point_base()
-        }
-
-    def _get_mount_point_base(self):
-        """Returns the mount point base for the remote fs.
-
-           This method facilitates returning mount point base
-           for the specific remote fs. Override this method
-           in the respective driver to return the entry to be
-           used while attach/detach using brick in cinder.
-           If not overridden then it returns None without
-           raising exception to continue working for cases
-           when not used with brick.
-        """
-        LOG.debug("Driver specific implementation needs to return"
-                  " mount_point_base.")
-        return None
-
-    def create_volume(self, volume):
-        """Creates a volume.
-
-        :param volume: volume reference
-        """
-        self._ensure_shares_mounted()
-
-        volume['provider_location'] = self._find_share(volume['size'])
-
-        LOG.info(_('casted to %s') % volume['provider_location'])
-
-        self._do_create_volume(volume)
-
-        return {'provider_location': volume['provider_location']}
-
-    def _do_create_volume(self, volume):
-        """Create a volume on given remote share.
-
-        :param volume: volume reference
-        """
-        volume_path = self.local_path(volume)
-        volume_size = volume['size']
-
-        if getattr(self.configuration,
-                   self.driver_prefix + '_sparsed_volumes'):
-            self._create_sparsed_file(volume_path, volume_size)
-        else:
-            self._create_regular_file(volume_path, volume_size)
-
-        self._set_rw_permissions_for_all(volume_path)
-
-    def _ensure_shares_mounted(self):
-        """Look for remote shares in the flags and tries to mount them
-        locally.
-        """
-        self._mounted_shares = []
-
-        self._load_shares_config(getattr(self.configuration,
-                                         self.driver_prefix +
-                                         '_shares_config'))
-
-        for share in self.shares.keys():
-            try:
-                self._ensure_share_mounted(share)
-                self._mounted_shares.append(share)
-            except Exception as exc:
-                LOG.warning(_('Exception during mounting %s') % (exc,))
-
-        LOG.debug('Available shares %s' % self._mounted_shares)
-
-    def create_cloned_volume(self, volume, src_vref):
-        raise NotImplementedError()
-
-    def delete_volume(self, volume):
-        """Deletes a logical volume.
-
-        :param volume: volume reference
-        """
-        if not volume['provider_location']:
-            LOG.warn(_('Volume %s does not have provider_location specified, '
-                     'skipping'), volume['name'])
-            return
-
-        self._ensure_share_mounted(volume['provider_location'])
-
-        mounted_path = self.local_path(volume)
-
-        self._execute('rm', '-f', mounted_path, run_as_root=True)
-
-    def ensure_export(self, ctx, volume):
-        """Synchronously recreates an export for a logical volume."""
-        self._ensure_share_mounted(volume['provider_location'])
-
-    def create_export(self, ctx, volume):
-        """Exports the volume. Can optionally return a Dictionary of changes
-        to the volume object to be persisted.
-        """
-        pass
-
-    def remove_export(self, ctx, volume):
-        """Removes an export for a logical volume."""
-        pass
-
-    def delete_snapshot(self, snapshot):
-        """Do nothing for this driver, but allow manager to handle deletion
-           of snapshot in error state.
-        """
-        pass
-
-    def _create_sparsed_file(self, path, size):
-        """Creates file with 0 disk usage."""
-        self._execute('truncate', '-s', '%sG' % size,
-                      path, run_as_root=True)
-
-    def _create_regular_file(self, path, size):
-        """Creates regular file of given size. Takes a lot of time for large
-        files.
-        """
-
-        block_size_mb = 1
-        block_count = size * units.Gi / (block_size_mb * units.Mi)
-
-        self._execute('dd', 'if=/dev/zero', 'of=%s' % path,
-                      'bs=%dM' % block_size_mb,
-                      'count=%d' % block_count,
-                      run_as_root=True)
-
-    def _create_qcow2_file(self, path, size_gb):
-        """Creates a QCOW2 file of a given size."""
-
-        self._execute('qemu-img', 'create', '-f', 'qcow2',
-                      '-o', 'preallocation=metadata',
-                      path, str(size_gb * units.Gi),
-                      run_as_root=True)
-
-    def _set_rw_permissions_for_all(self, path):
-        """Sets 666 permissions for the path."""
-        self._execute('chmod', 'ugo+rw', path, run_as_root=True)
-
-    def local_path(self, volume):
-        """Get volume path (mounted locally fs path) for given volume
-        :param volume: volume reference
-        """
-        nfs_share = volume['provider_location']
-        return os.path.join(self._get_mount_point_for_share(nfs_share),
-                            volume['name'])
-
-    def copy_image_to_volume(self, context, volume, image_service, image_id):
-        """Fetch the image from image_service and write it to the volume."""
-        image_utils.fetch_to_raw(context,
-                                 image_service,
-                                 image_id,
-                                 self.local_path(volume),
-                                 self.configuration.volume_dd_blocksize,
-                                 size=volume['size'])
-
-        # NOTE (leseb): Set the virtual size of the image
-        # the raw conversion overwrote the destination file
-        # (which had the correct size)
-        # with the fetched glance image size,
-        # thus the initial 'size' parameter is not honored
-        # this sets the size to the one asked in the first place by the user
-        # and then verify the final virtual size
-        image_utils.resize_image(self.local_path(volume), volume['size'])
-
-        data = image_utils.qemu_img_info(self.local_path(volume))
-        virt_size = data.virtual_size / units.Gi
-        if virt_size != volume['size']:
-            raise exception.ImageUnacceptable(
-                image_id=image_id,
-                reason=(_("Expected volume size was %d") % volume['size'])
-                + (_(" but size is now %d") % virt_size))
-
-    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 _read_config_file(self, config_file):
-        # Returns list of lines in file
-        with open(config_file) as f:
-            return f.readlines()
-
-    def _load_shares_config(self, share_file):
-        self.shares = {}
-
-        for share in self._read_config_file(share_file):
-            # A configuration line may be either:
-            #  host:/vol_name
-            # or
-            #  host:/vol_name -o options=123,rw --other
-            if not share.strip():
-                # Skip blank or whitespace-only lines
-                continue
-            if share.startswith('#'):
-                continue
-
-            share_info = share.split(' ', 1)
-            # results in share_info =
-            #  [ 'address:/vol', '-o options=123,rw --other' ]
-
-            share_address = share_info[0].strip().decode('unicode_escape')
-            share_opts = share_info[1].strip() if len(share_info) > 1 else None
-
-            if not re.match(r'.+:/.+', share_address):
-                LOG.warn("Share %s ignored due to invalid format.  Must be of "
-                         "form address:/export." % share_address)
-                continue
-
-            self.shares[share_address] = share_opts
-
-        LOG.debug("shares loaded: %s", self.shares)
-
-    def _get_mount_point_for_share(self, path):
-        raise NotImplementedError()
-
-    def terminate_connection(self, volume, connector, **kwargs):
-        """Disallow connection from connector."""
-        pass
-
-    def get_volume_stats(self, refresh=False):
-        """Get volume stats.
-
-        If 'refresh' is True, update the stats first.
-        """
-        if refresh or not self._stats:
-            self._update_volume_stats()
-
-        return self._stats
-
-    def _update_volume_stats(self):
-        """Retrieve stats info from volume group."""
-
-        data = {}
-        backend_name = self.configuration.safe_get('volume_backend_name')
-        data['volume_backend_name'] = backend_name or self.volume_backend_name
-        data['vendor_name'] = 'Open Source'
-        data['driver_version'] = self.get_version()
-        data['storage_protocol'] = self.driver_volume_type
-
-        self._ensure_shares_mounted()
-
-        global_capacity = 0
-        global_free = 0
-        for share in self._mounted_shares:
-            capacity, free, used = self._get_capacity_info(share)
-            global_capacity += capacity
-            global_free += free
-
-        data['total_capacity_gb'] = global_capacity / float(units.Gi)
-        data['free_capacity_gb'] = global_free / float(units.Gi)
-        data['reserved_percentage'] = 0
-        data['QoS_support'] = False
-        self._stats = data
-
-    def _do_mount(self, cmd, ensure, share):
-        """Finalize mount command.
-
-        :param cmd: command to do the actual mount
-        :param ensure: boolean to allow remounting a share with a warning
-        :param share: description of the share for error reporting
-        """
-        try:
-            self._execute(*cmd, run_as_root=True)
-        except putils.ProcessExecutionError as exc:
-            if ensure and 'already mounted' in exc.stderr:
-                LOG.warn(_("%s is already mounted"), share)
-            else:
-                raise
-
-    def _get_capacity_info(self, nfs_share):
-        raise NotImplementedError()
-
-    def _find_share(self, volume_size_in_gib):
-        raise NotImplementedError()
-
-    def _ensure_share_mounted(self, nfs_share):
-        raise NotImplementedError()
 
 
-class NfsDriver(RemoteFsDriver):
+class NfsDriver(remotefs.RemoteFSDriver):
     """NFS based cinder driver. Creates file on NFS share for using it
     as block device on hypervisor.
     """
@@ -414,7 +86,7 @@ class NfsDriver(RemoteFsDriver):
         opts = getattr(self.configuration,
                        'nfs_mount_options',
                        CONF.nfs_mount_options)
-        self._remotefsclient = remotefs.RemoteFsClient(
+        self._remotefsclient = remotefs_brick.RemoteFsClient(
             'nfs', root_helper, execute=execute,
             nfs_mount_point_base=self.base,
             nfs_mount_options=opts)
diff --git a/cinder/volume/drivers/remotefs.py b/cinder/volume/drivers/remotefs.py
new file mode 100644 (file)
index 0000000..85e625f
--- /dev/null
@@ -0,0 +1,528 @@
+# Copyright (c) 2012 NetApp, Inc.
+# Copyright (c) 2014 Red Hat, Inc.
+# 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 hashlib
+import json
+import os
+import re
+import tempfile
+
+from oslo.config import cfg
+
+from cinder import exception
+from cinder.image import image_utils
+from cinder.openstack.common.gettextutils import _
+from cinder.openstack.common import log as logging
+from cinder.openstack.common import processutils as putils
+from cinder.openstack.common import units
+from cinder.volume import driver
+
+LOG = logging.getLogger(__name__)
+
+nas_opts = [
+    cfg.StrOpt('nas_ip',
+               default='',
+               help='IP address or Hostname of NAS system.'),
+    cfg.StrOpt('nas_login',
+               default='admin',
+               help='User name to connect to NAS system.'),
+    cfg.StrOpt('nas_password',
+               default='',
+               help='Password to connect to NAS system.',
+               secret=True),
+    cfg.IntOpt('nas_ssh_port',
+               default=22,
+               help='SSH port to use to connect to NAS system.'),
+    cfg.StrOpt('nas_private_key',
+               default='',
+               help='Filename of private key to use for SSH authentication.'),
+]
+
+CONF = cfg.CONF
+CONF.register_opts(nas_opts)
+
+
+class RemoteFSDriver(driver.VolumeDriver):
+    """Common base for drivers that work like NFS."""
+
+    driver_volume_type = None
+    driver_prefix = None
+    volume_backend_name = None
+    SHARE_FORMAT_REGEX = r'.+:/.+'
+
+    def __init__(self, *args, **kwargs):
+        super(RemoteFSDriver, self).__init__(*args, **kwargs)
+        self.shares = {}
+        self._mounted_shares = []
+
+    def check_for_setup_error(self):
+        """Just to override parent behavior."""
+        pass
+
+    def initialize_connection(self, volume, connector):
+        """Allow connection to connector and return connection info.
+
+        :param volume: volume reference
+        :param connector: connector reference
+        """
+        data = {'export': volume['provider_location'],
+                'name': volume['name']}
+        if volume['provider_location'] in self.shares:
+            data['options'] = self.shares[volume['provider_location']]
+        return {
+            'driver_volume_type': self.driver_volume_type,
+            'data': data,
+            'mount_point_base': self._get_mount_point_base()
+        }
+
+    def _get_mount_point_base(self):
+        """Returns the mount point base for the remote fs.
+
+           This method facilitates returning mount point base
+           for the specific remote fs. Override this method
+           in the respective driver to return the entry to be
+           used while attach/detach using brick in cinder.
+           If not overridden then it returns None without
+           raising exception to continue working for cases
+           when not used with brick.
+        """
+        LOG.debug("Driver specific implementation needs to return"
+                  " mount_point_base.")
+        return None
+
+    def create_volume(self, volume):
+        """Creates a volume.
+
+        :param volume: volume reference
+        """
+        self._ensure_shares_mounted()
+
+        volume['provider_location'] = self._find_share(volume['size'])
+
+        LOG.info(_('casted to %s') % volume['provider_location'])
+
+        self._do_create_volume(volume)
+
+        return {'provider_location': volume['provider_location']}
+
+    def _do_create_volume(self, volume):
+        """Create a volume on given remote share.
+
+        :param volume: volume reference
+        """
+        volume_path = self.local_path(volume)
+        volume_size = volume['size']
+
+        if getattr(self.configuration,
+                   self.driver_prefix + '_sparsed_volumes'):
+            self._create_sparsed_file(volume_path, volume_size)
+        else:
+            self._create_regular_file(volume_path, volume_size)
+
+        self._set_rw_permissions_for_all(volume_path)
+
+    def _ensure_shares_mounted(self):
+        """Look for remote shares in the flags and tries to mount them
+        locally.
+        """
+        self._mounted_shares = []
+
+        self._load_shares_config(getattr(self.configuration,
+                                         self.driver_prefix +
+                                         '_shares_config'))
+
+        for share in self.shares.keys():
+            try:
+                self._ensure_share_mounted(share)
+                self._mounted_shares.append(share)
+            except Exception as exc:
+                LOG.warning(_('Exception during mounting %s') % (exc,))
+
+        LOG.debug('Available shares %s' % self._mounted_shares)
+
+    def create_cloned_volume(self, volume, src_vref):
+        raise NotImplementedError()
+
+    def delete_volume(self, volume):
+        """Deletes a logical volume.
+
+        :param volume: volume reference
+        """
+        if not volume['provider_location']:
+            LOG.warn(_('Volume %s does not have provider_location specified, '
+                     'skipping'), volume['name'])
+            return
+
+        self._ensure_share_mounted(volume['provider_location'])
+
+        mounted_path = self.local_path(volume)
+
+        self._execute('rm', '-f', mounted_path, run_as_root=True)
+
+    def ensure_export(self, ctx, volume):
+        """Synchronously recreates an export for a logical volume."""
+        self._ensure_share_mounted(volume['provider_location'])
+
+    def create_export(self, ctx, volume):
+        """Exports the volume. Can optionally return a Dictionary of changes
+        to the volume object to be persisted.
+        """
+        pass
+
+    def remove_export(self, ctx, volume):
+        """Removes an export for a logical volume."""
+        pass
+
+    def delete_snapshot(self, snapshot):
+        """Do nothing for this driver, but allow manager to handle deletion
+           of snapshot in error state.
+        """
+        pass
+
+    def _create_sparsed_file(self, path, size):
+        """Creates file with 0 disk usage."""
+        self._execute('truncate', '-s', '%sG' % size,
+                      path, run_as_root=True)
+
+    def _create_regular_file(self, path, size):
+        """Creates regular file of given size. Takes a lot of time for large
+        files.
+        """
+
+        block_size_mb = 1
+        block_count = size * units.Gi / (block_size_mb * units.Mi)
+
+        self._execute('dd', 'if=/dev/zero', 'of=%s' % path,
+                      'bs=%dM' % block_size_mb,
+                      'count=%d' % block_count,
+                      run_as_root=True)
+
+    def _create_qcow2_file(self, path, size_gb):
+        """Creates a QCOW2 file of a given size."""
+
+        self._execute('qemu-img', 'create', '-f', 'qcow2',
+                      '-o', 'preallocation=metadata',
+                      path, str(size_gb * units.Gi),
+                      run_as_root=True)
+
+    def _set_rw_permissions_for_all(self, path):
+        """Sets 666 permissions for the path."""
+        self._execute('chmod', 'ugo+rw', path, run_as_root=True)
+
+    def local_path(self, volume):
+        """Get volume path (mounted locally fs path) for given volume
+        :param volume: volume reference
+        """
+        remotefs_share = volume['provider_location']
+        return os.path.join(self._get_mount_point_for_share(remotefs_share),
+                            volume['name'])
+
+    def copy_image_to_volume(self, context, volume, image_service, image_id):
+        """Fetch the image from image_service and write it to the volume."""
+        image_utils.fetch_to_raw(context,
+                                 image_service,
+                                 image_id,
+                                 self.local_path(volume),
+                                 self.configuration.volume_dd_blocksize,
+                                 size=volume['size'])
+
+        # NOTE (leseb): Set the virtual size of the image
+        # the raw conversion overwrote the destination file
+        # (which had the correct size)
+        # with the fetched glance image size,
+        # thus the initial 'size' parameter is not honored
+        # this sets the size to the one asked in the first place by the user
+        # and then verify the final virtual size
+        image_utils.resize_image(self.local_path(volume), volume['size'])
+
+        data = image_utils.qemu_img_info(self.local_path(volume))
+        virt_size = data.virtual_size / units.Gi
+        if virt_size != volume['size']:
+            raise exception.ImageUnacceptable(
+                image_id=image_id,
+                reason=(_("Expected volume size was %d") % volume['size'])
+                + (_(" but size is now %d") % virt_size))
+
+    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 _read_config_file(self, config_file):
+        # Returns list of lines in file
+        with open(config_file) as f:
+            return f.readlines()
+
+    def _load_shares_config(self, share_file):
+        self.shares = {}
+
+        for share in self._read_config_file(share_file):
+            # A configuration line may be either:
+            #  host:/vol_name
+            # or
+            #  host:/vol_name -o options=123,rw --other
+            if not share.strip():
+                # Skip blank or whitespace-only lines
+                continue
+            if share.startswith('#'):
+                continue
+
+            share_info = share.split(' ', 1)
+            # results in share_info =
+            #  [ 'address:/vol', '-o options=123,rw --other' ]
+
+            share_address = share_info[0].strip().decode('unicode_escape')
+            share_opts = share_info[1].strip() if len(share_info) > 1 else None
+
+            if not re.match(self.SHARE_FORMAT_REGEX, share_address):
+                LOG.warn(_("Share %s ignored due to invalid format.  Must be "
+                           "of form address:/export.") % share_address)
+                continue
+
+            self.shares[share_address] = share_opts
+
+        LOG.debug("shares loaded: %s", self.shares)
+
+    def _get_mount_point_for_share(self, path):
+        raise NotImplementedError()
+
+    def terminate_connection(self, volume, connector, **kwargs):
+        """Disallow connection from connector."""
+        pass
+
+    def get_volume_stats(self, refresh=False):
+        """Get volume stats.
+
+        If 'refresh' is True, update the stats first.
+        """
+        if refresh or not self._stats:
+            self._update_volume_stats()
+
+        return self._stats
+
+    def _update_volume_stats(self):
+        """Retrieve stats info from volume group."""
+
+        data = {}
+        backend_name = self.configuration.safe_get('volume_backend_name')
+        data['volume_backend_name'] = backend_name or self.volume_backend_name
+        data['vendor_name'] = 'Open Source'
+        data['driver_version'] = self.get_version()
+        data['storage_protocol'] = self.driver_volume_type
+
+        self._ensure_shares_mounted()
+
+        global_capacity = 0
+        global_free = 0
+        for share in self._mounted_shares:
+            capacity, free, used = self._get_capacity_info(share)
+            global_capacity += capacity
+            global_free += free
+
+        data['total_capacity_gb'] = global_capacity / float(units.Gi)
+        data['free_capacity_gb'] = global_free / float(units.Gi)
+        data['reserved_percentage'] = 0
+        data['QoS_support'] = False
+        self._stats = data
+
+    def _do_mount(self, cmd, ensure, share):
+        """Finalize mount command.
+
+        :param cmd: command to do the actual mount
+        :param ensure: boolean to allow remounting a share with a warning
+        :param share: description of the share for error reporting
+        """
+        try:
+            self._execute(*cmd, run_as_root=True)
+        except putils.ProcessExecutionError as exc:
+            if ensure and 'already mounted' in exc.stderr:
+                LOG.warn(_("%s is already mounted"), share)
+            else:
+                raise
+
+    def _get_capacity_info(self, share):
+        raise NotImplementedError()
+
+    def _find_share(self, volume_size_in_gib):
+        raise NotImplementedError()
+
+    def _ensure_share_mounted(self, share):
+        raise NotImplementedError()
+
+
+class RemoteFSSnapDriver(RemoteFSDriver):
+    """Base class for remotefs drivers implementing qcow2 snapshots.
+
+       Driver must implement:
+         _local_volume_dir(self, volume)
+    """
+
+    def __init__(self, *args, **kwargs):
+        self._remotefsclient = None
+        self.base = None
+        super(RemoteFSSnapDriver, self).__init__(*args, **kwargs)
+
+    def _local_volume_dir(self, volume):
+        raise NotImplementedError()
+
+    def _local_path_volume(self, volume):
+        path_to_disk = '%s/%s' % (
+            self._local_volume_dir(volume),
+            volume['name'])
+
+        return path_to_disk
+
+    def _local_path_volume_info(self, volume):
+        return '%s%s' % (self._local_path_volume(volume), '.info')
+
+    def _read_file(self, filename):
+        """This method is to make it easier to stub out code for testing.
+
+        Returns a string representing the contents of the file.
+        """
+
+        with open(filename, 'r') as f:
+            return f.read()
+
+    def _write_info_file(self, info_path, snap_info):
+        if 'active' not in snap_info.keys():
+            msg = _("'active' must be present when writing snap_info.")
+            raise exception.RemoteFSException(msg)
+
+        with open(info_path, 'w') as f:
+            json.dump(snap_info, f, indent=1, sort_keys=True)
+
+    def _qemu_img_info(self, path):
+        """Sanitize image_utils' qemu_img_info.
+
+        This code expects to deal only with relative filenames.
+        """
+
+        info = image_utils.qemu_img_info(path)
+        if info.image:
+            info.image = os.path.basename(info.image)
+        if info.backing_file:
+            info.backing_file = os.path.basename(info.backing_file)
+
+        return info
+
+    def _qemu_img_commit(self, path):
+        return self._execute('qemu-img', 'commit', path, run_as_root=True)
+
+    def _read_info_file(self, info_path, empty_if_missing=False):
+        """Return dict of snapshot information.
+
+           :param: info_path: path to file
+           :param: empty_if_missing: True=return empty dict if no file
+        """
+
+        if not os.path.exists(info_path):
+            if empty_if_missing is True:
+                return {}
+
+        return json.loads(self._read_file(info_path))
+
+    def _get_backing_chain_for_path(self, volume, path):
+        """Returns list of dicts containing backing-chain information.
+
+        Includes 'filename', and 'backing-filename' for each
+        applicable entry.
+
+        Consider converting this to use --backing-chain and --output=json
+        when environment supports qemu-img 1.5.0.
+
+        :param volume: volume reference
+        :param path: path to image file at top of chain
+
+        """
+
+        output = []
+
+        info = self._qemu_img_info(path)
+        new_info = {}
+        new_info['filename'] = os.path.basename(path)
+        new_info['backing-filename'] = info.backing_file
+
+        output.append(new_info)
+
+        while new_info['backing-filename']:
+            filename = new_info['backing-filename']
+            path = os.path.join(self._local_volume_dir(volume), filename)
+            info = self._qemu_img_info(path)
+            backing_filename = info.backing_file
+            new_info = {}
+            new_info['filename'] = filename
+            new_info['backing-filename'] = backing_filename
+
+            output.append(new_info)
+
+        return output
+
+    def _get_hash_str(self, base_str):
+        """Return a string that represents hash of base_str
+        (in a hex format).
+        """
+        return hashlib.md5(base_str).hexdigest()
+
+    def _get_mount_point_for_share(self, share):
+        """Return mount point for share.
+        :param share: example 172.18.194.100:/var/fs
+        """
+        return self._remotefsclient.get_mount_point(share)
+
+    def _get_available_capacity(self, share):
+        """Calculate available space on the share.
+        :param share: example 172.18.194.100:/var/fs
+        """
+        mount_point = self._get_mount_point_for_share(share)
+
+        out, _ = self._execute('df', '--portability', '--block-size', '1',
+                               mount_point, run_as_root=True)
+        out = out.splitlines()[1]
+
+        size = int(out.split()[1])
+        available = int(out.split()[3])
+
+        return available, size
+
+    def _get_capacity_info(self, remotefs_share):
+        available, size = self._get_available_capacity(remotefs_share)
+        return size, available, size - available
+
+    def _get_mount_point_base(self):
+        return self.base
+
+    def _ensure_share_writable(self, path):
+        """Ensure that the Cinder user can write to the share.
+
+        If not, raise an exception.
+
+        :param path: path to test
+        :raises: RemoteFSException
+        :returns: None
+        """
+
+        prefix = '.cinder-write-test-' + str(os.getpid()) + '-'
+
+        try:
+            tempfile.NamedTemporaryFile(prefix=prefix, dir=path)
+        except OSError:
+            msg = _('Share at %(dir)s is not writable by the '
+                    'Cinder volume service. Snapshot operations will not be '
+                    'supported.') % {'dir': path}
+            raise exception.RemoteFSException(msg)
index beb1f1787630412e4ec1c616082c829cda58c873..f9049628d1adf52f7e57b60a924b170aa580a86d 100644 (file)
 # Options defined in cinder.volume.drivers.nfs
 #
 
-# IP address or Hostname of NAS system. (string value)
-#nas_ip=
-
-# User name to connect to NAS system. (string value)
-#nas_login=admin
-
-# Password to connect to NAS system. (string value)
-#nas_password=
-
-# SSH port to use to connect to NAS system. (integer value)
-#nas_ssh_port=22
-
-# Filename of private key to use for SSH authentication.
-# (string value)
-#nas_private_key=
-
 # File with the list of available nfs shares (string value)
 #nfs_shares_config=/etc/cinder/nfs_shares
 
 #rados_connect_timeout=-1
 
 
+#
+# Options defined in cinder.volume.drivers.remotefs
+#
+
+# IP address or Hostname of NAS system. (string value)
+#nas_ip=
+
+# User name to connect to NAS system. (string value)
+#nas_login=admin
+
+# Password to connect to NAS system. (string value)
+#nas_password=
+
+# SSH port to use to connect to NAS system. (integer value)
+#nas_ssh_port=22
+
+# Filename of private key to use for SSH authentication.
+# (string value)
+#nas_private_key=
+
+
 #
 # Options defined in cinder.volume.drivers.san.hp.hp_3par_common
 #