Guest User

Untitled

a guest
Feb 25th, 2018
71
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 11.28 KB | None | 0 0
  1. #!/bin/sh
  2. # vim: ft=sh
  3. PROG=$(basename $0)
  4. BINDIR=$(dirname $(readlink -f $0))
  5. LOCKFILE="$BINDIR/$PROG.lck"
  6.  
  7. DESCRIPTION="[SZ]imple ZFS Replicator"
  8. QUICKGUIDE="$PROG - $DESCRIPTION
  9. [QUICK GUIDE]
  10. NOTE: Currently this program supports push-based replication only.
  11.  
  12. Assume you want to replicate zroot/data and all descendant datasets on
  13. the sending host to the receiving host's backup/data such like:
  14.  
  15. [sender(s)] [receiver(r)]
  16. zroot/data --> backup/data
  17. zroot/data/doc --> backup/data/doc
  18. zroot/data/photo --> backup/data/photo
  19. zroot/data/video --> backup/data/video
  20.  
  21. You can achieve this by taking the following steps.
  22.  
  23. 1. Create a dedicated user on both sending and receiving hosts.
  24. It's optional but recommended in production environment.
  25.  
  26. 2. Generate a SSH public key for the sending user.
  27. Then put it into the receiving user's ~/.ssh/authorized_keys.
  28.  
  29. 3. Create a base dataset with -u (unmounted) on the receiving host.
  30. If you are using property-based backup solution such as zfstools,
  31. you might want to explicitly disable it on the dataset.
  32.  
  33. r$ sudo zfs create -u backup/data
  34. r$ sudo zfs set com.sun:auto-snapshot=false backup/data
  35.  
  36. 4. Grant appropriate permissions on the source/destination dataset to
  37. the sending and receiving users.
  38.  
  39. s$ sudo zfs allow -u sender send,snapshot,hold zroot/data
  40. r$ sudo zfs allow -u receiver receive,create,mount,mountpoint,compression,recordsize backup/data
  41.  
  42. 5. Take an initial snapshot on the sending host.
  43. Then send it as a full replication stream to the receiving host.
  44.  
  45. s$ zfs snapshot -r zroot/data@first
  46. s$ zmplrepl -R zroot/data r:backup
  47.  
  48. 6. Afterwards, run the same command without -R again to synchronize.
  49.  
  50. s$ ./zmplrepl zroot/data r:backup
  51.  
  52. 7. If you want to mount the replicated datasets on the receiving host,
  53. run the following command.
  54.  
  55. r$ zfs list -o name -H -r backup/data | sudo xargs -n1 zfs mount
  56.  
  57. Optionally, you can make the datasets readonly to keep someone
  58. from accidentally changing their contents.
  59.  
  60. r$ sudo zfs set readonly=on backup/data
  61. " # END-QUOTE
  62.  
  63. _lock() {
  64. if ! ln -s $$ $LOCKFILE > /dev/null 2>&1; then
  65. return 1
  66. else
  67. trap '_unlock; exit 1' 1 2 3 11 15
  68. return 0
  69. fi
  70. }
  71.  
  72. _unlock() {
  73. rm -f $LOCKFILE
  74. rm -f $TF_SRCSNAP $TF_DSTSNAP
  75. trap 1 2 3 11 15
  76. }
  77.  
  78. _mktemp() {
  79. local name=$1
  80. local tmpfile
  81. if [ -n "$name" ]; then
  82. mktemp -q -t "$name"
  83. fi
  84. }
  85.  
  86. catv() {
  87. if [ $VERBOSE -eq 1 ]; then
  88. cat
  89. else
  90. cat >/dev/null
  91. fi
  92. }
  93.  
  94. echov() {
  95. if [ $VERBOSE -eq 1 ]; then
  96. echo "$@"
  97. fi
  98. }
  99.  
  100. echoerr() {
  101. echo "$@" >&2
  102. }
  103.  
  104. _msg() {
  105. local msg="$1"
  106. if [ -n "$msg" ]; then
  107. echoerr "$msg"
  108. fi
  109. }
  110.  
  111. err_exit() {
  112. _msg "$1"
  113. _unlock
  114. exit 1
  115. }
  116.  
  117. usage_exit() {
  118. _msg "$1"
  119. echoerr "$USAGE"
  120. err_exit
  121. }
  122.  
  123. guide_exit() {
  124. _msg "$1"
  125. echoerr "$QUICKGUIDE"
  126. err_exit
  127. }
  128.  
  129. echo_result() {
  130. local status=$?
  131. if [ -z "$DRYRUN" ]; then
  132. if [ $status = 0 ]; then
  133. echo "Info: SUCCESS"
  134. else
  135. echoerr "Error: Status=$status"
  136. fi
  137. fi
  138. }
  139.  
  140. run_command() {
  141. local host=$1
  142. shift
  143. if [ -n "$host" ]; then
  144. ssh $host "$@"
  145. else
  146. "$@"
  147. fi
  148. }
  149.  
  150. list_ds() {
  151. local host=$1
  152. local ds=$2
  153. local recursive=$3
  154. if [ -n "$ds" ]; then
  155. if [ $recursive -eq 1 ]; then
  156. run_command "$host" zfs list -o name -H -t filesystem -r $ds 2>/dev/null
  157. else
  158. echo $ds
  159. fi
  160. fi
  161. }
  162.  
  163. list_snap() {
  164. local host=$1
  165. local ds=$2
  166. if [ -n "$ds" ]; then
  167. run_command "$host" zfs list -o name -H -t snapshot -r $ds | fgrep $ds@ | sed -e 's/.*@//'
  168. fi
  169. }
  170.  
  171. zfs_send() {
  172. local host=$1
  173. shift
  174. run_command "$host" zfs send "$@"
  175. }
  176.  
  177. zfs_recv() {
  178. local host=$1
  179. shift
  180. if [ -n "$DRYRUN" ]; then
  181. cat
  182. else
  183. run_command "$host" zfs receive "$@"
  184. fi
  185. }
  186.  
  187. get_full_dstds() {
  188. local srcds=$1
  189. local dstds=$2
  190. local recursive=$3
  191. local replication=$4
  192. local full_dstds
  193.  
  194. if [ $recursive -eq 1 -o $replication -eq 1 ]; then
  195. # If -s is not specified, dstDs is a base dataset.
  196. full_dstds="$dstds/${srcds#*/}"
  197. else
  198. # If -s is specified, dstDs is already a full path dataset.
  199. full_dstds="$dstds"
  200. fi
  201. echo "$full_dstds"
  202. }
  203.  
  204. get_snap_ts() {
  205. local host=$1
  206. local fullsnap=$2
  207. if [ -n "$fullsnap" ]; then
  208. run_command "$host" zfs get -p -o value -H creation $fullsnap 2>/dev/null
  209. fi
  210. }
  211.  
  212. zfs_send_recv() {
  213. local srchost=$1
  214. local srcds=$2
  215. local fromsnap=$3
  216. local tosnap=$4
  217. local dsthost=$5
  218. local dstds=$6
  219. local recursive=$7
  220. local replication=$8
  221. local fullstream=$9
  222.  
  223. local sendopts recvopts
  224. local latest_common_snap latest_snap_on_dst latest_snap_on_src
  225. local oldest_snap_on_dst oldest_snap_on_src
  226.  
  227. local full_dstds=$(get_full_dstds "$srcds" "$dstds" "$recursive" "$replication")
  228. echo
  229. echo "--------------------"
  230. echo "Info: src=[$srchost${srchost:+:}$srcds]"
  231. echo -n "Info: dst=[$dsthost${dsthost:+:}$full_dstds]"
  232. if [ "$dstds" != "$full_dstds" ]; then
  233. echo " <- [$dsthost${dsthost:+:}$dstds]"
  234. else
  235. echo
  236. fi
  237. test -n "$DRYRUN" && echo "Info: $DRYRUN"
  238. list_snap "$dsthost" "$full_dstds" > "$TF_DSTSNAP" 2>/dev/null
  239. list_snap "$srchost" "$srcds" > "$TF_SRCSNAP" 2>/dev/null
  240.  
  241. latest_common_snap=$(fgrep -x -f $TF_SRCSNAP $TF_DSTSNAP | tail -n 1)
  242. latest_snap_on_dst=$(tail -n 1 $TF_DSTSNAP)
  243. latest_snap_on_src=$(tail -n 1 $TF_SRCSNAP)
  244. oldest_snap_on_dst=$(head -n 1 $TF_DSTSNAP)
  245. oldest_snap_on_src=$(head -n 1 $TF_SRCSNAP)
  246.  
  247. if [ -z "$oldest_snap_on_dst" ]; then
  248. echov "Info: No dataset on the receiver. Turn on -F (full)."
  249. fullstream=1
  250. elif [ -z "$latest_common_snap" ]; then
  251. echov "Info: No common snapshot. Turn on -F (full)."
  252. fullstream=1
  253. fi
  254.  
  255. if [ -n "$fromsnap" ]; then
  256. latest_common_snap="$fromsnap"
  257. fi
  258. if [ -n "$tosnap" ]; then
  259. latest_snap_on_src="$tosnap"
  260. fi
  261.  
  262. sendopts=""
  263. recvopts="-Fuvs"
  264. if [ $replication -eq 1 ]; then
  265. sendopts="${sendopts}${sendopts:+ }-R"
  266. recvopts="${recvopts}${recvopts:+ }-d"
  267. elif [ $recursive -eq 1 ]; then
  268. recvopts="${recvopts}${recvopts:+ }-d"
  269. fi
  270. if [ -n "$DRYRUN" ]; then
  271. sendopts="${sendopts}${sendopts:+ }-nv"
  272. fi
  273. if [ $PRESERVEPROP -eq 1 ]; then
  274. sendopts="${sendopts}${sendopts:+ }-p"
  275. fi
  276.  
  277. if [ $fullstream -eq 1 ]; then
  278. if [ -n "$latest_snap_on_src" ]; then
  279. echo "Info: full."
  280. echov "Info: ${srchost:+ssh }${srchost}${srchost:+ }zfs send $sendopts${sendopts:+ }$srcds@$latest_snap_on_src | ${dsthost:+ssh }${dsthost}${dsthost:+ }zfs recv $recvopts{$recvopts:+ }$dstds"
  281. echov "==="
  282. zfs_send "$srchost" $sendopts $srcds@$latest_snap_on_src | zfs_recv "$dsthost" $recvopts $dstds | sed 's/^/% /' 2>&1 | catv
  283. echo_result
  284. else
  285. echoerr "Error: No snapshot for full stream on the sender."
  286. fi
  287. else
  288. if [ -n "$latest_common_snap" ]; then
  289. if [ $SPARSEMODE -eq 1 ]; then
  290. sendopts="${sendopts}${sendopts:+ }-i"
  291. incrtype="incremental (-i)"
  292. else
  293. sendopts="${sendopts}${sendopts:+ }-I"
  294. incrtype="incremental (-I)"
  295. fi
  296. common_snap_ts=$(get_snap_ts "$srchost" $srcds@$latest_common_snap)
  297. latest_snap_ts=$(get_snap_ts "$srchost" $srcds@$latest_snap_on_src)
  298.  
  299. echov "Info: Latest common snap = $latest_common_snap ($common_snap_ts)"
  300. echov "Info: Latest snap on src = $latest_snap_on_src ($latest_snap_ts)"
  301. echov "Info: Latest snap on dst = $latest_snap_on_dst"
  302.  
  303.  
  304. if [ $common_snap_ts -lt $latest_snap_ts ]; then
  305. echo "Info: common($common_snap_ts) < latest($latest_snap_ts) -> $incrtype"
  306. echov "Info: ${srchost:+ssh }${srchost}${srchost:+ }zfs send $sendopts${sendopts:+ }$latest_common_snap $srcds@$latest_snap_on_src | ${dsthost:+ssh }${dsthost}${dsthost:+ }zfs recv $recvopts${recvopts:+ }$dstds"
  307. echov "==="
  308. zfs_send "$srchost" $sendopts $latest_common_snap $srcds@$latest_snap_on_src | zfs_recv "$dsthost" $recvopts $dstds | sed 's/^/% /' 2>&1 | catv
  309. echo_result
  310.  
  311. elif [ $common_snap_ts -eq $latest_snap_ts ]; then
  312. echo "Info: common($common_snap_ts) = latest($latest_snap_ts) -> Already in sync."
  313. echo "Info: NOTHING TO DO"
  314.  
  315. else
  316. echoerr "Error: common($common_snap_ts) > latest($latest_snap_ts) -> Oops! Weird."
  317. fi
  318. else
  319. echoerr "Error: No common snapshot for incremental send."
  320. fi
  321. fi
  322. }
  323.  
  324. parse_target() {
  325. t_origtarget=$1
  326. if echo "$t_origtarget" | fgrep -q ':'; then
  327. t_hostname=${t_origtarget%%:*}
  328. t_fullpath=${t_origtarget#*:}
  329. else
  330. t_hostname=
  331. t_fullpath=${t_origtarget}
  332. fi
  333. if echo "$t_fullpath" | fgrep -q '@'; then
  334. t_dataset=${t_fullpath%%@*}
  335. t_snapshot=${t_fullpath#*@}
  336. else
  337. t_dataset=${t_fullpath}
  338. t_snapshot=
  339. fi
  340. }
  341.  
  342. USAGE="$PROG - $DESCRIPTION
  343. [USAGE]
  344. $PROG [-nvpiF] [-R] [-f fromSnap] srcDs[@toSnap] [host:]dstDs(base)
  345. $PROG [-nvpiF] -s [-f fromSnap] srcDs[@toSnap] [host:]dstDs
  346. $PROG [-gh]
  347.  
  348. -n: Dry-run (zfs send -nv).
  349. -v: Be verbose.
  350. -p: Preserve properties on sender (zfs send -p). -R implies -p.
  351. -i: Use sparse mode (zfs send -i) when sending incremental stream.
  352. -F: Force full stream.
  353. By default, $PROG sends incremental stream if both sender and
  354. receiver have a common snapshot, while $PROG sends full stream
  355. if there is no common snapshot for both sides.
  356. -R: Send full replication stream (zfs send -R) based on the srcDs.
  357. By default, $PROG sends indivudual stream for each dataset
  358. under the srcDs.
  359. -f: Specify a fromSnap for incremental stream.
  360. By default, the latest common snapshot for both sides are used.
  361. -s: Use one-to-one stream. Set dstDs to a absolute destination dataset
  362. path for the srcDs when using -s.
  363. Otherwise, dstDs should be the base dataset for replication.
  364. Here are some examples.
  365. $PROG -s zroot/data/a/b/c r:backup/c
  366. -> 'zroot/data/a/b/c' is replicated to r's 'backup/c'.
  367. $PROG zroot/data/x/y/z r:backup
  368. -> 'zroot/data/x/y/z' is replicated to r's 'backup/data/x/y/z'.
  369. -g: Show quick guide and exit.
  370. -h: Show this usage and exit.
  371. " # END-QUOTE
  372.  
  373. CMDLINE="$PROG $@"
  374. DRYRUN=
  375. VERBOSE=0
  376. PRESERVEPROP=0
  377. SPARSEMODE=0
  378. fullstream=0
  379. fromsnap=
  380. recursive=1
  381. replication=0
  382. while getopts "nvpiFf:sRgh" opt
  383. do
  384. case "$opt" in
  385. n) DRYRUN="DRYRUN" ;;
  386. v) VERBOSE=1 ;;
  387. p) PRESERVEPROP=1 ;;
  388. i) SPARSEMODE=1 ;;
  389. F) fullstream=1 ;;
  390. f) fromsnap="$OPTARG" ;;
  391. s) recursive=0 ;;
  392. R) replication=1 ; recursive=0 ;;
  393. g) guide_exit ;;
  394. h) usage_exit ;;
  395. *) usage_exit ;;
  396. esac
  397. done
  398. shift $(( $OPTIND - 1 ))
  399.  
  400. _srcds_tosnap=$1
  401. _target=$2
  402.  
  403. parse_target "$_srcds_tosnap"
  404. srchost=$t_hostname
  405. srcds=$t_dataset
  406. tosnap=$t_snapshot
  407.  
  408. parse_target "$_target"
  409. dsthost=$t_hostname
  410. dstds=$t_dataset
  411.  
  412. if [ -z "$srcds" -o -z "$dstds" ]; then
  413. usage_exit
  414. fi
  415.  
  416. #
  417. # main
  418. #
  419. if ! _lock; then
  420. err_exit "Error: Another process seems to be running."
  421. fi
  422.  
  423. TF_SRCSNAP=$(_mktemp srcsnap) && TF_DSTSNAP=$(_mktemp dstsnap)
  424. if [ $? -ne 0 ]; then
  425. err_exit "Error: Cannot create temporary files."
  426. fi
  427.  
  428. SRCDS_LIST=$(list_ds "$srchost" $srcds $recursive)
  429. if [ -z "$SRCDS_LIST" ]; then
  430. err_exit "Error: No such srcDs [$srcds]${srchost:+ on}${srchost}."
  431. fi
  432.  
  433. BEGINTS=$(date '+%s')
  434. HR_BEGINTS=$(date -j -f '%s' $BEGINTS '+%Y%m%d-%H%M%S')
  435.  
  436. echo "# BEGIN"
  437. echo "# ---------- ${PROG} ----------"
  438. echo "# started at ${HR_BEGINTS} ($BEGINTS)"
  439. echo "# ---------- ${PROG} ----------"
  440. echo "# CMDLINE=[$CMDLINE]"
  441. echo "#"
  442.  
  443. for each_srcds in $SRCDS_LIST; do
  444. zfs_send_recv "$srchost" "$each_srcds" "$fromsnap" "$tosnap" "$dsthost" "$dstds" $recursive $replication $fullstream
  445. done
  446.  
  447. ENDTS=$(date '+%s')
  448. HR_ENDTS=$(date -j -f '%s' $ENDTS '+%Y%m%d-%H%M%S')
  449.  
  450. echo
  451. echo "#"
  452. echo "# ---------- ${PROG} ----------"
  453. echo "# started at ${HR_BEGINTS} ($BEGINTS)"
  454. echo "# finished at ${HR_ENDTS} ($ENDTS)"
  455. echo "# total time = $(expr $ENDTS - $BEGINTS) secs."
  456. echo "# ---------- ${PROG} ----------"
  457. echo "# END"
  458.  
  459. _unlock
  460. exit 0
Add Comment
Please, Sign In to add comment