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.

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