From 7f5b5209a1075e2d6bd48b3381694850b7dbba9c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Br=C3=BCsecke?= Date: Thu, 22 Oct 2026 19:38:18 +0300 Subject: [PATCH] Disks Service --- backend/disks/disk.py | 31 ++++++++++++++ backend/disks/disk_paths.py | 60 ++++++++++++++++++++++++++++ backend/disks/disk_utils.py | 30 ++++++++++++++ backend/disks/disks.py | 19 +++++++++ backend/disks/request_handler.py | 41 +++++++++++++++++++ docs/backend/disks.md | 59 +++++++++++++++++++++++++++ docs/backend/index.md | 1 + docs/index.md | 2 + infrastructure/systemd/disks.service | 12 ++++++ mkdocs.yml | 1 + 10 files changed, 256 insertions(+) create mode 100644 backend/disks/disk.py create mode 100644 backend/disks/disk_paths.py create mode 100644 backend/disks/disk_utils.py create mode 100644 backend/disks/disks.py create mode 100644 backend/disks/request_handler.py create mode 100644 docs/backend/disks.md create mode 100644 infrastructure/systemd/disks.service diff --git a/backend/disks/disk.py b/backend/disks/disk.py new file mode 100644 index 0000000..6a8f814 --- /dev/null +++ b/backend/disks/disk.py @@ -0,0 +1,31 @@ +import pathlib +import threading +import weakref + +import disk_paths +import disk_utils + +DICTIONARY_LOCK = threading.Lock() +DISK_LOCKS = weakref.WeakValueDictionary() + +def create_disk(request): + paths = disk_paths.DiskPaths(request) + workstation = request['workstation'] + + with disk_lock(paths): + if not paths.target_path.exists(): + disk_utils.create_os_disk(paths) + + if workstation['cpu'] == 'ARM' and not paths.nvram_target_path.exists(): + disk_utils.create_nvram_disk(paths) + +def delete_disk(request): + paths = disk_paths.DiskPaths(request) + + with disk_lock(paths): + paths.target_path.unlink(missing_ok=True) + paths.nvram_target_path.unlink(missing_ok=True) + +def disk_lock(paths): + with DICTIONARY_LOCK: + return DISK_LOCKS.setdefault(paths.target_filename, threading.Lock()) \ No newline at end of file diff --git a/backend/disks/disk_paths.py b/backend/disks/disk_paths.py new file mode 100644 index 0000000..daa05ea --- /dev/null +++ b/backend/disks/disk_paths.py @@ -0,0 +1,60 @@ +import pathlib + +class DiskPaths: + STORAGE_HOSTS_PATH = pathlib.Path("/var/opt/instant-workstation/storage-hosts") + + def __init__(self, request): + self.persistence_type = request['persistence_type'] + self.storage_host = request['storage_host'] + self.workstation = request['workstation'] + self.user_id = request['user_id'] + + @property + def nvram_filename(self): + return f"{self.workstation_key}_NVRAM.img" + + @property + def nvram_path(self): + return self.storage_host_path / 'firmware' / self.nvram_filename + + @property + def nvram_target_filename(self): + return f"{self.user_id}_{self.nvram_filename}" + + @property + def nvram_target_path(self): + return self.storage_host_path / self.persistence_type / self.nvram_target_filename + + @property + def staging_path(self): + return self.storage_host_path / 'staging' + + @property + def storage_host_path(self): + return self.STORAGE_HOSTS_PATH / self.storage_host + + @property + def target_filename(self): + return f"{self.user_id}_{self.template_filename}" + + @property + def target_path(self): + return self.storage_host_path / self.persistence_type / self.target_filename + + @property + def template_filename(self): + return f"{self.workstation_key}.qcow2" + + @property + def template_path(self): + return self.storage_host_path / 'template' / self.template_filename + + @property + def workstation_key(self): + return ( + f"{self.workstation['name']}_" + f"{self.workstation['version']}_" + f"{self.workstation['cpu']}_" + f"{self.workstation['bitness']}_" + f"{self.workstation['endianness']}" + ) \ No newline at end of file diff --git a/backend/disks/disk_utils.py b/backend/disks/disk_utils.py new file mode 100644 index 0000000..c2641d9 --- /dev/null +++ b/backend/disks/disk_utils.py @@ -0,0 +1,30 @@ +import pathlib +import shutil +import subprocess +import tempfile + +def create_os_disk(paths): + with tempfile.TemporaryDirectory(dir=paths.staging_path) as temporary_directory: + temporary_path = pathlib.Path(temporary_directory) / 'disk.qcow2' + + subprocess.run( + args=[ + 'qemu-img', 'create', + '--format', 'qcow2', + '--backing', paths.template_path, + '--backing-format', 'qcow2', + temporary_path + ], + check=True, + timeout=10 + ) + + temporary_path.replace(paths.target_path) + +def create_nvram_disk(paths): + with tempfile.TemporaryDirectory(dir=paths.staging_path) as temporary_directory: + temporary_path = pathlib.Path(temporary_directory) / 'NVRAM.img' + + shutil.copyfile(paths.nvram_path, temporary_path) + + temporary_path.replace(paths.nvram_target_path) \ No newline at end of file diff --git a/backend/disks/disks.py b/backend/disks/disks.py new file mode 100644 index 0000000..960e237 --- /dev/null +++ b/backend/disks/disks.py @@ -0,0 +1,19 @@ +import http.server +import signal + +import request_handler + +def shutdown(_signum, _frame): + raise SystemExit(0) + +SERVER_ADDRESS = ('', 50005) +SERVER = http.server.ThreadingHTTPServer(SERVER_ADDRESS, request_handler.RequestHandler) +SERVER.daemon_threads = False + +signal.signal(signal.SIGINT, shutdown) +signal.signal(signal.SIGTERM, shutdown) + +try: + SERVER.serve_forever() +finally: + SERVER.server_close() diff --git a/backend/disks/request_handler.py b/backend/disks/request_handler.py new file mode 100644 index 0000000..3111bc5 --- /dev/null +++ b/backend/disks/request_handler.py @@ -0,0 +1,41 @@ +import http.server +import json + +import disk + +class RequestHandler(http.server.BaseHTTPRequestHandler): + protocol_version = 'HTTP/1.1' + timeout = 10 + + def do_DELETE(self): + try: + disk.delete_disk(self.body()) + + except Exception as error: + self.log_error("Disk deletion failed: %s", error) + self.send_error(500) + return + + self.send_success() + + def do_POST(self): + try: + disk.create_disk(self.body()) + + except Exception as error: + self.log_error("Disk creation failed: %s", error) + self.send_error(500) + return + + self.send_success() + + def body(self): + content_length = int(self.headers.get('Content-Length', 0)) + return json.loads(self.rfile.read(content_length)) + + def send_success(self): + self.close_connection = True + self.send_response(200) + self.send_header('Content-Length', '0') + self.send_header('Connection', 'close') + self.end_headers() diff --git a/docs/backend/disks.md b/docs/backend/disks.md new file mode 100644 index 0000000..5b9fc46 --- /dev/null +++ b/docs/backend/disks.md @@ -0,0 +1,59 @@ +# Disks + +``` bash +cd /opt/instant-workstation/backend/disks +``` + +## Installation + +=== "Fedora" + + ``` bash + sudo dnf install python qemu-img + ``` + +=== "Ubuntu" + + ``` bash + sudo apt install python-is-python3 qemu-utils + ``` + +``` bash +sudo ln -s /opt/instant-workstation/infrastructure/systemd/disks.service /etc/systemd/system/. +``` + +``` bash +sudo systemctl daemon-reload +``` + +``` bash +sudo systemctl enable disks +``` + +``` bash +sudo systemctl start disks +``` + +``` bash +sudo systemctl status disks +``` + +## Updates + +### Service + +``` bash +git pull +``` + +``` bash +sudo systemctl daemon-reload +``` + +``` bash +sudo systemctl restart disks +``` + +``` bash +sudo systemctl status disks +``` \ No newline at end of file diff --git a/docs/backend/index.md b/docs/backend/index.md index 792fd3f..d2ceeea 100644 --- a/docs/backend/index.md +++ b/docs/backend/index.md @@ -2,4 +2,5 @@ | Service | Language | Toolchain | Host | Port | Last Updated | | --- | --- | --- | --- | --- | --- | +| [Disks](disks.md) | Python | CPython | 192.168.88.251 | 50005 | 2026-10-22 | | [Workstations](workstations.md) | JavaScript | Node.js | 192.168.88.228 | 50000 | 2026-09-02 | \ No newline at end of file diff --git a/docs/index.md b/docs/index.md index 1c6805a..c83dd9d 100644 --- a/docs/index.md +++ b/docs/index.md @@ -7,7 +7,9 @@ architecture-beta group backend(server)[Backend] group frontend(internet)[Frontend] + service disks(server)[Disks] in backend service workstations(server)[Workstations] in backend + service header(internet)[Header] in frontend header:R --> L:workstations diff --git a/infrastructure/systemd/disks.service b/infrastructure/systemd/disks.service new file mode 100644 index 0000000..cf4492c --- /dev/null +++ b/infrastructure/systemd/disks.service @@ -0,0 +1,12 @@ +[Install] +WantedBy=multi-user.target + +[Service] +ExecStart=python disks.py +Restart=on-failure +User=instant-workstation +WorkingDirectory=/opt/instant-workstation/backend/disks + +[Unit] +After=remote-fs.target +Wants=remote-fs.target \ No newline at end of file diff --git a/mkdocs.yml b/mkdocs.yml index 8c6f228..19b42ce 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -13,6 +13,7 @@ markdown_extensions: nav: - Backend: - backend/index.md + - Disks: backend/disks.md - Workstations: backend/workstations.md - Frontend: - frontend/index.md