Menuconfig Implementation and auto-qemu refactoring (#44)
[lunaix-os.git] / lunaix-os / scripts / qemu.py
index f2808e53d2911e44d25e3bed3d8956ecf1066943..f12881afc8360c9a5faaefb1d9f58541fd798702 100755 (executable)
@@ -3,8 +3,10 @@
 import subprocess, time, os, re, argparse, json
 from pathlib import PurePosixPath
 import logging
 import subprocess, time, os, re, argparse, json
 from pathlib import PurePosixPath
 import logging
+import uuid
 
 logger = logging.getLogger("auto_qemu")
 
 logger = logging.getLogger("auto_qemu")
+logging.basicConfig(level=logging.INFO)
 
 g_lookup = {}
 
 
 g_lookup = {}
 
@@ -51,12 +53,89 @@ def get_config(opt, path, default=None, required=False):
 def join_attrs(attrs):
     return ",".join(attrs)
 
 def join_attrs(attrs):
     return ",".join(attrs)
 
-def parse_protocol(opt):
-    protocol = get_config(opt, "protocol", "telnet")
-    addr     = get_config(opt, "addr", ":12345")
-    logfile  = get_config(opt, "logfile")
+def get_uniq():
+    return uuid.uuid4().hex[:8]
 
 
-    return (f"{protocol}:{addr}", logfile)
+def map_bool(b):
+    return "on" if b else "off"
+    
+
+
+#################################
+# IO Backend Definitions  
+#
+
+class IOBackend:
+    def __init__(self, opt, id_prefix="io") -> None:
+        self._type = get_config(opt, "type", required=True)
+        self._logfile = get_config(opt, "logfile")
+        self._id = f"{id_prefix}.{get_uniq()}"
+
+    def get_options(self):
+        opts = []
+        if self._logfile:
+            opts.append(f"logfile={self._logfile}")
+        return opts
+    
+    def to_cmdline(self):
+        return join_attrs([
+            self._type, f"id={self._id}", *self.get_options()
+        ])
+    
+    def name(self):
+        return self._id
+
+class FileIOBackend(IOBackend):
+    def __init__(self, opt) -> None:
+        super().__init__(opt)
+        self.__path = get_config(opt, "path", required=True)
+
+    def get_options(self):
+        opts = [
+            f"path={self.__path}"
+        ]
+        return opts + super().get_options()
+
+class SocketIOBackend(IOBackend):
+    def __init__(self, opt) -> None:
+        super().__init__(opt)
+        self.__protocol = self._type
+        self._type = "socket"
+        self.__host = get_config(opt, "host", default="localhost")
+        self.__port = get_config(opt, "port", required=True)
+        self.__server = bool(get_config(opt, "server", True))
+        self.__wait = bool(get_config(opt, "wait", True))
+
+    def get_options(self):
+        opts = [
+            f"host={self.__host}",
+            f"port={self.__port}",
+            f"server={map_bool(self.__server)}",
+            f"wait={map_bool(self.__wait)}",
+        ]
+        if self.__protocol == "telnet":
+            opts.append("telnet=on")
+        if self.__protocol == "ws":
+            opts.append("websocket=on")
+        return opts + super().get_options()
+    
+def select_backend(opt):
+    bopt = get_config(opt, "io", required=True)
+    backend_type = get_config(bopt, "type", required=True)
+    
+    if backend_type in ["telnet", "ws", "tcp"]:
+        return SocketIOBackend(bopt)
+    
+    if backend_type in ["file", "pipe", "serial", "parallel"]:
+        return FileIOBackend(bopt)
+    
+    return IOBackend(bopt)
+
+
+
+#################################
+# QEMU Emulated Device Definitions
+#
 
 class QEMUPeripherals:
     def __init__(self, name, opt) -> None:
 
 class QEMUPeripherals:
     def __init__(self, name, opt) -> None:
@@ -66,39 +145,71 @@ class QEMUPeripherals:
     def get_qemu_opts(self) -> list:
         pass
 
     def get_qemu_opts(self) -> list:
         pass
 
-class BasicSerialDevice(QEMUPeripherals):
+class ISASerialDevice(QEMUPeripherals):
     def __init__(self, opt) -> None:
     def __init__(self, opt) -> None:
-        super().__init__("serial", opt)
+        super().__init__("isa-serial", opt)
 
     def get_qemu_opts(self):
 
     def get_qemu_opts(self):
-        link, logfile = parse_protocol(self._opt)
+        chardev = select_backend(self._opt)
+
+        cmds = [ 
+            "isa-serial", 
+           f"id=com.{get_uniq()}", 
+           f"chardev={chardev.name()}" 
+        ]
 
 
-        cmds = [ link, "server", "nowait" ]
-        if logfile:
-            cmds.append(f"logfile={logfile}")
-        return [ "-serial", join_attrs(cmds) ]
+        return [ 
+            "-chardev", chardev.to_cmdline(),
+            "-device", join_attrs(cmds)
+         ]
     
 class PCISerialDevice(QEMUPeripherals):
     def __init__(self, opt) -> None:
         super().__init__("pci-serial", opt)
 
     def get_qemu_opts(self):
     
 class PCISerialDevice(QEMUPeripherals):
     def __init__(self, opt) -> None:
         super().__init__("pci-serial", opt)
 
     def get_qemu_opts(self):
-        uniq = hex(self.__hash__())[2:]
-        name = f"chrdev.{uniq}"
-        cmds = [ "pci-serial", f"id=uart.{uniq}", f"chardev={name}" ]
-        chrdev = [ "file", f"id={name}" ]
-        
-        logfile = get_config(self._opt, "logfile", required=True)
-        chrdev.append(f"path={logfile}")
+        chardev = select_backend(self._opt)
+
+        cmds = [ 
+            "pci-serial", 
+           f"id=uart.{get_uniq()}", 
+           f"chardev={chardev.name()}" 
+        ]
 
         return [ 
 
         return [ 
-            "-chardev", join_attrs(chrdev),
+            "-chardev", chardev.to_cmdline(),
             "-device", join_attrs(cmds)
          ]
     
 class AHCIBus(QEMUPeripherals):
     def __init__(self, opt) -> None:
         super().__init__("ahci", opt)
             "-device", join_attrs(cmds)
          ]
     
 class AHCIBus(QEMUPeripherals):
     def __init__(self, opt) -> None:
         super().__init__("ahci", opt)
+        
+    def __create_disklet(self, index, bus, opt):
+        d_type = get_config(opt, "type",   default="ide-hd")
+        d_img  = get_config(opt, "img",    required=True)
+        d_ro   = get_config(opt, "ro",     default=False)
+        d_fmt  = get_config(opt, "format", default="raw")
+        d_id   = f"disk_{index}"
+
+        if not os.path.exists(d_img):
+            logger.warning(f"AHCI bus: {d_img} not exists, skipped")
+            return []
+        
+        return [
+            "-drive", join_attrs([
+                f"id={d_id}",
+                f"file={d_img}",
+                f"readonly={'on' if d_ro else 'off'}",
+                f"if=none",
+                f"format={d_fmt}"
+            ]),
+            "-device", join_attrs([
+                d_type,
+                f"drive={d_id}",
+                f"bus={bus}.{index}"
+            ])
+        ]
 
     def get_qemu_opts(self):
         opt = self._opt
 
     def get_qemu_opts(self):
         opt = self._opt
@@ -106,31 +217,9 @@ class AHCIBus(QEMUPeripherals):
         name = name.strip().replace(" ", "_")
         cmds = [ "-device", f"ahci,id={name}" ]
 
         name = name.strip().replace(" ", "_")
         cmds = [ "-device", f"ahci,id={name}" ]
 
-        for i, disk in enumerate(get_config(opt, "disks", default=[])):
-            d_type = get_config(disk, "type",   default="ide-hd")
-            d_img  = get_config(disk, "img",    required=True)
-            d_ro   = get_config(disk, "ro",     default=False)
-            d_fmt  = get_config(disk, "format", default="raw")
-            d_id   = f"disk_{i}"
-
-            if not os.path.exists(d_img):
-                logger.warning(f"AHCI bus: {d_img} not exists, disk skipped")
-                continue
-            
-            cmds += [
-                "-drive", join_attrs([
-                    f"id={d_id},"
-                    f"file={d_img}",
-                    f"readonly={'on' if d_ro else 'off'}",
-                    f"if=none",
-                    f"format={d_fmt}"
-                ]),
-                "-device", join_attrs([
-                    d_type,
-                    f"drive={d_id}",
-                    f"bus={name}.{i}"
-                ])
-            ]
+        disklets = get_config(opt, "disks", default=[])
+        for i, disk in enumerate(disklets):
+            cmds += self.__create_disklet(i, name, disk)
         
         return cmds
     
         
         return cmds
     
@@ -148,36 +237,49 @@ class QEMUMonitor(QEMUPeripherals):
         super().__init__("monitor", opt)
 
     def get_qemu_opts(self):
         super().__init__("monitor", opt)
 
     def get_qemu_opts(self):
-        link, logfile = parse_protocol(self._opt)
+        
+        chardev = select_backend(self._opt)
 
         return [
 
         return [
-            "-monitor", join_attrs([
-                link,
-                "server",
-                "nowait", 
-                f"logfile={logfile}"
+            "-chardev", chardev.to_cmdline(),
+            "-mon", join_attrs([
+                chardev.name(),
+                "mode=readline",
             ])
         ]
 
             ])
         ]
 
-class QEMUExec:
-    devices = {
-        "basic_serial": BasicSerialDevice,
+class QEMUDevices:
+    __devs = {
+        "isa-serial": ISASerialDevice,
         "ahci": AHCIBus,
         "rtc": RTCDevice,
         "hmp": QEMUMonitor,
         "pci-serial": PCISerialDevice
     }
 
         "ahci": AHCIBus,
         "rtc": RTCDevice,
         "hmp": QEMUMonitor,
         "pci-serial": PCISerialDevice
     }
 
+    @staticmethod
+    def get(name):
+        if name not in QEMUDevices.__devs:
+            raise Exception(f"device class: {name} is not defined")
+        return QEMUDevices.__devs[name]
+
+
+
+#################################
+# QEMU Machine Definitions
+#
+
+class QEMUExec:
+    
+
     def __init__(self, options) -> None:
         self._opt = options
         self._devices = []
 
         for dev in get_config(options, "devices", default=[]):
             dev_class = get_config(dev, "class")
     def __init__(self, options) -> None:
         self._opt = options
         self._devices = []
 
         for dev in get_config(options, "devices", default=[]):
             dev_class = get_config(dev, "class")
-            if dev_class not in QEMUExec.devices:
-                raise Exception(f"device class: {dev_class} is not defined")
-            
-            self._devices.append(QEMUExec.devices[dev_class](dev))
+            device = QEMUDevices.get(dev_class)
+            self._devices.append(device(dev))
     
     def get_qemu_exec_name(self):
         pass
     
     def get_qemu_exec_name(self):
         pass
@@ -217,7 +319,7 @@ class QEMUExec:
     def add_peripheral(self, peripheral):
         self._devices.append(peripheral)
 
     def add_peripheral(self, peripheral):
         self._devices.append(peripheral)
 
-    def start(self, qemu_dir_override=""):
+    def start(self, qemu_dir_override="", dryrun=False, extras=[]):
         qemu_path = self.get_qemu_exec_name()
         qemu_path = os.path.join(qemu_dir_override, qemu_path)
         cmds = [
         qemu_path = self.get_qemu_exec_name()
         qemu_path = os.path.join(qemu_dir_override, qemu_path)
         cmds = [
@@ -230,7 +332,12 @@ class QEMUExec:
         for dev in self._devices:
             cmds += dev.get_qemu_opts()
 
         for dev in self._devices:
             cmds += dev.get_qemu_opts()
 
+        cmds += extras
         print(" ".join(cmds), "\n")
         print(" ".join(cmds), "\n")
+
+        if dryrun:
+            logger.info("[DRY RUN] QEMU not invoked")
+            return
         
         handle = subprocess.Popen(cmds)
         
         
         handle = subprocess.Popen(cmds)
         
@@ -257,9 +364,10 @@ def main():
 
     arg.add_argument("config_file")
     arg.add_argument("--qemu-dir", default="")
 
     arg.add_argument("config_file")
     arg.add_argument("--qemu-dir", default="")
+    arg.add_argument("--dry", action='store_true')
     arg.add_argument("-v", "--values", action='append', default=[])
 
     arg.add_argument("-v", "--values", action='append', default=[])
 
-    arg_opt = arg.parse_args()
+    arg_opt, extras = arg.parse_known_args()
 
     opts = {}
     with open(arg_opt.config_file, 'r') as f:
 
     opts = {}
     with open(arg_opt.config_file, 'r') as f:
@@ -277,7 +385,8 @@ def main():
     else:
         raise Exception(f"undefined arch: {arch}")
     
     else:
         raise Exception(f"undefined arch: {arch}")
     
-    q.start(arg_opt.qemu_dir)
+    extras = [ x for x in extras if x != '--']
+    q.start(arg_opt.qemu_dir, arg_opt.dry, extras)
 
 if __name__ == "__main__":
     main()
\ No newline at end of file
 
 if __name__ == "__main__":
     main()
\ No newline at end of file