302402df33
This patch makes a few small changes that are required in order to have the Cinder Backup service working on Windows. - all physial disks must be open in byte mode. 'rb+' must be used when writing. - reading passed the disk size boundary will not return an empty string, raising an IOError instead. For this reason, we're avoiding doing it. - we ensure that the chunk size is a multiple of the sector size. - the chmod command is not available on Windows. Although changing owners is possible, it is not needed. For this reason, the 'temporary_chown' helper will be a noop on Windows. It's easier to do it here rather than do platform checks wherever this gets called. - when connecting the volumes, we pass the 'expect_raw_disk' argument, which provides a hint to the connector about what we expect. This allows the SMBFS connector to return a mounted raw disk path instead of a virtual image path. - when the driver provides temporary snapshots to be used during the backup process, the API is bypassed. For this reason, we need to ensure that the snapshot state and progress gets updated accordingly. Otherwise, this breaks the nova assisted snapshot workflow. We're doing platform checks, ensuring that we don't break/change the current workflow. The Swift and Posix backup drivers are known to be working on Windows. Implements: blueprint windows-smb-backup Depends-On: #I20f791482fb0912772fa62d2949fa5becaec5675 Change-Id: I8769a135974240fdf7cebd4b6d74aaa439ba1f27
351 lines
14 KiB
Python
351 lines
14 KiB
Python
# 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 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 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()
|
|
|
|
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.iscsi_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
|
|
utils.generate_username())
|
|
chap_password = (self.configuration.chap_password or
|
|
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):
|
|
"""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)
|
|
# 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)
|
|
image_utils.upload_volume(context, image_service, image_meta,
|
|
temp_vhd_path, '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"] = '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
|