diff --git a/Makefile b/Makefile index 1868fad..ea8d373 100644 --- a/Makefile +++ b/Makefile @@ -110,6 +110,7 @@ all-apps/nassella/lldap-config/lldap_config.toml \ all-apps/ghost/.compose-env \ restic-env \ restic-password \ +restic-restore.sh \ all-apps/.env \ $(config_dir)ssh-keys cat cl.yaml | docker run --rm --volume $(config_dir)/ssh-keys:/pwd/ssh-keys --volume ${PWD}:/pwd --workdir /pwd -i quay.io/coreos/butane:latest -d /pwd > ignition.json @@ -146,7 +147,7 @@ restic-snapshots: $(apps_config) restic-password .PHONY: archive archive: tar -cf nassella-latest.tar all-apps cl.yaml init-restic.sh main.tf make-caddyfile.sh Makefile \ - make-generated.sh make-nextcloud-env.sh make-ghost-env.sh make-restic-generated.sh make-restic-password.sh restic-snapshots.sh copy-apps.sh \ + make-generated.sh make-nextcloud-env.sh make-ghost-env.sh make-restic-generated.sh make-restic-password.sh restic-snapshots.sh copy-apps.sh restic-restore.sh \ make-nassella-authelia-config.sh make-nassella-lldap-config.sh .terraform.lock.hcl cp nassella-latest.tar src/ diff --git a/all-apps/app.service b/all-apps/app.service index bbba8b2..fd4a850 100644 --- a/all-apps/app.service +++ b/all-apps/app.service @@ -1,7 +1,7 @@ [Unit] Description=Main App -After=docker.service -Requires=docker.service +After=restic-restore.service +Requires=docker.service restic-restore.service [Service] TimeoutStartSec=0 ExecStart=/bin/bash -c '/usr/bin/docker compose -f /app/docker-compose.yaml $(find /app -mindepth 2 -maxdepth 2 -type f -name docker-compose.yaml -exec echo -f {} \;) up' diff --git a/cl.yaml b/cl.yaml index 1be9d70..c228d8c 100644 --- a/cl.yaml +++ b/cl.yaml @@ -19,6 +19,21 @@ systemd: [Install] RequiredBy=local-fs.target + - name: restic-restore.service + enabled: true + contents: | + [Unit] + Description=Run once on first boot, if needed, to restore a backup + After=docker.service + Requires=docker.service + + [Service] + Type=oneshot + EnvironmentFile=/restic-env + ExecStart=/restic-restore.sh + + [Install] + WantedBy=multi-user.target - name: app.service enabled: true contents_local: app/app.service @@ -31,7 +46,7 @@ systemd: [Service] Type=oneshot EnvironmentFile=/restic-env - ExecStart=/usr/bin/bash -c "docker run --rm --volume /nassella:/nassella --volume /restic-password:/restic-password -e AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID} -e AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY} -i restic/restic:0.18.0 backup --verbose --repo s3:${BACKBLAZE_BUCKET_URL} --password-file /restic-password /nassella" + ExecStart=/usr/bin/bash -c "docker run --rm --volume /nassella:/nassella --volume /restic-password:/restic-password -e AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID} -e AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY} -i restic/restic:0.18.0 backup --verbose --repo s3:${BACKBLAZE_BUCKET_URL} --password-file /restic-password --tag daily_automatic /nassella" ExecStopPost=systemctl start app.service - name: restic-backup.timer @@ -91,8 +106,20 @@ storage: contents: local: restic-password - path: /restic-env + overwrite: true contents: local: restic-env + - path: /restic-restore.sh + mode: 0755 + contents: + local: restic-restore.sh + - path: /etc/ssh/sshd_config.d/custom.conf + overwrite: true + mode: 0600 + contents: + inline: | + PermitRootLogin no + AllowUsers core ### docker-compose sysext ### https://flatcar.github.io/sysext-bakery/docker_compose/ - path: /opt/extensions/docker-compose/docker-compose-2.34.0-x86-64.raw diff --git a/make-restic-generated.sh b/make-restic-generated.sh index 9319d00..2de8212 100755 --- a/make-restic-generated.sh +++ b/make-restic-generated.sh @@ -7,3 +7,7 @@ set -e echo "AWS_ACCESS_KEY_ID=\"$BACKBLAZE_KEY_ID\"" echo "AWS_SECRET_ACCESS_KEY=\"$BACKBLAZE_APPLICATION_KEY\"" echo "BACKBLAZE_BUCKET_URL=\"$BACKBLAZE_BUCKET_URL\"" + +if [ -n "$RESTIC_SNAPSHOT_ID" ]; then + echo "RESTIC_SNAPSHOT_ID=\"$RESTIC_SNAPSHOT_ID\"" +fi diff --git a/restic-restore.sh b/restic-restore.sh new file mode 100644 index 0000000..227bce9 --- /dev/null +++ b/restic-restore.sh @@ -0,0 +1,8 @@ +#!/bin/bash +MARKER=/var/lib/firstboot.done + +if [ ! -f "$MARKER" ] && [ -n "$RESTIC_SNAPSHOT_ID" ]; then + docker run --rm --volume /nassella:/nassella --volume /restic-password:/restic-password -e AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID} -e AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY} -i restic/restic:0.18.0 restore "$RESTIC_SNAPSHOT_ID" --verbose --repo s3:${BACKBLAZE_BUCKET_URL} --password-file /restic-password --target / +fi + +touch "$MARKER" diff --git a/src/nassella.scm b/src/nassella.scm index 0d0b546..2260565 100644 --- a/src/nassella.scm +++ b/src/nassella.scm @@ -1304,6 +1304,7 @@ chmod -R 777 /opt/keys"))) (get-most-recent-deployment-status db (session-user-id) instance-id))))))) (when (not (or (eq? status 'queued) (eq? status 'in-progress))) (let* ((instance-id (alist-ref "id" (current-params) equal?)) + (restic-snapshot-id (alist-ref 'restic-snapshot-id (current-params))) (results (with-db/transaction (lambda (db) @@ -1372,7 +1373,8 @@ chmod -R 777 /opt/keys"))) ("BACKBLAZE_KEY_ID" . ,(alist-ref 'backblaze-key-id service-config)) ("BACKBLAZE_APPLICATION_KEY" . ,(alist-ref 'backblaze-application-key service-config)) ("BACKBLAZE_BUCKET_URL" . ,(alist-ref 'backblaze-bucket-url service-config)) - ("RESTIC_PASSWORD" . ,restic-password))))) + ("RESTIC_PASSWORD" . ,restic-password) + ,@(if (and restic-snapshot-id (not (string=? restic-snapshot-id ""))) `(("RESTIC_SNAPSHOT_ID" . ,restic-snapshot-id)) '()))))) (with-output-to-file (string-append dir "/config/production.tfvars") (lambda () (map (lambda (e) @@ -1526,7 +1528,8 @@ chmod -R 777 /opt/keys"))) ,(alist-ref 'instance-id instance))) "Modify Setup")) (li "Upgrade Now (pending automatic upgrades scheduled for: )") - (li "Manage Backups") + (li (a (@ (href "/backups/" ,(alist-ref 'instance-id instance))) + "Manage Backups")) (li (a (@ (href "/destroy/" ,(alist-ref 'instance-id instance))) "Destroy - deletes data and configuration (confirmation required)")) (li (a (@ (href "/reset/" ,(alist-ref 'instance-id instance))) @@ -1744,6 +1747,27 @@ chmod -R 777 /opt/keys"))) ,output))) ))))) +(get/widgets + ("/backups/:id") + (let* ((instance-id (alist-ref "id" (current-params) equal?)) + ;; (res (with-db/transaction + ;; (lambda (db) + ;; `((status . ,(get-most-recent-deployment-status db (session-user-id) instance-id)) + ;; (progress . ,(get-most-recent-deployment-progress db (session-user-id) instance-id)))))) +) + `(App + (Main-Container + (VStack + (h1 "Backups") + (a (@ (href "/")) "Create Snapshot") ;; TODO + (table + (thead + (tr (th "Time") (th "Size") (th "Tag") (th "*"))) + (tbody + (tr (td "2026-04-22 22:24:41") (td "139.742 MiB") (td "") (td (a (@ (href "/")) "Restore"))) + (tr (td "2026-04-21 12:01:03") (td "139.742 MiB") (td "before upgrade") (td (a (@ (href "/")) "Restore"))) + (tr (td "2026-04-21 22:24:41") (td "129.742 MiB") (td "") (td (a (@ (href "/")) "Restore")))))))))) + (schematra-install) ))