restic backup overhaul
This commit is contained in:
parent
3beae7844f
commit
d3613a4ec4
22 changed files with 885 additions and 458 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -3,6 +3,7 @@
|
|||
**/*sync-conflict*
|
||||
age.key
|
||||
result*
|
||||
.decrypted~*
|
||||
.direnv
|
||||
.kube
|
||||
.github
|
||||
|
|
21
flake.lock
21
flake.lock
|
@ -695,26 +695,6 @@
|
|||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nix-index-database": {
|
||||
"inputs": {
|
||||
"nixpkgs": [
|
||||
"nixpkgs"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1734234111,
|
||||
"narHash": "sha256-icEMqBt4HtGH52PU5FHidgBrNJvOfXH6VQKNtnD1aw8=",
|
||||
"owner": "nix-community",
|
||||
"repo": "nix-index-database",
|
||||
"rev": "311d6cf3ad3f56cb051ffab1f480b2909b3f754d",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-community",
|
||||
"repo": "nix-index-database",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nix-inspect": {
|
||||
"inputs": {
|
||||
"nci": "nci",
|
||||
|
@ -1078,7 +1058,6 @@
|
|||
"hyprland-plugins": "hyprland-plugins",
|
||||
"krewfile": "krewfile",
|
||||
"lix-module": "lix-module",
|
||||
"nix-index-database": "nix-index-database",
|
||||
"nix-inspect": "nix-inspect",
|
||||
"nix-minecraft": "nix-minecraft",
|
||||
"nix-vscode-extensions": "nix-vscode-extensions",
|
||||
|
|
|
@ -47,13 +47,6 @@
|
|||
inputs.nixpkgs.follows = "nixpkgs";
|
||||
};
|
||||
|
||||
# nix-index database
|
||||
# https://github.com/nix-community/nix-index-database
|
||||
nix-index-database = {
|
||||
url = "github:nix-community/nix-index-database";
|
||||
inputs.nixpkgs.follows = "nixpkgs";
|
||||
};
|
||||
|
||||
# nix-inspect - inspect nix derivations usingn a TUI interface
|
||||
# https://github.com/bluskript/nix-inspect
|
||||
nix-inspect = {
|
||||
|
|
|
@ -1,5 +1,11 @@
|
|||
{ config, pkgs, lib, ... }:
|
||||
with lib; let
|
||||
{
|
||||
config,
|
||||
pkgs,
|
||||
lib,
|
||||
...
|
||||
}:
|
||||
with lib;
|
||||
let
|
||||
inherit (config.myHome) username homeDirectory;
|
||||
cfg = config.myHome.shell.fish;
|
||||
in
|
||||
|
@ -30,14 +36,22 @@ in
|
|||
nrs = "sudo nixos-rebuild switch --flake .";
|
||||
nvdiff = "nvd diff /run/current-system result";
|
||||
# rook & ceph versions.
|
||||
rcv =
|
||||
''
|
||||
kubectl \
|
||||
-n rook-ceph \
|
||||
get deployments \
|
||||
-l rook_cluster=rook-ceph \
|
||||
-o jsonpath='{range .items[*]}{.metadata.name}{" \treq/upd/avl: "}{.spec.replicas}{"/"}{.status.updatedReplicas}{"/"}{.status.readyReplicas}{" \trook-version="}{.metadata.labels.rook-version}{" \tceph-version="}{.metadata.labels.ceph-version}{"\n"}{end}'
|
||||
rcv = ''
|
||||
kubectl \
|
||||
-n rook-ceph \
|
||||
get deployments \
|
||||
-l rook_cluster=rook-ceph \
|
||||
-o jsonpath='{range .items[*]}{.metadata.name}{" \treq/upd/avl: "}{.spec.replicas}{"/"}{.status.updatedReplicas}{"/"}{.status.readyReplicas}{" \trook-version="}{.metadata.labels.rook-version}{" \tceph-version="}{.metadata.labels.ceph-version}{"\n"}{end}'
|
||||
'';
|
||||
};
|
||||
|
||||
functions = {
|
||||
nix-which = {
|
||||
body = ''
|
||||
set -l cmd $argv[1]
|
||||
nix-locate --whole-name --type x --type s "$cmd"
|
||||
'';
|
||||
};
|
||||
};
|
||||
|
||||
interactiveShellInit = ''
|
||||
|
|
|
@ -153,13 +153,6 @@
|
|||
# zfs.mountPoolsAtBoot = [ "eru" ];
|
||||
# NFS
|
||||
nfs.enable = true;
|
||||
# Restic
|
||||
resticBackup = {
|
||||
local.enable = false;
|
||||
remote.enable = false;
|
||||
local.noWarning = true;
|
||||
remote.noWarning = true;
|
||||
};
|
||||
};
|
||||
services = {
|
||||
libvirt-qemu.enable = true;
|
||||
|
|
28
nixos/hosts/shadowfax/config/backups.nix
Normal file
28
nixos/hosts/shadowfax/config/backups.nix
Normal file
|
@ -0,0 +1,28 @@
|
|||
{ ... }:
|
||||
{
|
||||
localbackup = {
|
||||
exclude = [
|
||||
"/home/*/.cache"
|
||||
];
|
||||
initialize = true;
|
||||
passwordFile = "/etc/nixos/secrets/restic-password";
|
||||
paths = [
|
||||
"/home"
|
||||
];
|
||||
repository = "/mnt/backup-hdd";
|
||||
};
|
||||
remotebackup = {
|
||||
extraOptions = [
|
||||
"sftp.command='ssh backup@host -i /etc/nixos/secrets/backup-private-key -s sftp'"
|
||||
];
|
||||
passwordFile = "/etc/nixos/secrets/restic-password";
|
||||
paths = [
|
||||
"/home"
|
||||
];
|
||||
repository = "sftp:backup@host:/backups/home";
|
||||
timerConfig = {
|
||||
OnCalendar = "00:05";
|
||||
RandomizedDelaySec = "5h";
|
||||
};
|
||||
};
|
||||
}
|
|
@ -1,6 +1,3 @@
|
|||
# Do not modify this file! It was generated by ‘nixos-generate-config’
|
||||
# and may be overwritten by future invocations. Please make changes
|
||||
# to /etc/nixos/configuration.nix instead.
|
||||
{
|
||||
config,
|
||||
lib,
|
||||
|
@ -104,16 +101,6 @@ in
|
|||
};
|
||||
|
||||
services = {
|
||||
xserver.videoDrivers = [ "nvidia" ];
|
||||
|
||||
# Prometheus exporters
|
||||
prometheus.exporters = {
|
||||
# Node Exporter - port 9100
|
||||
node.enable = true;
|
||||
# ZFS Exporter - port 9134
|
||||
zfs.enable = true;
|
||||
};
|
||||
|
||||
# Minio
|
||||
minio = {
|
||||
enable = true;
|
||||
|
@ -126,6 +113,14 @@ in
|
|||
enable = true;
|
||||
};
|
||||
|
||||
# Prometheus exporters
|
||||
prometheus.exporters = {
|
||||
# Node Exporter - port 9100
|
||||
node.enable = true;
|
||||
# ZFS Exporter - port 9134
|
||||
zfs.enable = true;
|
||||
};
|
||||
|
||||
# Smart daemon for monitoring disk health.
|
||||
smartd = {
|
||||
devices = smartdDevices;
|
||||
|
@ -141,6 +136,8 @@ in
|
|||
|
||||
# VSCode Compatibility Settings
|
||||
vscode-server.enable = true;
|
||||
|
||||
xserver.videoDrivers = [ "nvidia" ];
|
||||
};
|
||||
|
||||
# sops
|
||||
|
@ -164,24 +161,10 @@ in
|
|||
mode = "400";
|
||||
restartUnits = [ "syncthing.service" ];
|
||||
};
|
||||
"restic/plex/resticPassword" = {
|
||||
sopsFile = ./secrets.sops.yaml;
|
||||
owner = "jahanson";
|
||||
mode = "400";
|
||||
# restartUnits = [ "restic-plex.service" ];
|
||||
};
|
||||
"restic/plex/resticUri" = {
|
||||
sopsFile = ./secrets.sops.yaml;
|
||||
owner = "jahanson";
|
||||
mode = "400";
|
||||
# restartUnits = [ "restic-backup.service" ];
|
||||
};
|
||||
};
|
||||
|
||||
# System settings and services.
|
||||
mySystem = {
|
||||
purpose = "Production";
|
||||
|
||||
# Containers
|
||||
containers = {
|
||||
jellyfin.enable = true;
|
||||
|
@ -189,48 +172,17 @@ in
|
|||
plex.enable = true;
|
||||
scrypted.enable = true;
|
||||
};
|
||||
|
||||
# System
|
||||
system = {
|
||||
motd.networkInterfaces = [ "enp36s0f0" ];
|
||||
# Incus
|
||||
incus = {
|
||||
enable = true;
|
||||
preseed = import ./config/incus-preseed.nix { };
|
||||
};
|
||||
|
||||
# ZFS
|
||||
zfs.enable = true;
|
||||
zfs.mountPoolsAtBoot = [
|
||||
"nahar"
|
||||
"moria"
|
||||
"eru"
|
||||
];
|
||||
|
||||
# NFS
|
||||
nfs.enable = true;
|
||||
|
||||
resticBackup = {
|
||||
local.enable = false;
|
||||
remote.enable = false;
|
||||
local.noWarning = true;
|
||||
remote.noWarning = true;
|
||||
};
|
||||
};
|
||||
|
||||
purpose = "Production";
|
||||
# Services
|
||||
services = {
|
||||
podman.enable = true;
|
||||
# Misc
|
||||
libvirt-qemu.enable = true;
|
||||
|
||||
# Syncthing
|
||||
syncthing = {
|
||||
podman.enable = true;
|
||||
# Sanoid
|
||||
sanoid = {
|
||||
enable = true;
|
||||
user = "jahanson";
|
||||
publicCertPath = config.sops.secrets."syncthing/publicCert".path;
|
||||
privateKeyPath = config.sops.secrets."syncthing/privateKey".path;
|
||||
inherit (sanoidConfig.outputs) templates datasets;
|
||||
};
|
||||
|
||||
# Scrutiny
|
||||
scrutiny = {
|
||||
enable = true;
|
||||
|
@ -239,12 +191,36 @@ in
|
|||
containerVolumeLocation = "/nahar/containers/volumes/scrutiny";
|
||||
port = 8585;
|
||||
};
|
||||
|
||||
# Sanoid
|
||||
sanoid = {
|
||||
enable = true;
|
||||
inherit (sanoidConfig.outputs) templates datasets;
|
||||
# Syncthing
|
||||
syncthing = {
|
||||
enable = false;
|
||||
user = "jahanson";
|
||||
publicCertPath = config.sops.secrets."syncthing/publicCert".path;
|
||||
privateKeyPath = config.sops.secrets."syncthing/privateKey".path;
|
||||
};
|
||||
# ZFS nightly snapshot of container volumes
|
||||
zfs-nightly-snap = {
|
||||
enable = true;
|
||||
mountPath = "/mnt/restic_nightly_backup";
|
||||
zfsDataset = "nahar/containers/volumes";
|
||||
snapshotName = "restic_nightly_snap";
|
||||
startAt = "*-*-* 02:00:00 America/Chicago";
|
||||
};
|
||||
};
|
||||
# System
|
||||
system = {
|
||||
incus = {
|
||||
enable = true;
|
||||
preseed = import ./config/incus-preseed.nix { };
|
||||
};
|
||||
motd.networkInterfaces = [ "enp36s0f0" ];
|
||||
nfs.enable = true;
|
||||
zfs.enable = true;
|
||||
zfs.mountPoolsAtBoot = [
|
||||
"eru"
|
||||
"moria"
|
||||
"nahar"
|
||||
];
|
||||
};
|
||||
};
|
||||
}
|
||||
|
|
|
@ -70,12 +70,6 @@
|
|||
purpose = "Production";
|
||||
system = {
|
||||
motd.networkInterfaces = [ "enp2s0" "wlp3s0" ];
|
||||
resticBackup = {
|
||||
local.enable = false;
|
||||
remote.enable = false;
|
||||
local.noWarning = true;
|
||||
remote.noWarning = true;
|
||||
};
|
||||
};
|
||||
|
||||
services = {
|
||||
|
|
|
@ -7,19 +7,18 @@
|
|||
with lib;
|
||||
let
|
||||
app = "jellyfin";
|
||||
cfg = config.mySystem.containers.${app};
|
||||
group = "kah";
|
||||
image = "ghcr.io/jellyfin/jellyfin:${version}";
|
||||
user = "kah";
|
||||
# renovate: depName=ghcr.io/jellyfin/jellyfin datasource=docker
|
||||
version = "10.10.3";
|
||||
image = "ghcr.io/jellyfin/jellyfin:${version}";
|
||||
cfg = config.mySystem.containers.${app};
|
||||
volumeLocation = "/nahar/containers/volumes/jellyfin";
|
||||
in
|
||||
{
|
||||
# Options
|
||||
options.mySystem.containers.${app} = {
|
||||
enable = mkEnableOption "${app}";
|
||||
# TODO add to homepage
|
||||
# addToHomepage = mkEnableOption "Add ${app} to homepage" // {
|
||||
# default = true;
|
||||
# };
|
||||
openFirewall = mkEnableOption "Open firewall for ${app}" // {
|
||||
default = true;
|
||||
};
|
||||
|
@ -46,13 +45,13 @@ in
|
|||
${pkgs.podman}/bin/podman run \
|
||||
--rm \
|
||||
--name=${app} \
|
||||
--user=568:568 \
|
||||
--user="${toString config.users.users."${user}".uid}:${toString config.users.groups."${group}".gid}" \
|
||||
--device='nvidia.com/gpu=all' \
|
||||
--log-driver=journald \
|
||||
--cidfile=/run/${app}.ctr-id \
|
||||
--cgroups=no-conmon \
|
||||
--sdnotify=conmon \
|
||||
--volume="/nahar/containers/volumes/jellyfin:/config:rw" \
|
||||
--volume="${volumeLocation}:/config:rw" \
|
||||
--volume="/moria/media:/media:rw" \
|
||||
--volume="tmpfs:/cache:rw" \
|
||||
--volume="tmpfs:/transcode:rw" \
|
||||
|
@ -78,15 +77,46 @@ in
|
|||
# Firewall
|
||||
networking.firewall = mkIf cfg.openFirewall {
|
||||
allowedTCPPorts = [
|
||||
8096 # HTTP web interface
|
||||
8920 # HTTPS web interface
|
||||
8096 # HTTP web interface
|
||||
8920 # HTTPS web interface
|
||||
];
|
||||
allowedUDPPorts = [
|
||||
1900 # DLNA discovery
|
||||
7359 # Jellyfin auto-discovery
|
||||
1900 # DLNA discovery
|
||||
7359 # Jellyfin auto-discovery
|
||||
];
|
||||
};
|
||||
|
||||
sops.secrets = {
|
||||
"restic/jellyfin/env" = {
|
||||
sopsFile = ./secrets.sops.yaml;
|
||||
owner = user;
|
||||
group = group;
|
||||
mode = "0400";
|
||||
};
|
||||
"restic/jellyfin/password" = {
|
||||
sopsFile = ./secrets.sops.yaml;
|
||||
owner = user;
|
||||
group = group;
|
||||
mode = "0400";
|
||||
};
|
||||
"restic/jellyfin/template" = {
|
||||
sopsFile = ./secrets.sops.yaml;
|
||||
owner = user;
|
||||
group = group;
|
||||
mode = "0400";
|
||||
};
|
||||
};
|
||||
|
||||
# Restic backups for `jellyfin-local` and `jellyfin-remote`
|
||||
services.restic.backups = config.lib.mySystem.mkRestic {
|
||||
inherit app user;
|
||||
environmentFile = config.sops.secrets."restic/jellyfin/env".path;
|
||||
excludePaths = [ ];
|
||||
localResticTemplate = "/eru/restic/jellyfin";
|
||||
passwordFile = config.sops.secrets."restic/jellyfin/password".path;
|
||||
paths = [ volumeLocation ];
|
||||
remoteResticTemplateFile = config.sops.secrets."restic/jellyfin/template".path;
|
||||
};
|
||||
# TODO add nginx proxy
|
||||
# services.nginx.virtualHosts."${app}.${config.networking.domain}" = {
|
||||
# useACMEHost = config.networking.domain;
|
||||
|
@ -131,14 +161,5 @@ in
|
|||
# ];
|
||||
# }
|
||||
# ];
|
||||
|
||||
# TODO add restic backup
|
||||
# services.restic.backups = config.lib.mySystem.mkRestic {
|
||||
# inherit app user;
|
||||
# excludePaths = [ "Backups" ];
|
||||
# paths = [ appFolder ];
|
||||
# inherit appFolder;
|
||||
# };
|
||||
|
||||
};
|
||||
}
|
||||
|
|
88
nixos/modules/nixos/containers/jellyfin/secrets.sops.yaml
Normal file
88
nixos/modules/nixos/containers/jellyfin/secrets.sops.yaml
Normal file
|
@ -0,0 +1,88 @@
|
|||
restic:
|
||||
jellyfin:
|
||||
env: ENC[AES256_GCM,data:SlhZjwhe1xYlks1TCvM=,iv:RCGjs9JYKSOlK9J+4m20FS/nca+v9+a87aolAOMEpOI=,tag:4TbyZ7q4x/zw+MGwd8Y2oQ==,type:str]
|
||||
password: ENC[AES256_GCM,data:kacukf7Js/9RLFGegR8wTm11md0nVpNErrfFLXRVLGI2HA==,iv:7IPiDAlc8c3uJImn/+l4nqnD24Nij44nhFkbaqO7SHQ=,tag:1dIYZaiYX9Y5RmuvAypy3w==,type:str]
|
||||
template: ENC[AES256_GCM,data:rqxj00XCHNDoTb6rbfz+dyT4YwpsL9I2mAxZ8Mn3ngadeIHLOtLVzBvtoCMBli5Bhq3ZYYzjxRqtLQSRUz3AyeWr,iv:zKoSXoi2gkTFfAHZfhVf7uN5QTDaxHlEW3gSRbeBsQ8=,tag:eyNQBoxtOp39nQrH6dFstA==,type:str]
|
||||
sops:
|
||||
kms: []
|
||||
gcp_kms: []
|
||||
azure_kv: []
|
||||
hc_vault: []
|
||||
age:
|
||||
- recipient: age1d9p83j52m2xg0vh9k7q0uwlxwhs3y6tlv68yg9s2h9mdw2fmmsqshddz5m
|
||||
enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSAxaDJpYXRySUgzbGgrRVJ2
|
||||
bEtQS2N4VGNVTEFXYTNqWSt5ME1mYUM0Y1MwCnljajF5d0swS2w1ZjVXMVVKKzNJ
|
||||
UEhhbzlMVzlRbzVQMGhQKzJuZ0lqUEUKLS0tIFFMcUtTT2hEQ1lwUXNyaUF4MEVS
|
||||
TG8vV05iYWxkNmNFZVhDaTh3TmZRRzQKS6Pwx8S211SzAGYoandbGG9qrf8sFBaR
|
||||
JO/5KwkD5y5ZqFGdPTpJsZfYeIXkKLbrvgnbPUr92H9qANgPntVE7w==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
- recipient: age1m83ups8xn2jy4ayr8gw0pyn34smr0huqc5v76e4887az4vsl4yzsj0dlhd
|
||||
enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBXTHB3OUpBdmwvenc2Sjgw
|
||||
eUFwNVVQT0J5bUVVYnRPYS9qRlRjdjUwZ253CkJDd2RDeHBydGIyUndxNVdFUTdP
|
||||
K29seHQ5OGJOWDlVSnkzQTZvTXcwNHMKLS0tIGF2UG1XQUovc3Q2QlJqWHJ0MWdM
|
||||
cXlEVi8yZWxuQVlHUm1TUWFVL0NGVEEK3fqKRT/ZHfhlZxQ6QJP1zf1KogFo3krz
|
||||
xRXr2WfUPZ2E+R0VoatHVdmlnDPscqL2Okjd4q06Ow8qivDhXj8wWQ==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
- recipient: age18kj3xhlvgjeg2awwku3r8d95w360uysu0w5ejghnp4kh8qmtge5qwa2vjp
|
||||
enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSAxVlh3UkR3dzRXdWJEckY5
|
||||
UmdjRUtyQ3ZNbzBYeEwxZ3BGenczMVhyQVZzCmw5QTVwenRTN2xVcU5Sb2w4Y3V2
|
||||
d2FyTXpUZW5wSE9GMjlIOUV1eEQrY3cKLS0tIGdHaXBzd3M3TFFsUU8yTGVWaWdZ
|
||||
Mmt2bUxpb3RCb0o5cEU2WUt2OURkc3cKmb95HbzdeaFXaoga/xGpmcvant2xMIuu
|
||||
oCahUeaavMcpJ2/xujw89kNkKoszBAin72Y6pSWaHFiWPVVuwtRp5g==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
- recipient: age1lp6rrlvmytp9ka6q89m0e0am26222kwrn7aqd45hu07s3a6jv3gqty86eu
|
||||
enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA4QnF1K1dsdWpGODBtejZy
|
||||
enQ5TElWRzJZa3pURXVEZ2VzaVJ2RWtnNXg4CkRyU1g4L01IeU91cXRQY0hoOFJh
|
||||
NTdZL3dVeUg1aml1ZzFRSUtHVFlmZ0UKLS0tIHhSZGV3akdxOE5IcThwM0tXOVlQ
|
||||
SUtaZHhTcE05dWxjQTVBRFBTdTNwelkKSKEfNR1PE/qvHPdEyCBp0bl2EUJDGdlk
|
||||
0t9AoUMBI3W4WrGQjlPz3H1JkwmniEvB6hsC4KA2l6Kg0lLGY2OBWA==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
- recipient: age1e4sd6jjd4uxxsh9xmhdsnu6mqd5h8c4zz4gwme7lkw9ee949fc9q4px9df
|
||||
enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSByL2lZa09oUXlrK3JqWVMr
|
||||
WWhCbGpQNVdrUDBjNFVxTktrQjFFMSs1c0ZVCk1pT3Z3MXFMVTdCNzdZQWU3UEg4
|
||||
YWlsYkpZRnhYYVdUSzhjYjBnb0pzM00KLS0tIDdwRUV4S2IrdUJvZ2VqWnlkZmlN
|
||||
RTU3RTRXSGZWQzJLZ1B2R1BvdEczeUUK1YqO0cOA9S9F69s8civG7B5fBBa0mIHt
|
||||
W8jNV2d2ivDMNZKZztZ4CdfuvTybHdPIyGOQQ3KFi1RD2hp7OXow4g==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
- recipient: age19jm7uuam7gkacm3kh2v7uqgkvmmx0slmm9zwdjhd2ln9r60xzd7qh78c5a
|
||||
enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBzSUFTUStJazNsK015cTZT
|
||||
NjgxRU0rNzBzK3NKNU5MaldQanpTSUlKbVVNCm1IcHZ6TEI0aEZXSW1hVC9MVUJs
|
||||
b1RIRkpBZkMydGt1dDlzOGpwTytybGMKLS0tIGxRSld3ZjdtRTFadEFRNzVTQ0Zt
|
||||
VndNOFJMUTdQbEtrSHJENkQ3RmxOQ0EKL19WyFWH+1jqdchRoZHVsn6OCI0v8KxS
|
||||
lLNO2iFEISmzv18w9awANroZNmqod+YQgWB/DC/lob8HSUIG4uUatw==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
- recipient: age1nwnqxjuaxlt5g7fe8rnspvn2c36uuef4hzwuwa6cfjfalz2lrd4q4n5fpl
|
||||
enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBxRnlTVWVCNTB6ZmJ4Q1hN
|
||||
Um4xb3ZxaU13TkhDbUF6SzVlUW45WWNBZFdRCnV1K1pGWm90OGs5ckdDTzBHTm83
|
||||
WmplOGwxNFhwRkN6MVNTQTVTWnAyVGcKLS0tIHlwcll6cGZhbGxXM0hONk8wZ1lE
|
||||
aXdzYWtTMEJJU056RDJYbGF3Y05WTG8KBXCmARd1lq1/vwHciwUVlhyeoDmjeDl7
|
||||
Met1WpME/9R+P39I4fWNY9z6F60xeQsIVUPuQyr/K7T9xwayYwtZcQ==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
- recipient: age1a8z3p24v32l9yxm5z2l8h7rpc3nhacyfv4jvetk2lenrvsdstd3sdu2kaf
|
||||
enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBqbDAzNTExZUsyMUtCMDhy
|
||||
OU1rUWpGejNvVEJLalRTN284c041aThhYXdvCmFaODU3WHcvQkdEanI3OU9EQzJG
|
||||
Nk1JNjhoSFVqKysybVltbGRuazArcmsKLS0tIDd4RThtU1M5b3AzNXY2RjYxTjkz
|
||||
Q1p4dk96SXpIbnR3d25jNnk4YW5qRmsKUjMeb/+q4blAtiT58AHletkNt8xrvH7M
|
||||
FGAuRRuwIxBKrbCl4fAzM/CuEslvyr/Jrf4ulazI6l+hSwJNwKdimA==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
lastmodified: "2024-12-27T04:08:26Z"
|
||||
mac: ENC[AES256_GCM,data:sJ0EB7SGKAJLa4/H7yRt4L4jOpZKz0uKDJqnLnjTel+DL5m9lnn55vQILEV2ynOBf+xqYE60ZBxSe3MlPjYBXig/Zr6oJV7g1pwpvpS4rmyGUzHr9xGuVXOWuWzHIvNePkWozPATWvW/hBkSMlBVp54lTjY/KI/UnOuvwCs/uIk=,iv:CkHPpk4FiLfYu6PdVBoYfn8IG2tvRYnB2Noq18JEfl8=,tag:ctKf2pmp+JaZpahehG1JqA==,type:str]
|
||||
pgp: []
|
||||
unencrypted_suffix: _unencrypted
|
||||
version: 3.9.2
|
|
@ -7,19 +7,18 @@
|
|||
with lib;
|
||||
let
|
||||
app = "plex";
|
||||
cfg = config.mySystem.containers.${app};
|
||||
group = "kah";
|
||||
image = "ghcr.io/onedr0p/plex:${version}";
|
||||
user = "kah";
|
||||
# renovate: depName=ghcr.io/onedr0p/plex datasource=docker versioning=loose
|
||||
version = "1.41.3.9314-a0bfb8370";
|
||||
image = "ghcr.io/onedr0p/plex:${version}";
|
||||
cfg = config.mySystem.containers.${app};
|
||||
volumeLocation = "/nahar/containers/volumes/plex";
|
||||
in
|
||||
{
|
||||
# Options
|
||||
options.mySystem.containers.${app} = {
|
||||
enable = mkEnableOption "${app}";
|
||||
# TODO add to homepage
|
||||
# addToHomepage = mkEnableOption "Add ${app} to homepage" // {
|
||||
# default = true;
|
||||
# };
|
||||
openFirewall = mkEnableOption "Open firewall for ${app}" // {
|
||||
default = true;
|
||||
};
|
||||
|
@ -34,7 +33,7 @@ in
|
|||
after = [ "network.target" ];
|
||||
|
||||
serviceConfig = {
|
||||
ExecStartPre = "${pkgs.writeShellScript "scrypted-start-pre" ''
|
||||
ExecStartPre = "${pkgs.writeShellScript "plex-start-pre" ''
|
||||
set -o errexit
|
||||
set -o nounset
|
||||
set -o pipefail
|
||||
|
@ -42,6 +41,7 @@ in
|
|||
${pkgs.podman}/bin/podman rm -f ${app} || true
|
||||
rm -f /run/${app}.ctr-id
|
||||
''}";
|
||||
# TODO: mount /config instead of /config/Library/Application Support/Plex Media Server
|
||||
ExecStart = ''
|
||||
${pkgs.podman}/bin/podman run \
|
||||
--rm \
|
||||
|
@ -51,8 +51,8 @@ in
|
|||
--cidfile=/run/${app}.ctr-id \
|
||||
--cgroups=no-conmon \
|
||||
--sdnotify=conmon \
|
||||
--user=568:568 \
|
||||
--volume="/nahar/containers/volumes/plex:/config/Library/Application Support/Plex Media Server:rw" \
|
||||
--user="${toString config.users.users."${user}".uid}:${toString config.users.groups."${group}".gid}" \
|
||||
--volume="${volumeLocation}:/config:rw" \
|
||||
--volume="/moria/media:/media:rw" \
|
||||
--volume="tmpfs:/config/Library/Application Support/Plex Media Server/Logs:rw" \
|
||||
--volume="tmpfs:/tmp:rw" \
|
||||
|
@ -78,6 +78,38 @@ in
|
|||
];
|
||||
};
|
||||
|
||||
sops.secrets ={
|
||||
"restic/plex/env" = {
|
||||
sopsFile = ./secrets.sops.yaml;
|
||||
owner = user;
|
||||
group = group;
|
||||
mode = "0400";
|
||||
};
|
||||
"restic/plex/password" = {
|
||||
sopsFile = ./secrets.sops.yaml;
|
||||
owner = user;
|
||||
group = group;
|
||||
mode = "0400";
|
||||
};
|
||||
"restic/plex/template" = {
|
||||
sopsFile = ./secrets.sops.yaml;
|
||||
owner = user;
|
||||
group = group;
|
||||
mode = "0400";
|
||||
};
|
||||
};
|
||||
|
||||
# Restic backups for `plex-local` and `plex-remote`
|
||||
services.restic.backups = config.lib.mySystem.mkRestic {
|
||||
inherit app user;
|
||||
environmentFile = config.sops.secrets."restic/plex/env".path;
|
||||
excludePaths = [ "${volumeLocation}/Library/Application Support/Plex Media Server/Cache" ];
|
||||
localResticTemplate = "/eru/restic/plex";
|
||||
passwordFile = config.sops.secrets."restic/plex/password".path;
|
||||
paths = [ "${volumeLocation}/Library" ];
|
||||
remoteResticTemplateFile = config.sops.secrets."restic/plex/template".path;
|
||||
};
|
||||
|
||||
# TODO add nginx proxy
|
||||
# services.nginx.virtualHosts."${app}.${config.networking.domain}" = {
|
||||
# useACMEHost = config.networking.domain;
|
||||
|
@ -123,13 +155,6 @@ in
|
|||
# }
|
||||
# ];
|
||||
|
||||
# TODO add restic backup
|
||||
# services.restic.backups = config.lib.mySystem.mkRestic {
|
||||
# inherit app user;
|
||||
# excludePaths = [ "Backups" ];
|
||||
# paths = [ appFolder ];
|
||||
# inherit appFolder;
|
||||
# };
|
||||
|
||||
};
|
||||
}
|
||||
|
|
88
nixos/modules/nixos/containers/plex/secrets.sops.yaml
Normal file
88
nixos/modules/nixos/containers/plex/secrets.sops.yaml
Normal file
|
@ -0,0 +1,88 @@
|
|||
restic:
|
||||
plex:
|
||||
env: ENC[AES256_GCM,data:Kuo21H4HZ4YVAsmj/Lw=,iv:9QE3ghWliEFahgTXKxPE38lA/UW4XL/0QAVHxB/VYJM=,tag:TmLgSwzxI2PfdijRpqskvA==,type:str]
|
||||
password: ENC[AES256_GCM,data:aEBi5RRFR+PDXFseVHCcDbjRkXkMQLHTa7fygi6e971UNA==,iv:Iwg9IXp0NHqJP7BAPFqG0bLWWKdwC2wwiOJP7cz4E/M=,tag:KpKa8z/FlOgfygEue2xAtQ==,type:str]
|
||||
template: ENC[AES256_GCM,data:usADYx+OKwW+RMwZkyaDq8OpWR5pyYH9bOaIGaN06/aqZlPA5EwoW5C6BTTj0z+s1y8Xz/WhTHK/sljyV1ZLtC9D,iv:tdSrw/wnM9gC8Q5zo5PsGeWrTVtuyKQRU3Dyd2XWYpQ=,tag:mds0QglR2B+RXC/tpkTO4Q==,type:str]
|
||||
sops:
|
||||
kms: []
|
||||
gcp_kms: []
|
||||
azure_kv: []
|
||||
hc_vault: []
|
||||
age:
|
||||
- recipient: age1d9p83j52m2xg0vh9k7q0uwlxwhs3y6tlv68yg9s2h9mdw2fmmsqshddz5m
|
||||
enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBDS0F4REt5RklvVE5mU0Ra
|
||||
RkxvWHdYUHdMN3JxdjVBRTZkOWtCSTN5dHlnCmp5V2djZXEvbFlnZEhNaU5qcUtR
|
||||
U2RyQW5nSmNVT3FyNjhyU3E3MjRHcncKLS0tIHBZMmNlZ0llN0t6N09TeUY1ZWZm
|
||||
dzJ4STBQcVZyMFEyYUY0clBIQ0tCcWsKNZ2c9tVKrEbcaVn1Tk/7Fkc3ZMWDST5q
|
||||
LRiO+63fLkXj6LBOH8WOKDMKnYym+Ii1PiCV6QNizXE5H534xHpDQQ==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
- recipient: age1m83ups8xn2jy4ayr8gw0pyn34smr0huqc5v76e4887az4vsl4yzsj0dlhd
|
||||
enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBHV3pRM0RES201cUVKcCtK
|
||||
eEFBaUhsWlJnTTFvRFdxMFpFeUp6WWh6OFNBCmlNR3EyWXZxY09IenVVL3ViNmIx
|
||||
RC8wWjNlN2JlVTBDaE85aEw2ME1MaWcKLS0tIFlaNDV0bFBXNERhdmN4a1hLem1s
|
||||
UVNVTDhSellJTkRPZE9hN0wvanF3N2sKJ4Qp30gHicpGhowLCP+T4EdVy+wV1Pxk
|
||||
MF/yh9Og7mW1FvydCRz5GZZPWMGmEeC5c/SiqUD9xorXzzthYkUtTA==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
- recipient: age18kj3xhlvgjeg2awwku3r8d95w360uysu0w5ejghnp4kh8qmtge5qwa2vjp
|
||||
enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSAwdTV0enozam9NK1FpZUNT
|
||||
aUkzUnhHM0lqN1Q0Ymp4VzhOQjdXQmdLSkg4CkpvTmhmUlRMSmxpN0ZRWUFrQng1
|
||||
eXVrMzFjRUNiN2VDcTVBRW9nTVBlQzQKLS0tIEJtNk91SnBkcGNBbXRLSjhjMEx4
|
||||
NUZMYTd2MFdYT2YxYTdBMXpRM3ZFUUEKn2rRJePBAZeslthoSZ9+suhqcKZIHR0T
|
||||
z4PTQG6ZY1OVotIbK52JF34yi1FVH020UhFJolD3HZ7W+z1D88Mtqg==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
- recipient: age1lp6rrlvmytp9ka6q89m0e0am26222kwrn7aqd45hu07s3a6jv3gqty86eu
|
||||
enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBGY3crMXhpdFdZZjZLT1Fh
|
||||
QTV0RDVVQzJIbUtwUGwzWHo5QVYwK2JXMUM4ClZlMHJtSkR5dnZoanVxUW4yNlgy
|
||||
ZGRpbUs1TnZQb1NnS2VpaDgyL0NOZW8KLS0tIGFqeGo5dHYyaEl1ZkZxYWpKZGlY
|
||||
TmhxT1F4enhFelJ0bndrT2dqbG95VWsKzyQCNjbGXO98fjmmtrYhv7An4s+BLKpq
|
||||
TtiRp1PFtjb1EF6QwBhNWFuYE9QK08T3m0Dkr32A6PvuiAIrDfaCsQ==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
- recipient: age1e4sd6jjd4uxxsh9xmhdsnu6mqd5h8c4zz4gwme7lkw9ee949fc9q4px9df
|
||||
enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBMNmpEbEVHTllRT0tEM2xo
|
||||
Ulc4bDFVVGNNTTdwTUhvOXk4aGcvcjZpLzJNCmphZFl5NkdxZVIwWnFHWDdSa241
|
||||
dHAvYlVFb0lVNDFQSEdYWStWRjQvNHcKLS0tIFlYVHlWaGszL1pxYStmTWtFcXRW
|
||||
aGErRDZFeDNpMXFneU1jYTVLS2lsWVkK4Zggc2aIxTX+PnfDyPLXsifxnsRwrZ84
|
||||
v9G4lY/ReLaO6xHG/824W2ZIuDW5zsBtdbui8UFSwVsf1XxkOUzSDw==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
- recipient: age19jm7uuam7gkacm3kh2v7uqgkvmmx0slmm9zwdjhd2ln9r60xzd7qh78c5a
|
||||
enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBXQTNCT1dDbTNMM0Jla1Fv
|
||||
WHZ1RStHQTJ2aWd3U21BWml0VWFnT2lVOEhVClFuZDJrUUgyU05Pb1UrV3pXOVhw
|
||||
eUZCTGdvbUNZdGFoVW84VVpMSGNrSGsKLS0tIDgwOTlmMGxPN0xHb215dDRXOW4x
|
||||
S2dmd29FaU5PaHVzb1cvMDlQeTJCY1UK5iLYEfOJM8m6Tml8XhTz6O0NMb1a4y3Z
|
||||
auQnSKjRskPfSRbBf0stv1oAuTENHEbqJiQCOk2aWG+gO5ih36/Ffw==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
- recipient: age1nwnqxjuaxlt5g7fe8rnspvn2c36uuef4hzwuwa6cfjfalz2lrd4q4n5fpl
|
||||
enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSAvZERzWVNJLzFvaHRxL1hN
|
||||
Y2JKcmQzbWYveDlQd3VtL3U4ZE8vclZoeXk4ClNaMERjcnAyREZSNHg5YkgxL3lm
|
||||
MHlwR3JWVkEwZ1NlbzFtbDI4Wmh4cmsKLS0tIDlzUG8yd3daS0FpRm5KcWI5NEFq
|
||||
cjh6bWloSktCTEc4bStQb09UM0FvWncKa/mAWmXffFBGIfQtmQxAtZE/dwzPDdpN
|
||||
a1/eE3nx/9r4M2NhI7mAJbN2e1V7YgjW0xL0kKSSMLutYt6vN4bnEQ==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
- recipient: age1a8z3p24v32l9yxm5z2l8h7rpc3nhacyfv4jvetk2lenrvsdstd3sdu2kaf
|
||||
enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSAyQnQram51YWJMb1VFcGNo
|
||||
L3JJdkRyRzF3U3R6N2FWTlFJdThNb3JBVEZnCmRHZ2o3NFBhc0xOK1VYN2FaTHJV
|
||||
bWNVc0MvQlFTenk3MEJyYzJQM2JCeWMKLS0tIG40K2NUeHhENlJMRmFTSmN0amlW
|
||||
STBERjI3ZFNydXA1Vzcvb1BRZnlRZDQK6p/uU7z9Q0zr3uZLHEgNcK2IR144MFu3
|
||||
BurEmLIWzSfhLJTGkop6ODDKpKORapldMJTigGt2+QZ3jwQwBai9TQ==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
lastmodified: "2024-12-27T04:10:37Z"
|
||||
mac: ENC[AES256_GCM,data:xbJ6G5LQApSWWQfWdcN5DNsaaNdniprNgODtfXHoAGvmOf9r4tYXtI3LPwxXSpyvLIRDv1orasYOxH3m0h5+PIkqegasOuRHcXtRll8e05qD2p/RNPPSAiAl08EvRoJAuu0wYP/GR90/UfYMk6UeRKNFt5YFNdn5CZuyAV/drkc=,iv:zijqky8rBhmGwOJ3gkJDx8UVIFxEtxCPQaq4+2lgwZs=,tag:d1abcbyMZkSV4uolMS7eaA==,type:str]
|
||||
pgp: []
|
||||
unencrypted_suffix: _unencrypted
|
||||
version: 3.9.2
|
|
@ -1,49 +1,112 @@
|
|||
{ lib, config, pkgs, ... }:
|
||||
{
|
||||
lib,
|
||||
config,
|
||||
pkgs,
|
||||
...
|
||||
}:
|
||||
{
|
||||
|
||||
# container builder
|
||||
lib.mySystem.mkContainer = options: (
|
||||
let
|
||||
containerExtraOptions = lib.optionals (lib.attrsets.attrByPath [ "caps" "privileged" ] false options) [ "--privileged" ]
|
||||
++ lib.optionals (lib.attrsets.attrByPath [ "caps" "readOnly" ] false options) [ "--read-only" ]
|
||||
++ lib.optionals (lib.attrsets.attrByPath [ "caps" "tmpfs" ] false options) (map (folders: "--tmpfs=${folders}") options.caps.tmpfsFolders)
|
||||
++ lib.optionals (lib.attrsets.attrByPath [ "caps" "noNewPrivileges" ] false options) [ "--security-opt=no-new-privileges" ]
|
||||
++ lib.optionals (lib.attrsets.attrByPath [ "caps" "dropAll" ] false options) [ "--cap-drop=ALL" ];
|
||||
in
|
||||
{
|
||||
${options.app} = {
|
||||
image = "${options.image}";
|
||||
user = "${options.user}:${options.group}";
|
||||
environment = {
|
||||
TZ = config.time.timeZone;
|
||||
} // lib.attrsets.attrByPath [ "env" ] { } options;
|
||||
dependsOn = lib.attrsets.attrByPath [ "dependsOn" ] [ ] options;
|
||||
entrypoint = lib.attrsets.attrByPath [ "entrypoint" ] null options;
|
||||
cmd = lib.attrsets.attrByPath [ "cmd" ] [ ] options;
|
||||
environmentFiles = lib.attrsets.attrByPath [ "envFiles" ] [ ] options;
|
||||
volumes = [ "/etc/localtime:/etc/localtime:ro" ]
|
||||
++ lib.attrsets.attrByPath [ "volumes" ] [ ] options;
|
||||
ports = lib.attrsets.attrByPath [ "ports" ] [ ] options;
|
||||
extraOptions = containerExtraOptions;
|
||||
};
|
||||
}
|
||||
);
|
||||
lib.mySystem.mkContainer =
|
||||
options:
|
||||
(
|
||||
let
|
||||
containerExtraOptions =
|
||||
lib.optionals (lib.attrsets.attrByPath [ "caps" "privileged" ] false options) [ "--privileged" ]
|
||||
++ lib.optionals (lib.attrsets.attrByPath [ "caps" "readOnly" ] false options) [ "--read-only" ]
|
||||
++ lib.optionals (lib.attrsets.attrByPath [ "caps" "tmpfs" ] false options) (
|
||||
map (folders: "--tmpfs=${folders}") options.caps.tmpfsFolders
|
||||
)
|
||||
++ lib.optionals (lib.attrsets.attrByPath [ "caps" "noNewPrivileges" ] false options) [
|
||||
"--security-opt=no-new-privileges"
|
||||
]
|
||||
++ lib.optionals (lib.attrsets.attrByPath [ "caps" "dropAll" ] false options) [ "--cap-drop=ALL" ];
|
||||
in
|
||||
{
|
||||
${options.app} = {
|
||||
image = "${options.image}";
|
||||
user = "${options.user}:${options.group}";
|
||||
environment = {
|
||||
TZ = config.time.timeZone;
|
||||
} // lib.attrsets.attrByPath [ "env" ] { } options;
|
||||
dependsOn = lib.attrsets.attrByPath [ "dependsOn" ] [ ] options;
|
||||
entrypoint = lib.attrsets.attrByPath [ "entrypoint" ] null options;
|
||||
cmd = lib.attrsets.attrByPath [ "cmd" ] [ ] options;
|
||||
environmentFiles = lib.attrsets.attrByPath [ "envFiles" ] [ ] options;
|
||||
volumes = [
|
||||
"/etc/localtime:/etc/localtime:ro"
|
||||
] ++ lib.attrsets.attrByPath [ "volumes" ] [ ] options;
|
||||
ports = lib.attrsets.attrByPath [ "ports" ] [ ] options;
|
||||
extraOptions = containerExtraOptions;
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
# build a restic restore set for both local and remote
|
||||
lib.mySystem.mkRestic = options: (
|
||||
## Creates a standardized restic backup configuration for both local and remote backups per app.
|
||||
# One S3 bucket per server. Each app has its own repository in the bucket.
|
||||
# Or backup each app it's own remote repository.
|
||||
# Takes an attribute set with:
|
||||
# - app: name of the application (used for backup naming)
|
||||
# - user: user to run the backup as
|
||||
# - localResticTemplate: template for local restic backup
|
||||
# - passwordFile: path to the password file
|
||||
# - paths: list of paths to backup
|
||||
# - remoteResticTemplate: template for remote restic backup
|
||||
# - environmentFile (optional): path to the env file
|
||||
# - excludePaths (optional): list of paths to exclude from backup
|
||||
# Configures:
|
||||
# - Daily backups at 02:05 with 3h random delay
|
||||
# - Retention: 7 daily, 5 weekly, 12 monthly backups
|
||||
# - Automatic stale lock removal
|
||||
# - Uses system-configured backup paths and credentials
|
||||
#
|
||||
# Example usage:
|
||||
# services.restic.backups = config.lib.mySystem.mkRestic {
|
||||
# app = "nextcloud";
|
||||
# paths = [ "/nahar/containers/volumes/nextcloud" ];
|
||||
# excludePaths = [ "/nahar/containers/volumes/nextcloud/data/cache" ];
|
||||
# user = "kah";
|
||||
# localResticTemplate = "/eru/restic/nextcloud";
|
||||
# remoteResticTemplate = "rest:https://user:password@x.repo.borgbase.com";
|
||||
# remoteResticTemplate = "s3:https://x.r2.cloudflarestorage.com/resticRepos";
|
||||
# remoteResticTemplateFile = "/run/secrets/restic/nextcloud/template";
|
||||
# passwordFile = "/run/secrets/restic/nextcloud/password";
|
||||
# environmentFile = "/run/secrets/restic/nextcloud/env";
|
||||
# };
|
||||
# This creates two backup jobs:
|
||||
# - nextcloud-local: backs up to local storage
|
||||
# - nextcloud-remote: backs up to remote storage (e.g. S3)
|
||||
lib.mySystem.mkRestic =
|
||||
options:
|
||||
let
|
||||
# excludePaths is optional
|
||||
excludePaths = if builtins.hasAttr "excludePaths" options then options.excludePaths else [ ];
|
||||
# Decide which mutually exclusive options to use
|
||||
remoteResticTemplateFile =
|
||||
if builtins.hasAttr "remoteResticTemplateFile" options then
|
||||
options.remoteResticTemplateFile
|
||||
else
|
||||
null;
|
||||
remoteResticTemplate =
|
||||
if builtins.hasAttr "remoteResticTemplate" options then
|
||||
options.remoteResticTemplate
|
||||
else
|
||||
null;
|
||||
# 2:05 daily backup with 3h random delay
|
||||
timerConfig = {
|
||||
OnCalendar = "02:05";
|
||||
Persistent = true;
|
||||
RandomizedDelaySec = "3h";
|
||||
};
|
||||
# 7 daily, 5 weekly, 12 monthly backups
|
||||
pruneOpts = [
|
||||
"--keep-daily 7"
|
||||
"--keep-weekly 5"
|
||||
"--keep-monthly 12"
|
||||
];
|
||||
# Initialize the repository if it doesn't exist
|
||||
initialize = true;
|
||||
# Only one backup is ever running at a time it's safe to say that we can remove stale locks
|
||||
backupPrepareCommand = ''
|
||||
# remove stale locks - this avoids some occasional annoyance
|
||||
#
|
||||
|
@ -53,28 +116,33 @@
|
|||
{
|
||||
# local backup
|
||||
"${options.app}-local" = {
|
||||
inherit pruneOpts timerConfig initialize backupPrepareCommand;
|
||||
inherit
|
||||
pruneOpts
|
||||
timerConfig
|
||||
initialize
|
||||
backupPrepareCommand
|
||||
;
|
||||
inherit (options) user passwordFile environmentFile;
|
||||
# Move the path to the zfs snapshot path
|
||||
paths = map (x: "${config.mySystem.system.resticBackup.mountPath}/${x}") options.paths;
|
||||
passwordFile = config.sops.secrets."services/restic/password".path;
|
||||
exclude = excludePaths;
|
||||
repository = "${config.mySystem.system.resticBackup.local.location}/${options.appFolder}";
|
||||
# inherit (options) user;
|
||||
paths = map (x: "${config.mySystem.services.zfs-nightly-snap.mountPath}/${x}") options.paths;
|
||||
exclude = map (x: "${config.mySystem.services.zfs-nightly-snap.mountPath}/${x}") options.excludePaths;
|
||||
repository = "${options.localResticTemplate}";
|
||||
};
|
||||
|
||||
# remote backup
|
||||
"${options.app}-remote" = {
|
||||
inherit pruneOpts timerConfig initialize backupPrepareCommand;
|
||||
inherit
|
||||
pruneOpts
|
||||
timerConfig
|
||||
initialize
|
||||
backupPrepareCommand
|
||||
;
|
||||
inherit (options) user passwordFile environmentFile;
|
||||
# Move the path to the zfs snapshot path
|
||||
paths = map (x: "${config.mySystem.system.resticBackup.mountPath}/${x}") options.paths;
|
||||
environmentFile = config.sops.secrets."services/restic/env".path;
|
||||
passwordFile = config.sops.secrets."services/restic/password".path;
|
||||
repository = "${config.mySystem.system.resticBackup.remote.location}/${options.appFolder}";
|
||||
exclude = excludePaths;
|
||||
# inherit (options) user;
|
||||
paths = map (x: "${config.mySystem.services.zfs-nightly-snap.mountPath}/${x}") options.paths;
|
||||
repository = remoteResticTemplate;
|
||||
repositoryFile = remoteResticTemplateFile;
|
||||
exclude = map (x: "${config.mySystem.services.zfs-nightly-snap.mountPath}/${x}") options.excludePaths;
|
||||
};
|
||||
|
||||
}
|
||||
);
|
||||
|
||||
};
|
||||
}
|
||||
|
|
|
@ -7,11 +7,12 @@
|
|||
./libvirt-qemu
|
||||
./matchbox
|
||||
./nginx
|
||||
./nix-index-daily
|
||||
./onepassword-connect
|
||||
./podman
|
||||
./reboot-required-check.nix
|
||||
./restic
|
||||
./sanoid
|
||||
./syncthing
|
||||
./zfs-nightly-snap
|
||||
];
|
||||
}
|
||||
|
|
|
@ -5,55 +5,45 @@
|
|||
...
|
||||
}:
|
||||
let
|
||||
cfg = config.services.nix-index-daily;
|
||||
cfg = config.mySystem.services.nix-index-daily;
|
||||
in
|
||||
{
|
||||
options.services.nix-index-daily = {
|
||||
options.mySystem.services.nix-index-daily = {
|
||||
enable = lib.mkEnableOption "Automatic daily nix-index database updates";
|
||||
|
||||
user = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
description = "User account under which to run nix-index";
|
||||
example = "alice";
|
||||
example = "jahanson";
|
||||
};
|
||||
|
||||
startTime = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
default = "daily";
|
||||
description = "When to start the service. See systemd.time(7)";
|
||||
example = "03:00";
|
||||
};
|
||||
|
||||
randomizedDelaySec = lib.mkOption {
|
||||
type = lib.types.int;
|
||||
default = 3600;
|
||||
description = "Random delay in seconds after startTime";
|
||||
example = 1800;
|
||||
example = "05:00";
|
||||
};
|
||||
};
|
||||
|
||||
config = lib.mkIf cfg.enable {
|
||||
users.users.${cfg.user}.packages = [ pkgs.nix-index ];
|
||||
|
||||
systemd.user.services.nix-index-update = {
|
||||
description = "Update nix-index database";
|
||||
script = "${pkgs.nix-index}/bin/nix-index";
|
||||
serviceConfig = {
|
||||
Type = "oneshot";
|
||||
systemd.user = {
|
||||
# Timer for nix-index update
|
||||
timers.nix-index-update = {
|
||||
wantedBy = [ "timers.target" ];
|
||||
partOf = [ "nix-index-update.service" ];
|
||||
timerConfig = {
|
||||
OnCalendar = cfg.startTime;
|
||||
Persistent = true;
|
||||
};
|
||||
};
|
||||
# Service for nix-index update
|
||||
services.nix-index-update = {
|
||||
description = "Update nix-index database";
|
||||
script = "${pkgs.nix-index}/bin/nix-index";
|
||||
serviceConfig = {
|
||||
Type = "oneshot";
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
systemd.user.timers.nix-index-update = {
|
||||
wantedBy = [ "timers.target" ];
|
||||
timerConfig = {
|
||||
OnCalendar = cfg.startTime;
|
||||
Persistent = true;
|
||||
RandomizedDelaySec = cfg.randomizedDelaySec;
|
||||
};
|
||||
};
|
||||
|
||||
# Ensure the services are enabled
|
||||
systemd.user.services.nix-index-update.enable = true;
|
||||
systemd.user.timers.nix-index-update.enable = true;
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,108 +0,0 @@
|
|||
{ lib, config, pkgs, ... }:
|
||||
with lib;
|
||||
let
|
||||
cfg = config.mySystem.system.resticBackup;
|
||||
in
|
||||
{
|
||||
options.mySystem.system.resticBackup = {
|
||||
local = {
|
||||
enable = mkEnableOption "Local backups" // { default = true; };
|
||||
noWarning = mkOption
|
||||
{
|
||||
type = types.bool;
|
||||
description = "Disable warning for local backups";
|
||||
default = false;
|
||||
};
|
||||
location = mkOption
|
||||
{
|
||||
type = types.str;
|
||||
description = "Location for local backups";
|
||||
default = "";
|
||||
};
|
||||
};
|
||||
remote = {
|
||||
enable = mkEnableOption "Remote backups" // { default = true; };
|
||||
noWarning = mkOption
|
||||
{
|
||||
type = types.bool;
|
||||
description = "Disable warning for remote backups";
|
||||
default = false;
|
||||
};
|
||||
location = mkOption
|
||||
{
|
||||
type = types.str;
|
||||
description = "Location for remote backups";
|
||||
default = "";
|
||||
};
|
||||
};
|
||||
mountPath = mkOption
|
||||
{
|
||||
type = types.str;
|
||||
description = "Location for snapshot mount";
|
||||
default = "/mnt/nightly_backup";
|
||||
};
|
||||
};
|
||||
config = {
|
||||
|
||||
# Warn if backups are disable and machine isnt a dev box
|
||||
warnings = [
|
||||
(mkIf (!cfg.local.noWarning && !cfg.local.enable && config.mySystem.purpose != "Development") "WARNING: Local backups are disabled for ${config.system.name}!")
|
||||
(mkIf (!cfg.remote.noWarning && !cfg.remote.enable && config.mySystem.purpose != "Development") "WARNING: Remote backups are disabled for ${config.system.name}!")
|
||||
];
|
||||
|
||||
sops.secrets = mkIf (cfg.local.enable || cfg.remote.enable) {
|
||||
"services/restic/password" = {
|
||||
sopsFile = ./secrets.sops.yaml;
|
||||
owner = "kah";
|
||||
group = "kah";
|
||||
};
|
||||
|
||||
"services/restic/env" = {
|
||||
sopsFile = ./secrets.sops.yaml;
|
||||
owner = "kah";
|
||||
group = "kah";
|
||||
};
|
||||
};
|
||||
|
||||
# useful commands:
|
||||
# view snapshots - zfs list -t snapshot
|
||||
|
||||
# below takes a snapshot of the zfs persist volume
|
||||
# ready for restic syncs
|
||||
# essentially its a nightly rotation of atomic state at 2am.
|
||||
|
||||
# this is the safest option, as if you run restic
|
||||
# on live services/databases/etc, you will have
|
||||
# a bad day when you try and restore
|
||||
# (backing up a in-use file can and will cause corruption)
|
||||
|
||||
# ref: https://cyounkins.medium.com/correct-backups-require-filesystem-snapshots-23062e2e7a15
|
||||
systemd = mkIf (cfg.local.enable || cfg.remote.enable) {
|
||||
|
||||
timers.restic_nightly_snapshot = {
|
||||
description = "Nightly ZFS snapshot timer";
|
||||
wantedBy = [ "timers.target" ];
|
||||
partOf = [ "restic_nightly_snapshot.service" ];
|
||||
timerConfig.OnCalendar = "2:00";
|
||||
timerConfig.Persistent = "true";
|
||||
};
|
||||
|
||||
# recreate snapshot and mount, ready for backup
|
||||
# I used mkdir -p over a nix tmpfile, as mkdir -p exits cleanly
|
||||
# if the folder already exists, and tmpfiles complain
|
||||
# if the folder exists and is already mounted.
|
||||
services.restic_nightly_snapshot = {
|
||||
description = "Nightly ZFS snapshot for Restic";
|
||||
path = with pkgs; [ zfs busybox ];
|
||||
serviceConfig.Type = "simple";
|
||||
script = ''
|
||||
mkdir -p /mnt/nightly_backup/ && \
|
||||
umount ${cfg.mountPath} || true && \
|
||||
zfs destroy rpool/safe/persist@restic_nightly_snap || true && \
|
||||
zfs snapshot rpool/safe/persist@restic_nightly_snap && \
|
||||
mount -t zfs rpool/safe/persist@restic_nightly_snap ${cfg.mountPath}
|
||||
'';
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
|
@ -1,88 +0,0 @@
|
|||
services:
|
||||
restic:
|
||||
password: ENC[AES256_GCM,data:QPU=,iv:6FYmdgpKLplg1uIkXNvyA+DW493xdMLsBLnbenabz+M=,tag:SVY2mEhoPP/exDOENzVRGg==,type:str]
|
||||
repository: ENC[AES256_GCM,data:VGtSJA==,iv:K4FnYzTrfVhjMWf4R7qgPUCdgWFlQAG8JJccfRYlEWM=,tag:43onghqVr44slin0rlIUgQ==,type:str]
|
||||
env: ENC[AES256_GCM,data:TWUJ/GE84CTiLo1Gud+XsA==,iv:gKC1VcWnGqEwn5+e5jIqsIfipi3X2oHGvrG0rgqQl9E=,tag:QIBfXblvSDxAVYbZGAN3Mg==,type:str]
|
||||
sops:
|
||||
kms: []
|
||||
gcp_kms: []
|
||||
azure_kv: []
|
||||
hc_vault: []
|
||||
age:
|
||||
- recipient: age1d9p83j52m2xg0vh9k7q0uwlxwhs3y6tlv68yg9s2h9mdw2fmmsqshddz5m
|
||||
enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSAvRUJEU25EaUhacWFBOVg5
|
||||
TWI3NmtkWFpONHRVZ1BVSVRsQzMraVdmblFBCmd2NzcwMGRTMTR6ck9lcGZSQmVi
|
||||
dHlFeS9RNENKcDEvS2FiRTVrYjVlUGcKLS0tIG1VSW9sejVWZmJHQXlIOVpLMjds
|
||||
SHV6U2ZhUnVpQVNROGNjNEtZZXI1bEUKXjSwBNA8ylfo4CWlefFfajm2JdYtjUVK
|
||||
bqXlIH/nG+nQ+I4Rj1XHo7hAuxCatuN0bGVBkSlzqIZk58/JladwFg==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
- recipient: age1m83ups8xn2jy4ayr8gw0pyn34smr0huqc5v76e4887az4vsl4yzsj0dlhd
|
||||
enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBMWis3TWZ0djY4YnJNek9N
|
||||
T2VXK0IzaStkMisyaUs5MTVHeXY4bytoUWdnCmlmTmRXRlRwOUZVQm5aWkxSKzFB
|
||||
UzhtbWd2Q09sbTJPeDRWeTFESkcwWUUKLS0tIDVaN0d4UGlTZUhIaXVKaXJRNThS
|
||||
algwTTZsVzNTQngzVUwyU2lpNll0bU0Kjz+34mvPPAfGUQKMH6LXawGou9HjBTjJ
|
||||
p9vxncB+7ykvT4e4Z0PpPE/Zo5yvi9rt1T8bZ6dG7GA5vuE/4BarCA==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
- recipient: age18kj3xhlvgjeg2awwku3r8d95w360uysu0w5ejghnp4kh8qmtge5qwa2vjp
|
||||
enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSByK2FNS0tJaTdRQzA0VVky
|
||||
aERMTVdqRzBwWFV1WFJJcVRKSTFIUlh3U0E0CmFKZm9jUHBpRjJCZk9PVkNWVEFU
|
||||
RURReEhGNTRmWWpLa1ZNdVFHK3FQQWMKLS0tIHcrMTBiMGhlcFc3RzlmVEp2OEpX
|
||||
ZHZLdXV4a05NaGRmR2Z1SkZCV25kNUEKHU1v1OK0d2ud7QL+gEoA8R4Z5YgVSP42
|
||||
IvnEQxjjXZjC4p+OjFErKcWrVb+3DGzqF1vngJVrXmIgOx/SZKTa/Q==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
- recipient: age1lp6rrlvmytp9ka6q89m0e0am26222kwrn7aqd45hu07s3a6jv3gqty86eu
|
||||
enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA3MytrUFpsMUVpT3pTNWlq
|
||||
NjMrRjI5a3NqNzlNV2JlczJRNXNicVZaWVdNCjNnRHM2RGV1SEh6M0U3T0NvdlNQ
|
||||
a1JIZFp5bHJwMXlNd29DQ2MwckRrczAKLS0tIHdmd2lFZ1FWTFFMUExPeWRXd2U3
|
||||
RU9UYXJESnAyYXFITTN0cm5QelR2T1UK3XUlIGQED91sUPc1ITq1rXLj/xhkGM9s
|
||||
R4bsTK5RqpXE+RmGfxeAMP7Om424vjM76l6DU2JkoZietDwR35UA8w==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
- recipient: age1e4sd6jjd4uxxsh9xmhdsnu6mqd5h8c4zz4gwme7lkw9ee949fc9q4px9df
|
||||
enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBjc0haNU95V3JRUlpuUjha
|
||||
SHpOWThJWVMwbElRaFcrL21jYXA2SFBHeFR3CnV1MkRxbG9QV1dWdjJxWENtQk5L
|
||||
M1g0cDJXRjN0VFhiRXZKbG1yS3hXaG8KLS0tIEtScWorRENpbFZWMjVXNnIxTTdi
|
||||
djdBdThNMzFZdlI4TVBJSjdxeXg0VE0Kcwsa/et9gMSlm46rt0vZ/dFy3ZCZQ5Oi
|
||||
WLJ492+srIeE47Gpye2jN2XAmM4exCijYkZeQvPpLIFvBFmQCK30hQ==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
- recipient: age19jm7uuam7gkacm3kh2v7uqgkvmmx0slmm9zwdjhd2ln9r60xzd7qh78c5a
|
||||
enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBMTDI0QXZaMlZLUW9ST0lW
|
||||
Q1M1ZmlpTHpvM0NHejFSNEx0UUFnTVJIN0U4CllRcnVpUjFqOUZRRk5CWXZqT0V0
|
||||
YWwweld0TE9zZGFmUTVDVVl6eDNETzAKLS0tIGtEanVWTHgxSk9Ld3NRYndOL3dZ
|
||||
WXJrUWtncDZjVE50dmw2MHRCelpzZ2cKfLIQbrTsVGXY+UZCC5p/7+bXKHhv8nxt
|
||||
dvvr+VGnH57jmELqSUoWOgefJ6GFNcCoGSYHZ9cn0UgvhZgx1Wpoow==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
- recipient: age1nwnqxjuaxlt5g7fe8rnspvn2c36uuef4hzwuwa6cfjfalz2lrd4q4n5fpl
|
||||
enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBRN2M0VmVCQ0JaNVhnRzBj
|
||||
Z2Vqbk9GZUtaZlExYTRPQ3ZJWHIvU283cFRBCjExQnJvZy9SMndJd0VqdUpCSDFJ
|
||||
ZmJpVFJ1em9iNnNOcnFTQUExeGZESm8KLS0tIGdnWXNtNEg2SHpjRW1mR28vVDRv
|
||||
VFVRcDh0TlVXR3pYRk1Ybkx3MjhOaVEKsViUc14dePdnukQa3ud/EesnvZL7OCM1
|
||||
HWJYP81C9O4mU1kwRYtC0lGxMQX6aWiFZ5e2ImSi3w+mBP+KihfmBw==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
- recipient: age1a8z3p24v32l9yxm5z2l8h7rpc3nhacyfv4jvetk2lenrvsdstd3sdu2kaf
|
||||
enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBCUlZ1TER2anNCRHBKQm1v
|
||||
QjhybHFCc1dod1djeWxkRmhBSC9YTW5IV0NJCkM5c3hkYWtLZnJHNVpPYUh4TzBR
|
||||
U3ZaMEdSTVNsenV0RVorTTZMUXdYT3MKLS0tIDV1dWxjbXNtekZaUk9xaVdOYU93
|
||||
UUpVako2MGVobTcvNWRsTWMwZm5ZSVEK1uI5dVSI4vY5hw0oxj21mJYoZB2Jq52z
|
||||
e+RDvcyBFRsS+238UCVi5qDdA8DcnQ2uRiBxKDGC2P3RoVU5TeCfTQ==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
lastmodified: "2024-09-18T23:57:27Z"
|
||||
mac: ENC[AES256_GCM,data:88ZnGTkV1xxZO7UuVm5clZrHUMeiqAG++4X4DbCJGwqL+VDagYVhsui1+PzN62h6TgXtARecHON8TXd8z/NF4ekiY+LAcMC3m9x5AzmGYa7Qd5FKht1O6RfRORBDrojj251cqCifDxeGPq3C/X4Zi8Jg4KTSk1lAJoXMsqJQ3+c=,iv:8NnKOlzXD1jRVQ/tgoChEb0YY18Y7VpEiq85YhupTws=,tag:eUbLR66sNqQ2VIQW0/CBwA==,type:str]
|
||||
pgp: []
|
||||
unencrypted_suffix: _unencrypted
|
||||
version: 3.8.1
|
229
nixos/modules/nixos/services/zfs-nightly-snap/default.nix
Normal file
229
nixos/modules/nixos/services/zfs-nightly-snap/default.nix
Normal file
|
@ -0,0 +1,229 @@
|
|||
{
|
||||
config,
|
||||
lib,
|
||||
pkgs,
|
||||
...
|
||||
}:
|
||||
|
||||
let
|
||||
cfg = config.mySystem.services.zfs-nightly-snap;
|
||||
|
||||
# Replaces/Creates and mounts a ZFS snapshot
|
||||
resticSnapAndMount = pkgs.writeShellApplication {
|
||||
name = "zfs-nightly-snap";
|
||||
|
||||
runtimeInputs = with pkgs; [
|
||||
busybox # for id, mount, umount, mkdir, grep, echo
|
||||
zfs # for zfs
|
||||
];
|
||||
|
||||
text = ''
|
||||
# Check if running as root
|
||||
if [ "$(id -u)" -ne 0 ]; then
|
||||
echo "Error: This script must be run as root."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
BACKUP_DIRECTORY="${cfg.mountPath}"
|
||||
ZFS_DATASET="${cfg.zfsDataset}"
|
||||
SNAPSHOT_NAME="${cfg.snapshotName}"
|
||||
|
||||
# functions sourced from: https://github.com/Jip-Hop/zfs-backup-snapshots
|
||||
# some enhancements made to the original code to adhere to best practices
|
||||
# mounts all zfs filesystems under $ZFS_DATASET
|
||||
function mount_dataset() {
|
||||
# ensure BACKUP_DIRECTORY exists
|
||||
mkdir -p "$BACKUP_DIRECTORY"
|
||||
# get list of all zfs filesystems under $ZFS_DATASET
|
||||
# exclude if mountpoint "legacy" and "none" mountpoint
|
||||
# order by shallowest mountpoint first (determined by number of slashes)
|
||||
mapfile -t fs < <(zfs list "$ZFS_DATASET" -r -H -o name,mountpoint | grep -E "(legacy)$|(none)$" -v | awk '{print gsub("/","/", $2), $1}' | sort -n | cut -d' ' -f2-)
|
||||
|
||||
for fs in "''${fs[@]}"; do
|
||||
mount_latest_snap "''${fs}" "$BACKUP_DIRECTORY"
|
||||
done
|
||||
return 0
|
||||
}
|
||||
|
||||
# umounts and cleans up the backup directory
|
||||
# usage: zfs_backup_cleanup BACKUP_DIRECTORY
|
||||
function zfs_backup_cleanup() {
|
||||
# get all filesystems mounted within the backup directory
|
||||
mapfile -t fs < <(tac /etc/mtab | cut -d " " -f 2 | grep "''${1}")
|
||||
|
||||
# umount said filesystems
|
||||
for i in "''${fs[@]}"; do
|
||||
echo "Unmounting $i"
|
||||
umount "$i"
|
||||
done
|
||||
|
||||
# delete empty directories from within the backup directory
|
||||
find "''${1}" -type d -empty -delete
|
||||
}
|
||||
|
||||
# gets the name of the newest snapshot given a zfs filesystem
|
||||
# usage: get_latest_snap filesystem
|
||||
function zfs_latest_snap() {
|
||||
snapshot=$(zfs list -H -t snapshot -o name -S creation -d1 "''${1}" | head -1 | cut -d '@' -f 2)
|
||||
if [[ -z $snapshot ]]; then
|
||||
# if there's no snapshot then let's ignore it
|
||||
echo "No snapshot exists for ''${1}, it will not be backed up."
|
||||
return 1
|
||||
fi
|
||||
echo "$snapshot"
|
||||
}
|
||||
|
||||
# gets the path of a snapshot given a zfs filesystem and a snapshot name
|
||||
# usage zfs_snapshot_mountpoint filesystem snapshot
|
||||
function zfs_snapshot_mountpoint() {
|
||||
# get mountpoint for filesystem
|
||||
mountpoint=$(zfs list -H -o mountpoint "''${1}")
|
||||
|
||||
# exit if filesystem doesn't exist
|
||||
if [[ $? == 1 ]]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
# build out path
|
||||
path="''${mountpoint}/.zfs/snapshot/''${2}"
|
||||
|
||||
# check to make sure path exists
|
||||
if stat "''${path}" &> /dev/null; then
|
||||
echo "''${path}"
|
||||
return 0
|
||||
else
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# mounts latest snapshot in directory
|
||||
# usage: mount_latest_snap filesystem BACKUP_DIRECTORY
|
||||
function mount_latest_snap() {
|
||||
local mount_point="''${2}"
|
||||
local filesystem="''${1}"
|
||||
|
||||
# get name of latest snapshot
|
||||
snapshot=$(zfs_latest_snap "''${filesystem}")
|
||||
|
||||
# if there's no snapshot then let's ignore it
|
||||
if [[ $? == 1 ]]; then
|
||||
echo "No snapshot exists for ''${filesystem}, it will not be backed up."
|
||||
return 1
|
||||
fi
|
||||
|
||||
sourcepath=$(zfs_snapshot_mountpoint "''${filesystem}" "''${snapshot}")
|
||||
# if the filesystem is not mounted/path doesn't exist then let's ignore as well
|
||||
if [[ $? == 1 ]]; then
|
||||
echo "Cannot find snapshot ''${snapshot} for ''${filesystem}, perhaps it's not mounted? Anyways, it will not be backed up."
|
||||
return 1
|
||||
fi
|
||||
|
||||
# mountpath may be inside a previously mounted snapshot
|
||||
mountpath="$mount_point/''${filesystem}"
|
||||
|
||||
# mount to backup directory using a bind filesystem
|
||||
mkdir -p "''${mountpath}"
|
||||
echo "mount ''${sourcepath} => ''${mountpath}"
|
||||
mount --bind --read-only "''${sourcepath}" "''${mountpath}"
|
||||
return 0
|
||||
}
|
||||
|
||||
# Unmount and cleanup if necessary
|
||||
zfs_backup_cleanup "$BACKUP_DIRECTORY"
|
||||
|
||||
# Check if snapshot exists
|
||||
echo "Previous snapshot:"
|
||||
zfs list -t snapshot | grep "$ZFS_DATASET@$SNAPSHOT_NAME" || true
|
||||
|
||||
# Attempt to destroy existing snapshot
|
||||
echo "Attempting to destroy existing snapshot..."
|
||||
if zfs list -t snapshot | grep -q "$ZFS_DATASET@$SNAPSHOT_NAME"; then
|
||||
if zfs destroy -r "$ZFS_DATASET@$SNAPSHOT_NAME"; then
|
||||
echo "Successfully destroyed old snapshot"
|
||||
else
|
||||
echo "Failed to destroy existing snapshot"
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
echo "No existing snapshot found"
|
||||
fi
|
||||
|
||||
# Create new snapshot
|
||||
if ! zfs snapshot -r "$ZFS_DATASET@$SNAPSHOT_NAME"; then
|
||||
echo "Failed to create snapshot"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "New snapshot created:"
|
||||
zfs list -t snapshot | grep "$ZFS_DATASET@$SNAPSHOT_NAME"
|
||||
|
||||
# Mount the snapshot
|
||||
if ! mount_dataset; then
|
||||
echo "Failed to mount snapshot"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Successfully created and mounted snapshot at $BACKUP_DIRECTORY"
|
||||
mount | grep "$BACKUP_DIRECTORY"
|
||||
'';
|
||||
};
|
||||
in
|
||||
{
|
||||
options.mySystem.services.zfs-nightly-snap = {
|
||||
enable = lib.mkEnableOption "ZFS nightly snapshot service";
|
||||
|
||||
mountPath = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
description = "Location for the nightly snapshot mount";
|
||||
default = "/mnt/nightly_backup";
|
||||
};
|
||||
zfsDataset = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
description = "Location of the dataset to be snapshot";
|
||||
default = "nahar/containers/volumes";
|
||||
};
|
||||
snapshotName = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
description = "Name of the nightly snapshot";
|
||||
default = "restic_nightly_snap";
|
||||
};
|
||||
startAt = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
default = "*-*-* 02:00:00 America/Chicago"; # Every day at 2 AM
|
||||
description = "When to create and mount the ZFS snapshot. Defaults to 2 AM.";
|
||||
};
|
||||
};
|
||||
|
||||
config = lib.mkIf cfg.enable {
|
||||
# Warn if backups are disabled and machine isnt a dev box
|
||||
warnings = [
|
||||
(lib.mkIf (
|
||||
!cfg.enable && config.mySystem.purpose != "Development"
|
||||
) "WARNING: ZFS nightly snapshot is disabled for ${config.system.name}!")
|
||||
];
|
||||
|
||||
# Adding script to system packages
|
||||
environment.systemPackages = [ resticSnapAndMount ];
|
||||
|
||||
systemd = {
|
||||
# Timer for nightly snapshot
|
||||
timers.zfs-nightly-snap = {
|
||||
wantedBy = [ "timers.target" ];
|
||||
timerConfig = {
|
||||
OnCalendar = cfg.startAt;
|
||||
Persistent = true; # Run immediately if we missed the last trigger time
|
||||
};
|
||||
};
|
||||
# Service for nightly snapshot
|
||||
services.zfs-nightly-snap = {
|
||||
description = "Create and mount nightly ZFS snapshot";
|
||||
serviceConfig = {
|
||||
Type = "oneshot";
|
||||
ExecStart = "${lib.getExe resticSnapAndMount}";
|
||||
};
|
||||
requires = [ "zfs.target" ];
|
||||
after = [ "zfs.target" ];
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
153
nixos/modules/nixos/services/zfs-nightly-snap/snap-and-mount.sh
Executable file
153
nixos/modules/nixos/services/zfs-nightly-snap/snap-and-mount.sh
Executable file
|
@ -0,0 +1,153 @@
|
|||
#!/usr/bin/env nix-shell
|
||||
#!nix-shell -I nixpkgs=/etc/nix/inputs/nixpkgs -i bash -p busybox zfs
|
||||
# shellcheck disable=SC1008
|
||||
|
||||
set -e # Exit on error
|
||||
|
||||
BACKUP_DIRECTORY="/mnt/restic_nightly_backup"
|
||||
ZFS_DATASET="nahar/containers/volumes"
|
||||
SNAPSHOT_NAME="restic_nightly_snap"
|
||||
|
||||
# Check if running as root
|
||||
if [ "$(id -u)" -ne 0 ]; then
|
||||
echo "Error: This script must be run as root."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# functions sourced from: https://github.com/Jip-Hop/zfs-backup-snapshots
|
||||
# some enhancements made to the original code to adhere to best practices
|
||||
# mounts all zfs filesystems under $ZFS_DATASET
|
||||
function mount_dataset() {
|
||||
# ensure BACKUP_DIRECTORY exists
|
||||
mkdir -p $BACKUP_DIRECTORY
|
||||
# get list of all zfs filesystems under $ZFS_DATASET
|
||||
# exclude if mountpoint "legacy" and "none" mountpoint
|
||||
# order by shallowest mountpoint first (determined by number of slashes)
|
||||
mapfile -t fs < <(zfs list "$ZFS_DATASET" -r -H -o name,mountpoint | grep -E "(legacy)$|(none)$" -v | awk '{print gsub("/","/", $2), $1}' | sort -n | cut -d' ' -f2-)
|
||||
|
||||
for fs in "${fs[@]}"; do
|
||||
mount_latest_snap "${fs}" "${BACKUP_DIRECTORY}"
|
||||
done
|
||||
return 0
|
||||
}
|
||||
|
||||
# umounts and cleans up the backup directory
|
||||
# usage: zfs_backup_cleanup BACKUP_DIRECTORY
|
||||
function zfs_backup_cleanup() {
|
||||
# get all filesystems mounted within the backup directory
|
||||
mapfile -t fs < <(tac /etc/mtab | cut -d " " -f 2 | grep "${1}")
|
||||
|
||||
# umount said filesystems
|
||||
for i in "${fs[@]}"; do
|
||||
echo "Unmounting $i"
|
||||
umount "$i"
|
||||
done
|
||||
|
||||
# delete empty directories from within the backup directory
|
||||
find "${1}" -type d -empty -delete
|
||||
}
|
||||
|
||||
# gets the name of the newest snapshot given a zfs filesystem
|
||||
# usage: get_latest_snap filesystem
|
||||
function zfs_latest_snap() {
|
||||
snapshot=$(zfs list -H -t snapshot -o name -S creation -d1 "${1}" | head -1 | cut -d '@' -f 2)
|
||||
if [[ -z $snapshot ]]; then
|
||||
# if there's no snapshot then let's ignore it
|
||||
echo "No snapshot exists for ${1}, it will not be backed up."
|
||||
return 1
|
||||
fi
|
||||
echo "$snapshot"
|
||||
}
|
||||
|
||||
# gets the path of a snapshot given a zfs filesystem and a snapshot name
|
||||
# usage zfs_snapshot_mountpoint filesystem snapshot
|
||||
function zfs_snapshot_mountpoint() {
|
||||
# get mountpoint for filesystem
|
||||
mountpoint=$(zfs list -H -o mountpoint "${1}")
|
||||
|
||||
# exit if filesystem doesn't exist
|
||||
if [[ $? == 1 ]]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
# build out path
|
||||
path="${mountpoint}/.zfs/snapshot/${2}"
|
||||
|
||||
# check to make sure path exists
|
||||
if stat "${path}" &> /dev/null; then
|
||||
echo "${path}"
|
||||
return 0
|
||||
else
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# mounts latest snapshot in directory
|
||||
# usage: mount_latest_snap filesystem BACKUP_DIRECTORY
|
||||
function mount_latest_snap() {
|
||||
BACKUP_DIRECTORY="${2}"
|
||||
filesystem="${1}"
|
||||
|
||||
# get name of latest snapshot
|
||||
snapshot=$(zfs_latest_snap "${filesystem}")
|
||||
|
||||
# if there's no snapshot then let's ignore it
|
||||
if [[ $? == 1 ]]; then
|
||||
echo "No snapshot exists for ${filesystem}, it will not be backed up."
|
||||
return 1
|
||||
fi
|
||||
|
||||
sourcepath=$(zfs_snapshot_mountpoint "${filesystem}" "${snapshot}")
|
||||
# if the filesystem is not mounted/path doesn't exist then let's ignore as well
|
||||
if [[ $? == 1 ]]; then
|
||||
echo "Cannot find snapshot ${snapshot} for ${filesystem}, perhaps it's not mounted? Anyways, it will not be backed up."
|
||||
return 1
|
||||
fi
|
||||
|
||||
# mountpath may be inside a previously mounted snapshot
|
||||
mountpath=${BACKUP_DIRECTORY}/${filesystem}
|
||||
|
||||
# mount to backup directory using a bind filesystem
|
||||
mkdir -p "${mountpath}"
|
||||
echo "mount ${sourcepath} => ${mountpath}"
|
||||
mount --bind --read-only "${sourcepath}" "${mountpath}"
|
||||
return 0
|
||||
}
|
||||
|
||||
# Unmount and cleanup if necessary
|
||||
zfs_backup_cleanup "$BACKUP_DIRECTORY"
|
||||
|
||||
# Check if snapshot exists
|
||||
echo "Previous snapshot:"
|
||||
zfs list -t snapshot | grep "$ZFS_DATASET@$SNAPSHOT_NAME" || true
|
||||
|
||||
# Attempt to destroy existing snapshot
|
||||
echo "Attempting to destroy existing snapshot..."
|
||||
if zfs list -t snapshot | grep -q "$ZFS_DATASET@$SNAPSHOT_NAME"; then
|
||||
if zfs destroy -r "$ZFS_DATASET@$SNAPSHOT_NAME"; then
|
||||
echo "Successfully destroyed old snapshot"
|
||||
else
|
||||
echo "Failed to destroy existing snapshot"
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
echo "No existing snapshot found"
|
||||
fi
|
||||
|
||||
# Create new snapshot
|
||||
if ! zfs snapshot -r "$ZFS_DATASET@$SNAPSHOT_NAME"; then
|
||||
echo "Failed to create snapshot"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "New snapshot created:"
|
||||
zfs list -t snapshot | grep "$ZFS_DATASET@$SNAPSHOT_NAME"
|
||||
|
||||
# Mount the snapshot
|
||||
if ! mount_dataset; then
|
||||
echo "Failed to mount snapshot"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Successfully created and mounted snapshot at $BACKUP_DIRECTORY"
|
||||
mount | grep "$BACKUP_DIRECTORY"
|
|
@ -1,4 +1,10 @@
|
|||
{ config, lib, pkgs, modulesPath, ... }:
|
||||
{
|
||||
config,
|
||||
lib,
|
||||
pkgs,
|
||||
modulesPath,
|
||||
...
|
||||
}:
|
||||
|
||||
with lib;
|
||||
{
|
||||
|
@ -9,11 +15,9 @@ with lib;
|
|||
# Not sure at this point a good way to manage globals in one place
|
||||
# without mono-repo config.
|
||||
|
||||
imports =
|
||||
[
|
||||
(modulesPath + "/installer/scan/not-detected.nix") # Generated by nixos-config-generate
|
||||
./global
|
||||
];
|
||||
imports = [
|
||||
./global
|
||||
];
|
||||
config = {
|
||||
boot.tmp.cleanOnBoot = true;
|
||||
mySystem = {
|
||||
|
@ -24,10 +28,6 @@ with lib;
|
|||
system.packages = [ pkgs.bat ];
|
||||
domain = "hsn.dev";
|
||||
shell.fish.enable = true;
|
||||
|
||||
# But wont enable plugins globally, leave them for workstations
|
||||
# TODO: Make per device option
|
||||
system.resticBackup.remote.location = "s3:https://x.r2.cloudflarestorage.com/nixos-restic";
|
||||
};
|
||||
|
||||
environment.systemPackages = with pkgs; [
|
||||
|
|
|
@ -13,15 +13,6 @@
|
|||
mySystem = {
|
||||
services.openssh.enable = true;
|
||||
security.wheelNeedsSudoPassword = false;
|
||||
|
||||
# Restic backups disabled.
|
||||
# TODO: configure storagebox for hetzner backups
|
||||
system.resticBackup = {
|
||||
local.enable = false;
|
||||
local.noWarning = true;
|
||||
remote.enable = false;
|
||||
remote.noWarning = true;
|
||||
};
|
||||
};
|
||||
|
||||
networking.useDHCP = lib.mkDefault true;
|
||||
|
|
|
@ -13,15 +13,6 @@
|
|||
mySystem = {
|
||||
services.openssh.enable = true;
|
||||
security.wheelNeedsSudoPassword = false;
|
||||
|
||||
# Restic backups disabled.
|
||||
# TODO: configure storagebox for hetzner backups
|
||||
system.resticBackup = {
|
||||
local.enable = false;
|
||||
local.noWarning = true;
|
||||
remote.enable = false;
|
||||
remote.noWarning = true;
|
||||
};
|
||||
};
|
||||
|
||||
networking.useDHCP = lib.mkDefault true;
|
||||
|
|
Loading…
Reference in a new issue