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.

2582 lines
79 KiB

  1. #!/bin/bash
  2. . /etc/shlib >/dev/null 2>&1 || {
  3. echo "Error: you don't have kal-shlib-core installed."
  4. echo ""
  5. echo " You might want to add `deb.kalysto.org` deb repository, you'll need root access,"
  6. echo " so you might want to run these command after a \`sudo -i\` for instance..."
  7. echo ""
  8. echo " echo deb https://deb.kalysto.org no-dist kal-alpha kal-beta kal-main \\"
  9. echo " > /etc/apt/sources.list.d/kalysto.org.list"
  10. echo " wget -O - https://deb.kalysto.org/conf/public-key.gpg | apt-key add -"
  11. echo " apt-get update -o Dir::Etc::sourcelist=sources.list.d/kalysto.org.list \\"
  12. echo " -o Dir::Etc::sourceparts=- -o APT::Get::List-Cleanup=0"
  13. echo ""
  14. echo " Then install package kal-shlib-*:"
  15. echo ""
  16. echo " apt install kal-shlib-{common,cmdline,config,cache,docker,pretty}"
  17. echo ""
  18. exit 1
  19. } >&2
  20. include common
  21. include parse
  22. include cmdline
  23. include config
  24. include cache
  25. include fn
  26. include docker
  27. [[ "${BASH_SOURCE[0]}" != "${0}" ]] && SOURCED=true
  28. version=0.1
  29. desc='Install backup'
  30. help=""
  31. version_gt() { test "$(printf '%s\n' "$@" | sort -V | head -n 1)" != "$1"; }
  32. read-0a-err() {
  33. local ret="$1" eof="" idx=0 last=
  34. read -r -- "${ret?}" <<<"0"
  35. shift
  36. while [ "$1" ]; do
  37. last=$idx
  38. read -r -- "$1" || {
  39. ## Put this last value in ${!ret}
  40. eof="$1"
  41. read -r -- "$ret" <<<"${!eof}"
  42. break
  43. }
  44. ((idx++))
  45. shift
  46. done
  47. [ -z "$eof" ] || {
  48. if [ "$last" != 0 ]; then
  49. echo "Error: read-0a-err couldn't fill all value" >&2
  50. read -r -- "$ret" <<<"127"
  51. else
  52. if [ -z "${!ret}" ]; then
  53. echo "Error: last value is not a number, did you finish with an errorlevel ?" >&2
  54. read -r -- "$ret" <<<"126"
  55. fi
  56. fi
  57. false
  58. }
  59. }
  60. p-0a-err() {
  61. "$@"
  62. echo -n "$?"
  63. }
  64. docker:running-container-projects() {
  65. :cache: scope=session
  66. docker ps --format '{{.Label "com.docker.compose.project"}}' | sort | uniq
  67. }
  68. decorator._mangle_fn docker:running-container-projects
  69. ssh:mk-private-key() {
  70. local host="$1" service_name="$2"
  71. (
  72. settmpdir VPS_TMPDIR
  73. ssh-keygen -t rsa -N "" -f "$VPS_TMPDIR/rsync_rsa" -C "$service_name@$host" >/dev/null
  74. cat "$VPS_TMPDIR/rsync_rsa"
  75. )
  76. }
  77. mailcow:has-images-running() {
  78. local images
  79. images=$(docker ps --format '{{.Image}}' | sort | uniq)
  80. [[ $'\n'"$images" == *$'\n'"mailcow/"* ]]
  81. }
  82. mailcow:has-container-project-mentionning-mailcow() {
  83. local projects
  84. projects=$(docker:running-container-projects) || return 1
  85. [[ $'\n'"$projects"$'\n' == *mailcow* ]]
  86. }
  87. mailcow:has-running-containers() {
  88. mailcow:has-images-running ||
  89. mailcow:has-container-project-mentionning-mailcow
  90. }
  91. mailcow:get-root() {
  92. :cache: scope=session
  93. local dir
  94. for dir in {/opt{,/apps},/root}/mailcow-dockerized; do
  95. [ -d "$dir" ] || continue
  96. [ -r "$dir/mailcow.conf" ] || continue
  97. echo "$dir"
  98. return 0
  99. done
  100. return 1
  101. }
  102. decorator._mangle_fn mailcow:get-root
  103. compose:get-compose-yml() {
  104. :cache: scope=session
  105. local path
  106. path=$(DEBUG=1 DRY_RUN=1 compose 2>&1 | egrep '^\s+-e HOST_COMPOSE_YML_FILE=' | cut -f 2- -d "=" | cut -f 1 -d " ")
  107. [ -e "$path" ] || return 1
  108. echo "$path"
  109. }
  110. decorator._mangle_fn compose:get-compose-yml
  111. export -f compose:get-compose-yml
  112. compose:has-container-project-myc() {
  113. local projects
  114. projects=$(docker:running-container-projects) || return 1
  115. [[ $'\n'"$projects"$'\n' == *$'\n'"myc"$'\n'* ]]
  116. }
  117. compose:service:exists() {
  118. local project="$1" service="$2" service_cfg
  119. service_cfg=$(cat "$(compose:get-compose-yml)" |
  120. shyaml get-value -y "$service" 2>/dev/null) || return 1
  121. [ -n "$service_cfg" ]
  122. }
  123. compose:file:value-change() {
  124. local key="$1" value="$2"
  125. local compose_yml
  126. if ! compose_yml=$(compose:get-compose-yml); then
  127. err "Couldn't locate your 'compose.yml' file."
  128. return 1
  129. fi
  130. yaml:file:value-change "$compose_yml" "$key" "$value" || return 1
  131. }
  132. export -f compose:file:value-change
  133. yaml:file:value-change() {
  134. local file="$1" key="$2" value="$3" first=1 count=0 diff=""
  135. (
  136. cd "${file%/*}"
  137. while read-0 hunk; do
  138. if [ -n "$first" ]; then
  139. diff+="$hunk"
  140. first=
  141. continue
  142. fi
  143. if [[ "$hunk" =~ $'\n'"+"[[:space:]]+"${key##*.}:" ]]; then
  144. ((count++))
  145. diff+="$hunk" >&2
  146. else
  147. :
  148. # echo "discarding:" >&2
  149. # e "$hunk" | prefix " | " >&2
  150. fi
  151. done < <(
  152. export DEBUG=
  153. settmpdir YQ_TEMP
  154. cp "${file}" "$YQ_TEMP/compose.yml" &&
  155. yq -i ".${key} = \"${value}\"" "$YQ_TEMP/compose.yml" &&
  156. sed -ri 's/^([^# ])/\n\0/g' "$YQ_TEMP/compose.yml" &&
  157. diff -u0 -Z "${file}" "$YQ_TEMP/compose.yml" |
  158. sed -r "s/^(@@.*)$/\x00\1/g;s%^(\+\+\+) [^\t]+%\1 ${file}%g"
  159. printf "\0"
  160. )
  161. if [[ "$count" == 0 ]]; then
  162. err "No change made to '$file'."
  163. return 1
  164. fi
  165. if [[ "$count" != 1 ]]; then
  166. err "compose file change request seems dubious and was refused:"
  167. e "$diff" | prefix " | " >&2
  168. return 1
  169. fi
  170. echo Applying: >&2
  171. e "$diff" | prefix " | " >&2
  172. patch <<<"$diff"
  173. ) || exit 1
  174. }
  175. export -f yaml:file:value-change
  176. type:is-mailcow() {
  177. mailcow:get-root >/dev/null ||
  178. mailcow:has-running-containers
  179. }
  180. type:is-compose() {
  181. compose:get-compose-yml >/dev/null &&
  182. compose:has-container-project-myc
  183. }
  184. vps:get-type() {
  185. :cache: scope=session
  186. local fn
  187. for fn in $(declare -F | cut -f 3 -d " " | egrep "^type:is-"); do
  188. "$fn" && {
  189. echo "${fn#type:is-}"
  190. return 0
  191. }
  192. done
  193. return 1
  194. }
  195. decorator._mangle_fn vps:get-type
  196. mirror-dir:sources() {
  197. :cache: scope=session
  198. if ! shyaml get-values default.sources < /etc/mirror-dir/config.yml; then
  199. err "Couldn't query 'default.sources' in '/etc/mirror-dir/config.yml'."
  200. return 1
  201. fi
  202. }
  203. decorator._mangle_fn mirror-dir:sources
  204. mirror-dir:check-add() {
  205. local elt="$1" sources
  206. sources=$(mirror-dir:sources) || return 1
  207. if [[ $'\n'"$sources"$'\n' == *$'\n'"$elt"$'\n'* ]]; then
  208. info "Volume $elt already in sources"
  209. else
  210. Elt "Adding directory $elt"
  211. sed -i "/sources:/a\ - \"${elt}\"" \
  212. /etc/mirror-dir/config.yml
  213. Feedback || return 1
  214. fi
  215. }
  216. mirror-dir:check-add-vol() {
  217. local elt="$1"
  218. mirror-dir:check-add "/var/lib/docker/volumes/*_${elt}-*/_data"
  219. }
  220. ## The first colon is to prevent auto-export of function from shlib
  221. : ; bash-bug-5() { { cat; } < <(e) >/dev/null; ! cat "$1"; } && bash-bug-5 <(e) 2>/dev/null &&
  222. export BASH_BUG_5=1 && unset -f bash_bug_5
  223. wrap() {
  224. local label="$1" code="$2"
  225. shift 2
  226. export VERBOSE=1
  227. interpreter=/bin/bash
  228. if [ -n "$BASH_BUG_5" ]; then
  229. (
  230. settmpdir tmpdir
  231. fname=${label##*/}
  232. e "$code" > "$tmpdir/$fname" &&
  233. chmod +x "$tmpdir/$fname" &&
  234. Wrap -vsd "$label" -- "$interpreter" "$tmpdir/$fname" "$@"
  235. )
  236. else
  237. Wrap -vsd "$label" -- "$interpreter" <(e "$code") "$@"
  238. fi
  239. }
  240. ping_check() {
  241. #global ignore_ping_check
  242. local host="$1"
  243. ip=$(getent ahosts "$host" | egrep "^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+\s+" |
  244. head -n 1 | cut -f 1 -d " ") || return 1
  245. my_ip=$(curl -s myip.kal.fr)
  246. if [ "$ip" != "$my_ip" ]; then
  247. if [ -n "$ignore_ping_check" ]; then
  248. warn "IP of '$host' ($ip) doesn't match mine ($my_ip). Ignoring due to ``--ignore-ping-check`` option."
  249. else
  250. err "IP of '$host' ($ip) doesn't match mine ($my_ip). Use ``--ignore-ping-check`` to ignore check."
  251. return 1
  252. fi
  253. fi
  254. }
  255. mailcow:install-backup() {
  256. local BACKUP_SERVER="$1" ignore_ping_check="$2" mailcow_root DOMAIN
  257. ## find installation
  258. mailcow_root=$(mailcow:get-root) || {
  259. err "Couldn't find a valid mailcow root directory."
  260. return 1
  261. }
  262. ## check ok
  263. DOMAIN=$(cat "$mailcow_root/.env" | grep ^MAILCOW_HOSTNAME= | cut -f 2 -d =) || {
  264. err "Couldn't find MAILCOW_HOSTNAME in file \"$mailcow_root/.env\"."
  265. return 1
  266. }
  267. ping_check "$DOMAIN" || return 1
  268. MYSQL_ROOT_PASSWORD=$(cat "$mailcow_root/.env" | grep ^DBROOT= | cut -f 2 -d =) || {
  269. err "Couldn't find DBROOT in file \"$mailcow_root/.env\"."
  270. return 1
  271. }
  272. if docker compose >/dev/null 2>&1; then
  273. MYSQL_CONTAINER=${MYSQL_CONTAINER:-mailcowdockerized-mysql-mailcow-1}
  274. else
  275. MYSQL_CONTAINER=${MYSQL_CONTAINER:-mailcowdockerized_mysql-mailcow_1}
  276. fi
  277. container_id=$(docker ps -f name="$MYSQL_CONTAINER" --format "{{.ID}}")
  278. if [ -z "$container_id" ]; then
  279. err "Couldn't find docker container named '$MYSQL_CONTAINER'."
  280. return 1
  281. fi
  282. export KEY_BACKUP_ID="mailcow"
  283. export MYSQL_ROOT_PASSWORD
  284. export MYSQL_CONTAINER
  285. export BACKUP_SERVER
  286. export DOMAIN
  287. wrap "Install rsync-backup on host" "
  288. cd /srv/charm-store/rsync-backup
  289. bash ./hooks/install.d/60-install.sh
  290. " || return 1
  291. wrap "Mysql dump install" "
  292. cd /srv/charm-store/mariadb
  293. bash ./hooks/install.d/60-backup.sh
  294. " || return 1
  295. ## Using https://github.com/mailcow/mailcow-dockerized/blob/master/helper-scripts/backup_and_restore.sh
  296. for elt in "vmail{,-attachments-vol}" crypt redis rspamd postfix; do
  297. mirror-dir:check-add-vol "$elt" || return 1
  298. done
  299. mirror-dir:check-add "$mailcow_root" || return 1
  300. mirror-dir:check-add "/var/backups/mysql" || return 1
  301. mirror-dir:check-add "/etc" || return 1
  302. dest="$BACKUP_SERVER"
  303. dest="${dest%/*}"
  304. ssh_options=()
  305. if [[ "$dest" == *":"* ]]; then
  306. port="${dest##*:}"
  307. dest="${dest%%:*}"
  308. ssh_options=(-p "$port")
  309. else
  310. port=""
  311. dest="${dest%%:*}"
  312. fi
  313. info "You can run this following command from an host having admin access to $dest:"
  314. echo " (Or send it to a backup admin of $dest)" >&2
  315. echo "ssh ${ssh_options[@]} myadmin@$dest ssh-key add '$(cat /var/lib/rsync/.ssh/id_rsa.pub)'"
  316. }
  317. compose:has_domain() {
  318. local compose_file="$1" host="$2" name conf relation relation_value domain server_aliases
  319. while read-0 name conf ; do
  320. name=$(e "$name" | shyaml get-value)
  321. if [[ "$name" =~ ^[a-zA-Z0-9-]+\.[a-zA-Z0-9-]+ ]]; then
  322. [ "$host" == "$name" ] && return 0
  323. fi
  324. rel=$(e "$conf" | shyaml -y get-value relations 2>/dev/null) || continue
  325. for relation in web-proxy publish-dir; do
  326. relation_value=$(e "$rel" | shyaml -y get-value "$relation" 2>/dev/null) || continue
  327. while read-0 label conf_relation; do
  328. domain=$(e "$conf_relation" | shyaml get-value "domain" 2>/dev/null) && {
  329. [ "$host" == "$domain" ] && return 0
  330. }
  331. server_aliases=$(e "$conf_relation" | shyaml get-values "server-aliases" 2>/dev/null) && {
  332. [[ $'\n'"$server_aliases" == *$'\n'"$host"$'\n'* ]] && return 0
  333. }
  334. done < <(e "$relation_value" | shyaml -y key-values-0)
  335. done
  336. done < <(shyaml -y key-values-0 < "$compose_file")
  337. return 1
  338. }
  339. compose:install-backup() {
  340. local BACKUP_SERVER="$1" service_name="$2" compose_file="$3" ignore_ping_check="$4" ignore_domain_check="$5"
  341. ## XXXvlab: far from perfect as it mimics and depends internal
  342. ## logic of current default way to get a domain in compose-core
  343. host=$(hostname)
  344. if ! compose:has_domain "$compose_file" "$host"; then
  345. if [ -n "$ignore_domain_check" ]; then
  346. warn "domain of '$host' not found in compose file '$compose_file'. Ignoring due to ``--ignore-domain-check`` option."
  347. else
  348. err "domain of '$host' not found in compose file '$compose_file'. Use ``--ignore-domain-check`` to ignore check."
  349. return 1
  350. fi
  351. fi
  352. ping_check "$host" || return 1
  353. if [ -e "/root/.ssh/rsync_rsa" ]; then
  354. warn "deleting private key in /root/.ssh/rsync_rsa, as we are not using it anymore."
  355. rm -fv /root/.ssh/rsync_rsa
  356. fi
  357. if [ -e "/root/.ssh/rsync_rsa.pub" ]; then
  358. warn "deleting public key in /root/.ssh/rsync_rsa.pub, as we are not using it anymore."
  359. rm -fv /root/.ssh/rsync_rsa.pub
  360. fi
  361. if service_cfg=$(cat "$compose_file" |
  362. shyaml get-value -y "$service_name" 2>/dev/null); then
  363. info "Entry for service ${DARKYELLOW}$service_name${NORMAL}" \
  364. "is already present in '$compose_file'."
  365. cfg=$(e "$service_cfg" | shyaml get-value -y options) || {
  366. err "No ${WHITE}options${NORMAL} in ${DARKYELLOW}$service_name${NORMAL}'s" \
  367. "entry in '$compose_file'."
  368. return 1
  369. }
  370. private_key=$(e "$cfg" | shyaml get-value private-key) || return 1
  371. target=$(e "$cfg" | shyaml get-value target) || return 1
  372. if [ "$target" != "$BACKUP_SERVER" ]; then
  373. err "Existing backup target '$target' is different" \
  374. "from specified '$BACKUP_SERVER'"
  375. return 1
  376. fi
  377. else
  378. private_key=$(ssh:mk-private-key "$host" "$service_name")
  379. cat <<EOF >> "$compose_file"
  380. $service_name:
  381. options:
  382. ident: $host
  383. target: $BACKUP_SERVER
  384. private-key: |
  385. $(e "$private_key" | sed -r 's/^/ /g')
  386. EOF
  387. fi
  388. dest="$BACKUP_SERVER"
  389. dest="${dest%/*}"
  390. ssh_options=()
  391. if [[ "$dest" == *":"* ]]; then
  392. port="${dest##*:}"
  393. dest="${dest%%:*}"
  394. ssh_options=(-p "$port")
  395. else
  396. port=""
  397. dest="${dest%%:*}"
  398. fi
  399. info "You can run this following command from an host having admin access to $dest:"
  400. echo " (Or send it to a backup admin of $dest)" >&2
  401. ## We remove ending label (label will be added or not in the
  402. ## private key, and thus here, depending on the version of
  403. ## openssh-client)
  404. public_key=$(ssh-keygen -y -f <(e "$private_key"$'\n') | sed -r 's/ [^ ]+@[^ ]+$//')
  405. echo "ssh ${ssh_options[@]} myadmin@$dest ssh-key add '$public_key compose@$host'"
  406. }
  407. backup-action() {
  408. local action="$1"
  409. shift
  410. vps_type=$(vps:get-type) || {
  411. err "Failed to get type of installation."
  412. return 1
  413. }
  414. if ! fn.exists "${vps_type}:${action}"; then
  415. err "type '${vps_type}' has no ${vps_type}:${action} implemented yet."
  416. return 1
  417. fi
  418. "${vps_type}:${action}" "$@"
  419. }
  420. compose:get_default_backup_host_ident() {
  421. local service_name="$1" ## Optional
  422. local compose_file service_cfg cfg target
  423. compose_file=$(compose:get-compose-yml)
  424. service_name="${service_name:-rsync-backup}"
  425. if ! service_cfg=$(cat "$compose_file" |
  426. shyaml get-value -y "$service_name" 2>/dev/null); then
  427. err "No service named '$service_name' found in 'compose.yml'."
  428. return 1
  429. fi
  430. cfg=$(e "$service_cfg" | shyaml get-value -y options) || {
  431. err "No ${WHITE}options${NORMAL} in ${DARKYELLOW}$service_name${NORMAL}'s" \
  432. "entry in '$compose_file'."
  433. return 1
  434. }
  435. if ! target=$(e "$cfg" | shyaml get-value target); then
  436. err "No ${WHITE}options.target${NORMAL} in ${DARKYELLOW}$service_name${NORMAL}'s" \
  437. "entry in '$compose_file'."
  438. fi
  439. if ! target=$(e "$cfg" | shyaml get-value target); then
  440. err "No ${WHITE}options.target${NORMAL} in ${DARKYELLOW}$service_name${NORMAL}'s" \
  441. "entry in '$compose_file'."
  442. fi
  443. if ! ident=$(e "$cfg" | shyaml get-value ident); then
  444. err "No ${WHITE}options.ident${NORMAL} in ${DARKYELLOW}$service_name${NORMAL}'s" \
  445. "entry in '$compose_file'."
  446. fi
  447. echo "$target $ident"
  448. }
  449. mailcow:get_default_backup_host_ident() {
  450. local content cron_line ident found dest cmd_line
  451. if ! [ -e "/etc/cron.d/mirror-dir" ]; then
  452. err "No '/etc/cron.d/mirror-dir' found."
  453. return 1
  454. fi
  455. content=$(cat /etc/cron.d/mirror-dir) || {
  456. err "Can't read '/etc/cron.d/mirror-dir'."
  457. return 1
  458. }
  459. if ! cron_line=$(e "$content" | grep "mirror-dir backup"); then
  460. err "Can't find 'mirror-dir backup' line in '/etc/cron.d/mirror-dir'."
  461. return 1
  462. fi
  463. cron_line=${cron_line%|*}
  464. cmd_line=(${cron_line#*root})
  465. found=
  466. dest=
  467. for arg in "${cmd_line[@]}"; do
  468. [ -n "$found" ] && {
  469. dest="$arg"
  470. break
  471. }
  472. [ "$arg" == "-d" ] && {
  473. found=1
  474. }
  475. done
  476. if ! [[ "$dest" =~ ^[\'\"a-zA-Z0-9:/.-]+$ ]]; then
  477. err "Can't find valid destination in 'mirror-dir backup' arguments from '/etc/cron.d/mirror-dir'."
  478. return 1
  479. fi
  480. if [[ "$dest" == \"*\" ]] || [[ "$dest" == \'*\' ]]; then
  481. ## unquoting, the eval should be safe because of previous check
  482. dest=$(eval e "$dest")
  483. fi
  484. if [ -z "$dest" ]; then
  485. err "Can't find destination in 'mirror-dir backup' arguments from '/etc/cron.d/mirror-dir'."
  486. return 1
  487. fi
  488. ## looking for ident
  489. found=
  490. ident=
  491. for arg in "${cmd_line[@]}"; do
  492. [ -n "$found" ] && {
  493. ident="$arg"
  494. break
  495. }
  496. [ "$arg" == "-h" ] && {
  497. found=1
  498. }
  499. done
  500. if ! [[ "$ident" =~ ^[\'\"a-zA-Z0-9.-]+$ ]]; then
  501. err "Can't find valid identifier in 'mirror-dir backup' arguments from '/etc/cron.d/mirror-dir'."
  502. return 1
  503. fi
  504. if [[ "$ident" == \"*\" ]] || [[ "$ident" == \'*\' ]]; then
  505. ## unquoting, the eval should be safe because of previous check
  506. ident=$(eval e "$ident")
  507. fi
  508. if [ -z "$ident" ]; then
  509. err "Can't find destination in 'mirror-dir backup' arguments from '/etc/cron.d/mirror-dir'."
  510. return 1
  511. fi
  512. echo "$dest $ident"
  513. }
  514. compose:service:containers() {
  515. local project="$1" service="$2"
  516. docker ps \
  517. --filter label="com.docker.compose.project=$project" \
  518. --filter label="compose.master-service=$service" \
  519. --format="{{.ID}}"
  520. }
  521. export -f compose:service:containers
  522. compose:service:container_one() {
  523. local project="$1" service="$2" container_id
  524. {
  525. read-0a container_id || {
  526. err "service ${DARKYELLOW}$service${NORMAL} has no running container."
  527. return 1
  528. }
  529. if read-0a _; then
  530. err "service ${DARKYELLOW}$service${NORMAL} has more than one running container."
  531. return 1
  532. fi
  533. } < <(compose:service:containers "$project" "$service")
  534. echo "$container_id"
  535. }
  536. export -f compose:service:container_one
  537. compose:service:container_first() {
  538. local project="$1" service="$2" container_id
  539. {
  540. read-0a container_id || {
  541. err "service ${DARKYELLOW}$service${NORMAL} has no running container."
  542. return 1
  543. }
  544. if read-0a _; then
  545. warn "service ${DARKYELLOW}$service${NORMAL} has more than one running container."
  546. fi
  547. } < <(compose:service:containers "$project" "$service")
  548. echo "$container_id"
  549. }
  550. export -f compose:service:container_first
  551. docker:running_containers() {
  552. :cache: scope=session
  553. docker ps --format="{{.ID}}"
  554. }
  555. decorator._mangle_fn docker:running_containers
  556. export -f docker:running_containers
  557. compose:project:containers() {
  558. local project="$1" opts
  559. opts+=(--filter label="com.docker.compose.project=$project")
  560. docker ps "${opts[@]}" \
  561. --format="{{.ID}}"
  562. }
  563. export -f compose:project:containers
  564. compose:charm:containers() {
  565. local project="$1" charm="$2"
  566. docker ps \
  567. --filter label="com.docker.compose.project=$project" \
  568. --filter label="compose.charm=$charm" \
  569. --format="{{.ID}}"
  570. }
  571. export -f compose:charm:containers
  572. compose:charm:container_one() {
  573. local project="$1" charm="$2" container_id
  574. {
  575. read-0a container_id || {
  576. err "charm ${DARKPINK}$charm${NORMAL} has no running container in project '$project'."
  577. return 1
  578. }
  579. if read-0a _; then
  580. err "charm ${DARKPINK}$charm${NORMAL} has more than one running container."
  581. return 1
  582. fi
  583. } < <(compose:charm:containers "$project" "$charm")
  584. echo "$container_id"
  585. }
  586. export -f compose:charm:container_one
  587. compose:charm:container_first() {
  588. local project="$1" charm="$2" container_id
  589. {
  590. read-0a container_id || {
  591. warn "charm ${DARKYELLOW}$charm${NORMAL} has no running container in project '$project'."
  592. }
  593. if read-0a _; then
  594. warn "charm ${DARKYELLOW}$charm${NORMAL} has more than one running container."
  595. fi
  596. } < <(compose:charm:containers "$project" "$charm")
  597. echo "$container_id"
  598. }
  599. export -f compose:charm:container_first
  600. compose:get_url() {
  601. local project_name="$1" service="$2" data_file network ip
  602. data_dir=("/var/lib/compose/relations/${project_name}/${service}-"*"/web-proxy")
  603. if [ "${#data_dir[@]}" -gt 1 ]; then
  604. err "More than one web-proxy relation." \
  605. "Current 'vps' algorithm is insufficient" \
  606. "to figure out which relation is concerned"
  607. return 1
  608. fi
  609. data_file="${data_dir[0]}/data"
  610. if [ -d "${data_file%/*}" ]; then
  611. (
  612. set -o pipefail
  613. ## users can't cat directly the content
  614. docker run --rm \
  615. -v "${data_file%/*}":/tmp/dummy alpine \
  616. cat "/tmp/dummy/${data_file##*/}" |
  617. shyaml get-value url
  618. )
  619. else
  620. ## Assume there are no frontend relation here, the url is direct IP
  621. container_id=$(compose:service:container_one "${project_name}" "${service}") || return 1
  622. network_ip=$(docker:container:network_ip_one "${container_id}") || return 1
  623. IFS=":" read -r network ip <<<"$network_ip"
  624. tcp_port=
  625. for port in $(docker:exposed_ports "$container_id"); do
  626. IFS="/" read port type <<<"$port"
  627. [ "$type" == "tcp" ] || continue
  628. tcp_port="$port"
  629. break
  630. done
  631. echo -n "http://$ip"
  632. [ -n "$tcp_port" ] && echo ":$tcp_port"
  633. fi || {
  634. err "Failed querying ${service} to frontend relation to get url."
  635. return 1
  636. }
  637. }
  638. export -f compose:get_url
  639. compose:container:service() {
  640. local container="$1" service
  641. if ! service=$(docker:container:label "$container" "compose.service"); then
  642. err "Failed to get service name from container ${container}."
  643. return 1
  644. fi
  645. if [ -z "$service" ]; then
  646. err "No service found for container ${container}."
  647. return 1
  648. fi
  649. echo "$service"
  650. }
  651. export -f compose:container:service
  652. compose:psql() {
  653. local project_name="$1" dbname="$2" container_id
  654. shift 2
  655. container_id=$(compose:charm:container_one "$project_name" "postgres") || return 1
  656. docker exec -i "${container_id}" psql -U postgres "$dbname" "$@"
  657. }
  658. export -f compose:psql
  659. compose:mongo() {
  660. local project_name="$1" dbname="$2" container_id
  661. container_id=$(compose:charm:container_one "$project_name" "mongo") || return 1
  662. docker exec -i "${container_id}" mongo --quiet "$dbname"
  663. }
  664. export -f compose:mongo
  665. compose:pgm() {
  666. local project_name="$1" container_network_ip container_ip container_network
  667. shift
  668. container_id=$(compose:charm:container_one "$project_name" "postgres") || return 1
  669. service_name=$(compose:container:service "$container_id") || return 1
  670. image_id=$(docker:container:image "$container_id") || return 1
  671. container_network_ip=$(docker:container:network_ip_one "$container_id") || return 1
  672. IFS=":" read -r container_network container_ip <<<"$container_network_ip"
  673. pgpass="/srv/datastore/data/${service_name}/var/lib/postgresql/data/pgpass"
  674. local final_pgm_docker_run_opts+=(
  675. -u 0 -e prefix_pg_local_command=" "
  676. --network "${container_network}"
  677. -e PGHOST="$container_ip"
  678. -e PGUSER=postgres
  679. -v "$pgpass:/root/.pgpass"
  680. "${pgm_docker_run_opts[@]}"
  681. )
  682. cmd=(docker run --rm \
  683. "${final_pgm_docker_run_opts[@]}" \
  684. "${image_id}" pgm "$@"
  685. )
  686. echo "${cmd[@]}"
  687. "${cmd[@]}"
  688. }
  689. export -f compose:pgm
  690. postgres:dump() {
  691. local project_name="$1" src="$2" dst="$3"
  692. (
  693. settmpdir PGM_TMP_LOCATION
  694. pgm_docker_run_opts=('-v' "${PGM_TMP_LOCATION}:/tmp/dump")
  695. compose:pgm "$project_name" cp -f "$src" "/tmp/dump/dump.gz" &&
  696. mv "$PGM_TMP_LOCATION/dump.gz" "$dst"
  697. ) || return 1
  698. }
  699. export -f postgres:dump
  700. postgres:restore() {
  701. local project_name="$1" src="$2" dst="$3"
  702. full_src_path=$(readlink -e "$src") || exit 1
  703. (
  704. pgm_docker_run_opts=('-v' "${full_src_path}:/tmp/dump.gz")
  705. compose:pgm "$project_name" cp -f "/tmp/dump.gz" "$dst"
  706. ) || return 1
  707. }
  708. export -f postgres:restore
  709. odoo:get_public_user_id() {
  710. local project_name="$1" dbname="$2"
  711. echo "select res_id from ir_model_data where model = 'res.users' and name = 'public_user';" |
  712. compose:psql "$project_name" "$dbname" -qAt
  713. }
  714. cyclos:set_root_url() {
  715. local project_name="$1" dbname="$2" url="$3"
  716. echo "UPDATE configurations SET root_url = '$url';" |
  717. compose:psql "$project_name" "$dbname" || {
  718. err "Failed to set cyclos url value in '$dbname' database."
  719. return 1
  720. }
  721. }
  722. export -f cyclos:set_root_url
  723. cyclos:unlock() {
  724. local project_name="$1" dbname="$2"
  725. echo "delete from database_lock;" |
  726. compose:psql "${project_name}" "${dbname}"
  727. }
  728. export -f cyclos:unlock
  729. rocketchat:drop-indexes() {
  730. local project_name="$1" dbname="$2"
  731. echo "db.users.dropIndexes()" |
  732. compose:mongo "${project_name}" "${dbname}"
  733. }
  734. export -f rocketchat:drop-indexes
  735. compose:project_name() {
  736. if [ -z "$PROJECT_NAME" ]; then
  737. PROJECT_NAME=$(compose --get-project-name) || {
  738. err "Couldn't get project name."
  739. return 1
  740. }
  741. if [ -z "$PROJECT_NAME" -o "$PROJECT_NAME" == "orphan" ]; then
  742. err "Couldn't get project name, probably because 'compose.yml' wasn't found."
  743. echo " Please ensure to either configure a global 'compose.yml' or run this command" >&2
  744. echo " in a compose project (with 'compose.yml' on the top level directory)." >&2
  745. return 1
  746. fi
  747. export PROJECT_NAME
  748. fi
  749. echo "$PROJECT_NAME"
  750. }
  751. export -f compose:project_name
  752. compose:get_cron_docker_cmd() {
  753. local cron_line cmd_line docker_cmd
  754. project_name=$(compose:project_name) || return 1
  755. container=$(compose:service:containers "${project_name}" "cron") || {
  756. err "Can't find service 'cron' in project ${project_name}."
  757. return 1
  758. }
  759. if docker exec "$container" test -e /etc/cron.d/rsync-backup; then
  760. if ! cron_line=$(docker exec "${project_name}"_cron_1 cat /etc/cron.d/rsync-backup | grep "\* \* \*"); then
  761. err "Can't find cron_line in cron container."
  762. return 1
  763. fi
  764. elif docker exec "$container" test -e /etc/crontabs/root; then
  765. if ! cron_line=$(docker exec "$container" cat /etc/crontabs/root | grep " launch-rsync-backup " | grep "\* \* \*"); then
  766. err "Can't find cron_line in cron container."
  767. return 1
  768. fi
  769. else
  770. err "Unrecognized cron container:"
  771. echo " Can't find neither:" >&2
  772. echo " - /etc/cron.d/rsync-backup for old-style cron services" >&2
  773. echo " - nor /etc/crontabs/root for new-style cron services." >&2
  774. return 1
  775. fi
  776. cron_line=${cron_line%|*}
  777. cron_line=${cron_line%"2>&1"*}
  778. cmd_line="${cron_line#*root}"
  779. eval "args=($cmd_line)"
  780. ## should be last argument
  781. docker_cmd=$(echo ${args[@]: -1})
  782. if ! [[ "$docker_cmd" == "docker run --rm -e "* ]]; then
  783. echo "docker command found should start with 'docker run'." >&2
  784. echo "Here's command:" >&2
  785. echo " $docker_cmd" >&2
  786. return 1
  787. fi
  788. e "$docker_cmd"
  789. }
  790. compose:recover-target() {
  791. local backup_host="$1" ident="$2" src="$3" dst="$4" service_name="${5:-rsync-backup}" project_name
  792. project_name=$(compose:project_name) || return 1
  793. docker_image="${project_name}_${service_name}"
  794. if ! docker_has_image "$docker_image"; then
  795. compose build "${service_name}" || {
  796. err "Couldn't find nor build image for service '$service_name'."
  797. return 1
  798. }
  799. fi
  800. dst="${dst%/}" ## remove final slash
  801. ssh_options=(-o StrictHostKeyChecking=no)
  802. if [[ "$backup_host" == *":"* ]]; then
  803. port="${backup_host##*:}"
  804. backup_host="${backup_host%%:*}"
  805. ssh_options+=(-p "$port")
  806. else
  807. port=""
  808. backup_host="${backup_host%%:*}"
  809. fi
  810. rsync_opts=(
  811. -e "ssh ${ssh_options[*]} -i /var/lib/rsync/.ssh/id_rsa -l rsync"
  812. -azvArH --delete --delete-excluded
  813. --partial --partial-dir .rsync-partial
  814. --numeric-ids
  815. )
  816. if [ "$DRY_RUN" ]; then
  817. rsync_opts+=("-n")
  818. fi
  819. cmd=(
  820. docker run --rm --entrypoint rsync \
  821. -v "/srv/datastore/config/${service_name}/var/lib/rsync":/var/lib/rsync \
  822. -v "${dst%/*}":/mnt/dest \
  823. "$docker_image" \
  824. "${rsync_opts[@]}" "$backup_host":"/var/mirror/$ident/$src" "/mnt/dest/${dst##*/}"
  825. )
  826. echo "${WHITE}Launching: ${NORMAL} ${cmd[@]}"
  827. "${cmd[@]}"
  828. }
  829. mailcow:recover-target() {
  830. local backup_host="$1" ident="$2" src="$3" dst="$4"
  831. dst="${dst%/}" ## remove final slash
  832. ssh_options=(-o StrictHostKeyChecking=no)
  833. if [[ "$backup_host" == *":"* ]]; then
  834. port="${backup_host##*:}"
  835. backup_host="${backup_host%%:*}"
  836. ssh_options+=(-p "$port")
  837. else
  838. port=""
  839. backup_host="${backup_host%%:*}"
  840. fi
  841. rsync_opts=(
  842. -e "ssh ${ssh_options[*]} -i /var/lib/rsync/.ssh/id_rsa -l rsync"
  843. -azvArH --delete --delete-excluded
  844. --partial --partial-dir .rsync-partial
  845. --numeric-ids
  846. )
  847. if [ "$DRY_RUN" ]; then
  848. rsync_opts+=("-n")
  849. fi
  850. cmd=(
  851. rsync "${rsync_opts[@]}" "$backup_host":"/var/mirror/$ident/$src" "${dst}"
  852. )
  853. echo "${WHITE}Launching: ${NORMAL} ${cmd[@]}"
  854. "${cmd[@]}"
  855. }
  856. nextcloud:src:version() {
  857. local version
  858. if ! version=$(cat "/srv/datastore/data/${nextcloud_service}/var/www/html/version.php" 2>/dev/null); then
  859. err "Can't find version.php file to get last version installed."
  860. exit 1
  861. fi
  862. version=$(e "$version" | grep 'VersionString =' | cut -f 3 -d ' ' | cut -f 2 -d "'")
  863. if [ -z "$version" ]; then
  864. err "Can't figure out version from version.php content."
  865. exit 1
  866. fi
  867. echo "$version"
  868. }
  869. container:health:check-fix:container-aliveness() {
  870. local container_id="$1"
  871. timeout 5s docker inspect "$container_id" >/dev/null 2>&1
  872. errlvl=$?
  873. if [ "$errlvl" == 124 ]; then
  874. service_name=$(docker ps --filter id="$container_id" --format '{{.Label "com.docker.compose.service"}}')
  875. container_name=($(docker ps --filter id="$container_id" --format '{{.Names}}'))
  876. pid=$(ps ax -o pid,command -ww | grep docker-containerd-shim |
  877. grep "/$container_id" |
  878. sed -r 's/^ *//g' |
  879. cut -f 1 -d " ")
  880. if [ -z "$pid" ]; then
  881. err "container ${DARKCYAN}${container_name[0]}${NORMAL} for ${DARKYELLOW}$service_name${NORMAL} doesn't answer to 'inspect' command. Can't find its PID neither."
  882. return 1
  883. fi
  884. echo "container ${DARKCYAN}${container_name[0]}${NORMAL} for ${DARKYELLOW}$service_name${NORMAL} doesn't answer to 'inspect' command (pid: $pid)."
  885. Wrap -d "kill pid $pid and restart" <<EOF
  886. kill "$pid"
  887. sleep 2
  888. docker restart "$container_id"
  889. EOF
  890. fi
  891. return $errlvl
  892. }
  893. container:health:check-fix:no-matching-entries() {
  894. local container_id="$1"
  895. out=$(docker exec "$container_id" echo 2>&1)
  896. errlvl=$?
  897. [ "$errlvl" == 0 ] && return 0
  898. service_name=$(docker ps --filter id="$container_id" --format '{{.Label "com.docker.compose.service"}}')
  899. container_name=($(docker ps --filter id="$container_id" --format '{{.Names}}'))
  900. if [ "$errlvl" == 126 ] && [[ "$out" == *"no matching entries in passwd file"* ]]; then
  901. echo "container ${DARKCYAN}${container_name[0]}${NORMAL} for ${DARKYELLOW}$service_name${NORMAL} has ${DARKRED}no-matching-entries${NORMAL} bug." >&2
  902. Wrap -d "restarting container of ${DARKYELLOW}$service_name${NORMAL} twice" <<EOF
  903. docker restart "$container_id"
  904. sleep 2
  905. docker restart "$container_id"
  906. EOF
  907. return 2
  908. fi
  909. warn "Unknown issue with ${DARKYELLOW}$service_name${NORMAL}'s container:"
  910. echo " ${WHITE}cmd:${NORMAL} docker exec -ti $container_id echo" >&2
  911. echo "$out" | prefix " ${DARKGRAY}|${NORMAL} " >&2
  912. echo " ${DARKGRAY}..${NORMAL} leaving this as-is."
  913. return 1
  914. }
  915. docker:api() {
  916. local endpoint="$1"
  917. curl -sS --unix-socket /var/run/docker.sock "http://localhost$endpoint"
  918. }
  919. docker:containers:id() {
  920. docker:api /containers/json | jq -r ".[] | .Id"
  921. }
  922. docker:containers:names() {
  923. docker:api /containers/json | jq -r '.[] | .Names[0] | ltrimstr("/")'
  924. }
  925. docker:container:stats() {
  926. container="$1"
  927. docker:api "/containers/$container/stats?stream=false"
  928. }
  929. docker:containers:stats() {
  930. :cache: scope=session
  931. local jobs='' line container id_names sha names name data service project
  932. local DC="com.docker.compose"
  933. local PSF_values=(
  934. ".ID" ".Names" ".Label \"$DC.project\"" ".Label \"$DC.service\"" ".Image"
  935. )
  936. local PSF="$(printf "{{%s}} " "${PSF_values[@]}")"
  937. id_names=$(docker ps -a --format="$PSF") || return 1
  938. ## Create a docker container table from name/sha to service, project, image_name
  939. declare -A resolve
  940. while read-0a line; do
  941. sha=${line%% *}; line=${line#* }
  942. names=${line%% *}; line=${line#* }
  943. names=(${names//,/ })
  944. for name in "${names[@]}"; do
  945. resolve["$name"]="$line"
  946. done
  947. resolve["$sha"]="$line"
  948. done < <(printf "%s\n" "$id_names")
  949. declare -A data
  950. while read-0a line; do
  951. name=${line%% *}; line=${line#* }
  952. ts=${line%% *}; line=${line#* }
  953. resolved="${resolve["$name"]}"
  954. project=${resolved%% *}; resolved=${resolved#* }
  955. service=${resolved%% *}; resolved=${resolved#* }
  956. image_name="$resolved"
  957. if [ -z "$service" ]; then
  958. project="@"
  959. service=$(docker inspect "$image_name" | jq -r '.[0].RepoTags[0]')
  960. service=${service//\//_}
  961. fi
  962. if [ -n "${data["$project/$service"]}" ]; then
  963. previous=(${data["$project/$service"]})
  964. previous=(${previous[@]:1})
  965. current=($line)
  966. sum=()
  967. i=0; max=${#previous[@]}
  968. while (( i < max )); do
  969. sum+=($((${previous[$i]} + ${current[$i]})))
  970. ((i++))
  971. done
  972. data["$project/$service"]="$ts ${sum[*]}"
  973. else
  974. data["$project/$service"]="$ts $line"
  975. fi
  976. done < <(
  977. for container in "$@"; do
  978. (
  979. docker:container:stats "${container}" |
  980. jq -r '
  981. (.name | ltrimstr("/"))
  982. + " " + (.read | sub("\\.[0-9]+Z"; "Z") | fromdate | tostring)
  983. + " " + (.memory_stats.usage | tostring)
  984. + " " + (.memory_stats.stats.inactive_file | tostring)
  985. + " " + ((.memory_stats.usage - .memory_stats.stats.inactive_file) | tostring)
  986. + " " + (.memory_stats.limit | tostring)
  987. + " " + (.networks.eth0.rx_bytes | tostring)
  988. + " " + (.networks.eth0.rx_packets | tostring)
  989. + " " + (.networks.eth0.rx_errors | tostring)
  990. + " " + (.networks.eth0.rx_dropped | tostring)
  991. + " " + (.networks.eth0.tx_bytes | tostring)
  992. + " " + (.networks.eth0.tx_packets | tostring)
  993. + " " + (.networks.eth0.tx_errors | tostring)
  994. + " " + (.networks.eth0.tx_dropped | tostring)
  995. '
  996. ) &
  997. jobs=1
  998. done
  999. [ -n "$jobs" ] && wait
  1000. )
  1001. for label in "${!data[@]}"; do
  1002. echo "$label ${data[$label]}"
  1003. done
  1004. }
  1005. decorator._mangle_fn docker:containers:stats
  1006. export -f docker:containers:stats
  1007. col:normalize:size() {
  1008. local alignment=$1
  1009. awk -v alignment="$alignment" '{
  1010. # Store the entire line in the lines array.
  1011. lines[NR] = $0;
  1012. # Split the line into fields.
  1013. split($0, fields);
  1014. # Update max for each field.
  1015. for (i = 1; i <= length(fields); i++) {
  1016. if (length(fields[i]) > max[i]) {
  1017. max[i] = length(fields[i]);
  1018. }
  1019. }
  1020. }
  1021. END {
  1022. # Print lines with fields padded to max.
  1023. for (i = 1; i <= NR; i++) {
  1024. split(lines[i], fields);
  1025. line = "";
  1026. for (j = 1; j <= length(fields); j++) {
  1027. # Get alignment for the current field.
  1028. align = substr(alignment, j, 1);
  1029. if (align != "+") {
  1030. align = "-"; # Default to left alignment if not "+".
  1031. }
  1032. line = line sprintf("%" align max[j] "s ", fields[j]);
  1033. }
  1034. print line;
  1035. }
  1036. }'
  1037. }
  1038. rrd:create() {
  1039. local prefix="$1"
  1040. shift
  1041. local label="$1" step="300" src_def
  1042. shift
  1043. if [ -z "$VAR_DIR" ]; then
  1044. err "Unset \$VAR_DIR, can't create rrd graph"
  1045. return 1
  1046. fi
  1047. mkdir -p "$VAR_DIR"
  1048. if ! [ -d "$VAR_DIR" ]; then
  1049. err "Invalid \$VAR_DIR: '$VAR_DIR' is not a directory"
  1050. return 1
  1051. fi
  1052. if ! type -p rrdtool >/dev/null 2>&1; then
  1053. apt-get install rrdtool -y --force-yes </dev/null
  1054. if ! type -p rrdtool 2>/dev/null 2>&1; then
  1055. err "Couldn't find nor install 'rrdtool'."
  1056. return 1
  1057. fi
  1058. fi
  1059. local RRD_PATH="$VAR_DIR/rrd"
  1060. local RRD_FILE="$RRD_PATH/$prefix/$label.rrd"
  1061. mkdir -p "${RRD_FILE%/*}"
  1062. if [ -f "$RRD_FILE" ]; then
  1063. err "File '$RRD_FILE' already exists, use a different label."
  1064. return 1
  1065. fi
  1066. local rrd_ds_opts=()
  1067. for src_def in "$@"; do
  1068. IFS=":" read -r name type min max rra_types <<<"$src_def"
  1069. rra_types=${rra_types:-average,max,min}
  1070. rrd_ds_opts+=("DS:$name:$type:900:$min:$max")
  1071. done
  1072. local step=120
  1073. local times=( ## with steps 120 is 2mn datapoint
  1074. 2m:1w
  1075. 6m:3w
  1076. 30m:12w
  1077. 3h:1y
  1078. 1d:10y
  1079. 1w:2080w
  1080. )
  1081. rrd_rra_opts=()
  1082. for time in "${times[@]}"; do
  1083. rrd_rra_opts+=("RRA:"{AVERAGE,MIN,MAX}":0.5:$time")
  1084. done
  1085. cmd=(
  1086. rrdtool create "$RRD_FILE" \
  1087. --step "$step" \
  1088. "${rrd_ds_opts[@]}" \
  1089. "${rrd_rra_opts[@]}"
  1090. )
  1091. "${cmd[@]}" || {
  1092. err "Failed command: ${cmd[@]}"
  1093. return 1
  1094. }
  1095. }
  1096. rrd:update() {
  1097. local prefix="$1"
  1098. shift
  1099. while read-0a data; do
  1100. [ -z "$data" ] && continue
  1101. IFS="~" read -ra data <<<"${data// /\~}"
  1102. label="${data[0]}"
  1103. ts="${data[1]}"
  1104. for arg in "$@"; do
  1105. IFS="|" read -r name arg <<<"$arg"
  1106. rrd_label="${label}/${name}"
  1107. rrd_create_opt=()
  1108. rrd_update_opt="$ts"
  1109. for col_def in ${arg//,/ }; do
  1110. col=${col_def%%:*}; create_def=${col_def#*:}
  1111. rrd_update_opt="${rrd_update_opt}:${data[$col]}"
  1112. rrd_create_opt+=("$create_def")
  1113. done
  1114. local RRD_ROOT_PATH="$VAR_DIR/rrd"
  1115. local RRD_PATH="$RRD_ROOT_PATH/${prefix%/}"
  1116. local RRD_FILE="${RRD_PATH%/}/${rrd_label#/}.rrd"
  1117. if ! [ -f "$RRD_FILE" ]; then
  1118. info "Creating new RRD file '${RRD_FILE#$RRD_ROOT_PATH/}'"
  1119. if ! rrd:create "$prefix" "${rrd_label}" "${rrd_create_opt[@]}" </dev/null ; then
  1120. err "Couldn't create new RRD file ${rrd_label} with options: '${rrd_create_opt[*]}'"
  1121. return 1
  1122. fi
  1123. fi
  1124. rrdtool update "$RRD_FILE" "$rrd_update_opt" || {
  1125. err "update failed with options: '$rrd_update_opt'"
  1126. return 1
  1127. }
  1128. done
  1129. done
  1130. }
  1131. [ "$SOURCED" ] && return 0
  1132. ##
  1133. ## Command line processing
  1134. ##
  1135. cmdline.spec.gnu
  1136. cmdline.spec.reporting
  1137. cmdline.spec.gnu install
  1138. cmdline.spec::cmd:install:run() {
  1139. :
  1140. }
  1141. cmdline.spec.gnu get-type
  1142. cmdline.spec::cmd:get-type:run() {
  1143. vps:get-type
  1144. }
  1145. cmdline.spec:install:cmd:backup:run() {
  1146. : :posarg: BACKUP_SERVER 'Target backup server'
  1147. : :optfla: --ignore-domain-check \
  1148. "Allow to bypass the domain check in
  1149. compose file (only used in compose
  1150. installation)."
  1151. : :optfla: --ignore-ping-check "Allow to bypass the ping check of host."
  1152. local vps_type
  1153. vps_type=$(vps:get-type) || {
  1154. err "Failed to get type of installation."
  1155. return 1
  1156. }
  1157. if ! fn.exists "${vps_type}:install-backup"; then
  1158. err "type '${vps_type}' has no backup installation implemented yet."
  1159. return 1
  1160. fi
  1161. opts=()
  1162. [ "$opt_ignore_ping_check" ] &&
  1163. opts+=("--ignore-ping-check")
  1164. if [ "$vps_type" == "compose" ]; then
  1165. [ "$opt_ignore_domain_check" ] &&
  1166. opts+=("--ignore-domain-check")
  1167. fi
  1168. "cmdline.spec:install:cmd:${vps_type}-backup:run" "${opts[@]}" "$BACKUP_SERVER"
  1169. }
  1170. DEFAULT_BACKUP_SERVICE_NAME=rsync-backup
  1171. cmdline.spec.gnu compose-backup
  1172. cmdline.spec:install:cmd:compose-backup:run() {
  1173. : :posarg: BACKUP_SERVER 'Target backup server'
  1174. : :optval: --service-name,-s "YAML service name in compose
  1175. file to check for existence of key.
  1176. Defaults to '$DEFAULT_BACKUP_SERVICE_NAME'"
  1177. : :optval: --compose-file,-f "Compose file location. Defaults to
  1178. the value of '\$DEFAULT_COMPOSE_FILE'"
  1179. : :optfla: --ignore-domain-check \
  1180. "Allow to bypass the domain check in
  1181. compose file."
  1182. : :optfla: --ignore-ping-check "Allow to bypass the ping check of host."
  1183. local service_name compose_file
  1184. [ -e "/etc/compose/local.conf" ] && source /etc/compose/local.conf
  1185. compose_file=${opt_compose_file:-$DEFAULT_COMPOSE_FILE}
  1186. service_name=${opt_service_name:-$DEFAULT_BACKUP_SERVICE_NAME}
  1187. if ! [ -e "$compose_file" ]; then
  1188. err "Compose file not found in '$compose_file'."
  1189. return 1
  1190. fi
  1191. compose:install-backup "$BACKUP_SERVER" "$service_name" "$compose_file" \
  1192. "$opt_ignore_ping_check" "$opt_ignore_domain_check"
  1193. }
  1194. cmdline.spec:install:cmd:mailcow-backup:run() {
  1195. : :posarg: BACKUP_SERVER 'Target backup server'
  1196. : :optfla: --ignore-ping-check "Allow to bypass the ping check of host."
  1197. "mailcow:install-backup" "$BACKUP_SERVER" "$opt_ignore_ping_check"
  1198. }
  1199. cmdline.spec.gnu backup
  1200. cmdline.spec::cmd:backup:run() {
  1201. local vps_type
  1202. vps_type=$(vps:get-type) || {
  1203. err "Failed to get type of installation."
  1204. return 1
  1205. }
  1206. if ! fn.exists "cmdline.spec:backup:cmd:${vps_type}:run"; then
  1207. err "type '${vps_type}' has no backup process implemented yet."
  1208. return 1
  1209. fi
  1210. "cmdline.spec:backup:cmd:${vps_type}:run"
  1211. }
  1212. cmdline.spec:backup:cmd:mailcow:run() {
  1213. local cmd_line cron_line cmd
  1214. for f in mysql-backup mirror-dir; do
  1215. [ -e "/etc/cron.d/$f" ] || {
  1216. err "Can't find '/etc/cron.d/$f'."
  1217. echo " Have you forgotten to run 'vps install backup BACKUP_HOST' ?" >&2
  1218. return 1
  1219. }
  1220. if ! cron_line=$(cat "/etc/cron.d/$f" |
  1221. grep -v "^#" | grep "\* \* \*"); then
  1222. err "Can't find cron_line in '/etc/cron.d/$f'." \
  1223. "Have you modified it ?"
  1224. return 1
  1225. fi
  1226. cron_line=${cron_line%|*}
  1227. cmd_line=(${cron_line#*root})
  1228. if [ "$f" == "mirror-dir" ]; then
  1229. cmd=()
  1230. for arg in "${cmd_line[@]}"; do
  1231. [ "$arg" != "-q" ] && cmd+=("$arg")
  1232. done
  1233. else
  1234. cmd=("${cmd_line[@]}")
  1235. fi
  1236. code="${cmd[*]}"
  1237. echo "${WHITE}Launching:${NORMAL} ${code}"
  1238. {
  1239. {
  1240. (
  1241. ## Some commands are using colors that are already
  1242. ## set by this current program and will trickle
  1243. ## down unwantedly
  1244. ansi_color no
  1245. eval "${code}"
  1246. ) | sed -r "s/^/ ${GRAY}|${NORMAL} /g"
  1247. set_errlvl "${PIPESTATUS[0]}"
  1248. } 3>&1 1>&2 2>&3 | sed -r "s/^/ $DARKRED\!$NORMAL /g"
  1249. set_errlvl "${PIPESTATUS[0]}"
  1250. } 3>&1 1>&2 2>&3
  1251. if [ "$?" != "0" ]; then
  1252. err "Failed."
  1253. return 1
  1254. fi
  1255. done
  1256. info "Mysql backup and subsequent mirror-dir ${DARKGREEN}succeeded${NORMAL}."
  1257. }
  1258. set_errlvl() { return "${1:-1}"; }
  1259. cmdline.spec:backup:cmd:compose:run() {
  1260. local cron_line args
  1261. project_name=$(compose:project_name) || return 1
  1262. docker_cmd=$(compose:get_cron_docker_cmd) || return 1
  1263. echo "${WHITE}Launching:${NORMAL} docker exec -i "${project_name}_cron_1" $docker_cmd"
  1264. {
  1265. {
  1266. eval "docker exec -i \"${project_name}_cron_1\" $docker_cmd" | sed -r "s/^/ ${GRAY}|${NORMAL} /g"
  1267. set_errlvl "${PIPESTATUS[0]}"
  1268. } 3>&1 1>&2 2>&3 | sed -r "s/^/ $DARKRED\!$NORMAL /g"
  1269. set_errlvl "${PIPESTATUS[0]}"
  1270. } 3>&1 1>&2 2>&3
  1271. if [ "$?" != "0" ]; then
  1272. err "Failed."
  1273. return 1
  1274. fi
  1275. info "mirror-dir ${DARKGREEN}succeeded${NORMAL}."
  1276. }
  1277. cmdline.spec.gnu recover-target
  1278. cmdline.spec::cmd:recover-target:run() {
  1279. : :posarg: BACKUP_DIR 'Source directory on backup side'
  1280. : :posarg: HOST_DIR 'Target directory on host side'
  1281. : :optval: --backup-host,-B "The backup host"
  1282. : :optfla: --dry-run,-n "Don't do anything, instead tell what it
  1283. would do."
  1284. ## if no backup host take the one by default
  1285. backup_host="$opt_backup_host"
  1286. if [ -z "$backup_host" ]; then
  1287. backup_host_ident=$(backup-action get_default_backup_host_ident) || return 1
  1288. read -r backup_host ident <<<"$backup_host_ident"
  1289. fi
  1290. if [[ "$BACKUP_DIR" == /* ]]; then
  1291. err "BACKUP_DIR must be a relative path from the root of your backup."
  1292. return 1
  1293. fi
  1294. REAL_HOST_DIR=$(realpath "$HOST_DIR") || {
  1295. err "Can't find HOST_DIR '$HOST_DIR'."
  1296. return 1
  1297. }
  1298. export DRY_RUN="${opt_dry_run}"
  1299. backup-action recover-target "$backup_host" "$ident" "$BACKUP_DIR" "$REAL_HOST_DIR"
  1300. }
  1301. cmdline.spec.gnu odoo
  1302. cmdline.spec::cmd:odoo:run() {
  1303. :
  1304. }
  1305. cmdline.spec.gnu restart
  1306. cmdline.spec:odoo:cmd:restart:run() {
  1307. : :optval: --service,-s "The service (defaults to 'odoo')"
  1308. local out odoo_service
  1309. odoo_service="${opt_service:-odoo}"
  1310. project_name=$(compose:project_name) || return 1
  1311. if ! out=$(docker restart "${project_name}_${odoo_service}_1" 2>&1); then
  1312. if [[ "$out" == *"no matching entries in passwd file" ]]; then
  1313. warn "Catched docker bug. Restarting once more."
  1314. if ! out=$(docker restart "${project_name}_${odoo_service}_1"); then
  1315. err "Can't restart container ${project_name}_${odoo_service}_1 (restarted twice)."
  1316. echo " output:" >&2
  1317. echo "$out" | prefix " ${GRAY}|${NORMAL} " >&2
  1318. exit 1
  1319. fi
  1320. else
  1321. err "Couldn't restart container ${project_name}_${odoo_service}_1 (and no restart bug detected)."
  1322. exit 1
  1323. fi
  1324. fi
  1325. info "Container ${project_name}_${odoo_service}_1 was ${DARKGREEN}successfully${NORMAL} restarted."
  1326. }
  1327. cmdline.spec.gnu restore
  1328. cmdline.spec:odoo:cmd:restore:run() {
  1329. : :posarg: ZIP_DUMP_LOCATION 'Source odoo dump file to restore
  1330. (can be a local file or an url)'
  1331. : :optval: --service,-s "The service (defaults to 'odoo')"
  1332. : :optval: --database,-D 'Target database (default if not specified)'
  1333. : :optfla: --neutralize,-n "Restore database in neutralized state."
  1334. : :optfla: --debug,-d "Display more information."
  1335. local out
  1336. odoo_service="${opt_service:-odoo}"
  1337. if [[ "$ZIP_DUMP_LOCATION" == "http://"* ]] ||
  1338. [[ "$ZIP_DUMP_LOCATION" == "https://"* ]]; then
  1339. settmpdir ZIP_TMP_LOCATION
  1340. tmp_location="$ZIP_TMP_LOCATION/dump.zip"
  1341. curl -k -s -L "$ZIP_DUMP_LOCATION" > "$tmp_location" || {
  1342. err "Couldn't get '$ZIP_DUMP_LOCATION'."
  1343. exit 1
  1344. }
  1345. if [[ "$(dd if="$tmp_location" count=2 bs=1 2>/dev/null)" != "PK" ]]; then
  1346. err "Download doesn't seem to be a zip file."
  1347. dd if="$tmp_location" count=1 bs=256 | hd | prefix " ${GRAY}|${NORMAL} " >&2
  1348. exit 1
  1349. fi
  1350. info "Successfully downloaded '$ZIP_DUMP_LOCATION'"
  1351. echo " in '$tmp_location'." >&2
  1352. ZIP_DUMP_LOCATION="$tmp_location"
  1353. fi
  1354. [ -e "$ZIP_DUMP_LOCATION" ] || {
  1355. err "No file '$ZIP_DUMP_LOCATION' found." >&2
  1356. exit 1
  1357. }
  1358. opts_compose=()
  1359. [ -t 1 ] && opts_compose+=("--color")
  1360. [ "$opt_debug" ] && {
  1361. VERBOSE=1
  1362. opts_compose+=("--debug")
  1363. }
  1364. opts_load=()
  1365. [ "$opt_neutralize" ] && opts_load+=("--neutralize")
  1366. project_name=$(compose:project_name) || exit 1
  1367. container:health:check-fix:no-matching-entries "${project_name}_${odoo_service}_1"
  1368. case "$?" in
  1369. 0)
  1370. debug "Container ${project_name}_${odoo_service}_1 is healthy."
  1371. ;;
  1372. 1) err "Container ${project_name}_${odoo_service}_1 is not healthy."
  1373. exit 1
  1374. ;;
  1375. 2) info "Container ${project_name}_${odoo_service}_1 was fixed."
  1376. ;;
  1377. esac
  1378. msg_dbname=default
  1379. [ -n "$opt_database" ] && msg_dbname="'$opt_database'"
  1380. Wrap -vsd "drop $msg_dbname database of service ${DARKYELLOW}$odoo_service${NORMAL}" -- \
  1381. compose --no-hooks "${opts_compose[@]}" drop "$odoo_service" $opt_database || {
  1382. err "Error dropping $msg_dbname database of service ${DARKYELLOW}$odoo_service${NORMAL}:"
  1383. [ -z "$opt_debug" ] && {
  1384. echo " Use \`\`--debug\`\` (or \`\`-d\`\`) to get more information." >&2
  1385. }
  1386. exit 1
  1387. }
  1388. Wrap -vsd "restore $msg_dbname database of service ${DARKYELLOW}$odoo_service${NORMAL}" -- \
  1389. compose --no-hooks "${opts_compose[@]}" \
  1390. load "$odoo_service" $opt_database "${opts_load[@]}" < "$ZIP_DUMP_LOCATION" || {
  1391. err "Error restoring service ${DARKYELLOW}$odoo_service${NORMAL} to $msg_dbname database."
  1392. [ -z "$opt_debug" ] && {
  1393. echo " Use \`\`--debug\`\` (or \`\`-d\`\`) to get more information." >&2
  1394. }
  1395. exit 1
  1396. }
  1397. ## Restart odoo, ensure there is no bugs lingering on it.
  1398. cmdline.spec:odoo:cmd:restart:run --service "$odoo_service" || exit 1
  1399. }
  1400. cmdline.spec.gnu dump
  1401. cmdline.spec:odoo:cmd:dump:run() {
  1402. : :posarg: DUMP_ZIPFILE 'Target path to store odoo dump zip file.'
  1403. : :optval: --database,-d 'Target database (default if not specified)'
  1404. : :optval: --service,-s "The service (defaults to 'odoo')"
  1405. odoo_service="${opt_service:-odoo}"
  1406. msg_dbname=default
  1407. [ -n "$opt_database" ] && msg_dbname="'$opt_database'"
  1408. compose --no-hooks save "$odoo_service" $opt_database > "$DUMP_ZIPFILE" || {
  1409. err "Error dumping ${DARKYELLOW}$odoo_service${NORMAL}'s $msg_dbname database to '$DUMP_ZIPFILE'."
  1410. exit 1
  1411. }
  1412. info "Successfully dumped ${DARKYELLOW}$odoo_service${NORMAL}'s $msg_dbname database to '$DUMP_ZIPFILE'."
  1413. }
  1414. cmdline.spec.gnu drop
  1415. cmdline.spec:odoo:cmd:drop:run() {
  1416. : :optval: --database,-d 'Target database (default if not specified)'
  1417. : :optval: --service,-s "The service (defaults to 'odoo')"
  1418. odoo_service="${opt_service:-odoo}"
  1419. msg_dbname=default
  1420. [ -n "$opt_database" ] && msg_dbname="'$opt_database'"
  1421. compose --no-hooks drop "$odoo_service" $opt_database || {
  1422. err "Error dropping ${DARKYELLOW}$odoo_service${NORMAL}'s $msg_dbname database."
  1423. exit 1
  1424. }
  1425. info "Successfully dropped ${DARKYELLOW}$odoo_service${NORMAL}'s $msg_dbname database."
  1426. }
  1427. cmdline.spec.gnu set-cyclos-url
  1428. cmdline.spec:odoo:cmd:set-cyclos-url:run() {
  1429. : :optval: --database,-d "Target database ('odoo' if not specified)"
  1430. : :optval: --service,-s "The cyclos service name (defaults to 'cyclos')"
  1431. local URL
  1432. dbname=${opt_database:-odoo}
  1433. cyclos_service="${opt_service:-cyclos}"
  1434. project_name=$(compose:project_name) || exit 1
  1435. URL=$(compose:get_url "${project_name}" "${cyclos_service}") || exit 1
  1436. Wrap -d "set cyclos url to '$URL'" <<EOF || exit 1
  1437. echo "UPDATE res_company SET cyclos_server_url = '$URL/api' WHERE id=1;" |
  1438. compose:psql "$project_name" "$dbname" || {
  1439. err "Failed to set cyclos url value in '$dbname' database."
  1440. exit 1
  1441. }
  1442. EOF
  1443. }
  1444. cmdline.spec.gnu fix-sso
  1445. cmdline.spec:odoo:cmd:fix-sso:run() {
  1446. : :optval: --database,-d "Target database ('odoo' if not specified)"
  1447. local public_user_id project_name dbname
  1448. dbname=${opt_database:-odoo}
  1449. project_name=$(compose:project_name) || exit 1
  1450. public_user_id=$(odoo:get_public_user_id "${project_name}" "${dbname}") || exit 1
  1451. Wrap -d "fix website's object to 'public_user' (id=$public_user_id)" <<EOF || exit 1
  1452. echo "UPDATE website SET user_id = $public_user_id;" |
  1453. compose:psql "$project_name" "$dbname" || {
  1454. err "Failed to set website's object user_id to public user's id ($public_user_id) in '$dbname' database."
  1455. exit 1
  1456. }
  1457. EOF
  1458. }
  1459. cmdline.spec.gnu cyclos
  1460. cmdline.spec::cmd:cyclos:run() {
  1461. :
  1462. }
  1463. cmdline.spec:cyclos:cmd:dump:run() {
  1464. : :posarg: DUMP_GZFILE 'Target path to store odoo dump gz file.'
  1465. : :optval: --database,-d "Target database ('cyclos' if not specified)"
  1466. : :optval: --service,-s "The cyclos service name (defaults to 'cyclos')"
  1467. cyclos_service="${opt_service:-cyclos}"
  1468. cyclos_database="${opt_database:-cyclos}"
  1469. project_name=$(compose:project_name) || exit 1
  1470. container_id=$(compose:service:container_one "$project_name" "${cyclos_service}") || exit 1
  1471. Wrap -d "stop ${DARKYELLOW}${cyclos_service}${NORMAL}'s container" -- \
  1472. docker stop "$container_id" || exit 1
  1473. Wrap -d "Dump postgres database '${cyclos_database}'." -- \
  1474. postgres:dump "${project_name}" "$cyclos_database" "$DUMP_GZFILE" || exit 1
  1475. Wrap -d "start ${DARKYELLOW}${cyclos_service}${NORMAL}'s container" -- \
  1476. docker start "${container_id}" || exit 1
  1477. }
  1478. cmdline.spec.gnu restore
  1479. cmdline.spec:cyclos:cmd:restore:run() {
  1480. : :posarg: GZ_DUMP_LOCATION 'Source cyclos dump file to restore
  1481. (can be a local file or an url)'
  1482. : :optval: --service,-s "The service (defaults to 'cyclos')"
  1483. : :optval: --database,-d 'Target database (default if not specified)'
  1484. local out
  1485. cyclos_service="${opt_service:-cyclos}"
  1486. cyclos_database="${opt_database:-cyclos}"
  1487. project_name=$(compose:project_name) || exit 1
  1488. url=$(compose:get_url "${project_name}" "${cyclos_service}") || return 1
  1489. container_id=$(compose:service:container_one "$project_name" "${cyclos_service}") || exit 1
  1490. if [[ "$GZ_DUMP_LOCATION" == "http://"* ]] ||
  1491. [[ "$GZ_DUMP_LOCATION" == "https://"* ]]; then
  1492. settmpdir GZ_TMP_LOCATION
  1493. tmp_location="$GZ_TMP_LOCATION/dump.gz"
  1494. Wrap -d "get '$GZ_DUMP_LOCATION'" <<EOF || exit 1
  1495. ## Note that curll version before 7.76.0 do not have
  1496. curl -k -s -L "$GZ_DUMP_LOCATION" --fail \\
  1497. > "$tmp_location" || {
  1498. echo "Error fetching ressource. Is url correct ?" >&2
  1499. exit 1
  1500. }
  1501. if [[ "\$(dd if="$tmp_location" count=2 bs=1 2>/dev/null |
  1502. hexdump -v -e "/1 \"%02x\"")" != "1f8b" ]]; then
  1503. err "Download doesn't seem to be a gzip file."
  1504. dd if="$tmp_location" count=1 bs=256 | hd | prefix " ${GRAY}|${NORMAL} " >&2
  1505. exit 1
  1506. fi
  1507. EOF
  1508. GZ_DUMP_LOCATION="$tmp_location"
  1509. fi
  1510. [ -e "$GZ_DUMP_LOCATION" ] || {
  1511. err "No file '$GZ_DUMP_LOCATION' found." >&2
  1512. exit 1
  1513. }
  1514. Wrap -d "stop ${DARKYELLOW}${cyclos_service}${NORMAL}'s container" -- \
  1515. docker stop "$container_id" || exit 1
  1516. ## XXXvlab: making the assumption that the postgres username should
  1517. ## be the same as the cyclos service selected (which is the default,
  1518. ## but not always the case).
  1519. Wrap -d "restore postgres database '${cyclos_database}'." -- \
  1520. postgres:restore "$project_name" "$GZ_DUMP_LOCATION" "${cyclos_service}@${cyclos_database}" || exit 1
  1521. ## ensure that the database is not locked
  1522. Wrap -d "check and remove database lock if any" -- \
  1523. cyclos:unlock "${project_name}" "${cyclos_database}" || exit 1
  1524. Wrap -d "set root url to '$url'" -- \
  1525. cyclos:set_root_url "${project_name}" "${cyclos_database}" "${url}" || exit 1
  1526. Wrap -d "start ${DARKYELLOW}${cyclos_service}${NORMAL}'s container" -- \
  1527. docker start "${container_id}" || exit 1
  1528. }
  1529. cmdline.spec.gnu set-root-url
  1530. cmdline.spec:cyclos:cmd:set-root-url:run() {
  1531. : :optval: --database,-d "Target database ('cyclos' if not specified)"
  1532. : :optval: --service,-s "The cyclos service name (defaults to 'cyclos')"
  1533. local URL
  1534. cyclos_database=${opt_database:-cyclos}
  1535. cyclos_service="${opt_service:-cyclos}"
  1536. project_name=$(compose:project_name) || exit 1
  1537. url=$(compose:get_url "${project_name}" "${cyclos_service}") || exit 1
  1538. container_id=$(compose:service:container_one "${project_name}" "${cyclos_service}") || exit 1
  1539. Wrap -d "stop ${DARKYELLOW}${cyclos_service}${NORMAL}'s container" -- \
  1540. docker stop "$container_id" || exit 1
  1541. Wrap -d "set root url to '$url'" -- \
  1542. cyclos:set_root_url "${project_name}" "${cyclos_database}" "${url}" || exit 1
  1543. Wrap -d "start ${DARKYELLOW}${cyclos_service}${NORMAL}'s container" -- \
  1544. docker start "${container_id}" || exit 1
  1545. }
  1546. cmdline.spec.gnu unlock
  1547. cmdline.spec:cyclos:cmd:unlock:run() {
  1548. : :optval: --database,-d "Target database ('cyclos' if not specified)"
  1549. : :optval: --service,-s "The cyclos service name (defaults to 'cyclos')"
  1550. local URL
  1551. cyclos_database=${opt_database:-cyclos}
  1552. cyclos_service="${opt_service:-cyclos}"
  1553. project_name=$(compose:project_name) || exit 1
  1554. container_id=$(compose:service:container_one "${project_name}" "${cyclos_service}") || exit 1
  1555. Wrap -d "stop ${DARKYELLOW}${cyclos_service}${NORMAL}'s container" -- \
  1556. docker stop "$container_id" || exit 1
  1557. Wrap -d "check and remove database lock if any" -- \
  1558. cyclos:unlock "${project_name}" "${cyclos_database}" || exit 1
  1559. Wrap -d "start ${DARKYELLOW}${cyclos_service}${NORMAL}'s container" -- \
  1560. docker start "${container_id}" || exit 1
  1561. }
  1562. cmdline.spec.gnu rocketchat
  1563. cmdline.spec::cmd:rocketchat:run() {
  1564. :
  1565. }
  1566. cmdline.spec.gnu drop-indexes
  1567. cmdline.spec:rocketchat:cmd:drop-indexes:run() {
  1568. : :optval: --database,-d "Target database ('rocketchat' if not specified)"
  1569. : :optval: --service,-s "The rocketchat service name (defaults to 'rocketchat')"
  1570. local URL
  1571. rocketchat_database=${opt_database:-rocketchat}
  1572. rocketchat_service="${opt_service:-rocketchat}"
  1573. project_name=$(compose:project_name) || exit 1
  1574. container_id=$(compose:service:container_one "${project_name}" "${rocketchat_service}") || exit 1
  1575. Wrap -d "stop ${DARKYELLOW}${rocketchat_service}${NORMAL}'s container" -- \
  1576. docker stop "$container_id" || exit 1
  1577. errlvl=0
  1578. Wrap -d "drop indexes" -- \
  1579. rocketchat:drop-indexes "${project_name}" "${rocketchat_database}" || {
  1580. errlvl=1
  1581. errmsg="Failed to drop indexes"
  1582. }
  1583. Wrap -d "start ${DARKYELLOW}${rocketchat_service}${NORMAL}'s container" -- \
  1584. docker start "${container_id}" || exit 1
  1585. if [ "$errlvl" != 0 ]; then
  1586. err "$errmsg"
  1587. fi
  1588. exit "$errlvl"
  1589. }
  1590. cmdline.spec.gnu nextcloud
  1591. cmdline.spec::cmd:nextcloud:run() {
  1592. :
  1593. }
  1594. cmdline.spec.gnu upgrade
  1595. cmdline.spec:nextcloud:cmd:upgrade:run() {
  1596. : :posarg: [TARGET_VERSION] "Target version to migrate to"
  1597. : :optval: --service,-s "The nexcloud service name (defaults to 'nextcloud')"
  1598. local URL
  1599. nextcloud_service="${opt_service:-nextcloud}"
  1600. project_name=$(compose:project_name) || exit 1
  1601. containers=$(compose:service:containers "${project_name}" "${nextcloud_service}") || exit 1
  1602. container_stopped=()
  1603. if [ -n "$containers" ]; then
  1604. for container in $containers; do
  1605. Wrap -d "stop ${DARKYELLOW}${nextcloud_service}${NORMAL}'s container" -- \
  1606. docker stop "$container" || {
  1607. err "Failed to stop container '$container'."
  1608. exit 1
  1609. }
  1610. container_stopped+=("$container")
  1611. done
  1612. fi
  1613. before_version=$(nextcloud:src:version) || exit 1
  1614. ## -q to remove the display of ``compose`` related information
  1615. ## like relation resolution.
  1616. ## --no-hint to remove the final hint about modifying your
  1617. ## ``compose.yml``.
  1618. compose -q upgrade "$nextcloud_service" --no-hint "$TARGET_VERSION"
  1619. errlvl="$?"
  1620. after_version=$(nextcloud:src:version)
  1621. if [ "$after_version" != "$before_version" ]; then
  1622. desc="update \`compose.yml\` to set ${DARKYELLOW}$nextcloud_service${NORMAL}'s "
  1623. desc+="docker image to actual code version ${WHITE}${after_version}${NORMAL}"
  1624. Wrap -d "$desc" -- \
  1625. compose:file:value-change \
  1626. "${nextcloud_service}.docker-compose.image" \
  1627. "docker.0k.io/nextcloud:${after_version}-myc" || exit 1
  1628. fi
  1629. if [ "$errlvl" == 0 ]; then
  1630. echo "${WHITE}Launching final compose${NORMAL}"
  1631. compose up || exit 1
  1632. fi
  1633. exit "$errlvl"
  1634. }
  1635. cmdline.spec.gnu check-fix
  1636. cmdline.spec::cmd:check-fix:run() {
  1637. : :posarg: [SERVICES...] "Optional service to check"
  1638. : :optval: --check,-c "Specify a check or a list of checks separated by commas"
  1639. : :optfla: --silent,-s "Don't ouput anything if everything goes well"
  1640. local project_name service_name containers container check
  1641. all_checks=$(declare -F |
  1642. egrep '^declare -fx? container:health:check-fix:[^ ]+$' |
  1643. cut -f 4 -d ":")
  1644. checks=(${opt_check//,/ })
  1645. for check in "${checks[@]}"; do
  1646. fn.exists container:health:check-fix:$check || {
  1647. err "check '$check' not found."
  1648. return 1
  1649. }
  1650. done
  1651. if [ "${#checks[*]}" == 0 ]; then
  1652. checks=($all_checks)
  1653. fi
  1654. ## XXXvlab: could make it parallel
  1655. project_name=$(compose:project_name) || exit 1
  1656. containers=($(compose:project:containers "${project_name}")) || exit 1
  1657. found=
  1658. for container in "${containers[@]}"; do
  1659. service_name=$(docker ps --filter id="$container" --format '{{.Label "com.docker.compose.service"}}')
  1660. if [ "${#SERVICES[@]}" -gt 0 ]; then
  1661. [[ " ${SERVICES[*]} " == *" $service_name "* ]] || continue
  1662. fi
  1663. found=1
  1664. one_bad=
  1665. for check in "${checks[@]}"; do
  1666. if ! container:health:check-fix:"$check" "$container"; then
  1667. one_bad=1
  1668. fi
  1669. done
  1670. if [ -z "$opt_silent" ] && [ -z "$one_bad" ]; then
  1671. Elt "containers have been checked for ${DARKYELLOW}$service_name${NORMAL}"
  1672. Feedback
  1673. fi
  1674. done
  1675. if [ -z "$found" ]; then
  1676. if [ -z "$opt_silent" ]; then
  1677. if [ "${#SERVICES[@]}" -gt 0 ]; then
  1678. warn "No container for given services found in current project '$project_name'."
  1679. else
  1680. warn "No container found for current project '$project_name'."
  1681. fi
  1682. fi
  1683. return 1
  1684. fi
  1685. }
  1686. awk:require() {
  1687. local require_at_least="$1" version already_installed
  1688. while true; do
  1689. if ! version=$(awk --version 2>/dev/null); then
  1690. version=""
  1691. else
  1692. version=${version%%,*}
  1693. version=${version##* }
  1694. fi
  1695. if [ -z "$version" ] || version_gt "$require_at_least" "$version"; then
  1696. if [ -z "$already_installed" ]; then
  1697. if [ -z "$version" ]; then
  1698. info "No 'gawk' available, probably using a clone. Installing 'gawk'..."
  1699. else
  1700. info "Found gawk version '$version'. Updating 'gawk'..."
  1701. fi
  1702. apt-get install gawk -y </dev/null || {
  1703. err "Failed to install 'gawk'."
  1704. return 1
  1705. }
  1706. already_installed=true
  1707. else
  1708. if [ -z "$version" ]; then
  1709. err "No 'gawk' available even after having installed one"
  1710. else
  1711. err "'gawk' version '$version' is lower than required" \
  1712. "'$require_at_least' even after updating 'gawk'."
  1713. fi
  1714. return 1
  1715. fi
  1716. continue
  1717. fi
  1718. return 0
  1719. done
  1720. }
  1721. cmdline.spec.gnu stats
  1722. cmdline.spec::cmd:stats:run() {
  1723. : :optval: --format,-f "Either 'silent', 'raw', or 'pretty', default is pretty."
  1724. : :optfla: --silent,-s "Shorthand for '--format silent'"
  1725. : :optval: --resource,-r 'resource(s) separated with a comma'
  1726. local project_name service_name containers container check
  1727. if [[ -n "${opt_silent}" ]]; then
  1728. if [[ -n "${opt_format}" ]]; then
  1729. err "'--silent' conflict with option '--format'."
  1730. return 1
  1731. fi
  1732. opt_format=s
  1733. fi
  1734. opt_format="${opt_format:-pretty}"
  1735. case "${opt_format}" in
  1736. raw|r)
  1737. opt_format="raw"
  1738. :
  1739. ;;
  1740. silent|s)
  1741. opt_format="silent"
  1742. ;;
  1743. pretty|p)
  1744. opt_format="pretty"
  1745. awk:require 4.1.4 || return 1
  1746. ;;
  1747. *)
  1748. err "Invalid value '$opt_format' for option --format"
  1749. echo " use either 'raw' (shorthand 'r'), 'silent' (shorthand 's') or pretty (shorthand 'p')." >&2
  1750. return 1
  1751. esac
  1752. local resources=(c.{memory,network} load_avg)
  1753. if [ -n "${opt_resource}" ]; then
  1754. resources=(${opt_resource//,/ })
  1755. fi
  1756. local not_found=()
  1757. for resource in "${resources[@]}"; do
  1758. if ! fn.exists "stats:$resource"; then
  1759. not_found+=("$resource")
  1760. fi
  1761. done
  1762. if [[ "${#not_found[@]}" -gt 0 ]]; then
  1763. not_found_msg=$(printf "%s, " "${not_found[@]}")
  1764. not_found_msg=${not_found_msg%, }
  1765. err "Unsupported resource(s) provided: ${not_found_msg}"
  1766. echo " resource must be one-of:" >&2
  1767. declare -F | egrep -- '-fx? stats:[a-zA-Z0-9_.]+$' | cut -f 3- -d " " | cut -f 2- -d ":" | prefix " - " >&2
  1768. return 1
  1769. fi
  1770. :state-dir:
  1771. for resource in "${resources[@]}"; do
  1772. if [ "$opt_format" == "pretty" ]; then
  1773. echo "${WHITE}$resource${NORMAL}:"
  1774. stats:"$resource" "$opt_format" 2>&1 | prefix " "
  1775. else
  1776. stats:"$resource" "$opt_format" 2>&1 | prefix "$resource "
  1777. fi
  1778. set_errlvl "${PIPESTATUS[0]}" || return 1
  1779. done
  1780. }
  1781. stats:c.memory() {
  1782. local format="$1"
  1783. local out
  1784. container_to_check=($(docker:running_containers)) || exit 1
  1785. out=$(docker:containers:stats "${container_to_check[@]}")
  1786. printf "%s\n" "$out" | rrd:update "containers" "memory|3:usage:GAUGE:U:U,4:inactive:GAUGE:U:U" || {
  1787. return 1
  1788. }
  1789. case "${format:-p}" in
  1790. raw|r)
  1791. printf "%s\n" "$out" | cut -f 1-5 -d " "
  1792. ;;
  1793. pretty|p)
  1794. awk:require 4.1.4 || return 1
  1795. {
  1796. echo "container" "__total____" "buffered____" "resident____"
  1797. printf "%s\n" "$out" |
  1798. awk '
  1799. {
  1800. offset = strftime("%z", $2);
  1801. print $1, substr($0, index($0,$3));
  1802. }' | cut -f 1-4 -d " " |
  1803. numfmt --field 2-4 --to=iec-i --format=%8.1fB |
  1804. sed -r 's/(\.[0-9])([A-Z]?iB)/\1:\2/g' |
  1805. sort
  1806. } | col:normalize:size -+++ |
  1807. sed -r 's/(\.[0-9]):([A-Z]?iB)/\1 \2/g' |
  1808. header:make
  1809. ;;
  1810. esac
  1811. }
  1812. stats:c.network() {
  1813. local format="$1"
  1814. local out
  1815. container_to_check=($(docker:running_containers)) || exit 1
  1816. out=$(docker:containers:stats "${container_to_check[@]}")
  1817. cols=(
  1818. {rx,tx}_{bytes,packets,errors,dropped}
  1819. )
  1820. idx=5 ## starting column idx for next fields
  1821. defs=()
  1822. for col in "${cols[@]}"; do
  1823. defs+=("$((idx++)):${col}:COUNTER:U:U")
  1824. done
  1825. OLDIFS="$IFS"
  1826. IFS="," defs="${defs[*]}"
  1827. IFS="$OLDIFS"
  1828. printf "%s\n" "$out" |
  1829. rrd:update "containers" \
  1830. "network|${defs}" || {
  1831. return 1
  1832. }
  1833. case "${format:-p}" in
  1834. raw|r)
  1835. printf "%s\n" "$out" | cut -f 1,2,7- -d " "
  1836. ;;
  1837. pretty|p)
  1838. awk:require 4.1.4 || return 1
  1839. {
  1840. echo "container" "_" "_" "_" "RX" "_" "_" "_" "TX"
  1841. echo "_" "__bytes____" "__packets" "__errors" "__dropped" "__bytes____" "__packets" "__errors" "__dropped"
  1842. printf "%s\n" "$out" |
  1843. awk '
  1844. {
  1845. offset = strftime("%z", $2);
  1846. print $1, substr($0, index($0,$7));
  1847. }' |
  1848. numfmt --field 2,6 --to=iec-i --format=%8.1fB |
  1849. numfmt --field 3,4,5,7,8,9 --to=si --format=%8.1f |
  1850. sed -r 's/(\.[0-9])([A-Z]?(iB|B)?)/\1:\2/g' |
  1851. sort
  1852. } | col:normalize:size -++++++++ |
  1853. sed -r '
  1854. s/(\.[0-9]):([A-Z]?iB)/\1 \2/g;
  1855. s/(\.[0-9]):([KMGTPE])/\1 \2/g;
  1856. s/ ([0-9]+)\.0:B/\1 /g;
  1857. s/ ([0-9]+)\.0:/\1 /g;
  1858. ' |
  1859. header:make 2
  1860. ;;
  1861. esac
  1862. }
  1863. header:make() {
  1864. local nb_line="${1:-1}"
  1865. local line
  1866. while ((nb_line-- > 0)); do
  1867. read-0a line
  1868. echo "${GRAY}$(printf "%s" "$line" | sed -r 's/_/ /g')${NORMAL}"
  1869. done
  1870. cat
  1871. }
  1872. stats:load_avg() {
  1873. local format="$1"
  1874. local out
  1875. out=$(host:sys:load_avg)
  1876. printf "%s\n" "$out" | rrd:update "" "load_avg|2:load_avg_1:GAUGE:U:U,3:load_avg_5:GAUGE:U:U,4:load_avg_15:GAUGE:U:U" || {
  1877. return 1
  1878. }
  1879. case "${format:-p}" in
  1880. raw|r)
  1881. printf "%s\n" "$out" | cut -f 2-5 -d " "
  1882. ;;
  1883. pretty|p)
  1884. {
  1885. echo "___1m" "___5m" "__15m"
  1886. printf "%s\n" "$out" | cut -f 3-5 -d " "
  1887. } | col:normalize:size +++ | header:make
  1888. ;;
  1889. esac
  1890. }
  1891. host:sys:load_avg() {
  1892. local uptime
  1893. uptime="$(uptime)"
  1894. uptime=${uptime##*: }
  1895. uptime=${uptime//,/}
  1896. printf "%s " "" "$(date +%s)" "$uptime"
  1897. }
  1898. cmdline.spec.gnu mongo
  1899. cmdline.spec::cmd:mongo:run() {
  1900. :
  1901. }
  1902. cmdline.spec.gnu upgrade
  1903. cmdline.spec:mongo:cmd:upgrade:run() {
  1904. : :posarg: [TARGET_VERSION] "Target version to migrate to"
  1905. : :optval: --service,-s "The mongo service name (defaults to 'mongo')"
  1906. : :optfla: --debug,-d "Display debugging information"
  1907. local URL
  1908. mongo_service="${opt_service:-mongo}"
  1909. available_actions=$(compose --get-available-actions) || exit 1
  1910. available_actionable_services=($(e "$available_actions" | yq 'keys().[]'))
  1911. if [[ " ${available_actionable_services[*]} " != *" $mongo_service "* ]]; then
  1912. err "Service '$mongo_service' was not found in current 'compose.yml'."
  1913. exit 1
  1914. fi
  1915. opts_compose=()
  1916. if [ -n "$opt_debug" ]; then
  1917. opts_compose+=("--debug")
  1918. else
  1919. opts_compose+=("-q")
  1920. fi
  1921. project_name=$(compose:project_name) || exit 1
  1922. containers="$(compose:service:containers "${project_name}" "${mongo_service}")" || exit 1
  1923. ## XXXvlab: quick hack, to make more beautiful later
  1924. cron_container=$(compose:service:containers "${project_name}" "cron")
  1925. containers="$containers $cron_container"
  1926. docker stop "$cron_container" >/dev/null 2>&1 || true
  1927. before_version=
  1928. uptodate=
  1929. upgraded=
  1930. msgerr=()
  1931. while read-0a-err errlvl line; do
  1932. echo "$line"
  1933. rline=$(printf "%s" "$line" | sed_compat "s/$__color_sequence_regex//g")
  1934. case "$rline" in
  1935. "II Current mongo version: "*)
  1936. before_version="${rline#II Current mongo version: }"
  1937. ;;
  1938. "II ${mongo_service} is already up-to-date.")
  1939. if [ -z "$before_version" ]; then
  1940. msgerr+=("expected a 'current version' line before the 'up-to-date' one.")
  1941. continue
  1942. fi
  1943. after_version="$before_version"
  1944. uptodate=1
  1945. ;;
  1946. "II Successfully upgraded from ${before_version} to "*)
  1947. after_version="${rline#II Successfully upgraded from ${before_version} to }"
  1948. upgraded=1
  1949. ;;
  1950. *)
  1951. :
  1952. ;;
  1953. esac
  1954. done < <(
  1955. ## -q to remove the display of ``compose`` related information
  1956. ## like relation resolution.
  1957. ## -c on the upgrade action to force color
  1958. ansi_color=yes p-0a-err compose -c "${opts_compose[@]}" upgrade "$mongo_service" --no-hint -c "$TARGET_VERSION"
  1959. )
  1960. if [ "$errlvl" != 0 ]; then
  1961. exit "$errlvl"
  1962. fi
  1963. if [ -n "$uptodate" ]; then
  1964. for container in "${containers[@]}"; do
  1965. [ -n "$container" ] || continue
  1966. Wrap -d "start ${DARKYELLOW}${mongo_service}${NORMAL}'s container" -- \
  1967. docker start "$container" || {
  1968. err "Failed to start container '$container'."
  1969. exit 1
  1970. }
  1971. done
  1972. exit 0
  1973. fi
  1974. if [ -z "$upgraded" ]; then
  1975. err "Unexpected output of 'upgrade' action with errorlevel 0 and without success"
  1976. exit 1
  1977. fi
  1978. desc="update \`compose.yml\` to set ${DARKYELLOW}$mongo_service${NORMAL}'s "
  1979. desc+="docker image to actual code version ${WHITE}${after_version}${NORMAL}"
  1980. Wrap -d "$desc" -- \
  1981. compose:file:value-change \
  1982. "${mongo_service}.docker-compose.image" \
  1983. "docker.0k.io/mongo:${after_version}-myc" || exit 1
  1984. echo "${WHITE}Launching final compose${NORMAL}"
  1985. compose up || exit 1
  1986. }
  1987. cmdline.spec.gnu postgres
  1988. cmdline.spec::cmd:postgres:run() {
  1989. :
  1990. }
  1991. cmdline.spec.gnu upgrade
  1992. cmdline.spec:postgres:cmd:upgrade:run() {
  1993. : :posarg: [TARGET_VERSION] "Target version to migrate to"
  1994. : :optval: --service,-s "The postgre service name (defaults to 'postgres')"
  1995. : :optfla: --debug,-d "Display debugging information"
  1996. local URL
  1997. depends yq
  1998. postgres_service="${opt_service:-postgres}"
  1999. available_actions=$(compose --get-available-actions) || exit 1
  2000. available_actionable_services=($(e "$available_actions" | yq 'keys().[]'))
  2001. if [[ " ${available_actionable_services[*]} " != *" $postgres_service "* ]]; then
  2002. err "Service '$postgres_service' was not found in current 'compose.yml'."
  2003. exit 1
  2004. fi
  2005. opts_compose=()
  2006. if [ -n "$opt_debug" ]; then
  2007. opts_compose+=("--debug")
  2008. else
  2009. opts_compose+=("-q")
  2010. fi
  2011. project_name=$(compose:project_name) || exit 1
  2012. containers=($(compose:service:containers "${project_name}" "${postgres_service}")) || exit 1
  2013. ## XXXvlab: quick hack, to make more beautiful later
  2014. cron_container=$(compose:service:containers "${project_name}" "cron")
  2015. containers+=("$cron_container")
  2016. docker stop "$cron_container" >/dev/null 2>&1 || true
  2017. before_version=
  2018. uptodate=
  2019. upgraded=
  2020. msgerr=()
  2021. while read-0a-err errlvl line; do
  2022. echo "$line"
  2023. rline=$(printf "%s" "$line" | sed_compat "s/$__color_sequence_regex//g")
  2024. case "$rline" in
  2025. "II Current postgres version: "*)
  2026. before_version="${rline#II Current postgres version: }"
  2027. ;;
  2028. "II ${postgres_service} is already up-to-date.")
  2029. if [ -z "$before_version" ]; then
  2030. msgerr+=("expected a 'current version' line before the 'up-to-date' one.")
  2031. continue
  2032. fi
  2033. after_version="$before_version"
  2034. uptodate=1
  2035. ;;
  2036. "II Successfully upgraded from ${before_version} to "*)
  2037. after_version="${rline#II Successfully upgraded from ${before_version} to }"
  2038. upgraded=1
  2039. ;;
  2040. *)
  2041. :
  2042. ;;
  2043. esac
  2044. done < <(
  2045. ## -q to remove the display of ``compose`` related information
  2046. ## like relation resolution.
  2047. ## -c on the upgrade action to force color
  2048. ansi_color=yes p-0a-err compose -q -c "${opts_compose[@]}" upgrade "$postgres_service" --no-hint -c "$TARGET_VERSION" 2>&1
  2049. )
  2050. if [ "$errlvl" != 0 ]; then
  2051. exit "$errlvl"
  2052. fi
  2053. if [ -n "$uptodate" ]; then
  2054. for container in "${containers[@]}"; do
  2055. [ -n "$container" ] || continue
  2056. Wrap -d "start ${DARKYELLOW}${postgres_service}${NORMAL}'s container" -- \
  2057. docker start "$container" || {
  2058. err "Failed to start container '$container'."
  2059. exit 1
  2060. }
  2061. done
  2062. exit 0
  2063. fi
  2064. if [ -z "$upgraded" ]; then
  2065. err "Unexpected output of 'upgrade' action with errorlevel 0 and without success"
  2066. exit 1
  2067. fi
  2068. desc="update \`compose.yml\` to set ${DARKYELLOW}$postgres_service${NORMAL}'s "
  2069. desc+="docker image to actual code version ${WHITE}${after_version}${NORMAL}"
  2070. Wrap -d "$desc" -- \
  2071. compose:file:value-change \
  2072. "${postgres_service}.docker-compose.image" \
  2073. "docker.0k.io/postgres:${after_version}-myc" || exit 1
  2074. echo "${WHITE}Launching final compose${NORMAL}"
  2075. compose up || exit 1
  2076. }
  2077. cmdline.spec.gnu bench
  2078. cmdline.spec::cmd:bench:run() {
  2079. depends sysbench
  2080. nbthread=$(lscpu | egrep "^CPU\(s\):" | cut -f 2 -d : | xargs echo)
  2081. single=$(sysbench cpu --cpu-max-prime=20000 run --threads=1 | grep "events per" | cut -f 2 -d : | xargs echo)
  2082. threaded=$(sysbench cpu --cpu-max-prime=20000 run --threads="$nbthread" | grep "events per" | cut -f 2 -d : | xargs echo)
  2083. echo "$threaded / $single / $nbthread"
  2084. }
  2085. cmdline.spec::cmd:monujo:run() {
  2086. :
  2087. }
  2088. cmdline.spec.gnu monujo
  2089. cmdline.spec:monujo:cmd:set-version:run() {
  2090. : :posarg: TARGET_VERSION "Target version to put in options"
  2091. : :optval: --service,-s "The monujo service name (defaults to 'monujo')"
  2092. local URL
  2093. monujo_service="${opt_service:-monujo}"
  2094. project_name=$(compose:project_name) || exit 1
  2095. ## check if service exists in compose.yml
  2096. compose:service:exists "$project_name" "$monujo_service" || {
  2097. err "Service '$monujo_service' was not found in current 'compose.yml'."
  2098. exit 1
  2099. }
  2100. Wrap -d "Changing ${DARKYELLOW}$monujo_service${NORMAL} version" -- \
  2101. compose:file:value-change \
  2102. "${monujo_service}.options.version" \
  2103. "${TARGET_VERSION}" || exit 1
  2104. }
  2105. cmdline::parse "$@"