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 , validate_call
2425from qemu .qmp import QMPClient
2526from qemu .qmp .protocol import ConnectError , Runstate
2627
@@ -153,19 +154,6 @@ async def on(self) -> None: # noqa: C901
153154 ]
154155
155156 if root .exists ():
156- # Resize disk if configured
157- if self .parent .disk_size :
158- self .logger .info (f"Resizing disk to { self .parent .disk_size } " )
159- resize_proc = await run_process (
160- ["qemu-img" , "resize" , str (root ), self .parent .disk_size ],
161- stdout = PIPE ,
162- stderr = PIPE ,
163- )
164- try :
165- resize_proc .check_returncode ()
166- except CalledProcessError as e :
167- raise RuntimeError (f"Failed to resize disk: { resize_proc .stderr .decode ()} " ) from e
168-
169157 proc = await run_process (
170158 ["qemu-img" , "info" , "--output=json" , str (root )],
171159 stdout = PIPE ,
@@ -175,6 +163,7 @@ async def on(self) -> None: # noqa: C901
175163 proc .check_returncode ()
176164 info = json .loads (proc .stdout )
177165 image_format = info .get ("format" , "raw" )
166+ current_virtual_size = info .get ("virtual-size" ) or root .stat ().st_size
178167 match image_format :
179168 case "raw" | "qcow2" | "qcow" | "vmdk" :
180169 image_driver = image_format
@@ -183,6 +172,36 @@ async def on(self) -> None: # noqa: C901
183172 except CalledProcessError :
184173 self .logger .warning ("unable to detect image format, assuming raw" )
185174 image_driver = "raw"
175+ current_virtual_size = root .stat ().st_size
176+
177+ # Resize disk if configured
178+ if self .parent .disk_size :
179+ # Convert QEMU binary format (20G) to Pydantic format (20GiB)
180+ s = self .parent .disk_size
181+ requested = int (TypeAdapter (ByteSize ).validate_python (s + "iB" if s [- 1 ] in "kmgtKMGT" else s ))
182+
183+ if requested < current_virtual_size :
184+ raise RuntimeError (
185+ f"Shrinking disk is not supported: current { ByteSize (current_virtual_size ).human_readable ()} , "
186+ f"requested { self .parent .disk_size } "
187+ )
188+
189+ available = shutil .disk_usage (root .parent ).free
190+ if requested > available :
191+ raise RuntimeError (
192+ f"Not enough disk space: need { ByteSize (requested ).human_readable ()} , "
193+ f"only { ByteSize (available ).human_readable ()} available"
194+ )
195+
196+ if requested > current_virtual_size :
197+ self .logger .info (f"Resizing disk to { ByteSize (requested ).human_readable ()} " )
198+ proc = await run_process (
199+ ["qemu-img" , "resize" , str (root ), str (requested )],
200+ stdout = PIPE ,
201+ stderr = PIPE ,
202+ )
203+ if proc .returncode != 0 :
204+ raise RuntimeError (f"Failed to resize disk: { proc .stderr .decode ()} " )
186205
187206 cmdline += [
188207 "-blockdev" ,
0 commit comments