Utilisation de .bashrc local sur ssh et consolidation de l'historique des commandes

Si vous devez travailler avec un grand nombre de machines distantes via ssh, alors la question se pose de savoir comment unifier l'environnement shell sur ces machines. Copier .bashrc à l'avance n'est pas très pratique et souvent impossible. Considérons la copie directement lors de la connexion:



[ -z "$PS1" ] && return

sshb() {
    scp ~/.bashrc ${1}:
    ssh $1
}

# the rest of the .bashrc
alias c=cat
...


C'est une manière très naïve avec plusieurs inconvénients évidents:



  • Vous pouvez remplacer un .bashrc existant
  • Au lieu d'une connexion, nous établissons 2
  • En conséquence, vous devrez également vous connecter 2 fois.
  • L'argument de fonction ne peut être que l'adresse de la machine distante


Option améliorée:



[ -z "$PS1" ] && return

sshb() {
    local ssh="ssh -S ~/.ssh/control-socket-$(tr -cd '[:alnum:]' < /dev/urandom|head -c8)"
    $ssh -fNM "$@"
    $ssh placeholder "cat >~/.bash-ssh" <~/.bashrc
    $ssh "$@" -t "bash --rcfile ~/.bash-ssh -i"
    $ssh placeholder -O exit >/dev/null 2>&1
}

# the rest of the .bashrc
alias c=cat
...


Désormais, nous n'utilisons qu'une seule connexion par multiplexage. .bashrc est copié dans un fichier qui n'est pas utilisé par défaut par bash et nous le spécifions explicitement via l'option --rcfile. L'argument de fonction peut être non seulement l'adresse de la machine distante, mais également d'autres options ssh.



En principe, on pourrait s'arrêter là, mais la solution qui en résulte présente un inconvénient désagréable. Si vous exécutez screen ou tmux, le .bashrc sur la machine distante sera utilisé et tous vos alias et fonctions seront perdus. Heureusement, cela peut être surmonté. Pour ce faire, nous devons créer un script wrapper, que nous déclarerons comme notre nouveau shell. Supposons pour simplifier que nous avons déjà un script wrapper sur la machine distante et qu'il se trouve dans ~ / bin / bash-ssh. Le script ressemble à ceci:



#!/bin/bash
exec /bin/bash --rcfile ~/.bash-ssh “$@


Et .bashrc comme ceci:



[ -n "$SSH_TTY" ] && export SHELL="$HOME/bin/bash-ssh"

[ -z "$PS1" ] && return

sshb() {
    local ssh="ssh -S ~/.ssh/control-socket-$(tr -cd '[:alnum:]' < /dev/urandom|head -c8)"
    $ssh -fNM "$@"
    $ssh placeholder "cat >~/.bash-ssh" <~/.bashrc
    $ssh "$@" -t "bash --rcfile ~/.bash-ssh -i"
    $ssh placeholder -O exit >/dev/null 2>&1
}

# the rest of the .bashrc
alias c=cat
...


Si la variable SSH_TTY existe, nous comprenons que nous sommes sur la machine distante et remplaçons la variable SHELL. A partir de maintenant, lors du démarrage d'un nouveau shell interactif, un script sera lancé qui démarrera bash avec une configuration non standard enregistrée lors de l'établissement d'une session ssh.



Pour obtenir une solution de travail pratique, il reste à comprendre comment créer un script wrapper sur une machine distante. En principe, vous pouvez le créer dans la configuration bash que nous sauvegardons comme ceci:



[ -n "$SSH_TTY" ] && {
    mkdir -p "$HOME/bin"
    export SHELL="$HOME/bin/bash-ssh"
    echo -e '#!/bin/bash\nexec /bin/bash --rcfile ~/.bash-ssh "$@"' >$SHELL
    chmod +x $SHELL
}


Mais vous pouvez réellement vous en tirer avec un seul fichier ~ / .bash-ssh:



#!/bin/bash

[ -n "$SSH_TTY" ] && [ "${BASH_SOURCE[0]}" == "${0}" ] && exec bash --rcfile "$SHELL" "$@"

[ -z "$PS1" ] && return

sshb() {
    local ssh="ssh -S ~/.ssh/control-socket-$(tr -cd '[:alnum:]' < /dev/urandom|head -c8)"
    $ssh -fNM "$@"
    $ssh placeholder "cat >~/.bash-ssh" <~/.bashrc
    $ssh "$@" -t 'SHELL=~/.bash-ssh; chmod +x $SHELL; bash --rcfile $SHELL -i'
    $ssh placeholder -O exit >/dev/null 2>&1
}


# the rest of the .bashrc
alias c=cat
...


Maintenant, le fichier ~ / .bash-ssh est à la fois un script autonome et une configuration bash. Cela fonctionne comme ça. Sur la machine locale, les commandes après [-n "$ SSH_TTY"] sont ignorées. Sur la machine distante, la fonction sshb crée un fichier ~ / .bash-ssh et l'utilise comme configuration pour démarrer une session interactive. La construction ["$ {BASH_SOURCE [0]}" == "$ {0}"] vous permet de déterminer si un fichier est téléchargé par un autre script ou lancé en tant que script autonome. Par conséquent, lorsque ~ / .bash-ssh est utilisé



  • as config - exec est ignoré
  • comme un script - le contrôle passe au bash et l'exécution de ~ / .bash-ssh se termine par l'exec.


Désormais, lors de la connexion via ssh, votre environnement se ressemblera partout. Il est beaucoup plus pratique de travailler de cette façon, mais l'historique de l'exécution des commandes restera sur les machines auxquelles vous vous êtes connecté. Personnellement, j'aimerais sauvegarder l'historique localement afin de pouvoir revoir ce que j'ai fait exactement sur certaines machines dans le passé. Pour ce faire, nous avons besoin des composants suivants:



  • Serveur TCP sur la machine locale qui recevrait les données d'une socket et les redirigerait vers un fichier
  • Transfert du port d'écoute de ce serveur vers la machine avec laquelle nous nous connectons via ssh
  • PROMPT_COMMAND dans les paramètres bash, qui enverrait une mise à jour de l'historique au port transféré à la fin de la commande


Cela peut être fait comme ceci:



#!/bin/bash

[ -n "$SSH_TTY" ] && [ "${BASH_SOURCE[0]}" == "${0}" ] && exec bash --rcfile "$SHELL" "$@"

[ -z "$PS1" ] && return

[ -z "$SSH_TTY" ] && {
    history_port=26574
    netstat -lnt|grep -q ":${history_port}\b" || {
        umask 077 && nc -kl 127.0.0.1 "$history_port" >>~/.bash_eternal_history &
    }
}

HISTSIZE=$((1024 * 1024))
HISTFILESIZE=$HISTSIZE
HISTTIMEFORMAT='%t%F %T%t'

update_eternal_history() {
    local histfile_size=$(stat -c %s $HISTFILE)
    history -a
    ((histfile_size == $(stat -c %s $HISTFILE))) && return
    local history_line="${USER}\t${HOSTNAME}\t${PWD}\t$(history 1)"
    local history_sink=$(readlink ~/.bash-ssh.history 2>/dev/null)
    [ -n "$history_sink" ] && echo -e "$history_line" >"$history_sink" 2>/dev/null && return
    local old_umask=$(umask)
    umask 077
    echo -e "$history_line" >> ~/.bash_eternal_history
    umask $old_umask
}

[[ "$PROMPT_COMMAND" == *update_eternal_history* ]] || export PROMPT_COMMAND="update_eternal_history;$PROMPT_COMMAND"

sshb() {
    local ssh="ssh -S ~/.ssh/control-socket-$(tr -cd '[:alnum:]' < /dev/urandom|head -c8)"
    $ssh -fNM "$@"
    local bashrc=~/.bashrc
    [ -r ~/.bash-ssh ] && bashrc=~/.bash-ssh && history_port=$(basename $(readlink ~/.bash-ssh.history))
    local history_remote_port="$($ssh -O forward -R 0:127.0.0.1:$history_port placeholder)"
    $ssh placeholder "cat >~/.bash-ssh; ln -nsf /dev/tcp/127.0.0.1/$history_remote_port ~/.bash-ssh.history" < $bashrc
    $ssh "$@" -t 'SHELL=~/.bash-ssh; chmod +x $SHELL; bash --rcfile $SHELL -i'
    $ssh placeholder -O exit >/dev/null 2>&1
}

# the rest of the .bashrc
alias c=cat
...


Le bloc après [-z "$ SSH_TTY"] ne fonctionne que sur la machine locale. Nous vérifions si le port est occupé et sinon nous exécutons netcat dessus, dont la sortie est redirigée vers un fichier.



La fonction update_eternal_history est appelée juste avant que l'invite bash ne s'affiche. Cette fonction vérifie si la dernière commande était un doublon et sinon, l'envoie au port retransmis. Si le port n'est pas configuré (dans le cas d'une machine locale) ou si une erreur s'est produite lors de l'envoi, la sauvegarde est transférée dans un fichier local.



La fonction sshb a été complétée en définissant une redirection de port et en créant un lien symbolique qui sera utilisé par update_eternal_history pour envoyer des données au serveur.



Cette solution n'est pas sans inconvénients:



  • Le port pour netcat est codé en dur, il y a un risque de conflit
  • ( - - ), , ,


Mon propre fichier .bashrc peut être consulté ici .



Si vous avez des idées sur la façon d'améliorer la solution proposée, merci de les partager dans les commentaires.



Mise à jour. Sur ubuntu 16.04, j'ai rencontré un problème: netcat se fige sur plusieurs connexions et prend 100% du processeur. Je suis passé à socat, des tests préliminaires ont montré que tout allait bien. Ajout d'une logique de gestion du lien symbolique, qui détermine l'adresse à laquelle l'historique est envoyé. Il s'est avéré comme ceci:



#!/bin/bash

[ -n "$SSH_TTY" ] && [ "${BASH_SOURCE[0]}" == "${0}" ] && exec bash --rcfile "$SHELL" "$@"

[ -z "$PS1" ] && return

[ -z "$SSH_TTY" ] && command -v socat >/dev/null && {
    history_port=26574
    netstat -lnt|grep -q ":${history_port}\b" || {
        umask 077 && socat -u TCP4-LISTEN:$history_port,bind=127.0.0.1,reuseaddr,fork OPEN:$HOME/.bash_eternal_history,creat,append &
    }
}

HISTSIZE=$((1024 * 1024))
HISTFILESIZE=$HISTSIZE
HISTTIMEFORMAT='%t%F %T%t'

update_eternal_history() {
    local histfile_size=$(stat -c %s $HISTFILE)
    history -a
    ((histfile_size == $(stat -c %s $HISTFILE))) && return
    local history_line="${USER}\t${HOSTNAME}\t${PWD}\t$(history 1)"
    local history_sink=$(readlink ~/.bash-ssh.history 2>/dev/null)
    [ -n "$history_sink" ] && echo -e "$history_line" >"$history_sink" 2>/dev/null && return
    local old_umask=$(umask)
    umask 077
    echo -e "$history_line" >> ~/.bash_eternal_history
    umask $old_umask
}

[[ "$PROMPT_COMMAND" == *update_eternal_history* ]] || PROMPT_COMMAND="update_eternal_history;$PROMPT_COMMAND"

sshb() {
    local ssh="ssh -S ~/.ssh/control-socket-$(tr -cd '[:alnum:]' < /dev/urandom|head -c8)"
    local bashrc=~/.bashrc
    local history_command="rm -f ~/.bash-ssh.history"
    [ -r ~/.bash-ssh ] && bashrc=~/.bash-ssh && history_port=$(basename $(readlink ~/.bash-ssh.history 2>/dev/null))
    $ssh -fNM "$@"
    [ -n "$history_port" ] && {
        local history_remote_port="$($ssh -O forward -R 0:127.0.0.1:$history_port placeholder)"
        history_command="ln -nsf /dev/tcp/127.0.0.1/$history_remote_port ~/.bash-ssh.history"
    }
    $ssh placeholder "${history_command}; cat >~/.bash-ssh" < $bashrc
    $ssh "$@" -t 'SHELL=~/.bash-ssh; chmod +x $SHELL; bash --rcfile $SHELL -i'
    $ssh placeholder -O exit >/dev/null 2>&1
}

# the rest of the .bashrc
alias c=cat
...



All Articles