77import subprocess
88import tempfile
99import uuid
10+ from dataclasses import dataclass
1011
1112from packaging import version
1213
1314import lib .commands as commands
1415from lib .common import (
15- DiskDevName ,
1616 _param_add ,
1717 _param_clear ,
1818 _param_get ,
3131from lib .vm import VM
3232from lib .xo import xo_cli , xo_object_exists
3333
34- from typing import TYPE_CHECKING , Literal , TypedDict , overload
34+ from typing import TYPE_CHECKING , Literal , overload
3535
3636if TYPE_CHECKING :
3737 from lib .pool import Pool
@@ -55,14 +55,14 @@ class Host:
5555 pool : "Pool"
5656
5757 # Data extraction is automatic, no conversion from str is done.
58- BlockDeviceInfo = TypedDict ( 'BlockDeviceInfo' , { "name" : str ,
59- "kname" : str ,
60- "pkname" : str ,
61- "size" : str ,
62- "log-sec" : str ,
63- "type" : str ,
64- })
65- BLOCK_DEVICES_FIELDS = ',' . join ( k . upper () for k in BlockDeviceInfo . __annotations__ )
58+ @ dataclass
59+ class BlockDeviceInfo :
60+ name : str # short kernel name: "sda", "md0", "dm-3"
61+ path : str # full device path: "/dev/sda", "/dev/md/myarray", "/dev/mapper/mpathb"
62+ size : int # bytes
63+ log_sec : int # logical sector size; 0 for md/mpath
64+ type : str # "disk", "md", "mpath"
65+ available : bool # not mounted, not member of md/lvm/mpath/zfs
6666
6767 block_devices_info : list [BlockDeviceInfo ]
6868
@@ -669,48 +669,115 @@ def management_pif(self) -> PIF:
669669
670670 def rescan_block_devices_info (self ) -> None :
671671 """
672- Initalize static informations about the disks.
672+ Initialize information about block devices: local disks, mdadm arrays, and multipath devices .
673673
674- Despite those being static, it can be necessary to rescan,
674+ Despite those being mostly static, it can be necessary to rescan,
675675 when we test how XCP-ng reacts to changes of hardware (or
676676 reconfiguration of device blocksize), or after a reboot.
677677 """
678- output_string = self .ssh (
679- f'lsblk --pairs --bytes -I 8,259 --output { Host .BLOCK_DEVICES_FIELDS } '
680- ) # limit to: sd, blkext
678+ # Majors: 8=SCSI/SATA, 65-71,128-135=SCSI extended, 259=NVMe/blkext
679+ LOCAL_MAJORS = '8,65,66,67,68,69,70,71,128,129,130,131,132,133,134,135,259'
680+ LSBLK_FIELDS = 'NAME,KNAME,PKNAME,SIZE,LOG-SEC,TYPE'
681+
682+ devices : list [Host .BlockDeviceInfo ] = []
683+
684+ # --- Local block devices ---
685+ raw = self .ssh (f'lsblk --pairs --bytes -I { LOCAL_MAJORS } --output { LSBLK_FIELDS } ' )
686+ rows = [
687+ {key .lower (): val .strip ('"' )
688+ for key , val in re .findall (r'(\S+)=(".*?"|\S+)' , line )}
689+ for line in raw .strip ().splitlines ()
690+ ] if raw .strip () else []
691+
692+ # build set of device names that are parents of something (have children)
693+ pknames = {r ['pkname' ] for r in rows if r .get ('pkname' )}
694+ # leaf disks: no parent, not a partition, and not themselves parents
695+ for r in rows :
696+ if r .get ('pkname' ) or r .get ('type' ) == 'part' or r ['kname' ] in pknames :
697+ continue
698+ disk_name = r ['name' ]
699+ dev = f'/dev/{ disk_name } '
700+ available = self ._disk_is_available_local (disk_name )
701+ devices .append (Host .BlockDeviceInfo (
702+ name = disk_name ,
703+ path = dev ,
704+ size = int (r ['size' ]),
705+ log_sec = int (r ['log-sec' ]),
706+ type = 'disk' ,
707+ available = available ,
708+ ))
709+
710+ # --- mdadm arrays ---
711+ # lsblk -I 9 does not reliably enumerate md devices on all kernels/versions,
712+ # so enumerate via /sys/block/md* and query each device individually.
713+ md_names = self .ssh ('ls /sys/block/ | grep -E "^md[0-9]"' ).strip ().splitlines ()
714+ for md_name in md_names :
715+ md_name = md_name .strip ()
716+ if not md_name :
717+ continue
718+ raw = self .ssh (f'lsblk --pairs --bytes /dev/{ md_name } --output NAME,SIZE,LOG-SEC,TYPE' )
719+ for line in raw .strip ().splitlines ():
720+ r = {key .lower (): val .strip ('"' )
721+ for key , val in re .findall (r'(\S+)=(".*?"|\S+)' , line )}
722+ if r .get ('name' ) != md_name :
723+ continue
724+ if r .get ('type' ) not in ('raid0' , 'raid1' , 'raid4' , 'raid5' , 'raid6' , 'raid10' , 'linear' ):
725+ break
726+ mountpoint = self .ssh (f'lsblk --noheadings -o MOUNTPOINT /dev/{ md_name } ' ).strip ()
727+ devices .append (Host .BlockDeviceInfo (
728+ name = md_name ,
729+ path = f'/dev/{ md_name } ' ,
730+ size = int (r ['size' ]),
731+ log_sec = int (r .get ('log-sec' , '0' )),
732+ type = 'md' ,
733+ available = len (mountpoint ) == 0 ,
734+ ))
735+ break
681736
682- self .block_devices_info = [
683- Host .BlockDeviceInfo ({key .lower (): value .strip ('"' ) # type: ignore[misc]
684- for key , value in re .findall (r'(\S+)=(".*?"|\S+)' , line )})
685- for line in output_string .strip ().splitlines ()
686- ]
687- logging .debug ("blockdevs found: %s" , [disk ["name" ] for disk in self .block_devices_info ])
737+ # --- multipath devices ---
738+ raw_mpath = self .ssh ('lsblk --pairs --bytes -I 253 --output NAME,SIZE,LOG-SEC,TYPE,DM-NAME' )
739+ for line in raw_mpath .strip ().splitlines ():
740+ r = {key .lower (): val .strip ('"' )
741+ for key , val in re .findall (r'(\S+)=(".*?"|\S+)' , line )}
742+ if not r or r .get ('type' ) != 'mpath' :
743+ continue
744+ dm_name = r ['name' ] # e.g. "dm-3"
745+ dm_alias = r .get ('dm-name' , '' ).strip () # e.g. "mpathb"
746+ path = f'/dev/mapper/{ dm_alias } ' if dm_alias else f'/dev/{ dm_name } '
747+ mountpoint = self .ssh (f'lsblk --noheadings -o MOUNTPOINT /dev/{ dm_name } ' ).strip ()
748+ devices .append (Host .BlockDeviceInfo (
749+ name = dm_name ,
750+ path = path ,
751+ size = int (r ['size' ]),
752+ log_sec = int (r .get ('log-sec' , '0' )),
753+ type = 'mpath' ,
754+ available = len (mountpoint ) == 0 ,
755+ ))
756+
757+ self .block_devices_info = sorted (devices , key = lambda d : d .name )
758+ logging .debug ("blockdevs found: %s" , [d .name for d in self .block_devices_info ])
759+
760+ def _disk_is_available_local (self , disk : str ) -> bool :
761+ """Check if a local block device is not in use (not mounted, not a member of md/lvm/mpath/zfs)."""
762+ # 1. Check mountpoints
763+ mountpoint = self .ssh (f'lsblk --noheadings -o MOUNTPOINT /dev/{ disk } ' ).strip ()
764+ if mountpoint :
765+ return False
766+ # 2. Check if md member
767+ result = self .ssh_with_result (f'mdadm --examine /dev/{ disk } 2>/dev/null' )
768+ if result .returncode == 0 :
769+ return False
770+ # 3. Check if LVM member
771+ result = self .ssh_with_result (f'pvs /dev/{ disk } 2>/dev/null' )
772+ if result .returncode == 0 :
773+ return False
774+ # 4. Check if ZFS pool member (zpool may not be installed)
775+ zpool_status = self .ssh ('zpool status 2>/dev/null || true' ).strip ()
776+ return not (zpool_status and f'/dev/{ disk } ' in zpool_status )
688777
689778 def disks (self ) -> list [Host .BlockDeviceInfo ]:
690- """ List of BlockDeviceInfo for all disks. """
691- # store the names of the parent devices to filter out the devices with children
692- pknames = set (disk ['pkname' ] for disk in self .block_devices_info if disk ['pkname' ])
693- # filter out partitions from block_devices
694- return sorted (
695- (
696- disk
697- for disk in self .block_devices_info
698- if (not disk ["pkname" ] or disk ['type' ] == 'raid0' ) and disk ['kname' ] not in pknames
699- ),
700- key = lambda disk : disk ["name" ],
701- )
702-
703- def disk_is_available (self , disk : DiskDevName ) -> bool :
704- """
705- Check if a disk is unmounted and appears available for use.
706-
707- It may or may not contain identifiable filesystem or partition label.
708- If there are no mountpoints, it is assumed that the disk is not in use.
709-
710- Warn: This function may misclassify LVM_member disks (e.g. in XOSTOR, RAID, ZFS) as "available".
711- Such disks may not have mountpoints but still be in use.
712- """
713- return len (self .ssh (f'lsblk --noheadings -o MOUNTPOINT /dev/{ disk } ' ).strip ()) == 0
779+ """ List of all block devices (local disks, mdadm arrays, multipath devices). """
780+ return list (self .block_devices_info )
714781
715782 def file_exists (self , filepath : str , regular_file : bool = True ) -> bool :
716783 option = '-f' if regular_file else '-e'
0 commit comments