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.

574 lines
16 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. "$hostname" "${ssh_options[@]}" -- "${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=2 -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_install_backup() {
  208. local vps="$1" admin server
  209. vps_connection_check "$vps" </dev/null || return 1
  210. read-0 admin server
  211. if ! type=$(ssh:run "root@$vps" -- vps get-type); then
  212. err "Could not get type."
  213. return 1
  214. fi
  215. if ! out=$(ssh:run "root@$vps" -- vps install backup "$server" 2>&1); then
  216. err "Command 'vps install backup $server' failed."
  217. return 1
  218. fi
  219. already_present=
  220. if e "$out" | grep "^II Entry for service .* is already present" >/dev/null 2>&1; then
  221. already_present=1
  222. info "Backup entry is already present in 'compose.yml' of '$vps'"
  223. fi
  224. out="${out%$'\n'}"
  225. out="${out#*$'\n'}"
  226. key="${out%\'*}"
  227. key="${key##*\'}"
  228. if ! [[ "$key" =~ ^"ssh-rsa "[a-zA-Z0-9/+]+" "[a-zA-Z0-9._-]+"@"[a-zA-Z0-9._-]+$ ]]; then
  229. err "Unexpected output from 'vps install backup $server'. Can't find key."
  230. echo "$out" | prefix " ${GRAY}|$NORMAL " >&2
  231. echo " Extracted key:" >&2
  232. echo "$key" | prefix " ${GRAY}|$NORMAL " >&2
  233. return 1
  234. fi
  235. if [ "$type" == "compose" ] && [ -z "$already_present" ]; then
  236. ssh:run "root@$vps" -- compose --debug up || {
  237. err "Command 'compose --debug up' failed."
  238. return 1
  239. }
  240. fi
  241. dest="$server"
  242. dest="${dest%/*}"
  243. ssh_options=()
  244. if [[ "$dest" == *":"* ]]; then
  245. port="${dest##*:}"
  246. dest="${dest%%:*}"
  247. ssh_options=(-p "$port")
  248. else
  249. port=""
  250. dest="${dest%%:*}"
  251. fi
  252. cmd=(ssh "${ssh_options[@]}" "$admin"@"$dest" ssh-key add "$key")
  253. echo "${WHITE}Launching:${NORMAL} ${cmd[@]}"
  254. "${cmd[@]}" || {
  255. err "Failed add key to backup server '$dest'."
  256. return 1
  257. }
  258. echo "${WHITE}Launching backup${NORMAL} from '$vps'"
  259. ssh:run "root@$vps" -- vps backup || {
  260. err "First backup failed to run."
  261. return 1
  262. }
  263. echo "Backup is ${GREEN}up and running${NORMAL}."
  264. }
  265. vps_udpate() {
  266. local vps="$1"
  267. vps_connection_check "$vps" || return 1
  268. ssh:run "root@$vps" -- myc-update </dev/null
  269. }
  270. vps_bash() {
  271. local vps="$1"
  272. vps_connection_check "$vps" </dev/null || return 1
  273. ssh:run "root@$vps" -- bash
  274. }
  275. vps_mux() {
  276. local fn="$1" vps_done VPS max_size vps
  277. shift
  278. VPS=($(printf "%s\n" "$@" | sort))
  279. max_size=0
  280. declare -A vps_done;
  281. new_vps=()
  282. for name in "${VPS[@]}"; do
  283. [ -n "${vps_done[$name]}" ] && {
  284. warn "duplicate vps '$name' provided. Ignoring."
  285. continue
  286. }
  287. vps_done["$name"]=1
  288. new_vps+=("$name")
  289. size_name="${#name}"
  290. [ "$max_size" -lt "${size_name}" ] &&
  291. max_size="$size_name"
  292. done
  293. settmpdir "_0KM_TMP_DIR"
  294. cat > "$_0KM_TMP_DIR/code"
  295. for vps in "${new_vps[@]}"; do
  296. label=$(printf "%-${max_size}s" "$vps")
  297. (
  298. {
  299. {
  300. "$fn" "$vps" < "$_0KM_TMP_DIR/code"
  301. } 3>&1 1>&2 2>&3 | sed -r "s/^/$DARKCYAN$label$NORMAL $DARKRED\!$NORMAL /g"
  302. set_errlvl "${PIPESTATUS[0]}"
  303. } 3>&1 1>&2 2>&3 | sed -r "s/^/$DARKCYAN$label$NORMAL $DARKGRAY\|$NORMAL /g"
  304. set_errlvl "${PIPESTATUS[0]}"
  305. ) &
  306. done
  307. wait
  308. }
  309. [ "$SOURCED" ] && return 0
  310. ##
  311. ## Command line processing
  312. ##
  313. cmdline.spec.gnu
  314. cmdline.spec.reporting
  315. cmdline.spec.gnu vps-setup
  316. cmdline.spec::cmd:vps-setup:run() {
  317. : :posarg: HOST 'Target host to check/fix ssh-access'
  318. depends sshpass shyaml
  319. KEY_PATH="ssh-access.public-keys"
  320. local keys=$(config get-value -y "ssh-access.public-keys") || true
  321. if [ -z "$keys" ]; then
  322. err "No ssh publickeys configured in config file."
  323. echo " Looking for keys in ${WHITE}${KEY_PATH}${NORMAL}" \
  324. "in config file." >&2
  325. config:exists --message 2>&1 | prefix " "
  326. if [ "${PIPESTATUS[0]}" == "0" ]; then
  327. echo " Config file found in $(config:filename)"
  328. fi
  329. return 1
  330. fi
  331. local tkey=$(e "$keys" | shyaml get-type)
  332. if [ "$tkey" != "sequence" ]; then
  333. err "Value type of ${WHITE}${KEY_PATH}${NORMAL} unexpected (is $tkey, expecting sequence)."
  334. echo " Check content of $(config:filename), and make sure to use a sequence." >&2
  335. return 1
  336. fi
  337. local IP NAME keys host_pass_connected
  338. if ! IP=$(resolve "$HOST"); then
  339. err "'$HOST' name unresolvable."
  340. exit 1
  341. fi
  342. NAME="$HOST"
  343. if [ "$IP" != "$HOST" ]; then
  344. NAME="$HOST ($IP)"
  345. fi
  346. if ! is-port-open "$IP" "22"; then
  347. err "$NAME unreachable or port 22 closed."
  348. exit 1
  349. fi
  350. debug "Host $IP's port 22 is open."
  351. if ! host_pass_connected=$(ssh:open-try \
  352. {root,debian}@"$HOST"); then
  353. err "Could not connect to {root,debian}@$HOST with publickey nor password."
  354. exit 1
  355. fi
  356. read-0a host password <<<"$host_pass_connected"
  357. sudo_if_necessary=
  358. if [ "$password" -o "${host%%@*}" != "root" ]; then
  359. if ! ssh:run "$host" -- sudo -n true >/dev/null 2>&1; then
  360. err "Couldn't do a password-less sudo from $host."
  361. echo " This is not yet supported."
  362. exit 1
  363. else
  364. sudo_if_necessary=sudo
  365. fi
  366. fi
  367. Section Checking access
  368. while read-0 key; do
  369. prefix="${key%% *}"
  370. if [ "$prefix" != "ssh-rsa" ]; then
  371. err "Unsupported key:"$'\n'"$key"
  372. return 1
  373. fi
  374. label="${key##* }"
  375. Elt "considering adding key ${DARKYELLOW}$label${NORMAL}"
  376. dest="/root/.ssh/authorized_keys"
  377. if ssh:run "$host" -- $sudo_if_necessary grep "\"$key\"" "$dest" >/dev/null 2>&1 </dev/null; then
  378. print_info "already present"
  379. print_status noop
  380. Feed
  381. else
  382. if echo "$key" | ssh:run "$host" -- $sudo_if_necessary tee -a "$dest" >/dev/null; then
  383. print_info added
  384. else
  385. echo
  386. Feedback failure
  387. return 1
  388. fi
  389. Feedback success
  390. fi
  391. done < <(e "$keys" | shyaml get-values-0)
  392. Section Checking ovh hostname file
  393. Elt "Checking /etc/ovh-hostname"
  394. if ! ssh:run "$host" -- $sudo_if_necessary [ -e "/etc/ovh-hostname" ]; then
  395. print_info "creating"
  396. ssh:run "$host" -- $sudo_if_necessary cp /etc/hostname /etc/ovh-hostname
  397. ovhname=$(ssh:run "$host" -- $sudo_if_necessary cat /etc/ovh-hostname)
  398. Elt "Checking /etc/ovh-hostname: $ovhname"
  399. Feedback || return 1
  400. else
  401. ovhname=$(ssh:run "$host" -- $sudo_if_necessary cat /etc/ovh-hostname)
  402. Elt "Checking /etc/ovh-hostname: $ovhname"
  403. print_info "already present"
  404. print_status noop
  405. Feed
  406. fi
  407. if ! is_ovh_domain_name "$HOST" && ! str_is_ipv4 "$HOST"; then
  408. Section Checking hostname
  409. Elt "Checking /etc/hostname..."
  410. if [ "$old" != "$HOST" ]; then
  411. old="$(ssh:run "$host" -- $sudo_if_necessary cat /etc/hostname)"
  412. Elt "Hostname is '$old'"
  413. if is_ovh_hostname "$old"; then
  414. Elt "Hostname '$old' --> '$HOST'"
  415. print_info "creating"
  416. echo "$HOST" | ssh:run "$host" -- $sudo_if_necessary tee /etc/hostname >/dev/null &&
  417. ssh:run "$host" -- $sudo_if_necessary hostname "$HOST"
  418. Feedback || return 1
  419. else
  420. print_info "not changing"
  421. print_status noop
  422. Feed
  423. fi
  424. else
  425. print_info "already set"
  426. print_status noop
  427. Feed
  428. fi
  429. else
  430. info "Not changing domain as '$HOST' doesn't seem to be final domain."
  431. fi
  432. }
  433. cmdline.spec.gnu vps-check
  434. cmdline.spec::cmd:vps-check:run() {
  435. : :posarg: [VPS...] 'Target host(s) to check'
  436. echo "" |
  437. vps_mux vps_check "${VPS[@]}"
  438. }
  439. cmdline.spec.gnu vps-install
  440. cmdline.spec::cmd:vps-install:run() {
  441. :
  442. }
  443. cmdline.spec.gnu backup
  444. cmdline.spec:vps-install:cmd:backup:run() {
  445. : :posarg: BACKUP_TARGET 'Backup target.
  446. (ie: myadmin@backup.domain.org:10023/256)'
  447. : :posarg: [VPS...] 'Target host(s) to check'
  448. if [ "${#VPS[@]}" == 0 ]; then
  449. warn "VPS list provided in command line is empty. Nothing will be done."
  450. return 0
  451. fi
  452. if ! [[ "$BACKUP_TARGET" == *"@"* ]]; then
  453. err "Missing admin account identifier in backup target."
  454. echo " Have you forgottent to specify an account, ie 'myadmin@<MYBACKUP_SERVER>' ?)"
  455. return 1
  456. fi
  457. admin=${BACKUP_TARGET%%@*}
  458. server=${BACKUP_TARGET#*@}
  459. p0 "$admin" "$server" |
  460. vps_mux vps_install_backup "${VPS[@]}"
  461. }
  462. cmdline.spec.gnu vps-update
  463. cmdline.spec::cmd:vps-update:run() {
  464. : :posarg: [VPS...] 'Target host to check'
  465. echo "" |
  466. vps_mux vps_update "${VPS[@]}"
  467. }
  468. cmdline.spec.gnu vps-mux
  469. cmdline.spec::cmd:vps-mux:run() {
  470. : :posarg: [VPS...] 'Target host(s) to check'
  471. cat | vps_mux vps_bash "${VPS[@]}"
  472. }
  473. cmdline.spec.gnu vps-space
  474. cmdline.spec::cmd:vps-space:run() {
  475. : :posarg: [VPS...] 'Target host(s) to check'
  476. echo "df /srv -h | tail -n +2 | sed -r 's/ +/ /g' | cut -f 2-5 -d ' '" |
  477. vps_mux vps_bash "${VPS[@]}"
  478. }
  479. cmdline::parse "$@"