diff --git a/manila/share/drivers/hp/hp_3par_driver.py b/manila/share/drivers/hp/hp_3par_driver.py index 276dcb079c..fc07eac80f 100644 --- a/manila/share/drivers/hp/hp_3par_driver.py +++ b/manila/share/drivers/hp/hp_3par_driver.py @@ -31,6 +31,7 @@ from manila.i18n import _LI from manila.share import driver from manila.share.drivers.hp import hp_3par_mediator from manila.share import share_types +from manila import utils HP3PAR_OPTS = [ cfg.StrOpt('hp3par_api_url', @@ -80,13 +81,19 @@ LOG = log.getLogger(__name__) class HP3ParShareDriver(driver.ShareDriver): """HP 3PAR driver for Manila. - Supports NFS and CIFS protocols on arrays with File Persona. - """ + Supports NFS and CIFS protocols on arrays with File Persona. - VERSION = "1.0.01" + Version history: + 1.0.00 - Begin Liberty development (post-Kilo) + 1.0.01 - Report thin/dedup/hp_flash_cache capabilities + 1.0.02 - Add share server/share network support + + """ + + VERSION = "1.0.02" def __init__(self, *args, **kwargs): - super(HP3ParShareDriver, self).__init__(False, *args, **kwargs) + super(HP3ParShareDriver, self).__init__((True, False), *args, **kwargs) self.configuration = kwargs.get('configuration', None) self.configuration.append_config_values(HP3PAR_OPTS) @@ -103,11 +110,13 @@ class HP3ParShareDriver(driver.ShareDriver): {'driver_name': self.__class__.__name__, 'version': self.VERSION}) - self.share_ip_address = self.configuration.hp3par_share_ip_address - if not self.share_ip_address: - raise exception.HP3ParInvalid( - _("Unsupported configuration. " - "hp3par_share_ip_address is not set.")) + if not self.driver_handles_share_servers: + self.share_ip_address = self.configuration.hp3par_share_ip_address + if not self.share_ip_address: + raise exception.HP3ParInvalid( + _("Unsupported configuration. " + "hp3par_share_ip_address must be set when " + "driver_handles_share_servers is False.")) mediator = hp_3par_mediator.HP3ParMediator( hp3par_username=self.configuration.hp3par_username, @@ -163,8 +172,60 @@ class HP3ParShareDriver(driver.ShareDriver): return sha1.hexdigest() + def get_network_allocations_number(self): + return 1 + + @staticmethod + def _validate_network_type(network_type): + if network_type not in ('flat', 'vlan', None): + reason = _('Invalid network type. %s is not supported by the ' + '3PAR driver.') + raise exception.NetworkBadConfigurationException( + reason=reason % network_type) + + def _setup_server(self, network_info, metadata=None): + LOG.debug("begin _setup_server with %s", network_info) + + self._validate_network_type(network_info['network_type']) + + ip = network_info['network_allocations'][0]['ip_address'] + subnet = utils.cidr_to_netmask(network_info['cidr']) + vlantag = network_info['segmentation_id'] + + self._hp3par.create_fsip(ip, subnet, vlantag, self.fpg, self.vfs) + + return { + 'share_server_name': network_info['server_id'], + 'share_server_id': network_info['server_id'], + 'ip': ip, + 'subnet': subnet, + 'vlantag': vlantag if vlantag else 0, + 'fpg': self.fpg, + 'vfs': self.vfs, + } + + def _teardown_server(self, server_details, security_services=None): + LOG.debug("begin _teardown_server with %s", server_details) + + self._hp3par.remove_fsip(server_details.get('ip'), + server_details.get('fpg'), + server_details.get('vfs')) + + def _get_share_ip(self, share_server): + return share_server['backend_details'].get('ip') if share_server else ( + self.share_ip_address) + @staticmethod def _build_export_location(protocol, ip, path): + + if not ip: + message = _('Failed to build export location due to missing IP.') + raise exception.InvalidInput(message) + + if not path: + message = _('Failed to build export location due to missing path.') + raise exception.InvalidInput(message) + if protocol == 'NFS': location = ':'.join((ip, path)) elif protocol == 'CIFS': @@ -173,6 +234,7 @@ class HP3ParShareDriver(driver.ShareDriver): message = _('Invalid protocol. Expected NFS or CIFS. ' 'Got %s.') % protocol raise exception.InvalidInput(message) + return location @staticmethod @@ -194,7 +256,7 @@ class HP3ParShareDriver(driver.ShareDriver): def create_share(self, context, share, share_server=None): """Is called to create share.""" - ip = self.share_ip_address + ip = self._get_share_ip(share_server) protocol = share['share_proto'] extra_specs = share_types.get_extra_specs_from_share(share) @@ -215,7 +277,7 @@ class HP3ParShareDriver(driver.ShareDriver): share_server=None): """Is called to create share from snapshot.""" - ip = self.share_ip_address + ip = self._get_share_ip(share_server) protocol = share['share_proto'] extra_specs = share_types.get_extra_specs_from_share(share) diff --git a/manila/share/drivers/hp/hp_3par_mediator.py b/manila/share/drivers/hp/hp_3par_mediator.py index c87b1604bc..78f471e74c 100644 --- a/manila/share/drivers/hp/hp_3par_mediator.py +++ b/manila/share/drivers/hp/hp_3par_mediator.py @@ -53,8 +53,16 @@ SMB_EXTRA_SPECS_MAP = { class HP3ParMediator(object): + """3PAR client-facing code for the 3PAR driver. - VERSION = "1.0.01" + Version history: + 1.0.00 - Begin Liberty development (post-Kilo) + 1.0.01 - Report thin/dedup/hp_flash_cache capabilities + 1.0.02 - Add share server/share network support + + """ + + VERSION = "1.0.02" def __init__(self, **kwargs): @@ -828,3 +836,88 @@ class HP3ParMediator(object): self._change_access(DENY, project_id, share_id, share_proto, access_type, access_to, fpg, vfs) + + def fsip_exists(self, fsip): + """Try to get FSIP. Return True if it exists.""" + + vfs = fsip['vfs'] + fpg = fsip['fspool'] + + try: + result = self._client.getfsip(vfs, fpg=fpg) + LOG.debug("getfsip result: %s", result) + except Exception as e: + LOG.exception(e) + msg = (_('Failed to get FSIPs for FPG/VFS %(fspool)s/%(vfs)s.') % + fsip) + LOG.exception(msg) + raise exception.ShareBackendException(msg=msg) + + for member in result['members']: + if all(item in member.items() for item in fsip.items()): + return True + + return False + + def create_fsip(self, ip, subnet, vlantag, fpg, vfs): + + vlantag_str = six.text_type(vlantag) if vlantag else '0' + + # Try to create it. It's OK if it already exists. + try: + result = self._client.createfsip(ip, + subnet, + vfs, + fpg=fpg, + vlantag=vlantag_str) + LOG.debug("createfsip result: %s", result) + + except Exception as e: + LOG.exception(e) + msg = (_('Failed to create FSIP for %s') % ip) + LOG.exception(msg) + raise exception.ShareBackendException(msg=msg) + + # Verify that it really exists. + fsip = { + 'fspool': fpg, + 'vfs': vfs, + 'address': ip, + 'prefixLen': subnet, + 'vlanTag': vlantag_str, + } + if not self.fsip_exists(fsip): + msg = (_('Failed to get FSIP after creating it for ' + 'FPG/VFS/IP/subnet/VLAN ' + '%(fspool)s/%(vfs)s/' + '%(address)s/%(prefixLen)s/%(vlanTag)s.') % fsip) + LOG.exception(msg) + raise exception.ShareBackendException(msg=msg) + + def remove_fsip(self, ip, fpg, vfs): + + if not (vfs and ip): + # If there is no VFS and/or IP, then there is no FSIP to remove. + return + + try: + result = self._client.removefsip(vfs, ip, fpg=fpg) + LOG.debug("removefsip result: %s", result) + + except Exception as e: + LOG.exception(e) + msg = (_('Failed to remove FSIP %s') % ip) + LOG.exception(msg) + raise exception.ShareBackendException(msg=msg) + + # Verify that it really no longer exists. + fsip = { + 'fspool': fpg, + 'vfs': vfs, + 'address': ip, + } + if self.fsip_exists(fsip): + msg = (_('Failed to remove FSIP for FPG/VFS/IP ' + '%(fspool)s/%(vfs)s/%(address)s.') % fsip) + LOG.exception(msg) + raise exception.ShareBackendException(msg=msg) diff --git a/manila/tests/share/drivers/hp/test_hp_3par_constants.py b/manila/tests/share/drivers/hp/test_hp_3par_constants.py index 1e3dbb3f14..faeae3c2a1 100644 --- a/manila/tests/share/drivers/hp/test_hp_3par_constants.py +++ b/manila/tests/share/drivers/hp/test_hp_3par_constants.py @@ -25,11 +25,16 @@ API_URL = 'https://1.2.3.4:8080/api/v1' TIMEOUT = 60 PORT = 22 SHARE_TYPE_ID = 123456789 +CIDR_PREFIX = '24' # Constants to use with Mock and expect in results EXPECTED_IP_10203040 = '10.20.30.40' EXPECTED_IP_1234 = '1.2.3.4' EXPECTED_IP_127 = '127.0.0.1' +EXPECTED_SUBNET = '255.255.255.0' # based on CIDR_PREFIX above +EXPECTED_VLAN_TYPE = 'vlan' +EXPECTED_VLAN_TAG = '101' +EXPECTED_SERVER_ID = '1a1a1a1a-2b2b-3c3c-4d4d-5e5e5e5e5e5e' EXPECTED_PROJECT_ID = 'osf-nfs-project-id' EXPECTED_SHARE_ID = 'osf-share-id' EXPECTED_SHARE_NAME = 'share-name' @@ -50,6 +55,22 @@ GET_FSQUOTA = {'message': None, 'total': 1, 'members': [{'hardBlock': '1024', 'softBlock': '1024'}]} +EXPECTED_FSIP = { + 'fspool': EXPECTED_FPG, + 'vfs': EXPECTED_VFS, + 'address': EXPECTED_IP_1234, + 'prefixLen': EXPECTED_SUBNET, + 'vlanTag': EXPECTED_VLAN_TAG, +} + +OTHER_FSIP = { + 'fspool': EXPECTED_FPG, + 'vfs': EXPECTED_VFS, + 'address': '9.9.9.9', + 'prefixLen': EXPECTED_SUBNET, + 'vlanTag': EXPECTED_VLAN_TAG, +} + NFS_SHARE_INFO = { 'project_id': EXPECTED_PROJECT_ID, 'id': EXPECTED_SHARE_ID, diff --git a/manila/tests/share/drivers/hp/test_hp_3par_driver.py b/manila/tests/share/drivers/hp/test_hp_3par_driver.py index bd69e725cc..f994755454 100644 --- a/manila/tests/share/drivers/hp/test_hp_3par_driver.py +++ b/manila/tests/share/drivers/hp/test_hp_3par_driver.py @@ -34,7 +34,7 @@ class HP3ParDriverTestCase(test.TestCase): # Create a mock configuration with attributes and a safe_get() self.conf = mock.Mock() - self.conf.driver_handles_share_servers = False + self.conf.driver_handles_share_servers = True self.conf.hp3par_debug = constants.EXPECTED_HP_DEBUG self.conf.hp3par_username = constants.USERNAME self.conf.hp3par_password = constants.PASSWORD @@ -45,7 +45,7 @@ class HP3ParDriverTestCase(test.TestCase): self.conf.hp3par_fpg = constants.EXPECTED_FPG self.conf.hp3par_san_ssh_port = constants.PORT self.conf.ssh_conn_timeout = constants.TIMEOUT - self.conf.hp3par_share_ip_address = constants.EXPECTED_IP_10203040 + self.conf.hp3par_share_ip_address = None self.conf.hp3par_fstore_per_share = False self.conf.network_config_group = 'test_network_config_group' @@ -89,6 +89,21 @@ class HP3ParDriverTestCase(test.TestCase): self.assertEqual(constants.EXPECTED_VFS, self.driver.vfs) + def test_driver_setup_no_dhss_success(self): + """Driver do_setup without any errors with dhss=False.""" + + self.conf.driver_handles_share_servers = False + self.conf.hp3par_share_ip_address = constants.EXPECTED_IP_10203040 + + self.test_driver_setup_success() + + def test_driver_setup_no_ss_no_ip(self): + """Configured IP address is required for dhss=False.""" + + self.conf.driver_handles_share_servers = False + self.assertRaises(exception.HP3ParInvalid, + self.driver.do_setup, None) + def test_driver_with_setup_error(self): """Driver do_setup when the mediator setup fails.""" @@ -145,7 +160,6 @@ class HP3ParDriverTestCase(test.TestCase): self.driver._hp3par = self.mock_mediator self.driver.vfs = constants.EXPECTED_VFS self.driver.fpg = constants.EXPECTED_FPG - self.driver.share_ip_address = self.conf.hp3par_share_ip_address self.mock_object(hp3pardriver, 'share_types') get_extra_specs = hp3pardriver.share_types.get_extra_specs_from_share get_extra_specs.return_value = constants.EXPECTED_EXTRA_SPECS @@ -154,7 +168,8 @@ class HP3ParDriverTestCase(test.TestCase): expected_share_id, expected_size): """Re-usable code for create share.""" context = None - share_server = None + share_server = { + 'backend_details': {'ip': constants.EXPECTED_IP_10203040}} share = { 'display_name': constants.EXPECTED_SHARE_NAME, 'host': constants.EXPECTED_HOST, @@ -175,7 +190,11 @@ class HP3ParDriverTestCase(test.TestCase): expected_size): """Re-usable code for create share from snapshot.""" context = None - share_server = None + share_server = { + 'backend_details': { + 'ip': constants.EXPECTED_IP_10203040, + }, + } share = { 'display_name': constants.EXPECTED_SHARE_NAME, 'host': constants.EXPECTED_HOST, @@ -436,6 +455,37 @@ class HP3ParDriverTestCase(test.TestCase): ] self.mock_mediator.assert_has_calls(expected_calls) + def test_driver_get_share_stats_not_ready(self): + """Protect against stats update before driver is ready.""" + + self.mock_object(hp3pardriver, 'LOG') + + expected_result = { + 'driver_handles_share_servers': True, + 'QoS_support': False, + 'driver_version': self.driver.VERSION, + 'free_capacity_gb': 0, + 'max_over_subscription_ratio': None, + 'reserved_percentage': 0, + 'provisioned_capacity_gb': 0, + 'share_backend_name': 'HP_3PAR', + 'snapshot_support': True, + 'storage_protocol': 'NFS_CIFS', + 'thin_provisioning': True, + 'total_capacity_gb': 0, + 'vendor_name': 'HP', + 'pools': None, + } + + result = self.driver.get_share_stats(refresh=True) + self.assertEqual(expected_result, result) + + expected_calls = [ + mock.call.info('Skipping capacity and capabilities update. ' + 'Setup has not completed.') + ] + hp3pardriver.LOG.assert_has_calls(expected_calls) + def test_driver_get_share_stats_no_refresh(self): """Driver does not call mediator when refresh=False.""" @@ -464,8 +514,8 @@ class HP3ParDriverTestCase(test.TestCase): } expected_result = { + 'driver_handles_share_servers': True, 'QoS_support': False, - 'driver_handles_share_servers': False, 'driver_version': expected_version, 'free_capacity_gb': expected_free, 'max_over_subscription_ratio': None, @@ -500,7 +550,7 @@ class HP3ParDriverTestCase(test.TestCase): expected_result = { 'QoS_support': False, - 'driver_handles_share_servers': False, + 'driver_handles_share_servers': True, 'driver_version': expected_version, 'free_capacity_gb': 0, 'max_over_subscription_ratio': None, @@ -549,3 +599,84 @@ class HP3ParDriverTestCase(test.TestCase): # Don't test with same regex as the code uses. for c in "'\".,;": self.assertNotIn(c, comment) + + def test_get_network_allocations_number(self): + self.assertEqual(1, self.driver.get_network_allocations_number()) + + def test_build_export_location_bad_protocol(self): + self.assertRaises(exception.InvalidInput, + self.driver._build_export_location, + "BOGUS", + constants.EXPECTED_IP_1234, + constants.EXPECTED_SHARE_PATH) + + def test_build_export_location_bad_ip(self): + self.assertRaises(exception.InvalidInput, + self.driver._build_export_location, + constants.NFS, + None, + None) + + def test_build_export_location_bad_path(self): + self.assertRaises(exception.InvalidInput, + self.driver._build_export_location, + constants.NFS, + constants.EXPECTED_IP_1234, + None) + + def test_setup_server(self): + """Setup server by creating a new FSIP.""" + + self.init_driver() + + network_info = { + 'network_allocations': [ + {'ip_address': constants.EXPECTED_IP_1234}], + 'cidr': '/'.join((constants.EXPECTED_IP_1234, + constants.CIDR_PREFIX)), + 'network_type': constants.EXPECTED_VLAN_TYPE, + 'segmentation_id': constants.EXPECTED_VLAN_TAG, + 'server_id': constants.EXPECTED_SERVER_ID, + } + + expected_result = { + 'share_server_name': constants.EXPECTED_SERVER_ID, + 'share_server_id': constants.EXPECTED_SERVER_ID, + 'ip': constants.EXPECTED_IP_1234, + 'subnet': constants.EXPECTED_SUBNET, + 'vlantag': constants.EXPECTED_VLAN_TAG, + 'fpg': constants.EXPECTED_FPG, + 'vfs': constants.EXPECTED_VFS, + } + + result = self.driver._setup_server(network_info) + + expected_calls = [ + mock.call.create_fsip(constants.EXPECTED_IP_1234, + constants.EXPECTED_SUBNET, + constants.EXPECTED_VLAN_TAG, + constants.EXPECTED_FPG, + constants.EXPECTED_VFS) + ] + self.mock_mediator.assert_has_calls(expected_calls) + + self.assertEqual(expected_result, result) + + def test_teardown_server(self): + + self.init_driver() + + server_details = { + 'ip': constants.EXPECTED_IP_1234, + 'fpg': constants.EXPECTED_FPG, + 'vfs': constants.EXPECTED_VFS, + } + + self.driver._teardown_server(server_details) + + expected_calls = [ + mock.call.remove_fsip(constants.EXPECTED_IP_1234, + constants.EXPECTED_FPG, + constants.EXPECTED_VFS) + ] + self.mock_mediator.assert_has_calls(expected_calls) diff --git a/manila/tests/share/drivers/hp/test_hp_3par_mediator.py b/manila/tests/share/drivers/hp/test_hp_3par_mediator.py index b90720b7a7..941d446dd9 100644 --- a/manila/tests/share/drivers/hp/test_hp_3par_mediator.py +++ b/manila/tests/share/drivers/hp/test_hp_3par_mediator.py @@ -1432,6 +1432,207 @@ class HP3ParMediatorTestCase(test.TestCase): self.assertEqual(expected_result, result) + def test_fsip_exists(self): + self.init_mediator() + + # Make the result member a superset of the fsip items. + fsip_plus = constants.EXPECTED_FSIP.copy() + fsip_plus.update({'k': 'v', 'k2': 'v2'}) + + self.mock_client.getfsip.return_value = { + 'total': 3, + 'members': [{'bogus1': 1}, fsip_plus, {'bogus2': '2'}] + } + + self.assertTrue(self.mediator.fsip_exists(constants.EXPECTED_FSIP)) + + self.mock_client.getfsip.assert_called_once_with( + constants.EXPECTED_VFS, + fpg=constants.EXPECTED_FPG) + + def test_fsip_does_not_exist(self): + self.init_mediator() + + self.mock_client.getfsip.return_value = { + 'total': 3, + 'members': [{'bogus1': 1}, constants.OTHER_FSIP, {'bogus2': '2'}] + } + + self.assertFalse(self.mediator.fsip_exists(constants.EXPECTED_FSIP)) + + self.mock_client.getfsip.assert_called_once_with( + constants.EXPECTED_VFS, + fpg=constants.EXPECTED_FPG) + + def test_fsip_exists_exception(self): + self.init_mediator() + + class FakeException(Exception): + pass + + self.mock_client.getfsip.side_effect = FakeException() + + self.assertRaises(exception.ShareBackendException, + self.mediator.fsip_exists, + constants.EXPECTED_FSIP) + + self.mock_client.getfsip.assert_called_once_with( + constants.EXPECTED_VFS, + fpg=constants.EXPECTED_FPG) + + def test_create_fsip_success(self): + self.init_mediator() + + # Make the result member a superset of the fsip items. + fsip_plus = constants.EXPECTED_FSIP.copy() + fsip_plus.update({'k': 'v', 'k2': 'v2'}) + + self.mock_client.getfsip.return_value = { + 'total': 3, + 'members': [{'bogus1': 1}, fsip_plus, {'bogus2': '2'}] + } + + self.mediator.create_fsip(constants.EXPECTED_IP_1234, + constants.EXPECTED_SUBNET, + constants.EXPECTED_VLAN_TAG, + constants.EXPECTED_FPG, + constants.EXPECTED_VFS) + + self.mock_client.getfsip.assert_called_once_with( + constants.EXPECTED_VFS, + fpg=constants.EXPECTED_FPG) + + expected_calls = [ + mock.call.createfsip(constants.EXPECTED_IP_1234, + constants.EXPECTED_SUBNET, + constants.EXPECTED_VFS, + fpg=constants.EXPECTED_FPG, + vlantag=constants.EXPECTED_VLAN_TAG), + mock.call.getfsip(constants.EXPECTED_VFS, + fpg=constants.EXPECTED_FPG), + ] + self.mock_client.assert_has_calls(expected_calls) + + def test_create_fsip_exception(self): + self.init_mediator() + + class FakeException(Exception): + pass + + self.mock_client.createfsip.side_effect = FakeException() + + self.assertRaises(exception.ShareBackendException, + self.mediator.create_fsip, + constants.EXPECTED_IP_1234, + constants.EXPECTED_SUBNET, + constants.EXPECTED_VLAN_TAG, + constants.EXPECTED_FPG, + constants.EXPECTED_VFS) + + self.mock_client.createfsip.assert_called_once_with( + constants.EXPECTED_IP_1234, + constants.EXPECTED_SUBNET, + constants.EXPECTED_VFS, + fpg=constants.EXPECTED_FPG, + vlantag=constants.EXPECTED_VLAN_TAG) + + def test_create_fsip_get_none(self): + self.init_mediator() + + self.mock_client.getfsip.return_value = {'members': []} + + self.assertRaises(exception.ShareBackendException, + self.mediator.create_fsip, + constants.EXPECTED_IP_1234, + constants.EXPECTED_SUBNET, + constants.EXPECTED_VLAN_TAG, + constants.EXPECTED_FPG, + constants.EXPECTED_VFS) + + expected_calls = [ + mock.call.createfsip(constants.EXPECTED_IP_1234, + constants.EXPECTED_SUBNET, + constants.EXPECTED_VFS, + fpg=constants.EXPECTED_FPG, + vlantag=constants.EXPECTED_VLAN_TAG), + mock.call.getfsip(constants.EXPECTED_VFS, + fpg=constants.EXPECTED_FPG), + ] + self.mock_client.assert_has_calls(expected_calls) + + def test_remove_fsip_success(self): + self.init_mediator() + + self.mock_client.getfsip.return_value = { + 'members': [constants.OTHER_FSIP] + } + + self.mediator.remove_fsip(constants.EXPECTED_IP_1234, + constants.EXPECTED_FPG, + constants.EXPECTED_VFS) + + expected_calls = [ + mock.call.removefsip(constants.EXPECTED_VFS, + constants.EXPECTED_IP_1234, + fpg=constants.EXPECTED_FPG), + mock.call.getfsip(constants.EXPECTED_VFS, + fpg=constants.EXPECTED_FPG), + ] + self.mock_client.assert_has_calls(expected_calls) + + @ddt.data(('ip', None), + ('ip', ''), + (None, 'vfs'), + ('', 'vfs'), + (None, None), + ('', '')) + @ddt.unpack + def test_remove_fsip_without_ip_or_vfs(self, ip, vfs): + self.init_mediator() + self.mediator.remove_fsip(ip, constants.EXPECTED_FPG, vfs) + self.assertFalse(self.mock_client.removefsip.called) + + def test_remove_fsip_not_gone(self): + self.init_mediator() + + self.mock_client.getfsip.return_value = { + 'members': [constants.EXPECTED_FSIP] + } + + self.assertRaises(exception.ShareBackendException, + self.mediator.remove_fsip, + constants.EXPECTED_IP_1234, + constants.EXPECTED_FPG, + constants.EXPECTED_VFS) + + expected_calls = [ + mock.call.removefsip(constants.EXPECTED_VFS, + constants.EXPECTED_IP_1234, + fpg=constants.EXPECTED_FPG), + mock.call.getfsip(constants.EXPECTED_VFS, + fpg=constants.EXPECTED_FPG), + ] + self.mock_client.assert_has_calls(expected_calls) + + def test_remove_fsip_exception(self): + self.init_mediator() + + class FakeException(Exception): + pass + + self.mock_client.removefsip.side_effect = FakeException() + + self.assertRaises(exception.ShareBackendException, + self.mediator.remove_fsip, + constants.EXPECTED_IP_1234, + constants.EXPECTED_FPG, + constants.EXPECTED_VFS) + + self.mock_client.removefsip.assert_called_once_with( + constants.EXPECTED_VFS, + constants.EXPECTED_IP_1234, + fpg=constants.EXPECTED_FPG) + class OptionMatcher(object): """Options string order can vary. Compare as lists."""