diff --git a/.gitignore b/.gitignore index 213602f..db879a2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,4 @@ -config/ +/config/ web/ux/ test* .DS_Store diff --git a/deploy/GUI/web/ux/buttons.md b/deploy/GUI/web/ux/buttons.md deleted file mode 100644 index f26c0bd..0000000 --- a/deploy/GUI/web/ux/buttons.md +++ /dev/null @@ -1,22 +0,0 @@ -# Buttons - -1. Button with : -- an offset ring on focus -- a small shadow -- a nice transition -- a disabled feature - -```html -px-10 py-3 bg-fuchsia-600 rounded-xl font-bold shadow-lg shadow-fuchsia-600/30 transition duration-200 hover:bg-fuchsia-500 focus:outline-none focus:ring-2 focus:ring-fuchsia-500 focus:ring-offset-2 focus:ring-offset-slate-700 disabled:opacity-50 disabled:cursor-not-allowed -``` - -2. Button with : -- a grow transformation on hover -- a gradient -- a small shadow -- a nice transition -- a disabled feature - -```html -px-10 py-3 text-white font-bold rounded-xl scale-100 transition duration-200 ease-in bg-gradient-to-r from-fuchsia-500 via-fuchsia-600 to-fuchsia-700 hover:scale-105 hover:bg-gradient-to-br shadow-lg shadow-fuchsia-500/30 dark:shadow-lg dark:shadow-fuchsia-800/80 disabled:opacity-50 disabled:cursor-not-allowed -``` \ No newline at end of file diff --git a/deploy/GUI/web/ux/color_palette.md b/deploy/GUI/web/ux/color_palette.md deleted file mode 100644 index bf8b63d..0000000 --- a/deploy/GUI/web/ux/color_palette.md +++ /dev/null @@ -1,10 +0,0 @@ -# Color palette - -|Usage|Name|Tailwind code|Matching border| -|-|-|-|-| -|Body background|Slate|slate-900|N/A| -|Cards Background|Slate|slate-800|slate-700| -|Accent|Fuchsia|fuchsia-600|fuchsia-600 -|Warning|Amber|amber-500|amber-400| -|Success|Emerald|emerald-500|emerald-400| -|Failure|Rose|rose-600|rose-500| \ No newline at end of file diff --git a/deploy/GUI/web/ux/css_guidelines.md b/deploy/GUI/web/ux/css_guidelines.md deleted file mode 100644 index da251a5..0000000 --- a/deploy/GUI/web/ux/css_guidelines.md +++ /dev/null @@ -1,8 +0,0 @@ -# Guidelines - -|Type|Importance|Classes| -|-|-|-| -|Title|1|text-4xl font-bold text-sky-400| -|Title|2|text-4xl font-bold text-sky-400| -|Title|3|text-4xl font-bold text-sky-400| -|Title|4|text-4xl font-bold text-sky-400| \ No newline at end of file diff --git a/deploy/GUI/web/ux/text.md b/deploy/GUI/web/ux/text.md deleted file mode 100644 index 3175b65..0000000 --- a/deploy/GUI/web/ux/text.md +++ /dev/null @@ -1,43 +0,0 @@ -# Taglines, subtitles and descriptions - -## Global - -**Tagline :** Reclaim Your Digital Independence. - -## Devices - -### Numbus Server -The Infrastructure - -**Tagline :** Reclaim Your Cloud. - -**Subtitle :** Professional-grade hosting, strictly kept under your roof. - -**Detailed Description :** The Numbus Server is the heartbeat of your digital sovereignty. Built on an immutable NixOS foundation, it provides a rock-solid environment to host the services you rely on—from Nextcloud and Passbolt to Home Assistant and Frigate. Whether you are a small business looking to internalize your data or a power user securing your smart home, the Numbus Server delivers containerized efficiency and VM flexibility without the "Big Tech" tax. It’s not just a server; it’s your private corner of the internet, accessible from anywhere but controlled only by you. - -### Numbus Backup Server -The Guardian - -**Tagline :** Your Digital Safety Net. - -**Subtitle :** Automated, high-efficiency protection for your entire ecosystem. - -**Detailed Description :** Data loss and downtime aren't just frustrating; they're disastrous. The Numbus Backup Server is your ecosystem's ultimate guardian. It works silently in the background, automatically pulling high-efficiency backups from your Numbus devices the moment they start charging. But it does more than just store files—it actively acts as a watchdog. By constantly monitoring your Numbus Server and other critical network devices, it immediately alerts you the second something goes offline. It is automated peace of mind for your entire digital infrastructure. - -### Numbus Computer -The Workstation - -**Tagline :** The No-Bullshit Workstation. - -**Subtitle :** A modern, privacy-respecting machine built for work, creation, and play — without the corporate bloat. - -**Description :** Experience computing the way it was meant to be: entirely under your control. The Numbus Computer is a powerful workstation that strips away unwanted telemetry, forced updates, and bundled adware. Under the hood, it harnesses the unparalleled stability of NixOS, but we've hidden the complexity behind a sleek, intuitive interface. Our default apps make managing drivers, software, and system options a breeze. Whether you are coding, browsing, or gaming via Proton, the Numbus Computer delivers a snappy, secure, and truly "libre" desktop experience that respects your privacy while delivering the raw power of the hardware you paid for. - -### Numbus TV -The Experience - -**Tagline :** Entertainment, Liberated. - -**Subtitle :** A premium cinematic experience free from trackers and forced subscriptions. - -**Detailed Description :** The Numbus TV reclaims your living room. Unlike "Smart" TVs that have essentially become surveillance machines and clutter your screen with unwanted ads, the Numbus TV offers a clean, classy interface powered by KDE Bigscreen. It’s a full-power PC in a TV’s body, allowing you to stream, browse, and play on your own terms. It doesn't force you into a specific ecosystem; it simply provides a beautiful, privacy-hardened portal to your favorite media. No spying, no "walled gardens"—just the big screen experience, perfected. \ No newline at end of file diff --git a/flake.nix b/flake.nix index ad30c1b..d1a4772 100644 --- a/flake.nix +++ b/flake.nix @@ -36,11 +36,12 @@ }; in { nixosModules = { - common = ./modules/common; - server = ./modules/server; - backup = ./modules/backup; - computer = ./modules/computer; - tv = ./modules/tv; + common = ./modules/common/default.nix; + server = ./modules/server/default.nix; + backup = ./modules/backup/default.nix; + console = ./modules/console/default.nix; + computer = ./modules/computer/default.nix; + tv = ./modules/tv/default.nix; }; nixosConfigurations = { @@ -63,6 +64,12 @@ deviceType = "computer"; }; + numbus-console = mkNumbus { + deviceModule = self.nixosModules.console; + nixpkgsRef = inputs.nixpkgs; + deviceType = "console"; + }; + numbus-tv = mkNumbus { deviceModule = self.nixosModules.tv; nixpkgsRef = inputs.nixpkgs; diff --git a/modules/backup/hardware/boot.nix b/modules/backup/hardware/boot.nix deleted file mode 100644 index 86de227..0000000 --- a/modules/backup/hardware/boot.nix +++ /dev/null @@ -1,13 +0,0 @@ -{ config, ... }: - -{ - config = { - boot.initrd.systemd.enable = true; - boot.loader.systemd-boot.enable = true; - boot.loader.efi.canTouchEfiVariables = true; - - boot.kernel.sysctl = { - "vm.overcommit_memory" = 1; - }; - }; -} \ No newline at end of file diff --git a/modules/backup/hardware/cpu.nix b/modules/backup/hardware/cpu.nix deleted file mode 100644 index d3fe470..0000000 --- a/modules/backup/hardware/cpu.nix +++ /dev/null @@ -1,9 +0,0 @@ -{ config, ... }: - -{ - config = { - hardware.enableRedistributableFirmware = true; - hardware.cpu.intel.updateMicrocode = true; - hardware.cpu.amd.updateMicrocode = true; - }; -} \ No newline at end of file diff --git a/modules/backup/hardware/default.nix b/modules/backup/hardware/default.nix deleted file mode 100644 index 8a652d9..0000000 --- a/modules/backup/hardware/default.nix +++ /dev/null @@ -1,10 +0,0 @@ -{ ... }: - -{ - imports = [ - ./boot.nix - ./cpu.nix - ./disks.nix - ./pcie-coral.nix - ]; -} \ No newline at end of file diff --git a/modules/backup/hardware/disks.nix b/modules/backup/hardware/disks.nix deleted file mode 100644 index 6e1acee..0000000 --- a/modules/backup/hardware/disks.nix +++ /dev/null @@ -1,328 +0,0 @@ -{ config, lib, pkgs, ... }: - -with lib; - -let - cfg = config.numbus.hardware; - - mkDataDisk = idx: device: { - name = "content-${toString idx}"; - value = { - type = "disk"; - device = device; - content = { - type = "gpt"; - partitions = { - luks = { - size = "100%"; - content = { - type = "luks"; - name = "crypted-content-${toString idx}"; - initrdUnlock = false; - settings = { - keyFile = "/etc/secrets/disks/content-${toString idx}"; - allowDiscards = true; - crypttabExtraOpts = [ "nofail" ]; - }; - content = { - type = "filesystem"; - format = cfg.dataDisksFilesystem; - mountpoint = "/mnt/content-${toString idx}"; - mountOptions = [ "nofail" "noauto" ]; - }; - }; - }; - }; - }; - }; - }; - - mkParityDisk = idx: device: { - name = "parity-${toString idx}"; - value = { - type = "disk"; - device = device; - content = { - type = "gpt"; - partitions = { - luks = { - size = "100%"; - content = { - type = "luks"; - name = "crypted-parity-${toString idx}"; - initrdUnlock = false; - settings = { - keyFile = "/etc/secrets/disks/parity-${toString idx}"; - allowDiscards = true; - crypttabExtraOpts = [ "nofail" ]; - }; - content = { - type = "filesystem"; - format = cfg.parityDisksFilesystem; - mountpoint = "/mnt/parity-${toString idx}"; - mountOptions = [ "nofail" "noauto" ]; - }; - }; - }; - }; - }; - }; - }; - - isMirror = length cfg.bootDisksList > 1; - - bootDisksConfig = if isMirror then { - mdadm = { - boot = { - type = "mdadm"; - level = 1; - metadata = "1.2"; - content = { - type = "filesystem"; - format = "vfat"; - mountpoint = "/boot"; - mountOptions = [ "umask=0077" ]; - }; - }; - }; - disk = listToAttrs (imap1 (i: device: { - name = "boot-${toString i}"; - value = { - type = "disk"; - device = device; - content = { - type = "gpt"; - partitions = { - ESP = { - size = "1G"; - type = "EF00"; - content = { - type = "mdraid"; - name = "boot"; - }; - }; - luks = { - size = "100%"; - content = { - type = "luks"; - name = "crypted-boot-${toString i}"; - settings = { - allowDiscards = true; - keyFile = "/etc/secrets/disks/boot-${toString i}"; - }; - content = { - type = "lvm_pv"; - vg = "pool"; - }; - }; - }; - }; - }; - }; - }) cfg.bootDisksList); - } else { - disk = { - "boot-1" = { - type = "disk"; - device = head cfg.bootDisksList; - content = { - type = "gpt"; - partitions = { - ESP = { - size = "1G"; - type = "EF00"; - content = { - type = "filesystem"; - format = "vfat"; - mountpoint = "/boot"; - mountOptions = [ "umask=0077" ]; - }; - }; - luks = { - size = "100%"; - content = { - type = "luks"; - name = "crypted-boot-1"; - settings = { - keyFile = "/etc/secrets/disks/boot-1"; - allowDiscards = true; - }; - content = { - type = "lvm_pv"; - vg = "pool"; - }; - }; - }; - }; - }; - }; - }; - }; - - lvmConfig = { - lvm_vg = { - pool = { - type = "lvm_vg"; - lvs = { - swap = { - size = cfg.swapSize; - content = { - type = "swap"; - }; - } // optionalAttrs isMirror { lvm_type = "mirror"; }; - snapraid = { - size = "1G"; - content = { - type = "filesystem"; - format = "btrfs"; - mountpoint = "/mnt/content-0"; - }; - } // optionalAttrs isMirror { lvm_type = "mirror"; }; - root = { - size = "100%"; - content = { - type = "btrfs"; - extraArgs = [ "-f" ]; - subvolumes = { - "/rootfs" = { - mountpoint = "/"; - mountOptions = [ "compress=zstd" "noatime" ]; - }; - "/home" = { - mountpoint = "/home"; - mountOptions = [ "compress=zstd" ]; - }; - "/nix" = { - mountpoint = "/nix"; - mountOptions = [ "compress=zstd" "noatime" ]; - }; - }; - }; - } // optionalAttrs isMirror { lvm_type = "mirror"; }; - }; - }; - }; - }; - -in -{ - options.numbus.hardware = { - dataDisksList = mkOption { - type = types.listOf types.str; - default = []; - example = [ "/dev/disk/by-id/WD_Blue_ATO431_159Ejz224G0000382b" "/dev/disk/by-id/Seagate_Barracuda_159Ejz224G" ]; - description = "List by-id path of devices for data disks"; - }; - parityDisksList = mkOption { - type = types.listOf types.str; - default = []; - example = [ "/dev/disk/by-id/WD_Blue_ATO431_159Ejz224G0000382b" "/dev/disk/by-id/Seagate_Barracuda_159Ejz224G" ]; - description = "List of by-id path of devices for parity disks"; - }; - bootDisksList = mkOption { - type = types.listOf types.str; - default = []; - example = [ "/dev/disk/by-id/nvme_SAMSUNG_MZVPYEHCO_159Ejz224G0000" "/dev/disk/by-id/ata-San_Disk_159Ejz224G" ]; - description = "List of by-id path of devices for boot disks"; - }; - spindownDisksList = mkOption { - type = types.listOf types.str; - default = []; - example = [ "/dev/disk/by-id/WD_Blue_ATO431_159Ejz224G0000382b" "/dev/disk/by-id/Seagate_Barracuda_159Ejz224G" ]; - description = "List of by-id path of devices to spindown when inactive to save power (HDD only)"; - }; - swapSize = mkOption { - type = types.str; - default = "16G"; - example = "16G"; - description = "Size of the swap partition"; - }; - dataDisksFilesystem = mkOption { - type = types.enum [ "xfs" "ext4" "btrfs" ]; - default = "xfs"; - example = "xfs"; - description = "Filesystem for data disks. Available filesystem options : xfs, ext4, btrfs"; - }; - parityDisksFilesystem = mkOption { - type = types.enum [ "xfs" "ext4" "btrfs" ]; - default = "xfs"; - example = "xfs"; - description = "Filesystem for parity disks. Available filesystem options : xfs, ext4, btrfs"; - }; - }; - - config = mkIf (cfg.bootDisksList != []) { - disko.devices = mkMerge [ - bootDisksConfig - lvmConfig - { - disk = listToAttrs (imap1 mkDataDisk cfg.dataDisksList); - } - { - disk = listToAttrs (imap1 mkParityDisk cfg.parityDisksList); - } - ]; - - services.snapraid = { - enable = true; - contentFiles = [ "/mnt/content-0/snapraid.content" ] ++ - (map (i: "/mnt/content-${toString i}/snapraid.content") (range 1 (length cfg.dataDisksList))); - parityFiles = map (i: "/mnt/parity-${toString i}/snapraid.parity") (range 1 (length cfg.parityDisksList)); - dataDisks = listToAttrs (imap1 (i: _: nameValuePair "d${toString i}" "/mnt/content-${toString i}") cfg.dataDisksList); - }; - - fileSystems."/mnt/data" = { - device = "/mnt/content-*"; - fsType = "fuse.mergerfs"; - options = [ - "category.create=ff" - "cache.files=partial" - "dropcacheonclose=true" - "defaults" - "noauto" - "nofail" - "allow_other" - "moveonenospc=1" - "minfreespace=50G" - "func.getattr=newest" - "fsname=mergerfs_data" - "x-mount.mkdir" - "x-systemd.automount" - ]; - }; - - systemd.services.mount-disks = { - description = "Mount data and parity disks"; - before = [ "mnt-data.mount" ]; - requiredBy = [ "mnt-data.mount" ]; - path = [ pkgs.cryptsetup pkgs.util-linux ]; - serviceConfig = { - Type = "oneshot"; - RemainAfterExit = true; - }; - script = let - mountDataDisk = i: '' - if [ ! -e /dev/mapper/crypted-content-${toString i} ]; then - cryptsetup luksOpen --key-file /etc/secrets/disks/content-${toString i} /dev/disk/by-partlabel/disk-content-${toString i}-luks crypted-content-${toString i} - fi - mkdir -p /mnt/content-${toString i} - if ! mountpoint -q /mnt/content-${toString i}; then - mount -t ${cfg.dataDisksFilesystem} /dev/mapper/crypted-content-${toString i} /mnt/content-${toString i} - fi - ''; - mountParityDisk = i: '' - if [ ! -e /dev/mapper/crypted-parity-${toString i} ]; then - cryptsetup luksOpen --key-file /etc/secrets/disks/parity-${toString i} /dev/disk/by-partlabel/disk-parity-${toString i}-luks crypted-parity-${toString i} - fi - mkdir -p /mnt/parity-${toString i} - if ! mountpoint -q /mnt/parity-${toString i}; then - mount -t ${cfg.parityDisksFilesystem} /dev/mapper/crypted-parity-${toString i} /mnt/parity-${toString i} - fi - ''; - in '' - ${concatMapStrings mountDataDisk (range 1 (length cfg.dataDisksList))} - ${concatMapStrings mountParityDisk (range 1 (length cfg.parityDisksList))} - ''; - }; - }; -} \ No newline at end of file diff --git a/modules/backup/hardware/spindown.nix b/modules/backup/hardware/spindown.nix deleted file mode 100644 index 219d113..0000000 --- a/modules/backup/hardware/spindown.nix +++ /dev/null @@ -1,24 +0,0 @@ -{ config, lib, ... }: - -with lib; - -{ - options.numbus-backup = { - hardware = { - HddSpindown = { - enable = mkOption { - description = "Spin down Hard drives when inactive in order to save power."; - type = types.bool; - example = true; - default = true; - }; - optimize = mkOption { - description = "Optimize services to reduce HDD wakeups when HddSpindown is enabled. Can be set to \"compatible\" to optimize all compatible services, or a list of service names to optimize."; - type = types.nullOr (types.either (types.enum [ "compatible" ]) (types.listOf types.str)); - default = "compatible"; - example = "[ \"crafty\" \"gitea\" ]"; - }; - }; - }; - }; -} \ No newline at end of file diff --git a/modules/backup/mail/clamav.nix b/modules/backup/mail/clamav.nix deleted file mode 100644 index a4c99bc..0000000 --- a/modules/backup/mail/clamav.nix +++ /dev/null @@ -1,89 +0,0 @@ -{ config, lib, pkgs, ... }: - -with lib; - -let - cfg = config.numbus.services.clamav; - clamav_notifier = pkgs.writeScript "clamav-notify.sh" '' - #!${pkgs.bash}/bin/bash - - # Check if triggered by Real-time event (file exists) - if [ -f /var/lib/clamav/virus_event.env ]; then - source /var/lib/clamav/virus_event.env - rm /var/lib/clamav/virus_event.env - fi - - ADMIN_EMAIL="${config.numbus.mail.adminAddress}" - USER_EMAIL="${config.numbus.mail.userAddress}" - OWNER_NAME="${config.numbus.owner}" - - if [ -n "$CLAM_VIRUSEVENT_VIRUSNAME" ]; then - # --- Real-time / VirusEvent Mode --- - SUBJECT="Numbus Server Alert: Virus Detected (Real-time)" - - # Retrieve logs from clamav-daemon - LOGS=$(journalctl -u clamav-daemon.service -n 50 --no-pager | grep "FOUND") - - TECH_BODY=" - ClamAV Real-time Alert: - Server owner: $OWNER_NAME - - Virus detected: $CLAM_VIRUSEVENT_VIRUSNAME - File: $CLAM_VIRUSEVENT_FILENAME - - Logs: - $LOGS - - Action taken: Access blocked (OnAccessPrevention). - Please investigate manually. - " - - FRIENDLY_BODY="Cher/Chère $OWNER_NAME, - - L'antivirus de votre serveur a détecté et bloqué une menace en temps réel. - Fichier : $CLAM_VIRUSEVENT_FILENAME - - Votre administrateur a été notifié. - " - else - # --- Scheduled Scan Summary Mode --- - SUBJECT="Numbus Server Alert: Virus Detected during Scheduled Scan" - - # Retrieve logs (clamdscan prints FOUND when a virus is detected) - LOGS=$(journalctl -u clamav-periodic-scan.service -n 100 --no-pager | grep "FOUND") - - TECH_BODY=" - ClamAV Scan Alert: - Server owner: $OWNER_NAME - - Viruses detected: - $LOGS - - Action taken: Detection only. - Please investigate manually. - " - - FRIENDLY_BODY="Cher/Chère $OWNER_NAME, - - L'antivirus de votre serveur a détecté une menace potentielle lors de l'analyse périodique. - Votre administrateur a été notifié avec les détails techniques. - Nous vous conseillons d'être prudent avec vos fichiers récents. - " - fi - - printf "Subject: [ADMIN] %s\n\n%s" "$SUBJECT" "$TECH_BODY" | /run/wrappers/bin/sendmail -t "$ADMIN_EMAIL" - printf "Subject: [Alerte] Menace détectée sur votre serveur Numbus\n\n%s\n\nMerci de votre confiance,\nL'équipe de support,\nNumbus-Server." "$FRIENDLY_BODY" | /run/wrappers/bin/sendmail -t "$USER_EMAIL" - ''; -in - -{ - config = mkIf cfg.enable { - systemd.services.clamav-virus-notify = { - description = "Email notification for ClamAV virus detection"; - serviceConfig = { - Type = "oneshot"; - ExecStart = "${clamav_notifier}"; - }; - }; - }; -} \ No newline at end of file diff --git a/modules/backup/mail/default.nix b/modules/backup/mail/default.nix deleted file mode 100644 index a7af913..0000000 --- a/modules/backup/mail/default.nix +++ /dev/null @@ -1,10 +0,0 @@ -{ ... }: - -{ - imports = [ - ./clamav.nix - ./smart.nix - ./systemd.nix - ./smtp.nix - ]; -} \ No newline at end of file diff --git a/modules/backup/mail/smart.nix b/modules/backup/mail/smart.nix deleted file mode 100644 index c84e0ab..0000000 --- a/modules/backup/mail/smart.nix +++ /dev/null @@ -1,61 +0,0 @@ -{ config, pkgs, ... }: - -let - smartd_notifier = pkgs.writeScript "smartd-notify.sh" '' - #!${pkgs.bash}/bin/bash - - # 1. Send Technical Email to Admin - ADMIN_EMAIL="${config.numbus.mail.adminAddress}" - SUBJECT="Numbus Server Alert: $SMARTD_FAILTYPE on $SMARTD_DEVICE" - - TECH_BODY=" - SMARTD Alert Details: - Server owner: $OWNER_NAME - Device: $SMARTD_DEVICE - Type: $SMARTD_DEVICETYPE - Failure Type: $SMARTD_FAILTYPE - Message: $SMARTD_MESSAGE - - Full Message: - $SMARTD_FULLMESSAGE - " - printf "Subject: [ADMIN] $SUBJECT\n\n$TECH_BODY" | /run/wrappers/bin/sendmail -t "$ADMIN_EMAIL" - - # 2. Send Friendly Email to Owner - USER_EMAIL="${config.numbus.mail.userAddress}" - OWNER_NAME="${config.numbus.owner}" - - FRIENDLY_BODY="Cher/Chère $OWNER_NAME, - - Votre serveur a automatiquement détecté une panne matérielle de disque dur. - Ce genre de panne est tout à fait normal selon l'âge de votre matériel et n'entraîne - dans la grande majorité des cas aucune perte de données grâce au système de - stockage redondant préventif. - - Votre administrateur a été notifié de cette panne. Il vous recontactera dans de très - brefs délais afin de procéder au remplacement, si nécessaire, du disque dur défaillant. - - Merci de votre confiance, - L'équipe de support, - Numbus-Server." - - printf "Subject: [Alerte] Défaillance matérielle sur votre serveur Numbus\n\n$FRIENDLY_BODY" | /run/wrappers/bin/sendmail -t "$USER_EMAIL" - ''; -in - -{ - services.smartd = { - enable = true; - defaults.autodetected = "-a -o on -S on -s (S/../.././00|L/../../6/01) -n standby,q -M exec ${smartd_notifier}"; - notifications = { - wall = { - enable = true; - }; - mail = { - enable = true; - sender = config.numbus.mail.fromAddress; - recipient = "${config.numbus.mail.userAddress},${config.numbus.mail.adminAddress}"; - }; - }; - }; -} \ No newline at end of file diff --git a/modules/backup/mail/smtp.nix b/modules/backup/mail/smtp.nix deleted file mode 100644 index 55b84f5..0000000 --- a/modules/backup/mail/smtp.nix +++ /dev/null @@ -1,84 +0,0 @@ -{ config, pkgs, lib, ... }: - -with lib; - -let - cfg = config.numbus.mail; -in - -{ - options.numbus.mail = { - enable = mkEnableOption "Email sending functionality"; - - userAddress = mkOption { - description = "The address of the user this server will send emails to"; - type = types.str; - example = "user@your-domain.com"; - }; - - adminAddress = mkOption { - description = "The address of the admin this server will send emails to"; - type = types.str; - example = "admin@your-domain.com"; - }; - - smtpUsername = mkOption { - description = "The username/email that will be use to authenticate to the SMTP server"; - type = types.str; - example = "your-smtp-enabled-address@your-domain.com"; - }; - - smtpPasswordPath = mkOption { - description = "The path to a file containing the password that will be use to authenticate to the SMTP server"; - type = types.path; - example = /run/secrets/smtp-password; - }; - - fromAddress = mkOption { - description = "This server will send emails from this address"; - type = types.str; - default = "numbus-server-noreply@${config.numbus.services.domain}"; - example = "numbus-server-noreply@your-domain.com"; - }; - - smtpServer = mkOption { - description = "The SMTP server address your server will use to send emails"; - type = types.str; - default = "smtp.gmail.com"; - example = "smtp.your-provider.com"; - }; - - smtpPort = mkOption { - description = "The SMTP port your server will connect to to send emails"; - type = types.port; - default = 587; - example = 587; - }; - }; - - config = mkIf cfg.enable { - environment.etc."aliases".text = '' - root: ${cfg.userAddress}, ${cfg.adminAddress} - default: ${cfg.userAddress}, ${cfg.adminAddress} - ''; - - programs.msmtp = { - enable = true; - defaults = { - aliases = "/etc/aliases"; - timeout = 60; - syslog = "on"; - }; - accounts.default = { - auth = true; - host = cfg.smtpServer; - port = cfg.smtpPort; - from = cfg.fromAddress; - user = cfg.smtpUsername; - tls = true; - tls_starttls = true; - passwordeval = "${pkgs.coreutils}/bin/cat ${cfg.smtpPasswordPath}"; - }; - }; - }; -} \ No newline at end of file diff --git a/modules/backup/mail/systemd.nix b/modules/backup/mail/systemd.nix deleted file mode 100644 index 0b7a566..0000000 --- a/modules/backup/mail/systemd.nix +++ /dev/null @@ -1,55 +0,0 @@ -{ config, pkgs, ... }: - -let - systemd_notifier = pkgs.writeScript "systemd-email-notify.sh" '' - #!${pkgs.bash}/bin/bash - - # The failing service name is passed as the first argument - UNIT=$1 - - # 1. Send Technical Email to Admin - ADMIN_EMAIL="${config.numbus.mail.adminAddress}" - SUBJECT="Numbus Server Alert: Service $UNIT Failed" - - # Retrieve recent logs for context - LOGS=$(journalctl -u "$UNIT" -n 20 --no-pager) - - TECH_BODY=" - Systemd Service Failure Alert: - Server owner: ${config.numbus.owner} - Service: $UNIT - - Recent Logs: - $LOGS - " - printf "Subject: [ADMIN] $SUBJECT\n\n$TECH_BODY" | /run/wrappers/bin/sendmail -t "$ADMIN_EMAIL" - - # 2. Send Friendly Email to Owner - USER_EMAIL="${config.numbus.mail.userAddress}" - OWNER_NAME="${config.numbus.owner}" - - FRIENDLY_BODY="Cher/Chère $OWNER_NAME, - - Votre serveur a détecté une défaillance du service $UNIT. - Le système a tenté de gérer l'erreur, mais une intervention peut être nécessaire. - - Votre administrateur a été notifié de cet incident avec les détails techniques nécessaires. - Il interviendra si une action manuelle est requise. - - Merci de votre confiance, - L'équipe de support, - Numbus-Server." - - printf "Subject: [Alerte] Erreur sur votre serveur Numbus\n\n$FRIENDLY_BODY" | /run/wrappers/bin/sendmail -t "$USER_EMAIL" - ''; -in -{ - systemd.services."service-failure-notify@" = { - description = "Email notification for failed service %i"; - onFailure = [ ]; - serviceConfig = { - Type = "oneshot"; - ExecStart = "${systemd_notifier} %i"; - }; - }; -} \ No newline at end of file diff --git a/modules/backup/misc/default.nix b/modules/backup/misc/default.nix deleted file mode 100644 index bc990b8..0000000 --- a/modules/backup/misc/default.nix +++ /dev/null @@ -1,10 +0,0 @@ -{ ... }: - -{ - imports = [ - ./internationalisation.nix - ./power.nix - ./update.nix - ./users.nix - ]; -} \ No newline at end of file diff --git a/modules/backup/misc/internationalisation.nix b/modules/backup/misc/internationalisation.nix deleted file mode 100644 index 4eff542..0000000 --- a/modules/backup/misc/internationalisation.nix +++ /dev/null @@ -1,24 +0,0 @@ -{ config, lib, ... }: - -{ - config = { - i18n.defaultLocale = "${config.numbus.locale}.UTF-8"; - i18n.extraLocaleSettings = { - LC_ADDRESS = "${config.numbus.locale}.UTF-8"; - LC_IDENTIFICATION = "${config.numbus.locale}.UTF-8"; - LC_MEASUREMENT = "${config.numbus.locale}.UTF-8"; - LC_MONETARY = "${config.numbus.locale}.UTF-8"; - LC_NAME = "${config.numbus.locale}.UTF-8"; - LC_NUMERIC = "${config.numbus.locale}.UTF-8"; - LC_PAPER = "${config.numbus.locale}.UTF-8"; - LC_TELEPHONE = "${config.numbus.locale}.UTF-8"; - LC_TIME = "${config.numbus.locale}.UTF-8"; - }; - - console.keyMap = lib.toLower config.numbus.language; - services.xserver.xkb = { - layout = lib.toLower config.numbus.language; - variant = ""; - }; - }; -} \ No newline at end of file diff --git a/modules/backup/misc/power.nix b/modules/backup/misc/power.nix deleted file mode 100644 index f1c73f9..0000000 --- a/modules/backup/misc/power.nix +++ /dev/null @@ -1,30 +0,0 @@ -{ config, lib, pkgs, ... }: - -let - hardDrives = config.numbus.hardware.spindownDisksList; -in - -{ - config = { - systemd.services.hd-idle = { - description = "External HD spin down daemon"; - wantedBy = [ "multi-user.target" ]; - serviceConfig = { - Type = "simple"; - ExecStart = - let - idleTime = toString 1800; - hardDriveParameter = lib.strings.concatMapStringsSep " " (x: "-a ${x} -i ${idleTime}") hardDrives; - in - "${pkgs.hd-idle}/bin/hd-idle -i 0 ${hardDriveParameter}"; - }; - }; - - services.autoaspm.enable = true; - powerManagement.powertop.enable = true; - boot.kernelParams = [ - "pcie_aspm=force" - "consoleblank=60" - ]; - }; -} \ No newline at end of file diff --git a/modules/backup/misc/update.nix b/modules/backup/misc/update.nix deleted file mode 100644 index 26b22dd..0000000 --- a/modules/backup/misc/update.nix +++ /dev/null @@ -1,23 +0,0 @@ -{ config, inputs, ... }: - -{ - config = { - system.autoUpgrade = { - enable = true; - allowReboot = false; - flake = inputs.self.outPath; - flags = [ "--print-build-logs" ]; - dates = "02:00"; - randomizedDelaySec = "45min"; - }; - - nix.gc = { - automatic = true; - dates = "weekly"; - options = "--delete-older-than 7d"; - }; - - nix.settings.experimental-features = [ "nix-command" "flakes" ]; - nix.settings.auto-optimise-store = true; - }; -} \ No newline at end of file diff --git a/modules/backup/misc/users.nix b/modules/backup/misc/users.nix deleted file mode 100644 index 9ab0662..0000000 --- a/modules/backup/misc/users.nix +++ /dev/null @@ -1,16 +0,0 @@ -{ config, pkgs, ... }: - -{ - users.users.numbus-admin = { - shell = pkgs.fish; - isNormalUser = true; - description = "Numbus Admin"; - extraGroups = [ "wheel" ]; - uid = 1000; - initialPassword = "changeMe!"; - # required for auto start before user login - linger = true; - # required for rootless container with multiple users - autoSubUidGidRange = true; - }; -} \ No newline at end of file diff --git a/modules/common/hardware/boot-params.nix b/modules/common/hardware/boot-params.nix new file mode 100644 index 0000000..e40e80f --- /dev/null +++ b/modules/common/hardware/boot-params.nix @@ -0,0 +1,31 @@ +{ config, deviceType, ... }: + +{ + config = mkMerge [ + ({ + boot = { + plymouth.enable = true; + # Enable "Silent boot" + consoleLogLevel = 3; + initrd.verbose = false; + loader.timeout = 1; + }; + }) + + ( mkIf (deviceType == "computer" || deviceType == "tv") { + # Bootloader options + boot = { + initrd.systemd.enable = true; + loader.systemd-boot.enable = true; + loader.efi.canTouchEfiVariables = true; + kernelParams = [ + "quiet" + "udev.log_level=3" + "systemd.show_status=auto" + "pcie_aspm=force" + "consoleblank=60" + ]; + }; + }) + ]; +} \ No newline at end of file diff --git a/modules/common/hardware/default.nix b/modules/common/hardware/default.nix index d580f5a..4c8b35a 100644 --- a/modules/common/hardware/default.nix +++ b/modules/common/hardware/default.nix @@ -4,6 +4,8 @@ imports = [ # To test ./disks/default.nix + ./boot-params.nix ./cpu.nix + ./graphics.nix ]; } \ No newline at end of file diff --git a/modules/computer/hardware/nvidia.nix b/modules/common/hardware/graphics.nix similarity index 91% rename from modules/computer/hardware/nvidia.nix rename to modules/common/hardware/graphics.nix index 648c770..508ccd1 100644 --- a/modules/computer/hardware/nvidia.nix +++ b/modules/common/hardware/graphics.nix @@ -1,11 +1,11 @@ { config, lib, pkgs, ... }: let - cfg = config.numbus-computer.hardware; + cfg = config.numbus.hardware; in { - options.numbus-computer.hardware = { + options.numbus.hardware = { nvidia = { enable = mkEnableOption "Wether to install the NVIDIA driver. Required for better performance with NVIDIA graphics cards." } @@ -44,10 +44,7 @@ in # Only available from driver 515.43.04+ open = false; - # Enable the Nvidia settings menu, - # accessible via `nvidia-settings`. nvidiaSettings = true; - # Optionally, you may need to select the appropriate driver version for your specific GPU. package = config.boot.kernelPackages.nvidiaPackages.stable; }; diff --git a/modules/backup/hardware/pcie-coral.nix b/modules/common/hardware/pcie-coral.nix similarity index 96% rename from modules/backup/hardware/pcie-coral.nix rename to modules/common/hardware/pcie-coral.nix index 06fe328..32170d6 100644 --- a/modules/backup/hardware/pcie-coral.nix +++ b/modules/common/hardware/pcie-coral.nix @@ -103,9 +103,9 @@ in { options.numbus.hardware.pcie-coral = lib.mkEnableOption "PCIe Coral TPU support"; - config = lib.mkIf cfg { - services.udev.packages = [ libedgetpu ]; + config = lib.mkIf cfg.enable { + services.udev.packages = [ "libedgetpu" ]; users.groups.plugdev = {}; - boot.extraModulePackages = [ gasket ]; + boot.extraModulePackages = [ "gasket" ]; }; } \ No newline at end of file diff --git a/modules/server/mail/clamav.nix b/modules/common/mail/clamav.nix similarity index 100% rename from modules/server/mail/clamav.nix rename to modules/common/mail/clamav.nix diff --git a/modules/server/mail/systemd.nix b/modules/common/mail/systemd.nix similarity index 100% rename from modules/server/mail/systemd.nix rename to modules/common/mail/systemd.nix diff --git a/modules/common/web/cockpit-numbus/cockpit.nix b/modules/common/web/cockpit-numbus/cockpit.nix new file mode 100644 index 0000000..83f7c56 --- /dev/null +++ b/modules/common/web/cockpit-numbus/cockpit.nix @@ -0,0 +1,28 @@ +{ pkgs, ... }: + +let + cockpit-numbus = pkgs.stdenv.mkDerivation { + name = "cockpit-numbus"; + src = ./cockpit-numbus; + installPhase = '' + mkdir -p $out/share/cockpit/numbus + cp -r * $out/share/cockpit/numbus + ''; + }; +in + +{ + services.cockpit = { + enable = true; + port = 9090; + openFirewall = false; + settings = { + WebService = { + AllowUnencrypted = true; + }; + }; + }; + + # Link the extension into the system cockpit path + environment.systemPackages = [ cockpit-numbus ]; +} diff --git a/deploy/GUI/web/pages/installation.html b/modules/common/web/default.nix similarity index 100% rename from deploy/GUI/web/pages/installation.html rename to modules/common/web/default.nix diff --git a/modules/computer/hardware/boot.nix b/modules/computer/hardware/boot.nix deleted file mode 100644 index 3812345..0000000 --- a/modules/computer/hardware/boot.nix +++ /dev/null @@ -1,24 +0,0 @@ -{ config, ... }: - -{ - config = { - # Bootloader options - boot.initrd.systemd.enable = true; - boot.loader.systemd-boot.enable = true; - boot.loader.efi.canTouchEfiVariables = true; - - # Boot splash instead of log messages - boot = { - plymouth.enable = true; - # Enable "Silent boot" - consoleLogLevel = 3; - initrd.verbose = false; - kernelParams = [ - "quiet" - "udev.log_level=3" - "systemd.show_status=auto" - ]; - loader.timeout = 1; - }; - }; -} \ No newline at end of file diff --git a/modules/computer/hardware/cpu.nix b/modules/computer/hardware/cpu.nix deleted file mode 100644 index d3fe470..0000000 --- a/modules/computer/hardware/cpu.nix +++ /dev/null @@ -1,9 +0,0 @@ -{ config, ... }: - -{ - config = { - hardware.enableRedistributableFirmware = true; - hardware.cpu.intel.updateMicrocode = true; - hardware.cpu.amd.updateMicrocode = true; - }; -} \ No newline at end of file diff --git a/modules/computer/hardware/default.nix b/modules/computer/hardware/default.nix deleted file mode 100644 index 0d931d1..0000000 --- a/modules/computer/hardware/default.nix +++ /dev/null @@ -1,10 +0,0 @@ -{ ... }: - -{ - imports=[ - ./boot.nix - ./cpu.nix - ./disks.nix - ./nvidia.nix - ]; -} \ No newline at end of file diff --git a/modules/computer/hardware/disks.nix b/modules/computer/hardware/disks.nix deleted file mode 100644 index 6e1acee..0000000 --- a/modules/computer/hardware/disks.nix +++ /dev/null @@ -1,328 +0,0 @@ -{ config, lib, pkgs, ... }: - -with lib; - -let - cfg = config.numbus.hardware; - - mkDataDisk = idx: device: { - name = "content-${toString idx}"; - value = { - type = "disk"; - device = device; - content = { - type = "gpt"; - partitions = { - luks = { - size = "100%"; - content = { - type = "luks"; - name = "crypted-content-${toString idx}"; - initrdUnlock = false; - settings = { - keyFile = "/etc/secrets/disks/content-${toString idx}"; - allowDiscards = true; - crypttabExtraOpts = [ "nofail" ]; - }; - content = { - type = "filesystem"; - format = cfg.dataDisksFilesystem; - mountpoint = "/mnt/content-${toString idx}"; - mountOptions = [ "nofail" "noauto" ]; - }; - }; - }; - }; - }; - }; - }; - - mkParityDisk = idx: device: { - name = "parity-${toString idx}"; - value = { - type = "disk"; - device = device; - content = { - type = "gpt"; - partitions = { - luks = { - size = "100%"; - content = { - type = "luks"; - name = "crypted-parity-${toString idx}"; - initrdUnlock = false; - settings = { - keyFile = "/etc/secrets/disks/parity-${toString idx}"; - allowDiscards = true; - crypttabExtraOpts = [ "nofail" ]; - }; - content = { - type = "filesystem"; - format = cfg.parityDisksFilesystem; - mountpoint = "/mnt/parity-${toString idx}"; - mountOptions = [ "nofail" "noauto" ]; - }; - }; - }; - }; - }; - }; - }; - - isMirror = length cfg.bootDisksList > 1; - - bootDisksConfig = if isMirror then { - mdadm = { - boot = { - type = "mdadm"; - level = 1; - metadata = "1.2"; - content = { - type = "filesystem"; - format = "vfat"; - mountpoint = "/boot"; - mountOptions = [ "umask=0077" ]; - }; - }; - }; - disk = listToAttrs (imap1 (i: device: { - name = "boot-${toString i}"; - value = { - type = "disk"; - device = device; - content = { - type = "gpt"; - partitions = { - ESP = { - size = "1G"; - type = "EF00"; - content = { - type = "mdraid"; - name = "boot"; - }; - }; - luks = { - size = "100%"; - content = { - type = "luks"; - name = "crypted-boot-${toString i}"; - settings = { - allowDiscards = true; - keyFile = "/etc/secrets/disks/boot-${toString i}"; - }; - content = { - type = "lvm_pv"; - vg = "pool"; - }; - }; - }; - }; - }; - }; - }) cfg.bootDisksList); - } else { - disk = { - "boot-1" = { - type = "disk"; - device = head cfg.bootDisksList; - content = { - type = "gpt"; - partitions = { - ESP = { - size = "1G"; - type = "EF00"; - content = { - type = "filesystem"; - format = "vfat"; - mountpoint = "/boot"; - mountOptions = [ "umask=0077" ]; - }; - }; - luks = { - size = "100%"; - content = { - type = "luks"; - name = "crypted-boot-1"; - settings = { - keyFile = "/etc/secrets/disks/boot-1"; - allowDiscards = true; - }; - content = { - type = "lvm_pv"; - vg = "pool"; - }; - }; - }; - }; - }; - }; - }; - }; - - lvmConfig = { - lvm_vg = { - pool = { - type = "lvm_vg"; - lvs = { - swap = { - size = cfg.swapSize; - content = { - type = "swap"; - }; - } // optionalAttrs isMirror { lvm_type = "mirror"; }; - snapraid = { - size = "1G"; - content = { - type = "filesystem"; - format = "btrfs"; - mountpoint = "/mnt/content-0"; - }; - } // optionalAttrs isMirror { lvm_type = "mirror"; }; - root = { - size = "100%"; - content = { - type = "btrfs"; - extraArgs = [ "-f" ]; - subvolumes = { - "/rootfs" = { - mountpoint = "/"; - mountOptions = [ "compress=zstd" "noatime" ]; - }; - "/home" = { - mountpoint = "/home"; - mountOptions = [ "compress=zstd" ]; - }; - "/nix" = { - mountpoint = "/nix"; - mountOptions = [ "compress=zstd" "noatime" ]; - }; - }; - }; - } // optionalAttrs isMirror { lvm_type = "mirror"; }; - }; - }; - }; - }; - -in -{ - options.numbus.hardware = { - dataDisksList = mkOption { - type = types.listOf types.str; - default = []; - example = [ "/dev/disk/by-id/WD_Blue_ATO431_159Ejz224G0000382b" "/dev/disk/by-id/Seagate_Barracuda_159Ejz224G" ]; - description = "List by-id path of devices for data disks"; - }; - parityDisksList = mkOption { - type = types.listOf types.str; - default = []; - example = [ "/dev/disk/by-id/WD_Blue_ATO431_159Ejz224G0000382b" "/dev/disk/by-id/Seagate_Barracuda_159Ejz224G" ]; - description = "List of by-id path of devices for parity disks"; - }; - bootDisksList = mkOption { - type = types.listOf types.str; - default = []; - example = [ "/dev/disk/by-id/nvme_SAMSUNG_MZVPYEHCO_159Ejz224G0000" "/dev/disk/by-id/ata-San_Disk_159Ejz224G" ]; - description = "List of by-id path of devices for boot disks"; - }; - spindownDisksList = mkOption { - type = types.listOf types.str; - default = []; - example = [ "/dev/disk/by-id/WD_Blue_ATO431_159Ejz224G0000382b" "/dev/disk/by-id/Seagate_Barracuda_159Ejz224G" ]; - description = "List of by-id path of devices to spindown when inactive to save power (HDD only)"; - }; - swapSize = mkOption { - type = types.str; - default = "16G"; - example = "16G"; - description = "Size of the swap partition"; - }; - dataDisksFilesystem = mkOption { - type = types.enum [ "xfs" "ext4" "btrfs" ]; - default = "xfs"; - example = "xfs"; - description = "Filesystem for data disks. Available filesystem options : xfs, ext4, btrfs"; - }; - parityDisksFilesystem = mkOption { - type = types.enum [ "xfs" "ext4" "btrfs" ]; - default = "xfs"; - example = "xfs"; - description = "Filesystem for parity disks. Available filesystem options : xfs, ext4, btrfs"; - }; - }; - - config = mkIf (cfg.bootDisksList != []) { - disko.devices = mkMerge [ - bootDisksConfig - lvmConfig - { - disk = listToAttrs (imap1 mkDataDisk cfg.dataDisksList); - } - { - disk = listToAttrs (imap1 mkParityDisk cfg.parityDisksList); - } - ]; - - services.snapraid = { - enable = true; - contentFiles = [ "/mnt/content-0/snapraid.content" ] ++ - (map (i: "/mnt/content-${toString i}/snapraid.content") (range 1 (length cfg.dataDisksList))); - parityFiles = map (i: "/mnt/parity-${toString i}/snapraid.parity") (range 1 (length cfg.parityDisksList)); - dataDisks = listToAttrs (imap1 (i: _: nameValuePair "d${toString i}" "/mnt/content-${toString i}") cfg.dataDisksList); - }; - - fileSystems."/mnt/data" = { - device = "/mnt/content-*"; - fsType = "fuse.mergerfs"; - options = [ - "category.create=ff" - "cache.files=partial" - "dropcacheonclose=true" - "defaults" - "noauto" - "nofail" - "allow_other" - "moveonenospc=1" - "minfreespace=50G" - "func.getattr=newest" - "fsname=mergerfs_data" - "x-mount.mkdir" - "x-systemd.automount" - ]; - }; - - systemd.services.mount-disks = { - description = "Mount data and parity disks"; - before = [ "mnt-data.mount" ]; - requiredBy = [ "mnt-data.mount" ]; - path = [ pkgs.cryptsetup pkgs.util-linux ]; - serviceConfig = { - Type = "oneshot"; - RemainAfterExit = true; - }; - script = let - mountDataDisk = i: '' - if [ ! -e /dev/mapper/crypted-content-${toString i} ]; then - cryptsetup luksOpen --key-file /etc/secrets/disks/content-${toString i} /dev/disk/by-partlabel/disk-content-${toString i}-luks crypted-content-${toString i} - fi - mkdir -p /mnt/content-${toString i} - if ! mountpoint -q /mnt/content-${toString i}; then - mount -t ${cfg.dataDisksFilesystem} /dev/mapper/crypted-content-${toString i} /mnt/content-${toString i} - fi - ''; - mountParityDisk = i: '' - if [ ! -e /dev/mapper/crypted-parity-${toString i} ]; then - cryptsetup luksOpen --key-file /etc/secrets/disks/parity-${toString i} /dev/disk/by-partlabel/disk-parity-${toString i}-luks crypted-parity-${toString i} - fi - mkdir -p /mnt/parity-${toString i} - if ! mountpoint -q /mnt/parity-${toString i}; then - mount -t ${cfg.parityDisksFilesystem} /dev/mapper/crypted-parity-${toString i} /mnt/parity-${toString i} - fi - ''; - in '' - ${concatMapStrings mountDataDisk (range 1 (length cfg.dataDisksList))} - ${concatMapStrings mountParityDisk (range 1 (length cfg.parityDisksList))} - ''; - }; - }; -} \ No newline at end of file diff --git a/deploy/GUI/web/pages/post-installation.html b/modules/console/default.nix similarity index 100% rename from deploy/GUI/web/pages/post-installation.html rename to modules/console/default.nix diff --git a/modules/server/hardware/boot.nix b/modules/server/hardware/boot.nix deleted file mode 100644 index 89c82ad..0000000 --- a/modules/server/hardware/boot.nix +++ /dev/null @@ -1,11 +0,0 @@ -{ config, ... }: - -{ - boot.initrd.systemd.enable = true; - boot.loader.systemd-boot.enable = true; - boot.loader.efi.canTouchEfiVariables = true; - - boot.kernel.sysctl = { - "vm.overcommit_memory" = 1; - }; -} \ No newline at end of file diff --git a/modules/server/hardware/default.nix b/modules/server/hardware/default.nix deleted file mode 100644 index f1a7eb8..0000000 --- a/modules/server/hardware/default.nix +++ /dev/null @@ -1,12 +0,0 @@ -{ ... }: - -{ - imports = [ - # Tested - ./boot.nix - ./cpu.nix - # To test - ./pcie-coral.nix - ./disks/default.nix - ]; -} \ No newline at end of file diff --git a/modules/server/hardware/pcie-coral.nix b/modules/server/hardware/pcie-coral.nix deleted file mode 100644 index db7709b..0000000 --- a/modules/server/hardware/pcie-coral.nix +++ /dev/null @@ -1,111 +0,0 @@ -{ config, lib, pkgs, ... }: - -let - cfg = config.numbus-server.hardware.pcie-coral; - - gasket-driver = { stdenv, lib, fetchFromGitHub, kernel }: stdenv.mkDerivation rec { - pname = "gasket"; - version = "1.0-18"; - - src = fetchFromGitHub { - owner = "google"; - repo = "gasket-driver"; - rev = "97aeba584efd18983850c36dcf7384b0185284b3"; - sha256 = "pJwrrI7jVKFts4+bl2xmPIAD01VKFta2SRuElerQnTo="; - }; - - makeFlags = [ - "-C" - "${kernel.dev}/lib/modules/${kernel.modDirVersion}/build" - "M=$(PWD)" - ]; - buildFlags = [ "modules" ]; - - installFlags = [ "INSTALL_MOD_PATH=${placeholder "out"}" ]; - installTargets = [ "modules_install" ]; - - sourceRoot = "source/src"; - hardeningDisable = [ "pic" "format" ]; - nativeBuildInputs = kernel.moduleBuildDependencies; - - meta = with lib; { - description = "The Coral Gasket Driver allows usage of the Coral EdgeTPU on Linux systems."; - homepage = "https://github.com/google/gasket-driver"; - license = licenses.gpl2; - maintainers = [ maintainers.kylehendricks ]; - platforms = platforms.linux; - }; - }; - - libedgetpu-pkg = { stdenv, lib, fetchFromGitHub, libusb1, abseil-cpp, flatbuffers, xxd }: - let - flatbuffers_1_12 = flatbuffers.overrideAttrs (oldAttrs: rec { - version = "1.12.0"; - NIX_CFLAGS_COMPILE = "-Wno-error=class-memaccess -Wno-error=maybe-uninitialized"; - cmakeFlags = (oldAttrs.cmakeFlags or []) ++ ["-DFLATBUFFERS_BUILD_SHAREDLIB=ON"]; - NIX_CXXSTDLIB_COMPILE = "-std=c++17"; - configureFlags = (oldAttrs.configureFlags or []) ++ ["--enable-shared"]; - src = fetchFromGitHub { - owner = "google"; - repo = "flatbuffers"; - rev = "v${version}"; - sha256 = "sha256-L1B5Y/c897Jg9fGwT2J3+vaXsZ+lfXnskp8Gto1p/Tg="; - }; - }); - - in stdenv.mkDerivation rec { - pname = "libedgetpu"; - version = "grouper"; - - src = fetchFromGitHub { - owner = "google-coral"; - repo = pname; - rev = "release-${version}"; - sha256 = "sha256-73hwItimf88Iqnb40lk4ul/PzmCNIfdt6Afi+xjNiBE="; - }; - - makeFlags = ["-f" "makefile_build/Makefile" "libedgetpu" ]; - - buildInputs = [ - libusb1 - abseil-cpp - flatbuffers_1_12 - ]; - - nativeBuildInputs = [ - xxd - ]; - - NIX_CXXSTDLIB_COMPILE = "-std=c++17"; - - TFROOT = "${fetchFromGitHub { - owner = "tensorflow"; - repo = "tensorflow"; - rev = "v2.7.4"; - sha256 = "sha256-liDbUAdaVllB0b74aBeqNxkYNu/zPy7k3CevzRF5dk0="; - }}"; - - enableParallelBuilding = false; - - installPhase = '' - mkdir -p $out/lib - cp out/direct/k8/libedgetpu.so.1.0 $out/lib - ln -s $out/lib/libedgetpu.so.1.0 $out/lib/libedgetpu.so.1 - mkdir -p $out/lib/udev/rules.d - cp debian/edgetpu-accelerator.rules $out/lib/udev/rules.d/99-edgetpu-accelerator.rules - ''; - }; - - gasket = config.boot.kernelPackages.callPackage gasket-driver {}; - libedgetpu = pkgs.callPackage libedgetpu-pkg {}; -in - -{ - options.numbus-server.hardware.pcie-coral = lib.mkEnableOption "PCIe Coral TPU support"; - - config = lib.mkIf cfg.enable { - services.udev.packages = [ libedgetpu ]; - users.groups.plugdev = {}; - boot.extraModulePackages = [ gasket ]; - }; -} \ No newline at end of file diff --git a/modules/server/mail/default.nix b/modules/server/mail/default.nix deleted file mode 100644 index b5f75e8..0000000 --- a/modules/server/mail/default.nix +++ /dev/null @@ -1,12 +0,0 @@ -{ ... }: - -{ - imports = [ - # To test - ./clamav.nix - ./smart.nix - ./systemd.nix - ./smtp.nix - ./disk-space.nix - ]; -} \ No newline at end of file diff --git a/modules/server/networking/networking.nix b/modules/server/networking/networking.nix index f5350fb..0228286 100644 --- a/modules/server/networking/networking.nix +++ b/modules/server/networking/networking.nix @@ -44,16 +44,16 @@ in # Allow rootless containers to bind to port 53 and up boot.kernel.sysctl."net.ipv4.ip_unprivileged_port_start" = 53; - networking.bridges.br0.interfaces = [ "${cfg.interface}" ]; + networking.bridges.br0.interfaces = [ cfg.interface ]; networking.interfaces."${cfg.interface}".useDHCP = false; networking.interfaces.br0.useDHCP = false; networking.nameservers = cfg.dnsServers; networking.interfaces.br0.ipv4.addresses = [{ - address = "${cfg.ipAddress}"; + address = cfg.ipAddress; prefixLength = 24; }]; networking.defaultGateway = { - address = "${cfg.routerIpAddress}"; + address = cfg.routerIpAddress; interface = "br0"; }; }; diff --git a/modules/server/packages/packages.nix b/modules/server/packages/default-packages.nix similarity index 72% rename from modules/server/packages/packages.nix rename to modules/server/packages/default-packages.nix index 13dc56e..b1f9a19 100644 --- a/modules/server/packages/packages.nix +++ b/modules/server/packages/default-packages.nix @@ -4,22 +4,29 @@ nixpkgs.config.allowUnfree = true; environment.systemPackages = with pkgs; [ - git - ncdu - fastfetch - tpm2-tss - sops + # Secrets age - powertop - pciutils + sops + # HDD tools hdparm hd-idle hddtemp smartmontools + ncdu + # CPU tools cpufrequtils intel-gpu-tools + # Filesystem tools snapraid mergerfs mergerfs-tools + # Powersave tools + powertop + # PCI devices tools + pciutils + tpm2-tss + # Misc + git + fastfetch ]; } \ No newline at end of file diff --git a/modules/server/packages/default.nix b/modules/server/packages/default.nix index 175367d..1a231b5 100644 --- a/modules/server/packages/default.nix +++ b/modules/server/packages/default.nix @@ -5,7 +5,5 @@ # To test ./packages.nix ./podman.nix - ./ssh.nix - ./terminal.nix ]; } \ No newline at end of file diff --git a/deploy/TUI/deploy.sh b/script/deploy bak.sh similarity index 100% rename from deploy/TUI/deploy.sh rename to script/deploy bak.sh diff --git a/script/deploy.sh b/script/deploy.sh new file mode 100755 index 0000000..6e644c3 --- /dev/null +++ b/script/deploy.sh @@ -0,0 +1,1036 @@ +#!/usr/bin/env nix-shell +#!nix-shell -i bash -p bash nano coreutils gnused gum fastfetch xkcdpass sops ssh-to-age age sshpass envsubst pciutils usbutils mosquitto curl jq yq python3 + + + +# --- UTILITY FUNCTIONS ---> +echod() { + MESSAGE=${1} + + if [[ ${DEBUG} -eq 1 ]]; then + echo -e ${MESSAGE} + fi +} + +ssh_to_host() { + local COMMAND="${1}" + ssh -i "${EXTRA_FILES_PATH}/home/numbus-admin/.ssh/id_ed25519" "${TARGET_USER}@${LIVE_TARGET_IP}" "${COMMAND}" +} + +get_valid_input() { + local VAR_NAME="${1}" + local HEADER="${2}" + local PLACEHOLDER="${3}" + local REGEX="${4}" + local MANDATORY="${5:-true}" + local SENSITIVE="${6:-false}" + + if [[ "${MANDATORY}" == "true" ]]; then + local PROMPT="(Required) > " + elif [[ "${MANDATORY}" == "false" ]]; then + local PROMPT="(Optional) > " + fi + + while true; do + local INPUT=$(gum input --header "${HEADER}" --prompt "${PROMPT}" --placeholder "${PLACEHOLDER}") + + # Handle empty input + if [[ -z "${INPUT}" ]]; then + if [[ "${MANDATORY}" == true ]]; then + gum style --foreground "#ff0000" -- "✖ This field is mandatory." + continue + else + INPUT="" + break + fi + fi + + # Handle Regex Validation + if [[ -n "${REGEX}" ]]; then + if [[ "${INPUT}" =~ ${REGEX} ]]; then + export "${VAR_NAME}"="${INPUT}" + break + else + gum style --foreground "#ff0000" -- "✖ Invalid format. Please try again." + fi + else + export "${VAR_NAME}"="${INPUT}" + break + fi + done +} +# --- UTILITY FUNCTIONS ---< + + + +# --- GLOBAL FUNCTIONS ---> +cleanup() { + echo -e "\n ✅ Cleaning up..." + + rm -${DIR_RM_FLAGS} ${TMP_FILES_PATH}/ + + if ps -p ${BRIDGE_PID:-} > /dev/null; then + kill ${BRIDGE_PID} + fi +} + +compatibility_check() { + TEST_FAIL=0 + + if [[ -r /etc/os-release ]] && grep -qi '^ID=nixos\b' /etc/os-release; then + echo -e "\n ✅ NixOS system detected." + else + TEST_FAIL=$((TEST_FAIL + 1)) + echo -e "\n ❌ You are not on a NixOS based system. This is required to continue." + fi + + if [[ "$(uname -m)" == "x86_64" ]]; then + echo -e "\n ✅ x86_64 system detected." + else + TEST_FAIL=$((TEST_FAIL + 1)) + echo -e "\n ❌ You are not on a x86_64 based system. This is required to continue." + fi + + if [[ ${TEST_FAIL} -gt 0 ]]; then + COMPATIBILITY_OVERRIDE=$(gum choose --header "Some compatibility checks failed. The installation will very likely fail. Continue ?" \ + "No" \ + "Yes, I know what I am doing") + [[ "${COMPATIBILITY_OVERRIDE}" == "No" ]] && exit 1 + [[ "${COMPATIBILITY_OVERRIDE}" != "No" ]] && echo -e "\n ⚠️ Continuing anyways, this is not supported by Numbus." + fi + + return 0 +} + +hierarchy_preparation() { + echod "\n 🔄 Preparing the folder hierarchy for the final configuration..." + + if [[ -e config/* ]]; then + echo " ⚠️ It seems you have already run this script. Previously generated files need to be cleaned up." + OLD_CONFIG_PATH="trash/$(date +"%Y-%m-%d-%Hh%M")/" + mkdir -${MKDIR_FLAGS} ${OLD_CONFIG_PATH} + mv -${MV_FLAGS} config/ ${OLD_CONFIG_PATH} + echo " ✅ Your files have been moved to the ${OLD_CONFIG_PATH} directory. You can retrieve them there if needed." + fi + + # Script folders + mkdir -${MKDIR_FLAGS} ${TMP_FILES_PATH}/config + mkdir -${MKDIR_FLAGS} ${TMP_FILES_PATH}/logs + mkdir -${MKDIR_FLAGS} ${TMP_FILES_PATH}/tmp + [[ ${WEB_MODE} -eq 1 ]] && mkdir -${MKDIR_FLAGS} ${TMP_FILES_PATH}/web + + # Secrets + mkdir -${MKDIR_FLAGS} ${EXTRA_FILES_PATH}/home/numbus-admin/.ssh/ + mkdir -${MKDIR_FLAGS} ${EXTRA_FILES_PATH}/var/lib/sops-nix/ + mkdir -${MKDIR_FLAGS} ${EXTRA_FILES_PATH}/etc/nixos/secrets/disks + mkdir -${MKDIR_FLAGS} ${EXTRA_FILES_PATH}/etc/nixos/secrets/system + if [[ "${DEVICE_TYPE}" == "server" || "${DEVICE_TYPE}" == "backup" ]]; then + mkdir -${MKDIR_FLAGS} ${EXTRA_FILES_PATH}/etc/nixos/secrets/podman + fi + + echod "\n ✅ Folder hierarchy ready" +} + +hardware_detection() { + local TMPFILE="/tmp/nixos-installation-hw-detection" + + ssh_to_host 'bash -s' << SSHEND +TARGET_GRAPHICS_BRAND=() + +for brand in Intel AMD NVIDIA; do + if lspci -nn 2>/dev/null | grep -i "vga" | grep -iq "\${brand}"; then + TARGET_GRAPHICS="true" + TARGET_GRAPHICS_BRAND+=("\${brand}") + else + TARGET_GRAPHICS="false" + fi +done + +ls /dev/dri/ > /dev/null 2>&1 | grep -iq "renderD128" && TARGET_GRAPHICS_RENDERER="true" || TARGET_GRAPHICS_RENDERER="false" +lsusb > /dev/null 2>&1 | grep -iq "google" && TARGET_USB_CORAL="true" || TARGET_USB_CORAL="false" +lspci -nn > /dev/null 2>&1 | grep -iq "089a" && TARGET_PCIE_CORAL="true" || TARGET_PCIE_CORAL="false" +ls /dev/serial/by-id/ > /dev/null 2>&1 | grep -i "zigbee" && TARGET_ZIGBEE_DEVICE=\$(ls /dev/serial/by-id/ > /dev/null 2>&1 | grep -i "zigbee" | head -n 1) || TARGET_ZIGBEE_DEVICE="" + +TARGET_INTERFACE=\$(ip -4 route show default | awk '{print \$5}' | head -n1) + +if ls -l /sys/class/tpm/tpm0/ > /dev/null 2>&1; then + TARGET_TPM="true" + TARGET_TPM_VERSION=\$(cat /sys/class/tpm/tpm0/tpm_version_major) +else + TARGET_TPM="false" + TARGET_TPM_VERSION="N/A" +fi + +HDD=1 +DISK_DEVPATH=() +DISK_NAME=() +DISK_TYPE=() +DISK_HEALTH=() +DISK_ID=() + +for DISK in \$(lsblk -x SIZE -d -n -e 7,11 -o NAME); do + + # Disk name and simple path + DISK_DEVPATH+=("/dev/\$DISK") + DISK_NAME+=("\$DISK") + # Disk type + HDD=\$(cat /sys/block/\$DISK/queue/rotational) + TRANSPORT_PROTOCOL=\$(lsblk -x SIZE -d -n -e 7,11 -o TRAN /dev/\$DISK) + if [[ "\$DISK" == "nvme*" ]]; then DISK_TYPE+=("NVMe"); + elif [[ "\$TRANSPORT_PROTOCOL" == "usb" ]]; then DISK_TYPE+=("USB"); + elif [[ "\$HDD" -eq 1 ]]; then DISK_TYPE+=("HDD"); + elif [[ "\$HDD" -eq 0 ]]; then DISK_TYPE+=("SSD"); + else DISK_TYPE+=("Other") + fi + + # Disk health + if [[ \$(echo "${LIVE_TARGET_PASSWORD}" | sudo -S smartctl -H /dev/\$DISK 2>/dev/null | grep 'self-assessment' | awk '{print \$6}') == "PASSED" ]]; then + DISK_HEALTH+=("PASSED") + else + DISK_HEALTH+=("N/A") + fi + # Disk ID + DISK_ID+=("\$(ls -l /dev/disk/by-id | grep -m1 "../../\$DISK" | awk '{print "/dev/disk/by-id/" \$9}')") + DISK_SIZE+=("\$(lsblk -x SIZE -d -n -e 7,11 -o SIZE /dev/\$DISK)") +done + +echo "# Hardware detection results on \$(date)" > "${TMPFILE}" +for var in \ + TARGET_GRAPHICS \ + TARGET_GRAPHICS_RENDERER \ + TARGET_USB_CORAL \ + TARGET_PCIE_CORAL \ + TARGET_ZIGBEE_DEVICE \ + TARGET_INTERFACE \ + TARGET_TPM \ + TARGET_TPM_VERSION; do + echo "export \${var}=\${!var}" >> "${TMPFILE}" +done + +for var in \ + TARGET_GRAPHICS_BRAND \ + DISK_DEVPATH \ + DISK_NAME \ + DISK_TYPE \ + DISK_HEALTH \ + DISK_ID \ + DISK_SIZE; do + declare -p \${var} | sed 's/^declare /declare -g /' >> "${TMPFILE}" +done +SSHEND + + scp -i "${EXTRA_FILES_PATH}/home/numbus-admin/.ssh/id_ed25519" "${TARGET_USER}@${LIVE_TARGET_IP}":"${TMPFILE}" "${TMPFILE}" &> /dev/null + source "${TMPFILE}" + + local DISK_FLAT_ARRAY=() + for i in "${!DISK_NAME[@]}"; do + DISK_FLAT_ARRAY+=("${DISK_NAME[$i]}" "${DISK_DEVPATH[$i]}" "${DISK_TYPE[$i]}" "${DISK_HEALTH[$i]}" "${DISK_ID[$i]}" "${DISK_SIZE[$i]}") + done + + jq -n \ + --argjson graphics_enabled "${TARGET_GRAPHICS:-false}" \ + --argjson graphics_renderer "${TARGET_GRAPHICS_RENDERER:-false}" \ + --argjson tpu_usb "${TARGET_USB_CORAL:-false}" \ + --argjson tpu_pcie "${TARGET_PCIE_CORAL:-false}" \ + --argjson tpm_enabled "${TARGET_TPM:-false}" \ + --arg tpm_version "${TARGET_TPM_VERSION:-N/A}" \ + --arg zigbee_device "${TARGET_ZIGBEE_DEVICE:-}" \ + --arg interface "${TARGET_INTERFACE:-}" \ + --argjson brands "$(jq -n '$ARGS.positional' --args ${TARGET_GRAPHICS_BRAND[@]:-})" \ + ' + { + graphics: { enabled: $graphics_enabled, brands: $brands, renderer: $graphics_renderer }, + tpu: { usb: $tpu_usb, pcie: $tpu_pcie }, + tpm: { enabled: $tpm_enabled, version: $tpm_version }, + zigbee: { device: $zigbee_device }, + network: { interface: $interface }, + disks: [ + $ARGS.positional | range(0; length; 6) as $i | { + name: .[$i], path: .[$i+1], type: .[$i+2], health: .[$i+3], id: .[$i+4], size: .[$i+5] + } + ] + }' --args "${DISK_FLAT_ARRAY[@]:-}" > ${HARDWARE_DATA_PATH} + + if ssh_to_host "sudo nixos-generate-config --no-filesystems --show-hardware-config" > ${EXTRA_FILES_PATH}/etc/nixos/hardware-configuration.nix; then + echo -e "\n✅ Hardware configuration generated" + else + echo -e "\n❌ Failed to generate hardware configuration" + exit 1 + fi +} +# --- GLOBAL FUNCTIONS ---< + + + +# --- MAIN WEB FUNCTIONS ---> +launch_gui() { + echo -e "\n ➡️ You will now proceed to the configuration of your device through your browser" + echo -e "\n 🚀 Launching Numbus Configurator..." + + python3 "${BRIDGE_SCRIPT}" > /dev/null 2>&1 & + export BRIDGE_PID=$! + xdg-open "http://localhost:${WEBSERVER_PORT}" 2>/dev/null || open "http://localhost:${WEBSERVER_PORT}" 2>/dev/null || true + + sleep 5 + + echo -e "\n ➡️ If it doesn't automatically, open your browser at: $(gum style --foreground 212 "http://localhost:${WEBSERVER_PORT}")" + +} +# --- MAIN WEB FUNCTIONS ---< + + + +# --- MAIN TUI FUNCTIONS ---> +preparation() { + echo -e "\n ➡️ This script will now guide you through the configuration and gather the necessary information." + + echo "" + RAW_DEVICE_TYPE=$(gum choose --header "Choose the device you want to deploy :" \ + "Numbus Server : Professional-grade hosting, strictly kept under your roof." \ + "Numbus Backup Server : Automated, high-efficiency protection for your entire ecosystem." \ + "Numbus Computer : A modern, privacy-respecting machine built for work, creation, and play — without the corporate bloat." \ + "Numbus TV : A premium cinematic experience free from trackers and forced subscriptions." \ + "Numbus Game Console : An unbreakable Steam bigscreen experience.") + + case "${RAW_DEVICE_TYPE}" in + "Numbus Server : "* ) DEVICE_TYPE="server" ;; + "Numbus Backup Server : "* ) DEVICE_TYPE="backup" ;; + "Numbus Computer : "* ) DEVICE_TYPE="computer" ;; + "Numbus TV : "* ) DEVICE_TYPE="tv" ;; + "Numbus Game Console : "* ) DEVICE_TYPE="console" ;; + esac + + RAW_DEPLOYMENT_MODE=$(gum choose --header "Choose your preferred deployment mode :" \ + "Interactive : You don't already have a configuration." \ + "Non-interactive : You have a valid configuration hosted on a Git platform.") + + case "${RAW_DEPLOYMENT_MODE}" in + "Interactive : "* ) DEPLOYMENT_MODE="interactive" ;; + "Non-interactive : "* ) DEPLOYMENT_MODE="non-interactive" ;; + esac + + if [[ "${DEPLOYMENT_MODE}" == "non-interactive" ]]; then + git_url() { + IMPORTED_CONFIG_URL=$(gum input --placeholder "https://yourgitplatform.tld/your-user/repo-containing-the-configuration" --header "Please provide the URL to the git repository containing your configuration :") + } + + git_url + + until git clone "${IMPORTED_CONFIG_URL}" imported_configuration; do + echo -e "\n ⚠️ This did not work correctly." + + echo -e "\n Is this URL correct [y/n] ? ${IMPORTED_CONFIG_URL}" + read URL + + if [[ "${URL^^}" == "N" ]]; then + git_url + fi + + echo -e "\n You will be prompted for your credentials again. Make sure that they are correct." + done + fi + + echo "" + gum format -- \ + "➡️ To continue, you need to start the target device in a NixOS live environment : + 1. Download the NixOS iso from the **[official website](https://nixos.org/download/)**. + 2. Flash it to a USB stick. (use a flashing tool like **[Rufus](https://rufus.ie/en/#download)**, **[BalenaEtcher](https://etcher.balena.io/#download-etcher)**, **[Impression](https://flathub.org/en/apps/io.gitlab.adhami3310.Impression)**, ...) + 3. Make sure your computer allows booting from USB drives and is in UEFI mode. + 4. Boot into the NixOS live environment. + 5. Launch a terminal. Set a password using \`passwd\` and find the IP address using \`ip a\`" + + echo "" + gum confirm "Is the device ready ?" || { echo "❌ You need to prepare the device. The script cannot continue."; exit 1; } + + # LIVE TARGET SETTINGS + user_input "LIVE_TARGET_IP" " Please provide the IP address of the target host :" "For example : 192.168.1.100" "${IP_REGEX}" + user_input "LIVE_TARGET_PASSWORD" " Please enter the password for '${TARGET_USER}@${LIVE_TARGET_IP}' :" "${LIVE_TARGET_IP}'s password" "" "" "true" + + # INTERNATIONALIZATION SETTINGS + user_input "INTERNATIONALIZATION_TIMEZONE" " Please provide the wanted timezone :" "For example : Europe/Paris, Europe/Berlin, Europe/London, etc" + user_input "INTERNATIONALIZATION_LANGUAGE" " Please provide the wanted language :" "For example : French, Deutsch, English, etc" + user_input "INTERNATIONALIZATION_COUNTRY" " Please provide your country :" "For example : France, Germany, Great-Britain, etc" +} + +configuration() { + if [[ "${DEVICE_TYPE}" == "server" ]]; then + + # Users & Groups + user_input "SERVER_OWNER_NAME" " Please provide the name of the owner of this server :" "For example : Steve" + user_input "SERVER_ADMIN_EMAIL" " Please provide a valid ADMIN email address (ACME, system failures notifications, etc) :" "For example : myemail@mydomain.mytld" "${EMAIL_REGEX}" + user_input "AUTHORIZED_SSH_PUBLIC_KEY" " Please provide the SSH public key of an authorized device (or a comma-separated list) :" "For example : ssh-ed25519 AAAAC3Nzam0uYewNAbxL8Fci8 user@your-pc or ssh-* * *, ssh-* * *, etc" "${SSH_KEY_REGEX}" "Invalid SSH key format (must start with ssh-...)." + + echo -e "\n\n ➡️ You will access your services via a domain name (e.g. cloud.mydomain.com) and containers need credentials to create those subdomains" + # TRAEFIK SETTINGS + user_input "DOMAIN_NAME" " Please provide the domain name (FQDN) your home server will use :" "For example : yourdomain.com" "${DOMAIN_REGEX}" + user_input "CLOUDFLARE_DNS_API_TOKEN" " Please provide a cloudflare API token with DNS zone permission :" "For example : bA7hdvCOuXGytlNKohi3ZGtlVpf5CHpLuCMiJrE" "" "" "true" + + echo -e "\n\n ➡️ Some services will be able to send you emails. For that you need an email that supports sending emails (like Gmail for example)" + # SMTP SETTINGS + user_input "SMTP_SERVER_USERNAME" " Please provide a valid sender email address :" "For example : myemail@gmail.com" "${EMAIL_REGEX}" + user_input "SMTP_SERVER_PASSWORD" " Please provide the password of this email address :" "abcd efgh ijkl mnop" "" "" "true" + user_input "SMTP_SERVER_HOST" " Please provide the SMTP server endpoint :" "For Gmail : smtp.gmail.com" "${DOMAIN_REGEX}" "Invalid domain name format." + user_input "SMTP_SERVER_PORT" " Please provide the smtp TLS port :" "For Gmail : 587" "${PORT_REGEX}" "Invalid port number." + + echo -e "\n\n ➡️ This server will connect to your local network and you will configure its IP address\n" + # NETWORK SETTINGS + user_input "NETWORK_SUBNET" " Please provide your network subnet :" "For example 192.168.1.0/24" "${SUBNET_REGEX}" "Invalid subnet format (e.g. 192.168.1.1/24)." + user_input "NETWORK_ROUTER_IP" " Please provide the ip address of your router :" "Most likely 192.168.1.1 or 192.168.1.254" "${IP_REGEX}" "Invalid IP address format." + user_input "HOME_SERVER_IP" " Please choose the ip address that your server will use (i.e. any address in the 192.168.1.1/24 range that is not in use.) :" "For example 192.168.1.5" "${IP_REGEX}" "Invalid IP address format." + elif [[ "${DEVICE_TYPE}" == "backup" ]]; then + : + elif [[ "${DEVICE_TYPE}" == "computer" ]]; then + : + elif [[ "${DEVICE_TYPE}" == "tv" ]]; then + : + fi +} + +setup_ssh() { + echod "\n ✅ Generating new SSH key for numbus-admin..." + + chmod 700 ${EXTRA_FILES_PATH}/home/numbus-admin/.ssh/ + ssh-keygen -t "ed25519" -C "numbus-admin@numbus-server" -f "${EXTRA_FILES_PATH}/home/numbus-admin/.ssh/id_ed25519" -N "" -q + + if [[ ${DEBUG} -eq 1 ]]; then + echo -e "\n ➡️ Copying SSH key to target host '${TARGET_USER}@${LIVE_TARGET_IP}'..." + fi + + if sshpass -p "${LIVE_TARGET_PASSWORD}" ssh-copy-id -o StrictHostKeyChecking=no -i "${EXTRA_FILES_PATH}/home/numbus-admin/.ssh/id_ed25519" "${TARGET_USER}@${LIVE_TARGET_IP}"; then + if [[ ${DEBUG} -eq 1 ]]; then + echo -e "\n ✅ SSH key copied successfully" + fi + else + echo -e "\n ❌ Failed to copy SSH key. Please check the host IP and password." + exit 1 + fi +} + +services_selection() { + services_choice() { + local SERVICES_LIST=( "${1[@]}" ) + local SERVICES_DESCRIPTION=( "${2[@]}" ) + local FINAL_VARIABLE="${3}" + local HEADER="${4}" + local LIMIT="${5:---no-limit}" + + local SELECTED_SERVICES=() + local SELECTED_SERVICES_DESCRIPTION=() + + local SELECTED_SERVICES_DESCRIPTION=$(gum choose ${LIMIT} --header "${HEADER}" "${SERVICES_DESCRIPTION[@]}") + + for i in ${!SERVICES_LIST[@]}; do + if printf '%s' "${SELECTED_SERVICES_DESCRIPTION}" | grep -iq "${SERVICES_LIST[${i}]}"; then + SELECTED_SERVICES+=("${SERVICES_LIST[${i}]}") + fi + done + + export "${FINAL_VARIABLE}=(${SELECTED_SERVICES[@]})" + } + + echo -e "\n\n ➡️ You will now select the services you want installed on your server:" + + services_choice "${DNS_SERVICES_LIST[@]}" "${DNS_SERVICES_DESCRIPTION[@]}" "SELECTED_DNS_SERVICE" "Choose your preferred DNS service :" "--limit=1" + services_choice "${WEB_APPLICATIONS_LIST[@]}" "${WEB_APPLICATIONS_DESCRIPTION[@]}" "SELECTED_WEB_APPLICATIONS" "Choose the web applications you want to install :" + services_choice "${SYSTEM_SERVICES_LIST[@]}" "${SYSTEM_SERVICES_DESCRIPTION[@]}" "SELECTED_SYSTEM_SERVICES" "Choose the system services you want to install :" + + gum confirm "Do you want to edit the default subdomain of your services ?" || { echo -e "\n\n✅ Continuing..."; return 0; } + + for service in ${SELECTED_WEB_APPLICATIONS[@]} ${SELECTED_DNS_SERVICE[@]}; do + if gum confirm "Change the subdomain of ${service} ?"; then + SELECTED_WEB_APPLICATIONS_SUBDOMAIN+=( "$(gum input --placeholder "${service}" --header "Please provide the desired subdomain for ${service}:")" ) + fi + done + + return 0 +} + +disks_selection() { + gum style --border normal --margin "1" --padding "1 2" --border-foreground 212 " + ⚠️ $(gum style --foreground 212 'WARNING:') You will choose the disks to install NixOS on. + !! PLEASE MAKE SURE YOU BACKED UP ANY IMPORTANT DATA !! + !! ALL DATA WILL BE WIPED ON THE DISKS YOU CHOOSE !! + Please press CTRL+C to abort. + " + gum confirm "Do you understand and wish to proceed?" || { echo -e "\n\n❌ Aborting as requested."; exit 1; } + + echo -e "\n\n 🔎 Fetching and analyzing disks from target host... (This may take a moment)" + + if [[ "${#DISK_NAME[@]}" -eq 0 ]]; then + echo -e "\n❌ No disks found on the target host. Aborting." + exit 1 + fi + + local HEADER=$(printf " %-12s %-12s %-12s %-12s %s" "Device" "Type" "Size" "SMART" "Path") + + for i in ${!DISK_NAME[@]}; do + local GUM_PRINTED_ELEMENT=$(printf "%-12s %-12s %-12s %-12s %s" \ + "${DISK_NAME[${i}]}" "${DISK_TYPE[${i}]}" "${DISK_SIZE[${i}]}" \ + "${DISK_HEALTH[${i}]}" "${DISK_DEVPATH[${i}]}") + local GUM_PRINTED_ELEMENTS+=("${GUM_PRINTED_ELEMENT}") + done + + echo "" + gum style --foreground 212 "➡️ Please choose one (stripe) or two (mirror) disks for your NixOS boot installation :" + + local SELECTED_BOOT_DISK=$(gum choose --limit 2 --header "${HEADER}" "${GUM_PRINTED_ELEMENTS[@]}") + + for i in ${!DISK_NAME[@]}; do + if printf '%s' "$SELECTED_BOOT_DISK" | grep -iqw "${DISK_NAME[${i}]}"; then + BOOT_DISKS_ID_LIST+=("\"${DISK_ID[${i}]:-${DISK_DEVPATH[${i}]}}\"") + BOOT_DISKS_NAME+=("${DISK_NAME[${i}]}") + unset "GUM_PRINTED_ELEMENTS[${i}]" + fi + done + + echo "" + gum style --foreground 212 "➡️ Please choose data and parity disks (up to 9 total) :" + + local SELECTED_DATA_DISK=$(gum choose --limit 9 --header "$HEADER" "${GUM_PRINTED_ELEMENTS[@]}") + + for i in ${!DISK_NAME[@]}; do + if printf '%s' "$SELECTED_DATA_DISK" | grep -iq "${DISK_NAME[${i}]}"; then + DATA_DISKS_ID+=("${DISK_ID[${i}]:-${DISK_DEVPATH[${i}]}}") + DATA_DISKS_TYPE+=("${DISK_TYPE[${i}]}") + fi + done + + if [[ "${#DATA_DISKS_ID[@]}" -eq 1 ]]; then + export PARITY_DISK_NUMBER=0 + export CONTENT_DISK_NUMBER=1 + export PARITY_DISK_LIST=() + export CONTENT_DISK_LIST=("\"${DATA_DISKS_ID[0]}\"") + else + export PARITY_DISK_NUMBER=$(((${#DATA_DISKS_ID[@]} + 2) / 3)) + export CONTENT_DISK_NUMBER=$((${#DATA_DISKS_ID[@]} - PARITY_DISK_NUMBER)) + for i in $(seq 0 $(($CONTENT_DISK_NUMBER - 1))); do + CONTENT_DISK_LIST+=("\"${DATA_DISKS_ID[${i}]}\"") + done + for i in $(seq $CONTENT_DISK_NUMBER $((${#DATA_DISKS_ID[@]} - 1))); do + PARITY_DISK_LIST+=("\"${DATA_DISKS_ID[${i}]}\"") + done + fi + + if [[ "${#DATA_DISKS_ID[@]}" -gt 0 ]]; then + for i in ${!DATA_DISKS_ID[@]}; do + if [[ "${DATA_DISKS_TYPE[${i}]}" == "HDD" ]]; then + SPINDOWN_DISKS_LIST+=("\"${DATA_DISKS_ID[${i}]}\"") + fi + done + fi + + export SPINDOWN_DISKS_LIST + export BOOT_DISKS_ID_LIST + export PARITY_DISK_LIST + export CONTENT_DISK_LIST +} + +server_config_generation() { + echod "\n 📝 Generating structured settings.json..." + + # Create a temporary JSON file with all the collected variables + # This file will be read by the Nix configuration using builtins.fromJSON + jq -n \ + --arg tz "$INTERNATIONALIZATION_TIMEZONE" \ + --arg lang "$INTERNATIONALIZATION_LANGUAGE" \ + --arg owner "$SERVER_OWNER_NAME" \ + --arg ip "$HOME_SERVER_IP" \ + --arg iface "$TARGET_INTERFACE" \ + --arg router "$NETWORK_ROUTER_IP" \ + --arg domain "$DOMAIN_NAME" \ + --argjson cockpit_enabled "true" \ + --arg dns "${SELECTED_DNS_SERVICE[0]}" \ + --argjson apps "$(printf '%s\n' "${SELECTED_WEB_APPLICATIONS[@]}" | jq -R . | jq -s .)" \ + '{ + system: { + timeZone: $tz, + language: $lang, + owner: $owner + }, + network: { + ipAddress: $ip, + interface: $iface, + routerIp: $router + }, + services: { + domain: $domain, + dnsProvider: $dns, + enabledApps: $apps, + managementConsole: $cockpit_enabled + } + }' > "${EXTRA_FILES_PATH}/etc/nixos/settings.json" + + echo -e "{\n numbus.settings = builtins.fromJSON (builtins.readFile ./settings.json);\n}" > "${CONFIGURATION_PATH}" + + # Ensure the settings file is writable by the management service + # and that the directory is prepared for local git tracking + chmod 664 "${EXTRA_FILES_PATH}/etc/nixos/settings.json" +} + +# The existing network_config_generation and services_config_generation functions +# are now redundant as the logic is centralized in the JSON export. +mail_config_generation() { + echo -e "\n # Mail settings" >> ${CONFIGURATION_PATH} + echo -e " numbus.mail.enable = true;" >> ${CONFIGURATION_PATH} + echo -e " numbus.mail.userAddress = \"${SERVER_USER_EMAIL}\";" >> ${CONFIGURATION_PATH} + echo -e " numbus.mail.adminAddress = \"${SERVER_ADMIN_EMAIL}\";" >> ${CONFIGURATION_PATH} + echo -e " numbus.mail.smtpUsername = \"${SMTP_SERVER_USERNAME}\";" >> ${CONFIGURATION_PATH} + echo -e " numbus.mail.smtpPasswordPath = config.sops.secrets.smtpPassword.path;" >> ${CONFIGURATION_PATH} + + if [[ "${SMTP_SERVER_HOST}" != "smtp.gmail.com" ]]; then + echo -e " numbus.mail.smtpServer = \"${SMTP_SERVER_HOST}\";" >> ${CONFIGURATION_PATH} + fi + if [[ "${SMTP_SERVER_PORT}" != "587" ]]; then + echo -e " numbus.mail.smtpPort = ${SMTP_SERVER_PORT};" >> ${CONFIGURATION_PATH} + fi +} + +disk_config_generation() { + echo -e "\n # Hardware settings" >> ${CONFIGURATION_PATH} + if [[ "${TARGET_PCIE_CORAL}" == "true" ]]; then + echo " numbus.hardware.pcie-coral.enable = true;" >> ${CONFIGURATION_PATH} + fi + echo -e " numbus.hardware.bootDisksList = [ ${BOOT_DISKS_ID_LIST[@]} ];" >> ${CONFIGURATION_PATH} + echo -e " numbus.hardware.dataDisksList = [ ${CONTENT_DISK_LIST[@]} ];" >> ${CONFIGURATION_PATH} + echo -e " numbus.hardware.parityDisksList = [ ${PARITY_DISK_LIST[@]} ];" >> ${CONFIGURATION_PATH} + echo -e " numbus.hardware.spindownDisksList = [ ${SPINDOWN_DISKS_LIST[@]} ];" >> ${CONFIGURATION_PATH} + echo "}" >> ${CONFIGURATION_PATH} +} + +keys_generation() { + for i in $(seq 1 "${#BOOT_DISKS_ID_LIST[@]}"); do + PASS="$(xkcdpass)" + echo -n "$PASS" > "${EXTRA_FILES_PATH}/etc/secrets/disks/boot-${i}" + chmod 600 "${EXTRA_FILES_PATH}/etc/secrets/disks/boot-${i}" + ssh_to_host 'bash -s' << EOF +echo "$LIVE_TARGET_PASSWORD" | sudo -S mkdir -p /etc/secrets/disks/ +echo "$LIVE_TARGET_PASSWORD" | sudo -S bash -c "printf '%s' '$PASS' > /etc/secrets/disks/boot-${i}" +echo "$LIVE_TARGET_PASSWORD" | sudo -S chmod 600 /etc/secrets/disks/boot-${i} +EOF + done + for i in $(seq 1 "$CONTENT_DISK_NUMBER"); do + PASS="$(xkcdpass)" + echo -n "$PASS" > "${EXTRA_FILES_PATH}/etc/secrets/disks/content-${i}" + chmod 600 "${EXTRA_FILES_PATH}/etc/secrets/disks/content-${i}" + ssh_to_host 'bash -s' << EOF +echo "$LIVE_TARGET_PASSWORD" | sudo -S bash -c "printf '%s' '$PASS' > /etc/secrets/disks/content-${i}" +echo "$LIVE_TARGET_PASSWORD" | sudo -S chmod 600 /etc/secrets/disks/content-${i} +EOF + done + for i in $(seq 1 "$PARITY_DISK_NUMBER"); do + PASS="$(xkcdpass)" + echo -n "$PASS" > "${EXTRA_FILES_PATH}/etc/secrets/disks/parity-${i}" + chmod 600 "${EXTRA_FILES_PATH}/etc/secrets/disks/parity-${i}" + ssh_to_host 'bash -s' << EOF +echo "$LIVE_TARGET_PASSWORD" | sudo -S bash -c "printf '%s' '$PASS' > /etc/secrets/disks/parity-${i}" +echo "$LIVE_TARGET_PASSWORD" | sudo -S chmod 600 /etc/secrets/disks/parity-${i} +EOF + done + + local SSH_KEYS_FORMATTED="" + if [[ "$(declare -p AUTHORIZED_SSH_PUBLIC_KEY 2>/dev/null)" =~ "declare -a" ]]; then + for key in "${AUTHORIZED_SSH_PUBLIC_KEY[@]}"; do + SSH_KEYS_FORMATTED+=" $key"$'\n' + done + else + SSH_KEYS_FORMATTED=" $AUTHORIZED_SSH_PUBLIC_KEY"$'\n' + fi + export SSH_KEYS_FORMATTED + + echo -e "\n ✅ Generating sops-nix keys..." + ssh-to-age -private-key -i ${EXTRA_FILES_PATH}/home/numbus-admin/.ssh/id_ed25519 > ${EXTRA_FILES_PATH}/var/lib/sops-nix/key.txt + export SOPS_PUBLIC_KEY=$(age-keygen -y ${EXTRA_FILES_PATH}/var/lib/sops-nix/key.txt) + + echo -e "\n ✅ Generating sops-nix configuration files..." + envsubst < templates/nix-config/sops-nix/.sops.yaml > ${EXTRA_FILES_PATH}/etc/nixos/.sops.yaml + + echo -e "\n ✅ Encrypting secrets in the correct file..." + envsubst < "templates/nix-config/sops-nix/secrets.yaml" \ + | sops encrypt --filename-override secrets.yaml \ + --input-type yaml --output-type yaml \ + --age $SOPS_PUBLIC_KEY \ + --output ${EXTRA_FILES_PATH}/etc/nixos/secrets/secrets.yaml +} + +sum_up() { + DISK_RECAP_CONTENT=$(cat << EOF +### Disk Configuration Summary + +Please review the selected disk layout before proceeding. + +**Boot Disks (${#BOOT_DISKS_ID_LIST[@]}) :** + +* **Boot 1:** \`${BOOT_DISKS_ID_LIST[0]}\` +$( [[ -n "${BOOT_DISKS_ID_LIST[1]:-}" ]] && echo "* **Boot 2:** \`${BOOT_DISKS_ID_LIST[1]}\`" ) + +**Data Disks ($CONTENT_DISK_NUMBER) :** + +$( [[ $CONTENT_DISK_NUMBER -gt 0 ]] && j=1 && for i in $(seq 0 $(($CONTENT_DISK_NUMBER - 1))); do echo "* **Data ${j}:** \`${DATA_DISKS_ID[${i}]}\`" && j=$((j + 1)); done ) +$( [[ $CONTENT_DISK_NUMBER -eq 0 ]] && echo "* *Not configured*" ) + +**Parity Disks ($PARITY_DISK_NUMBER) :** + +$( [[ $PARITY_DISK_NUMBER -gt 0 ]] && j=1 && for i in $(seq $CONTENT_DISK_NUMBER $((${#DATA_DISKS_ID[@]} - 1))); do echo "* **Parity ${j}:** \`${DATA_DISKS_ID[${i}]}\`" && j=$((j + 1)); done ) +$( [[ $PARITY_DISK_NUMBER -eq 0 ]] && echo "* *Not configured*" ) + +EOF +) + + gum style --border normal --margin "1" --padding "1 2" --border-foreground 212 "$(gum format <<< "${DISK_RECAP_CONTENT}")" + gum confirm "➡️ Proceed with this disk configuration?" || { echo -e "\n\n❌ Aborting as requested."; exit 1; } + + SERVICES_RECAP_CONTENT=$(cat << EOF +### Services Configuration Summary + +Please review the selected services before proceeding. + +**DNS Service (${#SELECTED_DNS_SERVICE[@]}) :** + +$(echo "* \`${SELECTED_DNS_SERVICE[0]^}\`") + +**Web Applications (${#SELECTED_WEB_APPLICATIONS[@]}) :** + +$(for app in "${SELECTED_WEB_APPLICATIONS[@]}"; do echo "* \`${app^}\`"; done) + +**System Services (${#SELECTED_SYSTEM_SERVICES[@]}) :** + +$(for service in "${SELECTED_SYSTEM_SERVICES[@]}"; do echo "* \`${service^}\`"; done) + +EOF +) + + gum style --border normal --margin "1" --padding "1 2" --border-foreground 212 "$(gum format <<< "${SERVICES_RECAP_CONTENT}")" + gum confirm "➡️ Proceed with this services configuration?" || { echo -e "\n\n❌ Aborting as requested."; exit 1; } + + DISK_RECAP_CONTENT=$(cat << EOF +### Secrets Summary + +Please save the following secrets to a secure place (i.e. your local password manager, or a hidden sheet of paper). + +**Boot Disks (${#BOOT_DISKS_ID_LIST[@]}) :** + +* **Disk 1 Secret Key :** \`$( cat ${EXTRA_FILES_PATH}/etc/secrets/disks/boot-1 )\` +$( [[ -n "${BOOT_DISKS_ID_LIST[1]:-}" ]] && echo "* **Disk 2 secret key :** \`$( cat ${EXTRA_FILES_PATH}/etc/secrets/disks/boot-2 )\`" ) + +**Data Disks ($CONTENT_DISK_NUMBER):** + +$( [[ $CONTENT_DISK_NUMBER -eq 0 ]] && echo "* *Not configured*" ) +$( [[ $CONTENT_DISK_NUMBER -gt 0 ]] && j=1 && for i in $(seq 0 $(($CONTENT_DISK_NUMBER - 1))); do echo "* **Disk ${j} Secret Key :** \`$( cat ${EXTRA_FILES_PATH}/etc/secrets/disks/content-${j} )\`" && j=$((j + 1)); done ) + +**Parity Disks ($PARITY_DISK_NUMBER):** + +$( [[ $PARITY_DISK_NUMBER -eq 0 ]] && echo "* *Not configured*" ) +$( [[ $PARITY_DISK_NUMBER -gt 0 ]] && j=1 && for i in $(seq $CONTENT_DISK_NUMBER $((${#DATA_DISKS_ID[@]} - 1))); do echo "* **Disk ${j} Secret Key :** \`$( cat ${EXTRA_FILES_PATH}/etc/secrets/disks/parity-${j} )\`" && j=$((j + 1)); done ) + +EOF +) + + gum style --border normal --margin "1" --padding "1 2" --border-foreground 212 "$(gum format <<< "${DISK_RECAP_CONTENT}")" + gum confirm "✅ I have stored these credentials in a safe place" || { echo -e "\n\n❌ Please store these credentials in a safe place as you will need them later."; exit 1; } + + gum confirm "➡️ Would you like to manually edit the configuration (⚠️ advanced users only)" || { echo -e "\n\n✅ continuing with the installation..."; return 0; } + + nano ${EXTRA_FILES_PATH}/etc/nixos/configuration.nix +} + +cloudflare_dns_setup() { + gum confirm "➡️ This script can automatically create DNS records for your services. Proceed? (recommended)" || { echo -e "\n\n ⚠️ skipping the DNS records creation step..."; return 0; } + + local ZONE_ID + local RECORD_COUNT + local IS_MATCHING + local DNS_RECORDS + + create_records() { + local SUBDOMAIN="${1}" + local CREATION_STATUS + + CREATION_STATUS=$(curl -s -X POST "https://api.cloudflare.com/client/v4/zones/${ZONE_ID}/dns_records" \ + -H "Authorization: Bearer ${CLOUDFLARE_DNS_API_TOKEN}" \ + -H "Content-Type: application/json" \ + --data "{\"type\":\"A\",\"name\":\"${SUBDOMAIN}\",\"content\":\"${HOME_SERVER_IP}\",\"ttl\":1,\"proxied\":false}" | jq -r '.success') + + if [[ "${CREATION_STATUS}" == "true" ]]; then + echo " ✅ Successfully created a DNS record for ${SUBDOMAIN}" + else + echo -e "❌ Failed to create a DNS record for ${SUBDOMAIN}. Check documentation to \n + learn how you can create them manually." + fi + } + + erase_records() { + local SUBDOMAIN="${1}" + + gum style --border normal --margin "1" --padding "1 2" --border-foreground 212 " + ⚠️ $(gum style --foreground 212 'WARNING:') One or more existing type A DNS records found for \`${SUBDOMAIN}\`. + This script can clear those DNS records for you and create the correct ones needed for the server. + If you are unsure that these records are actually in use, please select \"no\"." + gum confirm "Select \"yes\" to clear ALL EXISTING type A DNS records for this subdomain and automatically create the correct ones." \ + || { echo -e "\n ⚠️ DNS records for ${SUBDOMAIN} will not be updated"; return 0; } + + RECORD_IDS=$(curl -s -X GET "https://api.cloudflare.com/client/v4/zones/${ZONE_ID}/dns_records?name=${SUBDOMAIN}&type=A" \ + -H "Authorization: Bearer ${CLOUDFLARE_DNS_API_TOKEN}" \ + -H "Content-Type: application/json" | jq -r '.result[].id') + + for id in ${RECORD_IDS}; do + curl -s -X DELETE "https://api.cloudflare.com/client/v4/zones/${ZONE_ID}/dns_records/${id}" \ + -H "Authorization: Bearer ${CLOUDFLARE_DNS_API_TOKEN}" \ + -H "Content-Type: application/json" > /dev/null 2>&1 + done + + create_records "${SUBDOMAIN}" + } + + echo -e "\n\n ☁️ Configuring Cloudflare DNS records..." + + i=0 + for service in "${SELECTED_WEB_APPLICATIONS[@]}"; do + if [[ -n "${SELECTED_WEB_APPLICATIONS_SUBDOMAIN[${i}]:-}" ]]; then + SELECTED_SERVICES_DNS+=( "${SELECTED_WEB_APPLICATIONS_SUBDOMAIN[${i}]}.${DOMAIN_NAME}" ) + else + SELECTED_SERVICES_DNS+=( "${service}.${DOMAIN_NAME}" ) + fi + i=$((i + 1)) + [[ "${service}" == "nextcloud" ]] && SELECTED_SERVICES_DNS+=( "onlyoffice.${DOMAIN_NAME}" "whiteboard.${DOMAIN_NAME}" ) + done + + if [[ -n "${SELECTED_DNS_SERVICE_SUBDOMAIN[0]:-}" ]]; then + SELECTED_SERVICES_DNS+=( "${SELECTED_DNS_SERVICE_SUBDOMAIN[0]}.${DOMAIN_NAME}" ) + else + SELECTED_SERVICES_DNS+=( "${SELECTED_DNS_SERVICE}.${DOMAIN_NAME}" ) + fi + + # Get Zone ID + ZONE_ID=$(curl -s -X GET "https://api.cloudflare.com/client/v4/zones?name=${DOMAIN_NAME}" \ + -H "Authorization: Bearer ${CLOUDFLARE_DNS_API_TOKEN}" \ + -H "Content-Type: application/json" | jq -r '.result[0].id') + + if [[ "${ZONE_ID}" == "null" || -z "${ZONE_ID}" ]]; then + echo -e "\n\n ⚠️ Could not fetch Zone ID for ${DOMAIN_NAME}. Please check your Cloudflare \"DNS ZONE\" API token" + echo "Check the Numbus-Server documentation to learn how to get one." + fi + + # Check for existing records and create them if non-existent + for service_domain in "${SELECTED_SERVICES_DNS[@]}"; do + DNS_RECORDS=$(curl -s -X GET "https://api.cloudflare.com/client/v4/zones/${ZONE_ID}/dns_records?name=${service_domain}&type=A" \ + -H "Authorization: Bearer ${CLOUDFLARE_DNS_API_TOKEN}" \ + -H "Content-Type: application/json") + + RECORD_COUNT=$(echo "${DNS_RECORDS}" | jq '.result | length') + + if [[ "${RECORD_COUNT}" -eq 0 ]]; then + echo -e "\n ⚠️ No DNS record found for ${service_domain}" + create_records "${service_domain}" + elif [[ "${RECORD_COUNT}" -eq 1 ]]; then + if [[ $(echo "${DNS_RECORDS}" | jq ".result[0].content == \"${HOME_SERVER_IP}\"") == "true" ]]; then + echo -e "\n ✅ DNS record already configured for ${service_domain}" + else + echo -e "\n ⚠️ No DNS record found for ${service_domain}" + erase_records "${service_domain}" + fi + elif [[ "${RECORD_COUNT}" -gt 1 ]]; then + erase_records "${service_domain}" + fi + done +} + +deploy() { + git -C . add -f "${EXTRA_FILES_PATH}/" + git -C . add -f "templates/" + git -C . add -f "deploy.conf" + + # Initialize a git repo in the configuration to be deployed + # This allows the Management UI on the appliance to commit changes + # and provide a local history/rollback UI to the user. + if [ ! -d "${EXTRA_FILES_PATH}/etc/nixos/.git" ]; then + git -C "${EXTRA_FILES_PATH}/etc/nixos" init -q + git -C "${EXTRA_FILES_PATH}/etc/nixos" add . + git -C "${EXTRA_FILES_PATH}/etc/nixos" commit -m "Initial bootstrap via Numbus Deploy" -q + fi + + echo -e "\n\n🔄 Deploying to the remote server..." + nix flake update --flake ./${EXTRA_FILES_PATH}/etc/nixos + nix run github:nix-community/nixos-anywhere -- \ + --flake ${EXTRA_FILES_PATH}/etc/nixos#numbus-server \ + --extra-files ${EXTRA_FILES_PATH} \ + --chown "/home/numbus-admin/" 1000:1000 \ + --target-host ${TARGET_USER}@${LIVE_TARGET_IP} + + echo -e "\n\n✅ Installation successfull !" + sleep 1 +} + +postrun_action() { + TARGET_USER="numbus-admin" + LIVE_TARGET_IP="${HOME_SERVER_IP}" + LIVE_TARGET_PASSWORD="changeMe!" + + echo -e "\n\n Now the remote machine will reboot. You will need to input the boot disk(s) passphrase. + This will be the only time you will have to do so, it will be automatic in the future." + + gum spin --title "Rebooting the remote..." -- sleep 120 + + gum confirm "➡️ Select \"yes\" once the machine rebooted and you unlocked the disks." || { echo -e "\n\n❌ Aborting as requested."; exit 1; } + + FOUND="false" + i="0" + while [[ "${FOUND}" == "false" ]]; do + if ping -c1 -W1 $HOME_SERVER_IP >/dev/null 2>&1; then + FOUND="true" + echo -e "\n✅ Ping ${HOME_SERVER_IP} successful ! Continuing..." + else + i=$((i + 1)) + if [[ "${i}" -gt 150 ]]; then + echo -e "\n\n❌ Could not connect to the server after 150 retries. \ + This is most likely due to a networking issue. Please double check your network settings. Aborting." + exit 1 + fi + fi + done + + if [[ "${TARGET_TPM}" == "true" && ${TARGET_TPM_VERSION} -eq 2 ]]; then + gum style --border normal --margin "1" --padding "1 2" --border-foreground 212 " + A TPM version 2 has been detected on the system. You can choose to enable automatic disk decryption on boot. + Enabling automatic disk decryption on boot means that you won't have to enter your disk password everytime you start your server. + This comes in very handy if you don't plan to leave your server accessible with a keyboard or if you don't have an IP KVM. + + Note : This feature is currently vulnerable to on-site attacks. This means that an attacker with physical access to your machine + could steal the password from the TPM, and therefore have access to all your date. + + Do you want to enable automatic disk decryption on boot ?" + + if gum confirm "➡️ I understand, 'yes' to proceed."; then + sshpass -p "${LIVE_TARGET_PASSWORD}" ssh -i "${EXTRA_FILES_PATH}/home/numbus-admin/.ssh/id_ed25519" "${TARGET_USER}@${LIVE_TARGET_IP}" 'bash -s' << EOF +echo "Enrolling boot disk key to TPM..." + +BOOT_DISKS_NAME=(${BOOT_DISKS_NAME[@]}) +DEBUG=${DEBUG} +DISK_PATH="" +j=1 + +for i in \${!BOOT_DISKS_NAME[@]}; do + if echo "\${BOOT_DISKS_NAME[\${i}]}" | grep -iq "nvme"; then + [[ "\${DEBUG}" == "true" ]] && echo "NVMe detected..." + DISK_PATH="/dev/\${BOOT_DISKS_NAME[\${i}]}p2" + else + [[ "\${DEBUG}" == "true" ]] && echo "Non-NVMe drive detected..." + DISK_PATH="/dev/\${BOOT_DISKS_NAME[\${i}]}2" + fi + [[ "\${DEBUG}" == "true" ]] && echo "Issuing enroll command for disk \${DISK_PATH}..." + echo ${LIVE_TARGET_PASSWORD} | sudo -S systemd-cryptenroll --tpm2-device=auto --tpm2-pcrs=0+7 --unlock-key-file=/etc/secrets/disks/boot-\${j} \${DISK_PATH} + j=\$((j + 1)) +done + +echo "Getting PCRS 15 hash..." +PCR_HASH=\$(echo ${LIVE_TARGET_PASSWORD} | sudo -S systemd-analyze pcrs 15 --json=short) + +echo ${LIVE_TARGET_PASSWORD} | sudo -S sed -i "s|PCR_HASH|\${PCR_HASH}|" /etc/nixos/configuration.nix +EOF + else + echo "Skipping TPM configuration." + fi + else + echo "No supported TPM detected (TPM version 2 required). Skipping TPM configuration." + fi + + gum style --border normal --margin "1" --padding "1 2" --border-foreground 212 " + ⚠️ $(gum style --foreground 212 'WARNING:') You will now set the password of the numbus-admin user. +You will almost never user it. Consider using a very strong password : you can write it down +securely on a hidden sheet of paper or add it to your password manager (locally with Passbolt + with any other online password manager provider)." + + gum confirm "➡️ I understand, 'yes' to proceed." || { echo -e "\n\n❌ Aborting as requested."; exit 1; } + + echo $LIVE_TARGET_PASSWORD | sudo -S passwd numbus-admin +} + +nix_update() { + echo -e "\n\n🔄 Updating NixOS on the remote server..." + + nixos-rebuild --target-host numbus-admin@${LIVE_TARGET_IP} \ + --use-remote-sudo switch --flake ${EXTRA_FILES_PATH}/etc/nixos#numbus-server +} +# --- MAIN FUNCTIONS ---< + + + +# --- DEFAULT VARIABLES ---> +WEBSERVER_PORT=${WEBSERVER_PORT:-8088} + +LIVE_DATA_PATH="/run/user/$(id -u)/numbus/web/live_settings.json" +HARDWARE_DATA_PATH="/run/user/$(id -u)/numbus/web/hardware.json" + +CONFIG_FILE="../config/numbus.yaml" + +TARGET_USER="nixos" + +TMP_FILES_PATH="/run/user/$(id -u)/numbus-$(date +"%Y-%m-%d-%Hh%M")" +EXTRA_FILES_PATH="${TMP_FILES_PATH}/config" + +if [[ ${DEBUG-0} -eq 1 ]]; then + FILES_CP_FLAGS="vau" + FILES_RM_FLAGS="vf" + DIR_RM_FLAGS="rvf" + MKDIR_FLAGS="pv" + MV_FLAGS="vu" +else + DEBUG=0 + FILES_CP_FLAGS="au" + FILES_RM_FLAGS="f" + DIR_RM_FLAGS="rf" + MKDIR_FLAGS="p" + MV_FLAGS="u" +fi + +IP_REGEX='^([0-9]{1,3}\.){3}[0-9]{1,3}$' +SUBNET_REGEX='^([0-9]{1,3}\.){3}[0-9]{1,3}/[0-9]{1,2}$' +DOMAIN_REGEX='^([a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$' +EMAIL_REGEX='^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$' +PORT_REGEX='^[0-9]{1,5}$' +SSH_KEY_REGEX='^ssh-[a-z0-9]+ [A-Za-z0-9+/]+.*' +PHONE_REGEX='^\+[1-9][0-9]{7,14}$' + +GUM_INPUT_PADDING="1 1" +GUM_INPUT_HEADER_FOREGROUND="212" +GUM_INPUT_CURSOR_FOREGROUND="212" +GUM_INPUT_TIMEOUT="3600" +# --- DEFAULTS VARIABLES ---< + + + +# --- PRE MAIN LOGIC ---> +set -euo pipefail +clear + +trap cleanup EXIT +compatibility_check +# --- PRE MAIN LOGIC ---< + + + +# --- MAIN LOGIC ---> +echo """ + _ ____ ____ ______ __ ______ + / |/ / / / / |/ / _ )/ / / / __/ + / / /_/ / /|_/ / _ / /_/ /\ \ +/_/|_/\____/_/ /_/____/\____/___/ +""" + +DEPLOYMENT_STRATEGY=$(gum choose --header "Choose your preferred deployment strategy :" \ +"I don't have a configuration" \ +"I have a valid configuration hosted on a Git platform") + +if [[ "${DEPLOYMENT_STRATEGY}" == "I don't have a configuration" ]]; then + BRIDGE_SCRIPT="../web/logic/interactive.py" + launch_gui +else + DEPLOYMENT_MODE=$(gum choose --header "Choose your preferred deployment mode :" \ + "Through my web browser (Recommended for beginners)" \ + "Through my terminal (TUI)") + if [[ "${DEPLOYMENT_MODE}" == "Through my web browser (Recommended for beginners)" ]]; then + BRIDGE_SCRIPT="../web/logic/non-interactive.py" + launch_gui + else + launch_tui + fi +fi \ No newline at end of file diff --git a/script/terminology.md b/script/terminology.md new file mode 100644 index 0000000..b9fd977 --- /dev/null +++ b/script/terminology.md @@ -0,0 +1,6 @@ +# Terminology for the variables used + +|Variable|Meaning|Possible values| +|-|-------|-| +|DEPLOYMENT_STRATEGY|Either deploy the machine with a config you already have or let the script guide you through the config options|**interactive** or **non-interactive**| +|DEPLOYMENT_MODE|Either configure the machine through your terminal (TUI) or through a slick web UI (GUI)|**TUI** (only available for **non-interactive** strategy) or **GUI**| \ No newline at end of file diff --git a/deploy/GUI/web/index.html b/web/index.html similarity index 100% rename from deploy/GUI/web/index.html rename to web/index.html diff --git a/web/logic/interactive.py b/web/logic/interactive.py new file mode 100644 index 0000000..3d9774e --- /dev/null +++ b/web/logic/interactive.py @@ -0,0 +1,55 @@ +import http.server +import json +import os +import sys + +### Variables --> +SECRET_PATH = "/run/user/{}/numbus".format(os.getuid()) if os.path.exists("/run/user/{}".format(os.getuid())) else "../secrets" +os.makedirs(SECRET_PATH, exist_ok=True) +LOGS_DIR = "../web/logs/" +PAGES_DIR = "../web/pages/" +CONFIG_DIR = "../web/config/" +SIGNALS_DIR = "../web/signal/" +### <-- Variables + +class BridgeHandler(http.server.SimpleHTTPRequestHandler): + def do_GET(self): + # Route for logs: /logs?type=out or /logs?type=err + if self.path.startswith('/logs'): + log_type = "out" if "type=err" not in self.path else "err" + log_path = os.path.join(LOGS_DIR, f'deploy-{log_type}.log') + + self.send_response(200) + self.send_header('Content-type', 'text/plain') + self.send_header('Access-Control-Allow-Origin', '*') + self.end_headers() + + if os.path.exists(log_path): + with open(log_path, 'r') as f: + # Read last 50 lines for better context during errors + self.wfile.write("".join(f.readlines()[-50:]).encode()) + return + return http.server.SimpleHTTPRequestHandler.do_GET(self) + + def do_POST(self): + content_length = int(self.headers['Content-Length']) + post_data = self.rfile.read(content_length) + + if self.path == '/discovery': + # Store secrets in memory-backed filesystem + with open(os.path.join(SECRET_PATH, "live_settings.json"), "wb") as f: + f.write(post_data) + self.send_response(200) + self.end_headers() + # Signal Bash that discovery data is ready + with open(os.path.join(SIGNALS_DIR, ".discovery_ready"), "w") as f: f.write("1") + + elif self.path == '/deploy': + with open(os.path.join(CONFIG_DIR, "numbus.yaml"), "wb") as f: + f.write(post_data) + self.send_response(200) + self.end_headers() + with open(os.path.join(SIGNALS_DIR, ".deploy_signal"), "w") as f: f.write("1") + +os.chdir(PAGES_DIR) +http.server.HTTPServer(('localhost', 8088), BridgeHandler).serve_forever() \ No newline at end of file diff --git a/deploy/GUI/web/logic/bridge.py b/web/logic/non-interactive.py similarity index 100% rename from deploy/GUI/web/logic/bridge.py rename to web/logic/non-interactive.py diff --git a/deploy/GUI/web/media/dark/numbus-backup-server-dark.svg b/web/media/dark/numbus-backup-server-dark.svg similarity index 100% rename from deploy/GUI/web/media/dark/numbus-backup-server-dark.svg rename to web/media/dark/numbus-backup-server-dark.svg diff --git a/deploy/GUI/web/media/dark/numbus-computer-dark.svg b/web/media/dark/numbus-computer-dark.svg similarity index 100% rename from deploy/GUI/web/media/dark/numbus-computer-dark.svg rename to web/media/dark/numbus-computer-dark.svg diff --git a/deploy/GUI/web/media/dark/numbus-server-dark.svg b/web/media/dark/numbus-server-dark.svg similarity index 100% rename from deploy/GUI/web/media/dark/numbus-server-dark.svg rename to web/media/dark/numbus-server-dark.svg diff --git a/deploy/GUI/web/media/dark/numbus-template-dark.svg b/web/media/dark/numbus-template-dark.svg similarity index 100% rename from deploy/GUI/web/media/dark/numbus-template-dark.svg rename to web/media/dark/numbus-template-dark.svg diff --git a/deploy/GUI/web/media/dark/numbus-tv-dark.svg b/web/media/dark/numbus-tv-dark.svg similarity index 100% rename from deploy/GUI/web/media/dark/numbus-tv-dark.svg rename to web/media/dark/numbus-tv-dark.svg diff --git a/deploy/GUI/web/media/favicon.ico b/web/media/favicon.ico similarity index 100% rename from deploy/GUI/web/media/favicon.ico rename to web/media/favicon.ico diff --git a/deploy/GUI/web/media/light/numbus-backup-server-light.svg b/web/media/light/numbus-backup-server-light.svg similarity index 100% rename from deploy/GUI/web/media/light/numbus-backup-server-light.svg rename to web/media/light/numbus-backup-server-light.svg diff --git a/deploy/GUI/web/media/light/numbus-computer-light.svg b/web/media/light/numbus-computer-light.svg similarity index 100% rename from deploy/GUI/web/media/light/numbus-computer-light.svg rename to web/media/light/numbus-computer-light.svg diff --git a/deploy/GUI/web/media/light/numbus-server-light.svg b/web/media/light/numbus-server-light.svg similarity index 100% rename from deploy/GUI/web/media/light/numbus-server-light.svg rename to web/media/light/numbus-server-light.svg diff --git a/deploy/GUI/web/media/light/numbus-template-light.svg b/web/media/light/numbus-template-light.svg similarity index 100% rename from deploy/GUI/web/media/light/numbus-template-light.svg rename to web/media/light/numbus-template-light.svg diff --git a/deploy/GUI/web/media/light/numbus-tv-light.svg b/web/media/light/numbus-tv-light.svg similarity index 100% rename from deploy/GUI/web/media/light/numbus-tv-light.svg rename to web/media/light/numbus-tv-light.svg diff --git a/deploy/GUI/web/media/logo.png b/web/media/logo.png similarity index 100% rename from deploy/GUI/web/media/logo.png rename to web/media/logo.png diff --git a/deploy/GUI/web/pages/configuration.html b/web/pages/configuration.html similarity index 100% rename from deploy/GUI/web/pages/configuration.html rename to web/pages/configuration.html diff --git a/web/pages/installation.html b/web/pages/installation.html new file mode 100644 index 0000000..e69de29 diff --git a/web/pages/post-installation.html b/web/pages/post-installation.html new file mode 100644 index 0000000..e69de29 diff --git a/deploy/GUI/web/pages/preparation.html b/web/pages/preparation.html similarity index 100% rename from deploy/GUI/web/pages/preparation.html rename to web/pages/preparation.html