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,121 @@ 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
681-
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 ])
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/ 2>/dev/null | grep -E "^md[0-9]" || true' ).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 (
719+ f'lsblk --pairs --bytes /dev/{ md_name } --output NAME,SIZE,LOG-SEC,TYPE 2>/dev/null || true'
720+ )
721+ for line in raw .strip ().splitlines ():
722+ r = {key .lower (): val .strip ('"' )
723+ for key , val in re .findall (r'(\S+)=(".*?"|\S+)' , line )}
724+ if r .get ('name' ) != md_name :
725+ continue
726+ if r .get ('type' ) not in ('raid0' , 'raid1' , 'raid4' , 'raid5' , 'raid6' , 'raid10' , 'linear' ):
727+ break
728+ mountpoint = self .ssh (
729+ f'lsblk --noheadings -o MOUNTPOINT /dev/{ md_name } 2>/dev/null || true'
730+ ).strip ()
731+ devices .append (Host .BlockDeviceInfo (
732+ name = md_name ,
733+ path = f'/dev/{ md_name } ' ,
734+ size = int (r ['size' ]),
735+ log_sec = int (r .get ('log-sec' , '0' ) or '0' ),
736+ type = 'md' ,
737+ available = len (mountpoint ) == 0 ,
738+ ))
739+ break
688740
689- 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" ],
741+ # --- multipath devices ---
742+ raw_mpath = self .ssh (
743+ 'lsblk --pairs --bytes -I 253 --output NAME,SIZE,LOG-SEC,TYPE,DM-NAME 2>/dev/null || true'
701744 )
745+ for line in raw_mpath .strip ().splitlines ():
746+ r = {key .lower (): val .strip ('"' )
747+ for key , val in re .findall (r'(\S+)=(".*?"|\S+)' , line )}
748+ if not r or r .get ('type' ) != 'mpath' :
749+ continue
750+ dm_name = r ['name' ] # e.g. "dm-3"
751+ dm_alias = r .get ('dm-name' , '' ).strip () # e.g. "mpathb"
752+ path = f'/dev/mapper/{ dm_alias } ' if dm_alias else f'/dev/{ dm_name } '
753+ mountpoint = self .ssh (f'lsblk --noheadings -o MOUNTPOINT /dev/{ dm_name } 2>/dev/null || true' ).strip ()
754+ devices .append (Host .BlockDeviceInfo (
755+ name = dm_name ,
756+ path = path ,
757+ size = int (r ['size' ]),
758+ log_sec = int (r .get ('log-sec' , '0' ) or '0' ),
759+ type = 'mpath' ,
760+ available = len (mountpoint ) == 0 ,
761+ ))
762+
763+ self .block_devices_info = sorted (devices , key = lambda d : d .name )
764+ logging .debug ("blockdevs found: %s" , [d .name for d in self .block_devices_info ])
765+
766+ def _disk_is_available_local (self , disk : str ) -> bool :
767+ """Check if a local block device is not in use (not mounted, not a member of md/lvm/mpath/zfs)."""
768+ # 1. Check mountpoints
769+ mountpoint = self .ssh (f'lsblk --noheadings -o MOUNTPOINT /dev/{ disk } ' ).strip ()
770+ if mountpoint :
771+ return False
772+ # 2. Check if md member
773+ result = self .ssh_with_result (f'mdadm --examine /dev/{ disk } 2>/dev/null' )
774+ if result .returncode == 0 :
775+ return False
776+ # 3. Check if LVM member
777+ result = self .ssh_with_result (f'pvs /dev/{ disk } 2>/dev/null' )
778+ if result .returncode == 0 :
779+ return False
780+ # 4. Check if ZFS pool member (zpool may not be installed)
781+ zpool_status = self .ssh ('zpool status 2>/dev/null || true' ).strip ()
782+ return not (zpool_status and f'/dev/{ disk } ' in zpool_status )
702783
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
784+ def disks (self ) -> list [Host .BlockDeviceInfo ]:
785+ """ List of all block devices (local disks, mdadm arrays, multipath devices). """
786+ return list (self .block_devices_info )
714787
715788 def file_exists (self , filepath : str , regular_file : bool = True ) -> bool :
716789 option = '-f' if regular_file else '-e'
0 commit comments