From b9d813cde194c978178a6ded8d81ef3a176209c7 Mon Sep 17 00:00:00 2001 From: Erich Blume Date: Wed, 18 Feb 2026 08:24:25 -0800 Subject: [PATCH] Add NixOS configuration for ringtail workstation (#207) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary - NixOS flake for ringtail (gaming/compute workstation, RTX 4080) in `nixos/ringtail/` - Declarative disk partitioning via disko (GPT, 512M EFI + ext4 root on NVMe) - NVIDIA proprietary drivers, sway/Wayland desktop, greetd, PipeWire, Steam - Tailscale integration for tailnet connectivity - Ansible playbook + `mise run provision-ringtail` for ongoing management - Pulumi auth key (`tag:homelab`, `tag:blumeops`) for tailnet bootstrap ## Deployment Order 1. **Merge PR** 2. `pulumi up` in tailscale stack → creates auth key 3. Retrieve auth key: `pulumi stack output ringtail_authkey --show-secrets` 4. On ringtail NixOS installer: - `nix run github:nix-community/disko -- --mode disko /tmp/disk-config.nix` (or from cloned repo) - `nixos-install --flake github:eblume/blumeops?dir=nixos/ringtail#ringtail` 5. Reboot, `tailscale up --auth-key=` 6. Verify: `tailscale status`, SSH from gilbert ## Test plan - [ ] Review NixOS configuration for completeness - [ ] Verify disko partition layout matches ringtail hardware - [ ] Run `pulumi preview` for tailscale stack - [ ] Install NixOS on ringtail - [ ] Confirm tailscale connectivity - [ ] Confirm sway desktop works - [ ] Test `mise run provision-ringtail` for ongoing management 🤖 Generated with [Claude Code](https://claude.com/claude-code) Reviewed-on: https://forge.ops.eblu.me/eblume/blumeops/pulls/207 --- ansible/inventory/hosts.yml | 3 + ansible/playbooks/ringtail.yml | 25 +++++ .../feature-ringtail-nixos.feature.md | 1 + mise-tasks/provision-ringtail | 9 ++ nixos/ringtail/configuration.nix | 104 ++++++++++++++++++ nixos/ringtail/disk-config.nix | 84 ++++++++++++++ nixos/ringtail/flake.nix | 23 ++++ nixos/ringtail/hardware-configuration.nix | 18 +++ pulumi/tailscale/__main__.py | 15 ++- 9 files changed, 281 insertions(+), 1 deletion(-) create mode 100644 ansible/playbooks/ringtail.yml create mode 100644 docs/changelog.d/feature-ringtail-nixos.feature.md create mode 100755 mise-tasks/provision-ringtail create mode 100644 nixos/ringtail/configuration.nix create mode 100644 nixos/ringtail/disk-config.nix create mode 100644 nixos/ringtail/flake.nix create mode 100644 nixos/ringtail/hardware-configuration.nix diff --git a/ansible/inventory/hosts.yml b/ansible/inventory/hosts.yml index b69f7a0..73746bf 100644 --- a/ansible/inventory/hosts.yml +++ b/ansible/inventory/hosts.yml @@ -5,6 +5,9 @@ all: hosts: indri: ansible_host: indri + ringtail: + ansible_host: ringtail + ansible_user: eblume workstations: hosts: gilbert: diff --git a/ansible/playbooks/ringtail.yml b/ansible/playbooks/ringtail.yml new file mode 100644 index 0000000..3234f77 --- /dev/null +++ b/ansible/playbooks/ringtail.yml @@ -0,0 +1,25 @@ +--- +- name: Configure ringtail (NixOS) + hosts: ringtail + become: true + + tasks: + - name: Ensure blumeops repo is present + ansible.builtin.git: + repo: "https://forge.ops.eblu.me/eblume/blumeops.git" + dest: /etc/blumeops + version: main + register: _repo + + - name: Rebuild NixOS + ansible.builtin.command: + cmd: nixos-rebuild switch --flake /etc/blumeops/nixos/ringtail#ringtail + register: _rebuild + changed_when: "'activating the configuration' in _rebuild.stdout" + when: _repo.changed + + - name: Verify tailscale is connected + ansible.builtin.command: tailscale status --self --json + register: _ts_status + changed_when: false + failed_when: "'Running' not in _ts_status.stdout" diff --git a/docs/changelog.d/feature-ringtail-nixos.feature.md b/docs/changelog.d/feature-ringtail-nixos.feature.md new file mode 100644 index 0000000..035afbd --- /dev/null +++ b/docs/changelog.d/feature-ringtail-nixos.feature.md @@ -0,0 +1 @@ +Add NixOS configuration for ringtail (gaming/compute workstation with RTX 4080). Includes declarative disk partitioning via disko, NVIDIA drivers, sway/Wayland desktop, Steam, Tailscale, and Ansible-driven provisioning. diff --git a/mise-tasks/provision-ringtail b/mise-tasks/provision-ringtail new file mode 100755 index 0000000..cb5effe --- /dev/null +++ b/mise-tasks/provision-ringtail @@ -0,0 +1,9 @@ +#!/usr/bin/env bash +#MISE description="Run ansible playbook to provision ringtail (NixOS)" + +set -euo pipefail + +export MISE_TASK_OUTPUT=interleave + +cd ansible +ansible-playbook playbooks/ringtail.yml "$@" diff --git a/nixos/ringtail/configuration.nix b/nixos/ringtail/configuration.nix new file mode 100644 index 0000000..45d0449 --- /dev/null +++ b/nixos/ringtail/configuration.nix @@ -0,0 +1,104 @@ +{ config, pkgs, ... }: + +{ + # Bootloader + boot.loader.systemd-boot.enable = true; + boot.loader.efi.canTouchEfiVariables = true; + + # Networking + networking.hostName = "ringtail"; + networking.networkmanager.enable = true; + + # Time zone + time.timeZone = "America/Los_Angeles"; + + # Locale + i18n.defaultLocale = "en_US.UTF-8"; + + # NVIDIA proprietary drivers + hardware.graphics.enable = true; + services.xserver.videoDrivers = [ "nvidia" ]; + hardware.nvidia = { + modesetting.enable = true; + open = false; # Use proprietary driver for RTX 4080 + nvidiaSettings = true; + package = config.boot.kernelPackages.nvidiaPackages.stable; + }; + + # Wayland / Sway + programs.sway = { + enable = true; + wrapperFeatures.gtk = true; + extraPackages = with pkgs; [ + swaylock + swayidle + wezterm # terminal + wmenu # app launcher + mako # notifications + grim # screenshots + slurp # region selection + ]; + }; + security.polkit.enable = true; + + # Enable greetd as display manager for sway + services.greetd = { + enable = true; + settings = { + default_session = { + command = "${pkgs.greetd.tuigreet}/bin/tuigreet --time --cmd sway"; + user = "greeter"; + }; + }; + }; + + # PipeWire for audio + services.pipewire = { + enable = true; + alsa.enable = true; + pulse.enable = true; + }; + + # Steam + programs.steam = { + enable = true; + dedicatedServer.openFirewall = true; + }; + + # Tailscale + services.tailscale.enable = true; + + # SSH + services.openssh = { + enable = true; + settings = { + PasswordAuthentication = false; + PermitRootLogin = "no"; + }; + }; + + # User account + users.users.eblume = { + isNormalUser = true; + initialPassword = "changeme"; + extraGroups = [ "wheel" "networkmanager" "video" ]; + openssh.authorizedKeys.keys = [ + "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILmh1SSCdDAyu3vkSQH7kAXEPDi8APyjo9JXDTjtha2j" + ]; + }; + + # System packages + environment.systemPackages = with pkgs; [ + git + vim + htop + curl + wget + ]; + + # Enable nix flakes + nix.settings.experimental-features = [ "nix-command" "flakes" ]; + + # NixOS release + system.stateVersion = "25.11"; +} diff --git a/nixos/ringtail/disk-config.nix b/nixos/ringtail/disk-config.nix new file mode 100644 index 0000000..f383212 --- /dev/null +++ b/nixos/ringtail/disk-config.nix @@ -0,0 +1,84 @@ +{ + disko.devices = { + disk = { + nvme = { + type = "disk"; + device = "/dev/nvme0n1"; + content = { + type = "gpt"; + partitions = { + ESP = { + size = "512M"; + type = "EF00"; + content = { + type = "filesystem"; + format = "vfat"; + mountpoint = "/boot"; + mountOptions = [ "umask=0077" ]; + }; + }; + root = { + size = "100%"; + content = { + type = "filesystem"; + format = "ext4"; + mountpoint = "/"; + }; + }; + }; + }; + }; + games = { + type = "disk"; + device = "/dev/sda"; + content = { + type = "gpt"; + partitions = { + games = { + size = "100%"; + content = { + type = "filesystem"; + format = "ext4"; + mountpoint = "/mnt/games"; + }; + }; + }; + }; + }; + storage1 = { + type = "disk"; + device = "/dev/sdb"; + content = { + type = "gpt"; + partitions = { + storage1 = { + size = "100%"; + content = { + type = "filesystem"; + format = "ext4"; + mountpoint = "/mnt/storage1"; + }; + }; + }; + }; + }; + storage2 = { + type = "disk"; + device = "/dev/sdc"; + content = { + type = "gpt"; + partitions = { + storage2 = { + size = "100%"; + content = { + type = "filesystem"; + format = "ext4"; + mountpoint = "/mnt/storage2"; + }; + }; + }; + }; + }; + }; + }; +} diff --git a/nixos/ringtail/flake.nix b/nixos/ringtail/flake.nix new file mode 100644 index 0000000..8bfac2f --- /dev/null +++ b/nixos/ringtail/flake.nix @@ -0,0 +1,23 @@ +{ + description = "NixOS configuration for ringtail (gaming/compute workstation)"; + + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.11"; + disko = { + url = "github:nix-community/disko"; + inputs.nixpkgs.follows = "nixpkgs"; + }; + }; + + outputs = { nixpkgs, disko, ... }: { + nixosConfigurations.ringtail = nixpkgs.lib.nixosSystem { + system = "x86_64-linux"; + modules = [ + disko.nixosModules.disko + ./disk-config.nix + ./hardware-configuration.nix + ./configuration.nix + ]; + }; + }; +} diff --git a/nixos/ringtail/hardware-configuration.nix b/nixos/ringtail/hardware-configuration.nix new file mode 100644 index 0000000..7a1481e --- /dev/null +++ b/nixos/ringtail/hardware-configuration.nix @@ -0,0 +1,18 @@ +# Do not modify this file! It was generated by 'nixos-generate-config' +# and may be overwritten by future invocations. Please make changes +# to configuration.nix instead. +{ config, lib, pkgs, modulesPath, ... }: + +{ + imports = + [ (modulesPath + "/installer/scan/not-detected.nix") + ]; + + boot.initrd.availableKernelModules = [ "nvme" "xhci_pci" "ahci" "usb_storage" "usbhid" "sd_mod" ]; + boot.initrd.kernelModules = [ ]; + boot.kernelModules = [ "kvm-amd" ]; + boot.extraModulePackages = [ ]; + + nixpkgs.hostPlatform = lib.mkDefault "x86_64-linux"; + hardware.cpu.amd.updateMicrocode = lib.mkDefault config.hardware.enableRedistributableFirmware; +} diff --git a/pulumi/tailscale/__main__.py b/pulumi/tailscale/__main__.py index 631780e..6d3dbf0 100644 --- a/pulumi/tailscale/__main__.py +++ b/pulumi/tailscale/__main__.py @@ -5,7 +5,7 @@ This program manages: - Device tags for infrastructure classification Devices are tagged based on their role: -- tag:homelab - Server infrastructure (indri) +- tag:homelab - Server infrastructure (indri, ringtail) - tag:workstation - Development machines that can manage homelab (gilbert) - tag:nas - Network-attached storage (sifaka) - tag:blumeops - Resources managed by this IaC @@ -82,10 +82,23 @@ flyio_key = tailscale.TailnetKey( expiry=7776000, # 90 days ) +# Auth key for ringtail (gaming/compute workstation, NixOS) +# Used during bootstrap: `tailscale up --auth-key=` +# Once ringtail is on the tailnet, add DeviceTags resource for ongoing management. +ringtail_key = tailscale.TailnetKey( + "ringtail-key", + reusable=False, + ephemeral=False, + preauthorized=True, + tags=["tag:homelab", "tag:blumeops"], + expiry=86400, # 24 hours - single use for bootstrap +) + # ============== Exports ============== pulumi.export("acl_id", acl.id) pulumi.export("policy_hash", policy_hash) pulumi.export("flyio_authkey", flyio_key.key) +pulumi.export("ringtail_authkey", ringtail_key.key) pulumi.export("indri_device_id", indri.node_id) pulumi.export("indri_tags", indri_tags.tags)