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.

2041 lines
69 KiB

  1. #!/bin/bash
  2. . /etc/shlib
  3. include common
  4. include parse
  5. include cmdline
  6. include config
  7. [[ "${BASH_SOURCE[0]}" != "${0}" ]] && SOURCED=true
  8. version=0.1
  9. desc='Manage 0k related installs'
  10. help=""
  11. ##
  12. ## Functions
  13. ##
  14. is-port-open() {
  15. local host="$1" port="$2" timeout=5
  16. start="$SECONDS"
  17. debug "Testing if $host's port $2 is open ..."
  18. while true; do
  19. timeout 1 bash -c "</dev/tcp/${host}/${port}" >/dev/null 2>&1 && break
  20. sleep 0.2
  21. if [ "$((SECONDS - start))" -gt "$timeout" ]; then
  22. return 1
  23. fi
  24. done
  25. debug " .. $host's port $2 is open."
  26. return 0
  27. }
  28. resolve() {
  29. local ent hostname="$1"
  30. debug "Resolving $1 ..."
  31. if ent=$(getent ahosts "$hostname"); then
  32. ent=$(echo "$ent" | egrep ^"[0-9]+.[0-9]+.[0-9]+.[0-9]+\s+" | \
  33. head -n 1 | awk '{ print $1 }')
  34. debug " .. resolved $1 to $ent."
  35. echo "$ent"
  36. else
  37. debug " .. couldn't resolve $1."
  38. return 1
  39. fi
  40. }
  41. set_errlvl() { return "${1:-1}"; }
  42. export master_pid=$$
  43. ssh:open() {
  44. local hostname ssh_cmd ssh_options
  45. ssh_cmd=(ssh)
  46. ssh_options=()
  47. while [ "$#" != 0 ]; do
  48. case "$1" in
  49. "--stdin-password")
  50. ssh_cmd=(sshpass "${ssh_cmd[@]}")
  51. ;;
  52. -o)
  53. ssh_options+=("$1" "$2")
  54. shift
  55. ;;
  56. *)
  57. [ -z "$hostname" ] && hostname="$1" || {
  58. err "Surnumerous positional argument '$1'. Expecting only hostname."
  59. return 1
  60. }
  61. ;;
  62. esac
  63. shift
  64. done
  65. full_cmd=(
  66. "${ssh_cmd[@]}"
  67. -o ControlPath=/tmp/ssh-control-master-${master_pid}-$hostname \
  68. -o ControlMaster=auto -o ControlPersist=900 \
  69. -o ConnectTimeout=5 -o StrictHostKeyChecking=no \
  70. "${ssh_options[@]}" \
  71. "$hostname" "$@" -- true)
  72. "${full_cmd[@]}" >/dev/null 2>&1 || {
  73. err "Failed: ${full_cmd[*]}"
  74. return 1
  75. }
  76. trap_add EXIT,INT 'ssh:quit "$hostname"'
  77. }
  78. ssh:rsync() {
  79. local src="$1" dst="$2"
  80. hostname=${src%%:*}
  81. hostname=${hostname#*@}
  82. local rsync_ssh_options=(
  83. -o ControlPath="/tmp/ssh-control-master-${master_pid}-$hostname"
  84. -o ControlMaster=auto
  85. -o ControlPersist=900
  86. -o ConnectTimeout=10
  87. -o StrictHostKeyChecking=no
  88. )
  89. if ! ssh:run "root@$hostname" -- type -p rsync </dev/null >/dev/null; then
  90. info "No 'rsync' available on '$hostname'. Requesting installation..."
  91. ssh:run "root@$hostname" -- apt-get install rsync -y || {
  92. err "Installation of 'rsync' failed on '$hostname'"
  93. return 1
  94. }
  95. fi
  96. local cmd=(
  97. rsync -e "ssh ${rsync_ssh_options[*]}"
  98. -azvArH --delete --delete-excluded
  99. --partial --partial-dir .rsync-partial
  100. "$src" "$dst"
  101. )
  102. "${cmd[@]}"
  103. }
  104. ssh:open-try() {
  105. local opts hostnames
  106. opts=()
  107. hostnames=()
  108. while [ "$#" != 0 ]; do
  109. case "$1" in
  110. -o)
  111. opts+=("$1" "$2")
  112. shift
  113. ;;
  114. *)
  115. hostnames+=("$1")
  116. ;;
  117. esac
  118. shift
  119. done
  120. password=''
  121. for host in "${hostnames[@]}"; do
  122. debug "Trying $host with publickey."
  123. ssh:open -o PreferredAuthentications=publickey \
  124. "${opts[@]}" \
  125. "$host" >/dev/null 2>&1 && {
  126. echo "$host"$'\n'"$password"$'\n'
  127. return 0
  128. }
  129. debug " .. failed connecting to $host with publickey."
  130. done
  131. local times=0 password
  132. while [ "$((++times))" -le 3 ]; do
  133. read -sp "$HOST's password: " password
  134. errlvl="$?"
  135. echo >&2
  136. if [ "$errlvl" -gt 0 ]; then
  137. exit 1
  138. fi
  139. for host in "${hostnames[@]}"; do
  140. debug "Trying $host with password ($times/3)"
  141. echo "$password" | ssh:open -o PreferredAuthentications=password \
  142. --stdin-password \
  143. "${opts[@]}" \
  144. "$host" >/dev/null 2>&1 && {
  145. echo "$host"$'\n'"$password"$'\n'
  146. return 0
  147. }
  148. debug " .. failed connecting to $host with password."
  149. done
  150. err "login failed. Try again... ($((times+1))/3)"
  151. done
  152. return 1
  153. }
  154. rscolcat:check-install() {
  155. type -p rscolcat >/dev/null || {
  156. err "'rscolcat' is not installed. Please install it."
  157. echo " sudo wget https://docker.0k.io/downloads/rscolcat-0.1.0 -O /usr/local/bin/rscolcat && " >&2
  158. echo " sudo chmod +x /usr/local/bin/rscolcat" >&2
  159. return 1
  160. }
  161. }
  162. ssh:run() {
  163. local hostname="$1" ssh_options cmd
  164. shift
  165. ssh_options=()
  166. cmd=()
  167. while [ "$#" != 0 ]; do
  168. case "$1" in
  169. "--")
  170. shift
  171. cmd+=("$@")
  172. break
  173. ;;
  174. *)
  175. ssh_options+=("$1")
  176. ;;
  177. esac
  178. shift
  179. done
  180. ## XXXvlab: keeping in case we need some debug
  181. # debug "$DARKCYAN$hostname$NORMAL $WHITE\$$NORMAL" "$@"
  182. # debug "Running cmd: ${cmd[*]}"
  183. # for arg in "${cmd[@]}"; do
  184. # debug "$arg"
  185. # done
  186. {
  187. {
  188. ssh -o ControlPath=/tmp/ssh-control-master-${master_pid}-$hostname \
  189. -o ControlMaster=auto -o ControlPersist=900 \
  190. -o "StrictHostKeyChecking=no" \
  191. "${ssh_options[@]}" "$hostname" -- "${cmd[@]}"
  192. } 3>&1 1>&2 2>&3 ## | sed -r "s/^/$DARKCYAN$hostname$NORMAL $DARKRED\!$NORMAL /g"
  193. set_errlvl "${PIPESTATUS[0]}"
  194. } 3>&1 1>&2 2>&3
  195. }
  196. ssh:quit() {
  197. local hostname="$1"
  198. shift
  199. ssh -o ControlPath=/tmp/ssh-control-master-${master_pid} \
  200. -o ControlMaster=auto -o ControlPersist=900 -O exit \
  201. "$hostname" 2>/dev/null
  202. }
  203. is_ovh_domain_name() {
  204. local domain="$1"
  205. [[ "$domain" == *.ovh.net ]] && return 0
  206. [[ "$domain" == "ns"*".ip-"*".eu" ]] && return 0
  207. return 1
  208. }
  209. is_ovh_hostname() {
  210. local domain="$1"
  211. [[ "$domain" =~ ^vps-[0-9a-f]*$ ]] && return 0
  212. [[ "$domain" =~ ^vps[0-9]*$ ]] && return 0
  213. return 1
  214. }
  215. vps_connection_check() {
  216. local vps="$1"
  217. ip=$(resolve "$vps") ||
  218. { echo "${DARKRED}no-resolve${NORMAL}"; return 1; }
  219. is-port-open "$ip" "22" </dev/null ||
  220. { echo "${DARKRED}no-port-22-open${NORMAL}"; return 1; }
  221. ssh:open -o ConnectTimeout=10 -o PreferredAuthentications=publickey \
  222. "root@$vps" >/dev/null 2>&1 ||
  223. { echo "${DARKRED}no-ssh-root-access${NORMAL}"; return 1; }
  224. }
  225. vps_check() {
  226. local vps="$1"
  227. vps_connection_check "$vps" </dev/null || return 1
  228. if size=$(
  229. echo "df /srv -h | tail -n +2 | sed -r 's/ +/ /g' | cut -f 5 -d ' ' | cut -f 1 -d %" |
  230. ssh:run "root@$vps" -- bash); then
  231. if [ "$size" -gt "90" ]; then
  232. echo "${DARKRED}above-90%-disk-usage${NORMAL}"
  233. elif [ "$size" -gt "75" ]; then
  234. echo "${DARKYELLOW}above-75%-disk-usage${NORMAL}"
  235. fi
  236. else
  237. echo "${DARKRED}no-size${NORMAL}"
  238. fi </dev/null
  239. compose_content=$(ssh:run "root@$vps" -- cat /opt/apps/myc-deploy/compose.yml </dev/null) ||
  240. { echo "${DARKRED}no-compose${NORMAL}"; return 1; }
  241. echo "$compose_content" | yq -e ".rsync-backup" >/dev/null 2>&1 ||
  242. { echo "${DARKRED}no-backup${NORMAL}"; return 1; }
  243. }
  244. backup:setup-rsync() {
  245. local admin="$1" vps="$2" server="$3" id="$4"
  246. [ -z "${BACKUP_SSH_SERVER}" ] || {
  247. err "Unexpected error: '\$BACKUP_SSH_SERVER' is already set in '$FUNCNAME'."
  248. return 1
  249. }
  250. BACKUP_SSH_OPTIONS=(-o StrictHostKeyChecking=no)
  251. if [[ "$server" == *":"* ]]; then
  252. BACKUP_SSH_OPTIONS+=(-p "${server#*:}")
  253. BACKUP_SSH_SERVER=${server%%:*}
  254. else
  255. BACKUP_SSH_SERVER="$server"
  256. fi
  257. if ! private_key=$(ssh "${BACKUP_SSH_OPTIONS[@]}" \
  258. "$admin"@"${BACKUP_SSH_SERVER}" request-recovery-key "$id"); then
  259. err "Couldn't request a recovery key for '$id' with account '$admin'."
  260. return 1
  261. fi
  262. if ! VPS_TMP_DIR=$(echo "mktemp -d" | ssh:run "root@$vps" -- bash); then
  263. err "Couldn't create a temporary directory on vps"
  264. return 1
  265. fi
  266. cat <<EOF | ssh:run "root@$vps" -- bash || return 1
  267. touch "$VPS_TMP_DIR/recover_key" &&
  268. chmod go-rwx "$VPS_TMP_DIR/recover_key" &&
  269. printf "%s\n" "$private_key" >> "$VPS_TMP_DIR/recover_key"
  270. EOF
  271. BACKUP_SSH_OPTIONS+=(-i "$VPS_TMP_DIR/recover_key" -l rsync)
  272. BACKUP_VPS_TARGET="$vps"
  273. BACKUP_IDENT="$id"
  274. echo "type -p rsync >/dev/null 2>&1 || apt-get install -y rsync </dev/null" |
  275. ssh:run "root@$vps" -- bash || return 1
  276. }
  277. backup:rsync() {
  278. local ssh_options
  279. [ -n "${BACKUP_SSH_SERVER}" ] || {
  280. err "Unexpected error: '\$BACKUP_SSH_SERVER' is not set in 'rsync_exists'."
  281. return 1
  282. }
  283. rsync_options=()
  284. while [[ "$1" == "-"* ]]; do
  285. rsync_options+=("$1")
  286. shift
  287. done
  288. local src="$1" dst="$2"
  289. cat <<EOF | ssh:run "root@${BACKUP_VPS_TARGET}" -- bash
  290. rsync -e "ssh ${BACKUP_SSH_OPTIONS[*]}" \
  291. -azvArH --delete --delete-excluded \
  292. --partial --partial-dir .rsync-partial \
  293. --numeric-ids ${rsync_options[*]} \
  294. "${BACKUP_SSH_SERVER}":/var/mirror/"${BACKUP_IDENT}${src}" "${dst}"
  295. EOF
  296. }
  297. backup:path_exists() {
  298. local src="$1"
  299. [ -n "${BACKUP_SSH_SERVER}" ] || {
  300. err "Unexpected error: '\$BACKUP_SSH_SERVER' is not set in 'rsync_exists'."
  301. return 1
  302. }
  303. cat <<EOF | ssh:run "root@${BACKUP_VPS_TARGET}" -- bash >/dev/null 2>&1
  304. rsync -e "ssh ${BACKUP_SSH_OPTIONS[*]}" \
  305. -nazvArH --numeric-ids \
  306. "${BACKUP_SSH_SERVER}":/var/mirror/"${BACKUP_IDENT}${src}" "/tmp/dummy"
  307. EOF
  308. }
  309. file:vps_backup_recover() {
  310. local admin="$1" server="$2" id="$3" path="$4" vps="$5" vps_path="$6"
  311. backup:rsync "${path}" "${vps_path}" || return 1
  312. if [[ "$path" == *"/" ]]; then
  313. if [ "$path" == "$vps_path"/ ]; then
  314. msg_target="Directory '$path'"
  315. else
  316. msg_target="Directory '$path' -> '$vps_path'"
  317. fi
  318. else
  319. if [ "$path" == "$vps_path" ]; then
  320. msg_target="File '$path'"
  321. else
  322. msg_target="File '$path' -> '$vps_path'"
  323. fi
  324. fi
  325. info "$msg_target was ${GREEN}successfully${NORMAL} restored on $vps."
  326. }
  327. mailcow:vps_backup_recover() {
  328. local admin="$1" server="$2" id="$3" path="$4" vps="$5" vps_path="$6"
  329. if ! compose_yml_files=$(cat <<EOF | ssh:run "root@$vps" -- bash
  330. urn=com.docker.compose.project
  331. docker ps -f "label=\$urn=mailcowdockerized" \
  332. --format="{{.Label \"\$urn.working_dir\"}}/{{.Label \"\$urn.config_files\"}}" |
  333. uniq
  334. EOF
  335. ); then
  336. err "Couldn't get list of running projects"
  337. return 1
  338. fi
  339. stopped_containers=
  340. if [ -n "$compose_yml_files" ]; then
  341. echo "Found running mailcowdockerized containers" >&2
  342. if [[ "$compose_yml_files" == *$'\n'* ]]; then
  343. err "Running containers are confusing, did not find only one mailcowdockerized project."
  344. return 1
  345. fi
  346. if ! echo "[ -e \"$compose_yml_files\" ]" | ssh:run "root@$vps" -- bash ; then
  347. ## For some reason, sometimes $urn.config_files holds an absolute path
  348. compose_yml_files=/${compose_yml_files#*//}
  349. if ! echo "[ -e \"$compose_yml_files\" ]" | ssh:run "root@$vps" -- bash ; then
  350. err "Running containers are confusing, they don't point to an existing docker-compose.yml."
  351. return 1
  352. fi
  353. fi
  354. echo "Containers where launched from '$compose_yml_files'" >&2
  355. COMPOSE_FILE="$compose_yml_files"
  356. ENV_FILE="${COMPOSE_FILE%/*}/.env"
  357. if ! echo "[ -e \"${ENV_FILE}\" ]" | ssh:run "root@$vps" -- bash ; then
  358. err "Running containers are confusing, docker-compose.yml has no '.env' next to it."
  359. return 1
  360. fi
  361. echo "${WHITE}Bringing mailcowdockerized down${NORMAL}"
  362. echo "docker-compose -f \"${COMPOSE_FILE}\" --env-file \"${ENV_FILE}\" down" |
  363. ssh:run "root@$vps" -- bash
  364. stopped_containers=1
  365. fi
  366. if [[ "$path" == "/"* ]]; then
  367. ##
  368. ## Additional intelligence to simple file copy
  369. ##
  370. if [[ "$path" == "/var/lib/docker/volumes/mailcowdockerized_*-vol-1/_data"* ]]; then
  371. volume_name=${path#/var/lib/docker/volumes/}
  372. volume_name=${volume_name%%/*}
  373. volume_dir=${path%%"$volume_name"*}
  374. ## Create volumes if not existent
  375. if ! ssh:run "root@$vps" -- "
  376. [ -d '${volume_dir}' ] ||
  377. docker run --rm -v ${volume_name}:/tmp/dummy docker.0k.io/alpine:3.9
  378. [ -d '${volume_dir}' ]
  379. "; then
  380. err "Couldn't find nor create '${volume_dir}'."
  381. return 1
  382. fi
  383. fi
  384. echo "${WHITE}Sync from backup ${path} to VPS ${vps_path}${NORMAL}" >&2
  385. backup:rsync "${path}" "${vps_path}" || return 1
  386. if [[ "$path" == *"/" ]]; then
  387. if [ "$path" == "$vps_path"/ ]; then
  388. msg_target="Directory '$path'"
  389. else
  390. msg_target="Directory '$path' -> '$vps_path'"
  391. fi
  392. else
  393. if [ "$path" == "$vps_path" ]; then
  394. msg_target="File '$path'"
  395. else
  396. msg_target="File '$path' -> '$vps_path'"
  397. fi
  398. fi
  399. else
  400. ALL_TARGETS=(mailcow postfix rspamd redis crypt vmail{,-attachments} mysql)
  401. if [[ -n "$path" ]]; then
  402. targets=()
  403. bad_targets=()
  404. for target in ${path//,/ }; do
  405. if [[ " ${ALL_TARGETS[*]} " != *" $target "* ]]; then
  406. bad_targets+=("$target")
  407. fi
  408. targets+=("$target")
  409. done
  410. if [ "${#bad_targets[@]}" -gt 0 ]; then
  411. bad_target_msg=$(printf "%s, " "${bad_targets[@]}")
  412. err "Unknown components: ${bad_target_msg%, }. These are allowed components:"
  413. printf " - %s\n" "${ALL_TARGETS[@]}" >&2
  414. return 1
  415. fi
  416. msg_target="Partial mailcow backup"
  417. else
  418. targets=("${ALL_TARGETS[@]}")
  419. msg_target="Full mailcow backup"
  420. fi
  421. for target in "${targets[@]}"; do
  422. case "$target" in
  423. postfix|rspamd|redis|crypt|vmail|vmail-attachments)
  424. volume_name="mailcowdockerized_${target}-vol-1"
  425. volume_dir="/var/lib/docker/volumes/${volume_name}/_data"
  426. if ! backup:path_exists "${volume_dir}/"; then
  427. warn "No '$volume_name' in backup. This might be expected."
  428. continue
  429. fi
  430. ## Create volumes if not existent
  431. if ! ssh:run "root@$vps" -- "
  432. [ -d '${volume_dir}' ] ||
  433. docker run --rm -v ${volume_name}:/tmp/dummy docker.0k.io/alpine:3.9
  434. [ -d '${volume_dir}' ]
  435. "; then
  436. err "Couldn't find nor create '${volume_dir}'."
  437. return 1
  438. fi
  439. echo "${WHITE}Downloading of $volume_name${NORMAL}"
  440. backup:rsync "${volume_dir}/" "${volume_dir}" || return 1
  441. ;;
  442. mailcow)
  443. ## Mailcow git base
  444. COMPOSE_FILE=
  445. for mailcow_dir in /opt/{apps/,}mailcow-dockerized; do
  446. backup:path_exists "${mailcow_dir}/" || continue
  447. ## this possibly change last value
  448. COMPOSE_FILE="$mailcow_dir/docker-compose.yml"
  449. ENV_FILE="$mailcow_dir/.env"
  450. echo "${WHITE}Download of $mailcow_dir${NORMAL}"
  451. backup:rsync "${mailcow_dir}"/ "${mailcow_dir}" || return 1
  452. break
  453. done
  454. if [ -z "$COMPOSE_FILE" ]; then
  455. err "Can't find mailcow base installation path in backup."
  456. return 1
  457. fi
  458. ;;
  459. mysql)
  460. if [ -z "$COMPOSE_FILE" ]; then
  461. ## Mailcow git base
  462. compose_files=()
  463. for mailcow_dir in /opt/{apps/,}mailcow-dockerized; do
  464. ssh:run "root@$vps" -- "[ -e \"$mailcow_dir/docker-compose.yml\" ]" || continue
  465. ## this possibly change last value
  466. compose_files+=("$mailcow_dir/docker-compose.yml")
  467. done
  468. if [ "${#compose_files[@]}" == 0 ]; then
  469. err "No compose file found for mailcow installation."
  470. return 1
  471. elif [ "${#compose_files[@]}" -gt 1 ]; then
  472. err "Multiple compose files for mailcow found:"
  473. for f in "${compose_files[@]}"; do
  474. echo " - $f" >&2
  475. done
  476. echo "Can't decide which to use for mounting mysql container." >&2
  477. return 1
  478. fi
  479. COMPOSE_FILE="${compose_files[0]}"
  480. ENV_FILE="${COMPOSE_FILE%/*}/.env"
  481. if ! ssh:run "root@$vps" -- "[ -e \"${COMPOSE_FILE%/*}/.env\" ]"; then
  482. err "No env file in '$ENV_FILE' found."
  483. return 1
  484. fi
  485. fi
  486. ## Mysql database
  487. echo "${WHITE}Downloading last backup of mysql backups${NORMAL}"
  488. backup:rsync "/var/backups/mysql/" "/var/backups/mysql" || return 1
  489. if ! env_content=$(echo "cat '$ENV_FILE'" | ssh:run "root@$vps" -- bash); then
  490. err "Can't access env file: '$ENV_FILE'."
  491. return 1
  492. fi
  493. root_password=$(printf "%s\n" "$env_content" | grep ^DBROOT= | cut -f 2 -d =)
  494. echo "${WHITE}Bringing mysql-mailcow up${NORMAL}"
  495. if ! image=$(cat <<EOF | ssh:run "root@$vps" -- bash
  496. shyaml get-value services.mysql-mailcow.image < "${COMPOSE_FILE}"
  497. EOF
  498. ); then
  499. err "Failed to get image name of service 'mysql-mailcow' in 'compose.yml'."
  500. return 1
  501. fi
  502. if [ -z "$(ssh:run "root@$vps" -- docker images -q "$image")" ]; then
  503. info "Image '$image' not available, pull it."
  504. if ! ssh:run "root@$vps" -- \
  505. docker-compose -f "${COMPOSE_FILE}" --env-file "${ENV_FILE}" \
  506. pull mysql-mailcow; then
  507. err "Failed to pull image of service 'mysql-mailcow'."
  508. return 1
  509. fi
  510. fi
  511. if ! container_id=$(cat <<EOF | ssh:run "root@$vps" -- bash
  512. echo "[client]
  513. password=$root_password" > "$VPS_TMP_DIR/my.cnf"
  514. docker-compose -f "${COMPOSE_FILE}" --env-file "${ENV_FILE}" \
  515. run -d \
  516. -v "$VPS_TMP_DIR/my.cnf:/root/.my.cnf:ro" \
  517. mysql-mailcow
  518. EOF
  519. ); then
  520. err "Failed to bring up mysql-mailcow"
  521. return 1
  522. fi
  523. START="$SECONDS"
  524. retries=0
  525. timeout=600
  526. while true; do
  527. ((retries++))
  528. echo " waiting for mysql db..." \
  529. "(retry $retries, $(($SECONDS - $START))s elapsed, timeout is ${timeout}s)" >&2
  530. cat <<EOF | ssh:run "root@$vps" -- bash && break
  531. echo "SELECT 1;" | docker exec -i "$container_id" mysql >/dev/null 2>&1
  532. EOF
  533. if (($SECONDS - $START > $timeout)); then
  534. err "Failed to connect to mysql-mailcow."
  535. echo "docker-compose -f \"${COMPOSE_FILE}\" --env-file \"${ENV_FILE}\" down" |
  536. ssh:run "root@$vps" -- bash
  537. return 1
  538. fi
  539. sleep 0.4
  540. done
  541. DBUSER=$(printf "%s\n" "$env_content" | grep ^DBUSER= | cut -f 2 -d =)
  542. DBPASS=$(printf "%s\n" "$env_content" | grep ^DBPASS= | cut -f 2 -d =)
  543. echo "${WHITE}Uploading mysql dump${NORMAL}"
  544. cat <<EOF | ssh:run "root@$vps" -- bash
  545. echo "
  546. DROP DATABASE IF EXISTS mailcow;
  547. CREATE DATABASE mailcow;
  548. GRANT ALL PRIVILEGES ON mailcow.* TO '$DBUSER'@'%' IDENTIFIED BY '$DBPASS';
  549. " | docker exec -i "$container_id" mysql
  550. zcat /var/backups/mysql/mailcow/*.gz | docker exec -i "$container_id" mysql mailcow
  551. EOF
  552. if [ "$?" != 0 ]; then
  553. err "Failed to load mysql dump."
  554. echo "docker-compose -f \"${COMPOSE_FILE}\" --env-file \"${ENV_FILE}\" down" |
  555. ssh:run "root@$vps" -- bash
  556. return 1
  557. fi
  558. echo "${WHITE}Bringing mysql-mailcow down${NORMAL}"
  559. echo "docker-compose -f \"${COMPOSE_FILE}\" --env-file \"${ENV_FILE}\" down" |
  560. ssh:run "root@$vps" -- bash
  561. ;;
  562. *)
  563. err "Unknown component '$target'. Bailing out."
  564. return 1
  565. esac
  566. done
  567. ssh:run "root@$vps" -- "rm -rf '$VPS_TMP_DIR'"
  568. fi
  569. if [ -n "$stopped_containers" ]; then
  570. echo "${WHITE}Starting mailcow${NORMAL}" >&2
  571. echo "docker-compose -f \"${COMPOSE_FILE}\" --env-file \"${ENV_FILE}\" up -d" |
  572. ssh:run "root@$vps" -- bash
  573. fi
  574. info "$msg_target was ${GREEN}successfully${NORMAL} restored on $vps."
  575. }
  576. NTFY_TOPIC_FILE="/etc/ntfy/topics.yml"
  577. NTFY_CONFIG_FILE="/etc/ntfy/ntfy.conf"
  578. subscribe:ntfy:topic-file-exists() {
  579. local vps="$1"
  580. if ! out=$(echo "[ -f \"$NTFY_TOPIC_FILE\" ] && echo ok || true" | \
  581. ssh:run "root@$vps" -- bash); then
  582. err "Unable to check for existence of '$NTFY_TOPIC_FILE'."
  583. fi
  584. if [ -z "$out" ]; then
  585. err "File '$NTFY_TOPIC_FILE' not found on $vps."
  586. return 1
  587. fi
  588. }
  589. subscribe:ntfy:config-file-exists() {
  590. local vps="$1"
  591. if ! out=$(echo "[ -f \"$NTFY_CONFIG_FILE\" ] && echo ok || true" | \
  592. ssh:run "root@$vps" -- bash); then
  593. err "Unable to check for existence of '$NTFY_CONFIG_FILE'."
  594. fi
  595. if [ -z "$out" ]; then
  596. err "File '$NTFY_CONFIG_FILE' not found on $vps."
  597. return 1
  598. fi
  599. }
  600. ntfy:rm() {
  601. local channel="$1" topic="$2" vps="$3"
  602. subscribe:ntfy:topic-file-exists "$vps" || return 1
  603. if ! out=$(echo "yq -i 'del(.[\"$channel\"][] | select(. == \"$TOPIC\"))' \"$NTFY_TOPIC_FILE\"" | \
  604. ssh:run "root@$vps" -- bash); then
  605. err "Failed to remove channel '$channel' from '$NTFY_TOPIC_FILE'."
  606. return 1
  607. fi
  608. info "Channel '$channel' removed from '$NTFY_TOPIC_FILE' on $vps."
  609. ssh:run "root@$vps" -- cat "$NTFY_TOPIC_FILE"
  610. }
  611. ntfy:add() {
  612. local channel="$1" topic="$2" vps="$3"
  613. vps_connection_check "$vps" </dev/null || return 1
  614. subscribe:ntfy:topic-file-exists "$vps" || return 1
  615. if ! out=$(echo "yq '. | has(\"$channel\")' \"$NTFY_TOPIC_FILE\"" | \
  616. ssh:run "root@$vps" -- bash); then
  617. err "Failed to check if channel '$channel' with topic '$topic' is already in '$NTFY_TOPIC_FILE'."
  618. return 1
  619. fi
  620. if [ "$out" != "true" ]; then
  621. ## Channel does not exist
  622. if ! out=$(echo "yq -i '.[\"$channel\"] = []' \"$NTFY_TOPIC_FILE\"" | \
  623. ssh:run "root@$vps" -- bash); then
  624. err "Failed to create a new channel '$channel' entry in '$NTFY_TOPIC_FILE'."
  625. return 1
  626. fi
  627. else
  628. ## Channel exists
  629. if ! out=$(echo "yq '.[\"$channel\"] | any_c(. == \"$topic\")' \"$NTFY_TOPIC_FILE\"" | \
  630. ssh:run "root@$vps" -- bash); then
  631. err "Failed to check if channel '$channel' with topic '$topic' is already in '$NTFY_TOPIC_FILE'."
  632. return 1
  633. fi
  634. if [ "$out" == "true" ]; then
  635. info "Channel '$channel' with topic '$topic' already exists in '$NTFY_TOPIC_FILE'."
  636. return 0
  637. fi
  638. fi
  639. if ! out=$(echo "yq -i '.[\"$channel\"] += [\"$topic\"]' \"$NTFY_TOPIC_FILE\"" | \
  640. ssh:run "root@$vps" -- bash); then
  641. err "Failed to add channel '$channel' with topic '$topic' to '$NTFY_TOPIC_FILE'."
  642. return 1
  643. fi
  644. info "Channel '$channel' added with topic '$topic' to '$NTFY_TOPIC_FILE' on $vps."
  645. }
  646. NTFY_BROKER_SERVER="ntfy.0k.io"
  647. ntfy:topic-access() {
  648. local action="$1" topic="$2" vps="$3"
  649. subscribe:ntfy:config-file-exists "$vps" || return 1
  650. local user
  651. user=$(ntfy:get-login "$vps") || return 1
  652. case "$action" in
  653. "write")
  654. ssh "ntfy@$NTFY_BROKER_SERVER" "topic-access" \
  655. "$user" "$topic" "write-only" </dev/null || {
  656. err "Failed to grant write access to '$user' for topic '$topic'."
  657. return 1
  658. }
  659. info "Granted write access for '$user' to topic '$topic'."
  660. ;;
  661. "remove")
  662. ssh "ntfy@$NTFY_BROKER_SERVER" "topic-access" -r "$user" "$topic" </dev/null || {
  663. err "Failed to reset access of '$user' for topic '$topic'."
  664. return 1
  665. }
  666. info "Access for '$user' to topic '$topic' was resetted successfully."
  667. ;;
  668. *)
  669. err "Invalid action '$action'."
  670. return 1
  671. ;;
  672. esac
  673. }
  674. ntfy:get-login() {
  675. local vps="$1"
  676. if ! out=$(echo ". \"$NTFY_CONFIG_FILE\" && echo \"\$LOGIN\"" | \
  677. ssh:run "root@$vps" -- bash); then
  678. err "Failed to get ntfy login from '$NTFY_CONFIG_FILE'."
  679. return 1
  680. fi
  681. if [ -z "$out" ]; then
  682. err "Unexpected empty login retrieved from sourcing '$NTFY_CONFIG_FILE'."
  683. return 1
  684. fi
  685. echo "$out"
  686. }
  687. subscribe:add() {
  688. local vps="$1"
  689. read-0 channel topic || {
  690. err "Couldn't read CHANNEL and TOPIC arguments."
  691. return 1
  692. }
  693. vps_connection_check "$vps" </dev/null || return 1
  694. ntfy:topic-access "write" "$topic" "$vps" </dev/null || return 1
  695. ntfy:add "$channel" "$topic" "$vps" || {
  696. err "Failed to add channel '$channel' with topic '$topic' to '$NTFY_TOPIC_FILE'."
  697. echo " Removing topic access." >&2
  698. ntfy:topic-access "remove" "$topic" "$vps" </dev/null
  699. return 1
  700. }
  701. }
  702. subscribe:rm() {
  703. local vps="$1"
  704. read-0 channel topic || {
  705. err "Couldn't read CHANNEL and TOPIC arguments."
  706. return 1
  707. }
  708. vps_connection_check "$vps" </dev/null || return 1
  709. ntfy:rm "$channel" "$topic" "$vps" || return 1
  710. ntfy:topic-access "remove" "$topic" "$vps" </dev/null || {
  711. err "Failed to remove topic access for '$topic' on '$vps'."
  712. return 1
  713. }
  714. }
  715. vps_backup_recover() {
  716. local vps="$1" admin server id path rtype force type
  717. read-0 admin server id path rtype force
  718. if [[ "$vps" == *":"* ]]; then
  719. vps_path=${vps#*:}
  720. vps=${vps%%:*}
  721. else
  722. vps_path=
  723. fi
  724. vps_connection_check "$vps" </dev/null || {
  725. err "Failed to access '$vps'."
  726. return 1
  727. }
  728. if type=$(ssh:run "root@$vps" -- vps get-type); then
  729. info "VPS $vps seems to be of ${WHITE}$type${NORMAL} type"
  730. else
  731. warn "Couldn't find type of vps '$vps' (command 'vps get-type' failed on vps)."
  732. fi
  733. if [ -z "$path" ]; then
  734. if [ -n "$vps_path" ]; then
  735. err "You can't provide a VPS with path as destination if you don't provide a path in backup source."
  736. return 1
  737. fi
  738. info "No path provided in backup, so we assume you want ${WHITE}full recovery${NORMAL}."
  739. if [ "$rtype" != "$type" ]; then
  740. if [ -n "$force" ]; then
  741. warn "Backup found is of ${rtype:-unknown} type, while vps is of ${type:-unknown} type."
  742. else
  743. err "Backup found is of ${rtype:-unknown} type, while vps is of ${type:-unknown} type. (use \`\`-f\`\` to force)"
  744. return 1
  745. fi
  746. fi
  747. else
  748. if [ "$path" == "/" ]; then
  749. if [ -z "$vps_path" ]; then
  750. err "Recovery of '/' (full backup files) requires that you provide a vps path also."
  751. return 1
  752. fi
  753. if [ "$vps_path" == "/" ]; then
  754. err "Recovery of '/' (full backup files) requires that you provide" \
  755. "a vps path different from '/' also."
  756. return 1
  757. fi
  758. fi
  759. fi
  760. ## Sets VPS and internal global variable to allow rsync to work
  761. ## from vps to backup server.
  762. backup:setup-rsync "$admin" "$vps" "$server" "$id" || return 1
  763. if [[ "$path" == "/"* ]]; then
  764. if ! backup:path_exists "${path}"; then
  765. err "File or directory '$path' not found in backup."
  766. return 1
  767. fi
  768. if [ -z "$vps_path" ]; then
  769. if [[ "$path" != *"/" ]] && backup:path_exists "${path}"/ ; then
  770. path="$path/"
  771. fi
  772. vps_path=${path%/}
  773. vps_path=${vps_path:-/}
  774. fi
  775. fi
  776. case "$rtype-$type" in
  777. mailcow-*)
  778. ## Supports having $path and $vps_path set or unset, with additional behavior
  779. mailcow:vps_backup_recover "$admin" "$server" "$id" "$path" "$vps" "$vps_path"
  780. ;;
  781. *-*)
  782. if [[ "$path" == "/"* ]]; then
  783. ## For now, will require having $path and $vps_path set, no additional behaviors
  784. file:vps_backup_recover "$admin" "$server" "$id" "$path" "$vps" "$vps_path"
  785. else
  786. if [ -n "$path" ]; then
  787. err "Partial component recover of ${rtype:-unknown} backup type on" \
  788. "${type:-unknown} type VPS is not yet implemented."
  789. return 1
  790. else
  791. err "Full recover of ${rtype:-unknown} backup type on" \
  792. "${type:-unknown} type VPS is not yet implemented."
  793. return 1
  794. fi
  795. fi
  796. ;;
  797. esac
  798. }
  799. vps_install_backup() {
  800. local vps="$1" admin server
  801. vps_connection_check "$vps" </dev/null || return 1
  802. read-0 admin server
  803. if ! type=$(ssh:run "root@$vps" -- vps get-type </dev/null); then
  804. err "Could not get type."
  805. return 1
  806. fi
  807. backup_opts=()
  808. local opt
  809. while read-0 opt; do
  810. case "$opt" in
  811. --ignore-domain-check|--ignore-ping-check)
  812. backup_opts+=("$opt")
  813. ;;
  814. *)
  815. err "Unknown option '$opt'."
  816. return 1
  817. ;;
  818. esac
  819. done
  820. if ! out=$(ssh:run "root@$vps" -- vps install backup "$server" "${backup_opts[@]}" 2>&1); then
  821. err "Command 'vps install backup $server ${backup_opts[@]}' on $vps failed:"
  822. echo "$out" | prefix " ${DARKGRAY}|${NORMAL} " >&2
  823. return 1
  824. fi
  825. ## Format of output:
  826. ##
  827. ## II Entry for service rsync-backup is already present in '/opt/apps/myc-deploy/compose.yml'.
  828. ## II You can run this following command from an host having admin access to core-07.0k.io:
  829. ## (Or send it to a backup admin of core-07.0k.io)
  830. ## ssh -p 10023 myadmin@core-07.0k.io ssh-key add 'ssh-rsa AAAAAAAD...QCJ\
  831. ## 8HH6pVgEpu1twyxpr9xTt7eh..WaJdgPoxmiEwGfjMMNGxs39ggOTKUuSFSmOv8TiA1fzY\
  832. ## s85hF...dKP1qByJU1k= compose@odoo.sikle.fr'
  833. key="ssh-rsa ${out##*\'ssh-rsa }" ## remove everything before last "'ssh-rsa"
  834. key="${key%\'*}" ## remove everything after last '
  835. if ! [[ "$key" =~ ^"ssh-rsa "[a-zA-Z0-9/+=]+" "[a-zA-Z0-9._-]+"@"[a-zA-Z0-9._-]+$ ]]; then
  836. err "Unexpected output from 'vps install backup $server'. Can't find key."
  837. echo "$out" | prefix " ${GRAY}|$NORMAL " >&2
  838. echo " Extracted key:" >&2
  839. echo "$key" | prefix " ${GRAY}|$NORMAL " >&2
  840. return 1
  841. fi
  842. if [ "$type" == "compose" ]; then
  843. if ! ssh:run "root@$vps" -- \
  844. docker exec myc_cron_1 \
  845. grep rsync-backup /etc/crontabs/root >/dev/null 2>&1; then
  846. ssh:run "root@$vps" -- compose --debug up || {
  847. err "Command 'compose --debug up' failed."
  848. return 1
  849. }
  850. if ! ssh:run "root@$vps" -- \
  851. docker exec myc_cron_1 \
  852. grep rsync-backup /etc/crontabs/root >/dev/null 2>&1; then
  853. err "Launched 'compose up' successfully but ${YELLOW}cron${NORMAL} container is not setup as expected."
  854. echo " Was waiting for existence of a line mentionning 'rsync-backup' in '/etc/crontabs/root' in it." >&2
  855. return 1
  856. fi
  857. fi
  858. fi
  859. dest="$server"
  860. dest="${dest%/*}"
  861. ssh_options=()
  862. if [[ "$dest" == *":"* ]]; then
  863. port="${dest##*:}"
  864. dest="${dest%%:*}"
  865. ssh_options=(-p "$port")
  866. else
  867. port=""
  868. dest="${dest%%:*}"
  869. fi
  870. cmd=(ssh "${ssh_options[@]}" "$admin"@"$dest" ssh-key add "$key")
  871. echo "${WHITE}Launching:${NORMAL} ${cmd[@]}"
  872. "${cmd[@]}" || {
  873. err "Failed add key to backup server '$dest'."
  874. return 1
  875. }
  876. echo "${WHITE}Launching backup${NORMAL} from '$vps'"
  877. ssh:run "root@$vps" -- vps backup || {
  878. err "First backup failed to run."
  879. return 1
  880. }
  881. echo "Backup is ${GREEN}up and running${NORMAL}."
  882. }
  883. vps_update() {
  884. local vps="$1"
  885. vps_connection_check "$vps" || return 1
  886. ssh:run "root@$vps" -- myc-update </dev/null
  887. }
  888. vps_bash() {
  889. local vps="$1"
  890. vps_connection_check "$vps" </dev/null || return 1
  891. ssh:run "root@$vps" -- bash
  892. }
  893. vps_mux() {
  894. local fn="$1" vps_done VPS max_size vps
  895. shift
  896. VPS=($(printf "%s\n" "$@" | sort))
  897. max_size=0
  898. declare -A vps_done;
  899. new_vps=()
  900. for name in "${VPS[@]}"; do
  901. [ -n "${vps_done[$name]}" ] && {
  902. warn "duplicate vps '$name' provided. Ignoring."
  903. continue
  904. }
  905. vps_done["$name"]=1
  906. new_vps+=("$name")
  907. size_name="${#name}"
  908. [ "$max_size" -lt "${size_name}" ] &&
  909. max_size="$size_name"
  910. done
  911. settmpdir "_0KM_TMP_DIR"
  912. cat > "$_0KM_TMP_DIR/code"
  913. for vps in "${new_vps[@]}"; do
  914. label=$(printf "%-${max_size}s" "$vps")
  915. (
  916. {
  917. {
  918. "$fn" "$vps" < "$_0KM_TMP_DIR/code"
  919. } 3>&1 1>&2 2>&3 | sed -r "s/^/$DARKCYAN$label$NORMAL $DARKRED\!$NORMAL /g"
  920. set_errlvl "${PIPESTATUS[0]}"
  921. } 3>&1 1>&2 2>&3 | sed -r "s/^/$DARKCYAN$label$NORMAL $DARKGRAY\|$NORMAL /g"
  922. set_errlvl "${PIPESTATUS[0]}"
  923. ) &
  924. done
  925. wait
  926. }
  927. [ "$SOURCED" ] && return 0
  928. ##
  929. ## Command line processing
  930. ##
  931. cmdline.spec.gnu
  932. cmdline.spec.reporting
  933. cmdline.spec.gnu vps-setup
  934. cmdline.spec::cmd:vps-setup:run() {
  935. : :posarg: HOST 'Target host to check/fix ssh-access'
  936. : :optfla: --force,-f "Will force domain name change, even if
  937. current hostname was not recognized as
  938. an ovh domain name. "
  939. depends sshpass shyaml
  940. KEY_PATH="ssh-access.public-keys"
  941. local keys=$(config get-value -y "ssh-access.public-keys") || true
  942. if [ -z "$keys" ]; then
  943. err "No ssh publickeys configured in config file."
  944. echo " Looking for keys in ${WHITE}${KEY_PATH}${NORMAL}" \
  945. "in config file." >&2
  946. config:exists --message 2>&1 | prefix " "
  947. if [ "${PIPESTATUS[0]}" == "0" ]; then
  948. echo " Config file found in $(config:filename)"
  949. fi
  950. return 1
  951. fi
  952. local tkey=$(e "$keys" | shyaml get-type)
  953. if [ "$tkey" != "sequence" ]; then
  954. err "Value type of ${WHITE}${KEY_PATH}${NORMAL} unexpected (is $tkey, expecting sequence)."
  955. echo " Check content of $(config:filename), and make sure to use a sequence." >&2
  956. return 1
  957. fi
  958. local IP NAME keys host_pass_connected
  959. if ! IP=$(resolve "$HOST"); then
  960. err "'$HOST' name unresolvable."
  961. exit 1
  962. fi
  963. NAME="$HOST"
  964. if [ "$IP" != "$HOST" ]; then
  965. NAME="$HOST ($IP)"
  966. fi
  967. if ! is-port-open "$IP" "22"; then
  968. err "$NAME unreachable or port 22 closed."
  969. exit 1
  970. fi
  971. debug "Host $IP's port 22 is open."
  972. if ! host_pass_connected=$(ssh:open-try \
  973. {root,debian}@"$HOST"); then
  974. err "Could not connect to {root,debian}@$HOST with publickey nor password."
  975. exit 1
  976. fi
  977. read-0a host password <<<"$host_pass_connected"
  978. sudo_if_necessary=
  979. if [ "$password" -o "${host%%@*}" != "root" ]; then
  980. if ! ssh:run "$host" -- sudo -n true >/dev/null 2>&1; then
  981. err "Couldn't do a password-less sudo from $host."
  982. echo " This is not yet supported."
  983. exit 1
  984. else
  985. sudo_if_necessary=sudo
  986. fi
  987. fi
  988. SUPPORTED_KEY_TYPE=(ssh-rsa ssh-ed25519)
  989. Section Checking access
  990. while read-0 key; do
  991. prefix="${key%% *}"
  992. if ! [[ " ${SUPPORTED_KEY_TYPE[*]} " == *" $prefix "* ]]; then
  993. err "Unsupported key:"$'\n'"$key"
  994. echo " Please use only key of the following type:" >&2
  995. printf " - %s\n" "${SUPPORTED_KEY_TYPE[@]}" >&2
  996. return 1
  997. fi
  998. label="${key##* }"
  999. Elt "considering adding key ${DARKYELLOW}$label${NORMAL}"
  1000. dest="/root/.ssh/authorized_keys"
  1001. if ssh:run "$host" -- $sudo_if_necessary grep "\"$key\"" "$dest" >/dev/null 2>&1 </dev/null; then
  1002. print_info "already present"
  1003. print_status noop
  1004. Feed
  1005. else
  1006. if echo "$key" | ssh:run "$host" -- $sudo_if_necessary tee -a "$dest" >/dev/null; then
  1007. print_info added
  1008. else
  1009. echo
  1010. Feedback failure
  1011. return 1
  1012. fi
  1013. Feedback success
  1014. fi
  1015. done < <(e "$keys" | shyaml get-values-0)
  1016. Section Checking ovh hostname file
  1017. Elt "Checking /etc/ovh-hostname"
  1018. if ! ssh:run "$host" -- $sudo_if_necessary [ -e "/etc/ovh-hostname" ]; then
  1019. print_info "creating"
  1020. ssh:run "$host" -- $sudo_if_necessary cp /etc/hostname /etc/ovh-hostname
  1021. ovhname=$(ssh:run "$host" -- $sudo_if_necessary cat /etc/ovh-hostname)
  1022. Elt "Checking /etc/ovh-hostname: $ovhname"
  1023. Feedback || return 1
  1024. else
  1025. ovhname=$(ssh:run "$host" -- $sudo_if_necessary cat /etc/ovh-hostname)
  1026. Elt "Checking /etc/ovh-hostname: $ovhname"
  1027. print_info "already present"
  1028. print_status noop
  1029. Feed
  1030. fi
  1031. if ! is_ovh_domain_name "$HOST" && ! str_is_ipv4 "$HOST"; then
  1032. Section Checking hostname
  1033. Elt "Checking /etc/hostname..."
  1034. old_etc_hostname="$(ssh:run "$host" -- $sudo_if_necessary cat /etc/hostname)"
  1035. if [ "$old_etc_hostname" != "$HOST" ]; then
  1036. Elt "/etc/hostname is '$old_etc_hostname'"
  1037. if is_ovh_hostname "$old_etc_hostname" || [ -n "$opt_force" ]; then
  1038. Elt "Hostname '$old_etc_hostname' --> '$HOST'"
  1039. print_info "creating"
  1040. echo "$HOST" | ssh:run "$host" -- $sudo_if_necessary tee /etc/hostname >/dev/null &&
  1041. ssh:run "$host" -- $sudo_if_necessary hostname "$HOST"
  1042. Feedback || return 1
  1043. else
  1044. Elt "Hostname '$old_etc_hostname' isn't an ovh domain"
  1045. print_info "no change"
  1046. print_status noop
  1047. Feed
  1048. warn "Domain name was not changed because it was already set"
  1049. echo " (use \`-f\` or \`--force\`) to force domain name change to $HOST." >&2
  1050. fi
  1051. else
  1052. print_info "already set"
  1053. print_status noop
  1054. Feed
  1055. fi
  1056. Elt "Checking consistency between /etc/hostname and \`hostname\`..."
  1057. etc_hostname="$(ssh:run "$host" -- $sudo_if_necessary cat /etc/hostname)"
  1058. transient_hostname="$(ssh:run "$host" -- $sudo_if_necessary hostname)"
  1059. if [ "$etc_hostname" != "$transient_hostname" ]; then
  1060. print_info "change"
  1061. ssh:run "$host" -- $sudo_if_necessary hostname "$etc_hostname"
  1062. Feedback || return 1
  1063. else
  1064. print_info "consistent"
  1065. print_status noop
  1066. Feed
  1067. fi
  1068. else
  1069. info "Not changing domain as '$HOST' doesn't seem to be final domain."
  1070. fi
  1071. }
  1072. cmdline.spec.gnu vps-check
  1073. cmdline.spec::cmd:vps-check:run() {
  1074. : :posarg: [VPS...] 'Target host(s) to check'
  1075. echo "" |
  1076. vps_mux vps_check "${VPS[@]}"
  1077. }
  1078. cmdline.spec.gnu vps-install
  1079. cmdline.spec::cmd:vps-install:run() {
  1080. :
  1081. }
  1082. cmdline.spec.gnu backup
  1083. cmdline.spec:vps-install:cmd:backup:run() {
  1084. : :posarg: BACKUP_TARGET 'Backup target.
  1085. (ie: myadmin@backup.domain.org:10023/256)'
  1086. : :optfla: --ignore-domain-check \
  1087. "Allow to bypass the domain check in
  1088. compose file (only used in compose
  1089. installation)."
  1090. : :optfla: --ignore-ping-check "Allow to bypass the ping check of host."
  1091. : :posarg: [VPS...] 'Target host(s) to check'
  1092. if [ "${#VPS[@]}" == 0 ]; then
  1093. warn "VPS list provided in command line is empty. Nothing will be done."
  1094. return 0
  1095. fi
  1096. if ! [[ "$BACKUP_TARGET" == *"@"* ]]; then
  1097. err "Missing admin account identifier in backup target."
  1098. echo " Have you forgottent to specify an account, ie 'myadmin@<MYBACKUP_SERVER>' ?)"
  1099. return 1
  1100. fi
  1101. admin=${BACKUP_TARGET%%@*}
  1102. server=${BACKUP_TARGET#*@}
  1103. opts=()
  1104. [ -n "$opt_ignore_ping_check" ] &&
  1105. opts+=("--ignore-ping-check")
  1106. [ -n "$opt_ignore_domain_check" ] &&
  1107. opts+=("--ignore-domain-check")
  1108. p0 "$admin" "$server" "${opts[@]}" |
  1109. vps_mux vps_install_backup "${VPS[@]}"
  1110. }
  1111. cmdline.spec.gnu vps-backup
  1112. cmdline.spec::cmd:vps-backup:run() {
  1113. :
  1114. }
  1115. cmdline.spec.gnu ls
  1116. cmdline.spec:vps-backup:cmd:ls:run() {
  1117. : :posarg: BACKUP_ID 'Backup id.
  1118. (ie: myadmin@backup.domain.org:10023)'
  1119. if ! [[ "$BACKUP_ID" == *"@"* ]]; then
  1120. err "Missing admin account identifier in backup id."
  1121. echo " Have you forgottent to specify an admin account ?" \
  1122. "ie 'myadmin@<MYBACKUP_SERVER>#<BACKUP_IDENT>' ?)"
  1123. return 1
  1124. fi
  1125. id=${BACKUP_ID##*#}
  1126. BACKUP_TARGET=${BACKUP_ID%#*}
  1127. admin=${BACKUP_TARGET%%@*}
  1128. server=${BACKUP_TARGET#*@}
  1129. ## XXXvlab: in this first implementation we expect to have access
  1130. ## to the server main ssh port 22, so we won't use the provided port.
  1131. ssh_options=()
  1132. if [[ "$server" == *":"* ]]; then
  1133. ssh_options+=(-p "${server#*:}")
  1134. server=${server%%:*}
  1135. fi
  1136. ssh "${ssh_options[@]}" "$admin"@"$server" ssh-key ls
  1137. }
  1138. cmdline.spec.gnu recover
  1139. cmdline.spec:vps-backup:cmd:recover:run() {
  1140. : :posarg: BACKUP_ID 'Backup id.
  1141. (ie: myadmin@backup.domain.org:10023#mx.myvps.org
  1142. myadmin@ark-01.org#myid:/a/path
  1143. admin@ark-02.io#myid:myqsl,mailcow)'
  1144. : :posarg: VPS_PATH 'Target host(s) to check.
  1145. (ie: myvps.com
  1146. myvps.com:/a/path)'
  1147. : :optval: --date,-D '"last", or label of version to recover. (Default: "last").'
  1148. : :optfla: --force,-f 'Will allow you to bypass some checks.'
  1149. if ! [[ "$BACKUP_ID" == *"@"* ]]; then
  1150. err "Missing admin account identifier in backup id."
  1151. echo " Have you forgottent to specify an admin account ?" \
  1152. "ie 'myadmin@<MYBACKUP_SERVER>#<BACKUP_IDENT>' ?)"
  1153. return 1
  1154. fi
  1155. if ! [[ "$BACKUP_ID" == *"@"*"#"* ]]; then
  1156. err "Missing backup label identifier in backup id."
  1157. echo " Have you forgottent to specify a backup label identifier ?" \
  1158. "ie 'myadmin@<MYBACKUP_SERVER>#<BACKUP_IDENT>' ?)"
  1159. return 1
  1160. fi
  1161. id_path=${BACKUP_ID#*#}
  1162. if [[ "$id_path" == *":"* ]]; then
  1163. id=${id_path%%:*}
  1164. path=${id_path#*:}
  1165. else
  1166. id="$id_path"
  1167. path=
  1168. fi
  1169. BACKUP_TARGET=${BACKUP_ID%#*}
  1170. admin=${BACKUP_TARGET%%@*}
  1171. server=${BACKUP_TARGET#*@}
  1172. ssh_options=()
  1173. if [[ "$server" == *":"* ]]; then
  1174. ssh_options+=(-p "${server#*:}")
  1175. ssh_server=${server%%:*}
  1176. fi
  1177. BACKUP_PATH="/srv/datastore/data/rsync-backup-target/var/mirror"
  1178. if ! content=$(ssh:run "$admin"@"${ssh_server}" "${ssh_options[@]}" -- ssh-key ls 2>/dev/null); then
  1179. err "Access denied to '$admin@${server}'."
  1180. return 1
  1181. fi
  1182. idents=$(echo "$content" | sed -r "s/"$'\e'"\[[0-9]+(;[0-9]+)*m//g" | cut -f 2 -d " ")
  1183. if ! [[ $'\n'"$idents"$'\n' == *$'\n'"$id"$'\n'* ]]; then
  1184. err "Given backup id '$id' not found in $admin@${server}'s idents."
  1185. return 1
  1186. fi
  1187. rtype=$(ssh:run "$admin"@"${ssh_server}" "${ssh_options[@]}" -- ssh-key get-type "$id" ) &&
  1188. info "Backup archive matches ${WHITE}${rtype}${NORMAL} type"
  1189. p0 "$admin" "$server" "$id" "$path" "$rtype" "$opt_force" |
  1190. vps_backup_recover "${VPS_PATH}"
  1191. }
  1192. cmdline.spec.gnu vps-update
  1193. cmdline.spec::cmd:vps-update:run() {
  1194. : :posarg: [VPS...] 'Target host to check'
  1195. echo "" |
  1196. vps_mux vps_update "${VPS[@]}"
  1197. }
  1198. cmdline.spec.gnu vps-mux
  1199. cmdline.spec::cmd:vps-mux:run() {
  1200. : :posarg: [VPS...] 'Target host(s) to check'
  1201. cat | vps_mux vps_bash "${VPS[@]}"
  1202. }
  1203. cmdline.spec.gnu vps-space
  1204. cmdline.spec::cmd:vps-space:run() {
  1205. : :posarg: [VPS...] 'Target host(s) to check'
  1206. echo "df /srv -h | tail -n +2 | sed -r 's/ +/ /g' | cut -f 2-5 -d ' '" |
  1207. vps_mux vps_bash "${VPS[@]}"
  1208. }
  1209. cmdline.spec.gnu vps-stats
  1210. cmdline.spec::cmd:vps-stats:run() {
  1211. : :posarg: [VPS...] 'Target host(s) to get stats'
  1212. : :optfla: --follow,-f 'Refresh graph every 2m'
  1213. : :optval: --timespan,-t 'timespan START[..END]'
  1214. : :optval: --resource,-r 'resource(s) separated with a comma'
  1215. : :optval: --interval,-i 'refersh interval (default: 60s)'
  1216. local opts_rrdfetch=( -a )
  1217. if [ -n "${opt_timespan}" ]; then
  1218. start=${opt_timespan%\.\.*}
  1219. opts_rrdfetch+=(-s "$start")
  1220. if [ "$start" != "${opt_timespan}" ]; then
  1221. end=${opt_timespan#*..}
  1222. opts_rrdfetch+=(-e "$end")
  1223. fi
  1224. fi
  1225. local resources=(c.memory c.network load_avg disk)
  1226. if [ -n "${opt_resource}" ]; then
  1227. resources=(${opt_resource//,/ })
  1228. fi
  1229. local not_found=()
  1230. for resource in "${resources[@]}"; do
  1231. if ! fn.exists "graph:def:$resource"; then
  1232. not_found+=("$resource")
  1233. fi
  1234. done
  1235. if [[ "${#not_found[@]}" -gt 0 ]]; then
  1236. not_found_msg=$(printf "%s, " "${not_found[@]}")
  1237. not_found_msg=${not_found_msg%, }
  1238. err "Unsupported resource(s) provided: ${not_found_msg}"
  1239. echo " resource must be one-of:" >&2
  1240. declare -F | egrep 'graph:def:[a-zA-Z_.]+$' | cut -f 3- -d " " | cut -f 3- -d ":" | prefix " - " >&2
  1241. return 1
  1242. fi
  1243. if [ "${#VPS[@]}" == 0 ]; then
  1244. err "You must provide a VPS list as positional arguments"
  1245. return 1
  1246. fi
  1247. include cache
  1248. if [ -z "$VAR_DIR" ]; then
  1249. err "Unset \$VAR_DIR, can't downlowd rrd graph"
  1250. return 1
  1251. fi
  1252. mkdir -p "$VAR_DIR/rrd"
  1253. if ! [ -d "$VAR_DIR/rrd" ]; then
  1254. err "Invalid \$VAR_DIR: '$VAR_DIR/rrd' is not a directory"
  1255. return 1
  1256. fi
  1257. (
  1258. for vps in "${VPS[@]}"; do
  1259. (
  1260. {
  1261. {
  1262. ssh:open "root@$vps" 2>/dev/null || {
  1263. err "Can't open connection $vps."
  1264. return 1
  1265. }
  1266. while true; do
  1267. if ssh:run "root@$vps" -- "[ -d '/var/lib/vps/rrd' ]"; then
  1268. echo "${WHITE}Collecting stats${NORMAL}..."
  1269. {
  1270. {
  1271. ssh:rsync "root@$vps:/var/lib/vps/rrd/" "${VAR_DIR}/rrd/${vps}"
  1272. } 3>&1 1>&2 2>&3 | prefix " ${DARKRED}\!${NORMAL} "
  1273. set_errlvl "${PIPESTATUS[0]}"
  1274. } 3>&1 1>&2 2>&3 | prefix " ${GRAY}|${NORMAL} "
  1275. echo " ${GRAY}..${NORMAL} ${DARKGREEN}done${NORMAL} collecting stats"
  1276. else
  1277. warn "No stats found. Did you run 'myc-update' on the vps ?."
  1278. fi
  1279. [ -z "$opt_follow" ] && break
  1280. echo "${WHITE}Sleeping ${DARKYELLOW}${opt_interval:-60}${NORMAL}s..."
  1281. sleep "${opt_interval:-60}"
  1282. echo " ${GRAY}..${NORMAL} ${DARKGREEN}done${NORMAL} sleeping"
  1283. done
  1284. } 3>&1 1>&2 2>&3 | prefix " ${DARKRED}\!${GRAY} collect(${DARKCYAN}$vps${GRAY})${NORMAL} "
  1285. set_errlvl "${PIPESTATUS[0]}"
  1286. } 3>&1 1>&2 2>&3 | prefix " ${GRAY}| collect(${DARKCYAN}$vps${GRAY})${NORMAL} " >&2
  1287. ) &
  1288. done
  1289. wait
  1290. ) &
  1291. collect_pid="$!"
  1292. if [ -z "$opt_follow" ]; then
  1293. echo "${WHITE}Fetching last stats${NORMAL}${GRAY}..${NORMAL}" >&2
  1294. wait
  1295. echo " ${GRAY}..${DARKGREEN} done${NORMAL} fetching stats" >&2
  1296. else
  1297. collect_end_msg=" ${GRAY}..${NORMAL} ${DARKGREEN}stop${NORMAL} collecting daemon (pid: ${DARKYELLOW}$collect_pid${NORMAL})"
  1298. trap_add EXIT \
  1299. "printf '%s\n' \"$collect_end_msg\" && kill $collect_pid"
  1300. echo "${WHITE}Start collecting daemon${NORMAL} (pid: ${DARKYELLOW}$collect_pid${NORMAL}) ${GRAY}..${NORMAL}" >&2
  1301. fi
  1302. ( depends gnuplot ) || {
  1303. echo ""
  1304. echo " Gnuplot is required to display graphs..." \
  1305. "You might want to try to install ${WHITE}gnuplot${NORMAL} with:"
  1306. echo ""
  1307. echo " apt install gnuplot"
  1308. echo ""
  1309. return 1
  1310. } >&2
  1311. ( depends rrdtool ) || {
  1312. echo ""
  1313. echo " Rrdtool is required to display graphs..." \
  1314. "You might want to try to install ${WHITE}rrdtool${NORMAL} with:"
  1315. echo ""
  1316. echo " apt install rrdtool"
  1317. echo ""
  1318. return 1
  1319. } >&2
  1320. rrd_tmpdir=$(mktemp -d -t rrd.XXXXXX)
  1321. export rrd_tmpdir
  1322. export GNUTERM=qt
  1323. ## rrdtool fetch will use comma for floating point depending on some locals !
  1324. export LC_ALL=C
  1325. exec {PFD}> >(exec gnuplot 2>/dev/null)
  1326. gnuplot_pid="$!"
  1327. if [ -z "$opt_follow" ]; then
  1328. echo "${WHITE}Draw gnuplot graph${GRAY}..${NORMAL}" >&2
  1329. else
  1330. gnuplot_end_msg=" ${GRAY}..${NORMAL} ${DARKGREEN}stop${NORMAL} gnuplot process (pid: $gnuplot_pid)"
  1331. trap_add EXIT \
  1332. "printf '%s\n' \"$gnuplot_end_msg\" && kill $gnuplot_pid"
  1333. echo "${WHITE}Start gnuplot process${NORMAL} (pid: $gnuplot_pid) ${GRAY}..${NORMAL}" >&2
  1334. fi
  1335. echo "set term qt noraise replotonresize" >&$PFD
  1336. while true; do
  1337. {
  1338. i=0
  1339. data_start_ts=
  1340. data_stop_ts=
  1341. for resource in "${resources[@]}"; do
  1342. for vps in "${VPS[@]}"; do
  1343. ((i++))
  1344. {
  1345. {
  1346. rrd_vps_path="$VAR_DIR/rrd/$vps"
  1347. if [ -d "$rrd_vps_path" ]; then
  1348. graph:def:"${resource}" "$vps" "$i" "${opts_rrdfetch[@]}"
  1349. else
  1350. warn "No data yet, ignoring..."
  1351. fi
  1352. } 3>&1 1>&2 2>&3 | prefix " ${DARKRED}\!${GRAY} graph(${DARKCYAN}$vps${GRAY}:${WHITE}$resource${NORMAL})${NORMAL} "
  1353. set_errlvl "${PIPESTATUS[0]}"
  1354. } 3>&1 1>&2 2>&3 || continue 1
  1355. done
  1356. done
  1357. } >&$PFD
  1358. if [ -z "$opt_follow" ]; then
  1359. echo " ${GRAY}..${DARKGREEN} done${NORMAL} gnuplot graphing" >&2
  1360. break
  1361. else
  1362. {
  1363. echo "${WHITE}Sleeping ${DARKYELLOW}${opt_interval:-60}${NORMAL}s..."
  1364. sleep "${opt_interval:-60}"
  1365. echo " ${GRAY}..${NORMAL} ${DARKGREEN}done${NORMAL} sleeping"
  1366. } | prefix " ${GRAY}| gnuplot:${NORMAL} " >&2
  1367. fi
  1368. done
  1369. if [ -n "$opt_follow" ]; then
  1370. echo "Waiting for child process to finish.." >&2
  1371. wait
  1372. echo " ..done" >&2
  1373. else
  1374. echo "pause mouse close" >&$PFD
  1375. fi
  1376. }
  1377. graph:def:c.memory() {
  1378. local vps="$1" i="$2"
  1379. shift 2
  1380. local opts_rrdfetch=("$@")
  1381. local resource="memory"
  1382. rrd_vps_path="$VAR_DIR/rrd/$vps"
  1383. [ -d "$rrd_vps_path/containers" ] || {
  1384. warn "No containers data yet for vps '$vps'... Ignoring"
  1385. return 0
  1386. }
  1387. containers=(
  1388. $(
  1389. cd "$rrd_vps_path/containers";
  1390. find -maxdepth 3 -mindepth 3 -name "${resource}.rrd" -type f |
  1391. sed -r 's%^./([^/]+/[^/]+)/[^/]+.rrd$%\1%g'
  1392. )
  1393. )
  1394. gnuplot_line_config=(
  1395. "set term qt $i title \"$vps $resource\" replotonresize noraise"
  1396. "set title '$vps'"
  1397. "set xdata time"
  1398. "set timefmt '%s'"
  1399. "set ylabel '$resource Usage'"
  1400. "set format y '%s'"
  1401. "set ytics format ' %g'"
  1402. "set mouse mouseformat 6"
  1403. "set yrange [0:*] "
  1404. "set border behind"
  1405. )
  1406. printf "%s\n" "${gnuplot_line_config[@]}"
  1407. first=1
  1408. for container in "${containers[@]}"; do
  1409. rrdfetch_cmd="'< rrdtool fetch \"$rrd_vps_path/containers/$container/$resource.rrd\""
  1410. rrdfetch_cmd+=" AVERAGE ${opts_rrdfetch[*]} | \\"$'\n'
  1411. rrdfetch_cmd+=" tail -n +2 | \\"$'\n'
  1412. rrdfetch_cmd+=" egrep -v \"^$\" | sed -r \"s/ -?nan/ -/g;s/^([0-9]+): /\\1 /g\"'"
  1413. rrdfetch_cmd_bash=$(eval echo "${rrdfetch_cmd}")
  1414. rrdfetch_cmd_bash=${rrdfetch_cmd_bash#< }
  1415. first_ts=
  1416. first_ts=$(eval "$rrdfetch_cmd_bash" | head -n 1 | cut -f 1 -d " ")
  1417. if [ -z "$first_ts" ]; then
  1418. warn "No data for $container on vps $vps, skipping..."
  1419. continue
  1420. fi
  1421. last_ts=$(eval "$rrdfetch_cmd_bash" | tail -n 1 | cut -f 1 -d " ")
  1422. if [[ -z "$data_start_ts" ]] || [[ "$data_start_ts" > "$first_ts" ]]; then
  1423. data_start_ts="$first_ts"
  1424. fi
  1425. if [[ -z "$data_stop_ts" ]] || [[ "$data_stop_ts" < "$last_ts" ]]; then
  1426. data_stop_ts="$last_ts"
  1427. fi
  1428. if [ -n "$first" ]; then
  1429. first=
  1430. echo "plot \\"
  1431. else
  1432. echo ", \\"
  1433. fi
  1434. container="${container//\'/}"
  1435. container="${container//@/\\@}"
  1436. echo -n " ${rrdfetch_cmd} u 1:(\$3/(1000*1000*1000)) w lines title '${container//_/\\_}'"
  1437. done
  1438. echo
  1439. }
  1440. graph:def:c.network() {
  1441. local vps="$1" i="$2"
  1442. shift 2
  1443. local opts_rrdfetch=("$@")
  1444. local resource="network"
  1445. rrd_vps_path="$VAR_DIR/rrd/$vps"
  1446. [ -d "$rrd_vps_path/containers" ] || {
  1447. warn "No containers data yet for vps '$vps'... Ignoring"
  1448. return 0
  1449. }
  1450. containers=(
  1451. $(
  1452. cd "$rrd_vps_path/containers";
  1453. find -maxdepth 3 -mindepth 3 -name "${resource}.rrd" -type f |
  1454. sed -r 's%^./([^/]+/[^/]+)/[^/]+.rrd$%\1%g'
  1455. )
  1456. )
  1457. gnuplot_line_config=(
  1458. "set term qt $i title \"$vps $resource\" replotonresize noraise"
  1459. "set title '$vps'"
  1460. "set xdata time"
  1461. "set timefmt '%s'"
  1462. "set ylabel '$resource Usage'"
  1463. "set format y '%s'"
  1464. "set ytics format ' %.2f MiB/s'"
  1465. "set mouse mouseformat 6"
  1466. "set yrange [0:*] "
  1467. "set border behind"
  1468. )
  1469. printf "%s\n" "${gnuplot_line_config[@]}"
  1470. first=1
  1471. for container in "${containers[@]}"; do
  1472. rrdfetch_cmd="'< rrdtool fetch \"$rrd_vps_path/containers/$container/$resource.rrd\""
  1473. rrdfetch_cmd+=" AVERAGE ${opts_rrdfetch[*]} | \\"$'\n'
  1474. rrdfetch_cmd+=" tail -n +2 | \\"$'\n'
  1475. rrdfetch_cmd+=" egrep -v \"^$\" | sed -r \"s/ -?nan/ -/g;s/^([0-9]+): /\\1 /g\"'"
  1476. rrdfetch_cmd_bash=$(eval echo "${rrdfetch_cmd}")
  1477. rrdfetch_cmd_bash=${rrdfetch_cmd_bash#< }
  1478. first_ts=
  1479. first_ts=$(eval "$rrdfetch_cmd_bash" | head -n 1 | cut -f 1 -d " ")
  1480. if [ -z "$first_ts" ]; then
  1481. warn "No data for $container on vps $vps, skipping..."
  1482. continue
  1483. fi
  1484. last_ts=$(eval "$rrdfetch_cmd_bash" | tail -n 1 | cut -f 1 -d " ")
  1485. if [[ -z "$data_start_ts" ]] || [[ "$data_start_ts" > "$first_ts" ]]; then
  1486. data_start_ts="$first_ts"
  1487. fi
  1488. if [[ -z "$data_stop_ts" ]] || [[ "$data_stop_ts" < "$last_ts" ]]; then
  1489. data_stop_ts="$last_ts"
  1490. fi
  1491. if [ -n "$first" ]; then
  1492. first=
  1493. echo "plot \\"
  1494. else
  1495. echo ", \\"
  1496. fi
  1497. container="${container//\'/}"
  1498. container="${container//@/\\@}"
  1499. echo -n " ${rrdfetch_cmd} u 1:((\$3 / 1024) / 1024) w lines title '${container//_/\\_}'"
  1500. done
  1501. echo
  1502. }
  1503. graph:def:load_avg() {
  1504. local vps="$1" i="$2"
  1505. shift 2
  1506. local opts_rrdfetch=("$@")
  1507. rrd_vps_path="$VAR_DIR/rrd/$vps"
  1508. [ -f "$rrd_vps_path/$resource.rrd" ] || {
  1509. warn "No containers data yet for vps '$vps'... Ignoring"
  1510. return 0
  1511. }
  1512. gnuplot_line_config=(
  1513. "set term qt $i title \"$vps $resource\" replotonresize noraise"
  1514. "set title '$vps'"
  1515. "set xdata time"
  1516. "set timefmt '%s'"
  1517. "set ylabel '${resource//_/\\_} Usage'"
  1518. "set format y '%s'"
  1519. "set ytics format '%g'"
  1520. "set mouse mouseformat 6"
  1521. "set yrange [0:*] "
  1522. "set border behind"
  1523. )
  1524. printf "%s\n" "${gnuplot_line_config[@]}"
  1525. first=1
  1526. for value in 1m:2 5m:3 15m:4; do
  1527. label="${value%:*}"
  1528. col_num="${value#*:}"
  1529. rrdfetch_cmd="'< rrdtool fetch \"$rrd_vps_path/$resource.rrd\""
  1530. rrdfetch_cmd+=" AVERAGE ${opts_rrdfetch[*]} | \\"$'\n'
  1531. rrdfetch_cmd+=" tail -n +2 | \\"$'\n'
  1532. rrdfetch_cmd+=" egrep -v \"^$\" | sed -r \"s/ -?nan/ -/g;s/^([0-9]+): /\\1 /g\"'"
  1533. rrdfetch_cmd_bash=$(eval echo "${rrdfetch_cmd}")
  1534. rrdfetch_cmd_bash=${rrdfetch_cmd_bash#< }
  1535. first_ts=
  1536. first_ts=$(eval "$rrdfetch_cmd_bash" | head -n 1 | cut -f 1 -d " ")
  1537. if [ -z "$first_ts" ]; then
  1538. warn "No data for $resource on vps $vps, skipping..."
  1539. continue
  1540. fi
  1541. last_ts=$(eval "$rrdfetch_cmd_bash" | tail -n 1 | cut -f 1 -d " ")
  1542. if [[ -z "$data_start_ts" ]] || [[ "$data_start_ts" > "$first_ts" ]]; then
  1543. data_start_ts="$first_ts"
  1544. fi
  1545. if [[ -z "$data_stop_ts" ]] || [[ "$data_stop_ts" < "$last_ts" ]]; then
  1546. data_stop_ts="$last_ts"
  1547. fi
  1548. if [ -n "$first" ]; then
  1549. first=
  1550. echo "plot \\"
  1551. else
  1552. echo ", \\"
  1553. fi
  1554. container="${container//\'/}"
  1555. container="${container//@/\\@}"
  1556. echo -n " ${rrdfetch_cmd} u 1:$col_num w lines title '${label}'"
  1557. done
  1558. echo
  1559. }
  1560. graph:def:disk() {
  1561. local vps="$1" i="$2"
  1562. shift 2
  1563. local opts_rrdfetch=("$@")
  1564. rrd_vps_path="$VAR_DIR/rrd/$vps"
  1565. [ -f "$rrd_vps_path/$resource.rrd" ] || {
  1566. warn "No containers data yet for vps '$vps'... Ignoring"
  1567. return 0
  1568. }
  1569. gnuplot_line_config=(
  1570. "set term qt $i title \"$vps $resource\" replotonresize noraise"
  1571. "set title '$vps'"
  1572. "set xdata time"
  1573. "set timefmt '%s'"
  1574. "set ylabel '${resource//_/\\_} Usage'"
  1575. "set format y '%s'"
  1576. "set ytics format '%g GiB'"
  1577. "set mouse mouseformat 6"
  1578. "set yrange [0:*] "
  1579. "set border behind"
  1580. )
  1581. printf "%s\n" "${gnuplot_line_config[@]}"
  1582. first=1
  1583. for value in used:2 size:3; do
  1584. label="${value%:*}"
  1585. col_num="${value#*:}"
  1586. rrdfetch_cmd="'< rrdtool fetch \"$rrd_vps_path/$resource.rrd\""
  1587. rrdfetch_cmd+=" AVERAGE ${opts_rrdfetch[*]} | \\"$'\n'
  1588. rrdfetch_cmd+=" tail -n +2 | \\"$'\n'
  1589. rrdfetch_cmd+=" egrep -v \"^$\" | sed -r \"s/ -?nan/ -/g;s/^([0-9]+): /\\1 /g\"'"
  1590. rrdfetch_cmd_bash=$(eval echo "${rrdfetch_cmd}")
  1591. rrdfetch_cmd_bash=${rrdfetch_cmd_bash#< }
  1592. first_ts=
  1593. first_ts=$(eval "$rrdfetch_cmd_bash" | head -n 1 | cut -f 1 -d " ")
  1594. if [ -z "$first_ts" ]; then
  1595. warn "No data for $resource on vps $vps, skipping..."
  1596. continue
  1597. fi
  1598. last_ts=$(eval "$rrdfetch_cmd_bash" | tail -n 1 | cut -f 1 -d " ")
  1599. if [[ -z "$data_start_ts" ]] || [[ "$data_start_ts" > "$first_ts" ]]; then
  1600. data_start_ts="$first_ts"
  1601. fi
  1602. if [[ -z "$data_stop_ts" ]] || [[ "$data_stop_ts" < "$last_ts" ]]; then
  1603. data_stop_ts="$last_ts"
  1604. fi
  1605. if [ -n "$first" ]; then
  1606. first=
  1607. echo "plot \\"
  1608. else
  1609. echo ", \\"
  1610. fi
  1611. container="${container//\'/}"
  1612. container="${container//@/\\@}"
  1613. echo -n " ${rrdfetch_cmd} u 1:(\$${col_num}/(1024*1024)) w lines title '${label}'"
  1614. done
  1615. echo
  1616. }
  1617. graph:def:disk-2() {
  1618. local vps="$1" i="$2"
  1619. shift 2
  1620. local opts_rrdfetch=("$@")
  1621. rrd_vps_path="$VAR_DIR/rrd/$vps"
  1622. rscolcat:check-install || return 1
  1623. folders=(
  1624. $(
  1625. cd "$rrd_vps_path/$resource/data";
  1626. find -maxdepth 2 -mindepth 2 -name "size.rrd" -type f |
  1627. sed -r 's%^./([^/]+)/[^/]+.rrd$%data/\1%g'
  1628. )
  1629. )
  1630. gnuplot_line_config=(
  1631. "set term qt $i title \"$vps $resource\" replotonresize noraise"
  1632. "set title '$vps'"
  1633. "set xdata time"
  1634. "set timefmt '%s'"
  1635. "set ylabel '${resource//_/\\_} Usage'"
  1636. "set format y '%s'"
  1637. "set ytics format '%g GiB'"
  1638. "set mouse mouseformat 6"
  1639. "set yrange [0:*] "
  1640. "set border behind"
  1641. 'set key outside top right'
  1642. )
  1643. declare -A colors
  1644. colors[host/other]="#000000"
  1645. colors[docker/build_cache]="#87CEEB"
  1646. colors[docker/containers]="#0047AB"
  1647. colors[docker/images]="#00FFFF"
  1648. colors[docker/local_volumes]="#6495ED"
  1649. colors[docker/logs]="#000080"
  1650. printf "%s\n" "${gnuplot_line_config[@]}"
  1651. first=1
  1652. rrdfetch_cmd="'< rscolcat concat"
  1653. lines_def=()
  1654. legend_def=()
  1655. label="${value%:*}"
  1656. idx=1
  1657. previous_sum=0
  1658. values=(
  1659. host/other
  1660. docker/{build_cache,local_volumes,logs,containers,images}
  1661. "${folders[@]}"
  1662. )
  1663. declare -A values_colnb
  1664. for subvalue in "${values[@]}" ; do
  1665. idx=$((idx + 1))
  1666. values_colnb["$subvalue"]="$idx"
  1667. [ -e "$rrd_vps_path/$resource/$subvalue/size.rrd" ] || {
  1668. warn "No data yet for $subvalue on vps $vps, ignoring..."
  1669. continue
  1670. }
  1671. rrdfetch_cmd+=" \\"$'\n'
  1672. rrdfetch_cmd+=" <(rrdtool fetch \"$rrd_vps_path/$resource/$subvalue/size.rrd\""
  1673. rrdfetch_cmd+=" AVERAGE ${opts_rrdfetch[*]} | \\"$'\n'
  1674. rrdfetch_cmd+=" tail -n +2 | \\"$'\n'
  1675. rrdfetch_cmd+=" egrep -v \"^$\" | sed -r \"s/ -?nan/ -/g;s/^([0-9]+): /\\1 /g\")"
  1676. done
  1677. rrdfetch_cmd+="'"
  1678. rrdfetch_cmd_bash=$(eval echo "${rrdfetch_cmd}")
  1679. rrdfetch_cmd_bash=${rrdfetch_cmd_bash#< }
  1680. eval "$rrdfetch_cmd_bash" > "$rrd_tmpdir/data.txt" || return 1
  1681. first_ts=
  1682. first_ts=$(cat "$rrd_tmpdir/data.txt" | head -n 1 | cut -f 1 -d " ")
  1683. if [ -z "$first_ts" ]; then
  1684. err "No data for $resource on vps $vps, skipping..."
  1685. return 1
  1686. fi
  1687. last_ts=$(cat "$rrd_tmpdir/data.txt" | tail -n 1 | cut -f 1 -d " ")
  1688. last_valued_line=$(cat "$rrd_tmpdir/data.txt" | egrep -v '^[0-9]+( -)+' | tail -n 1) || return 1
  1689. if [[ -z "$data_start_ts" ]] || [[ "$data_start_ts" > "$first_ts" ]]; then
  1690. data_start_ts="$first_ts"
  1691. fi
  1692. if [[ -z "$data_stop_ts" ]] || [[ "$data_stop_ts" < "$last_ts" ]]; then
  1693. data_stop_ts="$last_ts"
  1694. fi
  1695. last_values=(${last_valued_line#* })
  1696. idx=0
  1697. data_sorted_values=$(
  1698. for last_value in "${last_values[@]}"; do
  1699. [[ "${values[$idx]}" != "data/"* ]] && {
  1700. idx=$((idx + 1))
  1701. continue
  1702. }
  1703. if [ "$last_value" == "-" ]; then
  1704. last_value=0
  1705. fi
  1706. printf "%s %.0f\n" "${values[$idx]}" "$last_value"
  1707. idx=$((idx + 1))
  1708. done | sort -k 2 -n | cut -f 1 -d " "
  1709. )
  1710. data_sorted_values=(${data_sorted_values})
  1711. color_step=$(( 255 / ${#data_sorted_values[@]} ))
  1712. toggle=10
  1713. for label in "${data_sorted_values[@]}"; do
  1714. if [ "$toggle" -eq 10 ]; then
  1715. toggle=240
  1716. else
  1717. toggle=10
  1718. fi
  1719. colors[$label]=$(printf "#%02x%02x%02x" "$toggle" $(( 255 - (color_step * idx) )) $(( color_step * idx)) )
  1720. idx=$((idx + 1))
  1721. done
  1722. echo "plot \\"
  1723. first=1
  1724. line_def=""
  1725. ordered_values=(
  1726. host/other
  1727. docker/{build_cache,local_volumes,logs,containers,images}
  1728. "${data_sorted_values[@]}"
  1729. )
  1730. idx=0
  1731. for subvalue in "${ordered_values[@]}" ; do
  1732. colnb="${values_colnb[$subvalue]}"
  1733. sum_str="(valid(${colnb}) ? column(${colnb}) : 0)"
  1734. line_def=""
  1735. if [ -n "$first" ]; then
  1736. current_sum+="${sum_str}"
  1737. first=
  1738. line_def="u 1:(0):(1/(1/((${current_sum})/(1024*1024)))) w filledcurves title '${subvalue//_/\\_}'"
  1739. else
  1740. echo -n ", \\"$'\n'
  1741. current_sum+=" + $sum_str"
  1742. line_def="u 1:(1/(1/((${previous_sum})/(1024*1024)))):(1/(1/((${current_sum})/(1024*1024)))) w filledcurves title '${subvalue//_/\\_}'"
  1743. fi
  1744. previous_sum="$current_sum"
  1745. if [[ -n "${colors[$subvalue]}" ]]; then
  1746. line_def+=" lc rgb '${colors[$subvalue]}'"
  1747. fi
  1748. echo -n " '$rrd_tmpdir/data.txt' ${line_def}"
  1749. done
  1750. ## Add line with "available" value from host/available
  1751. echo -n ", \\"$'\n'
  1752. echo -n " '< rrdtool fetch \"$rrd_vps_path/$resource/host/available/size.rrd\""
  1753. echo -n " AVERAGE ${opts_rrdfetch[*]} | \\"$'\n'
  1754. echo -n " tail -n +2 | \\"$'\n'
  1755. echo -n " egrep -v \"^$\" | sed -r \"s/ -?nan/ -/g;s/^([0-9]+): /\\1 /g\"'"
  1756. echo -n " u 1:(\$2/(1024*1024)) w lines title 'available' lw 2 lc rgb '#FF0000'"
  1757. ## Add dashed line with 75% of "available" value from host/available
  1758. echo -n ", \\"$'\n'
  1759. echo -n " '< rrdtool fetch \"$rrd_vps_path/$resource/host/available/size.rrd\""
  1760. echo -n " AVERAGE ${opts_rrdfetch[*]} | \\"$'\n'
  1761. echo -n " tail -n +2 | \\"$'\n'
  1762. echo -n " egrep -v \"^$\" | sed -r \"s/ -?nan/ -/g;s/^([0-9]+): /\\1 /g\"'"
  1763. echo -n " u 1:((3 * \$2)/(4*1024*1024)) w lines title '75% available' lw 1 dashtype 2 lc rgb '#FF0000'"
  1764. ## Add dashed line with 95% of "available" value from host/available
  1765. echo -n ", \\"$'\n'
  1766. echo -n " '< rrdtool fetch \"$rrd_vps_path/$resource/host/available/size.rrd\""
  1767. echo -n " AVERAGE ${opts_rrdfetch[*]} | \\"$'\n'
  1768. echo -n " tail -n +2 | \\"$'\n'
  1769. echo -n " egrep -v \"^$\" | sed -r \"s/ -?nan/ -/g;s/^([0-9]+): /\\1 /g\"'"
  1770. echo -n " u 1:((9 * \$2)/(10*1024*1024)) w lines title '90% available' lw 2 dashtype 2 lc rgb '#FF0000'"
  1771. echo
  1772. }
  1773. cmdline.spec.gnu vps-subscribe
  1774. cmdline.spec::cmd:vps-subscribe:run() {
  1775. :
  1776. }
  1777. cmdline.spec.gnu add
  1778. cmdline.spec:vps-subscribe:cmd:add:run() {
  1779. : :posarg: CHANNEL 'Channel which will be sent to given topic'
  1780. : :posarg: TOPIC 'Ntfy topic to recieve messages of given channel
  1781. (format: "[MYSERVER:]MYTOPICS"
  1782. Examples: "ntfy.0k.io:main,storage,alerts",
  1783. "main{1,3,7}"
  1784. )'
  1785. : :posarg: [VPS...] 'Target host(s) to get stats'
  1786. printf "%s\0" "$CHANNEL" "$TOPIC" |
  1787. vps_mux subscribe:add "${VPS[@]}"
  1788. }
  1789. cmdline.spec.gnu rm
  1790. cmdline.spec:vps-subscribe:cmd:rm:run() {
  1791. : :posarg: CHANNEL 'Channel which will be sent to given topic'
  1792. : :posarg: TOPIC 'Ntfy topic to recieve messages of given channel
  1793. (format: "[MYSERVER:]MYTOPICS"
  1794. Examples: "ntfy.0k.io:main,storage,alerts",
  1795. "main{1,3,7}"
  1796. )'
  1797. : :posarg: [VPS...] 'Target host(s) to get stats'
  1798. printf "%s\0" "$CHANNEL" "$TOPIC" |
  1799. vps_mux subscribe:rm "${VPS[@]}"
  1800. }
  1801. cmdline::parse "$@"