diff --git a/README.org b/README.org index 3af5c01..258c65d 100644 --- a/README.org +++ b/README.org @@ -39,12 +39,26 @@ mycéliandre. Un compose de base est aussi proposé. **** Hôte linux base debian 9 et debian 10 +***** Accès ssh standardisé + +Via la commande =0km=, depuis votre poste, permet de standardiser +l'accès avec vos clés SSH, et positionner le nom de domaine si +nécessaire. + +#+BEGIN_SRC sh +0km vps-setup HOST +#+END_SRC + +***** Déploiement de la solution + +Depuis le VPS, en root: + #+BEGIN_SRC sh export WITHOUT_DOCKER_CLEAN=1 ## only if you want to remove docker clean from cron wget https://justodooit.myceliandre.fr/r/deploy -qO - | bash #+END_SRC -If you want to setup odoo admin password and domain in one go: +Si vous souhaitez positionner le nom de domaine: #+BEGIN_SRC sh export WITHOUT_DOCKER_CLEAN=1 ## only if you want to remove docker clean from cron @@ -52,14 +66,6 @@ export DOMAIN=myhost.com wget https://justodooit.myceliandre.fr/r/deploy -qO - | bash #+END_SRC -Considérer la possibilité de mettre en place la clé =0k-admin= pour -accès au compte =root=: - -#+begin_src sh -myc-root-allow-0k-admin -#+end_src - - **** Hôte macosx - install bash, docker diff --git a/bin/0km b/bin/0km new file mode 100755 index 0000000..272240f --- /dev/null +++ b/bin/0km @@ -0,0 +1,343 @@ +#!/bin/bash + + +. /etc/shlib + +include common +include parse +include cmdline +include config + + +[[ "${BASH_SOURCE[0]}" != "${0}" ]] && SOURCED=true + +version=0.1 +desc='Manage 0k related installs' +help="" + + +is-port-open() { + local host="$1" port="$2" timeout=5 + start="$SECONDS" + debug "Testing if $host's port $2 is open ..." + while true; do + timeout 1 bash -c "/dev/null 2>&1 && break + sleep 0.2 + if [ "$((SECONDS - start))" -gt "$timeout" ]; then + return 1 + fi + done +} + +resolve() { + local ent hostname="$1" + debug "Resolving $1 ..." + if ent=$(getent ahosts "$hostname"); then + ent=$(echo "$ent" | egrep ^"[0-9]+.[0-9]+.[0-9]+.[0-9]+\s+" | \ + head -n 1 | awk '{ print $1 }') + debug " .. resolved $1 to $ent." + echo "$ent" + else + debug " .. couldn't resolve $1." + return 1 + fi +} + + +set_errlvl() { return "${1:-1}"; } + +export master_pid=$$ +ssh:open() { + local hostname ssh_cmd ssh_options + ssh_cmd=(ssh) + ssh_options=() + while [ "$#" != 0 ]; do + case "$1" in + "--stdin-password") + ssh_cmd=(sshpass "${ssh_cmd[@]}") + ;; + -o) + ssh_options+=("$1" "$2") + shift + ;; + *) + [ -z "$hostname" ] && hostname="$1" || { + err "Surnumerous positional argument '$1'. Expecting only hostname." + return 1 + } + ;; + esac + shift + done + "${ssh_cmd[@]}" -o ControlPath=/tmp/ssh-control-master-${master_pid} \ + -o ControlMaster=auto -o ControlPersist=900 \ + -o ConnectTimeout=5 -o "StrictHostKeyChecking=no" \ + "${ssh_options[@]}" \ + "$hostname" "$@" -- true || { + err Failed: ssh -o ControlPath=/tmp/ssh-control-master-${master_pid} \ + -o ControlMaster=auto -o ControlPersist=900 \ + "$hostname" "$@" -- true + return 1 + } + trap_add EXIT,INT 'ssh:quit "$hostname"' +} + + +ssh:open-try() { + local opts hostnames + opts=() + hostnames=() + while [ "$#" != 0 ]; do + case "$1" in + -o) + opts+=("$1" "$2") + shift + ;; + *) + hostnames+=("$1") + ;; + esac + shift + done + password='' + for host in "${hostnames[@]}"; do + debug "Trying $host with publickey." + ssh:open -o PreferredAuthentications=publickey \ + "${opts[@]}" \ + "$host" >/dev/null 2>&1 && { + echo "$host"$'\n'"$password"$'\n' + return 0 + } + debug " .. failed connecting to $host with publickey." + done + local times=0 password + while [ "$((++times))" -le 3 ]; do + read -sp "$HOST's password: " password + errlvl="$?" + echo >&2 + if [ "$errlvl" -gt 0 ]; then + exit 1 + fi + for host in "${hostnames[@]}"; do + debug "Trying $host with password ($times/3)" + echo "$password" | ssh:open -o PreferredAuthentications=password \ + --stdin-password \ + "${opts[@]}" \ + "$host" >/dev/null 2>&1 && { + echo "$host"$'\n'"$password"$'\n' + return 0 + } + debug " .. failed connecting to $host with password." + done + err "login failed. Try again... ($((times+1))/3)" + done + return 1 +} + + +ssh:run() { + local hostname="$1" ssh_options cmd + shift + + ssh_options=() + cmd=() + while [ "$#" != 0 ]; do + case "$1" in + "--") + shift + cmd+=("$@") + break + ;; + *) + ssh_options+=("$1") + ;; + esac + shift + done + #echo "$DARKCYAN$hostname$NORMAL $WHITE\$$NORMAL" "$@" + debug "Running cmd: ${cmd[@]}" + for arg in "${cmd[@]}"; do + debug "$arg" + done + { + { + ssh -o ControlPath=/tmp/ssh-control-master-${master_pid} \ + -o ControlMaster=auto -o ControlPersist=900 \ + -o "StrictHostKeyChecking=no" \ + "$hostname" "${ssh_options[@]}" -- "${cmd[@]}" + } 3>&1 1>&2 2>&3 | sed -r "s/^/$DARKCYAN$hostname$NORMAL $DARKRED\!$NORMAL /g" + set_errlvl "${PIPESTATUS[0]}" + } 3>&1 1>&2 2>&3 +} + +ssh:quit() { + local hostname="$1" + shift + ssh -o ControlPath=/tmp/ssh-control-master-${master_pid} \ + -o ControlMaster=auto -o ControlPersist=900 -O exit \ + "$hostname" 2>/dev/null +} + + +is_ovh_domain_name() { + local domain="$1" + + [[ "$domain" == *.ovh.net ]] && return 0 + [[ "$domain" == "ns"*".ip-"*".eu" ]] && return 0 + return 1 +} + +is_ovh_hostname() { + local domain="$1" + + [[ "$domain" =~ ^vps-[0-9a-f]*$ ]] && return 0 + [[ "$domain" =~ ^vps[0-9]*$ ]] && return 0 + return 1 +} + + +[ "$SOURCED" ] && return 0 + +## +## Command line processing +## + + +cmdline.spec.gnu +cmdline.spec.reporting + +cmdline.spec.gnu vps-setup + +cmdline.spec::cmd:vps-setup:run() { + + : :posarg: HOST 'Target host to check/fix ssh-access' + + depends sshpass shyaml + + KEY_PATH="ssh-access.public-keys" + local keys=$(config get-value -y "ssh-access.public-keys") || true + if [ -z "$keys" ]; then + err "No ssh publickeys configured in config file." + echo " Looking for keys in ${WHITE}${KEY_PATH}${NORMAL}" \ + "in config file." >&2 + config:exists --message 2>&1 | prefix " " + if [ "${PIPESTATUS[0]}" == "0" ]; then + echo " Config file found in $(config:filename)" + fi + return 1 + fi + local tkey=$(e "$keys" | shyaml get-type) + if [ "$tkey" != "sequence" ]; then + err "Value type of ${WHITE}${KEY_PATH}${NORMAL} unexpected (is $tkey, expecting sequence)." + echo " Check content of $(config:filename), and make sure to use a sequence." >&2 + return 1 + fi + + local IP NAME keys host_pass_connected + if ! IP=$(resolve "$HOST"); then + err "'$HOST' name unresolvable." + exit 1 + fi + + NAME="$HOST" + if [ "$IP" != "$HOST" ]; then + NAME="$HOST ($IP)" + fi + + if ! is-port-open "$IP" "22"; then + err "$NAME unreachable or port 22 closed." + exit 1 + fi + debug "Host $IP's port 22 is open." + if ! host_pass_connected=$(ssh:open-try \ + {root,debian}@"$HOST"); then + err "Could not connect to {root,debian}@$HOST with publickey nor password." + exit 1 + fi + + read-0a host password <<<"$host_pass_connected" + + sudo_if_necessary= + if [ "$password" -o "${host%%@*}" != "root" ]; then + if ! ssh:run "$host" -- sudo -n true >/dev/null 2>&1; then + err "Couldn't do a password-less sudo from $host." + echo " This is not yet supported." + exit 1 + else + sudo_if_necessary=sudo + fi + fi + + Section Checking access + while read-0 key; do + prefix="${key%% *}" + if [ "$prefix" != "ssh-rsa" ]; then + err "Unsupported key:"$'\n'"$key" + return 1 + fi + label="${key##* }" + Elt "considering adding key ${DARKYELLOW}$label${NORMAL}" + dest="/root/.ssh/authorized_keys" + if ssh:run "$host" -- $sudo_if_necessary grep "\"$key\"" "$dest" >/dev/null 2>&1; then + print_info "already present" + print_status noop + Feed + else + if echo "$key" | ssh:run "$host" -- $sudo_if_necessary tee -a "$dest" >/dev/null; then + print_info added + else + echo + Feedback failure + return 1 + fi + Feedback success + fi + done < <(e "$keys" | shyaml get-values-0) + + Section Checking ovh hostname file + Elt "Checking /etc/ovh-hostname" + if ! ssh:run "$host" -- $sudo_if_necessary [ -e "/etc/ovh-hostname" ]; then + print_info "creating" + ssh:run "$host" -- $sudo_if_necessary cp /etc/hostname /etc/ovh-hostname + ovhname=$(ssh:run "$host" -- $sudo_if_necessary cat /etc/ovh-hostname) + Elt "Checking /etc/ovh-hostname: $ovhname" + Feedback || return 1 + else + ovhname=$(ssh:run "$host" -- $sudo_if_necessary cat /etc/ovh-hostname) + Elt "Checking /etc/ovh-hostname: $ovhname" + print_info "already present" + print_status noop + Feed + fi + + if ! is_ovh_domain_name "$HOST" && ! str_is_ipv4 "$HOST"; then + Section Checking hostname + Elt "Checking /etc/hostname..." + if [ "$old" != "$HOST" ]; then + old="$(ssh:run "$host" -- $sudo_if_necessary cat /etc/hostname)" + Elt "Hostname is '$old'" + if is_ovh_hostname "$old"; then + Elt "Hostname '$old' --> '$HOST'" + print_info "creating" + echo "$HOST" | ssh:run "$host" -- $sudo_if_necessary tee /etc/hostname >/dev/null && + ssh:run "$host" -- $sudo_if_necessary hostname "$HOST" + Feedback || return 1 + else + print_info "not changing" + print_status noop + Feed + fi + + else + print_info "already set" + print_status noop + Feed + fi + else + info "Not changing domain as '$HOST' doesn't seem to be final domain." + fi + +} + + +cmdline::parse "$@"