From: Jacob Gregor Date: Thu, 1 Oct 2015 18:52:34 +0000 (-0500) Subject: Cleanup/move code in Storwize Driver X-Git-Url: https://review.fuel-infra.org/gitweb?a=commitdiff_plain;h=0b1bc1925cf7d2147c07a60f7b981b15b725b9c6;p=openstack-build%2Fcinder-build.git Cleanup/move code in Storwize Driver This patch updates the name of helper.py to storwize_svc_common.py. In addition, ssh.py has been collapsed into storwize_svc_common.py to reduce redundant code. Unit tests were also updated to reflect these updates. This patch is the first of a series of patches with the aim of bringing the storwize_svc driver in line with other san based Cinder drivers. Co-Authored By: Kendall Nelson Co-Authored By: Slade Baumann Partially implements: blueprint refactor-storwize-driver-for-mitaka Change-Id: I7725a008105ae54e49f90c74b32aa11713e983c9 --- diff --git a/cinder/tests/unit/test_storwize_svc.py b/cinder/tests/unit/test_storwize_svc.py index 8bf592aab..596f8912f 100644 --- a/cinder/tests/unit/test_storwize_svc.py +++ b/cinder/tests/unit/test_storwize_svc.py @@ -35,8 +35,7 @@ from cinder.tests.unit import utils as testutils from cinder import utils from cinder.volume import configuration as conf from cinder.volume.drivers.ibm import storwize_svc -from cinder.volume.drivers.ibm.storwize_svc import helpers -from cinder.volume.drivers.ibm.storwize_svc import ssh +from cinder.volume.drivers.ibm.storwize_svc import storwize_svc_common from cinder.volume import qos_specs from cinder.volume import volume_types @@ -1956,7 +1955,7 @@ class StorwizeSVCDriverTestCase(test.TestCase): 'nofmtdisk': False} return opt - @mock.patch.object(helpers.StorwizeHelpers, 'add_vdisk_qos') + @mock.patch.object(storwize_svc_common.StorwizeHelpers, 'add_vdisk_qos') @mock.patch.object(storwize_svc.StorwizeSVCDriver, '_get_vdisk_params') def test_storwize_svc_create_volume_with_qos(self, get_vdisk_params, add_vdisk_qos): @@ -1993,7 +1992,8 @@ class StorwizeSVCDriverTestCase(test.TestCase): self._reset_flags() # Test prestartfcmap failing - with mock.patch.object(ssh.StorwizeSSH, 'prestartfcmap') as prestart: + with mock.patch.object( + storwize_svc_common.StorwizeSSH, 'prestartfcmap') as prestart: prestart.side_effect = exception.VolumeBackendAPIException self.assertRaises(exception.VolumeBackendAPIException, self.driver.create_snapshot, snap1) @@ -2040,7 +2040,8 @@ class StorwizeSVCDriverTestCase(test.TestCase): snap_novol) # Fail the snapshot - with mock.patch.object(ssh.StorwizeSSH, 'prestartfcmap') as prestart: + with mock.patch.object( + storwize_svc_common.StorwizeSSH, 'prestartfcmap') as prestart: prestart.side_effect = exception.VolumeBackendAPIException self.assertRaises(exception.VolumeBackendAPIException, self.driver.create_volume_from_snapshot, @@ -2084,7 +2085,7 @@ class StorwizeSVCDriverTestCase(test.TestCase): self.driver.delete_volume(vol1) self._assert_vol_exists(vol1['name'], False) - @mock.patch.object(helpers.StorwizeHelpers, 'add_vdisk_qos') + @mock.patch.object(storwize_svc_common.StorwizeHelpers, 'add_vdisk_qos') def test_storwize_svc_create_volfromsnap_clone_with_qos(self, add_vdisk_qos): vol1 = self._create_volume() @@ -2395,7 +2396,7 @@ class StorwizeSVCDriverTestCase(test.TestCase): # lsfabric can return [] and initilize_connection will still # complete successfully - with mock.patch.object(helpers.StorwizeHelpers, + with mock.patch.object(storwize_svc_common.StorwizeHelpers, 'get_conn_fc_wwpns') as conn_fc_wwpns: conn_fc_wwpns.return_value = [] self._set_flag('storwize_svc_npiv_compatibility_mode', @@ -2813,8 +2814,10 @@ class StorwizeSVCDriverTestCase(test.TestCase): volume_types.destroy(self.ctxt, type_id) qos_specs.delete(self.ctxt, qos_spec['qos_specs']['id']) - @mock.patch.object(helpers.StorwizeHelpers, 'disable_vdisk_qos') - @mock.patch.object(helpers.StorwizeHelpers, 'update_vdisk_qos') + @mock.patch.object(storwize_svc_common.StorwizeHelpers, + 'disable_vdisk_qos') + @mock.patch.object(storwize_svc_common.StorwizeHelpers, + 'update_vdisk_qos') def test_storwize_svc_retype_no_copy(self, update_vdisk_qos, disable_vdisk_qos): self.driver.do_setup(None) @@ -2936,8 +2939,10 @@ class StorwizeSVCDriverTestCase(test.TestCase): 'failed') self.driver.delete_volume(volume) - @mock.patch.object(helpers.StorwizeHelpers, 'disable_vdisk_qos') - @mock.patch.object(helpers.StorwizeHelpers, 'update_vdisk_qos') + @mock.patch.object(storwize_svc_common.StorwizeHelpers, + 'disable_vdisk_qos') + @mock.patch.object(storwize_svc_common.StorwizeHelpers, + 'update_vdisk_qos') def test_storwize_svc_retype_need_copy(self, update_vdisk_qos, disable_vdisk_qos): self.driver.do_setup(None) @@ -3035,7 +3040,7 @@ class StorwizeSVCDriverTestCase(test.TestCase): self.assertEqual((7, 2, 0, 0), res['code_level'], 'Get code level error') - @mock.patch.object(helpers.StorwizeHelpers, 'rename_vdisk') + @mock.patch.object(storwize_svc_common.StorwizeHelpers, 'rename_vdisk') def test_storwize_update_migrated_volume(self, rename_vdisk): ctxt = testutils.get_test_admin_context() current_volume_id = 'fake_volume_id' @@ -3126,7 +3131,7 @@ class StorwizeSVCDriverTestCase(test.TestCase): wwpns = ['ff00000000000000', 'ff00000000000001'] connector = {'host': 'storwize-svc-test', 'wwpns': wwpns} - with mock.patch.object(helpers.StorwizeHelpers, + with mock.patch.object(storwize_svc_common.StorwizeHelpers, 'get_conn_fc_wwpns') as get_mappings: get_mappings.return_value = ['AABBCCDDEEFF0001', 'AABBCCDDEEFF0002', @@ -3160,7 +3165,7 @@ class StorwizeSVCDriverTestCase(test.TestCase): wwpns = ['ff00000000000000', 'ff00000000000001'] connector = {'host': 'storwize-svc-test', 'wwpns': wwpns} - with mock.patch.object(helpers.StorwizeHelpers, + with mock.patch.object(storwize_svc_common.StorwizeHelpers, 'get_conn_fc_wwpns') as get_mappings: get_mappings.return_value = ['AABBCCDDEEFF0001', 'AABBCCDDEEFF0002', @@ -3193,7 +3198,7 @@ class StorwizeSVCDriverTestCase(test.TestCase): wwpns = ['ff00000000000000', 'ff00000000000001'] connector = {'host': 'storwize-svc-test', 'wwpns': wwpns} - with mock.patch.object(helpers.StorwizeHelpers, + with mock.patch.object(storwize_svc_common.StorwizeHelpers, 'get_conn_fc_wwpns') as get_mappings: get_mappings.return_value = ['AABBCCDDEEFF0001', 'AABBCCDDEEFF0002', @@ -3514,7 +3519,7 @@ class StorwizeSVCDriverTestCase(test.TestCase): connector = {'host': 'storwize-svc-test', 'wwpns': wwpns} # Initialise the connection - with mock.patch.object(helpers.StorwizeHelpers, + with mock.patch.object(storwize_svc_common.StorwizeHelpers, 'get_conn_fc_wwpns') as conn_fc_wwpns: conn_fc_wwpns.return_value = [] init_ret = self.driver.initialize_connection(volume, connector) @@ -3785,15 +3790,17 @@ class StorwizeSVCDriverTestCase(test.TestCase): class CLIResponseTestCase(test.TestCase): def test_empty(self): - self.assertEqual(0, len(ssh.CLIResponse(''))) - self.assertEqual(0, len(ssh.CLIResponse(('', 'stderr')))) + self.assertEqual(0, len( + storwize_svc_common.CLIResponse(''))) + self.assertEqual(0, len( + storwize_svc_common.CLIResponse(('', 'stderr')))) def test_header(self): raw = r'''id!name 1!node1 2!node2 ''' - resp = ssh.CLIResponse(raw, with_header=True) + resp = storwize_svc_common.CLIResponse(raw, with_header=True) self.assertEqual(2, len(resp)) self.assertEqual('1', resp[0]['id']) self.assertEqual('2', resp[1]['id']) @@ -3813,7 +3820,7 @@ age!40 home address!s3 home address!s4 ''' - resp = ssh.CLIResponse(raw, with_header=False) + resp = storwize_svc_common.CLIResponse(raw, with_header=False) self.assertEqual([('s1', 'Bill', 's1'), ('s2', 'Bill2', 's2'), ('s3', 'John', 's3'), ('s4', 'John2', 's4')], list(resp.select('home address', 'name', @@ -3824,7 +3831,7 @@ home address!s4 1!node1!!500507680200C744!online 2!node2!!500507680200C745!online ''' - resp = ssh.CLIResponse(raw) + resp = storwize_svc_common.CLIResponse(raw) self.assertEqual(2, len(resp)) self.assertEqual('1', resp[0]['id']) self.assertEqual('500507680200C744', resp[0]['WWNN']) @@ -3840,7 +3847,7 @@ port_id!500507680240C744 port_status!inactive port_speed!8Gb ''' - resp = ssh.CLIResponse(raw, with_header=False) + resp = storwize_svc_common.CLIResponse(raw, with_header=False) self.assertEqual(1, len(resp)) self.assertEqual('1', resp[0]['id']) self.assertEqual([('500507680210C744', 'active'), @@ -3851,7 +3858,7 @@ port_speed!8Gb class StorwizeHelpersTestCase(test.TestCase): def setUp(self): super(StorwizeHelpersTestCase, self).setUp() - self.helpers = helpers.StorwizeHelpers(None) + self.storwize_svc_common = storwize_svc_common.StorwizeHelpers(None) def test_compression_enabled(self): fake_license_without_keys = {} @@ -3862,10 +3869,12 @@ class StorwizeHelpersTestCase(test.TestCase): # Check when keys of return licenses do not contain # 'license_compression_enclosures' and 'license_compression_capacity' - with mock.patch.object(ssh.StorwizeSSH, 'lslicense') as lslicense: + with mock.patch.object( + storwize_svc_common.StorwizeSSH, 'lslicense') as lslicense: lslicense.return_value = fake_license_without_keys - self.assertFalse(self.helpers.compression_enabled()) + self.assertFalse(self.storwize_svc_common.compression_enabled()) - with mock.patch.object(ssh.StorwizeSSH, 'lslicense') as lslicense: + with mock.patch.object( + storwize_svc_common.StorwizeSSH, 'lslicense') as lslicense: lslicense.return_value = fake_license - self.assertTrue(self.helpers.compression_enabled()) + self.assertTrue(self.storwize_svc_common.compression_enabled()) diff --git a/cinder/volume/drivers/ibm/storwize_svc/__init__.py b/cinder/volume/drivers/ibm/storwize_svc/__init__.py index e33c62aef..ac03b394f 100644 --- a/cinder/volume/drivers/ibm/storwize_svc/__init__.py +++ b/cinder/volume/drivers/ibm/storwize_svc/__init__.py @@ -49,8 +49,10 @@ from cinder import exception from cinder.i18n import _, _LE, _LI, _LW from cinder import utils from cinder.volume import driver -from cinder.volume.drivers.ibm.storwize_svc import helpers as storwize_helpers -from cinder.volume.drivers.ibm.storwize_svc import replication as storwize_rep +from cinder.volume.drivers.ibm.storwize_svc import ( + replication as storwize_rep) +from cinder.volume.drivers.ibm.storwize_svc import ( + storwize_svc_common as storwize_helpers) from cinder.volume.drivers.san import san from cinder.volume import volume_types from cinder.zonemanager import utils as fczm_utils diff --git a/cinder/volume/drivers/ibm/storwize_svc/ssh.py b/cinder/volume/drivers/ibm/storwize_svc/ssh.py deleted file mode 100644 index 11596dee5..000000000 --- a/cinder/volume/drivers/ibm/storwize_svc/ssh.py +++ /dev/null @@ -1,470 +0,0 @@ -# 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 oslo_concurrency import processutils -from oslo_log import log as logging -import six - -from cinder import exception -from cinder.i18n import _, _LE - -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', '!', - '"%s"' % 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): - ssh_cmd = ['svcinfo', 'lsfabric', '-delim', '!'] - if wwpn: - ssh_cmd.extend(['-wwpn', wwpn]) - elif host: - ssh_cmd.extend(['-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(_LE('storwize_svc_multihostmap_enabled is set ' - 'to False, not allowing multi host mapping.')) - raise exception.VolumeDriverException( - message=_('CMMVC6071E The VDisk-to-host mapping was not ' - 'created because the VDisk is already mapped ' - 'to a host.\n"')) - - 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', - '"%s"' % 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 lsvdisks_from_filter(self, filter_name, value): - """Performs an lsvdisk command, filtering the results as specified. - - Returns an iterable for all matching vdisks. - """ - ssh_cmd = ['svcinfo', 'lsvdisk', '-bytes', '-delim', '!', - '-filtervalue', '%s=%s' % (filter_name, value)] - return self.run_ssh_info(ssh_cmd, with_header=True) - - 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 mkfcmap(self, source, target, full_copy, consistgrp=None): - ssh_cmd = ['svctask', 'mkfcmap', '-source', source, '-target', - target, '-autodelete'] - if not full_copy: - ssh_cmd.extend(['-copyrate', '0']) - if consistgrp: - ssh_cmd.extend(['-consistgrp', consistgrp]) - 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 prestartfcconsistgrp(self, fc_consist_group): - ssh_cmd = ['svctask', 'prestartfcconsistgrp', fc_consist_group] - self.run_ssh_assert_no_output(ssh_cmd) - - def startfcconsistgrp(self, fc_consist_group): - ssh_cmd = ['svctask', 'startfcconsistgrp', fc_consist_group] - self.run_ssh_assert_no_output(ssh_cmd) - - def stopfcconsistgrp(self, fc_consist_group): - ssh_cmd = ['svctask', 'stopfcconsistgrp', fc_consist_group] - 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 lsfcconsistgrp(self, fc_consistgrp): - ssh_cmd = ['svcinfo', 'lsfcconsistgrp', '-delim', '!', fc_consistgrp] - out, err = self._ssh(ssh_cmd) - return CLIResponse((out, err), ssh_cmd=ssh_cmd, delim='!', - with_header=False) - - def mkfcconsistgrp(self, fc_consist_group): - ssh_cmd = ['svctask', 'mkfcconsistgrp', '-name', fc_consist_group] - return self.run_ssh_check_created(ssh_cmd) - - def rmfcconsistgrp(self, fc_consist_group): - ssh_cmd = ['svctask', 'rmfcconsistgrp', '-force', fc_consist_group] - return self.run_ssh_assert_no_output(ssh_cmd) - - def addvdiskcopy(self, vdisk, dest_pool, params): - ssh_cmd = (['svctask', 'addvdiskcopy'] + params + ['-mdiskgrp', - '"%s"' % 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 lsvdisksyncprogress(self, vdisk, copy_id): - ssh_cmd = ['svcinfo', 'lsvdisksyncprogress', '-delim', '!', - '-copy', copy_id, vdisk] - return self.run_ssh_info(ssh_cmd, with_header=True)[0] - - def rmvdiskcopy(self, vdisk, copy_id): - ssh_cmd = ['svctask', 'rmvdiskcopy', '-copy', copy_id, vdisk] - self.run_ssh_assert_no_output(ssh_cmd) - - def addvdiskaccess(self, vdisk, iogrp): - ssh_cmd = ['svctask', 'addvdiskaccess', '-iogrp', iogrp, vdisk] - self.run_ssh_assert_no_output(ssh_cmd) - - def rmvdiskaccess(self, vdisk, iogrp): - ssh_cmd = ['svctask', 'rmvdiskaccess', '-iogrp', iogrp, vdisk] - self.run_ssh_assert_no_output(ssh_cmd) - - def lsportfc(self, node_id): - ssh_cmd = ['svcinfo', 'lsportfc', '-delim', '!', - '-filtervalue', 'node_id=%s' % node_id] - return self.run_ssh_info(ssh_cmd, with_header=True) - - -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, six.string_types) 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 the 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, six.string_types): - 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': hds, - 'row': 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_ diff --git a/cinder/volume/drivers/ibm/storwize_svc/helpers.py b/cinder/volume/drivers/ibm/storwize_svc/storwize_svc_common.py similarity index 72% rename from cinder/volume/drivers/ibm/storwize_svc/helpers.py rename to cinder/volume/drivers/ibm/storwize_svc/storwize_svc_common.py index deef1fbf6..c7050967d 100644 --- a/cinder/volume/drivers/ibm/storwize_svc/helpers.py +++ b/cinder/volume/drivers/ibm/storwize_svc/storwize_svc_common.py @@ -21,6 +21,7 @@ import unicodedata from eventlet import greenthread +from oslo_concurrency import processutils from oslo_log import log as logging from oslo_service import loopingcall from oslo_utils import excutils @@ -30,7 +31,6 @@ import six from cinder import context from cinder import exception from cinder.i18n import _, _LE, _LI, _LW -from cinder.volume.drivers.ibm.storwize_svc import ssh as storwize_ssh from cinder.volume import qos_specs from cinder.volume import utils from cinder.volume import volume_types @@ -40,6 +40,348 @@ DEFAULT_TIMEOUT = 15 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', '!', + '"%s"' % 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): + ssh_cmd = ['svcinfo', 'lsfabric', '-delim', '!'] + if wwpn: + ssh_cmd.extend(['-wwpn', wwpn]) + elif host: + ssh_cmd.extend(['-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(_LE('storwize_svc_multihostmap_enabled is set ' + 'to False, not allowing multi host mapping.')) + raise exception.VolumeDriverException( + message=_('CMMVC6071E The VDisk-to-host mapping was not ' + 'created because the VDisk is already mapped ' + 'to a host.\n"')) + + 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', + '"%s"' % pool, '-iogrp', six.text_type(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 lsvdisks_from_filter(self, filter_name, value): + """Performs an lsvdisk command, filtering the results as specified. + + Returns an iterable for all matching vdisks. + """ + ssh_cmd = ['svcinfo', 'lsvdisk', '-bytes', '-delim', '!', + '-filtervalue', '%s=%s' % (filter_name, value)] + return self.run_ssh_info(ssh_cmd, with_header=True) + + 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', six.text_type(amount), + '-unit', 'gb', vdisk]) + self.run_ssh_assert_no_output(ssh_cmd) + + def mkfcmap(self, source, target, full_copy, consistgrp=None): + ssh_cmd = ['svctask', 'mkfcmap', '-source', source, '-target', + target, '-autodelete'] + if not full_copy: + ssh_cmd.extend(['-copyrate', '0']) + if consistgrp: + ssh_cmd.extend(['-consistgrp', consistgrp]) + 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 prestartfcconsistgrp(self, fc_consist_group): + ssh_cmd = ['svctask', 'prestartfcconsistgrp', fc_consist_group] + self.run_ssh_assert_no_output(ssh_cmd) + + def startfcconsistgrp(self, fc_consist_group): + ssh_cmd = ['svctask', 'startfcconsistgrp', fc_consist_group] + self.run_ssh_assert_no_output(ssh_cmd) + + def stopfcconsistgrp(self, fc_consist_group): + ssh_cmd = ['svctask', 'stopfcconsistgrp', fc_consist_group] + 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 lsfcconsistgrp(self, fc_consistgrp): + ssh_cmd = ['svcinfo', 'lsfcconsistgrp', '-delim', '!', fc_consistgrp] + out, err = self._ssh(ssh_cmd) + return CLIResponse((out, err), ssh_cmd=ssh_cmd, delim='!', + with_header=False) + + def mkfcconsistgrp(self, fc_consist_group): + ssh_cmd = ['svctask', 'mkfcconsistgrp', '-name', fc_consist_group] + return self.run_ssh_check_created(ssh_cmd) + + def rmfcconsistgrp(self, fc_consist_group): + ssh_cmd = ['svctask', 'rmfcconsistgrp', '-force', fc_consist_group] + return self.run_ssh_assert_no_output(ssh_cmd) + + def addvdiskcopy(self, vdisk, dest_pool, params): + ssh_cmd = (['svctask', 'addvdiskcopy'] + params + ['-mdiskgrp', + '"%s"' % 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 lsvdisksyncprogress(self, vdisk, copy_id): + ssh_cmd = ['svcinfo', 'lsvdisksyncprogress', '-delim', '!', + '-copy', copy_id, vdisk] + return self.run_ssh_info(ssh_cmd, with_header=True)[0] + + def rmvdiskcopy(self, vdisk, copy_id): + ssh_cmd = ['svctask', 'rmvdiskcopy', '-copy', copy_id, vdisk] + self.run_ssh_assert_no_output(ssh_cmd) + + def addvdiskaccess(self, vdisk, iogrp): + ssh_cmd = ['svctask', 'addvdiskaccess', '-iogrp', iogrp, vdisk] + self.run_ssh_assert_no_output(ssh_cmd) + + def rmvdiskaccess(self, vdisk, iogrp): + ssh_cmd = ['svctask', 'rmvdiskaccess', '-iogrp', iogrp, vdisk] + self.run_ssh_assert_no_output(ssh_cmd) + + def lsportfc(self, node_id): + ssh_cmd = ['svcinfo', 'lsportfc', '-delim', '!', + '-filtervalue', 'node_id=%s' % node_id] + return self.run_ssh_info(ssh_cmd, with_header=True) + + class StorwizeHelpers(object): # All the supported QoS key are saved in this dict. When a new @@ -52,7 +394,7 @@ class StorwizeHelpers(object): 'type': int}} def __init__(self, run_ssh): - self.ssh = storwize_ssh.StorwizeSSH(run_ssh) + self.ssh = StorwizeSSH(run_ssh) self.check_fcmapping_interval = 3 @staticmethod @@ -1091,3 +1433,106 @@ class StorwizeHelpers(object): def change_vdisk_primary_copy(self, vdisk, copy_id): self.ssh.chvdisk(vdisk, ['-primary', copy_id]) + + +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, six.string_types) 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 the 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, six.string_types): + 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': hds, + 'row': 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_