Uso de .bashrc local sobre ssh y consolidación del historial de comandos

Si tiene que trabajar con una gran cantidad de máquinas remotas a través de ssh, entonces surge la pregunta de cómo unificar el entorno de shell en estas máquinas. Copiar .bashrc por adelantado no es muy conveniente y, a menudo, imposible. Consideremos copiar directamente durante la conexión:



[ -z "$PS1" ] && return

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

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


Esta es una forma muy ingenua con varias desventajas obvias:



  • Puede sobrescribir un .bashrc existente
  • En lugar de una conexión, establecemos 2
  • Como resultado, también tendrá que iniciar sesión 2 veces.
  • El argumento de la función solo puede ser la dirección de la máquina remota


Opción mejorada:



[ -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
...


Ahora solo usamos una conexión mediante multiplexación. .bashrc se copia a un archivo que no es utilizado por bash por defecto y lo especificamos explícitamente a través de la opción --rcfile. El argumento de la función puede ser no solo la dirección de la máquina remota, sino también otras opciones de ssh.



En principio, uno podría detenerse en esto, pero la solución resultante tiene un inconveniente desagradable. Si ejecuta screen o tmux, se utilizará el .bashrc en la máquina remota y se perderán todos sus alias y funciones. Afortunadamente, esto se puede superar. Para hacer esto, necesitamos crear un script contenedor, que declararemos como nuestro nuevo shell. Supongamos, por simplicidad, que ya tenemos un script contenedor en la máquina remota y está ubicado en ~ / bin / bash-ssh. El guión se ve así:



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


Y .bashrc así:



[ -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 existe la variable SSH_TTY, entendemos que estamos en la máquina remota y anulamos la variable SHELL. A partir de este punto, cuando se lanza un nuevo shell interactivo, se lanzará un script que iniciará bash con una configuración no estándar guardada cuando se estableció una sesión ssh.



Para obtener una solución de trabajo conveniente, queda por descubrir cómo crear un script de envoltura en una máquina remota. En principio, puede crearlo en la configuración de bash que guardamos así:



[ -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
}


Pero, de hecho, puede arreglárselas con un solo archivo ~ / .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
...


Ahora, el archivo ~ / .bash-ssh es tanto un script independiente como una configuración bash. Funciona así. En la máquina local, los comandos después de [-n "$ SSH_TTY"] se ignoran. En la máquina remota, la función sshb crea un archivo ~ / .bash-ssh y lo usa como configuración para iniciar una sesión interactiva. La construcción ["$ {BASH_SOURCE [0]}" == "$ {0}"] le permite determinar si un archivo se carga con otro script o se inicia como un script independiente. Como resultado, cuando se usa ~ / .bash-ssh



  • como config - exec se ignora
  • como un script - el control pasa al bash y la ejecución de ~ / .bash-ssh termina con el exec.


Ahora, cuando se conecte a través de ssh, su entorno se verá igual en todas partes. Es mucho más conveniente trabajar de esta manera, pero el historial de ejecución de comandos permanecerá en las máquinas a las que se conectó. Personalmente, me gustaría guardar el historial localmente para poder repasar qué he hecho exactamente en algunas máquinas en el pasado. Para hacer esto, necesitamos los siguientes componentes:



  • Servidor Tcp en la máquina local que recibiría datos de un socket y los redirigiría a un archivo
  • Reenviar el puerto de escucha de este servidor a la máquina con la que nos estamos conectando vía ssh
  • PROMPT_COMMAND en la configuración de bash, que enviaría la actualización del historial al puerto reenviado al completar el comando


Esto se puede hacer así:



#!/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
...


El bloque después de [-z "$ SSH_TTY"] solo funciona en la máquina local. Comprobamos si el puerto está ocupado y si no, ejecutamos netcat en él, cuya salida se redirige a un archivo.



La función update_eternal_history se llama justo antes de que se muestre el indicador de bash. Esta función comprueba si el último comando fue un duplicado y, si no, lo envía al puerto reenviado. Si el puerto no está configurado (en el caso de una máquina local) o si ocurrió un error durante el envío, el guardado va a un archivo local.



La función sshb se complementó estableciendo un reenvío de puertos y creando un enlace simbólico que será utilizado por update_eternal_history para enviar datos al servidor.



Esta solución no está exenta de inconvenientes:



  • El puerto para netcat está codificado, existe la posibilidad de que se produzca un conflicto
  • ( - - ), , ,


Mi propio .bashrc se puede ver aquí .



Si tiene ideas sobre cómo mejorar la solución propuesta, comparta los comentarios.



Actualizar. En ubuntu 16.04, me encontré con un problema: netcat se congela en múltiples conexiones y ocupa el 100% de la CPU. Cambié a socat, las pruebas preliminares mostraron que todo está bien. También se agregó lógica para administrar el enlace simbólico, que determina la dirección donde se envía el historial. Resultó así:



#!/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