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.

387 lines
11 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_check() {
  175. local vps="$1"
  176. ip=$(resolve "$vps") ||
  177. { echo "no-resolve"; return; }
  178. ping -c 1 -w 1 "$ip" >/dev/null 2>&1 ||
  179. { echo "no-ping"; }
  180. is-port-open "$ip" "22" ||
  181. { echo "no-port-22-open"; return; }
  182. ssh:open -o ConnectTimeout=2 -o PreferredAuthentications=publickey \
  183. "root@$vps" >/dev/null 2>&1 ||
  184. { echo "no-ssh-root-access"; return; }
  185. compose_content=$(ssh:run "root@$vps" -- cat /opt/apps/myc-deploy/compose.yml </dev/null) ||
  186. { echo "no-compose"; return; }
  187. echo "$compose_content" | grep backup >/dev/null 2>&1 ||
  188. { echo "no-backup"; return; }
  189. }
  190. [ "$SOURCED" ] && return 0
  191. ##
  192. ## Command line processing
  193. ##
  194. cmdline.spec.gnu
  195. cmdline.spec.reporting
  196. cmdline.spec.gnu vps-setup
  197. cmdline.spec::cmd:vps-setup:run() {
  198. : :posarg: HOST 'Target host to check/fix ssh-access'
  199. depends sshpass shyaml
  200. KEY_PATH="ssh-access.public-keys"
  201. local keys=$(config get-value -y "ssh-access.public-keys") || true
  202. if [ -z "$keys" ]; then
  203. err "No ssh publickeys configured in config file."
  204. echo " Looking for keys in ${WHITE}${KEY_PATH}${NORMAL}" \
  205. "in config file." >&2
  206. config:exists --message 2>&1 | prefix " "
  207. if [ "${PIPESTATUS[0]}" == "0" ]; then
  208. echo " Config file found in $(config:filename)"
  209. fi
  210. return 1
  211. fi
  212. local tkey=$(e "$keys" | shyaml get-type)
  213. if [ "$tkey" != "sequence" ]; then
  214. err "Value type of ${WHITE}${KEY_PATH}${NORMAL} unexpected (is $tkey, expecting sequence)."
  215. echo " Check content of $(config:filename), and make sure to use a sequence." >&2
  216. return 1
  217. fi
  218. local IP NAME keys host_pass_connected
  219. if ! IP=$(resolve "$HOST"); then
  220. err "'$HOST' name unresolvable."
  221. exit 1
  222. fi
  223. NAME="$HOST"
  224. if [ "$IP" != "$HOST" ]; then
  225. NAME="$HOST ($IP)"
  226. fi
  227. if ! is-port-open "$IP" "22"; then
  228. err "$NAME unreachable or port 22 closed."
  229. exit 1
  230. fi
  231. debug "Host $IP's port 22 is open."
  232. if ! host_pass_connected=$(ssh:open-try \
  233. {root,debian}@"$HOST"); then
  234. err "Could not connect to {root,debian}@$HOST with publickey nor password."
  235. exit 1
  236. fi
  237. read-0a host password <<<"$host_pass_connected"
  238. sudo_if_necessary=
  239. if [ "$password" -o "${host%%@*}" != "root" ]; then
  240. if ! ssh:run "$host" -- sudo -n true >/dev/null 2>&1; then
  241. err "Couldn't do a password-less sudo from $host."
  242. echo " This is not yet supported."
  243. exit 1
  244. else
  245. sudo_if_necessary=sudo
  246. fi
  247. fi
  248. Section Checking access
  249. while read-0 key; do
  250. prefix="${key%% *}"
  251. if [ "$prefix" != "ssh-rsa" ]; then
  252. err "Unsupported key:"$'\n'"$key"
  253. return 1
  254. fi
  255. label="${key##* }"
  256. Elt "considering adding key ${DARKYELLOW}$label${NORMAL}"
  257. dest="/root/.ssh/authorized_keys"
  258. if ssh:run "$host" -- $sudo_if_necessary grep "\"$key\"" "$dest" >/dev/null 2>&1 </dev/null; then
  259. print_info "already present"
  260. print_status noop
  261. Feed
  262. else
  263. if echo "$key" | ssh:run "$host" -- $sudo_if_necessary tee -a "$dest" >/dev/null; then
  264. print_info added
  265. else
  266. echo
  267. Feedback failure
  268. return 1
  269. fi
  270. Feedback success
  271. fi
  272. done < <(e "$keys" | shyaml get-values-0)
  273. Section Checking ovh hostname file
  274. Elt "Checking /etc/ovh-hostname"
  275. if ! ssh:run "$host" -- $sudo_if_necessary [ -e "/etc/ovh-hostname" ]; then
  276. print_info "creating"
  277. ssh:run "$host" -- $sudo_if_necessary cp /etc/hostname /etc/ovh-hostname
  278. ovhname=$(ssh:run "$host" -- $sudo_if_necessary cat /etc/ovh-hostname)
  279. Elt "Checking /etc/ovh-hostname: $ovhname"
  280. Feedback || return 1
  281. else
  282. ovhname=$(ssh:run "$host" -- $sudo_if_necessary cat /etc/ovh-hostname)
  283. Elt "Checking /etc/ovh-hostname: $ovhname"
  284. print_info "already present"
  285. print_status noop
  286. Feed
  287. fi
  288. if ! is_ovh_domain_name "$HOST" && ! str_is_ipv4 "$HOST"; then
  289. Section Checking hostname
  290. Elt "Checking /etc/hostname..."
  291. if [ "$old" != "$HOST" ]; then
  292. old="$(ssh:run "$host" -- $sudo_if_necessary cat /etc/hostname)"
  293. Elt "Hostname is '$old'"
  294. if is_ovh_hostname "$old"; then
  295. Elt "Hostname '$old' --> '$HOST'"
  296. print_info "creating"
  297. echo "$HOST" | ssh:run "$host" -- $sudo_if_necessary tee /etc/hostname >/dev/null &&
  298. ssh:run "$host" -- $sudo_if_necessary hostname "$HOST"
  299. Feedback || return 1
  300. else
  301. print_info "not changing"
  302. print_status noop
  303. Feed
  304. fi
  305. else
  306. print_info "already set"
  307. print_status noop
  308. Feed
  309. fi
  310. else
  311. info "Not changing domain as '$HOST' doesn't seem to be final domain."
  312. fi
  313. }
  314. cmdline.spec.gnu vps-check
  315. cmdline.spec::cmd:vps-check:run() {
  316. : :posarg: [VPS...] 'Target host to check'
  317. VPS=($(printf "%s\n" "${VPS[@]}" | sort))
  318. declare -A vps_done;
  319. for vps in "${VPS[@]}"; do
  320. [ "${vps_done[$vps]}" ] && {
  321. warn "duplicate vps '$vps' provided. Ignoring."
  322. continue
  323. }
  324. vps_done[$vps]=1
  325. (
  326. vps_check "$vps" 2>&1 | sed "s/^/$vps: /g"
  327. [ "${PIPESTATUS[0]}" == 0 ]
  328. ) &
  329. done
  330. wait
  331. }
  332. cmdline::parse "$@"