1111from secrets import randbits
1212from subprocess import PIPE , CalledProcessError , Popen , TimeoutExpired
1313from tempfile import TemporaryDirectory
14+ from typing import Literal
1415
1516import yaml
1617from anyio import fail_after , run_process , sleep
1718from anyio .streams .file import FileReadStream , FileWriteStream
18- from jumpstarter_driver_network .driver import UnixNetwork , VsockNetwork
19+ from jumpstarter_driver_network .driver import TcpNetwork , UnixNetwork , VsockNetwork
1920from jumpstarter_driver_opendal .driver import FlasherInterface
2021from jumpstarter_driver_power .driver import PowerInterface , PowerReading
2122from jumpstarter_driver_pyserial .driver import PySerial
22- from pydantic import validate_call
23+ from pydantic import BaseModel , Field , validate_call
2324from qemu .qmp import QMPClient
2425from qemu .qmp .protocol import ConnectError , Runstate
2526
@@ -113,7 +114,13 @@ async def on(self) -> None: # noqa: C901
113114 "-serial" ,
114115 "pty" ,
115116 "-netdev" ,
116- "user,id=eth0" ,
117+ "," .join (
118+ ["user" , "id=eth0" ]
119+ + [
120+ "hostfwd={}:{}:{}-:{}" .format (v .protocol , v .hostaddr , v .hostport , v .guestport )
121+ for k , v in self .parent .hostfwd .items ()
122+ ]
123+ ),
117124 ]
118125
119126 devices = [
@@ -219,6 +226,13 @@ def close(self):
219226 self .off ()
220227
221228
229+ class Hostfwd (BaseModel ):
230+ protocol : Literal ["tcp" ] = "tcp"
231+ hostaddr : str = "127.0.0.1"
232+ hostport : int = Field (ge = 1 , le = 65535 )
233+ guestport : int = Field (ge = 1 , le = 65535 )
234+
235+
222236@dataclass (kw_only = True )
223237class Qemu (Driver ):
224238 arch : str = field (default_factory = platform .machine )
@@ -233,6 +247,8 @@ class Qemu(Driver):
233247
234248 default_partitions : dict [str , Path ] = field (default_factory = dict )
235249
250+ hostfwd : dict [str , Hostfwd ] = field (default_factory = dict )
251+
236252 _tmp_dir : TemporaryDirectory = field (init = False , default_factory = TemporaryDirectory )
237253
238254 @classmethod
@@ -243,6 +259,7 @@ def __post_init__(self):
243259 if hasattr (super (), "__post_init__" ):
244260 super ().__post_init__ ()
245261
262+ self .hostfwd = {k : Hostfwd .model_validate (v ) for k , v in self .hostfwd .items ()}
246263 self .default_partitions = {k : Path (v ) for k , v in self .default_partitions .items ()}
247264
248265 self .children ["power" ] = QemuPower (parent = self )
@@ -251,6 +268,11 @@ def __post_init__(self):
251268 self .children ["vnc" ] = UnixNetwork (path = self ._vnc )
252269 self .children ["ssh" ] = VsockNetwork (cid = self ._cid , port = 22 )
253270
271+ for k , v in self .hostfwd .items ():
272+ match v .protocol :
273+ case "tcp" :
274+ self .children [k ] = TcpNetwork (host = v .hostaddr , port = v .hostport )
275+
254276 @property
255277 def _pty (self ) -> str :
256278 return str (Path (self ._tmp_dir .name ) / "pty" )
0 commit comments