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.

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