migrate to clan

This commit is contained in:
2025-05-16 15:46:47 +02:00
parent f817ba1405
commit f1ec59c3af
60 changed files with 225 additions and 391 deletions

21
machines/genepi/acme.nix Normal file
View File

@@ -0,0 +1,21 @@
{ config, ... }:
{
security.acme = {
acceptTerms = true;
defaults.email = "admin@rpqt.fr";
};
age.secrets.gandi.file = ../../secrets/gandi.age;
security.acme = {
certs."home.rpqt.fr" = {
group = config.services.nginx.group;
domain = "home.rpqt.fr";
extraDomainNames = [ "*.home.rpqt.fr" ];
dnsProvider = "gandiv5";
dnsPropagationCheck = true;
environmentFile = config.age.secrets.gandi.path;
};
};
}

19
machines/genepi/boot.nix Normal file
View File

@@ -0,0 +1,19 @@
{ config, ... }:
{
boot.initrd.availableKernelModules = [
"xhci_pci"
"usbhid"
"usb_storage"
];
boot.loader = {
grub.enable = false;
generic-extlinux-compatible.enable = true;
};
boot.supportedFilesystems = [
"btrfs"
"vfat"
];
}

View File

@@ -0,0 +1,11 @@
{ keys, ... }:
{
imports = [
../../modules/remote-builder.nix
];
roles.remote-builder = {
enable = true;
authorizedKeys = [ keys.hosts.haze ];
};
}

View File

@@ -0,0 +1,47 @@
{
inputs,
...
}:
{
imports = [
inputs.agenix.nixosModules.default
inputs.impermanence.nixosModules.impermanence
./acme.nix
./boot.nix
./builder.nix
./dns.nix
./freshrss.nix
./glance.nix
./homeassistant.nix
# ./immich.nix
./monitoring
./mpd.nix
./network.nix
./nginx.nix
./persistence.nix
./syncthing.nix
./taskchampion.nix
../../system
../../modules/borgbackup.nix
inputs.clan-core.clanModules.state-version
inputs.clan-core.clanModules.trusted-nix-caches
inputs.home-manager.nixosModules.home-manager
{
home-manager.useGlobalPkgs = true;
home-manager.useUserPackages = true;
home-manager.users.rpqt = ./home.nix;
}
];
networking.hostName = "genepi";
clan.core.networking.targetHost = "root@genepi.local";
nix.gc = {
automatic = true;
dates = "weekly";
options = "--delete-older-than 30d";
};
}

86
machines/genepi/disko.nix Normal file
View File

@@ -0,0 +1,86 @@
{
disko.devices.disk.main = {
type = "disk";
device = "/dev/disk/by-id/ata-WD_Green_M.2_2280_480GB_2251E6411147";
content = {
type = "gpt";
partitions = {
ESP = {
priority = 1;
name = "ESP";
start = "1M";
end = "512M";
type = "EF00";
content = {
type = "filesystem";
format = "vfat";
mountpoint = "/boot";
mountOptions = [ "umask=0077" ];
};
};
root = {
end = "-4G";
content = {
type = "btrfs";
extraArgs = [
"-L"
"nixos"
"-f" # Override existing partition
];
subvolumes = {
"/root" = {
mountpoint = "/";
mountOptions = [
"subvol=root"
"compress=zstd"
"noatime"
];
};
"/home" = {
mountpoint = "/home";
mountOptions = [
"subvol=home"
"compress=zstd"
"noatime"
];
};
"/nix" = {
mountpoint = "/nix";
mountOptions = [
"subvol=nix"
"compress=zstd"
"noatime"
];
};
"/persist" = {
mountpoint = "/persist";
mountOptions = [
"subvol=persist"
"compress=zstd"
"noatime"
];
};
"/log" = {
mountpoint = "/var/log";
mountOptions = [
"subvol=log"
"compress=zstd"
"noatime"
];
};
};
};
};
swap = {
size = "100%";
content = {
type = "swap";
};
};
};
};
};
fileSystems."/persist".neededForBoot = true;
fileSystems."/var/log".neededForBoot = true;
}

35
machines/genepi/dns.nix Normal file
View File

@@ -0,0 +1,35 @@
{ config, lib, ... }:
let
domain = "home.rpqt.fr";
genepi = {
ip = "100.83.123.79";
subdomains = [
"glance"
"grafana"
"images"
"rss"
"tw"
];
};
in
{
networking.firewall.interfaces."${config.services.tailscale.interfaceName}" = {
allowedTCPPorts = [ 53 ];
allowedUDPPorts = [ 53 ];
};
services.unbound = {
enable = true;
resolveLocalQueries = false;
settings = {
server = {
interface = [ "${config.services.tailscale.interfaceName}" ];
access-control = [ "100.0.0.0/8 allow" ];
local-zone = lib.map (subdomain: ''"${subdomain}.${domain}." redirect'') genepi.subdomains;
local-data = lib.map (subdomain: ''"${subdomain}.${domain}. IN A ${genepi.ip}"'') genepi.subdomains;
};
};
};
}

View File

@@ -0,0 +1,26 @@
{ config, ... }:
let
domain = "home.rpqt.fr";
subdomain = "rss.${domain}";
in
{
age.secrets.freshrss = {
file = ../../secrets/freshrss.age;
mode = "700";
owner = config.services.freshrss.user;
};
services.freshrss = {
enable = true;
baseUrl = "https://${subdomain}";
virtualHost = "${subdomain}";
defaultUser = "rpqt";
passwordFile = config.age.secrets.freshrss.path;
};
services.nginx.virtualHosts.${config.services.freshrss.virtualHost} = {
forceSSL = true;
useACMEHost = "${domain}";
};
}

View File

@@ -0,0 +1,187 @@
{
theme = {
light = true;
background-color = "0 0 95";
primary-color = "0 0 10";
negative-color = "0 90 50";
};
pages = [
{
name = "Home";
columns = [
{
size = "small";
widgets = [
{
type = "calendar";
first-day-of-week = "monday";
}
{
type = "server-stats";
servers = [
{
type = "local";
name = "Genepi";
}
];
}
];
}
{
size = "full";
widgets = [
{
type = "search";
autofocus = true;
}
{
type = "monitor";
cache = "1m";
title = "Services";
sites = [
{
title = "Immich";
url = "https://images.home.rpqt.fr";
icon = "si:immich";
}
{
title = "Grafana";
url = "https://grafana.home.rpqt.fr";
icon = "si:grafana";
}
{
title = "FreshRSS";
url = "https://rss.home.rpqt.fr";
icon = "si:rss";
}
];
}
];
}
{
size = "small";
widgets = [
{
type = "weather";
location = "Grenoble, France";
units = "metric";
hour-format = "24h";
}
];
}
];
}
{
name = "Feeds";
columns = [
{
size = "small";
widgets = [
{
type = "rss";
title = "Blogs";
limit = 10;
collapse-after = 5;
cache = "12h";
feeds = [
{
url = "https://rss.home.rpqt.fr/api/query.php?user=rpqt&t=74HfeLZ6Wu9h4MmjNR38Rz&f=rss";
}
];
}
{
type = "rss";
title = "Status & Updates";
limit = 3;
cache = "12h";
feeds = [
{
url = "https://status.sr.ht/index.xml";
}
];
}
];
}
{
size = "full";
widgets = [
{
type = "group";
widgets = [
{
type = "hacker-news";
}
{
type = "lobsters";
}
];
}
{
type = "group";
widgets = [
{
type = "reddit";
subreddit = "selfhosted";
show-thumbnails = true;
}
{
type = "reddit";
subreddit = "homelab";
show-thumbnails = true;
}
];
}
{
type = "videos";
channels = [
"UCR-DXc1voovS8nhAvccRZhg"
"UCsBjURrPoezykLs9EqgamOA"
];
}
];
}
{
size = "small";
widgets = [
{
type = "releases";
cache = "1d";
repositories = [
"glanceapp/glance"
];
}
{
type = "custom-api";
title = "Random Fact";
cache = "6h";
url = "https://uselessfacts.jsph.pl/api/v2/facts/random";
template = ''
<p class="size-h4 color-paragraph">{{ .JSON.String "text" }}</p>
'';
}
{
type = "custom-api";
title = "Steam Specials";
cache = "12h";
url = "https://store.steampowered.com/api/featuredcategories?cc=us";
template = ''
<ul class="list list-gap-10 collapsible-container" data-collapse-after="5">
{{ range .JSON.Array "specials.items" }}
<li>
<a class="size-h4 color-highlight block text-truncate" href="https://store.steampowered.com/app/{{ .Int "id" }}/">{{ .String "name" }}</a>
<ul class="list-horizontal-text">
<li>{{ div (.Int "final_price" | toFloat) 100 | printf "$%.2f" }}</li>
{{ $discount := .Int "discount_percent" }}
<li{{ if ge $discount 40 }} class="color-positive"{{ end }}>{{ $discount }}% off</li>
</ul>
</li>
{{ end }}
</ul>
'';
}
];
}
];
}
];
}

View File

@@ -0,0 +1,18 @@
{ config, ... }:
let
domain = "home.rpqt.fr";
subdomain = "glance.${domain}";
in
{
services.glance = {
enable = true;
settings = ./glance-config.nix;
};
services.nginx.virtualHosts.${subdomain} = {
forceSSL = true;
useACMEHost = "${domain}";
locations."/".proxyPass =
"http://127.0.0.1:${toString config.services.glance.settings.server.port}";
};
}

View File

@@ -0,0 +1,23 @@
{ inputs, pkgs, ... }:
{
imports = [
# inputs.nixos-hardware.nixosModules.raspberry-pi-4
];
nixpkgs.hostPlatform = "aarch64-linux";
hardware.enableRedistributableFirmware = true;
# hardware = {
# raspberry-pi."4".apply-overlays-dtmerge.enable = true;
# deviceTree = {
# enable = true;
# filter = "*rpi-4-*.dtb";
# };
# };
environment.systemPackages = with pkgs; [
libraspberrypi
raspberrypi-eeprom
];
}

32
machines/genepi/home.nix Normal file
View File

@@ -0,0 +1,32 @@
{
config,
pkgs,
lib,
...
}:
{
home.username = "rpqt";
home.homeDirectory = lib.mkForce "/home/rpqt";
home.packages = [
pkgs.helix
pkgs.ripgrep
pkgs.eza
];
programs.zsh.enable = true;
programs.starship.enable = true;
programs.atuin.enable = true;
# This value determines the Home Manager release that your configuration is
# compatible with. This helps avoid breakage when a new Home Manager release
# introduces backwards incompatible changes.
#
# You should not change this value, even if you update Home Manager. If you do
# want to update the value, then make sure to first check the Home Manager
# release notes.
home.stateVersion = "24.11";
# Let Home Manager install and manage itself
programs.home-manager.enable = true;
}

View File

@@ -0,0 +1,10 @@
{
virtualisation.oci-containers.containers.homeassistant = {
volumes = [ "home-assistant:/config" ];
environment.TZ = "Europe/Paris";
image = "ghcr.io/home-assistant/home-assistant:stable";
extraOptions = [
"--network=host"
];
};
}

View File

@@ -0,0 +1,30 @@
{ config, ... }:
let
domain = "home.rpqt.fr";
subdomain = "images.${domain}";
in
{
services.immich = {
enable = true;
settings = {
server.externalDomain = "https://${subdomain}";
};
};
services.nginx.virtualHosts.${subdomain} = {
forceSSL = true;
useACMEHost = "${domain}";
locations."/" = {
proxyPass = "http://${toString config.services.immich.host}:${toString config.services.immich.port}";
proxyWebsockets = true;
extraConfig = ''
client_max_body_size 50000M;
proxy_read_timeout 600s;
proxy_send_timeout 600s;
send_timeout 600s;
'';
};
};
clan.core.state.userdata.folders = [ "/var/lib/immich" ];
}

View File

@@ -0,0 +1,6 @@
{
imports = [
./grafana.nix
./prometheus.nix
];
}

View File

@@ -0,0 +1,40 @@
{ config, ... }:
let
domain = "home.rpqt.fr";
in
{
services.grafana = {
enable = true;
settings = {
server = {
http_port = 3000;
domain = "grafana.${domain}";
};
};
provision = {
enable = true;
datasources = {
settings = {
datasources = [
{
name = "Prometheus";
type = "prometheus";
access = "proxy";
url = "http://127.0.0.1:${toString config.services.prometheus.port}";
isDefault = true;
}
];
};
};
};
};
services.nginx.virtualHosts.${config.services.grafana.settings.server.domain} = {
forceSSL = true;
useACMEHost = "${domain}";
locations."/" = {
proxyPass = "http://127.0.0.1:${toString config.services.grafana.settings.server.http_port}";
proxyWebsockets = true;
};
};
}

View File

@@ -0,0 +1,63 @@
{
lib,
self,
...
}:
let
allHosts = self.nixosConfigurations;
hostInTailnetFilter = k: v: v.config.services.tailscale.enable;
tailnetHosts = lib.filterAttrs hostInTailnetFilter allHosts;
# Explicitly list the exporters as some are deprecated and can't be evaluated
possibleExporterNames = [
"node"
];
getEnabledExporters =
hostname: host:
lib.filterAttrs (k: v: v.enable == true) (
lib.getAttrs possibleExporterNames host.config.services.prometheus.exporters
);
enabledExporters = lib.mapAttrs getEnabledExporters tailnetHosts;
mkScrapeConfigExporter = hostname: exporterName: exporterCfg: {
job_name = "${hostname}-${exporterName}";
static_configs = [ { targets = [ "${hostname}:${toString exporterCfg.port}" ]; } ];
relabel_configs = [
{
target_label = "instance";
replacement = "${hostname}";
}
{
target_label = "job";
replacement = "${exporterName}";
}
];
};
mkScrapeConfigHost = hostname: exporters: lib.mapAttrs (mkScrapeConfigExporter hostname) exporters;
scrapeConfigsByHost = lib.mapAttrs mkScrapeConfigHost enabledExporters;
autogenScrapeConfigs = lib.flatten (
map builtins.attrValues (builtins.attrValues scrapeConfigsByHost)
);
in
{
services.prometheus = {
enable = true;
port = 9001;
scrapeConfigs = autogenScrapeConfigs;
exporters = {
node = {
enable = true;
enabledCollectors = [ "systemd" ];
port = 9002;
};
};
};
clan.core.state.userdata.folders = [ "/var/lib/prometheus2" ];
}

27
machines/genepi/mpd.nix Normal file
View File

@@ -0,0 +1,27 @@
{ config, ... }:
{
services.mpd = {
enable = true;
musicDirectory = "/home/rpqt/Media/Music";
extraConfig = ''
audio_output {
type "pulse"
name "Pulse Audio"
}
'';
network.listenAddress = "any";
};
services.pulseaudio.enable = true;
# Workaround: run PulseAudio system-wide so that the mpd user can access it
services.pulseaudio.systemWide = true;
# Fixes the stutter when changing volume (found this randomly)
services.pulseaudio.daemon.config.flat-volumes = "no";
users.users.${config.services.mpd.user}.extraGroups = [ "pulse-access" ];
users.users.rpqt.homeMode = "755";
}

View File

@@ -0,0 +1,6 @@
{
# Tailscale seems to break when not using resolved
services.resolved.enable = true;
networking.useDHCP = true;
networking.interfaces.tailscale0.useDHCP = false;
}

View File

@@ -0,0 +1,7 @@
{
services.nginx = {
enable = true;
recommendedProxySettings = true;
recommendedTlsSettings = true;
};
}

View File

@@ -0,0 +1,64 @@
{ lib, ... }:
{
environment.persistence."/persist" = {
enable = true;
directories = [
"/var/lib/nixos"
"/var/lib/acme"
"/var/lib/prometheus2"
"/var/lib/immich"
"/var/lib/redis-immich"
"/var/lib/postgresql"
"/var/lib/grafana"
"/var/lib/freshrss"
"/var/lib/tailscale"
];
files = [
# so that systemd doesn't think each boot is the first
"/etc/machine-id"
# ssh host keys
"/etc/ssh/ssh_host_rsa_key"
"/etc/ssh/ssh_host_rsa_key.pub"
"/etc/ssh/ssh_host_ed25519_key"
"/etc/ssh/ssh_host_ed25519_key.pub"
];
users.rpqt = {
directories = [ ];
files = [ ];
home = "/home/rpqt";
};
};
# Empty root and remove snapshots older than 30 days
# boot.initrd.postDeviceCommands = lib.mkAfter ''
# mkdir /btrfs_tmp
# mount /dev/disk/by-label/nixos /btrfs_tmp
# if [[ -e /btrfs_tmp/root ]]; then
# mkdir -p /btrfs_tmp/old_roots
# timestamp=$(date --date="@$(stat -c %Y /btrfs_tmp/root)" "+%Y-%m-%-d_%H:%M:%S")
# mv /btrfs_tmp/root "/btrfs_tmp/old_roots/$timestamp"
# fi
# delete_subvolume_recursively() {
# IFS=$'\n'
# for i in $(btrfs subvolume list -o "$1" | cut -f 9- -d ' '); do
# delete_subvolume_recursively "/btrfs_tmp/$i"
# done
# btrfs subvolume delete "$1"
# }
# for i in $(find /btrfs_tmp/old_roots/ -maxdepth 1 -mtime +30); do
# delete_subvolume_recursively "$i"
# done
# btrfs subvolume create /btrfs_tmp/root
# umount /btrfs_tmp
# rmdir /btrfs_tmp
# '';
# Give agenix persistent paths so it can load secrets before the mount
age.identityPaths = [
"/persist/etc/ssh/ssh_host_ed25519_key"
"/persist/etc/ssh/ssh_host_rsa_key"
];
}

View File

@@ -0,0 +1,57 @@
{
config,
...
}:
let
user = "rpqt";
home = config.users.users.${user}.home;
in
{
services.syncthing = {
enable = true;
user = user;
group = "users";
dataDir = home;
configDir = "${home}/.config/syncthing";
openDefaultPorts = true;
overrideDevices = true;
overrideFolders = true;
settings = {
devices = {
"haze" = {
id = "QUX6KGF-7KNFTGD-RAX5OWC-NFQGRNK-S2TC2DQ-DQRWDTK-KMBTQXT-EVNRDQG";
};
"pixel-7a" = {
id = "IZE7B4Z-LKTJY6Q-77NN4JG-ADYRC77-TYPZTXE-Q35BWV2-AEO7Q3R-ZE63IAU";
};
};
folders = {
"Documents" = {
path = "${home}/Documents";
devices = [
"haze"
];
};
"Music" = {
path = "${home}/Media/Music";
devices = [
"haze"
"pixel-7a"
];
};
"Pictures" = {
path = "${home}/Media/Pictures";
devices = [
"haze"
];
};
"Videos" = {
path = "${home}/Media/Videos";
devices = [
"haze"
];
};
};
};
};
}

View File

@@ -0,0 +1,19 @@
{ config, lib, ... }:
let
domain = "home.rpqt.fr";
subdomain = "tw.${domain}";
hasImpermanence = config.environment.persistence."/persist".enable;
in
{
services.taskchampion-sync-server.enable = true;
services.taskchampion-sync-server.dataDir =
(lib.optionalString hasImpermanence "/persist") + "/var/lib/taskchampion-sync-server";
services.nginx.virtualHosts.${subdomain} = {
forceSSL = true;
useACMEHost = "${domain}";
locations."/".proxyPass =
"http://127.0.0.1:${toString config.services.taskchampion-sync-server.port}";
};
}