# Copyright 2012 Pedro Navarro Perez # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. """ Volume driver for Windows Server 2012 This driver requires ISCSI target role installed """ import contextlib import os from os_win import utilsfactory from oslo_config import cfg from oslo_log import log as logging from oslo_utils import fileutils from oslo_utils import units from oslo_utils import uuidutils from cinder.common import constants from cinder import exception from cinder.image import image_utils from cinder import interface from cinder.volume import configuration from cinder.volume import driver from cinder.volume import volume_utils LOG = logging.getLogger(__name__) windows_opts = [ cfg.StrOpt('windows_iscsi_lun_path', default=r'C:\iSCSIVirtualDisks', help='Path to store VHD backed volumes'), ] CONF = cfg.CONF CONF.register_opts(windows_opts, group=configuration.SHARED_CONF_GROUP) @interface.volumedriver class WindowsISCSIDriver(driver.ISCSIDriver): """Executes volume driver commands on Windows Storage server.""" VERSION = '1.0.0' # ThirdPartySystems wiki page CI_WIKI_NAME = "Microsoft_iSCSI_CI" def __init__(self, *args, **kwargs): super(WindowsISCSIDriver, self).__init__(*args, **kwargs) self.configuration = kwargs.get('configuration', None) if self.configuration: self.configuration.append_config_values(windows_opts) self._vhdutils = utilsfactory.get_vhdutils() self._tgt_utils = utilsfactory.get_iscsi_target_utils() self._hostutils = utilsfactory.get_hostutils() @staticmethod def get_driver_options(): return windows_opts def do_setup(self, context): """Setup the Windows Volume driver. Called one time by the manager after the driver is loaded. Validate the flags we care about """ fileutils.ensure_tree(self.configuration.windows_iscsi_lun_path) fileutils.ensure_tree(CONF.image_conversion_dir) def check_for_setup_error(self): """Check that the driver is working and can communicate.""" self._get_portals() def _get_portals(self): available_portals = set(self._tgt_utils.get_portal_locations( available_only=True, fail_if_none_found=True)) LOG.debug("Available iSCSI portals: %s", available_portals) iscsi_port = self.configuration.target_port iscsi_ips = ([self.configuration.target_ip_address] + self.configuration.target_secondary_ip_addresses) requested_portals = {':'.join([iscsi_ip, str(iscsi_port)]) for iscsi_ip in iscsi_ips} unavailable_portals = requested_portals - available_portals if unavailable_portals: LOG.warning("The following iSCSI portals were requested but " "are not available: %s.", unavailable_portals) selected_portals = requested_portals & available_portals if not selected_portals: err_msg = "None of the configured iSCSI portals are available." raise exception.VolumeDriverException(err_msg) return list(selected_portals) def _get_host_information(self, volume, multipath=False): """Getting the portal and port information.""" target_name = self._get_target_name(volume) available_portals = self._get_portals() properties = self._tgt_utils.get_target_information(target_name) # Note(lpetrut): the WT_Host CHAPSecret field cannot be accessed # for security reasons. auth = volume.provider_auth if auth: (auth_method, auth_username, auth_secret) = auth.split() properties['auth_method'] = auth_method properties['auth_username'] = auth_username properties['auth_password'] = auth_secret properties['target_portal'] = available_portals[0] properties['target_discovered'] = False properties['target_lun'] = 0 properties['volume_id'] = volume.id if multipath: properties['target_portals'] = available_portals properties['target_iqns'] = [properties['target_iqn'] for portal in available_portals] properties['target_luns'] = [properties['target_lun'] for portal in available_portals] return properties def initialize_connection(self, volume, connector): """Driver entry point to attach a volume to an instance.""" initiator_name = connector['initiator'] target_name = volume.provider_location self._tgt_utils.associate_initiator_with_iscsi_target(initiator_name, target_name) properties = self._get_host_information(volume, connector.get('multipath')) return { 'driver_volume_type': 'iscsi', 'data': properties, } def terminate_connection(self, volume, connector, **kwargs): """Driver entry point to unattach a volume from an instance. Unmask the LUN on the storage system so the given initiator can no longer access it. """ initiator_name = connector['initiator'] target_name = volume.provider_location self._tgt_utils.deassociate_initiator(initiator_name, target_name) def create_volume(self, volume): """Driver entry point for creating a new volume.""" vhd_path = self.local_path(volume) vol_name = volume.name vol_size_mb = volume.size * 1024 self._tgt_utils.create_wt_disk(vhd_path, vol_name, size_mb=vol_size_mb) def local_path(self, volume, disk_format=None): base_vhd_folder = self.configuration.windows_iscsi_lun_path if not disk_format: disk_format = self._tgt_utils.get_supported_disk_format() disk_fname = "%s.%s" % (volume.name, disk_format) return os.path.join(base_vhd_folder, disk_fname) def delete_volume(self, volume): """Driver entry point for destroying existing volumes.""" vol_name = volume.name vhd_path = self.local_path(volume) self._tgt_utils.remove_wt_disk(vol_name) fileutils.delete_if_exists(vhd_path) def create_snapshot(self, snapshot): """Driver entry point for creating a snapshot.""" # Getting WT_Snapshot class vol_name = snapshot.volume_name snapshot_name = snapshot.name self._tgt_utils.create_snapshot(vol_name, snapshot_name) def create_volume_from_snapshot(self, volume, snapshot): """Driver entry point for exporting snapshots as volumes.""" snapshot_name = snapshot.name vol_name = volume.name vhd_path = self.local_path(volume) self._tgt_utils.export_snapshot(snapshot_name, vhd_path) self._tgt_utils.import_wt_disk(vhd_path, vol_name) def delete_snapshot(self, snapshot): """Driver entry point for deleting a snapshot.""" snapshot_name = snapshot.name self._tgt_utils.delete_snapshot(snapshot_name) def ensure_export(self, context, volume): # iSCSI targets exported by WinTarget persist after host reboot. pass def _get_target_name(self, volume): return "%s%s" % (self.configuration.target_prefix, volume.name) def create_export(self, context, volume, connector): """Driver entry point to get the export info for a new volume.""" target_name = self._get_target_name(volume) updates = {} if not self._tgt_utils.iscsi_target_exists(target_name): self._tgt_utils.create_iscsi_target(target_name) updates['provider_location'] = target_name if self.configuration.use_chap_auth: chap_username = (self.configuration.chap_username or volume_utils.generate_username()) chap_password = (self.configuration.chap_password or volume_utils.generate_password()) self._tgt_utils.set_chap_credentials(target_name, chap_username, chap_password) updates['provider_auth'] = ' '.join(('CHAP', chap_username, chap_password)) # This operation is idempotent self._tgt_utils.add_disk_to_target(volume.name, target_name) return updates def remove_export(self, context, volume): """Driver entry point to remove an export for a volume.""" target_name = self._get_target_name(volume) self._tgt_utils.delete_iscsi_target(target_name) def copy_image_to_volume(self, context, volume, image_service, image_id, disable_sparse=False): """Fetch the image from image_service and create a volume using it.""" # Convert to VHD and file back to VHD vhd_type = self._tgt_utils.get_supported_vhd_type() with image_utils.temporary_file(suffix='.vhd') as tmp: volume_path = self.local_path(volume) image_utils.fetch_to_vhd(context, image_service, image_id, tmp, self.configuration.volume_dd_blocksize, disable_sparse=disable_sparse) # The vhd must be disabled and deleted before being replaced with # the desired image. self._tgt_utils.change_wt_disk_status(volume.name, enabled=False) os.unlink(volume_path) self._vhdutils.convert_vhd(tmp, volume_path, vhd_type) self._vhdutils.resize_vhd(volume_path, volume.size << 30, is_file_max_size=False) self._tgt_utils.change_wt_disk_status(volume.name, enabled=True) @contextlib.contextmanager def _temporary_snapshot(self, volume_name): try: snap_uuid = uuidutils.generate_uuid() snapshot_name = '%s-tmp-snapshot-%s' % (volume_name, snap_uuid) self._tgt_utils.create_snapshot(volume_name, snapshot_name) yield snapshot_name finally: self._tgt_utils.delete_snapshot(snapshot_name) def copy_volume_to_image(self, context, volume, image_service, image_meta): """Copy the volume to the specified image.""" disk_format = self._tgt_utils.get_supported_disk_format() temp_vhd_path = os.path.join(CONF.image_conversion_dir, str(image_meta['id']) + '.' + disk_format) try: with self._temporary_snapshot(volume.name) as tmp_snap_name: # qemu-img cannot access VSS snapshots, for which reason it # must be exported first. self._tgt_utils.export_snapshot(tmp_snap_name, temp_vhd_path) volume_utils.upload_volume( context, image_service, image_meta, temp_vhd_path, volume, 'vhd') finally: fileutils.delete_if_exists(temp_vhd_path) def create_cloned_volume(self, volume, src_vref): """Creates a clone of the specified volume.""" src_vol_name = src_vref.name vol_name = volume.name vol_size = volume.size new_vhd_path = self.local_path(volume) with self._temporary_snapshot(src_vol_name) as tmp_snap_name: self._tgt_utils.export_snapshot(tmp_snap_name, new_vhd_path) self._vhdutils.resize_vhd(new_vhd_path, vol_size << 30, is_file_max_size=False) self._tgt_utils.import_wt_disk(new_vhd_path, vol_name) def _get_capacity_info(self): drive = os.path.splitdrive( self.configuration.windows_iscsi_lun_path)[0] (size, free_space) = self._hostutils.get_volume_info(drive) total_gb = size / units.Gi free_gb = free_space / units.Gi return (total_gb, free_gb) def _update_volume_stats(self): """Retrieve stats info for Windows device.""" LOG.debug("Updating volume stats") total_gb, free_gb = self._get_capacity_info() data = {} backend_name = self.configuration.safe_get('volume_backend_name') data["volume_backend_name"] = backend_name or self.__class__.__name__ data["vendor_name"] = 'Microsoft' data["driver_version"] = self.VERSION data["storage_protocol"] = constants.ISCSI data['total_capacity_gb'] = total_gb data['free_capacity_gb'] = free_gb data['reserved_percentage'] = self.configuration.reserved_percentage data['QoS_support'] = False self._stats = data def extend_volume(self, volume, new_size): """Extend an Existing Volume.""" old_size = volume.size LOG.debug("Extend volume from %(old_size)s GB to %(new_size)s GB.", {'old_size': old_size, 'new_size': new_size}) additional_size_mb = (new_size - old_size) * 1024 self._tgt_utils.extend_wt_disk(volume.name, additional_size_mb) def backup_use_temp_snapshot(self): return False