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.

471 lines
13 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. "${ssh_cmd[@]}" -o ControlPath=/tmp/ssh-control-master-${master_pid} \
  64. -o ControlMaster=auto -o ControlPersist=900 \
  65. -o ConnectTimeout=5 -o "StrictHostKeyChecking=no" \
  66. "${ssh_options[@]}" \
  67. "$hostname" "$@" -- true || {
  68. err Failed: ssh -o ControlPath=/tmp/ssh-control-master-${master_pid} \
  69. -o ControlMaster=auto -o ControlPersist=900 \
  70. "$hostname" "$@" -- true
  71. return 1
  72. }
  73. trap_add EXIT,INT 'ssh:quit "$hostname"'
  74. }
  75. ssh:open-try() {
  76. local opts hostnames
  77. opts=()
  78. hostnames=()
  79. while [ "$#" != 0 ]; do
  80. case "$1" in
  81. -o)
  82. opts+=("$1" "$2")
  83. shift
  84. ;;
  85. *)
  86. hostnames+=("$1")
  87. ;;
  88. esac
  89. shift
  90. done
  91. password=''
  92. for host in "${hostnames[@]}"; do
  93. debug "Trying $host with publickey."
  94. ssh:open -o PreferredAuthentications=publickey \
  95. "${opts[@]}" \
  96. "$host" >/dev/null 2>&1 && {
  97. echo "$host"$'\n'"$password"$'\n'
  98. return 0
  99. }
  100. debug " .. failed connecting to $host with publickey."
  101. done
  102. local times=0 password
  103. while [ "$((++times))" -le 3 ]; do
  104. read -sp "$HOST's password: " password
  105. errlvl="$?"
  106. echo >&2
  107. if [ "$errlvl" -gt 0 ]; then
  108. exit 1
  109. fi
  110. for host in "${hostnames[@]}"; do
  111. debug "Trying $host with password ($times/3)"
  112. echo "$password" | ssh:open -o PreferredAuthentications=password \
  113. --stdin-password \
  114. "${opts[@]}" \
  115. "$host" >/dev/null 2>&1 && {
  116. echo "$host"$'\n'"$password"$'\n'
  117. return 0
  118. }
  119. debug " .. failed connecting to $host with password."
  120. done
  121. err "login failed. Try again... ($((times+1))/3)"
  122. done
  123. return 1
  124. }
  125. ssh:run() {
  126. local hostname="$1" ssh_options cmd
  127. shift
  128. ssh_options=()
  129. cmd=()
  130. while [ "$#" != 0 ]; do
  131. case "$1" in
  132. "--")
  133. shift
  134. cmd+=("$@")
  135. break
  136. ;;
  137. *)
  138. ssh_options+=("$1")
  139. ;;
  140. esac
  141. shift
  142. done
  143. #echo "$DARKCYAN$hostname$NORMAL $WHITE\$$NORMAL" "$@"
  144. debug "Running cmd: ${cmd[@]}"
  145. for arg in "${cmd[@]}"; do
  146. debug "$arg"
  147. done
  148. {
  149. {
  150. ssh -o ControlPath=/tmp/ssh-control-master-${master_pid}-$hostname \
  151. -o ControlMaster=auto -o ControlPersist=900 \
  152. -o "StrictHostKeyChecking=no" \
  153. "$hostname" "${ssh_options[@]}" -- "${cmd[@]}"
  154. } 3>&1 1>&2 2>&3 ## | sed -r "s/^/$DARKCYAN$hostname$NORMAL $DARKRED\!$NORMAL /g"
  155. set_errlvl "${PIPESTATUS[0]}"
  156. } 3>&1 1>&2 2>&3
  157. }
  158. ssh:quit() {
  159. local hostname="$1"
  160. shift
  161. ssh -o ControlPath=/tmp/ssh-control-master-${master_pid} \
  162. -o ControlMaster=auto -o ControlPersist=900 -O exit \
  163. "$hostname" 2>/dev/null
  164. }
  165. is_ovh_domain_name() {
  166. local domain="$1"
  167. [[ "$domain" == *.ovh.net ]] && return 0
  168. [[ "$domain" == "ns"*".ip-"*".eu" ]] && return 0
  169. return 1
  170. }
  171. is_ovh_hostname() {
  172. local domain="$1"
  173. [[ "$domain" =~ ^vps-[0-9a-f]*$ ]] && return 0
  174. [[ "$domain" =~ ^vps[0-9]*$ ]] && return 0
  175. return 1
  176. }
  177. vps_connection_check() {
  178. local vps="$1"
  179. ip=$(resolve "$vps") ||
  180. { echo "${DARKRED}no-resolve${NORMAL}"; return 1; }
  181. is-port-open "$ip" "22" </dev/null ||
  182. { echo "${DARKRED}no-port-22-open${NORMAL}"; return 1; }
  183. ssh:open -o ConnectTimeout=2 -o PreferredAuthentications=publickey \
  184. "root@$vps" >/dev/null 2>&1 ||
  185. { echo "${DARKRED}no-ssh-root-access${NORMAL}"; return 1; }
  186. }
  187. vps_check() {
  188. local vps="$1"
  189. vps_connection_check "$vps" </dev/null || return 1
  190. if size=$(
  191. echo "df /srv -h | tail -n +2 | sed -r 's/ +/ /g' | cut -f 5 -d ' ' | cut -f 1 -d %" |
  192. ssh:run "root@$vps" -- bash); then
  193. if [ "$size" -gt "90" ]; then
  194. echo "${DARKRED}above-90%-disk-usage${NORMAL}"
  195. elif [ "$size" -gt "75" ]; then
  196. echo "${DARKYELLOW}above-75%-disk-usage${NORMAL}"
  197. fi
  198. else
  199. echo "${DARKRED}no-size${NORMAL}"
  200. fi </dev/null
  201. compose_content=$(ssh:run "root@$vps" -- cat /opt/apps/myc-deploy/compose.yml </dev/null) ||
  202. { echo "${DARKRED}no-compose${NORMAL}"; return; }
  203. echo "$compose_content" | grep backup >/dev/null 2>&1 ||
  204. { echo "${DARKRED}no-backup${NORMAL}"; return; }
  205. }
  206. vps_udpate() {
  207. local vps="$1"
  208. vps_connection_check "$vps" || return 1
  209. ssh:run "root@$vps" -- myc-update </dev/null
  210. }
  211. vps_bash() {
  212. local vps="$1"
  213. vps_connection_check "$vps" </dev/null || return 1
  214. ssh:run "root@$vps" -- bash
  215. }
  216. vps_mux() {
  217. local fn="$1" vps_done VPS max_size vps
  218. shift
  219. VPS=($(printf "%s\n" "$@" | sort))
  220. max_size=0
  221. declare -A vps_done;
  222. new_vps=()
  223. for name in "${VPS[@]}"; do
  224. [ -n "${vps_done[$name]}" ] && {
  225. warn "duplicate vps '$name' provided. Ignoring."
  226. continue
  227. }
  228. vps_done["$name"]=1
  229. new_vps+=("$name")
  230. size_name="${#name}"
  231. [ "$max_size" -lt "${size_name}" ] &&
  232. max_size="$size_name"
  233. done
  234. code=$(cat)
  235. for vps in "${new_vps[@]}"; do
  236. label=$(printf "%-${max_size}s" "$vps")
  237. (
  238. {
  239. {
  240. echo "$code" | "$fn" "$vps"
  241. } 3>&1 1>&2 2>&3 | sed -r "s/^/$DARKCYAN$label$NORMAL $DARKRED\!$NORMAL /g"
  242. set_errlvl "${PIPESTATUS[0]}"
  243. } 3>&1 1>&2 2>&3 | sed -r "s/^/$DARKCYAN$label$NORMAL $DARKGRAY\|$NORMAL /g"
  244. set_errlvl "${PIPESTATUS[0]}"
  245. ) &
  246. done
  247. wait
  248. }
  249. [ "$SOURCED" ] && return 0
  250. ##
  251. ## Command line processing
  252. ##
  253. cmdline.spec.gnu
  254. cmdline.spec.reporting
  255. cmdline.spec.gnu vps-setup
  256. cmdline.spec::cmd:vps-setup:run() {
  257. : :posarg: HOST 'Target host to check/fix ssh-access'
  258. depends sshpass shyaml
  259. KEY_PATH="ssh-access.public-keys"
  260. local keys=$(config get-value -y "ssh-access.public-keys") || true
  261. if [ -z "$keys" ]; then
  262. err "No ssh publickeys configured in config file."
  263. echo " Looking for keys in ${WHITE}${KEY_PATH}${NORMAL}" \
  264. "in config file." >&2
  265. config:exists --message 2>&1 | prefix " "
  266. if [ "${PIPESTATUS[0]}" == "0" ]; then
  267. echo " Config file found in $(config:filename)"
  268. fi
  269. return 1
  270. fi
  271. local tkey=$(e "$keys" | shyaml get-type)
  272. if [ "$tkey" != "sequence" ]; then
  273. err "Value type of ${WHITE}${KEY_PATH}${NORMAL} unexpected (is $tkey, expecting sequence)."
  274. echo " Check content of $(config:filename), and make sure to use a sequence." >&2
  275. return 1
  276. fi
  277. local IP NAME keys host_pass_connected
  278. if ! IP=$(resolve "$HOST"); then
  279. err "'$HOST' name unresolvable."
  280. exit 1
  281. fi
  282. NAME="$HOST"
  283. if [ "$IP" != "$HOST" ]; then
  284. NAME="$HOST ($IP)"
  285. fi
  286. if ! is-port-open "$IP" "22"; then
  287. err "$NAME unreachable or port 22 closed."
  288. exit 1
  289. fi
  290. debug "Host $IP's port 22 is open."
  291. if ! host_pass_connected=$(ssh:open-try \
  292. {root,debian}@"$HOST"); then
  293. err "Could not connect to {root,debian}@$HOST with publickey nor password."
  294. exit 1
  295. fi
  296. read-0a host password <<<"$host_pass_connected"
  297. sudo_if_necessary=
  298. if [ "$password" -o "${host%%@*}" != "root" ]; then
  299. if ! ssh:run "$host" -- sudo -n true >/dev/null 2>&1; then
  300. err "Couldn't do a password-less sudo from $host."
  301. echo " This is not yet supported."
  302. exit 1
  303. else
  304. sudo_if_necessary=sudo
  305. fi
  306. fi
  307. Section Checking access
  308. while read-0 key; do
  309. prefix="${key%% *}"
  310. if [ "$prefix" != "ssh-rsa" ]; then
  311. err "Unsupported key:"$'\n'"$key"
  312. return 1
  313. fi
  314. label="${key##* }"
  315. Elt "considering adding key ${DARKYELLOW}$label${NORMAL}"
  316. dest="/root/.ssh/authorized_keys"
  317. if ssh:run "$host" -- $sudo_if_necessary grep "\"$key\"" "$dest" >/dev/null 2>&1 </dev/null; then
  318. print_info "already present"
  319. print_status noop
  320. Feed
  321. else
  322. if echo "$key" | ssh:run "$host" -- $sudo_if_necessary tee -a "$dest" >/dev/null; then
  323. print_info added
  324. else
  325. echo
  326. Feedback failure
  327. return 1
  328. fi
  329. Feedback success
  330. fi
  331. done < <(e "$keys" | shyaml get-values-0)
  332. Section Checking ovh hostname file
  333. Elt "Checking /etc/ovh-hostname"
  334. if ! ssh:run "$host" -- $sudo_if_necessary [ -e "/etc/ovh-hostname" ]; then
  335. print_info "creating"
  336. ssh:run "$host" -- $sudo_if_necessary cp /etc/hostname /etc/ovh-hostname
  337. ovhname=$(ssh:run "$host" -- $sudo_if_necessary cat /etc/ovh-hostname)
  338. Elt "Checking /etc/ovh-hostname: $ovhname"
  339. Feedback || return 1
  340. else
  341. ovhname=$(ssh:run "$host" -- $sudo_if_necessary cat /etc/ovh-hostname)
  342. Elt "Checking /etc/ovh-hostname: $ovhname"
  343. print_info "already present"
  344. print_status noop
  345. Feed
  346. fi
  347. if ! is_ovh_domain_name "$HOST" && ! str_is_ipv4 "$HOST"; then
  348. Section Checking hostname
  349. Elt "Checking /etc/hostname..."
  350. if [ "$old" != "$HOST" ]; then
  351. old="$(ssh:run "$host" -- $sudo_if_necessary cat /etc/hostname)"
  352. Elt "Hostname is '$old'"
  353. if is_ovh_hostname "$old"; then
  354. Elt "Hostname '$old' --> '$HOST'"
  355. print_info "creating"
  356. echo "$HOST" | ssh:run "$host" -- $sudo_if_necessary tee /etc/hostname >/dev/null &&
  357. ssh:run "$host" -- $sudo_if_necessary hostname "$HOST"
  358. Feedback || return 1
  359. else
  360. print_info "not changing"
  361. print_status noop
  362. Feed
  363. fi
  364. else
  365. print_info "already set"
  366. print_status noop
  367. Feed
  368. fi
  369. else
  370. info "Not changing domain as '$HOST' doesn't seem to be final domain."
  371. fi
  372. }
  373. cmdline.spec.gnu vps-check
  374. cmdline.spec::cmd:vps-check:run() {
  375. : :posarg: [VPS...] 'Target host(s) to check'
  376. echo "" |
  377. vps_mux vps_check "${VPS[@]}"
  378. }
  379. cmdline.spec.gnu vps-update
  380. cmdline.spec::cmd:vps-update:run() {
  381. : :posarg: [VPS...] 'Target host to check'
  382. echo "" |
  383. vps_mux vps_update "${VPS[@]}"
  384. }
  385. cmdline.spec.gnu vps-mux
  386. cmdline.spec::cmd:vps-mux:run() {
  387. : :posarg: [VPS...] 'Target host(s) to check'
  388. cat | vps_mux vps_bash "${VPS[@]}"
  389. }
  390. cmdline.spec.gnu vps-space
  391. cmdline.spec::cmd:vps-space:run() {
  392. : :posarg: [VPS...] 'Target host(s) to check'
  393. echo "df /srv -h | tail -n +2 | sed -r 's/ +/ /g' | cut -f 2-5 -d ' '" |
  394. vps_mux vps_bash "${VPS[@]}"
  395. }
  396. cmdline::parse "$@"