Web app support for restoring restic snapshots.
This commit is contained in:
@@ -26,7 +26,7 @@ WORKDIR /var/html-widgets
|
||||
RUN chicken-install
|
||||
WORKDIR /var/
|
||||
|
||||
RUN chicken-install srfi-1 srfi-13 srfi-18 srfi-158 srfi-194 \
|
||||
RUN chicken-install srfi-1 srfi-13 srfi-18 srfi-19 srfi-158 srfi-194 \
|
||||
sxml-transforms schematra \
|
||||
uri-common http-client medea intarweb \
|
||||
sql-null openssl postgresql crypto-tools
|
||||
|
||||
142
src/nassella.scm
142
src/nassella.scm
@@ -1,5 +1,5 @@
|
||||
;; (load "db")
|
||||
;; (load "mocks")
|
||||
;; (load "src/db.scm")
|
||||
;; (load "src/mocks.scm")
|
||||
(include "db")
|
||||
(include "mocks")
|
||||
|
||||
@@ -12,10 +12,12 @@
|
||||
(chicken irregex)
|
||||
(chicken file)
|
||||
(chicken condition)
|
||||
(chicken sort)
|
||||
|
||||
(rename srfi-1 (delete srfi1:delete))
|
||||
srfi-13
|
||||
srfi-18
|
||||
srfi-19
|
||||
srfi-158
|
||||
srfi-194
|
||||
|
||||
@@ -842,6 +844,64 @@ chmod -R 777 /opt/keys")))
|
||||
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789")
|
||||
40)))
|
||||
|
||||
;; example return value
|
||||
;; (((time . "2026-04-22T22:24:41.701047574Z") (tree . "42c8556ee6ff87eb2b69a7bc23350026182bc015d7f32ec646d9540f43461754") (paths "/nassella") (hostname . "f042d0fae493") (username . "root") (program_version . "restic 0.18.0") (summary (backup_start . "2026-04-22T22:24:41.701047574Z") (backup_end . "2026-04-22T22:24:51.006181344Z") (files_new . 4069) (files_changed . 0) (files_unmodified . 0) (dirs_new . 108) (dirs_changed . 0) (dirs_unmodified . 0) (data_blobs . 1346) (tree_blobs . 67) (data_added . 69399644) (data_added_packed . 11545191) (total_files_processed . 4069) (total_bytes_processed . 146529878)) (id . "77e70711caca6774dabc255dd63dfcaa788c1bd2c1536fda133442e2b5164473") (short_id . "77e70711")) ((time . "2026-04-23T10:00:05.876773568Z") (tree . "5a1650880a1e688576f941b37892617fcb43022b103a7394d994833b6b05ae75") (paths "/nassella") (hostname . "c4e48b3a29e9") (username . "root") (program_version . "restic 0.18.0") (summary (backup_start . "2026-04-23T10:00:05.876773568Z") (backup_end . "2026-04-23T10:00:15.208834993Z") (files_new . 4069) (files_changed . 0) (files_unmodified . 0) (dirs_new . 108) (dirs_changed . 0) (dirs_unmodified . 0) (data_blobs . 21) (tree_blobs . 46) (data_added . 2903468) (data_added_packed . 470424) (total_files_processed . 4069) (total_bytes_processed . 146503956)) (id . "312e57caf39295ef7be69569631232f2b1b445322636091d63c2082b48b09079") (short_id . "312e57ca")) ((time . "2026-04-24T01:41:40.890895516Z") (tree . "4367a5665a1c6ebb9ea5ddbe40485d58485d3610a7a07a9e22ee0bfea0f044cc") (paths "/nassella") (hostname . "7d01b0ae1b2e") (username . "root") (tags "daily_automatic") (program_version . "restic 0.18.0") (summary (backup_start . "2026-04-24T01:41:40.890895516Z") (backup_end . "2026-04-24T01:41:46.743356138Z") (files_new . 4074) (files_changed . 0) (files_unmodified . 0) (dirs_new . 111) (dirs_changed . 0) (dirs_unmodified . 0) (data_blobs . 29) (tree_blobs . 51) (data_added . 3009785) (data_added_packed . 481008) (total_files_processed . 4074) (total_bytes_processed . 146593600)) (id . "5e7c5f51c46aee69b85e30fade3d6a3b83d07bbe6fa112c75b759966d8848376") (short_id . "5e7c5f51")) ((time . "2026-04-24T10:00:00.956302962Z") (tree . "6edaa49535f5ed860f19c8790d6e4db23b8f79c1b8eb40ac59e73ff524522305") (paths "/nassella") (hostname . "11d8deac4263") (username . "root") (tags "daily_automatic") (program_version . "restic 0.18.0") (summary (backup_start . "2026-04-24T10:00:00.956302962Z") (backup_end . "2026-04-24T10:00:07.649733529Z") (files_new . 4074) (files_changed . 0) (files_unmodified . 0) (dirs_new . 112) (dirs_changed . 0) (dirs_unmodified . 0) (data_blobs . 22) (tree_blobs . 68) (data_added . 4097676) (data_added_packed . 601696) (total_files_processed . 4074) (total_bytes_processed . 146594976)) (id . "50f613854ed4d521b596af4435ba4e3f17587124d0a0de248b19f6052117a689") (short_id . "50f61385")) ((time . "2026-04-25T10:00:01.061830384Z") (tree . "ac797c256a6ee09f51ed504c4234e1ced6cef8e1845c67424618658ea6d55abe") (paths "/nassella") (hostname . "f03ffbd8dfe9") (username . "root") (tags "daily_automatic") (program_version . "restic 0.18.0") (summary (backup_start . "2026-04-25T10:00:01.061830384Z") (backup_end . "2026-04-25T10:00:07.594710184Z") (files_new . 4074) (files_changed . 0) (files_unmodified . 0) (dirs_new . 112) (dirs_changed . 0) (dirs_unmodified . 0) (data_blobs . 21) (tree_blobs . 49) (data_added . 2899995) (data_added_packed . 474478) (total_files_processed . 4074) (total_bytes_processed . 146594977)) (id . "7cb5b3badbcebf8222efe863d61371d8d5017f9df8eeecef42a05125fa33555f") (short_id . "7cb5b3ba")) ((time . "2026-04-26T10:00:01.071960675Z") (tree . "63f60b2b42909fe374a4db97c6dae53b382e69480f17ca1e6b26f218d859d328") (paths "/nassella") (hostname . "6a7def9f0992") (username . "root") (tags "daily_automatic") (program_version . "restic 0.18.0") (summary (backup_start . "2026-04-26T10:00:01.071960675Z") (backup_end . "2026-04-26T10:00:07.762125559Z") (files_new . 4074) (files_changed . 0) (files_unmodified . 0) (dirs_new . 112) (dirs_changed . 0) (dirs_unmodified . 0) (data_blobs . 22) (tree_blobs . 49) (data_added . 2900108) (data_added_packed . 474994) (total_files_processed . 4074) (total_bytes_processed . 146594979)) (id . "f6436aee79cfc8b70d49e81479a46ddce0614e81804607fc1f35a15079e7fe82") (short_id . "f6436aee")))
|
||||
|
||||
(define (restic-snapshots user-id instance-id)
|
||||
(let* ((password-path (conc "restic-password-" user-id "-" instance-id))
|
||||
(res
|
||||
(with-db/transaction
|
||||
(lambda (db)
|
||||
`((restic-password . ,(get-instance-restic-password db user-id instance-id))
|
||||
(service-config . ,(get-user-service-config db user-id instance-id))))))
|
||||
(restic-password (alist-ref 'restic-password res))
|
||||
(service-config (alist-ref 'service-config res)))
|
||||
(dynamic-wind
|
||||
(lambda ()
|
||||
(with-output-to-file password-path (lambda () (display restic-password))))
|
||||
(lambda ()
|
||||
(receive (in-port out-port pid err-port)
|
||||
(cond-expand
|
||||
(dev
|
||||
(process* "docker" `("run" "--rm" "--volume"
|
||||
,(conc (current-directory) "/" password-path ":/restic-password")
|
||||
"-e" ,(conc "AWS_ACCESS_KEY_ID="
|
||||
(alist-ref 'backblaze-key-id service-config))
|
||||
"-e" ,(conc "AWS_SECRET_ACCESS_KEY="
|
||||
(alist-ref 'backblaze-application-key service-config))
|
||||
"-i" "restic/restic:0.18.0" "snapshots"
|
||||
"--repo" ,(conc "s3:" (alist-ref 'backblaze-bucket-url service-config))
|
||||
"--password-file" "/restic-password"
|
||||
"--json")))
|
||||
(else
|
||||
(process* "restic"
|
||||
`("snapshots"
|
||||
"--repo" ,(conc "s3:" (alist-ref 'backblaze-bucket-url service-config))
|
||||
"--password-file" password-path
|
||||
"--json")
|
||||
`(("AWS_ACCESS_KEY_ID" . ,(alist-ref 'backblaze-key-id service-config))
|
||||
("AWS_SECRET_ACCESS_KEY" . ,(alist-ref 'backblaze-application-key service-config))))))
|
||||
(let ((thread
|
||||
(thread-start!
|
||||
(lambda ()
|
||||
(let loop ()
|
||||
(thread-sleep! 1)
|
||||
;; We do a non-blocking wait here so that we don't
|
||||
;; block the entire web process.
|
||||
(receive (wait-pid exit-normal status) (process-wait pid #t)
|
||||
(if (= wait-pid 0) ;; wait-pid is 0 until the process has finished
|
||||
(loop)
|
||||
(if exit-normal
|
||||
(let ((res (with-input-from-port in-port read-json)))
|
||||
;; left here for debugging and to clear ports
|
||||
(with-input-from-port err-port read-string)
|
||||
res)
|
||||
(begin (log-to (debug-log) "restic-snapshots: docker command error")
|
||||
(error "restic-snapshots docker command had abnormal exit"))))))))))
|
||||
(thread-join! thread))))
|
||||
(lambda ()
|
||||
(delete-file password-path)))))
|
||||
|
||||
(with-schematra-app app
|
||||
(lambda ()
|
||||
|
||||
@@ -1292,8 +1352,6 @@ chmod -R 777 /opt/keys")))
|
||||
(VStack
|
||||
(Form-Nav (@ (back-to ,(conc "/config/wizard/machine2/" instance-id)) (submit-button "Launch")))))))))
|
||||
|
||||
;; TODO run restic-init if needed (like the first run or if the backblaze
|
||||
;; config changes
|
||||
;; TODO should this perform a backup and then run the systemctl stop app command first?
|
||||
(post "/config/wizard/review-submit/:id"
|
||||
(let* ((instance-id (alist-ref "id" (current-params) equal?))
|
||||
@@ -1747,14 +1805,18 @@ chmod -R 777 /opt/keys")))
|
||||
,output)))
|
||||
)))))
|
||||
|
||||
(define (roundx n)
|
||||
(/ (round (* 10 n)) 10.0))
|
||||
|
||||
;; The restic date doesn't parse well all the time with srfi-19
|
||||
;; so we remove the nanoseconds and set it to UTC (which it is)
|
||||
(define (normalize-restic-date s) (string-append (string-take s (string-index s #\.)) "Z+00:00"))
|
||||
(define (restic-date-string->date s) (string->date (normalize-restic-date s) "~Y-~m-~dT~H:~M:~S~z"))
|
||||
|
||||
(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))))))
|
||||
)
|
||||
(bytes-in-mib 1048576.0))
|
||||
`(App
|
||||
(Main-Container
|
||||
(VStack
|
||||
@@ -1762,11 +1824,65 @@ chmod -R 777 /opt/keys")))
|
||||
(a (@ (href "/")) "Create Snapshot") ;; TODO
|
||||
(table
|
||||
(thead
|
||||
(tr (th "Time") (th "Size") (th "Tag") (th "*")))
|
||||
(tr (th "Time") (th "Data Added (MiB)") (th "Total Size (MiB)") (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"))))))))))
|
||||
,@(map (lambda (snapshot)
|
||||
`(tr
|
||||
(td ,(alist-ref 'time snapshot))
|
||||
(td ,(roundx
|
||||
(/ (or (alist-ref 'total_bytes_processed (alist-ref 'summary snapshot)) 0) bytes-in-mib)))
|
||||
(td ,(or (alist-ref 'tags snapshot) ""))
|
||||
(td (a (@ (href ,(conc "/backups/" instance-id "/"
|
||||
(alist-ref 'short_id snapshot))))
|
||||
"Restore"))))
|
||||
(sort
|
||||
(restic-snapshots (session-user-id) instance-id)
|
||||
(lambda (a b) (date>? (restic-date-string->date (alist-ref 'time a))
|
||||
(restic-date-string->date (alist-ref 'time b)))))))))))))
|
||||
|
||||
(get/widgets
|
||||
("/backups/:instance_id/:restic_id")
|
||||
(let* ((instance-id (alist-ref "instance_id" (current-params) equal?))
|
||||
(restic-id (alist-ref "restic_id" (current-params) equal?))
|
||||
(snapshot-info (find (lambda (snapshot)
|
||||
(string=? (alist-ref 'short_id snapshot) restic-id))
|
||||
(restic-snapshots (session-user-id) instance-id)))
|
||||
(results
|
||||
(with-db/transaction
|
||||
(lambda (db)
|
||||
`((selected-apps . ,(map
|
||||
car
|
||||
(filter cdr
|
||||
(get-user-selected-apps db (session-user-id) instance-id))))
|
||||
(app-config . ,(get-user-app-config db (session-user-id) instance-id))
|
||||
(service-config . ,(get-user-service-config db (session-user-id) instance-id))))))
|
||||
(selected-apps (cons 'log-viewer (alist-ref 'selected-apps results)))
|
||||
(app-config (alist-ref 'app-config results))
|
||||
(config (alist-ref 'config app-config))
|
||||
(root-domain (alist-ref 'root-domain app-config))
|
||||
(service-config (alist-ref 'service-config results)))
|
||||
`(App
|
||||
(Main-Container
|
||||
(VStack
|
||||
(h1 "Instance Snapshot Restore")
|
||||
(ul (li "Snapshot date: " ,(alist-ref 'time snapshot-info))
|
||||
(li "Snapshot tags: " ,(alist-ref 'tags snapshot-info)))
|
||||
(h2 "Root Domain")
|
||||
,root-domain
|
||||
(h2 "Apps")
|
||||
(ul ,@(map (lambda (app) `(li ,app " @ "
|
||||
,(alist-ref 'subdomain (alist-ref app config))
|
||||
"."
|
||||
,root-domain))
|
||||
selected-apps))
|
||||
(h2 "Machine")
|
||||
(ul (li "Region: " ,(alist-ref 'digitalocean-region service-config))
|
||||
(li "Size: " ,(alist-ref 'digitalocean-size service-config)))
|
||||
(form
|
||||
(@ (action ,(conc "/config/wizard/review-submit/" instance-id)) (method POST))
|
||||
(Field (@ (name "restic-snapshot-id") (type "hidden") (value ,restic-id)))
|
||||
(VStack
|
||||
(Form-Nav (@ (back-to ,(conc "/backups/" instance-id)) (submit-button "Restore"))))))))))
|
||||
|
||||
(schematra-install)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user