#!/bin/bash # -*- coding: utf-8 mode: sh -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8 source "$(dirname -- "$0")/lib/ulib/auto" || exit 1 function display_help() { uecho "$scriptname: se connecter par ssh à un ou plusieurs hôtes USAGE $scriptname [options] hosts $scriptname [options] @hostsfile $scriptname -r hosts $scriptname --parse hosts OPTIONS hosts @hostsfile Spécifier un ou plusieurs hôtes distants sur lequels faire la connexion. Pour spécifier plusieurs hôtes, il faut les séparer par un espace ou le caractère ':', e.g. 'host1 host2' ou 'host1:host2'. Si la spécification contient les caractères { et }, l'expansion est effectuée, e.g 'root@{host1,host2}.univ.run' La forme @hostsfile permet de lire la liste des hôtes depuis le fichier hostsfile, à raison d'un hôte par ligne. Toutes les options de ssh sont reconnues. Les options longues suivantes sont reconnues comme alias de certaines options courtes de ssh: --quiet alias de -q, activer le mode non verbeux --tty alias de -t, forcer l'allocation d'un TTY --login USER alias de -l, spécifier le user avec lequel se connecter --port PORT alias de -p, spécifier le port sur lequel se connecter Les options suivantes sont exclusives à ce script: -d, --domain DOMAIN Spécifier un domaine par défaut pour les hôtes qui sont spécifiés sans domaine. -z, --ssh SSH Spécifier l'exécutable à utiliser pour lancer ssh. -r, --remove Lancer 'ssh-keygen -R' pour chacun des hôtes spécifiés avant de s'y connecter. Par exemple: $scriptname -r host.tld est équivalent à: ssh-keygen -R host.tld ssh-keygen -R host ssh-keygen -R 10.10.1.5 ssh host.tld si l'adresse ip de host.tld est 10.10.1.5 Quand cette option est spécifié, l'option -j est reconnue et permet de NE PAS se reconnecter à l'hôte juste après avoir nettoyé les clés. Avec l'option -j, TOUS les arguments sont des noms d'hôte puisqu'aucune connexion n'est effectuée. --exec --no-exec Avec --exec, si un seul hôte est spécifié, lancer le processus ssh avec exec, pour éviter d'encombrer la mémoire. C'est l'option par défaut. Avec --no-exec, ne jamais utiliser exec pour lancer ssh. --parse Afficher la définition des variables ssh, options, hosts et args qui permettent d'effectuer la connexion à partir d'un autre script. Exemple: "'eval "$(ussh --parse args...)" for host in "${hosts[@]}"; do ${exec:+exec} "$ssh" "${options[@]}" "$host" "${args[@]}" done'" --cc Assumer que nutools est installé sur l'hôte distant, et y lancer uwatch avec l'option --cc, pour permettre de garder la connexion active dans le cadre d'une redirection de port. Si la variable UTOOLS_USSH_RSYNC_SUPPORT contient une valeur non vide, l'analyse des arguments s'arrête à la première valeur qui n'est pas une option, afin de permettre l'utilisation de ce script avec l'option -e de rsync." } __PARSED_HOSTS=() __PARSED_FILES=() function parse_hostsfile() { # Lire chacun des fichiers $* et initialiser __PARSED_HOSTS avec la liste # des hôtes mentionnés dans les fichiers. local inputfile basedir inputs input for inputfile in "$@"; do inputfile="$(abspath "$inputfile")" array_contains __PARSED_FILES "$inputfile" && { ewarn "$(ppath "$inputfile"): inclusion récursive" continue } array_add __PARSED_FILES "$inputfile" basedir="$(dirname "$inputfile")" array_from_lines inputs "$(<"$inputfile" filter_conf)" || { ewarn "$inputfile: fichier ingnoré" continue } for input in "${inputs[@]}"; do if [ "${input#@}" != "$input" ]; then # fichier inclus parse_hostsfile "$(abspath "${input#@}" "$basedir")" else array_addu __PARSED_HOSTS "$input" fi done done } function __expand_braces() { if [[ "$1" == *{* ]] && [[ "$1" == *}* ]]; then eval "echo $1" else echo "$1" fi } function __dot_is_localhost() { [ "$1" == "." ] && echo "localhost" || echo "$1" } function __fix_domain() { local user host splituserhost "$1" user host if [ "$host" == localhost ]; then : # ne pas corriger localhost elif [ "${host%.}" != "$host" ]; then # si le nom se termine par ., le prendre tel quel host="${host%.}" elif ! [[ "$host" == *.* ]]; then # sinon rajouter le domaine par défaut le cas échéant host="$host${DOMAIN:+.$DOMAIN}" fi echo "${user:+$user@}$host" } function fix_hosts() { # Si hosts contient des éléments multiple, comme a:b, séparer ces # éléments. i.e (a b:c "d e") --> (a b c d e) # Supporter la syntaxe @hostsfile qui permet de charger la liste des hôtes # depuis un fichier. # Remplacer aussi les '.' par 'localhost' array_map hosts __expand_braces array_fix_paths hosts ":" array_fix_paths hosts " " local -a _hosts _tmphosts host for host in "${hosts[@]}"; do host="${host%/}" if [ "${host#@}" != "$host" ]; then __PARSED_HOSTS=() parse_hostsfile "${host#@}" array_fix_paths __PARSED_HOSTS array_extendu _hosts __PARSED_HOSTS else array_addu _hosts "$host" fi done array_map _hosts __dot_is_localhost array_map _hosts __fix_domain array_copy hosts _hosts } function remove_key() { estep "$1" ssh-keygen -R "$1" >&/dev/null } function remove_keys() { urequire ipcalc local -a __hosts; array_copy __hosts hosts local -a allhosts hosts ips; local host hostname user ip array_copy allhosts __hosts etitle "Suppression des entrées dans ~/.ssh/known_hosts" for host in "${allhosts[@]}"; do splituserhost "$host" user host if ipcalc_checkip "$host" >/dev/null; then ip="$host" remove_key "$ip" resolv_hosts hosts "$ip" for host in "${hosts[@]}"; do remove_key "$host" hostname="${host%%.*}" [ "$hostname" != "$host" ] && remove_key "$hostname" done else remove_key "$host" hostname="${host%%.*}" [ "$hostname" != "$host" ] && remove_key "$hostname" resolv_ips ips "$host" for ip in "${ips[@]}"; do remove_key "$ip" done fi done eend } function __parse_ssh_config_user() { local config="$1" if [ -f "$config" ]; then awkrun host="$host" ' BEGIN { in_host = 0 found_user = 0 } !found_user && tolower($1) == "host" { in_host = 0 for (i = 2; i <= NF; i++) { if ($i == host) { in_host = 1 break } } } in_host && tolower($1) == "user" { print "SSH_USER=\"" $2 "\"" found_user = 1 } END { if (found_user) print "true" else print "false" } ' -- "$config" else echo "false" fi } function __update_sshopts_user() { # Déterminer le compte utilisateur de connexion et le placer dans la # variables SSH_USER. Analyser pour cela les arguments qui sont fournis dans # les paramètres 2..@. Ajouter l'option -l $DEFAULT_USER au tableau sshopts # si aucune définition pour l'hôte $1 n'existe dans l'un des fichiers de # configuration de ssh et si l'utilisateur n'a pas déjà spécifié un compte # utilisateur. SSH_USER= # L'utilisateur est peut-être fourni avec l'hôte if [ -n "$user" ]; then SSH_USER="$user" return fi # Analyser les configuration de ssh eval "$(__parse_ssh_config_user ~/.ssh/config)" && return eval "$(__parse_ssh_config_user /etc/ssh/ssh_config)" && return # Analyser les arguments local i=1 while [ $i -le $# ]; do if [ "${!i}" == -l ]; then i=$(($i + 1)) SSH_USER="${!i}" fi i=$(($i + 1)) done [ -n "$SSH_USER" ] && return # Rajouter l'option -l au début. De cette façon, on laisse la possibilité à # l'utilisateur de surcharger cette option, dans le cas où nous n'avons pas # pu analyser les arguments correctement SSH_USER="$DEFAULT_USER" sshopts=(${DEFAULT_USER:+-l "$DEFAULT_USER"} "${sshopts[@]}") } function __parse_ssh_config_port() { local config="$1" if [ -f "$config" ]; then awkrun host="$host" ' BEGIN { in_host = 0 found_port = 0 } !found_port && tolower($1) == "host" { in_host = 0 for (i = 2; i <= NF; i++) { if ($i == host) { in_host = 1 break } } } in_host && tolower($1) == "port" { print "SSH_PORT=\"" $2 "\"" found_port = 1 } END { if (found_port) print "true" else print "false" } ' -- "$config" else echo "false" fi } function __update_sshopts_port() { # Déterminer le port de connexion et le placer dans la variable SSH_PORT. # Analyser pour cela les arguments qui sont fournis dans les paramètres 2..@ SSH_PORT=22 # Analyser les configuration de ssh eval "$(__parse_ssh_config_port ~/.ssh/config)" && return eval "$(__parse_ssh_config_port /etc/ssh/ssh_config)" && return # Analyser les arguments local i=1 while [ $i -le $# ]; do if [ "${!i}" == -p ]; then i=$(($i + 1)) SSH_PORT="${!i}" fi i=$(($i + 1)) done } function __update_sshopts_cm() { # Rajouter le cas échéant les options pour le multiplexage des connexions au # tableau sshopts, pour l'hôte $1 # SSH_USER doit être défini local found cmhost cmkey tmphost shared control persist local -a hosts for cmhost in "${CMHOSTS[@]}"; do [ -n "$cmhost" ] || continue cmkey="$cmhost"; cmkey="${cmkey//./_}"; cmkey="${cmkey//-/_}" array_copy hosts "${cmkey}_HOSTS" if [ -z "${hosts[0]}" ]; then if [[ "$cmhost" == *.* ]]; then hosts=("$cmhost" "${cmhost%%.*}") else hosts=("$cmhost${DEFAULT_DOMAIN:+.$DEFAULT_DOMAIN}" "$cmhost") fi fi if array_contains hosts "$host"; then found=1 break fi done [ -n "$found" ] || return shared="${cmkey}_SHARED"; shared="${!shared}" [ -n "$shared" ] || shared=1 # connexion multiplexée par défaut is_yes "$shared" || return control="${cmkey}_CONTROL"; control="${!control}" [ -n "$control" ] || control="$HOME/.ssh/@user@@host:@port" control="${control//@user/$SSH_USER}" control="${control//@port/$SSH_PORT}" control="${control//@host/${hosts[0]}}" persist="${cmkey}_PERSIST"; persist="${!persist}" [ -n "$persist" ] || persist=auto sshopts=("${sshopts[@]}" -o ControlPath="$control" -o ControlMaster=auto) if [ "$persist" == auto ]; then local version major minor version="$(ssh -V 2>&1)" version="${version#OpenSSH_}"; major="${version%%.*}" minor="${version#$major.}"; minor="${minor:0:1}" if [ -z "$major" ]; then # ne devrait pas se produire... mais au cas où persist= elif [ "$major" -ge 6 ]; then persist="$DEFAULT_PERSIST" elif [ "$major" -eq 5 ]; then if [ "$minor" -ge 6 ]; then persist="$DEFAULT_PERSIST" else persist= fi elif [ "$major" -le 4 ]; then persist= fi fi [ -n "$persist" ] && sshopts=("${sshopts[@]}" -o ControlPersist="$persist") } function __update_sshopts() { local user host splituserhost "$1" user host; shift __update_sshopts_user "$@" __update_sshopts_port "$@" __update_sshopts_cm } function show_vars() { local -a sshopts [ "${#hosts[*]}" -gt 1 ] && exec= set_array_cmd hosts echo_setv ssh "$SSH" echo_setv exec "$exec" set_array_cmd args @ "$@" for host in "${hosts[@]}"; do array_copy sshopts SSHOPTS __update_sshopts "$host" "$@" echo_setv host "$host" set_array_cmd options sshopts done } function do_ssh() { local -a sshopts local onehost r if [ ${#hosts[*]} -eq 0 -a $# -eq 0 ]; then ${exec:+exec} "$SSH" "${SSHOPTS[@]}" return elif [ "${#hosts[*]}" -gt 1 ]; then onehost= exec= else onehost=1 fi for host in "${hosts[@]}"; do [ -z "$onehost" ] && etitle "$host" array_copy sshopts SSHOPTS __update_sshopts "$host" "$@" ${exec:+exec} "$SSH" "${sshopts[@]}" "$host" "$@" || r=$? [ -z "$onehost" ] && eend done return "${r:-0}" } function __update_SSHOPTS() { SSHOPTS=("${SSHOPTS[@]}" "$@"); } # charger la configuration set_defaults nutools set_defaults ussh [ -n "$DEFAULT_USER" ] || DEFAULT_USER="$USSH_USER" [ -n "$DEFAULT_DOMAIN" ] || DEFAULT_DOMAIN="$USSH_DOMAIN" [ -n "$DEFAULT_PERSIST" ] || DEFAULT_PERSIST=5m DOMAIN="$DEFAULT_DOMAIN" SSH= remove= remove_only= exec=1 parse= uwatch= parse_opts ${UTOOLS_USSH_RSYNC_SUPPORT:++} "${PRETTYOPTS[@]}" \ --help '$exit_with display_help' \ -1,-2,-4,-6,-A,-a,-C,-f,-g,-K,-k,-M,-N,-n,-q,-s,-T,-t,-V,-v,-X,-x,-Y,-y '$__update_SSHOPTS "$option_"' \ -b:,-c:,-D:,-e:,-F:,-I:,-i:,-L:,-l:,-m:,-O:,-o:,-p:,-R:,-S:,-W:,-w: '$__update_SSHOPTS "$option_" "$value_"' \ --quiet '$__update_SSHOPTS -q' \ --tty '$__update_SSHOPTS -t' \ --login: '$__update_SSHOPTS -l "$value_"' \ --port: '$__update_SSHOPTS -p "$value_"' \ -d:,--domain: DOMAIN= \ -z:,--ssh: SSH= \ -r,--remove '$remove=1; parse=' \ -j remove_only=1 \ --exec exec=1 \ --no-exec exec= \ --parse '$parse=1; remove=' \ --cc uwatch=1 \ @ args -- "$@" && set -- "${args[@]}" || die "$args" if [ -n "$remove" ]; then [ -n "$*" ] || die "Vous devez spécifier les hôtes à supprimer de ~/.ssh/known_hosts" if [ -n "$remove_only" ]; then hosts=("$@") fix_hosts remove_keys exit 0 fi hosts=("$1"); shift fix_hosts remove_keys elif [ -n "$1" ]; then hosts=("$1"); shift fix_hosts fi [ -n "$SSH" ] || SSH=ssh if [ -n "$parse" ]; then show_vars "$@" elif [ -n "$uwatch" ]; then __update_SSHOPTS -t do_ssh "/usr/local/nutools/uwatch --cc" else do_ssh "$@" fi