Adds Dell EqualLogic volume driver for Cinder
Change-Id: I9668c99de5e14b6a94a52f3ef0cbb8a8d7e18b85 Implements: blueprint eql-volume-driver
This commit is contained in:
parent
a67390dba7
commit
1d95920082
300
cinder/tests/test_eqlx.py
Normal file
300
cinder/tests/test_eqlx.py
Normal file
@ -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, '')
|
453
cinder/volume/drivers/eqlx.py
Normal file
453
cinder/volume/drivers/eqlx.py
Normal file
@ -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()
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user