From e92994d2f3506010441d15e124a7de7c7431f84d Mon Sep 17 00:00:00 2001
From: "Alexander \"PapaTutuWawa" <papatutuwawa@polynom.me>
Date: Sat, 29 Mar 2025 15:47:29 +0100
Subject: [PATCH] Cleanup

---
 src/openec2/actions/describe_instances.py  |  5 ++--
 src/openec2/actions/run_instances.py       |  6 ++---
 src/openec2/actions/start_instances.py     |  5 ++--
 src/openec2/actions/stop_instances.py      |  5 ++--
 src/openec2/actions/terminate_instances.py |  5 ++--
 src/openec2/api/run_instances.py           |  1 -
 src/openec2/config.py                      | 28 ++++++++++++++++++++--
 src/openec2/libvirt.py                     | 24 +++++++++++++++++++
 src/openec2/utils/cloudinit.py             | 19 ---------------
 9 files changed, 60 insertions(+), 38 deletions(-)
 create mode 100644 src/openec2/libvirt.py
 delete mode 100644 src/openec2/utils/cloudinit.py

diff --git a/src/openec2/actions/describe_instances.py b/src/openec2/actions/describe_instances.py
index 9d18421..0684230 100644
--- a/src/openec2/actions/describe_instances.py
+++ b/src/openec2/actions/describe_instances.py
@@ -3,8 +3,8 @@ import uuid
 from fastapi import Response
 from fastapi.datastructures import QueryParams
 from sqlmodel import select
-import libvirt
 
+from openec2.libvirt import LibvirtSingleton
 from openec2.api.describe_instances import InstanceDescription, DescribeInstancesResponse, DescribeInstancesResponseReservationSet, describe_instance
 from openec2.api.shared import InstanceState
 from openec2.config import OpenEC2Config
@@ -17,7 +17,7 @@ def describe_instances(
     db: DatabaseDep,
 ):
     response_items: list[InstanceDescription] = []
-    conn = libvirt.open("qemu:///system")
+    conn = LibvirtSingleton.of().connection
     for instance in db.exec(select(Instance)).all():
         dom = conn.lookupByName(instance.id)
         running = dom.isActive()
@@ -25,7 +25,6 @@ def describe_instances(
         response_items.append(
             describe_instance(instance, dom),
         )
-    conn.close()
 
     return Response(
         DescribeInstancesResponse(
diff --git a/src/openec2/actions/run_instances.py b/src/openec2/actions/run_instances.py
index a482fd5..6f2f531 100644
--- a/src/openec2/actions/run_instances.py
+++ b/src/openec2/actions/run_instances.py
@@ -6,8 +6,8 @@ import os
 
 from fastapi.datastructures import QueryParams
 from sqlmodel import select
-import libvirt
 
+from openec2.libvirt import LibvirtSingleton
 from openec2.config import OpenEC2Config
 from openec2.db import DatabaseDep
 from openec2.db.instance import Instance
@@ -15,7 +15,6 @@ from openec2.db.image import AMI
 from openec2.api.run_instances import RunInstanceResponse, RunInstanceInstanceSet
 from openec2.api.describe_instances import describe_instance
 from openec2.utils.array import parse_array_objects
-from openec2.utils.cloudinit import create_cloudinit_image
 
 def create_libvirt_domain(
     name: str,
@@ -121,7 +120,7 @@ def run_instances(
     db.commit()
     print("Inserted new instance")
 
-    conn = libvirt.open("qemu:///system")
+    conn = LibvirtSingleton.of().connection
     domain = conn.defineXML(
         create_libvirt_domain(
             instance_id,
@@ -133,7 +132,6 @@ def run_instances(
     )
     domain.create()
     description = describe_instance(instance, domain)
-    conn.close()
 
     return RunInstanceResponse(
         request_id=uuid.uuid4().hex,
diff --git a/src/openec2/actions/start_instances.py b/src/openec2/actions/start_instances.py
index 0c927d3..ca6a39f 100644
--- a/src/openec2/actions/start_instances.py
+++ b/src/openec2/actions/start_instances.py
@@ -3,8 +3,8 @@ import uuid
 from fastapi import HTTPException
 from fastapi.datastructures import QueryParams
 from sqlmodel import select
-import libvirt
 
+from openec2.libvirt import LibvirtSingleton
 from openec2.config import OpenEC2Config
 from openec2.db import DatabaseDep
 from openec2.db.instance import Instance
@@ -17,13 +17,13 @@ def start_instances(
     config: OpenEC2Config,
     db: DatabaseDep,
 ):
+    conn = LibvirtSingleton.of().connection
     instances: list[StartInstancesResponseInstancesSetInstance] = []
     for instance_id in parse_array_plain("InstanceId", params):
         instance = db.exec(select(Instance).where(Instance.id == instance_id)).first()
         if instance is None:
             raise HTTPException(status_code=404, detail="Unknown instance")
 
-        conn = libvirt.open("qemu:///system")
         dom = conn.lookupByName(instance_id)
 
         running = dom.isActive()
@@ -38,7 +38,6 @@ def start_instances(
             code=16 if running else 80,
             name="running" if running else "stopped",
         )
-        conn.close()
 
         instances.append(
             StartInstancesResponseInstancesSetInstance(
diff --git a/src/openec2/actions/stop_instances.py b/src/openec2/actions/stop_instances.py
index 61bd5eb..61e4f38 100644
--- a/src/openec2/actions/stop_instances.py
+++ b/src/openec2/actions/stop_instances.py
@@ -3,8 +3,8 @@ import uuid
 from fastapi import HTTPException
 from fastapi.datastructures import QueryParams
 from sqlmodel import select
-import libvirt
 
+from openec2.libvirt import LibvirtSingleton
 from openec2.config import OpenEC2Config
 from openec2.db import DatabaseDep
 from openec2.db.instance import Instance
@@ -18,13 +18,13 @@ def stop_instances(
     config: OpenEC2Config,
     db: DatabaseDep,
 ):
+    conn = LibvirtSingleton.of().connection
     instances: list[StopInstancesResponseInstancesSetInstance] = []
     for instance_id in parse_array_plain("InstanceId", params):
         instance = db.exec(select(Instance).where(Instance.id == instance_id)).first()
         if instance is None:
             raise HTTPException(status_code=404, detail="Unknown instance")
 
-        conn = libvirt.open("qemu:///system")
         dom = conn.lookupByName(instance_id)
         running = dom.isActive()
         prev_state = InstanceState(
@@ -38,7 +38,6 @@ def stop_instances(
             code=16 if running else 80,
             name="running" if running else "stopped",
         )
-        conn.close()
 
         instances.append(
             StopInstancesResponseInstancesSetInstance(
diff --git a/src/openec2/actions/terminate_instances.py b/src/openec2/actions/terminate_instances.py
index 4dd5efe..4eb15c5 100644
--- a/src/openec2/actions/terminate_instances.py
+++ b/src/openec2/actions/terminate_instances.py
@@ -3,8 +3,8 @@ import logging
 from fastapi import HTTPException
 from fastapi.datastructures import QueryParams
 from sqlmodel import select
-import libvirt
 
+from openec2.libvirt import LibvirtSingleton
 from openec2.config import OpenEC2Config
 from openec2.db import DatabaseDep
 from openec2.db.instance import Instance
@@ -18,17 +18,16 @@ def terminate_instances(
     config: OpenEC2Config,
     db: DatabaseDep,
 ):
+    conn = LibvirtSingleton.of().connection
     for instance_id in parse_array_plain("InstanceId", params):
         instance = db.exec(select(Instance).where(Instance.id == instance_id)).first()
         if instance is None:
             raise HTTPException(status_code=404, detail="Unknown instance")
 
-        conn = libvirt.open("qemu:///system")
         dom = conn.lookupByName(instance_id)
         if dom.isActive():
             dom.shutdown()
         dom.undefine()
-        conn.close()
 
         # Delete files
         logger.debug(f"Removing {config.instances.location / instance_id}")
diff --git a/src/openec2/api/run_instances.py b/src/openec2/api/run_instances.py
index 6ac9cae..1eed661 100644
--- a/src/openec2/api/run_instances.py
+++ b/src/openec2/api/run_instances.py
@@ -1,5 +1,4 @@
 from pydantic_xml import BaseXmlModel, element
-import libvirt
 
 from openec2.db.instance import Instance
 from openec2.api.shared import InstanceState
diff --git a/src/openec2/config.py b/src/openec2/config.py
index 21a37ba..4415a14 100644
--- a/src/openec2/config.py
+++ b/src/openec2/config.py
@@ -13,12 +13,16 @@ class _OpenEC2InstanceConfig(BaseModel):
 
     types: dict[str, _OpenEC2InstanceType]
 
+class _OpenEC2LibvirtConfig(BaseModel):
+    connection: str
+
 class _OpenEC2Config(BaseModel):
     images: Path
     seed: Path
     instances: _OpenEC2InstanceConfig
+    libvirt: _OpenEC2LibvirtConfig
 
-def get_config() -> _OpenEC2Config:
+def _get_config() -> _OpenEC2Config:
     # TODO: Read from disk
     return _OpenEC2Config(
         images=Path("/home/alexander/openec2/images"),
@@ -33,6 +37,26 @@ def get_config() -> _OpenEC2Config:
                 ),
             },
         ),
+        libvirt=_OpenEC2LibvirtConfig(
+            connection="qemu:///system"
+        ),
     )
 
-OpenEC2Config = Annotated[_OpenEC2Config, Depends(get_config)]
+class ConfigSingleton:
+    __instance: "ConfigSingleton | None" = None
+
+    config: _OpenEC2Config
+
+    def __init__(self):
+        self.config = _get_config()
+
+    def get_config(self) -> _OpenEC2Config:
+        return self.config
+
+    @staticmethod
+    def of() -> "ConfigSingleton":
+        if ConfigSingleton.__instance is None:
+            ConfigSingleton.__instance = ConfigSingleton()
+        return ConfigSingleton.__instance
+
+OpenEC2Config = Annotated[_OpenEC2Config, Depends(ConfigSingleton.of().get_config)]
diff --git a/src/openec2/libvirt.py b/src/openec2/libvirt.py
new file mode 100644
index 0000000..649b529
--- /dev/null
+++ b/src/openec2/libvirt.py
@@ -0,0 +1,24 @@
+import libvirt
+
+from openec2.config import ConfigSingleton
+
+
+class LibvirtSingleton:
+    __instance: "LibvirtSingleton | None" = None
+
+    # The connection to libvirt
+    connection: libvirt.virConnect
+
+    def __init__(self):
+        self.connection = libvirt.open(
+            ConfigSingleton.of().config.libvirt.connection,
+        )
+
+    def __del__(self):
+        self.connection.close()
+
+    @staticmethod
+    def of() -> "LibvirtSingleton":
+        if LibvirtSingleton.__instance is None:
+            LibvirtSingleton.__instance = LibvirtSingleton()
+        return LibvirtSingleton.__instance
diff --git a/src/openec2/utils/cloudinit.py b/src/openec2/utils/cloudinit.py
deleted file mode 100644
index 8aa2672..0000000
--- a/src/openec2/utils/cloudinit.py
+++ /dev/null
@@ -1,19 +0,0 @@
-import os
-from pathlib import Path
-import tempfile
-
-def create_cloudinit_image(
-    seed_file: Path,
-    instance_id: str,
-    user_data: str,
-):
-    with tempfile.TemporaryDirectory() as _tmp:
-        tmp = Path(_tmp)
-        with (tmp / "meta-data").open("w") as f:
-            f.write(f"instance-id: {instance_id}\nlocal-hostname: {instance_id}")
-        with (tmp / "user-data").open("w") as f:
-            f.write(user_data)
-
-        os.system(f"truncate --size 2M {seed_file}")
-        os.system(f"mkfs.vfat -n cidata {seed_file}")
-        os.system(f"mcopy -oi {seed_file} {tmp / "meta-data"} {tmp / "user-data"} ::")