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.

910 lines
26 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. }
  26. resolve() {
  27. local ent hostname="$1"
  28. debug "Resolving $1 ..."
  29. if ent=$(getent ahosts "$hostname"); then
  30. ent=$(echo "$ent" | egrep ^"[0-9]+.[0-9]+.[0-9]+.[0-9]+\s+" | \
  31. head -n 1 | awk '{ print $1 }')
  32. debug " .. resolved $1 to $ent."
  33. echo "$ent"
  34. else
  35. debug " .. couldn't resolve $1."
  36. return 1
  37. fi
  38. }
  39. set_errlvl() { return "${1:-1}"; }
  40. export master_pid=$$
  41. ssh:open() {
  42. local hostname ssh_cmd ssh_options
  43. ssh_cmd=(ssh)
  44. ssh_options=()
  45. while [ "$#" != 0 ]; do
  46. case "$1" in
  47. "--stdin-password")
  48. ssh_cmd=(sshpass "${ssh_cmd[@]}")
  49. ;;
  50. -o)
  51. ssh_options+=("$1" "$2")
  52. shift
  53. ;;
  54. *)
  55. [ -z "$hostname" ] && hostname="$1" || {
  56. err "Surnumerous positional argument '$1'. Expecting only hostname."
  57. return 1
  58. }
  59. ;;
  60. esac
  61. shift
  62. done
  63. full_cmd=(
  64. "${ssh_cmd[@]}"
  65. -o ControlPath=/tmp/ssh-control-master-${master_pid}-$hostname \
  66. -o ControlMaster=auto -o ControlPersist=900 \
  67. -o ConnectTimeout=5 -o StrictHostKeyChecking=no \
  68. "${ssh_options[@]}" \
  69. "$hostname" "$@" -- true)
  70. "${full_cmd[@]}" </dev/null >/dev/null 2>&1 || {
  71. err "Failed: ${full_cmd[*]}"
  72. return 1
  73. }
  74. trap_add EXIT,INT 'ssh:quit "$hostname"'
  75. }
  76. ssh:open-try() {
  77. local opts hostnames
  78. opts=()
  79. hostnames=()
  80. while [ "$#" != 0 ]; do
  81. case "$1" in
  82. -o)
  83. opts+=("$1" "$2")
  84. shift
  85. ;;
  86. *)
  87. hostnames+=("$1")
  88. ;;
  89. esac
  90. shift
  91. done
  92. password=''
  93. for host in "${hostnames[@]}"; do
  94. debug "Trying $host with publickey."
  95. ssh:open -o PreferredAuthentications=publickey \
  96. "${opts[@]}" \
  97. "$host" >/dev/null 2>&1 && {
  98. echo "$host"$'\n'"$password"$'\n'
  99. return 0
  100. }
  101. debug " .. failed connecting to $host with publickey."
  102. done
  103. local times=0 password
  104. while [ "$((++times))" -le 3 ]; do
  105. read -sp "$HOST's password: " password
  106. errlvl="$?"
  107. echo >&2
  108. if [ "$errlvl" -gt 0 ]; then
  109. exit 1
  110. fi
  111. for host in "${hostnames[@]}"; do
  112. debug "Trying $host with password ($times/3)"
  113. echo "$password" | ssh:open -o PreferredAuthentications=password \
  114. --stdin-password \
  115. "${opts[@]}" \
  116. "$host" >/dev/null 2>&1 && {
  117. echo "$host"$'\n'"$password"$'\n'
  118. return 0
  119. }
  120. debug " .. failed connecting to $host with password."
  121. done
  122. err "login failed. Try again... ($((times+1))/3)"
  123. done
  124. return 1
  125. }
  126. ssh:run() {
  127. local hostname="$1" ssh_options cmd
  128. shift
  129. ssh_options=()
  130. cmd=()
  131. while [ "$#" != 0 ]; do
  132. case "$1" in
  133. "--")
  134. shift
  135. cmd+=("$@")
  136. break
  137. ;;
  138. *)
  139. ssh_options+=("$1")
  140. ;;
  141. esac
  142. shift
  143. done
  144. #echo "$DARKCYAN$hostname$NORMAL $WHITE\$$NORMAL" "$@"
  145. debug "Running cmd: ${cmd[@]}"
  146. for arg in "${cmd[@]}"; do
  147. debug "$arg"
  148. done
  149. {
  150. {
  151. ssh -o ControlPath=/tmp/ssh-control-master-${master_pid}-$hostname \
  152. -o ControlMaster=auto -o ControlPersist=900 \
  153. -o "StrictHostKeyChecking=no" \
  154. "${ssh_options[@]}" "$hostname" -- "${cmd[@]}"
  155. } 3>&1 1>&2 2>&3 ## | sed -r "s/^/$DARKCYAN$hostname$NORMAL $DARKRED\!$NORMAL /g"
  156. set_errlvl "${PIPESTATUS[0]}"
  157. } 3>&1 1>&2 2>&3
  158. }
  159. ssh:quit() {
  160. local hostname="$1"
  161. shift
  162. ssh -o ControlPath=/tmp/ssh-control-master-${master_pid} \
  163. -o ControlMaster=auto -o ControlPersist=900 -O exit \
  164. "$hostname" 2>/dev/null
  165. }
  166. is_ovh_domain_name() {
  167. local domain="$1"
  168. [[ "$domain" == *.ovh.net ]] && return 0
  169. [[ "$domain" == "ns"*".ip-"*".eu" ]] && return 0
  170. return 1
  171. }
  172. is_ovh_hostname() {
  173. local domain="$1"
  174. [[ "$domain" =~ ^vps-[0-9a-f]*$ ]] && return 0
  175. [[ "$domain" =~ ^vps[0-9]*$ ]] && return 0
  176. return 1
  177. }
  178. vps_connection_check() {
  179. local vps="$1"
  180. ip=$(resolve "$vps") ||
  181. { echo "${DARKRED}no-resolve${NORMAL}"; return 1; }
  182. is-port-open "$ip" "22" </dev/null ||
  183. { echo "${DARKRED}no-port-22-open${NORMAL}"; return 1; }
  184. ssh:open -o ConnectTimeout=10 -o PreferredAuthentications=publickey \
  185. "root@$vps" >/dev/null 2>&1 ||
  186. { echo "${DARKRED}no-ssh-root-access${NORMAL}"; return 1; }
  187. }
  188. vps_check() {
  189. local vps="$1"
  190. vps_connection_check "$vps" </dev/null || return 1
  191. if size=$(
  192. echo "df /srv -h | tail -n +2 | sed -r 's/ +/ /g' | cut -f 5 -d ' ' | cut -f 1 -d %" |
  193. ssh:run "root@$vps" -- bash); then
  194. if [ "$size" -gt "90" ]; then
  195. echo "${DARKRED}above-90%-disk-usage${NORMAL}"
  196. elif [ "$size" -gt "75" ]; then
  197. echo "${DARKYELLOW}above-75%-disk-usage${NORMAL}"
  198. fi
  199. else
  200. echo "${DARKRED}no-size${NORMAL}"
  201. fi </dev/null
  202. compose_content=$(ssh:run "root@$vps" -- cat /opt/apps/myc-deploy/compose.yml </dev/null) ||
  203. { echo "${DARKRED}no-compose${NORMAL}"; return 1; }
  204. echo "$compose_content" | grep backup >/dev/null 2>&1 ||
  205. { echo "${DARKRED}no-backup${NORMAL}"; return 1; }
  206. }
  207. vps:rsync() {
  208. local vps="$1" id="$2" src="$3" dst="$4"
  209. if [[ "$src" != *":"* ]]; then
  210. err "Third argument '$src' should be a remote (include the server name as prefix)."
  211. return 1
  212. fi
  213. server=${src%%:*}
  214. src=${src#*:}
  215. cat <<EOF | ssh:run "root@$vps" -- bash
  216. rsync -e "ssh ${ssh_options[*]}" \
  217. -azvArH --delete --delete-excluded \
  218. --partial --partial-dir .rsync-partial \
  219. --numeric-ids \
  220. "$server":/var/mirror/"${id}${src}" "${dst}"
  221. EOF
  222. }
  223. mailcow:vps_backup_recover() {
  224. local admin="$1" server="$2" id="$3" vps="$4"
  225. ## Request recovery key
  226. ssh_options=()
  227. if [[ "$server" == *":"* ]]; then
  228. ssh_options+=(-p "${server#*:}" -o StrictHostKeyChecking=no)
  229. server=${server%%:*}
  230. fi
  231. if ! private_key=$(ssh "${ssh_options[@]}" "$admin"@"$server" request-recovery-key "$id"); then
  232. err "Couldn't request a recovery key for '$id' with account '$admin'."
  233. return 1
  234. fi
  235. echo "type -p rsync >/dev/null 2>&1 || apt-get install -y rsync </dev/null" |
  236. ssh:run "root@$vps" -- bash
  237. if ! VPS_TMP_DIR=$(echo "mktemp -d" | ssh:run "root@$vps" -- bash); then
  238. err "Couldn't create a temporary directory on vps"
  239. return 1
  240. fi
  241. cat <<EOF | ssh:run "root@$vps" -- bash || return 1
  242. touch "$VPS_TMP_DIR/recover_key" &&
  243. chmod go-rwx "$VPS_TMP_DIR/recover_key" &&
  244. printf "%s\n" "$private_key" >> "$VPS_TMP_DIR/recover_key"
  245. EOF
  246. ssh_options+=(-i "$VPS_TMP_DIR/recover_key" -l rsync)
  247. if ! compose_yml_files=$(cat <<EOF | ssh:run "root@$vps" -- bash
  248. urn=com.docker.compose.project
  249. docker ps -f "label=\$urn=mailcowdockerized" \
  250. --format='{{.Label "\$urn.working_dir"}}/{{.Label "\$urn.config_files"}}' |
  251. uniq
  252. EOF
  253. ); then
  254. err "Couldn't get list of running projects"
  255. return 1
  256. fi
  257. stopped_containers=
  258. if [ -n "$compose_yml_files" ]; then
  259. echo "Found running mailcowdockerized containers" >&2
  260. if [[ "$compose_yml_files" == *$'\n'* ]]; then
  261. err "Running containers are confusing, did not find only one mailcowdockerized project."
  262. return 1
  263. fi
  264. if ! echo "[ -e \"$compose_yml_files\" ]" | ssh:run "root@$vps" -- bash ; then
  265. err "Running containers are confusing, they don't point to an existing docker-compose.yml."
  266. return 1
  267. fi
  268. echo "Containers where launched from '$compose_yml_files'" >&2
  269. COMPOSE_FILE="$compose_yml_files"
  270. ENV_FILE="${COMPOSE_FILE%/*}/.env"
  271. if ! echo "[ -e \"${ENV_FILE}\" ]" | ssh:run "root@$vps" -- bash ; then
  272. err "Running containers are confusing, docker-compose.yml has no '.env' next to it."
  273. return 1
  274. fi
  275. echo "${WHITE}Bringing mailcowdockerized down${NORMAL}"
  276. echo "docker-compose -f \"${COMPOSE_FILE}\" --env-file \"${ENV_FILE}\" down" |
  277. ssh:run "root@$vps" -- bash
  278. stopped_containers=1
  279. fi
  280. for vol in postfix rspamd redis crypt vmail{,-attachments}; do
  281. volume_name="mailcowdockerized_${vol}-vol-1"
  282. volume_dir="/var/lib/docker/volumes/${volume_name}/_data"
  283. if ! ssh:run "root@$server" -- "[ -d '$BACKUP_PATH/${id}${volume_dir}' ]"; then
  284. continue
  285. fi
  286. ## Create volumes if not existent
  287. if ! ssh:run "root@$vps" -- "
  288. [ -d '${volume_dir}' ] ||
  289. docker run --rm -v ${volume_name}:/tmp/dummy docker.0k.io/alpine:3.9
  290. [ -d '${volume_dir}' ]
  291. "; then
  292. err "Couldn't find nor create '${volume_dir}'."
  293. return 1
  294. fi
  295. echo "${WHITE}Downloading of $volume_name${NORMAL}"
  296. vps:rsync "$vps" "$id" "$server":"${volume_dir}/" "${volume_dir}" || return 1
  297. done
  298. ## Mailcow git base
  299. for mailcow_dir in /opt/{apps/,}mailcow-dockerized; do
  300. if ! ssh:run "root@$server" -- "[ -d '$BACKUP_PATH/${id}${mailcow_dir}' ]"; then
  301. continue
  302. else
  303. ## this possibly change last value
  304. COMPOSE_FILE="$mailcow_dir/docker-compose.yml"
  305. ENV_FILE="$mailcow_dir/.env"
  306. echo "${WHITE}Download of $mailcow_dir${NORMAL}"
  307. vps:rsync "$vps" "$id" "$server":"${mailcow_dir}"/ "${mailcow_dir}" || return 1
  308. break
  309. fi
  310. done
  311. ## Mysql database
  312. echo "${WHITE}Downloading last backup of mysql backups${NORMAL}"
  313. vps:rsync "$vps" "$id" "$server":"/var/backups/mysql/" "/var/backups/mysql" || return 1
  314. if ! env_content=$(echo "cat '$ENV_FILE'" | ssh:run "root@$vps" -- bash); then
  315. err "Can't access env file: '$ENV_FILE'."
  316. return 1
  317. fi
  318. root_password=$(printf "%s\n" "$env_content" | grep ^DBROOT= | cut -f 2 -d =)
  319. echo "${WHITE}Bringing mysql-mailcow up${NORMAL}"
  320. if ! container_id=$(cat <<EOF | ssh:run "root@$vps" -- bash
  321. echo "[client]
  322. password=$root_password" > "$VPS_TMP_DIR/my.cnf"
  323. docker-compose -f "${COMPOSE_FILE}" --env-file "${ENV_FILE}" \
  324. run -d \
  325. -v "$VPS_TMP_DIR/my.cnf:/root/.my.cnf:ro" \
  326. mysql-mailcow
  327. EOF
  328. ); then
  329. err "Failed to bring up mysql-mailcow"
  330. return 1
  331. fi
  332. START="$SECONDS"
  333. while true; do
  334. echo " trying to connect..." >&2
  335. cat <<EOF | ssh:run "root@$vps" -- bash && break
  336. echo "SELECT 1;" | docker exec -i "$container_id" mysql >/dev/null 2>&1
  337. EOF
  338. if (($SECONDS - $START > 10)); then
  339. err "Failed to connect to mysql-mailcow."
  340. echo "docker-compose -f \"${COMPOSE_FILE}\" --env-file \"${ENV_FILE}\" down" |
  341. ssh:run "root@$vps" -- bash
  342. return 1
  343. fi
  344. sleep 0.3
  345. done
  346. DBUSER=$(printf "%s\n" "$env_content" | grep ^DBUSER= | cut -f 2 -d =)
  347. DBPASS=$(printf "%s\n" "$env_content" | grep ^DBPASS= | cut -f 2 -d =)
  348. echo "${WHITE}Uploading mysql dump${NORMAL}"
  349. cat <<EOF | ssh:run "root@$vps" -- bash
  350. echo "
  351. DROP DATABASE IF EXISTS mailcow;
  352. CREATE DATABASE mailcow;
  353. GRANT ALL PRIVILEGES ON mailcow.* TO '$DBUSER'@'%' IDENTIFIED BY '$DBPASS';
  354. " | docker exec -i "$container_id" mysql
  355. zcat /var/backups/mysql/mailcow/*.gz | docker exec -i "$container_id" mysql mailcow
  356. EOF
  357. if [ "$?" != 0 ]; then
  358. err "Failed to load mysql dump."
  359. echo "docker-compose -f \"${COMPOSE_FILE}\" --env-file \"${ENV_FILE}\" down" |
  360. ssh:run "root@$vps" -- bash
  361. return 1
  362. fi
  363. echo "${WHITE}Bringing mysql-mailcow down${NORMAL}"
  364. echo "docker-compose -f \"${COMPOSE_FILE}\" --env-file \"${ENV_FILE}\" down" |
  365. ssh:run "root@$vps" -- bash
  366. ssh:run "root@$vps" -- "rm -rf '$VPS_TMP_DIR'"
  367. if [ -n "$stopped_containers" ]; then
  368. echo "${WHITE}Starting mailcow${NORMAL}" >&2
  369. echo "docker-compose -f \"${COMPOSE_FILE}\" --env-file \"${ENV_FILE}\" up -d" |
  370. ssh:run "root@$vps" -- bash
  371. fi
  372. info "Mailcow was ${GREEN}successfully${NORMAL} restored."
  373. }
  374. vps_backup_recover() {
  375. local vps="$1" admin server id rtype force
  376. vps_connection_check "$vps" </dev/null || return 1
  377. read-0 admin server id rtype force
  378. if ! type=$(ssh:run "root@$vps" -- vps get-type); then
  379. err "Could not get type."
  380. return 1
  381. fi
  382. if [ "$rtype" != "$type" ]; then
  383. if [ -n "$force" ]; then
  384. warn "Backup found is of ${rtype:-unknown} type, while vps is of $type type."
  385. else
  386. err "Backup found is of ${rtype:-unknown} type, while vps is of $type type. (use \`\`-f\`\` to force)"
  387. return 1
  388. fi
  389. fi
  390. case "$rtype" in
  391. mailcow)
  392. mailcow:vps_backup_recover "$admin" "$server" "$id" "$vps"
  393. ;;
  394. *)
  395. err "Recover on $type type VPS is not yet implemented."
  396. return 1
  397. ;;
  398. esac
  399. }
  400. vps_install_backup() {
  401. local vps="$1" admin server
  402. vps_connection_check "$vps" </dev/null || return 1
  403. read-0 admin server
  404. if ! type=$(ssh:run "root@$vps" -- vps get-type); then
  405. err "Could not get type."
  406. return 1
  407. fi
  408. if ! out=$(ssh:run "root@$vps" -- vps install backup "$server" 2>&1); then
  409. err "Command 'vps install backup $server' failed."
  410. return 1
  411. fi
  412. already_present=
  413. if e "$out" | grep "^II Entry for service .* is already present" >/dev/null 2>&1; then
  414. already_present=1
  415. info "Backup entry is already present in 'compose.yml' of '$vps'"
  416. fi
  417. out="${out%$'\n'}"
  418. out="${out#*$'\n'}"
  419. key="${out%\'*}"
  420. key="${key##*\'}"
  421. if ! [[ "$key" =~ ^"ssh-rsa "[a-zA-Z0-9/+]+" "[a-zA-Z0-9._-]+"@"[a-zA-Z0-9._-]+$ ]]; then
  422. err "Unexpected output from 'vps install backup $server'. Can't find key."
  423. echo "$out" | prefix " ${GRAY}|$NORMAL " >&2
  424. echo " Extracted key:" >&2
  425. echo "$key" | prefix " ${GRAY}|$NORMAL " >&2
  426. return 1
  427. fi
  428. if [ "$type" == "compose" ] && [ -z "$already_present" ]; then
  429. ssh:run "root@$vps" -- compose --debug up || {
  430. err "Command 'compose --debug up' failed."
  431. return 1
  432. }
  433. fi
  434. dest="$server"
  435. dest="${dest%/*}"
  436. ssh_options=()
  437. if [[ "$dest" == *":"* ]]; then
  438. port="${dest##*:}"
  439. dest="${dest%%:*}"
  440. ssh_options=(-p "$port")
  441. else
  442. port=""
  443. dest="${dest%%:*}"
  444. fi
  445. cmd=(ssh "${ssh_options[@]}" "$admin"@"$dest" ssh-key add "$key")
  446. echo "${WHITE}Launching:${NORMAL} ${cmd[@]}"
  447. "${cmd[@]}" || {
  448. err "Failed add key to backup server '$dest'."
  449. return 1
  450. }
  451. echo "${WHITE}Launching backup${NORMAL} from '$vps'"
  452. ssh:run "root@$vps" -- vps backup || {
  453. err "First backup failed to run."
  454. return 1
  455. }
  456. echo "Backup is ${GREEN}up and running${NORMAL}."
  457. }
  458. vps_udpate() {
  459. local vps="$1"
  460. vps_connection_check "$vps" || return 1
  461. ssh:run "root@$vps" -- myc-update </dev/null
  462. }
  463. vps_bash() {
  464. local vps="$1"
  465. vps_connection_check "$vps" </dev/null || return 1
  466. ssh:run "root@$vps" -- bash
  467. }
  468. vps_mux() {
  469. local fn="$1" vps_done VPS max_size vps
  470. shift
  471. VPS=($(printf "%s\n" "$@" | sort))
  472. max_size=0
  473. declare -A vps_done;
  474. new_vps=()
  475. for name in "${VPS[@]}"; do
  476. [ -n "${vps_done[$name]}" ] && {
  477. warn "duplicate vps '$name' provided. Ignoring."
  478. continue
  479. }
  480. vps_done["$name"]=1
  481. new_vps+=("$name")
  482. size_name="${#name}"
  483. [ "$max_size" -lt "${size_name}" ] &&
  484. max_size="$size_name"
  485. done
  486. settmpdir "_0KM_TMP_DIR"
  487. cat > "$_0KM_TMP_DIR/code"
  488. for vps in "${new_vps[@]}"; do
  489. label=$(printf "%-${max_size}s" "$vps")
  490. (
  491. {
  492. {
  493. "$fn" "$vps" < "$_0KM_TMP_DIR/code"
  494. } 3>&1 1>&2 2>&3 | sed -r "s/^/$DARKCYAN$label$NORMAL $DARKRED\!$NORMAL /g"
  495. set_errlvl "${PIPESTATUS[0]}"
  496. } 3>&1 1>&2 2>&3 | sed -r "s/^/$DARKCYAN$label$NORMAL $DARKGRAY\|$NORMAL /g"
  497. set_errlvl "${PIPESTATUS[0]}"
  498. ) &
  499. done
  500. wait
  501. }
  502. [ "$SOURCED" ] && return 0
  503. ##
  504. ## Command line processing
  505. ##
  506. cmdline.spec.gnu
  507. cmdline.spec.reporting
  508. cmdline.spec.gnu vps-setup
  509. cmdline.spec::cmd:vps-setup:run() {
  510. : :posarg: HOST 'Target host to check/fix ssh-access'
  511. depends sshpass shyaml
  512. KEY_PATH="ssh-access.public-keys"
  513. local keys=$(config get-value -y "ssh-access.public-keys") || true
  514. if [ -z "$keys" ]; then
  515. err "No ssh publickeys configured in config file."
  516. echo " Looking for keys in ${WHITE}${KEY_PATH}${NORMAL}" \
  517. "in config file." >&2
  518. config:exists --message 2>&1 | prefix " "
  519. if [ "${PIPESTATUS[0]}" == "0" ]; then
  520. echo " Config file found in $(config:filename)"
  521. fi
  522. return 1
  523. fi
  524. local tkey=$(e "$keys" | shyaml get-type)
  525. if [ "$tkey" != "sequence" ]; then
  526. err "Value type of ${WHITE}${KEY_PATH}${NORMAL} unexpected (is $tkey, expecting sequence)."
  527. echo " Check content of $(config:filename), and make sure to use a sequence." >&2
  528. return 1
  529. fi
  530. local IP NAME keys host_pass_connected
  531. if ! IP=$(resolve "$HOST"); then
  532. err "'$HOST' name unresolvable."
  533. exit 1
  534. fi
  535. NAME="$HOST"
  536. if [ "$IP" != "$HOST" ]; then
  537. NAME="$HOST ($IP)"
  538. fi
  539. if ! is-port-open "$IP" "22"; then
  540. err "$NAME unreachable or port 22 closed."
  541. exit 1
  542. fi
  543. debug "Host $IP's port 22 is open."
  544. if ! host_pass_connected=$(ssh:open-try \
  545. {root,debian}@"$HOST"); then
  546. err "Could not connect to {root,debian}@$HOST with publickey nor password."
  547. exit 1
  548. fi
  549. read-0a host password <<<"$host_pass_connected"
  550. sudo_if_necessary=
  551. if [ "$password" -o "${host%%@*}" != "root" ]; then
  552. if ! ssh:run "$host" -- sudo -n true >/dev/null 2>&1; then
  553. err "Couldn't do a password-less sudo from $host."
  554. echo " This is not yet supported."
  555. exit 1
  556. else
  557. sudo_if_necessary=sudo
  558. fi
  559. fi
  560. Section Checking access
  561. while read-0 key; do
  562. prefix="${key%% *}"
  563. if [ "$prefix" != "ssh-rsa" ]; then
  564. err "Unsupported key:"$'\n'"$key"
  565. return 1
  566. fi
  567. label="${key##* }"
  568. Elt "considering adding key ${DARKYELLOW}$label${NORMAL}"
  569. dest="/root/.ssh/authorized_keys"
  570. if ssh:run "$host" -- $sudo_if_necessary grep "\"$key\"" "$dest" >/dev/null 2>&1 </dev/null; then
  571. print_info "already present"
  572. print_status noop
  573. Feed
  574. else
  575. if echo "$key" | ssh:run "$host" -- $sudo_if_necessary tee -a "$dest" >/dev/null; then
  576. print_info added
  577. else
  578. echo
  579. Feedback failure
  580. return 1
  581. fi
  582. Feedback success
  583. fi
  584. done < <(e "$keys" | shyaml get-values-0)
  585. Section Checking ovh hostname file
  586. Elt "Checking /etc/ovh-hostname"
  587. if ! ssh:run "$host" -- $sudo_if_necessary [ -e "/etc/ovh-hostname" ]; then
  588. print_info "creating"
  589. ssh:run "$host" -- $sudo_if_necessary cp /etc/hostname /etc/ovh-hostname
  590. ovhname=$(ssh:run "$host" -- $sudo_if_necessary cat /etc/ovh-hostname)
  591. Elt "Checking /etc/ovh-hostname: $ovhname"
  592. Feedback || return 1
  593. else
  594. ovhname=$(ssh:run "$host" -- $sudo_if_necessary cat /etc/ovh-hostname)
  595. Elt "Checking /etc/ovh-hostname: $ovhname"
  596. print_info "already present"
  597. print_status noop
  598. Feed
  599. fi
  600. if ! is_ovh_domain_name "$HOST" && ! str_is_ipv4 "$HOST"; then
  601. Section Checking hostname
  602. Elt "Checking /etc/hostname..."
  603. if [ "$old" != "$HOST" ]; then
  604. old="$(ssh:run "$host" -- $sudo_if_necessary cat /etc/hostname)"
  605. Elt "Hostname is '$old'"
  606. if is_ovh_hostname "$old"; then
  607. Elt "Hostname '$old' --> '$HOST'"
  608. print_info "creating"
  609. echo "$HOST" | ssh:run "$host" -- $sudo_if_necessary tee /etc/hostname >/dev/null &&
  610. ssh:run "$host" -- $sudo_if_necessary hostname "$HOST"
  611. Feedback || return 1
  612. else
  613. print_info "not changing"
  614. print_status noop
  615. Feed
  616. fi
  617. else
  618. print_info "already set"
  619. print_status noop
  620. Feed
  621. fi
  622. else
  623. info "Not changing domain as '$HOST' doesn't seem to be final domain."
  624. fi
  625. }
  626. cmdline.spec.gnu vps-check
  627. cmdline.spec::cmd:vps-check:run() {
  628. : :posarg: [VPS...] 'Target host(s) to check'
  629. echo "" |
  630. vps_mux vps_check "${VPS[@]}"
  631. }
  632. cmdline.spec.gnu vps-install
  633. cmdline.spec::cmd:vps-install:run() {
  634. :
  635. }
  636. cmdline.spec.gnu backup
  637. cmdline.spec:vps-install:cmd:backup:run() {
  638. : :posarg: BACKUP_TARGET 'Backup target.
  639. (ie: myadmin@backup.domain.org:10023/256)'
  640. : :posarg: [VPS...] 'Target host(s) to check'
  641. if [ "${#VPS[@]}" == 0 ]; then
  642. warn "VPS list provided in command line is empty. Nothing will be done."
  643. return 0
  644. fi
  645. if ! [[ "$BACKUP_TARGET" == *"@"* ]]; then
  646. err "Missing admin account identifier in backup target."
  647. echo " Have you forgottent to specify an account, ie 'myadmin@<MYBACKUP_SERVER>' ?)"
  648. return 1
  649. fi
  650. admin=${BACKUP_TARGET%%@*}
  651. server=${BACKUP_TARGET#*@}
  652. p0 "$admin" "$server" |
  653. vps_mux vps_install_backup "${VPS[@]}"
  654. }
  655. cmdline.spec.gnu vps-backup
  656. cmdline.spec::cmd:vps-backup:run() {
  657. :
  658. }
  659. cmdline.spec.gnu ls
  660. cmdline.spec:vps-backup:cmd:ls:run() {
  661. : :posarg: BACKUP_ID 'Backup id.
  662. (ie: myadmin@backup.domain.org:10023)'
  663. if ! [[ "$BACKUP_ID" == *"@"* ]]; then
  664. err "Missing admin account identifier in backup id."
  665. echo " Have you forgottent to specify an admin account ?" \
  666. "ie 'myadmin@<MYBACKUP_SERVER>#<BACKUP_IDENT>' ?)"
  667. return 1
  668. fi
  669. id=${BACKUP_ID##*#}
  670. BACKUP_TARGET=${BACKUP_ID%#*}
  671. admin=${BACKUP_TARGET%%@*}
  672. server=${BACKUP_TARGET#*@}
  673. ## XXXvlab: in this first implementation we expect to have access
  674. ## to the server main ssh port 22, so we won't use the provided port.
  675. ssh_options=()
  676. if [[ "$server" == *":"* ]]; then
  677. ssh_options+=(-p "${server#*:}")
  678. server=${server%%:*}
  679. fi
  680. ssh "${ssh_options[@]}" "$admin"@"$server" ssh-key ls
  681. }
  682. cmdline.spec.gnu recover
  683. cmdline.spec:vps-backup:cmd:recover:run() {
  684. : :posarg: BACKUP_ID 'Backup id.
  685. (ie: myadmin@backup.domain.org:10023#mx.myvps.org)'
  686. : :posarg: [VPS...] 'Target host(s) to check'
  687. : :optval: --date,-D '"last", or label of version to recover. (Default: "last").'
  688. : :optfla: --force,-f 'Will allow you to bypass some checks.'
  689. if [ "${#VPS[@]}" == 0 ]; then
  690. warn "VPS list provided in command line is empty. Nothing will be done."
  691. return 0
  692. fi
  693. if ! [[ "$BACKUP_ID" == *"@"* ]]; then
  694. err "Missing admin account identifier in backup id."
  695. echo " Have you forgottent to specify an admin account ?" \
  696. "ie 'myadmin@<MYBACKUP_SERVER>#<BACKUP_IDENT>' ?)"
  697. return 1
  698. fi
  699. if ! [[ "$BACKUP_ID" == *"@"*"#"* ]]; then
  700. err "Missing backup label identifier in backup id."
  701. echo " Have you forgottent to specify a backup label identifier ?" \
  702. "ie 'myadmin@<MYBACKUP_SERVER>#<BACKUP_IDENT>' ?)"
  703. return 1
  704. fi
  705. id=${BACKUP_ID##*#}
  706. BACKUP_TARGET=${BACKUP_ID%#*}
  707. admin=${BACKUP_TARGET%%@*}
  708. server=${BACKUP_TARGET#*@}
  709. ## XXXvlab: in this first implementation we expect to have access
  710. ## to the server main ssh port 22, so we won't use the provided port.
  711. # ssh_options=()
  712. if [[ "$server" == *":"* ]]; then
  713. # ssh_options+=(-p "${server#*:}")
  714. ssh_server=${server%%:*}
  715. fi
  716. BACKUP_PATH="/srv/datastore/data/rsync-backup-target/var/mirror"
  717. if ! rtype=$(echo "
  718. if [ -d '$BACKUP_PATH/$id/var/lib/docker/volumes/mailcowdockerized_crypt-vol-1' ]; then
  719. echo mailcow
  720. elif [ -d '$BACKUP_PATH/$id/compose.yml' ]; then
  721. echo compose
  722. fi
  723. true
  724. " | ssh:run "root@${ssh_server}" -- bash ); then
  725. err "Could not get backup type."
  726. return 1
  727. fi
  728. if [ -z "$rtype" ]; then
  729. err "Unknown type of backup on '${server}#${id}'."
  730. return 1
  731. fi
  732. p0 "$admin" "$server" "$id" "$rtype" "$opt_force" |
  733. vps_mux vps_backup_recover "${VPS[@]}"
  734. }
  735. cmdline.spec.gnu vps-update
  736. cmdline.spec::cmd:vps-update:run() {
  737. : :posarg: [VPS...] 'Target host to check'
  738. echo "" |
  739. vps_mux vps_update "${VPS[@]}"
  740. }
  741. cmdline.spec.gnu vps-mux
  742. cmdline.spec::cmd:vps-mux:run() {
  743. : :posarg: [VPS...] 'Target host(s) to check'
  744. cat | vps_mux vps_bash "${VPS[@]}"
  745. }
  746. cmdline.spec.gnu vps-space
  747. cmdline.spec::cmd:vps-space:run() {
  748. : :posarg: [VPS...] 'Target host(s) to check'
  749. echo "df /srv -h | tail -n +2 | sed -r 's/ +/ /g' | cut -f 2-5 -d ' '" |
  750. vps_mux vps_bash "${VPS[@]}"
  751. }
  752. cmdline::parse "$@"