414 lines
15 KiB
Python
414 lines
15 KiB
Python
# Copyright 2014 IBM Corp.
|
|
# All Rights Reserved.
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
# not use this file except in compliance with the License. You may obtain
|
|
# a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
# License for the specific language governing permissions and limitations
|
|
# under the License.
|
|
#
|
|
|
|
import re
|
|
|
|
from cinder import exception
|
|
from cinder.openstack.common import log as logging
|
|
from cinder.openstack.common import processutils
|
|
|
|
LOG = logging.getLogger(__name__)
|
|
|
|
|
|
class StorwizeSSH(object):
|
|
"""SSH interface to IBM Storwize family and SVC storage systems."""
|
|
def __init__(self, run_ssh):
|
|
self._ssh = run_ssh
|
|
|
|
def _run_ssh(self, ssh_cmd):
|
|
try:
|
|
return self._ssh(ssh_cmd)
|
|
except processutils.ProcessExecutionError as e:
|
|
msg = (_('CLI Exception output:\n command: %(cmd)s\n '
|
|
'stdout: %(out)s\n stderr: %(err)s') %
|
|
{'cmd': ssh_cmd,
|
|
'out': e.stdout,
|
|
'err': e.stderr})
|
|
LOG.error(msg)
|
|
raise exception.VolumeBackendAPIException(data=msg)
|
|
|
|
def run_ssh_info(self, ssh_cmd, delim='!', with_header=False):
|
|
"""Run an SSH command and return parsed output."""
|
|
raw = self._run_ssh(ssh_cmd)
|
|
return CLIResponse(raw, ssh_cmd=ssh_cmd, delim=delim,
|
|
with_header=with_header)
|
|
|
|
def run_ssh_assert_no_output(self, ssh_cmd):
|
|
"""Run an SSH command and assert no output returned."""
|
|
out, err = self._run_ssh(ssh_cmd)
|
|
if len(out.strip()) != 0:
|
|
msg = (_('Expected no output from CLI command %(cmd)s, '
|
|
'got %(out)s') % {'cmd': ' '.join(ssh_cmd), 'out': out})
|
|
LOG.error(msg)
|
|
raise exception.VolumeBackendAPIException(data=msg)
|
|
|
|
def run_ssh_check_created(self, ssh_cmd):
|
|
"""Run an SSH command and return the ID of the created object."""
|
|
out, err = self._run_ssh(ssh_cmd)
|
|
try:
|
|
match_obj = re.search(r'\[([0-9]+)\],? successfully created', out)
|
|
return match_obj.group(1)
|
|
except (AttributeError, IndexError):
|
|
msg = (_('Failed to parse CLI output:\n command: %(cmd)s\n '
|
|
'stdout: %(out)s\n stderr: %(err)s') %
|
|
{'cmd': ssh_cmd,
|
|
'out': out,
|
|
'err': err})
|
|
LOG.error(msg)
|
|
raise exception.VolumeBackendAPIException(data=msg)
|
|
|
|
def lsnode(self, node_id=None):
|
|
with_header = True
|
|
ssh_cmd = ['svcinfo', 'lsnode', '-delim', '!']
|
|
if node_id:
|
|
with_header = False
|
|
ssh_cmd.append(node_id)
|
|
return self.run_ssh_info(ssh_cmd, with_header=with_header)
|
|
|
|
def lslicense(self):
|
|
ssh_cmd = ['svcinfo', 'lslicense', '-delim', '!']
|
|
return self.run_ssh_info(ssh_cmd)[0]
|
|
|
|
def lssystem(self):
|
|
ssh_cmd = ['svcinfo', 'lssystem', '-delim', '!']
|
|
return self.run_ssh_info(ssh_cmd)[0]
|
|
|
|
def lsmdiskgrp(self, pool):
|
|
ssh_cmd = ['svcinfo', 'lsmdiskgrp', '-bytes', '-delim', '!', pool]
|
|
return self.run_ssh_info(ssh_cmd)[0]
|
|
|
|
def lsiogrp(self):
|
|
ssh_cmd = ['svcinfo', 'lsiogrp', '-delim', '!']
|
|
return self.run_ssh_info(ssh_cmd, with_header=True)
|
|
|
|
def lsportip(self):
|
|
ssh_cmd = ['svcinfo', 'lsportip', '-delim', '!']
|
|
return self.run_ssh_info(ssh_cmd, with_header=True)
|
|
|
|
@staticmethod
|
|
def _create_port_arg(port_type, port_name):
|
|
if port_type == 'initiator':
|
|
port = ['-iscsiname']
|
|
else:
|
|
port = ['-hbawwpn']
|
|
port.append(port_name)
|
|
return port
|
|
|
|
def mkhost(self, host_name, port_type, port_name):
|
|
port = self._create_port_arg(port_type, port_name)
|
|
ssh_cmd = ['svctask', 'mkhost', '-force'] + port
|
|
ssh_cmd += ['-name', '"%s"' % host_name]
|
|
return self.run_ssh_check_created(ssh_cmd)
|
|
|
|
def addhostport(self, host, port_type, port_name):
|
|
port = self._create_port_arg(port_type, port_name)
|
|
ssh_cmd = ['svctask', 'addhostport', '-force'] + port + ['"%s"' % host]
|
|
self.run_ssh_assert_no_output(ssh_cmd)
|
|
|
|
def lshost(self, host=None):
|
|
with_header = True
|
|
ssh_cmd = ['svcinfo', 'lshost', '-delim', '!']
|
|
if host:
|
|
with_header = False
|
|
ssh_cmd.append('"%s"' % host)
|
|
return self.run_ssh_info(ssh_cmd, with_header=with_header)
|
|
|
|
def add_chap_secret(self, secret, host):
|
|
ssh_cmd = ['svctask', 'chhost', '-chapsecret', secret, '"%s"' % host]
|
|
self.run_ssh_assert_no_output(ssh_cmd)
|
|
|
|
def lsiscsiauth(self):
|
|
ssh_cmd = ['svcinfo', 'lsiscsiauth', '-delim', '!']
|
|
return self.run_ssh_info(ssh_cmd, with_header=True)
|
|
|
|
def lsfabric(self, wwpn=None, host=None):
|
|
if wwpn:
|
|
ssh_cmd = ['svcinfo', 'lsfabric', '-wwpn', wwpn, '-delim', '!']
|
|
elif host:
|
|
ssh_cmd = ['svcinfo', 'lsfabric', '-host', '"%s"' % host]
|
|
else:
|
|
msg = (_('Must pass wwpn or host to lsfabric.'))
|
|
LOG.error(msg)
|
|
raise exception.VolumeDriverException(message=msg)
|
|
return self.run_ssh_info(ssh_cmd, with_header=True)
|
|
|
|
def mkvdiskhostmap(self, host, vdisk, lun, multihostmap):
|
|
"""Map vdisk to host.
|
|
|
|
If vdisk already mapped and multihostmap is True, use the force flag.
|
|
"""
|
|
ssh_cmd = ['svctask', 'mkvdiskhostmap', '-host', '"%s"' % host,
|
|
'-scsi', lun, vdisk]
|
|
out, err = self._ssh(ssh_cmd, check_exit_code=False)
|
|
if 'successfully created' in out:
|
|
return
|
|
if not err:
|
|
msg = (_('Did not find success message nor error for %(fun)s: '
|
|
'%(out)s') % {'out': out, 'fun': ssh_cmd})
|
|
raise exception.VolumeBackendAPIException(data=msg)
|
|
if err.startswith('CMMVC6071E'):
|
|
if not multihostmap:
|
|
LOG.error(_('storwize_svc_multihostmap_enabled is set '
|
|
'to False, not allowing multi host mapping.'))
|
|
msg = 'CMMVC6071E The VDisk-to-host mapping '\
|
|
'was not created because the VDisk is '\
|
|
'already mapped to a host.\n"'
|
|
raise exception.VolumeDriverException(message=msg)
|
|
|
|
ssh_cmd.insert(ssh_cmd.index('mkvdiskhostmap') + 1, '-force')
|
|
return self.run_ssh_check_created(ssh_cmd)
|
|
|
|
def rmvdiskhostmap(self, host, vdisk):
|
|
ssh_cmd = ['svctask', 'rmvdiskhostmap', '-host', '"%s"' % host, vdisk]
|
|
self.run_ssh_assert_no_output(ssh_cmd)
|
|
|
|
def lsvdiskhostmap(self, vdisk):
|
|
ssh_cmd = ['svcinfo', 'lsvdiskhostmap', '-delim', '!', vdisk]
|
|
return self.run_ssh_info(ssh_cmd, with_header=True)
|
|
|
|
def lshostvdiskmap(self, host):
|
|
ssh_cmd = ['svcinfo', 'lshostvdiskmap', '-delim', '!', '"%s"' % host]
|
|
return self.run_ssh_info(ssh_cmd, with_header=True)
|
|
|
|
def rmhost(self, host):
|
|
ssh_cmd = ['svctask', 'rmhost', '"%s"' % host]
|
|
self.run_ssh_assert_no_output(ssh_cmd)
|
|
|
|
def mkvdisk(self, name, size, units, pool, opts, params):
|
|
ssh_cmd = ['svctask', 'mkvdisk', '-name', name, '-mdiskgrp', pool,
|
|
'-iogrp', str(opts['iogrp']), '-size', size, '-unit',
|
|
units] + params
|
|
return self.run_ssh_check_created(ssh_cmd)
|
|
|
|
def rmvdisk(self, vdisk, force=True):
|
|
ssh_cmd = ['svctask', 'rmvdisk']
|
|
if force:
|
|
ssh_cmd += ['-force']
|
|
ssh_cmd += [vdisk]
|
|
self.run_ssh_assert_no_output(ssh_cmd)
|
|
|
|
def lsvdisk(self, vdisk):
|
|
"""Return vdisk attributes or None if it doesn't exist."""
|
|
ssh_cmd = ['svcinfo', 'lsvdisk', '-bytes', '-delim', '!', vdisk]
|
|
out, err = self._ssh(ssh_cmd, check_exit_code=False)
|
|
if not len(err):
|
|
return CLIResponse((out, err), ssh_cmd=ssh_cmd, delim='!',
|
|
with_header=False)[0]
|
|
if err.startswith('CMMVC5754E'):
|
|
return None
|
|
msg = (_('CLI Exception output:\n command: %(cmd)s\n '
|
|
'stdout: %(out)s\n stderr: %(err)s') %
|
|
{'cmd': ssh_cmd,
|
|
'out': out,
|
|
'err': err})
|
|
LOG.error(msg)
|
|
raise exception.VolumeBackendAPIException(data=msg)
|
|
|
|
def chvdisk(self, vdisk, params):
|
|
ssh_cmd = ['svctask', 'chvdisk'] + params + [vdisk]
|
|
self.run_ssh_assert_no_output(ssh_cmd)
|
|
|
|
def movevdisk(self, vdisk, iogrp):
|
|
ssh_cmd = ['svctask', 'movevdisk', '-iogrp', iogrp, vdisk]
|
|
self.run_ssh_assert_no_output(ssh_cmd)
|
|
|
|
def expandvdisksize(self, vdisk, amount):
|
|
ssh_cmd = (['svctask', 'expandvdisksize', '-size', str(amount),
|
|
'-unit', 'gb', vdisk])
|
|
self.run_ssh_assert_no_output(ssh_cmd)
|
|
|
|
def migratevdisk(self, vdisk, dest_pool):
|
|
ssh_cmd = ['svctask', 'migratevdisk', '-mdiskgrp', dest_pool,
|
|
'-vdisk', vdisk]
|
|
self.run_ssh_assert_no_output(ssh_cmd)
|
|
|
|
def mkfcmap(self, source, target, full_copy):
|
|
ssh_cmd = ['svctask', 'mkfcmap', '-source', source, '-target',
|
|
target, '-autodelete']
|
|
if not full_copy:
|
|
ssh_cmd.extend(['-copyrate', '0'])
|
|
out, err = self._ssh(ssh_cmd, check_exit_code=False)
|
|
if 'successfully created' not in out:
|
|
msg = (_('CLI Exception output:\n command: %(cmd)s\n '
|
|
'stdout: %(out)s\n stderr: %(err)s') %
|
|
{'cmd': ssh_cmd,
|
|
'out': out,
|
|
'err': err})
|
|
LOG.error(msg)
|
|
raise exception.VolumeBackendAPIException(data=msg)
|
|
try:
|
|
match_obj = re.search(r'FlashCopy Mapping, id \[([0-9]+)\], '
|
|
'successfully created', out)
|
|
fc_map_id = match_obj.group(1)
|
|
except (AttributeError, IndexError):
|
|
msg = (_('Failed to parse CLI output:\n command: %(cmd)s\n '
|
|
'stdout: %(out)s\n stderr: %(err)s') %
|
|
{'cmd': ssh_cmd,
|
|
'out': out,
|
|
'err': err})
|
|
LOG.error(msg)
|
|
raise exception.VolumeBackendAPIException(data=msg)
|
|
return fc_map_id
|
|
|
|
def prestartfcmap(self, fc_map_id):
|
|
ssh_cmd = ['svctask', 'prestartfcmap', fc_map_id]
|
|
self.run_ssh_assert_no_output(ssh_cmd)
|
|
|
|
def startfcmap(self, fc_map_id):
|
|
ssh_cmd = ['svctask', 'startfcmap', fc_map_id]
|
|
self.run_ssh_assert_no_output(ssh_cmd)
|
|
|
|
def chfcmap(self, fc_map_id, copyrate='50', autodel='on'):
|
|
ssh_cmd = ['svctask', 'chfcmap', '-copyrate', copyrate,
|
|
'-autodelete', autodel, fc_map_id]
|
|
self.run_ssh_assert_no_output(ssh_cmd)
|
|
|
|
def stopfcmap(self, fc_map_id):
|
|
ssh_cmd = ['svctask', 'stopfcmap', fc_map_id]
|
|
self.run_ssh_assert_no_output(ssh_cmd)
|
|
|
|
def rmfcmap(self, fc_map_id):
|
|
ssh_cmd = ['svctask', 'rmfcmap', '-force', fc_map_id]
|
|
self.run_ssh_assert_no_output(ssh_cmd)
|
|
|
|
def lsvdiskfcmappings(self, vdisk):
|
|
ssh_cmd = ['svcinfo', 'lsvdiskfcmappings', '-delim', '!', vdisk]
|
|
return self.run_ssh_info(ssh_cmd, with_header=True)
|
|
|
|
def lsfcmap(self, fc_map_id):
|
|
ssh_cmd = ['svcinfo', 'lsfcmap', '-filtervalue',
|
|
'id=%s' % fc_map_id, '-delim', '!']
|
|
return self.run_ssh_info(ssh_cmd, with_header=True)
|
|
|
|
def addvdiskcopy(self, vdisk, dest_pool, params):
|
|
ssh_cmd = (['svctask', 'addvdiskcopy'] + params + ['-mdiskgrp',
|
|
dest_pool, vdisk])
|
|
return self.run_ssh_check_created(ssh_cmd)
|
|
|
|
def lsvdiskcopy(self, vdisk, copy_id=None):
|
|
ssh_cmd = ['svcinfo', 'lsvdiskcopy', '-delim', '!']
|
|
with_header = True
|
|
if copy_id:
|
|
ssh_cmd += ['-copy', copy_id]
|
|
with_header = False
|
|
ssh_cmd += [vdisk]
|
|
return self.run_ssh_info(ssh_cmd, with_header=with_header)
|
|
|
|
def rmvdiskcopy(self, vdisk, copy_id):
|
|
ssh_cmd = ['svctask', 'rmvdiskcopy', '-copy', copy_id, vdisk]
|
|
self.run_ssh_assert_no_output(ssh_cmd)
|
|
|
|
|
|
class CLIResponse(object):
|
|
'''Parse SVC CLI output and generate iterable.'''
|
|
|
|
def __init__(self, raw, ssh_cmd=None, delim='!', with_header=True):
|
|
super(CLIResponse, self).__init__()
|
|
if ssh_cmd:
|
|
self.ssh_cmd = ' '.join(ssh_cmd)
|
|
else:
|
|
self.ssh_cmd = 'None'
|
|
self.raw = raw
|
|
self.delim = delim
|
|
self.with_header = with_header
|
|
self.result = self._parse()
|
|
|
|
def select(self, *keys):
|
|
for a in self.result:
|
|
vs = []
|
|
for k in keys:
|
|
v = a.get(k, None)
|
|
if isinstance(v, basestring) or v is None:
|
|
v = [v]
|
|
if isinstance(v, list):
|
|
vs.append(v)
|
|
for item in zip(*vs):
|
|
if len(item) == 1:
|
|
yield item[0]
|
|
else:
|
|
yield item
|
|
|
|
def __getitem__(self, key):
|
|
try:
|
|
return self.result[key]
|
|
except KeyError:
|
|
msg = (_('Did not find expected key %(key)s in %(fun)s: %(raw)s') %
|
|
{'key': key, 'fun': self.ssh_cmd, 'raw': self.raw})
|
|
raise exception.VolumeBackendAPIException(data=msg)
|
|
|
|
def __iter__(self):
|
|
for a in self.result:
|
|
yield a
|
|
|
|
def __len__(self):
|
|
return len(self.result)
|
|
|
|
def _parse(self):
|
|
def get_reader(content, delim):
|
|
for line in content.lstrip().splitlines():
|
|
line = line.strip()
|
|
if line:
|
|
yield line.split(delim)
|
|
else:
|
|
yield []
|
|
|
|
if isinstance(self.raw, basestring):
|
|
stdout, stderr = self.raw, ''
|
|
else:
|
|
stdout, stderr = self.raw
|
|
reader = get_reader(stdout, self.delim)
|
|
result = []
|
|
|
|
if self.with_header:
|
|
hds = tuple()
|
|
for row in reader:
|
|
hds = row
|
|
break
|
|
for row in reader:
|
|
cur = dict()
|
|
if len(hds) != len(row):
|
|
msg = (_('Unexpected CLI response: header/row mismatch. '
|
|
'header: %(header)s, row: %(row)s')
|
|
% {'header': str(hds), 'row': str(row)})
|
|
raise exception.VolumeBackendAPIException(data=msg)
|
|
for k, v in zip(hds, row):
|
|
CLIResponse.append_dict(cur, k, v)
|
|
result.append(cur)
|
|
else:
|
|
cur = dict()
|
|
for row in reader:
|
|
if row:
|
|
CLIResponse.append_dict(cur, row[0], ' '.join(row[1:]))
|
|
elif cur: # start new section
|
|
result.append(cur)
|
|
cur = dict()
|
|
if cur:
|
|
result.append(cur)
|
|
return result
|
|
|
|
@staticmethod
|
|
def append_dict(dict_, key, value):
|
|
key, value = key.strip(), value.strip()
|
|
obj = dict_.get(key, None)
|
|
if obj is None:
|
|
dict_[key] = value
|
|
elif isinstance(obj, list):
|
|
obj.append(value)
|
|
dict_[key] = obj
|
|
else:
|
|
dict_[key] = [obj, value]
|
|
return dict_
|