]> review.fuel-infra Code Review - openstack-build/cinder-build.git/commitdiff
Add CHAP support to PureISCSIDriver
authorPatrick East <patrick.east@purestorage.com>
Fri, 20 Feb 2015 02:49:23 +0000 (18:49 -0800)
committerPatrick East <patrick.east@purestorage.com>
Thu, 5 Mar 2015 17:23:46 +0000 (09:23 -0800)
This change adds support for iSCSI CHAP with randomly generated
secrets for each initiator. Due to some restrictions with storing meta
data in the backend these secrets need to be stored someplace accessible
from the driver. We will use the driver specific initiator data in the
Cinder DB to store them.

Implements: blueprint pure-iscsi-chap-support
Change-Id: I5dfc29602a41ed565af074368947c99aafdc0ab8

cinder/tests/test_pure.py
cinder/volume/drivers/pure.py

index 52c4732eb6e0ba631922348ce0441b108123bac9..949775c8c157c3f0f7a385ebb108be59232d928e 100644 (file)
@@ -114,6 +114,15 @@ 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):
     def __init__(self, target=None, rest_version=None, code=None,
@@ -129,11 +138,12 @@ 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.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
@@ -384,18 +394,52 @@ class PureISCSIDriverTestCase(test.TestCase):
         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)
@@ -428,36 +472,69 @@ class PureISCSIDriverTestCase(test.TestCase):
         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):
@@ -470,7 +547,7 @@ class PureISCSIDriverTestCase(test.TestCase):
                 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)
@@ -485,7 +562,7 @@ class PureISCSIDriverTestCase(test.TestCase):
                 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)
 
@@ -500,7 +577,7 @@ class PureISCSIDriverTestCase(test.TestCase):
                 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)
 
index af87a249293efe460f370f1a9db2b9d001893b31..4520a8d5a08287529ac559cd15c1aa33febc20fa 100644 (file)
@@ -32,6 +32,7 @@ from cinder.i18n import _, _LE, _LI, _LW
 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
@@ -41,7 +42,8 @@ except ImportError:
 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."),
 ]
 
@@ -51,6 +53,8 @@ CONF.register_opts(PURE_OPTS)
 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"
 
@@ -96,10 +100,14 @@ def _generate_purity_host_name(name):
     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']
 
@@ -231,11 +239,17 @@ class PureISCSIDriver(san.SanISCSIDriver):
                                   " %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": {
@@ -246,6 +260,16 @@ class PureISCSIDriver(san.SanISCSIDriver):
                 "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
@@ -288,22 +312,70 @@ class PureISCSIDriver(san.SanISCSIDriver):
         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:
@@ -324,6 +396,14 @@ class PureISCSIDriver(san.SanISCSIDriver):
         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):