]> review.fuel-infra Code Review - openstack-build/cinder-build.git/commitdiff
Adds Dell EqualLogic volume driver for Cinder
authorAlexander Gordeev <agordeev@mirantis.com>
Fri, 31 May 2013 09:22:38 +0000 (13:22 +0400)
committerJohn Griffith <john.griffith@solidfire.com>
Wed, 11 Sep 2013 13:33:01 +0000 (07:33 -0600)
Change-Id: I9668c99de5e14b6a94a52f3ef0cbb8a8d7e18b85
Implements: blueprint eql-volume-driver

cinder/tests/test_eqlx.py [new file with mode: 0644]
cinder/volume/drivers/eqlx.py [new file with mode: 0644]
etc/cinder/cinder.conf.sample

diff --git a/cinder/tests/test_eqlx.py b/cinder/tests/test_eqlx.py
new file mode 100644 (file)
index 0000000..f85b89a
--- /dev/null
@@ -0,0 +1,300 @@
+#    Copyright (c) 2013 Dell Inc.
+#    Copyright 2013 OpenStack LLC
+#
+#    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 time
+
+import mox
+import paramiko
+
+from cinder import context
+from cinder import exception
+from cinder.openstack.common import log as logging
+from cinder.openstack.common import processutils
+from cinder import test
+from cinder.volume import configuration as conf
+from cinder.volume.drivers import eqlx
+
+
+LOG = logging.getLogger(__name__)
+
+
+class DellEQLSanISCSIDriverTestCase(test.TestCase):
+
+    def setUp(self):
+        super(DellEQLSanISCSIDriverTestCase, self).setUp()
+        self.configuration = mox.MockObject(conf.Configuration)
+        self.configuration.append_config_values(mox.IgnoreArg())
+        self.configuration.san_is_local = False
+        self.configuration.san_ip = "10.0.0.1"
+        self.configuration.san_login = "foo"
+        self.configuration.san_password = "bar"
+        self.configuration.san_ssh_port = 16022
+        self.configuration.san_thin_provision = True
+        self.configuration.eqlx_pool = 'non-default'
+        self.configuration.eqlx_use_chap = True
+        self.configuration.eqlx_group_name = 'group-0'
+        self.configuration.eqlx_cli_timeout = 30
+        self.configuration.eqlx_cli_max_retries = 5
+        self.configuration.eqlx_chap_login = 'admin'
+        self.configuration.eqlx_chap_password = 'password'
+        self.configuration.volume_name_template = 'volume_%s'
+        self._context = context.get_admin_context()
+        self.driver = eqlx.DellEQLSanISCSIDriver(
+            configuration=self.configuration)
+        self.volume_name = "fakevolume"
+        self.volid = "fakeid"
+        self.connector = {'ip': '10.0.0.2',
+                          'initiator': 'iqn.1993-08.org.debian:01:222',
+                          'host': 'fakehost'}
+        self.fake_iqn = 'iqn.2003-10.com.equallogic:group01:25366:fakev'
+        self.driver._group_ip = '10.0.1.6'
+        self.properties = {
+            'target_discoverd': True,
+            'target_portal': '%s:3260' % self.driver._group_ip,
+            'target_iqn': self.fake_iqn,
+            'volume_id': 1}
+        self._model_update = {
+            'provider_location': "%s:3260,1 %s 0" % (self.driver._group_ip,
+                                                     self.fake_iqn),
+            'provider_auth': 'CHAP %s %s' % (
+                self.configuration.eqlx_chap_login,
+                self.configuration.eqlx_chap_password)
+        }
+
+    def _fake_get_iscsi_properties(self, volume):
+        return self.properties
+
+    def test_create_volume(self):
+        self.driver._eql_execute = self.mox.\
+            CreateMock(self.driver._eql_execute)
+        volume = {'name': self.volume_name, 'size': 1}
+        self.driver._eql_execute('volume', 'create', volume['name'],
+                                 "%sG" % (volume['size']), 'pool',
+                                 self.configuration.eqlx_pool,
+                                 'thin-provision').\
+            AndReturn(['iSCSI target name is %s.' % self.fake_iqn])
+        self.mox.ReplayAll()
+        model_update = self.driver.create_volume(volume)
+        self.assertEqual(model_update, self._model_update)
+
+    def test_delete_volume(self):
+        self.driver._eql_execute = self.mox.\
+            CreateMock(self.driver._eql_execute)
+        volume = {'name': self.volume_name, 'size': 1}
+        self.driver._eql_execute('volume', 'select', volume['name'], 'show')
+        self.driver._eql_execute('volume', 'select', volume['name'], 'offline')
+        self.driver._eql_execute('volume', 'delete', volume['name'])
+        self.mox.ReplayAll()
+        self.driver.delete_volume(volume)
+
+    def test_delete_absent_volume(self):
+        self.driver._eql_execute = self.mox.\
+            CreateMock(self.driver._eql_execute)
+        volume = {'name': self.volume_name, 'size': 1, 'id': self.volid}
+        self.driver._eql_execute('volume', 'select', volume['name'], 'show').\
+            AndRaise(processutils.ProcessExecutionError(
+                stdout='% Error ..... does not exist.\n'))
+        self.mox.ReplayAll()
+        self.driver.delete_volume(volume)
+
+    def test_ensure_export(self):
+        self.driver._eql_execute = self.mox.\
+            CreateMock(self.driver._eql_execute)
+        volume = {'name': self.volume_name, 'size': 1}
+        self.driver._eql_execute('volume', 'select', volume['name'], 'show')
+        self.mox.ReplayAll()
+        self.driver.ensure_export({}, volume)
+
+    def test_create_snapshot(self):
+        self.driver._eql_execute = self.mox.\
+            CreateMock(self.driver._eql_execute)
+        snapshot = {'name': 'fakesnap', 'volume_name': 'fakevolume_name'}
+        snap_name = 'fake_snap_name'
+        self.driver._eql_execute('volume', 'select', snapshot['volume_name'],
+                                 'snapshot', 'create-now').\
+            AndReturn(['Snapshot name is %s' % snap_name])
+        self.driver._eql_execute('volume', 'select', snapshot['volume_name'],
+                                 'snapshot', 'rename', snap_name,
+                                 snapshot['name'])
+        self.mox.ReplayAll()
+        self.driver.create_snapshot(snapshot)
+
+    def test_create_volume_from_snapshot(self):
+        self.driver._eql_execute = self.mox.\
+            CreateMock(self.driver._eql_execute)
+        snapshot = {'name': 'fakesnap', 'volume_name': 'fakevolume_name'}
+        volume = {'name': self.volume_name}
+        self.driver._eql_execute('volume', 'select', snapshot['volume_name'],
+                                 'snapshot', 'select', snapshot['name'],
+                                 'clone', volume['name']).\
+            AndReturn(['iSCSI target name is %s.' % self.fake_iqn])
+        self.mox.ReplayAll()
+        model_update = self.driver.create_volume_from_snapshot(volume,
+                                                               snapshot)
+        self.assertEqual(model_update, self._model_update)
+
+    def test_create_cloned_volume(self):
+        self.driver._eql_execute = self.mox.\
+            CreateMock(self.driver._eql_execute)
+        src_vref = {'id': 'fake_uuid'}
+        volume = {'name': self.volume_name}
+        src_volume_name = self.configuration.\
+            volume_name_template % src_vref['id']
+        self.driver._eql_execute('volume', 'select', src_volume_name, 'clone',
+                                 volume['name']).\
+            AndReturn(['iSCSI target name is %s.' % self.fake_iqn])
+        self.mox.ReplayAll()
+        model_update = self.driver.create_cloned_volume(volume, src_vref)
+        self.assertEqual(model_update, self._model_update)
+
+    def test_delete_snapshot(self):
+        self.driver._eql_execute = self.mox.\
+            CreateMock(self.driver._eql_execute)
+        snapshot = {'name': 'fakesnap', 'volume_name': 'fakevolume_name'}
+        self.driver._eql_execute('volume', 'select', snapshot['volume_name'],
+                                 'snapshot', 'delete', snapshot['name'])
+        self.mox.ReplayAll()
+        self.driver.delete_snapshot(snapshot)
+
+    def test_initialize_connection(self):
+        self.driver._eql_execute = self.mox.\
+            CreateMock(self.driver._eql_execute)
+        volume = {'name': self.volume_name}
+        self.stubs.Set(self.driver, "_get_iscsi_properties",
+                       self._fake_get_iscsi_properties)
+        self.driver._eql_execute('volume', 'select', volume['name'], 'access',
+                                 'create', 'initiator',
+                                 self.connector['initiator'],
+                                 'authmethod chap',
+                                 'username',
+                                 self.configuration.eqlx_chap_login)
+        self.mox.ReplayAll()
+        iscsi_properties = self.driver.initialize_connection(volume,
+                                                             self.connector)
+        self.assertEqual(iscsi_properties['data'],
+                         self._fake_get_iscsi_properties(volume))
+
+    def test_terminate_connection(self):
+        self.driver._eql_execute = self.mox.\
+            CreateMock(self.driver._eql_execute)
+        volume = {'name': self.volume_name}
+        self.driver._eql_execute('volume', 'select', volume['name'], 'access',
+                                 'delete', '1')
+        self.mox.ReplayAll()
+        self.driver.terminate_connection(volume, self.connector)
+
+    def test_do_setup(self):
+        self.driver._eql_execute = self.mox.\
+            CreateMock(self.driver._eql_execute)
+        fake_group_ip = '10.1.2.3'
+        for feature in ('confirmation', 'paging', 'events', 'formatoutput'):
+            self.driver._eql_execute('cli-settings', feature, 'off')
+        self.driver._eql_execute('grpparams', 'show').\
+            AndReturn(['Group-Ipaddress: %s' % fake_group_ip])
+        self.mox.ReplayAll()
+        self.driver.do_setup(self._context)
+        self.assertEqual(fake_group_ip, self.driver._group_ip)
+
+    def test_update_volume_status(self):
+        self.driver._eql_execute = self.mox.\
+            CreateMock(self.driver._eql_execute)
+        self.driver._eql_execute('pool', 'select',
+                                 self.configuration.eqlx_pool, 'show').\
+            AndReturn(['TotalCapacity: 111GB', 'FreeSpace: 11GB'])
+        self.mox.ReplayAll()
+        self.driver._update_volume_status()
+        self.assertEqual(self.driver._stats['total_capacity_gb'], 111.0)
+        self.assertEqual(self.driver._stats['free_capacity_gb'], 11.0)
+
+    def test_get_space_in_gb(self):
+        self.assertEqual(self.driver._get_space_in_gb('123.0GB'), 123.0)
+        self.assertEqual(self.driver._get_space_in_gb('123.0TB'), 123.0 * 1024)
+        self.assertEqual(self.driver._get_space_in_gb('1024.0MB'), 1.0)
+
+    def test_get_output(self):
+
+        def _fake_recv(ignore_arg):
+            return '%s> ' % self.configuration.eqlx_group_name
+
+        chan = self.mox.CreateMock(paramiko.Channel)
+        self.stubs.Set(chan, "recv", _fake_recv)
+        self.assertEqual(self.driver._get_output(chan), [_fake_recv(None)])
+
+    def test_get_prefixed_value(self):
+        lines = ['Line1 passed', 'Line1 failed']
+        prefix = ['Line1', 'Line2']
+        expected_output = [' passed', None]
+        self.assertEqual(self.driver._get_prefixed_value(lines, prefix[0]),
+                         expected_output[0])
+        self.assertEqual(self.driver._get_prefixed_value(lines, prefix[1]),
+                         expected_output[1])
+
+    def test_ssh_execute(self):
+        ssh = self.mox.CreateMock(paramiko.SSHClient)
+        chan = self.mox.CreateMock(paramiko.Channel)
+        transport = self.mox.CreateMock(paramiko.Transport)
+        self.mox.StubOutWithMock(self.driver, '_get_output')
+        self.mox.StubOutWithMock(chan, 'invoke_shell')
+        expected_output = ['NoError: test run']
+        ssh.get_transport().AndReturn(transport)
+        transport.open_session().AndReturn(chan)
+        chan.invoke_shell()
+        self.driver._get_output(chan).AndReturn(expected_output)
+        cmd = 'this is dummy command'
+        chan.send('stty columns 255' + '\r')
+        self.driver._get_output(chan).AndReturn(expected_output)
+        chan.send(cmd + '\r')
+        self.driver._get_output(chan).AndReturn(expected_output)
+        chan.close()
+        self.mox.ReplayAll()
+        self.assertEqual(self.driver._ssh_execute(ssh, cmd), expected_output)
+
+    def test_ssh_execute_error(self):
+        ssh = self.mox.CreateMock(paramiko.SSHClient)
+        chan = self.mox.CreateMock(paramiko.Channel)
+        transport = self.mox.CreateMock(paramiko.Transport)
+        self.mox.StubOutWithMock(self.driver, '_get_output')
+        self.mox.StubOutWithMock(ssh, 'get_transport')
+        self.mox.StubOutWithMock(chan, 'invoke_shell')
+        expected_output = ['Error: test run', '% Error']
+        ssh.get_transport().AndReturn(transport)
+        transport.open_session().AndReturn(chan)
+        chan.invoke_shell()
+        self.driver._get_output(chan).AndReturn(expected_output)
+        cmd = 'this is dummy command'
+        chan.send('stty columns 255' + '\r')
+        self.driver._get_output(chan).AndReturn(expected_output)
+        chan.send(cmd + '\r')
+        self.driver._get_output(chan).AndReturn(expected_output)
+        chan.close()
+        self.mox.ReplayAll()
+        self.assertRaises(processutils.ProcessExecutionError,
+                          self.driver._ssh_execute, ssh, cmd)
+
+    def test_with_timeout(self):
+        @eqlx.with_timeout
+        def no_timeout(cmd, *args, **kwargs):
+            return 'no timeout'
+
+        @eqlx.with_timeout
+        def w_timeout(cmd, *args, **kwargs):
+            time.sleep(1)
+
+        self.assertEqual(no_timeout('fake cmd'), 'no timeout')
+        self.assertRaises(exception.VolumeBackendAPIException,
+                          w_timeout, 'fake cmd', timeout=0.1)
+
+    def test_local_path(self):
+        self.assertRaises(NotImplementedError, self.driver.local_path, '')
diff --git a/cinder/volume/drivers/eqlx.py b/cinder/volume/drivers/eqlx.py
new file mode 100644 (file)
index 0000000..27ce684
--- /dev/null
@@ -0,0 +1,453 @@
+#    Copyright (c) 2013 Dell Inc.
+#    Copyright 2013 OpenStack LLC
+#
+#    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.
+
+"""Volume driver for Dell EqualLogic Storage."""
+
+import functools
+import random
+
+import eventlet
+from eventlet import greenthread
+import greenlet
+from oslo.config import cfg
+
+from cinder import exception
+from cinder.openstack.common import excutils
+from cinder.openstack.common import log as logging
+from cinder.openstack.common import processutils
+from cinder import utils
+from cinder.volume.drivers.san import SanISCSIDriver
+
+LOG = logging.getLogger(__name__)
+
+eqlx_opts = [
+    cfg.StrOpt('eqlx_group_name',
+               default='group-0',
+               help='Group name to use for creating volumes'),
+    cfg.IntOpt('eqlx_cli_timeout',
+               default=30,
+               help='Timeout for the Group Manager cli command execution'),
+    cfg.IntOpt('eqlx_cli_max_retries',
+               default=5,
+               help='Maximum retry count for reconnection'),
+    cfg.BoolOpt('eqlx_use_chap',
+                default=False,
+                help='Use CHAP authentificaion for targets?'),
+    cfg.StrOpt('eqlx_chap_login',
+               default='admin',
+               help='Existing CHAP account name'),
+    cfg.StrOpt('eqlx_chap_password',
+               default='password',
+               help='Password for specified CHAP account name',
+               secret=True),
+    cfg.StrOpt('eqlx_pool',
+               default='default',
+               help='Pool in which volumes will be created')
+]
+
+
+CONF = cfg.CONF
+CONF.register_opts(eqlx_opts)
+
+
+def with_timeout(f):
+    @functools.wraps(f)
+    def __inner(self, *args, **kwargs):
+        timeout = kwargs.pop('timeout', None)
+        gt = eventlet.spawn(f, self, *args, **kwargs)
+        if timeout is None:
+            return gt.wait()
+        else:
+            kill_thread = eventlet.spawn_after(timeout, gt.kill)
+            try:
+                res = gt.wait()
+            except greenlet.GreenletExit:
+                raise exception.VolumeBackendAPIException(
+                    data="Command timed out")
+            else:
+                kill_thread.cancel()
+                return res
+
+    return __inner
+
+
+class DellEQLSanISCSIDriver(SanISCSIDriver):
+    """Implements commands for Dell EqualLogic SAN ISCSI management.
+
+    To enable the driver add the following line to the cinder configuration:
+        volume_driver=cinder.volume.drivers.eqlx.DellEQLSanISCSIDriver
+
+    Driver's prerequisites are:
+        - a separate volume group set up and running on the SAN
+        - SSH access to the SAN
+        - a special user must be created which must be able to
+            - create/delete volumes and snapshots;
+            - clone snapshots into volumes;
+            - modify volume access records;
+
+    The access credentials to the SAN are provided by means of the following
+    flags
+        san_ip=<ip_address>
+        san_login=<user name>
+        san_password=<user password>
+        san_private_key=<file containig SSH prvate key>
+
+    Thin provision of volumes is enabled by default, to disable it use:
+        san_thin_provision=false
+
+    In order to use target CHAP authentication (which is disabled by default)
+    SAN administrator must create a local CHAP user and specify the following
+    flags for the driver:
+        eqlx_use_chap=true
+        eqlx_chap_login=<chap_login>
+        eqlx_chap_password=<chap_password>
+
+    eqlx_group_name parameter actually represents the CLI prompt message
+    without '>' ending. E.g. if prompt looks like 'group-0>', then the
+    parameter must be set to 'group-0'
+
+    Also, the default CLI command execution timeout is 30 secs. Adjustable by
+        eqlx_cli_timeout=<seconds>
+    """
+
+    VERSION = "1.0.0"
+
+    def __init__(self, *args, **kwargs):
+        super(DellEQLSanISCSIDriver, self).__init__(*args, **kwargs)
+        self.configuration.append_config_values(eqlx_opts)
+        self._group_ip = None
+        self.sshpool = None
+
+    def _get_output(self, chan):
+        out = ''
+        ending = '%s> ' % self.configuration.eqlx_group_name
+        while not out.endswith(ending):
+            out += chan.recv(102400)
+
+        LOG.debug(_("CLI output\n%s"), out)
+        return out.splitlines()
+
+    def _get_prefixed_value(self, lines, prefix):
+        for line in lines:
+            if line.startswith(prefix):
+                return line[len(prefix):]
+        return
+
+    @with_timeout
+    def _ssh_execute(self, ssh, command, *arg, **kwargs):
+        transport = ssh.get_transport()
+        chan = transport.open_session()
+        chan.invoke_shell()
+
+        LOG.debug(_("Reading CLI MOTD"))
+        self._get_output(chan)
+
+        cmd = 'stty columns 255'
+        LOG.debug(_("Setting CLI terminal width: '%s'"), cmd)
+        chan.send(cmd + '\r')
+        out = self._get_output(chan)
+
+        LOG.debug(_("Sending CLI command: '%s'"), command)
+        chan.send(command + '\r')
+        out = self._get_output(chan)
+
+        chan.close()
+
+        if any(line.startswith(('% Error', 'Error:')) for line in out):
+            desc = _("Error executing EQL command")
+            cmdout = '\n'.join(out)
+            LOG.error(cmdout)
+            raise processutils.ProcessExecutionError(
+                stdout=cmdout, cmd=command, description=desc)
+        return out
+
+    def _run_ssh(self, cmd_list, attempts=1):
+        utils.check_ssh_injection(cmd_list)
+        command = ' '. join(cmd_list)
+
+        if not self.sshpool:
+            password = self.configuration.san_password
+            privatekey = self.configuration.san_private_key
+            min_size = self.configuration.ssh_min_pool_conn
+            max_size = self.configuration.ssh_max_pool_conn
+            self.sshpool = utils.SSHPool(self.configuration.san_ip,
+                                         self.configuration.san_ssh_port,
+                                         self.configuration.ssh_conn_timeout,
+                                         self.configuration.san_login,
+                                         password=password,
+                                         privatekey=privatekey,
+                                         min_size=min_size,
+                                         max_size=max_size)
+        try:
+            total_attempts = attempts
+            with self.sshpool.item() as ssh:
+                while attempts > 0:
+                    attempts -= 1
+                    try:
+                        LOG.info(_('EQL-driver: executing "%s"') % command)
+                        return self._ssh_execute(
+                            ssh, command,
+                            timeout=self.configuration.eqlx_cli_timeout)
+                    except processutils.ProcessExecutionError:
+                        raise
+                    except Exception as e:
+                        LOG.exception(e)
+                        greenthread.sleep(random.randint(20, 500) / 100.0)
+                msg = (_("SSH Command failed after '%(total_attempts)r' "
+                         "attempts : '%(command)s'") %
+                       {'total_attempts': total_attempts, 'command': command})
+                raise exception.VolumeBackendAPIException(data=msg)
+
+        except Exception:
+            with excutils.save_and_reraise_exception():
+                LOG.error(_("Error running SSH command: %s") % command)
+
+    def _eql_execute(self, *args, **kwargs):
+            return self._run_ssh(
+                args, attempts=self.configuration.eqlx_cli_max_retries)
+
+    def _get_volume_data(self, lines):
+        prefix = 'iSCSI target name is '
+        target_name = self._get_prefixed_value(lines, prefix)[:-1]
+        lun_id = "%s:%s,1 %s 0" % (self._group_ip, '3260', target_name)
+        model_update = {}
+        model_update['provider_location'] = lun_id
+        if self.configuration.eqlx_use_chap:
+            model_update['provider_auth'] = 'CHAP %s %s' % \
+                (self.configuration.eqlx_chap_login,
+                 self.configuration.eqlx_chap_password)
+        return model_update
+
+    def _get_space_in_gb(self, val):
+        scale = 1.0
+        part = 'GB'
+        if val.endswith('MB'):
+            scale = 1.0 / 1024
+            part = 'MB'
+        elif val.endswith('TB'):
+            scale = 1.0 * 1024
+            part = 'TB'
+        return scale * float(val.partition(part)[0])
+
+    def _update_volume_status(self):
+        """Retrieve status info from volume group."""
+
+        LOG.debug(_("Updating volume status"))
+        data = {}
+        backend_name = "eqlx"
+        if self.configuration:
+            backend_name = self.configuration.safe_get('volume_backend_name')
+        data["volume_backend_name"] = backend_name or 'eqlx'
+        data["vendor_name"] = 'Dell'
+        data["driver_version"] = self.VERSION
+        data["storage_protocol"] = 'iSCSI'
+
+        data['reserved_percentage'] = 0
+        data['QoS_support'] = False
+
+        data['total_capacity_gb'] = 'infinite'
+        data['free_capacity_gb'] = 'infinite'
+
+        for line in self._eql_execute('pool', 'select',
+                                      self.configuration.eqlx_pool, 'show'):
+            if line.startswith('TotalCapacity:'):
+                out_tup = line.rstrip().partition(' ')
+                data['total_capacity_gb'] = self._get_space_in_gb(out_tup[-1])
+            if line.startswith('FreeSpace:'):
+                out_tup = line.rstrip().partition(' ')
+                data['free_capacity_gb'] = self._get_space_in_gb(out_tup[-1])
+
+        self._stats = data
+
+    def _check_volume(self, volume):
+        """Check if the volume exists on the Array."""
+        command = ['volume', 'select', volume['name'], 'show']
+        try:
+            self._eql_execute(*command)
+        except processutils.ProcessExecutionError as err:
+            with excutils.save_and_reraise_exception():
+                if err.stdout.find('does not exist.\n') > -1:
+                    LOG.debug(_('Volume %s does not exist, '
+                                'it may have already been deleted'),
+                              volume['name'])
+                    raise exception.VolumeNotFound(volume_id=volume['id'])
+
+    def do_setup(self, context):
+        """Disable cli confirmation and tune output format."""
+        try:
+            disabled_cli_features = ('confirmation', 'paging', 'events',
+                                     'formatoutput')
+            for feature in disabled_cli_features:
+                self._eql_execute('cli-settings', feature, 'off')
+
+            for line in self._eql_execute('grpparams', 'show'):
+                if line.startswith('Group-Ipaddress:'):
+                    out_tup = line.rstrip().partition(' ')
+                    self._group_ip = out_tup[-1]
+
+            LOG.info(_("EQL-driver: Setup is complete, group IP is %s"),
+                     self._group_ip)
+        except Exception:
+            with excutils.save_and_reraise_exception():
+                LOG.error(_('Failed to setup the Dell EqualLogic driver'))
+
+    def create_volume(self, volume):
+        """Create a volume."""
+        try:
+            cmd = ['volume', 'create',
+                   volume['name'], "%sG" % (volume['size'])]
+            if self.configuration.eqlx_pool != 'default':
+                cmd.append('pool')
+                cmd.append(self.configuration.eqlx_pool)
+            if self.configuration.san_thin_provision:
+                cmd.append('thin-provision')
+            out = self._eql_execute(*cmd)
+            return self._get_volume_data(out)
+        except Exception:
+            with excutils.save_and_reraise_exception():
+                LOG.error(_('Failed to create volume %s'), volume['name'])
+
+    def delete_volume(self, volume):
+        """Delete a volume."""
+        try:
+            self._check_volume(volume)
+            self._eql_execute('volume', 'select', volume['name'], 'offline')
+            self._eql_execute('volume', 'delete', volume['name'])
+        except exception.VolumeNotFound:
+            LOG.warn(_('Volume %s was not found while trying to delete it'),
+                     volume['name'])
+        except Exception:
+            with excutils.save_and_reraise_exception():
+                LOG.error(_('Failed to delete volume %s'), volume['name'])
+
+    def create_snapshot(self, snapshot):
+        """"Create snapshot of existing volume on appliance."""
+        try:
+            out = self._eql_execute('volume', 'select',
+                                    snapshot['volume_name'],
+                                    'snapshot', 'create-now')
+            prefix = 'Snapshot name is '
+            snap_name = self._get_prefixed_value(out, prefix)
+            self._eql_execute('volume', 'select', snapshot['volume_name'],
+                              'snapshot', 'rename', snap_name,
+                              snapshot['name'])
+        except Exception:
+            with excutils.save_and_reraise_exception():
+                LOG.error(_('Failed to create snapshot of volume %s'),
+                          snapshot['volume_name'])
+
+    def create_volume_from_snapshot(self, volume, snapshot):
+        """Create new volume from other volume's snapshot on appliance."""
+        try:
+            out = self._eql_execute('volume', 'select',
+                                    snapshot['volume_name'], 'snapshot',
+                                    'select', snapshot['name'],
+                                    'clone', volume['name'])
+            return self._get_volume_data(out)
+        except Exception:
+            with excutils.save_and_reraise_exception():
+                LOG.error(_('Failed to create volume from snapshot %s'),
+                          snapshot['name'])
+
+    def create_cloned_volume(self, volume, src_vref):
+        """Creates a clone of the specified volume."""
+        try:
+            src_volume_name = self.configuration.\
+                volume_name_template % src_vref['id']
+            out = self._eql_execute('volume', 'select', src_volume_name,
+                                    'clone', volume['name'])
+            return self._get_volume_data(out)
+        except Exception:
+            with excutils.save_and_reraise_exception():
+                LOG.error(_('Failed to create clone of volume %s'),
+                          volume['name'])
+
+    def delete_snapshot(self, snapshot):
+        """Delete volume's snapshot."""
+        try:
+            self._eql_execute('volume', 'select', snapshot['volume_name'],
+                              'snapshot', 'delete', snapshot['name'])
+        except Exception:
+            with excutils.save_and_reraise_exception():
+                LOG.error(_('Failed to delete snapshot %(snap)s of '
+                            'volume %(vol)s'),
+                          {'snap': snapshot['name'],
+                           'vol': snapshot['volume_name']})
+
+    def initialize_connection(self, volume, connector):
+        """Restrict access to a volume."""
+        try:
+            cmd = ['volume', 'select', volume['name'], 'access', 'create',
+                   'initiator', connector['initiator']]
+            if self.configuration.eqlx_use_chap:
+                cmd.extend(['authmethod chap', 'username',
+                            self.configuration.eqlx_chap_login])
+            self._eql_execute(*cmd)
+            iscsi_properties = self._get_iscsi_properties(volume)
+            return {
+                'driver_volume_type': 'iscsi',
+                'data': iscsi_properties
+            }
+        except Exception:
+            with excutils.save_and_reraise_exception():
+                LOG.error(_('Failed to initialize connection to volume %s'),
+                          volume['name'])
+
+    def terminate_connection(self, volume, connector, force=False, **kwargs):
+        """Remove access restictions from a volume."""
+        try:
+            self._eql_execute('volume', 'select', volume['name'],
+                              'access', 'delete', '1')
+        except Exception:
+            with excutils.save_and_reraise_exception():
+                LOG.error(_('Failed to terminate connection to volume %s'),
+                          volume['name'])
+
+    def create_export(self, context, volume):
+        """Create an export of a volume.
+
+        Driver has nothing to do here for the volume has been exported
+        already by the SAN, right after it's creation.
+        """
+        pass
+
+    def ensure_export(self, context, volume):
+        """Ensure an export of a volume.
+
+        Driver has nothing to do here for the volume has been exported
+        already by the SAN, right after it's creation. We will just make
+        sure that the volume exists on the array and issue a warning.
+        """
+        try:
+            self._check_volume(volume)
+        except exception.VolumeNotFound:
+            LOG.warn(_('Volume %s is not found!, it may have been deleted'),
+                     volume['name'])
+        except Exception:
+            with excutils.save_and_reraise_exception():
+                LOG.error(_('Failed to ensure export of volume %s'),
+                          volume['name'])
+
+    def remove_export(self, context, volume):
+        """Remove an export of a volume.
+
+        Driver has nothing to do here for the volume has been exported
+        already by the SAN, right after it's creation.
+        Nothing to remove since there's nothing exported.
+        """
+        pass
+
+    def local_path(self, volume):
+        raise NotImplementedError()
index e50d8e46f79c099da2d6dea363a0b855f1c54a2b..4fd56312af7d90d957ec315cb03651893cd6de3f 100644 (file)
 #coraid_repository_key=coraid_repository
 
 
+#
+# Options defined in cinder.volume.drivers.eqlx
+#
+
+# Group name to use for creating volumes (string value)
+#eqlx_group_name=group-0
+
+# Timeout for the Group Manager cli command execution (integer
+# value)
+#eqlx_cli_timeout=30
+
+# Maximum retry count for reconnection (integer value)
+#eqlx_cli_max_retries=5
+
+# Use CHAP authentificaion for targets? (boolean value)
+#eqlx_use_chap=false
+
+# Existing CHAP account name (string value)
+#eqlx_chap_login=admin
+
+# Password for specified CHAP account name (string value)
+#eqlx_chap_password=password
+
+# Pool in which volumes will be created (string value)
+#eqlx_pool=default
+
+
 #
 # Options defined in cinder.volume.drivers.glusterfs
 #
 #volume_dd_blocksize=1M
 
 
-# Total option count: 370
+# Total option count: 377