1# Copyright (c) 2017 DataCore Software Corp. All Rights Reserved.
2#
3#    Licensed under the Apache License, Version 2.0 (the "License"); you may
4#    not use this file except in compliance with the License. You may obtain
5#    a copy of the License at
6#
7#         http://www.apache.org/licenses/LICENSE-2.0
8#
9#    Unless required by applicable law or agreed to in writing, software
10#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12#    License for the specific language governing permissions and limitations
13#    under the License.
14
15"""iSCSI Driver for DataCore SANsymphony storage array."""
16
17from oslo_config import cfg
18from oslo_log import log as logging
19from oslo_utils import excutils
20
21from cinder import exception as cinder_exception
22from cinder.i18n import _
23from cinder import interface
24from cinder import utils as cinder_utils
25from cinder.volume.drivers.datacore import driver
26from cinder.volume.drivers.datacore import exception as datacore_exception
27from cinder.volume.drivers.datacore import passwd
28from cinder.volume.drivers.datacore import utils as datacore_utils
29from cinder.volume import utils as volume_utils
30
31
32LOG = logging.getLogger(__name__)
33
34datacore_iscsi_opts = [
35    cfg.ListOpt('datacore_iscsi_unallowed_targets',
36                default=[],
37                help='List of iSCSI targets that cannot be used to attach '
38                     'volume. To prevent the DataCore iSCSI volume driver '
39                     'from using some front-end targets in volume attachment, '
40                     'specify this option and list the iqn and target machine '
41                     'for each target as the value, such as '
42                     '<iqn:target name>, <iqn:target name>, '
43                     '<iqn:target name>.'),
44    cfg.BoolOpt('datacore_iscsi_chap_enabled',
45                default=False,
46                help='Configure CHAP authentication for iSCSI connections.'),
47    cfg.StrOpt('datacore_iscsi_chap_storage',
48               default=None,
49               help='iSCSI CHAP authentication password storage file.'),
50]
51
52CONF = cfg.CONF
53CONF.register_opts(datacore_iscsi_opts)
54
55
56@interface.volumedriver
57class ISCSIVolumeDriver(driver.DataCoreVolumeDriver):
58    """DataCore SANsymphony iSCSI volume driver.
59
60    Version history:
61
62    .. code-block:: none
63
64        1.0.0 - Initial driver
65
66    """
67
68    VERSION = '1.0.0'
69    STORAGE_PROTOCOL = 'iSCSI'
70    CI_WIKI_NAME = 'DataCore_CI'
71
72    def __init__(self, *args, **kwargs):
73        super(ISCSIVolumeDriver, self).__init__(*args, **kwargs)
74        self.configuration.append_config_values(datacore_iscsi_opts)
75        self._password_storage = None
76
77    def do_setup(self, context):
78        """Perform validations and establish connection to server.
79
80        :param context: Context information
81        """
82
83        super(ISCSIVolumeDriver, self).do_setup(context)
84
85        password_storage_path = getattr(self.configuration,
86                                        'datacore_iscsi_chap_storage', None)
87        if (self.configuration.datacore_iscsi_chap_enabled
88                and not password_storage_path):
89            raise cinder_exception.InvalidInput(
90                _("datacore_iscsi_chap_storage not set."))
91        elif password_storage_path:
92            self._password_storage = passwd.PasswordFileStorage(
93                self.configuration.datacore_iscsi_chap_storage)
94
95    def validate_connector(self, connector):
96        """Fail if connector doesn't contain all the data needed by the driver.
97
98        :param connector: Connector information
99        """
100
101        required_data = ['host', 'initiator']
102        for required in required_data:
103            if required not in connector:
104                LOG.error("The volume driver requires %(data)s "
105                          "in the connector.", {'data': required})
106                raise cinder_exception.InvalidConnectorException(
107                    missing=required)
108
109    def initialize_connection(self, volume, connector):
110        """Allow connection to connector and return connection info.
111
112        :param volume: Volume object
113        :param connector: Connector information
114        :return: Connection information
115        """
116
117        LOG.debug("Initialize connection for volume %(volume)s for "
118                  "connector %(connector)s.",
119                  {'volume': volume['id'], 'connector': connector})
120
121        virtual_disk = self._get_virtual_disk_for(volume, raise_not_found=True)
122
123        if virtual_disk.DiskStatus != 'Online':
124            LOG.warning("Attempting to attach virtual disk %(disk)s "
125                        "that is in %(state)s state.",
126                        {'disk': virtual_disk.Id,
127                         'state': virtual_disk.DiskStatus})
128
129        server_group = self._get_our_server_group()
130
131        @cinder_utils.synchronized(
132            'datacore-backend-%s' % server_group.Id, external=True)
133        def serve_virtual_disk():
134            available_ports = self._api.get_ports()
135
136            iscsi_initiator = self._get_initiator(connector['host'],
137                                                  connector['initiator'],
138                                                  available_ports)
139
140            iscsi_targets = self._get_targets(virtual_disk, available_ports)
141
142            if not iscsi_targets:
143                msg = (_("Suitable targets not found for "
144                         "virtual disk %(disk)s for volume %(volume)s.")
145                       % {'disk': virtual_disk.Id, 'volume': volume['id']})
146                LOG.error(msg)
147                raise cinder_exception.VolumeDriverException(message=msg)
148
149            auth_params = self._setup_iscsi_chap_authentication(
150                iscsi_targets, iscsi_initiator)
151
152            virtual_logical_units = self._map_virtual_disk(
153                virtual_disk, iscsi_targets, iscsi_initiator)
154
155            return iscsi_targets, virtual_logical_units, auth_params
156
157        targets, logical_units, chap_params = serve_virtual_disk()
158
159        target_portal = datacore_utils.build_network_address(
160            targets[0].PortConfigInfo.PortalsConfig.iScsiPortalConfigInfo[0]
161            .Address.Address,
162            targets[0].PortConfigInfo.PortalsConfig.iScsiPortalConfigInfo[0]
163            .TcpPort)
164
165        connection_data = {}
166
167        if chap_params:
168            connection_data['auth_method'] = 'CHAP'
169            connection_data['auth_username'] = chap_params[0]
170            connection_data['auth_password'] = chap_params[1]
171
172        connection_data['target_discovered'] = False
173        connection_data['target_iqn'] = targets[0].PortName
174        connection_data['target_portal'] = target_portal
175        connection_data['target_lun'] = logical_units[targets[0]].Lun.Quad
176        connection_data['volume_id'] = volume['id']
177        connection_data['access_mode'] = 'rw'
178
179        LOG.debug("Connection data: %s", connection_data)
180
181        return {
182            'driver_volume_type': 'iscsi',
183            'data': connection_data,
184        }
185
186    def _map_virtual_disk(self, virtual_disk, targets, initiator):
187        logical_disks = self._api.get_logical_disks()
188
189        logical_units = {}
190        created_mapping = {}
191        created_devices = []
192        created_domains = []
193        try:
194            for target in targets:
195                target_domain = self._get_target_domain(target, initiator)
196                if not target_domain:
197                    target_domain = self._api.create_target_domain(
198                        initiator.HostId, target.HostId)
199                    created_domains.append(target_domain)
200
201                nexus = self._api.build_scsi_port_nexus_data(
202                    initiator.Id, target.Id)
203
204                target_device = self._get_target_device(
205                    target_domain, target, initiator)
206                if not target_device:
207                    target_device = self._api.create_target_device(
208                        target_domain.Id, nexus)
209                    created_devices.append(target_device)
210
211                logical_disk = self._get_logical_disk_on_host(
212                    virtual_disk.Id, target.HostId, logical_disks)
213
214                logical_unit = self._get_logical_unit(
215                    logical_disk, target_device)
216                if not logical_unit:
217                    logical_unit = self._create_logical_unit(
218                        logical_disk, nexus, target_device)
219                    created_mapping[logical_unit] = target_device
220                logical_units[target] = logical_unit
221        except Exception:
222            with excutils.save_and_reraise_exception():
223                LOG.exception("Mapping operation for virtual disk %(disk)s "
224                              "failed with error.",
225                              {'disk': virtual_disk.Id})
226                try:
227                    for logical_unit in created_mapping:
228                        nexus = self._api.build_scsi_port_nexus_data(
229                            created_mapping[logical_unit].InitiatorPortId,
230                            created_mapping[logical_unit].TargetPortId)
231                        self._api.unmap_logical_disk(
232                            logical_unit.LogicalDiskId, nexus)
233                    for target_device in created_devices:
234                        self._api.delete_target_device(target_device.Id)
235                    for target_domain in created_domains:
236                        self._api.delete_target_domain(target_domain.Id)
237                except datacore_exception.DataCoreException as e:
238                    LOG.warning("An error occurred on a cleanup after "
239                                "failed mapping operation: %s.", e)
240
241        return logical_units
242
243    def _get_target_domain(self, target, initiator):
244        target_domains = self._api.get_target_domains()
245        target_domain = datacore_utils.get_first_or_default(
246            lambda domain: (domain.InitiatorHostId == initiator.HostId
247                            and domain.TargetHostId == target.HostId),
248            target_domains,
249            None)
250        return target_domain
251
252    def _get_target_device(self, target_domain, target, initiator):
253        target_devices = self._api.get_target_devices()
254        target_device = datacore_utils.get_first_or_default(
255            lambda device: (device.TargetDomainId == target_domain.Id
256                            and device.InitiatorPortId == initiator.Id
257                            and device.TargetPortId == target.Id),
258            target_devices,
259            None)
260        return target_device
261
262    def _get_logical_unit(self, logical_disk, target_device):
263        logical_units = self._api.get_logical_units()
264        logical_unit = datacore_utils.get_first_or_default(
265            lambda unit: (unit.LogicalDiskId == logical_disk.Id
266                          and unit.VirtualTargetDeviceId == target_device.Id),
267            logical_units,
268            None)
269        return logical_unit
270
271    def _create_logical_unit(self, logical_disk, nexus, target_device):
272        free_lun = self._api.get_next_free_lun(target_device.Id)
273        logical_unit = self._api.map_logical_disk(logical_disk.Id,
274                                                  nexus,
275                                                  free_lun,
276                                                  logical_disk.ServerHostId,
277                                                  'Client')
278        return logical_unit
279
280    def _check_iscsi_chap_configuration(self, iscsi_chap_enabled, targets):
281        logical_units = self._api.get_logical_units()
282        target_devices = self._api.get_target_devices()
283
284        for logical_unit in logical_units:
285            target_device_id = logical_unit.VirtualTargetDeviceId
286            target_device = datacore_utils.get_first(
287                lambda device, key=target_device_id: device.Id == key,
288                target_devices)
289            target_port_id = target_device.TargetPortId
290            target = datacore_utils.get_first_or_default(
291                lambda target_port, key=target_port_id: target_port.Id == key,
292                targets,
293                None)
294            if (target and iscsi_chap_enabled ==
295                    (target.ServerPortProperties.Authentication == 'None')):
296                msg = _("iSCSI CHAP authentication can't be configured for "
297                        "target %s. Device exists that served through "
298                        "this target.") % target.PortName
299                LOG.error(msg)
300                raise cinder_exception.VolumeDriverException(message=msg)
301
302    def _setup_iscsi_chap_authentication(self, targets, initiator):
303        iscsi_chap_enabled = self.configuration.datacore_iscsi_chap_enabled
304
305        self._check_iscsi_chap_configuration(iscsi_chap_enabled, targets)
306
307        server_group = self._get_our_server_group()
308        update_access_token = False
309        access_token = None
310        chap_secret = None
311        if iscsi_chap_enabled:
312            authentication = 'CHAP'
313            chap_secret = self._password_storage.get_password(
314                server_group.Id, initiator.PortName)
315            update_access_token = False
316            if not chap_secret:
317                chap_secret = volume_utils.generate_password(length=15)
318                self._password_storage.set_password(
319                    server_group.Id, initiator.PortName, chap_secret)
320                update_access_token = True
321            access_token = self._api.build_access_token(
322                initiator.PortName,
323                None,
324                None,
325                False,
326                initiator.PortName,
327                chap_secret)
328        else:
329            authentication = 'None'
330            if self._password_storage:
331                self._password_storage.delete_password(server_group.Id,
332                                                       initiator.PortName)
333        changed_targets = {}
334        try:
335            for target in targets:
336                if iscsi_chap_enabled:
337                    target_iscsi_nodes = getattr(target.iSCSINodes, 'Node', [])
338                    iscsi_node = datacore_utils.get_first_or_default(
339                        lambda node: node.Name == initiator.PortName,
340                        target_iscsi_nodes,
341                        None)
342                    if (not iscsi_node
343                            or not iscsi_node.AccessToken.TargetUsername
344                            or update_access_token):
345                        self._api.set_access_token(target.Id, access_token)
346                properties = target.ServerPortProperties
347                if properties.Authentication != authentication:
348                    changed_targets[target] = properties.Authentication
349                    properties.Authentication = authentication
350                    self._api.set_server_port_properties(
351                        target.Id, properties)
352        except Exception:
353            with excutils.save_and_reraise_exception():
354                LOG.exception("Configuring of iSCSI CHAP authentication for "
355                              "initiator %(initiator)s failed.",
356                              {'initiator': initiator.PortName})
357                try:
358                    for target in changed_targets:
359                        properties = target.ServerPortProperties
360                        properties.Authentication = changed_targets[target]
361                        self._api.set_server_port_properties(
362                            target.Id, properties)
363                except datacore_exception.DataCoreException as e:
364                    LOG.warning("An error occurred on a cleanup after  failed "
365                                "configuration of iSCSI CHAP authentication "
366                                "on initiator %(initiator)s: %(error)s.",
367                                {'initiator': initiator.PortName, 'error': e})
368        if iscsi_chap_enabled:
369            return initiator.PortName, chap_secret
370
371    def _get_initiator(self, host, iqn, available_ports):
372        client = self._get_client(host, create_new=True)
373
374        iscsi_initiator_ports = self._get_host_iscsi_initiator_ports(
375            client, available_ports)
376
377        iscsi_initiator = datacore_utils.get_first_or_default(
378            lambda port: port.PortName == iqn,
379            iscsi_initiator_ports,
380            None)
381
382        if not iscsi_initiator:
383            scsi_port_data = self._api.build_scsi_port_data(
384                client.Id, iqn, 'Initiator', 'iSCSI')
385            iscsi_initiator = self._api.register_port(scsi_port_data)
386        return iscsi_initiator
387
388    def _get_targets(self, virtual_disk, available_ports):
389        unallowed_targets = self.configuration.datacore_iscsi_unallowed_targets
390        iscsi_target_ports = self._get_frontend_iscsi_target_ports(
391            available_ports)
392        server_port_map = {}
393        for target_port in iscsi_target_ports:
394            if target_port.HostId in server_port_map:
395                server_port_map[target_port.HostId].append(target_port)
396            else:
397                server_port_map[target_port.HostId] = [target_port]
398        iscsi_targets = []
399        if virtual_disk.FirstHostId in server_port_map:
400            iscsi_targets += server_port_map[virtual_disk.FirstHostId]
401        if virtual_disk.SecondHostId in server_port_map:
402            iscsi_targets += server_port_map[virtual_disk.SecondHostId]
403        iscsi_targets = [target for target in iscsi_targets
404                         if target.PortName not in unallowed_targets]
405        return iscsi_targets
406
407    @staticmethod
408    def _get_logical_disk_on_host(virtual_disk_id,
409                                  host_id, logical_disks):
410        logical_disk = datacore_utils.get_first(
411            lambda disk: (disk.ServerHostId == host_id
412                          and disk.VirtualDiskId == virtual_disk_id),
413            logical_disks)
414        return logical_disk
415
416    @staticmethod
417    def _is_iscsi_frontend_port(port):
418        if (port.PortType == 'iSCSI'
419                and port.PortMode == 'Target'
420                and port.HostId
421                and port.PresenceStatus == 'Present'
422                and hasattr(port, 'IScsiPortStateInfo')):
423            port_roles = port.ServerPortProperties.Role.split()
424            port_state = (port.IScsiPortStateInfo.PortalsState
425                          .PortalStateInfo[0].State)
426            if 'Frontend' in port_roles and port_state == 'Ready':
427                return True
428        return False
429
430    @staticmethod
431    def _get_frontend_iscsi_target_ports(ports):
432        return [target_port for target_port in ports
433                if ISCSIVolumeDriver._is_iscsi_frontend_port(target_port)]
434
435    @staticmethod
436    def _get_host_iscsi_initiator_ports(host, ports):
437        return [port for port in ports
438                if port.PortType == 'iSCSI'
439                and port.PortMode == 'Initiator'
440                and port.HostId == host.Id]
441