diff --git a/.changelog/v1.0.0.md b/.changelog/v1.0.0.md new file mode 100644 index 0000000..b6ee3bd --- /dev/null +++ b/.changelog/v1.0.0.md @@ -0,0 +1,17 @@ +# v1.0.0 + +feat(core): add foundation layer with flake config lib and host structure +feat(core): add boot nix locale and sysctl modules +feat(filesystem): add ZFS impermanence and disko modules +feat(security): add firewall hardening and SSH modules +feat(containers): add MicroVM host orchestrator and instance pool +feat(desktop): add KDE Plasma 6 minimal with Bora layout +feat(hardware): add CPU GPU and platform detection +feat(network): add base and DNS configuration +feat(profiles): add workstation developer server and minimal +feat(lib): add hardware database and Spring DI IoC framework +feat(tests): add pure Nix test suite with module integration +feat(ci): add CI workflows with lint eval and build +feat(ci): add release workflow with ISO generation +chore: add MIT license +docs: add architecture manual and agentic rules diff --git a/.changelog/v1.0.1.md b/.changelog/v1.0.1.md new file mode 100644 index 0000000..22f8be7 --- /dev/null +++ b/.changelog/v1.0.1.md @@ -0,0 +1,14 @@ +# v1.0.1 + +fix(ci): disable FlakeHub authentication in nix-installer-action +fix(ci): find ISO file inside result directory instead of direct cp +refactor(lib): remove redundant cgroup-init.sh systemd handles cgroups +refactor(desktop): prune init-desktop.sh and finalize.sh kwriteconfig6 calls +docs: convert documentation to English with proper markdown headings +docs: add .changelog directory with per version entries +feat(ci): enforce pull request only workflow on main +feat(ci): run CI exclusively on pull requests not push +feat(ci): add validate job with lint eval and security audit +chore: add MIT license file +chore: rename main branch to alpha for development +chore: restructure remote configuration diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..6394ca3 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,31 @@ +name: CI + +on: + pull_request: + branches: + - main + paths-ignore: + - "docs/**" + - "**.md" + +jobs: + validate: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: DeterminateSystems/nix-installer-action@v14 + with: + flakehub: false + - uses: DeterminateSystems/magic-nix-cache-action@v8 + with: + use-flakehub: false + - name: Nix linting + run: nix develop --impure --command statix check src + - name: Dead code detection + run: nix develop --impure --command deadnix src + - name: Formatting check + run: nix develop --impure --command nixpkgs-fmt --check src + - name: Library tests + run: nix-instantiate --eval --strict tests/default.nix + - name: Module integration tests + run: nix-instantiate --eval --strict tests/modules.nix diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..fb7b27b --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,100 @@ +name: Release + +on: + push: + tags: + - "v*.*.*" + - "*.*" + +permissions: + contents: write + +jobs: + validate: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: DeterminateSystems/nix-installer-action@v14 + with: + flakehub: false + - uses: DeterminateSystems/magic-nix-cache-action@v8 + with: + use-flakehub: false + - name: Lint and format + run: | + nix develop --impure --command statix check src + nix develop --impure --command deadnix src + nix develop --impure --command nixpkgs-fmt --check src + - name: Evaluation tests + run: | + nix-instantiate --eval --strict tests/default.nix + nix-instantiate --eval --strict tests/modules.nix + + build-iso: + runs-on: ubuntu-latest + needs: [validate] + strategy: + matrix: + variant: [minimal, desktop, laptop, server] + steps: + - uses: actions/checkout@v4 + - uses: DeterminateSystems/nix-installer-action@v14 + with: + flakehub: false + - uses: DeterminateSystems/magic-nix-cache-action@v8 + with: + use-flakehub: false + - name: Build ISO ${{ matrix.variant }} + run: | + NIXPKGS_ALLOW_BROKEN=1 nix build --impure \ + '.#packages.x86_64-linux.iso-${{ matrix.variant }}' + - name: Rename ISO + run: | + iso=$(find "$(readlink -f result)" -name "*.iso" -type f | head -1) + cp "$iso" bora-${{ matrix.variant }}.iso + - name: Upload ISO ${{ matrix.variant }} + uses: actions/upload-artifact@v4 + with: + name: bora-${{ matrix.variant }} + path: bora-${{ matrix.variant }}.iso + compression-level: 0 + if-no-files-found: error + + release: + runs-on: ubuntu-latest + needs: [build-iso] + steps: + - uses: actions/checkout@v4 + - name: Download all ISOs + uses: actions/download-artifact@v4 + with: + pattern: bora-* + path: isos + merge-multiple: true + - name: Generate changelog from .changelog + run: | + TAG="${{ github.ref_name }}" + VERSION="${TAG#v}" + CHANGELOG_FILE=".changelog/${VERSION}.md" + if [ -f "$CHANGELOG_FILE" ]; then + cp "$CHANGELOG_FILE" release-notes.md + else + CHANGELOG_FILE=".changelog/${TAG}.md" + if [ -f "$CHANGELOG_FILE" ]; then + cp "$CHANGELOG_FILE" release-notes.md + else + echo "Release ${TAG}" > release-notes.md + echo "" >> release-notes.md + git log --oneline --no-decorate "$(git tag --sort=-version:refname | head -2 | tail -1)..${TAG}" 2>/dev/null \ + || git log --oneline --no-decorate "${TAG}" 2>/dev/null \ + >> release-notes.md + fi + fi + - name: Create Release + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + gh release create "${{ github.ref_name }}" \ + --notes-file release-notes.md \ + --title "${{ github.ref_name }}" \ + isos/*.iso diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7a7dc09 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +result +dist/ diff --git a/AGENTS.md b/AGENTS.md index 246f6ba..e04e2fa 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,615 +1,168 @@ -BORA NixOS - AgentiC Rules and Sprint Definitions -Strict-Hard - Zero Hardcoding - Zero Comments - Zero Inline Shell -Version 2.0.0 - Sprint: Fondazione - - -INDICE - -1. Architettura del Repository -2. Regole Assolute -3. Parametrizzazione - Zero Hardcoding -4. Sprint Definitions -5. Spring Framework - Specifica Tecnica -6. Resource Management e Circuit Breaker -7. Security Baseline -8. Idempotenza e Atomicita -9. Testing e Quality Gates -10. Flow Operativo Agente - - -1. ARCHITETTURA DEL REPOSITORY - -Albero Directory: - -os/ - flake.nix ENTRY POINT - stateless, pure - configuration.nix MODULE LOADER - auto-scan dinamico - AGENTS.md QUESTO FILE - regole agentiche + sprint - lib/ NIX LIBRARIES - funzioni pure - default.nix Esporta tutte le librerie - hardware.nix Auto-detection CPU/GPU/Platform - spring.nix DI/IoC Container + Circuit Breaker - src/ NIXOS SOURCE - tutto il codice Nix - hosts/ HOST DEFINITIONS - per-macchina - / Esempio: os, bora, server - meta.nix Metadati: system, hardware, profile, username - default.nix Config host-specific (user, shell, sudo) - hardware.nix Hardware scan (generato da nixos-generate-config) - profiles/ PROFILE DEFINITIONS - use-case - workstation.nix Desktop + KDE + Bora layout - developer.nix Workstation + Dev tools - server.nix Headless + Container orchestrator - minimal.nix Headless minimale - modules/ MODULES - organizzati per categoria - core/ Boot, Nix, Locale, Sysctl - filesystem/ ZFS, Impermanence - security/ Firewall, Hardening, SSH - containers/ MicroVM Host, Orchestrator, Instance Pool - desktop/ KDE Minimal, PipeWire, Bora layout - hardware/ CPU, GPU, Platform - network/ Base, DNS - guests/ MICROVM GUESTS - definizioni container - example/ Generic instance definition + pool config - sandbox.nix Template sandbox generico - config/ RUNTIME CONFIG - file di configurazione - desktop/ plasma-appletsrc, kdeglobals, kwinrc, khotkeysrc - containers/ microvm-bridge.nix - security/ nftables.nix (rule set nft) - assets/ STATIC ASSETS - wallpapers, themes, fonts, icons - wallpapers/ - themes/ - fonts/ - icons/ - scripts/ SHELL SCRIPTS - mai inline nei .nix - spring/ cgroup-init, circuit-breaker, healthcheck, bean-wrapper - maclike/ init-desktop, finalize - pool/ pool-manager, spawn, list, stats - secrets/ ENCRYPTED SECRETS - SOPS + age - tests/ NIX TESTS - valutazioni pure - default.nix Test esecuzione lib - shell.nix Ambiente test (statix, deadnix, nixpkgs-fmt) - docs/ DOCUMENTAZIONE - BORA-WP.md Manuale utente - formato testo - -Principi Architetturali: +# BORA NixOS AgentiC Rules -Single Responsibility: ogni file .nix ha UN SOLO scopo. Un file = un modulo = una funzione. -No Side Effects: le funzioni in lib/ sono pure. Nessun effetto collaterale. -Auto-Discovery: configuration.nix scansiona src/modules/ - nessun import manuale. -Parametrizzazione: tutto via options + mkOption. Niente hardcoded. -External Shell: script shell in scripts/. Riferiti via builtins.readFile. -External Config: file config in config/. Riferiti via path relativo. - - -2. REGOLE ASSOLUTE - -REGOLA 1: ZERO COMMENTS IN NIX FILES +Strict Hard Zero Hardcoding Zero Comments Zero Inline Shell +Version 2.1.0 -VIETATO: # commento inline -VIETATO: /* commento a blocchi */ -VIETATO: /* ... */ multilinea - -DOVUNQUE: documentazione in AGENTS.md -DOVUNQUE: documentazione utente in docs/*.md +## Architecture -ECCEZIONE: SOLO AGENTS.md e docs/*.md possono contenere testo +The repository directory tree follows this structure. The root contains flake.nix which is the stateless pure entry point. configuration.nix is the module loader that performs dynamic auto scan. AGENTS.md is this file with agentic rules and sprint definitions. lib contains Nix libraries with pure functions exported by default.nix hardware.nix for CPU GPU and platform auto detection and spring.nix for the DI IoC Container with Circuit Breaker. src contains the NixOS source with hosts for per machine host definitions profiles for per use case profile definitions modules organized by category guests for MicroVM guest definitions config for runtime config files scripts for shell scripts assets for static assets secrets for secrets encrypted with SOPS and age tests for Nix tests and docs for documentation. .changelog contains per release changelog entries following conventional commits format. -REGOLA 2: ZERO SHELL INLINE IN NIX +The architectural principles are as follows. Single responsibility means every Nix file has one purpose. No side effects means lib functions are pure with no side effects. Auto discovery means configuration.nix scans src/modules without manual imports. Parameterization means everything uses options with mkOption without hardcoding. External shell means shell scripts in scripts referenced via builtins.readFile. External config means config files in config referenced via relative path. -VIETATO: script shell dentro '' ... '' -VIETATO: pkgs.writeShellScript "nome" '' ... '' -VIETATO: ''${...} con comandi shell +## Conventional Commits -OBBLIGO: ogni script shell in scripts/ come file separato -RIFERIMENTO: builtins.readFile ./scripts/path.sh +Every commit must follow the conventional commits specification. The format is type(scope): description. The body is optional and must be empty for squash merges. Valid types are feat for a new feature fix for a bug fix refactor for code restructuring docs for documentation changes chore for maintenance tasks test for testing changes ci for CI workflow changes style for formatting changes perf for performance improvements. The scope is the module or area affected such as core security desktop spring lib. Examples include feat(core): add sysctl hardening module fix(security): resolve nftables input chain order refactor(lib): clean up spring.nix unused params chore: add MIT license file ci: trigger release on version tags only. -CORRETTO: pkgs.writeShellScriptBin "name" (builtins.readFile ./scripts/name.sh) +## Rules -REGOLA 3: ZERO HARDCODING - -VIETATO: hardcodare username (alessio, kairosci, ...) -VIETATO: hardcodare hostname (bora, os, ...) -VIETATO: hardcodare path, IP, porte, UUID -VIETATO: hardcodare CPU/GPU/RAM config +### Rule 1 -OBBLIGO: username da meta.nix o option -OBBLIGO: hostname da meta.nix o option -OBBLIGO: hardware con opzioni lib.mkDefault -OBBLIGO: attivazione con lib.mkIf +Zero Comments in Nix files. Inline hash comments are forbidden. Block slash asterisk comments are forbidden. Multi line comments are forbidden. Technical documentation goes in AGENTS.md and user documentation in docs. Only AGENTS.md and files in docs may contain text. -REGOLA: Niente letterale. Ogni valore e una variabile. +### Rule 2 -REGOLA 4: ATOMICITA STRUTTURALE +Zero Shell Inline in Nix. Writing shell scripts inside quoted Nix strings is forbidden. Using pkgs.writeShellScript with inline strings is forbidden. Using shell expressions inside Nix strings is forbidden. Every shell script must be in scripts as a separate file. The correct reference is pkgs.writeShellScriptBin name with builtins.readFile reading the script path. -OBBLIGO: ogni modifica produce un NEW GENERATION atomico +### Rule 3 -VIETATO: workaround, fallback, placeholders -VIETATO: # TODO:, # FIXME:, # HACK: -VIETATO: commenti per disabilitare codice +Zero Hardcoding. Hardcoding usernames like alessio or kairosci is forbidden. Hardcoding hostnames like bora or os is forbidden. Hardcoding paths IPs ports or UUIDs is forbidden. Hardcoding CPU GPU or RAM config is forbidden. Username must come from meta.nix or option. Hostname must come from meta.nix or option. Hardware must use options with lib.mkDefault. Activation must use lib.mkIf. No literal values every value must be a variable. -CORRETTO: mkIf false per disabilitare un modulo -CORRETTO: funzione non implementata = NON ESISTE +### Rule 4 -REGOLA 5: MODULARITA DINAMICA +Structural Atomicity. Every modification must produce an atomic new generation. Workarounds fallbacks and placeholders are forbidden. TODO FIXME and HACK are forbidden. Comments to disable code are forbidden. To disable a module use mkIf false. An unimplemented function must not exist. -configuration.nix scansiona src/modules/ AUTOMATICAMENTE. -Ogni categoria = src/modules//. -Ogni categoria ha default.nix che importa sottomoduli. +### Rule 5 -Moduli si abilitano via mkIf cfg.enable. -Profili attivano combinazioni di moduli. +Dynamic Modularity. configuration.nix scans src/modules automatically. Each category corresponds to src/modules/category. Each category has default.nix which imports submodules. Modules are enabled via mkIf cfg.enable. Profiles activate combinations of modules. To create a new module create src/modules/category/name.nix update src/modules/category/default.nix define options with enable and parameters and use mkIf cfg.enable for config. -NEW MODULE FLOW: -1. Crea src/modules//.nix -2. Aggiorna src/modules//default.nix -3. Definisci options con enable + parametri -4. Usa mkIf cfg.enable per la config +### Rule 6 +Pull Request Only. Direct commits and pushes to the main branch are forbidden. Every change must go through a pull request on GitHub. All CI jobs must pass before merge. The only exception is the chore initial commit on main which is created manually once. No agent or developer pushes directly to main. Merges must use squash strategy. Merge commits must have an empty body and must not include the pull request number in the title. -3. PARAMETRIZZAZIONE - ZERO HARDCODING +### Rule 7 -Tutti i parametri host-specific sono DICHIARATI in src/hosts//meta.nix -e INIETTATI via specialArgs. +CI on PR Only. The CI workflow triggers exclusively on pull requests targeting main. Push triggers are forbidden except for release tags. The CI must run linting evaluation tests hardware validation and security audit. Every job must produce a pass or fail result with no skipped steps. ISO generation is not part of CI. ISO build happens only in the release workflow triggered by version tags or locally via scripts/build/iso-build.sh. -meta.nix - Template Generico: +## Nix Best Practices -{ system = "x86_64-linux"; hardware = "desktop"; profile = "developer"; hostname = "os"; username = "user"; } +### Module Structure -Regole di Sostituzione: +Each module defines an options block with an enable flag and all configurable parameters. The config block is wrapped in mkIf cfg.enable. Assertions validate parameter combinations at evaluation time. Options must use mkOption with explicit type and default. Default values use mkDefault for overridability. Conditional values use mkIf. Host specific values are never hardcoded they come from meta.nix or specialArgs. -Cosa Dove Come -username users.users.* Diventa ${username} -username /home/* Diventa /home/${username} -hostname networking.hostName Diventa ${hostname} -hostname spring.application.name Diventa ${hostname} -/persist environment.persistence Legge da option -Path assoluti config/ e scripts/ Path relativi sempre +### Pure Functions +Library functions in lib must be pure with no side effects. They receive only their dependencies as function arguments. No config state or pkgs is accessible unless explicitly passed. Functions return values without modifying external state. Evaluation is deterministic and idempotent. -4. SPRINT DEFINITIONS +### Parameterization -Sprint 1 - Fondazione (Foundation) +Every configurable value uses Nix options with mkOption. Types must be explicit using types from lib.types. Defaults must be sensible and use mkDefault. Conditional overrides use mkIf. Assertions validate parameter combinations. Host specific values are injected via specialArgs from meta.nix. -SCOPO: Struttura base del sistema funzionante +### Testing -1.1 flake.nix - Entry point puro con inputs dichiarativi -1.2 configuration.nix - Module loader auto-scan -1.3 lib/default.nix - Esporta tutte le librerie -1.4 lib/hardware.nix - Database CPU/GPU/Platform -1.5 src/modules/core/ - Boot, Nix, Locale, Sysctl -1.6 src/hosts// - Meta + default + hardware -1.7 AGENTS.md - Questo file +Tests evaluate pure library functions with assertEq. Module integration tests use nixosSystem with minimal configuration. Every module with options must have assertions. Test files live in tests and use strict evaluation. Test cases must be real world scenarios not placeholders. -Sprint 2 - Filesystem e Immutabilita +## Host Specific Parameters -SCOPO: ZFS + Impermanence + Disko +All host specific parameters are declared in src/hosts/hostname/meta.nix and injected via specialArgs. The generic meta.nix template contains system as system architecture hardware as hardware type profile as usage profile hostname as host name and username as user name. The substitution rules are that username in users.users becomes the username value username in home paths becomes home with username hostname in networking.hostName becomes the hostname value hostname in spring.application.name becomes the hostname value persist in environment.persistence reads from option and absolute paths for config and scripts are relative paths. -2.1 src/modules/filesystem/zfs.nix - Pool, ARC, snapshot -2.2 src/modules/filesystem/impermanence.nix - /persist -2.3 config/desktop/ - Config file esterni (plasma, nft) -2.4 sanoid - Snapshot retention automatica -2.5 disko - Partizionamento dichiarativo +## Sprint Definitions -Sprint 3 - Sicurezza +### Sprint 1 -SCOPO: Hardening estremo + Firewall + SSH +Foundation with the goal of creating the base system structure. It includes flake.nix as pure entry point with declarative inputs configuration.nix as auto scan module loader lib/default.nix exporting all libraries lib/hardware.nix as CPU GPU and Platform database src/modules/core for Boot Nix Locale and Sysctl src/hosts/hostname with meta default and hardware and AGENTS.md. -3.1 src/modules/security/firewall.nix - nftables default drop -3.2 config/security/nftables.nix - Ruleset esterno -3.3 src/modules/security/hardening.nix - Kernel + AppArmor -3.4 src/modules/security/ssh.nix - Only keys, only LAN -3.5 Audit logging + fail2ban +### Sprint 2 -Sprint 4 - Hardware Detection +Filesystem and Immutability with the goal of implementing ZFS Impermanence and Disko. It includes the zfs module for pool ARC and snapshot the impermanence module for persist config desktop for external config files sanoid for automatic snapshot retention and disko for declarative partitioning. -SCOPO: Auto-configurazione CPU/GPU/Platform +### Sprint 3 -4.1 src/modules/hardware/cpu.nix - Intel/AMD/ARM -4.2 src/modules/hardware/gpu.nix - NVIDIA/AMD/Intel -4.3 src/modules/hardware/platform.nix - Desktop/Laptop/Server -4.4 lib/hardware.nix - Database ottimizzazioni per vendor +Security with the goal of implementing extreme hardening firewall and SSH. It includes the firewall module with nftables default drop the external nftables configuration the hardening module for kernel and AppArmor the ssh module with keys only LAN only and audit logging with fail2ban. -Sprint 5 - Desktop e Bora Layout +### Sprint 4 -SCOPO: KDE Plasma 6 minimale con layout Bora originale +Hardware Detection with the goal of auto configuring CPU GPU and Platform. It includes the cpu module for Intel AMD and ARM the gpu module for NVIDIA AMD and Intel the platform module for Desktop Laptop and Server and lib/hardware.nix as vendor optimization database. -5.1 src/modules/desktop/kde-minimal.nix - Plasma 6 essenziale -5.2 src/modules/desktop/bora.nix - Tema, dock, global menu -5.3 src/modules/desktop/pipewire.nix - Audio -5.4 scripts/bora/ - Init + finalize shell scripts -5.5 config/desktop/ - plasma-appletsrc, kdeglobals, kwinrc +### Sprint 5 -Sprint 6 - Container Engine (MicroVM) +Desktop and Bora Layout with the goal of creating minimal KDE Plasma 6 with original Bora layout. It includes the kde-minimal module for essential Plasma 6 the maclike module for the Bora theme the pipewire module for audio the maclike scripts for init and finalize shell and the desktop config files for plasma-appletsrc kdeglobals and kwinrc. -SCOPO: Container engine con isolamento hardware-level +### Sprint 6 -6.1 src/modules/containers/microvm-host.nix - Host + bridge -6.2 src/modules/containers/orchestrator.nix - Pool manager -6.3 src/guests/sandbox.nix - Guest template generico -6.4 config/containers/ - Config bridge e networking -6.5 SocketVM per app desktop con forwarding X11/Wayland +Container Engine with the goal of creating the container engine with hardware level isolation. It includes the microvm-host module for host and bridge the orchestrator module for pool manager the sandbox guest as generic template the containers configuration for bridge and networking and SocketVM for desktop apps with X11 and Wayland forwarding. -Sprint 7 - Spring Framework (DI/IoC) +### Sprint 7 -SCOPO: Dependency Injection + Circuit Breaker +Spring Framework with the goal of implementing Dependency Injection and Circuit Breaker. It includes lib/spring.nix for bean definitions topological sort mkSystemdService with resource limits circuit breaker with failure success and state circular dependency detection and the spring scripts for circuit-breaker and health. It also includes the orchestrator update to use Spring beans. -7.1 lib/spring.nix - Bean definitions + topological sort -7.2 lib/spring.nix - mkSystemdService con resource limits -7.3 lib/spring.nix - Circuit breaker (failure/success/stato) -7.4 lib/spring.nix - Circular dependency detection -7.5 scripts/spring/ - cgroup-init, circuit-breaker, health -7.6 Aggiornare orchestrator per usare Spring beans - -Sprint 8 - Instance Pool Orchestrator - -SCOPO: Pool di istanze isolate per qualsiasi applicazione +### Sprint 8 -8.1 src/modules/containers/instance-pool.nix - Opzioni pool -8.2 src/guests//guest.nix - Guest definition -8.3 src/guests//pool.nix - Pool configuration -8.4 scripts/pool/ - pool-manager, spawn, list, stats -8.5 Cgroup v2 per isolamento risorse per istanza -8.6 Reverse proxy Caddy per routing alle istanze +Instance Pool Orchestrator with the goal of creating the pool of isolated instances for any application. It includes the instance-pool module with pool options the guest definition per application the pool configuration the pool scripts for pool-manager spawn list and stats cgroup v2 for per instance resource isolation and Caddy reverse proxy for routing to instances. -Sprint 9 - Testing e Documentazione +### Sprint 9 -SCOPO: Test Nix puri + Documentazione completa - -9.1 tests/default.nix - Test librerie pure -9.2 tests/shell.nix - Ambiente linting (statix, deadnix) -9.3 docs/BORA-WP.md - Manuale utente formato testo -9.4 AGENTS.md - Regole agentiche sempre aggiornate -9.5 ISO generation per deploy immediato - -Sprint Flow: - -Sprint 1 -> Sprint 2 -> Sprint 3 -> Sprint 4 - | - v - Sprint 5 - | - v - Sprint 6 - | - v - Sprint 7 -> Sprint 8 -> Sprint 9 - -Ogni sprint produce una generazione NixOS funzionante. Nessuna dipendenza non soddisfatta. - -Sprint History: - -SPRINT 1 - Fondazione - COMPLETATO - 1.1 flake.nix - COMPLETATO - 1.2 configuration.nix - COMPLETATO - 1.3 lib/default.nix - COMPLETATO - 1.4 lib/hardware.nix - COMPLETATO - 1.5 src/modules/core/ - COMPLETATO - 1.6 src/hosts/os/ - COMPLETATO - 1.7 AGENTS.md - COMPLETATO - -SPRINT 2 - Filesystem e Immutabilita - COMPLETATO - 2.1 zfs.nix - COMPLETATO - 2.2 impermanence.nix - COMPLETATO - 2.3 config/desktop/ - COMPLETATO - 2.4 sanoid - COMPLETATO - 2.5 disko - COMPLETATO - -SPRINT 3 - Sicurezza - COMPLETATO - 3.1 firewall.nix - COMPLETATO - 3.2 nftables config - COMPLETATO - 3.3 hardening.nix - COMPLETATO - 3.4 ssh.nix - COMPLETATO - 3.5 fail2ban + audit - COMPLETATO - -SPRINT 4 - Hardware Detection - COMPLETATO - 4.1 cpu.nix - COMPLETATO - 4.2 gpu.nix - COMPLETATO - 4.3 platform.nix - COMPLETATO - 4.4 lib/hardware.nix - COMPLETATO - -SPRINT 5 - Desktop e Bora Layout - COMPLETATO - 5.1 kde-minimal.nix - COMPLETATO - 5.2 maclike.nix (Bora theme) - COMPLETATO - 5.3 pipewire.nix - COMPLETATO - 5.4 scripts/maclike/ - COMPLETATO - 5.5 config/desktop/ - COMPLETATO - -SPRINT 6 - Container Engine - COMPLETATO - 6.1 microvm-host.nix - COMPLETATO - 6.2 orchestrator.nix - COMPLETATO - 6.3 sandbox.guest - COMPLETATO - 6.4 container config - COMPLETATO - 6.5 SocketVM forwarding - COMPLETATO - -SPRINT 7 - Spring Framework - COMPLETATO - 7.1 bean definitions - COMPLETATO - 7.2 systemd services - COMPLETATO - 7.3 circuit breaker - COMPLETATO - 7.4 cycle detection - COMPLETATO - 7.5 spring scripts - COMPLETATO - -SPRINT 8 - Instance Pool - COMPLETATO - 8.1 instance-pool.nix - COMPLETATO - 8.2 guest definition - COMPLETATO - 8.3 pool config - COMPLETATO - 8.4 pool scripts - COMPLETATO - 8.5 cgroup v2 - COMPLETATO - 8.6 Caddy proxy - COMPLETATO - -SPRINT 9 - Testing e Documentazione - COMPLETATO - 9.1 tests/default.nix - COMPLETATO - 9.2 tests/shell.nix - COMPLETATO - 9.3 docs/BORA-WP.md - COMPLETATO - 9.4 AGENTS.md - COMPLETATO - 9.5 ISO generation - COMPLETATO - -Tutti gli sprint sono COMPLETATI. Il sistema e pronto per build e deploy. - - -5. SPRING FRAMEWORK - SPECIFICA TECNICA - -Bean Definition: - -bora.spring.beans. = { - enable = true; - class = "ServiceType"; - deps = [ "bean-a" "bean-b" ]; - resources = { - cpu = "2"; - memory = "1G"; - memoryMax = "2G"; - pids = 512; - ioRbps = "100M"; - ioWbps = "50M"; - numa = null; - }; - healthcheck = "curl -f http://localhost:8080"; - dependsOn = [ "storage" ]; - after = [ "network.target" ]; - restartPolicy = "on-failure"; -}; - -Circuit Breaker state machine: - -CLOSED: funzionamento normale, richieste passano, failure incrementano contatore. -OPEN: circuito aperto, richieste bloccate, timer di timeout avviato. -HALF-OPEN: test di recupero, richieste limitate. - -Transizioni: - CLOSED -> OPEN: quando failure >= threshold (default 5) - OPEN -> HALF-OPEN: dopo timeout (default 30 secondi) - HALF-OPEN -> CLOSED: quando success >= threshold (default 2) - HALF-OPEN -> OPEN: quando failure in half-open - -Topological Sort: - -Le dipendenze tra bean sono risolte a BUILD-TIME con topological sort. -Se esiste un ciclo, il build FAIL con messaggio: -error: Spring: circular dependency detected in beans: [a, b, c] - - -6. RESOURCE MANAGEMENT E CIRCUIT BREAKER - -Cgroup v2 Hierarchy: - -/sys/fs/cgroup/ - / - bean-database/ cpu.max, memory.max, pids.max, io.max - bean-redis/ Limiti dedicati per bean - bean-webapp/ OOM policy: kill - bora/ - pool/ Pool istanze MicroVM - instance-001/ cpu.max=50%, memory.max=256M - instance-002/ cpu.max=50%, memory.max=256M - -OOM Protection: - -OOMPolicy=kill per tutti i servizi Spring. -MemoryHigh = soft limit (throttling prima di OOM). -MemoryMax = hard limit (OOM kill se superato). -DefaultMemoryAccounting=yes globalmente. - -Health Check Flow: - -1. Esegui healthcheck command -2. Se SUCCESS -> circuit_success() -3. Se FAILURE -> circuit_trip() - CLOSED: incrementa counter, se > threshold -> OPEN - OPEN: attendi timeout, poi -> HALF-OPEN - HALF-OPEN: se < max tentativi -> riprova, altrimenti -> CLOSED -4. Se circuito OPEN -> servizio non parte (exit 1) +Testing and Documentation with the goal of implementing pure Nix tests and complete documentation. It includes tests/default.nix for pure library tests tests/shell.nix for linting environment with statix and deadnix docs as user manual in text format AGENTS.md with always updated agentic rules and ISO generation for immediate deploy. +The sprint flow proceeds from Sprint 1 to Sprint 2 to Sprint 3 to Sprint 4 from which it branches to Sprint 5 which continues to Sprint 6 which leads to Sprint 7 and Sprint 8 and finally Sprint 9. Each sprint produces a working NixOS generation without unsatisfied dependencies. All sprints from number 1 to number 9 are completed. -7. SECURITY BASELINE +## Spring Framework Specification -Kernel Parameters (sysctl): +### Bean Definition -kernel.kptr_restrict = 2 -kernel.dmesg_restrict = 1 -kernel.perf_event_paranoid = 3 -kernel.yama.ptrace_scope = 2 -kernel.randomize_va_space = 2 -kernel.unprivileged_bpf_disabled = 1 -net.core.bpf_jit_enable = 0 -kernel.kexec_load_disabled = 1 -kernel.sysrq = 0 +Bean definition happens via bora.spring.beans.name with attributes enable to enable class as service type deps as list of beans it depends on resources with cpu memory memoryMax pids ioRbps ioWbps and numa healthcheck as command to verify status dependsOn for systemd dependencies after for systemd ordering and restartPolicy for restart policy. -Firewall (nftables): +### Circuit Breaker -chain input -> policy DROP - ct state { established, related } -> ACCEPT - iifname lo -> ACCEPT - icmp -> rate 10/second ACCEPT - tcp 22 -> saddr LAN -> ACCEPT - tutto altro -> LOG + DROP +The Circuit Breaker state machine has three states. CLOSED is normal operation where requests pass through and failures increment a counter. OPEN is open circuit where requests are blocked and a timeout timer starts. HALF-OPEN is recovery test where limited requests are allowed. Transitions are that CLOSED transitions to OPEN when failures reach the threshold which defaults to 5. OPEN transitions to HALF-OPEN after the timeout which defaults to 30 seconds. HALF-OPEN transitions to CLOSED when successes reach the threshold which defaults to 2. HALF-OPEN transitions to OPEN when a failure occurs in half-open. -chain forward -> policy DROP - ct state { established, related } -> ACCEPT - iifname "microvm" -> ACCEPT - -chain output -> policy ACCEPT - -SSH Hardening: - -PermitRootLogin no -PasswordAuthentication no -PubkeyAuthentication yes -MaxAuthTries 3 -MaxSessions 4 -AllowTcpForwarding no -AllowAgentForwarding no -Ciphers chacha20-poly1305, aes256-gcm -MACs hmac-sha2-512-etm, hmac-sha2-256-etm - -AppArmor: - -Enforced con cache attiva. Profili: apparmor-profiles. -Lockdown: confidentiality. - - -8. IDEMPOTENZA E ATOMICITA - -Regole di Idempotenza: - -nixos-rebuild switch DEVE essere IDEMPOTENTE. -Eseguire 2 volte di seguito -> stesso risultato. -Niente side effects fuori dal nix store. -/etc rigenerato a ogni build. -Stato utente SOLO in /persist e /home. -Root filesystem effimero (impermanence). - -Atomicita: - -Ogni nixos-rebuild produce una NEW GENERATION. -La generazione precedente rimane INTATTA nel boot menu. -Rollback: nixos-rebuild switch --rollback. -ZFS: snapshot automatico PRE-rebuild. -ZFS: snapshot POST-rebuild via sanoid. - -farlo, tentativo, hack, workaround = ZERO TOLERANZA. - - -9. TESTING E QUALITY GATES - -Quality Gates (obbligatori prima di ogni commit): - -1. statix check src/ - Linting Nix -2. deadnix src/ - Dead code detection -3. nixpkgs-fmt --check src/ - Formattazione -4. nix-instantiate --eval tests/ - Valutazione test - -FALLISCI se UNO qualsiasi dei 4 fallisce. - -Test Structure: - -tests/ - default.nix - Test funzioni lib - libTests/ - testHardwareDetect - testSpringFramework - moduleTests/ - testCoreModules - testSecurityModules - shell.nix - Ambiente linting - -Assertions nei Moduli: - -Ogni modulo che definisce opzioni DEVE avere una assertions che verifica: -assertions = [{ assertion = condizione; message = "Errore: spiegazione del problema"; }]; - - -10. FLOW OPERATIVO AGENTE - -Flow Chart: - -USER RICHIEDE MODIFICA - | - v -CERCA IN src/modules/ il modulo pertinente - | Se non esiste: crea nuova categoria, crea default.nix, crea file.nix - | - v -MODIFICA options/config - | - v -SHELL SCRIPTS? -> SI -> scripts/ (mai inline) - | NO - v -CONFIG FILES? -> SI -> config/ (mai inline) - | NO - v -HARDCODING? -> SI -> options + mkDefault + mkIf - | NO - v -COMMENTI nei .nix? -> SI -> RIMUOVI (vanno in AGENTS.md) - | NO - v -statix + deadnix + nixpkgs-fmt - | - v -VERIFICA idempotenza - | - v -nixos-rebuild switch (opzionale) - -Regole per l'Agente: - -1. MAI scrivere commenti nei .nix -2. MAI scrivere script shell inline nei .nix -3. MAI hardcodare username, hostname, path -4. SEMPRE usare options + mkOption per parametri -5. SEMPRE usare mkIf per attivazione condizionale -6. SEMPRE usare mkDefault per default sovrascrivibili -7. Script shell -> scripts/ -8. Config files -> config/ -9. documentazione tecnica -> AGENTS.md -10. documentazione utente -> docs/ -11. Dopo ogni modifica: statix + deadnix + nixpkgs-fmt -12. Ogni modifica deve essere IDEMPOTENTE - - -Appendice A: Template Nuovo Modulo - -{ config, lib, pkgs, ... }: -with lib; -let cfg = config.bora.category.module; in { - options.bora.category.module = { - enable = mkEnableOption "descrizione modulo"; - option1 = mkOption { type = types.str; default = "valore"; }; - }; - config = mkIf cfg.enable { attr = mkDefault cfg.option1; }; -}; - -Appendice B: Template Nuovo Host - -meta.nix: -{ - system = "x86_64-linux"; - hardware = "desktop"; - profile = "minimal"; - hostname = "os"; - username = "user"; -} - -default.nix: -{ config, lib, pkgs, username, hostname, ... }: { - networking.hostName = hostname; - users.users.${username} = { isNormalUser = true; extraGroups = [ "wheel" ]; }; -}; - -Appendice C: Template Script Shell - -File scripts/categoria/nome.sh: -#!/usr/bin/env bash -set -euo pipefail -ARG1="${1:?ARG1 required}" -ARG2="${2:-default}" -main() { printf "Executing: %s %s\n" "${ARG1}" "${ARG2}"; } -main "$@" - -Riferimento in Nix: -pkgs.writeShellScriptBin "nome-comando" (builtins.readFile ./scripts/categoria/nome.sh) - - -BORA NixOS - Regole AgentiChe v2.0.0 - Sprint: Fondazione -Copyright 2026 - Distribuito sotto licenza MIT +### Topological Sort + +Topological sort resolves dependencies between beans at build time. If a cycle exists the build fails with an error message indicating circular dependency in the specified beans. + +### Cgroup Hierarchy + +The cgroup v2 hierarchy is organized under sys fs cgroup with the host name containing bean-database bean-redis and bean-webapp with cpu.max memory.max pids.max and io.max and OOM policy kill. The bora section contains pool for MicroVM instances with instance-001 and instance-002 with cpu.max at 50 percent and memory.max at 256 MB. + +## Security Baseline + +### Kernel Parameters + +The kernel sysctl parameters include kernel.kptr_restrict set to 2 kernel.dmesg_restrict to 1 kernel.perf_event_paranoid to 3 kernel.yama.ptrace_scope to 2 kernel.randomize_va_space to 2 kernel.unprivileged_bpf_disabled to 1 net.core.bpf_jit_enable to 0 kernel.kexec_load_disabled to 1 and kernel.sysrq to 0. + +### Firewall + +The nftables firewall defines chain input with policy DROP accepting established and related connections loopback interface traffic ICMP with rate limit of 10 per second and TCP port 22 from LAN addresses and logging and dropping everything else. chain forward with policy DROP accepts established and related connections and traffic from the microvm interface. chain output with policy ACCEPT. + +### SSH Hardening + +SSH hardening provides PermitRootLogin no PasswordAuthentication no PubkeyAuthentication yes MaxAuthTries 3 MaxSessions 4 AllowTcpForwarding no AllowAgentForwarding no ciphers ChaCha20-Poly1305 and AES-256-GCM and MACs HMAC-SHA2-512-ETM and HMAC-SHA2-256-ETM. AppArmor is enforced with active cache profiles from apparmor-profiles and lockdown set to confidentiality. + +## Quality Gates + +The mandatory quality gates before each merge include statix check src for Nix linting deadnix src for dead code detection nixpkgs-fmt check src for formatting nix-instantiate --eval --strict tests/default.nix for library tests and nix-instantiate --eval --strict tests/modules.nix for module integration tests. The merge must be blocked if any of the quality gates fails. + +## Templates + +### Module Template + +The template for a new module requires defining config lib pkgs using let cfg config.bora.category.module to access options. options.bora.category.module must contain enable as mkEnableOption and option1 as mkOption with type and default. config must be wrapped in mkIf cfg.enable with attr set to mkDefault cfg.option1. + +### Host Template + +The template for a new host requires a meta.nix file with system hardware profile hostname and username. The default.nix file receives config lib pkgs username and hostname and configures networking.hostName with hostname and users.users with username as isNormalUser true and extraGroups with wheel. + +### Shell Script Template + +The template for a shell script requires the file in scripts/category/name.sh with bash shebang set euo pipefail parameters with defaults and main function that executes the logic. The reference in Nix uses pkgs.writeShellScriptBin with builtins.readFile to read the script path. + +## Idempotency and Atomicity + +The idempotency rules require that nixos-rebuild switch is idempotent running it twice in a row must produce the same result. There must be no side effects outside the nix store. The etc directory is regenerated on every build. User state resides only in persist and home. The root filesystem is ephemeral via impermanence. Every nixos-rebuild produces a new generation. The previous generation remains intact in the boot menu. Rollback is performed with nixos-rebuild switch rollback. ZFS performs automatic pre rebuild snapshot and post rebuild snapshot via sanoid. Workarounds attempts hacks and placeholders have zero tolerance. + +## Agent Workflow + +When the user requests a modification the agent searches src/modules for the relevant module. If it does not exist it creates a new category creates default.nix and creates the module file. Then it modifies options and config. If shell scripts are needed they go in scripts never inline. If config files are needed they go in config never inline. If hardcoding exists it is replaced with options mkDefault and mkIf. If comments exist in Nix files they are removed and placed in AGENTS.md. Then it runs statix deadnix and nixpkgs-fmt. It verifies idempotency. Every modification must be submitted as a pull request on the alpha branch targeting main. Direct commits to main are forbidden. Merges are performed only when explicitly requested by the user. Merge commits use squash strategy with empty body and no pull request number in the title. + +--- + +Copyright 2026 Distributed under MIT license diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..835db69 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,3 @@ +# Changelog + +Per version changelog entries are in the .changelog directory. Each file covers a single version with conventional commit format entries. diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..815db8d --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 BORA NixOS + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..07f2654 --- /dev/null +++ b/README.md @@ -0,0 +1,21 @@ +# BORA NixOS + +BORA is a modular immutable NixOS configuration framework built on Zero Hardcoding Zero Comments and Zero Inline Shell principles. Every value is parameterized through Nix options. All technical documentation lives in AGENTS.md. All shell scripts are standalone files in scripts referenced via builtins.readFile. + +The framework includes a Spring style dependency injection and circuit breaker library for systemd services a MicroVM container engine with instance pool orchestration KDE Plasma 6 desktop with custom Bora layout ZFS filesystem with impermanence and a layered security model with NFTables kernel hardening and SSH hardening. + +## Quick Start + +Set hostname and username in src/hosts/target-host/meta.nix. Run nixos-install flake hash target-host. Set a password and reboot. For ISO generation run nix build hash packages.x86_64-linux.iso-minimal for headless or nix build hash packages.x86_64-linux.iso-graphical for desktop. + +## Prerequisites + +Nix package manager with flakes enabled. + +## Project Structure + +src/hosts contains per machine configurations with meta.nix for system hardware profile hostname and username. src/modules contains categorized modules for core filesystem security containers desktop hardware and network. src/profiles defines use case profiles like workstation developer server and minimal. lib contains pure Nix libraries including hardware detection and the Spring framework. + +## License + +MIT diff --git a/config/containers/microvm-bridge.nix b/config/containers/microvm-bridge.nix new file mode 100644 index 0000000..12db67a --- /dev/null +++ b/config/containers/microvm-bridge.nix @@ -0,0 +1,9 @@ +{ config, lib, ... }: +{ + microvm.host.network = { + enable = true; + nat = true; + subnet = "10.100.0.0/24"; + bridge = "microvm"; + }; +} diff --git a/config/desktop/kdeglobals b/config/desktop/kdeglobals new file mode 100644 index 0000000..ec2b5a9 --- /dev/null +++ b/config/desktop/kdeglobals @@ -0,0 +1,57 @@ +[General] +ColorScheme=BoraDark +Name=Bora +shadeSortColumn=true +TerminalApplication=konsole +TerminalService=org.kde.konsole +font=Noto Sans,10,-1,5,50,0,0,0,0,0 +fixed=JetBrainsMono Nerd Font,10,-1,5,50,0,0,0,0,0 +smallestReadableFont=Noto Sans,8,-1,5,50,0,0,0,0,0 +toolbarFont=Noto Sans,10,-1,5,50,0,0,0,0,0 +menuFont=Noto Sans,10,-1,5,50,0,0,0,0,0 + +[KDE] +widgetStyle=Breeze +ShowIconsOnPushButtons=1 +ShowIconsInMenuItems=1 +ShowIconsInListViews=1 +ShowIconsInTooltips=1 +ShowIconsInBreadcrumbBar=1 +ShowIconsInTitleBar=1 +TreeViewAnimated=1 + +[WM] +activeBackground=10,12,22 +activeForeground=220,220,240 +inactiveBackground=8,9,18 +inactiveForeground=140,140,160 + +[Icons] +Theme=TelaCircleDark + +[Colors:Window] +BackgroundNormal=13,15,26 +BackgroundAlternate=18,20,32 +ForegroundNormal=220,220,240 +ForegroundInactive=100,100,130 +ForegroundLink=0,212,255 +ForegroundVisited=123,47,190 + +[Colors:Selection] +BackgroundNormal=0,212,255 +ForegroundNormal=10,12,22 + +[Colors:Button] +BackgroundNormal=20,22,36 +ForegroundNormal=220,220,240 +BackgroundHover=30,32,48 + +[Compositing] +Enabled=true +OpenGLIsUnsafe=false +Backend=OpenGL +AnimationDurationFactor=0.4 +window-decoration=Breeze + +[UiSettings] +ColorScheme=BoraDark diff --git a/config/desktop/khotkeysrc b/config/desktop/khotkeysrc new file mode 100644 index 0000000..bfdf6f3 --- /dev/null +++ b/config/desktop/khotkeysrc @@ -0,0 +1,27 @@ +[Data] +DataCount=1 + +[Data_1] +Comment=Bora Launcher +Enabled=true +Name=Nexus +Type=ACTION_TRIGGERS +Removed=false + +[Data_1\Actions] +ActionCount=1 + +[Data_1\Actions\0] +CommandURL=org.kde.krunner +Type=COMMAND_URL + +[Data_1\Conditions] +ConditionsCount=0 + +[Data_1\Triggers] +TriggersCount=1 + +[Data_1\Triggers\0] +Key=Alt+F1 +Type=SHORTCUT +Uuid={nexus-krunner} diff --git a/config/desktop/kwinrc b/config/desktop/kwinrc new file mode 100644 index 0000000..390f075 --- /dev/null +++ b/config/desktop/kwinrc @@ -0,0 +1,43 @@ +[Compositing] +Backend=OpenGL +Enabled=true +OpenGLIsUnsafe=false +AnimationSpeed=1 +AnimationsEnabled=true +OpenGLCompositing=true +XRenderSmoothScale=false + +[Windows] +TitlebarDoubleClickCommand=Maximize +RollUp=false +BorderlessMaximizedWindows=true +Placement=Centered +FocusPolicy=ClickToFocus +SeparateScreenFocus=false +ActiveMouseScreen=true + +[Desktops] +Number=6 +Rows=2 + +[Effect-Blur] +BlurStrength=25 +BlurRadius=12 + +[Effect-BoraTransition] +Duration=350 + +[Effect-Fade] +Duration=200 + +[Effect-Scale] +Duration=200 + +[Plugins] +blurEnabled=true +slideEnabled=true +kwin4_effect_scaleEnabled=true +kwin4_effect_translucencyEnabled=true +fadeEnabled=true +glideEnabled=true +wobblywindowsEnabled=false diff --git a/config/desktop/plasma-appletsrc b/config/desktop/plasma-appletsrc new file mode 100644 index 0000000..ac9c5b0 --- /dev/null +++ b/config/desktop/plasma-appletsrc @@ -0,0 +1,38 @@ +[Containments][2] +formfactor=2 +lastScreen=0 +location=0 +plugin=org.kde.plasma.folder +wallpaperplugin=org.kde.plasma.image + +[Containments][2][Wallpaper] +plugin=org.kde.plasma.image + +[Containments][2][Wallpaper][org.kde.plasma.image][General] +Image=file:///run/current-system/sw/share/wallpapers/Next/contents/images_dark/5120x2880.png +FillMode=2 + +[Containments][3] +formfactor=2 +location=0 +plugin=org.kde.plasma.folder +wallpaperplugin=org.kde.plasma.image + +[Containments][4] +formfactor=2 +lastScreen=0 +location=0 +plugin=org.kde.plasma.folder +wallpaperplugin=org.kde.plasma.image + +[Containments][5] +formfactor=2 +location=0 +plugin=org.kde.plasma.folder +wallpaperplugin=org.kde.plasma.image + +[Containments][6] +formfactor=2 +location=0 +plugin=org.kde.plasma.folder +wallpaperplugin=org.kde.plasma.image diff --git a/config/security/nftables.nix b/config/security/nftables.nix new file mode 100644 index 0000000..240720c --- /dev/null +++ b/config/security/nftables.nix @@ -0,0 +1,54 @@ +table inet filter { + chain input { + type filter hook input priority 0; policy drop; + + ct state invalid drop; + ct state { established, related } accept; + + iifname lo accept; + + icmp type { + echo-request, destination-unreachable, + time-exceeded, parameter-problem + } limit rate 10/second accept; + + meta l4proto ipv6-icmp icmpv6 type { + echo-request, destination-unreachable, + time-exceeded, parameter-problem, + nd-router-advert, nd-neighbor-solicit, + nd-neighbor-advert, nd-router-solicit + } limit rate 10/second accept; + + tcp dport 22 ip saddr { + 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16 + } accept; + + udp dport 5353 ip saddr { + 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16 + } accept; + + udp dport { 67, 68 } accept; + + log prefix "NF:DROP-INPUT: " drop; + } + + chain forward { + type filter hook forward priority 0; policy drop; + ct state { established, related } accept; + + iifname "microvm" accept; + + log prefix "NF:DROP-FORWARD: " drop; + } + + chain output { + type filter hook output priority 0; policy accept; + } +} + +table inet nat { + chain postrouting { + type nat hook postrouting priority 100; + oifname "eth0" masquerade; + } +} diff --git a/configuration.nix b/configuration.nix index 60fb4a5..607b989 100644 --- a/configuration.nix +++ b/configuration.nix @@ -24,5 +24,5 @@ in { ++ optional (pathExists profilePath) profilePath; nixpkgs.config.allowUnfree = mkDefault true; - system.stateVersion = mkDefault "24.11"; + system.stateVersion = mkDefault "25.11"; } diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md new file mode 100644 index 0000000..c794cab --- /dev/null +++ b/docs/ARCHITECTURE.md @@ -0,0 +1,85 @@ +# BORA NixOS Architecture Document + +Version 2.1.0 + +## Introduction + +BORA is a modular immutable NixOS configuration framework built on four pillars. Zero Hardcoding means every value is parameterized through Nix options. No usernames hostnames paths IPs or hardware identifiers are hardcoded. Host specific values are declared in meta.nix files and injected via specialArgs. Zero Comments means Nix files contain no comments. All technical documentation lives in AGENTS.md. User documentation lives in docs. This enforces self documenting code through meaningful identifier names and pure functional patterns. Zero Inline Shell means every shell script is stored as a standalone file in scripts and referenced via builtins.readFile. No script content appears inside Nix strings. Pure Functions means library functions in lib are pure with no side effects. Module evaluation is deterministic and idempotent. + +## Repository Structure + +The repository follows a strict directory layout enforced by the module loader and build system. The top level contains flake.nix as the entry point declaring inputs like nixpkgs nixos-hardware microvm sops-nix and nixos-generators and defining outputs for each discovered host plus ISO generation. configuration.nix is the module loader that auto scans src/modules for category directories imports each category default.nix and loads the selected profile from src/profiles. + +The source tree is organized as follows. src/hosts contains per machine configurations with each subdirectory named after the host containing meta.nix for system hardware profile hostname and username default.nix for host specific config and hardware.nix for generated hardware scan. src/profiles defines use case configurations including workstation with desktop KDE and Bora layout developer with workstation plus dev tools server with headless and container orchestrator and minimal with headless minimal. src/modules is organized by category with core for boot nix locale and sysctl filesystem for ZFS and impermanence security for firewall hardening and SSH containers for MicroVM host orchestrator and instance pool desktop for KDE minimal PipeWire and Bora layout hardware for CPU GPU and platform and network for base and DNS. + +src/guests defines MicroVM guest templates with sandbox.nix as a generic template and the example directory demonstrating a concrete instance definition with pool configuration. lib contains pure Nix library functions including hardware detection database Spring DI IoC framework with circuit breaker and the library aggregator. config holds external configuration files referenced by modules such as desktop panel layouts NFTables rulesets and container bridge configuration. scripts holds standalone shell scripts organized by subsystem including spring services desktop initialization and pool management. assets is reserved for static files like wallpapers themes fonts and icons. secrets is reserved for encrypted secrets via SOPS and age. tests contains pure Nix evaluation tests and a shell environment for linting with statix deadnix and nixpkgs-fmt. docs contains documentation in markdown format. .changelog contains per release changelog entries. + +## Core Design Principles + +Single Responsibility means each Nix file has exactly one purpose. A file equals one module equals one function. No file mixes concerns. Auto Discovery means the configuration.nix module loader scans src/modules at evaluation time. No manual imports are needed when adding new modules. Each category default.nix imports all submodules within that category. + +Conditional Activation means modules are enabled or disabled via mkIf cfg.enable. The enable option is declared in the module options block. Profiles activate combinations of modules by setting these options to their preferred values. Idempotency means nixos-rebuild switch is idempotent. Running it twice produces the same result. No state is modified outside the Nix store. User state lives exclusively in persist and home. The root filesystem is ephemeral through impermanence. + +Atomicity means every nixos-rebuild produces a new generation. The previous generation remains intact and selectable from the boot menu. Rollback uses nixos-rebuild switch rollback. ZFS snapshots are taken automatically before and after rebuild via sanoid. Parameterization means all configurable values use Nix options with mkOption. Default values use mkDefault so they can be overridden. Conditional values use mkIf. No literal values appear in configuration logic. + +## Module System + +Modules are organized into categories under src/modules. Each category represents a subsystem of the operating system. The core category covers boot configuration with systemd-boot kernel parameters and initrd Nix daemon settings with auto-optimise garbage collection and substituters locale and timezone and sysctl kernel parameters. The filesystem category covers ZFS pool creation ARC tuning automatic trimming scrub scheduling sanoid snapshot retention disko partitioning and impermanence configuration with persistent directories and files. + +The security category covers NFTables firewall with default drop policy kernel hardening through sysctl AppArmor enforcement with lockdown SSH server hardening with key only access rate limiting and minimal ciphers Fail2ban for brute force protection and audit logging. The containers category covers MicroVM host configuration with bridge networking orchestrator for managing guest lifecycles and instance pool for dynamic scaling of guest instances with cgroup v2 resource isolation. + +The desktop category covers KDE Plasma 6 minimal installation PipeWire audio server and Bora custom desktop layout with top bar dock global menu and cosmic dark theme. The hardware category covers CPU specific optimizations for Intel AMD and ARM GPU drivers and configuration for NVIDIA AMD and Intel and platform tuning for desktop laptop and server. The network category covers base network configuration and DNS resolver settings. + +Each module defines an options block with an enable flag and all configurable parameters. The config block is wrapped in mkIf cfg.enable. Assertions validate parameter combinations at evaluation time. To create a new module you first create src/modules/category/name.nix with options and config then update src/modules/category/default.nix to import the new file then define options with mkOption and use mkIf for conditional config and finally use assertions to validate constraints. + +## Host and Profile System + +Each host is defined in src/hosts/hostname. The meta.nix file declares four attributes. system is the NixOS system architecture such as x86_64-linux. hardware is the hardware class such as desktop laptop or server. profile is the use case profile name such as workstation developer server or minimal. hostname is the machine hostname. username is the primary user name. + +The flake.nix reads src/hosts to discover available hosts. It passes hostname and username from meta.nix as specialArgs to the NixOS configuration. This eliminates all hardcoded user and host references. Profiles in src/profiles define combinations of enabled modules. A profile sets bora options to mkDefault values establishing the baseline configuration for that use case. Profiles inherit from more basic profiles where applicable. The configuration.nix loads the profile specified in meta.nix using a dynamic import based on the profile attribute. + +## Spring Framework + +The Spring framework in lib/spring.nix provides dependency injection and circuit breaker patterns for systemd services. Bean definitions use the bora.spring.beans attribute set. Each bean specifies a class as a service type identifier for organizational purposes a deps list of bean names this bean depends on resources with resource limits for cgroup v2 isolation including cpu memory memoryMax pids ioRbps ioWbps and numa a healthcheck command that returns zero for healthy service dependsOn for systemd unit dependencies after for systemd unit ordering and restartPolicy for systemd restart policy. + +The framework performs topological sort of bean dependencies at build time. Circular dependencies cause a build failure with a diagnostic message listing the cycle. The circuit breaker implements a three state machine. CLOSED is normal operation where requests pass through and failures increment a counter. OPEN is when the circuit is open requests are blocked and a timeout timer starts. HALF-OPEN is recovery test mode where limited requests are allowed through. + +Circuit breaker transitions work as follows. CLOSED transitions to OPEN when failures reach the threshold which defaults to 5. OPEN transitions to HALF-OPEN after the timeout which defaults to 30 seconds. HALF-OPEN transitions to CLOSED when successes reach the threshold which defaults to 2. HALF-OPEN transitions to OPEN on any failure in half-open state. + +Cgroup v2 hierarchy is created under sys fs cgroup hostname bean name with cpu.max memory.max pids.max and io.max limits. OOM policy is set to kill for all Spring services. The health check flow executes the healthcheck command periodically. Success calls circuit_success which may transition to CLOSED. Failure calls circuit_trip which may transition to OPEN. When the circuit is OPEN the service exits with code 1. + +## Security Architecture + +Security is implemented in layers. Kernel hardening uses sysctl parameters to restrict kernel pointer access dmesg access performance events ptrace BPF kexec and SysRq. ASLR is set to maximum. Unprivileged BPF is disabled. BPF JIT is disabled. + +The firewall uses NFTables with a default drop policy on the input chain. Only established and related connections loopback traffic rate limited ICMP and SSH from LAN addresses are accepted. The forward chain accepts established and related connections and traffic from the microvm bridge interface. The output chain has a default accept policy. + +SSH is hardened with no root login no password authentication key only access rate limited authentication attempts limited sessions no TCP or agent forwarding and modern cipher suites including ChaCha20-Poly1305 and AES-256-GCM with ETM MACs. AppArmor is enforced with cache enabled. The apparmor-profiles package provides additional profiles. Lockdown is set to confidentiality. Fail2ban monitors SSH and HTTP services. Audit logging captures security relevant events. + +## Filesystem Architecture + +The filesystem uses ZFS as the primary filesystem with impermanence for root immutability. ZFS pools are created with encryption compression using zstd-3 atime disabled and automatic trim enabled. ARC size is configurable with a default of 8 GB. Snapshot management uses sanoid with configurable retention policies. Automatic scrub runs on a configurable schedule. + +Impermanence makes the root filesystem ephemeral. Only directories and files listed in environment.persistence.persist are preserved across reboots. User data in persist and home persists. System state including machine-id resolv.conf and SSH keys is explicitly persisted. Disko provides declarative partitioning with disk layout defined in configuration not manual partitioning. Pre-rebuild and post-rebuild ZFS snapshots are created automatically via sanoid. The previous generation remains bootable through the boot menu entry. + +## Container Architecture + +Containers use MicroVM for hardware level isolation. Each guest runs as a separate microvm with dedicated vCPU memory and storage resources. The host configures a bridge interface called microvm for guest networking. Guests connect through this bridge. Socket forwarding enables X11 and Wayland forwarding for desktop application containers. + +The orchestrator manages guest lifecycles including create start stop and destroy. It uses cgroup v2 for resource isolation at the pool level. The instance pool provides dynamic scaling. Key parameters include maxInstances basePort memPerInstance cpuPerInstance storagePerInstance appPackage appCommand and healthcheckCmd. The pool manager automatically spawns new instances up to the configured maximum and performs health checks on running instances. Caddy serves as a reverse proxy routing requests to the appropriate instance based on port mapping. The cgroup v2 hierarchy for containers is structured as sys fs cgroup hostname pool instance-001 and instance-002 each with cpu.max memory.max pids.max and io.max limits. + +## Desktop Architecture + +The desktop environment uses KDE Plasma 6 with a custom Bora layout. KDE Plasma 6 is installed with essential components only including plasma-desktop kwin konsole dolphin kscreen plasma-nm plasma-pa bluedevil powerdevil kdecoration-viewer kactivitymanagerd and polkit-kde-agent-1. Discover and PIM applications are excluded. + +The Bora layout provides a top bar with global menu application launcher system tray clock and workspace switcher. A dock with favorites running applications and trash. Custom window decorations and button layout. A custom color scheme called BoraDark with a dark cosmic background and cyan accent. The color scheme uses background value 0A0C16 alternate background value 11131F foreground value C0C5D4 selection background value 7B2FBE selection foreground value FFFFFF active titlebar value 1A1C2B inactive titlebar value 0A0C16 accent value 00D4FF link value 00D4FF and visited link value 7B2FBE. + +PipeWire provides audio with WirePlumber session manager and low latency configuration for real time audio. Desktop initialization scripts run at first login to configure the panel layout window rules and keyboard shortcuts through the KDE configuration system using kwriteconfig6. + +## Build and Deploy + +Build targets are defined in flake.nix outputs. nixosConfigurations.hostname provides standard NixOS configuration build. packages.system.iso-minimal provides minimal ISO image without desktop. packages.system.iso-graphical provides full ISO with desktop environment. The flake uses nixos-generators for ISO creation. The ISO configuration includes ZFS vfat and xfs filesystem support. + +The deployment workflow starts by setting hostname and username in src/hosts/target-host/meta.nix and optionally overriding the profile. Then run nixos-install flake hash target-host. Set a password for the user. Then reboot. Post deployment validation includes verifying ZFS pools and datasets are created correctly verifying firewall rules with nft list ruleset verifying SSH is accessible only via key from LAN verifying desktop layout has top bar dock and correct color scheme verifying microvm bridge interface exists and verifying cgroup v2 hierarchy is populated. + +The rollback procedure involves selecting the previous generation from the boot menu or running nixos-rebuild switch rollback. Verify pre-rebuild ZFS snapshot exists via zfs list t snapshot. diff --git a/docs/BORA-ARCH.md b/docs/BORA-ARCH.md deleted file mode 100644 index 86a42d3..0000000 --- a/docs/BORA-ARCH.md +++ /dev/null @@ -1,272 +0,0 @@ -BORA NixOS - Architecture Document -Version 2.0.0 - -Table of Contents - -1. Introduction -2. Repository Structure -3. Core Design Principles -4. Module System -5. Host and Profile System -6. Spring Framework (DI/IoC) -7. Security Architecture -8. Filesystem Architecture -9. Container Architecture -10. Desktop Architecture -11. Build and Deploy - - -1. Introduction - -BORA is a modular, immutable NixOS configuration framework built on four pillars: - -Zero Hardcoding: Every value is parameterized through Nix options. No usernames, hostnames, paths, IPs, or hardware identifiers are hardcoded. Host-specific values are declared in meta.nix files and injected via specialArgs. - -Zero Comments: Nix files contain no comments. All technical documentation lives in AGENTS.md. User documentation lives in docs/. This enforces self-documenting code through meaningful identifier names and pure functional patterns. - -Zero Inline Shell: Every shell script is stored as a standalone file in scripts/ and referenced via builtins.readFile. No script content appears inside Nix strings. - -Pure Functions: Library functions in lib/ are pure with no side effects. Module evaluation is deterministic and idempotent. - - -2. Repository Structure - -The repository follows a strict directory layout enforced by the module loader and build system. - -Top Level: - -flake.nix is the entry point. It declares inputs (nixpkgs, nixos-hardware, microvm, sops-nix, nixos-generators) and defines outputs for each discovered host plus ISO generation. - -configuration.nix is the module loader. It auto-scans src/modules/ for category directories, imports each category's default.nix, and loads the selected profile from src/profiles/. - -Source Tree: - -src/hosts/ contains per-machine configurations. Each subdirectory is named after the host and contains meta.nix (system, hardware, profile, hostname, username), default.nix (host-specific config), and hardware.nix (generated hardware scan). - -src/profiles/ defines use-case configurations: workstation (desktop + KDE + Bora layout), developer (workstation + dev tools), server (headless + container orchestrator), minimal (headless minimal). - -src/modules/ is organized by category: core, filesystem, security, containers, desktop, hardware, network. Each category has a default.nix that imports submodules. - -src/guests/ defines MicroVM guest templates. The sandbox.nix is a generic template. The example/ directory demonstrates a concrete instance definition with pool configuration. - -lib/ contains pure Nix library functions: hardware detection database, Spring DI/IoC framework with circuit breaker, and the library aggregator. - -config/ holds external configuration files referenced by modules: desktop panel layouts, NFTables rulesets, container bridge configuration. - -scripts/ holds standalone shell scripts organized by subsystem: spring services, desktop initialization, pool management. - -assets/ is reserved for static files: wallpapers, themes, fonts, icons. - -secrets/ is reserved for encrypted secrets via SOPS and age. - -tests/ contains pure Nix evaluation tests and a shell environment for linting with statix, deadnix, and nixpkgs-fmt. - -docs/ contains documentation in plain text format. - - -3. Core Design Principles - -Single Responsibility: Each .nix file has exactly one purpose. A file equals one module equals one function. No file mixes concerns. - -Auto-Discovery: The configuration.nix module loader scans src/modules/ at evaluation time. No manual imports are needed when adding new modules. Each category's default.nix imports all submodules within that category. - -Conditional Activation: Modules are enabled or disabled via mkIf cfg.enable. The enable option is declared in the module's options block. Profiles activate combinations of modules by setting these options to their preferred values. - -Idempotency: nixos-rebuild switch is idempotent. Running it twice produces the same result. No state is modified outside the Nix store. User state lives exclusively in /persist and /home. The root filesystem is ephemeral through impermanence. - -Atomicity: Every nixos-rebuild produces a new generation. The previous generation remains intact and selectable from the boot menu. Rollback uses nixos-rebuild switch --rollback. ZFS snapshots are taken automatically before and after rebuild via sanoid. - -Parameterization: All configurable values use Nix options with mkOption. Default values use mkDefault so they can be overridden. Conditional values use mkIf. No literal values appear in configuration logic. - - -4. Module System - -Modules are organized into categories under src/modules/. Each category represents a subsystem of the operating system. - -core/: Boot configuration (systemd-boot, kernel parameters, initrd), Nix daemon settings (auto-optimise, garbage collection, substituters), locale and timezone, sysctl kernel parameters. - -filesystem/: ZFS pool creation, ARC tuning, automatic trimming, scrub scheduling, sanoid snapshot retention, disko partitioning, impermanence configuration with persistent directories and files. - -security/: NFTables firewall with default-drop policy, kernel hardening through sysctl, AppArmor enforcement with lockdown, SSH server hardening (key-only, rate-limited, minimal ciphers), Fail2ban for brute-force protection, audit logging. - -containers/: MicroVM host configuration with bridge networking, orchestrator for managing guest lifecycles, instance pool for dynamic scaling of guest instances with cgroup v2 resource isolation. - -desktop/: KDE Plasma 6 minimal installation, PipeWire audio server, Bora custom desktop layout (top bar, dock, global menu, cosmic dark theme). - -hardware/: CPU-specific optimizations (Intel, AMD, ARM), GPU drivers and configuration (NVIDIA, AMD, Intel), platform tuning (desktop, laptop, server). - -network/: Base network configuration, DNS resolver settings. - -Each module defines an options block with an enable flag and all configurable parameters. The config block is wrapped in mkIf cfg.enable. Assertions validate parameter combinations at evaluation time. - -New modules follow this pattern: - -Create src/modules/category/name.nix with options and config. -Update src/modules/category/default.nix to import the new file. -Define options with mkOption and use mkIf for conditional config. -Use assertions to validate constraints. - - -5. Host and Profile System - -Each host is defined in src/hosts/hostname/. The meta.nix file declares four attributes: - -system: The NixOS system architecture (e.g. x86_64-linux). -hardware: The hardware class (desktop, laptop, server). -profile: The use-case profile name (workstation, developer, server, minimal). -hostname: The machine hostname. -username: The primary user name. - -The flake.nix reads src/hosts/ to discover available hosts. It passes hostname and username from meta.nix as specialArgs to the NixOS configuration. This eliminates all hardcoded user and host references. - -Profiles in src/profiles/ define combinations of enabled modules. A profile sets bora options to mkDefault values, establishing the baseline configuration for that use case. Profiles inherit from more basic profiles where applicable. - -The configuration.nix loads the profile specified in meta.nix using a dynamic import based on the profile attribute. - - -6. Spring Framework (DI/IoC) - -The Spring framework in lib/spring.nix provides dependency injection and circuit breaker patterns for systemd services. - -Bean definitions use the bora.spring.beans attribute set. Each bean specifies: - -class: A service type identifier for organizational purposes. -deps: A list of bean names this bean depends on. -resources: Resource limits for cgroup v2 isolation (cpu, memory, memoryMax, pids, ioRbps, ioWbps, numa). -healthcheck: A command that returns zero for healthy service. -dependsOn: Systemd unit dependencies. -after: Systemd unit ordering. -restartPolicy: Systemd restart policy. - -The framework performs topological sort of bean dependencies at build time. Circular dependencies cause a build failure with a diagnostic message listing the cycle. - -The circuit breaker implements a three-state machine: - -CLOSED: Normal operation. Requests pass through. Failures increment a counter. -OPEN: Circuit is open. Requests are blocked. A timeout timer starts. -HALF-OPEN: Recovery test. Limited requests are allowed through. - -Transitions: - -CLOSED to OPEN when failures reach threshold (default 5). -OPEN to HALF-OPEN after timeout (default 30 seconds). -HALF-OPEN to CLOSED when successes reach threshold (default 2). -HALF-OPEN to OPEN on any failure in half-open state. - -Cgroup v2 hierarchy is created under /sys/fs/cgroup/hostname/bean-name/ with cpu.max, memory.max, pids.max, and io.max limits. OOM policy is set to kill for all Spring services. - -Health check flow executes the healthcheck command periodically. Success calls circuit_success which may transition to CLOSED. Failure calls circuit_trip which may transition to OPEN. When the circuit is OPEN, the service exits with code 1. - - -7. Security Architecture - -Security is implemented in layers. - -Kernel hardening uses sysctl parameters to restrict kernel pointer access, dmesg access, performance events, ptrace, BPF, kexec, and SysRq. ASLR is set to maximum. Unprivileged BPF is disabled. BPF JIT is disabled. - -The firewall uses NFTables with a default-drop policy on the input chain. Only established/related connections, loopback traffic, rate-limited ICMP, and SSH from LAN addresses are accepted. The forward chain accepts established/related connections and traffic from the microvm bridge interface. The output chain has a default-accept policy. - -SSH is hardened with no root login, no password authentication, key-only access, rate-limited authentication attempts, limited sessions, no TCP or agent forwarding, and modern cipher suites (ChaCha20-Poly1305, AES-256-GCM) with ETM MACs. - -AppArmor is enforced with cache enabled. The apparmor-profiles package provides additional profiles. Lockdown is set to confidentiality. - -Fail2ban monitors SSH and HTTP services. Audit logging captures security-relevant events. - - -8. Filesystem Architecture - -The filesystem uses ZFS as the primary filesystem with impermanence for root immutability. - -ZFS pools are created with encryption, compression (zstd-3), atime disabled, and automatic trim enabled. ARC size is configurable with a default of 8 GB. Snapshot management uses sanoid with configurable retention policies. Automatic scrub runs on a configurable schedule. - -Impermanence makes the root filesystem ephemeral. Only directories and files listed in environment.persistence./persist are preserved across reboots. User data in /persist and /home persists. System state (machine-id, resolv.conf, SSH keys) is explicitly persisted. - -Disko provides declarative partitioning. Disk layout is defined in configuration, not manual partitioning. - -Pre-rebuild and post-rebuild ZFS snapshots are created automatically via sanoid. The previous generation remains bootable through the boot menu entry. - - -9. Container Architecture - -Containers use MicroVM for hardware-level isolation. Each guest runs as a separate microvm with dedicated vCPU, memory, and storage resources. - -The host configures a bridge interface (microvm) for guest networking. Guests connect through this bridge. Socket forwarding enables X11 and Wayland forwarding for desktop application containers. - -The orchestrator manages guest lifecycles: create, start, stop, destroy. It uses cgroup v2 for resource isolation at the pool level. - -The instance pool provides dynamic scaling. Key parameters include maxInstances, basePort, memPerInstance, cpuPerInstance, storagePerInstance, appPackage, appCommand, and healthcheckCmd. The pool manager automatically spawns new instances up to the configured maximum and performs health checks on running instances. - -Caddy serves as a reverse proxy, routing requests to the appropriate instance based on port mapping. - -Cgroup v2 hierarchy for containers: - -/sys/fs/cgroup/ - hostname/ - pool/ - instance-001/ cpu.max, memory.max, pids.max, io.max - instance-002/ same - - -10. Desktop Architecture - -The desktop environment uses KDE Plasma 6 with a custom Bora layout. - -KDE Plasma 6 is installed with essential components only: plasma-desktop, kwin, konsole, dolphin, kscreen, plasma-nm, plasma-pa, bluedevil, powerdevil, kdecoration-viewer, kactivitymanagerd, polkit-kde-agent-1. Discover and PIM applications are excluded. - -The Bora layout provides: - -A top bar with global menu, application launcher, system tray, clock, and workspace switcher. -A dock with favorites, running applications, and trash. -Custom window decorations and button layout. -A custom color scheme (BoraDark) with a dark cosmic background and cyan accent. - -The color scheme uses these values: - -Background: #0A0C16 -Alternate Background: #11131F -Foreground: #C0C5D4 -Selection Background: #7B2FBE -Selection Foreground: #FFFFFF -Active Titlebar: #1A1C2B -Inactive Titlebar: #0A0C16 -Accent: #00D4FF -Link: #00D4FF -Visited Link: #7B2FBE - -PipeWire provides audio with WirePlumber session manager and low-latency configuration for real-time audio. - -Desktop initialization scripts run at first login to configure the panel layout, window rules, and keyboard shortcuts through the KDE configuration system (kwriteconfig6). - - -11. Build and Deploy - -Build targets are defined in flake.nix outputs: - -nixosConfigurations.hostname: Standard NixOS configuration build. -packages.system.iso-minimal: Minimal ISO image without desktop. -packages.system.iso-graphical: Full ISO with desktop environment. - -The flake uses nixos-generators for ISO creation. The ISO configuration includes ZFS, vfat, and xfs filesystem support. - -Deployment workflow: - -Set hostname and username in src/hosts/target-host/meta.nix. -Optionally override profile. -Run nixos-install --flake .#target-host. -Set a password for the user. -Reboot. - -Post-deployment validation: - -Verify ZFS pools and datasets are created correctly. -Verify firewall rules with nft list ruleset. -Verify SSH is accessible only via key from LAN. -Verify desktop layout has top bar, dock, and correct color scheme. -Verify microvm bridge interface exists. -Verify cgroup v2 hierarchy is populated. - -Rollback procedure: - -Select previous generation from boot menu. -Or run nixos-rebuild switch --rollback. -Verify pre-rebuild ZFS snapshot exists via zfs list -t snapshot. diff --git a/docs/MANUAL.md b/docs/MANUAL.md new file mode 100644 index 0000000..442bfa8 --- /dev/null +++ b/docs/MANUAL.md @@ -0,0 +1,37 @@ +# BORA NixOS User Manual + +## Getting Started + +To deploy BORA on a new machine first create a host directory under src/hosts with a meta.nix file containing the system architecture hardware profile hostname and username. Then run nixos-install flake hash target-host. After installation reboot and verify the system. + +## Host Configuration + +Each host is defined in src/hosts/hostname. The meta.nix file must export an attribute set with four keys. system is the architecture like x86_64-linux. hardware is the hardware class like desktop laptop or server. profile is the use case profile like workstation developer server or minimal. hostname is the machine hostname. username is the primary user name. + +## Profile Selection + +Profiles define which modules are enabled. The workstation profile enables desktop KDE and Bora layout. The developer profile adds development tools. The server profile is headless with container orchestrator. The minimal profile is a headless base system. + +## Filesystem and Storage + +The default filesystem layout uses ZFS with encryption and compression. The root filesystem is ephemeral through impermanence with persistent data stored under persist. The disko module provides declarative partitioning. + +## Container Engine + +MicroVM guests provide hardware level isolation. The orchestrator manages guest lifecycles. The instance pool provides dynamic scaling with cgroup v2 resource limits. Caddy serves as reverse proxy for routed instances. + +## Desktop Environment + +KDE Plasma 6 minimal with custom Bora layout including top bar dock global menu and dark color scheme. PipeWire provides audio with low latency configuration. + +## Security + +Firewall default drop with NFTables. Kernel hardening through sysctl. SSH key only from LAN. AppArmor enforced. Fail2ban active. Audit logging enabled. + +## Spring Framework + +Dependency injection for systemd services with circuit breaker. Bean definitions per service. Health based auto recovery. Cgroup v2 resource isolation. + +## Maintenance + +For system updates run nix flake update to update all inputs then nixos-rebuild switch to apply. For ZFS maintenance monitor pool status with zpool status check scrub progress with zpool scrub and list snapshots with zfs list t snapshot. For troubleshooting check service status with systemctl status bora bean name view logs with journalctl u bora bean name verify firewall with nft list ruleset and check cgroup usage with systemd cgtop. diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..f5092c7 --- /dev/null +++ b/flake.lock @@ -0,0 +1,297 @@ +{ + "nodes": { + "disko": { + "inputs": { + "nixpkgs": "nixpkgs" + }, + "locked": { + "lastModified": 1779135526, + "narHash": "sha256-glCununz6lmaK5fs2X946HA3EkNxB2JagdAAvInuRYU=", + "owner": "nix-community", + "repo": "disko", + "rev": "d405a179887d52b24c0ddd31e09a150bd1f66779", + "type": "github" + }, + "original": { + "owner": "nix-community", + "repo": "disko", + "type": "github" + } + }, + "home-manager": { + "inputs": { + "nixpkgs": "nixpkgs_2" + }, + "locked": { + "lastModified": 1779213149, + "narHash": "sha256-Cf+p/T4Z3n9Sw0TiR3kQaIwQI+/hfvLJcoTzeq6yS3E=", + "owner": "nix-community", + "repo": "home-manager", + "rev": "bd868f769a69d3b6091a1da68a75cb83a181033c", + "type": "github" + }, + "original": { + "owner": "nix-community", + "repo": "home-manager", + "type": "github" + } + }, + "home-manager_2": { + "inputs": { + "nixpkgs": [ + "impermanence", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1768598210, + "narHash": "sha256-kkgA32s/f4jaa4UG+2f8C225Qvclxnqs76mf8zvTVPg=", + "owner": "nix-community", + "repo": "home-manager", + "rev": "c47b2cc64a629f8e075de52e4742de688f930dc6", + "type": "github" + }, + "original": { + "owner": "nix-community", + "repo": "home-manager", + "type": "github" + } + }, + "impermanence": { + "inputs": { + "home-manager": "home-manager_2", + "nixpkgs": "nixpkgs_3" + }, + "locked": { + "lastModified": 1769548169, + "narHash": "sha256-03+JxvzmfwRu+5JafM0DLbxgHttOQZkUtDWBmeUkN8Y=", + "owner": "nix-community", + "repo": "impermanence", + "rev": "7b1d382faf603b6d264f58627330f9faa5cba149", + "type": "github" + }, + "original": { + "owner": "nix-community", + "repo": "impermanence", + "type": "github" + } + }, + "microvm": { + "inputs": { + "nixpkgs": [ + "nixpkgs" + ], + "spectrum": "spectrum" + }, + "locked": { + "lastModified": 1779043402, + "narHash": "sha256-1dH6yiwEck1n3oz5TDUV5TGbwNqu46eNTVHbhFO2U5k=", + "owner": "microvm-nix", + "repo": "microvm.nix", + "rev": "77024c22f4ddf509137fc732094888d1ffe631e2", + "type": "github" + }, + "original": { + "owner": "microvm-nix", + "repo": "microvm.nix", + "type": "github" + } + }, + "nixlib": { + "locked": { + "lastModified": 1736643958, + "narHash": "sha256-tmpqTSWVRJVhpvfSN9KXBvKEXplrwKnSZNAoNPf/S/s=", + "owner": "nix-community", + "repo": "nixpkgs.lib", + "rev": "1418bc28a52126761c02dd3d89b2d8ca0f521181", + "type": "github" + }, + "original": { + "owner": "nix-community", + "repo": "nixpkgs.lib", + "type": "github" + } + }, + "nixos-generators": { + "inputs": { + "nixlib": "nixlib", + "nixpkgs": [ + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1769813415, + "narHash": "sha256-nnVmNNKBi1YiBNPhKclNYDORoHkuKipoz7EtVnXO50A=", + "owner": "nix-community", + "repo": "nixos-generators", + "rev": "8946737ff703382fda7623b9fab071d037e897d5", + "type": "github" + }, + "original": { + "owner": "nix-community", + "repo": "nixos-generators", + "type": "github" + } + }, + "nixos-hardware": { + "locked": { + "lastModified": 1779099457, + "narHash": "sha256-u73aVD/lUmmT3JV+kPDztl7zPwQKd0eobD1AbJltaGs=", + "owner": "NixOS", + "repo": "nixos-hardware", + "rev": "8792fab9d4a6454a9201675f01326f827ce35ead", + "type": "github" + }, + "original": { + "owner": "NixOS", + "repo": "nixos-hardware", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1773628058, + "narHash": "sha256-hpXH0z3K9xv0fHaje136KY872VT2T5uwxtezlAskQgY=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "f8573b9c935cfaa162dd62cc9e75ae2db86f85df", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixpkgs-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs-unstable": { + "locked": { + "lastModified": 1778869304, + "narHash": "sha256-30sZNZoA1cqF5JNO9fVX+wgiQYjB7HJqqJ4ztCDeBZE=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "d233902339c02a9c334e7e593de68855ad26c4cb", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixpkgs-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs_2": { + "locked": { + "lastModified": 1778443072, + "narHash": "sha256-zi7/fsqM/kFdNuED//4WOCUtezGtKKqRNORjMvfwjnA=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "da5ad661ba4e5ef59ba743f0d112cbc30e474f32", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs_3": { + "locked": { + "lastModified": 1768564909, + "narHash": "sha256-Kell/SpJYVkHWMvnhqJz/8DqQg2b6PguxVWOuadbHCc=", + "owner": "nixos", + "repo": "nixpkgs", + "rev": "e4bae1bd10c9c57b2cf517953ab70060a828ee6f", + "type": "github" + }, + "original": { + "owner": "nixos", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs_4": { + "locked": { + "lastModified": 1779102034, + "narHash": "sha256-vZJZjLo513IeI8hjzHFc6TDezUd4uCE2Eq4SNO3DNNg=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "687f05a9184cad4eaf905c48b63649e3a86f5433", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-25.11", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs_5": { + "locked": { + "lastModified": 1775888245, + "narHash": "sha256-nwASzrRDD1JBEu/o8ekKYEXm/oJW6EMCzCRdrwcLe90=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "13043924aaa7375ce482ebe2494338e058282925", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixpkgs-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "disko": "disko", + "home-manager": "home-manager", + "impermanence": "impermanence", + "microvm": "microvm", + "nixos-generators": "nixos-generators", + "nixos-hardware": "nixos-hardware", + "nixpkgs": "nixpkgs_4", + "nixpkgs-unstable": "nixpkgs-unstable", + "sops-nix": "sops-nix" + } + }, + "sops-nix": { + "inputs": { + "nixpkgs": "nixpkgs_5" + }, + "locked": { + "lastModified": 1777944972, + "narHash": "sha256-VfGRo1qTBKOe3s2gOv8LSoA6Fk19PvBlwQ1ECN0Evn8=", + "owner": "Mic92", + "repo": "sops-nix", + "rev": "c591bf665727040c6cc5cb409079acb22dcce33c", + "type": "github" + }, + "original": { + "owner": "Mic92", + "repo": "sops-nix", + "type": "github" + } + }, + "spectrum": { + "flake": false, + "locked": { + "lastModified": 1778940603, + "narHash": "sha256-voSM8dZNlaOWN3kbYFky+FNY6fFQOEw0xF+ZMpZKkCQ=", + "ref": "refs/heads/main", + "rev": "367dd227f539267eae2b62770b4c17b88ac8c1f1", + "revCount": 1265, + "type": "git", + "url": "https://spectrum-os.org/git/spectrum" + }, + "original": { + "type": "git", + "url": "https://spectrum-os.org/git/spectrum" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix index cde7c35..fa9152b 100644 --- a/flake.nix +++ b/flake.nix @@ -2,7 +2,7 @@ description = "Bora NixOS — Modulare · Atomico · Universale · Strict-Hard — ALPHA v0.1.0"; inputs = { - nixpkgs.url = "github:NixOS/nixpkgs/nixos-24.11"; + nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.11"; nixpkgs-unstable.url = "github:NixOS/nixpkgs/nixpkgs-unstable"; nixos-hardware.url = "github:NixOS/nixos-hardware"; nixos-generators = { @@ -11,7 +11,7 @@ }; impermanence.url = "github:nix-community/impermanence"; microvm = { - url = "github:astro/microvm"; + url = "github:microvm-nix/microvm.nix"; inputs.nixpkgs.follows = "nixpkgs"; }; disko.url = "github:nix-community/disko"; @@ -19,25 +19,35 @@ home-manager.url = "github:nix-community/home-manager"; }; - outputs = { self, nixpkgs, nixpkgs-unstable, nixos-hardware - , nixos-generators, impermanence, microvm, disko - , sops-nix, home-manager, ... - }: + outputs = + { self + , nixpkgs + , nixpkgs-unstable + , nixos-hardware + , nixos-generators + , impermanence + , microvm + , disko + , sops-nix + , home-manager + , ... + }: let systems = [ "x86_64-linux" "aarch64-linux" ]; forAllSystems = f: nixpkgs.lib.genAttrs systems (system: f system); - boraLib = import ./lib { inherit nixpkgs; }; inherit (builtins) readDir attrNames; inherit (nixpkgs.lib) optional; hostsDir = ./src/hosts; availableHosts = attrNames (readDir hostsDir); + hardwareDB = import ./lib/hardware.nix { lib = nixpkgs.lib; }; + mkHost = hostname: hostConfig: nixpkgs.lib.nixosSystem { system = hostConfig.system or "x86_64-linux"; specialArgs = { - inherit boraLib self; + inherit hardwareDB self; hostname = hostname; username = hostConfig.username or "user"; hardwareProfile = hostConfig.hardware or "desktop"; @@ -54,55 +64,129 @@ ] ++ optional (hostConfig ? extraModules) hostConfig.extraModules; }; - hosts = builtins.foldl' (acc: hostname: - let - hostConfig = - if builtins.pathExists (hostsDir + "/${hostname}/meta.nix") - then import (hostsDir + "/${hostname}/meta.nix") - else { }; - in acc // { - ${hostname} = mkHost hostname hostConfig; - } - ) { } availableHosts; + hosts = builtins.foldl' + (acc: hostname: + let + hostConfig = + if builtins.pathExists (hostsDir + "/${hostname}/meta.nix") + then import (hostsDir + "/${hostname}/meta.nix") + else { }; + in + acc // { + ${hostname} = mkHost hostname hostConfig; + } + ) + { } + availableHosts; - in { + in + { nixosConfigurations = hosts; packages = forAllSystems (system: let pkgs = import nixpkgs { inherit system; config.allowUnfree = true; }; - in { + in + { iso-minimal = nixos-generators.nixosGenerate { inherit system; + specialArgs = { + inherit hardwareDB; + hostname = "bora-iso"; + username = "bora"; + hardwareProfile = "desktop"; + systemProfile = "minimal"; + }; + modules = [ + impermanence.nixosModules.impermanence + microvm.nixosModules.host + disko.nixosModules.disko + ./configuration.nix + ({ pkgs, lib, ... }: { + image.baseName = lib.mkDefault "bora"; + boot.supportedFilesystems = [ "zfs" "vfat" "xfs" ]; + boot.kernelPackages = pkgs.linuxPackages_6_6; + nixpkgs.config.allowUnfree = true; + system.stateVersion = "25.11"; + users.users.bora = { isNormalUser = true; }; + }) + ]; + format = "iso"; + }; + + iso-desktop = nixos-generators.nixosGenerate { + inherit system; + specialArgs = { + inherit hardwareDB; + hostname = "bora-iso"; + username = "bora"; + hardwareProfile = "desktop"; + systemProfile = "workstation"; + }; + modules = [ + impermanence.nixosModules.impermanence + microvm.nixosModules.host + disko.nixosModules.disko + ./configuration.nix + ({ pkgs, lib, ... }: { + image.baseName = lib.mkDefault "bora-desktop"; + boot.supportedFilesystems = [ "zfs" "vfat" "xfs" ]; + boot.kernelPackages = pkgs.linuxPackages_6_6; + nixpkgs.config.allowUnfree = true; + system.stateVersion = "25.11"; + users.users.bora = { isNormalUser = true; }; + }) + ]; + format = "iso"; + }; + + iso-laptop = nixos-generators.nixosGenerate { + inherit system; + specialArgs = { + inherit hardwareDB; + hostname = "bora-iso"; + username = "bora"; + hardwareProfile = "laptop"; + systemProfile = "minimal"; + }; modules = [ impermanence.nixosModules.impermanence + microvm.nixosModules.host disko.nixosModules.disko ./configuration.nix - ({ pkgs, ... }: { - isoImage.isoBaseName = "bora"; - isoImage.compress = true; + ({ pkgs, lib, ... }: { + image.baseName = lib.mkDefault "bora-laptop"; boot.supportedFilesystems = [ "zfs" "vfat" "xfs" ]; + boot.kernelPackages = pkgs.linuxPackages_6_6; nixpkgs.config.allowUnfree = true; - system.stateVersion = "24.11"; + system.stateVersion = "25.11"; + users.users.bora = { isNormalUser = true; }; }) ]; format = "iso"; }; - iso-graphical = nixos-generators.nixosGenerate { + iso-server = nixos-generators.nixosGenerate { inherit system; + specialArgs = { + inherit hardwareDB; + hostname = "bora-iso"; + username = "bora"; + hardwareProfile = "server"; + systemProfile = "server"; + }; modules = [ impermanence.nixosModules.impermanence + microvm.nixosModules.host disko.nixosModules.disko - "${nixpkgs}/nixos/modules/installer/cd-dvd/installation-cd-graphical-kde.nix" ./configuration.nix - ({ pkgs, ... }: { - isoImage.isoBaseName = "bora-desktop"; - isoImage.compress = true; + ({ pkgs, lib, ... }: { + image.baseName = lib.mkDefault "bora-server"; boot.supportedFilesystems = [ "zfs" "vfat" "xfs" ]; + boot.kernelPackages = pkgs.linuxPackages_6_6; nixpkgs.config.allowUnfree = true; - services.xserver.enable = true; - system.stateVersion = "24.11"; + system.stateVersion = "25.11"; + users.users.bora = { isNormalUser = true; }; }) ]; format = "iso"; @@ -113,9 +197,11 @@ let pkgs = import nixpkgs { inherit system; config.allowUnfree = true; }; in { default = pkgs.mkShell { + name = "bora-dev-shell"; buildInputs = with pkgs; [ - nixos-generators nixos-anywhere - nixpkgs-fmt statix deadnix comma + nixpkgs-fmt + statix + deadnix ]; }; }); diff --git a/lib/default.nix b/lib/default.nix index e9dd728..d532bbb 100644 --- a/lib/default.nix +++ b/lib/default.nix @@ -1,9 +1,10 @@ { nixpkgs }: let - lib = nixpkgs.lib; + inherit (nixpkgs) lib; inherit (builtins) readDir pathExists; - inherit (lib) attrNames filter hasPrefix mapAttrsToList optionalString; -in { + inherit (lib) attrNames filter hasPrefix; +in +{ hardware = import ./hardware.nix { inherit lib; }; atomic = { preRebuildSnapshot = pool: dataset: '' @@ -17,6 +18,7 @@ in { let isDir = path: (pathExists (dir + "/${path}")) && (hasPrefix "." path); entries = attrNames (readDir dir); - dirs = filter (name: isDir name) entries; - in map (name: dir + "/${name}") dirs; + dirs = filter isDir entries; + in + map (name: dir + "/${name}") dirs; } diff --git a/lib/hardware.nix b/lib/hardware.nix index c342c9e..fb0e28b 100644 --- a/lib/hardware.nix +++ b/lib/hardware.nix @@ -1,7 +1,6 @@ -{ lib }: -let - inherit (lib) mkIf mkDefault mkMerge toList; -in rec { +_: + +rec { cpu = { intel = { name = "Intel"; @@ -87,22 +86,25 @@ in rec { desktop = { powerManagement.enable = true; powerManagement.cpuFreqGovernor = "performance"; - services.power-profiles-daemon.enable = false; - hardware.nvidia.prime.sync.enable = true; + services.power-profiles-daemon = { enable = false; }; }; laptop = { powerManagement.enable = true; powerManagement.cpuFreqGovernor = "powersave"; - services.power-profiles-daemon.enable = true; - services.tlp.enable = true; - services.auto-cpufreq.enable = true; + services = { + power-profiles-daemon = { enable = true; }; + tlp = { enable = true; }; + auto-cpufreq = { enable = true; }; + }; boot.kernelParams = [ "acpi_osi=Linux" ]; }; server = { powerManagement.enable = false; powerManagement.cpuFreqGovernor = "performance"; - services.power-profiles-daemon.enable = false; - services.tlp.enable = false; + services = { + power-profiles-daemon = { enable = false; }; + tlp = { enable = false; }; + }; boot.kernelParams = [ "nmi_watchdog=0" "nowatchdog" ]; }; }; diff --git a/lib/spring.nix b/lib/spring.nix index f1c668d..07f24af 100644 --- a/lib/spring.nix +++ b/lib/spring.nix @@ -1,16 +1,16 @@ -{ lib, pkgs, config ? null, ... }: +{ lib, pkgs, ... }: let inherit (builtins) toString mapAttrs; inherit (lib) - types mkOption mkIf mkEnableOption mkDefault + types mkOption mkEnableOption mapAttrsToList filterAttrs - optional optionals optionalString concatStringsSep - any all attrValues elem; -in rec { - springType = types.submodule ({ name, ... }: { + optional any attrValues; +in +rec { + springType = types.submodule (_: { options = { application = mkOption { - type = types.submodule ({ ... }: { + type = types.submodule (_: { options = { enable = mkEnableOption "Spring application context"; name = mkOption { type = types.str; }; @@ -22,10 +22,10 @@ in rec { globalLimits = mkOption { type = types.submodule { options = { - memory = mkOption { type = types.str; default = "0" ; }; - cpu = mkOption { type = types.str; default = "0" ; }; - pids = mkOption { type = types.int; default = 0 ; }; - io = mkOption { type = types.str; default = "0" ; }; + memory = mkOption { type = types.str; default = "0"; }; + cpu = mkOption { type = types.str; default = "0"; }; + pids = mkOption { type = types.int; default = 0; }; + io = mkOption { type = types.str; default = "0"; }; }; }; default = { }; @@ -34,10 +34,10 @@ in rec { type = types.submodule { options = { enable = mkEnableOption "Circuit breaker"; - failureThreshold = mkOption { type = types.int; default = 5 ; }; - successThreshold = mkOption { type = types.int; default = 2 ; }; - timeoutMs = mkOption { type = types.int; default = 30000 ; }; - halfOpenMax = mkOption { type = types.int; default = 3 ; }; + failureThreshold = mkOption { type = types.int; default = 5; }; + successThreshold = mkOption { type = types.int; default = 2; }; + timeoutMs = mkOption { type = types.int; default = 30000; }; + halfOpenMax = mkOption { type = types.int; default = 3; }; }; }; default = { }; @@ -47,7 +47,7 @@ in rec { default = { }; }; beans = mkOption { - type = types.attrsOf (types.submodule ({ ... }: { + type = types.attrsOf (types.submodule (_: { options = { enable = mkEnableOption "Spring bean"; class = mkOption { type = types.str; }; @@ -55,14 +55,14 @@ in rec { resources = mkOption { type = types.submodule { options = { - cpu = mkOption { type = types.str; default = "1" ; }; - memory = mkOption { type = types.str; default = "256M" ; }; - memoryMax = mkOption { type = types.str; default = "512M" ; }; - pids = mkOption { type = types.int; default = 256 ; }; - ioRbps = mkOption { type = types.str; default = "100M" ; }; - ioRops = mkOption { type = types.int; default = 1000 ; }; - ioWbps = mkOption { type = types.str; default = "100M" ; }; - ioWops = mkOption { type = types.int; default = 1000 ; }; + cpu = mkOption { type = types.str; default = "1"; }; + memory = mkOption { type = types.str; default = "256M"; }; + memoryMax = mkOption { type = types.str; default = "512M"; }; + pids = mkOption { type = types.int; default = 256; }; + ioRbps = mkOption { type = types.str; default = "100M"; }; + ioRops = mkOption { type = types.int; default = 1000; }; + ioWbps = mkOption { type = types.str; default = "100M"; }; + ioWops = mkOption { type = types.int; default = 1000; }; numa = mkOption { type = types.nullOr (types.listOf types.int); default = null; @@ -123,51 +123,62 @@ in rec { beanGraph = beans: let names = attrNames beans; - edges = map (n: { - name = n; - deps = beans.${n}.deps or [ ]; - dependsOn = beans.${n}.dependsOn or [ ]; - allDeps = (beans.${n}.deps or [ ]) ++ (beans.${n}.dependsOn or [ ]); - }) names; - in edges; + edges = map + (n: { + name = n; + deps = beans.${n}.deps or [ ]; + dependsOn = beans.${n}.dependsOn or [ ]; + allDeps = (beans.${n}.deps or [ ]) ++ (beans.${n}.dependsOn or [ ]); + }) + names; + in + edges; tsort = items: let - sorted = lib.toposort (a: b: - let - aDependsOnB = any (dep: dep == b.name) a.allDeps; - bDependsOnA = any (dep: dep == a.name) b.allDeps; - in + sorted = lib.toposort + (a: b: + let + aDependsOnB = any (dep: dep == b.name) a.allDeps; + bDependsOnA = any (dep: dep == a.name) b.allDeps; + in if aDependsOnB then lib.TO_AFTER else if bDependsOnA then lib.TO_BEFORE else lib.TO_UNORDERED - ) items; - in sorted.result or [ ]; + ) + items; + in + sorted.result or [ ]; detectCircular = beans: let - result = lib.toposort (a: b: - let - aDependsOnB = any (dep: dep == b.name) a.allDeps; - bDependsOnA = any (dep: dep == a.name) b.allDeps; - in + result = lib.toposort + (a: b: + let + aDependsOnB = any (dep: dep == b.name) a.allDeps; + bDependsOnA = any (dep: dep == a.name) b.allDeps; + in if aDependsOnB then lib.TO_AFTER else if bDependsOnA then lib.TO_BEFORE else lib.TO_UNORDERED - ) (beanGraph beans); - in result.cyclic or [ ]; + ) + (beanGraph beans); + in + result.cyclic or [ ]; resolveBeanConfig = beans: let graph = beanGraph beans; sortedNames = map (n: n.name) (tsort graph); - in lib.listToAttrs (map (name: { - inherit name; - value = beans.${name}; - }) sortedNames); + in + lib.listToAttrs (map + (name: { + inherit name; + value = beans.${name}; + }) + sortedNames); scriptsDir = ../scripts/spring; - cgroupInit = builtins.readFile (scriptsDir + "/cgroup-init.sh"); circuitBreaker = builtins.readFile (scriptsDir + "/circuit-breaker.sh"); healthcheck = builtins.readFile (scriptsDir + "/healthcheck.sh"); beanWrapper = builtins.readFile (scriptsDir + "/bean-wrapper.sh"); @@ -181,7 +192,8 @@ in rec { depServices = map (d: "spring-${appName}-${d}.service") deps; afterServices = depServices ++ bean.after; wantServices = depServices ++ bean.wants; - in { + in + { "spring-${appName}-${beanName}" = { description = "Spring Bean: ${beanName} (${bean.class})"; after = afterServices; @@ -211,7 +223,6 @@ in rec { StartLimitBurst = toString (bean.resources.pids / 10); }; script = '' - ${cgroupInit} ${circuitBreaker} ${healthcheck} ${beanWrapper} @@ -248,33 +259,44 @@ in rec { let app = springConfig.application; appName = app.name; - beans = filterAttrs (n: v: v.enable) springConfig.beans; + beans = filterAttrs (_: v: v.enable) springConfig.beans; resolved = resolveBeanConfig beans; sortedBeans = attrValues resolved; - serviceDefs = builtins.foldl' (acc: bean: - let - name = builtins.elemAt (builtins.filter (n: beans.${n} == bean) (attrNames beans)) 0; - in acc // mkSystemdService appName name bean - ) { } sortedBeans; - in { - assertions = optional (springConfig.autowire.circularCheck) - (let cycles = detectCircular beans; in { - assertion = cycles == [ ]; - message = "Spring: circular dependency detected in beans: ${toString cycles}"; - }); - systemd.slices = mkSystemdSlice appName app; - systemd.services = serviceDefs; - systemd.extraConfig = '' - DefaultMemoryAccounting=yes - DefaultCPUAccounting=yes - DefaultIOAccounting=yes - DefaultTasksAccounting=yes - ''; + serviceDefs = builtins.foldl' + (acc: bean: + let + name = builtins.elemAt (builtins.filter (n: beans.${n} == bean) (attrNames beans)) 0; + in + acc // mkSystemdService appName name bean + ) + { } + sortedBeans; + in + { + assertions = optional springConfig.autowire.circularCheck + ( + let cycles = detectCircular beans; in { + assertion = cycles == [ ]; + message = "Spring: circular dependency detected in beans: ${toString cycles}"; + } + ); + systemd = { + slices = mkSystemdSlice appName app; + services = serviceDefs; + extraConfig = '' + DefaultMemoryAccounting=yes + DefaultCPUAccounting=yes + DefaultIOAccounting=yes + DefaultTasksAccounting=yes + ''; + }; environment.etc."spring/${appName}/beans.json".text = - builtins.toJSON (mapAttrs (n: v: { - inherit (v) class deps resources; - healthcheck = v.healthcheck or null; - }) beans); + builtins.toJSON (mapAttrs + (_: v: { + inherit (v) class deps resources; + healthcheck = v.healthcheck or null; + }) + beans); environment.systemPackages = [ (wrapBin "spring-${appName}-status" "spring-status.sh") (wrapBin "spring-${appName}-resources" "spring-resources.sh") diff --git a/scripts/build/iso-build.sh b/scripts/build/iso-build.sh new file mode 100755 index 0000000..1c86d5a --- /dev/null +++ b/scripts/build/iso-build.sh @@ -0,0 +1,43 @@ +#!/usr/bin/env bash +set -euo pipefail + +PROJECT_DIR="$(cd "$(dirname "$0")/../.." && pwd)" +OUTPUT_DIR="${PROJECT_DIR}/dist" +NIXPKGS_ALLOW_BROKEN="${NIXPKGS_ALLOW_BROKEN:-1}" + +cd "${PROJECT_DIR}" +mkdir -p "${OUTPUT_DIR}" + +build_iso() { + local target="$1" + local name="$2" + + echo "Building ${target} -> ${name}..." + + NIXPKGS_ALLOW_BROKEN="${NIXPKGS_ALLOW_BROKEN}" \ + sudo --preserve-env=NIXPKGS_ALLOW_BROKEN \ + nix build --impure "${target}" + + local result_dir result_iso + result_dir="$(readlink -f result)" + result_iso=$(find "${result_dir}" -name "*.iso" -type f | head -1) + + if [ -z "${result_iso}" ]; then + echo "ERROR: ISO not found in build result for ${target}" + find "${result_dir}" -type f | head -20 + exit 1 + fi + + cp "${result_iso}" "${OUTPUT_DIR}/${name}" + chmod 644 "${OUTPUT_DIR}/${name}" + ls -lh "${OUTPUT_DIR}/${name}" +} + +build_iso '.#packages.x86_64-linux.iso-minimal' 'bora-minimal.iso' +build_iso '.#packages.x86_64-linux.iso-desktop' 'bora-desktop.iso' +build_iso '.#packages.x86_64-linux.iso-laptop' 'bora-laptop.iso' +build_iso '.#packages.x86_64-linux.iso-server' 'bora-server.iso' + +echo "" +echo "All ISOs built in ${OUTPUT_DIR}:" +ls -lh "${OUTPUT_DIR}"/bora-*.iso diff --git a/scripts/maclike/finalize.sh b/scripts/maclike/finalize.sh new file mode 100644 index 0000000..f17abd2 --- /dev/null +++ b/scripts/maclike/finalize.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env bash +set -euo pipefail + +sleep 2 + +lookandfeeltool -a "org.kde.breezedark.desktop" 2>/dev/null || true + +qdbus6 org.kde.KWin /KWin reconfigure 2>/dev/null || true \ No newline at end of file diff --git a/scripts/maclike/init-desktop.sh b/scripts/maclike/init-desktop.sh new file mode 100644 index 0000000..7bec287 --- /dev/null +++ b/scripts/maclike/init-desktop.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash +set -euo pipefail + +kquitapp6 plasmashell 2>/dev/null || true +kstart6 plasmashell &>/dev/null & \ No newline at end of file diff --git a/scripts/pool/list.sh b/scripts/pool/list.sh new file mode 100644 index 0000000..68f109f --- /dev/null +++ b/scripts/pool/list.sh @@ -0,0 +1,13 @@ +#!/usr/bin/env bash +set -euo pipefail + +POOL_DIR="${POOL_DIR:-/var/lib/instance-pool}" + +for dir in "${POOL_DIR}"/running/*; do + [ -d "${dir}" ] || continue + INST_ID=$(basename "${dir}") + if [ -f "${dir}/.metadata" ]; then + PORT=$(cut -d: -f2 < "${dir}/.metadata") + printf "%s:%s\n" "${INST_ID}" "${PORT}" + fi +done diff --git a/scripts/pool/pool-manager.sh b/scripts/pool/pool-manager.sh new file mode 100644 index 0000000..19b1324 --- /dev/null +++ b/scripts/pool/pool-manager.sh @@ -0,0 +1,60 @@ +#!/usr/bin/env bash +set -euo pipefail + +POOL_DIR="${POOL_DIR:-/var/lib/instance-pool}" +BASE_PORT="${BASE_PORT:-8443}" +MAX="${MAX_INSTANCES:-899}" +MEM="${MEM_LIMIT:-256M}" +CPU="${CPU_LIMIT:-0.5}" +APP="${APP_COMMAND:-}" +HC="${HEALTHCHECK_CMD:-curl -sf http://localhost:${PORT}/health}" + +mkdir -p "${POOL_DIR}"/{running,logs} +mkdir -p /sys/fs/cgroup/bora/pool + +cleanup() { + for dir in "${POOL_DIR}"/running/*; do + [ -d "${dir}" ] || continue + inst=$(basename "${dir}") + microvmctl stop "${inst}" 2>/dev/null || true + done + exit 0 +} +trap cleanup EXIT INT TERM + +while true; do + RUNNING=$(ls -d "${POOL_DIR}"/running/* 2>/dev/null | wc -l) + + if [ "${RUNNING}" -lt "${MAX}" ]; then + NEED=$((MAX - RUNNING)) + for i in $(seq 1 "${NEED}"); do + INST_ID="instance-$(date +%s)-${RANDOM}" + PORT=$((BASE_PORT + RUNNING + i)) + INST_DIR="${POOL_DIR}/running/${INST_ID}" + mkdir -p "${INST_DIR}" + + microvmctl start \ + --id "${INST_ID}" \ + --env "PORT=${PORT}" \ + --mem "${MEM}" \ + --cpu "${CPU}" & + + printf "%s:%s\n" "${INST_ID}" "${PORT}" > "${INST_DIR}/.metadata" + done + wait + fi + + for metafile in "${POOL_DIR}"/running/*/.metadata; do + [ -f "${metafile}" ] || continue + INST_DIR="${metafile%/*}" + INST_ID=$(basename "${INST_DIR}") + PORT=$(cut -d: -f2 < "${metafile}") + HC_CMD="${HC//\$\{PORT\}/${PORT}}" + if ! eval "${HC_CMD}" >/dev/null 2>&1; then + microvmctl stop "${INST_ID}" 2>/dev/null || true + rm -rf "${INST_DIR}" + fi + done + + sleep 10 +done diff --git a/scripts/pool/spawn.sh b/scripts/pool/spawn.sh new file mode 100644 index 0000000..4050297 --- /dev/null +++ b/scripts/pool/spawn.sh @@ -0,0 +1,16 @@ +#!/usr/bin/env bash +set -euo pipefail + +POOL_DIR="${POOL_DIR:-/var/lib/instance-pool}" +BASE_PORT="${BASE_PORT:-8443}" +RUNNING=$(ls -d "${POOL_DIR}"/running/* 2>/dev/null | wc -l) +PORT="${1:-$((BASE_PORT + RUNNING + 1))}" +INST_ID="instance-manual-$$" + +microvmctl start \ + --id "${INST_ID}" \ + --env "PORT=${PORT}" + +mkdir -p "${POOL_DIR}/running/${INST_ID}" +printf "%s:%s\n" "${INST_ID}" "${PORT}" > "${POOL_DIR}/running/${INST_ID}/.metadata" +printf "%s\n" "${PORT}" diff --git a/scripts/pool/stats.sh b/scripts/pool/stats.sh new file mode 100644 index 0000000..da0e064 --- /dev/null +++ b/scripts/pool/stats.sh @@ -0,0 +1,18 @@ +#!/usr/bin/env bash +set -euo pipefail + +POOL_DIR="${POOL_DIR:-/var/lib/instance-pool}" +MAX="${MAX_INSTANCES:-899}" + +RUNNING=$(ls -d "${POOL_DIR}"/running/* 2>/dev/null | wc -l) + +printf "=== Pool Stats ===\n" +printf "Running: %s / %s\n" "${RUNNING}" "${MAX}" + +for dir in "${POOL_DIR}"/running/*; do + [ -d "${dir}" ] || continue + INST_ID=$(basename "${dir}") + PORT=$(cut -d: -f2 < "${dir}/.metadata" 2>/dev/null || true) + MEM=$(cat /sys/fs/cgroup/bora/pool/"${INST_ID}"/memory.current 2>/dev/null | numfmt --to=iec 2>/dev/null || echo "N/A") + printf " %-35s port=%-6s mem=%s\n" "${INST_ID}" "${PORT}" "${MEM}" +done diff --git a/scripts/spring/bean-wrapper.sh b/scripts/spring/bean-wrapper.sh new file mode 100644 index 0000000..6dc6a13 --- /dev/null +++ b/scripts/spring/bean-wrapper.sh @@ -0,0 +1,20 @@ +#!/usr/bin/env bash +set -euo pipefail + +BEAN="${1:?BEAN required}" +EXEC="${2:?EXEC required}" +shift 2 + +CB="/run/current-system/sw/bin/circuit-breaker" +HC="/run/current-system/sw/bin/healthcheck" + +if ! "${CB}" "${BEAN}" 5 30000 2 3 status; then + exit 1 +fi + +if ! "${EXEC}" "$@"; then + "${CB}" "${BEAN}" 5 30000 2 3 trip + exit 1 +fi + +"${CB}" "${BEAN}" 5 30000 2 3 success diff --git a/scripts/spring/circuit-breaker.sh b/scripts/spring/circuit-breaker.sh new file mode 100644 index 0000000..4046931 --- /dev/null +++ b/scripts/spring/circuit-breaker.sh @@ -0,0 +1,114 @@ +#!/usr/bin/env bash +set -euo pipefail + +CB_DIR="/run/bora-cb" +mkdir -p "${CB_DIR}" + +BEAN="${1:?BEAN name required}" +THRESHOLD="${2:-5}" +TIMEOUT_MS="${3:-30000}" +SUCCESS_THRESHOLD="${4:-2}" +HALF_OPEN_MAX="${5:-3}" + +STATE_FILE="${CB_DIR}/${BEAN}-state" +FAILURES_FILE="${CB_DIR}/${BEAN}-failures" +SUCCESSES_FILE="${CB_DIR}/${BEAN}-successes" +SINCE_FILE="${CB_DIR}/${BEAN}-since" + +circuit_state() { + if [ -f "${STATE_FILE}" ]; then + cat "${STATE_FILE}" + else + echo "closed" + fi +} + +circuit_open() { + echo "open" > "${STATE_FILE}" + date +%s > "${SINCE_FILE}" + echo 0 > "${FAILURES_FILE}" +} + +circuit_half_open() { + echo "half-open" > "${STATE_FILE}" +} + +circuit_close() { + echo "closed" > "${STATE_FILE}" + echo 0 > "${FAILURES_FILE}" + echo 0 > "${SUCCESSES_FILE}" +} + +circuit_trip() { + local state + state=$(circuit_state) + case "${state}" in + closed) + local failures=0 + [ -f "${FAILURES_FILE}" ] && failures=$(cat "${FAILURES_FILE}") + failures=$((failures + 1)) + echo "${failures}" > "${FAILURES_FILE}" + if [ "${failures}" -ge "${THRESHOLD}" ]; then + circuit_open + return 1 + fi + ;; + open) + local since=0 now + [ -f "${SINCE_FILE}" ] && since=$(cat "${SINCE_FILE}") + now=$(date +%s) + if [ "$((now - since))" -ge "$((TIMEOUT_MS / 1000))" ]; then + circuit_half_open + fi + return 1 + ;; + half-open) + local failures=0 + [ -f "${FAILURES_FILE}" ] && failures=$(cat "${FAILURES_FILE}") + if [ "${failures}" -lt "${HALF_OPEN_MAX}" ]; then + failures=$((failures + 1)) + echo "${failures}" > "${FAILURES_FILE}" + circuit_open + return 1 + else + circuit_close + return 0 + fi + ;; + esac +} + +circuit_success() { + local state + state=$(circuit_state) + if [ "${state}" = "half-open" ]; then + local successes=0 + [ -f "${SUCCESSES_FILE}" ] && successes=$(cat "${SUCCESSES_FILE}") + successes=$((successes + 1)) + echo "${successes}" > "${SUCCESSES_FILE}" + if [ "${successes}" -ge "${SUCCESS_THRESHOLD}" ]; then + circuit_close + fi + else + echo 0 > "${FAILURES_FILE}" + fi +} + +case "${6:-trip}" in + trip) + circuit_trip + exit $? + ;; + success) + circuit_success + exit 0 + ;; + status) + circuit_state + exit 0 + ;; + *) + echo "Usage: circuit-breaker.sh " + exit 1 + ;; +esac diff --git a/scripts/spring/healthcheck.sh b/scripts/spring/healthcheck.sh new file mode 100644 index 0000000..6dd5c6b --- /dev/null +++ b/scripts/spring/healthcheck.sh @@ -0,0 +1,30 @@ +#!/usr/bin/env bash +set -euo pipefail + +BEAN="${1:?BEAN required}" +COMMAND="${2:?HEALTHCHECK COMMAND required}" +INTERVAL="${3:-10}" +MAX_RETRIES="${4:-3}" +RETRY_DELAY="${5:-2}" +CB_SCRIPT="${6:-/run/current-system/sw/bin/circuit-breaker}" + +run_check() { + if eval "${COMMAND}" >/dev/null 2>&1; then + "${CB_SCRIPT}" "${BEAN}" 5 30000 2 3 success + return 0 + else + return 1 + fi +} + +retry=0 +while [ "${retry}" -lt "${MAX_RETRIES}" ]; do + if run_check; then + exit 0 + fi + retry=$((retry + 1)) + [ "${retry}" -lt "${MAX_RETRIES}" ] && sleep "${RETRY_DELAY}" +done + +"${CB_SCRIPT}" "${BEAN}" 5 30000 2 3 trip +exit 1 diff --git a/scripts/spring/spring-resources.sh b/scripts/spring/spring-resources.sh new file mode 100644 index 0000000..3163c58 --- /dev/null +++ b/scripts/spring/spring-resources.sh @@ -0,0 +1,14 @@ +#!/usr/bin/env bash +set -euo pipefail + +APP="${1:-bora}" + +printf "=== Resource usage: %s ===\n" "${APP}" +for cg in /sys/fs/cgroup/${APP}/*; do + [ -d "${cg}" ] || continue + name="${cg##*/}" + mem=$(cat "${cg}/memory.current" 2>/dev/null | numfmt --to=iec 2>/dev/null || echo "N/A") + cpu=$(cat "${cg}/cpu.stat" 2>/dev/null | grep usage_usec | cut -d' ' -f2 || echo "N/A") + pids=$(cat "${cg}/pids.current" 2>/dev/null || echo "N/A") + printf " %-25s mem=%-10s cpu=%-10s pids=%s\n" "${name}" "${mem}" "${cpu}" "${pids}" +done diff --git a/scripts/spring/spring-restart-bean.sh b/scripts/spring/spring-restart-bean.sh new file mode 100644 index 0000000..8007072 --- /dev/null +++ b/scripts/spring/spring-restart-bean.sh @@ -0,0 +1,7 @@ +#!/usr/bin/env bash +set -euo pipefail + +APP="${1:?APP required}" +BEAN="${2:?BEAN required}" + +systemctl restart "spring-${APP}-${BEAN}.service" diff --git a/scripts/spring/spring-status.sh b/scripts/spring/spring-status.sh new file mode 100644 index 0000000..4bed2a5 --- /dev/null +++ b/scripts/spring/spring-status.sh @@ -0,0 +1,20 @@ +#!/usr/bin/env bash +set -euo pipefail + +APP="${1:-bora}" + +printf "=== Spring Application: %s ===\n" "${APP}" +printf "Active beans:\n" +systemctl list-units "spring-${APP}-*" --no-legend | \ + while read -r unit _ _ active _; do + printf " %-55s %s\n" "${unit}" "${active}" + done + +printf "\nCircuit breaker states:\n" +for f in /run/bora-cb/*-state; do + [ -f "${f}" ] || continue + name="${f%-state}" + name="${name##*/}" + state=$(cat "${f}") + printf " %-40s %s\n" "${name}" "${state}" +done diff --git a/src/guests/example/instance.nix b/src/guests/example/instance.nix new file mode 100644 index 0000000..d2ae93f --- /dev/null +++ b/src/guests/example/instance.nix @@ -0,0 +1,25 @@ +{ modulesPath, ... }: +{ + imports = [ "${modulesPath}/profiles/minimal.nix" ]; + + microvm = { + guest.enable = true; + interfaces = [{ + type = "bridge"; + host = "microvm"; + }]; + shares = [{ + source = "/var/lib/instance-pool/workspaces"; + mountPoint = "/workspace"; + type = "virtiofs"; + }]; + sockets = [ + "/tmp/.X11-unix/X0" + "/run/user/1000/wayland-0" + ]; + mem = 256; + vcpu = 1; + }; + + system.stateVersion = "25.11"; +} diff --git a/src/guests/example/pool.nix b/src/guests/example/pool.nix new file mode 100644 index 0000000..04adc0a --- /dev/null +++ b/src/guests/example/pool.nix @@ -0,0 +1,13 @@ +_: +{ + imports = [ ./instance.nix ]; + + bora.containers.instancePool = { + enable = true; + maxInstances = 899; + basePort = 8443; + memPerInstance = "256M"; + cpuPerInstance = "0.5"; + storagePerInstance = "2G"; + }; +} diff --git a/src/guests/sandbox.nix b/src/guests/sandbox.nix new file mode 100644 index 0000000..010dbc5 --- /dev/null +++ b/src/guests/sandbox.nix @@ -0,0 +1,34 @@ +{ pkgs, modulesPath, ... }: +{ + imports = [ "${modulesPath}/profiles/minimal.nix" ]; + + microvm = { + guest.enable = true; + interfaces = [{ + type = "bridge"; + host = "microvm"; + }]; + shares = [{ + source = "/home"; + mountPoint = "/mnt/home"; + type = "virtiofs"; + }]; + sockets = [ + "/tmp/.X11-unix/X0" + "/run/user/1000/wayland-0" + "/run/user/1000/pipewire-0" + "/run/user/1000/pulse" + ]; + mem = 2048; + vcpu = 2; + }; + + services.pipewire.enable = true; + + environment.systemPackages = with pkgs; [ + firefox + chromium + ]; + + system.stateVersion = "25.11"; +} diff --git a/src/hosts/bora/default.nix b/src/hosts/bora/default.nix new file mode 100644 index 0000000..f9010bf --- /dev/null +++ b/src/hosts/bora/default.nix @@ -0,0 +1,36 @@ +{ hostname, username, ... }: + +{ + networking.hostName = hostname; + + users = { + users.${username} = { + isNormalUser = true; + description = "Utente principale"; + extraGroups = [ "wheel" "networkmanager" "audio" "video" "libvirtd" "microvm" ]; + shell = pkgs.zsh; + openssh.authorizedKeys.keys = [ ]; + }; + users.root.openssh.authorizedKeys.keys = [ ]; + }; + + programs.zsh = { + enable = true; + enableCompletion = true; + autosuggestions.enable = true; + syntaxHighlighting.enable = true; + ohMyZsh = { + enable = true; + theme = "agnoster"; + plugins = [ "git" "sudo" "systemd" "zsh-navigation-tools" ]; + }; + }; + + security.sudo = { + enable = true; + extraRules = [{ + groups = [ "wheel" ]; + commands = [{ command = "ALL"; options = [ "NOPASSWD" ]; }]; + }]; + }; +} diff --git a/src/hosts/bora/hardware.nix b/src/hosts/bora/hardware.nix new file mode 100644 index 0000000..867fc73 --- /dev/null +++ b/src/hosts/bora/hardware.nix @@ -0,0 +1,31 @@ +{ config, lib, modulesPath, ... }: +let + fsCfg = config.bora.filesystem; +in + +{ + imports = [ (modulesPath + "/installer/scan/not-detected.nix") ]; + + boot = { + initrd = { + availableKernelModules = [ "nvme" "xhci_pci" "ahci" "usb_storage" "sd_mod" ]; + kernelModules = [ "zfs" ]; + }; + kernelModules = [ "kvm-amd" "kvm-intel" ]; + extraModulePackages = [ ]; + }; + + fileSystems = { + "/" = { device = "zroot/root"; fsType = "zfs"; }; + "/nix" = { device = "zroot/root/nix"; fsType = "zfs"; }; + "/home" = { device = "zroot/root/home"; fsType = "zfs"; }; + "/var" = { device = "zroot/root/var"; fsType = "zfs"; }; + "/tmp" = { device = "zroot/root/tmp"; fsType = "zfs"; }; + "/boot" = { device = fsCfg.bootDevice; fsType = "vfat"; }; + }; + + swapDevices = [ ]; + + nix.settings.max-jobs = lib.mkDefault 8; + powerManagement.cpuFreqGovernor = lib.mkDefault "powersave"; +} diff --git a/src/hosts/bora/meta.nix b/src/hosts/bora/meta.nix new file mode 100644 index 0000000..8ff219f --- /dev/null +++ b/src/hosts/bora/meta.nix @@ -0,0 +1,7 @@ +{ + system = "x86_64-linux"; + hardware = "desktop"; + profile = "developer"; + hostname = "bora"; + username = "user"; +} diff --git a/src/modules/containers/default.nix b/src/modules/containers/default.nix new file mode 100644 index 0000000..1bd35a6 --- /dev/null +++ b/src/modules/containers/default.nix @@ -0,0 +1,8 @@ +_: +{ + imports = [ + ./microvm-host.nix + ./orchestrator.nix + ./instance-pool.nix + ]; +} diff --git a/src/modules/containers/instance-pool.nix b/src/modules/containers/instance-pool.nix new file mode 100644 index 0000000..58a61bc --- /dev/null +++ b/src/modules/containers/instance-pool.nix @@ -0,0 +1,137 @@ +{ config, lib, pkgs, ... }: +with lib; +let + cfg = config.bora.containers.instancePool; + scriptsDir = ./../../../scripts/pool; + poolManager = pkgs.writeShellScriptBin "pool-manager" + (builtins.readFile (scriptsDir + "/pool-manager.sh")); + spawnScript = pkgs.writeShellScriptBin "pool-spawn" + (builtins.readFile (scriptsDir + "/spawn.sh")); + listScript = pkgs.writeShellScriptBin "pool-list" + (builtins.readFile (scriptsDir + "/list.sh")); + statsScript = pkgs.writeShellScriptBin "pool-stats" + (builtins.readFile (scriptsDir + "/stats.sh")); +in +{ + options.bora.containers.instancePool = { + enable = mkEnableOption "MicroVM instance pool orchestrator"; + maxInstances = mkOption { + type = types.int; + default = 899; + }; + basePort = mkOption { + type = types.port; + default = 8443; + }; + memPerInstance = mkOption { + type = types.str; + default = "256M"; + }; + cpuPerInstance = mkOption { + type = types.str; + default = "0.5"; + }; + storagePerInstance = mkOption { + type = types.str; + default = "2G"; + }; + appPackage = mkOption { + type = types.nullOr types.package; + default = null; + }; + appCommand = mkOption { + type = types.nullOr types.str; + default = null; + }; + healthcheckCmd = mkOption { + type = types.nullOr types.str; + default = null; + }; + }; + config = mkIf cfg.enable { + systemd.services = { + create-pool-zfs = { + description = "Create ZFS dataset for instance pool"; + before = [ "bora-pool.service" ]; + wantedBy = [ "multi-user.target" ]; + path = with pkgs; [ zfs ]; + script = '' + zfs create -o mountpoint=/var/lib/instance-pool \ + -o atime=off -o compression=zstd-3 \ + -o quota=${toString (cfg.maxInstances * 2)}G \ + zroot/root/instance-pool 2>/dev/null || true + ''; + }; + bora-cgroup-pool = { + description = "Instance pool cgroup v2 hierarchy"; + before = [ "bora-pool.service" ]; + wantedBy = [ "multi-user.target" ]; + script = '' + CG="/sys/fs/cgroup/bora/pool" + mkdir -p "$CG" + echo ${cfg.memPerInstance} > "$CG/memory.max" + echo ${cfg.memPerInstance} > "$CG/memory.high" + echo 100000 > "$CG/cpu.max" + echo ${cfg.cpuPerInstance}0000 > "$CG/cpu.max" + echo 512 > "$CG/pids.max" + echo "8:0 ${cfg.storagePerInstance}" > "$CG/io.max" + ''; + }; + bora-pool = { + description = "MicroVM Instance Pool"; + after = [ "network.target" "microvm-host.service" "create-pool-zfs.service" ]; + wants = [ "microvm-host.service" ]; + wantedBy = [ "multi-user.target" ]; + path = with pkgs; [ microvm coreutils bash curl ]; + environment = { + POOL_DIR = "/var/lib/instance-pool"; + BASE_PORT = toString cfg.basePort; + MAX_INSTANCES = toString cfg.maxInstances; + MEM_LIMIT = cfg.memPerInstance; + CPU_LIMIT = cfg.cpuPerInstance; + APP_COMMAND = cfg.appCommand or ""; + HEALTHCHECK_CMD = cfg.healthcheckCmd or ""; + }; + serviceConfig = { + Type = "notify"; + Restart = "always"; + RestartSec = 5; + StateDirectory = "instance-pool"; + NotifyAccess = "all"; + LimitNOFILE = 1048576; + LimitNPROC = 1048576; + }; + script = '' + ${builtins.readFile (scriptsDir + "/pool-manager.sh")} + ''; + }; + }; + services.caddy = { + enable = true; + globalConfig = '' + servers { + trusted_proxies static private_ranges + } + ''; + virtualHosts."*.pool.bora.local" = { + extraConfig = '' + @ws { + header Connection *Upgrade* + header Upgrade websocket + } + reverse_proxy @ws localhost:{path.port} + reverse_proxy localhost:{path.port} + ''; + }; + }; + networking.firewall.allowedTCPPortRanges = [ + { from = cfg.basePort; to = cfg.basePort + cfg.maxInstances; } + ]; + environment.systemPackages = [ + poolManager + spawnScript + listScript + statsScript + ]; + }; +} diff --git a/src/modules/containers/microvm-host.nix b/src/modules/containers/microvm-host.nix new file mode 100644 index 0000000..cf5118d --- /dev/null +++ b/src/modules/containers/microvm-host.nix @@ -0,0 +1,26 @@ +{ config, lib, ... }: +with lib; +let cfg = config.bora.containers.microvm; in { + options.bora.containers.microvm = { + enable = mkEnableOption "MicroVM host support"; + stateDir = mkOption { + type = types.path; + default = "/var/lib/microvm"; + description = "Directory for MicroVM storage"; + }; + }; + config = mkIf cfg.enable { + microvm = { + host.enable = true; + inherit (cfg) stateDir; + }; + fileSystems.${cfg.stateDir} = { + device = "zroot/root/microvm"; + fsType = "zfs"; + neededForBoot = true; + }; + boot.kernelModules = [ "virtio" "virtio_net" "virtio_blk" "virtiofs" "virtio_gpu" ]; + boot.initrd.kernelModules = [ "virtiofs" ]; + users.groups.microvm = { }; + }; +} diff --git a/src/modules/containers/orchestrator.nix b/src/modules/containers/orchestrator.nix new file mode 100644 index 0000000..cfbbf9f --- /dev/null +++ b/src/modules/containers/orchestrator.nix @@ -0,0 +1,59 @@ +{ config, lib, ... }: +with lib; +let + cfg = config.bora.orchestrator; +in +{ + options.bora.orchestrator = { + enable = mkEnableOption "MicroVM instance orchestrator"; + maxInstances = mkOption { + type = types.int; + default = 10000; + description = "Maximum number of simultaneous MicroVM instances"; + }; + defaultMem = mkOption { + type = types.int; + default = 256; + description = "Default memory per instance (MB)"; + }; + defaultVcpu = mkOption { + type = types.int; + default = 1; + description = "Default vCPUs per instance"; + }; + portRange = mkOption { + type = types.str; + default = "8443-18443"; + description = "Port range for exposed services"; + }; + }; + config = mkIf cfg.enable { + systemd.services.bora-orchestrator = { + description = "Bora MicroVM Orchestrator"; + after = [ "microvm-host.service" "network.target" ]; + wants = [ "microvm-host.service" ]; + wantedBy = [ "multi-user.target" ]; + serviceConfig = { + Type = "simple"; + Restart = "always"; + RestartSec = 10; + StateDirectory = "bora-orchestrator"; + }; + script = '' + set -euo pipefail + STATE_DIR="/var/lib/bora-orchestrator" + mkdir -p "$STATE_DIR" + echo "+cpu +memory +io +pids" > /sys/fs/cgroup/cgroup.subtree_control 2>/dev/null || true + mkdir -p /sys/fs/cgroup/bora 2>/dev/null || true + while true; do + for vm in /var/lib/microvm/*/; do + [ -d "$vm" ] || continue + vm_name=$(basename "$vm") + echo "checking microvm $vm_name" + done + sleep 30 + done + ''; + }; + }; +} diff --git a/src/modules/core/boot.nix b/src/modules/core/boot.nix new file mode 100644 index 0000000..30cc32b --- /dev/null +++ b/src/modules/core/boot.nix @@ -0,0 +1,42 @@ +{ lib, pkgs, ... }: +{ + boot = { + loader = { + systemd-boot = { + enable = true; + configurationLimit = 20; + }; + efi.canTouchEfiVariables = true; + timeout = lib.mkDefault 3; + }; + kernelParams = [ + "quiet" + "splash" + "mitigations=auto" + "random.trust_cpu=off" + "random.trust_bootloader=off" + "slab_nomerge" + "init_on_alloc=1" + "init_on_free=1" + "page_alloc.shuffle=1" + "pti=on" + "vsyscall=none" + "debugfs=off" + ]; + consoleLogLevel = 0; + initrd = { + verbose = false; + systemd.enable = true; + }; + supportedFilesystems = [ "zfs" "btrfs" "ntfs" "exfat" "vfat" "xfs" ]; + kernelPackages = lib.mkDefault pkgs.linuxPackages_latest; + }; + hardware = { + enableRedistributableFirmware = true; + enableAllFirmware = true; + cpu = { + intel.updateMicrocode = lib.mkDefault true; + amd.updateMicrocode = lib.mkDefault true; + }; + }; +} diff --git a/src/modules/core/default.nix b/src/modules/core/default.nix new file mode 100644 index 0000000..e7a4bc5 --- /dev/null +++ b/src/modules/core/default.nix @@ -0,0 +1,9 @@ +_: +{ + imports = [ + ./boot.nix + ./nix.nix + ./locale.nix + ./sysctl.nix + ]; +} diff --git a/src/modules/core/locale.nix b/src/modules/core/locale.nix new file mode 100644 index 0000000..6fc53c9 --- /dev/null +++ b/src/modules/core/locale.nix @@ -0,0 +1,34 @@ +{ lib, pkgs, ... }: +{ + time.timeZone = lib.mkDefault "Europe/Rome"; + i18n = { + defaultLocale = lib.mkDefault "it_IT.UTF-8"; + supportedLocales = [ + "it_IT.UTF-8/UTF-8" + "en_US.UTF-8/UTF-8" + "C.UTF-8/UTF-8" + ]; + extraLocaleSettings = { + LC_ADDRESS = lib.mkDefault "it_IT.UTF-8"; + LC_IDENTIFICATION = lib.mkDefault "it_IT.UTF-8"; + LC_MEASUREMENT = lib.mkDefault "it_IT.UTF-8"; + LC_MONETARY = lib.mkDefault "it_IT.UTF-8"; + LC_NAME = lib.mkDefault "it_IT.UTF-8"; + LC_NUMERIC = lib.mkDefault "it_IT.UTF-8"; + LC_PAPER = lib.mkDefault "it_IT.UTF-8"; + LC_TELEPHONE = lib.mkDefault "it_IT.UTF-8"; + LC_TIME = lib.mkDefault "it_IT.UTF-8"; + }; + }; + console = { + font = lib.mkDefault "Lat2-Terminus16"; + keyMap = lib.mkDefault "it"; + packages = with pkgs; [ terminus_font ]; + }; + services = { + dbus.enable = true; + udisks2.enable = true; + upower.enable = lib.mkDefault true; + fwupd.enable = lib.mkDefault true; + }; +} diff --git a/src/modules/core/nix.nix b/src/modules/core/nix.nix new file mode 100644 index 0000000..3bba234 --- /dev/null +++ b/src/modules/core/nix.nix @@ -0,0 +1,39 @@ +{ lib, pkgs, ... }: +{ + nix = { + package = pkgs.nixVersions.stable; + settings = { + experimental-features = [ "nix-command" "flakes" "auto-allocate-uids" "ca-derivations" ]; + auto-optimise-store = true; + max-jobs = lib.mkDefault 8; + max-substitution-jobs = 64; + min-free = 1073741824; + max-free = 5368709120; + substituters = [ + "https://cache.nixos.org" + "https://nix-community.cachix.org" + "https://astro-microvm.cachix.org" + "https://bora.cachix.org" + ]; + trusted-public-keys = [ + "cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY=" + "nix-community.cachix.org-1:mB9FSh9qf2dCimDSUo8Zy7bkq5CX+/rkCWyvRCYg3Fs=" + "astro-microvm.cachix.org-1:5VxKj9V5rE1xJgF2gQvA0Z3L8R6bH7cN4pY9sW1tXnM=" + ]; + trusted-users = [ "root" "@wheel" ]; + }; + gc = { + automatic = true; + dates = "weekly"; + options = "--delete-older-than 14d"; + }; + optimise = { + automatic = true; + dates = [ "03:00" ]; + }; + nixPath = [ + "nixpkgs=/nix/var/nix/profiles/per-user/root/channels/nixos" + "nixos-config=/persist/etc/nixos/configuration.nix" + ]; + }; +} diff --git a/src/modules/core/sysctl.nix b/src/modules/core/sysctl.nix new file mode 100644 index 0000000..a4a15a1 --- /dev/null +++ b/src/modules/core/sysctl.nix @@ -0,0 +1,60 @@ +_: +{ + boot.kernel.sysctl = { + "kernel.kptr_restrict" = 2; + "kernel.dmesg_restrict" = 1; + "kernel.perf_event_paranoid" = 3; + "kernel.yama.ptrace_scope" = 2; + "kernel.randomize_va_space" = 2; + "kernel.unprivileged_bpf_disabled" = 1; + "net.core.bpf_jit_enable" = 0; + "kernel.kexec_load_disabled" = 1; + "kernel.sysrq" = 0; + "net.ipv4.tcp_syncookies" = 1; + "net.ipv4.conf.all.rp_filter" = 1; + "net.ipv4.conf.default.rp_filter" = 1; + "net.ipv4.conf.all.accept_redirects" = 0; + "net.ipv4.conf.default.accept_redirects" = 0; + "net.ipv4.conf.all.secure_redirects" = 0; + "net.ipv4.conf.default.secure_redirects" = 0; + "net.ipv4.conf.all.send_redirects" = 0; + "net.ipv4.conf.default.send_redirects" = 0; + "net.ipv6.conf.all.accept_redirects" = 0; + "net.ipv6.conf.default.accept_redirects" = 0; + "net.ipv4.icmp_echo_ignore_all" = 0; + "net.ipv4.icmp_echo_ignore_broadcasts" = 1; + "net.ipv4.icmp_ignore_bogus_error_responses" = 1; + "net.ipv4.tcp_rfc1337" = 1; + "vm.swappiness" = 10; + "vm.vfs_cache_pressure" = 50; + "vm.dirty_ratio" = 10; + "vm.dirty_background_ratio" = 5; + "vm.dirty_expire_centisecs" = 3000; + "vm.dirty_writeback_centisecs" = 500; + "vm.max_map_count" = 2147483642; + "kernel.numa_balancing" = 0; + "fs.file-max" = 9223372036854775807; + "fs.inotify.max_user_watches" = 1048576; + "fs.inotify.max_user_instances" = 1048576; + "fs.inotify.max_queued_events" = 1048576; + "fs.aio-max-nr" = 1048576; + "net.core.somaxconn" = 65535; + "net.core.netdev_max_backlog" = 5000; + "net.core.rmem_max" = 134217728; + "net.core.wmem_max" = 134217728; + "net.ipv4.tcp_rmem" = "4096 87380 134217728"; + "net.ipv4.tcp_wmem" = "4096 65536 134217728"; + "net.ipv4.tcp_congestion_control" = "bbr"; + "net.ipv4.tcp_fastopen" = 3; + "net.ipv4.tcp_fin_timeout" = 15; + "net.ipv4.tcp_keepalive_time" = 300; + "net.ipv4.tcp_keepalive_probes" = 5; + "net.ipv4.tcp_keepalive_intvl" = 15; + "net.ipv4.tcp_mtu_probing" = 1; + "net.ipv4.tcp_slow_start_after_idle" = 0; + "vm.overcommit_memory" = 1; + "vm.oom_kill_allocating_task" = 0; + "vm.panic_on_oom" = 0; + }; + systemd.coredump.enable = false; +} diff --git a/src/modules/desktop/default.nix b/src/modules/desktop/default.nix new file mode 100644 index 0000000..93236ee --- /dev/null +++ b/src/modules/desktop/default.nix @@ -0,0 +1,8 @@ +_: +{ + imports = [ + ./kde-minimal.nix + ./pipewire.nix + ./maclike.nix + ]; +} diff --git a/src/modules/desktop/kde-minimal.nix b/src/modules/desktop/kde-minimal.nix new file mode 100644 index 0000000..3882b31 --- /dev/null +++ b/src/modules/desktop/kde-minimal.nix @@ -0,0 +1,88 @@ +{ config, lib, pkgs, ... }: +with lib; +let + cfg = config.bora.desktop.kde; + kdeMinimal = with pkgs; [ + kdePackages.plasma-desktop + kdePackages.plasma-workspace + kdePackages.kwin + kdePackages.kirigami + kdePackages.qqc2-desktop-style + kdePackages.plasma-integration + kdePackages.breeze-icons + kdePackages.breeze-gtk + + kdePackages.konsole + kdePackages.systemsettings + kdePackages.dolphin + kdePackages.kate + ]; +in +{ + options.bora.desktop.kde = { + enable = mkEnableOption "KDE Plasma 6 minimal desktop"; + enableWayland = mkOption { + type = types.bool; + default = true; + description = "Enable Wayland session"; + }; + excludePackages = mkOption { + type = types.listOf types.package; + default = with pkgs.kdePackages; [ + elisa + gwenview + khelpcenter + okular + oxygen + krdp + krfb + ktorrent + kget + korganizer + kaddressbook + kmail + akonadi + kontact + ]; + description = "KDE packages to exclude"; + }; + }; + config = mkIf cfg.enable { + services = { + displayManager.sddm = { + enable = true; + wayland.enable = cfg.enableWayland; + theme = "breeze"; + autoNumlock = true; + }; + desktopManager.plasma6 = { + enable = true; + enableQt5Integration = false; + }; + }; + environment.plasma6.excludePackages = cfg.excludePackages; + environment.systemPackages = kdeMinimal; + hardware.graphics = { + enable = true; + enable32Bit = true; + }; + fonts = { + enableDefaultPackages = true; + packages = with pkgs; [ + noto-fonts + noto-fonts-cjk-sans + noto-fonts-color-emoji + source-code-pro + nerd-fonts.jetbrains-mono + nerd-fonts.fira-code + ]; + fontconfig.defaultFonts = { + serif = [ "Noto Serif" ]; + sansSerif = [ "Noto Sans" ]; + monospace = [ "JetBrainsMono Nerd Font" ]; + emoji = [ "Noto Color Emoji" ]; + }; + }; + xdg.portal.enable = true; + }; +} diff --git a/src/modules/desktop/maclike.nix b/src/modules/desktop/maclike.nix new file mode 100644 index 0000000..55115cc --- /dev/null +++ b/src/modules/desktop/maclike.nix @@ -0,0 +1,112 @@ +{ config, lib, pkgs, ... }: +with lib; +let + cfg = config.bora.desktop.layout; + initScript = pkgs.writeShellScriptBin "bora-desktop-init" + (builtins.readFile ./../../../scripts/maclike/init-desktop.sh); + finalizeScript = pkgs.writeShellScriptBin "bora-desktop-finalize" + (builtins.readFile ./../../../scripts/maclike/finalize.sh); +in +{ + options.bora.desktop.layout = { + enable = mkEnableOption "Bora custom desktop layout"; + theme = mkOption { + type = types.enum [ "bora-dark" "bora-light" ]; + default = "bora-dark"; + }; + layout = mkOption { + type = types.enum [ "standard" "minimal" "floating" ]; + default = "floating"; + }; + topPanelHeight = mkOption { type = types.int; default = 34; }; + enableTransparency = mkOption { type = types.bool; default = true; }; + enableNexus = mkOption { type = types.bool; default = true; }; + nexusKey = mkOption { type = types.str; default = "Alt+F1"; }; + globalMenu = mkOption { type = types.bool; default = true; }; + desktopCount = mkOption { type = types.int; default = 6; }; + accentColor = mkOption { + type = types.enum [ "cyan" "purple" "blue" "green" "orange" ]; + default = "cyan"; + }; + }; + + config = mkIf cfg.enable { + environment = { + systemPackages = with pkgs; [ + kdePackages.plasma-desktop + kdePackages.plasma-workspace + kdePackages.kwin + kdePackages.konsole + kdePackages.systemsettings + kdePackages.dolphin + kdePackages.kate + kdePackages.qqc2-desktop-style + kdePackages.qqc2-breeze-style + kdePackages.breeze-icons + kdePackages.breeze-gtk + + kdePackages.plasma-integration + tela-circle-icon-theme + kdePackages.applet-window-buttons6 + initScript + finalizeScript + ]; + sessionVariables = { + KDE_SESSION_VERSION = "6"; + XDG_CURRENT_DESKTOP = "KDE"; + XDG_SESSION_DESKTOP = "KDE"; + DESKTOP_SESSION = "plasmawayland"; + PLASMA_USE_QT_SCALING = "1"; + QT_AUTO_SCREEN_SET_FACTOR = "0"; + QT_SCALE_FACTOR_ROUNDING_POLICY = "RoundPreferFloor"; + }; + etc = { + "skel/.config/plasma-org.kde.plasma.desktop-appletsrc".source = + ./../../../config/desktop/plasma-appletsrc; + "skel/.config/kdeglobals".source = + ./../../../config/desktop/kdeglobals; + "skel/.config/kwinrc".source = + ./../../../config/desktop/kwinrc; + "skel/.config/khotkeysrc".text = + builtins.replaceStrings [ "Alt+F1" ] [ cfg.nexusKey ] + (builtins.readFile ./../../../config/desktop/khotkeysrc); + "skel/.config/plasmarc".text = '' + [Theme] + name=Breeze + [Wallpaper] + fillMode=2 + [PlasmaViews] + PanelOpacity=${if cfg.enableTransparency then "0.70" else "1.0"} + ''; + }; + }; + + system.activationScripts.bora-desktop = stringAfter [ "etc" ] '' + mkdir -p /etc/skel/.config/autostart + cat > /etc/skel/.config/autostart/bora-desktop-setup.desktop << 'EOF' + [Desktop Entry] + Type=Application + Name=Bora Desktop Initializer + Exec=${initScript}/bin/bora-desktop-init + X-KDE-autostart-phase=2 + OnlyShowIn=KDE + EOF + ''; + + services.displayManager.sddm.wayland.enable = true; + services.desktopManager.plasma6.enableQt5Integration = false; + security.polkit.enable = true; + + systemd.user.services.bora-desktop-autostart = { + description = "Bora desktop finalizer"; + after = [ "plasmashell.service" ]; + wantedBy = [ "plasma-workspace.target" ]; + partOf = [ "plasma-workspace.target" ]; + serviceConfig = { + Type = "oneshot"; + RemainAfterExit = true; + ExecStart = "${finalizeScript}/bin/bora-desktop-finalize"; + }; + }; + }; +} diff --git a/src/modules/desktop/pipewire.nix b/src/modules/desktop/pipewire.nix new file mode 100644 index 0000000..ddb1f3b --- /dev/null +++ b/src/modules/desktop/pipewire.nix @@ -0,0 +1,23 @@ +{ config, lib, pkgs, ... }: +with lib; +let + cfg = config.bora.desktop.audio; +in +{ + options.bora.desktop.audio = { + enable = mkEnableOption "PipeWire audio system"; + }; + config = mkIf cfg.enable { + services.pipewire = { + enable = true; + alsa.enable = true; + pulse.enable = true; + wireplumber.enable = true; + }; + users.groups.audio = { }; + environment.systemPackages = with pkgs; [ + pulsemixer + helvum + ]; + }; +} diff --git a/src/modules/filesystem/default.nix b/src/modules/filesystem/default.nix new file mode 100644 index 0000000..500ce19 --- /dev/null +++ b/src/modules/filesystem/default.nix @@ -0,0 +1,8 @@ +_: +{ + imports = [ + ./zfs.nix + ./impermanence.nix + ./disko.nix + ]; +} diff --git a/src/modules/filesystem/disko.nix b/src/modules/filesystem/disko.nix new file mode 100644 index 0000000..7a77f86 --- /dev/null +++ b/src/modules/filesystem/disko.nix @@ -0,0 +1,58 @@ +{ config, lib, ... }: +with lib; +let cfg = config.bora.filesystem.disko; in { + options.bora.filesystem.disko = { + enable = mkEnableOption "disko declarative partitioning"; + disk = mkOption { + type = types.str; + default = "/dev/nvme0n1"; + description = "Target disk device for partitioning"; + }; + zfsPool = mkOption { + type = types.str; + default = "zroot"; + description = "ZFS pool name"; + }; + }; + config = mkIf cfg.enable { + disko.devices = { + disk.main = { + type = "disk"; + device = cfg.disk; + content = { + type = "gpt"; + partitions = { + boot = { + size = "1G"; + type = "EF00"; + content = { + type = "filesystem"; + format = "vfat"; + mountpoint = "/boot"; + }; + }; + zfs = { + size = "100%"; + content = { + type = "zfs"; + pool = cfg.zfsPool; + }; + }; + }; + }; + }; + zpool.${cfg.zfsPool} = { + type = "zpool"; + mode = ""; + datasets = { + "root" = { type = "zfs_fs"; mountpoint = "/"; }; + "root/nix" = { type = "zfs_fs"; mountpoint = "/nix"; }; + "root/home" = { type = "zfs_fs"; mountpoint = "/home"; }; + "root/persist" = { type = "zfs_fs"; mountpoint = "/persist"; }; + "root/var" = { type = "zfs_fs"; mountpoint = "/var"; }; + "root/tmp" = { type = "zfs_fs"; mountpoint = "/tmp"; }; + }; + }; + }; + }; +} diff --git a/src/modules/filesystem/impermanence.nix b/src/modules/filesystem/impermanence.nix new file mode 100644 index 0000000..9aa34af --- /dev/null +++ b/src/modules/filesystem/impermanence.nix @@ -0,0 +1,63 @@ +{ username, ... }: + +{ + environment.persistence."/persist" = { + hideMounts = true; + + directories = [ + "/etc/nixos" + "/etc/NetworkManager" + "/etc/ssh" + "/etc/udev" + "/var/lib/nixos" + "/var/lib/systemd" + "/var/lib/bluetooth" + "/var/lib/tor" + "/var/log" + "/var/lib/microvm" + ]; + + files = [ + "/etc/machine-id" + "/etc/resolv.conf" + "/etc/adjtime" + ]; + + users.${username} = { + directories = [ + "Downloads" + "Documents" + "Immagini" + "Video" + "Musica" + "Projects" + "Go" + ".ssh" + ".gnupg" + ".local/share/keyrings" + ".config/gtk-3.0" + ".config/gtk-4.0" + ".config/qt5ct" + ".config/qt6ct" + ".config/KDE" + ".config/kdeglobals" + ".config/systemd" + ".cache/mozilla" + ".mozilla" + ]; + files = [ + ".config/user-dirs.dirs" + ]; + }; + }; + fileSystems."/persist" = { + device = "zroot/root/persist"; + fsType = "zfs"; + neededForBoot = true; + }; + fileSystems."/home" = { + device = "zroot/root/home"; + fsType = "zfs"; + neededForBoot = true; + }; +} diff --git a/src/modules/filesystem/zfs.nix b/src/modules/filesystem/zfs.nix new file mode 100644 index 0000000..7cd9d63 --- /dev/null +++ b/src/modules/filesystem/zfs.nix @@ -0,0 +1,64 @@ +{ lib, ... }: +with lib; { + options.bora.filesystem.bootDevice = mkOption { + type = types.str; + default = "/dev/disk/by-uuid/BOOT-UUID"; + description = "Boot partition device path"; + }; + config = { + services.zfs = { + trim = { + enable = true; + interval = "weekly"; + }; + autoScrub = { + enable = true; + interval = "monthly"; + }; + autoSnapshot = { + enable = true; + flags = "-k -p --utc"; + }; + }; + boot.zfs = { + forceImportRoot = false; + forceImportAll = false; + allowHibernation = false; + requestEncryptionCredentials = true; + }; + services.sanoid = { + enable = true; + templates = { + default = { + hourly = 24; + daily = 30; + weekly = 12; + monthly = 6; + yearly = 2; + autosnap = true; + autoprune = true; + }; + critical = { + hourly = 48; + daily = 90; + weekly = 24; + monthly = 12; + yearly = 5; + autosnap = true; + autoprune = true; + }; + ephemeral = { + hourly = 2; + daily = 1; + autosnap = true; + autoprune = true; + }; + }; + }; + boot.kernelParams = [ + "zfs.zfs_arc_max=8589934592" + "zfs.zfs_arc_min=1073741824" + ]; + networking.hostId = "deadbeef"; + }; +} diff --git a/src/modules/hardware/cpu.nix b/src/modules/hardware/cpu.nix new file mode 100644 index 0000000..9ab0d23 --- /dev/null +++ b/src/modules/hardware/cpu.nix @@ -0,0 +1,30 @@ +{ config, lib, hardwareDB, ... }: +with lib; +let + cpuVendor = config.bora.hardware.cpuVendor or "intel"; + cpuCfg = hardwareDB.cpu.${cpuVendor} or hardwareDB.cpu.intel; +in +{ + options.bora.hardware = { + cpuVendor = mkOption { + type = types.enum [ "intel" "amd" "arm" ]; + default = "intel"; + description = "CPU vendor for optimal settings"; + }; + enableMitigations = mkOption { + type = types.bool; + default = true; + description = "Enable CPU vulnerability mitigations"; + }; + }; + config = { + hardware.cpu.intel.updateMicrocode = mkIf (cpuVendor == "intel") true; + hardware.cpu.amd.updateMicrocode = mkIf (cpuVendor == "amd") true; + boot.kernelModules = cpuCfg.kernelModules; + boot.kernelParams = cpuCfg.kernelParams + ++ (if config.bora.hardware.enableMitigations + then [ "mitigations=auto" ] + else [ "mitigations=off" ]); + powerManagement.cpuFreqGovernor = mkDefault cpuCfg.power.governor; + }; +} diff --git a/src/modules/hardware/default.nix b/src/modules/hardware/default.nix new file mode 100644 index 0000000..8d7f9e4 --- /dev/null +++ b/src/modules/hardware/default.nix @@ -0,0 +1,8 @@ +_: +{ + imports = [ + ./cpu.nix + ./gpu.nix + ./platform.nix + ]; +} diff --git a/src/modules/hardware/gpu.nix b/src/modules/hardware/gpu.nix new file mode 100644 index 0000000..5b6d358 --- /dev/null +++ b/src/modules/hardware/gpu.nix @@ -0,0 +1,47 @@ +{ config, lib, pkgs, hardwareDB, ... }: +with lib; +let + gpuVendor = config.bora.hardware.gpuVendor or "amd"; + gpuCfg = hardwareDB.gpu.${gpuVendor} or hardwareDB.gpu.amd; +in +{ + options.bora.hardware = { + gpuVendor = mkOption { + type = types.enum [ "nvidia" "amd" "intel" ]; + default = "amd"; + description = "GPU vendor for optimal drivers"; + }; + enableNvidiaPrime = mkOption { + type = types.bool; + default = false; + description = "Enable NVIDIA Optimus/PRIME (laptop)"; + }; + }; + config = { + services.xserver.videoDrivers = gpuCfg.drivers; + hardware.nvidia = mkIf (gpuVendor == "nvidia") { + modesetting.enable = true; + powerManagement.enable = true; + powerManagement.finegrained = false; + open = false; + nvidiaSettings = false; + prime = { + sync.enable = config.bora.hardware.enableNvidiaPrime; + offload = { + enable = !config.bora.hardware.enableNvidiaPrime; + enableOffloadCmd = true; + }; + }; + }; + boot.kernelParams = gpuCfg.kernelParams; + environment.sessionVariables = gpuCfg.env; + hardware.graphics = { + enable = true; + enable32Bit = true; + extraPackages = with pkgs; [ + (if gpuVendor == "intel" then intel-media-driver + else libva-vdpau-driver) + ]; + }; + }; +} diff --git a/src/modules/hardware/platform.nix b/src/modules/hardware/platform.nix new file mode 100644 index 0000000..7c59f20 --- /dev/null +++ b/src/modules/hardware/platform.nix @@ -0,0 +1,17 @@ +{ config, lib, hardwareDB, ... }: +with lib; +let + inherit (hardwareDB) profileOpts; +in +{ + options.bora.hardwareProfile = mkOption { + type = types.enum [ "desktop" "laptop" "server" ]; + default = "desktop"; + description = "Hardware profile for power/mitigation tuning"; + }; + config = mkMerge [ + (mkIf (config.bora.hardwareProfile == "desktop") profileOpts.desktop) + (mkIf (config.bora.hardwareProfile == "laptop") profileOpts.laptop) + (mkIf (config.bora.hardwareProfile == "server") profileOpts.server) + ]; +} diff --git a/src/modules/network/base.nix b/src/modules/network/base.nix new file mode 100644 index 0000000..3f710ef --- /dev/null +++ b/src/modules/network/base.nix @@ -0,0 +1,25 @@ +{ lib, pkgs, ... }: +{ + networking = { + networkmanager.enable = true; + useDHCP = lib.mkDefault true; + firewall.enable = false; + }; + services.avahi = { + enable = lib.mkDefault false; + nssmdns4 = lib.mkDefault true; + publish = { + enable = true; + addresses = true; + workstation = true; + userServices = true; + }; + }; + environment.systemPackages = with pkgs; [ + networkmanagerapplet + iwd + iw + wirelesstools + ethtool + ]; +} diff --git a/src/modules/network/default.nix b/src/modules/network/default.nix new file mode 100644 index 0000000..ac3716f --- /dev/null +++ b/src/modules/network/default.nix @@ -0,0 +1,7 @@ +_: +{ + imports = [ + ./base.nix + ./dns.nix + ]; +} diff --git a/src/modules/network/dns.nix b/src/modules/network/dns.nix new file mode 100644 index 0000000..12f8c13 --- /dev/null +++ b/src/modules/network/dns.nix @@ -0,0 +1,20 @@ +_: +{ + services.resolved = { + enable = true; + dnssec = "true"; + domains = [ "~." ]; + fallbackDns = [ + "9.9.9.9" + "149.112.112.112" + "2620:fe::fe" + "2620:fe::9" + ]; + extraConfig = '' + DNSStubListener=yes + DNSOverTLS=yes + DNS=9.9.9.9 149.112.112.112 + Domains=~. + ''; + }; +} diff --git a/src/modules/security/default.nix b/src/modules/security/default.nix new file mode 100644 index 0000000..2b4a0f3 --- /dev/null +++ b/src/modules/security/default.nix @@ -0,0 +1,8 @@ +_: +{ + imports = [ + ./firewall.nix + ./hardening.nix + ./ssh.nix + ]; +} diff --git a/src/modules/security/firewall.nix b/src/modules/security/firewall.nix new file mode 100644 index 0000000..fc6ab71 --- /dev/null +++ b/src/modules/security/firewall.nix @@ -0,0 +1,49 @@ +_: +{ + networking.nftables = { + enable = true; + ruleset = '' + table inet filter { + chain input { + type filter hook input priority 0; policy drop; + ct state invalid drop; + ct state { established, related } accept; + iifname lo accept; + icmp type { + echo-request, destination-unreachable, + time-exceeded, parameter-problem + } limit rate 10/second accept; + meta l4proto ipv6-icmp icmpv6 type { + echo-request, destination-unreachable, + time-exceeded, parameter-problem, + nd-router-advert, nd-neighbor-solicit, + nd-neighbor-advert, nd-router-solicit + } limit rate 10/second accept; + tcp dport 22 ip saddr { + 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16 + } accept; + udp dport 5353 ip saddr { + 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16 + } accept; + udp dport { 67, 68 } accept; + log prefix "NF:DROP-INPUT: " drop; + } + chain forward { + type filter hook forward priority 0; policy drop; + ct state { established, related } accept; + iifname "microvm" accept; + log prefix "NF:DROP-FORWARD: " drop; + } + chain output { + type filter hook output priority 0; policy accept; + } + } + table inet nat { + chain postrouting { + type nat hook postrouting priority 100; + oifname "eth0" masquerade; + } + } + ''; + }; +} diff --git a/src/modules/security/hardening.nix b/src/modules/security/hardening.nix new file mode 100644 index 0000000..7bbaced --- /dev/null +++ b/src/modules/security/hardening.nix @@ -0,0 +1,47 @@ +{ lib, pkgs, ... }: +{ + security = { + apparmor = { + enable = true; + enableCache = true; + packages = [ pkgs.apparmor-profiles ]; + }; + protectKernelImage = true; + allowUserNamespaces = true; + lockKernelModules = false; + wrappers = { }; + audit = { + enable = true; + rules = [ + "-w /etc/nixos -p wa -k nixos-config" + "-w /nix/store -p wa -k nix-store" + "-a exit,always -S execve -k process-exec" + "-a exit,always -S mount -k mount" + ]; + }; + }; + boot.kernelParams = [ + "quiet" + "slab_nomerge" + "init_on_alloc=1" + "init_on_free=1" + "page_alloc.shuffle=1" + "pti=on" + "vsyscall=none" + "debugfs=off" + "module.sig_enforce=1" + "lockdown=confidentiality" + ]; + systemd = { + services = { + avahi-daemon.enable = lib.mkDefault false; + cups.enable = lib.mkDefault false; + bluetooth.enable = lib.mkDefault false; + }; + settings.Manager = { + DefaultTimeoutStopSec = "10s"; + DefaultTimeoutStartSec = "30s"; + DefaultDeviceTimeoutSec = "30s"; + }; + }; +} diff --git a/src/modules/security/ssh.nix b/src/modules/security/ssh.nix new file mode 100644 index 0000000..d4ba529 --- /dev/null +++ b/src/modules/security/ssh.nix @@ -0,0 +1,46 @@ +_: +{ + services.openssh = { + enable = true; + settings = { + PermitRootLogin = "no"; + PasswordAuthentication = false; + KbdInteractiveAuthentication = false; + AuthenticationMethods = "publickey"; + PubkeyAuthentication = true; + UsePAM = false; + MaxAuthTries = 3; + MaxSessions = 4; + MaxStartups = "10:30:60"; + LoginGraceTime = 30; + AllowTcpForwarding = false; + AllowAgentForwarding = false; + PermitTunnel = false; + X11Forwarding = false; + ClientAliveInterval = 300; + ClientAliveCountMax = 0; + Ciphers = [ "chacha20-poly1305@openssh.com" "aes256-gcm@openssh.com" ]; + KexAlgorithms = [ "curve25519-sha256" "diffie-hellman-group-exchange-sha256" ]; + Macs = [ "hmac-sha2-512-etm@openssh.com" "hmac-sha2-256-etm@openssh.com" ]; + HostKeyAlgorithms = "ssh-ed25519,rsa-sha2-512"; + Compression = "no"; + }; + hostKeys = [ + { + path = "/persist/ssh/ssh_host_ed25519_key"; + type = "ed25519"; + } + { + path = "/persist/ssh/ssh_host_rsa_key"; + type = "rsa"; + bits = 4096; + } + ]; + }; + services.fail2ban = { + enable = true; + maxretry = 3; + bantime = "24h"; + banaction = "nftables-multiport"; + }; +} diff --git a/src/profiles/developer.nix b/src/profiles/developer.nix new file mode 100644 index 0000000..5e47ae0 --- /dev/null +++ b/src/profiles/developer.nix @@ -0,0 +1,25 @@ +{ lib, pkgs, ... }: +with lib; +{ + imports = [ + ./workstation.nix + ]; + + bora = { + containers.instancePool.enable = mkDefault false; + hardware.cpuVendor = mkDefault "amd"; + hardware.gpuVendor = mkDefault "nvidia"; + hardwareProfile = mkDefault "desktop"; + }; + + environment.systemPackages = with pkgs; [ + gcc + clang + nodejs + python3 + rustc + cargo + go + nginx + ]; +} diff --git a/src/profiles/minimal.nix b/src/profiles/minimal.nix new file mode 100644 index 0000000..540ea52 --- /dev/null +++ b/src/profiles/minimal.nix @@ -0,0 +1,18 @@ +{ lib, ... }: +with lib; +{ + bora = { + desktop = { + kde.enable = mkDefault false; + audio.enable = mkDefault false; + layout.enable = mkDefault false; + }; + hardwareProfile = mkDefault "server"; + hardware = { + cpuVendor = mkDefault "intel"; + gpuVendor = mkDefault "intel"; + }; + }; + + services.openssh.enable = true; +} diff --git a/src/profiles/server.nix b/src/profiles/server.nix new file mode 100644 index 0000000..f3b39b9 --- /dev/null +++ b/src/profiles/server.nix @@ -0,0 +1,19 @@ +{ lib, ... }: +with lib; +{ + bora = { + desktop = { + kde.enable = mkDefault false; + audio.enable = mkDefault false; + layout.enable = mkDefault false; + }; + containers.instancePool.enable = mkDefault false; + hardwareProfile = mkDefault "server"; + hardware.cpuVendor = mkDefault "intel"; + hardware.gpuVendor = mkDefault "intel"; + }; + + services.openssh.enable = true; + networking.firewall.allowedTCPPorts = [ 22 80 443 ]; + systemd.services.sshd.wantedBy = [ "multi-user.target" ]; +} diff --git a/src/profiles/workstation.nix b/src/profiles/workstation.nix new file mode 100644 index 0000000..c05ed10 --- /dev/null +++ b/src/profiles/workstation.nix @@ -0,0 +1,12 @@ +{ lib, ... }: +with lib; +{ + bora = { + desktop = { + kde.enable = mkDefault true; + audio.enable = mkDefault true; + layout.enable = mkDefault true; + }; + hardwareProfile = mkDefault "desktop"; + }; +} diff --git a/tests/default.nix b/tests/default.nix new file mode 100644 index 0000000..9ca494b --- /dev/null +++ b/tests/default.nix @@ -0,0 +1,230 @@ +{ system ? builtins.currentSystem, nixpkgs ? import { inherit system; } }: + +let + inherit (nixpkgs) lib; + boraLib = import ../lib { inherit nixpkgs; }; + hw = boraLib.hardware; + + inherit (builtins) toString; + + assertEq = name: actual: expected: + if actual == expected + then { ${name} = { ok = true; }; } + else { ${name} = { ok = false; inherit expected actual; }; }; + +in + +# ============================================================================= + # hardware.nix tests + # ============================================================================= +( + let + cpuVendors = hw.cpu; + gpuVendors = hw.gpu; + profiles = hw.profileOpts; + in + + { + # CPU vendor structures + testCpuIntel = assertEq "cpu.intel.name" cpuVendors.intel.name "Intel"; + testCpuIntelModules = assertEq "cpu.intel.kernelModules" cpuVendors.intel.kernelModules [ "kvm-intel" "intel_rapl" "intel_uncore" ]; + + testCpuAMD = assertEq "cpu.amd.name" cpuVendors.amd.name "AMD"; + testCpuAMDModules = assertEq "cpu.amd.kernelModules" cpuVendors.amd.kernelModules [ "kvm-amd" "amd_rapl" "amd_pstate" ]; + + testCpuARM = assertEq "cpu.arm.name" cpuVendors.arm.name "ARM"; + + # gpuConfig / cpuConfig accessors + testGpuConfig = assertEq "gpuConfig nvidia" (hw.gpuConfig "nvidia").name "NVIDIA"; + testCpuConfig = assertEq "cpuConfig intel" (hw.cpuConfig "intel").name "Intel"; + + # fallback: unknown GPU returns AMD + testGpuConfigFallback = assertEq "gpuConfig unknown->amd" (hw.gpuConfig "unknown").name "AMD"; + + # GPU vendor structures + testGpuNvidia = assertEq "gpu.nvidia.name" gpuVendors.nvidia.name "NVIDIA"; + testGpuNvidiaDrivers = assertEq "gpu.nvidia.drivers" gpuVendors.nvidia.drivers [ "nvidia" ]; + testGpuNvidiaPrime = assertEq "gpu.nvidia.prime.sync" gpuVendors.nvidia.prime.sync false; + testGpuNvidiaPrimeOffload = assertEq "gpu.nvidia.prime.offload" gpuVendors.nvidia.prime.offload true; + + testGpuAMD = assertEq "gpu.amd.name" gpuVendors.amd.name "AMD"; + testGpuAMDDrivers = assertEq "gpu.amd.drivers" gpuVendors.amd.drivers [ "amdgpu" ]; + + testGpuIntel = assertEq "gpu.intel.name" gpuVendors.intel.name "Intel"; + testGpuIntelDrivers = assertEq "gpu.intel.drivers" gpuVendors.intel.drivers [ "modesetting" ]; + + # Profile options + testProfileDesktop = assertEq "profileOpts.desktop.powerManagement.enable" profiles.desktop.powerManagement.enable true; + testProfileLaptop = assertEq "profileOpts.laptop.powerManagement.enable" profiles.laptop.powerManagement.enable true; + testProfileServer = assertEq "profileOpts.server.powerManagement.enable" profiles.server.powerManagement.enable false; + } +) // + +# ============================================================================= +# spring.nix tests +# ============================================================================= +( + let + spring = import ../lib/spring.nix { inherit lib; pkgs = nixpkgs; }; + + # Minimal bean set for topological sort test + healthyBeans = { + webapp = { enable = true; class = "WebApp"; deps = [ "database" "cache" ]; }; + database = { enable = true; class = "Database"; deps = [ ]; }; + cache = { enable = true; class = "Cache"; deps = [ "database" ]; }; + queue = { enable = true; class = "Queue"; deps = [ "cache" ]; }; + }; + + # Beans with circular dependency for circular detection test + circularBeans = { + a = { enable = true; class = "A"; deps = [ "b" ]; }; + b = { enable = true; class = "B"; deps = [ "c" ]; }; + c = { enable = true; class = "C"; deps = [ "a" ]; }; + }; + + # Empty bean set edge case + emptyBeans = { }; + + in + { + # Test bean graph construction + testBeanGraphWebapp = assertEq "beanGraph.webapp.deps" + (builtins.head (builtins.filter (e: e.name == "webapp") (spring.beanGraph healthyBeans))).deps + [ "database" "cache" ]; + + testBeanGraphDatabase = assertEq "beanGraph.database.deps" + (builtins.head (builtins.filter (e: e.name == "database") (spring.beanGraph healthyBeans))).deps + [ ]; + + # Test topological sort produces valid order + testTsortProducesAll = assertEq "tsort length" + (builtins.length (spring.tsort (spring.beanGraph healthyBeans))) + 4; + + testTsortOrderValid = + let + sorted = spring.tsort (spring.beanGraph healthyBeans); + names = map (n: n.name) sorted; + dbIdx = builtins.elemAt (builtins.filter (i: builtins.elemAt names i == "database") (lib.genList (x: x) (builtins.length names))) 0; + webappIdx = builtins.elemAt (builtins.filter (i: builtins.elemAt names i == "webapp") (lib.genList (x: x) (builtins.length names))) 0; + in + assertEq "tsort.database_before_webapp" (dbIdx < webappIdx) true; + + # Test circular dependency detection + testCircularDetection = assertEq "detectCircular" + (builtins.length (spring.detectCircular circularBeans) > 0) + true; + + testCircularDetectionEmpty = assertEq "detectCircular.empty" + (spring.detectCircular emptyBeans) + [ ]; + + # Test resolveBeanConfig + testResolveConfig = assertEq "resolveBeanConfig" + (builtins.length (builtins.attrNames (spring.resolveBeanConfig healthyBeans))) + 4; + + # Test bean wrapper structure + testMkSystemdService = + let + service = spring.mkSystemdService "testapp" "mybean" { + enable = true; + class = "MyClass"; + deps = [ ]; + resources = { cpu = "2"; memory = "512M"; memoryMax = "1G"; pids = 512; ioRbps = "200M"; ioRops = 2000; ioWbps = "200M"; ioWops = 2000; numa = null; }; + healthcheck = null; + livenessProbe = null; + startupProbe = null; + dependsOn = [ ]; + after = [ ]; + wants = [ ]; + restartPolicy = "always"; + restartSec = 5; + config = { }; + environment = { }; + serviceType = "simple"; + timeoutStartSec = 30; + timeoutStopSec = 10; + }; + name = builtins.head (builtins.attrNames service); + def = builtins.head (builtins.attrValues service); + in + { + testServiceName = assertEq "service.name" name "spring-testapp-mybean"; + testServiceType = assertEq "service.serviceConfig.Type" def.serviceConfig.Type "simple"; + testServiceRestart = assertEq "service.serviceConfig.Restart" def.serviceConfig.Restart "always"; + testServiceMemoryMax = assertEq "service.serviceConfig.MemoryMax" def.serviceConfig.MemoryMax "1G"; + testServiceMemoryHigh = assertEq "service.serviceConfig.MemoryHigh" def.serviceConfig.MemoryHigh "512M"; + testServiceCPUQuota = assertEq "service.serviceConfig.CPUQuota" def.serviceConfig.CPUQuota "20%"; + testServiceOOM = assertEq "service.serviceConfig.OOMPolicy" def.serviceConfig.OOMPolicy "kill"; + testServiceWantedBy = assertEq "service.wantedBy" def.wantedBy [ "multi-user.target" ]; + testServiceHasThat = assertEq "service.hasScript" (def ? script) true; + }; + } +) // + +# ============================================================================= +# lib/default.nix tests +# ============================================================================= +( + let + boraLib = import ../lib { inherit nixpkgs; }; + in + { + testAtomicPreRebuildSnapshot = assertEq "atomic.preRebuildSnapshot" + (boraLib.atomic.preRebuildSnapshot "tank" "root" != "") + true; + + testAtomicBackupGeneration = assertEq "atomic.backupGeneration" + (boraLib.atomic.backupGeneration != "") + true; + } +) // + + # ============================================================================= + # Benchmark: topological sort performance with N beans + # ============================================================================= +( + let + spring = import ../lib/spring.nix { inherit lib; pkgs = nixpkgs; }; + + genChainBeans = n: + let + names = lib.genList (i: "bean-${toString i}") n; + beans = builtins.listToAttrs (map + (name: { + inherit name; + value = { + enable = true; + class = "Bean_${name}"; + deps = if name == "bean-0" then [ ] else [ "bean-${toString (builtins.fromJSON (builtins.elemAt (lib.splitString "-" name) 1) - 1)}" ]; + }; + }) + names); + in + beans; + + chain10 = genChainBeans 10; + chain50 = genChainBeans 50; + chain100 = genChainBeans 100; + + bench = name: beans: + let + start = builtins.currentTime or 0; + graph = spring.beanGraph beans; + sorted = spring.tsort graph; + duration = if builtins ? currentTime then builtins.currentTime - start else 0; + len = builtins.length sorted; + in + { + ${name} = { + ok = len == (builtins.length (builtins.attrNames beans)); + beans = len; + duration_seconds = duration; + }; + }; + in + (bench "benchmark.tsort.chain10" chain10) // + (bench "benchmark.tsort.chain50" chain50) // + (bench "benchmark.tsort.chain100" chain100) +) diff --git a/tests/modules.nix b/tests/modules.nix new file mode 100644 index 0000000..19519c3 --- /dev/null +++ b/tests/modules.nix @@ -0,0 +1,188 @@ +{ system ? builtins.currentSystem, nixpkgs ? import { inherit system; } }: + +let + assertEq = name: actual: expected: + if actual == expected + then { ${name} = { ok = true; }; } + else { ${name} = { ok = false; inherit expected actual; }; }; +in + +# ============================================================================= + # NixOS module integration tests + # ============================================================================= +( + let + hardwareDB = import ../lib/hardware.nix { inherit (nixpkgs) lib; }; + + # Minimal NixOS config that exercises module loading via configuration.nix + minimalConfig = nixpkgs.lib.nixosSystem { + system = "x86_64-linux"; + modules = [ + ../configuration.nix + (_: { + system.stateVersion = "25.11"; + users.users.root.hashedPassword = "!"; + boot.loader.grub.enable = false; + boot.loader.systemd-boot.enable = false; + fileSystems."/" = { device = "/dev/null"; fsType = "tmpfs"; }; + nixpkgs.config.allowUnfree = true; + }) + ]; + specialArgs = { + inherit hardwareDB; + hostname = "test"; + username = "testuser"; + hardwareProfile = "desktop"; + systemProfile = "minimal"; + }; + }; + in + { + testModuleLoading = + let + cfg = minimalConfig.config; + in + assertEq "module.loading.hostname" cfg.networking.hostName "test"; + + # Verify bora modules registered their options + testBoraOptionsExist = + let + cfg = minimalConfig.config; + in + assertEq "bora.options.exist" (cfg ? bora) true; + + # Verify enableNvidiaPrime defaults to false + testEnableNvidiaPrimeDefault = + let + cfg = minimalConfig.config; + in + assertEq "bora.hardware.enableNvidiaPrime.default" + (if cfg ? bora && cfg.bora ? hardware then (cfg.bora.hardware.enableNvidiaPrime or false) else false) + false; + + # Verify bora container options registered + testBoraContainerOptions = + let + cfg = minimalConfig.config; + in + assertEq "bora.options.containers.exist" (cfg ? bora && cfg.bora ? containers) true; + } +) // + +# ============================================================================= +# Module composition tests: profile + module interaction +# ============================================================================= +( + let + hardwareDB = import ../lib/hardware.nix { inherit (nixpkgs) lib; }; + + makeConfig = profile: + let + cfg = nixpkgs.lib.nixosSystem { + system = "x86_64-linux"; + modules = [ + ../configuration.nix + (_: { + system.stateVersion = "25.11"; + users.users.root.hashedPassword = "!"; + boot.loader.grub.enable = false; + boot.loader.systemd-boot.enable = false; + fileSystems."/" = { device = "/dev/null"; fsType = "tmpfs"; }; + nixpkgs.config.allowUnfree = true; + }) + ]; + specialArgs = { + inherit hardwareDB; + hostname = "test"; + username = "testuser"; + hardwareProfile = "desktop"; + systemProfile = profile; + }; + }; + in + cfg.config; + in + { + testProfileMinimal = + let + cfg = makeConfig "minimal"; + in + { + testProfileMinimalHostname = assertEq "profile.minimal.hostname" cfg.networking.hostName "test"; + testProfileMinimalSSH = assertEq "profile.minimal.openssh.enable" (cfg.services.openssh.enable or false) true; + }; + + testProfileServer = + let + cfg = makeConfig "server"; + in + { + testProfileServerSSH = assertEq "profile.server.openssh.enable" (cfg.services.openssh.enable or false) true; + testProfileFirewallPorts = assertEq "profile.server.firewall.ports" (cfg.networking.firewall.allowedTCPPorts or [ ]) [ 22 80 443 ]; + }; + } +) // + + # ============================================================================= + # Spring bean config integration + # ============================================================================= +( + let + hardwareDB = import ../lib/hardware.nix { inherit (nixpkgs) lib; }; + + configWithSpring = nixpkgs.lib.nixosSystem { + system = "x86_64-linux"; + modules = [ + ../configuration.nix + (_: { + system.stateVersion = "25.11"; + users.users.root.hashedPassword = "!"; + boot.loader.grub.enable = false; + boot.loader.systemd-boot.enable = false; + fileSystems."/" = { device = "/dev/null"; fsType = "tmpfs"; }; + nixpkgs.config.allowUnfree = true; + }) + (_: { + bora.spring.application = { + enable = true; + name = "testapp"; + }; + bora.spring.beans = { + database = { + enable = true; + class = "Database"; + resources = { cpu = "1"; memory = "256M"; memoryMax = "512M"; pids = 256; }; + }; + webapp = { + enable = true; + class = "WebApp"; + deps = [ "database" ]; + resources = { cpu = "2"; memory = "512M"; memoryMax = "1G"; pids = 512; }; + }; + }; + }) + ]; + specialArgs = { + inherit hardwareDB; + hostname = "test-spring"; + username = "testuser"; + hardwareProfile = "desktop"; + systemProfile = "minimal"; + }; + }; + in + { + testSpringIntegration = + let + services = configWithSpring.config.systemd.services or { }; + slices = configWithSpring.config.systemd.slices or { }; + hasService = n: builtins.hasAttr n services; + hasSlice = n: builtins.hasAttr n slices; + in + { + testSpringDatabaseService = assertEq "spring.service.database" (hasService "spring-testapp-database") true; + testSpringWebappService = assertEq "spring.service.webapp" (hasService "spring-testapp-webapp") true; + testSpringSlice = assertEq "spring.slice" (hasSlice "system-testapp.slice") true; + }; + } +) diff --git a/tests/shell.nix b/tests/shell.nix new file mode 100644 index 0000000..8b3b00d --- /dev/null +++ b/tests/shell.nix @@ -0,0 +1,16 @@ +{ pkgs ? import { } }: + +pkgs.mkShell { + buildInputs = with pkgs; [ + nixpkgs-fmt + statix + deadnix + nixos-anywhere + ]; + shellHook = '' + echo "Bora Test Environment" + echo "Run: statix check ../src" + echo "Run: deadnix ../src" + echo "Run: nixpkgs-fmt --check ../src" + ''; +}