def _get_client_id(self): request = ("https://%(server_ip)s:%(server_port)s/" "api/types/Client/instances/getByIp::%(sdc_ip)s/" % { 'server_ip': self.server_ip, 'server_port': self.server_port, 'sdc_ip': self.local_sdc_ip }) LOG.info(_LI("ScaleIO get client id by ip request: %(request)s"), {'request': request}) r = requests.get(request, auth=(self.server_username, self.server_token), verify=False) r = self._check_response(r, request) sdc_id = r.json() if not sdc_id: msg = (_("Client with ip %(sdc_ip)s was not found.") % { 'sdc_ip': self.local_sdc_ip }) raise exception.BrickException(message=msg) if r.status_code != 200 and "errorCode" in sdc_id: msg = (_("Error getting sdc id from ip %(sdc_ip)s: %(err)s") % { 'sdc_ip': self.local_sdc_ip, 'err': sdc_id['message'] }) LOG.error(msg) raise exception.BrickException(message=msg) LOG.info(_LI("ScaleIO sdc id is %(sdc_id)s."), {'sdc_id': sdc_id}) return sdc_id
def extend_volume(self, volume_path): """Signal the SCSI subsystem to test for volume resize. This function tries to signal the local system's kernel that an already attached volume might have been resized. """ LOG.debug("extend volume %s", volume_path) device = self.get_device_info(volume_path) LOG.debug("Volume device info = %s", device) device_id = ("%(host)s:%(channel)s:%(id)s:%(lun)s" % { 'host': device['host'], 'channel': device['channel'], 'id': device['id'], 'lun': device['lun'] }) scsi_path = ("/sys/bus/scsi/drivers/sd/%(device_id)s" % { 'device_id': device_id }) size = self.get_device_size(volume_path) LOG.debug("Starting size: %s", size) # now issue the device rescan rescan_path = "%(scsi_path)s/rescan" % {'scsi_path': scsi_path} self.echo_scsi_command(rescan_path, "1") new_size = self.get_device_size(volume_path) LOG.debug("volume size after scsi device rescan %s", new_size) scsi_wwn = self.get_scsi_wwn(volume_path) mpath_device = self.find_multipath_device_path(scsi_wwn) if mpath_device: # Force a reconfigure so that resize works self.multipath_reconfigure() size = self.get_device_size(mpath_device) LOG.info(_LI("mpath(%(device)s) current size %(size)s"), { 'device': mpath_device, 'size': size }) result = self.multipath_resize_map(scsi_wwn) if 'fail' in result: msg = (_LI("Multipathd failed to update the size mapping of " "multipath device %(scsi_wwn)s volume %(volume)s") % { 'scsi_wwn': scsi_wwn, 'volume': volume_path }) LOG.error(msg) return None new_size = self.get_device_size(mpath_device) LOG.info(_LI("mpath(%(device)s) new size %(size)s"), { 'device': mpath_device, 'size': new_size }) return new_size else: return new_size
def extend_volume(self, volume_path): """Signal the SCSI subsystem to test for volume resize. This function tries to signal the local system's kernel that an already attached volume might have been resized. """ LOG.debug("extend volume %s", volume_path) device = self.get_device_info(volume_path) LOG.debug("Volume device info = %s", device) device_id = ("%(host)s:%(channel)s:%(id)s:%(lun)s" % {'host': device['host'], 'channel': device['channel'], 'id': device['id'], 'lun': device['lun']}) scsi_path = ("/sys/bus/scsi/drivers/sd/%(device_id)s" % {'device_id': device_id}) size = self.get_device_size(volume_path) LOG.debug("Starting size: %s", size) # now issue the device rescan rescan_path = "%(scsi_path)s/rescan" % {'scsi_path': scsi_path} self.echo_scsi_command(rescan_path, "1") new_size = self.get_device_size(volume_path) LOG.debug("volume size after scsi device rescan %s", new_size) scsi_wwn = self.get_scsi_wwn(volume_path) mpath_device = self.find_multipath_device_path(scsi_wwn) if mpath_device: # Force a reconfigure so that resize works self.multipath_reconfigure() size = self.get_device_size(mpath_device) LOG.info(_LI("mpath(%(device)s) current size %(size)s"), {'device': mpath_device, 'size': size}) result = self.multipath_resize_map(scsi_wwn) if 'fail' in result: msg = (_LI("Multipathd failed to update the size mapping of " "multipath device %(scsi_wwn)s volume %(volume)s") % {'scsi_wwn': scsi_wwn, 'volume': volume_path}) LOG.error(msg) return None new_size = self.get_device_size(mpath_device) LOG.info(_LI("mpath(%(device)s) new size %(size)s"), {'device': mpath_device, 'size': new_size}) return new_size else: return new_size
def _get_volume_id(self): volname_encoded = urllib.parse.quote(self.volume_name, '') volname_double_encoded = urllib.parse.quote(volname_encoded, '') LOG.debug(_( "Volume name after double encoding is %(volume_name)s."), {'volume_name': volname_double_encoded} ) request = ( "https://%(server_ip)s:%(server_port)s/api/types/Volume/instances" "/getByName::%(encoded_volume_name)s" % { 'server_ip': self.server_ip, 'server_port': self.server_port, 'encoded_volume_name': volname_double_encoded } ) LOG.info( _LI("ScaleIO get volume id by name request: %(request)s"), {'request': request} ) r = requests.get(request, auth=(self.server_username, self.server_token), verify=False) r = self._check_response(r, request) volume_id = r.json() if not volume_id: msg = (_("Volume with name %(volume_name)s wasn't found.") % {'volume_name': self.volume_name}) LOG.error(msg) raise exception.BrickException(message=msg) if r.status_code != self.OK_STATUS_CODE and "errorCode" in volume_id: msg = ( _("Error getting volume id from name %(volume_name)s: " "%(err)s") % {'volume_name': self.volume_name, 'err': volume_id['message']} ) LOG.error(msg) raise exception.BrickException(message=msg) LOG.info(_LI("ScaleIO volume id is %(volume_id)s."), {'volume_id': volume_id}) return volume_id
def _find_volume_path(self): LOG.info(_LI( "Looking for volume %(volume_id)s, maximum tries: %(tries)s"), {'volume_id': self.volume_id, 'tries': self.device_scan_attempts} ) # look for the volume in /dev/disk/by-id directory by_id_path = self.get_search_path() disk_filename = self._wait_for_volume_path(by_id_path) full_disk_name = ("%(path)s/%(filename)s" % {'path': by_id_path, 'filename': disk_filename}) LOG.info(_LI("Full disk name is %(full_path)s"), {'full_path': full_disk_name}) return full_disk_name
def _wait_for_volume_path(self, path): if not os.path.isdir(path): msg = (_("ScaleIO volume %(volume_id)s not found at " "expected path.") % { 'volume_id': self.volume_id }) LOG.debug(msg) raise exception.BrickException(message=msg) disk_filename = None filenames = os.listdir(path) LOG.info(_LI("Files found in %(path)s path: %(files)s "), { 'path': path, 'files': filenames }) for filename in filenames: if (filename.startswith("emc-vol") and filename.endswith(self.volume_id)): disk_filename = filename break if not disk_filename: msg = (_("ScaleIO volume %(volume_id)s not found.") % { 'volume_id': self.volume_id }) LOG.debug(msg) raise exception.BrickException(message=msg) return disk_filename
def _wait_for_volume_path(self, path): if not os.path.isdir(path): msg = ( _("ScaleIO volume %(volume_id)s not found at " "expected path.") % {'volume_id': self.volume_id} ) LOG.debug(msg) raise exception.BrickException(message=msg) disk_filename = None filenames = os.listdir(path) LOG.info(_LI( "Files found in %(path)s path: %(files)s "), {'path': path, 'files': filenames} ) for filename in filenames: if (filename.startswith("emc-vol") and filename.endswith(self.volume_id)): disk_filename = filename break if not disk_filename: msg = (_("ScaleIO volume %(volume_id)s not found.") % {'volume_id': self.volume_id}) LOG.debug(msg) raise exception.BrickException(message=msg) return disk_filename
def attach_volume(self, context, **kwargs): """Shadows the device and passes an unencrypted version to the instance. Transparent disk encryption is achieved by mounting the volume via dm-crypt and passing the resulting device to the instance. The instance is unaware of the underlying encryption due to modifying the original symbolic link to refer to the device mounted by dm-crypt. """ key = self._get_key(context).get_encoded() passphrase = self._get_passphrase(key) try: self._open_volume(passphrase, **kwargs) except putils.ProcessExecutionError as e: if e.exit_code == 1 and not is_luks(self._root_helper, self.dev_path, execute=self._execute): # the device has never been formatted; format it and try again LOG.info(_LI("%s is not a valid LUKS device;" " formatting device for first use"), self.dev_path) self._format_volume(passphrase, **kwargs) self._open_volume(passphrase, **kwargs) else: raise # modify the original symbolic link to refer to the decrypted device self._execute('ln', '--symbolic', '--force', '/dev/mapper/%s' % self.dev_name, self.symlink_path, root_helper=self._root_helper, run_as_root=True, check_exit_code=True)
def attach_volume(self, context, **kwargs): """Shadows the device and passes an unencrypted version to the instance. Transparent disk encryption is achieved by mounting the volume via dm-crypt and passing the resulting device to the instance. The instance is unaware of the underlying encryption due to modifying the original symbolic link to refer to the device mounted by dm-crypt. """ key = self._get_key(context).get_encoded() passphrase = self._get_passphrase(key) try: self._open_volume(passphrase, **kwargs) except processutils.ProcessExecutionError as e: if e.exit_code == 2: # NOTE(lyarwood): Workaround bug#1633518 by attempting to use # a mangled passphrase to open the device.. LOG.info( _LI("Unable to open %s with the current passphrase, " "attempting to use a mangled passphrase to open " "the volume."), self.dev_path) self._open_volume(self._get_mangled_passphrase(key), **kwargs) # modify the original symbolic link to refer to the decrypted device self._execute('ln', '--symbolic', '--force', '/dev/mapper/%s' % self.dev_name, self.symlink_path, root_helper=self._root_helper, run_as_root=True, check_exit_code=True)
def extend_volume(self, connection_properties): """Update the local kernel's size information. Try and update the local kernel's size information for an iSCSI volume. """ LOG.info(_LI("Extend volume for %s"), connection_properties) volume_paths = self.get_volume_paths(connection_properties) LOG.info(_LI("Found paths for volume %s"), volume_paths) if volume_paths: return self._linuxscsi.extend_volume(volume_paths[0]) else: LOG.warning(_LW("Couldn't find any volume paths on the host to " "extend volume for %(props)s"), {'props': connection_properties}) raise exception.VolumePathsNotFound()
def _get_volume_id(self): volname_encoded = urllib.parse.quote(self.volume_name, '') volname_double_encoded = urllib.parse.quote(volname_encoded, '') LOG.debug(_("Volume name after double encoding is %(volume_name)s."), {'volume_name': volname_double_encoded}) request = ( "https://%(server_ip)s:%(server_port)s/api/types/Volume/instances" "/getByName::%(encoded_volume_name)s" % { 'server_ip': self.server_ip, 'server_port': self.server_port, 'encoded_volume_name': volname_double_encoded }) LOG.info(_LI("ScaleIO get volume id by name request: %(request)s"), {'request': request}) r = requests.get(request, auth=(self.server_username, self.server_token), verify=False) r = self._check_response(r, request) volume_id = r.json() if not volume_id: msg = (_("Volume with name %(volume_name)s wasn't found.") % { 'volume_name': self.volume_name }) LOG.error(msg) raise exception.BrickException(message=msg) if r.status_code != self.OK_STATUS_CODE and "errorCode" in volume_id: msg = (_("Error getting volume id from name %(volume_name)s: " "%(err)s") % { 'volume_name': self.volume_name, 'err': volume_id['message'] }) LOG.error(msg) raise exception.BrickException(message=msg) LOG.info(_LI("ScaleIO volume id is %(volume_id)s."), {'volume_id': volume_id}) return volume_id
def extend_volume(self, connection_properties): """Update the local kernel's size information. Try and update the local kernel's size information for an iSCSI volume. """ LOG.info(_LI("Extend volume for %s"), connection_properties) volume_paths = self.get_volume_paths(connection_properties) LOG.info(_LI("Found paths for volume %s"), volume_paths) if volume_paths: return self._linuxscsi.extend_volume(volume_paths[0]) else: LOG.warning( _LW("Couldn't find any volume paths on the host to " "extend volume for %(props)s"), {'props': connection_properties}) raise exception.VolumePathsNotFound()
def _find_volume_path(self): LOG.info( _LI("Looking for volume %(volume_id)s, maximum tries: %(tries)s"), { 'volume_id': self.volume_id, 'tries': self.device_scan_attempts }) # look for the volume in /dev/disk/by-id directory by_id_path = self.get_search_path() disk_filename = self._wait_for_volume_path(by_id_path) full_disk_name = ("%(path)s/%(filename)s" % { 'path': by_id_path, 'filename': disk_filename }) LOG.info(_LI("Full disk name is %(full_path)s"), {'full_path': full_disk_name}) return full_disk_name
def connect_volume(self, connection_properties): volume_connected = False for (initiator_name, target_portal, target_iqn, target_lun) in self._get_all_paths(connection_properties): try: msg = _LI("Attempting to establish an iSCSI session to " "target %(target_iqn)s on portal %(target_portal)s " "accessing LUN %(target_lun)s using initiator " "%(initiator_name)s.") LOG.info( msg, dict(target_portal=target_portal, target_iqn=target_iqn, target_lun=target_lun, initiator_name=initiator_name)) self._iscsi_utils.login_storage_target( target_lun=target_lun, target_iqn=target_iqn, target_portal=target_portal, auth_username=connection_properties.get('auth_username'), auth_password=connection_properties.get('auth_password'), mpio_enabled=self.use_multipath, initiator_name=initiator_name, ensure_lun_available=False) self._iscsi_utils.ensure_lun_available( target_iqn=target_iqn, target_lun=target_lun, rescan_attempts=self.device_scan_attempts, retry_interval=self.device_scan_interval) if not volume_connected: (device_number, device_path) = ( self._iscsi_utils.get_device_number_and_path( target_iqn, target_lun)) volume_connected = True if not self.use_multipath: break except os_win_exc.OSWinException: LOG.exception(_LE("Could not establish the iSCSI session.")) if not volume_connected: raise exception.BrickException( _("Could not connect volume %s.") % connection_properties) scsi_wwn = self._get_scsi_wwn(device_number) device_info = { 'type': 'block', 'path': device_path, 'number': device_number, 'scsi_wwn': scsi_wwn } return device_info
def _wait_for_discovery(aoe_path): if os.path.exists(aoe_path): raise loopingcall.LoopingCallDone if waiting_status['tries'] >= self.device_scan_attempts: raise exception.VolumeDeviceNotFound(device=aoe_path) LOG.info(_LI("AoE volume not yet found at: %(path)s. " "Try number: %(tries)s"), {'path': aoe_device, 'tries': waiting_status['tries']}) self._aoe_discover() waiting_status['tries'] += 1
def mount(self, share, flags=None): """Mount the Scality ScaleOut FS. The `share` argument is ignored because you can't mount several SOFS at the same type on a single server. But we want to keep the same method signature for class inheritance purpose. """ if self._mount_base in self._read_mounts(): LOG.info(_LI('Already mounted: %s'), self._mount_base) return self._execute('mkdir', '-p', self._mount_base, check_exit_code=0) super(ScalityRemoteFsClient, self)._do_mount( 'sofs', '/etc/sfused.conf', self._mount_base)
def disconnect_volume(self, connection_properties, device_info): """Detach the volume from instance.""" disco_id = connection_properties['disco_id'] disco_dev = '/dev/dms%s' % (disco_id) LOG.debug("detaching %s", disco_dev) if os.path.exists(disco_dev): ret = self._send_disco_vol_cmd(self.server_ip, self.server_port, 2, disco_id) if ret is not None: msg = _("Detach volume failed") raise exception.BrickException(message=msg) else: LOG.info(_LI("Volume already detached from host"))
def __init__(self, root_helper, driver=None, device_scan_attempts=initiator.DEVICE_SCAN_ATTEMPTS_DEFAULT, *args, **kwargs): """Init DISCO connector.""" super(DISCOConnector, self).__init__( root_helper, driver=driver, device_scan_attempts=device_scan_attempts, *args, **kwargs ) LOG.info(_LI("Init DISCO connector")) self.server_port = None self.server_ip = None
def mount(self, share, flags=None): """Mount given share.""" mount_path = self.get_mount_point(share) if mount_path in self._read_mounts(): LOG.info(_LI('Already mounted: %s'), mount_path) return self._execute('mkdir', '-p', mount_path, check_exit_code=0) if self._mount_type == 'nfs': self._mount_nfs(share, mount_path, flags) else: self._do_mount(self._mount_type, share, mount_path, self._mount_options, flags)
def _get_client_id(self): request = ( "https://%(server_ip)s:%(server_port)s/" "api/types/Client/instances/getByIp::%(sdc_ip)s/" % { 'server_ip': self.server_ip, 'server_port': self.server_port, 'sdc_ip': self.local_sdc_ip } ) LOG.info(_LI("ScaleIO get client id by ip request: %(request)s"), {'request': request}) r = requests.get( request, auth=(self.server_username, self.server_token), verify=False ) r = self._check_response(r, request) sdc_id = r.json() if not sdc_id: msg = (_("Client with ip %(sdc_ip)s was not found.") % {'sdc_ip': self.local_sdc_ip}) raise exception.BrickException(message=msg) if r.status_code != 200 and "errorCode" in sdc_id: msg = (_("Error getting sdc id from ip %(sdc_ip)s: %(err)s") % {'sdc_ip': self.local_sdc_ip, 'err': sdc_id['message']}) LOG.error(msg) raise exception.BrickException(message=msg) LOG.info(_LI("ScaleIO sdc id is %(sdc_id)s."), {'sdc_id': sdc_id}) return sdc_id
def connect_volume(self, connection_properties): volume_connected = False for (initiator_name, target_portal, target_iqn, target_lun) in self._get_all_paths(connection_properties): try: msg = _LI("Attempting to establish an iSCSI session to " "target %(target_iqn)s on portal %(target_portal)s " "accessing LUN %(target_lun)s using initiator " "%(initiator_name)s.") LOG.info(msg, dict(target_portal=target_portal, target_iqn=target_iqn, target_lun=target_lun, initiator_name=initiator_name)) self._iscsi_utils.login_storage_target( target_lun=target_lun, target_iqn=target_iqn, target_portal=target_portal, auth_username=connection_properties.get('auth_username'), auth_password=connection_properties.get('auth_password'), mpio_enabled=self.use_multipath, initiator_name=initiator_name, rescan_attempts=self.device_scan_attempts) if not volume_connected: (device_number, device_path) = ( self._iscsi_utils.get_device_number_and_path( target_iqn, target_lun)) volume_connected = True if not self.use_multipath: break except os_win_exc.OSWinException: LOG.exception(_LE("Could not establish the iSCSI session.")) if not volume_connected: raise exception.BrickException( _("Could not connect volume %s.") % connection_properties) scsi_wwn = self._get_scsi_wwn(device_number) device_info = {'type': 'block', 'path': device_path, 'number': device_number, 'scsi_wwn': scsi_wwn} return device_info
def mount(self, share, flags=None): """Mount given share.""" mount_path = self.get_mount_point(share) if mount_path in self._read_mounts(): LOG.info(_LI('Already mounted: %s'), mount_path) return self._execute('mkdir', '-p', mount_path, check_exit_code=0) if self._mount_type == 'nfs': self._mount_nfs(share, mount_path, flags) elif self._mount_type == 'vzstorage': self._mount_vzstorage(share, mount_path, flags) else: self._do_mount(self._mount_type, share, mount_path, self._mount_options, flags)
def _mount_disco_volume(self, path, volume_id): """Send request to mount volume on physical host.""" LOG.debug("Enter in mount disco volume %(port)s " "and %(ip)s.", { 'port': self.server_port, 'ip': self.server_ip }) if not os.path.exists(path): ret = self._send_disco_vol_cmd(self.server_ip, self.server_port, 1, volume_id) if ret is not None: msg = _("Attach volume failed") raise exception.BrickException(message=msg) else: LOG.info(_LI("Volume already attached to host"))
def find_multipath_device_path(self, wwn): """Look for the multipath device file for a volume WWN. Multipath devices can show up in several places on a linux system. 1) When multipath friendly names are ON: a device file will show up in /dev/disk/by-id/dm-uuid-mpath-<WWN> /dev/disk/by-id/dm-name-mpath<N> /dev/disk/by-id/scsi-mpath<N> /dev/mapper/mpath<N> 2) When multipath friendly names are OFF: /dev/disk/by-id/dm-uuid-mpath-<WWN> /dev/disk/by-id/scsi-<WWN> /dev/mapper/<WWN> """ LOG.info(_LI("Find Multipath device file for volume WWN %(wwn)s"), {'wwn': wwn}) # First look for the common path wwn_dict = {'wwn': wwn} path = "/dev/disk/by-id/dm-uuid-mpath-%(wwn)s" % wwn_dict try: self.wait_for_path(path) return path except exception.VolumeDeviceNotFound: pass # for some reason the common path wasn't found # lets try the dev mapper path path = "/dev/mapper/%(wwn)s" % wwn_dict try: self.wait_for_path(path) return path except exception.VolumeDeviceNotFound: pass # couldn't find a path LOG.warning( _LW("couldn't find a valid multipath device path for " "%(wwn)s"), wwn_dict) return None
def mount(self, share, flags=None): share_norm_path = self._get_share_norm_path(share) use_local_path = (self._local_path_for_loopback and self._smbutils.is_local_share(share_norm_path)) if use_local_path: LOG.info(_LI("Skipping mounting local share %(share_path)s."), dict(share_path=share_norm_path)) else: mount_options = " ".join([self._mount_options or '', flags or '']) username, password = self._parse_credentials(mount_options) if not self._smbutils.check_smb_mapping(share_norm_path): self._smbutils.mount_smb_share(share_norm_path, username=username, password=password) if self._mount_base: self._create_mount_point(share, use_local_path)
def find_multipath_device_path(self, wwn): """Look for the multipath device file for a volume WWN. Multipath devices can show up in several places on a linux system. 1) When multipath friendly names are ON: a device file will show up in /dev/disk/by-id/dm-uuid-mpath-<WWN> /dev/disk/by-id/dm-name-mpath<N> /dev/disk/by-id/scsi-mpath<N> /dev/mapper/mpath<N> 2) When multipath friendly names are OFF: /dev/disk/by-id/dm-uuid-mpath-<WWN> /dev/disk/by-id/scsi-<WWN> /dev/mapper/<WWN> """ LOG.info(_LI("Find Multipath device file for volume WWN %(wwn)s"), {'wwn': wwn}) # First look for the common path wwn_dict = {'wwn': wwn} path = "/dev/disk/by-id/dm-uuid-mpath-%(wwn)s" % wwn_dict try: self.wait_for_path(path) return path except exception.VolumeDeviceNotFound: pass # for some reason the common path wasn't found # lets try the dev mapper path path = "/dev/mapper/%(wwn)s" % wwn_dict try: self.wait_for_path(path) return path except exception.VolumeDeviceNotFound: pass # couldn't find a path LOG.warning(_LW("couldn't find a valid multipath device path for " "%(wwn)s"), wwn_dict) return None
def get_lv_info(root_helper, vg_name=None, lv_name=None): """Retrieve info about LVs (all, in a VG, or a single LV). :param root_helper: root_helper to use for execute :param vg_name: optional, gathers info for only the specified VG :param lv_name: optional, gathers info for only the specified LV :returns: List of Dictionaries with LV info """ cmd = LVM.LVM_CMD_PREFIX + [ 'lvs', '--noheadings', '--unit=g', '-o', 'vg_name,name,size', '--nosuffix' ] if lv_name is not None and vg_name is not None: cmd.append("%s/%s" % (vg_name, lv_name)) elif vg_name is not None: cmd.append(vg_name) try: (out, _err) = priv_rootwrap.execute(*cmd, root_helper=root_helper, run_as_root=True) except putils.ProcessExecutionError as err: with excutils.save_and_reraise_exception(reraise=True) as ctx: if "not found" in err.stderr or "Failed to find" in err.stderr: ctx.reraise = False LOG.info( _LI("Logical Volume not found when querying " "LVM info. (vg_name=%(vg)s, lv_name=%(lv)s"), { 'vg': vg_name, 'lv': lv_name }) out = None lv_list = [] if out is not None: volumes = out.split() iterator = moves.zip(*[iter(volumes)] * 3) # pylint: disable=E1101 for vg, name, size in iterator: lv_list.append({"vg": vg, "name": name, "size": size}) return lv_list
def attach_volume(self, context, **kwargs): """Shadows the device and passes an unencrypted version to the instance. Transparent disk encryption is achieved by mounting the volume via dm-crypt and passing the resulting device to the instance. The instance is unaware of the underlying encryption due to modifying the original symbolic link to refer to the device mounted by dm-crypt. """ key = self._get_key(context).get_encoded() passphrase = self._get_passphrase(key) try: self._open_volume(passphrase, **kwargs) except putils.ProcessExecutionError as e: if e.exit_code == 1 and not is_luks(self._root_helper, self.dev_path, execute=self._execute): # the device has never been formatted; format it and try again LOG.info(_LI("%s is not a valid LUKS device;" " formatting device for first use"), self.dev_path) self._format_volume(passphrase, **kwargs) self._open_volume(passphrase, **kwargs) elif e.exit_code == 2: # NOTE(lyarwood): Workaround bug#1633518 by replacing any # mangled passphrases that are found on the volume. # TODO(lyarwood): Remove workaround during R. LOG.warning(_LW("%s is not usable with the current " "passphrase, attempting to use a mangled " "passphrase to open the volume."), self.dev_path) self._unmangle_volume(key, passphrase, **kwargs) self._open_volume(passphrase, **kwargs) else: raise # modify the original symbolic link to refer to the decrypted device self._execute('ln', '--symbolic', '--force', '/dev/mapper/%s' % self.dev_name, self.symlink_path, root_helper=self._root_helper, run_as_root=True, check_exit_code=True)
def _check_response(self, response, request, is_get_request=True, params=None): if response.status_code == 401 or response.status_code == 403: LOG.info( _LI("Token is invalid, " "going to re-login to get a new one")) login_request = ( "https://%(server_ip)s:%(server_port)s/api/login" % { 'server_ip': self.server_ip, 'server_port': self.server_port }) r = requests.get(login_request, auth=(self.server_username, self.server_password), verify=False) token = r.json() # repeat request with valid token LOG.debug( _("Going to perform request %(request)s again " "with valid token"), {'request': request}) if is_get_request: res = requests.get(request, auth=(self.server_username, token), verify=False) else: headers = {'content-type': 'application/json'} res = requests.post(request, data=json.dumps(params), headers=headers, auth=(self.server_username, token), verify=False) self.server_token = token return res return response
def validate_initiators(self): """Validates the list of requested initiator HBAs to be used when establishing iSCSI sessions. """ valid_initiator_list = True if not self.initiator_list: LOG.info(_LI("No iSCSI initiator was explicitly requested. " "The Microsoft iSCSI initiator will choose the " "initiator when establishing sessions.")) else: available_initiators = self._iscsi_utils.get_iscsi_initiators() for initiator in self.initiator_list: if initiator not in available_initiators: msg = _LW("The requested initiator %(req_initiator)s " "is not in the list of available initiators: " "%(avail_initiators)s.") LOG.warning(msg, dict(req_initiator=initiator, avail_initiators=available_initiators)) valid_initiator_list = False return valid_initiator_list
def _check_response(self, response, request, is_get_request=True, params=None): if response.status_code == 401 or response.status_code == 403: LOG.info(_LI("Token is invalid, " "going to re-login to get a new one")) login_request = ( "https://%(server_ip)s:%(server_port)s/api/login" % {'server_ip': self.server_ip, 'server_port': self.server_port} ) r = requests.get( login_request, auth=(self.server_username, self.server_password), verify=False ) token = r.json() # repeat request with valid token LOG.debug(_("Going to perform request %(request)s again " "with valid token"), {'request': request}) if is_get_request: res = requests.get(request, auth=(self.server_username, token), verify=False) else: headers = {'content-type': 'application/json'} res = requests.post( request, data=json.dumps(params), headers=headers, auth=(self.server_username, token), verify=False ) self.server_token = token return res return response
def get_lv_info(root_helper, vg_name=None, lv_name=None): """Retrieve info about LVs (all, in a VG, or a single LV). :param root_helper: root_helper to use for execute :param vg_name: optional, gathers info for only the specified VG :param lv_name: optional, gathers info for only the specified LV :returns: List of Dictionaries with LV info """ cmd = LVM.LVM_CMD_PREFIX + ['lvs', '--noheadings', '--unit=g', '-o', 'vg_name,name,size', '--nosuffix'] if lv_name is not None and vg_name is not None: cmd.append("%s/%s" % (vg_name, lv_name)) elif vg_name is not None: cmd.append(vg_name) try: (out, _err) = putils.execute(*cmd, root_helper=root_helper, run_as_root=True) except putils.ProcessExecutionError as err: with excutils.save_and_reraise_exception(reraise=True) as ctx: if "not found" in err.stderr or "Failed to find" in err.stderr: ctx.reraise = False LOG.info(_LI("Logical Volume not found when querying " "LVM info. (vg_name=%(vg)s, lv_name=%(lv)s"), {'vg': vg_name, 'lv': lv_name}) out = None lv_list = [] if out is not None: volumes = out.split() iterator = moves.zip(*[iter(volumes)] * 3) # pylint: disable=E1101 for vg, name, size in iterator: lv_list.append({"vg": vg, "name": name, "size": size}) return lv_list
def validate_initiators(self): """Validates the list of requested initiator HBAs to be used when establishing iSCSI sessions. """ valid_initiator_list = True if not self.initiator_list: LOG.info( _LI("No iSCSI initiator was explicitly requested. " "The Microsoft iSCSI initiator will choose the " "initiator when establishing sessions.")) else: available_initiators = self._iscsi_utils.get_iscsi_initiators() for initiator in self.initiator_list: if initiator not in available_initiators: msg = _LW("The requested initiator %(req_initiator)s " "is not in the list of available initiators: " "%(avail_initiators)s.") LOG.warning( msg, dict(req_initiator=initiator, avail_initiators=available_initiators)) valid_initiator_list = False return valid_initiator_list
def _wait_for_device_discovery(host_devices): tries = self.tries for device in host_devices: LOG.debug("Looking for Fibre Channel dev %(device)s", {'device': device}) if os.path.exists(device): self.host_device = device # get the /dev/sdX device. This is used # to find the multipath device. self.device_name = os.path.realpath(device) raise loopingcall.LoopingCallDone() if self.tries >= self.device_scan_attempts: LOG.error(_LE("Fibre Channel volume device not found.")) raise exception.NoFibreChannelVolumeDeviceFound() LOG.info(_LI("Fibre Channel volume device not yet found. " "Will rescan & retry. Try number: %(tries)s."), {'tries': tries}) self._linuxfc.rescan_hosts(hbas, connection_properties['target_lun']) self.tries = self.tries + 1
def mount(self, share, flags=None): share = share.replace('/', '\\') use_local_path = (self._local_path_for_loopback and self._smbutils.is_local_share(share)) if use_local_path: LOG.info(_LI("Skipping mounting local share %(share_path)s."), dict(share_path=share)) else: mount_options = " ".join( [self._mount_options or '', flags or '']) username, password = self._parse_credentials(mount_options) if not self._smbutils.check_smb_mapping( share): self._smbutils.mount_smb_share(share, username=username, password=password) if self._mount_base: share_name = self.get_share_name(share) symlink_dest = (share if not use_local_path else self.get_local_share_path(share_name)) self._create_mount_point(symlink_dest)
def attach_volume(self, context, **kwargs): """Shadows the device and passes an unencrypted version to the instance. Transparent disk encryption is achieved by mounting the volume via dm-crypt and passing the resulting device to the instance. The instance is unaware of the underlying encryption due to modifying the original symbolic link to refer to the device mounted by dm-crypt. """ key = self._get_key(context).get_encoded() passphrase = self._get_passphrase(key) try: self._open_volume(passphrase, **kwargs) except putils.ProcessExecutionError as e: if e.exit_code == 1 and not is_luks( self._root_helper, self.dev_path, execute=self._execute): # the device has never been formatted; format it and try again LOG.info( _LI("%s is not a valid LUKS device;" " formatting device for first use"), self.dev_path) self._format_volume(passphrase, **kwargs) self._open_volume(passphrase, **kwargs) else: raise # modify the original symbolic link to refer to the decrypted device self._execute('ln', '--symbolic', '--force', '/dev/mapper/%s' % self.dev_name, self.symlink_path, root_helper=self._root_helper, run_as_root=True, check_exit_code=True)
def _get_potential_volume_paths(self, connection_properties, connect_to_portal=True, use_rescan=True): """Build a list of potential volume paths that exist. Given a list of target_portals in the connection_properties, a list of paths might exist on the system during discovery. This method's job is to build that list of potential paths for a volume that might show up. This is used during connect_volume time, in which case we want to connect to the iSCSI target portal. During get_volume_paths time, we are looking to find a list of existing volume paths for the connection_properties. In this case, we don't want to connect to the portal. If we blindly try and connect to a portal, it could create a new iSCSI session that didn't exist previously, and then leave it stale. :param connection_properties: The dictionary that describes all of the target volume attributes. :type connection_properties: dict :param connect_to_portal: Do we want to try a new connection to the target portal(s)? Set this to False if you want to search for existing volumes, not discover new volumes. :param connect_to_portal: bool :param use_rescan: Issue iSCSI rescan during discovery? :type use_rescan: bool :returns: dict """ target_props = None connected_to_portal = False if self.use_multipath: LOG.info(_LI("Multipath discovery for iSCSI enabled")) # Multipath installed, discovering other targets if available try: ips_iqns = self._discover_iscsi_portals(connection_properties) except Exception: if 'target_portals' in connection_properties: raise exception.TargetPortalsNotFound( target_portals=connection_properties['target_portals']) elif 'target_portal' in connection_properties: raise exception.TargetPortalNotFound( target_portal=connection_properties['target_portal']) else: raise if not connection_properties.get('target_iqns'): # There are two types of iSCSI multipath devices. One which # shares the same iqn between multiple portals, and the other # which use different iqns on different portals. # Try to identify the type by checking the iscsiadm output # if the iqn is used by multiple portals. If it is, it's # the former, so use the supplied iqn. Otherwise, it's the # latter, so try the ip,iqn combinations to find the targets # which constitutes the multipath device. main_iqn = connection_properties['target_iqn'] all_portals = set([ip for ip, iqn in ips_iqns]) match_portals = set( [ip for ip, iqn in ips_iqns if iqn == main_iqn]) if len(all_portals) == len(match_portals): ips_iqns = zip(all_portals, [main_iqn] * len(all_portals)) for ip, iqn in ips_iqns: props = copy.deepcopy(connection_properties) props['target_portal'] = ip props['target_iqn'] = iqn if connect_to_portal: if self._connect_to_iscsi_portal(props): connected_to_portal = True if use_rescan: self._rescan_iscsi() host_devices = self._get_device_path(connection_properties) else: LOG.info(_LI("Multipath discovery for iSCSI not enabled.")) iscsi_sessions = [] if not connect_to_portal: iscsi_sessions = self._get_iscsi_sessions() host_devices = [] target_props = connection_properties for props in self._iterate_all_targets(connection_properties): if connect_to_portal: if self._connect_to_iscsi_portal(props): target_props = props connected_to_portal = True host_devices = self._get_device_path(props) break else: LOG.warning( _LW('Failed to connect to iSCSI portal %(portal)s.' ), {'portal': props['target_portal']}) else: # If we aren't trying to connect to the portal, we # want to find ALL possible paths from all of the # alternate portals if props['target_portal'] in iscsi_sessions: paths = self._get_device_path(props) host_devices = list(set(paths + host_devices)) if connect_to_portal and not connected_to_portal: msg = _("Could not login to any iSCSI portal.") LOG.error(msg) raise exception.FailedISCSITargetPortalLogin(message=msg) return host_devices, target_props
def connect_volume(self, connection_properties): """Attach the volume to instance_name. :param connection_properties: The valid dictionary that describes all of the target volume attributes. :type connection_properties: dict :returns: dict connection_properties for iSCSI must include: target_portal(s) - ip and optional port target_iqn(s) - iSCSI Qualified Name target_lun(s) - LUN id of the volume Note that plural keys may be used when use_multipath=True """ device_info = {'type': 'block'} # At this point the host_devices may be an empty list host_devices, target_props = self._get_potential_volume_paths( connection_properties) # The /dev/disk/by-path/... node is not always present immediately # TODO(justinsb): This retry-with-delay is a pattern, move to utils? tries = 0 # Loop until at least 1 path becomes available while all(map(lambda x: not os.path.exists(x), host_devices)): if tries >= self.device_scan_attempts: raise exception.VolumeDeviceNotFound(device=host_devices) LOG.info( _LI("ISCSI volume not yet found at: %(host_devices)s. " "Will rescan & retry. Try number: %(tries)s."), { 'host_devices': host_devices, 'tries': tries }) # The rescan isn't documented as being necessary(?), but it helps if self.use_multipath: self._rescan_iscsi() # We need to refresh the paths as the devices may be empty host_devices, target_props = ( self._get_potential_volume_paths(connection_properties)) else: if (tries): host_devices = self._get_device_path(target_props) self._run_iscsiadm(target_props, ("--rescan", )) tries = tries + 1 if all(map(lambda x: not os.path.exists(x), host_devices)): time.sleep(tries**2) else: break if tries != 0: LOG.debug( "Found iSCSI node %(host_devices)s " "(after %(tries)s rescans)", { 'host_devices': host_devices, 'tries': tries }) # Choose an accessible host device host_device = next(dev for dev in host_devices if os.path.exists(dev)) # find out the WWN of the device device_wwn = self._linuxscsi.get_scsi_wwn(host_device) LOG.debug("Device WWN = '%(wwn)s'", {'wwn': device_wwn}) device_info['scsi_wwn'] = device_wwn if self.use_multipath: (host_device, multipath_id) = (super(ISCSIConnector, self)._discover_mpath_device( device_wwn, connection_properties, host_device)) if multipath_id: device_info['multipath_id'] = multipath_id device_info['path'] = host_device LOG.debug("connect_volume returning %s", device_info) return device_info
def _connect_to_iscsi_portal(self, connection_properties): # NOTE(vish): If we are on the same host as nova volume, the # discovery makes the target so we don't need to # run --op new. Therefore, we check to see if the # target exists, and if we get 255 (Not Found), then # we run --op new. This will also happen if another # volume is using the same target. LOG.info(_LI("Trying to connect to iSCSI portal %(portal)s"), {"portal": connection_properties['target_portal']}) try: self._run_iscsiadm(connection_properties, ()) except putils.ProcessExecutionError as exc: # iscsiadm returns 21 for "No records found" after version 2.0-871 if exc.exit_code in [21, 255]: self._run_iscsiadm( connection_properties, ('--interface', self._get_transport(), '--op', 'new')) else: raise if connection_properties.get('auth_method'): self._iscsiadm_update(connection_properties, "node.session.auth.authmethod", connection_properties['auth_method']) self._iscsiadm_update(connection_properties, "node.session.auth.username", connection_properties['auth_username']) self._iscsiadm_update(connection_properties, "node.session.auth.password", connection_properties['auth_password']) # Duplicate logins crash iscsiadm after load, # so we scan active sessions to see if the node is logged in. out = self._run_iscsiadm_bare(["-m", "session"], run_as_root=True, check_exit_code=[0, 1, 21])[0] or "" portals = [{ 'portal': p.split(" ")[2], 'iqn': p.split(" ")[3] } for p in out.splitlines() if p.startswith("tcp:")] stripped_portal = connection_properties['target_portal'].split(",")[0] if len(portals) == 0 or len([ s for s in portals if stripped_portal == s['portal'].split(",")[0] and s['iqn'] == connection_properties['target_iqn'] ]) == 0: try: self._run_iscsiadm(connection_properties, ("--login", ), check_exit_code=[0, 255]) except putils.ProcessExecutionError as err: # exit_code=15 means the session already exists, so it should # be regarded as successful login. if err.exit_code not in [15]: LOG.warning( _LW('Failed to login iSCSI target %(iqn)s ' 'on portal %(portal)s (exit code ' '%(err)s).'), { 'iqn': connection_properties['target_iqn'], 'portal': connection_properties['target_portal'], 'err': err.exit_code }) return False self._iscsiadm_update(connection_properties, "node.startup", "automatic") return True
def disconnect_volume(self, connection_properties, device_info): """Disconnect the ScaleIO volume. :param connection_properties: The dictionary that describes all of the target volume attributes. :type connection_properties: dict :param device_info: historical difference, but same as connection_props :type device_info: dict """ self.get_config(connection_properties) self.volume_id = self.volume_id or self._get_volume_id() LOG.info(_LI( "ScaleIO disconnect volume in ScaleIO brick volume driver." )) LOG.debug( _("ScaleIO Volume name: %(volume_name)s, SDC IP: %(sdc_ip)s, " "REST Server IP: %(server_ip)s"), {'volume_name': self.volume_name, 'sdc_ip': self.local_sdc_ip, 'server_ip': self.server_ip} ) LOG.info(_LI("ScaleIO sdc query guid command: %(cmd)s"), {'cmd': self.GET_GUID_CMD}) try: (out, err) = self._execute(*self.GET_GUID_CMD, run_as_root=True, root_helper=self._root_helper) LOG.info( _LI("Unmap volume %(cmd)s: stdout=%(out)s stderr=%(err)s"), {'cmd': self.GET_GUID_CMD, 'out': out, 'err': err} ) except putils.ProcessExecutionError as e: msg = _("Error querying sdc guid: %(err)s") % {'err': e.stderr} LOG.error(msg) raise exception.BrickException(message=msg) guid = out LOG.info(_LI("Current sdc guid: %(guid)s"), {'guid': guid}) params = {'guid': guid} headers = {'content-type': 'application/json'} request = ( "https://%(server_ip)s:%(server_port)s/api/instances/" "Volume::%(volume_id)s/action/removeMappedSdc" % {'server_ip': self.server_ip, 'server_port': self.server_port, 'volume_id': self.volume_id} ) LOG.info(_LI("Unmap volume request: %(request)s"), {'request': request}) r = requests.post( request, data=json.dumps(params), headers=headers, auth=(self.server_username, self.server_token), verify=False ) r = self._check_response(r, request, False, params) if r.status_code != self.OK_STATUS_CODE: response = r.json() error_code = response['errorCode'] if error_code == self.VOLUME_NOT_MAPPED_ERROR: LOG.warning(_LW( "Ignoring error unmapping volume %(volume_id)s: " "volume not mapped."), {'volume_id': self.volume_name} ) else: msg = (_("Error unmapping volume %(volume_id)s: %(err)s") % {'volume_id': self.volume_name, 'err': response['message']}) LOG.error(msg) raise exception.BrickException(message=msg)
def connect_volume(self, connection_properties): """Connect the volume. :param connection_properties: The dictionary that describes all of the target volume attributes. :type connection_properties: dict :returns: dict """ device_info = self.get_config(connection_properties) LOG.debug( _( "scaleIO Volume name: %(volume_name)s, SDC IP: %(sdc_ip)s, " "REST Server IP: %(server_ip)s, " "REST Server username: %(username)s, " "iops limit:%(iops_limit)s, " "bandwidth limit: %(bandwidth_limit)s." ), { 'volume_name': self.volume_name, 'volume_id': self.volume_id, 'sdc_ip': self.local_sdc_ip, 'server_ip': self.server_ip, 'username': self.server_username, 'iops_limit': self.iops_limit, 'bandwidth_limit': self.bandwidth_limit } ) LOG.info(_LI("ScaleIO sdc query guid command: %(cmd)s"), {'cmd': self.GET_GUID_CMD}) try: (out, err) = self._execute(*self.GET_GUID_CMD, run_as_root=True, root_helper=self._root_helper) LOG.info(_LI("Map volume %(cmd)s: stdout=%(out)s " "stderr=%(err)s"), {'cmd': self.GET_GUID_CMD, 'out': out, 'err': err}) except putils.ProcessExecutionError as e: msg = (_("Error querying sdc guid: %(err)s") % {'err': e.stderr}) LOG.error(msg) raise exception.BrickException(message=msg) guid = out LOG.info(_LI("Current sdc guid: %(guid)s"), {'guid': guid}) params = {'guid': guid, 'allowMultipleMappings': 'TRUE'} self.volume_id = self.volume_id or self._get_volume_id() headers = {'content-type': 'application/json'} request = ( "https://%(server_ip)s:%(server_port)s/api/instances/" "Volume::%(volume_id)s/action/addMappedSdc" % {'server_ip': self.server_ip, 'server_port': self.server_port, 'volume_id': self.volume_id} ) LOG.info(_LI("map volume request: %(request)s"), {'request': request}) r = requests.post( request, data=json.dumps(params), headers=headers, auth=(self.server_username, self.server_token), verify=False ) r = self._check_response(r, request, False, params) if r.status_code != self.OK_STATUS_CODE: response = r.json() error_code = response['errorCode'] if error_code == self.VOLUME_ALREADY_MAPPED_ERROR: LOG.warning(_LW( "Ignoring error mapping volume %(volume_name)s: " "volume already mapped."), {'volume_name': self.volume_name} ) else: msg = ( _("Error mapping volume %(volume_name)s: %(err)s") % {'volume_name': self.volume_name, 'err': response['message']} ) LOG.error(msg) raise exception.BrickException(message=msg) self.volume_path = self._find_volume_path() device_info['path'] = self.volume_path # Set QoS settings after map was performed if self.iops_limit is not None or self.bandwidth_limit is not None: params = {'guid': guid} if self.bandwidth_limit is not None: params['bandwidthLimitInKbps'] = self.bandwidth_limit if self.iops_limit is not None: params['iopsLimit'] = self.iops_limit request = ( "https://%(server_ip)s:%(server_port)s/api/instances/" "Volume::%(volume_id)s/action/setMappedSdcLimits" % {'server_ip': self.server_ip, 'server_port': self.server_port, 'volume_id': self.volume_id} ) LOG.info(_LI("Set client limit request: %(request)s"), {'request': request}) r = requests.post( request, data=json.dumps(params), headers=headers, auth=(self.server_username, self.server_token), verify=False ) r = self._check_response(r, request, False, params) if r.status_code != self.OK_STATUS_CODE: response = r.json() LOG.info(_LI("Set client limit response: %(response)s"), {'response': response}) msg = ( _("Error setting client limits for volume " "%(volume_name)s: %(err)s") % {'volume_name': self.volume_name, 'err': response['message']} ) LOG.error(msg) return device_info
def connect_volume(self, connection_properties): """Connect the volume. :param connection_properties: The dictionary that describes all of the target volume attributes. :type connection_properties: dict :returns: dict """ device_info = self.get_config(connection_properties) LOG.debug( _("scaleIO Volume name: %(volume_name)s, SDC IP: %(sdc_ip)s, " "REST Server IP: %(server_ip)s, " "REST Server username: %(username)s, " "iops limit:%(iops_limit)s, " "bandwidth limit: %(bandwidth_limit)s."), { 'volume_name': self.volume_name, 'volume_id': self.volume_id, 'sdc_ip': self.local_sdc_ip, 'server_ip': self.server_ip, 'username': self.server_username, 'iops_limit': self.iops_limit, 'bandwidth_limit': self.bandwidth_limit }) LOG.info(_LI("ScaleIO sdc query guid command: %(cmd)s"), {'cmd': self.GET_GUID_CMD}) try: (out, err) = self._execute(*self.GET_GUID_CMD, run_as_root=True, root_helper=self._root_helper) LOG.info( _LI("Map volume %(cmd)s: stdout=%(out)s " "stderr=%(err)s"), { 'cmd': self.GET_GUID_CMD, 'out': out, 'err': err }) except putils.ProcessExecutionError as e: msg = (_("Error querying sdc guid: %(err)s") % {'err': e.stderr}) LOG.error(msg) raise exception.BrickException(message=msg) guid = out LOG.info(_LI("Current sdc guid: %(guid)s"), {'guid': guid}) params = {'guid': guid, 'allowMultipleMappings': 'TRUE'} self.volume_id = self.volume_id or self._get_volume_id() headers = {'content-type': 'application/json'} request = ("https://%(server_ip)s:%(server_port)s/api/instances/" "Volume::%(volume_id)s/action/addMappedSdc" % { 'server_ip': self.server_ip, 'server_port': self.server_port, 'volume_id': self.volume_id }) LOG.info(_LI("map volume request: %(request)s"), {'request': request}) r = requests.post(request, data=json.dumps(params), headers=headers, auth=(self.server_username, self.server_token), verify=False) r = self._check_response(r, request, False, params) if r.status_code != self.OK_STATUS_CODE: response = r.json() error_code = response['errorCode'] if error_code == self.VOLUME_ALREADY_MAPPED_ERROR: LOG.warning( _LW("Ignoring error mapping volume %(volume_name)s: " "volume already mapped."), {'volume_name': self.volume_name}) else: msg = (_("Error mapping volume %(volume_name)s: %(err)s") % { 'volume_name': self.volume_name, 'err': response['message'] }) LOG.error(msg) raise exception.BrickException(message=msg) self.volume_path = self._find_volume_path() device_info['path'] = self.volume_path # Set QoS settings after map was performed if self.iops_limit is not None or self.bandwidth_limit is not None: params = {'guid': guid} if self.bandwidth_limit is not None: params['bandwidthLimitInKbps'] = self.bandwidth_limit if self.iops_limit is not None: params['iopsLimit'] = self.iops_limit request = ("https://%(server_ip)s:%(server_port)s/api/instances/" "Volume::%(volume_id)s/action/setMappedSdcLimits" % { 'server_ip': self.server_ip, 'server_port': self.server_port, 'volume_id': self.volume_id }) LOG.info(_LI("Set client limit request: %(request)s"), {'request': request}) r = requests.post(request, data=json.dumps(params), headers=headers, auth=(self.server_username, self.server_token), verify=False) r = self._check_response(r, request, False, params) if r.status_code != self.OK_STATUS_CODE: response = r.json() LOG.info(_LI("Set client limit response: %(response)s"), {'response': response}) msg = (_("Error setting client limits for volume " "%(volume_name)s: %(err)s") % { 'volume_name': self.volume_name, 'err': response['message'] }) LOG.error(msg) return device_info
def disconnect_volume(self, connection_properties, device_info): """Disconnect the ScaleIO volume. :param connection_properties: The dictionary that describes all of the target volume attributes. :type connection_properties: dict :param device_info: historical difference, but same as connection_props :type device_info: dict """ self.get_config(connection_properties) self.volume_id = self.volume_id or self._get_volume_id() LOG.info( _LI("ScaleIO disconnect volume in ScaleIO brick volume driver.")) LOG.debug( _("ScaleIO Volume name: %(volume_name)s, SDC IP: %(sdc_ip)s, " "REST Server IP: %(server_ip)s"), { 'volume_name': self.volume_name, 'sdc_ip': self.local_sdc_ip, 'server_ip': self.server_ip }) LOG.info(_LI("ScaleIO sdc query guid command: %(cmd)s"), {'cmd': self.GET_GUID_CMD}) try: (out, err) = self._execute(*self.GET_GUID_CMD, run_as_root=True, root_helper=self._root_helper) LOG.info( _LI("Unmap volume %(cmd)s: stdout=%(out)s stderr=%(err)s"), { 'cmd': self.GET_GUID_CMD, 'out': out, 'err': err }) except putils.ProcessExecutionError as e: msg = _("Error querying sdc guid: %(err)s") % {'err': e.stderr} LOG.error(msg) raise exception.BrickException(message=msg) guid = out LOG.info(_LI("Current sdc guid: %(guid)s"), {'guid': guid}) params = {'guid': guid} headers = {'content-type': 'application/json'} request = ("https://%(server_ip)s:%(server_port)s/api/instances/" "Volume::%(volume_id)s/action/removeMappedSdc" % { 'server_ip': self.server_ip, 'server_port': self.server_port, 'volume_id': self.volume_id }) LOG.info(_LI("Unmap volume request: %(request)s"), {'request': request}) r = requests.post(request, data=json.dumps(params), headers=headers, auth=(self.server_username, self.server_token), verify=False) r = self._check_response(r, request, False, params) if r.status_code != self.OK_STATUS_CODE: response = r.json() error_code = response['errorCode'] if error_code == self.VOLUME_NOT_MAPPED_ERROR: LOG.warning( _LW("Ignoring error unmapping volume %(volume_id)s: " "volume not mapped."), {'volume_id': self.volume_name}) else: msg = (_("Error unmapping volume %(volume_id)s: %(err)s") % { 'volume_id': self.volume_name, 'err': response['message'] }) LOG.error(msg) raise exception.BrickException(message=msg)
def _connect_to_iscsi_portal(self, connection_properties): # NOTE(vish): If we are on the same host as nova volume, the # discovery makes the target so we don't need to # run --op new. Therefore, we check to see if the # target exists, and if we get 255 (Not Found), then # we run --op new. This will also happen if another # volume is using the same target. LOG.info(_LI("Trying to connect to iSCSI portal %(portal)s"), {"portal": connection_properties['target_portal']}) try: self._run_iscsiadm(connection_properties, ()) except putils.ProcessExecutionError as exc: # iscsiadm returns 21 for "No records found" after version 2.0-871 if exc.exit_code in [21, 255]: self._run_iscsiadm(connection_properties, ('--interface', self._get_transport(), '--op', 'new')) else: raise if connection_properties.get('auth_method'): self._iscsiadm_update(connection_properties, "node.session.auth.authmethod", connection_properties['auth_method']) self._iscsiadm_update(connection_properties, "node.session.auth.username", connection_properties['auth_username']) self._iscsiadm_update(connection_properties, "node.session.auth.password", connection_properties['auth_password']) # Duplicate logins crash iscsiadm after load, # so we scan active sessions to see if the node is logged in. out = self._run_iscsiadm_bare(["-m", "session"], run_as_root=True, check_exit_code=[0, 1, 21])[0] or "" portals = [{'portal': p.split(" ")[2], 'iqn': p.split(" ")[3]} for p in out.splitlines() if p.startswith("tcp:")] stripped_portal = connection_properties['target_portal'].split(",")[0] if len(portals) == 0 or len([s for s in portals if stripped_portal == s['portal'].split(",")[0] and s['iqn'] == connection_properties['target_iqn']] ) == 0: try: self._run_iscsiadm(connection_properties, ("--login",), check_exit_code=[0, 255]) except putils.ProcessExecutionError as err: # exit_code=15 means the session already exists, so it should # be regarded as successful login. if err.exit_code not in [15]: LOG.warning(_LW('Failed to login iSCSI target %(iqn)s ' 'on portal %(portal)s (exit code ' '%(err)s).'), {'iqn': connection_properties['target_iqn'], 'portal': connection_properties[ 'target_portal'], 'err': err.exit_code}) return False self._iscsiadm_update(connection_properties, "node.startup", "automatic") return True
def connect_volume(self, connection_properties): """Attach the volume to instance_name. :param connection_properties: The valid dictionary that describes all of the target volume attributes. :type connection_properties: dict :returns: dict connection_properties for iSCSI must include: target_portal(s) - ip and optional port target_iqn(s) - iSCSI Qualified Name target_lun(s) - LUN id of the volume Note that plural keys may be used when use_multipath=True """ device_info = {'type': 'block'} # At this point the host_devices may be an empty list host_devices, target_props = self._get_potential_volume_paths( connection_properties) # The /dev/disk/by-path/... node is not always present immediately # TODO(justinsb): This retry-with-delay is a pattern, move to utils? tries = 0 # Loop until at least 1 path becomes available while all(map(lambda x: not os.path.exists(x), host_devices)): if tries >= self.device_scan_attempts: raise exception.VolumeDeviceNotFound(device=host_devices) LOG.info(_LI("ISCSI volume not yet found at: %(host_devices)s. " "Will rescan & retry. Try number: %(tries)s."), {'host_devices': host_devices, 'tries': tries}) # The rescan isn't documented as being necessary(?), but it helps if self.use_multipath: self._rescan_iscsi() # We need to refresh the paths as the devices may be empty host_devices, target_props = ( self._get_potential_volume_paths(connection_properties)) else: if (tries): host_devices = self._get_device_path(target_props) self._run_iscsiadm(target_props, ("--rescan",)) tries = tries + 1 if all(map(lambda x: not os.path.exists(x), host_devices)): time.sleep(tries ** 2) else: break if tries != 0: LOG.debug("Found iSCSI node %(host_devices)s " "(after %(tries)s rescans)", {'host_devices': host_devices, 'tries': tries}) # Choose an accessible host device host_device = next(dev for dev in host_devices if os.path.exists(dev)) # find out the WWN of the device device_wwn = self._linuxscsi.get_scsi_wwn(host_device) LOG.debug("Device WWN = '%(wwn)s'", {'wwn': device_wwn}) device_info['scsi_wwn'] = device_wwn if self.use_multipath: (host_device, multipath_id) = (super( ISCSIConnector, self)._discover_mpath_device( device_wwn, connection_properties, host_device)) if multipath_id: device_info['multipath_id'] = multipath_id device_info['path'] = host_device LOG.debug("connect_volume returning %s", device_info) return device_info
def _get_potential_volume_paths(self, connection_properties, connect_to_portal=True, use_rescan=True): """Build a list of potential volume paths that exist. Given a list of target_portals in the connection_properties, a list of paths might exist on the system during discovery. This method's job is to build that list of potential paths for a volume that might show up. This is used during connect_volume time, in which case we want to connect to the iSCSI target portal. During get_volume_paths time, we are looking to find a list of existing volume paths for the connection_properties. In this case, we don't want to connect to the portal. If we blindly try and connect to a portal, it could create a new iSCSI session that didn't exist previously, and then leave it stale. :param connection_properties: The dictionary that describes all of the target volume attributes. :type connection_properties: dict :param connect_to_portal: Do we want to try a new connection to the target portal(s)? Set this to False if you want to search for existing volumes, not discover new volumes. :param connect_to_portal: bool :param use_rescan: Issue iSCSI rescan during discovery? :type use_rescan: bool :returns: dict """ target_props = None connected_to_portal = False if self.use_multipath: LOG.info(_LI("Multipath discovery for iSCSI enabled")) # Multipath installed, discovering other targets if available try: ips_iqns = self._discover_iscsi_portals(connection_properties) except Exception: if 'target_portals' in connection_properties: raise exception.TargetPortalsNotFound( target_portals=connection_properties['target_portals']) elif 'target_portal' in connection_properties: raise exception.TargetPortalNotFound( target_portal=connection_properties['target_portal']) else: raise if not connection_properties.get('target_iqns'): # There are two types of iSCSI multipath devices. One which # shares the same iqn between multiple portals, and the other # which use different iqns on different portals. # Try to identify the type by checking the iscsiadm output # if the iqn is used by multiple portals. If it is, it's # the former, so use the supplied iqn. Otherwise, it's the # latter, so try the ip,iqn combinations to find the targets # which constitutes the multipath device. main_iqn = connection_properties['target_iqn'] all_portals = set([ip for ip, iqn in ips_iqns]) match_portals = set([ip for ip, iqn in ips_iqns if iqn == main_iqn]) if len(all_portals) == len(match_portals): ips_iqns = zip(all_portals, [main_iqn] * len(all_portals)) for ip, iqn in ips_iqns: props = copy.deepcopy(connection_properties) props['target_portal'] = ip props['target_iqn'] = iqn if connect_to_portal: if self._connect_to_iscsi_portal(props): connected_to_portal = True if use_rescan: self._rescan_iscsi() host_devices = self._get_device_path(connection_properties) else: LOG.info(_LI("Multipath discovery for iSCSI not enabled.")) iscsi_sessions = [] if not connect_to_portal: iscsi_sessions = self._get_iscsi_sessions() host_devices = [] target_props = connection_properties for props in self._iterate_all_targets(connection_properties): if connect_to_portal: if self._connect_to_iscsi_portal(props): target_props = props connected_to_portal = True host_devices = self._get_device_path(props) break else: LOG.warning(_LW( 'Failed to connect to iSCSI portal %(portal)s.'), {'portal': props['target_portal']}) else: # If we aren't trying to connect to the portal, we # want to find ALL possible paths from all of the # alternate portals if props['target_portal'] in iscsi_sessions: paths = self._get_device_path(props) host_devices = list(set(paths + host_devices)) if connect_to_portal and not connected_to_portal: msg = _("Could not login to any iSCSI portal.") LOG.error(msg) raise exception.FailedISCSITargetPortalLogin(message=msg) return host_devices, target_props