aboutsummaryrefslogtreecommitdiff
path: root/modules/nixos
diff options
context:
space:
mode:
authorFranck Cuny <franck@fcuny.net>2025-10-13 08:52:36 -0700
committerFranck Cuny <franck@fcuny.net>2025-10-13 08:52:36 -0700
commitbca7e01468e0bb43fdefaf3d13f5ecea44501417 (patch)
tree9d3134fa94cb9cef086b2d7f2dc65ef08617b986 /modules/nixos
parentdelete unused module (diff)
downloadinfra-bca7e01468e0bb43fdefaf3d13f5ecea44501417.tar.gz
move modules under nixos
Diffstat (limited to 'modules/nixos')
-rw-r--r--modules/nixos/backups.nix210
-rw-r--r--modules/nixos/default.nix2
-rw-r--r--modules/nixos/nas-client.nix84
3 files changed, 296 insertions, 0 deletions
diff --git a/modules/nixos/backups.nix b/modules/nixos/backups.nix
new file mode 100644
index 0000000..78b3144
--- /dev/null
+++ b/modules/nixos/backups.nix
@@ -0,0 +1,210 @@
+# Some examples for how to use this module
+#
+# Host with media files - backup /media only locally
+# my.modules.backups = {
+# enable = true;
+# passwordFile = config.age.secrets.restic_password.path
+# local.paths = [ "/media" "/home" "/var/lib/important" ];
+# remote.paths = [ "/home" "/var/lib/important" ]; # Excludes /media
+# };
+#
+# Another example - different exclusions for local vs remote
+# my.modules.backups = {
+# enable = true;
+# passwordFile = config.age.secrets.restic_password.path
+# local.paths = [ "/home" "/var/cache/downloads" ];
+# local.exclude = [ "*.tmp" ];
+# remote.paths = [ "/home" ]; # Skip cache directory for remote
+# remote.exclude = [ "*.tmp" "*.log" ]; # More aggressive exclusions for remote
+# };
+{
+ pkgs,
+ config,
+ lib,
+ ...
+}:
+let
+ cfg = config.my.modules.backups;
+
+ # Helper scripts for easy backup access
+ restic-local = pkgs.writeShellScriptBin "restic-local" ''
+ export RESTIC_REPOSITORY="${cfg.localBasePath}/${config.networking.hostName}"
+ export RESTIC_PASSWORD_FILE="${cfg.passwordFile}"
+ exec ${pkgs.restic}/bin/restic "$@"
+ '';
+
+ restic-remote = pkgs.writeShellScriptBin "restic-remote" ''
+ export RESTIC_REPOSITORY="${cfg.remoteBaseRepository}:/${config.networking.hostName}/"
+ export RESTIC_PASSWORD_FILE="${cfg.passwordFile}"
+ ${lib.optionalString (cfg.remote.environmentFile != null) ''
+ source ${cfg.remote.environmentFile}
+ ''}
+ exec ${pkgs.restic}/bin/restic "$@"
+ '';
+
+ # Common backup options shared between local and remote
+ backupOptions = {
+ paths = lib.mkOption {
+ type = lib.types.listOf lib.types.str;
+ default = [ ];
+ description = "Paths to backup";
+ example = [
+ "/home"
+ "/var/lib/important-data"
+ ];
+ };
+
+ exclude = lib.mkOption {
+ type = lib.types.listOf lib.types.str;
+ default = [ ];
+ description = "Paths to exclude from backup";
+ example = [
+ "*.tmp"
+ "/var/cache"
+ ];
+ };
+
+ extraBackupArgs = lib.mkOption {
+ type = lib.types.listOf lib.types.str;
+ default = [
+ "--exclude-caches"
+ "--compression=max"
+ ];
+ description = "Additional arguments to pass to restic backup";
+ };
+
+ pruneOpts = lib.mkOption {
+ type = lib.types.listOf lib.types.str;
+ default = [
+ "--keep-daily 7"
+ "--keep-weekly 4"
+ "--keep-monthly 3"
+ ];
+ description = "Pruning options for old backups";
+ };
+
+ timerConfig = lib.mkOption {
+ type = lib.types.attrs;
+ default = {
+ OnCalendar = "daily";
+ RandomizedDelaySec = "5m";
+ };
+ description = "Systemd timer configuration";
+ };
+ };
+in
+{
+ options.my.modules.backups = {
+ enable = lib.mkEnableOption "backups";
+
+ passwordFile = lib.mkOption {
+ type = lib.types.str;
+ default = config.age.secrets.restic_password.path;
+ description = "Path to file containing restic repository password";
+ example = "/run/secrets/restic-password";
+ };
+
+ localBasePath = lib.mkOption {
+ type = lib.types.str;
+ default = "/data/backups";
+ description = "Base path for local backup repositories";
+ example = "/mnt/backup-drive/backups";
+ };
+
+ remoteBaseRepository = lib.mkOption {
+ type = lib.types.str;
+ default = "gs:fcuny-infra-backups";
+ description = "Base repository URL for remote backups";
+ example = "s3:my-backup-bucket";
+ };
+
+ local = backupOptions;
+
+ remote = backupOptions // {
+ timerConfig = lib.mkOption {
+ type = lib.types.attrs;
+ default = {
+ OnCalendar = "daily";
+ # No randomized delay for remote to avoid overlap with local
+ };
+ description = "Systemd timer configuration for remote backups";
+ };
+
+ googleProjectId = lib.mkOption {
+ type = lib.types.nullOr lib.types.str;
+ default = "fcuny-infra";
+ description = "Google Cloud project ID for GCS backups";
+ example = "my-backup-project";
+ };
+
+ googleCredentialsFile = lib.mkOption {
+ type = lib.types.nullOr lib.types.str;
+ default = config.age.secrets.restic_gcs_credentials.path;
+ description = "Path to Google Cloud service account credentials file";
+ example = "/run/secrets/gcs-credentials";
+ };
+
+ environmentFile = lib.mkOption {
+ type = lib.types.nullOr lib.types.path;
+ default =
+ if cfg.remote.googleProjectId != null && cfg.remote.googleCredentialsFile != null then
+ pkgs.writeText "restic-gcs-env" ''
+ GOOGLE_PROJECT_ID=${cfg.remote.googleProjectId}
+ GOOGLE_APPLICATION_CREDENTIALS=${cfg.remote.googleCredentialsFile}
+ ''
+ else
+ null;
+ description = "Environment file for remote backup authentication";
+ };
+ };
+
+ helpers = lib.mkOption {
+ type = lib.types.bool;
+ default = true;
+ description = "Install helper scripts (restic-local, restic-remote)";
+ };
+ };
+
+ config = lib.mkIf cfg.enable {
+ environment.systemPackages = [
+ pkgs.restic
+ ]
+ ++ lib.optionals cfg.helpers [
+ restic-local
+ restic-remote
+ ];
+
+ services.restic.backups = lib.mkMerge [
+ # Local backup configuration - only if paths are specified
+ (lib.mkIf (cfg.local.paths != [ ]) {
+ local = {
+ initialize = true;
+ repository = "${cfg.localBasePath}/${config.networking.hostName}";
+ passwordFile = cfg.passwordFile;
+ paths = cfg.local.paths;
+ exclude = cfg.local.exclude;
+ extraBackupArgs = cfg.local.extraBackupArgs;
+ timerConfig = cfg.local.timerConfig;
+ pruneOpts = cfg.local.pruneOpts;
+ };
+ })
+
+ # Remote backup configuration - only if paths are specified
+ (lib.mkIf (cfg.remote.paths != [ ]) {
+ remote = {
+ initialize = true;
+ repository = "${cfg.remoteBaseRepository}:/${config.networking.hostName}/";
+ passwordFile = cfg.passwordFile;
+ paths = cfg.remote.paths;
+ exclude = cfg.remote.exclude;
+ extraBackupArgs = cfg.remote.extraBackupArgs;
+ timerConfig = cfg.remote.timerConfig;
+ pruneOpts = cfg.remote.pruneOpts;
+ }
+ // lib.optionalAttrs (cfg.remote.environmentFile != null) {
+ environmentFile = toString cfg.remote.environmentFile;
+ };
+ })
+ ];
+ };
+}
diff --git a/modules/nixos/default.nix b/modules/nixos/default.nix
index 47cf81e..73e144c 100644
--- a/modules/nixos/default.nix
+++ b/modules/nixos/default.nix
@@ -1,9 +1,11 @@
{ ... }:
{
imports = [
+ ./backups.nix
./base.nix
./cgroups.nix
./home-manager.nix
+ ./nas-client.nix
./nix.nix
./podman.nix
./remote-unlock.nix
diff --git a/modules/nixos/nas-client.nix b/modules/nixos/nas-client.nix
new file mode 100644
index 0000000..fe0952e
--- /dev/null
+++ b/modules/nixos/nas-client.nix
@@ -0,0 +1,84 @@
+{
+ config,
+ lib,
+ pkgs,
+ ...
+}:
+
+let
+ cfg = config.my.modules.nas-client;
+in
+{
+ options.my.modules.nas-client = with lib; {
+ enable = mkEnableOption "NAS client";
+
+ volumes = mkOption {
+ type = types.attrsOf (
+ types.submodule {
+ options = {
+ server = mkOption {
+ type = types.str;
+ example = "nas";
+ description = "Hostname of the server to connect to.";
+ };
+ remotePath = mkOption {
+ type = types.str;
+ example = "data";
+ description = "Remote path on the NAS to mount.";
+ };
+ mountPoint = mkOption {
+ type = types.str;
+ description = "Local directory where the volume will be mounted.";
+ };
+ uid = mkOption {
+ type = types.int;
+ default = 1000;
+ description = "User ID for mounted files.";
+ };
+ gid = mkOption {
+ type = types.int;
+ default = 1000;
+ description = "Group ID for mounted files.";
+ };
+ options = mkOption {
+ type = types.str;
+ default = "rw";
+ description = "Additional mount options.";
+ };
+ };
+ }
+ );
+ default = { };
+ description = "NAS volumes to mount.";
+ };
+ };
+
+ config = lib.mkIf cfg.enable {
+ boot.kernelModules = [
+ "cifs"
+ "cmac"
+ "sha256"
+ ];
+
+ # this is required to get the credentials options to work
+ environment.systemPackages = [ pkgs.cifs-utils ];
+
+ systemd.mounts = lib.mapAttrsToList (name: volume: {
+ description = "Mount for NAS volume ${name}";
+ what = "//${volume.server}/${volume.remotePath}";
+ where = volume.mountPoint;
+ unitConfig = {
+ # This ensures it uses mount.cifs
+ Type = "cifs";
+ };
+ type = "cifs"; # Explicitly specify CIFS type otherwise we ran into issues when using the credentials file option
+ options = "credentials=${config.age.secrets.nas_client_credentials.path},uid=${toString volume.uid},gid=${toString volume.gid},${volume.options}";
+ }) cfg.volumes;
+
+ systemd.automounts = lib.mapAttrsToList (name: volume: {
+ description = "Automount for NAS volume ${name}";
+ where = volume.mountPoint;
+ wantedBy = [ "multi-user.target" ];
+ }) cfg.volumes;
+ };
+}