"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):
def __init__(self, target=None, rest_version=None, code=None,
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.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
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",
- },
- }
+ 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)
+ 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)
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):
+ 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)
+ 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)
+ 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)
- # 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)
+ 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):
code=400,
text="Connection already exists"
)
- actual = self.driver._connect(VOLUME, CONNECTOR)
+ 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)
text="Connection already exists"
)
self.assertRaises(exception.PureDriverException, self.driver._connect,
- VOLUME, CONNECTOR)
+ VOLUME, CONNECTOR, None)
self.assertTrue(self.array.connect_host.called)
self.assertTrue(self.array.list_volume_private_connections)
text="Connection already exists"
)
self.assertRaises(self.purestorage_module.PureHTTPError,
- self.driver._connect, VOLUME, CONNECTOR)
+ self.driver._connect, VOLUME, CONNECTOR, None)
self.assertTrue(self.array.connect_host.called)
self.assertTrue(self.array.list_volume_private_connections)
from cinder.openstack.common import log as logging
from cinder import utils
from cinder.volume.drivers.san import san
+from cinder.volume import utils as volume_utils
try:
import purestorage
LOG = logging.getLogger(__name__)
PURE_OPTS = [
- cfg.StrOpt("pure_api_token", default=None,
+ cfg.StrOpt("pure_api_token",
+ default=None,
help="REST API authorization token."),
]
INVALID_CHARACTERS = re.compile(r"[^-a-zA-Z0-9]")
GENERATED_NAME = re.compile(r".*-[a-f0-9]{32}-cinder$")
+CHAP_SECRET_KEY = "PURE_TARGET_CHAP_SECRET"
+
ERR_MSG_NOT_EXIST = "does not exist"
ERR_MSG_PENDING_ERADICATION = "has been destroyed"
return "{name}-{uuid}-cinder".format(name=name, uuid=uuid.uuid4().hex)
+def _generate_chap_secret():
+ return volume_utils.generate_password()
+
+
class PureISCSIDriver(san.SanISCSIDriver):
"""Performs volume management on Pure Storage FlashArray."""
- VERSION = "2.0.4"
+ VERSION = "2.0.5"
SUPPORTED_REST_API_VERSIONS = ['1.2', '1.3', '1.4']
" %s"), err.text)
LOG.debug("Leave PureISCSIDriver.delete_snapshot.")
- def initialize_connection(self, volume, connector):
+ 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)
+ connection = self._connect(volume, connector, initiator_data)
properties = {
"driver_volume_type": "iscsi",
"data": {
"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 value: %s", str(properties))
return properties
raise exception.PureDriverException(
reason=_("No reachable iSCSI-enabled ports on target array."))
- def _connect(self, volume, connector):
+ 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
+
+ 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"])
- iqn = connector["initiator"]
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:
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):