You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

263 lines
8.3 KiB

  1. #!/bin/bash
  2. #:-
  3. . /etc/shlib
  4. #:-
  5. include common
  6. include parse
  7. include process
  8. depends shyaml lock
  9. [ "$UID" != "0" ] && echo "You must be root." && exit 1
  10. ##
  11. ## Here's an example crontab:
  12. ##
  13. ## SHELL=/bin/sh
  14. ## PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin
  15. ##
  16. ## 49 */2 * * * root mirror-dir -d core-05.0k.io:10023 -u rsync /etc /home /opt/apps 2>&1 | logger -t mirror-dir
  17. ##
  18. usage="usage: $exname -d DEST1 [-d DEST2 [...]] [-u USER] [DIR1 [DIR2 ...]]
  19. Preserve as much as possible the source structure, keeping hard-links, acl,
  20. exact numerical uids and gids, and being able to resume in very large files.
  21. Options:
  22. DIR1 ... DIRn
  23. Local directories that should be mirrored on destination(s).
  24. examples: /etc /home /var/backups
  25. If no directories are provided, the config file root
  26. entries will be used all as destination to copy.
  27. -d DESTn
  28. Can be repeated. Specifies host destination towards which
  29. files will be send. Note that you can specify port number after
  30. a colon and a bandwidth limit for rsync after a '/'.
  31. examples: -d liszt.musicalta:10022 -d 10.8.0.19/200
  32. -u USER (default: 'rsync')
  33. Local AND destination user to log as at both ends to transfer file.
  34. This local user need to have a NOPASSWD ssh login towards it's
  35. account on destination. This destination account should have
  36. full permissions access without passwd to write with rsync-server
  37. in the destination directory.
  38. -h STORE (default is taken of the hostname file)
  39. Set the destination store, this is the name of the directory where
  40. the files will all directories will be copied. Beware ! if 2 hosts
  41. use the same store, this means they'll conflictingly update the
  42. same destination directory. Only use this if you know what you
  43. are doing.
  44. "
  45. dests=()
  46. source_dirs=()
  47. hostname=
  48. while [ "$#" != 0 ]; do
  49. case "$1" in
  50. "-d")
  51. dests+=("$2")
  52. shift
  53. ;;
  54. "-h")
  55. hostname="$2"
  56. shift
  57. ;;
  58. "-u")
  59. user="$2"
  60. shift
  61. ;;
  62. *)
  63. source_dirs+=("$1")
  64. ;;
  65. esac
  66. shift
  67. done
  68. if test -z "$hostname"; then
  69. hostname=$(hostname)
  70. fi
  71. if test -z "$hostname"; then
  72. die "Couldn't figure a valid hostname. Please specify one with \`\`-h STORENAME\`\`."
  73. fi
  74. user=${user:-rsync}
  75. dest_path=/var/mirror/$hostname
  76. config_file="/etc/$exname/config.yml"
  77. if [ "${#source_dirs[@]}" == 0 ]; then
  78. if [ -e "$config_file" ]; then
  79. echo "No source provided on command line.. "
  80. echo " ..so reading '$config_file' for default sources..."
  81. source_dirs=($(eval echo $(shyaml get-values default.sources < "$config_file")))
  82. fi
  83. if [ "${#source_dirs[@]}" == 0 ]; then
  84. err "You must specify at least one source directory to mirror" \
  85. "on command line (or in a config file)."
  86. print_usage
  87. exit 1
  88. fi
  89. fi
  90. echo "Sources directories are: ${source_dirs[@]}"
  91. if [ "${#dests[@]}" == 0 ]; then
  92. err "You must specify at least a destination."
  93. print_usage
  94. exit 1
  95. fi
  96. state_dir=/var/run/mirror-dir
  97. mkdir -p "$state_dir"
  98. rsync_options=(${RSYNC_OPTIONS:-})
  99. ssh_options=(${SSH_OPTIONS:-})
  100. get_exclude_patterns() {
  101. local dir="$1"
  102. [ -e "$config_file" ] || return
  103. cat "$config_file" | shyaml get-values-0 "${dir//.\\./}.exclude" 2>/dev/null
  104. }
  105. append_trim() {
  106. local f="$1"
  107. shift
  108. e "$(date --rfc-3339=s) $*"$'\n' >> "$f" &&
  109. tail -n 5000 "$f" > "$f".tmp &&
  110. mv "$f"{.tmp,}
  111. }
  112. for dest in "${dests[@]}"; do
  113. dest_rsync_options=("${rsync_options[@]}")
  114. if [[ "$dest" == *"/"* ]]; then
  115. dest_rsync_options+=("--bwlimit" "${dest##*/}")
  116. dest="${dest%/*}"
  117. fi
  118. dest_for_session="$dest"
  119. for d in "${source_dirs[@]}"; do
  120. current_rsync_options=("${dest_rsync_options[@]}")
  121. session_id="$(echo "${dest_for_session}$d" | md5_compat)"
  122. session_id="${session_id:1:8}"
  123. if [[ "$dest" == *":"* ]]; then
  124. ssh_options+=("-p" "${dest#*:}")
  125. dest="${dest%%:*}"
  126. fi
  127. dirpath="$(dirname "$d")"
  128. if [ "$dirpath" == "/" ]; then
  129. dir="/$(basename "$d")"
  130. else
  131. dir="$dirpath/$(basename "$d")"
  132. fi
  133. [ -d "$dir" ] || {
  134. warn "ignoring '$dir' as it is not existing."
  135. continue
  136. }
  137. lock_label=$exname-$hostname-${session_id}
  138. tmp_exclude_patterns=/tmp/${lock_label}.exclude_patterns.tmp
  139. ## Adding the base of the dir if required... seems necessary with
  140. ## the rsync option that replicate the full path.
  141. has_exclude_pattern=
  142. while read-0 exclude_dir; do
  143. if [ -z "$has_exclude_pattern" ]; then
  144. echo "Adding exclude patterns for source '$dir':" >&2
  145. has_exclude_pattern=1
  146. fi
  147. if [[ "$exclude_dir" == "/"* ]]; then
  148. exclude_dir="$dir${exclude_dir}"
  149. fi
  150. echo " - $exclude_dir" >&2
  151. p0 "$exclude_dir"
  152. done < <(get_exclude_patterns "$dir") > "$tmp_exclude_patterns"
  153. if [ -n "$has_exclude_pattern" ]; then
  154. current_rsync_options+=("-0" "--exclude-from"="$tmp_exclude_patterns")
  155. else
  156. echo "No exclude patterns for '$dir'."
  157. fi
  158. echo ---------------------------------
  159. echo "Starting rsync: $d -> $dest ($(date))"
  160. echo nice -n 15 \
  161. rsync "${current_rsync_options[@]}" -azvARH \
  162. -e "'sudo -u $user ssh ${ssh_options[*]}'" \
  163. --delete --delete-excluded \
  164. --partial --partial-dir .rsync-partial \
  165. --numeric-ids "$dir/" "$user@$dest":"$dest_path"
  166. start="$SECONDS"
  167. retry=1
  168. errlvls=()
  169. while true; do
  170. lock "$lock_label" -v -D -k -- \
  171. nice -n 15 \
  172. rsync "${current_rsync_options[@]}" -azvARH \
  173. -e "sudo -u $user ssh ${ssh_options[*]}" \
  174. --delete --delete-excluded \
  175. --partial --partial-dir .rsync-partial \
  176. --numeric-ids "$dir/" "$user@$dest":"$dest_path"
  177. errlvl="$?"
  178. case "$errlvl" in
  179. 20) ## Received SIGUSR1, SIGINTT
  180. echo "!! Rsync received SIGUSR1 or SIGINT."
  181. echo " .. Full interruption while $d -> $dest and after $((SECONDS - start))s"
  182. append_trim "${state_dir}/${session_id}-fail" \
  183. "$dest $d $((SECONDS - start)) signal SIGUSR1, SIGINT or SIGHUP"
  184. break 2
  185. ;;
  186. 137|143) ## killed SIGKILL, SIGTERM, SIGINT
  187. echo "!! Rsync received $(kill -l "$errlvl")"
  188. echo " .. Full interruption while $d -> $dest and after $((SECONDS - start))s"
  189. append_trim "${state_dir}/${session_id}-fail" \
  190. "$dest $d $((SECONDS - start)) signal: $(kill -l "$errlvl")"
  191. break 2
  192. ;;
  193. 0)
  194. echo "Rsync finished with success $d -> $dest in $((SECONDS - start))s"
  195. append_trim "${state_dir}/${session_id}-success" \
  196. "$dest $d $((SECONDS - start)) OK"
  197. break
  198. ;;
  199. *)
  200. errlvls+=("$errlvl")
  201. echo "!! Rsync failed with an errorlevel $errlvl after $((SECONDS - start))s since start."
  202. if [ "$retry" -lt 3 ]; then
  203. echo "!! Triggering a retry ($((++retry))/3)"
  204. continue
  205. else
  206. echo "!! Tried 3 times, bailing out."
  207. echo " .. interruption of $d -> $dest after $((SECONDS - start))s"
  208. append_trim "${state_dir}/${session_id}-fail" \
  209. "$dest $d $((SECONDS - start))" \
  210. "Failed after 3 retries (errorlevels: ${errlvls[@]})"
  211. break
  212. fi
  213. ;;
  214. esac
  215. done
  216. if [ -n "$has_exclude_pattern" ]; then
  217. rm -fv "$tmp_exclude_patterns"
  218. fi
  219. done
  220. done