initial checkin; mostly complete
[distorted-backup] / bkpadmin.in
CommitLineData
99248ed2
MW
1#! /bin/sh
2###
3### Manage the backup archive structure
4###
5### (c) 2011 Mark Wooding
6###
7
8###----- Licensing notice ---------------------------------------------------
9###
10### This program is free software; you can redistribute it and/or modify
11### it under the terms of the GNU General Public License as published by
12### the Free Software Foundation; either version 2 of the License, or
13### (at your option) any later version.
14###
15### This program is distributed in the hope that it will be useful,
16### but WITHOUT ANY WARRANTY; without even the implied warranty of
17### MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18### GNU General Public License for more details.
19###
20### You should have received a copy of the GNU General Public License
21### along with this program; if not, write to the Free Software Foundation,
22### Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
23
24set -e
25
26## Configuration and testing.
27: ${BKP=/mnt/bkp} ${META=/mnt/bkpmeta}
28: ${KEYS=/etc/keys}
29
30case $(id -u) in 0) ;; *) exec userv root bkpadmin "$@" ;; esac
31
32###--------------------------------------------------------------------------
33### Common utilities.
34
35quis=${0##*/}
36version="@VERSION@"
37
38moan () {
39 ## Print a complaint to standard error.
40
41 echo >&2 "$quis: $*"
42}
43
44die () {
45 ## Print a complaint and exit.
46
47 moan "$*"
48 exit 1
49}
50
51cleanups=""
52addcleanup () {
53 cmd=$1
54 ## Add a cleanup command CMD to the list.
55
56 case "$cleanups" in
57 ?*)
58 ;;
59 *)
60 trap 'rc=$?; for c in $cleanups; do $c; done; exit $rc' \
61 EXIT INT TERM
62 ;;
63 esac
64 cleanups=${cleanups+$cleanups }$cmd
65}
66
67rmtmp () { case ${tmpdir+t} in t) rm -rf "$tmpdir" ;; esac }
68addcleanup rmtmp
69mktmp () {
70 ## Make a temporary directory and output its name.
71
72 case "${tmpdir+t}" in
73 t)
74 ;;
75 *)
76 i=0
77 while :; do
78 r=$(openssl rand -base64 12)
79 tmpdir=${TMPDIR-/tmp}/$quis.$$.$r
80 if mkdir -m700 "$tmpdir" >/dev/null 2>&1; then break; fi
81 case $i in ???) die "failed to create temporary directory" ;; esac
82 i=$(( $i + 1 ))
83 done
84 ;;
85 esac
86 echo "$tmpdir"
87}
88
89###--------------------------------------------------------------------------
90### Command dispatch.
91
92case "${USERV_USER+t}" in t) uservp=t ;; *) uservp=nil ;; esac
93
94USAGE="COMMAND [ARGUMENT ...]"
95cmdname=""
96cmdargs=$USAGE
97cmds=""
98_defcmd () {
99 name=$1; shift; args=$*
100 ## Define a command unconditionally.
101
102 cmds="${cmds:+$cmds
103}$name $args"
104}
105
106defcmd () {
107 ## Define a command for privileged users only.
108
109 case $uservp in nil) _defcmd "$@" ;; esac
110}
111
112defucmd () {
113 ## Define a command usable via userv.
114
115 _defcmd "$@"
116}
117
118usage () {
119 ## Write a usage message for the current command.
120
121 echo "usage: $quis${cmdname:+ $cmdname}${cmdargs:+ $cmdargs}"
122}
123
124usage_err () {
125 ## Fail with a usage error.
126
127 usage >&2
128 exit 1
129}
130
131lookupcmd () {
132 cmd=$1
133 ## Try to loop up the command CMD.
134
135 while read cmdname cmdargs; do
136 case $cmdname in "$cmd") return ;; esac
137 done <<EOF
138$cmds
139EOF
140 die "unknown command \`$cmd'"
141}
142
143defucmd help
144cmd_help () {
145 case $# in 0) ;; *) usage_err ;; esac
146
147 cat <<EOF
148$quis, version $version
149
150usage: $quis $USAGE
151
152Commands provided:
153EOF
154 while read cmd args; do
155 echo " $cmd${args:+ $args}"
156 done <<EOF
157$cmds
158EOF
159}
160
161###--------------------------------------------------------------------------
162### Utility functions.
163
164sign () {
165 file=$1
166 ## Sign the named FILE, producing a signature FILE.sig.
167
168 seccure-sign -F$KEYS/priv/backup-auth -cp256 -s"$file.sig" <"$file"
169}
170
171checkhost () {
172 ## Check that a host is defined.
173
174 case "${host+t}" in
175 t) ;; *) die "no host defined (use \`-H')" ;;
176 esac
177}
178
179checkthing () {
180 thing=$1 good=$2 what=$3 string=$4
181 ## Check that STRING is a valid THING -- i.e., it only consists of GOOD
182 ## characters.
183
184 case "$string" in
185 *[!$good]*)
186 die "bad $thing \`$string' given for $what"
187 ;;
188 esac
189}
190
191checknum () {
192 what=$1 string=$2
193 ## Check that STRING is at least plausibly numeric.
194
195 checkthing number "0-9" "$what" "$string"
196}
197
198checkpath () {
199 what=$1 string=$2
200 ## Check that STRING is a plausible pathname.
201
202 case "$string" in
203 .* | */.* | *[!-a-zA-Z0-9.,_#!%^+=@/:]*)
204 die "bad pathname \`$string' given for $what"
205 ;;
206 esac
207}
208
209checkword () {
210 what=$1 thing=$2
211 ## Check that THING doesn't need shell quoting, and doesn't interfere with
212 ## other common delimiter characters. (Colons aren't allowed because they
213 ## mess up /etc/passwd; slashes aren't allowed because they're directory
214 ## separators. Leading dots aren't allowed either. Hashes seem OK.)
215
216 checkthing word "-a-zA-Z0-9.,_#!%^+=@" "$what" "$string"
217}
218
219domkdir () {
220 dir=$1 owner=$2 mode=$3
221 ## Make a directory and set permissions on it.
222
223 mkdir -m755 "$dir"
224 chown $owner "$dir"
225 chmod $mode "$dir"
226}
227
228###--------------------------------------------------------------------------
229### Volume and volume group maintenance.
230
231currenttag () {
232 ## Output the tag of the mounted backup volume group.
233
234 dev=$(mntdev $BKP)
235 case "$dev" in
236 /dev/mapper/cbkp-*) echo "${dev#*-}"; return ;;
237 *) die "failed to parse tag from device name \`$dev'" ;;
238 esac
239}
240
241guesstag () {
242 ## Guess and print the tag of the available backup volume group. If there
243 ## is not exactly one volume group available, print an error and fail.
244
245 LVM_SUPPRESS_FD_WARNINGS=t vgs @backup --noheadings -o name,attr | {
246 match=""
247 while read name attr; do
248 case "$name" in bkp-*) ;; *) continue ;; esac
249 case "$attr" in ??x*) continue ;; esac
250 match="$match${match:+ }${name#bkp-}"
251 done
252 case "x$match" in
253 x) die "no backup volume groups available" ;;
254 x*\ *) die "multiple backup volume groups available: $match" ;;
255 esac
256 echo "$match"
257 }
258}
259
260mntdev () {
261 dir=$1
262 ## Output a device name for the filesystem mounted on DIR.
263
264 dev=$(mountpoint -d "$dir")
265 devname=$(udevadm info --query=name --path="/dev/block/$dev")
266 case "$devname" in
267 dm-*)
268 devname=mapper/$(dmsetup info -c --noheadings -oname "/dev/$devname")
269 ;;
270 esac
271 echo "/dev/$devname"
272}
273
274mntmeta () {
275 tag=$1
276 ## Mount the metadata volume of the backup volume group named TAG.
277
278 if ! mountpoint -q $META; then
279 mount "/dev/bkp-$tag/meta" $META
280 fi
281}
282
283cryptkey () {
284 ## Decrypt and output the key for the encrypted volume. This assumes that
285 ## the metadata volume is already mounted on /mnt/bkpmeta.
286
287 seccure-decrypt -q -m128 -cp256 -F$KEYS/priv/backup-disk <$META/cur/blob
288}
289
290decrypt () {
291 tag=$1
292 ## Decrypt but don't mount the encrypted volume of the backup volume group
293 ## named TAG.
294
295 mntmeta "$tag"
296 if [ ! -b "/dev/mapper/cbkp-$tag" ]; then
297 cryptkey | cryptsetup luksOpen --key-file=- \
298 "/dev/bkp-$tag/crypt" "cbkp-$tag"
299 fi
300}
301
302mntcrypt () {
303 tag=$1
304 ## Mount the encrypted subvolume of the backup volume group named TAG. The
305 ## metadata volume will be mounted if necessary.
306
307 decrypt "$tag"
308 if ! mountpoint -q $BKP; then
309 mount "/dev/mapper/cbkp-$tag" $BKP
310 fi
311}
312
313umnt () {
314 ## Unmounts a backup volume group: both the encrypted and metadata volumes
315 ## are unmounted.
316
317 if mountpoint -q $BKP; then
318 tag=$(currenttag) cryptclosep=t
319 else
320 cryptclosep=nil
321 fi
322 for i in bkp bkpmeta; do
323 if mountpoint -q /mnt/$i; then umount /mnt/$i; fi
324 done
325 case $cryptclosep in
326 t)
327 if [ -b "/dev/mapper/cbkp-$tag" ]; then
328 cryptsetup luksClose "cbkp-$tag"
329 fi
330 esac
331}
332
333defcmd initvol TAG DEVICE
334cmd_initvol () {
335 case $# in 2) ;; *) usage_err ;; esac
336 tag=$1 dev=$2
337
338 vgcreate --addtag @backup "bkp-$tag" "$dev"
339
340 lvcreate -L4M -nmeta "bkp-$tag"
341 mkfs -text2 -Lmeta "/dev/bkp-$tag/meta"
342 mntmeta "$tag"
343
344 mkdir -m755 $META/new
345 dd if=/dev/random bs=1 count=512 |
346 seccure-encrypt -m128 $(cat $KEYS/pub/backup-disk.pub) >$META/new/blob
347 mv $META/new $META/cur
348
349 lvcreate -l100%FREE -ncrypt "bkp-$tag"
350 cryptkey | cryptsetup luksFormat \
351 --cipher=twofish-xts-benbi:sha256 --hash=sha256 \
352 "/dev/bkp-$tag/crypt" -
353 decrypt "$tag"
354 mkfs -text2 -Lbackup -i1048576 "/dev/mapper/cbkp-$tag"
355 mntcrypt "$tag"
356}
357
358defucmd mount "[TAG]"
359cmd_mount () {
360 case $# in
361 0) tag=$(guesstag) check=nil ;;
362 1) tag=$1 check=t ;;
363 *) usage_err ;;
364 esac
365
366 if mountpoint -q $BKP; then
367 curtag=$(currenttag)
368 case "$check,$curtag" in "t,$tag") ;; t*) exit 1 ;; esac
369 else
370 mntcrypt "$tag"
371 fi
372}
373
374defcmd umount
375cmd_umount () {
376 case $# in 0) ;; *) usage_err ;; esac
377 mntp=nil
378 for i in bkp bkpmeta; do
379 if mountpoint -q /mnt/$i; then mntp=t; fi
380 done
381 case $mntp in
382 nil) die "backup volume not mounted" ;;
383 esac
384 umnt
385}
386
387###--------------------------------------------------------------------------
388### Archive maintenance.
389
390checkdir () {
391 key=$1 dir=$2
392 ## Check a directory which has `hashes' and `hashes.sig' files.
393
394 if ! seccure-verify -q -i"$dir/hashes" -- \
395 $(cat "$KEYS/$key") $(cat "$dir/hashes.sig")
396 then
397 die "failed to verify signature for \`$dir'"
398 fi
399
400 cd "$dir"
401 sha256sum --quiet -c hashes
402
403 tmpdir=$(mktmp)
404 find . -type f -print | sed 's:^\./::' | sort >"$tmpdir/present"
405 { echo hashes
406 echo hashes.sig
407 sed 's/^[a-f0-9]*[* ] //' hashes
408 } | sort >"$tmpdir/checked"
409 cd "$tmpdir"
410 diff -u checked present
411}
412
413fixperms () {
414 dir=$1 owner=$2 fmode=$3 dmode=$4
415 ## Fix the directory tree DIR so that everything is owned by OWNER (a
416 ## USER:GROUP pair) and has modes FMODE for files and DMODE for
417 ## directories.
418
419 ## Change all of the ownerships. This will prevent anyone else from
420 ## changing the permissions on the files. This assumes that chown(1) is
421 ## secure in recursive mode; I've checked that GNU chown seems correct.
422 chown -R $owner "$dir"
423
424 ## Paranoia: check that we correctly changed all of the files.
425 u=${owner%:*} g=${owner#*:}
426 (cd "$dir"; find . ! \( -user $u -group $g \) -ls) |
427 if read line; then
428 moan "failed to fix permssions on \`$dir'"
429 { echo $line; cat; } | sed 's/^/ /'
430 exit 1
431 fi
432
433 ## Now get to work on the file and directory permissions.
434 find "$dir" -type d -print0 | xargs -0r chmod $dmode
435 find "$dir" ! -type d -print0 | xargs -0r chmod $fmode
436}
437
438commitdir () {
439 dir=$1 target=$2
440 ## Commit an `prepare' directory DIR, moving its `incoming' files to
441 ## TARGET. This will choose the correct name for the directory, but
442 ## assumes that it's already correctly laid out. We assume that the
443 ## permissions on this directory are safe (e.g., they've already been fixed
444 ## using `fixperms'). On successful exit, DIR won't exist any more. The
445 ## shell variable `label' is set to the resulting archive name.
446
447 ## If there's no `incoming' directory, then there's nothing to do. Just
448 ## zap the directory and move on.
449 if [ ! -d "$dir/incoming" ]; then
450 rm -rf "$dir"
451 return
452 fi
453
454 ## Find the datestamp and level numbers to use for this directory. These
455 ## are created before the `incoming' directory, so they ought to exist.
456 read level date time tz <"$dir/meta"
457
458 ## Find a suitable sequence number for the target. This is rather ugly;
459 ## sorry.
460 seq=1
461 while :; do
462 anyp=nil
463 for i in "$target"/"$date#$seq".*; do
464 if [ -e "$i" ]; then anyp=t; break; fi
465 done
466 case $anyp in nil) break ;; esac
467 seq=$(( $seq + 1 ))
468 done
469
470 ## Move the directory.
471 label="$date#$seq.$level"
472 mv "$dir/incoming" "$target/$label"
473 rm -rf "$dir"
474
475 ## Update the catalogue. Replace an existing dump at the same level.
476 ## Assume that dates are monotonically increasing: add the new entry at the
477 ## end.
478 { found=nil
479 while read lab l d t; do
480 if [ $l -ne $level ]; then echo $label $l $d $t; fi
481 done <"$target"/CATALOGUE
482 echo $level $date $time $tz
483 } >"$target"/CATALOGUE.new
484 mv "$target"/CATALOGUE.new "$target"/CATALOGUE
485}
486
487defcmd initmeta
488cmd_initmeta () {
489 case $# in 0) ;; *) usage_err ;; esac
490
491 ## Make a `new' directory and start recording our files.
492 cd $META
493 rm -rf new
494 mkdir -m755 new
495 f=""
496
497 ## Copy the blob from the existing metadata.
498 cp cur/blob new/
499 f="$f blob"
500
501 ## Archive the key recovery information.
502 cd $KEYS
503 tar cfz $META/new/keys.tgz pub/ recov/
504 f="$f keys.tgz"
505
506 ## Copy user and group information.
507 cd $META/new
508 for i in passwd group; do
509 grep -E '^(root|backup|bkp-[[:alnum:]]+):' /etc/$i >$i
510 done
511 f="$f passwd group"
512
513 ## Build the hashes file, and sign it.
514 chown root:root $f
515 chmod 644 $f
516 sha256sum $f >hashes
517 sign hashes
518
519 ## Replace the old metadata.
520 cd $META
521 mv cur old
522 mv new cur
523 rm -rf old
524}
525
526defcmd chkmeta
527cmd_chkmeta () {
528 case $# in 0) ;; *) usage_err ;; esac
529
530 checkdir pub/backup-auth.pub $META/cur
531}
532
533today () {
534 ## Report the current date, as ISO8601. Allow an override.
535
536 case "${forceday+t}" in t) echo "$forceday" ;; *) date +%Y-%m-%d ;; esac
537}
538
539defucmd prep ASSET LEVEL \[DATE TIME TZ]
540cmd_prep () {
541 case $# in
542 2) set -- "$@" $(today) $(date +%H:%M:%S) $(date +%z) ;;
543 5) ;;
544 *) usage_err ;;
545 esac
546 asset=$1 level=$2 date=$3 time=$4 tz=$5
547 checkhost
548 checkword asset "$asset"
549 checknum level "$level"
550 checkthing date -0-9 date "$date"
551 checkthing time :0-9 time "$time"
552 checkthing timezone -+0-9 tz "$tz"
553
554 ## Make the host and asset directories if necessary.
555 cd $BKP
556 for i in $host $asset; do
557 if [ ! -d $i ]; then domkdir $i root:root 755; fi
558 cd $i
559 done
560 if [ ! -d failed ]; then domkdir failed root:root 755; fi
561 for i in . failed; do
562 if [ ! -f $i/CATALOGUE ]; then
563 touch $i/CATALOGUE
564 chown root:root $i/CATALOGUE
565 chmod 644 $i/CATALOGUE
566 fi
567 done
568
569 ## If an existing dump is in progress then archive it as a failure.
570 if [ -d prepare ]; then
571 if [ -d prepare/incoming ]; then
572 fixperms prepare/incoming root:root 640 755
573 fi
574 commitdir prepare failed/
575 fi
576
577 ## Make a new preparation directory.
578 domkdir prepare root:bkp-$host 755
579 echo $level $date $time $tz >prepare/meta
580 domkdir prepare/incoming bkp-$host:bkp-$host 2775
581
582 ## Print the directory name.
583 echo $BKP/$host/$asset/prepare/incoming
584}
585
586defucmd abort ASSET
587cmd_abort () {
588 case $# in 1) ;; *) usage_err ;; esac
589 asset=$1
590 checkhost
591 checkword asset "$asset"
592
593 ## Check that there's something to abort.
594 cd $BKP
595 if [ ! -d $host/$asset/prepare ]; then
596 die "no dump in progress for $host/$asset"
597 fi
598
599 ## Just throw it away.
600 rm -rf $host/$asset/prepare
601}
602
603defucmd fail ASSET
604cmd_fail () {
605 case $# in 1) ;; *) usage_err ;; esac
606 asset=$1
607 checkhost
608 checkword asset "$asset"
609
610 ## Check that there's something to fail.
611 cd $BKP
612 if [ ! -d $host/$asset/prepare ]; then
613 die "no dump in progress for $host/$asset"
614 fi
615
616 ## Archive the failure. This shouldn't be used to determine dump levels or
617 ## we'll have gaps when things get sorted out.
618 cd $host/$asset
619 if [ -d prepare/incoming ]; then
620 fixperms prepare/incoming root:root 640 755
621 fi
622 commitdir prepare failed/
623}
624
625julian () {
626 date=$1
627 ## Convert an ISO8601 DATE to a Julian Day Number.
628
629 ## Extract the components of the date and trim leading zeros (which will
630 ## cause things to be interpreted as octal and fail).
631 year=${date%%-*} rest=${date#*-}; month=${rest%%-*} day=${rest#*-}
632 year=${year#0} month=${month#0} day=${day#0}
633
634 ## The actual calculation: convert a (proleptic) Gregorian calendar date
635 ## into a Julian day number. This is taken from Wikipedia's page
636 ## http://en.wikipedia.org/wiki/Julian_day#Calculation but the commentary
637 ## is mine. The epoch is 4713BC-01-01 (proleptic) Julian, or 4714BC-11-24
638 ## proleptic Gregorian.
639
640 ## If the MONTH is January or February then set a = 1, otherwise set a = 0.
641 a=$(( (14 - $month)/12 ))
642
643 ## Compute a year offset relative to 4799BC-03-01. This puts the leap day
644 ## as the very last day in a year, which is very convenient. The offset
645 ## here is sufficient to make all y values positive (within the range of
646 ## the JDN calendar), and is a multiple of 400, which is the Gregorian
647 ## cycle length.
648 y=$(( $year + 4800 - $a ))
649
650 ## Compute the offset month number in that year. These months count from
651 ## zero, not one.
652 m=$(( $month + 12*$a - 3 ))
653
654 ## Now for the main event. The (153 m + 2)/5 term is a surprising but
655 ## correct trick for obtaining the number of days in the first m months of
656 ## the (shifted) year). The magic offset 32045 is what you get when you
657 ## plug the proper JDN epoch (year = -4713, month = 11, day = 24) into the
658 ## above machinery.
659 jdn=$(( $day + (153*$m + 2)/5 + 365*$y + $y/4 - $y/100 + $y/400 - 32045 ))
660
661 echo $jdn
662}
663
664dumplevel () {
665 fulldate=$1 lastdate=$2
666 ## Return the dump level, given that the most recent full dump occurred on
667 ## FULLDATE and the most revent dump of any kind occurred on LASTDATE.
668
669 ## Actually, we're much more interested in the day difference between these
670 ## two times.
671 fulljdn=$(julian $fulldate)
672 lastjdn=$(julian $lastdate)
673 now=$(today); nowjdn=$(julian $now)
674 lastday=$(( $lastjdn - $fulljdn ))
675 today=$(( $nowjdn - $fulljdn ))
676
677 ## If the difference is greater than 512 then we know we should do a full
678 ## dump. (This provides an upper bound for the search below. It should
679 ## never happen in practice, of course.)
680 if [ $(( $today - $lastday )) -ge 512 ]; then echo 0; return; fi
681
682 ## Now we work out the correct dump level. This will assume that the
683 ## previous dump had a sensible level. If dumps are omitted, then we will
684 ## choose a lower (more comprehensive) dump level than the schedule calls
685 ## for; such an overestimation will mean that we will probably end up
686 ## dumping too much again. This is the right error to make.
687 ##
688 ## We use a Towers of Hanoi schedule. If we're doing dumps every day, then
689 ## on day n since the last full dump, we work out the dump level as
690 ## follows: write n = 2^s t where t is odd (i.e., s is the number of
691 ## trailing zero bits in the binary representation of n); then the dump
692 ## level on day n is 9 - s. This is enough for 512 days without a full
693 ## dump, and it fails gracefully anyway.
694 ##
695 ## Now we have to deal with the problem of skipping dumps. Suppose the
696 ## last dump was on day m = 2^u v, and it's now day n = 2^s t. We ought to
697 ## take the lowest dump level of any intervening day, i.e., the dump level
698 ## is 9 - a for the largest a such that there exists b with m < l = 2^a b
699 ## <= n. We claim that such an l is unique. Suppose, to the contrary,
700 ## that m < 2^a b < 2^a b' <= n, with both b and b' odd. Then m < 2^{a+1}
701 ## (b + 1)/2 <= n, contradicting maximality of a.
702 ##
703 ## How does this help? Observe that n = 2^s t = 2^a b + o, for some o <
704 ## 2^a: if o >= 2^a then 2^a (b + 1) <= n contradicting uniqueness of l.
705 ## Similarly, m = 2^u v = 2^a b - r, for some r <= 2^a (otherwise m <
706 ## 2^a (b - 1), again contradicting uniqueness). Therefore, m and n are
707 ## identical from bit a + 1 onwards, and differ at bit a. In other words,
708 ## a is the position of the most significant set bit in m XOR n.
709 diff=$(( lastday ^ today ))
710
711 ## We know that the bit position must be less than 16.
712 t=16 n=0
713 while [ $diff -gt 1 ]; do
714 xx=$(( $diff >> $t ))
715 if [ $xx -gt 0 ]; then
716 diff=$xx n=$(( $n + $t ))
717 fi
718 t=$(( $t >> 1 ))
719 done
720
721 echo $(( 9 - $n ))
722}
723
724defucmd level ASSET
725cmd_level () {
726 case $# in 1) ;; *) usage_err ;; esac
727 asset=$1
728 checkhost
729 checkword asset "$asset"
730
731 ## Set the correct directory. If it doesn't exist then we obviously need a
732 ## level-0 dump.
733 cd $BKP
734 full="0 1970-01-01 00:00:00 +0000"
735 if [ ! -d $host/$asset ]; then echo $full; return; fi
736 cd $host/$asset
737
738 ## We need the time of the most recent dump of any kind, and the most
739 ## recent level-zero dump.
740 fulldate=none lastdate=none
741 while read label level date time tz; do
742 if [ $level -eq 0 ]; then fulldate=$date; fi
743 lastdate=$date
744 done <CATALOGUE
745 case $fulldate in none) echo $full; return ;; esac
746 level=$(dumplevel $fulldate $lastdate)
747
748 ## Determine the time of the most recent dump of the same or more inclusive
749 ## level.
750 date=none
751 while read lab l d t; do
752 if [ $l -le $level ]; then date=$d time=$t; fi
753 done <CATALOGUE
754 echo $level $date $time $tz
755}
756
757defucmd hash ASSET FILE HASH
758cmd_hash () {
759 case $# in 3) ;; *) usage_err ;; esac
760 asset=$1 file=$2 hash=$3
761 checkword asset "$asset"
762 checkpath file "$file"
763 checkword hash "$hash"
764
765 cd $BKP/$host/$asset/prepare
766
767 if [ -f hashes ]; then
768 while read h f; do
769 case "$f" in "$file") die "file \`$file' already hashed" ;; esac
770 done <hashes
771 cp hashes hashes.new
772 fi
773 echo "$hash $file" >>hashes.new
774 mv hashes.new hashes
775}
776
777defucmd commit ASSET
778cmd_commit () {
779 case $# in 1) ;; *) usage_err ;; esac
780 asset=$1
781 checkhost
782 checkword asset "$asset"
783
784 cd $BKP/$host/$asset/prepare
785 fixperms incoming root:bkp-$host 640 755
786 findargs=""
787
788 if [ -f hashes ]; then
789 while read hash name; do
790 if [ ! -f "incoming/$name" ]; then
791 die "precomputed hash for nonexistent or non-file \`$name'"
792 fi
793 findargs="$findargs ! -path incoming/$name"
794 done <hashes
795 cp hashes hashes.calc
796 fi
797
798 find incoming -type f $findargs -print0 | \
799 xargs -0r sha256sum | \
800 sed 's: incoming/: :' \
801 >>hashes.calc
802 sort -k2 hashes.calc >incoming/hashes
803 sign incoming/hashes
804 chmod 640 incoming/hashes incoming/hashes.sig
805 chown root:bkp-$host incoming/hashes incoming/hashes.sig
806
807 cd ..
808 commitdir prepare .
809 echo "$label"
810}
811
812defucmd check ASSET LABEL
813cmd_check () {
814 case $# in 2) ;; *) usage_err ;; esac
815 asset=$1 label=$2
816 checkhost
817 checkword asset "$asset"
818 checkword label "$label"
819
820 checkdir pub/backup-auth.pub $BKP/$host/$asset/$label
821}
822
823defucmd catalogue ASSET
824cmd_catalogue () {
825 case $# in 1) ;; *) usage_err ;; esac
826 asset=$1
827 checkhost
828 checkword asset "$asset"
829
830 cat $BKP/$host/$asset/CATALOGUE
831}
832
833defucmd outdated ASSET
834cmd_outdated () {
835 case $# in 1) ;; *) usage_err ;; esac
836 asset=$1
837 checkhost
838 checkword asset "$asset"
839
840 cd $BKP/$host/$asset
841 for i in [0-9]*#*.*; do
842 if [ -d "$i" ]; then echo "$i"; fi
843 done |
844 sort -rn |
845 { best=10
846 while read tag; do
847 date=${tag%%#*} level=${tag##*.}
848 if [ $level -le $best ]
849 then best=$level
850 else echo "$tag"
851 fi
852 done
853 }
854}
855
856###--------------------------------------------------------------------------
857### Main program.
858
859defcmd test CMD '[ARGS ...]'
860cmd_test () { "$@"; }
861
862case $uservp in
863 t)
864 host=${USERV_USER#bkp-}
865 opts="h"
866 ;;
867 nil)
868 unset host
869 opts="hH:D:"
870 ;;
871esac
872
873while getopts "$opts" opt; do
874 case "$opt" in
875 h) cmd_help; exit ;;
876 H) host=$OPTARG ;;
877 D) forceday=$OPTARG ;;
878 *) usage_err ;;
879 esac
880done
881shift $(( $OPTIND - 1 ))
882
883case $# in 0) usage_err ;; esac
884lookupcmd "$1"; shift
885cmd_$cmdname "$@"
886
887###----- That's all, folks --------------------------------------------------