44import logging
55import os
66import platform
7+ import shutil
78from collections .abc import AsyncGenerator
89from dataclasses import dataclass , field
910from functools import cached_property
2021from jumpstarter_driver_opendal .driver import FlasherInterface
2122from jumpstarter_driver_power .driver import PowerInterface , PowerReading
2223from jumpstarter_driver_pyserial .driver import PySerial
23- from pydantic import BaseModel , Field , validate_call
24+ from pydantic import BaseModel , ByteSize , Field , TypeAdapter , ValidationError , validate_call
2425from qemu .qmp import QMPClient
2526from qemu .qmp .protocol import ConnectError , Runstate
2627
@@ -169,6 +170,7 @@ async def on(self) -> None: # noqa: C901
169170 proc .check_returncode ()
170171 info = json .loads (proc .stdout )
171172 image_format = info .get ("format" , "raw" )
173+ current_virtual_size = info .get ("virtual-size" ) or root .stat ().st_size
172174 match image_format :
173175 case "raw" | "qcow2" | "qcow" | "vmdk" :
174176 image_driver = image_format
@@ -177,6 +179,34 @@ async def on(self) -> None: # noqa: C901
177179 except CalledProcessError :
178180 self .logger .warning ("unable to detect image format, assuming raw" )
179181 image_driver = "raw"
182+ current_virtual_size = root .stat ().st_size
183+
184+ # Resize disk if configured
185+ if self .parent .disk_size :
186+ requested = self .parent ._parse_size (self .parent .disk_size )
187+
188+ if requested < current_virtual_size :
189+ raise RuntimeError (
190+ f"Shrinking disk is not supported: current { ByteSize (current_virtual_size ).human_readable ()} , "
191+ f"requested { self .parent .disk_size } "
192+ )
193+
194+ available = shutil .disk_usage (root .parent ).free
195+ if requested > available :
196+ raise RuntimeError (
197+ f"Not enough disk space: need { ByteSize (requested ).human_readable ()} , "
198+ f"only { ByteSize (available ).human_readable ()} available"
199+ )
200+
201+ if requested > current_virtual_size :
202+ self .logger .info (f"Resizing disk to { ByteSize (requested ).human_readable ()} " )
203+ proc = await run_process (
204+ ["qemu-img" , "resize" , str (root ), str (requested )],
205+ stdout = PIPE ,
206+ stderr = PIPE ,
207+ )
208+ if proc .returncode != 0 :
209+ raise RuntimeError (f"Failed to resize disk: { proc .stderr .decode ()} " )
180210
181211 cmdline += [
182212 "-blockdev" ,
@@ -254,6 +284,7 @@ class Qemu(Driver):
254284
255285 smp : int = 2
256286 mem : str = "512M"
287+ disk_size : str | None = None # e.g., "20G" (resize disk before boot)
257288
258289 hostname : str = "demo"
259290 username : str = "jumpstarter"
@@ -372,3 +403,10 @@ def get_username(self) -> str:
372403 @validate_call (validate_return = True )
373404 def get_password (self ) -> str :
374405 return self .password
406+
407+ def _parse_size (self , size : str ) -> int :
408+ """Parse size string (e.g., '20G') to bytes."""
409+ try :
410+ return int (TypeAdapter (ByteSize ).validate_python (size + "iB" if size [- 1 ] in "kmgtKMGT" else size ))
411+ except (ValidationError , IndexError ):
412+ raise ValueError (f"Invalid size: '{ size } '. Use e.g. '20G', '512M', '2T'" ) from None
0 commit comments