impuls/lib/python3.11/site-packages/os_win/utils/storage/diskutils.py

397 lines
15 KiB
Python

# 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)