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.

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