-
Notifications
You must be signed in to change notification settings - Fork 0
/
zfs-backup.sh
executable file
·1306 lines (1098 loc) · 37.3 KB
/
zfs-backup.sh
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
#!/bin/sh
export PATH="/sbin:/usr/sbin:/usr/local/sbin:/bin:/usr/bin:/usr/local/sbin"
: ${BACKUPROOT:=/backup}
readonly ME=$(basename $0)
readonly COMMAND=$0
readonly enable_prop='zfs-backup:enabled'
ON_CLIENT=
ACTION=
usage( ) {
cat >&2 <<EOF
${ME}: Usage:
$ME __client
$ME __list_tags [-d] -F filesystem
$ME __list_fs [-d]
$ME backup [-dnvz] -h hostname -u user [-f filesystem:...] [-e filesystem:...]
$ME check [-dvz] -h hostname -u user [-f filesystem:...] [-e filesystem:...]
$ME full [-dnvz] -h hostname -u user [-f filesystem:...] [-e filesystem:...]
$ME list [-dvz] [-h hostname] [-u user ] [-f filesystem:...] [-e filesystem:...]
$ME nuke [-dnv] -h hostname -u user [-f filesystem:...] [-e filesystem:...]
$ME ping -h hostname -u user
$ME setup [-d] -h hostname -u user -f filesystem:... [-e filesystem:...]
'$ME __client' For internal use: should only be run as a forced
command from the backup user's authorized_keys file. It takes no
options.
'$ME __list_tags' For internal use on the client only. List the tags
for all the backups known for the given filesystem, in date order,
newest first.
'$ME __list_fs' For internal use on the client only. List all the
mounted filesystems of type ZFS which have been tagged with the
property '${enable_prop}=yes'.
'$ME ping' Test SSH connectivity and that authorized_keys has been set
up correctly on the client.
'$ME backup' sends the incremental changes between the previous backup
and now as an incremental zfs-send stream. This has actions both
client- and server-side. On the client it creates a snapshot, which
is only retained until the following backup completes. The snapshot
is copied over to the server by the backup process: hence the server
will contain the history of previous backups as a series of snapshots.
Unless a filesystem is given explicitly on the command line, this
defaults to operating on all filesystems with the property
'${enable_prop}' set to 'yes'.
'$ME check' reports on the settings both server and client side,
including the necessary SSH keys having connectivity, permissions
applied to the backup user by zfs-allow and that there is an initial
full copy of the filesystem on the backup server. ie. the systems are
correctly set up for backup.
'$ME full' does the initial send of the full filesystem from the
client to the backup server. This is necessary one time for the
initial setup, but subsequent use should be avoided as trying to
overwrite an existing ZFS will fail.
'$ME list' lists the snapshots available on the backup server,
optionally limiting the output to what is available a specific host or
a specific host and filesystem. If no filesystems are given
explicitly on the command line, needs the '-u user' option to be
specified in order to retreive the list of backed-up filesystems from
the client.
'$ME nuke' Deletes all backups for the given filesystem of the named
host. Deletes all backup related snapshots or bookmarks for that
filesystem on the client. There is no way to undo the effects of this
command, so don't do it unless you really mean it.
'$ME setup' Prints out shell scripts to create the filesystems needed
server-side, set up ZFS actions allowed to users necessary for the
backup to run (on both client and server) and ensures SSH authorized
keys are set up correctly. The scripts will need to be run by root as
directed on each server.
Options:
-d Debug mode: trace program execution to stderr.
-e Filesystems not to backup - as a colon separated list. Can be
given multiple times. Any additional exceptions will be added
to the list.
-f Filesystems to backup - as a colon separated list of the full
paths from the root directory eg. /usr/local/export:/home May
be given multiple times. Any additional filesystems will be
added to the list.
-h Hostname to backup.
-n Dry-run mode: Show what would be done, without committing
any changes.
-u Username on remote host.
-v Verbose operation: print information about progress to stderr.
-z Compress data over the wire. Enables SSH's Compression option.
Data are compressed on the target drive, and the usage numbers zfs
reports are by default in terms of the actual number of disk blocks
consumed. This may be significantly less than the apparent size of
the data transferred.
Compressing SSH traffic may or may not improve performance: you will
have to experiment to find the best setting. In general, compression
only helps on relatively low bandwidth, high RTT connections, and
where content is intrinsically compressible.
Other than the 'list' verb, always run the script on the server as the
same user, who is assumed to own the SSH key used for access and who
has write access to the per-host storage.
ToDo:
- record the original properties of the backed up ZFSes (dump to a
file at the mountpoint of the zfs before backup?)
EOF
exit 1
}
# $BACKUPKEY allows password-less access to the backup user account on
# the client machine. For security reasons, the key should be set up
# to run 'zfs-backup.sh __client' as a forced command: this will
# enforce running only the commands known to zfs-backup.sh
: ${BACKUPKEY:=$( eval echo ~$SERVERUSER)/.ssh/zfs-backup}
on_client() {
local clienthost="$1"
local clientuser="$2"
shift 2
ssh -o BatchMode=yes -o IdentitiesOnly=yes -o IdentityFile=$BACKUPKEY \
-o Compression=$option_z $clientuser@$clienthost $COMMAND \
${1+$@} || exit 1
}
# Echo the command line to stderr if in verbose mode, then run the
# command
runv() {
if [ -n $option_v ]; then
if [ "$ON_CLIENT" = 'yes' ]; then
echo >&2 "--> $@"
else
echo >&2 "==> $@"
fi
fi
"$@"
}
# The local zpool -- assumed to be the *biggest* one returned in the
# list if there are more than one. Set ZPOOL in the environment to
# override.
get_zpool() {
# Bah! zpool(8) doesn't have -s or -S options like zfs(8)
zpool list -Hp -o size,name | sort -rn | head -1 | cut -f 2
}
: ${ZPOOL:=$(get_zpool)}
# Generate the name of the ZFS used to store the backups server-side
# given the host and filesystem.
#
# Translate the filesystem path (which can contain all sorts of nasty
# stuff) into a sanitized directory name we can use for a ZFS.
#
# Note: According to the docs, a ZFS name can only consist of
# characters from
# [:alnum:] _ : . -
# but a filename can contain anything except / and \0. However we
# assume that people aren't using \n in filenames, so we can use that
# as the list separator.
#
# TODO: This is not guarranteed to generate a unique result, and
# failure to do so will end badly.
server_local_storage() {
local var_return="$1"
local clienthost="$2"
local filesystem="$3"
local path
# Without a hostname, the filesytem part is useless and will be
# ignored.
if [ -n "$clienthost" ]; then
path="/$clienthost"
if [ -n "$filesystem" ]; then
path="$path/$(echo $filesystem | tr -c '[:alnum:]_:.-\n' _)"
fi
fi
setvar "$var_return" "$ZPOOL$BACKUPROOT$path"
}
# Identify which ZFS is mounted containing the path of interest (not
# limited to ZFS mountpoints). Typically this is only done
# client-side. We make up our own ZFS devices and mountpoints
# server-side, which should be mostly invisible to users.
path_to_zfs() {
local var_return="$1"
local path="$2"
local zfs
zfs=$(zfs list -H -t filesystem -o name $path)
: ${zfs:?${ME}: Cannot find a ZFS mounted as filesystem \"$path\"}
setvar "$var_return" "$zfs"
}
# Find where the named zfs is mounted. Only returns the mountpoint, so
# not actually the inverse of path_to_zfs().
zfs_to_path() {
local var_return="$1"
local zfs="$2"
local mountpoint
mountpoint=$(zfs list -H -t filesystem -o mountpoint $zfs)
: ${mountpoint:?${ME}: Cannot find a mountpoint for ZFS \"$zfs\"}
setvar "$var_return" "$mountpoint"
}
# snapname is used for both snapshots and bookmarks -- it is a unique
# identifier formed using 16 random hex digits. The unique ID allows
# us to identify equivalent snapshot data between client and server
# sides -- where filesystem layouts will be quite different in
# general.
#
# We're constrained by not making the total pathname for the mounted
# snapshot too long, otherwise the .zfs/snapshot/ automount feature
# will not work and subsequently various other ZFS operations (umount,
# rename) return EBUSY. (Needs a 'zfs umount -f' to fix).
#
# 8 bytes of randomness => 16 hex digits = 2^64 = 18446744073709551616
# different possibilities, which should be enough that we just don't
# need to worry about collisions.
generate_snapname() {
local var_return="$1"
setvar "$var_return" "$(openssl rand -hex 8)"
}
# A regex to match the snapshot format.
readonly snap_match='[[:xdigit:]]{16}$'
# Extract the $tag from a fully or partially qualified snapshot or
# bookmark name (eg zpool/some/zfs@snapname @snapname
# zpool/some/zfs#bookmark #bookmark)
get_tag_from() {
local name="$1"
echo ${name##*[@#]}
}
# return the list of full snapshot or bookmark names of previous
# backups matching the specified tag. This is treated as only
# returning one item everywhere, but it is in principle capable of
# returning two...
get_prev_backup_by_tag() {
local var_return="$1"
local zfs="$2"
local tag="$3"
local prevbackups
prevbackups=$(zfs list -H -d 1 -t snapshot,bookmark -o name $zfs | \
grep -E "[@#]$tag\$")
: ${prevbackups:?${ME}: Cannot find the previous backup matching tag \"$tag\"}
setvar "$var_return" "$prevbackups"
}
# Create a snapshot
create_snapshot() {
local zfs="$1"
local snapname="$2"
if [ -z $option_n ]; then
runv zfs snapshot "$zfs@$snapname"
fi
}
# When we delete a snapshot, always create a matching bookmark
# instead. This means we can delete all backup related snapshots
# locally on the client, but still use the associated bookmark for
# incremental backups to the backup server. Bookmarks require tiny
# amounts of space to store, so just leave them in place on the client
# indefinitely.
delete_snapshot() {
local zfs="$1"
local snapname="$2"
if [ -z $option_n ]; then
runv zfs bookmark "$zfs@$snapname" "$zfs#$snapname"
fi
runv zfs destroy $option_n $option_v "$zfs@$snapname"
}
# All the zfs-backup related snapshots or bookmarks for a specific ZFS
# -- the filesystem will differ depending on whether we're on the
# client or the server.
get_zfs_objects() {
local var_return="$1"
local type="$2"
local zfs="$3"
local reversed="$4"
local sort_order
local zobj
if [ -z "$reversed" ]; then
sort_order='-s creation' # Oldest first
else
sort_order='-S creation' # Reversed: newest first
fi
case $type in
all)
type='snapshot,bookmark'
;;
bookmark|snapshot)
;;
*)
echo >&2 "$ME: $type not understood:" \
"try one of 'all', 'bookmark' or 'snapshot'"
exit 1
;;
esac
zobj=$( zfs list -H -t $type $sort_order -o name -d 1 $zfs | \
grep -E "[@#]$snap_match" )
setvar "$var_return" "$zobj"
}
# List the tags for all the backups (snapshots or bookmarks) known
# on the named filesystem, *newest* first.
list_tags() {
local filesystem="${1:?${ME}: Need a filesystem to list backup tags for}"
local prevbackups
local zfs
local backup
path_to_zfs zfs $filesystem
get_zfs_objects prevbackups all $zfs reversed
for backup in $prevbackups; do
get_tag_from $backup
done
}
# Read new-line separated list of filesystems and grep out everything
# also listed in the colon separated list $exclude.
exclude_fs() {
local exclude="$1"
local re="^($( echo $exclude | tr ':' '|' ))\$"
# A pure-shellish way of achieving the same sort of result:
# Could be slow...
#for f in $ff ; do
# for e in $ee ; do
# if [ "$e" = "$f" ]; then
# continue 2
# fi
# done
#
# echo $f
#done
grep -vE "$re"
}
# List of filesystems for backing up, one per line. These are the
# mounted filesytems tagged with the property 'zfs-backup:enable=yes'
client_list_fs() {
zfs list -H -r -o ${enable_prop},mounted,mountpoint zroot | \
awk -F \t '{ if($1 == "yes" && $2 == "yes") print $3 }' | \
paste -s -d : -
}
# List of the filesystems currently on the server for a particular host,
# excluding those on the exclusion list.
server_list_fs() {
local clienthost="$1"
local exclude="$2"
local storage
local filesystems
server_local_storage storage $clienthost
zfs list -H -d 1 -o name $storage | exclude_fs $exclude
}
# Generate the working set of filesystems: all of the filesystems
# given as -f arguments, or else all of the filesystems on the client
# with the $enable_prop set, excluding any filesystems given as -e
# arguments.
#
# Returns a list of filesystems, one per line.
client_filesystems() {
local clienthost="$1"
local clientuser="$2"
local fslist="$3"
local exclude="$4"
# Set the list of filesystems to backup automatically, based on
# properties set on the client machine.
if [ -z $fslist ]; then
fslist="$( on_client $clienthost $clientuser __list_fs $option_d )"
fi
# Convert : separated list to space separated list. do this in a
# sub-shell to avoid polluting the value of IFS elsewhere.
(
IFS=:
for fs in $fslist ; do
echo $fs
done
) | exclude_fs "$exclude"
}
# Test SSH connectvity - server pings, and client pongs in reply.
server_ping() {
local clienthost="${1:?${ME}: Need a hostname to check connectivity to}"
local clientuser="${2:?${ME}: Need a username to check connectivity as}"
local response
response=$( on_client $clienthost $clientuser ping )
if [ "$response" != "Pong" ]; then
echo >&2 "$ME: ERROR SSH connectivity test failed"
return 1
else
echo >&2 "--> OK: SSH connectivity to $clienthost as $clientuser" \
"is set up correctly."
fi
}
client_pong() {
echo "Pong"
}
# Check that the server user can write to /$BACKUPROOT/$clienthost/ --
# this is necessary so that the user can create the mount point for
# mounting the backed-up filesystem. Either owner or group writable
# should suffice.
check_access() {
local serveruser="$1"
local mountpoint="$2"
local owner
local group
local perms
local backupgroups
owner=$(stat -f '%Su' $mountpoint)
perms=$(stat -f '%SHp' $mountpoint)
if [ $owner = $serveruser ]; then
case $perms in
?w?)
echo >&2 "==> OK: \"$mountpoint\" owned and writable by $owner"
return 0
;;
*)
echo >&2 "${ME}: FAIL \"$mountpoint\" is owned but not" \
"writable by $owner"
return 1
;;
esac
fi
group=$(stat -f '%Sg' $mountpoint)
perms=$(stat -f '%SMp' $mountpoint)
backupgroups=$(id -Gn $serveruser)
for bgrp in $backupgroups; do
if [ $group = $bgrp ]; then
case $perms in
?w?)
echo >&2 "==> OK: \"$mountpoint\" is writable by group" \
"$group which $user belongs to"
return 0
;;
*)
echo >&2 "${ME}: FAIL \"$mountpoint\" has negative" \
"permissions for group $group which $user is a" \
"member of"
return 1
;;
esac
fi
done
# If we've got to here, either $mountpoint is world writable, or
# $serveruser has no write permissions. Neither of which is
# acceptable.
echo >&2 "${ME}: FAIL \"$mountpoint\" is neither owned and writable" \
"by $serveruser, nor does $serveruser belong to a group" \
"with write permissions on it."
return 1
}
# Check zfs has appropriate actions allowed to user for server-side use
check_zfs_server_actions() {
local zfs="$1"
local serveruser="$2"
local allow
local allowedflags=0
allow=$( zfs allow $zfs | grep "user $serveruser" | \
sed -e 's/^.* //' | tr ',\n' ' ' )
for a in $allow ; do
case $a in
create)
allowedflags=$(($allowedflags + 1))
;;
destroy)
allowedflags=$(($allowedflags + 10))
;;
mount)
allowedflags=$(($allowedflags + 100))
;;
receive)
allowedflags=$(($allowedflags + 1000))
;;
snapshot)
allowedflags=$(($allowedflags + 10000))
;;
*) # Extra actions -- warning
echo >&2 "$ME: Notice: extra action \"$a\" allowed" \
"to $serveruser on $zfs"
;;
esac
done
if [ $allowedflags -ne 11111 ]; then
echo >&2 "$ME: FAIL Missing one or more required ZFS" \
"actions from \"create,destroy,mount,receive,snapshot\"" \
"for $serveruser on $zfs"
return 1
fi
echo >&2 "==> OK: user $serveruser is allowed ZFS actions \"$allow\"" \
"on $zfs"
}
# Check for the existence of the local filesystem that backups will be
# written to, that it has the correct option settings and that it has
# the correct ZFS actions allowed for the backup user.
check_server_setup_for_client() {
local clienthost="$1"
local serveruser="$2"
local zfs
local mountpoint
server_local_storage zfs "$clienthost"
zfs_to_path mountpoint $zfs
echo >&2 "==> OK: backup storage \"$mountpoint\" exists"
check_zfs_server_actions $zfs $serveruser
check_access $serveruser $mountpoint
return 0
}
# The specific zfs for holding this client+filesystem backups -- will
# not exist before a 'full' is run (which is OK). If it exists, test
# that the allowed ZFS actions are appropriate and that it has
# snapshots with names in the expected pattern: ie. that it is in use
# for backups.
check_server_setup_for_filesystem() {
local clienthost="$1"
local serveruser="$2"
local filesystem="$3" # what's backed up on the client
local zfs
local zfs_state
local mountpoint # where it's backed up on the server
server_local_storage zfs $clienthost $filesystem
# Does the ZFS exist at all?
zfs_state=$( zfs list -H -o name -t filesystem $zfs 2>/dev/null )
if [ -z $zfs_state ]; then
echo >&2 "==> OK: Ready for initial full backup"
return 0
fi
zfs_to_path mountpoint $zfs
echo >&2 "==> OK: zfs $zfs exists and is mounted as $mountpoint"
# The ZFS exists -- so is it setup for use for backups? Check for
# existence of snapshots matching our naming convention
get_zfs_objects zfs_state 'snapshot' $zfs
if [ -z "$zfs_state" ]; then
echo >&2 "==> FAIL: zfs $zfs exists but does not contain any" \
"previous full or incremental backup."
echo >&2 "==> FAIL: zfs would be overwritten by backups." \
"Please move it out of the way."
return 1
fi
# It's being used for backups. Does it have the correct ZFS
# allowed actions settings? Ideally, these should be inherited
# from the parent ZFS, but we don't check for that.
check_zfs_server_actions $zfs $serveruser
}
# Check settings -- require the $BACKUPROOT/$clienthost zfs to exist
# and have the right allowed actions to be inherited by backed-up
# filesystems. Also require $BACKUPROOT/$clienthost to be mounted
# read/write by the current user (assumed to be the local userid that
# will run backups) -- specifically not root.
#
# Test for the existence of the filesystem dependent child zfs -- warn
# about needing to do a 'full' if this doesn't exist, otherwise list
# pre-existing backups.
#
# Test for ssh based connectivity to the client and that the
# filesystem on the client has the necessary actions allowed to the
# backup user.
server_check() {
local clienthost="${1:?${ME}: Need a hostname to backup}"
local clientuser="${2:?${ME}: Need a username to run as on the client}"
local filesystem
echo >&2 "==> Checking SERVER setup:"
check_server_setup_for_client $clienthost $SERVERUSER
while read filesystem ; do
echo >&2 "==> Checking FILESYSTEM $filesystem:"
check_server_setup_for_filesystem $clienthost $SERVERUSER $filesystem
on_client $clienthost $clientuser check \
$option_d $option_v -u $clientuser -F $filesystem
done
}
# Print out what needs to be done as preparation for backing up a new
# filesystem on a client machine by setting the required allowed
# actions. Needs to be run as root on the client.
generate_setup() {
local clienthost="${1:?${ME}: Need a client hostname to backup}"
local clientuser="${2:?${ME}: Need a user to run the backups as on $clienthost}"
local fslist="${3:?Need a list of filesystems to backup on $clienthost}"
local fs
local zfs
local backupserverip
local zfs_backup_pubkey
local scriptheader
readonly dashes='----------------------------------------------------------------'
cat >&2 <<EOF
$ME:
Save the following commands to a file and run them as root on
this (ie the backup) server. Check carefully that these commands
do what you expect and make sense.
$dashes
EOF
# Script header
scriptheader='#!/bin/sh -e
'
# Test for the existence of the BACKUP key; generate it if needed.
if [ ! -f ${BACKUPKEY} ]; then
cat <<EOF
$scriptheader
# Generate SSH key to use for backups. This key needs to be owned by
# and stored under the home directory of the user -- $SERVERUSER --
# who will run the backups. If should not have a a passphrase. It
# should only be used for this backup script. We suggest the ed25519 key
# type for speed, but you can substitute any of the available types.
ssh-keygen -t ed25519 -N '' -C $ME -f $BACKUPKEY
chown $SERVERUSER $BACKUPKEY
EOF
scriptheader=
fi
# Create the top-level ${ZPOOL}/backup zfs if needed. This mostly
# exists to inherit stuff from. Set some default properties that
# everything else will inherit.
server_local_storage zfs
if ! zfs list -H -t filesystem $zfs >/dev/null 2>&1 ; then
cat <<EOF
$scriptheader
# Create the top level ZFS -- all storage for backed-up hosts will be
# children of this, and all backups will be grandchidren. This will
# not store any data itself and need not be mounted: it exists only so
# that the other ZFSes can inherit properties from it.
#
# Note 1: We're using the sha256 checksum rather than the default
# fletcher4 checksum so that we can send deduplicated streams reliably
# (having multiple copies of backups in different locations is
# generally a good idea). It is also advisable if dedup is enabled on
# the ZFS in general.
#
# Note 2: Speaking of enabling dedup: this has significant memory
# requirements and tends to slow machines to a crawl. Don't enable
# unless you have huge quantities of ram.
#
# Note 3: Assuming these ZFSes are used in a typical backup pattern:
# ie. regular potentially large writes with only occasional need to
# read the data back, then there's no point caching any of the data in
# ARC or L2ARC. Indeed, the volumes of data involved in backups can
# cause significant memory pressure on the backup server. So set the
# primary and secondary cache strategies to 'metadata' only.
zfs create -o compression=lz4 -o atime=off -o exec=off -o setuid=off \\
-o canmount=off -o dedup=off -o checksum=sha256 \\
-o primarycache=metadata -o secondarycache=metadata \\
-o mountpoint=${BACKUPROOT} ${zfs}
EOF
scriptheader=
fi
# Create the per-host zfs and make it writable by the server backup user
server_local_storage zfs $clienthost
if ! zfs list -H -t filesystem $zfs >/dev/null 2>&1 ; then
cat <<EOF
$scriptheader
# Create the per-host ZFS for $clienthost -- this should be mounted
# and read-write by the user the backups run as server-side and
# preferably not accessible by any other user account.
zfs create -o canmount=on ${zfs}
chown $SERVERUSER ${BACKUPROOT}/$clienthost
chmod 0700 ${BACKUPROOT}/$clienthost
EOF
scriptheader=
fi
# Check that the backup user is allowed the required actions on
# ${ZPOOL}/backup/$clienthost and add them if necessary
if ! check_zfs_server_actions $zfs $SERVERUSER >/dev/null 2>&1 ; then
cat <<EOF
$scriptheader
# Allow the required actions 'create,destroy,mount,receive,snapshot'
# for the backup user $SERVERUSER to be able to receive snapshots and
# generally manage the local storage
zfs allow $SERVERUSER create,destroy,mount,receive,snapshot $zfs
EOF
scriptheader=
fi
echo >&2 $dashes
### Client-side setup
if [ ! -f ${BACKUPKEY} ]; then
cat >&2 <<EOF
${ME}:
Please generate the ssh key ${BACKUPKEY}
as shown above, and then rerun this command to get the actions
to setup a client machine.
$dashes
EOF
return 0
fi
zfs_backup_pubkey=$( cat ${BACKUPKEY}.pub )
# This is a gross hack. Return the IP of the interface that has
# had most outgoing trafic. No guarantees that this is anything
# like correct.
backupserverip=$( ( netstat -i -n -f inet ; netstat -i -n -f inet6 ) | \
sort -rn -k 8 | head -1 | cut -w -f 4)
cat >&2 <<EOF
$ME:
Save the following commands to a file and run them as root on
$clienthost
Check this script carefully as we cannot guarantee it will be
correct. You may need to adapt it if you are not using the usual
layout for SSH related files, and you may need to correct the IP
number autodetected for the backup server.
$dashes
EOF
cat <<EOF
#!/bin/sh -e
# SSH authorized_keys. Create directory if necessary
: \${SSH_DIR:=~${clientuser}/.ssh}
if [ ! -d \$SSH_DIR ]; then
mkdir -m 700 -p \$SSH_DIR
fi
# Backup any existing authorized_keys file
: \${AUTHORIZED_KEYS:=\$SSH_DIR/authorized_keys}
if [ -f \$AUTHORIZED_KEYS ]; then
cp -p \$AUTHORIZED_KEYS \${AUTHORIZED_KEYS}.bak
fi
# Add the zfs-backup public key, with needed constraints NOTE: verify
# that the IP number here corresponds to what your backup server would
# use to connect to the client
if ! grep -qF $COMMAND \${AUTHORIZED_KEYS}; then
echo "from=\"$backupserverip\",command=\"$COMMAND __client\",no-agent-forwarding,no-pty,no-X11-forwarding,no-port-forwarding $zfs_backup_pubkey" >> \$AUTHORIZED_KEYS
chown -R ${clientuser} \$SSH_DIR
fi
# Set allowed actions on the ZFSes to be backed-up
#
# Note: the ${enable_prop} property will be inherited by all child
# ZFSes of those listed here (in the usual way that ZFS properties are
# inherited). To backup a whole hierarchy of ZFSes you only need to
# mark the top level ZFS. To omit some ZFSes from such a hierarchy
# set the property as ${enable_prop}=no for them.
IFS=$( printf " \t:\n" )
for fs in $fslist ; do
: \${ZFS:=\$(zfs list -H -t filesystem -o name \$fs)}
zfs allow $clientuser bookmark,destroy,mount,send,snapshot \$ZFS
# Mark the ZFS as being backed-up by this script
zfs set ${enable_prop}=yes \$ZFS
done
EOF
echo $dashes 2>&1
}
# Check for correct allowed actions on the client filesystem to be
# backed-up
client_check() {
local clientuser="${1:?${ME}: Need a username to run backups as}"
local filesystem="${2:?${ME}: Need a filesystem to be backed up}"
local zfs
local mp
local allow
local allowedflags=0
path_to_zfs zfs $filesystem
mp=$( zfs list -o mountpoint -H $zfs )
if [ $mp != $filesystem ]; then
echo >&2 "${ME}: FAIL filesystem \"$filesystem\" is not a" \
"mount point"
exit 1
else
echo >&2 "--> OK: client filesystem \"$filesystem\" exists"
fi
allow=$( zfs allow $zfs | grep "user $clientuser" | \
sed -e 's/^.* //' | tr ',\n' ' ' )
for a in $allow ; do
case $a in
bookmark)
allowedflags=$(($allowedflags + 1))
;;
destroy)
allowedflags=$(($allowedflags + 10))
;;
mount)
allowedflags=$(($allowedflags + 100))
;;
send)
allowedflags=$(($allowedflags + 1000))
;;
snapshot)
allowedflags=$(($allowedflags + 10000))
;;
*) # Extra allowed ZFS actions...
echo >&2 "$ME: Warning: extra ZFS action \"$a\" allowed" \
"to $clientuser on $zfs"
;;
esac
done
if [ $allowedflags -ne 11111 ]; then
echo >&2 "$ME: FAIL Missing required allowed ZFS actions for" \
"$clientuser on $zfs"
exit 1
fi
echo >&2 "--> OK: user $clientuser is allowed ZFS actions \"$allow\"" \
"on $zfs"
return 0
}
# On the client: send a snapshot of a filesystem to the backup server.
# If the send doesn't succeed, destroy the snapshot.
send_snapshot() {
local zfs="$1"
local previous_snapshot="$2"
local this_snapshot="$3"
if [ -z $option_n ]; then
runv zfs send -i $previous_snapshot "$zfs@$this_snapshot" || \
runv zfs destroy "$zfs@$this_snapshot"
fi
}
# Initial backup -- send the whole filesystem snapshot to the backup
# server This should only ever happen one time, otherwise it will wipe
# out the snapshot history on the backup server. If the send doesn't
# succeed, destroy the snapshot.
send_zfs() {
local zfs="$1"
local snapname="$2"
runv zfs send $option_n $option_v "$zfs@$snapname" || \
runv zfs destroy $option_n "$zfs@$snapname"
}
# (on the backup server) Receive a filesystem or an incremental update
# stream -- stored in a tree beneath /$BACKUPROOT/$hostname/
#
# If we're receiving a filesystem, this creates
# $ZPOOL$BACKUPROOT/$hostname/$fs (where $fs is derived from
# $filesystem) and will inherit properties from
# $ZPOOL$BACKUPROOT/$hostname
receive_stream() {
local localstorage="$1"
runv zfs receive $option_n $option_v -F $localstorage
}
# Run the client side part of the backup
client_backup() {
local filesystem="${1:?${ME}: Need a filesystem to backup}"
local prevbackuptag="${2:?${ME}: Need a previous backup to create a delta from}"
local snapname
local prevbackup
local zfs
generate_snapname snapname
path_to_zfs zfs $filesystem
get_prev_backup_by_tag prevbackup $zfs $prevbackuptag
create_snapshot $zfs $snapname && \
send_snapshot $zfs $prevbackup $snapname && \
delete_snapshot $zfs $( get_tag_from $prevbackup )
}
# Find the tag of the most recent backup that is known both on the
# client and in our local store. Server-side this will always be a
# snapshot.
latest_common_backup() {
local localstorage="$1"
local clienthost="$2"
local clientuser="$3"
local filesystem="$4"
local clienttags
local serversnap
local prevbackuptag
clienttags=$(
on_client $clienthost $clientuser \
__list_tags $option_d -F $filesystem
)
for tag in $clienttags; do
get_prev_backup_by_tag serversnap $localstorage $tag
if [ $serversnap ]; then
prevbackuptag=$tag
break
fi
done
if [ -z $prevbackuptag ]; then
echo >&2 "${ME}: Fatal -- no previous backup of $filesystem exists." \
"Cannot generate delta"
exit 1
fi