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.

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