patch_retry.stop()
DRIVER_PATH = "cinder.volume.drivers.pure"
-DRIVER_OBJ = DRIVER_PATH + ".PureISCSIDriver"
+BASE_DRIVER_OBJ = DRIVER_PATH + ".PureBaseVolumeDriver"
+ISCSI_DRIVER_OBJ = DRIVER_PATH + ".PureISCSIDriver"
ARRAY_OBJ = DRIVER_PATH + ".FlashArray"
TARGET = "pure-target"
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": [],
- }
+PURE_HOST_NAME = pure.PureBaseVolumeDriver._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 = {
+ "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
- }
+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 = {
+ "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"
"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",
- }
+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"},
- ]
+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
- }
-
-CONNECTION_INFO = {"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",
- },
- }
+SPACE_INFO = {
+ "capacity": TOTAL_CAPACITY * units.Gi,
+ "total": USED_SPACE * units.Gi,
+}
+SPACE_INFO_EMPTY = {
+ "capacity": TOTAL_CAPACITY * units.Gi,
+ "total": 0,
+}
+
+CONNECTION_INFO = {
+ "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",
+ },
+}
class FakePureStorageHTTPError(Exception):
self.text = text
-class PureISCSIDriverTestCase(test.TestCase):
-
+class PureDriverTestCase(test.TestCase):
def setUp(self):
- super(PureISCSIDriverTestCase, self).setUp()
+ super(PureDriverTestCase, self).setUp()
self.mock_config = mock.Mock()
self.mock_config.san_ip = TARGET
self.mock_config.pure_api_token = API_TOKEN
self.mock_config.volume_backend_name = VOLUME_BACKEND_NAME
- self.mock_config.use_chap_auth = False
- self.driver = pure.PureISCSIDriver(configuration=self.mock_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.
func, *args, **kwargs)
mock_func.side_effect = None
+
+class PureBaseVolumeDriverTestCase(PureDriverTestCase):
+
+ def setUp(self):
+ super(PureBaseVolumeDriverTestCase, self).setUp()
+ self.driver = pure.PureBaseVolumeDriver(configuration=self.mock_config)
+ self.driver._array = self.array
+
def test_generate_purity_host_name(self):
- generate = pure._generate_purity_host_name
- result = generate("really-long-string-thats-a-bit-too-long")
+ result = self.driver._generate_purity_host_name(
+ "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.assertEqual(63, len(result))
self.assertTrue(pure.GENERATED_NAME.match(result))
- result = generate("!@#$%^-invalid&*")
+ result = self.driver._generate_purity_host_name("!@#$%^-invalid&*")
self.assertTrue(result.startswith("invalid---"))
self.assertTrue(result.endswith("-cinder"))
- self.assertEqual(len(result), 49)
+ self.assertEqual(49, len(result))
self.assertTrue(pure.GENERATED_NAME.match(result))
def test_create_volume(self):
self.assert_error_propagates([self.array.create_volume],
self.driver.create_volume, VOLUME)
- @mock.patch(DRIVER_OBJ + "._add_volume_to_consistency_group",
+ @mock.patch(BASE_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_from_snapshot, VOLUME, SNAPSHOT)
SNAPSHOT["volume_size"] = 2 # reset size
- @mock.patch(DRIVER_OBJ + "._add_volume_to_consistency_group",
+ @mock.patch(BASE_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)
+ @mock.patch(BASE_DRIVER_OBJ + "._extend_if_needed", autospec=True)
+ @mock.patch(BASE_DRIVER_OBJ + "._get_pgroup_vol_snap_name",
+ spec=pure.PureBaseVolumeDriver._get_pgroup_vol_snap_name)
def test_create_volume_from_cgsnapshot(self, mock_get_snap_name,
mock_extend_if_needed,
mock_add_to_cgroup):
self.driver.create_cloned_volume, VOLUME, SRC_VOL)
SRC_VOL["size"] = 2 # reset size
- @mock.patch(DRIVER_OBJ + "._add_volume_to_consistency_group",
+ @mock.patch(BASE_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"
"host": host_name_a,
"lun": 7,
"name": vol_name,
- "size": 3221225472
+ "size": 3221225472,
}, {
"host": host_name_b,
"lun": 2,
"name": vol_name,
- "size": 3221225472
+ "size": 3221225472,
}]
self.driver.delete_volume(VOLUME)
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 = CONNECTION_INFO
- 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, None)
- self.assert_error_propagates([mock_get_iscsi_port, mock_connection],
- self.driver.initialize_connection,
- VOLUME, CONNECTOR)
-
- @mock.patch(DRIVER_OBJ + "._connect")
- @mock.patch(DRIVER_OBJ + "._get_target_iscsi_port")
- def test_initialize_connection_with_auth(self, mock_get_iscsi_port,
- mock_connection):
- auth_type = "CHAP"
- chap_username = CONNECTOR["host"]
- chap_password = "password"
- mock_get_iscsi_port.return_value = ISCSI_PORTS[0]
- initiator_update = [{"key": pure.CHAP_SECRET_KEY,
- "value": chap_password}]
- mock_connection.return_value = {
- "vol": VOLUME["name"] + "-cinder",
- "lun": 1,
- "auth_username": chap_username,
- "auth_password": chap_password,
- }
- result = CONNECTION_INFO.copy()
- result["data"]["auth_method"] = auth_type
- result["data"]["auth_username"] = chap_username
- result["data"]["auth_password"] = chap_password
-
- self.mock_config.use_chap_auth = True
-
- # Branch where no credentials were generated
- real_result = self.driver.initialize_connection(VOLUME,
- CONNECTOR)
- mock_connection.assert_called_with(VOLUME, CONNECTOR, None)
- self.assertDictMatch(result, real_result)
-
- # Branch where new credentials were generated
- mock_connection.return_value["initiator_update"] = initiator_update
- result["initiator_update"] = initiator_update
- real_result = self.driver.initialize_connection(VOLUME,
- CONNECTOR)
- mock_connection.assert_called_with(VOLUME, CONNECTOR, None)
- self.assertDictMatch(result, real_result)
-
- 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_PATH + "._generate_chap_secret", autospec=True)
- @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, mock_gen_secret):
- 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, None)
- 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, None)
- 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)
-
- 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, None)
-
- self.mock_config.use_chap_auth = True
- chap_user = CONNECTOR["host"]
- chap_password = "sOmEseCr3t"
-
- # Branch where chap is used and credentials already exist
- initiator_data = [{"key": pure.CHAP_SECRET_KEY,
- "value": chap_password}]
- self.driver._connect(VOLUME, CONNECTOR, initiator_data)
- result["auth_username"] = chap_user
- result["auth_password"] = chap_password
- self.assertDictMatch(result, real_result)
- self.array.set_host.assert_called_with(PURE_HOST_NAME,
- host_user=chap_user,
- host_password=chap_password)
-
- # Branch where chap is used and credentials are generated
- mock_gen_secret.return_value = chap_password
- self.driver._connect(VOLUME, CONNECTOR, None)
- result["auth_username"] = chap_user
- result["auth_password"] = chap_password
- result["initiator_update"] = {
- "set_values": {
- pure.CHAP_SECRET_KEY: chap_password
- }
- }
- self.assertDictMatch(result, real_result)
- self.array.set_host.assert_called_with(PURE_HOST_NAME,
- host_user=chap_user,
- host_password=chap_password)
-
- @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, None)
- 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, None)
- 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, None)
- 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)
+ @mock.patch(BASE_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"}
self.assertFalse(self.array.list_host_connections.called)
self.assertFalse(self.array.delete_host.called)
- @mock.patch(DRIVER_OBJ + ".get_filter_function", autospec=True)
- @mock.patch(DRIVER_OBJ + "._get_provisioned_space", autospec=True)
+ @mock.patch(BASE_DRIVER_OBJ + ".get_filter_function", autospec=True)
+ @mock.patch(BASE_DRIVER_OBJ + "._get_provisioned_space", autospec=True)
def test_get_volume_stats(self, mock_space, mock_filter):
filter_function = "capabilities.total_volumes < 10"
mock_space.return_value = (PROVISIONED_CAPACITY * units.Gi, 100)
mock_filter.return_value = filter_function
- self.assertEqual(self.driver.get_volume_stats(), {})
+ 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),
- "total_volumes": 100,
- "filter_function": filter_function
- }
+ result = {
+ "volume_backend_name": VOLUME_BACKEND_NAME,
+ "vendor_name": "Pure Storage",
+ "driver_version": self.driver.VERSION,
+ "storage_protocol": None,
+ "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),
+ "total_volumes": 100,
+ "filter_function": filter_function,
+ }
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_filter_function", autospec=True)
- @mock.patch(DRIVER_OBJ + "._get_provisioned_space", autospec=True)
+ @mock.patch(BASE_DRIVER_OBJ + ".get_filter_function", autospec=True)
+ @mock.patch(BASE_DRIVER_OBJ + "._get_provisioned_space", autospec=True)
def test_get_volume_stats_empty_array(self, mock_space, mock_filter):
filter_function = "capabilities.total_volumes < 10"
mock_space.return_value = (PROVISIONED_CAPACITY * units.Gi, 100)
mock_filter.return_value = filter_function
- self.assertEqual(self.driver.get_volume_stats(), {})
+ 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,
- "total_volumes": 100,
- "filter_function": filter_function
- }
+ result = {
+ "volume_backend_name": VOLUME_BACKEND_NAME,
+ "vendor_name": "Pure Storage",
+ "driver_version": self.driver.VERSION,
+ "storage_protocol": None,
+ "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,
+ "total_volumes": 100,
+ "filter_function": filter_function,
+ }
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_filter_function", autospec=True)
- @mock.patch(DRIVER_OBJ + "._get_provisioned_space", autospec=True)
+ @mock.patch(BASE_DRIVER_OBJ + ".get_filter_function", autospec=True)
+ @mock.patch(BASE_DRIVER_OBJ + "._get_provisioned_space", autospec=True)
def test_get_volume_stats_nothing_provisioned(self, mock_space,
mock_filter):
filter_function = "capabilities.total_volumes < 10"
mock_space.return_value = (0, 0)
mock_filter.return_value = filter_function
- self.assertEqual(self.driver.get_volume_stats(), {})
+ 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,
- "total_volumes": 0,
- "filter_function": filter_function
- }
+ result = {
+ "volume_backend_name": VOLUME_BACKEND_NAME,
+ "vendor_name": "Pure Storage",
+ "driver_version": self.driver.VERSION,
+ "storage_protocol": None,
+ "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,
+ "total_volumes": 0,
+ "filter_function": filter_function,
+ }
real_result = self.driver.get_volume_stats(refresh=True)
self.assertDictMatch(result, real_result)
self.assertDictMatch(result, self.driver._stats)
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)
+ actual_name = self.driver._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)
+ actual_suffix = self.driver._get_pgroup_snap_suffix(cgsnap)
self.assertEqual(expected_suffix, actual_suffix)
def test_get_pgroup_snap_name(self):
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)
+ actual_name = self.driver._get_pgroup_snap_name(mock_cgsnap)
self.assertEqual(expected_name, actual_name)
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}
+ ".%(vol)s-cinder" % {
+ "cg": cg_id,
+ "snap": cgsnap_id,
+ "vol": volume_name,
+ }
- actual_name = pure._get_pgroup_vol_snap_name(mock_snap)
+ actual_name = self.driver._get_pgroup_vol_snap_name(mock_snap)
self.assertEqual(expected_name, actual_name)
model_update = self.driver.create_consistencygroup(None, mock_cgroup)
- expected_name = pure._get_pgroup_name_from_id(mock_cgroup.id)
+ expected_name = self.driver._get_pgroup_name_from_id(mock_cgroup.id)
self.array.create_pgroup.assert_called_with(expected_name)
self.assertEqual({'status': 'available'}, model_update)
[self.array.create_pgroup],
self.driver.create_consistencygroup, None, mock_cgroup)
- @mock.patch(DRIVER_OBJ + ".create_volume_from_snapshot")
- @mock.patch(DRIVER_OBJ + ".create_consistencygroup")
+ @mock.patch(BASE_DRIVER_OBJ + ".create_volume_from_snapshot")
+ @mock.patch(BASE_DRIVER_OBJ + ".create_consistencygroup")
def test_create_consistencygroup_from_src(self, mock_create_cg,
mock_create_vol):
mock_context = mock.Mock()
mock.Mock(), # group
[mock.Mock()]) # volumes
- @mock.patch(DRIVER_OBJ + ".delete_volume", autospec=True)
+ @mock.patch(BASE_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"
model_update, volumes = \
self.driver.delete_consistencygroup(mock_context, mock_cgroup)
- expected_name = pure._get_pgroup_name_from_id(mock_cgroup.id)
+ expected_name = self.driver._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'])
expected_addvollist = [vol['name'] + '-cinder' for vol in add_vols]
remove_vols = [
{'name': 'vol4'},
- {'name': 'vol5'}
+ {'name': 'vol5'},
]
expected_remvollist = [vol['name'] + '-cinder' for vol in remove_vols]
self.driver.update_consistencygroup(mock.Mock(), mock_group,
expected_addvollist = []
remove_vols = [
{'name': 'vol4'},
- {'name': 'vol5'}
+ {'name': 'vol5'},
]
expected_remvollist = [vol['name'] + '-cinder' for vol in remove_vols]
self.driver.update_consistencygroup(mock.Mock(), mock_group,
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)
+ cg_id = mock_cgsnap.consistencygroup_id
+ expected_pgroup_name = self.driver._get_pgroup_name_from_id(cg_id)
+ expected_snap_suffix = self.driver._get_pgroup_snap_suffix(mock_cgsnap)
self.array.create_pgroup_snapshot\
.assert_called_with(expected_pgroup_name,
suffix=expected_snap_suffix)
[self.array.create_pgroup_snapshot],
self.driver.create_cgsnapshot, mock_context, mock_cgsnap)
- @mock.patch(DRIVER_PATH + "._get_pgroup_snap_name", autospec=True)
+ @mock.patch(BASE_DRIVER_OBJ + "._get_pgroup_snap_name",
+ spec=pure.PureBaseVolumeDriver._get_pgroup_snap_name)
def test_delete_cgsnapshot(self, mock_get_snap_name):
snap_name = "consisgroup-4a2f7e3a-312a-40c5-96a8-536b8a0f" \
"e074-cinder.4a2f7e3a-312a-40c5-96a8-536b8a0fe075"
self.array.rename_volume.assert_called_with(vol_name,
unmanaged_vol_name)
+
+
+class PureISCSIDriverTestCase(PureDriverTestCase):
+
+ def setUp(self):
+ super(PureISCSIDriverTestCase, self).setUp()
+ self.mock_config.use_chap_auth = False
+ self.driver = pure.PureISCSIDriver(configuration=self.mock_config)
+ self.driver._array = self.array
+
+ @mock.patch(ISCSI_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 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(good_host, real_result)
+ self.assert_error_propagates([self.array.list_hosts],
+ self.driver._get_host, CONNECTOR)
+
+ @mock.patch(ISCSI_DRIVER_OBJ + "._connect")
+ @mock.patch(ISCSI_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 = CONNECTION_INFO
+ 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, None)
+ self.assert_error_propagates([mock_get_iscsi_port, mock_connection],
+ self.driver.initialize_connection,
+ VOLUME, CONNECTOR)
+
+ @mock.patch(ISCSI_DRIVER_OBJ + "._connect")
+ @mock.patch(ISCSI_DRIVER_OBJ + "._get_target_iscsi_port")
+ def test_initialize_connection_with_auth(self, mock_get_iscsi_port,
+ mock_connection):
+ auth_type = "CHAP"
+ chap_username = CONNECTOR["host"]
+ chap_password = "password"
+ mock_get_iscsi_port.return_value = ISCSI_PORTS[0]
+ initiator_update = [{"key": pure.CHAP_SECRET_KEY,
+ "value": chap_password}]
+ mock_connection.return_value = {
+ "vol": VOLUME["name"] + "-cinder",
+ "lun": 1,
+ "auth_username": chap_username,
+ "auth_password": chap_password,
+ }
+ result = CONNECTION_INFO.copy()
+ result["data"]["auth_method"] = auth_type
+ result["data"]["auth_username"] = chap_username
+ result["data"]["auth_password"] = chap_password
+
+ self.mock_config.use_chap_auth = True
+
+ # Branch where no credentials were generated
+ real_result = self.driver.initialize_connection(VOLUME,
+ CONNECTOR)
+ mock_connection.assert_called_with(VOLUME, CONNECTOR, None)
+ self.assertDictMatch(result, real_result)
+
+ # Branch where new credentials were generated
+ mock_connection.return_value["initiator_update"] = initiator_update
+ result["initiator_update"] = initiator_update
+ real_result = self.driver.initialize_connection(VOLUME,
+ CONNECTOR)
+ mock_connection.assert_called_with(VOLUME, CONNECTOR, None)
+ self.assertDictMatch(result, real_result)
+
+ self.assert_error_propagates([mock_get_iscsi_port, mock_connection],
+ self.driver.initialize_connection,
+ VOLUME, CONNECTOR)
+
+ @mock.patch(ISCSI_DRIVER_OBJ + "._choose_target_iscsi_port")
+ @mock.patch(ISCSI_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(ISCSI_PORTS[1], self.driver._get_target_iscsi_port())
+ 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(ISCSI_PORTS[2], self.driver._get_target_iscsi_port())
+ 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(ISCSI_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("cinder.volume.utils.generate_password", autospec=True)
+ @mock.patch(ISCSI_DRIVER_OBJ + "._get_host", autospec=True)
+ @mock.patch(ISCSI_DRIVER_OBJ + "._generate_purity_host_name", spec=True)
+ def test_connect(self, mock_generate, mock_host, mock_gen_secret):
+ 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, None)
+ 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, None)
+ 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)
+
+ 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, None)
+
+ self.mock_config.use_chap_auth = True
+ chap_user = CONNECTOR["host"]
+ chap_password = "sOmEseCr3t"
+
+ # Branch where chap is used and credentials already exist
+ initiator_data = [{"key": pure.CHAP_SECRET_KEY,
+ "value": chap_password}]
+ self.driver._connect(VOLUME, CONNECTOR, initiator_data)
+ result["auth_username"] = chap_user
+ result["auth_password"] = chap_password
+ self.assertDictMatch(result, real_result)
+ self.array.set_host.assert_called_with(PURE_HOST_NAME,
+ host_user=chap_user,
+ host_password=chap_password)
+
+ # Branch where chap is used and credentials are generated
+ mock_gen_secret.return_value = chap_password
+ self.driver._connect(VOLUME, CONNECTOR, None)
+ result["auth_username"] = chap_user
+ result["auth_password"] = chap_password
+ result["initiator_update"] = {
+ "set_values": {
+ pure.CHAP_SECRET_KEY: chap_password
+ },
+ }
+ self.assertDictMatch(result, real_result)
+ self.array.set_host.assert_called_with(PURE_HOST_NAME,
+ host_user=chap_user,
+ host_password=chap_password)
+
+ @mock.patch(ISCSI_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, None)
+ self.assertEqual(expected, actual)
+ self.assertTrue(self.array.connect_host.called)
+ self.assertTrue(self.array.list_volume_private_connections)
+
+ @mock.patch(ISCSI_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, None)
+ self.assertTrue(self.array.connect_host.called)
+ self.assertTrue(self.array.list_volume_private_connections)
+
+ @mock.patch(ISCSI_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, None)
+ self.assertTrue(self.array.connect_host.called)
+ self.assertTrue(self.array.list_volume_private_connections)
ERR_MSG_PENDING_ERADICATION = "has been destroyed"
-def _get_vol_name(volume):
- """Return the name of the volume Purity will use."""
- return volume["name"] + "-cinder"
+def log_debug_trace(f):
+ def wrapper(*args, **kwargs):
+ cls_name = args[0].__class__.__name__
+ method_name = "%(cls_name)s.%(method)s" % {"cls_name": cls_name,
+ "method": f.func_name}
+ LOG.debug("Enter " + method_name)
+ result = f(*args, **kwargs)
+ LOG.debug("Leave " + method_name)
+ return result
+ return wrapper
-def _get_snap_name(snapshot):
- """Return the name of the snapshot that Purity will use."""
- return "%s-cinder.%s" % (snapshot["volume_name"], snapshot["name"])
-
-def _get_pgroup_name_from_id(id):
- return "consisgroup-%s-cinder" % id
-
-
-def _get_pgroup_snap_suffix(cgsnapshot):
- return "cgsnapshot-%s-cinder" % cgsnapshot.id
-
-
-def _get_pgroup_snap_name(cgsnapshot):
- """Return the name of the pgroup snapshot that Purity will use"""
- return "%s.%s" % (_get_pgroup_name_from_id(cgsnapshot.consistencygroup_id),
- _get_pgroup_snap_suffix(cgsnapshot))
-
-
-def _get_pgroup_vol_snap_name(snapshot):
- """Return the name of the snapshot that Purity will use for a volume."""
- cg_name = _get_pgroup_name_from_id(snapshot.cgsnapshot.consistencygroup_id)
- cgsnapshot_id = _get_pgroup_snap_suffix(snapshot.cgsnapshot)
- volume_name = snapshot.volume_name
- return "%s.%s.%s-cinder" % (cg_name, cgsnapshot_id, volume_name)
-
-
-def _generate_purity_host_name(name):
- """Return a valid Purity host name based on the name passed in."""
- if len(name) > 23:
- name = name[0:23]
- name = INVALID_CHARACTERS.sub("-", name)
- name = name.lstrip("-")
- return "{name}-{uuid}-cinder".format(name=name, uuid=uuid.uuid4().hex)
-
-
-def _generate_chap_secret():
- return volume_utils.generate_password()
-
-
-class PureISCSIDriver(san.SanISCSIDriver):
+class PureBaseVolumeDriver(san.SanDriver):
"""Performs volume management on Pure Storage FlashArray."""
- VERSION = "2.0.6"
-
SUPPORTED_REST_API_VERSIONS = ['1.2', '1.3', '1.4']
def __init__(self, *args, **kwargs):
execute = kwargs.pop("execute", utils.execute)
- super(PureISCSIDriver, self).__init__(execute=execute, *args, **kwargs)
+ super(PureBaseVolumeDriver, self).__init__(execute=execute, *args,
+ **kwargs)
self.configuration.append_config_values(PURE_OPTS)
self._array = None
- self._iscsi_port = None
+ self._storage_protocol = None
self._backend_name = (self.configuration.volume_backend_name or
self.__class__.__name__)
self._array = purestorage.FlashArray(
self.configuration.san_ip,
api_token=self.configuration.pure_api_token)
- self._iscsi_port = self._choose_target_iscsi_port()
def check_for_setup_error(self):
# Avoid inheriting check_for_setup_error from SanDriver, which checks
# for san_password or san_private_key, not relevant to our driver.
pass
+ @log_debug_trace
def create_volume(self, volume):
"""Creates a volume."""
- LOG.debug("Enter PureISCSIDriver.create_volume.")
- vol_name = _get_vol_name(volume)
+ vol_name = self._get_vol_name(volume)
vol_size = volume["size"] * units.Gi
self._array.create_volume(vol_name, vol_size)
volume['consistencygroup_id'],
vol_name
)
- LOG.debug("Leave PureISCSIDriver.create_volume.")
+ @log_debug_trace
def create_volume_from_snapshot(self, volume, snapshot):
"""Creates a volume from a snapshot."""
- LOG.debug("Enter PureISCSIDriver.create_volume_from_snapshot.")
- vol_name = _get_vol_name(volume)
+ vol_name = self._get_vol_name(volume)
if snapshot['cgsnapshot_id']:
- snap_name = _get_pgroup_vol_snap_name(snapshot)
+ snap_name = self._get_pgroup_vol_snap_name(snapshot)
else:
- snap_name = _get_snap_name(snapshot)
+ snap_name = self._get_snap_name(snapshot)
self._array.copy_volume(snap_name, vol_name)
self._extend_if_needed(vol_name, snapshot["volume_size"],
volume['consistencygroup_id'],
vol_name
)
- LOG.debug("Leave PureISCSIDriver.create_volume_from_snapshot.")
+ @log_debug_trace
def create_cloned_volume(self, volume, src_vref):
"""Creates a clone of the specified volume."""
- LOG.debug("Enter PureISCSIDriver.create_cloned_volume.")
- vol_name = _get_vol_name(volume)
- src_name = _get_vol_name(src_vref)
+ vol_name = self._get_vol_name(volume)
+ src_name = self._get_vol_name(src_vref)
self._array.copy_volume(src_name, vol_name)
self._extend_if_needed(vol_name, src_vref["size"], volume["size"])
vol_name
)
- LOG.debug("Leave PureISCSIDriver.create_cloned_volume.")
-
def _extend_if_needed(self, vol_name, src_size, vol_size):
"""Extend the volume from size src_size to size vol_size."""
if vol_size > src_size:
vol_size = vol_size * units.Gi
self._array.extend_volume(vol_name, vol_size)
+ @log_debug_trace
def delete_volume(self, volume):
"""Disconnect all hosts and delete the volume"""
- LOG.debug("Enter PureISCSIDriver.delete_volume.")
- vol_name = _get_vol_name(volume)
+ vol_name = self._get_vol_name(volume)
try:
connected_hosts = \
self._array.list_volume_private_connections(vol_name)
ctxt.reraise = False
LOG.warning(_LW("Volume deletion failed with message: %s"),
err.text)
- LOG.debug("Leave PureISCSIDriver.delete_volume.")
+ @log_debug_trace
def create_snapshot(self, snapshot):
"""Creates a snapshot."""
- LOG.debug("Enter PureISCSIDriver.create_snapshot.")
- vol_name, snap_suff = _get_snap_name(snapshot).split(".")
+ vol_name, snap_suff = self._get_snap_name(snapshot).split(".")
self._array.create_snapshot(vol_name, suffix=snap_suff)
- LOG.debug("Leave PureISCSIDriver.create_snapshot.")
+ @log_debug_trace
def delete_snapshot(self, snapshot):
"""Deletes a snapshot."""
- LOG.debug("Enter PureISCSIDriver.delete_snapshot.")
- snap_name = _get_snap_name(snapshot)
+ snap_name = self._get_snap_name(snapshot)
try:
self._array.destroy_volume(snap_name)
except purestorage.PureHTTPError as err:
ctxt.reraise = False
LOG.error(_LE("Snapshot deletion failed with message:"
" %s"), err.text)
- LOG.debug("Leave PureISCSIDriver.delete_snapshot.")
def ensure_export(self, context, volume):
pass
def create_export(self, context, volume):
pass
- def initialize_connection(self, volume, connector, initiator_data=None):
- """Allow connection to connector and return connection info."""
- LOG.debug("Enter PureISCSIDriver.initialize_connection.")
- target_port = self._get_target_iscsi_port()
- connection = self._connect(volume, connector, initiator_data)
- properties = {
- "driver_volume_type": "iscsi",
- "data": {
- "target_iqn": target_port["iqn"],
- "target_portal": target_port["portal"],
- "target_lun": connection["lun"],
- "target_discovered": True,
- "access_mode": "rw",
- },
- }
-
- if self.configuration.use_chap_auth:
- properties["data"]["auth_method"] = "CHAP"
- properties["data"]["auth_username"] = connection["auth_username"]
- properties["data"]["auth_password"] = connection["auth_password"]
-
- initiator_update = connection.get("initiator_update", False)
- if initiator_update:
- properties["initiator_update"] = initiator_update
-
- LOG.debug("Leave PureISCSIDriver.initialize_connection.")
- return properties
-
- def _get_target_iscsi_port(self):
- """Return dictionary describing iSCSI-enabled port on target array."""
- try:
- self._run_iscsiadm_bare(["-m", "discovery", "-t", "sendtargets",
- "-p", self._iscsi_port["portal"]])
- except processutils.ProcessExecutionError as err:
- LOG.warning(_LW("iSCSI discovery of port %(port_name)s at "
- "%(port_portal)s failed with error: %(err_msg)s"),
- {"port_name": self._iscsi_port["name"],
- "port_portal": self._iscsi_port["portal"],
- "err_msg": err.stderr})
- self._iscsi_port = self._choose_target_iscsi_port()
- return self._iscsi_port
-
- @utils.retry(exception.PureDriverException, retries=3)
- def _choose_target_iscsi_port(self):
- """Find a reachable iSCSI-enabled port on target array."""
- ports = self._array.list_ports()
- iscsi_ports = [port for port in ports if port["iqn"]]
- for port in iscsi_ports:
- try:
- self._run_iscsiadm_bare(["-m", "discovery",
- "-t", "sendtargets",
- "-p", port["portal"]])
- except processutils.ProcessExecutionError as err:
- LOG.debug(("iSCSI discovery of port %(port_name)s at "
- "%(port_portal)s failed with error: %(err_msg)s"),
- {"port_name": port["name"],
- "port_portal": port["portal"],
- "err_msg": err.stderr})
- else:
- LOG.info(_LI("Using port %(name)s on the array at %(portal)s "
- "for iSCSI connectivity."),
- {"name": port["name"], "portal": port["portal"]})
- return port
- raise exception.PureDriverException(
- reason=_("No reachable iSCSI-enabled ports on target array."))
-
- def _get_chap_credentials(self, host, data):
- initiator_updates = None
- username = host
- password = None
- if data:
- for d in data:
- if d["key"] == CHAP_SECRET_KEY:
- password = d["value"]
- break
- if not password:
- password = _generate_chap_secret()
- initiator_updates = {
- "set_values": {
- CHAP_SECRET_KEY: password
- }
- }
- return username, password, initiator_updates
-
- @utils.synchronized('PureISCSIDriver._connect', external=True)
- def _connect(self, volume, connector, initiator_data):
- """Connect the host and volume; return dict describing connection."""
- connection = None
- iqn = connector["initiator"]
-
- if self.configuration.use_chap_auth:
- (chap_username, chap_password, initiator_update) = \
- self._get_chap_credentials(connector['host'], initiator_data)
-
- vol_name = _get_vol_name(volume)
- host = self._get_host(connector)
-
- if host:
- host_name = host["name"]
- LOG.info(_LI("Re-using existing purity host %(host_name)r"),
- {"host_name": host_name})
- if self.configuration.use_chap_auth:
- if not GENERATED_NAME.match(host_name):
- LOG.error(_LE("Purity host %(host_name)s is not managed "
- "by Cinder and can't have CHAP credentials "
- "modified. Remove IQN %(iqn)s from the host "
- "to resolve this issue."),
- {"host_name": host_name,
- "iqn": connector["initiator"]})
- raise exception.PureDriverException(
- reason=_("Unable to re-use a host that is not "
- "managed by Cinder with use_chap_auth=True,"))
- elif chap_username is None or chap_password is None:
- LOG.error(_LE("Purity host %(host_name)s is managed by "
- "Cinder but CHAP credentials could not be "
- "retrieved from the Cinder database."),
- {"host_name": host_name})
- raise exception.PureDriverException(
- reason=_("Unable to re-use host with unknown CHAP "
- "credentials configured."))
- else:
- host_name = _generate_purity_host_name(connector["host"])
- LOG.info(_LI("Creating host object %(host_name)r with IQN:"
- " %(iqn)s."), {"host_name": host_name, "iqn": iqn})
- self._array.create_host(host_name, iqnlist=[iqn])
-
- if self.configuration.use_chap_auth:
- self._array.set_host(host_name,
- host_user=chap_username,
- host_password=chap_password)
-
- try:
- connection = self._array.connect_host(host_name, vol_name)
- except purestorage.PureHTTPError as err:
- with excutils.save_and_reraise_exception() as ctxt:
- if (err.code == 400 and
- "Connection already exists" in err.text):
- # Happens if the volume is already connected to the host.
- ctxt.reraise = False
- LOG.warning(_LW("Volume connection already exists with "
- "message: %s"), err.text)
- # Get the info for the existing connection
- connected_hosts = \
- self._array.list_volume_private_connections(vol_name)
- for host_info in connected_hosts:
- if host_info["host"] == host_name:
- connection = host_info
- break
- if not connection:
- raise exception.PureDriverException(
- reason=_("Unable to connect or find connection to host"))
-
- if self.configuration.use_chap_auth:
- connection["auth_username"] = chap_username
- connection["auth_password"] = chap_password
-
- if initiator_update:
- connection["initiator_update"] = initiator_update
-
- return connection
-
def _get_host(self, connector):
- """Return dict describing existing Purity host object or None."""
- hosts = self._array.list_hosts()
- for host in hosts:
- if connector["initiator"] in host["iqn"]:
- return host
- return None
+ """Get a Purity Host that corresponds to the host in the connector.
+
+ This implementation is specific to the host type (iSCSI, FC, etc).
+ """
+ raise NotImplementedError
+ @log_debug_trace
def terminate_connection(self, volume, connector, **kwargs):
"""Terminate connection."""
- LOG.debug("Enter PureISCSIDriver.terminate_connection.")
- vol_name = _get_vol_name(volume)
+ vol_name = self._get_vol_name(volume)
host = self._get_host(connector)
if host:
host_name = host["name"]
self._disconnect_host(host_name, vol_name)
else:
- LOG.error(_LE("Unable to find host object in Purity with IQN: "
- "%(iqn)s."), {"iqn": connector["initiator"]})
- LOG.debug("Leave PureISCSIDriver.terminate_connection.")
+ LOG.error(_LE("Unable to disconnect host from volume."))
+ @log_debug_trace
def _disconnect_host(self, host_name, vol_name):
- LOG.debug("Enter PureISCSIDriver._disconnect_host.")
try:
self._array.disconnect_host(host_name, vol_name)
except purestorage.PureHTTPError as err:
LOG.info(_LI("Deleting unneeded host %(host_name)r."),
{"host_name": host_name})
self._array.delete_host(host_name)
- LOG.debug("Leave PureISCSIDriver._disconnect_host.")
+ @log_debug_trace
def get_volume_stats(self, refresh=False):
"""Return the current state of the volume service.
If 'refresh' is True, run the update first.
"""
- LOG.debug("Enter PureISCSIDriver.get_volume_stats.")
if refresh:
LOG.debug("Updating volume stats.")
self._update_stats()
- LOG.debug("Leave PureISCSIDriver.get_volume_stats.")
return self._stats
def _update_stats(self):
thin_provisioning = 20
else:
thin_provisioning = provisioned_space / used_space
- data = {"volume_backend_name": self._backend_name,
- "vendor_name": "Pure Storage",
- "driver_version": self.VERSION,
- "storage_protocol": "iSCSI",
- "total_capacity_gb": total_capacity,
- "free_capacity_gb": free_space,
- "reserved_percentage": 0,
- "consistencygroup_support": True,
- "thin_provisioning_support": True,
- "provisioned_capacity": provisioned_space,
- "max_over_subscription_ratio": thin_provisioning,
- "total_volumes": total_vols,
- "filter_function": self.get_filter_function()
- }
+ data = {
+ "volume_backend_name": self._backend_name,
+ "vendor_name": "Pure Storage",
+ "driver_version": self.VERSION,
+ "storage_protocol": self._storage_protocol,
+ "total_capacity_gb": total_capacity,
+ "free_capacity_gb": free_space,
+ "reserved_percentage": 0,
+ "consistencygroup_support": True,
+ "thin_provisioning_support": True,
+ "provisioned_capacity": provisioned_space,
+ "max_over_subscription_ratio": thin_provisioning,
+ "total_volumes": total_vols,
+ "filter_function": self.get_filter_function(),
+ }
self._stats = data
def _get_provisioned_space(self):
volumes = self._array.list_volumes(pending=True)
return sum(item["size"] for item in volumes), len(volumes)
+ @log_debug_trace
def extend_volume(self, volume, new_size):
"""Extend volume to new_size."""
- LOG.debug("Enter PureISCSIDriver.extend_volume.")
- vol_name = _get_vol_name(volume)
+ vol_name = self._get_vol_name(volume)
new_size = new_size * units.Gi
self._array.extend_volume(vol_name, new_size)
- LOG.debug("Leave PureISCSIDriver.extend_volume.")
def _add_volume_to_consistency_group(self, consistencygroup_id, vol_name):
- pgroup_name = _get_pgroup_name_from_id(consistencygroup_id)
+ pgroup_name = self._get_pgroup_name_from_id(consistencygroup_id)
self._array.set_pgroup(pgroup_name, addvollist=[vol_name])
+ @log_debug_trace
def create_consistencygroup(self, context, group):
"""Creates a consistencygroup."""
- LOG.debug("Enter PureISCSIDriver.create_consistencygroup")
- self._array.create_pgroup(_get_pgroup_name_from_id(group.id))
+ self._array.create_pgroup(self._get_pgroup_name_from_id(group.id))
model_update = {'status': 'available'}
-
- LOG.debug("Leave PureISCSIDriver.create_consistencygroup")
return model_update
+ @log_debug_trace
def create_consistencygroup_from_src(self, context, group, volumes,
cgsnapshot=None, snapshots=None):
- LOG.debug("Enter PureISCSIDriver.create_consistencygroup_from_src")
if cgsnapshot and snapshots:
self.create_consistencygroup(context, group)
else:
msg = _("create_consistencygroup_from_src only supports a"
" cgsnapshot source, other sources cannot be used.")
- raise exception.InvalidInput(msg)
+ raise exception.InvalidInput(reason=msg)
- LOG.debug("Leave PureISCSIDriver.create_consistencygroup_from_src")
return None, None
+ @log_debug_trace
def delete_consistencygroup(self, context, group):
"""Deletes a consistency group."""
- LOG.debug("Enter PureISCSIDriver.delete_consistencygroup")
try:
- self._array.destroy_pgroup(_get_pgroup_name_from_id(group.id))
+ self._array.destroy_pgroup(self._get_pgroup_name_from_id(group.id))
except purestorage.PureHTTPError as err:
with excutils.save_and_reraise_exception() as ctxt:
if (err.code == 400 and
model_update = {'status': group['status']}
- LOG.debug("Leave PureISCSIDriver.delete_consistencygroup")
return model_update, volumes
+ @log_debug_trace
def update_consistencygroup(self, context, group,
add_volumes=None, remove_volumes=None):
- LOG.debug("Enter PureISCSIDriver.update_consistencygroup")
- pgroup_name = _get_pgroup_name_from_id(group.id)
+ pgroup_name = self._get_pgroup_name_from_id(group.id)
if add_volumes:
- addvollist = [_get_vol_name(volume) for volume in add_volumes]
+ addvollist = [self._get_vol_name(vol) for vol in add_volumes]
else:
addvollist = []
if remove_volumes:
- remvollist = [_get_vol_name(volume) for volume in remove_volumes]
+ remvollist = [self._get_vol_name(vol) for vol in remove_volumes]
else:
remvollist = []
self._array.set_pgroup(pgroup_name, addvollist=addvollist,
remvollist=remvollist)
- LOG.debug("Leave PureISCSIDriver.update_consistencygroup")
return None, None, None
+ @log_debug_trace
def create_cgsnapshot(self, context, cgsnapshot):
"""Creates a cgsnapshot."""
- LOG.debug("Enter PureISCSIDriver.create_cgsnapshot")
- pgroup_name = _get_pgroup_name_from_id(cgsnapshot.consistencygroup_id)
- pgsnap_suffix = _get_pgroup_snap_suffix(cgsnapshot)
+ cg_id = cgsnapshot.consistencygroup_id
+ pgroup_name = self._get_pgroup_name_from_id(cg_id)
+ pgsnap_suffix = self._get_pgroup_snap_suffix(cgsnapshot)
self._array.create_pgroup_snapshot(pgroup_name, suffix=pgsnap_suffix)
snapshots = self.db.snapshot_get_all_for_cgsnapshot(
model_update = {'status': 'available'}
- LOG.debug("Leave PureISCSIDriver.create_cgsnapshot")
return model_update, snapshots
+ @log_debug_trace
def delete_cgsnapshot(self, context, cgsnapshot):
"""Deletes a cgsnapshot."""
- LOG.debug("Enter PureISCSIDriver.delete_cgsnapshot")
- pgsnap_name = _get_pgroup_snap_name(cgsnapshot)
+ pgsnap_name = self._get_pgroup_snap_name(cgsnapshot)
try:
# FlashArray.destroy_pgroup is also used for deleting
model_update = {'status': cgsnapshot.status}
- LOG.debug("Leave PureISCSIDriver.delete_cgsnapshot")
return model_update, snapshots
def _validate_manage_existing_ref(self, existing_ref):
if "name" not in existing_ref or not existing_ref["name"]:
raise exception.ManageExistingInvalidReference(
existing_ref=existing_ref,
- reason=_("PureISCSIDriver manage_existing requires a 'name'"
+ reason=_("manage_existing requires a 'name'"
" key to identify an existing volume."))
ref_vol_name = existing_ref['name']
existing_ref=existing_ref,
reason=_("Unable to find volume with name=%s") % ref_vol_name)
+ @log_debug_trace
def manage_existing(self, volume, existing_ref):
"""Brings an existing backend storage object under Cinder management.
We expect a volume name in the existing_ref that matches one in Purity.
"""
- LOG.debug("Enter PureISCSIDriver.manage_existing.")
self._validate_manage_existing_ref(existing_ref)
"volume connected to hosts. Please disconnect the "
"volume from existing hosts before importing."))
- new_vol_name = _get_vol_name(volume)
+ new_vol_name = self._get_vol_name(volume)
LOG.info(_LI("Renaming existing volume %(ref_name)s to %(new_name)s"),
{"ref_name": ref_vol_name, "new_name": new_vol_name})
self._array.rename_volume(ref_vol_name, new_vol_name)
- LOG.debug("Leave PureISCSIDriver.manage_existing.")
return None
+ @log_debug_trace
def manage_existing_get_size(self, volume, existing_ref):
"""Return size of volume to be managed by manage_existing.
We expect a volume name in the existing_ref that matches one in Purity.
"""
- LOG.debug("Enter PureISCSIDriver.manage_existing_get_size.")
volume_info = self._validate_manage_existing_ref(existing_ref)
size = math.ceil(float(volume_info["size"]) / units.Gi)
- LOG.debug("Leave PureISCSIDriver.manage_existing_get_size.")
return size
+ @log_debug_trace
def unmanage(self, volume):
"""Removes the specified volume from Cinder management.
The volume will be renamed with "-unmanaged" as a suffix
"""
- vol_name = _get_vol_name(volume)
+ vol_name = self._get_vol_name(volume)
unmanaged_vol_name = vol_name + "-unmanaged"
LOG.info(_LI("Renaming existing volume %(ref_name)s to %(new_name)s"),
{"ref_name": vol_name, "new_name": unmanaged_vol_name})
ctxt.reraise = False
LOG.warning(_LW("Volume unmanage was unable to rename "
"the volume, error message: %s"), err.text)
+
+ @staticmethod
+ def _get_vol_name(volume):
+ """Return the name of the volume Purity will use."""
+ return volume["name"] + "-cinder"
+
+ @staticmethod
+ def _get_snap_name(snapshot):
+ """Return the name of the snapshot that Purity will use."""
+ return "%s-cinder.%s" % (snapshot["volume_name"], snapshot["name"])
+
+ @staticmethod
+ def _get_pgroup_name_from_id(id):
+ return "consisgroup-%s-cinder" % id
+
+ @staticmethod
+ def _get_pgroup_snap_suffix(cgsnapshot):
+ return "cgsnapshot-%s-cinder" % cgsnapshot.id
+
+ @classmethod
+ def _get_pgroup_snap_name(cls, cgsnapshot):
+ """Return the name of the pgroup snapshot that Purity will use"""
+ cg_id = cgsnapshot.consistencygroup_id
+ return "%s.%s" % (cls._get_pgroup_name_from_id(cg_id),
+ cls._get_pgroup_snap_suffix(cgsnapshot))
+
+ @classmethod
+ def _get_pgroup_vol_snap_name(cls, snapshot):
+ """Return the name of the snapshot that Purity will use."""
+ cg_id = snapshot.cgsnapshot.consistencygroup_id
+ cg_name = cls._get_pgroup_name_from_id(cg_id)
+ cgsnapshot_id = cls._get_pgroup_snap_suffix(snapshot.cgsnapshot)
+ volume_name = snapshot.volume_name
+ return "%s.%s.%s-cinder" % (cg_name, cgsnapshot_id, volume_name)
+
+ @staticmethod
+ def _generate_purity_host_name(name):
+ """Return a valid Purity host name based on the name passed in."""
+ if len(name) > 23:
+ name = name[0:23]
+ name = INVALID_CHARACTERS.sub("-", name)
+ name = name.lstrip("-")
+ return "{name}-{uuid}-cinder".format(name=name, uuid=uuid.uuid4().hex)
+
+ def _connect_host_to_vol(self, host_name, vol_name):
+ connection = None
+ try:
+ connection = self._array.connect_host(host_name, vol_name)
+ except purestorage.PureHTTPError as err:
+ with excutils.save_and_reraise_exception() as ctxt:
+ if (err.code == 400 and
+ "Connection already exists" in err.text):
+ # Happens if the volume is already connected to the host.
+ ctxt.reraise = False
+ LOG.warning(_LW("Volume connection already exists with "
+ "message: %s"), err.text)
+ # Get the info for the existing connection
+ connected_hosts = \
+ self._array.list_volume_private_connections(vol_name)
+ for host_info in connected_hosts:
+ if host_info["host"] == host_name:
+ connection = host_info
+ break
+ if not connection:
+ raise exception.PureDriverException(
+ reason=_("Unable to connect or find connection to host"))
+
+ return connection
+
+
+class PureISCSIDriver(PureBaseVolumeDriver, san.SanISCSIDriver):
+
+ VERSION = "3.0.0"
+
+ def __init__(self, *args, **kwargs):
+ execute = kwargs.pop("execute", utils.execute)
+ super(PureISCSIDriver, self).__init__(execute=execute, *args, **kwargs)
+ self._storage_protocol = "iSCSI"
+ self._iscsi_port = None
+
+ def do_setup(self, context):
+ super(PureISCSIDriver, self).do_setup(context)
+ self._iscsi_port = self._choose_target_iscsi_port()
+
+ def _get_host(self, connector):
+ """Return dict describing existing Purity host object or None."""
+ hosts = self._array.list_hosts()
+ for host in hosts:
+ if connector["initiator"] in host["iqn"]:
+ return host
+ return None
+
+ @log_debug_trace
+ def initialize_connection(self, volume, connector, initiator_data=None):
+ """Allow connection to connector and return connection info."""
+ target_port = self._get_target_iscsi_port()
+ connection = self._connect(volume, connector, initiator_data)
+ properties = {
+ "driver_volume_type": "iscsi",
+ "data": {
+ "target_iqn": target_port["iqn"],
+ "target_portal": target_port["portal"],
+ "target_lun": connection["lun"],
+ "target_discovered": True,
+ "access_mode": "rw",
+ },
+ }
+
+ if self.configuration.use_chap_auth:
+ properties["data"]["auth_method"] = "CHAP"
+ properties["data"]["auth_username"] = connection["auth_username"]
+ properties["data"]["auth_password"] = connection["auth_password"]
+
+ initiator_update = connection.get("initiator_update", False)
+ if initiator_update:
+ properties["initiator_update"] = initiator_update
+
+ return properties
+
+ def _get_target_iscsi_port(self):
+ """Return dictionary describing iSCSI-enabled port on target array."""
+ try:
+ self._run_iscsiadm_bare(["-m", "discovery", "-t", "sendtargets",
+ "-p", self._iscsi_port["portal"]])
+ except processutils.ProcessExecutionError as err:
+ LOG.warning(_LW("iSCSI discovery of port %(port_name)s at "
+ "%(port_portal)s failed with error: %(err_msg)s"),
+ {"port_name": self._iscsi_port["name"],
+ "port_portal": self._iscsi_port["portal"],
+ "err_msg": err.stderr})
+ self._iscsi_port = self._choose_target_iscsi_port()
+ return self._iscsi_port
+
+ @utils.retry(exception.PureDriverException, retries=3)
+ def _choose_target_iscsi_port(self):
+ """Find a reachable iSCSI-enabled port on target array."""
+ ports = self._array.list_ports()
+ iscsi_ports = [port for port in ports if port["iqn"]]
+ for port in iscsi_ports:
+ try:
+ self._run_iscsiadm_bare(["-m", "discovery",
+ "-t", "sendtargets",
+ "-p", port["portal"]])
+ except processutils.ProcessExecutionError as err:
+ LOG.debug(("iSCSI discovery of port %(port_name)s at "
+ "%(port_portal)s failed with error: %(err_msg)s"),
+ {"port_name": port["name"],
+ "port_portal": port["portal"],
+ "err_msg": err.stderr})
+ else:
+ LOG.info(_LI("Using port %(name)s on the array at %(portal)s "
+ "for iSCSI connectivity."),
+ {"name": port["name"], "portal": port["portal"]})
+ return port
+ raise exception.PureDriverException(
+ reason=_("No reachable iSCSI-enabled ports on target array."))
+
+ @staticmethod
+ def _generate_chap_secret():
+ return volume_utils.generate_password()
+
+ @classmethod
+ def _get_chap_credentials(cls, host, data):
+ initiator_updates = None
+ username = host
+ password = None
+ if data:
+ for d in data:
+ if d["key"] == CHAP_SECRET_KEY:
+ password = d["value"]
+ break
+ if not password:
+ password = cls._generate_chap_secret()
+ initiator_updates = {
+ "set_values": {
+ CHAP_SECRET_KEY: password
+ }
+ }
+ return username, password, initiator_updates
+
+ @utils.synchronized('PureISCSIDriver._connect', external=True)
+ def _connect(self, volume, connector, initiator_data):
+ """Connect the host and volume; return dict describing connection."""
+ iqn = connector["initiator"]
+
+ if self.configuration.use_chap_auth:
+ (chap_username, chap_password, initiator_update) = \
+ self._get_chap_credentials(connector['host'], initiator_data)
+
+ vol_name = self._get_vol_name(volume)
+ host = self._get_host(connector)
+
+ if host:
+ host_name = host["name"]
+ LOG.info(_LI("Re-using existing purity host %(host_name)r"),
+ {"host_name": host_name})
+ if self.configuration.use_chap_auth:
+ if not GENERATED_NAME.match(host_name):
+ LOG.error(_LE("Purity host %(host_name)s is not managed "
+ "by Cinder and can't have CHAP credentials "
+ "modified. Remove IQN %(iqn)s from the host "
+ "to resolve this issue."),
+ {"host_name": host_name,
+ "iqn": connector["initiator"]})
+ raise exception.PureDriverException(
+ reason=_("Unable to re-use a host that is not "
+ "managed by Cinder with use_chap_auth=True,"))
+ elif chap_username is None or chap_password is None:
+ LOG.error(_LE("Purity host %(host_name)s is managed by "
+ "Cinder but CHAP credentials could not be "
+ "retrieved from the Cinder database."),
+ {"host_name": host_name})
+ raise exception.PureDriverException(
+ reason=_("Unable to re-use host with unknown CHAP "
+ "credentials configured."))
+ else:
+ host_name = self._generate_purity_host_name(connector["host"])
+ LOG.info(_LI("Creating host object %(host_name)r with IQN:"
+ " %(iqn)s."), {"host_name": host_name, "iqn": iqn})
+ self._array.create_host(host_name, iqnlist=[iqn])
+
+ if self.configuration.use_chap_auth:
+ self._array.set_host(host_name,
+ host_user=chap_username,
+ host_password=chap_password)
+
+ connection = self._connect_host_to_vol(host_name, vol_name)
+
+ if self.configuration.use_chap_auth:
+ connection["auth_username"] = chap_username
+ connection["auth_password"] = chap_password
+
+ if initiator_update:
+ connection["initiator_update"] = initiator_update
+
+ return connection