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.

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