Repository: sodafoundation/delfin

Issue:
Update DELLEMC VMAX driver for supporting Controller and Port resources
**Issue/Feature Description:**
Update DELLEMC VMAX driver for supporting Controller and Port resources

**Why this issue to fixed / feature is needed(give scenarios or use cases):**

**How to reproduce, in case of a bug:**

**Other Notes /  Environment Information: (Please give the env information, log link or any useful information for this issue)**



Discussion:
codecov[bot]: # [Codecov](https://codecov.io/gh/sodafoundation/delfin/pull/663?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=sodafoundation) Report
> Merging [#663](https://codecov.io/gh/sodafoundation/delfin/pull/663?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=sodafoundation) (a276148) into [master](https://codecov.io/gh/sodafoundation/delfin/commit/2dd529b0ce0d581871368c08ae5de291300226ba?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=sodafoundation) (2dd529b) will **decrease** coverage by `0.13%`.
> The diff coverage is `51.61%`.


```diff
@@            Coverage Diff             @@
##           master     #663      +/-   ##
==========================================
- Coverage   70.14%   70.01%   -0.14%     
==========================================
  Files         156      156              
  Lines       14411    14528     +117     
  Branches     1737     1764      +27     
==========================================
+ Hits        10109    10172      +63     
- Misses       3749     3781      +32     
- Partials      553      575      +22     
```

| [Impacted Files](https://codecov.io/gh/sodafoundation/delfin/pull/663?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=sodafoundation) | Coverage Δ | |
|---|---|---|
| [delfin/drivers/dell\_emc/vmax/rest.py](https://codecov.io/gh/sodafoundation/delfin/pull/663/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=sodafoundation#diff-ZGVsZmluL2RyaXZlcnMvZGVsbF9lbWMvdm1heC9yZXN0LnB5) | `32.71% <32.35%> (+0.23%)` | :arrow_up: |
| [delfin/drivers/dell\_emc/vmax/vmax.py](https://codecov.io/gh/sodafoundation/delfin/pull/663/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=sodafoundation#diff-ZGVsZmluL2RyaXZlcnMvZGVsbF9lbWMvdm1heC92bWF4LnB5) | `84.31% <66.66%> (+3.92%)` | :arrow_up: |
| [delfin/drivers/dell\_emc/vmax/client.py](https://codecov.io/gh/sodafoundation/delfin/pull/663/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=sodafoundation#diff-ZGVsZmluL2RyaXZlcnMvZGVsbF9lbWMvdm1heC9jbGllbnQucHk=) | `75.31% <73.46%> (-0.61%)` | :arrow_down: |
| [delfin/exception.py](https://codecov.io/gh/sodafoundation/delfin/pull/663/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=sodafoundation#diff-ZGVsZmluL2V4Y2VwdGlvbi5weQ==) | `98.96% <100.00%> (+0.02%)` | :arrow_up: |


Patch:
diff --git a/delfin/drivers/dell_emc/vmax/client.py b/delfin/drivers/dell_emc/vmax/client.py
index 4f07e46..c76727f 100644
--- a/delfin/drivers/dell_emc/vmax/client.py
+++ b/delfin/drivers/dell_emc/vmax/client.py
@@ -83,12 +83,9 @@ class VMAXClient(object):
                     msg = "Invalid array_id. Expected id: {}". \
                         format(array['symmetrixId'])
                     raise exception.InvalidInput(msg)
-        except exception.SSLCertificateFailed:
-            LOG.error('SSL certificate failed when init connection for VMax')
+        except Exception:
+            LOG.error("Failed to init_connection to VMAX")
             raise
-        except Exception as err:
-            msg = "Failed to get array details from VMAX: {}".format(err)
-            raise exception.StorageBackendException(msg)
 
         if not self.array_id:
             msg = "Input array_id is missing. Supported ids: {}". \
@@ -100,13 +97,9 @@ class VMAXClient(object):
             # Get the VMAX array properties
             return self.rest.get_vmax_array_details(version=self.uni_version,
                                                     array=self.array_id)
-        except exception.SSLCertificateFailed:
-            LOG.error('SSL certificate failed when get array info for VMax')
+        except Exception:
+            LOG.error("Failed to get array details from VMAX")
             raise
-        except Exception as err:
-            msg = "Failed to get array details from VMAX: {}".format(err)
-            LOG.error(msg)
-            raise exception.StorageBackendException(msg)
 
     def get_storage_capacity(self):
         try:
@@ -150,17 +143,11 @@ class VMAXClient(object):
             return total_capacity, used_capacity, free_capacity,\
                 raw_capacity, subscribed_capacity
 
-        except exception.SSLCertificateFailed:
-            LOG.error('SSL certificate failed when '
-                      'get storage capacity for VMax')
+        except Exception:
+            LOG.error("Failed to get capacity from VMAX")
             raise
-        except Exception as err:
-            msg = "Failed to get capacity from VMAX: {}".format(err)
-            LOG.error(msg)
-            raise exception.StorageBackendException(msg)
 
     def list_storage_pools(self, storage_id):
-
         try:
             # Get list of SRP pool names
             pools = self.rest.get_srp_by_name(
@@ -202,13 +189,9 @@ class VMAXClient(object):
 
             return pool_list
 
-        except exception.SSLCertificateFailed:
-            LOG.error('SSL certificate failed when list pools for VMax')
+        except Exception:
+            LOG.error("Failed to get pool metrics from VMAX")
             raise
-        except Exception as err:
-            msg = "Failed to get pool metrics from VMAX: {}".format(err)
-            LOG.error(msg)
-            raise exception.StorageBackendException(msg)
 
     def list_volumes(self, storage_id):
 
@@ -224,9 +207,9 @@ class VMAXClient(object):
             # TODO: Update constants.VolumeStatus to make mapping more precise
             switcher = {
                 'Ready': constants.VolumeStatus.AVAILABLE,
-                'Not Ready': constants.VolumeStatus.ERROR,
-                'Mixed': constants.VolumeStatus.ERROR,
-                'Write Disabled': constants.VolumeStatus.ERROR,
+                'Not Ready': constants.VolumeStatus.AVAILABLE,
+                'Mixed': constants.VolumeStatus.AVAILABLE,
+                'Write Disabled': constants.VolumeStatus.AVAILABLE,
                 'N/A': constants.VolumeStatus.ERROR,
             }
 
@@ -241,8 +224,8 @@ class VMAXClient(object):
                 used_cap = (total_cap * vol['allocated_percent']) / 100.0
                 free_cap = total_cap - used_cap
 
-                status = switcher.get(vol['status'],
-                                      constants.VolumeStatus.ERROR)
+                status = switcher.get(vol.get('status'),
+                                      constants.VolumeStatus.AVAILABLE)
 
                 description = "Dell EMC VMAX volume"
                 if vol['type'] == 'TDEV':
@@ -279,13 +262,119 @@ class VMAXClient(object):
 
             return volume_list
 
-        except exception.SSLCertificateFailed:
-            LOG.error('SSL certificate failed when list volumes for VMax')
+        except Exception:
+            LOG.error("Failed to get list volumes from VMAX")
+            raise
+
+    def list_controllers(self, storage_id):
+        try:
+            # Get list of Directors
+            directors = self.rest.get_director_list(self.array_id,
+                                                    self.uni_version)
+            controller_list = []
+            for director in directors:
+                director_info = self.rest.get_director(
+                    self.array_id, self.uni_version, director)
+
+                status = constants.ControllerStatus.NORMAL
+                if director_info.get('availability', '').upper() != 'ONLINE':
+                    status = constants.ControllerStatus.OFFLINE
+
+                controller = {
+                    'name': director_info['directorId'],
+                    'storage_id': storage_id,
+                    'native_controller_id': director_info['directorId'],
+                    'status': status,
+                    'location':
+                        'slot_' +
+                        str(director_info.get('director_slot_number')),
+                    'soft_version': None,
+                    'cpu_info': 'Cores-'
+                                + str(director_info.get('num_of_cores')),
+                    'memory_size': None
+
+                }
+                controller_list.append(controller)
+            return controller_list
+
+        except Exception:
+            LOG.error("Failed to get controller metrics from VMAX")
+            raise
+
+    def list_ports(self, storage_id):
+        try:
+            # Get list of Directors
+            directors = self.rest.get_director_list(self.array_id,
+                                                    self.uni_version)
+            switcher = {
+                'A': constants.PortLogicalType.MANAGEMENT,
+                'B': constants.PortLogicalType.SERVICE,
+                'C': constants.PortLogicalType.BACKEND,
+            }
+            port_list = []
+            for director in directors:
+                port_keys = self.rest.get_port_list(
+                    self.array_id, self.uni_version, director)
+                for port_key in port_keys:
+                    port_info = self.rest.get_port(
+                        self.array_id, self.uni_version,
+                        director, port_key['portId'])['symmetrixPort']
+
+                    connection_status = \
+                        constants.PortConnectionStatus.CONNECTED
+                    if port_info.get('port_status',
+                                     '').upper().find('OFF') != -1:
+                        connection_status = \
+                            constants.PortConnectionStatus.DISCONNECTED
+
+                    port_type = constants.PortType.OTHER
+                    if port_info.get('type', '').upper().find('FIBRE') != -1:
+                        port_type = constants.PortType.FC
+                    if port_info.get('type', '').upper().find('ETH') != -1:
+                        port_type = constants.PortType.ETH
+
+                    name = "{0}:{1}".format(port_key['directorId'],
+                                            port_key['portId'])
+
+                    director_emulation = port_key['directorId'][4]
+                    logical_type = switcher.get(
+                        director_emulation, constants.PortLogicalType.OTHER)
+                    if logical_type == constants.PortLogicalType.OTHER:
+                        port_prefix = port_key['directorId'][:2]
+                        if port_prefix in ['FA', 'FE', 'EA', 'EF', 'SE']:
+                            logical_type = constants.PortLogicalType.FRONTEND
+                        if port_prefix in ['DA', 'DF', 'DX']:
+                            logical_type = constants.PortLogicalType.BACKEND
+
+                    speed = int(port_info.get('negotiated_speed',
+                                              '0')) * units.Gi
+                    max_speed = int(port_info.get('max_speed',
+                                                  '0')) * units.Gi
+                    port_dict = {
+                        'name': name,
+                        'storage_id': storage_id,
+                        'native_port_id': port_key['portId'],
+                        'location': 'director_' + port_key['directorId'],
+                        'connection_status': connection_status,
+                        'health_status': constants.PortHealthStatus.NORMAL,
+                        'type': port_type,
+                        'logical_type': logical_type,
+                        'speed': speed,
+                        'max_speed': max_speed,
+                        'native_parent_id': port_key['directorId'],
+                        'wwn': port_info.get('identifier', None),
+                        'mac_address': None,
+                        'ipv4': port_info.get('ipv4_address'),
+                        'ipv4_mask': port_info.get('ipv4_netmask'),
+                        'ipv6': port_info.get('ipv6_address'),
+                        'ipv6_mask': None,
+                    }
+                    port_list.append(port_dict)
+            return port_list
+
+        except Exception:
+            LOG.error("Failed to get port metrics from VMAX")
             raise
-        except Exception as err:
-            msg = "Failed to get list volumes from VMAX: {}".format(err)
-            LOG.error(msg)
-            raise exception.StorageBackendException(msg)
 
     def list_alerts(self, query_para):
         """Get all alerts from an array."""
@@ -319,8 +408,7 @@ class VMAXClient(object):
                                             values=delfin_metrics[key])
                 metrics_array.append(m)
             return metrics_array
-        except Exception as err:
-            msg = "Failed to get performance metrics data for VMAX: {}".format(
-                err)
-            LOG.error(msg)
-            raise exception.StorageBackendException(msg)
+
+        except Exception:
+            LOG.error("Failed to get performance metrics data for VMAX")
+            raise
diff --git a/delfin/drivers/dell_emc/vmax/rest.py b/delfin/drivers/dell_emc/vmax/rest.py
index 94949a2..976c15a 100644
--- a/delfin/drivers/dell_emc/vmax/rest.py
+++ b/delfin/drivers/dell_emc/vmax/rest.py
@@ -33,6 +33,10 @@ from delfin.i18n import _
 
 LOG = logging.getLogger(__name__)
 SLOPROVISIONING = 'sloprovisioning'
+SYSTEM = 'system'
+SYMMETRIX = 'symmetrix'
+DIRECTOR = 'director'
+PORT = 'port'
 U4V_VERSION = '92'
 UCODE_5978 = '5978'
 # HTTP constants
@@ -371,6 +375,33 @@ class VMaxRest(object):
             private=private, version=version)
         return self.get_request(target_uri, resource_type, params)
 
+    def get_resource_kwargs(self, *args, **kwargs):
+        """Get resource details from the array.
+
+        :key version: Unisphere version -- int
+        :key no_version: if versionless uri -- bool
+        :key category: the resource category e.g. sloprovisioning
+        :key resource_level: resource level e.g. storagegroup
+        :key resource_level_id: resource level id
+        :key resource_type: optional resource type e.g. maskingview
+        :key resource_type_id: optional resource type id
+        :key resource: the name of a specific resource
+        :key resource_id: the name of a specific resource
+        :key object_type: optional name of resource
+        :key object_type_id: optional name of resource
+        :key params: query parameters  -- dict
+        :key private: empty string or '/private' if private url
+        :returns: resource object -- dict or None
+        """
+        resource_type = None
+        if args:
+            resource_type = args[2]
+        elif kwargs:
+            resource_type = kwargs.get('resource_level')
+        target_uri = self.build_uri(*args, **kwargs)
+        return self.get_request(
+            target_uri, resource_type, kwargs.get('params'))
+
     def get_array_detail(self, version=U4V_VERSION, array=''):
         """Get an array from its serial number.
         :param array: the array serial number
@@ -437,10 +468,10 @@ class VMaxRest(object):
         :returns: the VMax model
         """
         system_info = self.get_array_detail(version, array)
-        vmax_version = system_info.get('model')
+        vmax_model = system_info.get('model', 'VMAX')
         vmax_ucode = system_info.get('ucode')
-        vmax_display_name = system_info.get('display_name')
-        array_details = {"model": vmax_version,
+        vmax_display_name = system_info.get('display_name', vmax_model)
+        array_details = {"model": vmax_model,
                          "ucode": vmax_ucode,
                          "display_name": vmax_display_name}
         return array_details
@@ -530,6 +561,146 @@ class VMaxRest(object):
             pass
         return device_ids
 
+    def get_director(self, array, version, device_id):
+        """Get a VMAX director from array.
+        :param array: the array serial number
+        :param version: the unisphere version
+        :param device_id: the volume device id
+        :returns: volume dict
+        :raises: ControllerNotFound
+        """
+        director_dict = None
+        # Unisphere versions 90 and above
+        if int(version) > 84:
+            director_dict = self.get_resource(
+                array, SYSTEM, 'director', resource_name=device_id,
+                version=version)
+
+        # Unisphere versions 84
+        if int(version) == 84:
+            director_dict = self.get_resource(
+                array, SLOPROVISIONING, 'director', resource_name=device_id,
+                version=version)
+
+        if int(version) < 84:
+            LOG.error("Director is not supported in Unisphere version < 8.4")
+            return None
+
+        if not director_dict:
+            exception_message = (_("Director %(deviceID)s not found.")
+                                 % {'deviceID': device_id})
+            LOG.error(exception_message)
+            raise exception.ControllerNotFound(device_id)
+        return director_dict
+
+    def get_director_list(self, array, version, params=None):
+        """Get a filtered list of VMAX controllers from array.
+        :param array: the array serial number
+        :param version: the unisphere version
+        :param params: filter parameters
+        :returns: directors -- list
+        """
+        response = None
+        # Unisphere versions 90 and above
+        if int(version) > 84:
+            response = self.get_resource(
+                array, SYSTEM, 'director',
+                version=version, params=params)
+
+        # Unisphere versions 84
+        if int(version) == 84:
+            response = self.get_resource(
+                array, SLOPROVISIONING, 'director',
+                version=version, params=params)
+
+        if int(version) < 84:
+            LOG.error("Director not supported in Unisphere version < 8.4")
+            return []
+
+        if not response:
+            exception_message = (_("Get Director list failed.")
+                                 % {'deviceID': array})
+            LOG.error(exception_message)
+            raise exception.ControllerListNotFound(array)
+
+        return response.get('directorId', list()) if response else list()
+
+    def get_port(self, array, version, director_id, port_id):
+        """Get a VMAX director from array.
+        :param array: the array serial number
+        :param version: the unisphere version  -- int
+        :param director_id: the director id
+        :param port_id: the port id
+        :returns: volume dict
+        :raises: ControllerNotFound
+        """
+        port_dict = None
+        # Unisphere versions 90 and above
+        if int(version) > 84:
+            port_dict = self.get_resource_kwargs(
+                category=SYSTEM, version=version,
+                resource_level=SYMMETRIX, resource_level_id=array,
+                resource_type=DIRECTOR, resource_type_id=director_id,
+                resource=PORT, resource_id=port_id)
+
+        # Unisphere versions 84
+        if int(version) == 84:
+            port_dict = self.get_resource_kwargs(
+                category=SLOPROVISIONING, version=version,
+                resource_level=SYMMETRIX, resource_level_id=array,
+                resource_type=DIRECTOR, resource_type_id=director_id,
+                resource=PORT, resource_id=port_id)
+
+        if int(version) < 84:
+            LOG.error("Port get is not supported in Unisphere version < 8.4")
+            return None
+
+        if not port_dict:
+            exception_message = (_("Port %(deviceID)s not found.")
+                                 % {'deviceID': port_id})
+            LOG.error(exception_message)
+            raise exception.PortNotFound(port_id)
+        return port_dict
+
+    def get_port_list(self, array, version, director_id, params=None):
+        """Get a filtered list of VMAX controllers from array.
+        :param array: the array serial number
+        :param version: the unisphere version  -- int
+        :param params: filter parameters
+        :param director_id: director id
+        :returns: device_ids -- list
+        """
+        response = None
+        # Unisphere versions 90 and above
+        if int(version) > 84:
+            response = self.get_resource_kwargs(
+                category=SYSTEM, version=version,
+                resource_level=SYMMETRIX, resource_level_id=array,
+                resource_type=DIRECTOR, resource_type_id=director_id,
+                resource=PORT, params=params)
+
+        # Unisphere versions 84
+        if int(version) == 84:
+            response = self.get_resource_kwargs(
+                category=SLOPROVISIONING, version=version,
+                resource_level=SYMMETRIX, resource_level_id=array,
+                resource_type=DIRECTOR, resource_type_id=director_id,
+                resource=PORT, params=params)
+
+        if int(version) < 84:
+            LOG.error("Port list not supported in Unisphere version < 8.4")
+            return []
+
+        if not response:
+            exception_message = (_("Get Port list failed.")
+                                 % {'deviceID': array})
+            LOG.error(exception_message)
+            raise exception.PortListNotFound(array)
+
+        port_ids = response.get('symmetrixPortKey',
+                                list()) if response else list()
+        return port_ids
+
     def post_request(self, target_uri, payload):
         """Generate  a POST request.
         :param target_uri: the uri to query from unipshere REST API
diff --git a/delfin/drivers/dell_emc/vmax/vmax.py b/delfin/drivers/dell_emc/vmax/vmax.py
index cd7c2dd..b02b125 100644
--- a/delfin/drivers/dell_emc/vmax/vmax.py
+++ b/delfin/drivers/dell_emc/vmax/vmax.py
@@ -74,13 +74,13 @@ class VMAXStorageDriver(driver.StorageDriver):
         return self.client.list_volumes(self.storage_id)
 
     def list_controllers(self, context):
-        pass
+        return self.client.list_controllers(self.storage_id)
 
     def list_ports(self, context):
-        pass
+        return self.client.list_ports(self.storage_id)
 
     def list_disks(self, context):
-        pass
+        raise NotImplementedError
 
     def add_trap_config(self, context, trap_config):
         pass
diff --git a/delfin/exception.py b/delfin/exception.py
index 1f06be9..c4eb9b3 100644
--- a/delfin/exception.py
+++ b/delfin/exception.py
@@ -210,10 +210,18 @@ class ControllerNotFound(NotFound):
     msg_fmt = _("Controller {0} could not be found.")
 
 
+class ControllerListNotFound(NotFound):
+    msg_fmt = _("Controller List for {0} could not be found.")
+
+
 class PortNotFound(NotFound):
     msg_fmt = _("Port {0} could not be found.")
 
 
+class PortListNotFound(NotFound):
+    msg_fmt = _("Port List for {0} could not be found.")
+
+
 class DiskNotFound(NotFound):
     msg_fmt = _("Disk {0} could not be found.")
 


Test patch:
diff --git a/delfin/tests/unit/drivers/dell_emc/vmax/test_vmax.py b/delfin/tests/unit/drivers/dell_emc/vmax/test_vmax.py
index aa72e64..a80d4dc 100644
--- a/delfin/tests/unit/drivers/dell_emc/vmax/test_vmax.py
+++ b/delfin/tests/unit/drivers/dell_emc/vmax/test_vmax.py
@@ -145,7 +145,7 @@ class TestVMAXStorageDriver(TestCase):
         with self.assertRaises(Exception) as exc:
             driver.get_storage(context)
 
-        self.assertIn('Failed to get array details from VMAX',
+        self.assertIn('Exception from Storage Backend',
                       str(exc.exception))
 
         mock_array_details.side_effect = [{
@@ -157,7 +157,7 @@ class TestVMAXStorageDriver(TestCase):
         with self.assertRaises(Exception) as exc:
             driver.get_storage(context)
 
-        self.assertIn('Failed to get capacity from VMAX',
+        self.assertIn('Exception from Storage Backend',
                       str(exc.exception))
 
     @mock.patch.object(VMaxRest, 'get_srp_by_name')
@@ -205,14 +205,14 @@ class TestVMAXStorageDriver(TestCase):
         with self.assertRaises(Exception) as exc:
             driver.list_storage_pools(context)
 
-        self.assertIn('Failed to get pool metrics from VMAX',
+        self.assertIn('Exception from Storage Backend',
                       str(exc.exception))
 
         mock_srp.side_effect = [exception.StorageBackendException, pool_info]
         with self.assertRaises(Exception) as exc:
             driver.list_storage_pools(context)
 
-        self.assertIn('Failed to get pool metrics from VMAX',
+        self.assertIn('Exception from Storage Backend',
                       str(exc.exception))
 
     @mock.patch.object(VMaxRest, 'get_system_capacity')
@@ -319,7 +319,7 @@ class TestVMAXStorageDriver(TestCase):
         with self.assertRaises(Exception) as exc:
             driver.list_volumes(context)
 
-        self.assertIn('Failed to get list volumes from VMAX',
+        self.assertIn('Exception from Storage Backend',
                       str(exc.exception))
 
         mock_vols.side_effect = [['volume_1']]
@@ -328,7 +328,7 @@ class TestVMAXStorageDriver(TestCase):
         with self.assertRaises(Exception) as exc:
             driver.list_volumes(context)
 
-        self.assertIn('Failed to get list volumes from VMAX',
+        self.assertIn('Exception from Storage Backend',
                       str(exc.exception))
 
         mock_vols.side_effect = [exception.StorageBackendException]
@@ -337,7 +337,236 @@ class TestVMAXStorageDriver(TestCase):
         with self.assertRaises(Exception) as exc:
             driver.list_volumes(context)
 
-        self.assertIn('Failed to get list volumes from VMAX',
+        self.assertIn('Exception from Storage Backend',
+                      str(exc.exception))
+
+    @mock.patch.object(VMaxRest, 'get_resource')
+    @mock.patch.object(VMaxRest, 'get_array_detail')
+    @mock.patch.object(VMaxRest, 'get_uni_version')
+    @mock.patch.object(VMaxRest, 'get_unisphere_version')
+    def test_list_controllers(self, mock_unisphere_version,
+                              mock_version,
+                              mock_array, mock_res):
+        expected = [
+            {
+                'name': 'DF-1C',
+                'storage_id': '12345',
+                'native_controller_id': 'DF-1C',
+                'status': 'offline',
+                'location': 'slot_10',
+                'soft_version': None,
+                'cpu_info': 'Cores-64',
+                'memory_size': None
+            }
+        ]
+        kwargs = VMAX_STORAGE_CONF
+        mock_version.return_value = ['V9.0.2.7', '90']
+        mock_unisphere_version.return_value = ['V9.0.2.7', '90']
+        mock_array.return_value = {'symmetrixId': ['00112233']}
+        mock_res.side_effect = [
+            {'directorId': ['DF-1C', 'DF-2C']},
+            {
+                'availability': 'ON',
+                'directorId': 'DF-1C',
+                'director_number': 1,
+                'director_slot_number': 10,
+                'num_of_cores': 64,
+                'num_of_ports': 2,
+                'srdf_groups': [
+                    {
+                        'label': 'label_1',
+                        'rdf_group_number': 1
+                    }
+                ]
+            },
+            {
+                'availability': 'ON',
+                'directorId': 'DF-2C',
+                'director_number': 2,
+                'director_slot_number': 10,
+                'num_of_cores': 64,
+                'num_of_ports': 2,
+                'srdf_groups': [
+                    {
+                        'label': 'label_1',
+                        'rdf_group_number': 1
+                    }
+                ]
+            },
+            {'directorId': ['DF-1C', 'DF-2C']},
+            exception.StorageBackendException,
+            exception.StorageBackendException
+        ]
+
+        driver = VMAXStorageDriver(**kwargs)
+        self.assertEqual(driver.storage_id, "12345")
+        self.assertEqual(driver.client.array_id, "00112233")
+
+        ret = driver.list_controllers(context)
+        self.assertDictEqual(ret[0], expected[0])
+
+        with self.assertRaises(Exception) as exc:
+            driver.list_controllers(context)
+
+        self.assertIn('Exception from Storage Backend',
+                      str(exc.exception))
+
+        with self.assertRaises(Exception) as exc:
+            driver.list_controllers(context)
+
+        self.assertIn('Exception from Storage Backend:',
+                      str(exc.exception))
+
+    @mock.patch.object(VMaxRest, 'get_resource_kwargs')
+    @mock.patch.object(VMaxRest, 'get_director_list')
+    @mock.patch.object(VMaxRest, 'get_array_detail')
+    @mock.patch.object(VMaxRest, 'get_uni_version')
+    @mock.patch.object(VMaxRest, 'get_unisphere_version')
+    def test_list_ports(self, mock_unisphere_version,
+                        mock_version,
+                        mock_array, mock_dirs, mock_res):
+        expected = [{
+            'name': 'DF-1D:30',
+            'storage_id': '12345',
+            'native_port_id': '30',
+            'location': 'director_DF-1D',
+            'connection_status': 'connected',
+            'health_status': 'normal',
+            'type': 'other',
+            'logical_type': 'backend',
+            'speed': 0,
+            'max_speed': 10737418240,
+            'native_parent_id': 'DF-1D',
+            'wwn': None,
+            'mac_address': None,
+            'ipv4': None,
+            'ipv4_mask': None,
+            'ipv6': None,
+            'ipv6_mask': None
+        }]
+        kwargs = VMAX_STORAGE_CONF
+        mock_version.return_value = ['V9.0.2.7', '90']
+        mock_unisphere_version.return_value = ['V9.0.2.7', '90']
+        mock_array.return_value = {'symmetrixId': ['00112233']}
+        mock_dirs.return_value = ['DF-1C']
+        mock_res.side_effect = [
+            {
+                'symmetrixPortKey': [
+                    {
+                        'directorId': 'DF-1D',
+                        'portId': '30'
+                    },
+                    {
+                        'directorId': 'DF-2C',
+                        'portId': '0'
+                    }
+                ]
+            },
+            {
+                'symmetrixPort': {
+                    'aclx': False,
+                    'avoid_reset_broadcast': False,
+                    'common_serial_number': True,
+                    'director_status': 'Offline',
+                    'disable_q_reset_on_ua': False,
+                    'enable_auto_negotiate': False,
+                    'environ_set': False,
+                    'hp_3000_mode': False,
+                    'ip_addresses': [
+                        '192.168.0.51'
+                    ],
+                    'iscsi_target': False,
+                    'max_speed': '10',
+                    'negotiate_reset': False,
+                    'num_of_cores': 6,
+                    'num_of_mapped_vols': 0,
+                    'num_of_masking_views': 0,
+                    'num_of_port_groups': 0,
+                    'port_status': 'PendOn',
+                    'scsi_3': False,
+                    'scsi_support1': False,
+                    'siemens': False,
+                    'soft_reset': False,
+                    'spc2_protocol_version': False,
+                    'sunapee': False,
+                    'symmetrixPortKey': {
+                        'directorId': 'DF-1C',
+                        'portId': '30'
+                    },
+                    'type': 'GigE',
+                    'vnx_attached': False,
+                    'volume_set_addressing': False
+                }
+            },
+            {
+                'symmetrixPort': {
+                    'aclx': False,
+                    'avoid_reset_broadcast': False,
+                    'common_serial_number': True,
+                    'director_status': 'Offline',
+                    'disable_q_reset_on_ua': False,
+                    'enable_auto_negotiate': False,
+                    'environ_set': False,
+                    'hp_3000_mode': False,
+                    'ip_addresses': [
+                        '192.168.0.51'
+                    ],
+                    'iscsi_target': False,
+                    'max_speed': '10',
+                    'negotiate_reset': False,
+                    'num_of_cores': 6,
+                    'num_of_mapped_vols': 0,
+                    'num_of_masking_views': 0,
+                    'num_of_port_groups': 0,
+                    'port_status': 'PendOn',
+                    'scsi_3': False,
+                    'scsi_support1': False,
+                    'siemens': False,
+                    'soft_reset': False,
+                    'spc2_protocol_version': False,
+                    'sunapee': False,
+                    'symmetrixPortKey': {
+                        'directorId': 'DF-2C',
+                        'portId': '0'
+                    },
+                    'type': 'GigE',
+                    'vnx_attached': False,
+                    'volume_set_addressing': False
+                }
+            },
+            {
+                'symmetrixPortKey': [
+                    {
+                        'directorId': 'DF-1C',
+                        'portId': '30'
+                    },
+                    {
+                        'directorId': 'DF-2C',
+                        'portId': '0'
+                    }
+                ]
+            },
+            exception.StorageBackendException,
+            exception.StorageBackendException
+        ]
+
+        driver = VMAXStorageDriver(**kwargs)
+        self.assertEqual(driver.storage_id, "12345")
+        self.assertEqual(driver.client.array_id, "00112233")
+
+        ret = driver.list_ports(context)
+        self.assertDictEqual(ret[0], expected[0])
+
+        with self.assertRaises(Exception) as exc:
+            driver.list_ports(context)
+
+        self.assertIn('Exception from Storage Backend:',
+                      str(exc.exception))
+
+        with self.assertRaises(Exception) as exc:
+            driver.list_ports(context)
+
+        self.assertIn('Exception from Storage Backend:',
                       str(exc.exception))
 
     @mock.patch.object(VMaxRest, 'post_request')