diff --git a/README.md b/README.md new file mode 100644 index 00000000..0e3278b1 --- /dev/null +++ b/README.md @@ -0,0 +1,4 @@ + +SD-Karten-Images bauen: + +- `nix build .#nixosConfigurations..config.formats.sd-card-image` diff --git a/controller/app/models/module.py b/controller/app/models/module.py index 765f8ec7..59c57427 100644 --- a/controller/app/models/module.py +++ b/controller/app/models/module.py @@ -48,12 +48,17 @@ def write_nix( filename = f"{self.type}.nix" with open(path / filename, "w+") as f: - f.write("{ pkgs, lib, ... }:\n") + f.write("{ pkgs, lib, inputs, ... }:\n") f.write("{\n") - for attr, value in module_settings.settings.items(): - my_attr = getattr(self, attr) - assert isinstance(my_attr, models.Setting) - my_attr.write_nix(f, value, priority) + self.write_nix_settings(f, module_settings, priority) f.write("}\n") + + def write_nix_settings( + self, f, module_settings: models.ModuleSettings, priority: int + ): + for attr, value in module_settings.settings.items(): + my_attr = getattr(self, attr) + assert isinstance(my_attr, models.Setting) + my_attr.write_nix(f, value, priority) diff --git a/controller/app/models/modules/__init__.py b/controller/app/models/modules/__init__.py index e41b7bbf..0b6fc7f8 100644 --- a/controller/app/models/modules/__init__.py +++ b/controller/app/models/modules/__init__.py @@ -2,10 +2,12 @@ from .minio import Minio from .node_red import NodeRed from .thymis import ThymisController, ThymisDevice +from .whatever import WhateverModule ALL_MODULES = [ Minio(), NodeRed(), ThymisController(), ThymisDevice(), + WhateverModule(), ] diff --git a/controller/app/models/modules/thymis.py b/controller/app/models/modules/thymis.py index 72a97045..e7881aad 100644 --- a/controller/app/models/modules/thymis.py +++ b/controller/app/models/modules/thymis.py @@ -42,32 +42,31 @@ class ThymisDevice(Module): example="", ) - def write_nix( - self, - path: os.PathLike, - module_settings: models.ModuleSettings, - priority: int, - ): - filename = f"{self.type}.nix" + wifi_ssid: Setting = Setting( + name="thymis.config.wifi-ssid", + type="string", + default="", + description="The wifi ssid of the thymis device.", + example="", + ) + wifi_password: Setting = Setting( + name="thymis.config.wifi-password", + type="string", + default="", + description="The wifi password of the thymis device.", + example="", + ) + + def write_nix_settings(self, f, module_settings: ModuleSettings, priority: int): device_type = ( module_settings.settings["device_type"].value if "device_type" in module_settings.settings else self.device_type.default ) - with open(path / filename, "w+") as f: - f.write("{ inputs, pkgs, lib, ... }:\n") - f.write("{\n") - - f.write(f" imports = [\n") - # imports inputs.thymis.nixosModules.thymis-device- - f.write(f" inputs.thymis.nixosModules.thymis-device-{device_type}\n") - f.write(f" ];\n") - - for attr, value in module_settings.settings.items(): - my_attr = getattr(self, attr) - assert isinstance(my_attr, models.Setting) - my_attr.write_nix(f, value, priority) + f.write(f" imports = [\n") + f.write(f" inputs.thymis.nixosModules.thymis-device-{device_type}\n") + f.write(f" ];\n") - f.write("}\n") + return super().write_nix_settings(f, module_settings, priority) diff --git a/controller/app/models/modules/whatever.py b/controller/app/models/modules/whatever.py new file mode 100644 index 00000000..66d4b703 --- /dev/null +++ b/controller/app/models/modules/whatever.py @@ -0,0 +1,21 @@ +from app.models import Module, Setting +from app.models.setting import ModuleSettings + + +class WhateverModule(Module): + settings: Setting = Setting( + name="freeform.settings", + type="textarea", + default="", + description="The settings for the freeform module.", + example="", + ) + + def write_nix_settings(self, f, module_settings: ModuleSettings, priority: int): + settings = ( + module_settings.settings["settings"].value + if "settings" in module_settings.settings + else self.settings.default + ) + + f.write(settings) diff --git a/controller/app/models/state.py b/controller/app/models/state.py index 5fe0d926..706f3bac 100644 --- a/controller/app/models/state.py +++ b/controller/app/models/state.py @@ -7,6 +7,7 @@ from git import Repo import pathlib from app.models.modules import ALL_MODULES +import subprocess REPO_PATH = os.getenv("REPO_PATH") @@ -72,6 +73,8 @@ def write_nix(self, path: os.PathLike): assert device.hostname, "hostname cannot be empty" device_path = path / "hosts" / device.hostname device_path.mkdir(exist_ok=True) + # create a empty .gitignore file + os.mknod(device_path / ".gitignore") # write its modules for module_settings in device.modules: # module holds settings right now. @@ -85,6 +88,8 @@ def write_nix(self, path: os.PathLike): for tag in self.tags: tag_path = path / "tags" / tag.name tag_path.mkdir(exist_ok=True) + # create a empty .gitignore file + os.mknod(tag_path / ".gitignore") # write its modules for module_settings in tag.modules: # module holds settings right now. @@ -94,6 +99,9 @@ def write_nix(self, path: os.PathLike): if m.type == module_settings.type ) module.write_nix(tag_path, module_settings, tag.priority) + # run git add + repo = Repo.init(self.repo_dir()) + repo.git.add(".") def available_modules(self): return ALL_MODULES @@ -170,6 +178,30 @@ async def build_nix(self, q: List): else: self.update_status("success") + async def build_image_path(self, q: List, hostname: str): + await terminate_other_procs() + self.__q = q + self.__stdout = bytearray() + self.__stderr = bytearray() + self.update_status("started building") + + cmd = f"nix build {self.repo_dir()}#nixosConfigurations.{hostname}.config.formats.sd-card-image --out-link /tmp/thymis-devices.{hostname}" + + proc = await asyncio.create_subprocess_shell( + cmd, + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE, + ) + + other_procs.append(proc) + asyncio.create_task(self.stream_reader(proc.stdout, self.__stdout)) + asyncio.create_task(self.stream_reader(proc.stderr, self.__stderr)) + + r = await proc.wait() + if proc.returncode != 0: + raise Exception(f"failed to build image for {hostname}") + return f"/tmp/thymis-devices.{hostname}" + async def deploy(self, q: List): await terminate_other_procs() self.__q = q diff --git a/controller/app/routes/router.py b/controller/app/routes/router.py index 8bfc772e..75c5b0e2 100644 --- a/controller/app/routes/router.py +++ b/controller/app/routes/router.py @@ -3,10 +3,11 @@ from typing import List from app.models.state import State from fastapi import APIRouter, Depends, Request, BackgroundTasks, WebSocket +from fastapi.responses import FileResponse from ..dependencies import get_or_init_state from app import models from app.crud import state - +import subprocess router = APIRouter() @@ -56,7 +57,9 @@ async def build_status(websocket: WebSocket): @router.post("/action/deploy") def deploy( - summary: str, background_tasks: BackgroundTasks, state: State = Depends(get_or_init_state) + summary: str, + background_tasks: BackgroundTasks, + state: State = Depends(get_or_init_state), ): state.commit(summary) @@ -64,3 +67,19 @@ def deploy( background_tasks.add_task(state.deploy, last_build_status) return {"message": "nix deploy started"} + + +@router.get("/action/build-download-image") +async def build_download_image( + hostname: str, + background_tasks: BackgroundTasks, + state: State = Depends(get_or_init_state), +): + image_path = await state.build_image_path(q=last_build_status, hostname=hostname) + + # return the image bytes + return FileResponse( + image_path, + media_type="application/octet-stream", + filename=f"thymis-{hostname}.img", + ) diff --git a/frontend/src/lib/buildstatus.ts b/frontend/src/lib/buildstatus.ts index fd726cf8..388e3346 100644 --- a/frontend/src/lib/buildstatus.ts +++ b/frontend/src/lib/buildstatus.ts @@ -21,7 +21,7 @@ const startSocket = () => { console.log('build_status socket closed'); setTimeout(startSocket, 1000); }; -} +}; if (browser) { startSocket(); diff --git a/frontend/src/lib/config/ConfigTextarea.svelte b/frontend/src/lib/config/ConfigTextarea.svelte new file mode 100644 index 00000000..54e94539 --- /dev/null +++ b/frontend/src/lib/config/ConfigTextarea.svelte @@ -0,0 +1,16 @@ + + + diff --git a/frontend/src/lib/state.ts b/frontend/src/lib/state.ts index 996e8cba..77d93bc0 100644 --- a/frontend/src/lib/state.ts +++ b/frontend/src/lib/state.ts @@ -12,6 +12,10 @@ export type SettingTypes = | { type: 'path'; value: string; + } + | { + type: 'textarea'; + value: string; }; export type Setting = SettingTypes & { diff --git a/frontend/src/routes/config/+page.svelte b/frontend/src/routes/config/+page.svelte index 4300758f..313fcfa4 100644 --- a/frontend/src/routes/config/+page.svelte +++ b/frontend/src/routes/config/+page.svelte @@ -3,6 +3,7 @@ import { ListBox, ListBoxItem, type PopupSettings } from '@skeletonlabs/skeleton'; import ConfigBool from '$lib/config/ConfigBool.svelte'; import ConfigString from '$lib/config/ConfigString.svelte'; + import ConfigTextarea from '$lib/config/ConfigTextarea.svelte'; import type { PageData } from './$types'; import { queryParam } from 'sveltekit-search-params'; import { saveState, type Module, type Tag, type Device } from '$lib/state'; @@ -214,6 +215,14 @@ if ($selected != null) setSetting(modules[$selected], settingKey, value); }} /> + {:else if modules[$selected][settingKey].type == 'textarea'} + { + if ($selected != null) setSetting(modules[$selected], settingKey, value); + }} + /> {/if} {#if effectingSettings && effectingSettings.length >= 1} diff --git a/frontend/src/routes/overview/+page.svelte b/frontend/src/routes/overview/+page.svelte index 15905669..b5ed2c5b 100644 --- a/frontend/src/routes/overview/+page.svelte +++ b/frontend/src/routes/overview/+page.svelte @@ -47,6 +47,20 @@ ); saveState(state); } + + const downloadUri = (uri: string) => { + const link = document.createElement('a'); + link.href = uri; + link.download = 'image.img'; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + }; + + const buildAndDownloadImage = (device: Device) => { + console.log('Building and downloading image'); + downloadUri(`http://localhost:8000/action/build-download-image?hostname=${device.hostname}`); + }; diff --git a/frontend/tailwind.config.cjs b/frontend/tailwind.config.cjs index fde10566..4bece02e 100644 --- a/frontend/tailwind.config.cjs +++ b/frontend/tailwind.config.cjs @@ -16,18 +16,18 @@ module.exports = { theme: { extend: { height: { - "10v": "10vh", - "20v": "20vh", - "30v": "30vh", - "40v": "40vh", - "50v": "50vh", - "60v": "60vh", - "70v": "70vh", - "80v": "80vh", - "90v": "90vh", - "100v": "100vh", - }, - }, + '10v': '10vh', + '20v': '20vh', + '30v': '30vh', + '40v': '40vh', + '50v': '50vh', + '60v': '60vh', + '70v': '70vh', + '80v': '80vh', + '90v': '90vh', + '100v': '100vh' + } + } }, plugins: [ forms,