# Copyright (c) 2014 Pure Storage, Inc. # 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 sys import mock from oslo_concurrency import processutils from oslo_utils import units from cinder import exception from cinder import test def fake_retry(exceptions, interval=1, retries=3, backoff_rate=2): def _decorator(f): return f return _decorator mock.patch('cinder.utils.retry', fake_retry).start() sys.modules['purestorage'] = mock.Mock() from cinder.volume.drivers import pure DRIVER_PATH = "cinder.volume.drivers.pure" DRIVER_OBJ = DRIVER_PATH + ".PureISCSIDriver" ARRAY_OBJ = DRIVER_PATH + ".FlashArray" TARGET = "pure-target" API_TOKEN = "12345678-abcd-1234-abcd-1234567890ab" VOLUME_BACKEND_NAME = "Pure_iSCSI" PORT_NAMES = ["ct0.eth2", "ct0.eth3", "ct1.eth2", "ct1.eth3"] ISCSI_IPS = ["10.0.0." + str(i + 1) for i in range(len(PORT_NAMES))] HOSTNAME = "computenode1" PURE_HOST_NAME = pure._generate_purity_host_name(HOSTNAME) PURE_HOST = {"name": PURE_HOST_NAME, "hgroup": None, "iqn": [], "wwn": [], } REST_VERSION = "1.2" VOLUME_ID = "abcdabcd-1234-abcd-1234-abcdeffedcba" VOLUME = {"name": "volume-" + VOLUME_ID, "id": VOLUME_ID, "display_name": "fake_volume", "size": 2, "host": "irrelevant", "volume_type": None, "volume_type_id": None, "consistencygroup_id": None } VOLUME_WITH_CGROUP = VOLUME.copy() VOLUME_WITH_CGROUP['consistencygroup_id'] = \ "4a2f7e3a-312a-40c5-96a8-536b8a0fe074" SRC_VOL_ID = "dc7a294d-5964-4379-a15f-ce5554734efc" SRC_VOL = {"name": "volume-" + SRC_VOL_ID, "id": SRC_VOL_ID, "display_name": 'fake_src', "size": 2, "host": "irrelevant", "volume_type": None, "volume_type_id": None, "consistencygroup_id": None } SNAPSHOT_ID = "04fe2f9a-d0c4-4564-a30d-693cc3657b47" SNAPSHOT = {"name": "snapshot-" + SNAPSHOT_ID, "id": SNAPSHOT_ID, "volume_id": SRC_VOL_ID, "volume_name": "volume-" + SRC_VOL_ID, "volume_size": 2, "display_name": "fake_snapshot", "cgsnapshot_id": None } SNAPSHOT_WITH_CGROUP = SNAPSHOT.copy() SNAPSHOT_WITH_CGROUP['cgsnapshot_id'] = \ "4a2f7e3a-312a-40c5-96a8-536b8a0fe075" INITIATOR_IQN = "iqn.1993-08.org.debian:01:222" CONNECTOR = {"initiator": INITIATOR_IQN, "host": HOSTNAME} TARGET_IQN = "iqn.2010-06.com.purestorage:flasharray.12345abc" TARGET_PORT = "3260" ISCSI_PORTS = [{"name": name, "iqn": TARGET_IQN, "portal": ip + ":" + TARGET_PORT, "wwn": None, } for name, ip in zip(PORT_NAMES, ISCSI_IPS)] NON_ISCSI_PORT = {"name": "ct0.fc1", "iqn": None, "portal": None, "wwn": "5001500150015081", } PORTS_WITH = ISCSI_PORTS + [NON_ISCSI_PORT] PORTS_WITHOUT = [NON_ISCSI_PORT] VOLUME_CONNECTIONS = [{"host": "h1", "name": VOLUME["name"] + "-cinder"}, {"host": "h2", "name": VOLUME["name"] + "-cinder"}, ] TOTAL_CAPACITY = 50.0 USED_SPACE = 32.1 PROVISIONED_CAPACITY = 70.0 DEFAULT_OVER_SUBSCRIPTION = 20 SPACE_INFO = {"capacity": TOTAL_CAPACITY * units.Gi, "total": USED_SPACE * units.Gi } SPACE_INFO_EMPTY = {"capacity": TOTAL_CAPACITY * units.Gi, "total": 0 } class FakePureStorageHTTPError(Exception): def __init__(self, target=None, rest_version=None, code=None, headers=None, text=None): self.target = target self.rest_version = rest_version self.code = code self.headers = headers self.text = text class PureISCSIDriverTestCase(test.TestCase): def setUp(self): super(PureISCSIDriverTestCase, self).setUp() self.config = mock.Mock() self.config.san_ip = TARGET self.config.pure_api_token = API_TOKEN self.config.volume_backend_name = VOLUME_BACKEND_NAME self.driver = pure.PureISCSIDriver(configuration=self.config) self.array = mock.Mock() self.driver._array = self.array self.purestorage_module = pure.purestorage self.purestorage_module.PureHTTPError = FakePureStorageHTTPError @mock.patch(DRIVER_OBJ + "._choose_target_iscsi_port") def test_do_setup(self, mock_choose_target_iscsi_port): mock_choose_target_iscsi_port.return_value = ISCSI_PORTS[0] self.purestorage_module.FlashArray.return_value = self.array self.array.get_rest_version.return_value = \ self.driver.SUPPORTED_REST_API_VERSIONS[0] self.driver.do_setup(None) self.purestorage_module.FlashArray.assert_called_with( TARGET, api_token=API_TOKEN ) self.assertEqual(self.array, self.driver._array) self.assertEqual( self.driver.SUPPORTED_REST_API_VERSIONS, self.purestorage_module.FlashArray.supported_rest_versions ) mock_choose_target_iscsi_port.assert_called_with() self.assertEqual(ISCSI_PORTS[0], self.driver._iscsi_port) self.assert_error_propagates( [ self.purestorage_module.FlashArray, mock_choose_target_iscsi_port ], self.driver.do_setup, None ) def assert_error_propagates(self, mocks, func, *args, **kwargs): """Assert that errors from mocks propagate to func. Fail if exceptions raised by mocks are not seen when calling func(*args, **kwargs). Ensure that we are really seeing exceptions from the mocks by failing if just running func(*args, **kargs) raises an exception itself. """ func(*args, **kwargs) for mock_func in mocks: mock_func.side_effect = exception.PureDriverException( reason="reason") self.assertRaises(exception.PureDriverException, func, *args, **kwargs) mock_func.side_effect = None def test_generate_purity_host_name(self): generate = pure._generate_purity_host_name result = generate("really-long-string-thats-a-bit-too-long") self.assertTrue(result.startswith("really-long-string-that-")) self.assertTrue(result.endswith("-cinder")) self.assertEqual(len(result), 63) self.assertTrue(pure.GENERATED_NAME.match(result)) result = generate("!@#$%^-invalid&*") self.assertTrue(result.startswith("invalid---")) self.assertTrue(result.endswith("-cinder")) self.assertEqual(len(result), 49) self.assertTrue(pure.GENERATED_NAME.match(result)) def test_create_volume(self): self.driver.create_volume(VOLUME) self.array.create_volume.assert_called_with( VOLUME["name"] + "-cinder", 2 * units.Gi) self.assert_error_propagates([self.array.create_volume], self.driver.create_volume, VOLUME) @mock.patch(DRIVER_OBJ + "._add_volume_to_consistency_group", autospec=True) def test_create_volume_with_cgroup(self, mock_add_to_cgroup): vol_name = VOLUME_WITH_CGROUP["name"] + "-cinder" self.driver.create_volume(VOLUME_WITH_CGROUP) mock_add_to_cgroup\ .assert_called_with(self.driver, VOLUME_WITH_CGROUP['consistencygroup_id'], vol_name) def test_create_volume_from_snapshot(self): vol_name = VOLUME["name"] + "-cinder" snap_name = SNAPSHOT["volume_name"] + "-cinder." + SNAPSHOT["name"] # Branch where extend unneeded self.driver.create_volume_from_snapshot(VOLUME, SNAPSHOT) self.array.copy_volume.assert_called_with(snap_name, vol_name) self.assertFalse(self.array.extend_volume.called) self.assert_error_propagates( [self.array.copy_volume], self.driver.create_volume_from_snapshot, VOLUME, SNAPSHOT) self.assertFalse(self.array.extend_volume.called) # Branch where extend needed SNAPSHOT["volume_size"] = 1 # resize so smaller than VOLUME self.driver.create_volume_from_snapshot(VOLUME, SNAPSHOT) expected = [mock.call.copy_volume(snap_name, vol_name), mock.call.extend_volume(vol_name, 2 * units.Gi)] self.array.assert_has_calls(expected) self.assert_error_propagates( [self.array.copy_volume, self.array.extend_volume], self.driver.create_volume_from_snapshot, VOLUME, SNAPSHOT) SNAPSHOT["volume_size"] = 2 # reset size @mock.patch(DRIVER_OBJ + "._add_volume_to_consistency_group", autospec=True) @mock.patch(DRIVER_OBJ + "._extend_if_needed", autospec=True) @mock.patch(DRIVER_PATH + "._get_pgroup_vol_snap_name", autospec=True) def test_create_volume_from_cgsnapshot(self, mock_get_snap_name, mock_extend_if_needed, mock_add_to_cgroup): vol_name = VOLUME_WITH_CGROUP["name"] + "-cinder" snap_name = "consisgroup-4a2f7e3a-312a-40c5-96a8-536b8a0f" \ "e074-cinder.4a2f7e3a-312a-40c5-96a8-536b8a0fe075."\ + vol_name mock_get_snap_name.return_value = snap_name self.driver.create_volume_from_snapshot(VOLUME_WITH_CGROUP, SNAPSHOT_WITH_CGROUP) self.array.copy_volume.assert_called_with(snap_name, vol_name) self.assertTrue(mock_get_snap_name.called) self.assertTrue(mock_extend_if_needed.called) self.driver.create_volume_from_snapshot(VOLUME_WITH_CGROUP, SNAPSHOT_WITH_CGROUP) mock_add_to_cgroup\ .assert_called_with(self.driver, VOLUME_WITH_CGROUP['consistencygroup_id'], vol_name) def test_create_cloned_volume(self): vol_name = VOLUME["name"] + "-cinder" src_name = SRC_VOL["name"] + "-cinder" # Branch where extend unneeded self.driver.create_cloned_volume(VOLUME, SRC_VOL) self.array.copy_volume.assert_called_with(src_name, vol_name) self.assertFalse(self.array.extend_volume.called) self.assert_error_propagates( [self.array.copy_volume], self.driver.create_cloned_volume, VOLUME, SRC_VOL) self.assertFalse(self.array.extend_volume.called) # Branch where extend needed SRC_VOL["size"] = 1 # resize so smaller than VOLUME self.driver.create_cloned_volume(VOLUME, SRC_VOL) expected = [mock.call.copy_volume(src_name, vol_name), mock.call.extend_volume(vol_name, 2 * units.Gi)] self.array.assert_has_calls(expected) self.assert_error_propagates( [self.array.copy_volume, self.array.extend_volume], self.driver.create_cloned_volume, VOLUME, SRC_VOL) SRC_VOL["size"] = 2 # reset size @mock.patch(DRIVER_OBJ + "._add_volume_to_consistency_group", autospec=True) def test_create_cloned_volume_with_cgroup(self, mock_add_to_cgroup): vol_name = VOLUME_WITH_CGROUP["name"] + "-cinder" self.driver.create_cloned_volume(VOLUME_WITH_CGROUP, SRC_VOL) mock_add_to_cgroup\ .assert_called_with(self.driver, VOLUME_WITH_CGROUP['consistencygroup_id'], vol_name) def test_delete_volume_already_deleted(self): self.array.list_volume_private_connections.side_effect = \ self.purestorage_module.PureHTTPError( code=400, text="Volume does not exist" ) self.driver.delete_volume(VOLUME) self.assertFalse(self.array.destroy_volume.called) # Testing case where array.destroy_volume returns an exception # because volume has already been deleted self.array.list_volume_private_connections.side_effect = None self.array.list_volume_private_connections.return_value = {} self.array.destroy_volume.side_effect = \ self.purestorage_module.PureHTTPError( code=400, text="Volume does not exist" ) self.driver.delete_volume(VOLUME) self.assertTrue(self.array.destroy_volume.called) def test_delete_volume(self): vol_name = VOLUME["name"] + "-cinder" self.array.list_volume_private_connections.return_value = {} self.driver.delete_volume(VOLUME) expected = [mock.call.destroy_volume(vol_name)] self.array.assert_has_calls(expected) self.array.destroy_volume.side_effect = \ self.purestorage_module.PureHTTPError(code=400, text="reason") self.driver.delete_snapshot(SNAPSHOT) self.array.destroy_volume.side_effect = None self.assert_error_propagates([self.array.destroy_volume], self.driver.delete_volume, VOLUME) def test_delete_connected_volume(self): vol_name = VOLUME["name"] + "-cinder" host_name_a = "ha" host_name_b = "hb" self.array.list_volume_private_connections.return_value = [{ "host": host_name_a, "lun": 7, "name": vol_name, "size": 3221225472 }, { "host": host_name_b, "lun": 2, "name": vol_name, "size": 3221225472 }] self.driver.delete_volume(VOLUME) expected = [mock.call.list_volume_private_connections(vol_name), mock.call.disconnect_host(host_name_a, vol_name), mock.call.disconnect_host(host_name_b, vol_name), mock.call.destroy_volume(vol_name)] self.array.assert_has_calls(expected) def test_create_snapshot(self): vol_name = SRC_VOL["name"] + "-cinder" self.driver.create_snapshot(SNAPSHOT) self.array.create_snapshot.assert_called_with( vol_name, suffix=SNAPSHOT["name"] ) self.assert_error_propagates([self.array.create_snapshot], self.driver.create_snapshot, SNAPSHOT) def test_delete_snapshot(self): snap_name = SNAPSHOT["volume_name"] + "-cinder." + SNAPSHOT["name"] self.driver.delete_snapshot(SNAPSHOT) expected = [mock.call.destroy_volume(snap_name)] self.array.assert_has_calls(expected) self.array.destroy_volume.side_effect = \ self.purestorage_module.PureHTTPError(code=400, text="reason") self.driver.delete_snapshot(SNAPSHOT) self.array.destroy_volume.side_effect = None self.assert_error_propagates([self.array.destroy_volume], self.driver.delete_snapshot, SNAPSHOT) @mock.patch(DRIVER_OBJ + "._connect") @mock.patch(DRIVER_OBJ + "._get_target_iscsi_port") def test_initialize_connection(self, mock_get_iscsi_port, mock_connection): mock_get_iscsi_port.return_value = ISCSI_PORTS[0] mock_connection.return_value = {"vol": VOLUME["name"] + "-cinder", "lun": 1, } result = {"driver_volume_type": "iscsi", "data": {"target_iqn": TARGET_IQN, "target_portal": ISCSI_IPS[0] + ":" + TARGET_PORT, "target_lun": 1, "target_discovered": True, "access_mode": "rw", }, } real_result = self.driver.initialize_connection(VOLUME, CONNECTOR) self.assertDictMatch(result, real_result) mock_get_iscsi_port.assert_called_with() mock_connection.assert_called_with(VOLUME, CONNECTOR) self.assert_error_propagates([mock_get_iscsi_port, mock_connection], self.driver.initialize_connection, VOLUME, CONNECTOR) @mock.patch(DRIVER_OBJ + "._choose_target_iscsi_port") @mock.patch(DRIVER_OBJ + "._run_iscsiadm_bare") def test_get_target_iscsi_port(self, mock_iscsiadm, mock_choose_port): self.driver._iscsi_port = ISCSI_PORTS[1] self.assertEqual(self.driver._get_target_iscsi_port(), ISCSI_PORTS[1]) mock_iscsiadm.assert_called_with(["-m", "discovery", "-t", "sendtargets", "-p", ISCSI_PORTS[1]["portal"]]) self.assertFalse(mock_choose_port.called) mock_iscsiadm.side_effect = [processutils.ProcessExecutionError, None] mock_choose_port.return_value = ISCSI_PORTS[2] self.assertEqual(self.driver._get_target_iscsi_port(), ISCSI_PORTS[2]) mock_choose_port.assert_called_with() mock_iscsiadm.side_effect = processutils.ProcessExecutionError self.assert_error_propagates([mock_choose_port], self.driver._get_target_iscsi_port) @mock.patch(DRIVER_OBJ + "._run_iscsiadm_bare") def test_choose_target_iscsi_port(self, mock_iscsiadm): self.array.list_ports.return_value = PORTS_WITHOUT self.assertRaises(exception.PureDriverException, self.driver._choose_target_iscsi_port) self.array.list_ports.return_value = PORTS_WITH self.assertEqual(ISCSI_PORTS[0], self.driver._choose_target_iscsi_port()) self.assert_error_propagates([mock_iscsiadm, self.array.list_ports], self.driver._choose_target_iscsi_port) @mock.patch(DRIVER_OBJ + "._get_host", autospec=True) @mock.patch(DRIVER_PATH + "._generate_purity_host_name", autospec=True) def test_connect(self, mock_generate, mock_host): vol_name = VOLUME["name"] + "-cinder" result = {"vol": vol_name, "lun": 1} # Branch where host already exists mock_host.return_value = PURE_HOST self.array.connect_host.return_value = {"vol": vol_name, "lun": 1} real_result = self.driver._connect(VOLUME, CONNECTOR) self.assertEqual(result, real_result) mock_host.assert_called_with(self.driver, CONNECTOR) self.assertFalse(mock_generate.called) self.assertFalse(self.array.create_host.called) self.array.connect_host.assert_called_with(PURE_HOST_NAME, vol_name) # Branch where new host is created mock_host.return_value = None mock_generate.return_value = PURE_HOST_NAME real_result = self.driver._connect(VOLUME, CONNECTOR) mock_host.assert_called_with(self.driver, CONNECTOR) mock_generate.assert_called_with(HOSTNAME) self.array.create_host.assert_called_with(PURE_HOST_NAME, iqnlist=[INITIATOR_IQN]) self.assertEqual(result, real_result) # Branch where host is needed mock_generate.reset_mock() self.array.reset_mock() self.assert_error_propagates( [mock_host, mock_generate, self.array.connect_host, self.array.create_host], self.driver._connect, VOLUME, CONNECTOR) @mock.patch(DRIVER_OBJ + "._get_host", autospec=True) def test_connect_already_connected(self, mock_host): mock_host.return_value = PURE_HOST expected = {"host": PURE_HOST_NAME, "lun": 1} self.array.list_volume_private_connections.return_value = \ [expected, {"host": "extra", "lun": 2}] self.array.connect_host.side_effect = \ self.purestorage_module.PureHTTPError( code=400, text="Connection already exists" ) actual = self.driver._connect(VOLUME, CONNECTOR) self.assertEqual(expected, actual) self.assertTrue(self.array.connect_host.called) self.assertTrue(self.array.list_volume_private_connections) @mock.patch(DRIVER_OBJ + "._get_host", autospec=True) def test_connect_already_connected_list_hosts_empty(self, mock_host): mock_host.return_value = PURE_HOST self.array.list_volume_private_connections.return_value = {} self.array.connect_host.side_effect = \ self.purestorage_module.PureHTTPError( code=400, text="Connection already exists" ) self.assertRaises(exception.PureDriverException, self.driver._connect, VOLUME, CONNECTOR) self.assertTrue(self.array.connect_host.called) self.assertTrue(self.array.list_volume_private_connections) @mock.patch(DRIVER_OBJ + "._get_host", autospec=True) def test_connect_already_connected_list_hosts_exception(self, mock_host): mock_host.return_value = PURE_HOST self.array.list_volume_private_connections.side_effect = \ self.purestorage_module.PureHTTPError(code=400, text="") self.array.connect_host.side_effect = \ self.purestorage_module.PureHTTPError( code=400, text="Connection already exists" ) self.assertRaises(self.purestorage_module.PureHTTPError, self.driver._connect, VOLUME, CONNECTOR) self.assertTrue(self.array.connect_host.called) self.assertTrue(self.array.list_volume_private_connections) def test_get_host(self): good_host = PURE_HOST.copy() good_host.update(iqn=["another-wrong-iqn", INITIATOR_IQN]) bad_host = {"name": "bad-host", "iqn": ["wrong-iqn"]} self.array.list_hosts.return_value = [bad_host] real_result = self.driver._get_host(CONNECTOR) self.assertIs(real_result, None) self.array.list_hosts.return_value.append(good_host) real_result = self.driver._get_host(CONNECTOR) self.assertEqual(real_result, good_host) self.assert_error_propagates([self.array.list_hosts], self.driver._get_host, CONNECTOR) @mock.patch(DRIVER_OBJ + "._get_host", autospec=True) def test_terminate_connection(self, mock_host): vol_name = VOLUME["name"] + "-cinder" mock_host.return_value = {"name": "some-host"} # Branch with manually created host self.driver.terminate_connection(VOLUME, CONNECTOR) self.array.disconnect_host.assert_called_with("some-host", vol_name) self.assertFalse(self.array.list_host_connections.called) self.assertFalse(self.array.delete_host.called) # Branch with host added to host group self.array.reset_mock() self.array.list_host_connections.return_value = [] mock_host.return_value = PURE_HOST.copy() mock_host.return_value.update(hgroup="some-group") self.driver.terminate_connection(VOLUME, CONNECTOR) self.array.disconnect_host.assert_called_with(PURE_HOST_NAME, vol_name) self.assertTrue(self.array.list_host_connections.called) self.assertTrue(self.array.delete_host.called) # Branch with host still having connected volumes self.array.reset_mock() self.array.list_host_connections.return_value = [ {"lun": 2, "name": PURE_HOST_NAME, "vol": "some-vol"}] mock_host.return_value = PURE_HOST self.driver.terminate_connection(VOLUME, CONNECTOR) self.array.disconnect_host.assert_called_with(PURE_HOST_NAME, vol_name) self.array.list_host_connections.assert_called_with(PURE_HOST_NAME, private=True) self.assertFalse(self.array.delete_host.called) # Branch where host gets deleted self.array.reset_mock() self.array.list_host_connections.return_value = [] self.driver.terminate_connection(VOLUME, CONNECTOR) self.array.disconnect_host.assert_called_with(PURE_HOST_NAME, vol_name) self.array.list_host_connections.assert_called_with(PURE_HOST_NAME, private=True) self.array.delete_host.assert_called_with(PURE_HOST_NAME) # Branch where connection is missing and the host is still deleted self.array.reset_mock() self.array.disconnect_host.side_effect = \ self.purestorage_module.PureHTTPError(code=400, text="reason") self.driver.terminate_connection(VOLUME, CONNECTOR) self.array.disconnect_host.assert_called_with(PURE_HOST_NAME, vol_name) self.array.list_host_connections.assert_called_with(PURE_HOST_NAME, private=True) self.array.delete_host.assert_called_with(PURE_HOST_NAME) # Branch where an unexpected exception occurs self.array.reset_mock() self.array.disconnect_host.side_effect = \ self.purestorage_module.PureHTTPError( code=500, text="Some other error" ) self.assertRaises(self.purestorage_module.PureHTTPError, self.driver.terminate_connection, VOLUME, CONNECTOR) self.array.disconnect_host.assert_called_with(PURE_HOST_NAME, vol_name) self.assertFalse(self.array.list_host_connections.called) self.assertFalse(self.array.delete_host.called) @mock.patch(DRIVER_OBJ + "._get_provisioned_space", autospec=True) def test_get_volume_stats(self, mock_space): mock_space.return_value = PROVISIONED_CAPACITY * units.Gi self.assertEqual(self.driver.get_volume_stats(), {}) self.array.get.return_value = SPACE_INFO result = {"volume_backend_name": VOLUME_BACKEND_NAME, "vendor_name": "Pure Storage", "driver_version": self.driver.VERSION, "storage_protocol": "iSCSI", "total_capacity_gb": TOTAL_CAPACITY, "free_capacity_gb": TOTAL_CAPACITY - USED_SPACE, "reserved_percentage": 0, "consistencygroup_support": True, "thin_provisioning_support": True, "provisioned_capacity": PROVISIONED_CAPACITY, "max_over_subscription_ratio": (PROVISIONED_CAPACITY / USED_SPACE) } real_result = self.driver.get_volume_stats(refresh=True) self.assertDictMatch(result, real_result) self.assertDictMatch(result, self.driver._stats) @mock.patch(DRIVER_OBJ + "._get_provisioned_space", autospec=True) def test_get_volume_stats_empty_array(self, mock_space): mock_space.return_value = PROVISIONED_CAPACITY * units.Gi self.assertEqual(self.driver.get_volume_stats(), {}) self.array.get.return_value = SPACE_INFO_EMPTY result = {"volume_backend_name": VOLUME_BACKEND_NAME, "vendor_name": "Pure Storage", "driver_version": self.driver.VERSION, "storage_protocol": "iSCSI", "total_capacity_gb": TOTAL_CAPACITY, "free_capacity_gb": TOTAL_CAPACITY, "reserved_percentage": 0, "consistencygroup_support": True, "thin_provisioning_support": True, "provisioned_capacity": PROVISIONED_CAPACITY, "max_over_subscription_ratio": DEFAULT_OVER_SUBSCRIPTION } real_result = self.driver.get_volume_stats(refresh=True) self.assertDictMatch(result, real_result) self.assertDictMatch(result, self.driver._stats) @mock.patch(DRIVER_OBJ + "._get_provisioned_space", autospec=True) def test_get_volume_stats_nothing_provisioned(self, mock_space): mock_space.return_value = 0 self.assertEqual(self.driver.get_volume_stats(), {}) self.array.get.return_value = SPACE_INFO result = {"volume_backend_name": VOLUME_BACKEND_NAME, "vendor_name": "Pure Storage", "driver_version": self.driver.VERSION, "storage_protocol": "iSCSI", "total_capacity_gb": TOTAL_CAPACITY, "free_capacity_gb": TOTAL_CAPACITY - USED_SPACE, "reserved_percentage": 0, "consistencygroup_support": True, "thin_provisioning_support": True, "provisioned_capacity": 0, "max_over_subscription_ratio": DEFAULT_OVER_SUBSCRIPTION } real_result = self.driver.get_volume_stats(refresh=True) self.assertDictMatch(result, real_result) self.assertDictMatch(result, self.driver._stats) def test_extend_volume(self): vol_name = VOLUME["name"] + "-cinder" self.driver.extend_volume(VOLUME, 3) self.array.extend_volume.assert_called_with(vol_name, 3 * units.Gi) self.assert_error_propagates([self.array.extend_volume], self.driver.extend_volume, VOLUME, 3) def test_get_pgroup_name_from_id(self): id = "4a2f7e3a-312a-40c5-96a8-536b8a0fe074" expected_name = "consisgroup-%s-cinder" % id actual_name = pure._get_pgroup_name_from_id(id) self.assertEqual(expected_name, actual_name) def test_get_pgroup_snap_suffix(self): cgsnap = mock.Mock() cgsnap.id = "4a2f7e3a-312a-40c5-96a8-536b8a0fe074" expected_suffix = "cgsnapshot-%s-cinder" % cgsnap.id actual_suffix = pure._get_pgroup_snap_suffix(cgsnap) self.assertEqual(expected_suffix, actual_suffix) def test_get_pgroup_snap_name(self): cg_id = "4a2f7e3a-312a-40c5-96a8-536b8a0fe074" cgsnap_id = "4a2f7e3a-312a-40c5-96a8-536b8a0fe075" mock_cgsnap = mock.Mock() mock_cgsnap.consistencygroup_id = cg_id mock_cgsnap.id = cgsnap_id expected_name = "consisgroup-%(cg)s-cinder.cgsnapshot-%(snap)s-cinder"\ % {"cg": cg_id, "snap": cgsnap_id} actual_name = pure._get_pgroup_snap_name(mock_cgsnap) self.assertEqual(expected_name, actual_name) def test_get_pgroup_vol_snap_name(self): cg_id = "4a2f7e3a-312a-40c5-96a8-536b8a0fe074" cgsnap_id = "4a2f7e3a-312a-40c5-96a8-536b8a0fe075" volume_name = "volume-4a2f7e3a-312a-40c5-96a8-536b8a0fe075" mock_snap = mock.Mock() mock_snap.cgsnapshot = mock.Mock() mock_snap.cgsnapshot.consistencygroup_id = cg_id mock_snap.cgsnapshot.id = cgsnap_id mock_snap.volume_name = volume_name expected_name = "consisgroup-%(cg)s-cinder.cgsnapshot-%(snap)s-cinder"\ ".%(vol)s-cinder" % {"cg": cg_id, "snap": cgsnap_id, "vol": volume_name} actual_name = pure._get_pgroup_vol_snap_name(mock_snap) self.assertEqual(expected_name, actual_name) def test_create_consistencygroup(self): mock_cgroup = mock.Mock() mock_cgroup.id = "4a2f7e3a-312a-40c5-96a8-536b8a0fe074" model_update = self.driver.create_consistencygroup(None, mock_cgroup) expected_name = pure._get_pgroup_name_from_id(mock_cgroup.id) self.array.create_pgroup.assert_called_with(expected_name) self.assertEqual({'status': 'available'}, model_update) self.assert_error_propagates( [self.array.create_pgroup], self.driver.create_consistencygroup, None, mock_cgroup) @mock.patch(DRIVER_OBJ + ".delete_volume", autospec=True) def test_delete_consistencygroup(self, mock_delete_volume): mock_cgroup = mock.MagicMock() mock_cgroup.id = "4a2f7e3a-312a-40c5-96a8-536b8a0fe074" mock_cgroup['status'] = "deleted" mock_context = mock.Mock() self.driver.db = mock.Mock() mock_volume = mock.MagicMock() expected_volumes = [mock_volume] self.driver.db.volume_get_all_by_group.return_value = expected_volumes model_update, volumes = \ self.driver.delete_consistencygroup(mock_context, mock_cgroup) expected_name = pure._get_pgroup_name_from_id(mock_cgroup.id) self.array.destroy_pgroup.assert_called_with(expected_name) self.assertEqual(expected_volumes, volumes) self.assertEqual(mock_cgroup['status'], model_update['status']) mock_delete_volume.assert_called_with(self.driver, mock_volume) self.array.destroy_pgroup.side_effect = \ self.purestorage_module.PureHTTPError( code=400, text="Protection group has been destroyed." ) self.driver.delete_consistencygroup(mock_context, mock_cgroup) self.array.destroy_pgroup.assert_called_with(expected_name) mock_delete_volume.assert_called_with(self.driver, mock_volume) self.array.destroy_pgroup.side_effect = \ self.purestorage_module.PureHTTPError( code=400, text="Protection group does not exist" ) self.driver.delete_consistencygroup(mock_context, mock_cgroup) self.array.destroy_pgroup.assert_called_with(expected_name) mock_delete_volume.assert_called_with(self.driver, mock_volume) self.array.destroy_pgroup.side_effect = \ self.purestorage_module.PureHTTPError( code=400, text="Some other error" ) self.assertRaises(self.purestorage_module.PureHTTPError, self.driver.delete_consistencygroup, mock_context, mock_volume) self.array.destroy_pgroup.side_effect = \ self.purestorage_module.PureHTTPError( code=500, text="Another different error" ) self.assertRaises(self.purestorage_module.PureHTTPError, self.driver.delete_consistencygroup, mock_context, mock_volume) self.array.destroy_pgroup.side_effect = None self.assert_error_propagates( [self.array.destroy_pgroup], self.driver.delete_consistencygroup, mock_context, mock_cgroup) def test_create_cgsnapshot(self): mock_cgsnap = mock.Mock() mock_cgsnap.id = "4a2f7e3a-312a-40c5-96a8-536b8a0fe074" mock_cgsnap.consistencygroup_id = \ "4a2f7e3a-312a-40c5-96a8-536b8a0fe075" mock_context = mock.Mock() self.driver.db = mock.Mock() mock_snap = mock.MagicMock() expected_snaps = [mock_snap] self.driver.db.snapshot_get_all_for_cgsnapshot.return_value = \ expected_snaps model_update, snapshots = \ self.driver.create_cgsnapshot(mock_context, mock_cgsnap) expected_pgroup_name = \ pure._get_pgroup_name_from_id(mock_cgsnap.consistencygroup_id) expected_snap_suffix = pure._get_pgroup_snap_suffix(mock_cgsnap) self.array.create_pgroup_snapshot\ .assert_called_with(expected_pgroup_name, suffix=expected_snap_suffix) self.assertEqual({'status': 'available'}, model_update) self.assertEqual(expected_snaps, snapshots) self.assertEqual('available', mock_snap.status) self.assert_error_propagates( [self.array.create_pgroup_snapshot], self.driver.create_cgsnapshot, mock_context, mock_cgsnap) @mock.patch(DRIVER_PATH + "._get_pgroup_snap_name", autospec=True) def test_delete_cgsnapshot(self, mock_get_snap_name): snap_name = "consisgroup-4a2f7e3a-312a-40c5-96a8-536b8a0f" \ "e074-cinder.4a2f7e3a-312a-40c5-96a8-536b8a0fe075" mock_get_snap_name.return_value = snap_name mock_cgsnap = mock.Mock() mock_cgsnap.status = 'deleted' mock_context = mock.Mock() mock_snap = mock.MagicMock() expected_snaps = [mock_snap] self.driver.db = mock.Mock() self.driver.db.snapshot_get_all_for_cgsnapshot.return_value = \ expected_snaps model_update, snapshots = \ self.driver.delete_cgsnapshot(mock_context, mock_cgsnap) self.array.destroy_pgroup.assert_called_with(snap_name) self.assertEqual({'status': mock_cgsnap.status}, model_update) self.assertEqual(expected_snaps, snapshots) self.assertEqual('deleted', mock_snap.status) self.array.destroy_pgroup.side_effect = \ self.purestorage_module.PureHTTPError( code=400, text="Protection group snapshot has been destroyed." ) self.driver.delete_cgsnapshot(mock_context, mock_cgsnap) self.array.destroy_pgroup.assert_called_with(snap_name) self.array.destroy_pgroup.side_effect = \ self.purestorage_module.PureHTTPError( code=400, text="Protection group snapshot does not exist" ) self.driver.delete_cgsnapshot(mock_context, mock_cgsnap) self.array.destroy_pgroup.assert_called_with(snap_name) self.array.destroy_pgroup.side_effect = \ self.purestorage_module.PureHTTPError( code=400, text="Some other error" ) self.assertRaises(self.purestorage_module.PureHTTPError, self.driver.delete_cgsnapshot, mock_context, mock_cgsnap) self.array.destroy_pgroup.side_effect = \ self.purestorage_module.PureHTTPError( code=500, text="Another different error" ) self.assertRaises(self.purestorage_module.PureHTTPError, self.driver.delete_cgsnapshot, mock_context, mock_cgsnap) self.array.destroy_pgroup.side_effect = None self.assert_error_propagates( [self.array.destroy_pgroup], self.driver.delete_cgsnapshot, mock_context, mock_cgsnap) def test_manage_existing(self): ref_name = 'vol1' volume_ref = {'name': ref_name} self.array.list_volume_private_connections.return_value = [] vol_name = VOLUME['name'] + '-cinder' self.driver.manage_existing(VOLUME, volume_ref) self.array.list_volume_private_connections.assert_called_with(ref_name) self.array.rename_volume.assert_called_with(ref_name, vol_name) def test_manage_existing_error_propagates(self): self.array.list_volume_private_connections.return_value = [] self.assert_error_propagates( [self.array.list_volume_private_connections, self.array.rename_volume], self.driver.manage_existing, VOLUME, {'name': 'vol1'} ) def test_manage_existing_bad_ref(self): self.assertRaises(exception.ManageExistingInvalidReference, self.driver.manage_existing, VOLUME, {'bad_key': 'bad_value'}) self.assertRaises(exception.ManageExistingInvalidReference, self.driver.manage_existing, VOLUME, {'name': ''}) self.assertRaises(exception.ManageExistingInvalidReference, self.driver.manage_existing, VOLUME, {'name': None}) self.array.get_volume.side_effect = \ self.purestorage_module.PureHTTPError( text="Volume does not exist.", code=400 ) self.assertRaises(exception.ManageExistingInvalidReference, self.driver.manage_existing, VOLUME, {'name': 'non-existing-volume'}) def test_manage_existing_with_connected_hosts(self): ref_name = 'vol1' self.array.list_volume_private_connections.return_value = \ ["host1", "host2"] self.assertRaises(exception.ManageExistingInvalidReference, self.driver.manage_existing, VOLUME, {'name': ref_name}) self.array.list_volume_private_connections.assert_called_with(ref_name) self.assertFalse(self.array.rename_volume.called) def test_manage_existing_get_size(self): ref_name = 'vol1' volume_ref = {'name': ref_name} expected_size = 5 self.array.get_volume.return_value = {"size": 5368709120} size = self.driver.manage_existing_get_size(VOLUME, volume_ref) self.assertEqual(expected_size, size) self.array.get_volume.assert_called_with(ref_name) def test_manage_existing_get_size_error_propagates(self): self.array.get_volume.return_value = mock.MagicMock() self.assert_error_propagates([self.array.get_volume], self.driver.manage_existing_get_size, VOLUME, {'name': 'vol1'}) def test_manage_existing_get_size_bad_ref(self): self.assertRaises(exception.ManageExistingInvalidReference, self.driver.manage_existing_get_size, VOLUME, {'bad_key': 'bad_value'}) self.assertRaises(exception.ManageExistingInvalidReference, self.driver.manage_existing_get_size, VOLUME, {'name': ''}) self.assertRaises(exception.ManageExistingInvalidReference, self.driver.manage_existing_get_size, VOLUME, {'name': None}) def test_unmanage(self): vol_name = VOLUME['name'] + "-cinder" unmanaged_vol_name = vol_name + "-unmanaged" self.driver.unmanage(VOLUME) self.array.rename_volume.assert_called_with(vol_name, unmanaged_vol_name) def test_unmanage_error_propagates(self): self.assert_error_propagates([self.array.rename_volume], self.driver.unmanage, VOLUME) def test_unmanage_with_deleted_volume(self): vol_name = VOLUME['name'] + "-cinder" unmanaged_vol_name = vol_name + "-unmanaged" self.array.rename_volume.side_effect = \ self.purestorage_module.PureHTTPError( text="Volume does not exist.", code=400 ) self.driver.unmanage(VOLUME) self.array.rename_volume.assert_called_with(vol_name, unmanaged_vol_name)