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.

844 lines
23 KiB

  1. #!/bin/bash
  2. . /etc/shlib
  3. include common
  4. include parse
  5. include cmdline
  6. include config
  7. include cache
  8. include fn
  9. include docker
  10. [[ "${BASH_SOURCE[0]}" != "${0}" ]] && SOURCED=true
  11. version=0.1
  12. desc='Install backup'
  13. help=""
  14. docker:running-container-projects() {
  15. :cache: scope=session
  16. docker ps --format '{{.Label "com.docker.compose.project"}}' | sort | uniq
  17. }
  18. decorator._mangle_fn docker:running-container-projects
  19. ssh:mk-private-key() {
  20. local host="$1" service_name="$2"
  21. (
  22. settmpdir VPS_TMPDIR
  23. ssh-keygen -t rsa -N "" -f "$VPS_TMPDIR/rsync_rsa" -C "$service_name@$host" >/dev/null
  24. cat "$VPS_TMPDIR/rsync_rsa"
  25. )
  26. }
  27. mailcow:has-images-running() {
  28. local images
  29. images=$(docker ps --format '{{.Image}}' | sort | uniq)
  30. [[ $'\n'"$images" == *$'\n'"mailcow/"* ]]
  31. }
  32. mailcow:has-container-project-mentionning-mailcow() {
  33. local projects
  34. projects=$(docker:running-container-projects) || return 1
  35. [[ $'\n'"$projects"$'\n' == *mailcow* ]]
  36. }
  37. mailcow:has-running-containers() {
  38. mailcow:has-images-running ||
  39. mailcow:has-container-project-mentionning-mailcow
  40. }
  41. mailcow:get-root() {
  42. :cache: scope=session
  43. local dir
  44. for dir in {/opt{,/apps},/root}/mailcow-dockerized; do
  45. [ -d "$dir" ] || continue
  46. [ -r "$dir/mailcow.conf" ] || continue
  47. echo "$dir"
  48. return 0
  49. done
  50. return 1
  51. }
  52. decorator._mangle_fn mailcow:get-root
  53. compose:get-compose-yml() {
  54. :cache: scope=session
  55. local path
  56. [ -e "/etc/compose/local.conf" ] && . "/etc/compose/local.conf"
  57. path=${DEFAULT_COMPOSE_FILE:-/etc/compose/compose.yml}
  58. [ -e "$path" ] || return 1
  59. echo "$path"
  60. }
  61. decorator._mangle_fn compose:get-compose-yml
  62. compose:has-container-project-myc() {
  63. local projects
  64. projects=$(docker:running-container-projects) || return 1
  65. [[ $'\n'"$projects"$'\n' == *$'\n'"myc"$'\n'* ]]
  66. }
  67. type:is-mailcow() {
  68. mailcow:get-root >/dev/null ||
  69. mailcow:has-running-containers
  70. }
  71. type:is-compose() {
  72. compose:get-compose-yml >/dev/null &&
  73. compose:has-container-project-myc
  74. }
  75. vps:get-type() {
  76. :cache: scope=session
  77. local fn
  78. for fn in $(declare -F | cut -f 3 -d " " | egrep "^type:is-"); do
  79. "$fn" && {
  80. echo "${fn#type:is-}"
  81. return 0
  82. }
  83. done
  84. return 1
  85. }
  86. decorator._mangle_fn vps:get-type
  87. mirror-dir:sources() {
  88. :cache: scope=session
  89. if ! shyaml get-values default.sources < /etc/mirror-dir/config.yml; then
  90. err "Couldn't query 'default.sources' in '/etc/mirror-dir/config.yml'."
  91. return 1
  92. fi
  93. }
  94. decorator._mangle_fn mirror-dir:sources
  95. mirror-dir:check-add() {
  96. local elt="$1" sources
  97. sources=$(mirror-dir:sources) || return 1
  98. if [[ $'\n'"$sources"$'\n' == *$'\n'"$elt"$'\n'* ]]; then
  99. info "Volume $elt already in sources"
  100. else
  101. Elt "Adding directory $elt"
  102. sed -i "/sources:/a\ - \"${elt}\"" \
  103. /etc/mirror-dir/config.yml
  104. Feedback || return 1
  105. fi
  106. }
  107. mirror-dir:check-add-vol() {
  108. local elt="$1"
  109. mirror-dir:check-add "/var/lib/docker/volumes/*_${elt}-*/_data"
  110. }
  111. ## The first colon is to prevent auto-export of function from shlib
  112. : ; bash-bug-5() { { cat; } < <(e) >/dev/null; ! cat "$1"; } && bash-bug-5 <(e) 2>/dev/null &&
  113. export BASH_BUG_5=1 && unset -f bash_bug_5
  114. wrap() {
  115. local label="$1" code="$2"
  116. shift 2
  117. export VERBOSE=1
  118. interpreter=/bin/bash
  119. if [ -n "$BASH_BUG_5" ]; then
  120. (
  121. settmpdir tmpdir
  122. fname=${label##*/}
  123. e "$code" > "$tmpdir/$fname" &&
  124. chmod +x "$tmpdir/$fname" &&
  125. Wrap -vsd "$label" -- "$interpreter" "$tmpdir/$fname" "$@"
  126. )
  127. else
  128. Wrap -vsd "$label" -- "$interpreter" <(e "$code") "$@"
  129. fi
  130. }
  131. ping_check() {
  132. #global ignore_ping_check
  133. local host="$1"
  134. ip=$(getent ahosts "$host" | egrep "^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+\s+" |
  135. head -n 1 | cut -f 1 -d " ") || return 1
  136. my_ip=$(curl -s myip.kal.fr)
  137. if [ "$ip" != "$my_ip" ]; then
  138. if [ -n "$ignore_ping_check" ]; then
  139. warn "IP of '$host' ($ip) doesn't match mine ($my_ip). Ignoring due to ``--ignore-ping-check`` option."
  140. else
  141. err "IP of '$host' ($ip) doesn't match mine ($my_ip). Use ``--ignore-ping-check`` to ignore check."
  142. return 1
  143. fi
  144. fi
  145. }
  146. mailcow:install-backup() {
  147. local BACKUP_SERVER="$1" ignore_ping_check="$2" mailcow_root DOMAIN
  148. ## find installation
  149. mailcow_root=$(mailcow:get-root) || {
  150. err "Couldn't find a valid mailcow root directory."
  151. return 1
  152. }
  153. ## check ok
  154. DOMAIN=$(cat "$mailcow_root/.env" | grep ^MAILCOW_HOSTNAME= | cut -f 2 -d =) || {
  155. err "Couldn't find MAILCOW_HOSTNAME in file \"$mailcow_root/.env\"."
  156. return 1
  157. }
  158. ping_check "$DOMAIN" || return 1
  159. MYSQL_ROOT_PASSWORD=$(cat "$mailcow_root/.env" | grep ^DBROOT= | cut -f 2 -d =) || {
  160. err "Couldn't find DBROOT in file \"$mailcow_root/.env\"."
  161. return 1
  162. }
  163. MYSQL_CONTAINER=${MYSQL_CONTAINER:-mailcowdockerized_mysql-mailcow_1}
  164. container_id=$(docker ps -f name="$MYSQL_CONTAINER" --format "{{.ID}}")
  165. if [ -z "$container_id" ]; then
  166. err "Couldn't find docker container named '$MYSQL_CONTAINER'."
  167. return 1
  168. fi
  169. export MYSQL_ROOT_PASSWORD
  170. export MYSQL_CONTAINER
  171. export BACKUP_SERVER
  172. export DOMAIN
  173. wrap "Install rsync-backup on host" "
  174. cd /srv/charm-store/rsync-backup
  175. bash ./hooks/install.d/60-install.sh
  176. " || return 1
  177. wrap "Mysql dump install" "
  178. cd /srv/charm-store/mariadb
  179. bash ./hooks/install.d/60-backup.sh
  180. " || return 1
  181. ## Using https://github.com/mailcow/mailcow-dockerized/blob/master/helper-scripts/backup_and_restore.sh
  182. for elt in "vmail{,-attachments-vol}" crypt redis rspamd postfix; do
  183. mirror-dir:check-add-vol "$elt" || return 1
  184. done
  185. mirror-dir:check-add "$mailcow_root" || return 1
  186. mirror-dir:check-add "/var/backups/mysql" || return 1
  187. mirror-dir:check-add "/etc" || return 1
  188. dest="$BACKUP_SERVER"
  189. dest="${dest%/*}"
  190. ssh_options=()
  191. if [[ "$dest" == *":"* ]]; then
  192. port="${dest##*:}"
  193. dest="${dest%%:*}"
  194. ssh_options=(-p "$port")
  195. else
  196. port=""
  197. dest="${dest%%:*}"
  198. fi
  199. info "You can run this following command from an host having admin access to $dest:"
  200. echo " (Or send it to a backup admin of $dest)" >&2
  201. echo "ssh ${ssh_options[@]} myadmin@$dest ssh-key add '$(cat /var/lib/rsync/.ssh/id_rsa.pub)'"
  202. }
  203. compose:has_domain() {
  204. local compose_file="$1" host="$2" name conf relation relation_value domain server_aliases
  205. while read-0 name conf ; do
  206. name=$(e "$name" | shyaml get-value)
  207. if [[ "$name" =~ ^[a-zA-Z0-9-]+\.[a-zA-Z0-9-]+ ]]; then
  208. [ "$host" == "$name" ] && return 0
  209. fi
  210. rel=$(e "$conf" | shyaml -y get-value relations 2>/dev/null) || continue
  211. for relation in web-proxy publish-dir; do
  212. relation_value=$(e "$rel" | shyaml -y get-value "$relation" 2>/dev/null) || continue
  213. while read-0 label conf_relation; do
  214. domain=$(e "$conf_relation" | shyaml get-value "domain" 2>/dev/null) && {
  215. [ "$host" == "$domain" ] && return 0
  216. }
  217. server_aliases=$(e "$conf_relation" | shyaml get-values "server-aliases" 2>/dev/null) && {
  218. [[ $'\n'"$server_aliases" == *$'\n'"$host"$'\n'* ]] && return 0
  219. }
  220. done < <(e "$relation_value" | shyaml -y key-values-0)
  221. done
  222. done < <(shyaml -y key-values-0 < "$compose_file")
  223. return 1
  224. }
  225. compose:install-backup() {
  226. local BACKUP_SERVER="$1" service_name="$2" compose_file="$3" ignore_ping_check="$4" ignore_domain_check="$5"
  227. ## XXXvlab: far from perfect as it mimics and depends internal
  228. ## logic of current default way to get a domain in compose-core
  229. host=$(hostname)
  230. if ! compose:has_domain "$compose_file" "$host"; then
  231. if [ -n "$ignore_domain_check" ]; then
  232. warn "domain of '$host' not found in compose file '$compose_file'. Ignoring due to ``--ignore-domain-check`` option."
  233. else
  234. err "domain of '$host' not found in compose file '$compose_file'. Use ``--ignore-domain-check`` to ignore check."
  235. return 1
  236. fi
  237. fi
  238. ping_check "$host" || return 1
  239. if [ -e "/root/.ssh/rsync_rsa" ]; then
  240. warn "deleting private key in /root/.ssh/rsync_rsa, has we are not using it anymore."
  241. rm -fv /root/.ssh/rsync_rsa
  242. fi
  243. if [ -e "/root/.ssh/rsync_rsa.pub" ]; then
  244. warn "deleting public key in /root/.ssh/rsync_rsa.pub, has we are not using it anymore."
  245. rm -fv /root/.ssh/rsync_rsa.pub
  246. fi
  247. if service_cfg=$(cat "$compose_file" |
  248. shyaml get-value -y "$service_name" 2>/dev/null); then
  249. info "Entry for service ${DARKYELLOW}$service_name${NORMAL}" \
  250. "is already present in '$compose_file'."
  251. cfg=$(e "$service_cfg" | shyaml get-value -y options) || {
  252. err "No ${WHITE}options${NORMAL} in ${DARKYELLOW}$service_name${NORMAL}'s" \
  253. "entry in '$compose_file'."
  254. return 1
  255. }
  256. private_key=$(e "$cfg" | shyaml get-value private-key) || return 1
  257. target=$(e "$cfg" | shyaml get-value target) || return 1
  258. if [ "$target" != "$BACKUP_SERVER" ]; then
  259. err "Existing backup target '$target' is different" \
  260. "from specified '$BACKUP_SERVER'"
  261. return 1
  262. fi
  263. else
  264. private_key=$(ssh:mk-private-key "$host" "$service_name")
  265. cat <<EOF >> "$compose_file"
  266. $service_name:
  267. options:
  268. ident: $host
  269. target: $BACKUP_SERVER
  270. private-key: |
  271. $(e "$private_key" | sed -r 's/^/ /g')
  272. EOF
  273. fi
  274. dest="$BACKUP_SERVER"
  275. dest="${dest%/*}"
  276. ssh_options=()
  277. if [[ "$dest" == *":"* ]]; then
  278. port="${dest##*:}"
  279. dest="${dest%%:*}"
  280. ssh_options=(-p "$port")
  281. else
  282. port=""
  283. dest="${dest%%:*}"
  284. fi
  285. info "You can run this following command from an host having admin access to $dest:"
  286. echo " (Or send it to a backup admin of $dest)" >&2
  287. public_key=$(ssh-keygen -y -f <(e "$private_key"$'\n'))
  288. echo "ssh ${ssh_options[@]} myadmin@$dest ssh-key add '$public_key ${service_name}@$host'"
  289. }
  290. backup-action() {
  291. local action="$1"
  292. shift
  293. vps_type=$(vps:get-type) || {
  294. err "Failed to get type of installation."
  295. return 1
  296. }
  297. if ! fn.exists "${vps_type}:${action}"; then
  298. err "type '${vps_type}' has no ${vps_type}:${action} implemented yet."
  299. return 1
  300. fi
  301. "${vps_type}:${action}" "$@"
  302. }
  303. compose:get_default_backup_host_ident() {
  304. local service_name="$1" ## Optional
  305. local compose_file service_cfg cfg target
  306. compose_file=$(compose:get-compose-yml)
  307. service_name="${service_name:-rsync-backup}"
  308. if ! service_cfg=$(cat "$compose_file" |
  309. shyaml get-value -y "$service_name" 2>/dev/null); then
  310. err "No service named '$service_name' found in 'compose.yml'."
  311. return 1
  312. fi
  313. cfg=$(e "$service_cfg" | shyaml get-value -y options) || {
  314. err "No ${WHITE}options${NORMAL} in ${DARKYELLOW}$service_name${NORMAL}'s" \
  315. "entry in '$compose_file'."
  316. return 1
  317. }
  318. if ! target=$(e "$cfg" | shyaml get-value target); then
  319. err "No ${WHITE}options.target${NORMAL} in ${DARKYELLOW}$service_name${NORMAL}'s" \
  320. "entry in '$compose_file'."
  321. fi
  322. if ! target=$(e "$cfg" | shyaml get-value target); then
  323. err "No ${WHITE}options.target${NORMAL} in ${DARKYELLOW}$service_name${NORMAL}'s" \
  324. "entry in '$compose_file'."
  325. fi
  326. if ! ident=$(e "$cfg" | shyaml get-value ident); then
  327. err "No ${WHITE}options.ident${NORMAL} in ${DARKYELLOW}$service_name${NORMAL}'s" \
  328. "entry in '$compose_file'."
  329. fi
  330. echo "$target $ident"
  331. }
  332. mailcow:get_default_backup_host_ident() {
  333. local content cron_line ident found dest cmd_line
  334. if ! [ -e "/etc/cron.d/mirror-dir" ]; then
  335. err "No '/etc/cron.d/mirror-dir' found."
  336. return 1
  337. fi
  338. content=$(cat /etc/cron.d/mirror-dir) || {
  339. err "Can't read '/etc/cron.d/mirror-dir'."
  340. return 1
  341. }
  342. if ! cron_line=$(e "$content" | grep "mirror-dir backup"); then
  343. err "Can't find 'mirror-dir backup' line in '/etc/cron.d/mirror-dir'."
  344. return 1
  345. fi
  346. cron_line=${cron_line%|*}
  347. cmd_line=(${cron_line#*root})
  348. found=
  349. dest=
  350. for arg in "${cmd_line[@]}"; do
  351. [ -n "$found" ] && {
  352. dest="$arg"
  353. break
  354. }
  355. [ "$arg" == "-d" ] && {
  356. found=1
  357. }
  358. done
  359. if ! [[ "$dest" =~ ^[\'\"a-zA-Z0-9:/.-]+$ ]]; then
  360. err "Can't find valid destination in 'mirror-dir backup' arguments from '/etc/cron.d/mirror-dir'."
  361. return 1
  362. fi
  363. if [[ "$dest" == \"*\" ]] || [[ "$dest" == \'*\' ]]; then
  364. ## unquoting, the eval should be safe because of previous check
  365. dest=$(eval e "$dest")
  366. fi
  367. if [ -z "$dest" ]; then
  368. err "Can't find destination in 'mirror-dir backup' arguments from '/etc/cron.d/mirror-dir'."
  369. return 1
  370. fi
  371. ## looking for ident
  372. found=
  373. ident=
  374. for arg in "${cmd_line[@]}"; do
  375. [ -n "$found" ] && {
  376. ident="$arg"
  377. break
  378. }
  379. [ "$arg" == "-h" ] && {
  380. found=1
  381. }
  382. done
  383. if ! [[ "$ident" =~ ^[\'\"a-zA-Z0-9.-]+$ ]]; then
  384. err "Can't find valid identifier in 'mirror-dir backup' arguments from '/etc/cron.d/mirror-dir'."
  385. return 1
  386. fi
  387. if [[ "$ident" == \"*\" ]] || [[ "$ident" == \'*\' ]]; then
  388. ## unquoting, the eval should be safe because of previous check
  389. ident=$(eval e "$ident")
  390. fi
  391. if [ -z "$ident" ]; then
  392. err "Can't find destination in 'mirror-dir backup' arguments from '/etc/cron.d/mirror-dir'."
  393. return 1
  394. fi
  395. echo "$dest $ident"
  396. }
  397. compose:get_cron_docker_cmd() {
  398. local cron_line cmd_line docker_cmd
  399. if ! cron_line=$(docker exec myc_cron_1 cat /etc/cron.d/rsync-backup | grep "\* \* \*"); then
  400. err "Can't find cron_line in cron container."
  401. echo " Have you forgotten to run 'compose up' ?" >&2
  402. return 1
  403. fi
  404. cron_line=${cron_line%|*}
  405. cron_line=${cron_line%"2>&1"*}
  406. cmd_line="${cron_line#*root}"
  407. eval "args=($cmd_line)"
  408. ## should be last argument
  409. docker_cmd=$(echo ${args[@]: -1})
  410. if ! [[ "$docker_cmd" == "docker run --rm -e "* ]]; then
  411. echo "docker command found should start with 'docker run'." >&2
  412. echo "Here's command:" >&2
  413. echo " $docker_cmd" >&2
  414. return 1
  415. fi
  416. e "$docker_cmd"
  417. }
  418. compose:recover-target() {
  419. local backup_host="$1" ident="$2" src="$3" dst="$4" service_name="${5:-rsync-backup}"
  420. docker_image="myc_${service_name}"
  421. if ! docker_has_image "$docker_image"; then
  422. compose build "${service_name}" || {
  423. err "Couldn't find nor build image for service '$service_name'."
  424. return 1
  425. }
  426. fi
  427. dst="${dst%/}" ## remove final slash
  428. ssh_options=(-o StrictHostKeyChecking=no)
  429. if [[ "$backup_host" == *":"* ]]; then
  430. port="${backup_host##*:}"
  431. backup_host="${backup_host%%:*}"
  432. ssh_options+=(-p "$port")
  433. else
  434. port=""
  435. backup_host="${backup_host%%:*}"
  436. fi
  437. rsync_opts=(
  438. -e "ssh ${ssh_options[*]} -i /var/lib/rsync/.ssh/id_rsa -l rsync"
  439. -azvArH --delete --delete-excluded
  440. --partial --partial-dir .rsync-partial
  441. --numeric-ids
  442. )
  443. if [ "$DRY_RUN" ]; then
  444. rsync_opts+=("-n")
  445. fi
  446. cmd=(
  447. docker run --rm --entrypoint rsync \
  448. -v "/srv/datastore/config/${service_name}/var/lib/rsync":/var/lib/rsync \
  449. -v "${dst%/*}":/mnt/dest \
  450. "$docker_image" \
  451. "${rsync_opts[@]}" "$backup_host":"/var/mirror/$ident/$src" "/mnt/dest/${dst##*/}"
  452. )
  453. echo "${WHITE}Launching: ${NORMAL} ${cmd[@]}"
  454. "${cmd[@]}"
  455. }
  456. mailcow:recover-target() {
  457. local backup_host="$1" ident="$2" src="$3" dst="$4"
  458. dst="${dst%/}" ## remove final slash
  459. ssh_options=(-o StrictHostKeyChecking=no)
  460. if [[ "$backup_host" == *":"* ]]; then
  461. port="${backup_host##*:}"
  462. backup_host="${backup_host%%:*}"
  463. ssh_options+=(-p "$port")
  464. else
  465. port=""
  466. backup_host="${backup_host%%:*}"
  467. fi
  468. rsync_opts=(
  469. -e "ssh ${ssh_options[*]} -i /var/lib/rsync/.ssh/id_rsa -l rsync"
  470. -azvArH --delete --delete-excluded
  471. --partial --partial-dir .rsync-partial
  472. --numeric-ids
  473. )
  474. if [ "$DRY_RUN" ]; then
  475. rsync_opts+=("-n")
  476. fi
  477. cmd=(
  478. rsync "${rsync_opts[@]}" "$backup_host":"/var/mirror/$ident/$src" "${dst}"
  479. )
  480. echo "${WHITE}Launching: ${NORMAL} ${cmd[@]}"
  481. "${cmd[@]}"
  482. }
  483. [ "$SOURCED" ] && return 0
  484. ##
  485. ## Command line processing
  486. ##
  487. cmdline.spec.gnu
  488. cmdline.spec.reporting
  489. cmdline.spec.gnu install
  490. cmdline.spec::cmd:install:run() {
  491. :
  492. }
  493. cmdline.spec.gnu get-type
  494. cmdline.spec::cmd:get-type:run() {
  495. vps:get-type
  496. }
  497. cmdline.spec:install:cmd:backup:run() {
  498. : :posarg: BACKUP_SERVER 'Target backup server'
  499. : :optfla: --ignore-domain-check \
  500. "Allow to bypass the domain check in
  501. compose file (only used in compose
  502. installation)."
  503. : :optfla: --ignore-ping-check "Allow to bypass the ping check of host."
  504. local vps_type
  505. vps_type=$(vps:get-type) || {
  506. err "Failed to get type of installation."
  507. return 1
  508. }
  509. if ! fn.exists "${vps_type}:install-backup"; then
  510. err "type '${vps_type}' has no backup installation implemented yet."
  511. return 1
  512. fi
  513. opts=()
  514. [ "$opt_ignore_ping_check" ] &&
  515. opts+=("--ignore-ping-check")
  516. if [ "$vps_type" == "compose" ]; then
  517. [ "$opt_ignore_domain_check" ] &&
  518. opts+=("--ignore-domain-check")
  519. fi
  520. "cmdline.spec:install:cmd:${vps_type}-backup:run" "${opts[@]}" "$BACKUP_SERVER"
  521. }
  522. DEFAULT_BACKUP_SERVICE_NAME=rsync-backup
  523. cmdline.spec.gnu compose-backup
  524. cmdline.spec:install:cmd:compose-backup:run() {
  525. : :posarg: BACKUP_SERVER 'Target backup server'
  526. : :optval: --service-name,-s "YAML service name in compose
  527. file to check for existence of key.
  528. Defaults to '$DEFAULT_BACKUP_SERVICE_NAME'"
  529. : :optval: --compose-file,-f "Compose file location. Defaults to
  530. the value of '\$DEFAULT_COMPOSE_FILE'"
  531. : :optfla: --ignore-domain-check \
  532. "Allow to bypass the domain check in
  533. compose file."
  534. : :optfla: --ignore-ping-check "Allow to bypass the ping check of host."
  535. local service_name compose_file
  536. [ -e "/etc/compose/local.conf" ] && source /etc/compose/local.conf
  537. compose_file=${opt_compose_file:-$DEFAULT_COMPOSE_FILE}
  538. service_name=${opt_service_name:-$DEFAULT_BACKUP_SERVICE_NAME}
  539. if ! [ -e "$compose_file" ]; then
  540. err "Compose file not found in '$compose_file'."
  541. return 1
  542. fi
  543. compose:install-backup "$BACKUP_SERVER" "$service_name" "$compose_file" \
  544. "$opt_ignore_ping_check" "$opt_ignore_domain_check"
  545. }
  546. cmdline.spec:install:cmd:mailcow-backup:run() {
  547. : :posarg: BACKUP_SERVER 'Target backup server'
  548. : :optfla: --ignore-ping-check "Allow to bypass the ping check of host."
  549. "mailcow:install-backup" "$BACKUP_SERVER" "$opt_ignore_ping_check"
  550. }
  551. cmdline.spec.gnu backup
  552. cmdline.spec::cmd:backup:run() {
  553. local vps_type
  554. vps_type=$(vps:get-type) || {
  555. err "Failed to get type of installation."
  556. return 1
  557. }
  558. if ! fn.exists "cmdline.spec:backup:cmd:${vps_type}:run"; then
  559. err "type '${vps_type}' has no backup process implemented yet."
  560. return 1
  561. fi
  562. "cmdline.spec:backup:cmd:${vps_type}:run"
  563. }
  564. cmdline.spec:backup:cmd:mailcow:run() {
  565. local cmd_line cron_line cmd
  566. for f in mysql-backup mirror-dir; do
  567. [ -e "/etc/cron.d/$f" ] || {
  568. err "Can't find '/etc/cron.d/$f'."
  569. echo " Have you forgotten to run 'vps install backup BACKUP_HOST' ?" >&2
  570. return 1
  571. }
  572. if ! cron_line=$(cat "/etc/cron.d/$f" |
  573. grep -v "^#" | grep "\* \* \*"); then
  574. err "Can't find cron_line in '/etc/cron.d/$f'." \
  575. "Have you modified it ?"
  576. return 1
  577. fi
  578. cron_line=${cron_line%|*}
  579. cmd_line=(${cron_line#*root})
  580. if [ "$f" == "mirror-dir" ]; then
  581. cmd=()
  582. for arg in "${cmd_line[@]}"; do
  583. [ "$arg" != "-q" ] && cmd+=("$arg")
  584. done
  585. else
  586. cmd=("${cmd_line[@]}")
  587. fi
  588. code="${cmd[*]}"
  589. echo "${WHITE}Launching:${NORMAL} ${code}"
  590. {
  591. {
  592. (
  593. ## Some commands are using colors that are already
  594. ## set by this current program and will trickle
  595. ## down unwantedly
  596. ansi_color no
  597. eval "${code}"
  598. ) | sed -r "s/^/ ${GRAY}|${NORMAL} /g"
  599. set_errlvl "${PIPESTATUS[0]}"
  600. } 3>&1 1>&2 2>&3 | sed -r "s/^/ $DARKRED\!$NORMAL /g"
  601. set_errlvl "${PIPESTATUS[0]}"
  602. } 3>&1 1>&2 2>&3
  603. if [ "$?" != "0" ]; then
  604. err "Failed."
  605. return 1
  606. fi
  607. done
  608. info "Mysql backup and subsequent mirror-dir ${DARKGREEN}succeeded${NORMAL}."
  609. }
  610. set_errlvl() { return "${1:-1}"; }
  611. cmdline.spec:backup:cmd:compose:run() {
  612. local cron_line args
  613. docker_cmd=$(compose:get_cron_docker_cmd) || return 1
  614. echo "${WHITE}Launching:${NORMAL} docker exec -i myc_cron_1 $docker_cmd"
  615. {
  616. {
  617. eval "docker exec -i myc_cron_1 $docker_cmd" | sed -r "s/^/ ${GRAY}|${NORMAL} /g"
  618. set_errlvl "${PIPESTATUS[0]}"
  619. } 3>&1 1>&2 2>&3 | sed -r "s/^/ $DARKRED\!$NORMAL /g"
  620. set_errlvl "${PIPESTATUS[0]}"
  621. } 3>&1 1>&2 2>&3
  622. if [ "$?" != "0" ]; then
  623. err "Failed."
  624. return 1
  625. fi
  626. info "mirror-dir ${DARKGREEN}succeeded${NORMAL}."
  627. }
  628. cmdline.spec.gnu recover-target
  629. cmdline.spec::cmd:recover-target:run() {
  630. : :posarg: BACKUP_DIR 'Source directory on backup side'
  631. : :posarg: HOST_DIR 'Target directory on host side'
  632. : :optval: --backup-host,-B "The backup host"
  633. : :optfla: --dry-run,-n "Don't do anything, instead tell what it
  634. would do."
  635. ## if no backup host take the one by default
  636. backup_host="$opt_backup_host"
  637. if [ -z "$backup_host" ]; then
  638. backup_host_ident=$(backup-action get_default_backup_host_ident) || return 1
  639. read -r backup_host ident <<<"$backup_host_ident"
  640. fi
  641. if [[ "$BACKUP_DIR" == /* ]]; then
  642. err "BACKUP_DIR must be a relative path from the root of your backup."
  643. return 1
  644. fi
  645. REAL_HOST_DIR=$(realpath "$HOST_DIR") || {
  646. err "Can't find HOST_DIR '$HOST_DIR'."
  647. return 1
  648. }
  649. export DRY_RUN="${opt_dry_run}"
  650. backup-action recover-target "$backup_host" "$ident" "$BACKUP_DIR" "$REAL_HOST_DIR"
  651. }
  652. cmdline::parse "$@"