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