]> review.fuel-infra Code Review - openstack-build/cinder-build.git/commitdiff
Cleanup/move code in Storwize Driver
authorJacob Gregor <jgregor@us.ibm.com>
Thu, 1 Oct 2015 18:52:34 +0000 (13:52 -0500)
committerJohn Griffith <john.griffith8@gmail.com>
Tue, 13 Oct 2015 18:25:56 +0000 (18:25 +0000)
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 <kjnelson@us.ibm.com>
Co-Authored By: Slade Baumann <baumann@us.ibm.com>

Partially implements: blueprint refactor-storwize-driver-for-mitaka

Change-Id: I7725a008105ae54e49f90c74b32aa11713e983c9

cinder/tests/unit/test_storwize_svc.py
cinder/volume/drivers/ibm/storwize_svc/__init__.py
cinder/volume/drivers/ibm/storwize_svc/ssh.py [deleted file]
cinder/volume/drivers/ibm/storwize_svc/storwize_svc_common.py [moved from cinder/volume/drivers/ibm/storwize_svc/helpers.py with 72% similarity]

index 8bf592aab0e7707b5d710645fbfd071a1ab52385..596f8912f87a164f1c119c9d0b7dd1a515a6247c 100644 (file)
@@ -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())
index e33c62aef129d8f4305359c80ee2a87d61f5252a..ac03b394f85e09f2fcc41564f6479a48b92f67bd 100644 (file)
@@ -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 (file)
index 11596de..0000000
+++ /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_
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 deef1fbf60db9beb58d5351d9dfeea6fb1f2d748..c7050967d3a1eb0ed1e0282972941dbb2bcaa790 100644 (file)
@@ -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_