├── installer-files ├── var │ └── www │ │ └── html │ │ └── opinionated-debian-installer │ │ └── .empty ├── etc │ ├── dconf │ │ ├── profile │ │ │ └── user │ │ └── db │ │ │ └── local.d │ │ │ └── 01-favorite-apps │ ├── sddm.conf.d │ │ └── autologin.conf │ ├── lightdm │ │ └── lightdm.conf.d │ │ │ └── 10-autologin.conf │ ├── repart.d │ │ └── 01-installer.conf │ ├── gdm3 │ │ └── daemon.conf │ └── systemd │ │ └── system │ │ ├── installer_backend.service │ │ └── installer_tui.service ├── home │ └── live │ │ ├── Desktop │ │ └── installer.desktop │ │ └── .config │ │ └── plasma-welcomerc ├── usr │ └── share │ │ ├── applications │ │ └── installer.desktop │ │ └── plasma │ │ └── plasma-welcome │ │ └── intro-customization.desktop └── boot │ └── efi │ └── installer.ini ├── readme-files ├── gui.png ├── virt-manager-uefi.png ├── Screenshot_mok_import_01.png ├── Screenshot_mok_import_02.png ├── Screenshot_mok_import_03.png ├── Screenshot_mok_import_04.png ├── Screenshot_mok_import_05.png └── Screenshot_mok_import_06.png ├── internal-tools ├── upload.sh ├── virsh │ ├── image_as_usb.xml │ ├── image_as_virtio.xml │ ├── build.xml │ ├── disk_a_as_virtio.xml │ ├── build.sh │ └── test.sh ├── build-compiled-components.sh ├── dummy.sh ├── rss.py └── release.txt ├── frontend ├── src │ ├── main.js │ ├── assets │ │ ├── Screenshot_mok_import_01.png │ │ ├── Screenshot_mok_import_02.png │ │ ├── Screenshot_mok_import_03.png │ │ ├── Screenshot_mok_import_04.png │ │ ├── Screenshot_mok_import_05.png │ │ ├── Screenshot_mok_import_06.png │ │ ├── base.css │ │ ├── timezones.txt │ │ └── Ceratopsian_installer.svg │ ├── components │ │ └── Password.vue │ └── App.vue ├── index.html ├── package.json ├── .gitignore └── vite.config.js ├── .gitignore ├── backend ├── go.mod ├── test_data │ ├── login.json │ └── lsblk.json ├── backendHelpers.go ├── backendWebsocket.go ├── tui_test.go ├── tuiPassword.go ├── main.go ├── tuiModel.go ├── tuiWizard.go ├── tuiRest.go ├── backend.go ├── go.sum ├── tui.go ├── backendHandlers.go └── timezones.txt ├── .github ├── dependabot.yml └── workflows │ └── build.yml ├── README.md ├── make_image.sh └── installer.sh /installer-files/var/www/html/opinionated-debian-installer/.empty: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /installer-files/etc/dconf/profile/user: -------------------------------------------------------------------------------- 1 | user-db:user 2 | system-db:local -------------------------------------------------------------------------------- /readme-files/gui.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/r0b0/debian-installer/HEAD/readme-files/gui.png -------------------------------------------------------------------------------- /installer-files/etc/sddm.conf.d/autologin.conf: -------------------------------------------------------------------------------- 1 | [Autologin] 2 | User=live 3 | Session=plasma 4 | 5 | -------------------------------------------------------------------------------- /internal-tools/upload.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | rsync -av --delete ~/opinionated-debian-installer odin: 4 | -------------------------------------------------------------------------------- /installer-files/etc/lightdm/lightdm.conf.d/10-autologin.conf: -------------------------------------------------------------------------------- 1 | [Seat:*] 2 | autologin-user=live 3 | autologin-user-timeout=0 -------------------------------------------------------------------------------- /frontend/src/main.js: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue' 2 | import App from './App.vue' 3 | 4 | createApp(App).mount('#app') 5 | -------------------------------------------------------------------------------- /readme-files/virt-manager-uefi.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/r0b0/debian-installer/HEAD/readme-files/virt-manager-uefi.png -------------------------------------------------------------------------------- /readme-files/Screenshot_mok_import_01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/r0b0/debian-installer/HEAD/readme-files/Screenshot_mok_import_01.png -------------------------------------------------------------------------------- /readme-files/Screenshot_mok_import_02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/r0b0/debian-installer/HEAD/readme-files/Screenshot_mok_import_02.png -------------------------------------------------------------------------------- /readme-files/Screenshot_mok_import_03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/r0b0/debian-installer/HEAD/readme-files/Screenshot_mok_import_03.png -------------------------------------------------------------------------------- /readme-files/Screenshot_mok_import_04.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/r0b0/debian-installer/HEAD/readme-files/Screenshot_mok_import_04.png -------------------------------------------------------------------------------- /readme-files/Screenshot_mok_import_05.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/r0b0/debian-installer/HEAD/readme-files/Screenshot_mok_import_05.png -------------------------------------------------------------------------------- /readme-files/Screenshot_mok_import_06.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/r0b0/debian-installer/HEAD/readme-files/Screenshot_mok_import_06.png -------------------------------------------------------------------------------- /frontend/src/assets/Screenshot_mok_import_01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/r0b0/debian-installer/HEAD/frontend/src/assets/Screenshot_mok_import_01.png -------------------------------------------------------------------------------- /frontend/src/assets/Screenshot_mok_import_02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/r0b0/debian-installer/HEAD/frontend/src/assets/Screenshot_mok_import_02.png -------------------------------------------------------------------------------- /frontend/src/assets/Screenshot_mok_import_03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/r0b0/debian-installer/HEAD/frontend/src/assets/Screenshot_mok_import_03.png -------------------------------------------------------------------------------- /frontend/src/assets/Screenshot_mok_import_04.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/r0b0/debian-installer/HEAD/frontend/src/assets/Screenshot_mok_import_04.png -------------------------------------------------------------------------------- /frontend/src/assets/Screenshot_mok_import_05.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/r0b0/debian-installer/HEAD/frontend/src/assets/Screenshot_mok_import_05.png -------------------------------------------------------------------------------- /frontend/src/assets/Screenshot_mok_import_06.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/r0b0/debian-installer/HEAD/frontend/src/assets/Screenshot_mok_import_06.png -------------------------------------------------------------------------------- /installer-files/etc/repart.d/01-installer.conf: -------------------------------------------------------------------------------- 1 | # systemd-repart configuration file 2 | 3 | [Partition] 4 | Type=root 5 | # nothing configured => extend this partition -------------------------------------------------------------------------------- /installer-files/etc/dconf/db/local.d/01-favorite-apps: -------------------------------------------------------------------------------- 1 | [org/gnome/shell] 2 | favorite-apps=['installer.desktop', 'firefox-esr.desktop', 'libreoffice-writer.desktop', 'org.gnome.Nautilus.desktop', 'yelp.desktop'] 3 | -------------------------------------------------------------------------------- /internal-tools/virsh/image_as_usb.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /internal-tools/build-compiled-components.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | cd frontend 4 | rm -rf dist 5 | npm i 6 | npm run build 7 | 8 | cd ../backend 9 | CGO_ENABLED=0 go build -v -ldflags="-s -w" -o opinionated-installer 10 | -------------------------------------------------------------------------------- /installer-files/home/live/Desktop/installer.desktop: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | Encoding=UTF-8 3 | Version=1.0 4 | Type=Application 5 | Exec=firefox http://localhost:5000/ 6 | Name=Opinionated Debian Installer 7 | Icon=emblem-debian.png 8 | -------------------------------------------------------------------------------- /installer-files/usr/share/applications/installer.desktop: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | Encoding=UTF-8 3 | Version=1.0 4 | Type=Application 5 | Exec=firefox http://localhost:5000/ 6 | Name=Opinionated Debian Installer 7 | Icon=emblem-debian.png 8 | -------------------------------------------------------------------------------- /internal-tools/virsh/image_as_virtio.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /installer-files/home/live/.config/plasma-welcomerc: -------------------------------------------------------------------------------- 1 | # plasma6 welcome center customization 2 | # https://invent.kde.org/plasma/plasma-welcome#for-live-distributions 3 | 4 | [General] 5 | LiveEnvironment=true 6 | LiveInstaller=installer 7 | -------------------------------------------------------------------------------- /internal-tools/virsh/build.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /internal-tools/virsh/disk_a_as_virtio.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /installer-files/etc/gdm3/daemon.conf: -------------------------------------------------------------------------------- 1 | # /etc/gdm3/daemon.conf 2 | # enable autologin for the installer 3 | 4 | [daemon] 5 | AutomaticLoginEnable = true 6 | AutomaticLogin = live 7 | 8 | [security] 9 | 10 | [xdmcp] 11 | 12 | [chooser] 13 | 14 | [debug] 15 | -------------------------------------------------------------------------------- /internal-tools/dummy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echo Environ swap: ${ENABLE_SWAP} ${SWAP_SIZE} 4 | echo Environ nvidia: \"${NVIDIA_PACKAGE}\" 5 | echo Environ luks password: \"$LUKS_PASSWORD\" 6 | 7 | for i in {1..5}; do 8 | echo Counting ${i} 9 | echo ${i} > ${PROGRESS_PIPE} 10 | sleep 2 11 | done 12 | -------------------------------------------------------------------------------- /frontend/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Opinionated Debian Installer 7 | 8 | 9 |
10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.idea 2 | /venv 3 | /installer-files/var/www/html/opinionated-debian-installer/assets/ 4 | /installer-files/var/www/html/opinionated-debian-installer/index.html 5 | disk_wiped.txt 6 | efi-part.uuid 7 | first_phase_done.txt 8 | installer-image-part.uuid 9 | backend/opinionated-installer 10 | repart.d/01_efi.conf 11 | repart.d/02_baseImage.conf 12 | *.torrent 13 | feed.xml 14 | */.fleet 15 | -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "odin-vue", 3 | "type": "module", 4 | "version": "0.0.0", 5 | "scripts": { 6 | "dev": "vite", 7 | "build": "vite build", 8 | "preview": "vite preview --port 4173" 9 | }, 10 | "dependencies": { 11 | "vue": "^3.5.25" 12 | }, 13 | "devDependencies": { 14 | "@vitejs/plugin-vue": "^6.0.2", 15 | "vite": "^7.2.6" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /frontend/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | .DS_Store 12 | dist 13 | dist-ssr 14 | coverage 15 | *.local 16 | 17 | /cypress/videos/ 18 | /cypress/screenshots/ 19 | 20 | # Editor directories and files 21 | .vscode/* 22 | !.vscode/extensions.json 23 | .idea 24 | *.suo 25 | *.ntvs* 26 | *.njsproj 27 | *.sln 28 | *.sw? 29 | -------------------------------------------------------------------------------- /internal-tools/virsh/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | VM=debian2025 4 | 5 | virsh -c qemu:///system detach-device ${VM} --current --file image_as_usb.xml 6 | virsh -c qemu:///system detach-device ${VM} --current --file disk_a_as_virtio.xml 7 | 8 | virsh -c qemu:///system attach-device ${VM} --current --file build.xml 9 | virsh -c qemu:///system attach-device ${VM} --current --file image_as_virtio.xml 10 | 11 | virsh -c qemu:///system start ${VM} 12 | -------------------------------------------------------------------------------- /internal-tools/virsh/test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | VM=debian2025 4 | 5 | virsh -c qemu:///system detach-device ${VM} --current --file build.xml 6 | virsh -c qemu:///system detach-device ${VM} --current --file image_as_virtio.xml 7 | 8 | virsh -c qemu:///system attach-device ${VM} --current --file image_as_usb.xml 9 | virsh -c qemu:///system attach-device ${VM} --current --file disk_a_as_virtio.xml 10 | 11 | virsh -c qemu:///system start ${VM} 12 | -------------------------------------------------------------------------------- /installer-files/etc/systemd/system/installer_backend.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Opinionated Debian Installer - Back-End 3 | After=network.target 4 | 5 | [Service] 6 | Environment=INSTALLER_SCRIPT=/installer.sh 7 | EnvironmentFile=-/boot/efi/installer.ini 8 | ExecStart=/sbin/opinionated-installer backend 9 | RuntimeDirectory=installer 10 | WorkingDirectory=/run/installer 11 | Type=notify 12 | User=root 13 | Group=root 14 | 15 | [Install] 16 | WantedBy=multi-user.target 17 | -------------------------------------------------------------------------------- /frontend/vite.config.js: -------------------------------------------------------------------------------- 1 | import { fileURLToPath, URL } from 'url' 2 | 3 | import { defineConfig } from 'vite' 4 | import vue from '@vitejs/plugin-vue' 5 | 6 | // https://vitejs.dev/config/ 7 | export default defineConfig({ 8 | plugins: [vue()], 9 | resolve: { 10 | alias: { 11 | '@': fileURLToPath(new URL('./src', import.meta.url)) 12 | } 13 | }, 14 | base: process.env.NODE_ENV === 'production' 15 | ? './' // prod 16 | : '/', // dev 17 | }) 18 | -------------------------------------------------------------------------------- /installer-files/etc/systemd/system/installer_tui.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Opinionated Debian Installer - TUI Front-end 3 | After=installer_backend.service 4 | Conflicts=getty@tty1.service 5 | After=getty@tty1.service 6 | 7 | [Service] 8 | ExecStart=/sbin/opinionated-installer tui 9 | DynamicUser=yes 10 | Restart=always 11 | RestartSec=2 12 | StandardInput=tty 13 | StandardOutput=tty 14 | TTYReset=yes 15 | TTYVHangup=yes 16 | TTYVTDisallocate=yes 17 | 18 | [Install] 19 | WantedBy=multi-user.target 20 | -------------------------------------------------------------------------------- /backend/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/r0b0/debian-installer/backend 2 | 3 | go 1.25.5 4 | 5 | require ( 6 | github.com/gdamore/tcell/v2 v2.13.1 7 | github.com/google/uuid v1.6.0 8 | github.com/rivo/tview v0.42.0 9 | golang.org/x/net v0.47.0 10 | ) 11 | 12 | require ( 13 | github.com/gdamore/encoding v1.0.1 // indirect 14 | github.com/lucasb-eyer/go-colorful v1.3.0 // indirect 15 | github.com/rivo/uniseg v0.4.7 // indirect 16 | golang.org/x/sys v0.38.0 // indirect 17 | golang.org/x/term v0.37.0 // indirect 18 | golang.org/x/text v0.31.0 // indirect 19 | ) 20 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "npm" # See documentation for possible values 9 | directory: "/frontend" # Location of package manifests 10 | schedule: 11 | interval: "weekly" 12 | - package-ecosystem: gomod 13 | directory: "/backend" 14 | schedule: 15 | interval: "weekly" -------------------------------------------------------------------------------- /backend/test_data/login.json: -------------------------------------------------------------------------------- 1 | {"environ":{"BACK_END_IP_ADDRESS":"0.0.0.0","ENABLE_SWAP":"partition","ENABLE_TPM":"true","FLASK_RUN_FROM_CLI":"true","HOME":"/root","HOSTNAME":"debian","INSTALLER_SCRIPT":"/installer.sh","INVOCATION_ID":"6e60a19d1d7c40afa1d540f26c0020ae","JOURNAL_STREAM":"8:12207","LANG":"C.UTF-8","LOGNAME":"root","MEMORY_PRESSURE_WATCH":"/sys/fs/cgroup/system.slice/installer_backend.service/memory.pressure","MEMORY_PRESSURE_WRITE":"c29tZSAyMDAwMDAgMjAwMDAwMAA=","NOTIFY_SOCKET":"/run/systemd/notify","PATH":"/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin","RUNTIME_DIRECTORY":"/run/installer","SHELL":"/bin/bash","SWAP_SIZE":"1","SYSTEMD_EXEC_PID":"774","TIMEZONE":"UTC","USER":"root","USERNAME":"user","USER_FULL_NAME":"Debian User","WERKZEUG_SERVER_FD":"3"},"has_efi":true,"hostname":"debian","running":false} 2 | -------------------------------------------------------------------------------- /installer-files/usr/share/plasma/plasma-welcome/intro-customization.desktop: -------------------------------------------------------------------------------- 1 | # plasma6 welcome center customization 2 | # https://invent.kde.org/plasma/plasma-welcome#for-live-distributions 3 | 4 | [Desktop Entry] 5 | # Required since this is a Desktop file; ignored 6 | Type=Application 7 | 8 | # Required; becomes the first paragraph on the Welcome page 9 | Name=Opinionated Debian Installer 10 | 11 | # Optional; replaces the default image on the Welcome page 12 | # can be an icon name or an absolute path to an image on disk beginning with file:/ 13 | Icon=file://usr/share/pixmaps/Ceratopsian_installer.svg 14 | 15 | # Optional; replaces the default image caption on the Welcome page 16 | Comment=This is an unofficial installer for the Debian GNU/Linux operating system. 17 | 18 | # Optional; replaces the default URL opened when clicking on the Welcome page's image or icon 19 | URL=https://github.com/r0b0/debian-installer 20 | -------------------------------------------------------------------------------- /backend/backendHelpers.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | /* 4 | Opinionated Debian Installer 5 | Copyright (C) 2022-2025 Robert T. 6 | 7 | This program is free software: you can redistribute it and/or modify 8 | it under the terms of the GNU General Public License as published by 9 | the Free Software Foundation, either version 3 of the License, or 10 | (at your option) any later version. 11 | 12 | This program is distributed in the hope that it will be useful, 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | GNU General Public License for more details. 16 | 17 | You should have received a copy of the GNU General Public License 18 | along with this program. If not, see . 19 | */ 20 | 21 | import ( 22 | "encoding/json" 23 | "net/http" 24 | "os/exec" 25 | ) 26 | 27 | func writeJson(w http.ResponseWriter, data any) error { 28 | jData, err := json.Marshal(data) 29 | if err != nil { 30 | return err 31 | } 32 | w.Header().Set("Content-Type", "application/json") 33 | _, err = w.Write(jData) 34 | return err 35 | } 36 | 37 | func runAndGiveStdout(command ...string) ([]byte, error) { 38 | path, err := exec.LookPath(command[0]) 39 | if err != nil { 40 | return nil, err 41 | } 42 | out, err := exec.Command(path, command[1:]...).Output() 43 | if err != nil { 44 | return nil, err 45 | } 46 | return out, nil 47 | } 48 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Unit test and build compiled components 2 | 3 | permissions: 4 | contents: read 5 | 6 | on: [push, pull_request] 7 | 8 | jobs: 9 | 10 | build-backend: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v4 14 | 15 | - name: Set up Go 16 | uses: actions/setup-go@v5 17 | with: 18 | go-version: '1.25.5' 19 | cache-dependency-path: backend/go.sum 20 | 21 | - name: Test 22 | run: | 23 | cd backend 24 | go test -v 25 | 26 | - name: Build 27 | run: | 28 | cd backend 29 | export GOARCH=amd64 30 | export CGO_ENABLED=0 31 | go build -v -ldflags="-s -w" -o opinionated-installer 32 | 33 | - name: 'Upload Artifact' 34 | uses: actions/upload-artifact@v4 35 | with: 36 | name: opinionated-installer 37 | path: backend/opinionated-installer 38 | 39 | build-web-gui: 40 | runs-on: ubuntu-latest 41 | steps: 42 | - uses: actions/checkout@v4 43 | 44 | - name: Set up Node 45 | uses: actions/setup-node@v4 46 | with: 47 | node-version: '22' 48 | cache: 'npm' 49 | cache-dependency-path: 'frontend/package-lock.json' 50 | 51 | - name: Build 52 | run: | 53 | cd frontend 54 | npm ci 55 | npm run build 56 | 57 | - name: 'Upload Artifact' 58 | uses: actions/upload-artifact@v4 59 | with: 60 | name: browser-gui-frontend-dist 61 | path: frontend/dist/ 62 | -------------------------------------------------------------------------------- /installer-files/boot/efi/installer.ini: -------------------------------------------------------------------------------- 1 | ; opinionated debian installer configuration file 2 | ; https://github.com/r0b0/debian-installer 3 | 4 | ; IP address for the installer back-end to listen on 5 | ; change to 0.0.0.0 to listen on public interfaces THIS IS PROBABLY A SECURITY HOLE 6 | BACK_END_IP_ADDRESS=127.0.0.1 7 | 8 | ; ssh public key to add to user and root authorized_keys file 9 | ; this will also install openssh-server 10 | ;SSH_PUBLIC_KEY="ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCiUw0E54irh5RRKvJoXv/MahCHKD/ep4fc3FsZpOjvEHErD9PK/TuAI9ccXgAQj45Tw/TFGoWn9swdQBHtX7kQ0PBSgk9yI3G3u+wJDMacU79jhoUOrF70SZDxMyLIz5pCy/njrYLBsc7ONB5i8onyF2plhbzOdWSVbFEiGVNDUCgrIMyZ2bY9/EzZWiE2b/VdsopSGGVn8myy51lsb6qeCctGp6GEerPgfGWiRbkuiIJ1ia1I8wNwD4VyJ1H3EHcUxfiX7ZRVu0+TStqiI/crvG+bmz97HXdKexClWaTHP5rAqI+t2c/wHtpnsUbnEcWhFvnQbQyIZvHP1zku0lScZ9l+Ks5qy2Hf0MX/r20M63b6+3csdvPjUH150giKbPDW2brffv0c+McU9jPqbAljCI8QZeD7gI8AmpbcwpJVjDUh9JSrucHg3rGCtuvTo0T7Jc+5h/50vNuvzFEfESJwZK/220oH/oZ6AqQpLJrpKW4SfWcL3xNJZ7t52mIptI0= robo@aspire" 11 | 12 | ; command to execute after the installation is finished 13 | ;AFTER_INSTALLED_CMD=poweroff 14 | 15 | ; pre-seeding configuration of the installed system 16 | ;DISK=/dev/vda 17 | USERNAME=user 18 | USER_FULL_NAME="Debian User" 19 | ;USER_PASSWORD=hunter2 20 | ;ROOT_PASSWORD=changeme 21 | DISABLE_LUKS=false 22 | ;LUKS_PASSWORD=luke 23 | ENABLE_MOK_SIGNED_UKI=true 24 | ;MOK_ENROLL_PASSWORD=mokka 25 | ENABLE_TPM=true 26 | HOSTNAME=debian13 27 | TIMEZONE=UTC 28 | SWAP_SIZE=1 29 | ;NVIDIA_PACKAGE=nvidia-driver 30 | ENABLE_POPCON=false 31 | ENABLE_FLATHUB=true 32 | LOCALE=C.UTF-8 33 | 34 | ; automatically start the installation without user intervention 35 | ;AUTO_INSTALL=true 36 | -------------------------------------------------------------------------------- /internal-tools/rss.py: -------------------------------------------------------------------------------- 1 | from feedgen.feed import FeedGenerator 2 | from markdown_it import MarkdownIt 3 | 4 | md = MarkdownIt("gfm-like") 5 | 6 | def parse_all_tables(tokens: list): 7 | table = None 8 | row = None 9 | inline_is_data = False 10 | data = None 11 | for token in tokens: 12 | # print(f"token: {token}") 13 | 14 | if not isinstance(token, object): 15 | print(f"Weird token: {token}") 16 | continue 17 | if token.type == "tbody_open": 18 | table = [] 19 | elif token.type == "tbody_close": 20 | yield table 21 | table = None 22 | elif token.type == "tr_open" and table is not None: 23 | row = [] 24 | elif token.type == "tr_close" and table is not None: 25 | table.append(row) 26 | row = None 27 | elif token.type == "td_open" and row is not None: 28 | inline_is_data = True 29 | elif token.type == "td_close" and row is not None: 30 | inline_is_data = False 31 | row.append(data) 32 | elif token.type == "inline" and inline_is_data: 33 | if len(token.children) > 1: 34 | data = token.children 35 | else: 36 | data = token.content 37 | 38 | fg = FeedGenerator() 39 | fg.title('Opinionated Debian Installer') 40 | fg.description('Alternative debian installer for laptops and desktop PCs') 41 | fg.link(href="https://github.com/r0b0/debian-installer", rel="alternate") 42 | fg.link(href="https://objectstorage.eu-frankfurt-1.oraclecloud.com/n/fr2rf1wke5iq/b/public/o/feed.xml", rel="self") 43 | 44 | with open('README.md') as f: 45 | for table in parse_all_tables(md.parse(f.read())): 46 | # print(f"table: {table} rows: {len(table)}") 47 | if len(table) > 3: 48 | continue 49 | for row in table: 50 | fe = fg.add_entry() 51 | fe.title(f"{row[0]} - {row[1]}") 52 | url = row[3][0].attrs["href"] 53 | fe.link(href=url) 54 | fe.guid(url) 55 | 56 | fg.rss_file("feed.xml", pretty=True) 57 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /backend/backendWebsocket.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | /* 4 | Opinionated Debian Installer 5 | Copyright (C) 2022-2025 Robert T. 6 | 7 | This program is free software: you can redistribute it and/or modify 8 | it under the terms of the GNU General Public License as published by 9 | the Free Software Foundation, either version 3 of the License, or 10 | (at your option) any later version. 11 | 12 | This program is distributed in the hope that it will be useful, 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | GNU General Public License for more details. 16 | 17 | You should have received a copy of the GNU General Public License 18 | along with this program. If not, see . 19 | */ 20 | 21 | import ( 22 | "github.com/google/uuid" 23 | "golang.org/x/net/websocket" 24 | "log/slog" 25 | ) 26 | 27 | func (c *BackendContext) GetProcessOutput(ws *websocket.Conn) { 28 | slog.Debug("new websocket connected", "addr", ws.RemoteAddr().String()) 29 | _, err := ws.Write(c.cmdOutput.Bytes()) 30 | if err != nil { 31 | slog.Warn("failed to write existing buffer to the new socket", "error", err) 32 | return 33 | } 34 | done := c.addWebsocket(ws) 35 | name := <-done 36 | slog.Debug("closing websocket connection", "name", name) 37 | } 38 | 39 | func (c *BackendContext) addWebsocket(ws *websocket.Conn) chan string { 40 | name := uuid.New().String() 41 | c.websockets[name] = ws 42 | n := make(chan string) 43 | c.wsHandlers[name] = n 44 | return n 45 | } 46 | 47 | func (c *BackendContext) Write(p []byte) (int, error) { 48 | c.cmdOutput.Write(p) 49 | 50 | slog.Debug("writing a message to all web sockets", "data", p) 51 | for name, ws := range c.websockets { 52 | _, err := ws.Write(p) 53 | if err != nil { 54 | slog.Warn("failed to write to websocket, closing", "socket_addr", name, "error", err) 55 | c.closeWebSocket(name) 56 | } 57 | } 58 | return len(p), nil 59 | } 60 | 61 | func (c *BackendContext) closeWebSocket(name string) { 62 | done := c.wsHandlers[name] 63 | done <- name 64 | delete(c.websockets, name) 65 | delete(c.wsHandlers, name) 66 | } 67 | -------------------------------------------------------------------------------- /internal-tools/release.txt: -------------------------------------------------------------------------------- 1 | IMAGE=opinionated-debian-installer-trixie-gnome-20251206a 2 | 3 | sudo truncate -s 4301M /var/lib/libvirt/images/usb1.img 4 | (cd internal-tools/virsh && ./build.sh) 5 | internal-tools/build-compiled-components.sh 6 | internal-tools/upload.sh 7 | ssh odin 8 | # in the installer VM 9 | sudo su - 10 | rm -rf *; rm -f /var/cache/opinionated-debian-installer/bootstrap.btrfs; fdisk /dev/vdb 11 | # in fdisk 12 | d 13 | d 14 | w 15 | # back to shell 16 | /home/user/opinionated-debian-installer/make_image.sh 17 | # click up to "storing bootstrap data to /var/cache/opinionated-debian-installer/bootstrap.btrfs" 18 | # Ctrl+C, 19 | reboot 20 | # back to host 21 | ssh odin 22 | # in the installer VM 23 | sudo su - 24 | rm -rf *; fdisk /dev/vdb 25 | # in fdisk 26 | d 27 | d 28 | w 29 | # back to shell 30 | /home/user/opinionated-debian-installer/make_image.sh 31 | # click all the way through 32 | poweroff 33 | # back to host 34 | sudo cp /var/lib/libvirt/images/usb1.img /var/lib/libvirt/images/$IMAGE.img 35 | sudo truncate -s +500M /var/lib/libvirt/images/usb1.img 36 | (cd internal-tools/virsh/ && ./test.sh) 37 | # test the installer in the VM 38 | sudo sha256sum /var/lib/libvirt/images/$IMAGE.img 39 | # update README.md 40 | rclone copy /var/lib/libvirt/images/$IMAGE.img oracle_oci:public 41 | torrent-file-editor $IMAGE.torrent 42 | # Name: $IMAGE.img 43 | # Trackers: 44 | udp://tracker.opentrackr.org:1337/announce 45 | udp://open.demonii.com:1337/announce 46 | udp://open.stealth.si:80/announce 47 | udp://exodus.desync.com:6969/announce 48 | udp://explodie.org:6969/announce 49 | # Files: /var/lib/libvirt/images/$IMAGE.img 50 | # Piece size: 32MiB 51 | # Calculate piece hashes 52 | # Tree url-list string https://objectstorage.eu-frankfurt-1.oraclecloud.com/n/fr2rf1wke5iq/b/public/o/$IMAGE.img 53 | # Save As $IMAGE.torrent 54 | rclone copy $IMAGE.torrent oracle_oci:public 55 | cp /var/lib/libvirt/images/$IMAGE.img /var/lib/transmission-daemon/downloads/ 56 | # https://torrent.lamac.cc/transmission/web/ 57 | # Add $IMAGE.torrent 58 | python3 internal-tools/rss.py 59 | rclone copy feed.xml oracle_oci:public 60 | # git commit and push 61 | # post to mastodon https://mas.to/@r0b0 -------------------------------------------------------------------------------- /backend/tui_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | /* 4 | Opinionated Debian Installer 5 | Copyright (C) 2022-2025 Robert T. 6 | 7 | This program is free software: you can redistribute it and/or modify 8 | it under the terms of the GNU General Public License as published by 9 | the Free Software Foundation, either version 3 of the License, or 10 | (at your option) any later version. 11 | 12 | This program is distributed in the hope that it will be useful, 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | GNU General Public License for more details. 16 | 17 | You should have received a copy of the GNU General Public License 18 | along with this program. If not, see . 19 | */ 20 | 21 | import ( 22 | "os" 23 | "testing" 24 | ) 25 | 26 | func TestParseLsblkJson(t *testing.T) { 27 | f, err := os.Open("test_data/lsblk.json") 28 | if err != nil { 29 | t.Fatalf("Failed to open json file: %v", err) 30 | } 31 | devices, err := parseLsblkJson(f) 32 | if err != nil { 33 | t.Fatalf("Failed to parse json: %v", err) 34 | } 35 | if len(devices.Blockdevices) == 0 { 36 | t.Fatalf("No devices parsed: %v", devices.Blockdevices) 37 | } 38 | 39 | device := devices.Blockdevices[0] 40 | t.Logf("First device: %v", device) 41 | if "/dev/sda" != device.Path { 42 | t.Errorf("First device path = %s; want /dev/sda", device.Path) 43 | } 44 | } 45 | 46 | func TestParseLoginJson(t *testing.T) { 47 | f, err := os.Open("test_data/login.json") 48 | if err != nil { 49 | t.Fatalf("Failed to open json file: %v", err) 50 | } 51 | _, err = parseLoginJson(f) 52 | if err != nil { 53 | t.Fatalf("Failed to parse json: %v", err) 54 | } 55 | } 56 | 57 | func TestGetTimeZoneOffset(t *testing.T) { 58 | const UTC_OFFSET = 589 59 | o := getTimeZoneOffset("UTC") 60 | if UTC_OFFSET != o { 61 | t.Errorf("Offset of UTC timezone = %d; want %d", o, UTC_OFFSET) 62 | } 63 | } 64 | 65 | func TestGetSliceIndex(t *testing.T) { 66 | var WHERE = []string{"a", "b", "c"} 67 | const WHAT = "b" 68 | o := getSliceIndex(WHAT, WHERE) 69 | if 1 != o { 70 | t.Errorf("Slice index = %d; want %d", o, 2) 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /frontend/src/assets/base.css: -------------------------------------------------------------------------------- 1 | /* color palette from */ 2 | :root { 3 | --vt-c-white: #ffffff; 4 | --vt-c-white-soft: #f8f8f8; 5 | --vt-c-white-mute: #f2f2f2; 6 | 7 | --vt-c-black: #181818; 8 | --vt-c-black-soft: #222222; 9 | --vt-c-black-mute: #282828; 10 | 11 | --vt-c-indigo: #2c3e50; 12 | 13 | --vt-c-divider-light-1: rgba(60, 60, 60, 0.29); 14 | --vt-c-divider-light-2: rgba(60, 60, 60, 0.12); 15 | --vt-c-divider-dark-1: rgba(84, 84, 84, 0.65); 16 | --vt-c-divider-dark-2: rgba(84, 84, 84, 0.48); 17 | 18 | --vt-c-text-light-1: var(--vt-c-indigo); 19 | --vt-c-text-light-2: rgba(60, 60, 60, 0.66); 20 | --vt-c-text-dark-1: var(--vt-c-white); 21 | --vt-c-text-dark-2: rgba(235, 235, 235, 0.64); 22 | } 23 | 24 | /* semantic color variables for this project */ 25 | :root { 26 | --color-background: var(--vt-c-white); 27 | --color-background-soft: var(--vt-c-white-soft); 28 | --color-background-mute: var(--vt-c-white-mute); 29 | 30 | --color-border: var(--vt-c-divider-light-2); 31 | --color-border-hover: var(--vt-c-divider-light-1); 32 | 33 | --color-heading: var(--vt-c-text-light-1); 34 | --color-text: var(--vt-c-text-light-1); 35 | 36 | --section-gap: 160px; 37 | } 38 | 39 | @media (prefers-color-scheme: dark) { 40 | :root { 41 | --color-background: var(--vt-c-black); 42 | --color-background-soft: var(--vt-c-black-soft); 43 | --color-background-mute: var(--vt-c-black-mute); 44 | 45 | --color-border: var(--vt-c-divider-dark-2); 46 | --color-border-hover: var(--vt-c-divider-dark-1); 47 | 48 | --color-heading: var(--vt-c-text-dark-1); 49 | --color-text: var(--vt-c-text-dark-2); 50 | } 51 | } 52 | 53 | body { 54 | min-height: 100vh; 55 | color: var(--color-text); 56 | background: var(--color-background); 57 | transition: color 0.5s, background-color 0.5s; 58 | line-height: 1.6; 59 | font-family: Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, 60 | Cantarell, 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif; 61 | font-size: 15px; 62 | text-rendering: optimizeLegibility; 63 | -webkit-font-smoothing: antialiased; 64 | -moz-osx-font-smoothing: grayscale; 65 | } 66 | -------------------------------------------------------------------------------- /backend/tuiPassword.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | /* 4 | Opinionated Debian Installer 5 | Copyright (C) 2022-2025 Robert T. 6 | 7 | This program is free software: you can redistribute it and/or modify 8 | it under the terms of the GNU General Public License as published by 9 | the Free Software Foundation, either version 3 of the License, or 10 | (at your option) any later version. 11 | 12 | This program is distributed in the hope that it will be useful, 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | GNU General Public License for more details. 16 | 17 | You should have received a copy of the GNU General Public License 18 | along with this program. If not, see . 19 | */ 20 | 21 | import ( 22 | "fmt" 23 | "github.com/rivo/tview" 24 | ) 25 | 26 | type Password struct { 27 | form *tview.Form 28 | changed func(text string) 29 | valid func(valid bool) 30 | value1 string 31 | value2 string 32 | formItemIndex int 33 | label2Ok string 34 | label2Nok string 35 | } 36 | 37 | func AddPasswordToForm(form *tview.Form, label string, value string, changed func(text string), valid func(valid bool)) { 38 | p := Password{ 39 | form: form, 40 | changed: changed, 41 | valid: valid, 42 | value1: value, 43 | value2: value, 44 | formItemIndex: form.GetFormItemCount(), 45 | label2Ok: fmt.Sprintf("%s (repeat)", label), 46 | label2Nok: fmt.Sprintf("%s (NO MATCH)", label), 47 | } 48 | form.AddPasswordField(label, value, 0, '*', func(text string) { 49 | p.value1 = text 50 | p.changedFunc() 51 | }) 52 | form.AddPasswordField(p.label2Ok, value, 0, '*', func(text string) { 53 | p.value2 = text 54 | p.changedFunc() 55 | }) 56 | } 57 | 58 | func (p *Password) changedFunc() { 59 | if p.value1 == p.value2 { 60 | p.changed(p.value1) 61 | p.valid(true) 62 | p.inputField(1).SetLabel(p.label2Ok) 63 | } else { 64 | p.changed("") 65 | p.valid(false) 66 | p.inputField(1).SetLabel(p.label2Nok) 67 | } 68 | } 69 | 70 | func (p *Password) inputField(index int) *tview.InputField { 71 | item := p.form.GetFormItem(p.formItemIndex + index) 72 | inputField, ok := item.(*tview.InputField) 73 | if !ok { 74 | panic("Internal Error: AddPasswordField does not add an InputField?") 75 | } 76 | return inputField 77 | } 78 | -------------------------------------------------------------------------------- /backend/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | /* 4 | Opinionated Debian Installer 5 | Copyright (C) 2022-2025 Robert T. 6 | 7 | This program is free software: you can redistribute it and/or modify 8 | it under the terms of the GNU General Public License as published by 9 | the Free Software Foundation, either version 3 of the License, or 10 | (at your option) any later version. 11 | 12 | This program is distributed in the hope that it will be useful, 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | GNU General Public License for more details. 16 | 17 | You should have received a copy of the GNU General Public License 18 | along with this program. If not, see . 19 | */ 20 | 21 | import ( 22 | "errors" 23 | "flag" 24 | "fmt" 25 | "net" 26 | "os" 27 | ) 28 | 29 | func main() { 30 | tuiCmd := flag.NewFlagSet("tui", flag.ExitOnError) 31 | tuiBaseUrlString := tuiCmd.String("baseUrl", "http://localhost:5000", "base URL of the web service") 32 | 33 | backendCmd := flag.NewFlagSet("backend", flag.ExitOnError) 34 | backendPort := backendCmd.Int("listenPort", 5000, "listen tcp port for the web server") 35 | backendStatic := backendCmd.String("staticHtmlFolder", "/var/www/html/opinionated-debian-installer/", "folder with static html content") 36 | 37 | if len(os.Args) < 2 { 38 | fmt.Println("expected 'tui' or 'backend' subcommands") 39 | os.Exit(1) 40 | } 41 | 42 | switch os.Args[1] { 43 | case "tui": 44 | err := tuiCmd.Parse(os.Args[2:]) 45 | if errors.Is(err, flag.ErrHelp) { 46 | flag.Usage() 47 | os.Exit(0) 48 | } 49 | Tui(tuiBaseUrlString) 50 | return 51 | 52 | case "backend": 53 | err := backendCmd.Parse(os.Args[2:]) 54 | if errors.Is(err, flag.ErrHelp) { 55 | flag.Usage() 56 | os.Exit(0) 57 | } 58 | Backend(backendPort, backendStatic) 59 | return 60 | 61 | default: 62 | flag.Usage() 63 | os.Exit(3) 64 | } 65 | } 66 | 67 | func SystemdNotifyReady() error { 68 | socketName := os.Getenv("NOTIFY_SOCKET") 69 | if socketName == "" { 70 | return nil 71 | } 72 | systemdSocket := &net.UnixAddr{ 73 | Name: socketName, 74 | Net: "unixgram", 75 | } 76 | message := "READY=1" 77 | conn, err := net.DialUnix(systemdSocket.Net, nil, systemdSocket) 78 | if err != nil { 79 | return err 80 | } 81 | defer conn.Close() 82 | if _, err = conn.Write([]byte(message)); err != nil { 83 | return err 84 | } 85 | return nil 86 | } 87 | -------------------------------------------------------------------------------- /backend/tuiModel.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | /* 4 | Opinionated Debian Installer 5 | Copyright (C) 2022-2025 Robert T. 6 | 7 | This program is free software: you can redistribute it and/or modify 8 | it under the terms of the GNU General Public License as published by 9 | the Free Software Foundation, either version 3 of the License, or 10 | (at your option) any later version. 11 | 12 | This program is distributed in the hope that it will be useful, 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | GNU General Public License for more details. 16 | 17 | You should have received a copy of the GNU General Public License 18 | along with this program. If not, see . 19 | */ 20 | 21 | import ( 22 | _ "embed" 23 | "encoding/json" 24 | "io" 25 | "strings" 26 | ) 27 | 28 | // when adding fields here, you need to add them to startInstallation() in tuiRest.go too 29 | type Model struct { 30 | Disk string `json:"DISK"` 31 | DebianVersion string `json:"DEBIAN_VERSION"` 32 | Username string `json:"USERNAME"` 33 | UserFullName string `json:"USER_FULL_NAME"` 34 | UserPassword string `json:"USER_PASSWORD"` 35 | RootPassword string `json:"ROOT_PASSWORD"` 36 | DisableLuks string `json:"DISABLE_LUKS"` 37 | LuksPassword string `json:"LUKS_PASSWORD"` 38 | EnableMokUki string `json:"ENABLE_MOK_SIGNED_UKI"` 39 | MokPassword string `json:"MOK_ENROLL_PASSWORD"` 40 | EnableTpm string `json:"ENABLE_TPM"` 41 | Hostname string `json:"HOSTNAME"` 42 | Timezone string `json:"TIMEZONE"` 43 | SwapSize string `json:"SWAP_SIZE"` 44 | NvidiaPackage string `json:"NVIDIA_PACKAGE"` 45 | EnablePopcon string `json:"ENABLE_POPCON"` 46 | EnableFlathub string `json:"ENABLE_FLATHUB"` 47 | } 48 | type LoginResp struct { 49 | Environ Model `json:"environ"` 50 | HasEfi bool `json:"has_efi"` 51 | Hostname string `json:"hostname"` 52 | Running bool `json:"running"` 53 | } 54 | 55 | func parseLoginJson(data io.Reader) (Model, error) { 56 | var login LoginResp 57 | err := json.NewDecoder(data).Decode(&login) 58 | if err != nil { 59 | return Model{}, err 60 | } 61 | return login.Environ, nil 62 | } 63 | 64 | type BlockDevice struct { 65 | Path string `json:"path"` 66 | Model string `json:"Model"` 67 | Size string `json:"size"` 68 | } 69 | type LsblkResp struct { 70 | Blockdevices []BlockDevice `json:"blockdevices"` 71 | } 72 | 73 | func parseLsblkJson(data io.Reader) (LsblkResp, error) { 74 | var devices LsblkResp 75 | err := json.NewDecoder(data).Decode(&devices) 76 | if err != nil { 77 | return LsblkResp{}, err 78 | } 79 | return devices, nil 80 | } 81 | 82 | //go:embed timezones.txt 83 | var timezonesStr string 84 | var timezones = strings.Split(timezonesStr, "\n") 85 | 86 | func getTimeZoneOffset(tz string) int { 87 | return getSliceIndex(tz, timezones) 88 | } 89 | 90 | func getSliceIndex(what string, where []string) int { 91 | for i, t := range where { 92 | if what == t { 93 | return i 94 | } 95 | } 96 | return 0 97 | } 98 | -------------------------------------------------------------------------------- /backend/tuiWizard.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | /* 4 | Opinionated Debian Installer 5 | Copyright (C) 2022-2025 Robert T. 6 | 7 | This program is free software: you can redistribute it and/or modify 8 | it under the terms of the GNU General Public License as published by 9 | the Free Software Foundation, either version 3 of the License, or 10 | (at your option) any later version. 11 | 12 | This program is distributed in the hope that it will be useful, 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | GNU General Public License for more details. 16 | 17 | You should have received a copy of the GNU General Public License 18 | along with this program. If not, see . 19 | */ 20 | 21 | import ( 22 | "fmt" 23 | "github.com/gdamore/tcell/v2" 24 | "github.com/rivo/tview" 25 | ) 26 | 27 | type Wizard struct { 28 | pageNames []string 29 | forms []*tview.Form 30 | pagesContents []tview.Primitive 31 | Footer *tview.TextView 32 | pages *tview.Pages 33 | currentPage int 34 | } 35 | 36 | func NewWizard() Wizard { 37 | return Wizard{make([]string, 0), 38 | make([]*tview.Form, 0), 39 | make([]tview.Primitive, 0), 40 | tview.NewTextView().SetDynamicColors(true), 41 | nil, 42 | 0} 43 | } 44 | 45 | func (w *Wizard) AddForm(name string, form *tview.Form) *Wizard { 46 | return w.AddPage(name, form, form) 47 | } 48 | 49 | func (w *Wizard) AddPage(name string, form *tview.Form, item tview.Primitive) *Wizard { 50 | w.pageNames = append(w.pageNames, name) 51 | w.forms = append(w.forms, form) 52 | w.pagesContents = append(w.pagesContents, item) 53 | return w 54 | } 55 | 56 | func (w *Wizard) MakePages() *tview.Pages { 57 | w.pages = tview.NewPages() 58 | for i := range w.forms { 59 | form := w.forms[i] 60 | item := w.pagesContents[i] 61 | 62 | visible := i == 0 63 | hasNext := i < len(w.forms)-1 64 | hasPrev := i > 0 65 | if hasPrev { 66 | form.AddButton("Back", func() { 67 | w.SwitchToPage(i - 1) 68 | }) 69 | } 70 | if hasNext { 71 | form.AddButton("Next", func() { 72 | w.SwitchToPage(i + 1) 73 | }) 74 | } 75 | w.pages.AddPage(w.pageNames[i], item, true, visible) 76 | } 77 | w.updateFooter() 78 | return w.pages 79 | } 80 | 81 | func (w *Wizard) SwitchToPage(i int) { 82 | w.currentPage = i 83 | w.pages.SwitchToPage(w.pageNames[i]) 84 | w.updateFooter() 85 | } 86 | 87 | func (w *Wizard) updateFooter() { 88 | w.Footer.Clear() 89 | 90 | for i, name := range w.pageNames { 91 | var highlight string 92 | if i == w.currentPage { 93 | highlight = "[:red]" 94 | } else { 95 | highlight = "" 96 | } 97 | w.Footer.Write([]byte(fmt.Sprintf(" [:blue]F%d[-:-]%s %s [-:-] ", i+1, highlight, name))) 98 | } 99 | } 100 | 101 | func (w *Wizard) InputCapture(event *tcell.EventKey) *tcell.EventKey { 102 | fIndex := int(event.Key()) - int(tcell.KeyF1) 103 | if fIndex >= 0 && fIndex < len(w.pageNames) { 104 | w.SwitchToPage(fIndex) 105 | return nil 106 | } else { 107 | return event 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /frontend/src/components/Password.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 32 | 33 | 105 | 106 | 115 | -------------------------------------------------------------------------------- /backend/tuiRest.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | /* 4 | Opinionated Debian Installer 5 | Copyright (C) 2022-2025 Robert T. 6 | 7 | This program is free software: you can redistribute it and/or modify 8 | it under the terms of the GNU General Public License as published by 9 | the Free Software Foundation, either version 3 of the License, or 10 | (at your option) any later version. 11 | 12 | This program is distributed in the hope that it will be useful, 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | GNU General Public License for more details. 16 | 17 | You should have received a copy of the GNU General Public License 18 | along with this program. If not, see . 19 | */ 20 | 21 | import ( 22 | "fmt" 23 | "golang.org/x/net/websocket" 24 | "io" 25 | "net/http" 26 | "net/url" 27 | ) 28 | 29 | func loginToBackend(baseUrl *url.URL) (Model, error) { 30 | client := http.Client{} 31 | resp, err := client.Get(baseUrl.JoinPath("login").String()) 32 | if err != nil { 33 | return Model{}, err 34 | } 35 | defer resp.Body.Close() 36 | return parseLoginJson(resp.Body) 37 | } 38 | 39 | func getAvailableDrives(baseUrl *url.URL) ([]string, []string, error) { 40 | client := http.Client{} 41 | resp, err := client.Get(baseUrl.JoinPath("block_devices").String()) 42 | if err != nil { 43 | return []string{}, []string{}, err 44 | } 45 | defer resp.Body.Close() 46 | devices, err := parseLsblkJson(resp.Body) 47 | if err != nil { 48 | return []string{}, []string{}, err 49 | } 50 | var drives []string 51 | var driveDescriptions []string 52 | for _, device := range devices.Blockdevices { 53 | drives = append(drives, device.Path) 54 | driveDescription := fmt.Sprintf("%s %s (%s)", device.Path, device.Model, device.Size) 55 | driveDescriptions = append(driveDescriptions, driveDescription) 56 | } 57 | return drives, driveDescriptions, nil 58 | } 59 | 60 | func processOutput(baseUrl *url.URL, log io.Writer) { 61 | origin := "http://localhost/" 62 | wsUrl := baseUrl.JoinPath("process_output") 63 | wsUrl.Scheme = "ws" 64 | ws, err := websocket.Dial(wsUrl.String(), "", origin) 65 | if err != nil { 66 | LOG(log, "Failed to connect to web socket: %v", err) 67 | return 68 | } 69 | go func() { 70 | _, err := io.Copy(log, ws) 71 | if err != nil { 72 | LOG(log, "Error reading websocket: %v", err) 73 | } 74 | LOG(log, "Finished") 75 | }() 76 | } 77 | 78 | func (m *Model) startInstallation(baseUrl *url.URL, log io.Writer) error { 79 | post := url.Values{} 80 | 81 | post.Set("DISK", m.Disk) 82 | post.Set("DEBIAN_VERSION", m.DebianVersion) 83 | post.Set("USERNAME", m.Username) 84 | post.Set("USER_FULL_NAME", m.UserFullName) 85 | post.Set("USER_PASSWORD", m.UserPassword) 86 | post.Set("ROOT_PASSWORD", m.UserPassword) 87 | post.Set("DISABLE_LUKS", m.DisableLuks) 88 | post.Set("LUKS_PASSWORD", m.LuksPassword) 89 | post.Set("ENABLE_MOK_SIGNED_UKI", m.EnableMokUki) 90 | post.Set("MOK_ENROLL_PASSWORD", m.MokPassword) 91 | post.Set("ENABLE_TPM", m.EnableTpm) 92 | post.Set("HOSTNAME", m.Hostname) 93 | post.Set("TIMEZONE", m.Timezone) 94 | post.Set("SWAP_SIZE", m.SwapSize) 95 | post.Set("NVIDIA_PACKAGE", m.NvidiaPackage) 96 | post.Set("ENABLE_FLATHUB", m.EnableFlathub) 97 | post.Set("ENABLE_POPCON", m.EnablePopcon) 98 | client := http.Client{} 99 | resp, err := client.PostForm(baseUrl.JoinPath("install").String(), post) 100 | if err != nil { 101 | LOG(log, "Error posting form: %v", err) 102 | return err 103 | } 104 | defer resp.Body.Close() 105 | LOG(log, "Post status: %s", resp.Status) 106 | return nil 107 | } 108 | 109 | func stop(baseUrl *url.URL) error { 110 | client := http.Client{} 111 | resp, err := client.Get(baseUrl.JoinPath("clear").String()) 112 | if err != nil { 113 | return err 114 | } 115 | defer resp.Body.Close() 116 | return nil 117 | } 118 | -------------------------------------------------------------------------------- /backend/backend.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | /* 4 | Opinionated Debian Installer 5 | Copyright (C) 2022-2025 Robert T. 6 | 7 | This program is free software: you can redistribute it and/or modify 8 | it under the terms of the GNU General Public License as published by 9 | the Free Software Foundation, either version 3 of the License, or 10 | (at your option) any later version. 11 | 12 | This program is distributed in the hope that it will be useful, 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | GNU General Public License for more details. 16 | 17 | You should have received a copy of the GNU General Public License 18 | along with this program. If not, see . 19 | */ 20 | 21 | import ( 22 | "bytes" 23 | "errors" 24 | "fmt" 25 | "golang.org/x/net/context" 26 | "log/slog" 27 | "net/http" 28 | "os" 29 | "os/exec" 30 | "strings" 31 | 32 | "golang.org/x/net/websocket" 33 | ) 34 | 35 | type BackendContext struct { 36 | runningCmd *exec.Cmd 37 | runningParameters map[string]string 38 | cmdOutput bytes.Buffer 39 | websockets map[string]*websocket.Conn 40 | wsHandlers map[string]chan string 41 | ctx context.Context 42 | } 43 | 44 | func (c *BackendContext) doRunInstall() { 45 | c.runningCmd = exec.CommandContext(c.ctx, os.Getenv("INSTALLER_SCRIPT")) 46 | c.runningCmd.Stderr = c 47 | c.runningCmd.Stdout = c 48 | for k, v := range c.runningParameters { 49 | c.runningCmd.Env = append(c.runningCmd.Env, fmt.Sprintf("%s=%s", k, v)) 50 | } 51 | err := c.runningCmd.Start() 52 | if err != nil { 53 | slog.Error("failed to start the installer script", "error", err) 54 | } 55 | go c.waitForInstallerFinished() 56 | } 57 | 58 | func (c *BackendContext) waitForInstallerFinished() { 59 | slog.Debug("waiting for the installer to finish") 60 | err := c.runningCmd.Wait() 61 | if err != nil { 62 | slog.Error("command failed", "error", err) 63 | } else { 64 | slog.Info("command finished successfully") 65 | } 66 | for name := range c.websockets { 67 | // slog.Debug("closing websocket", "name", name) 68 | c.closeWebSocket(name) 69 | } 70 | } 71 | 72 | func Backend(listenPort *int, staticPath *string) { 73 | slog.SetLogLoggerLevel(slog.LevelDebug) 74 | 75 | backendIp, found := os.LookupEnv("BACK_END_IP_ADDRESS") 76 | if !found { 77 | slog.Warn("environment variable BACK_END_IP_ADDRESS not found, using localhost") 78 | backendIp = "localhost" 79 | } 80 | 81 | app := BackendContext{ 82 | runningCmd: nil, 83 | runningParameters: map[string]string{"NON_INTERACTIVE": "yes"}, 84 | cmdOutput: bytes.Buffer{}, 85 | websockets: make(map[string]*websocket.Conn), 86 | wsHandlers: make(map[string]chan string), 87 | ctx: context.Background(), 88 | } 89 | 90 | for _, s := range os.Environ() { 91 | keyValue := strings.Split(s, "=") 92 | app.runningParameters[keyValue[0]] = keyValue[1] 93 | } 94 | 95 | http.HandleFunc("/login", app.Login) 96 | http.HandleFunc("/block_devices", app.GetBlockDevices) 97 | http.HandleFunc("/install", app.Install) 98 | http.HandleFunc("/clear", app.Clear) 99 | http.HandleFunc("/process_status", app.ProcessStatus) 100 | http.HandleFunc("/download_log", app.DownloadLog) 101 | http.Handle("/process_output", websocket.Handler(app.GetProcessOutput)) 102 | http.Handle("/", http.FileServer(http.Dir(*staticPath))) 103 | 104 | autoInstall, found := os.LookupEnv("AUTO_INSTALL") 105 | if found && autoInstall == "true" { 106 | slog.Info("automatically starting the installation") 107 | app.doRunInstall() 108 | } 109 | 110 | err := SystemdNotifyReady() 111 | if err != nil { 112 | slog.Error("failed to notify systemd", "error", err) 113 | } 114 | 115 | slog.Info("Starting backend http server", "backendIp", backendIp, "port", *listenPort) 116 | err = http.ListenAndServe(fmt.Sprintf("%s:%d", backendIp, *listenPort), nil) 117 | if errors.Is(err, http.ErrServerClosed) { 118 | slog.Info("Server closed") 119 | } else { 120 | slog.Error("Failed to start server", "error", err) 121 | os.Exit(1) 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /backend/go.sum: -------------------------------------------------------------------------------- 1 | github.com/gdamore/encoding v1.0.1 h1:YzKZckdBL6jVt2Gc+5p82qhrGiqMdG/eNs6Wy0u3Uhw= 2 | github.com/gdamore/encoding v1.0.1/go.mod h1:0Z0cMFinngz9kS1QfMjCP8TY7em3bZYeeklsSDPivEo= 3 | github.com/gdamore/tcell/v2 v2.13.1 h1:Ca2N6mHxhXuElCgn+nfKuZjS7gwNiIRKHFiljrZQ26A= 4 | github.com/gdamore/tcell/v2 v2.13.1/go.mod h1:+Wfe208WDdB7INEtCsNrAN6O2m+wsTPk1RAovjaILlo= 5 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 6 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 7 | github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag= 8 | github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= 9 | github.com/rivo/tview v0.42.0 h1:b/ftp+RxtDsHSaynXTbJb+/n/BxDEi+W3UfF5jILK6c= 10 | github.com/rivo/tview v0.42.0/go.mod h1:cSfIYfhpSGCjp3r/ECJb+GKS7cGJnqV8vfjQPwoXyfY= 11 | github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= 12 | github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= 13 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 14 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 15 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 16 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= 17 | golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= 18 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 19 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 20 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= 21 | golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= 22 | golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= 23 | golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= 24 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 25 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 26 | golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 27 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 28 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 29 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 30 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 31 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 32 | golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 33 | golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= 34 | golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= 35 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 36 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 37 | golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= 38 | golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU= 39 | golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254= 40 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 41 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 42 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 43 | golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 44 | golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= 45 | golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= 46 | golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= 47 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 48 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 49 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= 50 | golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= 51 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 52 | -------------------------------------------------------------------------------- /backend/tui.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | /* 4 | Opinionated Debian Installer 5 | Copyright (C) 2022-2025 Robert T. 6 | 7 | This program is free software: you can redistribute it and/or modify 8 | it under the terms of the GNU General Public License as published by 9 | the Free Software Foundation, either version 3 of the License, or 10 | (at your option) any later version. 11 | 12 | This program is distributed in the hope that it will be useful, 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | GNU General Public License for more details. 16 | 17 | You should have received a copy of the GNU General Public License 18 | along with this program. If not, see . 19 | */ 20 | 21 | import ( 22 | "fmt" 23 | "github.com/gdamore/tcell/v2" 24 | "github.com/rivo/tview" 25 | "io" 26 | "net/url" 27 | "strconv" 28 | ) 29 | 30 | func LOG(l io.Writer, format string, args ...any) { 31 | _, _ = l.Write([]byte(fmt.Sprintf(format+"\n", args...))) 32 | } 33 | 34 | func Tui(baseUrlString *string) { 35 | baseUrl, err := url.Parse(*baseUrlString) 36 | if err != nil { 37 | panic(fmt.Sprintf("Invalid base url: %s", *baseUrlString)) 38 | } 39 | 40 | devices, deviceNames, err := getAvailableDrives(baseUrl) 41 | if err != nil { 42 | panic(fmt.Sprintf("Failed to get available drives from back-end: %v", err)) 43 | } 44 | 45 | m, err := loginToBackend(baseUrl) 46 | if err != nil { 47 | panic(fmt.Sprintf("Failed to get configuration from back-end: %v", err)) 48 | } 49 | 50 | greenColour := tcell.NewRGBColor(0x51, 0xa1, 0xd0) 51 | 52 | app := tview.NewApplication() 53 | 54 | logView := tview.NewTextView(). 55 | SetScrollable(true). 56 | ScrollToEnd(). 57 | SetLabelWidth(10). 58 | SetLabel(" Log"). 59 | SetChangedFunc(func() { 60 | app.Draw() 61 | }) 62 | 63 | dataOk := true 64 | 65 | diskForm := tview.NewForm(). 66 | AddDropDown("Device", deviceNames, getSliceIndex(m.Disk, devices), func(_ string, optionIndex int) { 67 | m.Disk = devices[optionIndex] 68 | }). 69 | AddCheckbox("Disable Encryption", m.DisableLuks == "true", func(checked bool) { 70 | if checked { 71 | m.DisableLuks = "true" 72 | } else { 73 | m.DisableLuks = "false" 74 | } 75 | }) 76 | AddPasswordToForm(diskForm, "Disk Encryption Passphrase", m.LuksPassword, func(text string) { 77 | m.LuksPassword = text 78 | }, func(valid bool) { 79 | dataOk = valid 80 | }) 81 | diskForm.AddCheckbox("Unlock with TPM", m.EnableTpm == "true", func(checked bool) { 82 | if checked { 83 | m.EnableTpm = "true" 84 | } else { 85 | m.EnableTpm = "false" 86 | } 87 | }) 88 | 89 | usersForm := tview.NewForm() 90 | AddPasswordToForm(usersForm, "Root Password", m.RootPassword, func(text string) { 91 | m.RootPassword = text 92 | }, func(valid bool) { 93 | dataOk = valid 94 | }) 95 | usersForm.AddInputField("Regular User Name", m.Username, 0, nil, func(text string) { 96 | m.Username = text 97 | }). 98 | AddInputField("Full Name", m.UserFullName, 0, nil, func(text string) { 99 | m.UserFullName = text 100 | }) 101 | AddPasswordToForm(usersForm, "Regular User Password", m.UserPassword, func(text string) { 102 | m.UserPassword = text 103 | }, func(valid bool) { 104 | dataOk = valid 105 | }) 106 | 107 | configForm := tview.NewForm(). 108 | AddInputField("Hostname", m.Hostname, 0, nil, func(text string) { 109 | m.Hostname = text 110 | }). 111 | AddDropDown("Time Zone", timezones, getTimeZoneOffset(m.Timezone), func(option string, _ int) { 112 | m.Timezone = option 113 | }). 114 | AddInputField("Swap Size", m.SwapSize, 0, func(textToCheck string, lastChar rune) bool { 115 | _, err := strconv.Atoi(textToCheck) 116 | return err == nil 117 | }, func(text string) { 118 | m.SwapSize = text 119 | }). 120 | AddCheckbox("Enable NVIDIA", m.NvidiaPackage == "nvidia-driver", func(checked bool) { 121 | if checked { 122 | m.NvidiaPackage = "nvidia-driver" 123 | } else { 124 | m.NvidiaPackage = "" 125 | } 126 | }). 127 | AddCheckbox("Enable Flathub", m.EnableFlathub == "true", func(checked bool) { 128 | if checked { 129 | m.EnableFlathub = "true" 130 | } else { 131 | m.EnableFlathub = "false" 132 | } 133 | }). 134 | AddCheckbox("Enable Popcon", m.EnablePopcon == "true", func(checked bool) { 135 | if checked { 136 | m.EnablePopcon = "true" 137 | } else { 138 | m.EnablePopcon = "false" 139 | } 140 | }) 141 | 142 | secureBootForm := tview.NewForm(). 143 | AddCheckbox("MOK-Signed UKI", m.EnableMokUki == "true", func(checked bool) { 144 | if checked { 145 | m.EnableMokUki = "true" 146 | } else { 147 | m.EnableMokUki = "false" 148 | } 149 | }) 150 | AddPasswordToForm(secureBootForm, "MOK Password", m.MokPassword, func(text string) { 151 | m.MokPassword = text 152 | }, func(valid bool) { 153 | dataOk = valid 154 | }) 155 | 156 | processingForm := tview.NewForm(). 157 | AddButton("Install OVERWRITING THE WHOLE DRIVE", func() { 158 | if !dataOk { 159 | LOG(logView, "Data not consistent") // TODO 160 | return 161 | } 162 | err := m.startInstallation(baseUrl, logView) 163 | if err != nil { 164 | LOG(logView, "Failed to start installation: %v", err) 165 | } 166 | }). 167 | AddButton("Stop", func() { 168 | err := stop(baseUrl) 169 | if err != nil { 170 | LOG(logView, "Failed to stop installation: %v", err) 171 | } 172 | }) 173 | 174 | processOutput(baseUrl, logView) 175 | 176 | wizard := NewWizard() 177 | wizard.AddForm("Device", diskForm). 178 | AddForm("Users", usersForm). 179 | AddForm("Configuration", configForm). 180 | AddForm("Secure Boot", secureBootForm). 181 | AddPage("Processing", processingForm, tview.NewFlex(). 182 | SetDirection(tview.FlexRow). 183 | AddItem(tview.NewTextView(). 184 | SetText(" Processing"), 3, 0, false). 185 | AddItem(processingForm, 3, 0, true). 186 | AddItem(logView, 0, 100, false)) 187 | 188 | mainFlex := tview.NewFlex(). 189 | SetDirection(tview.FlexRow). 190 | AddItem(wizard.MakePages(), 0, 100, true). 191 | AddItem(wizard.Footer, 1, 0, false) 192 | mainFlex.SetBorder(true). 193 | SetTitle("Opinionated Debian Installer"). 194 | SetTitleColor(greenColour). 195 | SetTitleAlign(tview.AlignCenter) 196 | 197 | app.SetInputCapture(wizard.InputCapture) 198 | 199 | _ = SystemdNotifyReady() 200 | 201 | if err := app.SetRoot(mainFlex, true).EnableMouse(true).SetFocus(mainFlex).Run(); err != nil { 202 | panic(err) 203 | } 204 | } 205 | -------------------------------------------------------------------------------- /backend/backendHandlers.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | /* 4 | Opinionated Debian Installer 5 | Copyright (C) 2022-2025 Robert T. 6 | 7 | This program is free software: you can redistribute it and/or modify 8 | it under the terms of the GNU General Public License as published by 9 | the Free Software Foundation, either version 3 of the License, or 10 | (at your option) any later version. 11 | 12 | This program is distributed in the hope that it will be useful, 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | GNU General Public License for more details. 16 | 17 | You should have received a copy of the GNU General Public License 18 | along with this program. If not, see . 19 | */ 20 | 21 | import ( 22 | "bytes" 23 | "log/slog" 24 | "net/http" 25 | "os" 26 | "strings" 27 | ) 28 | 29 | func (c *BackendContext) Login(w http.ResponseWriter, _ *http.Request) { 30 | type login struct { 31 | Hostname string `json:"hostname"` 32 | HasEfi bool `json:"has_efi"` 33 | HasNvidia bool `json:"has_nvidia"` 34 | SBState string `json:"sb_state"` 35 | Running bool `json:"running"` 36 | Environ map[string]string `json:"environ"` 37 | } 38 | data := login{} 39 | var err error 40 | data.Hostname, err = os.Hostname() 41 | if err != nil { 42 | slog.Error("failed to detect hostname", "error", err) 43 | http.Error(w, "failed to detect hostname", http.StatusInternalServerError) 44 | return 45 | } 46 | _, err = os.Stat("/sys/firmware/efi") 47 | if err == nil { 48 | data.HasEfi = true 49 | } else if os.IsNotExist(err) { 50 | data.HasEfi = false 51 | } else { 52 | slog.Error("failed to detect efi", "error", err) 53 | http.Error(w, "failed to detect efi", http.StatusInternalServerError) 54 | return 55 | } 56 | data.HasNvidia = detectNvidia() 57 | sbState, err := runAndGiveStdout("mokutil", "--sb-state") 58 | if err != nil { 59 | slog.Error("failed to detect secure boot state", "error", err) 60 | http.Error(w, "failed to detect secure boot state", http.StatusInternalServerError) 61 | return 62 | } 63 | data.SBState = string(sbState) 64 | data.Running = c.runningCmd != nil && c.runningCmd.Process != nil 65 | data.Environ = c.runningParameters 66 | err = writeJson(w, data) 67 | if err != nil { 68 | slog.Error("failed to write data", "error", err) 69 | http.Error(w, "failed to write data", http.StatusInternalServerError) 70 | return 71 | } 72 | } 73 | 74 | func detectNvidia() bool { 75 | out, err := runAndGiveStdout("nvidia-detect") 76 | if err != nil { 77 | slog.Warn("failed to run nvidia-detect, assuming no nvidia", "error", err) 78 | return false 79 | } 80 | outString := string(out) 81 | if strings.Contains(outString, "No NVIDIA GPU detected") { 82 | return false 83 | } 84 | if strings.Contains(outString, "nvidia-driver") { 85 | return true 86 | } 87 | return false 88 | } 89 | 90 | func (c *BackendContext) GetBlockDevices(w http.ResponseWriter, _ *http.Request) { 91 | out, err := runAndGiveStdout("lsblk", "-OJ") 92 | if err != nil { 93 | slog.Error("failed to execute lsblk", "error", err) 94 | http.Error(w, "failed to execute lsblk", http.StatusInternalServerError) 95 | return 96 | } 97 | w.Header().Set("Content-Type", "application/json") 98 | _, err = w.Write(out) 99 | if err != nil { 100 | slog.Error("failed to send output", "error", err) 101 | return 102 | } 103 | } 104 | 105 | func (c *BackendContext) Install(w http.ResponseWriter, r *http.Request) { 106 | if c.runningCmd != nil { 107 | slog.Error("already running") 108 | http.Error(w, "already running", http.StatusConflict) 109 | return 110 | } 111 | var err error 112 | contentType := r.Header.Get("Content-Type") 113 | switch { 114 | case strings.HasPrefix(contentType, "application/x-www-form-urlencoded"): 115 | err = r.ParseForm() 116 | case strings.HasPrefix(contentType, "multipart/form-data"): 117 | err = r.ParseMultipartForm(1024 * 1024) 118 | default: 119 | slog.Error("unknown content type", "content_type", r.Header.Get("Content-Type")) 120 | http.Error(w, "failed to parse form", http.StatusBadRequest) 121 | return 122 | } 123 | if err != nil { 124 | slog.Error("failed to parse form", "error", err) 125 | http.Error(w, "failed to parse form", http.StatusBadRequest) 126 | return 127 | } 128 | slog.Debug("Install button pressed") 129 | for k, v := range r.Form { 130 | slog.Debug(" form value", "key", k, "value", v[0]) 131 | c.runningParameters[k] = v[0] 132 | } 133 | c.doRunInstall() 134 | } 135 | 136 | func (c *BackendContext) ProcessStatus(w http.ResponseWriter, _ *http.Request) { 137 | type status struct { 138 | Status string `json:"status"` 139 | Output string `json:"output"` 140 | ReturnCode int `json:"return_code"` 141 | Command string `json:"command"` 142 | } 143 | s := status{ 144 | Status: "RUNNING", 145 | Output: c.cmdOutput.String(), 146 | ReturnCode: -1, 147 | Command: "", 148 | } 149 | if c.runningCmd == nil || c.runningCmd.Process == nil { 150 | http.Error(w, "no running process", http.StatusNotFound) 151 | return 152 | } 153 | if c.runningCmd.ProcessState != nil { 154 | s.Status = "FINISHED" 155 | s.ReturnCode = c.runningCmd.ProcessState.ExitCode() 156 | s.Command = strings.Join(c.runningCmd.Args, " ") 157 | } 158 | 159 | err := writeJson(w, s) 160 | if err != nil { 161 | slog.Error("failed to write data", "error", err) 162 | http.Error(w, "failed to write data", http.StatusInternalServerError) 163 | return 164 | } 165 | } 166 | 167 | func (c *BackendContext) DownloadLog(w http.ResponseWriter, _ *http.Request) { 168 | w.Header().Add("Content-Type", "text/plain;charset=UTF-8") 169 | w.Header().Add("Content-Disposition", "attachment;filename=installer.log") 170 | _, err := w.Write(c.cmdOutput.Bytes()) 171 | if err != nil { 172 | slog.Error("failed to write data", "error", err) 173 | return 174 | } 175 | } 176 | 177 | func (c *BackendContext) Clear(w http.ResponseWriter, _ *http.Request) { 178 | if c.runningCmd == nil || c.runningCmd.Process == nil { 179 | return 180 | } 181 | if c.runningCmd.ProcessState != nil { 182 | // already finished, clear 183 | c.runningCmd = nil 184 | c.cmdOutput = bytes.Buffer{} 185 | return 186 | } 187 | err := c.runningCmd.Cancel() 188 | if err != nil { 189 | slog.Error("failed to stop the process", "error", err) 190 | http.Error(w, "failed to stop the process", http.StatusInternalServerError) 191 | } 192 | } 193 | -------------------------------------------------------------------------------- /backend/timezones.txt: -------------------------------------------------------------------------------- 1 | # robo@aspire:/usr/share/zoneinfo$ find -type f,l |cut -c3- |grep -v "posix\\/" | grep -v "right\\/" > /tmp/timezones.txt 2 | Africa/Abidjan 3 | Africa/Accra 4 | Africa/Addis_Ababa 5 | Africa/Algiers 6 | Africa/Asmara 7 | Africa/Asmera 8 | Africa/Bamako 9 | Africa/Bangui 10 | Africa/Banjul 11 | Africa/Bissau 12 | Africa/Blantyre 13 | Africa/Brazzaville 14 | Africa/Bujumbura 15 | Africa/Cairo 16 | Africa/Casablanca 17 | Africa/Ceuta 18 | Africa/Conakry 19 | Africa/Dakar 20 | Africa/Dar_es_Salaam 21 | Africa/Djibouti 22 | Africa/Douala 23 | Africa/El_Aaiun 24 | Africa/Freetown 25 | Africa/Gaborone 26 | Africa/Harare 27 | Africa/Johannesburg 28 | Africa/Juba 29 | Africa/Kampala 30 | Africa/Khartoum 31 | Africa/Kigali 32 | Africa/Kinshasa 33 | Africa/Lagos 34 | Africa/Libreville 35 | Africa/Lome 36 | Africa/Luanda 37 | Africa/Lubumbashi 38 | Africa/Lusaka 39 | Africa/Malabo 40 | Africa/Maputo 41 | Africa/Maseru 42 | Africa/Mbabane 43 | Africa/Mogadishu 44 | Africa/Monrovia 45 | Africa/Nairobi 46 | Africa/Ndjamena 47 | Africa/Niamey 48 | Africa/Nouakchott 49 | Africa/Ouagadougou 50 | Africa/Porto-Novo 51 | Africa/Sao_Tome 52 | Africa/Timbuktu 53 | Africa/Tripoli 54 | Africa/Tunis 55 | Africa/Windhoek 56 | America/Adak 57 | America/Anchorage 58 | America/Anguilla 59 | America/Antigua 60 | America/Araguaina 61 | America/Argentina/Buenos_Aires 62 | America/Argentina/Catamarca 63 | America/Argentina/ComodRivadavia 64 | America/Argentina/Cordoba 65 | America/Argentina/Jujuy 66 | America/Argentina/La_Rioja 67 | America/Argentina/Mendoza 68 | America/Argentina/Rio_Gallegos 69 | America/Argentina/Salta 70 | America/Argentina/San_Juan 71 | America/Argentina/San_Luis 72 | America/Argentina/Tucuman 73 | America/Argentina/Ushuaia 74 | America/Aruba 75 | America/Asuncion 76 | America/Atikokan 77 | America/Atka 78 | America/Bahia 79 | America/Bahia_Banderas 80 | America/Barbados 81 | America/Belem 82 | America/Belize 83 | America/Blanc-Sablon 84 | America/Boa_Vista 85 | America/Bogota 86 | America/Boise 87 | America/Buenos_Aires 88 | America/Cambridge_Bay 89 | America/Campo_Grande 90 | America/Cancun 91 | America/Caracas 92 | America/Catamarca 93 | America/Cayenne 94 | America/Cayman 95 | America/Chicago 96 | America/Chihuahua 97 | America/Coral_Harbour 98 | America/Cordoba 99 | America/Costa_Rica 100 | America/Creston 101 | America/Cuiaba 102 | America/Curacao 103 | America/Danmarkshavn 104 | America/Dawson 105 | America/Dawson_Creek 106 | America/Denver 107 | America/Detroit 108 | America/Dominica 109 | America/Edmonton 110 | America/Eirunepe 111 | America/El_Salvador 112 | America/Ensenada 113 | America/Fort_Nelson 114 | America/Fort_Wayne 115 | America/Fortaleza 116 | America/Glace_Bay 117 | America/Godthab 118 | America/Goose_Bay 119 | America/Grand_Turk 120 | America/Grenada 121 | America/Guadeloupe 122 | America/Guatemala 123 | America/Guayaquil 124 | America/Guyana 125 | America/Halifax 126 | America/Havana 127 | America/Hermosillo 128 | America/Indiana/Indianapolis 129 | America/Indiana/Knox 130 | America/Indiana/Marengo 131 | America/Indiana/Petersburg 132 | America/Indiana/Tell_City 133 | America/Indiana/Vevay 134 | America/Indiana/Vincennes 135 | America/Indiana/Winamac 136 | America/Indianapolis 137 | America/Inuvik 138 | America/Iqaluit 139 | America/Jamaica 140 | America/Jujuy 141 | America/Juneau 142 | America/Kentucky/Louisville 143 | America/Kentucky/Monticello 144 | America/Knox_IN 145 | America/Kralendijk 146 | America/La_Paz 147 | America/Lima 148 | America/Los_Angeles 149 | America/Louisville 150 | America/Lower_Princes 151 | America/Maceio 152 | America/Managua 153 | America/Manaus 154 | America/Marigot 155 | America/Martinique 156 | America/Matamoros 157 | America/Mazatlan 158 | America/Mendoza 159 | America/Menominee 160 | America/Merida 161 | America/Metlakatla 162 | America/Mexico_City 163 | America/Miquelon 164 | America/Moncton 165 | America/Monterrey 166 | America/Montevideo 167 | America/Montreal 168 | America/Montserrat 169 | America/Nassau 170 | America/New_York 171 | America/Nipigon 172 | America/Nome 173 | America/Noronha 174 | America/North_Dakota/Beulah 175 | America/North_Dakota/Center 176 | America/North_Dakota/New_Salem 177 | America/Nuuk 178 | America/Ojinaga 179 | America/Panama 180 | America/Pangnirtung 181 | America/Paramaribo 182 | America/Phoenix 183 | America/Port-au-Prince 184 | America/Port_of_Spain 185 | America/Porto_Acre 186 | America/Porto_Velho 187 | America/Puerto_Rico 188 | America/Punta_Arenas 189 | America/Rainy_River 190 | America/Rankin_Inlet 191 | America/Recife 192 | America/Regina 193 | America/Resolute 194 | America/Rio_Branco 195 | America/Rosario 196 | America/Santa_Isabel 197 | America/Santarem 198 | America/Santiago 199 | America/Santo_Domingo 200 | America/Sao_Paulo 201 | America/Scoresbysund 202 | America/Shiprock 203 | America/Sitka 204 | America/St_Barthelemy 205 | America/St_Johns 206 | America/St_Kitts 207 | America/St_Lucia 208 | America/St_Thomas 209 | America/St_Vincent 210 | America/Swift_Current 211 | America/Tegucigalpa 212 | America/Thule 213 | America/Thunder_Bay 214 | America/Tijuana 215 | America/Toronto 216 | America/Tortola 217 | America/Vancouver 218 | America/Virgin 219 | America/Whitehorse 220 | America/Winnipeg 221 | America/Yakutat 222 | America/Yellowknife 223 | Antarctica/Casey 224 | Antarctica/Davis 225 | Antarctica/DumontDUrville 226 | Antarctica/Macquarie 227 | Antarctica/Mawson 228 | Antarctica/McMurdo 229 | Antarctica/Palmer 230 | Antarctica/Rothera 231 | Antarctica/South_Pole 232 | Antarctica/Syowa 233 | Antarctica/Troll 234 | Antarctica/Vostok 235 | Arctic/Longyearbyen 236 | Asia/Aden 237 | Asia/Almaty 238 | Asia/Amman 239 | Asia/Anadyr 240 | Asia/Aqtau 241 | Asia/Aqtobe 242 | Asia/Ashgabat 243 | Asia/Ashkhabad 244 | Asia/Atyrau 245 | Asia/Baghdad 246 | Asia/Bahrain 247 | Asia/Baku 248 | Asia/Bangkok 249 | Asia/Barnaul 250 | Asia/Beirut 251 | Asia/Bishkek 252 | Asia/Brunei 253 | Asia/Calcutta 254 | Asia/Chita 255 | Asia/Choibalsan 256 | Asia/Chongqing 257 | Asia/Chungking 258 | Asia/Colombo 259 | Asia/Dacca 260 | Asia/Damascus 261 | Asia/Dhaka 262 | Asia/Dili 263 | Asia/Dubai 264 | Asia/Dushanbe 265 | Asia/Famagusta 266 | Asia/Gaza 267 | Asia/Harbin 268 | Asia/Hebron 269 | Asia/Ho_Chi_Minh 270 | Asia/Hong_Kong 271 | Asia/Hovd 272 | Asia/Irkutsk 273 | Asia/Istanbul 274 | Asia/Jakarta 275 | Asia/Jayapura 276 | Asia/Jerusalem 277 | Asia/Kabul 278 | Asia/Kamchatka 279 | Asia/Karachi 280 | Asia/Kashgar 281 | Asia/Kathmandu 282 | Asia/Katmandu 283 | Asia/Khandyga 284 | Asia/Kolkata 285 | Asia/Krasnoyarsk 286 | Asia/Kuala_Lumpur 287 | Asia/Kuching 288 | Asia/Kuwait 289 | Asia/Macao 290 | Asia/Macau 291 | Asia/Magadan 292 | Asia/Makassar 293 | Asia/Manila 294 | Asia/Muscat 295 | Asia/Nicosia 296 | Asia/Novokuznetsk 297 | Asia/Novosibirsk 298 | Asia/Omsk 299 | Asia/Oral 300 | Asia/Phnom_Penh 301 | Asia/Pontianak 302 | Asia/Pyongyang 303 | Asia/Qatar 304 | Asia/Qostanay 305 | Asia/Qyzylorda 306 | Asia/Rangoon 307 | Asia/Riyadh 308 | Asia/Saigon 309 | Asia/Sakhalin 310 | Asia/Samarkand 311 | Asia/Seoul 312 | Asia/Shanghai 313 | Asia/Singapore 314 | Asia/Srednekolymsk 315 | Asia/Taipei 316 | Asia/Tashkent 317 | Asia/Tbilisi 318 | Asia/Tehran 319 | Asia/Tel_Aviv 320 | Asia/Thimbu 321 | Asia/Thimphu 322 | Asia/Tokyo 323 | Asia/Tomsk 324 | Asia/Ujung_Pandang 325 | Asia/Ulaanbaatar 326 | Asia/Ulan_Bator 327 | Asia/Urumqi 328 | Asia/Ust-Nera 329 | Asia/Vientiane 330 | Asia/Vladivostok 331 | Asia/Yakutsk 332 | Asia/Yangon 333 | Asia/Yekaterinburg 334 | Asia/Yerevan 335 | Atlantic/Azores 336 | Atlantic/Bermuda 337 | Atlantic/Canary 338 | Atlantic/Cape_Verde 339 | Atlantic/Faeroe 340 | Atlantic/Faroe 341 | Atlantic/Jan_Mayen 342 | Atlantic/Madeira 343 | Atlantic/Reykjavik 344 | Atlantic/South_Georgia 345 | Atlantic/St_Helena 346 | Atlantic/Stanley 347 | Australia/ACT 348 | Australia/Adelaide 349 | Australia/Brisbane 350 | Australia/Broken_Hill 351 | Australia/Canberra 352 | Australia/Currie 353 | Australia/Darwin 354 | Australia/Eucla 355 | Australia/Hobart 356 | Australia/LHI 357 | Australia/Lindeman 358 | Australia/Lord_Howe 359 | Australia/Melbourne 360 | Australia/NSW 361 | Australia/North 362 | Australia/Perth 363 | Australia/Queensland 364 | Australia/South 365 | Australia/Sydney 366 | Australia/Tasmania 367 | Australia/Victoria 368 | Australia/West 369 | Australia/Yancowinna 370 | Brazil/Acre 371 | Brazil/DeNoronha 372 | Brazil/East 373 | Brazil/West 374 | CET 375 | CST6CDT 376 | Canada/Atlantic 377 | Canada/Central 378 | Canada/Eastern 379 | Canada/Mountain 380 | Canada/Newfoundland 381 | Canada/Pacific 382 | Canada/Saskatchewan 383 | Canada/Yukon 384 | Chile/Continental 385 | Chile/EasterIsland 386 | Cuba 387 | EET 388 | EST 389 | EST5EDT 390 | Egypt 391 | Eire 392 | Etc/GMT 393 | Etc/GMT+0 394 | Etc/GMT+1 395 | Etc/GMT+10 396 | Etc/GMT+11 397 | Etc/GMT+12 398 | Etc/GMT+2 399 | Etc/GMT+3 400 | Etc/GMT+4 401 | Etc/GMT+5 402 | Etc/GMT+6 403 | Etc/GMT+7 404 | Etc/GMT+8 405 | Etc/GMT+9 406 | Etc/GMT-0 407 | Etc/GMT-1 408 | Etc/GMT-10 409 | Etc/GMT-11 410 | Etc/GMT-12 411 | Etc/GMT-13 412 | Etc/GMT-14 413 | Etc/GMT-2 414 | Etc/GMT-3 415 | Etc/GMT-4 416 | Etc/GMT-5 417 | Etc/GMT-6 418 | Etc/GMT-7 419 | Etc/GMT-8 420 | Etc/GMT-9 421 | Etc/GMT0 422 | Etc/Greenwich 423 | Etc/UCT 424 | Etc/UTC 425 | Etc/Universal 426 | Etc/Zulu 427 | Europe/Amsterdam 428 | Europe/Andorra 429 | Europe/Astrakhan 430 | Europe/Athens 431 | Europe/Belfast 432 | Europe/Belgrade 433 | Europe/Berlin 434 | Europe/Bratislava 435 | Europe/Brussels 436 | Europe/Bucharest 437 | Europe/Budapest 438 | Europe/Busingen 439 | Europe/Chisinau 440 | Europe/Copenhagen 441 | Europe/Dublin 442 | Europe/Gibraltar 443 | Europe/Guernsey 444 | Europe/Helsinki 445 | Europe/Isle_of_Man 446 | Europe/Istanbul 447 | Europe/Jersey 448 | Europe/Kaliningrad 449 | Europe/Kiev 450 | Europe/Kirov 451 | Europe/Lisbon 452 | Europe/Ljubljana 453 | Europe/London 454 | Europe/Luxembourg 455 | Europe/Madrid 456 | Europe/Malta 457 | Europe/Mariehamn 458 | Europe/Minsk 459 | Europe/Monaco 460 | Europe/Moscow 461 | Europe/Nicosia 462 | Europe/Oslo 463 | Europe/Paris 464 | Europe/Podgorica 465 | Europe/Prague 466 | Europe/Riga 467 | Europe/Rome 468 | Europe/Samara 469 | Europe/San_Marino 470 | Europe/Sarajevo 471 | Europe/Saratov 472 | Europe/Simferopol 473 | Europe/Skopje 474 | Europe/Sofia 475 | Europe/Stockholm 476 | Europe/Tallinn 477 | Europe/Tirane 478 | Europe/Tiraspol 479 | Europe/Ulyanovsk 480 | Europe/Uzhgorod 481 | Europe/Vaduz 482 | Europe/Vatican 483 | Europe/Vienna 484 | Europe/Vilnius 485 | Europe/Volgograd 486 | Europe/Warsaw 487 | Europe/Zagreb 488 | Europe/Zaporozhye 489 | Europe/Zurich 490 | GB 491 | GB-Eire 492 | GMT 493 | GMT+0 494 | GMT-0 495 | GMT0 496 | Greenwich 497 | HST 498 | Hongkong 499 | Iceland 500 | Indian/Antananarivo 501 | Indian/Chagos 502 | Indian/Christmas 503 | Indian/Cocos 504 | Indian/Comoro 505 | Indian/Kerguelen 506 | Indian/Mahe 507 | Indian/Maldives 508 | Indian/Mauritius 509 | Indian/Mayotte 510 | Indian/Reunion 511 | Iran 512 | Israel 513 | Jamaica 514 | Japan 515 | Kwajalein 516 | Libya 517 | MET 518 | MST 519 | MST7MDT 520 | Mexico/BajaNorte 521 | Mexico/BajaSur 522 | Mexico/General 523 | NZ 524 | NZ-CHAT 525 | Navajo 526 | PRC 527 | PST8PDT 528 | Pacific/Apia 529 | Pacific/Auckland 530 | Pacific/Bougainville 531 | Pacific/Chatham 532 | Pacific/Chuuk 533 | Pacific/Easter 534 | Pacific/Efate 535 | Pacific/Enderbury 536 | Pacific/Fakaofo 537 | Pacific/Fiji 538 | Pacific/Funafuti 539 | Pacific/Galapagos 540 | Pacific/Gambier 541 | Pacific/Guadalcanal 542 | Pacific/Guam 543 | Pacific/Honolulu 544 | Pacific/Johnston 545 | Pacific/Kiritimati 546 | Pacific/Kosrae 547 | Pacific/Kwajalein 548 | Pacific/Majuro 549 | Pacific/Marquesas 550 | Pacific/Midway 551 | Pacific/Nauru 552 | Pacific/Niue 553 | Pacific/Norfolk 554 | Pacific/Noumea 555 | Pacific/Pago_Pago 556 | Pacific/Palau 557 | Pacific/Pitcairn 558 | Pacific/Pohnpei 559 | Pacific/Ponape 560 | Pacific/Port_Moresby 561 | Pacific/Rarotonga 562 | Pacific/Saipan 563 | Pacific/Samoa 564 | Pacific/Tahiti 565 | Pacific/Tarawa 566 | Pacific/Tongatapu 567 | Pacific/Truk 568 | Pacific/Wake 569 | Pacific/Wallis 570 | Pacific/Yap 571 | Poland 572 | Portugal 573 | ROC 574 | ROK 575 | Singapore 576 | Turkey 577 | UCT 578 | US/Alaska 579 | US/Aleutian 580 | US/Arizona 581 | US/Central 582 | US/East-Indiana 583 | US/Eastern 584 | US/Hawaii 585 | US/Indiana-Starke 586 | US/Michigan 587 | US/Mountain 588 | US/Pacific 589 | US/Samoa 590 | UTC 591 | Universal 592 | W-SU 593 | WET 594 | Zulu 595 | -------------------------------------------------------------------------------- /frontend/src/assets/timezones.txt: -------------------------------------------------------------------------------- 1 | # robo@aspire:/usr/share/zoneinfo$ find -type f,l |cut -c3- |grep -v "posix\\/" | grep -v "right\\/" > /tmp/timezones.txt 2 | Africa/Abidjan 3 | Africa/Accra 4 | Africa/Addis_Ababa 5 | Africa/Algiers 6 | Africa/Asmara 7 | Africa/Asmera 8 | Africa/Bamako 9 | Africa/Bangui 10 | Africa/Banjul 11 | Africa/Bissau 12 | Africa/Blantyre 13 | Africa/Brazzaville 14 | Africa/Bujumbura 15 | Africa/Cairo 16 | Africa/Casablanca 17 | Africa/Ceuta 18 | Africa/Conakry 19 | Africa/Dakar 20 | Africa/Dar_es_Salaam 21 | Africa/Djibouti 22 | Africa/Douala 23 | Africa/El_Aaiun 24 | Africa/Freetown 25 | Africa/Gaborone 26 | Africa/Harare 27 | Africa/Johannesburg 28 | Africa/Juba 29 | Africa/Kampala 30 | Africa/Khartoum 31 | Africa/Kigali 32 | Africa/Kinshasa 33 | Africa/Lagos 34 | Africa/Libreville 35 | Africa/Lome 36 | Africa/Luanda 37 | Africa/Lubumbashi 38 | Africa/Lusaka 39 | Africa/Malabo 40 | Africa/Maputo 41 | Africa/Maseru 42 | Africa/Mbabane 43 | Africa/Mogadishu 44 | Africa/Monrovia 45 | Africa/Nairobi 46 | Africa/Ndjamena 47 | Africa/Niamey 48 | Africa/Nouakchott 49 | Africa/Ouagadougou 50 | Africa/Porto-Novo 51 | Africa/Sao_Tome 52 | Africa/Timbuktu 53 | Africa/Tripoli 54 | Africa/Tunis 55 | Africa/Windhoek 56 | America/Adak 57 | America/Anchorage 58 | America/Anguilla 59 | America/Antigua 60 | America/Araguaina 61 | America/Argentina/Buenos_Aires 62 | America/Argentina/Catamarca 63 | America/Argentina/ComodRivadavia 64 | America/Argentina/Cordoba 65 | America/Argentina/Jujuy 66 | America/Argentina/La_Rioja 67 | America/Argentina/Mendoza 68 | America/Argentina/Rio_Gallegos 69 | America/Argentina/Salta 70 | America/Argentina/San_Juan 71 | America/Argentina/San_Luis 72 | America/Argentina/Tucuman 73 | America/Argentina/Ushuaia 74 | America/Aruba 75 | America/Asuncion 76 | America/Atikokan 77 | America/Atka 78 | America/Bahia 79 | America/Bahia_Banderas 80 | America/Barbados 81 | America/Belem 82 | America/Belize 83 | America/Blanc-Sablon 84 | America/Boa_Vista 85 | America/Bogota 86 | America/Boise 87 | America/Buenos_Aires 88 | America/Cambridge_Bay 89 | America/Campo_Grande 90 | America/Cancun 91 | America/Caracas 92 | America/Catamarca 93 | America/Cayenne 94 | America/Cayman 95 | America/Chicago 96 | America/Chihuahua 97 | America/Coral_Harbour 98 | America/Cordoba 99 | America/Costa_Rica 100 | America/Creston 101 | America/Cuiaba 102 | America/Curacao 103 | America/Danmarkshavn 104 | America/Dawson 105 | America/Dawson_Creek 106 | America/Denver 107 | America/Detroit 108 | America/Dominica 109 | America/Edmonton 110 | America/Eirunepe 111 | America/El_Salvador 112 | America/Ensenada 113 | America/Fort_Nelson 114 | America/Fort_Wayne 115 | America/Fortaleza 116 | America/Glace_Bay 117 | America/Godthab 118 | America/Goose_Bay 119 | America/Grand_Turk 120 | America/Grenada 121 | America/Guadeloupe 122 | America/Guatemala 123 | America/Guayaquil 124 | America/Guyana 125 | America/Halifax 126 | America/Havana 127 | America/Hermosillo 128 | America/Indiana/Indianapolis 129 | America/Indiana/Knox 130 | America/Indiana/Marengo 131 | America/Indiana/Petersburg 132 | America/Indiana/Tell_City 133 | America/Indiana/Vevay 134 | America/Indiana/Vincennes 135 | America/Indiana/Winamac 136 | America/Indianapolis 137 | America/Inuvik 138 | America/Iqaluit 139 | America/Jamaica 140 | America/Jujuy 141 | America/Juneau 142 | America/Kentucky/Louisville 143 | America/Kentucky/Monticello 144 | America/Knox_IN 145 | America/Kralendijk 146 | America/La_Paz 147 | America/Lima 148 | America/Los_Angeles 149 | America/Louisville 150 | America/Lower_Princes 151 | America/Maceio 152 | America/Managua 153 | America/Manaus 154 | America/Marigot 155 | America/Martinique 156 | America/Matamoros 157 | America/Mazatlan 158 | America/Mendoza 159 | America/Menominee 160 | America/Merida 161 | America/Metlakatla 162 | America/Mexico_City 163 | America/Miquelon 164 | America/Moncton 165 | America/Monterrey 166 | America/Montevideo 167 | America/Montreal 168 | America/Montserrat 169 | America/Nassau 170 | America/New_York 171 | America/Nipigon 172 | America/Nome 173 | America/Noronha 174 | America/North_Dakota/Beulah 175 | America/North_Dakota/Center 176 | America/North_Dakota/New_Salem 177 | America/Nuuk 178 | America/Ojinaga 179 | America/Panama 180 | America/Pangnirtung 181 | America/Paramaribo 182 | America/Phoenix 183 | America/Port-au-Prince 184 | America/Port_of_Spain 185 | America/Porto_Acre 186 | America/Porto_Velho 187 | America/Puerto_Rico 188 | America/Punta_Arenas 189 | America/Rainy_River 190 | America/Rankin_Inlet 191 | America/Recife 192 | America/Regina 193 | America/Resolute 194 | America/Rio_Branco 195 | America/Rosario 196 | America/Santa_Isabel 197 | America/Santarem 198 | America/Santiago 199 | America/Santo_Domingo 200 | America/Sao_Paulo 201 | America/Scoresbysund 202 | America/Shiprock 203 | America/Sitka 204 | America/St_Barthelemy 205 | America/St_Johns 206 | America/St_Kitts 207 | America/St_Lucia 208 | America/St_Thomas 209 | America/St_Vincent 210 | America/Swift_Current 211 | America/Tegucigalpa 212 | America/Thule 213 | America/Thunder_Bay 214 | America/Tijuana 215 | America/Toronto 216 | America/Tortola 217 | America/Vancouver 218 | America/Virgin 219 | America/Whitehorse 220 | America/Winnipeg 221 | America/Yakutat 222 | America/Yellowknife 223 | Antarctica/Casey 224 | Antarctica/Davis 225 | Antarctica/DumontDUrville 226 | Antarctica/Macquarie 227 | Antarctica/Mawson 228 | Antarctica/McMurdo 229 | Antarctica/Palmer 230 | Antarctica/Rothera 231 | Antarctica/South_Pole 232 | Antarctica/Syowa 233 | Antarctica/Troll 234 | Antarctica/Vostok 235 | Arctic/Longyearbyen 236 | Asia/Aden 237 | Asia/Almaty 238 | Asia/Amman 239 | Asia/Anadyr 240 | Asia/Aqtau 241 | Asia/Aqtobe 242 | Asia/Ashgabat 243 | Asia/Ashkhabad 244 | Asia/Atyrau 245 | Asia/Baghdad 246 | Asia/Bahrain 247 | Asia/Baku 248 | Asia/Bangkok 249 | Asia/Barnaul 250 | Asia/Beirut 251 | Asia/Bishkek 252 | Asia/Brunei 253 | Asia/Calcutta 254 | Asia/Chita 255 | Asia/Choibalsan 256 | Asia/Chongqing 257 | Asia/Chungking 258 | Asia/Colombo 259 | Asia/Dacca 260 | Asia/Damascus 261 | Asia/Dhaka 262 | Asia/Dili 263 | Asia/Dubai 264 | Asia/Dushanbe 265 | Asia/Famagusta 266 | Asia/Gaza 267 | Asia/Harbin 268 | Asia/Hebron 269 | Asia/Ho_Chi_Minh 270 | Asia/Hong_Kong 271 | Asia/Hovd 272 | Asia/Irkutsk 273 | Asia/Istanbul 274 | Asia/Jakarta 275 | Asia/Jayapura 276 | Asia/Jerusalem 277 | Asia/Kabul 278 | Asia/Kamchatka 279 | Asia/Karachi 280 | Asia/Kashgar 281 | Asia/Kathmandu 282 | Asia/Katmandu 283 | Asia/Khandyga 284 | Asia/Kolkata 285 | Asia/Krasnoyarsk 286 | Asia/Kuala_Lumpur 287 | Asia/Kuching 288 | Asia/Kuwait 289 | Asia/Macao 290 | Asia/Macau 291 | Asia/Magadan 292 | Asia/Makassar 293 | Asia/Manila 294 | Asia/Muscat 295 | Asia/Nicosia 296 | Asia/Novokuznetsk 297 | Asia/Novosibirsk 298 | Asia/Omsk 299 | Asia/Oral 300 | Asia/Phnom_Penh 301 | Asia/Pontianak 302 | Asia/Pyongyang 303 | Asia/Qatar 304 | Asia/Qostanay 305 | Asia/Qyzylorda 306 | Asia/Rangoon 307 | Asia/Riyadh 308 | Asia/Saigon 309 | Asia/Sakhalin 310 | Asia/Samarkand 311 | Asia/Seoul 312 | Asia/Shanghai 313 | Asia/Singapore 314 | Asia/Srednekolymsk 315 | Asia/Taipei 316 | Asia/Tashkent 317 | Asia/Tbilisi 318 | Asia/Tehran 319 | Asia/Tel_Aviv 320 | Asia/Thimbu 321 | Asia/Thimphu 322 | Asia/Tokyo 323 | Asia/Tomsk 324 | Asia/Ujung_Pandang 325 | Asia/Ulaanbaatar 326 | Asia/Ulan_Bator 327 | Asia/Urumqi 328 | Asia/Ust-Nera 329 | Asia/Vientiane 330 | Asia/Vladivostok 331 | Asia/Yakutsk 332 | Asia/Yangon 333 | Asia/Yekaterinburg 334 | Asia/Yerevan 335 | Atlantic/Azores 336 | Atlantic/Bermuda 337 | Atlantic/Canary 338 | Atlantic/Cape_Verde 339 | Atlantic/Faeroe 340 | Atlantic/Faroe 341 | Atlantic/Jan_Mayen 342 | Atlantic/Madeira 343 | Atlantic/Reykjavik 344 | Atlantic/South_Georgia 345 | Atlantic/St_Helena 346 | Atlantic/Stanley 347 | Australia/ACT 348 | Australia/Adelaide 349 | Australia/Brisbane 350 | Australia/Broken_Hill 351 | Australia/Canberra 352 | Australia/Currie 353 | Australia/Darwin 354 | Australia/Eucla 355 | Australia/Hobart 356 | Australia/LHI 357 | Australia/Lindeman 358 | Australia/Lord_Howe 359 | Australia/Melbourne 360 | Australia/NSW 361 | Australia/North 362 | Australia/Perth 363 | Australia/Queensland 364 | Australia/South 365 | Australia/Sydney 366 | Australia/Tasmania 367 | Australia/Victoria 368 | Australia/West 369 | Australia/Yancowinna 370 | Brazil/Acre 371 | Brazil/DeNoronha 372 | Brazil/East 373 | Brazil/West 374 | CET 375 | CST6CDT 376 | Canada/Atlantic 377 | Canada/Central 378 | Canada/Eastern 379 | Canada/Mountain 380 | Canada/Newfoundland 381 | Canada/Pacific 382 | Canada/Saskatchewan 383 | Canada/Yukon 384 | Chile/Continental 385 | Chile/EasterIsland 386 | Cuba 387 | EET 388 | EST 389 | EST5EDT 390 | Egypt 391 | Eire 392 | Etc/GMT 393 | Etc/GMT+0 394 | Etc/GMT+1 395 | Etc/GMT+10 396 | Etc/GMT+11 397 | Etc/GMT+12 398 | Etc/GMT+2 399 | Etc/GMT+3 400 | Etc/GMT+4 401 | Etc/GMT+5 402 | Etc/GMT+6 403 | Etc/GMT+7 404 | Etc/GMT+8 405 | Etc/GMT+9 406 | Etc/GMT-0 407 | Etc/GMT-1 408 | Etc/GMT-10 409 | Etc/GMT-11 410 | Etc/GMT-12 411 | Etc/GMT-13 412 | Etc/GMT-14 413 | Etc/GMT-2 414 | Etc/GMT-3 415 | Etc/GMT-4 416 | Etc/GMT-5 417 | Etc/GMT-6 418 | Etc/GMT-7 419 | Etc/GMT-8 420 | Etc/GMT-9 421 | Etc/GMT0 422 | Etc/Greenwich 423 | Etc/UCT 424 | Etc/UTC 425 | Etc/Universal 426 | Etc/Zulu 427 | Europe/Amsterdam 428 | Europe/Andorra 429 | Europe/Astrakhan 430 | Europe/Athens 431 | Europe/Belfast 432 | Europe/Belgrade 433 | Europe/Berlin 434 | Europe/Bratislava 435 | Europe/Brussels 436 | Europe/Bucharest 437 | Europe/Budapest 438 | Europe/Busingen 439 | Europe/Chisinau 440 | Europe/Copenhagen 441 | Europe/Dublin 442 | Europe/Gibraltar 443 | Europe/Guernsey 444 | Europe/Helsinki 445 | Europe/Isle_of_Man 446 | Europe/Istanbul 447 | Europe/Jersey 448 | Europe/Kaliningrad 449 | Europe/Kiev 450 | Europe/Kirov 451 | Europe/Lisbon 452 | Europe/Ljubljana 453 | Europe/London 454 | Europe/Luxembourg 455 | Europe/Madrid 456 | Europe/Malta 457 | Europe/Mariehamn 458 | Europe/Minsk 459 | Europe/Monaco 460 | Europe/Moscow 461 | Europe/Nicosia 462 | Europe/Oslo 463 | Europe/Paris 464 | Europe/Podgorica 465 | Europe/Prague 466 | Europe/Riga 467 | Europe/Rome 468 | Europe/Samara 469 | Europe/San_Marino 470 | Europe/Sarajevo 471 | Europe/Saratov 472 | Europe/Simferopol 473 | Europe/Skopje 474 | Europe/Sofia 475 | Europe/Stockholm 476 | Europe/Tallinn 477 | Europe/Tirane 478 | Europe/Tiraspol 479 | Europe/Ulyanovsk 480 | Europe/Uzhgorod 481 | Europe/Vaduz 482 | Europe/Vatican 483 | Europe/Vienna 484 | Europe/Vilnius 485 | Europe/Volgograd 486 | Europe/Warsaw 487 | Europe/Zagreb 488 | Europe/Zaporozhye 489 | Europe/Zurich 490 | GB 491 | GB-Eire 492 | GMT 493 | GMT+0 494 | GMT-0 495 | GMT0 496 | Greenwich 497 | HST 498 | Hongkong 499 | Iceland 500 | Indian/Antananarivo 501 | Indian/Chagos 502 | Indian/Christmas 503 | Indian/Cocos 504 | Indian/Comoro 505 | Indian/Kerguelen 506 | Indian/Mahe 507 | Indian/Maldives 508 | Indian/Mauritius 509 | Indian/Mayotte 510 | Indian/Reunion 511 | Iran 512 | Israel 513 | Jamaica 514 | Japan 515 | Kwajalein 516 | Libya 517 | MET 518 | MST 519 | MST7MDT 520 | Mexico/BajaNorte 521 | Mexico/BajaSur 522 | Mexico/General 523 | NZ 524 | NZ-CHAT 525 | Navajo 526 | PRC 527 | PST8PDT 528 | Pacific/Apia 529 | Pacific/Auckland 530 | Pacific/Bougainville 531 | Pacific/Chatham 532 | Pacific/Chuuk 533 | Pacific/Easter 534 | Pacific/Efate 535 | Pacific/Enderbury 536 | Pacific/Fakaofo 537 | Pacific/Fiji 538 | Pacific/Funafuti 539 | Pacific/Galapagos 540 | Pacific/Gambier 541 | Pacific/Guadalcanal 542 | Pacific/Guam 543 | Pacific/Honolulu 544 | Pacific/Johnston 545 | Pacific/Kiritimati 546 | Pacific/Kosrae 547 | Pacific/Kwajalein 548 | Pacific/Majuro 549 | Pacific/Marquesas 550 | Pacific/Midway 551 | Pacific/Nauru 552 | Pacific/Niue 553 | Pacific/Norfolk 554 | Pacific/Noumea 555 | Pacific/Pago_Pago 556 | Pacific/Palau 557 | Pacific/Pitcairn 558 | Pacific/Pohnpei 559 | Pacific/Ponape 560 | Pacific/Port_Moresby 561 | Pacific/Rarotonga 562 | Pacific/Saipan 563 | Pacific/Samoa 564 | Pacific/Tahiti 565 | Pacific/Tarawa 566 | Pacific/Tongatapu 567 | Pacific/Truk 568 | Pacific/Wake 569 | Pacific/Wallis 570 | Pacific/Yap 571 | Poland 572 | Portugal 573 | ROC 574 | ROK 575 | Singapore 576 | Turkey 577 | UCT 578 | US/Alaska 579 | US/Aleutian 580 | US/Arizona 581 | US/Central 582 | US/East-Indiana 583 | US/Eastern 584 | US/Hawaii 585 | US/Indiana-Starke 586 | US/Michigan 587 | US/Mountain 588 | US/Pacific 589 | US/Samoa 590 | UTC 591 | Universal 592 | W-SU 593 | WET 594 | Zulu 595 | -------------------------------------------------------------------------------- /backend/test_data/lsblk.json: -------------------------------------------------------------------------------- 1 | { 2 | "blockdevices": [ 3 | { 4 | "alignment": 0, 5 | "disc-aln": 0, 6 | "dax": false, 7 | "disc-gran": "512B", 8 | "disc-max": "2G", 9 | "disc-zero": false, 10 | "fsavail": null, 11 | "fsroots": [ 12 | null 13 | ], 14 | "fssize": null, 15 | "fstype": null, 16 | "fsused": null, 17 | "fsuse%": null, 18 | "fsver": null, 19 | "group": "disk", 20 | "hctl": "0:0:0:0", 21 | "hotplug": false, 22 | "kname": "sda", 23 | "label": null, 24 | "log-sec": 512, 25 | "maj:min": "8:0", 26 | "min-io": 512, 27 | "mode": "brw-rw----", 28 | "model": "WDC WDS500G1R0B-68A4Z0", 29 | "name": "sda", 30 | "opt-io": 0, 31 | "owner": "root", 32 | "partflags": null, 33 | "partlabel": null, 34 | "parttype": null, 35 | "parttypename": null, 36 | "partuuid": null, 37 | "path": "/dev/sda", 38 | "phy-sec": 512, 39 | "pkname": null, 40 | "pttype": "gpt", 41 | "ptuuid": "00554620-2a90-42c1-a828-b743443bdb16", 42 | "ra": 128, 43 | "rand": false, 44 | "rev": "00WR", 45 | "rm": false, 46 | "ro": false, 47 | "rota": false, 48 | "rq-size": 64, 49 | "sched": "mq-deadline", 50 | "serial": "21140N440209", 51 | "size": "465.8G", 52 | "start": null, 53 | "state": "running", 54 | "subsystems": "block:scsi:pci", 55 | "mountpoint": null, 56 | "mountpoints": [ 57 | null 58 | ], 59 | "tran": "sata", 60 | "type": "disk", 61 | "uuid": null, 62 | "vendor": "ATA ", 63 | "wsame": "0B", 64 | "wwn": "0x5001b444a70f774d", 65 | "zoned": "none", 66 | "zone-sz": "0B", 67 | "zone-wgran": "0B", 68 | "zone-app": "0B", 69 | "zone-nr": 0, 70 | "zone-omax": 0, 71 | "zone-amax": 0, 72 | "children": [ 73 | { 74 | "alignment": 0, 75 | "disc-aln": 0, 76 | "dax": false, 77 | "disc-gran": "512B", 78 | "disc-max": "2G", 79 | "disc-zero": false, 80 | "fsavail": "109.9M", 81 | "fsroots": [ 82 | "/" 83 | ], 84 | "fssize": "199.8M", 85 | "fstype": "vfat", 86 | "fsused": "89.9M", 87 | "fsuse%": "45%", 88 | "fsver": "FAT16", 89 | "group": "disk", 90 | "hctl": null, 91 | "hotplug": false, 92 | "kname": "sda1", 93 | "label": null, 94 | "log-sec": 512, 95 | "maj:min": "8:1", 96 | "min-io": 512, 97 | "mode": "brw-rw----", 98 | "model": null, 99 | "name": "sda1", 100 | "opt-io": 0, 101 | "owner": "root", 102 | "partflags": null, 103 | "partlabel": null, 104 | "parttype": "ebd0a0a2-b9e5-4433-87c0-68b6b72699c7", 105 | "parttypename": "Microsoft basic data", 106 | "partuuid": "2fa0c774-df97-4732-b12a-d3cd6078a7fd", 107 | "path": "/dev/sda1", 108 | "phy-sec": 512, 109 | "pkname": "sda", 110 | "pttype": "gpt", 111 | "ptuuid": "00554620-2a90-42c1-a828-b743443bdb16", 112 | "ra": 128, 113 | "rand": false, 114 | "rev": null, 115 | "rm": false, 116 | "ro": false, 117 | "rota": false, 118 | "rq-size": 64, 119 | "sched": "mq-deadline", 120 | "serial": null, 121 | "size": "200M", 122 | "start": 2048, 123 | "state": null, 124 | "subsystems": "block:scsi:pci", 125 | "mountpoint": "/boot/efi", 126 | "mountpoints": [ 127 | "/boot/efi" 128 | ], 129 | "tran": null, 130 | "type": "part", 131 | "uuid": "5637-CBEB", 132 | "vendor": null, 133 | "wsame": "0B", 134 | "wwn": "0x5001b444a70f774d", 135 | "zoned": "none", 136 | "zone-sz": "0B", 137 | "zone-wgran": "0B", 138 | "zone-app": "0B", 139 | "zone-nr": 0, 140 | "zone-omax": 0, 141 | "zone-amax": 0 142 | },{ 143 | "alignment": 0, 144 | "disc-aln": 0, 145 | "dax": false, 146 | "disc-gran": "512B", 147 | "disc-max": "2G", 148 | "disc-zero": false, 149 | "fsavail": "278.3G", 150 | "fsroots": [ 151 | "/@home", "/@" 152 | ], 153 | "fssize": "465.6G", 154 | "fstype": "btrfs", 155 | "fsused": "178.5G", 156 | "fsuse%": "38%", 157 | "fsver": null, 158 | "group": "disk", 159 | "hctl": null, 160 | "hotplug": false, 161 | "kname": "sda2", 162 | "label": null, 163 | "log-sec": 512, 164 | "maj:min": "8:2", 165 | "min-io": 512, 166 | "mode": "brw-rw----", 167 | "model": null, 168 | "name": "sda2", 169 | "opt-io": 0, 170 | "owner": "root", 171 | "partflags": null, 172 | "partlabel": null, 173 | "parttype": "0fc63daf-8483-4772-8e79-3d69d8477de4", 174 | "parttypename": "Linux filesystem", 175 | "partuuid": "5cf34130-c0ab-4a2d-afde-968e87be62a4", 176 | "path": "/dev/sda2", 177 | "phy-sec": 512, 178 | "pkname": "sda", 179 | "pttype": "gpt", 180 | "ptuuid": "00554620-2a90-42c1-a828-b743443bdb16", 181 | "ra": 128, 182 | "rand": false, 183 | "rev": null, 184 | "rm": false, 185 | "ro": false, 186 | "rota": false, 187 | "rq-size": 64, 188 | "sched": "mq-deadline", 189 | "serial": null, 190 | "size": "465.6G", 191 | "start": 411648, 192 | "state": null, 193 | "subsystems": "block:scsi:pci", 194 | "mountpoint": "/home", 195 | "mountpoints": [ 196 | "/home", "/" 197 | ], 198 | "tran": null, 199 | "type": "part", 200 | "uuid": "c5467a99-e16a-4732-aa72-620e08e67746", 201 | "vendor": null, 202 | "wsame": "0B", 203 | "wwn": "0x5001b444a70f774d", 204 | "zoned": "none", 205 | "zone-sz": "0B", 206 | "zone-wgran": "0B", 207 | "zone-app": "0B", 208 | "zone-nr": 0, 209 | "zone-omax": 0, 210 | "zone-amax": 0 211 | } 212 | ] 213 | },{ 214 | "alignment": 0, 215 | "disc-aln": 0, 216 | "dax": false, 217 | "disc-gran": "512B", 218 | "disc-max": "2G", 219 | "disc-zero": false, 220 | "fsavail": null, 221 | "fsroots": [ 222 | null 223 | ], 224 | "fssize": null, 225 | "fstype": null, 226 | "fsused": null, 227 | "fsuse%": null, 228 | "fsver": null, 229 | "group": "disk", 230 | "hctl": "1:0:0:0", 231 | "hotplug": false, 232 | "kname": "sdb", 233 | "label": null, 234 | "log-sec": 512, 235 | "maj:min": "8:16", 236 | "min-io": 512, 237 | "mode": "brw-rw----", 238 | "model": "Patriot Burst", 239 | "name": "sdb", 240 | "opt-io": 0, 241 | "owner": "root", 242 | "partflags": null, 243 | "partlabel": null, 244 | "parttype": null, 245 | "parttypename": null, 246 | "partuuid": null, 247 | "path": "/dev/sdb", 248 | "phy-sec": 512, 249 | "pkname": null, 250 | "pttype": "dos", 251 | "ptuuid": "eedafa7d", 252 | "ra": 128, 253 | "rand": false, 254 | "rev": "61.2", 255 | "rm": false, 256 | "ro": false, 257 | "rota": false, 258 | "rq-size": 64, 259 | "sched": "mq-deadline", 260 | "serial": "DDDF078A1D3100106936", 261 | "size": "447.1G", 262 | "start": null, 263 | "state": "running", 264 | "subsystems": "block:scsi:pci", 265 | "mountpoint": null, 266 | "mountpoints": [ 267 | null 268 | ], 269 | "tran": "sata", 270 | "type": "disk", 271 | "uuid": null, 272 | "vendor": "ATA ", 273 | "wsame": "0B", 274 | "wwn": null, 275 | "zoned": "none", 276 | "zone-sz": "0B", 277 | "zone-wgran": "0B", 278 | "zone-app": "0B", 279 | "zone-nr": 0, 280 | "zone-omax": 0, 281 | "zone-amax": 0, 282 | "children": [ 283 | { 284 | "alignment": 0, 285 | "disc-aln": 0, 286 | "dax": false, 287 | "disc-gran": "512B", 288 | "disc-max": "2G", 289 | "disc-zero": false, 290 | "fsavail": "151.1G", 291 | "fsroots": [ 292 | "/notebookbackup", "/share", "/libvirt_images" 293 | ], 294 | "fssize": "447.1G", 295 | "fstype": "btrfs", 296 | "fsused": "295.5G", 297 | "fsuse%": "66%", 298 | "fsver": null, 299 | "group": "disk", 300 | "hctl": null, 301 | "hotplug": false, 302 | "kname": "sdb1", 303 | "label": null, 304 | "log-sec": 512, 305 | "maj:min": "8:17", 306 | "min-io": 512, 307 | "mode": "brw-rw----", 308 | "model": null, 309 | "name": "sdb1", 310 | "opt-io": 0, 311 | "owner": "root", 312 | "partflags": null, 313 | "partlabel": null, 314 | "parttype": "0x8e", 315 | "parttypename": "Linux LVM", 316 | "partuuid": "eedafa7d-01", 317 | "path": "/dev/sdb1", 318 | "phy-sec": 512, 319 | "pkname": "sdb", 320 | "pttype": "dos", 321 | "ptuuid": "eedafa7d", 322 | "ra": 128, 323 | "rand": false, 324 | "rev": null, 325 | "rm": false, 326 | "ro": false, 327 | "rota": false, 328 | "rq-size": 64, 329 | "sched": "mq-deadline", 330 | "serial": null, 331 | "size": "447.1G", 332 | "start": 2048, 333 | "state": null, 334 | "subsystems": "block:scsi:pci", 335 | "mountpoint": "/srv/notebookbackup", 336 | "mountpoints": [ 337 | "/srv/notebookbackup", "/srv/share", "/var/lib/libvirt/images" 338 | ], 339 | "tran": null, 340 | "type": "part", 341 | "uuid": "cef58ff1-d736-435c-9c2d-0d32c4e09f97", 342 | "vendor": null, 343 | "wsame": "0B", 344 | "wwn": null, 345 | "zoned": "none", 346 | "zone-sz": "0B", 347 | "zone-wgran": "0B", 348 | "zone-app": "0B", 349 | "zone-nr": 0, 350 | "zone-omax": 0, 351 | "zone-amax": 0 352 | } 353 | ] 354 | } 355 | ] 356 | } 357 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Opinionated Debian Installer 2 | 3 | This tool can be used to create a modern installation of Debian. 4 | Our opinions of what a modern installation of Debian should look like in 2025 are: 5 | 6 | - Debian 13 (Trixie) 7 | - Backports and non-free enabled 8 | - Firmware installed 9 | - Installed on btrfs subvolumes 10 | - Full disk encryption, unlocked by TPM 11 | - Authenticated boot with self-generated Machine Owner Keys 12 | - Fast installation using an image 13 | - Browser-based installer 14 | - One-click installation of a swap file, nvidia drivers or flathub 15 | 16 | ## Limitations 17 | 18 | - **The installer will take over your whole disk** 19 | - Amd64 with UEFI only 20 | - The installer is in English only 21 | 22 | ## Downloads 23 | 24 | | Desktop environment | Date | Size | Download | SHA-256 Checksum | 25 | |---------------------|----------|-------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------| 26 | | KDE Plasma | 20251102 | 5.1GB | [torrent](https://objectstorage.eu-frankfurt-1.oraclecloud.com/n/fr2rf1wke5iq/b/public/o/opinionated-debian-installer-trixie-kde-plasma-20251102a.torrent) / [slow](https://objectstorage.eu-frankfurt-1.oraclecloud.com/n/fr2rf1wke5iq/b/public/o/opinionated-debian-installer-trixie-kde-plasma-20251102a.img) | 717a41ff 2a5c68e0 d7505c07 f6ebf070 efda746d a0f542fb dcae5bfc 76f26364 | 27 | | Gnome | 20251206 | 4.3GB | [torrent](https://objectstorage.eu-frankfurt-1.oraclecloud.com/n/fr2rf1wke5iq/b/public/o/opinionated-debian-installer-trixie-gnome-20251206a.torrent) / [slow](https://objectstorage.eu-frankfurt-1.oraclecloud.com/n/fr2rf1wke5iq/b/public/o/opinionated-debian-installer-trixie-gnome-20251206a.img) | ee877011 88c05ca5 6b90ea3d f29741d9 73865fff 897198e9 3406beb0 dddaffe7 | 28 | | Server | 20251116 | 2.0GB | [torrent](https://objectstorage.eu-frankfurt-1.oraclecloud.com/n/fr2rf1wke5iq/b/public/o/opinionated-debian-installer-trixie-server-20251116a.torrent) / [slow](https://objectstorage.eu-frankfurt-1.oraclecloud.com/n/fr2rf1wke5iq/b/public/o/opinionated-debian-installer-trixie-server-20251116a.img) | d9494702 f50e45cd e3e30c2f d6865566 b4cac031 7da736d8 b3d9a193 175bd4af | 29 | 30 | ## Instructions 31 | 32 | 1. Download one of the live image files from the table above 33 | 2. Write the image file to a USB flash drive. **Do not use ventoy** or similar "clever" tools - they are not compatible with these images. If you need a GUI, use [etcher](https://github.com/balena-io/etcher/releases) or [win32DiskImager](https://sourceforge.net/projects/win32diskimager/files/Archive/) or just use dd - `dd if=opinionated-debian-installer*.img of=/dev/sdX bs=256M oflag=dsync status=progress` where sdX is your USB flash drive 34 | 3. Boot from the USB flash drive 35 | 4. Start the installer icon from the desktop/dash, fill in the form in the browser and press the big _Install_ button 36 | 5. (If you are using the fully authenticated boot mode: Reboot, enroll your MOK and reboot again) 37 | 6. Shutdown, remove the USB drive, boot debian and enjoy! 38 | 39 | ## Screencast & Screenshot 40 | 41 | Screenshot of the full installer GUI: 42 | 43 | ![gui screenshot](readme-files/gui.png) 44 | 45 | Video of installation of Debian with KDE Plasma (Bookworm version): 46 | 47 | [![Watch the video](https://img.youtube.com/vi/sbnKvGMcagI/maxresdefault.jpg)](https://youtu.be/sbnKvGMcagI?si=W9NvZygB8Z7-LCT8&t=92) 48 | 49 | ## FAQ 50 | 51 | **I have started to be asked for disk encryption password. 52 | Can I have my passwordless boot back?** 53 | 54 | You need to re-enroll the TPM to decrypt your drive. 55 | Find the path to the underlying device (with `lsblk` or similar) and use the following command (replacing /dev/vda2 with your device): 56 | 57 | sudo systemd-cryptenroll --tpm2-pcrs=secure-boot-policy+shim-policy \ 58 | --tpm2-device=auto --tpm2-pcrlock= --wipe-slot=tpm2 /dev/vda2 59 | 60 | **The installer is very slow to start up or does not start at all** 61 | 62 | You need fast USB storage. 63 | USB3 is strongly recommended, including any hubs, converters or extension cables you might be using. 64 | On slow storage, some systemd services might time out and the boot of the installer will not be successful. 65 | 66 | **How to set keyboard layout** 67 | 68 | Use the default debian method: 69 | 70 | sudo dpkg-reconfigure keyboard-configuration 71 | sudo setupcon 72 | 73 | ## SecureBoot 74 | 75 | There are two options in regard to SecureBoot: simple or full. 76 | 77 | The **simple mode** will just use shim, systemd-boot and kernel signed by Microsoft and Debian. 78 | Your initrd file will not be signed. 79 | 80 | If you Select the option **Enable MOK-signed UKI** in the installer, the **full mode** will apply. 81 | This is the most secure option. 82 | The installer will generate your Machine Owner Key (MOK) and configure the system to use Unified Kernel Image (UKI) which contains both the kernel and initrd. 83 | The MOK will be used to sign the UKI so that all the files involved in the boot process are authenticated. 84 | 85 | After the installation, on the next boot, you will be asked to enroll your MOK. 86 | Use the password you provided in the installer. 87 | See the screenshots of the process below: 88 |
89 | Screenshots of the MOK enrollment process 90 | 91 | ![mok enroll screenshot 1](readme-files/Screenshot_mok_import_01.png) 92 | ![mok enroll screenshot 2](readme-files/Screenshot_mok_import_02.png) 93 | ![mok enroll screenshot 3](readme-files/Screenshot_mok_import_03.png) 94 | ![mok enroll screenshot 4](readme-files/Screenshot_mok_import_04.png) 95 | ![mok enroll screenshot 5](readme-files/Screenshot_mok_import_05.png) 96 | ![mok enroll screenshot 6](readme-files/Screenshot_mok_import_06.png) 97 | 98 |
99 | 100 | We also recommend re-enrolling the TPM device to decrypt your drive with PCRs 7 (secure-boot-policy) and 14 (shim-policy) after the installation. 101 | Identify your underlying boot device (with `lsblk`) and use the following command (replacing /dev/vda2 with your device): 102 | 103 | sudo systemd-cryptenroll --tpm2-pcrs=secure-boot-policy+shim-policy \ 104 | --tpm2-device=auto --tpm2-pcrlock= --wipe-slot=tpm2 /dev/vda2 105 | 106 | This will prevent auto-decryption of your drive if SecureBoot is disabled or keys are tampered with. 107 | 108 | ## Details 109 | 110 | - GPT disk partitions are created on the designated disk drive: 111 | - UEFI ESP partition 112 | - Root partition - [LUKS](https://cryptsetup-team.pages.debian.net/cryptsetup/README.Debian.html) encrypted (rest of the drive) 113 | - GPT root partition is [auto-discoverable](https://www.freedesktop.org/software/systemd/man/systemd-gpt-auto-generator.html) 114 | - Btrfs subvolumes will be called `@` for `/`, `@home` for `/home` and `@swap` for swap (compatible with [timeshift](https://github.com/teejee2008/timeshift#supported-system-configurations)); the top-level subvolume will be mounted to `/root/btrfs1` 115 | - The system is installed using an image from the live iso. This will speed up the installation significantly and allow off-line installation. 116 | - [Dracut](https://github.com/dracutdevs/dracut/wiki/) is used instead of initramfs-tools 117 | - [Systemd-boot](https://www.freedesktop.org/wiki/Software/systemd/systemd-boot/) is used instead of grub 118 | - [Network-manager](https://wiki.debian.org/NetworkManager) is used for networking 119 | - [Systemd-cryptenroll](https://www.freedesktop.org/software/systemd/man/systemd-cryptenroll.html#--tpm2-device=PATH) is used to unlock the disk, using TPM (if available) 120 | - [Sudo](https://wiki.debian.org/sudo) is installed and configured for the created user 121 | 122 | ## (Optional) Configuration, Automatic Installation 123 | 124 | Edit [installer.ini](installer-files/boot/efi/installer.ini) on the first (vfat) partition of the installer image. 125 | It will allow you to pre-seed and automate the installation. 126 | 127 | If you edit it directly in the booted installer image, it is /boot/efi/installer.ini 128 | Reboot after editing the file for the new values to take effect. 129 | 130 | ## Headless Installation 131 | 132 | You can use the installer for server installation. 133 | 134 | As a start, edit the configuration file installer.ini (see above), set the option BACK_END_IP_ADDRESS to 0.0.0.0 and reboot the installer. 135 | **There is no encryption or authentication in the communication, so only do this on a trusted network.** 136 | 137 | You have several options to access the installer. 138 | Assuming the IP address of the installed machine is 192.168.1.29, and you can reach it from your PC: 139 | 140 | * Use the web interface in a browser on a PC - open `http://192.168.1.29:5000/` 141 | * Use the text mode interface - start `opinionated-installer tui -baseUrl http://192.168.1.29:5000` 142 | * Use curl - again, see the [installer.ini](installer-files/boot/efi/installer.ini) file for a list of all options for the form data in -F parameters: 143 | 144 | curl -v -F "DISK=/dev/vda" -F "USER_PASSWORD=hunter2" \ 145 | -F "ROOT_PASSWORD=changeme" -F "LUKS_PASSWORD=luke" \ 146 | http://192.168.1.29:5000/install 147 | 148 | * Use curl to prompt for logs: 149 | 150 | curl http://192.168.1.29:5000/download_log 151 | 152 | ## Testing 153 | 154 | If you are testing in a virtual machine, attaching the downloaded image file as a virtual disk, you need to extend it first. 155 | The image file that you downloaded is shrunk, there is no free space left in the filesystems. 156 | Use `truncate -s +500M opinionated*.img` to add 500MB to the virtual disk before you attach it to a virtual machine. 157 | The installer will expand the partitions and filesystem to fill the device. 158 | 159 | ### Libvirt 160 | 161 | To test with [libvirt](https://libvirt.org/), make sure to create the VM with UEFI: 162 | 163 | 1. Select the _Customize configuration before install_ option at the end of the new VM dialog 164 | 2. In the VM configuration window, _Overview_ tab, _Hypervisor Details_ section, select _Firmware_: _UEFI_ 165 | 166 | ![virt-manager uefi screenshot](readme-files/virt-manager-uefi.png) 167 | 168 | To add a TPM module, you need to install the [swtpm-tools](https://packages.debian.org/trixie/swtpm-tools) package. 169 | 170 | Attach the downloaded installer image file as _Device type: Disk device_, not ~~CD-ROM device~~. 171 | 172 | ### Hyper-V 173 | 174 | To test with the MS hyper-v virtualization, make sure to create your VM with [Generation 2](https://learn.microsoft.com/en-us/windows-server/virtualization/hyper-v/plan/Should-I-create-a-generation-1-or-2-virtual-machine-in-Hyper-V). 175 | This will enable UEFI. 176 | TPM can be enabled in the Security tab of the Hyper-V settings. 177 | 178 | You will also need to convert the installer image to VHDx format and make the file not sparse. 179 | You can use [qemu-img](https://www.qemu.org/docs/master/tools/qemu-img.html) ([windows download](https://qemu.weilnetz.de/w64/)) and fsutil like this: 180 | 181 | qemu-img convert -f raw -O vhdx opinionated-debian-installer-*.img odin.vhdx 182 | fsutil sparse setflag odin.vhdx 0 183 | 184 | Attach the generated VHDx file as a disk, not as a ~~CD~~. 185 | 186 | ## Hacking 187 | 188 | Alternatively to running the whole browser-based GUI, you can run the `installer.sh` script manually from a root shell. 189 | The end result will be exactly the same. 190 | Just don't forget to edit the configuration options (especially the `DISK` variable) before running it. 191 | 192 | ### Creating Your Own Installer Image 193 | 194 | 1. Insert a blank storage device 195 | 2. Edit the **DISK** and other variables at the top of `make_image.sh` 196 | 3. Execute `make_image.sh` as root 197 | 198 | In the first stage of image generation, you will get a _tasksel_ prompt where you can select a different set of packages for your image. 199 | 200 | ### Installer Image Structure 201 | 202 | There are two GPT partitions on the installer image: EFI boot partition and a Btrfs partition. 203 | The Btrfs filesystem is created in two phases. 204 | 205 | In the first phase, a basic, neutral debian installation is created by debootstrap, tasksel. 206 | At this point, a snapshot called **opinionated_installer_bootstrap** is created. 207 | When installing the target system, the installer will detect the snapshot and copy its contents to the target root subvolume using btrfs send/receive. 208 | 209 | In the second phase, all the installer-specific files are added to the installer Btrfs filesystem. 210 | Obviously, these are not part of the target installed system. 211 | 212 | ### Building the Frontend 213 | 214 | The frontend is a [vue](https://vuejs.org/) application. 215 | You need [npm](https://www.npmjs.com/) to build it. 216 | Run the following commands to build it: 217 | 218 | cd frontend 219 | npm run build 220 | 221 | ### Building the HTTP Backend and the Text-User-Interface Frontend 222 | 223 | The HTTP backend and TUI frontend is a [go](https://go.dev/) application. 224 | Run the following commands to build it: 225 | 226 | cd backend 227 | go build -o opinionated-installer 228 | 229 | ### Configuration Flow 230 | 231 | ```mermaid 232 | flowchart LR 233 | A[installer.ini] -->|EnvironmentFile| B(installer_backend.service) 234 | B -->|ExecStart| C[backend] 235 | D(Web Frontend) --->|HTTP POST| C 236 | E(TUI Frontend) --->|HTTP POST| C 237 | G(curl) --->|HTTP POST| C 238 | C -->|environment| F[installer.sh] 239 | ``` 240 | 241 | ### Output Flow 242 | 243 | ```mermaid 244 | flowchart RL 245 | C[backend] -->|stdout| B(installer_backend.service) 246 | C --->|websocket| D(Web Frontend) 247 | C --->|websocket| E(TUI Frontend) 248 | C --->|HTTP GET| G(curl) 249 | F[installer.sh] -->|stdout| C 250 | ``` 251 | 252 | ## Comparison 253 | 254 | The following table contains a comparison of features between our opinionated debian installer and official debian installers. 255 | 256 | | Feature | ODIN | [Netinstall](https://www.debian.org/CD/netinst/) | [Calamares](https://get.debian.org/debian-cd/current-live/amd64/iso-hybrid/) | 257 | |-----------------------------------------------------|-------|--------------------------------------------------|------------------------------------------------------------------------------| 258 | | Installer internationalization | N | Y | Y | 259 | | Mirror selection, HTTP proxy support | N | Y | N | 260 | | Manual disk partitioning, LVM, filesystem selection | N[4] | Y | Y | 261 | | Btrfs subvolumes | Y[2] | Y[3] | Y[2] | 262 | | Full drive encryption | **Y** | Y[1] | Y | 263 | | Passwordless unlock (TPM) | **Y** | N | N | 264 | | Fully authenticated boot (UKI+MOK) | **Y** | N | N | 265 | | Image-based installation | **Y** | N | N | 266 | | Non-free and backports | **Y** | N | N | 267 | | Browser-based installer | **Y** | N | N | 268 | 269 | [1] `/boot` needs a separate unencrypted partition 270 | 271 | [2] `@` and `@home` ([timeshift](https://github.com/linuxmint/timeshift#supported-system-configurations) compatible) 272 | 273 | [3] `@rootfs` 274 | 275 | [4] Fixed partitioning (see Details above), LUKS is automatic, BTRFS is used as filesystem 276 | 277 | ## Support The Project 278 | 279 | ### Seed The Torrents 280 | 281 | Please set up your torrent client to follow the RSS feed below and seed all new images: 282 | 283 | [feed.xml](https://objectstorage.eu-frankfurt-1.oraclecloud.com/n/fr2rf1wke5iq/b/public/o/feed.xml) 284 | 285 | ### Spread the Word 286 | 287 | Tell your friends about the installer. 288 | If you are active on social media, please share! -------------------------------------------------------------------------------- /make_image.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Opinionated Debian Installer 4 | # Copyright (C) 2022-2025 Robert T. 5 | # 6 | # This program is free software: you can redistribute it and/or modify 7 | # it under the terms of the GNU General Public License as published by 8 | # the Free Software Foundation, either version 3 of the License, or 9 | # (at your option) any later version. 10 | # 11 | # This program is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # GNU General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU General Public License 17 | # along with this program. If not, see . 18 | 19 | set -euo pipefail 20 | 21 | # edit this: 22 | DISK=/dev/vdb 23 | USERNAME=live 24 | DEBIAN_VERSION=trixie 25 | BACKPORTS_VERSION=${DEBIAN_VERSION}-backports 26 | FSFLAGS="compress=zstd:15" 27 | BOOTSTRAP_IMAGE=/var/cache/opinionated-debian-installer/bootstrap.btrfs 28 | 29 | target=/target 30 | SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) 31 | 32 | function notify { 33 | echo -en "\033[32m$*\033[0m> " 34 | read -r 35 | } 36 | 37 | notify install required packages 38 | apt update -y 39 | DEBIAN_FRONTEND=noninteractive apt install -y \ 40 | btrfs-progs \ 41 | debootstrap \ 42 | dosfstools \ 43 | golang-go \ 44 | npm \ 45 | systemd-repart \ 46 | uuid-runtime 47 | 48 | if [ ! -f efi-part.uuid ]; then 49 | echo generate uuid for efi partition 50 | uuidgen > efi-part.uuid 51 | fi 52 | if [ ! -f installer-image-part.uuid ]; then 53 | echo generate uuid for installer image partition 54 | uuidgen > installer-image-part.uuid 55 | fi 56 | efi_uuid=$(cat efi-part.uuid) 57 | installer_image_uuid=$(cat installer-image-part.uuid) 58 | 59 | notify setting up partitions on ${DISK} 60 | mkdir -p /mnt/btrfs1 61 | mkdir -p ${target}/home 62 | rm -rf repart.d 63 | mkdir -p repart.d 64 | 65 | cat < repart.d/01_efi.conf 66 | [Partition] 67 | Type=esp 68 | UUID=${efi_uuid} 69 | SizeMinBytes=300M 70 | SizeMaxBytes=300M 71 | Format=vfat 72 | EOF 73 | 74 | cat < repart.d/02_baseImage.conf 75 | [Partition] 76 | Type=root 77 | Label=Opinionated Debian Installer 78 | UUID=${installer_image_uuid} 79 | SizeMinBytes=200M 80 | Format=btrfs 81 | MakeDirectories=/@ /@swap /@home 82 | Subvolumes=/@ /@swap /@home 83 | GrowFileSystem=on 84 | Encrypt=off 85 | EOF 86 | 87 | if [ ! -f disk_wiped.txt ]; then 88 | wipefs --all ${DISK} 89 | touch disk_wiped.txt 90 | fi 91 | 92 | # sector-size: see https://github.com/systemd/systemd/issues/37801 93 | # remove with systemd 258 94 | systemd-repart --sector-size=512 --empty=allow --no-pager --definitions=repart.d --dry-run=no ${DISK} 95 | 96 | root_device=/dev/disk/by-partuuid/${installer_image_uuid} 97 | efi_device=/dev/disk/by-partuuid/${efi_uuid} 98 | kernel_params="rw quiet root=${root_device} rootfstype=btrfs rootflags=subvol=@ splash" 99 | 100 | if mountpoint -q "/mnt/btrfs1" ; then 101 | echo top-level subvolume already mounted on /mnt/btrfs1 102 | else 103 | notify mount top-level subvolume on /mnt/btrfs1 104 | mkdir -p /mnt/btrfs1 105 | mount "${root_device}" /mnt/btrfs1 -o ${FSFLAGS},subvolid=5 106 | fi 107 | 108 | if [ -f $BOOTSTRAP_IMAGE ]; then 109 | notify receiving bootstrap data from $BOOTSTRAP_IMAGE 110 | btrfs receive -f $BOOTSTRAP_IMAGE /mnt/btrfs1/ 111 | (cd /mnt/btrfs1; btrfs subvolume delete @; btrfs subvolume snapshot opinionated_installer_bootstrap @; btrfs subvolume delete opinionated_installer_bootstrap) 112 | fi 113 | 114 | if mountpoint -q "${target}" ; then 115 | echo root subvolume already mounted on ${target} 116 | else 117 | notify mount root and home subvolume on ${target} 118 | mkdir -p ${target} 119 | mount "${root_device}" ${target} -o ${FSFLAGS},subvol=@ 120 | mkdir -p ${target}/home 121 | mount "${root_device}" ${target}/home -o ${FSFLAGS},subvol=@home 122 | fi 123 | 124 | mkdir -p ${target}/var/cache/apt/archives 125 | if mountpoint -q "${target}/var/cache/apt/archives" ; then 126 | echo apt cache directory already bind mounted on target 127 | else 128 | notify bind mounting apt cache directory to target 129 | mount /var/cache/apt/archives ${target}/var/cache/apt/archives -o bind 130 | fi 131 | 132 | if [ ! -f ${target}/etc/debian_version ]; then 133 | notify install debian on ${target} 134 | debootstrap ${DEBIAN_VERSION} ${target} http://deb.debian.org/debian 135 | fi 136 | 137 | if mountpoint -q "${target}/proc" ; then 138 | echo bind mounts already set up on ${target} 139 | else 140 | notify bind mount dev, proc, sys, run, var/tmp on ${target} 141 | mount -t proc none ${target}/proc 142 | mount --make-rslave --rbind /sys ${target}/sys 143 | mount --make-rslave --rbind /dev ${target}/dev 144 | mount --make-rslave --rbind /run ${target}/run 145 | mount --make-rslave --rbind /var/tmp ${target}/var/tmp 146 | mount -t tmpfs tmpfs ${target}/boot 147 | fi 148 | 149 | notify setup sources list 150 | rm -f ${target}/etc/apt/sources.list 151 | mkdir -p ${target}/etc/apt/sources.list.d 152 | cat < ${target}/etc/apt/sources.list.d/debian.sources 153 | Types: deb 154 | URIs: http://deb.debian.org/debian/ 155 | Suites: ${DEBIAN_VERSION} 156 | Components: main contrib non-free non-free-firmware 157 | Signed-By: /usr/share/keyrings/debian-archive-keyring.gpg 158 | 159 | Types: deb 160 | URIs: http://deb.debian.org/debian/ 161 | Suites: ${DEBIAN_VERSION}-updates 162 | Components: main contrib non-free non-free-firmware 163 | Signed-By: /usr/share/keyrings/debian-archive-keyring.gpg 164 | 165 | Types: deb 166 | URIs: http://security.debian.org/debian-security/ 167 | Suites: ${DEBIAN_VERSION}-security 168 | Components: main contrib non-free non-free-firmware 169 | Signed-By: /usr/share/keyrings/debian-archive-keyring.gpg 170 | EOF 171 | 172 | cat < ${target}/etc/apt/sources.list.d/debian-backports.sources 173 | Types: deb 174 | URIs: http://deb.debian.org/debian/ 175 | Suites: ${DEBIAN_VERSION}-backports 176 | Components: main contrib non-free non-free-firmware 177 | Signed-By: /usr/share/keyrings/debian-archive-keyring.gpg 178 | EOF 179 | 180 | notify enable 32bit 181 | chroot ${target}/ dpkg --add-architecture i386 182 | 183 | notify "preconfigure locales (ignore warnings in this step)" 184 | echo "locales locales/locales_to_be_generated multiselect en_US.UTF-8 UTF-8" | chroot ${target}/ debconf-set-selections 185 | 186 | notify install required packages on ${target} 187 | cat < ${target}/tmp/packages.txt 188 | btrfsmaintenance 189 | locales 190 | adduser 191 | passwd 192 | sudo 193 | tasksel 194 | network-manager 195 | binutils 196 | console-setup 197 | exim4-daemon-light 198 | kpartx 199 | mokutil 200 | pigz 201 | pkg-config 202 | EOF 203 | cat < ${target}/tmp/packages_backports.txt 204 | systemd 205 | systemd-cryptsetup 206 | systemd-timesyncd 207 | systemd-ukify 208 | sbsigntool 209 | btrfs-progs 210 | dosfstools 211 | firmware-linux 212 | atmel-firmware 213 | bluez-firmware 214 | dahdi-firmware-nonfree 215 | firmware-amd-graphics 216 | firmware-ath9k-htc 217 | firmware-atheros 218 | firmware-bnx2 219 | firmware-bnx2x 220 | firmware-brcm80211 221 | firmware-carl9170 222 | firmware-cavium 223 | firmware-intel-misc 224 | firmware-intel-sound 225 | firmware-iwlwifi 226 | firmware-libertas 227 | firmware-misc-nonfree 228 | firmware-myricom 229 | firmware-netronome 230 | firmware-netxen 231 | firmware-qcom-soc 232 | firmware-qlogic 233 | firmware-realtek 234 | firmware-ti-connectivity 235 | firmware-zd1211 236 | cryptsetup 237 | lvm2 238 | mdadm 239 | plymouth-themes 240 | polkitd 241 | tpm2-tools 242 | tpm-udev 243 | EOF 244 | cat < ${target}/tmp/run2.sh 245 | #!/bin/bash 246 | set -euo pipefail 247 | 248 | export DEBIAN_FRONTEND=noninteractive 249 | apt update 250 | xargs apt install -y < /tmp/packages.txt 251 | xargs apt install -t ${BACKPORTS_VERSION} -y < /tmp/packages_backports.txt 252 | EOF 253 | chroot ${target}/ bash /tmp/run2.sh 254 | 255 | notify running tasksel 256 | chroot ${target}/ tasksel 257 | 258 | if mountpoint -q "${target}/var/cache/apt/archives" ; then 259 | notify unmounting apt cache directory from target 260 | umount ${target}/var/cache/apt/archives 261 | else 262 | echo apt cache directory not mounted to target 263 | fi 264 | 265 | notify downloading remaining .deb files for the installer 266 | cat < ${target}/tmp/run3.sh 267 | #!/bin/bash 268 | set -e 269 | 270 | export DEBIAN_FRONTEND=noninteractive 271 | apt install -y --download-only locales tasksel openssh-server flatpak 272 | apt install -t ${BACKPORTS_VERSION} -y --download-only systemd-boot systemd-boot-efi-signed dracut linux-image-amd64 popularity-contest 273 | if (dpkg --get-selections | grep -w install |grep -qs "task.*desktop"); then 274 | # libelf1t64:i386 - XXX workaround 2025-09-13 275 | apt install -t ${BACKPORTS_VERSION} -y --download-only linux-headers-amd64 nvidia-driver nvidia-driver-libs:i386 libelf1t64:i386 276 | fi 277 | if (dpkg --get-selections | grep -w install |grep -qs "task-kde-desktop"); then 278 | apt install -y --download-only plasma-discover-backend-flatpak 279 | fi 280 | if (dpkg --get-selections | grep -w install |grep -qs "task-gnome-desktop"); then 281 | apt install -y --download-only gnome-software-plugin-flatpak 282 | fi 283 | EOF 284 | chroot ${target}/ bash /tmp/run3.sh 285 | 286 | notify cleaning up 287 | chroot ${target}/ apt autoremove -y 288 | rm -f ${target}/etc/machine-id 289 | rm -f ${target}/etc/crypttab 290 | rm -f ${target}/var/log/*log 291 | rm -f ${target}/var/log/apt/*log 292 | 293 | if [ ! -f first_phase_done.txt ]; then 294 | notify create snapshot after first phase 295 | (cd /mnt/btrfs1; btrfs subvolume snapshot -r @ opinionated_installer_bootstrap) 296 | mkdir -p $(dirname $BOOTSTRAP_IMAGE) 297 | if [ ! -f $BOOTSTRAP_IMAGE ]; then 298 | notify storing bootstrap data to $BOOTSTRAP_IMAGE 299 | btrfs send --compressed-data /mnt/btrfs1/opinionated_installer_bootstrap > $BOOTSTRAP_IMAGE 300 | fi 301 | touch first_phase_done.txt 302 | fi 303 | 304 | function install_file() { 305 | echo "Copying $1 to ${target}" 306 | rm -rf "${target:?}/$1" 307 | cp -r "${SCRIPT_DIR}/installer-files/$1" "${target}/$1" 308 | } 309 | 310 | if mountpoint -q "${target}/var/cache/apt/archives" ; then 311 | echo apt cache directory already bind mounted on target 312 | else 313 | notify bind mounting apt cache directory to target 314 | mount /var/cache/apt/archives ${target}/var/cache/apt/archives -o bind 315 | fi 316 | 317 | if mountpoint -q "${target}/boot/efi" ; then 318 | echo efi esp partition ${efi_device} already mounted on ${target}/boot/efi 319 | else 320 | notify mount efi esp partition ${efi_device} on ${target}/boot/efi 321 | mkdir -p ${target}/boot/efi 322 | mount "${efi_device}" ${target}/boot/efi -o umask=077 323 | fi 324 | 325 | notify setup fstab 326 | mkdir -p ${target}/root/btrfs1 327 | cat < ${target}/etc/fstab 328 | PARTUUID=${installer_image_uuid} / btrfs defaults,subvol=@,x-systemd.growfs 0 1 329 | PARTUUID=${installer_image_uuid} /home btrfs defaults,subvol=@home 0 1 330 | PARTUUID=${installer_image_uuid} /root/btrfs1 btrfs defaults,subvolid=5 0 1 331 | PARTUUID=${efi_uuid} /boot/efi vfat defaults,umask=077 0 2 332 | EOF 333 | 334 | # TODO use systemd-firstboot 335 | 336 | if grep -qs 'root:\$' ${target}/etc/shadow ; then 337 | echo root password already set up 338 | else 339 | notify set up root password 340 | echo "root:live" > ${target}/tmp/passwd 341 | chroot ${target}/ bash -c "chpasswd < /tmp/passwd" 342 | rm ${target}/tmp/passwd 343 | fi 344 | 345 | if grep -qs "^${USERNAME}:" ${target}/etc/shadow ; then 346 | echo ${USERNAME} user already set up 347 | else 348 | notify set up ${USERNAME} user 349 | chroot ${target}/ useradd -m ${USERNAME} -s /bin/bash -G sudo 350 | echo "${USERNAME}:live" > ${target}/tmp/passwd 351 | chroot ${target}/ bash -c "chpasswd < /tmp/passwd" 352 | rm ${target}/tmp/passwd 353 | fi 354 | 355 | notify setup systemd-repart 356 | mkdir -p ${target}/etc/repart.d 357 | install_file etc/repart.d/01-installer.conf 358 | 359 | # place the icon in the apps menu 360 | mkdir -p ${target}/usr/share/applications 361 | install_file usr/share/applications/installer.desktop 362 | # kde - place the icon on the desktop 363 | mkdir -p ${target}/home/live/Desktop 364 | install_file home/live/Desktop/installer.desktop 365 | # kde - customize the welcome center 366 | mkdir -p ${target}/home/live/.config 367 | install_file home/live/.config/plasma-welcomerc 368 | mkdir -p ${target}/usr/share/pixmaps 369 | install_file usr/share/pixmaps/Ceratopsian_installer.svg 370 | mkdir -p ${target}/usr/share/plasma/plasma-welcome 371 | install_file usr/share/plasma/plasma-welcome/intro-customization.desktop 372 | # gnome - place the icon to the 'dash' 373 | if [ -f ${target}/usr/bin/dconf ]; then 374 | mkdir -p ${target}/etc/dconf/profile 375 | install_file etc/dconf/profile/user 376 | mkdir -p ${target}/etc/dconf/db/local.d 377 | install_file etc/dconf/db/local.d/01-favorite-apps 378 | echo running dconf update 379 | chroot ${target}/ dconf update 380 | else 381 | echo dconf not installed, skipping 382 | fi 383 | chown -R 1000:1000 ${target}/home/live 384 | 385 | notify configuring dracut 386 | mkdir -p ${target}/etc/dracut.conf.d 387 | cat < ${target}/etc/dracut.conf.d/90-odin.conf 388 | add_dracutmodules+=" systemd " 389 | omit_dracutmodules+=" lvm dmraid mdraid " 390 | kernel_cmdline="${kernel_params}" 391 | use_fstab="yes" 392 | add_fstab+=" /etc/fstab " 393 | hostonly="no" 394 | EOF 395 | cat < ${target}/etc/kernel/cmdline 396 | ${kernel_params} 397 | EOF 398 | 399 | notify install required installer packages on ${target} 400 | mkdir -p ${target}/etc/systemd/system 401 | cat < ${target}/tmp/run1.sh 402 | #!/bin/bash 403 | set -euo pipefail 404 | 405 | export DEBIAN_FRONTEND=noninteractive 406 | apt update -y 407 | apt upgrade -y 408 | apt install -y debootstrap uuid-runtime pv 409 | apt install -y -t ${BACKPORTS_VERSION} curl systemd-boot systemd-boot-efi-amd64-signed shim-signed systemd-repart dracut cryptsetup nvidia-detect 410 | apt purge initramfs-tools initramfs-tools-core initramfs-tools-bin busybox klibc-utils libklibc -y 411 | systemctl enable NetworkManager.service 412 | systemctl disable systemd-networkd.service # seems to fight with NetworkManager 413 | systemctl disable systemd-networkd.socket 414 | systemctl disable systemd-networkd-wait-online.service 415 | systemctl mask systemd-networkd-wait-online.service 416 | systemctl disable apt-daily-upgrade.timer 417 | systemctl disable apt-daily.timer 418 | EOF 419 | chroot ${target}/ bash /tmp/run1.sh 420 | 421 | notify install kernel on ${target} 422 | cat < ${target}/tmp/run1.sh 423 | #!/bin/bash 424 | set -euo pipefail 425 | 426 | # see https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=1095646 427 | ln -s /dev/null /etc/kernel/install.d/50-dracut.install 428 | 429 | export DEBIAN_FRONTEND=noninteractive 430 | apt -t ${BACKPORTS_VERSION} install linux-image-amd64 -y 431 | EOF 432 | chroot ${target}/ bash /tmp/run1.sh 433 | 434 | echo configuring autologin 435 | mkdir -p ${target}/etc/sddm.conf.d/ 436 | install_file etc/sddm.conf.d/autologin.conf 437 | mkdir -p ${target}/etc/gdm3 438 | install_file etc/gdm3/daemon.conf 439 | mkdir -p ${target}/etc/lightdm/lightdm.conf.d 440 | install_file etc/lightdm/lightdm.conf.d/10-autologin.conf 441 | 442 | notify cleaning up 443 | chroot ${target}/ apt autoremove -y 444 | rm -f ${target}/etc/machine-id 445 | rm -f ${target}/etc/crypttab 446 | rm -f ${target}/var/log/*log 447 | rm -f ${target}/var/log/apt/*log 448 | 449 | notify building the frontend 450 | (cd "${SCRIPT_DIR}/frontend" && npm install && npm run build) 451 | mkdir -p "${SCRIPT_DIR}/installer-files/var/www/html/opinionated-debian-installer" 452 | cp -r ${SCRIPT_DIR}/frontend/dist/* "${SCRIPT_DIR}/installer-files/var/www/html/opinionated-debian-installer" 453 | 454 | notify copying the opinionated debian installer to ${target} 455 | cp "${SCRIPT_DIR}/installer.sh" "${target}/" 456 | chmod +x ${target}/installer.sh 457 | mkdir -p ${target}/var/www/html 458 | install_file var/www/html/opinionated-debian-installer 459 | install_file etc/systemd/system/installer_backend.service 460 | install_file boot/efi/installer.ini 461 | chroot ${target}/ systemctl enable installer_backend 462 | 463 | (cd "${SCRIPT_DIR}/backend" && CGO_ENABLED=0 go build -v -ldflags="-s -w" -o opinionated-installer) 464 | 465 | notify installing tui frontend 466 | cp "${SCRIPT_DIR}/backend/opinionated-installer" "${target}/sbin/opinionated-installer" 467 | chmod +x ${target}/sbin/opinionated-installer 468 | install_file etc/systemd/system/installer_tui.service 469 | cat < ${target}/tmp/run1.sh 470 | #!/bin/bash 471 | set -euo pipefail 472 | 473 | if systemctl is-enabled display-manager.service ; then 474 | echo "A login manager enabled in systemd, disabling installer TUI frontend" 475 | systemctl disable installer_tui.service 476 | # we need to remove the file because systemd.preset would re-enable the unit 477 | rm /etc/systemd/system/installer_tui.service 478 | else 479 | echo "No login manager enabled in systemd, enabling installer TUI frontend" 480 | systemctl enable installer_tui.service 481 | fi 482 | EOF 483 | chroot ${target}/ bash /tmp/run1.sh 484 | rm -f ${target}/tmp/run1.sh 485 | 486 | notify note the filesystem usage 487 | df -h ${target} 488 | btrfs filesystem df ${target} 489 | df -h ${target}/boot/efi 490 | 491 | notify umounting the installer filesystem 492 | sync 493 | umount -R ${target} 494 | umount -R /mnt/btrfs1 495 | 496 | echo "INSTALLATION FINISHED" 497 | -------------------------------------------------------------------------------- /installer.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Opinionated Debian Installer 4 | # Copyright (C) 2022-2025 Robert T. 5 | # 6 | # This program is free software: you can redistribute it and/or modify 7 | # it under the terms of the GNU General Public License as published by 8 | # the Free Software Foundation, either version 3 of the License, or 9 | # (at your option) any later version. 10 | # 11 | # This program is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # GNU General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU General Public License 17 | # along with this program. If not, see . 18 | 19 | set -eo pipefail 20 | 21 | if [ -z "${NON_INTERACTIVE}" ]; then 22 | # edit this: 23 | DISK=/dev/vda 24 | USERNAME=user 25 | USER_FULL_NAME="Debian User" 26 | USER_PASSWORD=hunter2 27 | ROOT_PASSWORD=changeme 28 | DISABLE_LUKS=false 29 | LUKS_PASSWORD=luke 30 | ENABLE_MOK_SIGNED_UKI=true 31 | MOK_ENROLL_PASSWORD=mokka 32 | ENABLE_TPM=true 33 | HOSTNAME=debian13 34 | SWAP_SIZE=2 35 | NVIDIA_PACKAGE= 36 | ENABLE_POPCON=false 37 | ENABLE_FLATHUB=true 38 | LOCALE=C.UTF-8 39 | TIMEZONE=Europe/Bratislava 40 | SSH_PUBLIC_KEY= 41 | AFTER_INSTALLED_CMD= 42 | 43 | echo -e "\033[32m Opinionated Debian Installer \033[0m" 44 | echo Press Enter at green prompts 45 | fi 46 | 47 | function notify () { 48 | if [ -z "${NON_INTERACTIVE}" ]; then 49 | echo -en "\033[32m$*\033[0m> " 50 | read -r 51 | else 52 | echo "$*" 53 | fi 54 | } 55 | 56 | DEBIAN_VERSION=trixie 57 | BACKPORTS_VERSION=${DEBIAN_VERSION}-backports 58 | # do not enable this on a live-cd 59 | SHARE_APT_ARCHIVE=false 60 | FSFLAGS="compress=zstd:1" 61 | DEBIAN_FRONTEND=noninteractive 62 | export DEBIAN_FRONTEND 63 | 64 | if [ "$(id -u)" -ne 0 ]; then 65 | echo 'This script must be run by root' >&2 66 | exit 1 67 | fi 68 | 69 | if [ -z "${DISK}" ]; then 70 | echo "DISK variable is missing" >&2 71 | exit 2 72 | fi 73 | 74 | if [ "${DISABLE_LUKS}" != "true" ]; then 75 | if [ -z "${LUKS_PASSWORD}" ]; then 76 | echo "LUKS_PASSWORD variable is missing" >&2 77 | exit 3 78 | fi 79 | fi 80 | 81 | if [ -z "${NON_INTERACTIVE}" ]; then 82 | notify install required packages 83 | apt update -y 84 | apt install -y cryptsetup debootstrap uuid-runtime btrfs-progs dosfstools pv systemd-repart mokutil tpm2-tools 85 | fi 86 | 87 | KEYFILE=luks.key 88 | if [ ! -f ${KEYFILE} ]; then 89 | dd if=/dev/random of=${KEYFILE} bs=512 count=1 90 | chmod 600 ${KEYFILE} 91 | fi 92 | if [ ! -f efi-part.uuid ]; then 93 | uuidgen > efi-part.uuid 94 | fi 95 | if [ ! -f main-part.uuid ]; then 96 | uuidgen > main-part.uuid 97 | fi 98 | 99 | efi_part_uuid=$(cat efi-part.uuid) 100 | main_part_uuid=$(cat main-part.uuid) 101 | efi_partition=/dev/disk/by-partuuid/${efi_part_uuid} 102 | main_partition=/dev/disk/by-partuuid/${main_part_uuid} 103 | top_level_mount=/mnt/top_level_mount 104 | target=/target 105 | kernel_params="rw quiet rootfstype=btrfs rootflags=${FSFLAGS},subvol=@ rd.auto=1 splash" 106 | if [ "${DISABLE_LUKS}" != "true" ]; then 107 | kernel_params="rd.luks.options=tpm2-device=auto ${kernel_params}" 108 | luks_device_name=root 109 | root_device=/dev/mapper/${luks_device_name} 110 | else 111 | root_device=${main_partition} 112 | fi 113 | 114 | notify setting up partitions on ${DISK} 115 | rm -rf repart.d 116 | mkdir -p repart.d 117 | cat < repart.d/01_efi.conf 118 | [Partition] 119 | Type=esp 120 | UUID=${efi_part_uuid} 121 | SizeMinBytes=1024M 122 | SizeMaxBytes=1024M 123 | Format=vfat 124 | EOF 125 | 126 | cat < repart.d/02_root.conf 127 | [Partition] 128 | Type=root 129 | Label=Debian 130 | UUID=${main_part_uuid} 131 | Format=btrfs 132 | MakeDirectories=/@home 133 | Subvolumes=/@home 134 | EOF 135 | 136 | if [ "${DISABLE_LUKS}" == "true" ]; then 137 | echo "Encrypt=off" >> repart.d/02_root.conf 138 | elif [ "${ENABLE_TPM}" == "true" ]; then 139 | echo "Encrypt=key-file+tpm2" >> repart.d/02_root.conf 140 | else 141 | echo "Encrypt=key-file" >> repart.d/02_root.conf 142 | fi 143 | 144 | if [ ! -f disk_wiped.txt ]; then 145 | wipefs --all ${DISK} 146 | touch disk_wiped.txt 147 | fi 148 | 149 | # sector-size: see https://github.com/systemd/systemd/issues/37801 150 | # remove with systemd 258 151 | # tpm2-pcrs= If we are enrolling MOK, PCRs would reset anyway. If SB is disabled, we want to allow enabling it. 152 | # tpm2-pcrlock= XXX: wtf is pcrlock? 153 | systemd-repart --sector-size=512 --empty=allow --no-pager --definitions=repart.d --dry-run=no ${DISK} \ 154 | --key-file=${KEYFILE} --tpm2-device=auto --tpm2-pcrs= --tpm2-pcrlock= 155 | 156 | function wait_for_file { 157 | filename="$1" 158 | while [ ! -e $filename ] 159 | do 160 | echo waiting for $filename to be created 161 | sleep 3 162 | done 163 | } 164 | 165 | wait_for_file ${main_partition} 166 | 167 | if [ "${DISABLE_LUKS}" != "true" ]; then 168 | notify setup luks password on ${main_partition} 169 | echo -n "${LUKS_PASSWORD}" > /tmp/passwd 170 | cryptsetup --key-file=luks.key luksAddKey "${main_partition}" /tmp/passwd 171 | rm -f /tmp/passwd 172 | cryptsetup luksUUID "${main_partition}" > luks.uuid 173 | root_uuid=$(cat luks.uuid) 174 | if [ ! -e ${root_device} ]; then 175 | notify open luks on root 176 | cryptsetup luksOpen ${main_partition} ${luks_device_name} --key-file $KEYFILE 177 | fi 178 | fi 179 | 180 | btrfs_uuid=$(lsblk -no UUID ${root_device}) 181 | 182 | if mountpoint -q "${top_level_mount}" ; then 183 | echo top-level subvolume already mounted on ${top_level_mount} 184 | else 185 | notify mount top-level subvolume on ${top_level_mount} 186 | mkdir -p ${top_level_mount} 187 | mount ${root_device} ${top_level_mount} -o rw,${FSFLAGS},subvolid=5,skip_balance 188 | fi 189 | 190 | if [ -e /root/btrfs1/opinionated_installer_bootstrap ]; then 191 | if [ ! -f base_image_copied.txt ]; then 192 | notify send installer bootrstrap data - see nr of bytes transferred 193 | btrfs send --compressed-data /root/btrfs1/opinionated_installer_bootstrap | pv -nb | btrfs receive ${top_level_mount} 194 | (cd ${top_level_mount}; btrfs subvolume snapshot opinionated_installer_bootstrap @; btrfs subvolume delete opinionated_installer_bootstrap) 195 | touch base_image_copied.txt 196 | fi 197 | elif [ ! -e ${top_level_mount}/@ ]; then 198 | notify create @ subvolume on ${top_level_mount} 199 | btrfs subvolume create ${top_level_mount}/@ 200 | else 201 | notify the @ subvolume already created 202 | fi 203 | 204 | if [ ! -e ${top_level_mount}/@swap ]; then 205 | if [ ${SWAP_SIZE} -gt 0 ]; then 206 | notify create @swap subvolume for swap file on ${top_level_mount} 207 | btrfs subvolume create ${top_level_mount}/@swap 208 | chmod 700 ${top_level_mount}/@swap 209 | fi 210 | fi 211 | 212 | if mountpoint -q "${target}" ; then 213 | echo root subvolume already mounted on ${target} 214 | else 215 | notify mount root and home subvolume on ${target} 216 | mkdir -p ${target} 217 | mount ${root_device} ${target} -o ${FSFLAGS},subvol=@ 218 | mkdir -p ${target}/home 219 | mount ${root_device} ${target}/home -o ${FSFLAGS},subvol=@home 220 | if [ ${SWAP_SIZE} -gt 0 ]; then 221 | notify mount swap subvolume on ${target} 222 | mkdir -p ${target}/swap 223 | mount ${root_device} ${target}/swap -o noatime,subvol=@swap 224 | fi 225 | fi 226 | 227 | if [ ${SWAP_SIZE} -gt 0 ]; then 228 | if [ ! -e ${target}/swap/swapfile ]; then 229 | notify make swap file at ${target}/swap/swapfile 230 | btrfs filesystem mkswapfile --size ${SWAP_SIZE}G ${target}/swap/swapfile 231 | fi 232 | if ! grep -qs "${target}/swap/swapfile" /proc/swaps ; then 233 | notify enable swap file ${target}/swap/swapfile 234 | swapon ${target}/swap/swapfile 235 | fi 236 | swapfile_offset=$(btrfs inspect-internal map-swapfile -r ${target}/swap/swapfile) 237 | kernel_params="${kernel_params} resume=${root_device} resume_offset=${swapfile_offset}" 238 | fi 239 | 240 | if [ ! -f ${target}/etc/debian_version ]; then 241 | notify install debian on ${target} 242 | debootstrap ${DEBIAN_VERSION} ${target} http://deb.debian.org/debian 243 | fi 244 | 245 | if mountpoint -q "${target}/proc" ; then 246 | echo bind mounts already set up on ${target} 247 | else 248 | notify bind mount dev, proc, sys, run on ${target} 249 | mount -t proc none ${target}/proc 250 | mount --make-rslave --rbind /sys ${target}/sys 251 | mount --make-rslave --rbind /dev ${target}/dev 252 | mount --make-rslave --rbind /run ${target}/run 253 | mount --bind /etc/resolv.conf ${target}/etc/resolv.conf 254 | fi 255 | 256 | if mountpoint -q "${target}/boot/efi" ; then 257 | echo efi esp partition ${efi_partition} already mounted on ${target}/boot/efi 258 | else 259 | notify mount esp partition ${efi_partition} on ${target}/boot/efi 260 | mkdir -p ${target}/boot/efi 261 | mount ${efi_partition} ${target}/boot/efi -o umask=077 262 | fi 263 | 264 | notify setup locale, timezone, hostname, root password, kernel command line 265 | systemd-firstboot --root=${target} --locale=${LOCALE} --keymap=us --timezone=${TIMEZONE} \ 266 | --hostname=${HOSTNAME} --root-password=${ROOT_PASSWORD} --kernel-command-line="${kernel_params}" \ 267 | --force 268 | echo "127.0.1.1 ${HOSTNAME}" >> ${target}/etc/hosts 269 | echo "locales locales/locales_to_be_generated multiselect en_US.UTF-8 UTF-8" | chroot ${target}/ debconf-set-selections 270 | 271 | notify setup fstab 272 | mkdir -p ${target}/root/btrfs1 273 | cat < ${target}/etc/fstab 274 | UUID=${btrfs_uuid} / btrfs defaults,subvol=@,${FSFLAGS} 0 1 275 | UUID=${btrfs_uuid} /home btrfs defaults,subvol=@home,${FSFLAGS} 0 1 276 | UUID=${btrfs_uuid} /root/btrfs1 btrfs defaults,subvolid=5,${FSFLAGS} 0 1 277 | PARTUUID=${efi_part_uuid} /boot/efi vfat defaults,umask=077 0 2 278 | EOF 279 | 280 | if [ ${SWAP_SIZE} -gt 0 ]; then 281 | cat <> ${target}/etc/fstab 282 | UUID=${btrfs_uuid} /swap btrfs defaults,subvol=@swap,noatime,${FSFLAGS} 0 0 283 | /swap/swapfile none swap defaults 0 0 284 | EOF 285 | fi 286 | 287 | notify setup sources list 288 | rm -f ${target}/etc/apt/sources.list 289 | mkdir -p ${target}/etc/apt/sources.list.d 290 | cat < ${target}/etc/apt/sources.list.d/debian.sources 291 | Types: deb 292 | URIs: http://deb.debian.org/debian/ 293 | Suites: ${DEBIAN_VERSION} 294 | Components: main contrib non-free non-free-firmware 295 | Signed-By: /usr/share/keyrings/debian-archive-keyring.gpg 296 | 297 | Types: deb 298 | URIs: http://deb.debian.org/debian/ 299 | Suites: ${DEBIAN_VERSION}-updates 300 | Components: main contrib non-free non-free-firmware 301 | Signed-By: /usr/share/keyrings/debian-archive-keyring.gpg 302 | 303 | Types: deb 304 | URIs: http://security.debian.org/debian-security/ 305 | Suites: ${DEBIAN_VERSION}-security 306 | Components: main contrib non-free non-free-firmware 307 | Signed-By: /usr/share/keyrings/debian-archive-keyring.gpg 308 | EOF 309 | 310 | cat < ${target}/etc/apt/sources.list.d/debian-backports.sources 311 | Types: deb 312 | URIs: http://deb.debian.org/debian/ 313 | Suites: ${DEBIAN_VERSION}-backports 314 | Components: main contrib non-free non-free-firmware 315 | Signed-By: /usr/share/keyrings/debian-archive-keyring.gpg 316 | EOF 317 | 318 | if [ "$SHARE_APT_ARCHIVE" = true ] ; then 319 | mkdir -p ${target}/var/cache/apt/archives 320 | if mountpoint -q "${target}/var/cache/apt/archives" ; then 321 | echo apt cache directory already bind mounted on target 322 | else 323 | notify bind mounting apt cache directory to target 324 | mount /var/cache/apt/archives ${target}/var/cache/apt/archives -o bind 325 | fi 326 | fi 327 | 328 | notify enable 32bit 329 | chroot ${target}/ dpkg --add-architecture i386 330 | 331 | if [ ! -z "${USERNAME}" ]; then 332 | if grep -qs "^${USERNAME}:" ${target}/etc/shadow ; then 333 | echo ${USERNAME} user already set up 334 | else 335 | notify set up ${USERNAME} user 336 | chroot ${target}/ bash -c "adduser ${USERNAME} --disabled-password --gecos "${USER_FULL_NAME}"" 337 | chroot ${target}/ bash -c "adduser ${USERNAME} sudo" 338 | if [ ! -z "${USER_PASSWORD}" ]; then 339 | echo "${USERNAME}:${USER_PASSWORD}" > ${target}/tmp/passwd 340 | chroot ${target}/ bash -c "chpasswd < /tmp/passwd" 341 | rm -f ${target}/tmp/passwd 342 | fi 343 | fi 344 | fi 345 | 346 | if [ ! -z "${NVIDIA_PACKAGE}" ]; then 347 | # TODO the debian page says to do this instead: 348 | # echo "options nvidia-drm modeset=1" >> /etc/modprobe.d/nvidia-options.conf 349 | kernel_params="${kernel_params} nvidia-drm.modeset=1" 350 | fi 351 | 352 | notify configuring dracut and kernel command line 353 | mkdir -p ${target}/etc/dracut.conf.d 354 | cat < ${target}/etc/dracut.conf.d/89-btrfs.conf 355 | add_dracutmodules+=" systemd btrfs " 356 | EOF 357 | if [ "${DISABLE_LUKS}" != "true" ]; then 358 | cat < ${target}/etc/dracut.conf.d/90-luks.conf 359 | add_dracutmodules+=" crypt tpm2-tss " 360 | EOF 361 | fi 362 | 363 | if [ "${ENABLE_MOK_SIGNED_UKI}" == "true" ]; then 364 | cat < ${target}/etc/kernel/install.conf 365 | layout=uki 366 | uki_generator=ukify 367 | initrd_generator=dracut 368 | EOF 369 | cat < ${target}/etc/kernel/uki.conf 370 | [UKI] 371 | Cmdline=@/etc/kernel/cmdline 372 | SecureBootCertificate=/etc/kernel/mok.cert.pem 373 | SecureBootPrivateKey=/etc/kernel/mok.priv.pem 374 | EOF 375 | fi # ENABLE_MOK_SIGNED_UKI 376 | 377 | notify install required packages on ${target} 378 | if [ -z "${NON_INTERACTIVE}" ]; then 379 | chroot ${target}/ apt update -y 380 | fi 381 | cat < ${target}/tmp/run1.sh 382 | #!/bin/bash 383 | export DEBIAN_FRONTEND=noninteractive 384 | apt install -y locales tasksel network-manager sudo 385 | apt install -y -t ${BACKPORTS_VERSION} systemd shim-signed systemd-boot systemd-boot-efi-amd64-signed systemd-ukify sbsigntool dracut btrfs-progs cryptsetup tpm2-tools tpm-udev 386 | 387 | # see https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=1095646 388 | ln -s /dev/null /etc/kernel/install.d/50-dracut.install 389 | # XXX this didn't seem to work 390 | EOF 391 | chroot ${target}/ sh /tmp/run1.sh 392 | 393 | if [ "${ENABLE_MOK_SIGNED_UKI}" == "true" ]; then 394 | mokutil "--generate-hash=${MOK_ENROLL_PASSWORD}" > ${target}/tmp/mok.key 395 | cat < ${target}/tmp/run1.sh 396 | #!/bin/bash 397 | # generate cert and key in pem format in /etc/kernel/mok.*.pem 398 | ukify genkey --config /etc/kernel/uki.conf 399 | 400 | # convert to der format 401 | openssl x509 -in /etc/kernel/mok.cert.pem -out /etc/kernel/mok.cert.der -outform der 402 | openssl rsa -in /etc/kernel/mok.priv.pem -out /etc/kernel/mok.priv.der -outform der 403 | 404 | # symlink for DKMS 405 | mkdir -p /var/lib/dkms 406 | ln -s /etc/kernel/mok.priv.pem /var/lib/dkms/mok.key 407 | ln -s /etc/kernel/mok.cert.der /var/lib/dkms/mok.pub 408 | 409 | # symlink in "ubuntu" de-facto standard directory 410 | mkdir -p /var/lib/shim-signed/mok 411 | ln -s /etc/kernel/mok.cert.der /var/lib/shim-signed/mok/MOK-Kernel.der 412 | ln -s /etc/kernel/mok.cert.pem /var/lib/shim-signed/mok/MOK-Kernel.pem 413 | ln -s /etc/kernel/mok.priv.der /var/lib/shim-signed/mok/MOK-Kernel.priv 414 | 415 | # XXX: Failed to get Subject Key ID 416 | mokutil --import /etc/kernel/mok.cert.der --hash-file /tmp/mok.key 417 | EOF 418 | chroot ${target}/ sh /tmp/run1.sh 419 | rm -f ${target}/tmp/mok.key 420 | fi # ENABLE_MOK_SIGNED_UKI 421 | 422 | notify install kernel and firmware on ${target} 423 | cat < ${target}/tmp/packages.txt 424 | btrfsmaintenance 425 | locales 426 | adduser 427 | passwd 428 | sudo 429 | tasksel 430 | network-manager 431 | binutils 432 | console-setup 433 | exim4-daemon-light 434 | kpartx 435 | pigz 436 | pkg-config 437 | EOF 438 | cat < ${target}/tmp/packages_backports.txt 439 | linux-image-amd64 440 | systemd 441 | systemd-cryptsetup 442 | systemd-timesyncd 443 | btrfs-progs 444 | dosfstools 445 | firmware-linux 446 | atmel-firmware 447 | bluez-firmware 448 | dahdi-firmware-nonfree 449 | firmware-amd-graphics 450 | firmware-ath9k-htc 451 | firmware-atheros 452 | firmware-bnx2 453 | firmware-bnx2x 454 | firmware-brcm80211 455 | firmware-carl9170 456 | firmware-cavium 457 | firmware-intel-misc 458 | firmware-intel-sound 459 | firmware-iwlwifi 460 | firmware-libertas 461 | firmware-misc-nonfree 462 | firmware-myricom 463 | firmware-netronome 464 | firmware-netxen 465 | firmware-qcom-soc 466 | firmware-qlogic 467 | firmware-realtek 468 | firmware-ti-connectivity 469 | firmware-zd1211 470 | cryptsetup 471 | dracut 472 | lvm2 473 | mdadm 474 | plymouth-themes 475 | polkitd 476 | tpm2-tools 477 | tpm-udev 478 | EOF 479 | cat < ${target}/tmp/run2.sh 480 | #!/bin/bash 481 | set -euo pipefail 482 | 483 | export DEBIAN_FRONTEND=noninteractive 484 | xargs apt install -y < /tmp/packages.txt 485 | apt install -t ${BACKPORTS_VERSION} -y dracut initramfs-tools- initramfs-tools-core- initramfs-tools-bin- \ 486 | busybox- klibc-utils- libklibc- 487 | xargs apt install -t ${BACKPORTS_VERSION} -y < /tmp/packages_backports.txt 488 | systemctl disable systemd-networkd.service # seems to fight with NetworkManager 489 | systemctl disable systemd-networkd.socket 490 | systemctl disable systemd-networkd-wait-online.service 491 | EOF 492 | chroot ${target}/ bash /tmp/run2.sh 493 | 494 | if [ "$ENABLE_POPCON" = true ] ; then 495 | notify enabling popularity-contest 496 | cat < ${target}/tmp/run3.sh 497 | #!/bin/bash 498 | set -euo pipefail 499 | 500 | export DEBIAN_FRONTEND=noninteractive 501 | echo "popularity-contest popularity-contest/participate boolean true" | debconf-set-selections 502 | apt install -y popularity-contest 503 | EOF 504 | chroot ${target}/ bash /tmp/run3.sh 505 | fi 506 | 507 | if [ ! -z "${SSH_PUBLIC_KEY}" ]; then 508 | notify adding ssh public key to user and root authorized_keys file 509 | mkdir -p ${target}/root/.ssh 510 | chmod 700 ${target}/root/.ssh 511 | echo "${SSH_PUBLIC_KEY}" > ${target}/root/.ssh/authorized_keys 512 | chmod 600 ${target}/root/.ssh/authorized_keys 513 | 514 | if [ ! -z "${USERNAME}" ]; then 515 | mkdir -p ${target}/home/${USERNAME}/.ssh 516 | chmod 700 ${target}/home/${USERNAME}/.ssh 517 | echo "${SSH_PUBLIC_KEY}" > ${target}/home/${USERNAME}/.ssh/authorized_keys 518 | chmod 600 ${target}/home/${USERNAME}/.ssh/authorized_keys 519 | chroot ${target}/ chown -R ${USERNAME} /home/${USERNAME}/.ssh 520 | fi 521 | 522 | notify installing openssh-server 523 | chroot ${target}/ apt install -y openssh-server 524 | fi 525 | 526 | if [ -z "${NON_INTERACTIVE}" ]; then 527 | notify running tasksel 528 | # XXX this does not open for some reason 529 | chroot ${target}/ tasksel 530 | fi 531 | 532 | if [ "${ENABLE_FLATHUB}" = true ] ; then 533 | notify enabling flatpak and flathub 534 | cat < ${target}/tmp/run4.sh 535 | #!/bin/bash 536 | set -e 537 | 538 | export DEBIAN_FRONTEND=noninteractive 539 | apt install -y flatpak 540 | flatpak remote-add --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo 541 | if (dpkg --get-selections | grep -w install |grep -qs "task-kde-desktop"); then 542 | apt install -y plasma-discover-backend-flatpak 543 | fi 544 | if (dpkg --get-selections | grep -w install |grep -qs "task-gnome-desktop"); then 545 | apt install -y gnome-software-plugin-flatpak 546 | fi 547 | EOF 548 | chroot ${target}/ bash /tmp/run4.sh 549 | rm ${target}/tmp/run4.sh 550 | fi 551 | 552 | if [ ! -z "${NVIDIA_PACKAGE}" ]; then 553 | notify installing ${NVIDIA_PACKAGE} 554 | # XXX dracut-install: ERROR: installing nvidia-blacklists-nouveau.conf nvidia.conf 555 | cat < ${target}/etc/dracut.conf.d/10-nvidia.conf 556 | install_items+=" /etc/modprobe.d/nvidia-blacklists-nouveau.conf /etc/modprobe.d/nvidia.conf /etc/modprobe.d/nvidia-options.conf " 557 | EOF 558 | chroot ${target}/ apt install -t ${BACKPORTS_VERSION} -y "${NVIDIA_PACKAGE}" nvidia-driver-libs:i386 linux-headers-amd64 559 | fi 560 | 561 | notify cleaning up 562 | chroot ${target}/ apt autoremove -y 563 | 564 | notify umounting all filesystems 565 | if [ ${SWAP_SIZE} -gt 0 ]; then 566 | swapoff ${target}/swap/swapfile 567 | fi 568 | umount -R ${target} 569 | umount -R ${top_level_mount} 570 | 571 | if [ "${DISABLE_LUKS}" != "true" ]; then 572 | notify closing luks 573 | cryptsetup luksClose ${luks_device_name} || true 574 | fi 575 | 576 | notify INSTALLATION FINISHED 577 | 578 | if [ ! -z "${AFTER_INSTALLED_CMD}" ]; then 579 | notify running ${AFTER_INSTALLED_CMD} 580 | sh -c "${AFTER_INSTALLED_CMD}" 581 | fi 582 | -------------------------------------------------------------------------------- /frontend/src/App.vue: -------------------------------------------------------------------------------- 1 | 18 | 275 | 452 | 453 | 551 | -------------------------------------------------------------------------------- /frontend/src/assets/Ceratopsian_installer.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 18 | 38 | 43 | 44 | 46 | 49 | 53 | 57 | 58 | 61 | 65 | 69 | 70 | 73 | 77 | 81 | 82 | 85 | 89 | 93 | 94 | 97 | 101 | 105 | 106 | 117 | 128 | 139 | 150 | 161 | 167 | 168 | 170 | 177 | 183 | 189 | 195 | 201 | 207 | 208 | 213 | 217 | 223 | 229 | 235 | 241 | 247 | 253 | 259 | 265 | 271 | 277 | 283 | 289 | 290 | 291 | 295 | 299 | 303 | 307 | 311 | 315 | 316 | --------------------------------------------------------------------------------