2424from pathlib import Path
2525from shutil import which
2626from time import sleep
27- from typing import Any , NoReturn
27+ from typing import Any , Callable , NoReturn
2828
2929from virtme_ng .utils import (
3030 CACHE_DIR ,
3636 VIRTME_SSH_HOSTNAME_CID_SEPARATORS ,
3737 get_conf ,
3838 scsi_device_id ,
39+ strtobool ,
3940)
4041
4142from .. import architectures , mkinitramfs , modfinder , qemu_helpers , resources , virtmods
@@ -885,21 +886,131 @@ def quote_karg(arg: str) -> str:
885886class DiskArg :
886887 name : str
887888 path : str
889+ opts : dict [str , str ]
890+
891+ _OPTS_HELP = {
892+ # meta parameters
893+ "topology" : ("bool" , "Forward host device topology (sector and I/O sizes)" ),
894+ "iothread" : ("bool" , "Create a dedicated I/O thread for the disk" ),
895+ # general format parameters
896+ "format" : ("str" , "Disk image format (raw|qcow2)" ),
897+ # I/O driver parameters
898+ "cache" : ("str" , "Cache mode (none|writeback|writethrough|directsync|unsafe)" ),
899+ "aio" : ("str" , "Asynchronous I/O mode (native|threads|io_uring)" ),
900+ "discard" : ("bool" , "Pass through TRIM/UNMAP requests (true=unmap, false=ignore)" ),
901+ "detect-zeroes" : ("bool" , "Detect all-zero writes (true=on/unmap, false=off)" ),
902+ "queues" : ("int" , "Number of I/O queues" ),
903+ # topology parameters
904+ # "alignment": ("bytes", "Block alignment offset"),
905+ "log-sec" : ("bytes" , "Logical (LBA) sector size (typically 512 or 4096)" ),
906+ "phy-sec" : ("bytes" , "Physical (underlying) sector size (>=log-sec)" ),
907+ "min-io" : ("bytes" , "Minimum I/O request size" ),
908+ "opt-io" : ("bytes" , "Optimal I/O request size" ),
909+ "rota" : ("bool" , "Device is rotational" ),
910+ # "wzeroes": ("bytes", "Maximum WRITE ZEROES request size"),
911+ # "disc-aln": ("bytes", "TRIM/UNMAP alignment offset"),
912+ "disc-gran" : ("bytes" , "TRIM/UNMAP request granularity" ),
913+ # "disc-max": ("bytes", "Maximum TRIM/UNMAP request size"),
914+ # "disc-zero": ("bool", "TRIM/UNMAP zeroes data"),
915+ }
916+
917+ def __post_init__ (self ):
918+ if self .pop_opt ("topology" , strtobool , False ):
919+ self .opts = self .topology () | self .opts
920+
921+ def get_opt (self , name : str , parser : Callable [[str ], Any ] = str , default : Any = None ) -> Any :
922+ opt = self .opts .get (name , None )
923+ return parser (opt ) if opt is not None else default
924+
925+ def pop_opt (self , name : str , parser : Callable [[str ], Any ] = str , default : Any = None ) -> Any :
926+ opt = self .opts .pop (name , None )
927+ return parser (opt ) if opt is not None else default
928+
929+ def pop_opt_qemu (self , name : str , default : Any = None , * , parser : Callable [[str ], Any ] = str , dest : str | None = None ) -> str | None :
930+ opt = self .pop_opt (name , parser , default )
931+ # return DiskArg.qemu_opt(name=qemu if qemu is not None else name, value=opt)
932+ if opt is None :
933+ return None
934+ if isinstance (opt , bool ):
935+ opt = "on" if opt else "off"
936+ return f"{ dest if dest is not None else name } ={ opt } "
937+
938+ def topology (self ) -> dict [str , str ]:
939+ # Get the real device name (handles symlinks like /dev/mapper -> /dev/dm-X)
940+ real_path = os .path .realpath (self .path , strict = True )
941+ dev_name = os .path .basename (real_path )
942+ sys_base = Path (f'/sys/block/{ dev_name } ' )
943+
944+ attributes = {
945+ # 'alignment': ('alignment_offset', int),
946+ 'log-sec' : ('queue/logical_block_size' , int ),
947+ 'phy-sec' : ('queue/physical_block_size' , int ),
948+ 'min-io' : ('queue/minimum_io_size' , int ),
949+ 'opt-io' : ('queue/optimal_io_size' , int ),
950+ 'rota' : ('queue/rotational' , bool ),
951+ # 'wzeroes': ('queue/write_zeroes_max_bytes', int),
952+
953+ # 'disc-aln': ('discard_alignment', int),
954+ 'disc-gran' : ('queue/discard_granularity' , int ),
955+ # 'disc-max': ('queue/discard_max_bytes', int),
956+ # 'disc-zero': ('queue/discard_zeroes_data', bool),
957+ }
958+
959+ result = {}
960+ for key , (path , parser ) in attributes .items ():
961+ try :
962+ value = sys_base .joinpath (path ).read_text ().strip ()
963+ if parser is int :
964+ parsed = parser (value )
965+ if parsed <= 0 :
966+ continue
967+ result [key ] = value
968+ except FileNotFoundError :
969+ pass
970+ except ValueError :
971+ pass
972+ return result
888973
889974 # Validate name=path arguments from --disk and --blk-disk
890975 @classmethod
891976 def parse (cls , func : str , arg : str ) -> 'DiskArg' :
892- name , sep , fn = arg .partition ("=" )
977+ items = arg .split ("," )
978+
979+ namefile = items [0 ]
980+ extra = items [1 :]
981+
982+ name , sep , fn = namefile .partition ("=" )
893983 if not (name and sep and fn ):
894984 arg_fail (f"invalid argument to { func } : { arg } " )
895985 if "=" in fn or "," in fn :
896986 arg_fail (f"{ func } filenames cannot contain '=' or ',': { fn } " )
897987 if "=" in name or "," in name :
898988 arg_fail (f"{ func } device names cannot contain '=' or ',': { name } " )
899989
990+ opts = dict ()
991+ for i in extra :
992+ key , sep , value = i .partition ("=" )
993+ if not key :
994+ arg_fail (f"invalid argument to { func } : { arg } " )
995+ if sep :
996+ opts [key ] = value
997+ else :
998+ opts [key ] = "1"
999+
1000+ if "help" in opts :
1001+ print ("\n " .join ([
1002+ f"Possible { func } options:" ,
1003+ ] + [
1004+ "{:<20} {}" .format (f"{ key } =({ typ } )" , value )
1005+ for key , (typ , value ) in
1006+ DiskArg ._OPTS_HELP .items ()
1007+ ]))
1008+ sys .exit (0 )
1009+
9001010 return cls (
9011011 name = name ,
9021012 path = fn ,
1013+ opts = opts ,
9031014 )
9041015
9051016
@@ -1576,6 +1687,8 @@ def do_it() -> int:
15761687 if args .cpus :
15771688 qemuargs .extend (["-smp" , args .cpus ])
15781689
1690+ iothread_index = 0
1691+
15791692 if args .blk_disk :
15801693 for i , d in enumerate (args .blk_disk ):
15811694 driveid = f"blk-disk{ i } "
@@ -1585,55 +1698,157 @@ def do_it() -> int:
15851698 "if=none" ,
15861699 f"id={ driveid } " ,
15871700 f"file={ disk .path } " ,
1588- "format=raw" ,
15891701 ]
15901702 device_opts = [
15911703 arch .virtio_dev_type ("blk" ),
15921704 f"drive={ driveid } " ,
15931705 f"serial={ disk .name } " ,
15941706 ]
15951707
1708+ # we need those parameters multiple times
1709+ discard = disk .pop_opt ("discard" , parser = strtobool , default = None )
1710+ detect_zeroes = disk .pop_opt ("detect-zeroes" , parser = strtobool , default = None )
1711+ # we need this parameter both to transform other parameters and as itself later
1712+ # log_sec = disk.get_opt("log-sec", parser=int, default=512)
1713+
1714+ drive_opts .extend ([
1715+ disk .pop_opt_qemu ("format" , "raw" ),
1716+ disk .pop_opt_qemu ("cache" , None ),
1717+ disk .pop_opt_qemu ("aio" , None ),
1718+ f"discard={ "unmap" if discard else "ignore" } "
1719+ if discard is not None else None ,
1720+ f"detect-zeroes={ ("unmap" if discard else "on" ) if detect_zeroes else "off" } "
1721+ if detect_zeroes is not None else None ,
1722+ ])
1723+
1724+ device_opts .extend ([
1725+ f"discard={ "on" if discard else "off" } "
1726+ if discard is not None else None ,
1727+ disk .pop_opt_qemu ("disc-gran" , dest = "discard_granularity" ),
1728+ disk .pop_opt_qemu ("log-sec" , dest = "logical_block_size" ),
1729+ disk .pop_opt_qemu ("phy-sec" , dest = "physical_block_size" ),
1730+ # disk.pop_qemu("disc-max", dest="max-discard-sectors", parser=lambda arg: int(arg) / log_sec),
1731+ # disk.pop_qemu("wzeroes", dest="max-write-zeroes-sectors", parser=lambda arg: int(arg) / log_sec),
1732+ disk .pop_opt_qemu ("min-io" , dest = "min_io_size" ),
1733+ disk .pop_opt_qemu ("opt-io" , dest = "opt_io_size" ),
1734+ disk .pop_opt_qemu ("queues" , dest = "num-queues" ),
1735+ ])
1736+ # unused
1737+ disk .opts .pop ("rota" , None )
1738+
1739+ if disk .pop_opt ("iothread" , bool , False ):
1740+ iothreadid = f"iothread{ iothread_index } "
1741+ iothread_index += 1
1742+ qemuargs .extend ([
1743+ "-object" ,
1744+ f"iothread,id={ iothreadid } " ,
1745+ ])
1746+ device_opts .append (
1747+ f"iothread={ iothreadid } "
1748+ )
1749+
15961750 qemuargs .extend ([
15971751 "-drive" ,
15981752 "," .join (o for o in drive_opts if o is not None ),
15991753 "-device" ,
16001754 "," .join (o for o in device_opts if o is not None ),
16011755 ])
16021756
1603- if args .disk :
1604- qemuargs .extend (["-device" , "{},id=scsi" .format (arch .virtio_dev_type ("scsi" ))])
1757+ # any options that were not consumed are errors
1758+ if disk .opts :
1759+ raise ValueError (f"invalid --disk parameter: { d !r} \n (keys were not consumed: { disk .opts .keys ()} )" )
16051760
1761+ if args .disk :
16061762 for i , d in enumerate (args .disk ):
1763+ scsiid = f"scsi{ i } "
16071764 driveid = f"disk{ i } "
16081765 disk = DiskArg .parse ("--disk" , d )
16091766
16101767 # scsi-hd.device_id= is normally defaulted to scsi-hd.serial=,
16111768 # but it must not be longer than 20 characters
16121769 device_id = scsi_device_id (disk .name , 20 )
16131770
1771+ scsi_opts = [
1772+ arch .virtio_dev_type ("scsi" ),
1773+ f"id={ scsiid } " ,
1774+ ]
16141775 drive_opts = [
16151776 "if=none" ,
16161777 f"id={ driveid } " ,
16171778 f"file={ disk .path } " ,
1618- "format=raw" ,
16191779 ]
16201780 device_opts = [
16211781 "scsi-hd" ,
16221782 f"drive={ driveid } " ,
1783+ f"bus={ scsiid } .0" ,
16231784 "vendor=virtme" ,
16241785 "product=disk" ,
16251786 f"serial={ disk .name } " ,
16261787 f"device_id={ device_id } "
16271788 if device_id != disk .name else None ,
16281789 ]
16291790
1791+ # we need those parameters multiple times
1792+ discard = disk .pop_opt ("discard" , parser = strtobool , default = None )
1793+ detect_zeroes = disk .pop_opt ("detect-zeroes" , parser = strtobool , default = None )
1794+ # we need this parameter both to transform other parameters and as itself later
1795+ log_sec = disk .get_opt ("log-sec" )
1796+
1797+ drive_opts .extend ([
1798+ disk .pop_opt_qemu ("format" , "raw" ),
1799+ disk .pop_opt_qemu ("cache" , None ),
1800+ disk .pop_opt_qemu ("aio" , None ),
1801+ f"discard={ "unmap" if discard else "ignore" } "
1802+ if discard is not None else None ,
1803+ f"detect-zeroes={ ("unmap" if discard else "on" ) if detect_zeroes else "off" } "
1804+ if detect_zeroes is not None else None ,
1805+ ])
1806+
1807+ scsi_opts .extend ([
1808+ disk .pop_opt_qemu ("queues" , dest = "num-queues" ),
1809+ ])
1810+
1811+ device_opts .extend ([
1812+ disk .pop_opt_qemu ("disc-gran" , dest = "discard_granularity" ),
1813+ disk .pop_opt_qemu ("log-sec" , dest = "logical_block_size" ),
1814+ # convenience: QEMU does not automatically adjust physical_block_size
1815+ # to be not less than logical_block_size (it errors out instead), so we do it here
1816+ disk .pop_opt_qemu ("phy-sec" , dest = "physical_block_size" , default = log_sec ),
1817+ # disk.pop_qemu("disc-max", dest="max_unmap_size"),
1818+ # disk.pop_qemu("wzeroes", dest="???"),
1819+ disk .pop_opt_qemu ("min-io" , dest = "min_io_size" ),
1820+ disk .pop_opt_qemu ("opt-io" , dest = "opt_io_size" ),
1821+ # sic: set rotation_rate to "1" for non-rotating disks ("1" is a special value
1822+ # that means "non-rotating medium"), but set to "0" for rotating disks
1823+ # ("0" means "rotation rate not reported").
1824+ disk .pop_opt_qemu ("rota" , dest = "rotation_rate" ,
1825+ parser = lambda arg : "0" if strtobool (arg ) else "1" ),
1826+ ])
1827+
1828+ if disk .pop_opt ("iothread" , bool , False ):
1829+ iothreadid = f"iothread{ iothread_index } "
1830+ iothread_index += 1
1831+ qemuargs .extend ([
1832+ "-object" ,
1833+ f"iothread,id={ iothreadid } " ,
1834+ ])
1835+ scsi_opts .append (
1836+ f"iothread={ iothreadid } "
1837+ )
1838+
16301839 qemuargs .extend ([
16311840 "-drive" ,
16321841 "," .join (o for o in drive_opts if o is not None ),
16331842 "-device" ,
1843+ "," .join (o for o in scsi_opts if o is not None ),
1844+ "-device" ,
16341845 "," .join (o for o in device_opts if o is not None ),
16351846 ])
16361847
1848+ # any options that were not consumed are errors
1849+ if disk .opts :
1850+ raise ValueError (f"invalid --disk parameter: { d !r} \n (keys were not consumed: { disk .opts .keys ()} )" )
1851+
16371852 ret_path = None
16381853
16391854 def cleanup_script_retcode ():
0 commit comments