Remove SSH code from 3PAR drivers

This patch migrates all of the communication
to the 3PAR array into the client library.
Some of the calls to the array happen over ssh
and others happen over REST.  Now the drivers
don't care.

This allows us to change the external client
library to replace SSH calls to REST calls,
without the need of driver changes.

Change-Id: Ia5e94a349308055381001c373b91c444860115c7
This commit is contained in:
Walter A. Boring IV 2014-01-30 23:44:22 -08:00
parent a7a65ebdb4
commit 1724c5cee7
4 changed files with 871 additions and 1187 deletions

File diff suppressed because it is too large Load Diff

View File

@ -38,11 +38,9 @@ import ast
import base64
import json
import pprint
from random import randint
import re
import uuid
from eventlet import greenthread
import hp3parclient
from hp3parclient import client
from hp3parclient import exceptions as hpexceptions
@ -52,14 +50,12 @@ from cinder import context
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 import volume_types
LOG = logging.getLogger(__name__)
MIN_CLIENT_VERSION = '2.0.0'
MIN_CLIENT_VERSION = '2.9.0'
hp3par_opts = [
cfg.StrOpt('hp3par_api_url',
@ -113,10 +109,11 @@ class HP3PARCommon(object):
1.2.5 - Raise Ex when deleting snapshot with dependencies bug #1250249
1.2.6 - Allow optional specifying n:s:p for vlun creation bug #1269515
This update now requires 3.1.2 MU3 firmware
1.3.0 - Removed all SSH code. We rely on the hp3parclient now.
"""
VERSION = "1.2.6"
VERSION = "1.3.0"
stats = {}
@ -143,7 +140,6 @@ class HP3PARCommon(object):
hp3par_valid_keys = ['cpg', 'snap_cpg', 'provisioning', 'persona', 'vvs']
def __init__(self, config):
self.sshpool = None
self.config = config
self.hosts_naming_dict = dict()
self.client = None
@ -163,11 +159,20 @@ class HP3PARCommon(object):
client_version = hp3parclient.version
if (client_version < MIN_CLIENT_VERSION):
ex_msg = (_('Invalid hp3parclient version. Version %s or greater '
'required.') % MIN_CLIENT_VERSION)
ex_msg = (_('Invalid hp3parclient version found (%(found)s). '
'Version %(minimum)s or greater required.')
% {'found': client_version,
'minimum': MIN_CLIENT_VERSION})
LOG.error(ex_msg)
raise exception.InvalidInput(reason=ex_msg)
cl.setSSHOptions(self.config.san_ip,
self.config.san_login,
self.config.san_password,
port=self.config.san_ssh_port,
conn_timeout=self.config.ssh_conn_timeout,
privatekey=self.config.san_private_key)
return cl
def client_login(self):
@ -201,7 +206,7 @@ class HP3PARCommon(object):
try:
# make sure the default CPG exists
self.validate_cpg(self.config.hp3par_cpg)
self._set_connections()
self.client.setHighConnections()
finally:
self.client_logout()
@ -213,14 +218,6 @@ class HP3PARCommon(object):
LOG.error(err)
raise exception.InvalidInput(reason=err)
def _set_connections(self):
"""Set the number of concurrent connections.
The 3PAR WS API server has a limit of concurrent connections.
This is setting the number to the highest allowed, 15 connections.
"""
self._cli_run(['setwsapi', '-sru', 'high'])
def get_domain(self, cpg_name):
try:
cpg = self.client.getCPG(cpg_name)
@ -236,12 +233,12 @@ class HP3PARCommon(object):
def extend_volume(self, volume, new_size):
volume_name = self._get_3par_vol_name(volume['id'])
old_size = volume.size
old_size = volume['size']
growth_size = int(new_size) - old_size
LOG.debug("Extending Volume %s from %s to %s, by %s GB." %
(volume_name, old_size, new_size, growth_size))
try:
self._cli_run(['growvv', '-f', volume_name, '%dg' % growth_size])
self.client.growVolume(volume_name, growth_size)
except Exception:
with excutils.save_and_reraise_exception():
LOG.error(_("Error extending volume %s") % volume)
@ -299,96 +296,6 @@ class HP3PARCommon(object):
capacity = int(round(capacity / MiB))
return capacity
def _cli_run(self, cmd):
"""Runs a CLI command over SSH, without doing any result parsing."""
LOG.debug("SSH CMD = %s " % cmd)
(stdout, stderr) = self._run_ssh(cmd, False)
# we have to strip out the input and exit lines
tmp = stdout.split("\r\n")
out = tmp[5:len(tmp) - 2]
return out
def _ssh_execute(self, ssh, cmd, check_exit_code=True):
"""We have to do this in order to get CSV output from the CLI command.
We first have to issue a command to tell the CLI that we want the
output to be formatted in CSV, then we issue the real command.
"""
LOG.debug(_('Running cmd (SSH): %s'), cmd)
channel = ssh.invoke_shell()
stdin_stream = channel.makefile('wb')
stdout_stream = channel.makefile('rb')
stderr_stream = channel.makefile('rb')
stdin_stream.write('''setclienv csvtable 1
%s
exit
''' % cmd)
# stdin.write('process_input would go here')
# stdin.flush()
# NOTE(justinsb): This seems suspicious...
# ...other SSH clients have buffering issues with this approach
stdout = stdout_stream.read()
stderr = stderr_stream.read()
stdin_stream.close()
stdout_stream.close()
stderr_stream.close()
exit_status = channel.recv_exit_status()
# exit_status == -1 if no exit code was returned
if exit_status != -1:
LOG.debug(_('Result was %s') % exit_status)
if check_exit_code and exit_status != 0:
msg = _("command %s failed") % cmd
LOG.error(msg)
raise processutils.ProcessExecutionError(exit_code=exit_status,
stdout=stdout,
stderr=stderr,
cmd=cmd)
channel.close()
return (stdout, stderr)
def _run_ssh(self, cmd_list, check_exit=True, attempts=1):
utils.check_ssh_injection(cmd_list)
command = ' '. join(cmd_list)
if not self.sshpool:
self.sshpool = utils.SSHPool(self.config.san_ip,
self.config.san_ssh_port,
self.config.ssh_conn_timeout,
self.config.san_login,
password=self.config.san_password,
privatekey=
self.config.san_private_key,
min_size=
self.config.ssh_min_pool_conn,
max_size=
self.config.ssh_max_pool_conn)
try:
total_attempts = attempts
with self.sshpool.item() as ssh:
while attempts > 0:
attempts -= 1
try:
return self._ssh_execute(ssh, command,
check_exit_code=check_exit)
except Exception as e:
LOG.error(e)
greenthread.sleep(randint(20, 500) / 100.0)
msg = (_("SSH Command failed after '%(total_attempts)r' "
"attempts : '%(command)s'") %
{'total_attempts': total_attempts, 'command': command})
LOG.error(msg)
raise exception.CinderException(message=msg)
except Exception:
with excutils.save_and_reraise_exception():
LOG.error(_("Error running ssh command: %s") % command)
def _delete_3par_host(self, hostname):
self.client.deleteHost(hostname)
@ -568,41 +475,24 @@ exit
def _set_qos_rule(self, qos, vvs_name):
max_io = self._get_qos_value(qos, 'maxIOPS')
max_bw = self._get_qos_value(qos, 'maxBWS')
cmd = ['setqos']
if max_io is not None:
cmd.extend(['-io', '%s' % max_io])
if max_bw is not None:
cmd.extend(['-bw', '%sM' % max_bw])
cmd.append('vvset:' + vvs_name)
self._cli_run(cmd)
self.client.setQOSRule(vvs_name, max_io, max_bw)
def _add_volume_to_volume_set(self, volume, volume_name,
cpg, vvs_name, qos):
if vvs_name is not None:
# Admin has set a volume set name to add the volume to
out = self._cli_run(['createvvset', '-add', vvs_name, volume_name])
if out and len(out) == 1:
if 'does not exist' in out[0]:
msg = _('VV Set %s does not exist.') % vvs_name
LOG.error(msg)
raise exception.InvalidInput(reason=msg)
try:
self.client.addVolumeToVolumeSet(vvs_name, volume_name)
except hpexceptions.HTTPNotFound:
msg = _('VV Set %s does not exist.') % vvs_name
LOG.error(msg)
raise exception.InvalidInput(reason=msg)
else:
vvs_name = self._get_3par_vvs_name(volume['id'])
domain = self.get_domain(cpg)
if domain is not None:
self._cli_run(['createvvset', '-domain', domain, vvs_name])
else:
self._cli_run(['createvvset', vvs_name])
self.client.createVolumeSet(vvs_name, domain)
self._set_qos_rule(qos, vvs_name)
self._cli_run(['createvvset', '-add', vvs_name, volume_name])
def _remove_volume_set(self, vvs_name):
# Must first clear the QoS rules before removing the volume set
self._cli_run(['setqos', '-clear', 'vvset:%s' % (vvs_name)])
self._cli_run(['removevvset', '-f', vvs_name])
def _remove_volume_from_volume_set(self, volume_name, vvs_name):
self._cli_run(['removevvset', '-f', vvs_name, volume_name])
self.client.addVolumeToVolumeSet(vvs_name, volume_name)
def get_cpg(self, volume, allowSnap=False):
volume_name = self._get_3par_vol_name(volume['id'])
@ -768,16 +658,9 @@ exit
def _copy_volume(self, src_name, dest_name, cpg=None, snap_cpg=None,
tpvv=True):
# Virtual volume sets are not supported with the -online option
cmd = ['createvvcopy', '-p', src_name, '-online']
if snap_cpg:
cmd.extend(['-snp_cpg', snap_cpg])
if tpvv:
cmd.append('-tpvv')
if cpg:
cmd.append(cpg)
cmd.append(dest_name)
LOG.debug('Creating clone of a volume with %s' % cmd)
self._cli_run(cmd)
LOG.debug('Creating clone of a volume %s' % src_name)
self.client.copyVolume(src_name, dest_name, cpg,
snap_cpg, tpvv)
def get_next_word(self, s, search_string):
"""Return the next word.
@ -815,26 +698,6 @@ exit
LOG.error(str(ex))
raise exception.CinderException(ex)
def _get_vvset_from_3par(self, volume_name):
"""Get Virtual Volume Set from 3PAR.
The only way to do this currently is to try and delete the volume
to get the error message.
NOTE(walter-boring): don't call this unless you know the volume is
already in a vvset!
"""
cmd = ['removevv', '-f', volume_name]
LOG.debug("Issuing remove command to find vvset name %s" % cmd)
out = self._cli_run(cmd)
vvset_name = None
if out and len(out) > 1:
if out[1].startswith("Attempt to delete "):
words = out[1].split(" ")
vvset_name = words[len(words) - 1]
return vvset_name
def delete_volume(self, volume):
try:
volume_name = self._get_3par_vol_name(volume['id'])
@ -847,19 +710,19 @@ exit
if ex.get_code() == 34:
# This is a special case which means the
# volume is part of a volume set.
vvset_name = self._get_vvset_from_3par(volume_name)
vvset_name = self.client.findVolumeSet(volume_name)
LOG.debug("Returned vvset_name = %s" % vvset_name)
if vvset_name is not None and \
vvset_name.startswith('vvs-'):
# We have a single volume per volume set, so
# remove the volume set.
self._remove_volume_set(
self.client.deleteVolumeSet(
self._get_3par_vvs_name(volume['id']))
elif vvset_name is not None:
# We have a pre-defined volume set just remove the
# volume and leave the volume set.
self._remove_volume_from_volume_set(volume_name,
vvset_name)
self.client.removeVolumeFromVolumeSet(vvset_name,
volume_name)
self.client.deleteVolume(volume_name)
else:
LOG.error(str(ex))
@ -998,8 +861,7 @@ exit
volume_name = self._get_3par_vol_name(volume['id'])
if value is None:
value = ''
cmd = ['setvv', '-setkv', key + '=' + value, volume_name]
self._cli_run(cmd)
self.client.setVolumeMetaData(volume_name, key, value)
except Exception as ex:
msg = _('Failure in update_volume_key_value_pair:%s') % str(ex)
LOG.error(msg)
@ -1013,8 +875,7 @@ exit
self._get_3par_vol_name(volume['id']), str(key)))
try:
volume_name = self._get_3par_vol_name(volume['id'])
cmd = ['setvv', '-clrkey', key, volume_name]
self._cli_run(cmd)
self.client.removeVolumeMetaData(volume_name, key)
except Exception as ex:
msg = _('Failure in clear_volume_key_value_pair:%s') % str(ex)
LOG.error(msg)

View File

@ -54,10 +54,11 @@ class HP3PARFCDriver(cinder.volume.driver.FibreChannelDriver):
1.2.2 - Added try/finally around client login/logout.
1.2.3 - Added ability to add WWNs to host.
1.2.4 - Added metadata during attach/detach bug #1258033.
1.3.0 - Removed all SSH code. We rely on the hp3parclient now.
"""
VERSION = "1.2.4"
VERSION = "1.3.0"
def __init__(self, *args, **kwargs):
super(HP3PARFCDriver, self).__init__(*args, **kwargs)
@ -232,19 +233,22 @@ class HP3PARFCDriver(cinder.volume.driver.FibreChannelDriver):
the same wwn but with a different hostname, return the hostname
used by 3PAR.
"""
if domain is not None:
command = ['createhost', '-persona', persona_id, '-domain', domain,
hostname]
else:
command = ['createhost', '-persona', persona_id, hostname]
# first search for an existing host
host_found = None
for wwn in wwns:
command.append(wwn)
host_found = self.common.client.findHost(wwn=wwn)
if host_found is not None:
break
out = self.common._cli_run(command)
if out and len(out) > 1:
return self.common.parse_create_host_error(hostname, out)
return hostname
if host_found is not None:
self.common.hosts_naming_dict[hostname] = host_found
return host_found
else:
persona_id = int(persona_id)
self.common.client.createHost(hostname, FCWwns=wwns,
optional={'domain': domain,
'persona': persona_id})
return hostname
def _modify_3par_fibrechan_host(self, hostname, wwn):
mod_request = {'pathOperation': self.common.client.HOST_EDIT_ADD,

View File

@ -58,10 +58,11 @@ class HP3PARISCSIDriver(cinder.volume.driver.ISCSIDriver):
1.2.5 - Added metadata during attach/detach bug #1258033
1.2.6 - Use least-used iscsi n:s:p for iscsi volume attach bug #1269515
This update now requires 3.1.2 MU3 firmware
1.3.0 - Removed all SSH code. We rely on the hp3parclient now.
"""
VERSION = "1.2.6"
VERSION = "1.3.0"
def __init__(self, *args, **kwargs):
super(HP3PARISCSIDriver, self).__init__(*args, **kwargs)
@ -302,16 +303,21 @@ class HP3PARISCSIDriver(cinder.volume.driver.ISCSIDriver):
the same iqn but with a different hostname, return the hostname
used by 3PAR.
"""
if domain is not None:
cmd = ['createhost', '-iscsi', '-persona', persona_id, '-domain',
domain, hostname, iscsi_iqn]
# first search for an existing host
host_found = self.common.client.findHost(iqn=iscsi_iqn)
if host_found is not None:
self.common.hosts_naming_dict[hostname] = host_found
return host_found
else:
cmd = ['createhost', '-iscsi', '-persona', persona_id, hostname,
iscsi_iqn]
out = self.common._cli_run(cmd)
if out and len(out) > 1:
return self.common.parse_create_host_error(hostname, out)
return hostname
if isinstance(iscsi_iqn, str) or isinstance(iscsi_iqn, unicode):
iqn = [iscsi_iqn]
else:
iqn = iscsi_iqn
persona_id = int(persona_id)
self.common.client.createHost(hostname, iscsiNames=iqn,
optional={'domain': domain,
'persona': persona_id})
return hostname
def _modify_3par_iscsi_host(self, hostname, iscsi_iqn):
mod_request = {'pathOperation': self.common.client.HOST_EDIT_ADD,