9bd6e9b6f62b3628d68648350c657c021d56d9b6
[distro-setup] / btrbk-run
1 #!/bin/bash
2
3 # Configure & run btrbk & related work on Ian's computers.
4 # Copyright (C) 2024 Ian Kelling
5
6 # This program is free software: you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation, either version 3 of the License, or
9 # (at your option) any later version.
10
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU General Public License for more details.
15
16 # You should have received a copy of the GNU General Public License
17 # along with this program. If not, see <http://www.gnu.org/licenses/>.
18
19 # SPDX-License-Identifier: GPL-3.0-or-later
20
21
22 # todo: if we cancel in the middle of a btrfs send, then run again
23 # immediately, the received subvolume doesn't get a Received UUID:
24 # field, and we won't mount it. Need to figure out a solution that will
25 # fix this.
26
27
28 [[ $EUID == 0 ]] || exec sudo -E "${BASH_SOURCE[0]}" "$@"
29
30 set -e; . /usr/local/lib/bash-bear; set +e
31 shopt -s nullglob
32
33 usage() {
34 cat <<'EOF'
35 btrbk-run [OPTIONS] [run|resume|archive]
36 usually -t TARGET_HOST or -s SOURCE_HOST
37
38 Note, at source location, intentionally not executable, run and read
39 install-my-scripts.
40
41 EOF
42 echo "top of script file:"
43 sed -n '1,/^[# ]*end command line/{p;b};q' "$0"
44 exit $1
45 }
46
47
48
49 pre=btrbk-run
50
51
52
53 script_name="${BASH_SOURCE[0]}"
54 script_name="${script_name##*/}"
55
56
57 log-setup() {
58 if [[ ! $log_path ]]; then
59 mkdir -p /var/log/btrbk
60 log_path=/var/log/btrbk/$(date +%F_%T%:::z).log
61 fi
62 }
63 d() {
64 if $dry_run || $conf_only; then
65 printf "$pre dry-run: %s\n" "$*"
66 else
67 log-setup
68 printf "$pre running: %s\n" "$*" |& pee cat 'ts "%F %T" >>'$log_path
69 "$@" |& pee cat 'ts "%F %T" >>'$log_path
70 fi
71 }
72 m() { if $verbose; then printf "$pre %s\n" "$*"; fi; "$@"; }
73 e() { printf "$pre %s\n" "$*"; }
74
75 logq() {
76 local exit_code
77 exit_code=0
78 log-setup
79 printf "$pre running: %s\n" "$*" | pee cat 'ts "%F %T" >>'$log_path
80 e logging to $log_path
81 "$@" |& ts "%F %T" >>$log_path || exit_code=$?
82 printf "$pre exit code:%s of %s\n" "$exit_code" "$*" | pee cat 'ts "%F %T" >>'$log_path
83 if (( exit_code > 0 )); then
84 e "error: command exit code: $exit_code. exiting after tail -n50 $log_path"
85 tail -n50 $log_path
86 exit $exit_code
87 fi
88 }
89
90 die() { printf "$pre error: %s\n" "$*" >&2; echo "$pre exiting with status 1" >&2; exit 1; }
91 mexit() { echo "$pre exiting with status $1"; exit $1; }
92
93 uninstalled-file-die() {
94 die "file $1 is not latest. run install-my-scripts or rerun with -f"
95 }
96
97 set-location() {
98 case $HOSTNAME in
99 kw)
100 at_work=true
101 ;;
102 kd|frodo)
103 at_home=true
104 ;;
105 x2|x3|sy|so)
106 if [[ $(dig +short @10.2.0.1 -x 10.2.0.2 2>&1 ||:) == kd.b8.nz. ]] \
107 && ip n show 10.2.0.1 | grep . &>/dev/null; then
108 # note: logic duplicated in 11-iank
109 at_home=true
110 elif ping -q -c1 -w1 hal.office.fsf.org &>/dev/null \
111 && ip n show 192.168.0.26 | grep . &>/dev/null; then
112 at_work=true
113 fi
114 ;;
115 esac
116 }
117
118 exit-if-no-default-targets() {
119 if ! $force && [[ $HOSTNAME != "$MAIL_HOST" ]]; then
120 echo "MAIL_HOST=$MAIL_HOST, nothing to do"
121 mexit 0
122 fi
123 case $HOSTNAME in
124 kw|kd|frodo|x2|x3|sy|so) : ;;
125 *)
126 die "error: no default targets for this host, use -t"
127 ;;
128 esac
129 }
130
131 add-x3-target() {
132 # main work machine
133 if ping -q -c1 -w1 x3.office.fsf.org &>/dev/null; then
134 targets+=(x3.office.fsf.org)
135 elif ping -q -c1 -w1 x3.b8.nz &>/dev/null; then
136 # in case we took it home
137 targets+=(x3.b8.nz)
138 elif ping -q -c1 -w1 x3w.b8.nz &>/dev/null; then
139 targets+=(x3w.b8.nz)
140 else
141 targets+=(x3wg.b8.nz)
142 fi
143 }
144
145 add-wireless-target() {
146 local host
147 if [[ ! $1 ]]; then
148 set -- $h
149 fi
150 for host; do
151 # c = cabled, w = wireless
152 if ping -q -c1 -w1 ${host}c.b8.nz &>/dev/null; then
153 targets+=(${host}c.b8.nz)
154 elif ping -q -c1 -w1 $host.b8.nz &>/dev/null; then
155 targets+=($host.b8.nz)
156 elif ping -q -c1 -w1 ${host}w.b8.nz &>/dev/null; then
157 targets+=(${host}w.b8.nz)
158 fi
159 done
160 }
161
162 qconf() {
163 case $sub in
164 q)
165 # q has sensitive data i dont want to backup for so long
166 cat >>/etc/btrbk$conf_suf.conf <<EOF
167 snapshot_preserve $q_preserve
168 snapshot_preserve_min 2h
169 snapshot_dir btrbk
170 target_preserve $q_preserve
171 target_preserve_min 2h
172 EOF
173 ;;
174 esac
175
176 }
177
178
179 # latest $MAIL_HOST
180 if [[ -e /b/bash_unpublished/source-state ]]; then
181 source /b/bash_unpublished/source-state
182 fi
183
184 # note q is owned by root:1000
185
186 mountpoints=()
187
188 rsync_mountpoint=/q
189
190 ret=0
191 # default options
192 conf_only=false
193 dry_run=false # mostly for testing
194 rate_limit=no
195 verbose=true; verbose_arg="-l trace"
196 force=false
197 if [[ $INVOCATION_ID ]]; then
198 # INVOCATION_ID means running as a systemd service. we cant show progress in this case,
199 # but if we pass the arg, it will insert mbuffer into the command.
200 progress_arg=
201 else
202 progress_arg="--progress"
203 fi
204 incremental_strict=false
205 pull_reexec=false
206
207 default_args_file=/etc/btrbk-run.conf
208 if [[ -s $default_args_file ]]; then
209 # shellcheck disable=SC2046 # we want word splitting
210 set -- $(< $default_args_file) "$@"
211 # i havent used this feature yet, so warn about it
212 echo "$0: warning: default btrbk-run options set in $default_args_file (sleeping 5 seconds):"
213 cat $default_args_file
214 sleep 5
215 fi
216
217 once_args_file=/etc/btrbk-run-once.conf
218 if mv -f $once_args_file $once_args_file-tmp 2> >(sed '/No such file or directory/d'); then
219 # shellcheck disable=SC2046 # we want word splitting
220 set -- $(< $once_args_file-tmp) "$@"
221 # i havent used this feature yet, so warn about it
222 echo "$0: btrbk-run options set in $once_args_file:"
223 cat $once_args_file-tmp
224 rm -f $once_args_file-tmp
225 fi
226
227
228 targets=()
229 early=false
230 fast=false
231 kd_spread=false
232 check_installed=false
233 orig_args=("$@")
234 temp=$(getopt -l check-installed,fast,pull-reexec,help 23acefikl:m:npqrs:t:vh "$@") || usage 1
235 eval set -- "$temp"
236 while true; do
237 case $1 in
238 # for the rare case we want to run multiple instances at the same time
239 -2) conf_suf=2 ;;
240 -3) conf_suf=3 ;;
241 -a)
242 # all moiuntpoints
243 mountpoints=(/a /o /qr /qd /q)
244 ;;
245 # only creates the config file, does not run btrbk
246 -c) conf_only=true ;;
247 --check-installed)
248 check_installed=true
249 ;;
250 # quit early, just btrbk, no extra remounting etc.
251 -e) early=true ;;
252 # avoids some default behaviors:
253 # - no skipping hosts where xprintidle haven't been idle recently
254 # - exit if we can't ssh to 1 or more hosts
255 # - still set default hosts despite MAIL_HOST status
256 -f) force=true ;;
257 # skip various checks. when we run twice in a row for
258 # switch mail-host, no need to repeat the same checks again.
259 --fast) fast=true ;;
260 -i) incremental_strict=true ;;
261 # note this implies resume and -p because it is just meant to make
262 # other hosts have the same snapshots, not do any expiry or new
263 # backups.
264 -k) kd_spread=true ;;
265 # bytes per second, suffix k m g
266 -l) rate_limit=$2; shift ;;
267 # Comma separated mountpoints to backup. This has defaults set below.
268 -m) IFS=, mountpoints=($2); unset IFS; shift ;;
269 -n) dry_run=true ;;
270 # preserve existing snapshots and backups
271 -p) preserve_arg=-p ;;
272 # internal option for rerunning under newer SOURCE_HOST version.
273 --pull-reexec) pull_reexec=true;;
274 # quiet
275 -q) verbose=false; verbose_arg=; progress_arg= ;;
276 # source host to receive a backup from
277 -s)
278 source=$2
279 bbksource=$source
280 if [[ $source == *:* ]]; then
281 bbksource="[$source]"
282 fi
283 shift
284 ;;
285 # target hosts to send to. empty is valid for just doing local
286 # snapshot. we have default hosts we will populate.
287 -t) IFS=, targets=($2); unset IFS; shift ;;
288 # verbose.
289 -v)
290 verbose=true; verbose_arg="-l trace"
291 ;;
292 -h|--help) usage ;;
293 --) shift; break ;;
294 *) die "Internal error!" ;;
295 esac
296 shift
297 done
298
299 cmd_arg="$1"
300
301
302
303 if ! $force && { $check_installed || [[ ! $source ]]; } ; then
304 install_bin_files=(
305 mount-latest-subvol
306 check-subvol-stale
307 btrbk-run
308 )
309 for f in ${install_bin_files[@]}; do
310 if ! diff -q /a/bin/ds/$f /usr/local/bin/$f; then
311 uninstalled-file-die $f
312 fi
313 done
314 if ! diff -q /a/bin/bash-bear-trap/bash-bear /usr/local/lib/bash-bear; then
315 uninstalled-file-die bash-bear
316 fi
317 if $check_installed; then
318 exit 0
319 fi
320 fi
321
322
323 if $kd_spread; then
324 if [[ $cmd_arg && $cmd_arg != resume ]]; then
325 die "dont pass -k without resume or empty run arg"
326 fi
327 if [[ $HOSTNAME == "$MAIL_HOST" ]]; then
328 die "something went wrong, -k not meant to be run on MAIL_HOST"
329 fi
330 if [[ $HOSTNAME != kd ]]; then
331 die "something went wrong, -k only meant to run on kd"
332 fi
333 cmd_arg=resume
334 preserve_arg=-p
335 add-wireless-target sy so
336 fi
337
338 if [[ ! $cmd_arg ]]; then
339 cmd_arg=run
340 fi
341
342
343 std_preserve="36h 14d 8w 24m"
344 q_preserve="18h 14d 8w"
345
346 case $cmd_arg in
347 run|resume) : ;;
348
349 # This works better than the normal archive command. We have to
350 # specify the mount points, but that is what we are used to doing and
351 # we prefer it. Another difference is that archive works recursively
352 # and we don't care about that. Sometimes we may still want to run
353 # btrbk archive, but it doesn't even use the config file, so just
354 # run it directly, eg:
355 # time s btrbk -v archive /mnt/r7/amy/boot/btrbk ssh://bo/mnt/boot2/btrbk
356 archive)
357 cmd_arg=resume
358 std_preserve="999h 999d 999w 999m"
359 q_preserve="$std_preserve"
360 preserve_arg=-p
361 ;;
362 *) die "untested command arg" ;;
363 esac
364
365 if (( $# > 1 )); then
366 die: "only 1 nonoption arg is supported"
367 fi
368
369 if [[ -v targets && $source ]]; then
370 # note, this doesnt need to be the case, but
371 # we would need to think about it.
372 die "error: -t and -s are mutually exclusive"
373 fi
374
375 ### end options parsing
376
377 # remove path from earlier version of btrbk
378 rm -f /usr/sbin/btrbk
379 # note, this still works as intended if there is no /usr/bin/btrbk
380 if [[ /a/opt/btrbk/btrbk -nt /usr/bin/btrbk ]]; then
381 if [[ -e /b/distro-functions/src/package-manager-abstractions ]]; then
382 . /b/distro-functions/src/package-manager-abstractions
383 pi asciidoctor
384 fi
385 cd /a/opt/btrbk
386 m make install
387 cd /
388 fi
389
390 # TODO: i wonder if there should be an option to send to the default
391 # targets, plus any given on the command line.
392
393
394 at_work=false
395 at_home=false
396
397
398 # set default targets
399 if [[ ! -v targets && ! $source ]]; then
400 exit-if-no-default-targets
401 set-location
402 if $at_home; then
403 if ! $kd_spread && [[ $HOSTNAME != x3 ]]; then
404 add-x3-target
405 fi
406 if [[ $HOSTNAME != kd ]]; then
407 targets+=(kd.b8.nz)
408 fi
409 wireless_home_hosts=(
410 x2
411 sy
412 so
413 )
414 for h in ${wireless_home_hosts[@]}; do
415 if [[ $HOSTNAME != "$h" ]]; then
416 add-wireless-target
417 fi
418 done
419 elif $at_work; then
420 targets+=(b8.nz)
421 for h in x2 x3 kw; do
422 if [[ $HOSTNAME == "$h" ]]; then
423 continue
424 fi
425 if ping -q -c1 -w1 $h.office.fsf.org &>/dev/null; then
426 targets+=($h.office.fsf.org)
427 fi
428 done
429 else
430 targets+=(b8.nz)
431 fi
432 fi
433
434 if [[ ${mountpoints[0]} ]]; then
435 for mp in ${mountpoints[@]}; do
436 if [[ -e /nocow/btrfs-stale/$mp ]]; then
437 die "error: $mp is stale, mount-latest-subvol first"
438 fi
439 done
440 else
441 # set default mountpoints
442 if [[ ${targets[0]} == tp ]]; then
443 prospective_mps=(/a)
444 else
445 case $HOSTNAME in
446 *)
447 prospective_mps=()
448 if [[ $source ]]; then
449 source_state="$(ssh $source 'cat /a/bin/bash_unpublished/source-state; echo source_host=$HOSTNAME')"
450 eval "$source_state"
451 # shellcheck disable=SC2154 # assigned in the above eval.
452 if [[ $source_host == "$MAIL_HOST" ]]; then
453 prospective_mps+=(/o)
454 fi
455 if [[ $source_host == "$HOST2" ]]; then
456 prospective_mps+=(/a /qr /qd /q)
457 fi
458 else
459 if [[ $HOSTNAME == "$MAIL_HOST" ]]; then
460 prospective_mps+=(/o)
461 fi
462 if [[ $HOSTNAME == "$HOST2" ]]; then
463 prospective_mps+=(/a /qr /qd /q)
464 fi
465 if $kd_spread; then
466 prospective_mps=(/a /o /qr /qd /q)
467 fi
468 fi
469 # note: put q last just in case its specific retention options were to
470 # affect other config sections. I havent tested if that is the case.
471 ;;
472 esac
473 fi
474 for mp in ${prospective_mps[@]}; do # default mountpoints to sync
475 if [[ -e /nocow/btrfs-stale/$mp ]]; then
476 e "warning: $mp stale, not adding to default mountpoints"
477 continue
478 fi
479 if awk '{print $2}' /etc/fstab | grep -xF $mp &>/dev/null; then
480 mountpoints+=($mp)
481 fi
482 done
483 fi
484
485 tmp=$(( ${#mountpoints[@]} == 0 ))
486 if (( tmp )); then
487 die didnt get mountpoint arg and had no defaults
488 fi
489
490 ##### end command line parsing ########
491
492 #### begin pre-checks #####
493
494 # todo: this has a timing problem, since btrbk.timer could activate the service after this check.
495 if ! $fast && [[ $source ]]; then
496 if [[ $(ssh $source ps --no-headers -o comm 1) == systemd ]]; then
497 status=$(ssh $source systemctl is-active btrbk.service) || : # normally returns 3
498 case $status in
499 inactive|failed) : ;;
500 *)
501 echo "$0: error: btrbk is running on source. exiting out of caution"
502 mexit 1
503 esac
504 fi
505 fi
506
507 if ! command -v btrbk &>/dev/null; then
508 die "error: no btrbk binary found"
509 fi
510
511 # pull_reexec stops us from getting into an infinite loop if there is some
512 # kind of weird problem
513 pulla=false
514 for m in "${mountpoints[@]}"; do
515 if [[ $m == /a ]]; then
516 pulla=true
517 break
518 fi
519 done
520
521 if ! $pull_reexec && [[ $source ]] && $pulla && ! $force ; then
522 ssh root@$source btrbk-run --check-installed
523 fi
524
525 #### end pre-checks #####
526
527
528
529
530 # print some non-default opts
531 if $verbose; then
532 opts_show=()
533 if ! $conf_only; then
534 opts_show+=(conf_only=true)
535 fi
536 if ! $dry_run; then
537 opts_show+=(dry_run=true)
538 fi
539 if [[ $rate_limit != no ]]; then
540 opts_show+=("rate_limit=$rate_limit")
541 fi
542 if [[ $cmd_arg != run ]]; then
543 opts_show+=(cmd_arg=$cmd_arg)
544 fi
545 if (( ${#opts_show[@]} >= 1 )); then
546 first=true
547 for opt in ${opts_show[@]}; do
548 if $first; then
549 printf "%s" "$opt"
550 first=false
551 else
552 printf " %s" "$opt"
553 fi
554 done
555 echo
556 fi
557 fi
558
559
560 if ! $pull_reexec && [[ $source ]] && $pulla ; then
561 tmpf=$(mktemp)
562 m rsync -ra $source:/usr/local/bin/{mount-latest-subvol,check-subvol-stale} /usr/local/bin
563 m rsync -ra $source:/usr/local/lib/bash-bear /usr/local/lib
564 m scp $source:/a/bin/distro-setup/btrbk-run $tmpf
565 if ! diff -q $tmpf ${BASH_SOURCE[0]}; then
566 e "found different version on host $source. reexecing"
567 install -T $tmpf /usr/local/bin/btrbk-run
568 m /usr/local/bin/btrbk-run --pull-reexec "${orig_args[@]}"
569 mexit 0
570 fi
571 fi
572
573
574 if [[ -v targets ]]; then
575 echo "targets: ${targets[*]}"
576 fi
577 if [[ $source ]]; then
578 echo "source: $source"
579 fi
580 echo "mountpoints: ${mountpoints[*]}"
581
582
583 # todo: check if we have no snapshots yet, because I always want to run
584 # archive instead of run. Likely, I should give an error unless a cli
585 # override is passed. perhaps check-subvol-stale could give the error.
586 # see the error message "no snapshots found" in that file.
587 if ! $fast; then
588 # if our mountpoints are from stale snapshots,
589 # it doesn't make sense to do a backup.
590 m check-subvol-stale ${mountpoints[@]} || die "found stale mountpoints in ${mountpoints[*]}"
591
592 # for an initial run, btrbk requires the dir to exist.
593 mkdir -p /mnt/{root,o}/btrbk
594 fi
595 local_zone=$(date +%z)
596
597 if [[ $source ]]; then
598 if $fast; then
599 zone=$local_zone
600 else
601 if ! zone=$(ssh root@$source date +%z); then
602 if $conf_only; then
603 echo "$0: warning: failed to ssh to root@$source"
604 else
605 die failed to ssh to root@$source
606 fi
607 fi
608 if [[ $zone != "$local_zone" ]]; then
609 die "error: dont confuse yourself with multiple time zones. $h has different timezone than localhost"
610 fi
611 fi
612 else
613
614 sshable=()
615 sshfail=()
616 remote_str_cmd="mkdir -p /mnt/root/btrbk /mnt/o/btrbk && \
617 date +%z && \
618 df --output=size,pcent / | tail -n1"
619
620 for h in ${targets[@]}; do
621 if $fast || $conf_only; then
622 # Use some typical values in this case
623 root_size=$(( 1024 * 1024 * 2000 )) #2tb
624 percent_used=10
625 zone=$(date +%z)
626 elif remote_str=$(timeout -s 9 6 ssh root@$h "$remote_str_cmd"); then
627 mapfile -t tmp_array <<<"$remote_str"
628 zone="${tmp_array[0]}"
629 IFS=" " read -r root_size percent_used <<<"${tmp_array[1]}"
630 percent_used=${percent_used%%%}
631
632 tmp=$(( ${#tmp_array[@]} != 2 ))
633 if (( tmp )); then
634 die "error: didnt get 2 lines in test ssh to target $h. investigate"
635 fi
636 case $percent_used in
637 [0-9]|[1-9][0-9]) : ;;
638 *)
639 die "error: didnt get percent disk use in test ssh to target $h. investigate"
640 ;;
641 esac
642 else
643 sshfail+=($h)
644 continue
645 fi
646
647 # we may be booted into a bootstrap fs or something
648 min_root_kb=$(( 1024 * 1024 * 200 )) # 200 gb
649 tmp=$(( root_size < min_root_kb ))
650 if (( tmp )); then
651 e "warning: $h: root_size=$root_size < 200gb, perhaps it is booted to bootstrap vol. skipping for now"
652 continue
653 fi
654
655 tmp=$(( percent_used >= 98 ))
656 if (( tmp )); then
657 die "error: filesystem on target $h is $percent_used % full"
658 fi
659
660 # on sy, xprintidle is resetting every 12 seconds even when not
661 # idle, i dunno why, instead we are checking if the screen is locked,
662 # which is good enough.
663 #
664 # This is a separate ssh because the command can fail and thatis ok.
665 if ! $force; then
666 locked=false
667 if lock_info=$(timeout -s 9 6 ssh $h DISPLAY=:0 xscreensaver-command -time); then
668 if [[ $lock_info != *non-blanked* ]]; then
669 locked=true
670 fi
671 else
672 locked=true
673 fi
674 if ! $locked; then
675 # Ignore this host. i sometimes use a non-main machine for
676 # testing or web browsing, knowing that everything will be wiped
677 # by the next backup, but I dont want it to happen as Im using
678 # it from cronjob.
679 e "warning: $h: seems to be actively in use, skipping for now"
680 continue
681 fi
682 fi
683 sshable+=($h)
684 if [[ $zone != "$local_zone" ]]; then
685 die "error: dont confuse yourself with multiple time zones. $h has different timezone than localhost"
686 fi
687 done
688 if [[ ! ${sshable[*]} ]] || { $force && [[ ${sshfail[*]} ]]; }; then
689 die "see skipped host warning above or sshfail hosts: ${sshfail[*]}"
690 else
691 if [[ ${sshfail[*]} ]]; then
692 ret=1
693 e "error: failed to ssh to ${sshfail[*]} but continuing with other hosts"
694 fi
695 targets=(${sshable[@]})
696 fi
697 fi
698
699
700 cat >/etc/btrbk$conf_suf.conf <<EOF
701 ssh_identity /q/root/h
702 #ssh_identity /root/.ssh/home
703
704 # trying this out
705 #stream_compress zstd
706
707 # so we only run one at a time
708 lockfile /var/lock/btrbk$conf_suf.lock
709
710 # default format of short does not accomidate hourly preservation setting
711 timestamp_format long-iso
712
713 # only make a snapshot if things have changed
714 snapshot_create onchange
715 # I could make this different from target_preserve,
716 # if one disk had less space.
717 # for now, keeping them equal.
718 snapshot_preserve $std_preserve
719 snapshot_preserve_min 6h
720 snapshot_dir btrbk
721 # so, total backups = ~58
722 target_preserve $std_preserve
723 target_preserve_min 6h
724
725 # it seems very likely that not doing this could result in clone source not found
726 # errors, for example when expiry happens differently on different hosts,
727 # also, as btrbk does by default, if a failed send happens, on the next run it
728 # will warn about a stray subvolume, but then create a backup of a newer subvol
729 # and use an older subvol as the parent.
730 incremental_prefs sao:1
731
732 # if something fails and it's not obvious, try doing
733 # btrbk -l trace -v dryrun
734
735 rate_limit $rate_limit
736 EOF
737
738 if $incremental_strict; then
739 cat >>/etc/btrbk$conf_suf.conf <<EOF
740 incremental strict
741 EOF
742 fi
743
744
745 # make /q be last
746 mp_count=${#mountpoints[@]}
747 for (( i=0; i < mp_count - 1 ; i++ )); do
748 if [[ ${mountpoints[i]} == /q ]]; then
749 unset "mountpoints[i]"
750 mountpoints+=(/q)
751 fi
752 done
753
754
755
756 snap_list_cmds=()
757 tg_snaps=()
758 declare -A source_snaps
759
760 for m in ${mountpoints[@]}; do
761 case $m in
762 /o)
763 vol=/mnt/o
764 ;;
765 *)
766 vol=/mnt/root
767 ;;
768 esac
769
770 sub=${m#/}
771 snap_list_cmds+=("echo $vol/btrbk/$sub.*;")
772
773 if [[ $source ]]; then
774 tmp_a=($vol/btrbk/$sub.*)
775 tg_snaps+=("${tmp_a[*]}")
776 cat >>/etc/btrbk$conf_suf.conf <<EOF
777 volume ssh://$bbksource$vol
778 subvolume $sub
779 EOF
780 qconf
781 cat >>/etc/btrbk$conf_suf.conf <<EOF
782 target send-receive $vol/btrbk
783 EOF
784 else # we have targets
785 for snap in "$vol/btrbk/$sub."*; do
786 source_snaps[$snap]=t
787 done
788
789 cat >>/etc/btrbk$conf_suf.conf <<EOF
790 volume $vol
791 subvolume $sub
792 EOF
793 qconf
794 for tg in ${targets[@]}; do
795 # handle ipv6
796 if [[ $tg == *:* ]]; then
797 tg="[$tg]"
798 fi
799 cat >>/etc/btrbk$conf_suf.conf <<EOF
800 target send-receive ssh://$tg$vol/btrbk
801 EOF
802 done
803 fi
804 done
805
806 # Delete any subvols on the receiving host that don't exist on the
807 # sending host. Otherwise, the receiving host could have snapshots that
808 # aren't on the sending side, and thus become odd leaf subvols, and then
809 # btrbk could try to use them when we sync back, creating a weird tree
810 # instead of linear parent/child relationship. Maybe this could lead to
811 # a missing source subvol error, so lets avoid it.
812
813 get-orphan-tg-snaps() {
814 orphan_tg_snaps=()
815 for (( i=0; i < ${#mountpoints[@]}; i++ )); do
816 orphan_start_count=${#orphan_tg_snaps[@]}
817 tg_snap_count=0
818 for tg_snap in ${tg_snaps[$i]}; do
819 tg_snap_count=$(( tg_snap_count + 1 ))
820 if [[ ! ${source_snaps[$tg_snap]} ]]; then
821 orphan_tg_snaps+=("$tg_snap")
822 fi
823 done
824 orphan_mp_count=$(( ${#orphan_tg_snaps[@]} - orphan_start_count ))
825 # sanity checking
826 tmp=$(( tg_snap_count > 1 && tg_snap_count == orphan_mp_count ))
827 if (( tmp )) ; then
828 die "something went wrong checking orphans on $tg: for mountpoint ${mountpoints[$i]}, $orphan_mp_count"
829 fi
830 done
831 }
832
833 if [[ $source ]]; then
834 for snap in $(ssh root@$source "shopt -s nullglob; ${snap_list_cmds[*]}"); do
835 source_snaps[$snap]=t
836 done
837 get-orphan-tg-snaps
838 tmp=$(( ${#orphan_tg_snaps[*]} >= 1 ))
839 if (( tmp )); then
840 d btrfs sub del ${orphan_tg_snaps[*]}
841 fi
842 else # we have targets
843 for tg in ${targets[@]}; do
844 tmp_str=$(ssh root@$tg "shopt -s nullglob; ${snap_list_cmds[*]}")
845 mapfile -t tg_snaps <<<"$tmp_str"
846 get-orphan-tg-snaps
847 tmp=$(( ${#orphan_tg_snaps[*]} >= 1 ))
848 if (( tmp )); then
849 d ssh root@$tg "btrfs sub del ${orphan_tg_snaps[*]}"
850 fi
851 done
852 fi
853
854 # todo: umount first to ensure we don't have any errors
855 # todo: do some kill fuser stuff to make umount more reliable
856
857
858 if $conf_only; then
859 mexit 0
860 fi
861
862
863
864 if $dry_run; then
865 m btrbk -c /etc/btrbk$conf_suf.conf -v -n $cmd_arg
866 mexit 0
867 fi
868 # -q and just using the syslog option seemed nice,
869 # but it doesn't show when a send has a parent and when it doesn't.
870 logq btrbk -c /etc/btrbk$conf_suf.conf $preserve_arg $verbose_arg $progress_arg $cmd_arg
871
872 if $early; then
873 exit 0
874 fi
875
876 # todo: tp not valid anymore.
877 # if we have it, sync to systems which don't
878 if mountpoint $rsync_mountpoint >/dev/null; then
879 for tg in ${targets[@]}; do
880 case $tg in
881 tp)
882 dirs=(/p/c/machine_specific/tp)
883 for x in /p/c/machine_specific/*.hosts; do
884 if grep -qxF $tg $x; then
885 dirs+=(${x%.hosts})
886 fi
887 done
888 m rsync -aSAXPH --specials --devices --delete --relative ${dirs[@]} root@$tg:/
889 ;;
890 esac
891 done
892 fi
893
894 subvols=()
895 for mp in "${mountpoints[@]}"; do
896 subvols+=("${mp##*/}")
897 done
898 if [[ $source ]]; then
899 d mount-latest-subvol "${subvols[@]}"
900 else
901 for tg in ${targets[@]}; do
902 d /a/exe/mount-latest-remote "$tg" "${subvols[@]}" || ret=$?
903 done
904 fi
905
906 # todo, we get hostnames earlier, reuse that.
907 if [[ $ret == 0 ]]; then
908 for tg in ${targets[@]}; do
909 h=$(ssh $tg hostname)
910 if [[ $h == kd && $HOSTNAME == x3 && $HOSTNAME == "$MAIL_HOST" ]]; then
911 d ssh root@$tg 'btrbk-spread-wrap &>/dev/null </dev/null &'
912 fi
913 rsync --mkpath -a -f"- */" -f"+ *" /var/log/btrbk/ root@$tg:/var/log/btrbk/$tg
914 cmd=/usr/local/bin/mail-backup-clean
915 ssh root@$tg "if test -x $cmd; then $cmd; fi"
916 done
917 if [[ $source ]]; then
918 rsync --mkpath -a -f"- */" -f"+ *" $source:/var/log/btrbk/ /var/log/btrbk/$source
919 fi
920 fi
921
922 mexit $ret
923
924 # todo: move variable data we don't care about backing up
925 # to /nocow and symlink it.
926
927
928 # background on btrbk timezones. with short/long, timestamps use local time.
929 # for long, if your local time moves backwards, by moving timezones or
930 # for an hour when daylight savings changes it, you will temporarily get
931 # a more aggressive retention policy for the overlapping period, and
932 # vice versa for the opposite timezone move. The alternative is using
933 # long-iso, which puts timezone info into the timestamp, which means
934 # that instead of shifting time, you shift the start of day/week/month
935 # which is used for retention to your new local time, which means for
936 # example, if you moved forward by 8 hours, the daily/weekly/monthly
937 # retention will be 8 hours more aggressive since midnight is at a new
938 # time, unless you fake the timzeone using the TZ env variable.
939 # However, in the short term, there will be no inconsistencies.
940 # I don't see any problem with shifting when the day starts for
941 # retention, so I'm using long-iso.
942
943 # note to create a long-iso timestamp: date +%Y%m%dT%H%M%S%z