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.

1563 lines
46 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. export -f compose:get-compose-yml
  63. compose:has-container-project-myc() {
  64. local projects
  65. projects=$(docker:running-container-projects) || return 1
  66. [[ $'\n'"$projects"$'\n' == *$'\n'"myc"$'\n'* ]]
  67. }
  68. compose:file:value-change() {
  69. local key="$1" value="$2" first=1 count=0 diff=""
  70. (
  71. COMPOSE_YML_FILE=$(compose:get-compose-yml) || exit 1
  72. export COMPOSE_YML_FILE
  73. cd "${COMPOSE_YML_FILE%/*}"
  74. while read-0 hunk; do
  75. if [ -n "$first" ]; then
  76. diff+="$hunk"
  77. first=
  78. continue
  79. fi
  80. [[ "$hunk" =~ $'\n'"+"[[:space:]]+"${key##*.}:" ]] && {
  81. ((count++))
  82. diff+="$hunk" >&2
  83. }
  84. done < <(
  85. export DEBUG=
  86. settmpdir YQ_TEMP
  87. cp "${COMPOSE_YML_FILE}" "$YQ_TEMP/compose.yml" &&
  88. yq -i ".${key} = \"${value}\"" "$YQ_TEMP/compose.yml" &&
  89. diff -u1 "${COMPOSE_YML_FILE}" "$YQ_TEMP/compose.yml" |
  90. sed -r "s/^(@@.*)$/\x00\1/g;s%^(\+\+\+) [^\t]+%\1 ${COMPOSE_YML_FILE}%g"
  91. )
  92. if [[ "$count" == 0 ]]; then
  93. err "No change made to 'compose.yml'."
  94. return 1
  95. fi
  96. if [[ "$count" != 1 ]]; then
  97. err "compose file change request seems dubious and was refused."
  98. if [ -n "$DEBUG" ]; then
  99. e "$diff" | prefix " | "
  100. fi
  101. return 1
  102. fi
  103. echo Applying: >&2
  104. e "$diff" | prefix " | " >&2
  105. patch <<<"$diff"
  106. ) || exit 1
  107. }
  108. export -f compose:file:value-change
  109. type:is-mailcow() {
  110. mailcow:get-root >/dev/null ||
  111. mailcow:has-running-containers
  112. }
  113. type:is-compose() {
  114. compose:get-compose-yml >/dev/null &&
  115. compose:has-container-project-myc
  116. }
  117. vps:get-type() {
  118. :cache: scope=session
  119. local fn
  120. for fn in $(declare -F | cut -f 3 -d " " | egrep "^type:is-"); do
  121. "$fn" && {
  122. echo "${fn#type:is-}"
  123. return 0
  124. }
  125. done
  126. return 1
  127. }
  128. decorator._mangle_fn vps:get-type
  129. mirror-dir:sources() {
  130. :cache: scope=session
  131. if ! shyaml get-values default.sources < /etc/mirror-dir/config.yml; then
  132. err "Couldn't query 'default.sources' in '/etc/mirror-dir/config.yml'."
  133. return 1
  134. fi
  135. }
  136. decorator._mangle_fn mirror-dir:sources
  137. mirror-dir:check-add() {
  138. local elt="$1" sources
  139. sources=$(mirror-dir:sources) || return 1
  140. if [[ $'\n'"$sources"$'\n' == *$'\n'"$elt"$'\n'* ]]; then
  141. info "Volume $elt already in sources"
  142. else
  143. Elt "Adding directory $elt"
  144. sed -i "/sources:/a\ - \"${elt}\"" \
  145. /etc/mirror-dir/config.yml
  146. Feedback || return 1
  147. fi
  148. }
  149. mirror-dir:check-add-vol() {
  150. local elt="$1"
  151. mirror-dir:check-add "/var/lib/docker/volumes/*_${elt}-*/_data"
  152. }
  153. ## The first colon is to prevent auto-export of function from shlib
  154. : ; bash-bug-5() { { cat; } < <(e) >/dev/null; ! cat "$1"; } && bash-bug-5 <(e) 2>/dev/null &&
  155. export BASH_BUG_5=1 && unset -f bash_bug_5
  156. wrap() {
  157. local label="$1" code="$2"
  158. shift 2
  159. export VERBOSE=1
  160. interpreter=/bin/bash
  161. if [ -n "$BASH_BUG_5" ]; then
  162. (
  163. settmpdir tmpdir
  164. fname=${label##*/}
  165. e "$code" > "$tmpdir/$fname" &&
  166. chmod +x "$tmpdir/$fname" &&
  167. Wrap -vsd "$label" -- "$interpreter" "$tmpdir/$fname" "$@"
  168. )
  169. else
  170. Wrap -vsd "$label" -- "$interpreter" <(e "$code") "$@"
  171. fi
  172. }
  173. ping_check() {
  174. #global ignore_ping_check
  175. local host="$1"
  176. ip=$(getent ahosts "$host" | egrep "^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+\s+" |
  177. head -n 1 | cut -f 1 -d " ") || return 1
  178. my_ip=$(curl -s myip.kal.fr)
  179. if [ "$ip" != "$my_ip" ]; then
  180. if [ -n "$ignore_ping_check" ]; then
  181. warn "IP of '$host' ($ip) doesn't match mine ($my_ip). Ignoring due to ``--ignore-ping-check`` option."
  182. else
  183. err "IP of '$host' ($ip) doesn't match mine ($my_ip). Use ``--ignore-ping-check`` to ignore check."
  184. return 1
  185. fi
  186. fi
  187. }
  188. mailcow:install-backup() {
  189. local BACKUP_SERVER="$1" ignore_ping_check="$2" mailcow_root DOMAIN
  190. ## find installation
  191. mailcow_root=$(mailcow:get-root) || {
  192. err "Couldn't find a valid mailcow root directory."
  193. return 1
  194. }
  195. ## check ok
  196. DOMAIN=$(cat "$mailcow_root/.env" | grep ^MAILCOW_HOSTNAME= | cut -f 2 -d =) || {
  197. err "Couldn't find MAILCOW_HOSTNAME in file \"$mailcow_root/.env\"."
  198. return 1
  199. }
  200. ping_check "$DOMAIN" || return 1
  201. MYSQL_ROOT_PASSWORD=$(cat "$mailcow_root/.env" | grep ^DBROOT= | cut -f 2 -d =) || {
  202. err "Couldn't find DBROOT in file \"$mailcow_root/.env\"."
  203. return 1
  204. }
  205. MYSQL_CONTAINER=${MYSQL_CONTAINER:-mailcowdockerized_mysql-mailcow_1}
  206. container_id=$(docker ps -f name="$MYSQL_CONTAINER" --format "{{.ID}}")
  207. if [ -z "$container_id" ]; then
  208. err "Couldn't find docker container named '$MYSQL_CONTAINER'."
  209. return 1
  210. fi
  211. export KEY_BACKUP_ID="mailcow"
  212. export MYSQL_ROOT_PASSWORD
  213. export MYSQL_CONTAINER
  214. export BACKUP_SERVER
  215. export DOMAIN
  216. wrap "Install rsync-backup on host" "
  217. cd /srv/charm-store/rsync-backup
  218. bash ./hooks/install.d/60-install.sh
  219. " || return 1
  220. wrap "Mysql dump install" "
  221. cd /srv/charm-store/mariadb
  222. bash ./hooks/install.d/60-backup.sh
  223. " || return 1
  224. ## Using https://github.com/mailcow/mailcow-dockerized/blob/master/helper-scripts/backup_and_restore.sh
  225. for elt in "vmail{,-attachments-vol}" crypt redis rspamd postfix; do
  226. mirror-dir:check-add-vol "$elt" || return 1
  227. done
  228. mirror-dir:check-add "$mailcow_root" || return 1
  229. mirror-dir:check-add "/var/backups/mysql" || return 1
  230. mirror-dir:check-add "/etc" || return 1
  231. dest="$BACKUP_SERVER"
  232. dest="${dest%/*}"
  233. ssh_options=()
  234. if [[ "$dest" == *":"* ]]; then
  235. port="${dest##*:}"
  236. dest="${dest%%:*}"
  237. ssh_options=(-p "$port")
  238. else
  239. port=""
  240. dest="${dest%%:*}"
  241. fi
  242. info "You can run this following command from an host having admin access to $dest:"
  243. echo " (Or send it to a backup admin of $dest)" >&2
  244. echo "ssh ${ssh_options[@]} myadmin@$dest ssh-key add '$(cat /var/lib/rsync/.ssh/id_rsa.pub)'"
  245. }
  246. compose:has_domain() {
  247. local compose_file="$1" host="$2" name conf relation relation_value domain server_aliases
  248. while read-0 name conf ; do
  249. name=$(e "$name" | shyaml get-value)
  250. if [[ "$name" =~ ^[a-zA-Z0-9-]+\.[a-zA-Z0-9-]+ ]]; then
  251. [ "$host" == "$name" ] && return 0
  252. fi
  253. rel=$(e "$conf" | shyaml -y get-value relations 2>/dev/null) || continue
  254. for relation in web-proxy publish-dir; do
  255. relation_value=$(e "$rel" | shyaml -y get-value "$relation" 2>/dev/null) || continue
  256. while read-0 label conf_relation; do
  257. domain=$(e "$conf_relation" | shyaml get-value "domain" 2>/dev/null) && {
  258. [ "$host" == "$domain" ] && return 0
  259. }
  260. server_aliases=$(e "$conf_relation" | shyaml get-values "server-aliases" 2>/dev/null) && {
  261. [[ $'\n'"$server_aliases" == *$'\n'"$host"$'\n'* ]] && return 0
  262. }
  263. done < <(e "$relation_value" | shyaml -y key-values-0)
  264. done
  265. done < <(shyaml -y key-values-0 < "$compose_file")
  266. return 1
  267. }
  268. compose:install-backup() {
  269. local BACKUP_SERVER="$1" service_name="$2" compose_file="$3" ignore_ping_check="$4" ignore_domain_check="$5"
  270. ## XXXvlab: far from perfect as it mimics and depends internal
  271. ## logic of current default way to get a domain in compose-core
  272. host=$(hostname)
  273. if ! compose:has_domain "$compose_file" "$host"; then
  274. if [ -n "$ignore_domain_check" ]; then
  275. warn "domain of '$host' not found in compose file '$compose_file'. Ignoring due to ``--ignore-domain-check`` option."
  276. else
  277. err "domain of '$host' not found in compose file '$compose_file'. Use ``--ignore-domain-check`` to ignore check."
  278. return 1
  279. fi
  280. fi
  281. ping_check "$host" || return 1
  282. if [ -e "/root/.ssh/rsync_rsa" ]; then
  283. warn "deleting private key in /root/.ssh/rsync_rsa, has we are not using it anymore."
  284. rm -fv /root/.ssh/rsync_rsa
  285. fi
  286. if [ -e "/root/.ssh/rsync_rsa.pub" ]; then
  287. warn "deleting public key in /root/.ssh/rsync_rsa.pub, has we are not using it anymore."
  288. rm -fv /root/.ssh/rsync_rsa.pub
  289. fi
  290. if service_cfg=$(cat "$compose_file" |
  291. shyaml get-value -y "$service_name" 2>/dev/null); then
  292. info "Entry for service ${DARKYELLOW}$service_name${NORMAL}" \
  293. "is already present in '$compose_file'."
  294. cfg=$(e "$service_cfg" | shyaml get-value -y options) || {
  295. err "No ${WHITE}options${NORMAL} in ${DARKYELLOW}$service_name${NORMAL}'s" \
  296. "entry in '$compose_file'."
  297. return 1
  298. }
  299. private_key=$(e "$cfg" | shyaml get-value private-key) || return 1
  300. target=$(e "$cfg" | shyaml get-value target) || return 1
  301. if [ "$target" != "$BACKUP_SERVER" ]; then
  302. err "Existing backup target '$target' is different" \
  303. "from specified '$BACKUP_SERVER'"
  304. return 1
  305. fi
  306. else
  307. private_key=$(ssh:mk-private-key "$host" "$service_name")
  308. cat <<EOF >> "$compose_file"
  309. $service_name:
  310. options:
  311. ident: $host
  312. target: $BACKUP_SERVER
  313. private-key: |
  314. $(e "$private_key" | sed -r 's/^/ /g')
  315. EOF
  316. fi
  317. dest="$BACKUP_SERVER"
  318. dest="${dest%/*}"
  319. ssh_options=()
  320. if [[ "$dest" == *":"* ]]; then
  321. port="${dest##*:}"
  322. dest="${dest%%:*}"
  323. ssh_options=(-p "$port")
  324. else
  325. port=""
  326. dest="${dest%%:*}"
  327. fi
  328. info "You can run this following command from an host having admin access to $dest:"
  329. echo " (Or send it to a backup admin of $dest)" >&2
  330. public_key=$(ssh-keygen -y -f <(e "$private_key"$'\n'))
  331. echo "ssh ${ssh_options[@]} myadmin@$dest ssh-key add '$public_key compose@$host'"
  332. }
  333. backup-action() {
  334. local action="$1"
  335. shift
  336. vps_type=$(vps:get-type) || {
  337. err "Failed to get type of installation."
  338. return 1
  339. }
  340. if ! fn.exists "${vps_type}:${action}"; then
  341. err "type '${vps_type}' has no ${vps_type}:${action} implemented yet."
  342. return 1
  343. fi
  344. "${vps_type}:${action}" "$@"
  345. }
  346. compose:get_default_backup_host_ident() {
  347. local service_name="$1" ## Optional
  348. local compose_file service_cfg cfg target
  349. compose_file=$(compose:get-compose-yml)
  350. service_name="${service_name:-rsync-backup}"
  351. if ! service_cfg=$(cat "$compose_file" |
  352. shyaml get-value -y "$service_name" 2>/dev/null); then
  353. err "No service named '$service_name' found in 'compose.yml'."
  354. return 1
  355. fi
  356. cfg=$(e "$service_cfg" | shyaml get-value -y options) || {
  357. err "No ${WHITE}options${NORMAL} in ${DARKYELLOW}$service_name${NORMAL}'s" \
  358. "entry in '$compose_file'."
  359. return 1
  360. }
  361. if ! target=$(e "$cfg" | shyaml get-value target); then
  362. err "No ${WHITE}options.target${NORMAL} in ${DARKYELLOW}$service_name${NORMAL}'s" \
  363. "entry in '$compose_file'."
  364. fi
  365. if ! target=$(e "$cfg" | shyaml get-value target); then
  366. err "No ${WHITE}options.target${NORMAL} in ${DARKYELLOW}$service_name${NORMAL}'s" \
  367. "entry in '$compose_file'."
  368. fi
  369. if ! ident=$(e "$cfg" | shyaml get-value ident); then
  370. err "No ${WHITE}options.ident${NORMAL} in ${DARKYELLOW}$service_name${NORMAL}'s" \
  371. "entry in '$compose_file'."
  372. fi
  373. echo "$target $ident"
  374. }
  375. mailcow:get_default_backup_host_ident() {
  376. local content cron_line ident found dest cmd_line
  377. if ! [ -e "/etc/cron.d/mirror-dir" ]; then
  378. err "No '/etc/cron.d/mirror-dir' found."
  379. return 1
  380. fi
  381. content=$(cat /etc/cron.d/mirror-dir) || {
  382. err "Can't read '/etc/cron.d/mirror-dir'."
  383. return 1
  384. }
  385. if ! cron_line=$(e "$content" | grep "mirror-dir backup"); then
  386. err "Can't find 'mirror-dir backup' line in '/etc/cron.d/mirror-dir'."
  387. return 1
  388. fi
  389. cron_line=${cron_line%|*}
  390. cmd_line=(${cron_line#*root})
  391. found=
  392. dest=
  393. for arg in "${cmd_line[@]}"; do
  394. [ -n "$found" ] && {
  395. dest="$arg"
  396. break
  397. }
  398. [ "$arg" == "-d" ] && {
  399. found=1
  400. }
  401. done
  402. if ! [[ "$dest" =~ ^[\'\"a-zA-Z0-9:/.-]+$ ]]; then
  403. err "Can't find valid destination in 'mirror-dir backup' arguments from '/etc/cron.d/mirror-dir'."
  404. return 1
  405. fi
  406. if [[ "$dest" == \"*\" ]] || [[ "$dest" == \'*\' ]]; then
  407. ## unquoting, the eval should be safe because of previous check
  408. dest=$(eval e "$dest")
  409. fi
  410. if [ -z "$dest" ]; then
  411. err "Can't find destination in 'mirror-dir backup' arguments from '/etc/cron.d/mirror-dir'."
  412. return 1
  413. fi
  414. ## looking for ident
  415. found=
  416. ident=
  417. for arg in "${cmd_line[@]}"; do
  418. [ -n "$found" ] && {
  419. ident="$arg"
  420. break
  421. }
  422. [ "$arg" == "-h" ] && {
  423. found=1
  424. }
  425. done
  426. if ! [[ "$ident" =~ ^[\'\"a-zA-Z0-9.-]+$ ]]; then
  427. err "Can't find valid identifier in 'mirror-dir backup' arguments from '/etc/cron.d/mirror-dir'."
  428. return 1
  429. fi
  430. if [[ "$ident" == \"*\" ]] || [[ "$ident" == \'*\' ]]; then
  431. ## unquoting, the eval should be safe because of previous check
  432. ident=$(eval e "$ident")
  433. fi
  434. if [ -z "$ident" ]; then
  435. err "Can't find destination in 'mirror-dir backup' arguments from '/etc/cron.d/mirror-dir'."
  436. return 1
  437. fi
  438. echo "$dest $ident"
  439. }
  440. compose:service:containers() {
  441. local project="$1" service="$2"
  442. docker ps \
  443. --filter label="com.docker.compose.project=$project" \
  444. --filter label="compose.master-service=$service" \
  445. --format="{{.ID}}"
  446. }
  447. export -f compose:service:containers
  448. compose:service:container_one() {
  449. local project="$1" service="$2" container_id
  450. {
  451. read-0a container_id || {
  452. err "service ${DARKYELLOW}$service${NORMAL} has no running container."
  453. return 1
  454. }
  455. if read-0a _; then
  456. err "service ${DARKYELLOW}$service${NORMAL} has more than one running container."
  457. return 1
  458. fi
  459. } < <(compose:service:containers "$project" "$service")
  460. echo "$container_id"
  461. }
  462. export -f compose:service:container_one
  463. compose:service:container_first() {
  464. local project="$1" service="$2" container_id
  465. {
  466. read-0a container_id || {
  467. err "service ${DARKYELLOW}$service${NORMAL} has no running container."
  468. return 1
  469. }
  470. if read-0a _; then
  471. warn "service ${DARKYELLOW}$service${NORMAL} has more than one running container."
  472. fi
  473. } < <(compose:service:containers "$project" "$service")
  474. echo "$container_id"
  475. }
  476. export -f compose:service:container_first
  477. compose:charm:containers() {
  478. local project="$1" charm="$2"
  479. docker ps \
  480. --filter label="com.docker.compose.project=$project" \
  481. --filter label="compose.charm=$charm" \
  482. --format="{{.ID}}"
  483. }
  484. export -f compose:charm:containers
  485. compose:charm:container_one() {
  486. local project="$1" charm="$2" container_id
  487. {
  488. read-0a container_id || {
  489. err "charm ${DARKPINK}$charm${NORMAL} has no running container in project '$project'."
  490. return 1
  491. }
  492. if read-0a _; then
  493. err "charm ${DARKPINK}$charm${NORMAL} has more than one running container."
  494. return 1
  495. fi
  496. } < <(compose:charm:containers "$project" "$charm")
  497. echo "$container_id"
  498. }
  499. export -f compose:charm:container_one
  500. compose:charm:container_first() {
  501. local project="$1" charm="$2" container_id
  502. {
  503. read-0a container_id || {
  504. warn "charm ${DARKYELLOW}$charm${NORMAL} has no running container in project '$project'."
  505. }
  506. if read-0a _; then
  507. warn "charm ${DARKYELLOW}$charm${NORMAL} has more than one running container."
  508. fi
  509. } < <(compose:charm:containers "$project" "$charm")
  510. echo "$container_id"
  511. }
  512. export -f compose:charm:container_first
  513. compose:get_url() {
  514. local project_name="$1" service="$2" data_file network ip
  515. data_file="/var/lib/compose/relations/${project_name}/${service}-frontend/web-proxy/data"
  516. if [ -e "$data_file" ]; then
  517. (
  518. set -o pipefail
  519. cat "$data_file" | shyaml get-value url
  520. )
  521. else
  522. ## Assume there are no frontend relation here, the url is direct IP
  523. container_id=$(compose:service:container_one "${project_name}" "${service}") || return 1
  524. network_ip=$(docker:container:network_ip_one "${container_id}") || return 1
  525. IFS=":" read -r network ip <<<"$network_ip"
  526. tcp_port=
  527. for port in $(docker:exposed_ports "$container_id"); do
  528. IFS="/" read port type <<<"$port"
  529. [ "$type" == "tcp" ] || continue
  530. tcp_port="$port"
  531. break
  532. done
  533. echo -n "http://$ip"
  534. [ -n "$tcp_port" ] && echo ":$tcp_port"
  535. fi || {
  536. err "Failed querying ${service} to frontend relation to get url."
  537. return 1
  538. }
  539. }
  540. export -f compose:get_url
  541. compose:container:service() {
  542. local container="$1" service
  543. if ! service=$(docker:container:label "$container" "compose.service"); then
  544. err "Failed to get service name from container ${container}."
  545. return 1
  546. fi
  547. if [ -z "$service" ]; then
  548. err "No service found for container ${container}."
  549. return 1
  550. fi
  551. echo "$service"
  552. }
  553. export -f compose:container:service
  554. compose:psql() {
  555. local project_name="$1" dbname="$2" container_id
  556. container_id=$(compose:charm:container_one "$project_name" "postgres") || return 1
  557. docker exec -i "${container_id}" psql -U postgres "$dbname"
  558. }
  559. export -f compose:psql
  560. compose:mongo() {
  561. local project_name="$1" dbname="$2" container_id
  562. container_id=$(compose:charm:container_one "$project_name" "mongo") || return 1
  563. docker exec -i "${container_id}" mongo --quiet "$dbname"
  564. }
  565. export -f compose:mongo
  566. compose:pgm() {
  567. local project_name="$1" container_network_ip container_ip container_network
  568. shift
  569. container_id=$(compose:charm:container_one "$project_name" "postgres") || return 1
  570. service_name=$(compose:container:service "$container_id") || return 1
  571. image_id=$(docker:container:image "$container_id") || return 1
  572. container_network_ip=$(docker:container:network_ip_one "$container_id") || return 1
  573. IFS=":" read -r container_network container_ip <<<"$container_network_ip"
  574. pgpass="/srv/datastore/data/${service_name}/var/lib/postgresql/data/pgpass"
  575. local final_pgm_docker_run_opts+=(
  576. -u 0 -e prefix_pg_local_command=" "
  577. --network "${container_network}"
  578. -e PGHOST="$container_ip"
  579. -e PGUSER=postgres
  580. -v "$pgpass:/root/.pgpass"
  581. "${pgm_docker_run_opts[@]}"
  582. )
  583. cmd=(docker run --rm \
  584. "${final_pgm_docker_run_opts[@]}" \
  585. "${image_id}" pgm "$@"
  586. )
  587. echo "${cmd[@]}"
  588. "${cmd[@]}"
  589. }
  590. export -f compose:pgm
  591. postgres:dump() {
  592. local project_name="$1" src="$2" dst="$3"
  593. (
  594. settmpdir PGM_TMP_LOCATION
  595. pgm_docker_run_opts=('-v' "${PGM_TMP_LOCATION}:/tmp/dump")
  596. compose:pgm "$project_name" cp -f "$src" "/tmp/dump/dump.gz" &&
  597. mv "$PGM_TMP_LOCATION/dump.gz" "$dst"
  598. ) || return 1
  599. }
  600. export -f postgres:dump
  601. postgres:restore() {
  602. local project_name="$1" src="$2" dst="$3"
  603. full_src_path=$(readlink -e "$src") || exit 1
  604. (
  605. pgm_docker_run_opts=('-v' "${full_src_path}:/tmp/dump.gz")
  606. compose:pgm "$project_name" cp -f "/tmp/dump.gz" "$dst"
  607. ) || return 1
  608. }
  609. export -f postgres:restore
  610. cyclos:set_root_url() {
  611. local project_name="$1" dbname="$2" url="$3"
  612. echo "UPDATE configurations SET root_url = '$url';" |
  613. compose:psql "$project_name" "$dbname" || {
  614. err "Failed to set cyclos url value in '$dbname' database."
  615. return 1
  616. }
  617. }
  618. export -f cyclos:set_root_url
  619. cyclos:unlock() {
  620. local project_name="$1" dbname="$2"
  621. echo "delete from database_lock;" |
  622. compose:psql "${project_name}" "${dbname}"
  623. }
  624. export -f cyclos:unlock
  625. rocketchat:drop-indexes() {
  626. local project_name="$1" dbname="$2"
  627. echo "db.users.dropIndexes()" |
  628. compose:mongo "${project_name}" "${dbname}"
  629. }
  630. export -f rocketchat:drop-indexes
  631. compose:project_name() {
  632. if [ -z "$PROJECT_NAME" ]; then
  633. PROJECT_NAME=$(compose --get-project-name) || {
  634. err "Couldn't get project name."
  635. return 1
  636. }
  637. if [ -z "$PROJECT_NAME" -o "$PROJECT_NAME" == "orphan" ]; then
  638. err "Couldn't get project name, probably because 'compose.yml' wasn't found."
  639. echo " Please ensure to either configure a global 'compose.yml' or run this command" >&2
  640. echo " in a compose project (with 'compose.yml' on the top level directory)." >&2
  641. return 1
  642. fi
  643. export PROJECT_NAME
  644. fi
  645. echo "$PROJECT_NAME"
  646. }
  647. export -f compose:project_name
  648. compose:get_cron_docker_cmd() {
  649. local cron_line cmd_line docker_cmd
  650. project_name=$(compose:project_name) || return 1
  651. if ! cron_line=$(docker exec "${project_name}"_cron_1 cat /etc/cron.d/rsync-backup | grep "\* \* \*"); then
  652. err "Can't find cron_line in cron container."
  653. echo " Have you forgotten to run 'compose up' ?" >&2
  654. return 1
  655. fi
  656. cron_line=${cron_line%|*}
  657. cron_line=${cron_line%"2>&1"*}
  658. cmd_line="${cron_line#*root}"
  659. eval "args=($cmd_line)"
  660. ## should be last argument
  661. docker_cmd=$(echo ${args[@]: -1})
  662. if ! [[ "$docker_cmd" == "docker run --rm -e "* ]]; then
  663. echo "docker command found should start with 'docker run'." >&2
  664. echo "Here's command:" >&2
  665. echo " $docker_cmd" >&2
  666. return 1
  667. fi
  668. e "$docker_cmd"
  669. }
  670. compose:recover-target() {
  671. local backup_host="$1" ident="$2" src="$3" dst="$4" service_name="${5:-rsync-backup}" project_name
  672. project_name=$(compose:project_name) || return 1
  673. docker_image="${project_name}_${service_name}"
  674. if ! docker_has_image "$docker_image"; then
  675. compose build "${service_name}" || {
  676. err "Couldn't find nor build image for service '$service_name'."
  677. return 1
  678. }
  679. fi
  680. dst="${dst%/}" ## remove final slash
  681. ssh_options=(-o StrictHostKeyChecking=no)
  682. if [[ "$backup_host" == *":"* ]]; then
  683. port="${backup_host##*:}"
  684. backup_host="${backup_host%%:*}"
  685. ssh_options+=(-p "$port")
  686. else
  687. port=""
  688. backup_host="${backup_host%%:*}"
  689. fi
  690. rsync_opts=(
  691. -e "ssh ${ssh_options[*]} -i /var/lib/rsync/.ssh/id_rsa -l rsync"
  692. -azvArH --delete --delete-excluded
  693. --partial --partial-dir .rsync-partial
  694. --numeric-ids
  695. )
  696. if [ "$DRY_RUN" ]; then
  697. rsync_opts+=("-n")
  698. fi
  699. cmd=(
  700. docker run --rm --entrypoint rsync \
  701. -v "/srv/datastore/config/${service_name}/var/lib/rsync":/var/lib/rsync \
  702. -v "${dst%/*}":/mnt/dest \
  703. "$docker_image" \
  704. "${rsync_opts[@]}" "$backup_host":"/var/mirror/$ident/$src" "/mnt/dest/${dst##*/}"
  705. )
  706. echo "${WHITE}Launching: ${NORMAL} ${cmd[@]}"
  707. "${cmd[@]}"
  708. }
  709. mailcow:recover-target() {
  710. local backup_host="$1" ident="$2" src="$3" dst="$4"
  711. dst="${dst%/}" ## remove final slash
  712. ssh_options=(-o StrictHostKeyChecking=no)
  713. if [[ "$backup_host" == *":"* ]]; then
  714. port="${backup_host##*:}"
  715. backup_host="${backup_host%%:*}"
  716. ssh_options+=(-p "$port")
  717. else
  718. port=""
  719. backup_host="${backup_host%%:*}"
  720. fi
  721. rsync_opts=(
  722. -e "ssh ${ssh_options[*]} -i /var/lib/rsync/.ssh/id_rsa -l rsync"
  723. -azvArH --delete --delete-excluded
  724. --partial --partial-dir .rsync-partial
  725. --numeric-ids
  726. )
  727. if [ "$DRY_RUN" ]; then
  728. rsync_opts+=("-n")
  729. fi
  730. cmd=(
  731. rsync "${rsync_opts[@]}" "$backup_host":"/var/mirror/$ident/$src" "${dst}"
  732. )
  733. echo "${WHITE}Launching: ${NORMAL} ${cmd[@]}"
  734. "${cmd[@]}"
  735. }
  736. nextcloud:src:version() {
  737. local version
  738. if ! version=$(cat "/srv/datastore/data/${nextcloud_service}/var/www/html/version.php" 2>/dev/null); then
  739. err "Can't find version.php file to get last version installed."
  740. exit 1
  741. fi
  742. version=$(e "$version" | grep 'VersionString =' | cut -f 3 -d ' ' | cut -f 2 -d "'")
  743. if [ -z "$version" ]; then
  744. err "Can't figure out version from version.php content."
  745. exit 1
  746. fi
  747. echo "$version"
  748. }
  749. [ "$SOURCED" ] && return 0
  750. ##
  751. ## Command line processing
  752. ##
  753. cmdline.spec.gnu
  754. cmdline.spec.reporting
  755. cmdline.spec.gnu install
  756. cmdline.spec::cmd:install:run() {
  757. :
  758. }
  759. cmdline.spec.gnu get-type
  760. cmdline.spec::cmd:get-type:run() {
  761. vps:get-type
  762. }
  763. cmdline.spec:install:cmd:backup:run() {
  764. : :posarg: BACKUP_SERVER 'Target backup server'
  765. : :optfla: --ignore-domain-check \
  766. "Allow to bypass the domain check in
  767. compose file (only used in compose
  768. installation)."
  769. : :optfla: --ignore-ping-check "Allow to bypass the ping check of host."
  770. local vps_type
  771. vps_type=$(vps:get-type) || {
  772. err "Failed to get type of installation."
  773. return 1
  774. }
  775. if ! fn.exists "${vps_type}:install-backup"; then
  776. err "type '${vps_type}' has no backup installation implemented yet."
  777. return 1
  778. fi
  779. opts=()
  780. [ "$opt_ignore_ping_check" ] &&
  781. opts+=("--ignore-ping-check")
  782. if [ "$vps_type" == "compose" ]; then
  783. [ "$opt_ignore_domain_check" ] &&
  784. opts+=("--ignore-domain-check")
  785. fi
  786. "cmdline.spec:install:cmd:${vps_type}-backup:run" "${opts[@]}" "$BACKUP_SERVER"
  787. }
  788. DEFAULT_BACKUP_SERVICE_NAME=rsync-backup
  789. cmdline.spec.gnu compose-backup
  790. cmdline.spec:install:cmd:compose-backup:run() {
  791. : :posarg: BACKUP_SERVER 'Target backup server'
  792. : :optval: --service-name,-s "YAML service name in compose
  793. file to check for existence of key.
  794. Defaults to '$DEFAULT_BACKUP_SERVICE_NAME'"
  795. : :optval: --compose-file,-f "Compose file location. Defaults to
  796. the value of '\$DEFAULT_COMPOSE_FILE'"
  797. : :optfla: --ignore-domain-check \
  798. "Allow to bypass the domain check in
  799. compose file."
  800. : :optfla: --ignore-ping-check "Allow to bypass the ping check of host."
  801. local service_name compose_file
  802. [ -e "/etc/compose/local.conf" ] && source /etc/compose/local.conf
  803. compose_file=${opt_compose_file:-$DEFAULT_COMPOSE_FILE}
  804. service_name=${opt_service_name:-$DEFAULT_BACKUP_SERVICE_NAME}
  805. if ! [ -e "$compose_file" ]; then
  806. err "Compose file not found in '$compose_file'."
  807. return 1
  808. fi
  809. compose:install-backup "$BACKUP_SERVER" "$service_name" "$compose_file" \
  810. "$opt_ignore_ping_check" "$opt_ignore_domain_check"
  811. }
  812. cmdline.spec:install:cmd:mailcow-backup:run() {
  813. : :posarg: BACKUP_SERVER 'Target backup server'
  814. : :optfla: --ignore-ping-check "Allow to bypass the ping check of host."
  815. "mailcow:install-backup" "$BACKUP_SERVER" "$opt_ignore_ping_check"
  816. }
  817. cmdline.spec.gnu backup
  818. cmdline.spec::cmd:backup:run() {
  819. local vps_type
  820. vps_type=$(vps:get-type) || {
  821. err "Failed to get type of installation."
  822. return 1
  823. }
  824. if ! fn.exists "cmdline.spec:backup:cmd:${vps_type}:run"; then
  825. err "type '${vps_type}' has no backup process implemented yet."
  826. return 1
  827. fi
  828. "cmdline.spec:backup:cmd:${vps_type}:run"
  829. }
  830. cmdline.spec:backup:cmd:mailcow:run() {
  831. local cmd_line cron_line cmd
  832. for f in mysql-backup mirror-dir; do
  833. [ -e "/etc/cron.d/$f" ] || {
  834. err "Can't find '/etc/cron.d/$f'."
  835. echo " Have you forgotten to run 'vps install backup BACKUP_HOST' ?" >&2
  836. return 1
  837. }
  838. if ! cron_line=$(cat "/etc/cron.d/$f" |
  839. grep -v "^#" | grep "\* \* \*"); then
  840. err "Can't find cron_line in '/etc/cron.d/$f'." \
  841. "Have you modified it ?"
  842. return 1
  843. fi
  844. cron_line=${cron_line%|*}
  845. cmd_line=(${cron_line#*root})
  846. if [ "$f" == "mirror-dir" ]; then
  847. cmd=()
  848. for arg in "${cmd_line[@]}"; do
  849. [ "$arg" != "-q" ] && cmd+=("$arg")
  850. done
  851. else
  852. cmd=("${cmd_line[@]}")
  853. fi
  854. code="${cmd[*]}"
  855. echo "${WHITE}Launching:${NORMAL} ${code}"
  856. {
  857. {
  858. (
  859. ## Some commands are using colors that are already
  860. ## set by this current program and will trickle
  861. ## down unwantedly
  862. ansi_color no
  863. eval "${code}"
  864. ) | sed -r "s/^/ ${GRAY}|${NORMAL} /g"
  865. set_errlvl "${PIPESTATUS[0]}"
  866. } 3>&1 1>&2 2>&3 | sed -r "s/^/ $DARKRED\!$NORMAL /g"
  867. set_errlvl "${PIPESTATUS[0]}"
  868. } 3>&1 1>&2 2>&3
  869. if [ "$?" != "0" ]; then
  870. err "Failed."
  871. return 1
  872. fi
  873. done
  874. info "Mysql backup and subsequent mirror-dir ${DARKGREEN}succeeded${NORMAL}."
  875. }
  876. set_errlvl() { return "${1:-1}"; }
  877. cmdline.spec:backup:cmd:compose:run() {
  878. local cron_line args
  879. project_name=$(compose:project_name) || return 1
  880. docker_cmd=$(compose:get_cron_docker_cmd) || return 1
  881. echo "${WHITE}Launching:${NORMAL} docker exec -i "${project_name}_cron_1" $docker_cmd"
  882. {
  883. {
  884. eval "docker exec -i \"${project_name}_cron_1\" $docker_cmd" | sed -r "s/^/ ${GRAY}|${NORMAL} /g"
  885. set_errlvl "${PIPESTATUS[0]}"
  886. } 3>&1 1>&2 2>&3 | sed -r "s/^/ $DARKRED\!$NORMAL /g"
  887. set_errlvl "${PIPESTATUS[0]}"
  888. } 3>&1 1>&2 2>&3
  889. if [ "$?" != "0" ]; then
  890. err "Failed."
  891. return 1
  892. fi
  893. info "mirror-dir ${DARKGREEN}succeeded${NORMAL}."
  894. }
  895. cmdline.spec.gnu recover-target
  896. cmdline.spec::cmd:recover-target:run() {
  897. : :posarg: BACKUP_DIR 'Source directory on backup side'
  898. : :posarg: HOST_DIR 'Target directory on host side'
  899. : :optval: --backup-host,-B "The backup host"
  900. : :optfla: --dry-run,-n "Don't do anything, instead tell what it
  901. would do."
  902. ## if no backup host take the one by default
  903. backup_host="$opt_backup_host"
  904. if [ -z "$backup_host" ]; then
  905. backup_host_ident=$(backup-action get_default_backup_host_ident) || return 1
  906. read -r backup_host ident <<<"$backup_host_ident"
  907. fi
  908. if [[ "$BACKUP_DIR" == /* ]]; then
  909. err "BACKUP_DIR must be a relative path from the root of your backup."
  910. return 1
  911. fi
  912. REAL_HOST_DIR=$(realpath "$HOST_DIR") || {
  913. err "Can't find HOST_DIR '$HOST_DIR'."
  914. return 1
  915. }
  916. export DRY_RUN="${opt_dry_run}"
  917. backup-action recover-target "$backup_host" "$ident" "$BACKUP_DIR" "$REAL_HOST_DIR"
  918. }
  919. cmdline.spec.gnu odoo
  920. cmdline.spec::cmd:odoo:run() {
  921. :
  922. }
  923. cmdline.spec.gnu restart
  924. cmdline.spec:odoo:cmd:restart:run() {
  925. : :optval: --service,-s "The service (defaults to 'odoo')"
  926. local out odoo_service
  927. odoo_service="${opt_service:-odoo}"
  928. project_name=$(compose:project_name) || return 1
  929. if ! out=$(docker restart "${project_name}_${odoo_service}_1" 2>&1); then
  930. if [[ "$out" == *"no matching entries in passwd file" ]]; then
  931. warn "Catched docker bug. Restarting once more."
  932. if ! out=$(docker restart "${project_name}_${odoo_service}_1"); then
  933. err "Can't restart container ${project_name}_${odoo_service}_1 (restarted twice)."
  934. echo " output:" >&2
  935. echo "$out" | prefix " ${GRAY}|${NORMAL} " >&2
  936. exit 1
  937. fi
  938. else
  939. err "Couldn't restart container ${project_name}_${odoo_service}_1 (and no restart bug detected)."
  940. exit 1
  941. fi
  942. fi
  943. info "Container ${project_name}_${odoo_service}_1 was ${DARKGREEN}successfully${NORMAL} restarted."
  944. }
  945. cmdline.spec.gnu restore
  946. cmdline.spec:odoo:cmd:restore:run() {
  947. : :posarg: ZIP_DUMP_LOCATION 'Source odoo dump file to restore
  948. (can be a local file or an url)'
  949. : :optval: --service,-s "The service (defaults to 'odoo')"
  950. : :optval: --database,-d 'Target database (default if not specified)'
  951. local out
  952. odoo_service="${opt_service:-odoo}"
  953. if [[ "$ZIP_DUMP_LOCATION" == "http://"* ]] ||
  954. [[ "$ZIP_DUMP_LOCATION" == "https://"* ]]; then
  955. settmpdir ZIP_TMP_LOCATION
  956. tmp_location="$ZIP_TMP_LOCATION/dump.zip"
  957. curl -k -s -L "$ZIP_DUMP_LOCATION" > "$tmp_location" || {
  958. err "Couldn't get '$ZIP_DUMP_LOCATION'."
  959. exit 1
  960. }
  961. if [[ "$(dd if="$tmp_location" count=2 bs=1 2>/dev/null)" != "PK" ]]; then
  962. err "Download doesn't seem to be a zip file."
  963. dd if="$tmp_location" count=1 bs=256 | hd | prefix " ${GRAY}|${NORMAL} " >&2
  964. exit 1
  965. fi
  966. info "Successfully downloaded '$ZIP_DUMP_LOCATION'"
  967. echo " in '$tmp_location'." >&2
  968. ZIP_DUMP_LOCATION="$tmp_location"
  969. fi
  970. [ -e "$ZIP_DUMP_LOCATION" ] || {
  971. err "No file '$ZIP_DUMP_LOCATION' found." >&2
  972. exit 1
  973. }
  974. #cmdline.spec:odoo:cmd:restart:run --service "$odoo_service" || exit 1
  975. msg_dbname=default
  976. [ -n "$opt_database" ] && msg_dbname="'$opt_database'"
  977. compose --no-hooks drop "$odoo_service" $opt_database || {
  978. err "Error dropping $msg_dbname database of service ${DARKYELLOW}$odoo_service${NORMAL}."
  979. exit 1
  980. }
  981. compose --no-hooks load "$odoo_service" $opt_database < "$ZIP_DUMP_LOCATION" || {
  982. err "Error restoring service ${DARKYELLOW}$odoo_service${NORMAL} to $msg_dbname database."
  983. exit 1
  984. }
  985. info "Successfully restored ${DARKYELLOW}$odoo_service${NORMAL} to $msg_dbname database."
  986. ## Restart odoo, ensure there is no bugs lingering on it.
  987. cmdline.spec:odoo:cmd:restart:run --service "$odoo_service" || exit 1
  988. }
  989. cmdline.spec.gnu dump
  990. cmdline.spec:odoo:cmd:dump:run() {
  991. : :posarg: DUMP_ZIPFILE 'Target path to store odoo dump zip file.'
  992. : :optval: --database,-d 'Target database (default if not specified)'
  993. : :optval: --service,-s "The service (defaults to 'odoo')"
  994. odoo_service="${opt_service:-odoo}"
  995. msg_dbname=default
  996. [ -n "$opt_database" ] && msg_dbname="'$opt_database'"
  997. compose --no-hooks save "$odoo_service" $opt_database > "$DUMP_ZIPFILE" || {
  998. err "Error dumping ${DARKYELLOW}$odoo_service${NORMAL}'s $msg_dbname database to '$DUMP_ZIPFILE'."
  999. exit 1
  1000. }
  1001. info "Successfully dumped ${DARKYELLOW}$odoo_service${NORMAL}'s $msg_dbname database to '$DUMP_ZIPFILE'."
  1002. }
  1003. cmdline.spec.gnu drop
  1004. cmdline.spec:odoo:cmd:drop:run() {
  1005. : :optval: --database,-d 'Target database (default if not specified)'
  1006. : :optval: --service,-s "The service (defaults to 'odoo')"
  1007. odoo_service="${opt_service:-odoo}"
  1008. msg_dbname=default
  1009. [ -n "$opt_database" ] && msg_dbname="'$opt_database'"
  1010. compose --no-hooks drop "$odoo_service" $opt_database || {
  1011. err "Error dropping ${DARKYELLOW}$odoo_service${NORMAL}'s $msg_dbname database."
  1012. exit 1
  1013. }
  1014. info "Successfully dropped ${DARKYELLOW}$odoo_service${NORMAL}'s $msg_dbname database."
  1015. }
  1016. cmdline.spec.gnu set-cyclos-url
  1017. cmdline.spec:odoo:cmd:set-cyclos-url:run() {
  1018. : :optval: --database,-d "Target database ('odoo' if not specified)"
  1019. : :optval: --service,-s "The cyclos service name (defaults to 'cyclos')"
  1020. local URL
  1021. dbname=${opt_database:-odoo}
  1022. cyclos_service="${opt_service:-cyclos}"
  1023. project_name=$(compose:project_name) || exit 1
  1024. URL=$(compose:get_url "${project_name}" "${cyclos_service}") || exit 1
  1025. Wrap -d "set cyclos url to '$URL'" <<EOF || exit 1
  1026. echo "UPDATE res_company SET cyclos_server_url = '$URL/api' WHERE id=1;" |
  1027. compose:psql "$project_name" "$dbname" || {
  1028. err "Failed to set cyclos url value in '$dbname' database."
  1029. exit 1
  1030. }
  1031. EOF
  1032. }
  1033. cmdline.spec.gnu cyclos
  1034. cmdline.spec::cmd:cyclos:run() {
  1035. :
  1036. }
  1037. cmdline.spec:cyclos:cmd:dump:run() {
  1038. : :posarg: DUMP_GZFILE 'Target path to store odoo dump gz file.'
  1039. : :optval: --database,-d "Target database ('cyclos' if not specified)"
  1040. : :optval: --service,-s "The cyclos service name (defaults to 'cyclos')"
  1041. cyclos_service="${opt_service:-cyclos}"
  1042. cyclos_database="${opt_database:-cyclos}"
  1043. project_name=$(compose:project_name) || exit 1
  1044. container_id=$(compose:service:container_one "$project_name" "${cyclos_service}") || exit 1
  1045. Wrap -d "stop ${DARKYELLOW}${cyclos_service}${NORMAL}'s container" -- \
  1046. docker stop "$container_id" || exit 1
  1047. Wrap -d "Dump postgres database '${cyclos_database}'." -- \
  1048. postgres:dump "${project_name}" "$cyclos_database" "$DUMP_GZFILE" || exit 1
  1049. Wrap -d "start ${DARKYELLOW}${cyclos_service}${NORMAL}'s container" -- \
  1050. docker start "${container_id}" || exit 1
  1051. }
  1052. cmdline.spec.gnu restore
  1053. cmdline.spec:cyclos:cmd:restore:run() {
  1054. : :posarg: GZ_DUMP_LOCATION 'Source cyclos dump file to restore
  1055. (can be a local file or an url)'
  1056. : :optval: --service,-s "The service (defaults to 'cyclos')"
  1057. : :optval: --database,-d 'Target database (default if not specified)'
  1058. local out
  1059. cyclos_service="${opt_service:-cyclos}"
  1060. cyclos_database="${opt_database:-cyclos}"
  1061. project_name=$(compose:project_name) || exit 1
  1062. url=$(compose:get_url "${project_name}" "${cyclos_service}") || return 1
  1063. container_id=$(compose:service:container_one "$project_name" "${cyclos_service}") || exit 1
  1064. if [[ "$GZ_DUMP_LOCATION" == "http://"* ]] ||
  1065. [[ "$GZ_DUMP_LOCATION" == "https://"* ]]; then
  1066. settmpdir GZ_TMP_LOCATION
  1067. tmp_location="$GZ_TMP_LOCATION/dump.gz"
  1068. Wrap -d "get '$GZ_DUMP_LOCATION'" <<EOF || exit 1
  1069. ## Note that curll version before 7.76.0 do not have
  1070. curl -k -s -L "$GZ_DUMP_LOCATION" --fail \\
  1071. > "$tmp_location" || {
  1072. echo "Error fetching ressource. Is url correct ?" >&2
  1073. exit 1
  1074. }
  1075. if [[ "\$(dd if="$tmp_location" count=2 bs=1 2>/dev/null |
  1076. hexdump -v -e "/1 \"%02x\"")" != "1f8b" ]]; then
  1077. err "Download doesn't seem to be a gzip file."
  1078. dd if="$tmp_location" count=1 bs=256 | hd | prefix " ${GRAY}|${NORMAL} " >&2
  1079. exit 1
  1080. fi
  1081. EOF
  1082. GZ_DUMP_LOCATION="$tmp_location"
  1083. fi
  1084. [ -e "$GZ_DUMP_LOCATION" ] || {
  1085. err "No file '$GZ_DUMP_LOCATION' found." >&2
  1086. exit 1
  1087. }
  1088. Wrap -d "stop ${DARKYELLOW}${cyclos_service}${NORMAL}'s container" -- \
  1089. docker stop "$container_id" || exit 1
  1090. ## XXXvlab: making the assumption that the postgres username should
  1091. ## be the same as the cyclos service selected (which is the default,
  1092. ## but not always the case).
  1093. Wrap -d "restore postgres database '${cyclos_database}'." -- \
  1094. postgres:restore "$project_name" "$GZ_DUMP_LOCATION" "${cyclos_service}@${cyclos_database}" || exit 1
  1095. ## ensure that the database is not locked
  1096. Wrap -d "check and remove database lock if any" -- \
  1097. cyclos:unlock "${project_name}" "${cyclos_database}" || exit 1
  1098. Wrap -d "set root url to '$url'" -- \
  1099. cyclos:set_root_url "${project_name}" "${cyclos_database}" "${url}" || exit 1
  1100. Wrap -d "start ${DARKYELLOW}${cyclos_service}${NORMAL}'s container" -- \
  1101. docker start "${container_id}" || exit 1
  1102. }
  1103. cmdline.spec.gnu set-root-url
  1104. cmdline.spec:cyclos:cmd:set-root-url:run() {
  1105. : :optval: --database,-d "Target database ('cyclos' if not specified)"
  1106. : :optval: --service,-s "The cyclos service name (defaults to 'cyclos')"
  1107. local URL
  1108. cyclos_database=${opt_database:-cyclos}
  1109. cyclos_service="${opt_service:-cyclos}"
  1110. project_name=$(compose:project_name) || exit 1
  1111. url=$(compose:get_url "${project_name}" "${cyclos_service}") || exit 1
  1112. container_id=$(compose:service:container_one "${project_name}" "${cyclos_service}") || exit 1
  1113. Wrap -d "stop ${DARKYELLOW}${cyclos_service}${NORMAL}'s container" -- \
  1114. docker stop "$container_id" || exit 1
  1115. Wrap -d "set root url to '$url'" -- \
  1116. cyclos:set_root_url "${project_name}" "${cyclos_database}" "${url}" || exit 1
  1117. Wrap -d "start ${DARKYELLOW}${cyclos_service}${NORMAL}'s container" -- \
  1118. docker start "${container_id}" || exit 1
  1119. }
  1120. cmdline.spec.gnu unlock
  1121. cmdline.spec:cyclos:cmd:unlock:run() {
  1122. : :optval: --database,-d "Target database ('cyclos' if not specified)"
  1123. : :optval: --service,-s "The cyclos service name (defaults to 'cyclos')"
  1124. local URL
  1125. cyclos_database=${opt_database:-cyclos}
  1126. cyclos_service="${opt_service:-cyclos}"
  1127. project_name=$(compose:project_name) || exit 1
  1128. container_id=$(compose:service:container_one "${project_name}" "${cyclos_service}") || exit 1
  1129. Wrap -d "stop ${DARKYELLOW}${cyclos_service}${NORMAL}'s container" -- \
  1130. docker stop "$container_id" || exit 1
  1131. Wrap -d "check and remove database lock if any" -- \
  1132. cyclos:unlock "${project_name}" "${cyclos_database}" || exit 1
  1133. Wrap -d "start ${DARKYELLOW}${cyclos_service}${NORMAL}'s container" -- \
  1134. docker start "${container_id}" || exit 1
  1135. }
  1136. cmdline.spec.gnu rocketchat
  1137. cmdline.spec::cmd:rocketchat:run() {
  1138. :
  1139. }
  1140. cmdline.spec.gnu drop-indexes
  1141. cmdline.spec:rocketchat:cmd:drop-indexes:run() {
  1142. : :optval: --database,-d "Target database ('rocketchat' if not specified)"
  1143. : :optval: --service,-s "The rocketchat service name (defaults to 'rocketchat')"
  1144. local URL
  1145. rocketchat_database=${opt_database:-rocketchat}
  1146. rocketchat_service="${opt_service:-rocketchat}"
  1147. project_name=$(compose:project_name) || exit 1
  1148. container_id=$(compose:service:container_one "${project_name}" "${rocketchat_service}") || exit 1
  1149. Wrap -d "stop ${DARKYELLOW}${rocketchat_service}${NORMAL}'s container" -- \
  1150. docker stop "$container_id" || exit 1
  1151. errlvl=0
  1152. Wrap -d "drop indexes" -- \
  1153. rocketchat:drop-indexes "${project_name}" "${rocketchat_database}" || {
  1154. errlvl=1
  1155. errmsg="Failed to drop indexes"
  1156. }
  1157. Wrap -d "start ${DARKYELLOW}${rocketchat_service}${NORMAL}'s container" -- \
  1158. docker start "${container_id}" || exit 1
  1159. if [ "$errlvl" != 0 ]; then
  1160. err "$errmsg"
  1161. fi
  1162. exit "$errlvl"
  1163. }
  1164. cmdline.spec.gnu nextcloud
  1165. cmdline.spec::cmd:nextcloud:run() {
  1166. :
  1167. }
  1168. cmdline.spec.gnu upgrade
  1169. cmdline.spec:nextcloud:cmd:upgrade:run() {
  1170. : :posarg: [TARGET_VERSION] "Source cyclos dump file to restore"
  1171. : :optval: --service,-s "The nexcloud service name (defaults to 'nextcloud')"
  1172. local URL
  1173. nextcloud_service="${opt_service:-nextcloud}"
  1174. project_name=$(compose:project_name) || exit 1
  1175. containers=$(compose:service:containers "${project_name}" "${nextcloud_service}") || exit 1
  1176. container_stopped=()
  1177. if [ -n "$containers" ]; then
  1178. for container in $containers; do
  1179. Wrap -d "stop ${DARKYELLOW}${nextcloud_service}${NORMAL}'s container" -- \
  1180. docker stop "$container" || {
  1181. err "Failed to stop container '$container'."
  1182. exit 1
  1183. }
  1184. container_stopped+=("$container")
  1185. done
  1186. fi
  1187. before_version=$(nextcloud:src:version) || exit 1
  1188. ## -q to remove the display of ``compose`` related information
  1189. ## like relation resolution.
  1190. ## --no-hint to remove the final hint about modifying your
  1191. ## ``compose.yml``.
  1192. compose -q upgrade "$nextcloud_service" --no-hint
  1193. errlvl="$?"
  1194. after_version=$(nextcloud:src:version)
  1195. if [ "$after_version" != "$before_version" ]; then
  1196. desc="update \`compose.yml\` to set ${DARKYELLOW}$nextcloud_service${NORMAL}'s "
  1197. desc+="docker image to actual code version ${WHITE}${after_version}${NORMAL}"
  1198. Wrap -d "$desc" -- \
  1199. compose:file:value-change \
  1200. "${nextcloud_service}.docker-compose.image" \
  1201. "docker.0k.io/nextcloud:${after_version}-myc" || exit 1
  1202. fi
  1203. if [ "$errlvl" == 0 ]; then
  1204. echo "${WHITE}Launching final compose${NORMAL}"
  1205. compose up || exit 1
  1206. fi
  1207. exit "$errlvl"
  1208. }
  1209. cmdline::parse "$@"