]> review.fuel-infra Code Review - openstack-build/cinder-build.git/commitdiff
Factor out LVM code.
authorAvishay Traeger <avishay@il.ibm.com>
Wed, 9 Jan 2013 07:31:19 +0000 (09:31 +0200)
committerAvishay Traeger <avishay@il.ibm.com>
Sun, 13 Jan 2013 06:28:19 +0000 (08:28 +0200)
Currently volume drivers inherit from VolumeDriver and ISCSIDriver,
which contain LVM-specific code. The LVM-specific functions are
generally overridden. The problem is that there is no place for
generic code for all drivers to inherit, which the LVM driver can
override. This patch basically makes the VolumeDriver and ISCSIDriver
classes mostly empty and moves the LVM-specific code to lvm.py. Also,
moved the global _iscsi_location() and _iscsi_authentication()
functions into the new LVMISCSIDriver class since nobody else used
them.

Change-Id: I067c975de97913bdc39086ad203cddef0c393c7a
Implements: blueprint factor-out-lvm-code

cinder/tests/fake_driver.py
cinder/tests/fake_flags.py
cinder/tests/test_volume.py
cinder/volume/driver.py
cinder/volume/drivers/lvm.py [new file with mode: 0644]
cinder/volume/manager.py
etc/cinder/cinder.conf.sample

index 3fe19f3b3d1331373c87741c8fd8424c58eb8293..12cc5366cc5129bff9c2e521577aaf51614efa10 100644 (file)
 
 from cinder.openstack.common import log as logging
 from cinder.volume import driver
+from cinder.volume.drivers import lvm
 
 
 LOG = logging.getLogger(__name__)
 
 
-class FakeISCSIDriver(driver.ISCSIDriver):
+class FakeISCSIDriver(lvm.LVMISCSIDriver):
     """Logs calls instead of executing."""
     def __init__(self, *args, **kwargs):
         super(FakeISCSIDriver, self).__init__(execute=self.fake_execute,
index 0266a5ff631374b92742d11b052c49cc157a6a3d..1bc647de41ffa3e7ee502657e851c9ae3004f7fe 100644 (file)
@@ -20,7 +20,7 @@ from cinder import flags
 
 FLAGS = flags.FLAGS
 
-flags.DECLARE('iscsi_num_targets', 'cinder.volume.driver')
+flags.DECLARE('iscsi_num_targets', 'cinder.volume.drivers.lvm')
 flags.DECLARE('policy_file', 'cinder.policy')
 flags.DECLARE('volume_driver', 'cinder.volume.manager')
 flags.DECLARE('xiv_proxy', 'cinder.volume.drivers.xiv')
index dd25fd7e8d44dc3ec5a06d5f44c8fa8b542c2209..291a0c79c38c26c1aa7e0e7eb30eb7dc583177be 100644 (file)
@@ -873,7 +873,7 @@ class DriverTestCase(test.TestCase):
 
 class VolumeDriverTestCase(DriverTestCase):
     """Test case for VolumeDriver"""
-    driver_name = "cinder.volume.driver.VolumeDriver"
+    driver_name = "cinder.volume.drivers.lvm.LVMVolumeDriver"
 
     def test_delete_busy_volume(self):
         """Test deleting a busy volume."""
@@ -895,7 +895,7 @@ class VolumeDriverTestCase(DriverTestCase):
 
 class ISCSITestCase(DriverTestCase):
     """Test Case for ISCSIDriver"""
-    driver_name = "cinder.volume.driver.ISCSIDriver"
+    driver_name = "cinder.volume.drivers.lvm.LVMISCSIDriver"
 
     def _attach_volume(self):
         """Attach volumes to an instance. """
index 2450f2b6777729c7056c6a5abff635471877cd85..9a3cc70b065a3a8b174bc850e7f510e226142367 100644 (file)
@@ -20,52 +20,28 @@ Drivers for volumes.
 
 """
 
-import math
-import os
-import re
 import time
 
 from cinder import exception
 from cinder import flags
-from cinder.image import image_utils
 from cinder.openstack.common import cfg
 from cinder.openstack.common import log as logging
 from cinder.openstack.common import timeutils
 from cinder import utils
-from cinder.volume import iscsi
 
 
 LOG = logging.getLogger(__name__)
 
 volume_opts = [
-    cfg.StrOpt('volume_group',
-               default='cinder-volumes',
-               help='Name for the VG that will contain exported volumes'),
-    cfg.IntOpt('lvm_mirrors',
-               default=0,
-               help='If set, create lvms with multiple mirrors. Note that '
-                    'this requires lvm_mirrors + 2 pvs with available space'),
     cfg.IntOpt('num_shell_tries',
                default=3,
                help='number of times to attempt to run flakey shell commands'),
-    cfg.IntOpt('num_iscsi_scan_tries',
-               default=3,
-               help='number of times to rescan iSCSI target to find volume'),
-    cfg.IntOpt('iscsi_num_targets',
-               default=100,
-               help='Number of iscsi target ids per host'),
-    cfg.StrOpt('iscsi_target_prefix',
-               default='iqn.2010-10.org.openstack:',
-               help='prefix for iscsi volumes'),
-    cfg.StrOpt('iscsi_ip_address',
-               default='$my_ip',
-               help='use this ip for iscsi'),
-    cfg.IntOpt('iscsi_port',
-               default=3260,
-               help='The port that the iSCSI daemon is listening on'),
     cfg.IntOpt('reserved_percentage',
                default=0,
                help='The percentage of backend capacity is reserved'),
+    cfg.IntOpt('num_iscsi_scan_tries',
+               default=3,
+               help='number of times to rescan iSCSI target to find volume'),
 ]
 
 FLAGS = flags.FLAGS
@@ -101,91 +77,16 @@ class VolumeDriver(object):
                 time.sleep(tries ** 2)
 
     def check_for_setup_error(self):
-        """Returns an error if prerequisites aren't met"""
-        out, err = self._execute('vgs', '--noheadings', '-o', 'name',
-                                 run_as_root=True)
-        volume_groups = out.split()
-        if not FLAGS.volume_group in volume_groups:
-            exception_message = (_("volume group %s doesn't exist")
-                                 % FLAGS.volume_group)
-            raise exception.VolumeBackendAPIException(data=exception_message)
-
-    def _create_volume(self, volume_name, sizestr):
-        cmd = ['lvcreate', '-L', sizestr, '-n', volume_name,
-               FLAGS.volume_group]
-        if FLAGS.lvm_mirrors:
-            cmd += ['-m', FLAGS.lvm_mirrors, '--nosync']
-            terras = int(sizestr[:-1]) / 1024.0
-            if terras >= 1.5:
-                rsize = int(2 ** math.ceil(math.log(terras) / math.log(2)))
-                # NOTE(vish): Next power of two for region size. See:
-                #             http://red.ht/U2BPOD
-                cmd += ['-R', str(rsize)]
-
-        self._try_execute(*cmd, run_as_root=True)
-
-    def _copy_volume(self, srcstr, deststr, size_in_g):
-        # Use O_DIRECT to avoid thrashing the system buffer cache
-        direct_flags = ('iflag=direct', 'oflag=direct')
-
-        # Check whether O_DIRECT is supported
-        try:
-            self._execute('dd', 'count=0', 'if=%s' % srcstr, 'of=%s' % deststr,
-                          *direct_flags, run_as_root=True)
-        except exception.ProcessExecutionError:
-            direct_flags = ()
-
-        # Perform the copy
-        self._execute('dd', 'if=%s' % srcstr, 'of=%s' % deststr,
-                      'count=%d' % (size_in_g * 1024), 'bs=1M',
-                      *direct_flags, run_as_root=True)
-
-    def _volume_not_present(self, volume_name):
-        path_name = '%s/%s' % (FLAGS.volume_group, volume_name)
-        try:
-            self._try_execute('lvdisplay', path_name, run_as_root=True)
-        except Exception as e:
-            # If the volume isn't present
-            return True
-        return False
-
-    def _delete_volume(self, volume, size_in_g):
-        """Deletes a logical volume."""
-        # zero out old volumes to prevent data leaking between users
-        # TODO(ja): reclaiming space should be done lazy and low priority
-        dev_path = self.local_path(volume)
-        if FLAGS.secure_delete and os.path.exists(dev_path):
-            LOG.info(_("Performing secure delete on volume: %s")
-                     % volume['id'])
-            self._copy_volume('/dev/zero', dev_path, size_in_g)
-
-        self._try_execute('lvremove', '-f', "%s/%s" %
-                          (FLAGS.volume_group,
-                           self._escape_snapshot(volume['name'])),
-                          run_as_root=True)
-
-    def _sizestr(self, size_in_g):
-        if int(size_in_g) == 0:
-            return '100M'
-        return '%sG' % size_in_g
-
-    # Linux LVM reserves name that starts with snapshot, so that
-    # such volume name can't be created. Mangle it.
-    def _escape_snapshot(self, snapshot_name):
-        if not snapshot_name.startswith('snapshot'):
-            return snapshot_name
-        return '_' + snapshot_name
+        raise NotImplementedError()
 
     def create_volume(self, volume):
-        """Creates a logical volume. Can optionally return a Dictionary of
+        """Creates a volume. Can optionally return a Dictionary of
         changes to the volume object to be persisted."""
-        self._create_volume(volume['name'], self._sizestr(volume['size']))
+        raise NotImplementedError()
 
     def create_volume_from_snapshot(self, volume, snapshot):
         """Creates a volume from a snapshot."""
-        self._create_volume(volume['name'], self._sizestr(volume['size']))
-        self._copy_volume(self.local_path(snapshot), self.local_path(volume),
-                          snapshot['volume_size'])
+        raise NotImplementedError()
 
     def create_cloned_volume(self, volume, src_vref):
         """Creates a clone of the specified volume."""
@@ -205,52 +106,22 @@ class VolumeDriver(object):
             self.delete_snapshot(temp_snapshot)
 
     def delete_volume(self, volume):
-        """Deletes a logical volume."""
-        if self._volume_not_present(volume['name']):
-            # If the volume isn't present, then don't attempt to delete
-            return True
-
-        # TODO(yamahata): lvm can't delete origin volume only without
-        # deleting derived snapshots. Can we do something fancy?
-        out, err = self._execute('lvdisplay', '--noheading',
-                                 '-C', '-o', 'Attr',
-                                 '%s/%s' % (FLAGS.volume_group,
-                                            volume['name']),
-                                 run_as_root=True)
-        # fake_execute returns None resulting unit test error
-        if out:
-            out = out.strip()
-            if (out[0] == 'o') or (out[0] == 'O'):
-                raise exception.VolumeIsBusy(volume_name=volume['name'])
-
-        self._delete_volume(volume, volume['size'])
+        """Deletes a volume."""
+        raise NotImplementedError()
 
     def create_snapshot(self, snapshot):
         """Creates a snapshot."""
-        orig_lv_name = "%s/%s" % (FLAGS.volume_group, snapshot['volume_name'])
-        self._try_execute('lvcreate', '-L',
-                          self._sizestr(snapshot['volume_size']),
-                          '--name', self._escape_snapshot(snapshot['name']),
-                          '--snapshot', orig_lv_name, run_as_root=True)
+        raise NotImplementedError()
 
     def delete_snapshot(self, snapshot):
         """Deletes a snapshot."""
-        if self._volume_not_present(self._escape_snapshot(snapshot['name'])):
-            # If the snapshot isn't present, then don't attempt to delete
-            return True
-
-        # TODO(yamahata): zeroing out the whole snapshot triggers COW.
-        # it's quite slow.
-        self._delete_volume(snapshot, snapshot['volume_size'])
+        raise NotImplementedError()
 
     def local_path(self, volume):
-        # NOTE(vish): stops deprecation warning
-        escaped_group = FLAGS.volume_group.replace('-', '--')
-        escaped_name = self._escape_snapshot(volume['name']).replace('-', '--')
-        return "/dev/mapper/%s-%s" % (escaped_group, escaped_name)
+        raise NotImplementedError()
 
     def ensure_export(self, context, volume):
-        """Synchronously recreates an export for a logical volume."""
+        """Synchronously recreates an export for a volume."""
         raise NotImplementedError()
 
     def create_export(self, context, volume):
@@ -259,7 +130,7 @@ class VolumeDriver(object):
         raise NotImplementedError()
 
     def remove_export(self, context, volume):
-        """Removes an export for a logical volume."""
+        """Removes an export for a volume."""
         raise NotImplementedError()
 
     def initialize_connection(self, volume, connector):
@@ -324,185 +195,8 @@ class ISCSIDriver(VolumeDriver):
     """
 
     def __init__(self, *args, **kwargs):
-        self.tgtadm = iscsi.get_target_admin()
         super(ISCSIDriver, self).__init__(*args, **kwargs)
 
-    def set_execute(self, execute):
-        super(ISCSIDriver, self).set_execute(execute)
-        self.tgtadm.set_execute(execute)
-
-    def ensure_export(self, context, volume):
-        """Synchronously recreates an export for a logical volume."""
-        # NOTE(jdg): tgtadm doesn't use the iscsi_targets table
-        # TODO(jdg): In the future move all of the dependent stuff into the
-        # cooresponding target admin class
-        if not isinstance(self.tgtadm, iscsi.TgtAdm):
-            try:
-                iscsi_target = self.db.volume_get_iscsi_target_num(
-                    context,
-                    volume['id'])
-            except exception.NotFound:
-                LOG.info(_("Skipping ensure_export. No iscsi_target "
-                           "provisioned for volume: %s"), volume['id'])
-                return
-        else:
-            iscsi_target = 1  # dummy value when using TgtAdm
-
-        # Check for https://bugs.launchpad.net/cinder/+bug/1065702
-        old_name = None
-        volume_name = volume['name']
-        if (volume['provider_location'] is not None and
-                volume['name'] not in volume['provider_location']):
-
-            msg = _('Detected inconsistency in provider_location id')
-            LOG.debug(msg)
-            old_name = self._fix_id_migration(context, volume)
-            if 'in-use' in volume['status']:
-                volume_name = old_name
-                old_name = None
-
-        iscsi_name = "%s%s" % (FLAGS.iscsi_target_prefix, volume_name)
-        volume_path = "/dev/%s/%s" % (FLAGS.volume_group, volume_name)
-
-        # NOTE(jdg): For TgtAdm case iscsi_name is the ONLY param we need
-        # should clean this all up at some point in the future
-        self.tgtadm.create_iscsi_target(iscsi_name, iscsi_target,
-                                        0, volume_path,
-                                        check_exit_code=False,
-                                        old_name=old_name)
-
-    def _fix_id_migration(self, context, volume):
-        """Fix provider_location and dev files to address bug 1065702.
-
-        For volumes that the provider_location has NOT been updated
-        and are not currently in-use we'll create a new iscsi target
-        and remove the persist file.
-
-        If the volume is in-use, we'll just stick with the old name
-        and when detach is called we'll feed back into ensure_export
-        again if necessary and fix things up then.
-
-        Details at: https://bugs.launchpad.net/cinder/+bug/1065702
-        """
-
-        model_update = {}
-        pattern = re.compile(r":|\s")
-        fields = pattern.split(volume['provider_location'])
-        old_name = fields[3]
-
-        volume['provider_location'] = \
-            volume['provider_location'].replace(old_name, volume['name'])
-        model_update['provider_location'] = volume['provider_location']
-
-        self.db.volume_update(context, volume['id'], model_update)
-
-        start = os.getcwd()
-        os.chdir('/dev/%s' % FLAGS.volume_group)
-
-        try:
-            (out, err) = self._execute('readlink', old_name)
-        except exception.ProcessExecutionError:
-            link_path = '/dev/%s/%s' % (FLAGS.volume_group, old_name)
-            LOG.debug(_('Symbolic link %s not found') % link_path)
-            os.chdir(start)
-            return
-
-        rel_path = out.rstrip()
-        self._execute('ln',
-                      '-s',
-                      rel_path, volume['name'],
-                      run_as_root=True)
-        os.chdir(start)
-        return old_name
-
-    def _ensure_iscsi_targets(self, context, host):
-        """Ensure that target ids have been created in datastore."""
-        # NOTE(jdg): tgtadm doesn't use the iscsi_targets table
-        # TODO(jdg): In the future move all of the dependent stuff into the
-        # cooresponding target admin class
-        if not isinstance(self.tgtadm, iscsi.TgtAdm):
-            host_iscsi_targets = self.db.iscsi_target_count_by_host(context,
-                                                                    host)
-            if host_iscsi_targets >= FLAGS.iscsi_num_targets:
-                return
-
-            # NOTE(vish): Target ids start at 1, not 0.
-            for target_num in xrange(1, FLAGS.iscsi_num_targets + 1):
-                target = {'host': host, 'target_num': target_num}
-                self.db.iscsi_target_create_safe(context, target)
-
-    def create_export(self, context, volume):
-        """Creates an export for a logical volume."""
-
-        iscsi_name = "%s%s" % (FLAGS.iscsi_target_prefix, volume['name'])
-        volume_path = "/dev/%s/%s" % (FLAGS.volume_group, volume['name'])
-        model_update = {}
-
-        # TODO(jdg): In the future move all of the dependent stuff into the
-        # cooresponding target admin class
-        if not isinstance(self.tgtadm, iscsi.TgtAdm):
-            lun = 0
-            self._ensure_iscsi_targets(context, volume['host'])
-            iscsi_target = self.db.volume_allocate_iscsi_target(context,
-                                                                volume['id'],
-                                                                volume['host'])
-        else:
-            lun = 1  # For tgtadm the controller is lun 0, dev starts at lun 1
-            iscsi_target = 0  # NOTE(jdg): Not used by tgtadm
-
-        # Use the same method to generate the username and the password.
-        chap_username = utils.generate_username()
-        chap_password = utils.generate_password()
-        chap_auth = _iscsi_authentication('IncomingUser', chap_username,
-                                          chap_password)
-        # NOTE(jdg): For TgtAdm case iscsi_name is the ONLY param we need
-        # should clean this all up at some point in the future
-        tid = self.tgtadm.create_iscsi_target(iscsi_name,
-                                              iscsi_target,
-                                              0,
-                                              volume_path,
-                                              chap_auth)
-        model_update['provider_location'] = _iscsi_location(
-            FLAGS.iscsi_ip_address, tid, iscsi_name, lun)
-        model_update['provider_auth'] = _iscsi_authentication(
-            'CHAP', chap_username, chap_password)
-        return model_update
-
-    def remove_export(self, context, volume):
-        """Removes an export for a logical volume."""
-        # NOTE(jdg): tgtadm doesn't use the iscsi_targets table
-        # TODO(jdg): In the future move all of the dependent stuff into the
-        # cooresponding target admin class
-        if not isinstance(self.tgtadm, iscsi.TgtAdm):
-            try:
-                iscsi_target = self.db.volume_get_iscsi_target_num(
-                    context,
-                    volume['id'])
-            except exception.NotFound:
-                LOG.info(_("Skipping remove_export. No iscsi_target "
-                           "provisioned for volume: %s"), volume['id'])
-                return
-        else:
-            iscsi_target = 0
-
-        try:
-
-            # NOTE: provider_location may be unset if the volume hasn't
-            # been exported
-            location = volume['provider_location'].split(' ')
-            iqn = location[1]
-
-            # ietadm show will exit with an error
-            # this export has already been removed
-            self.tgtadm.show_target(iscsi_target, iqn=iqn)
-
-        except Exception as e:
-            LOG.info(_("Skipping remove_export. No iscsi_target "
-                       "is presently exported for volume: %s"), volume['id'])
-            return
-
-        self.tgtadm.remove_iscsi_target(iscsi_target, 0, volume['id'])
-
     def _do_iscsi_discovery(self, volume):
         #TODO(justinsb): Deprecate discovery and use stored info
         #NOTE(justinsb): Discovery won't work with CHAP-secured targets (?)
@@ -625,63 +319,6 @@ class ISCSIDriver(VolumeDriver):
     def terminate_connection(self, volume, connector, **kwargs):
         pass
 
-    def get_volume_stats(self, refresh=False):
-        """Get volume status.
-
-        If 'refresh' is True, run update the stats first."""
-        if 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 = {}
-
-        # Note(zhiteng): These information are driver/backend specific,
-        # each driver may define these values in its own config options
-        # or fetch from driver specific configuration file.
-        data["volume_backend_name"] = 'LVM_iSCSI'
-        data["vendor_name"] = 'Open Source'
-        data["driver_version"] = '1.0'
-        data["storage_protocol"] = 'iSCSI'
-
-        data['total_capacity_gb'] = 0
-        data['free_capacity_gb'] = 0
-        data['reserved_percentage'] = FLAGS.reserved_percentage
-        data['QoS_support'] = False
-
-        try:
-            out, err = self._execute('vgs', '--noheadings', '--nosuffix',
-                                     '--unit=G', '-o', 'name,size,free',
-                                     FLAGS.volume_group, run_as_root=True)
-        except exception.ProcessExecutionError as exc:
-            LOG.error(_("Error retrieving volume status: "), exc.stderr)
-            out = False
-
-        if out:
-            volume = out.split()
-            data['total_capacity_gb'] = float(volume[1])
-            data['free_capacity_gb'] = float(volume[2])
-
-        self._stats = data
-
-    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))
-
-    def copy_volume_to_image(self, context, volume, image_service, image_id):
-        """Copy the volume to the specified image."""
-        volume_path = self.local_path(volume)
-        with utils.temporary_chown(volume_path):
-            with utils.file_open(volume_path) as volume_file:
-                image_service.update(context, image_id, {}, volume_file)
-
 
 class FakeISCSIDriver(ISCSIDriver):
     """Logs calls instead of executing."""
@@ -707,11 +344,3 @@ class FakeISCSIDriver(ISCSIDriver):
         """Execute that simply logs the command."""
         LOG.debug(_("FAKE ISCSI: %s"), cmd)
         return (None, None)
-
-
-def _iscsi_location(ip, target, iqn, lun=None):
-    return "%s:%s,%s %s %s" % (ip, FLAGS.iscsi_port, target, iqn, lun)
-
-
-def _iscsi_authentication(chap, name, password):
-    return "%s %s %s" % (chap, name, password)
diff --git a/cinder/volume/drivers/lvm.py b/cinder/volume/drivers/lvm.py
new file mode 100644 (file)
index 0000000..14c2b5b
--- /dev/null
@@ -0,0 +1,461 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2010 United States Government as represented by the
+# Administrator of the National Aeronautics and Space Administration.
+# 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.
+"""
+Driver for Linux servers running LVM.
+
+"""
+
+import math
+import os
+import re
+
+from cinder import exception
+from cinder import flags
+from cinder.image import image_utils
+from cinder.openstack.common import cfg
+from cinder.openstack.common import log as logging
+from cinder import utils
+from cinder.volume import driver
+from cinder.volume import iscsi
+
+LOG = logging.getLogger(__name__)
+
+volume_opts = [
+    cfg.StrOpt('volume_group',
+               default='cinder-volumes',
+               help='Name for the VG that will contain exported volumes'),
+    cfg.IntOpt('lvm_mirrors',
+               default=0,
+               help='If set, create lvms with multiple mirrors. Note that '
+                    'this requires lvm_mirrors + 2 pvs with available space'),
+    cfg.IntOpt('iscsi_num_targets',
+               default=100,
+               help='Number of iscsi target ids per host'),
+    cfg.StrOpt('iscsi_target_prefix',
+               default='iqn.2010-10.org.openstack:',
+               help='prefix for iscsi volumes'),
+    cfg.StrOpt('iscsi_ip_address',
+               default='$my_ip',
+               help='The port that the iSCSI daemon is listening on'),
+    cfg.IntOpt('iscsi_port',
+               default=3260,
+               help='The port that the iSCSI daemon is listening on'), ]
+
+FLAGS = flags.FLAGS
+FLAGS.register_opts(volume_opts)
+
+
+class LVMVolumeDriver(driver.VolumeDriver):
+    """Executes commands relating to Volumes."""
+    def __init__(self, *args, **kwargs):
+        super(LVMVolumeDriver, self).__init__(*args, **kwargs)
+
+    def check_for_setup_error(self):
+        """Returns an error if prerequisites aren't met"""
+        out, err = self._execute('vgs', '--noheadings', '-o', 'name',
+                                 run_as_root=True)
+        volume_groups = out.split()
+        if not FLAGS.volume_group in volume_groups:
+            exception_message = (_("volume group %s doesn't exist")
+                                 % FLAGS.volume_group)
+            raise exception.VolumeBackendAPIException(data=exception_message)
+
+    def _create_volume(self, volume_name, sizestr):
+        cmd = ['lvcreate', '-L', sizestr, '-n', volume_name,
+               FLAGS.volume_group]
+        if FLAGS.lvm_mirrors:
+            cmd += ['-m', FLAGS.lvm_mirrors, '--nosync']
+            terras = int(sizestr[:-1]) / 1024.0
+            if terras >= 1.5:
+                rsize = int(2 ** math.ceil(math.log(terras) / math.log(2)))
+                # NOTE(vish): Next power of two for region size. See:
+                #             http://red.ht/U2BPOD
+                cmd += ['-R', str(rsize)]
+
+        self._try_execute(*cmd, run_as_root=True)
+
+    def _copy_volume(self, srcstr, deststr, size_in_g):
+        # Use O_DIRECT to avoid thrashing the system buffer cache
+        direct_flags = ('iflag=direct', 'oflag=direct')
+
+        # Check whether O_DIRECT is supported
+        try:
+            self._execute('dd', 'count=0', 'if=%s' % srcstr, 'of=%s' % deststr,
+                          *direct_flags, run_as_root=True)
+        except exception.ProcessExecutionError:
+            direct_flags = ()
+
+        # Perform the copy
+        self._execute('dd', 'if=%s' % srcstr, 'of=%s' % deststr,
+                      'count=%d' % (size_in_g * 1024), 'bs=1M',
+                      *direct_flags, run_as_root=True)
+
+    def _volume_not_present(self, volume_name):
+        path_name = '%s/%s' % (FLAGS.volume_group, volume_name)
+        try:
+            self._try_execute('lvdisplay', path_name, run_as_root=True)
+        except Exception as e:
+            # If the volume isn't present
+            return True
+        return False
+
+    def _delete_volume(self, volume, size_in_g):
+        """Deletes a logical volume."""
+        # zero out old volumes to prevent data leaking between users
+        # TODO(ja): reclaiming space should be done lazy and low priority
+        dev_path = self.local_path(volume)
+        if FLAGS.secure_delete and os.path.exists(dev_path):
+            LOG.info(_("Performing secure delete on volume: %s")
+                     % volume['id'])
+            self._copy_volume('/dev/zero', dev_path, size_in_g)
+
+        self._try_execute('lvremove', '-f', "%s/%s" %
+                          (FLAGS.volume_group,
+                           self._escape_snapshot(volume['name'])),
+                          run_as_root=True)
+
+    def _sizestr(self, size_in_g):
+        if int(size_in_g) == 0:
+            return '100M'
+        return '%sG' % size_in_g
+
+    # Linux LVM reserves name that starts with snapshot, so that
+    # such volume name can't be created. Mangle it.
+    def _escape_snapshot(self, snapshot_name):
+        if not snapshot_name.startswith('snapshot'):
+            return snapshot_name
+        return '_' + snapshot_name
+
+    def create_volume(self, volume):
+        """Creates a logical volume. Can optionally return a Dictionary of
+        changes to the volume object to be persisted."""
+        self._create_volume(volume['name'], self._sizestr(volume['size']))
+
+    def create_volume_from_snapshot(self, volume, snapshot):
+        """Creates a volume from a snapshot."""
+        self._create_volume(volume['name'], self._sizestr(volume['size']))
+        self._copy_volume(self.local_path(snapshot), self.local_path(volume),
+                          snapshot['volume_size'])
+
+    def delete_volume(self, volume):
+        """Deletes a logical volume."""
+        if self._volume_not_present(volume['name']):
+            # If the volume isn't present, then don't attempt to delete
+            return True
+
+        # TODO(yamahata): lvm can't delete origin volume only without
+        # deleting derived snapshots. Can we do something fancy?
+        out, err = self._execute('lvdisplay', '--noheading',
+                                 '-C', '-o', 'Attr',
+                                 '%s/%s' % (FLAGS.volume_group,
+                                            volume['name']),
+                                 run_as_root=True)
+        # fake_execute returns None resulting unit test error
+        if out:
+            out = out.strip()
+            if (out[0] == 'o') or (out[0] == 'O'):
+                raise exception.VolumeIsBusy(volume_name=volume['name'])
+
+        self._delete_volume(volume, volume['size'])
+
+    def create_snapshot(self, snapshot):
+        """Creates a snapshot."""
+        orig_lv_name = "%s/%s" % (FLAGS.volume_group, snapshot['volume_name'])
+        self._try_execute('lvcreate', '-L',
+                          self._sizestr(snapshot['volume_size']),
+                          '--name', self._escape_snapshot(snapshot['name']),
+                          '--snapshot', orig_lv_name, run_as_root=True)
+
+    def delete_snapshot(self, snapshot):
+        """Deletes a snapshot."""
+        if self._volume_not_present(self._escape_snapshot(snapshot['name'])):
+            # If the snapshot isn't present, then don't attempt to delete
+            return True
+
+        # TODO(yamahata): zeroing out the whole snapshot triggers COW.
+        # it's quite slow.
+        self._delete_volume(snapshot, snapshot['volume_size'])
+
+    def local_path(self, volume):
+        # NOTE(vish): stops deprecation warning
+        escaped_group = FLAGS.volume_group.replace('-', '--')
+        escaped_name = self._escape_snapshot(volume['name']).replace('-', '--')
+        return "/dev/mapper/%s-%s" % (escaped_group, escaped_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))
+
+    def copy_volume_to_image(self, context, volume, image_service, image_id):
+        """Copy the volume to the specified image."""
+        volume_path = self.local_path(volume)
+        with utils.temporary_chown(volume_path):
+            with utils.file_open(volume_path) as volume_file:
+                image_service.update(context, image_id, {}, volume_file)
+
+    def clone_image(self, volume, image_location):
+        return False
+
+
+class LVMISCSIDriver(LVMVolumeDriver, driver.ISCSIDriver):
+    """Executes commands relating to ISCSI volumes.
+
+    We make use of model provider properties as follows:
+
+    ``provider_location``
+      if present, contains the iSCSI target information in the same
+      format as an ietadm discovery
+      i.e. '<ip>:<port>,<portal> <target IQN>'
+
+    ``provider_auth``
+      if present, contains a space-separated triple:
+      '<auth method> <auth username> <auth password>'.
+      `CHAP` is the only auth_method in use at the moment.
+    """
+
+    def __init__(self, *args, **kwargs):
+        self.tgtadm = iscsi.get_target_admin()
+        super(LVMISCSIDriver, self).__init__(*args, **kwargs)
+
+    def set_execute(self, execute):
+        super(LVMISCSIDriver, self).set_execute(execute)
+        self.tgtadm.set_execute(execute)
+
+    def ensure_export(self, context, volume):
+        """Synchronously recreates an export for a logical volume."""
+        # NOTE(jdg): tgtadm doesn't use the iscsi_targets table
+        # TODO(jdg): In the future move all of the dependent stuff into the
+        # cooresponding target admin class
+        if not isinstance(self.tgtadm, iscsi.TgtAdm):
+            try:
+                iscsi_target = self.db.volume_get_iscsi_target_num(
+                    context,
+                    volume['id'])
+            except exception.NotFound:
+                LOG.info(_("Skipping ensure_export. No iscsi_target "
+                           "provisioned for volume: %s"), volume['id'])
+                return
+        else:
+            iscsi_target = 1  # dummy value when using TgtAdm
+
+        # Check for https://bugs.launchpad.net/cinder/+bug/1065702
+        old_name = None
+        volume_name = volume['name']
+        if (volume['provider_location'] is not None and
+                volume['name'] not in volume['provider_location']):
+
+            msg = _('Detected inconsistency in provider_location id')
+            LOG.debug(msg)
+            old_name = self._fix_id_migration(context, volume)
+            if 'in-use' in volume['status']:
+                volume_name = old_name
+                old_name = None
+
+        iscsi_name = "%s%s" % (FLAGS.iscsi_target_prefix, volume_name)
+        volume_path = "/dev/%s/%s" % (FLAGS.volume_group, volume_name)
+
+        # NOTE(jdg): For TgtAdm case iscsi_name is the ONLY param we need
+        # should clean this all up at some point in the future
+        self.tgtadm.create_iscsi_target(iscsi_name, iscsi_target,
+                                        0, volume_path,
+                                        check_exit_code=False,
+                                        old_name=old_name)
+
+    def _fix_id_migration(self, context, volume):
+        """Fix provider_location and dev files to address bug 1065702.
+
+        For volumes that the provider_location has NOT been updated
+        and are not currently in-use we'll create a new iscsi target
+        and remove the persist file.
+
+        If the volume is in-use, we'll just stick with the old name
+        and when detach is called we'll feed back into ensure_export
+        again if necessary and fix things up then.
+
+        Details at: https://bugs.launchpad.net/cinder/+bug/1065702
+        """
+
+        model_update = {}
+        pattern = re.compile(r":|\s")
+        fields = pattern.split(volume['provider_location'])
+        old_name = fields[3]
+
+        volume['provider_location'] = \
+            volume['provider_location'].replace(old_name, volume['name'])
+        model_update['provider_location'] = volume['provider_location']
+
+        self.db.volume_update(context, volume['id'], model_update)
+
+        start = os.getcwd()
+        os.chdir('/dev/%s' % FLAGS.volume_group)
+
+        try:
+            (out, err) = self._execute('readlink', old_name)
+        except exception.ProcessExecutionError:
+            link_path = '/dev/%s/%s' % (FLAGS.volume_group, old_name)
+            LOG.debug(_('Symbolic link %s not found') % link_path)
+            os.chdir(start)
+            return
+
+        rel_path = out.rstrip()
+        self._execute('ln',
+                      '-s',
+                      rel_path, volume['name'],
+                      run_as_root=True)
+        os.chdir(start)
+        return old_name
+
+    def _ensure_iscsi_targets(self, context, host):
+        """Ensure that target ids have been created in datastore."""
+        # NOTE(jdg): tgtadm doesn't use the iscsi_targets table
+        # TODO(jdg): In the future move all of the dependent stuff into the
+        # cooresponding target admin class
+        if not isinstance(self.tgtadm, iscsi.TgtAdm):
+            host_iscsi_targets = self.db.iscsi_target_count_by_host(context,
+                                                                    host)
+            if host_iscsi_targets >= FLAGS.iscsi_num_targets:
+                return
+
+            # NOTE(vish): Target ids start at 1, not 0.
+            for target_num in xrange(1, FLAGS.iscsi_num_targets + 1):
+                target = {'host': host, 'target_num': target_num}
+                self.db.iscsi_target_create_safe(context, target)
+
+    def create_export(self, context, volume):
+        """Creates an export for a logical volume."""
+
+        iscsi_name = "%s%s" % (FLAGS.iscsi_target_prefix, volume['name'])
+        volume_path = "/dev/%s/%s" % (FLAGS.volume_group, volume['name'])
+        model_update = {}
+
+        # TODO(jdg): In the future move all of the dependent stuff into the
+        # cooresponding target admin class
+        if not isinstance(self.tgtadm, iscsi.TgtAdm):
+            lun = 0
+            self._ensure_iscsi_targets(context, volume['host'])
+            iscsi_target = self.db.volume_allocate_iscsi_target(context,
+                                                                volume['id'],
+                                                                volume['host'])
+        else:
+            lun = 1  # For tgtadm the controller is lun 0, dev starts at lun 1
+            iscsi_target = 0  # NOTE(jdg): Not used by tgtadm
+
+        # Use the same method to generate the username and the password.
+        chap_username = utils.generate_username()
+        chap_password = utils.generate_password()
+        chap_auth = self._iscsi_authentication('IncomingUser', chap_username,
+                                               chap_password)
+        # NOTE(jdg): For TgtAdm case iscsi_name is the ONLY param we need
+        # should clean this all up at some point in the future
+        tid = self.tgtadm.create_iscsi_target(iscsi_name,
+                                              iscsi_target,
+                                              0,
+                                              volume_path,
+                                              chap_auth)
+        model_update['provider_location'] = self._iscsi_location(
+            FLAGS.iscsi_ip_address, tid, iscsi_name, lun)
+        model_update['provider_auth'] = self._iscsi_authentication(
+            'CHAP', chap_username, chap_password)
+        return model_update
+
+    def remove_export(self, context, volume):
+        """Removes an export for a logical volume."""
+        # NOTE(jdg): tgtadm doesn't use the iscsi_targets table
+        # TODO(jdg): In the future move all of the dependent stuff into the
+        # cooresponding target admin class
+        if not isinstance(self.tgtadm, iscsi.TgtAdm):
+            try:
+                iscsi_target = self.db.volume_get_iscsi_target_num(
+                    context,
+                    volume['id'])
+            except exception.NotFound:
+                LOG.info(_("Skipping remove_export. No iscsi_target "
+                           "provisioned for volume: %s"), volume['id'])
+                return
+        else:
+            iscsi_target = 0
+
+        try:
+
+            # NOTE: provider_location may be unset if the volume hasn't
+            # been exported
+            location = volume['provider_location'].split(' ')
+            iqn = location[1]
+
+            # ietadm show will exit with an error
+            # this export has already been removed
+            self.tgtadm.show_target(iscsi_target, iqn=iqn)
+
+        except Exception as e:
+            LOG.info(_("Skipping remove_export. No iscsi_target "
+                       "is presently exported for volume: %s"), volume['id'])
+            return
+
+        self.tgtadm.remove_iscsi_target(iscsi_target, 0, volume['id'])
+
+    def get_volume_stats(self, refresh=False):
+        """Get volume status.
+
+        If 'refresh' is True, run update the stats first."""
+        if 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 = {}
+
+        # Note(zhiteng): These information are driver/backend specific,
+        # each driver may define these values in its own config options
+        # or fetch from driver specific configuration file.
+        data["volume_backend_name"] = 'LVM_iSCSI'
+        data["vendor_name"] = 'Open Source'
+        data["driver_version"] = '1.0'
+        data["storage_protocol"] = 'iSCSI'
+
+        data['total_capacity_gb'] = 0
+        data['free_capacity_gb'] = 0
+        data['reserved_percentage'] = FLAGS.reserved_percentage
+        data['QoS_support'] = False
+
+        try:
+            out, err = self._execute('vgs', '--noheadings', '--nosuffix',
+                                     '--unit=G', '-o', 'name,size,free',
+                                     FLAGS.volume_group, run_as_root=True)
+        except exception.ProcessExecutionError as exc:
+            LOG.error(_("Error retrieving volume status: "), exc.stderr)
+            out = False
+
+        if out:
+            volume = out.split()
+            data['total_capacity_gb'] = float(volume[1])
+            data['free_capacity_gb'] = float(volume[2])
+
+        self._stats = data
+
+    def _iscsi_location(self, ip, target, iqn, lun=None):
+        return "%s:%s,%s %s %s" % (ip, FLAGS.iscsi_port, target, iqn, lun)
+
+    def _iscsi_authentication(self, chap, name, password):
+        return "%s %s %s" % (chap, name, password)
index 3567c985dc79cc2e5b44694db4b468c94e29bac3..72e7cd396500ee1f6bb9fc2d2995ae9b0352404b 100644 (file)
@@ -30,7 +30,7 @@ intact.
                   :class:`manager.Manager` (default:
                   :class:`cinder.volume.manager.Manager`).
 :volume_driver:  Used by :class:`Manager`.  Defaults to
-                 :class:`cinder.volume.driver.ISCSIDriver`.
+                 :class:`cinder.volume.drivers.lvm.LVMISCSIDriver`.
 :volume_group:  Name of the group that will contain exported volumes (default:
                 `cinder-volumes`)
 :num_shell_tries:  Number of times to attempt to run commands (default: 3)
@@ -59,7 +59,7 @@ QUOTAS = quota.QUOTAS
 
 volume_manager_opts = [
     cfg.StrOpt('volume_driver',
-               default='cinder.volume.driver.ISCSIDriver',
+               default='cinder.volume.drivers.lvm.LVMISCSIDriver',
                help='Driver to use for volume creation'),
 ]
 
@@ -95,7 +95,9 @@ MAPPING = {
     'cinder.volume.xiv.XIVDriver':
     'cinder.volume.drivers.xiv.XIVDriver',
     'cinder.volume.zadara.ZadaraVPSAISCSIDriver':
-    'cinder.volume.drivers.zadara.ZadaraVPSAISCSIDriver'}
+    'cinder.volume.drivers.zadara.ZadaraVPSAISCSIDriver',
+    'cinder.volume.driver.ISCSIDriver':
+    'cinder.volume.drivers.lvm.LVMISCSIDriver'}
 
 
 class VolumeManager(manager.SchedulerDependentManager):
index c2eb8b6e77b7eac18e2aece21e1c23dba21d9894..f31d6d0154f7355452fc87eb91e221a32f7fe0ef 100644 (file)
 
 ######## defined in cinder.volume.manager ########
 
-# volume_driver=cinder.volume.driver.ISCSIDriver
+# volume_driver=cinder.volume.drivers.lvm.LVMISCSIDriver
 #### (StrOpt) Driver to use for volume creation
 
 # volume_force_update_capabilities=false