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,99 @@ 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+ RAID_TYPES = {'raid0' , 'raid1' , 'raid4' , 'raid5' , 'raid6' , 'raid10' , 'linear' }
679+ USED_TYPES = RAID_TYPES | {'lvm' , 'mpath' , 'crypt' }
680+ LSBLK_FIELDS = 'NAME,KNAME,PKNAME,SIZE,LOG-SEC,TYPE,MOUNTPOINT'
681+
682+ devices : list [Host .BlockDeviceInfo ] = []
683+
684+ raw = self .ssh (f'lsblk --pairs --bytes --output { LSBLK_FIELDS } ' )
685+ rows = [
686+ {key .lower (): val .strip ('"' )
687+ for key , val in re .findall (r'(\S+)=(".*?"|\S+)' , line )}
688+ for line in raw .strip ().splitlines ()
689+ ] if raw .strip () else []
690+
691+ # build children map: kname -> list of child knames
692+ children : dict [str , list [str ]] = {}
693+ for r in rows :
694+ if r .get ('pkname' ):
695+ children .setdefault (r ['pkname' ], []).append (r ['kname' ])
696+
697+ # availability from lsblk fields: no mountpoint, not a "used" type, all descendants also free
698+ def _row_by_kname (kname : str ) -> dict [str , str ] | None :
699+ for r in rows :
700+ if r ['kname' ] == kname :
701+ return r
702+ return None
681703
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 ])
704+ def _all_available (kname : str ) -> bool :
705+ r = _row_by_kname (kname )
706+ if r is None :
707+ return False
708+ if r .get ('mountpoint' ) or r .get ('type' ) in USED_TYPES :
709+ return False
710+ return all (_all_available (c ) for c in children .get (kname , []))
711+
712+ seen_knames : set [str ] = set ()
713+
714+ for r in rows :
715+ kname = r ['kname' ]
716+ typ = r .get ('type' , '' )
717+
718+ # --- local disks ---
719+ if typ == 'disk' and not r .get ('pkname' ):
720+ devices .append (Host .BlockDeviceInfo (
721+ name = r ['name' ],
722+ path = f'/dev/{ r ["name" ]} ' ,
723+ size = int (r ['size' ]),
724+ log_sec = int (r ['log-sec' ]),
725+ type = 'disk' ,
726+ available = _all_available (kname ) and not r ['mountpoint' ].strip (),
727+ ))
728+
729+ # --- mdadm arrays (may appear once per member, deduplicate) ---
730+ elif typ in RAID_TYPES :
731+ if kname in seen_knames :
732+ continue
733+ seen_knames .add (kname )
734+ available = not r ['mountpoint' ].strip () and all (_all_available (c ) for c in children .get (kname , []))
735+ devices .append (Host .BlockDeviceInfo (
736+ name = r ['name' ],
737+ path = f'/dev/{ r ["name" ]} ' ,
738+ size = int (r ['size' ]),
739+ log_sec = int (r .get ('log-sec' , '0' )),
740+ type = 'md' ,
741+ available = available ,
742+ ))
743+
744+ # --- multipath devices (may appear once per path, deduplicate) ---
745+ elif typ == 'mpath' :
746+ if kname in seen_knames :
747+ continue
748+ seen_knames .add (kname )
749+ available = not r ['mountpoint' ].strip () and all (_all_available (c ) for c in children .get (kname , []))
750+ devices .append (Host .BlockDeviceInfo (
751+ name = kname ,
752+ path = f'/dev/mapper/{ r ["name" ]} ' ,
753+ size = int (r ['size' ]),
754+ log_sec = int (r .get ('log-sec' , '0' )),
755+ type = 'mpath' ,
756+ available = available ,
757+ ))
758+
759+ self .block_devices_info = sorted (devices , key = lambda d : d .name )
760+ logging .debug ("blockdevs found: %s" , [d .name for d in self .block_devices_info ])
688761
689762 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
763+ """ List of all block devices (local disks, mdadm arrays, multipath devices). """
764+ return list (self .block_devices_info )
714765
715766 def file_exists (self , filepath : str , regular_file : bool = True ) -> bool :
716767 option = '-f' if regular_file else '-e'
0 commit comments