# Copyright 2016 Cloudbase Solutions Srl # 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. import ctypes import os import re import threading from collections.abc import Iterable from oslo_log import log as logging from os_win._i18n import _ from os_win import _utils from os_win import constants from os_win import exceptions from os_win.utils import baseutils from os_win.utils import pathutils from os_win.utils import win32utils from os_win.utils.winapi import libs as w_lib kernel32 = w_lib.get_shared_lib_handle(w_lib.KERNEL32) LOG = logging.getLogger(__name__) class DEVICE_ID_VPD_PAGE(ctypes.BigEndianStructure): _fields_ = [ ('DeviceType', ctypes.c_ubyte, 5), ('Qualifier', ctypes.c_ubyte, 3), ('PageCode', ctypes.c_ubyte), ('PageLength', ctypes.c_uint16) ] class IDENTIFICATION_DESCRIPTOR(ctypes.Structure): _fields_ = [ ('CodeSet', ctypes.c_ubyte, 4), ('ProtocolIdentifier', ctypes.c_ubyte, 4), ('IdentifierType', ctypes.c_ubyte, 4), ('Association', ctypes.c_ubyte, 2), ('_reserved', ctypes.c_ubyte, 1), ('Piv', ctypes.c_ubyte, 1), ('_reserved', ctypes.c_ubyte), ('IdentifierLength', ctypes.c_ubyte) ] PDEVICE_ID_VPD_PAGE = ctypes.POINTER(DEVICE_ID_VPD_PAGE) PIDENTIFICATION_DESCRIPTOR = ctypes.POINTER(IDENTIFICATION_DESCRIPTOR) SCSI_ID_ASSOC_TYPE_DEVICE = 0 SCSI_ID_CODE_SET_BINARY = 1 SCSI_ID_CODE_SET_ASCII = 2 BUS_FILE_BACKED_VIRTUAL = 15 _RESCAN_LOCK = threading.Lock() class DiskUtils(baseutils.BaseUtils): _wmi_cimv2_namespace = 'root/cimv2' _wmi_storage_namespace = 'root/microsoft/windows/storage' def __init__(self): self._conn_cimv2 = self._get_wmi_conn(self._wmi_cimv2_namespace) self._conn_storage = self._get_wmi_conn(self._wmi_storage_namespace) self._win32_utils = win32utils.Win32Utils() # Physical device names look like \\.\PHYSICALDRIVE1 self._phys_dev_name_regex = re.compile(r'\\\\.*\\[a-zA-Z]*([\d]+)') self._pathutils = pathutils.PathUtils() def _get_disk_by_number(self, disk_number, msft_disk_cls=True): if msft_disk_cls: disk = self._conn_storage.Msft_Disk(Number=disk_number) else: disk = self._conn_cimv2.Win32_DiskDrive(Index=disk_number) if not disk: err_msg = _("Could not find the disk number %s") raise exceptions.DiskNotFound(err_msg % disk_number) return disk[0] def _get_disks_by_unique_id(self, unique_id, unique_id_format): # In some cases, multiple disks having the same unique id may be # exposed to the OS. This may happen if there are multiple paths # to the LUN and MPIO is not properly configured. This can be # valuable information to the caller. disks = self._conn_storage.Msft_Disk(UniqueId=unique_id, UniqueIdFormat=unique_id_format) if not disks: err_msg = _("Could not find any disk having unique id " "'%(unique_id)s' and unique id format " "'%(unique_id_format)s'") raise exceptions.DiskNotFound(err_msg % dict( unique_id=unique_id, unique_id_format=unique_id_format)) return disks def get_attached_virtual_disk_files(self): """Retrieve a list of virtual disks attached to the host. This doesn't include disks attached to Hyper-V VMs directly. """ disks = self._conn_storage.Msft_Disk(BusType=BUS_FILE_BACKED_VIRTUAL) return [ dict(location=disk.Location, number=disk.Number, offline=disk.IsOffline, readonly=disk.IsReadOnly) for disk in disks] def is_virtual_disk_file_attached(self, path): # There are multiple ways of checking this. The easiest way would be to # query the disk using virtdisk.dll:GetVirtualDiskInformation and look # for the IsLoaded attribute. The issue with that is that in some # cases, it won't be able to open in-use images. # # Instead, we'll get a list of attached virtual disks and see if the # path we got points to any of those, thus properly handling the # situation in which multiple paths can point to the same file # (e.g. when having symlinks, shares, UNC paths, etc). We still have # to open the files but at least we have better control over the open # flags. if not os.path.exists(path): LOG.debug("Image %s could not be found.", path) return False attached_disks = self.get_attached_virtual_disk_files() for disk in attached_disks: if self._pathutils.is_same_file(path, disk['location']): return True return False def get_disk_numbers_by_unique_id(self, unique_id, unique_id_format): disks = self._get_disks_by_unique_id(unique_id, unique_id_format) return [disk.Number for disk in disks] def get_disk_uid_and_uid_type(self, disk_number): disk = self._get_disk_by_number(disk_number) return disk.UniqueId, disk.UniqueIdFormat def is_mpio_disk(self, disk_number): disk = self._get_disk_by_number(disk_number) return disk.Path.lower().startswith(r'\\?\mpio') def refresh_disk(self, disk_number): disk = self._get_disk_by_number(disk_number) disk.Refresh() def get_device_name_by_device_number(self, device_number): disk = self._get_disk_by_number(device_number, msft_disk_cls=False) return disk.Name def get_device_number_from_device_name(self, device_name): matches = self._phys_dev_name_regex.findall(device_name) if matches: return matches[0] err_msg = _("Could not find device number for device: %s") raise exceptions.DiskNotFound(err_msg % device_name) def rescan_disks(self, merge_requests=False): """Perform a disk rescan. :param merge_requests: If this flag is set and a disk rescan is already pending, we'll just wait for it to finish without issuing a new rescan request. """ if merge_requests: rescan_pending = _RESCAN_LOCK.locked() if rescan_pending: LOG.debug("A disk rescan is already pending. " "Waiting for it to complete.") with _RESCAN_LOCK: if not rescan_pending: self._rescan_disks() else: self._rescan_disks() @_utils.retry_decorator(exceptions=(exceptions.x_wmi, exceptions.OSWinException)) def _rescan_disks(self): LOG.debug("Rescanning disks.") ret = self._conn_storage.Msft_StorageSetting.UpdateHostStorageCache() if isinstance(ret, Iterable): ret = ret[0] if ret: err_msg = _("Rescanning disks failed. Error code: %s.") raise exceptions.OSWinException(err_msg % ret) LOG.debug("Finished rescanning disks.") def get_disk_capacity(self, path, ignore_errors=False): """Returns total/free space for a given directory.""" norm_path = os.path.abspath(path) total_bytes = ctypes.c_ulonglong(0) free_bytes = ctypes.c_ulonglong(0) try: self._win32_utils.run_and_check_output( kernel32.GetDiskFreeSpaceExW, ctypes.c_wchar_p(norm_path), None, ctypes.pointer(total_bytes), ctypes.pointer(free_bytes), kernel32_lib_func=True) return total_bytes.value, free_bytes.value except exceptions.Win32Exception as exc: LOG.error("Could not get disk %(path)s capacity info. " "Exception: %(exc)s", dict(path=path, exc=exc)) if ignore_errors: return 0, 0 else: raise exc def get_disk_size(self, disk_number): """Returns the disk size, given a physical disk number.""" disk = self._get_disk_by_number(disk_number) return disk.Size def _parse_scsi_page_83(self, buff, select_supported_identifiers=False): """Parse SCSI Device Identification VPD (page 0x83 data). :param buff: a byte array containing the SCSI page 0x83 data. :param select_supported_identifiers: select identifiers supported by Windows, in the order of precedence. :returns: a list of identifiers represented as dicts, containing SCSI Unique IDs. """ identifiers = [] buff_sz = len(buff) buff = (ctypes.c_ubyte * buff_sz)(*bytearray(buff)) vpd_pg_struct_sz = ctypes.sizeof(DEVICE_ID_VPD_PAGE) if buff_sz < vpd_pg_struct_sz: reason = _('Invalid VPD page data.') raise exceptions.SCSIPageParsingError(page='0x83', reason=reason) vpd_page = ctypes.cast(buff, PDEVICE_ID_VPD_PAGE).contents vpd_page_addr = ctypes.addressof(vpd_page) total_page_sz = vpd_page.PageLength + vpd_pg_struct_sz if vpd_page.PageCode != 0x83: reason = _('Unexpected page code: %s') % vpd_page.PageCode raise exceptions.SCSIPageParsingError(page='0x83', reason=reason) if total_page_sz > buff_sz: reason = _('VPD page overflow.') raise exceptions.SCSIPageParsingError(page='0x83', reason=reason) if not vpd_page.PageLength: LOG.info('Page 0x83 data does not contain any ' 'identification descriptors.') return identifiers id_desc_offset = vpd_pg_struct_sz while id_desc_offset < total_page_sz: id_desc_addr = vpd_page_addr + id_desc_offset # Remaining buffer size id_desc_buff_sz = buff_sz - id_desc_offset identifier = self._parse_scsi_id_desc(id_desc_addr, id_desc_buff_sz) identifiers.append(identifier) id_desc_offset += identifier['raw_id_desc_size'] if select_supported_identifiers: identifiers = self._select_supported_scsi_identifiers(identifiers) return identifiers def _parse_scsi_id_desc(self, id_desc_addr, buff_sz): """Parse SCSI VPD identification descriptor.""" id_desc_struct_sz = ctypes.sizeof(IDENTIFICATION_DESCRIPTOR) if buff_sz < id_desc_struct_sz: reason = _('Identifier descriptor overflow.') raise exceptions.SCSIIdDescriptorParsingError(reason=reason) id_desc = IDENTIFICATION_DESCRIPTOR.from_address(id_desc_addr) id_desc_sz = id_desc_struct_sz + id_desc.IdentifierLength identifier_addr = id_desc_addr + id_desc_struct_sz if id_desc_sz > buff_sz: reason = _('Identifier overflow.') raise exceptions.SCSIIdDescriptorParsingError(reason=reason) identifier = (ctypes.c_ubyte * id_desc.IdentifierLength).from_address( identifier_addr) raw_id = bytearray(identifier) if id_desc.CodeSet == SCSI_ID_CODE_SET_ASCII: parsed_id = bytes( bytearray(identifier)).decode('ascii').strip('\x00') else: parsed_id = _utils.byte_array_to_hex_str(raw_id) id_dict = { 'code_set': id_desc.CodeSet, 'protocol': (id_desc.ProtocolIdentifier if id_desc.Piv else None), 'type': id_desc.IdentifierType, 'association': id_desc.Association, 'raw_id': raw_id, 'id': parsed_id, 'raw_id_desc_size': id_desc_sz, } return id_dict def _select_supported_scsi_identifiers(self, identifiers): # This method will filter out unsupported SCSI identifiers, # also sorting them based on the order of precedence. selected_identifiers = [] for id_type in constants.SUPPORTED_SCSI_UID_FORMATS: for identifier in identifiers: if identifier['type'] == id_type: selected_identifiers.append(identifier) return selected_identifiers def get_new_disk_policy(self): # This policy is also known as the 'SAN policy', describing # how new disks will be handled. storsetting = self._conn_storage.MSFT_StorageSetting.Get()[1] return storsetting.NewDiskPolicy def set_new_disk_policy(self, policy): """Sets the new disk policy, also known as SAN policy. :param policy: an integer value, one of the DISK_POLICY_* values defined in os_win.constants. """ self._conn_storage.MSFT_StorageSetting.Set( NewDiskPolicy=policy) def set_disk_online(self, disk_number): disk = self._get_disk_by_number(disk_number) err_code = disk.Online()[1] if err_code: err_msg = (_("Failed to bring disk '%(disk_number)s' online. " "Error code: %(err_code)s.") % dict(disk_number=disk_number, err_code=err_code)) raise exceptions.DiskUpdateError(message=err_msg) def set_disk_offline(self, disk_number): disk = self._get_disk_by_number(disk_number) err_code = disk.Offline()[1] if err_code: err_msg = (_("Failed to bring disk '%(disk_number)s' offline. " "Error code: %(err_code)s.") % dict(disk_number=disk_number, err_code=err_code)) raise exceptions.DiskUpdateError(message=err_msg) def set_disk_readonly_status(self, disk_number, read_only): disk = self._get_disk_by_number(disk_number) err_code = disk.SetAttributes(IsReadOnly=bool(read_only))[1] if err_code: err_msg = (_("Failed to set disk '%(disk_number)s' read-only " "status to '%(read_only)s'. " "Error code: %(err_code)s.") % dict(disk_number=disk_number, err_code=err_code, read_only=bool(read_only))) raise exceptions.DiskUpdateError(message=err_msg)