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.

472 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. 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_udpate() {
  208. local vps="$1"
  209. vps_connection_check "$vps" || return 1
  210. ssh:run "root@$vps" -- myc-update </dev/null
  211. }
  212. vps_bash() {
  213. local vps="$1"
  214. vps_connection_check "$vps" </dev/null || return 1
  215. ssh:run "root@$vps" -- bash
  216. }
  217. vps_mux() {
  218. local fn="$1" vps_done VPS max_size vps
  219. shift
  220. VPS=($(printf "%s\n" "$@" | sort))
  221. max_size=0
  222. declare -A vps_done;
  223. new_vps=()
  224. for name in "${VPS[@]}"; do
  225. [ -n "${vps_done[$name]}" ] && {
  226. warn "duplicate vps '$name' provided. Ignoring."
  227. continue
  228. }
  229. vps_done["$name"]=1
  230. new_vps+=("$name")
  231. size_name="${#name}"
  232. [ "$max_size" -lt "${size_name}" ] &&
  233. max_size="$size_name"
  234. done
  235. code=$(cat)
  236. for vps in "${new_vps[@]}"; do
  237. label=$(printf "%-${max_size}s" "$vps")
  238. (
  239. {
  240. {
  241. echo "$code" | "$fn" "$vps"
  242. } 3>&1 1>&2 2>&3 | sed -r "s/^/$DARKCYAN$label$NORMAL $DARKRED\!$NORMAL /g"
  243. set_errlvl "${PIPESTATUS[0]}"
  244. } 3>&1 1>&2 2>&3 | sed -r "s/^/$DARKCYAN$label$NORMAL $DARKGRAY\|$NORMAL /g"
  245. set_errlvl "${PIPESTATUS[0]}"
  246. ) &
  247. done
  248. wait
  249. }
  250. [ "$SOURCED" ] && return 0
  251. ##
  252. ## Command line processing
  253. ##
  254. cmdline.spec.gnu
  255. cmdline.spec.reporting
  256. cmdline.spec.gnu vps-setup
  257. cmdline.spec::cmd:vps-setup:run() {
  258. : :posarg: HOST 'Target host to check/fix ssh-access'
  259. depends sshpass shyaml
  260. KEY_PATH="ssh-access.public-keys"
  261. local keys=$(config get-value -y "ssh-access.public-keys") || true
  262. if [ -z "$keys" ]; then
  263. err "No ssh publickeys configured in config file."
  264. echo " Looking for keys in ${WHITE}${KEY_PATH}${NORMAL}" \
  265. "in config file." >&2
  266. config:exists --message 2>&1 | prefix " "
  267. if [ "${PIPESTATUS[0]}" == "0" ]; then
  268. echo " Config file found in $(config:filename)"
  269. fi
  270. return 1
  271. fi
  272. local tkey=$(e "$keys" | shyaml get-type)
  273. if [ "$tkey" != "sequence" ]; then
  274. err "Value type of ${WHITE}${KEY_PATH}${NORMAL} unexpected (is $tkey, expecting sequence)."
  275. echo " Check content of $(config:filename), and make sure to use a sequence." >&2
  276. return 1
  277. fi
  278. local IP NAME keys host_pass_connected
  279. if ! IP=$(resolve "$HOST"); then
  280. err "'$HOST' name unresolvable."
  281. exit 1
  282. fi
  283. NAME="$HOST"
  284. if [ "$IP" != "$HOST" ]; then
  285. NAME="$HOST ($IP)"
  286. fi
  287. if ! is-port-open "$IP" "22"; then
  288. err "$NAME unreachable or port 22 closed."
  289. exit 1
  290. fi
  291. debug "Host $IP's port 22 is open."
  292. if ! host_pass_connected=$(ssh:open-try \
  293. {root,debian}@"$HOST"); then
  294. err "Could not connect to {root,debian}@$HOST with publickey nor password."
  295. exit 1
  296. fi
  297. read-0a host password <<<"$host_pass_connected"
  298. sudo_if_necessary=
  299. if [ "$password" -o "${host%%@*}" != "root" ]; then
  300. if ! ssh:run "$host" -- sudo -n true >/dev/null 2>&1; then
  301. err "Couldn't do a password-less sudo from $host."
  302. echo " This is not yet supported."
  303. exit 1
  304. else
  305. sudo_if_necessary=sudo
  306. fi
  307. fi
  308. Section Checking access
  309. while read-0 key; do
  310. prefix="${key%% *}"
  311. if [ "$prefix" != "ssh-rsa" ]; then
  312. err "Unsupported key:"$'\n'"$key"
  313. return 1
  314. fi
  315. label="${key##* }"
  316. Elt "considering adding key ${DARKYELLOW}$label${NORMAL}"
  317. dest="/root/.ssh/authorized_keys"
  318. if ssh:run "$host" -- $sudo_if_necessary grep "\"$key\"" "$dest" >/dev/null 2>&1 </dev/null; then
  319. print_info "already present"
  320. print_status noop
  321. Feed
  322. else
  323. if echo "$key" | ssh:run "$host" -- $sudo_if_necessary tee -a "$dest" >/dev/null; then
  324. print_info added
  325. else
  326. echo
  327. Feedback failure
  328. return 1
  329. fi
  330. Feedback success
  331. fi
  332. done < <(e "$keys" | shyaml get-values-0)
  333. Section Checking ovh hostname file
  334. Elt "Checking /etc/ovh-hostname"
  335. if ! ssh:run "$host" -- $sudo_if_necessary [ -e "/etc/ovh-hostname" ]; then
  336. print_info "creating"
  337. ssh:run "$host" -- $sudo_if_necessary cp /etc/hostname /etc/ovh-hostname
  338. ovhname=$(ssh:run "$host" -- $sudo_if_necessary cat /etc/ovh-hostname)
  339. Elt "Checking /etc/ovh-hostname: $ovhname"
  340. Feedback || return 1
  341. else
  342. ovhname=$(ssh:run "$host" -- $sudo_if_necessary cat /etc/ovh-hostname)
  343. Elt "Checking /etc/ovh-hostname: $ovhname"
  344. print_info "already present"
  345. print_status noop
  346. Feed
  347. fi
  348. if ! is_ovh_domain_name "$HOST" && ! str_is_ipv4 "$HOST"; then
  349. Section Checking hostname
  350. Elt "Checking /etc/hostname..."
  351. if [ "$old" != "$HOST" ]; then
  352. old="$(ssh:run "$host" -- $sudo_if_necessary cat /etc/hostname)"
  353. Elt "Hostname is '$old'"
  354. if is_ovh_hostname "$old"; then
  355. Elt "Hostname '$old' --> '$HOST'"
  356. print_info "creating"
  357. echo "$HOST" | ssh:run "$host" -- $sudo_if_necessary tee /etc/hostname >/dev/null &&
  358. ssh:run "$host" -- $sudo_if_necessary hostname "$HOST"
  359. Feedback || return 1
  360. else
  361. print_info "not changing"
  362. print_status noop
  363. Feed
  364. fi
  365. else
  366. print_info "already set"
  367. print_status noop
  368. Feed
  369. fi
  370. else
  371. info "Not changing domain as '$HOST' doesn't seem to be final domain."
  372. fi
  373. }
  374. cmdline.spec.gnu vps-check
  375. cmdline.spec::cmd:vps-check:run() {
  376. : :posarg: [VPS...] 'Target host(s) to check'
  377. echo "" |
  378. vps_mux vps_check "${VPS[@]}"
  379. }
  380. cmdline.spec.gnu vps-update
  381. cmdline.spec::cmd:vps-update:run() {
  382. : :posarg: [VPS...] 'Target host to check'
  383. echo "" |
  384. vps_mux vps_update "${VPS[@]}"
  385. }
  386. cmdline.spec.gnu vps-mux
  387. cmdline.spec::cmd:vps-mux:run() {
  388. : :posarg: [VPS...] 'Target host(s) to check'
  389. cat | vps_mux vps_bash "${VPS[@]}"
  390. }
  391. cmdline.spec.gnu vps-space
  392. cmdline.spec::cmd:vps-space:run() {
  393. : :posarg: [VPS...] 'Target host(s) to check'
  394. echo "df /srv -h | tail -n +2 | sed -r 's/ +/ /g' | cut -f 2-5 -d ' '" |
  395. vps_mux vps_bash "${VPS[@]}"
  396. }
  397. cmdline::parse "$@"