diff --git a/cinder/tests/test_eqlx.py b/cinder/tests/test_eqlx.py new file mode 100644 index 00000000000..f85b89a950d --- /dev/null +++ b/cinder/tests/test_eqlx.py @@ -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 index 00000000000..27ce6842b82 --- /dev/null +++ b/cinder/volume/drivers/eqlx.py @@ -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= + san_login= + san_password= + san_private_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= + eqlx_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= + """ + + 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() diff --git a/etc/cinder/cinder.conf.sample b/etc/cinder/cinder.conf.sample index e50d8e46f79..4fd56312af7 100644 --- a/etc/cinder/cinder.conf.sample +++ b/etc/cinder/cinder.conf.sample @@ -1141,6 +1141,33 @@ #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 # @@ -1731,4 +1758,4 @@ #volume_dd_blocksize=1M -# Total option count: 370 +# Total option count: 377