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,15 @@ 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
64+ type : str # "disk", "md", "mpath"
65+ available : bool # not mounted, not member of md/lvm/mpath/zfs
66+ wwn : str = '' # LUN WWN (hex, no 0x prefix); same LUN has same WWN across hosts
6667
6768 block_devices_info : list [BlockDeviceInfo ]
6869
@@ -669,48 +670,126 @@ def management_pif(self) -> PIF:
669670
670671 def rescan_block_devices_info (self ) -> None :
671672 """
672- Initalize static informations about the disks.
673+ Initialize information about block devices: local disks, mdadm arrays, and multipath devices .
673674
674- Despite those being static, it can be necessary to rescan,
675+ Despite those being mostly static, it can be necessary to rescan,
675676 when we test how XCP-ng reacts to changes of hardware (or
676677 reconfiguration of device blocksize), or after a reboot.
678+
679+ Handled device scenarios and their effect on `available`:
680+
681+ - Plain disk, no children: available unless the disk itself has a
682+ mountpoint.
683+ - Partitioned disk: available if no partition is mounted and no partition
684+ has a used child (lvm, md, mpath, crypt); unavailable otherwise.
685+ - Disk member of an mdadm array: the disk itself is unavailable; the md
686+ array is added as a separate entry (type 'md'), deduplicated across
687+ member appearances, and available if it has no mountpoint and no used
688+ children.
689+ - LUN with multipath configured: each path appears as a 'disk' entry and
690+ a shared 'mpath' entry as its child. The path disks are unavailable
691+ (mpath child); the mpath device is added once (type 'mpath'),
692+ deduplicated by kname across path appearances.
693+ - LUN accessible through multiple paths without multipath configured:
694+ multiple 'disk' entries with no children but sharing the same WWN.
695+ Deduplicated to a single entry (first path seen) based on WWN.
677696 """
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 ])
697+ RAID_TYPES = {'raid0' , 'raid1' , 'raid4' , 'raid5' , 'raid6' , 'raid10' , 'linear' }
698+ USED_TYPES = RAID_TYPES | {'lvm' , 'mpath' , 'crypt' }
699+ LSBLK_FIELDS = 'NAME,KNAME,PKNAME,SIZE,LOG-SEC,TYPE,MOUNTPOINT,WWN'
688700
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" ],
701- )
701+ devices : list [Host .BlockDeviceInfo ] = []
702702
703- def disk_is_available (self , disk : DiskDevName ) -> bool :
704- """
705- Check if a disk is unmounted and appears available for use.
703+ raw = self .ssh (f'lsblk --pairs --bytes --output { LSBLK_FIELDS } ' )
706704
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.
705+ def _split_keys ( line : str ) -> list [ tuple [ str , str ]]:
706+ return re . findall ( r'(\S+)=(".*?"|\S+)' , line )
709707
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
708+ rows = [
709+ {key .lower (): val .strip ('"' ) for key , val in _split_keys (line )}
710+ for line in raw .strip ().splitlines ()
711+ ]
712+
713+ # build children map: kname -> list of child knames
714+ children : dict [str , list [str ]] = {}
715+ for r in rows :
716+ if r ['pkname' ]:
717+ children .setdefault (r ['pkname' ], []).append (r ['kname' ])
718+
719+ # availability from lsblk fields: no mountpoint, not a "used" type, all descendants also free
720+ def _row_by_kname (kname : str ) -> dict [str , str ] | None :
721+ for r in rows :
722+ if r ['kname' ] == kname :
723+ return r
724+ return None
725+
726+ def _all_available (kname : str ) -> bool :
727+ r = _row_by_kname (kname )
728+ if r is None :
729+ return False
730+ if r ['mountpoint' ] or r ['type' ] in USED_TYPES :
731+ return False
732+ return all (_all_available (c ) for c in children .get (kname , []))
733+
734+ seen_knames : set [str ] = set ()
735+ seen_wwns : set [str ] = set ()
736+
737+ for r in rows :
738+ # --- local disks ---
739+ if r ['type' ] == 'disk' and not r ['pkname' ]:
740+ wwn = r ['wwn' ]
741+ if wwn :
742+ if wwn in seen_wwns :
743+ continue
744+ seen_wwns .add (wwn )
745+ devices .append (Host .BlockDeviceInfo (
746+ name = r ['name' ],
747+ path = f'/dev/{ r ["name" ]} ' ,
748+ size = int (r ['size' ]),
749+ log_sec = int (r ['log-sec' ]),
750+ type = 'disk' ,
751+ available = _all_available (r ['kname' ]),
752+ wwn = wwn .removeprefix ('0x' ),
753+ ))
754+
755+ # --- mdadm arrays (may appear once per member, deduplicate) ---
756+ elif r ['type' ] in RAID_TYPES :
757+ if r ['kname' ] in seen_knames :
758+ continue
759+ seen_knames .add (r ['kname' ])
760+ available = not r ['mountpoint' ] and all (_all_available (c ) for c in children .get (r ['kname' ], []))
761+ devices .append (Host .BlockDeviceInfo (
762+ name = r ['name' ],
763+ path = f'/dev/{ r ["name" ]} ' ,
764+ size = int (r ['size' ]),
765+ log_sec = int (r ['log-sec' ]),
766+ type = 'md' ,
767+ available = available ,
768+ ))
769+
770+ # --- multipath devices (may appear once per path, deduplicate) ---
771+ elif r ['type' ] == 'mpath' :
772+ if r ['kname' ] in seen_knames :
773+ continue
774+ seen_knames .add (r ['kname' ])
775+ available = not r ['mountpoint' ] and all (_all_available (c ) for c in children .get (r ['kname' ], []))
776+ wwn = r ['name' ][1 :17 ] if re .fullmatch (r'3[0-9a-f]{32}' , r ['name' ]) else ''
777+ devices .append (Host .BlockDeviceInfo (
778+ name = r ['kname' ],
779+ path = f'/dev/mapper/{ r ["name" ]} ' ,
780+ size = int (r ['size' ]),
781+ log_sec = int (r ['log-sec' ]),
782+ type = 'mpath' ,
783+ available = available ,
784+ wwn = wwn ,
785+ ))
786+
787+ self .block_devices_info = sorted (devices , key = lambda d : d .size , reverse = True )
788+ logging .debug ("blockdevs found: %s" , [d .name for d in self .block_devices_info ])
789+
790+ def disks (self ) -> list [Host .BlockDeviceInfo ]:
791+ """ List of all block devices (local disks, mdadm arrays, multipath devices). """
792+ return list (self .block_devices_info )
714793
715794 def file_exists (self , filepath : str , regular_file : bool = True ) -> bool :
716795 option = '-f' if regular_file else '-e'
0 commit comments