Использование локального .bashrc через ssh и консолидация истории выполнения команд

Если вам приходится работать с большим количеством удаленных машин через ssh то возникает вопрос как унифицировать shell окружение на этих машинах. Копировать заранее .bashrc не очень удобно, а зачастую невозможно. Давайте рассмотрим вариант копирования непосредственно в процессе соединения:

[ -z "$PS1" ] && return

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

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

Это очень наивный способ с несколькими очевидными недостатками:


Улучшенный вариант:

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

Теперь мы используем только одно соединение за счет мультиплексирования. .bashrc копируется в файл, который не используется bash по умолчанию и мы явно указываем его через опцию --rcfile. Аргументом функции может быть не только адрес удаленной машины, но и другие опции ssh.

На этом в принципе можно было бы и остановиться, но полученное решение обладает неприятным недостатком. Если вы запустите screen или tmux, то будет использоваться тот .bashrc, который находится на удаленной машине и все ваши алиасы и функции потеряются. К счастью это можно побороть. Для этого надо создать скрипт-обертку, который мы объявим нашим новым шеллом. Давайте для простоты предположим, что скрипт-обертка на удаленной машине у нас уже есть и находится в ~/bin/bash-ssh. Выглядит скрипт вот так::

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

А .bashrc так:

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

Если существует переменная SSH_TTY мы понимаем, что находимся на удаленной машине и переопределяем переменную SHELL. С этого момента при запуске нового интерактивного шелла будет запускаться скрипт, который будет стартовать bash с нестандартным конфигом, сохраненный при установлении ssh сессии.

Для получения удобного рабочего решения осталось придумать как создавать на удаленной машине скрипт-обертку. В принципе можно создавать его в сохраняемом нами конфиге баша вот так:

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

Но на самом деле можно обойтись единственным файлом ~/.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
...

Теперь файл ~/.bash-ssh является одновременно и самостоятельным скриптом и конфигом bash. Работает это так. На локальной машине команды после [ -n "$SSH_TTY" ] игнорируются. На удаленной машине функция sshb создает файл ~/.bash-ssh и использует его как конфиг для запуска интерактивной сессии. Конструкция [ "${BASH_SOURCE[0]}" == "${0}" ] позволяет определить подгружается файл другим скриптом или запущен как самостоятельный скрипт. В результате, когда ~/.bash-ssh используется


Теперь при коннекте по ssh ваше окружение везде будет выглядеть одинаково. Так работать гораздо удобнее, но история выполнения команд будет оставаться на машинах, с которыми вы соединялись. Лично мне бы хотелось сохранять историю локально, чтобы иметь возможность освежить в памяти, что именно я делал на каких-то машинах в прошлом. Для того, чтобы это сделать нам нужны следующие компоненты:


Это можно реализовать так:

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

Блок после [ -z "$SSH_TTY" ] срабатывает только на локальной машине. Мы проверяем занят ли порт и если нет запускаем на нем netcat, вывод которого перенаправлен в файл.

Функция update_eternal_history вызывается непосредственно перед выводом на экран подсказки bash. Эта функция проверяет не была ли последняя команда дубликатом и если нет отправляет ее на отфорварженный порт. Если порт не сконфигурирован (в случае локальной машины) или если при отправке произошла ошибка сохранение идет в локальный файл.

Функция sshb дополнилась установлением форвардинга порта и созданием симлинки, которая будет использоваться update_eternal_history для отправки данных на сервер.

Это решение не лишено недостатков:


Мой собственный .bashrc можно посмотреть тут.

Если у вас есть идеи как можно улучшить предложенное решение пожалуйста делитесь в комментариях.

Update. На ubuntu 16.04 столкнулся с проблемой: netcat при нескольких соединениях подвисает и занимает 100% cpu. Переключился на socat, предварительное тетстирование показало, что все хорошо. Также добавил логики по управлению симлинкой, определяющей адрес куда отсылается история. Получилось вот так:

#!/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
...
@kt97679
24.11.2020 09:51 UTC
Первоисточник

Комментарии

@Sleuthhound
24.11.2020 05:05 UTC
+2
Пожалуй один совет: Если у Вас множество серверов, то Ansible Вам в помощь и не нужно придумывать велосипед.
@dmchmk
24.11.2020 05:44 UTC
+1
это может быть множество очень разных серверов очень разных проектов и людей, которые вот совсем не надо покрывать одним и тем же ансиблом
24.11.2020 08:23 UTC
+1
Разные сервера, разных проектов и людей — категорически не рекомендовал бы объединять bash_history
24.11.2020 15:13 UTC
+1
Пожалуйста обратите внимание, что косолидируется не та история, которая доступна команде history или используется по нажатию ctrl-r. Возможно я неудачно сформулировал, потому что я собираю лог комманд, который включает кроме команды время исполнения и рабочую директорию.
@kt97679
24.11.2020 06:40 UTC
+1
Прошу прощения, надо было явно указать, что я испльзую ssh для диагностики проблем, а не для ручного конфигурирования серверов.
@onix74
24.11.2020 06:47 UTC
0
  1. Не заметил нигде ничего про удаление ~/.ssh/control-socket-*. Не считаете это нужным или всё-таки как-то подчищаете за собой?
  2. "$ssh placeholder..." Какой сакральный смысл несёт placeholder? Теоретически, без этого тоже должно работать.


@kt97679
24.11.2020 07:09 UTC
0
~/.ssh/control-socket-* подчищается автоматически ssh-ем.

placeholder нужен, без него не работает.
24.11.2020 07:13 UTC
0
~/.ssh/control-socket-* подчищается автоматически ssh-ем.
В какой момент? Я пробовал — у меня не подчищается.

placeholder нужен, без него не работает.
У меня работает. Где почитать про эту конструкцию?
24.11.2020 07:20 UTC
0
В какой момент подчищается не знаю. Я на ubuntu 20.04, вот версия моего ssh:

OpenSSH_8.2p1 Ubuntu-4ubuntu0.1, OpenSSL 1.1.1f 31 Mar 2020

Про placeholder тоже не могу сказать ничего более определенного. На какой ОС вы пробуете и какая версия вашего ssh?
24.11.2020 07:25 UTC
0
OpenSSH_8.2p1 Ubuntu-4ubuntu0.1, OpenSSL 1.1.1f 31 Mar 2020
24.11.2020 07:36 UTC
0
kvt@joy:~$ ll .ssh/
total 92
drwx------  2 kvt kvt  4096 Nov 22 22:46 ./
drwxr-xr-x 36 kvt kvt  4096 Nov 22 22:47 ../
-rw-r--r--  1 kvt kvt    73 Nov 21  2018 config
-rw-------  1 kvt kvt  3414 Nov 21  2018 id_rsa
-rw-r--r--  1 kvt kvt   734 Nov 21  2018 id_rsa.pub
-rw-------  1 kvt kvt 36620 Nov 14 17:27 known_hosts
-rw-------  1 kvt kvt 34020 Jul 12 17:10 known_hosts.old
kvt@joy:~$ sshb rage.lan
kvt@rage ~ $ exit
exit
Shared connection to rage.lan closed.
kvt@joy:~$ ll .ssh/
total 92
drwx------  2 kvt kvt  4096 Nov 23 23:32 ./
drwxr-xr-x 36 kvt kvt  4096 Nov 22 22:47 ../
-rw-r--r--  1 kvt kvt    73 Nov 21  2018 config
-rw-------  1 kvt kvt  3414 Nov 21  2018 id_rsa
-rw-r--r--  1 kvt kvt   734 Nov 21  2018 id_rsa.pub
-rw-------  1 kvt kvt 36620 Nov 14 17:27 known_hosts
-rw-------  1 kvt kvt 34020 Jul 12 17:10 known_hosts.old
kvt@joy:~$

При этом в другом терминале после коннекта к rage.lan я вижу:
kvt@joy:~$ ll .ssh/
total 92
drwx------  2 kvt kvt  4096 Nov 23 23:33 ./
drwxr-xr-x 36 kvt kvt  4096 Nov 22 22:47 ../
-rw-r--r--  1 kvt kvt    73 Nov 21  2018 config
srw-------  1 kvt kvt     0 Nov 23 23:33 control-socket-hPR2WOnd=
-rw-------  1 kvt kvt  3414 Nov 21  2018 id_rsa
-rw-r--r--  1 kvt kvt   734 Nov 21  2018 id_rsa.pub
-rw-------  1 kvt kvt 36620 Nov 14 17:27 known_hosts
-rw-------  1 kvt kvt 34020 Jul 12 17:10 known_hosts.old
kvt@joy:~$ 

@saboteur_kiev
24.11.2020 08:31 UTC
0
А почему не что-то в этом роде?

sshb() {
    $ssh -fNM "$@" "curl -O http://mylocalserver/my_bashrc; ssh --rcfile ~/my_bashrc"
}

Соединение одно, будет один лишний bash на удаленной машине, но зато именно bash, а не dash/ksh
@kt97679
24.11.2020 15:15 UTC
0
Прошу прощения, я не понял, что вы предлагаете. С опцией -N ssh не выполняет команды, только устанавливает соединение. Можно попросить вас пояснить ваше предложение?
25.11.2020 16:30 UTC
0
тогда без опции N
предлагал просто положить bashrc на доступный всем сервер.
После подключения первым делом скачивать его на удаленную машину уже на ней, и запускать еще один bash уже с этим профайлом. Таким образом не нужно делать два подключения (для копирования конфига и подключения с конфигом)
26.11.2020 01:00 UTC
0
Если мы установили ssh соединение (которое мультиплексировано, так что 2-х подключений не происходит, я отметил это в статье), то мы гарантированно можем переслать конфигурацию с локальной машины. Если мы подгружаем конфигурацию откуда-то еще, то гарантии успешной скачки нет.

Но если для вас такой вариант удобнее, то его безусловно можно использовать.
@karavan_750
24.11.2020 15:38 UTC
0
ssh -S ~/.ssh/control-socket-$(tr -cd '[:alnum:]' < /dev/urandom|head -c8)
По-моему, так проще и нагляднее:
$ grep -iE '^[^#]*control' .ssh/config 
    ControlMaster auto
    ControlPersist 5
    ControlPath ~/.ssh/ssh_mux_%h_%p_%r

Если мы делаем настройки будущих ssh-сессий, то давайте это делать в конфиге клиента ssh, а не разбрасывать по всем возможным файлам инициализирующим наш профиль.
@dim2r
25.11.2020 17:35 UTC
0
а с putty есть вариант?
@kt97679
26.11.2020 00:54 UTC
0
К сожалению с putty я не работаю.
@Reborns
26.11.2020 17:14 UTC
0
Сюда бы еще и свой .vimrc прикрутить в комплекте :)
@kt97679
26.11.2020 17:17 UTC
0
Это абсолютно реально сделать. Просто добавьте вот эту строчку
$ssh placeholder "cat >~/.vimrc" < .vimrc

перед запуском интерактивного баша.
27.11.2020 05:39 UTC
0
Если я правильно понял то как-то так? Но у меня не сработало.
sshb() {
    local ssh="ssh -S ~/.ssh/control-socket-$(tr -cd '[:alnum:]' < /dev/urandom|head -c8)"
    $ssh -fNM "$@"
    $ssh placeholder "cat >~/.bash-ssh" <~/.bashrc
    $ssh placeholder "cat >~/.vimrc" < .vimrc
    $ssh "$@" -t 'SHELL=~/.bash-ssh; chmod +x $SHELL; bash --rcfile $SHELL -i'
    $ssh placeholder -O exit >/dev/null 2>&1
}
27.11.2020 14:53 UTC
0
Прошу прощения, вот так:
$ssh placeholder "cat >~/.vimrc" < ~/.vimrc

Надо явно указать, что .vimrc берется из корня домашней директории.
@Kirikekeks
29.11.2020 20:45 UTC
-1

Сложновато. Но интересно. Спасибо. Сам пользовать не буду из за сложности, я попсарь. Но иногда хороший джаз в хорошем исполнении замечательно ложится. ;-))