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

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

    [ -z "$PS1" ] && return
    
    sshb() {
        scp ~/.bashrc ${1}:
        ssh $1
    }
    
    # the rest of the .bashrc
    alias c=cat
    ...
    

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

    • Можно затереть уже существующий .bashrc
    • Вместо одного соединения мы устанавливаем 2
    • Как следствие авторизоваться придется тоже 2 раза
    • Аргумент функции может быть только адресом удаленной машины

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

    [ -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 используется

    • как конфиг — exec игнорируется
    • как скрипт — управление переходит башу и исполнение ~/.bash-ssh заканчивается тем самым exec-ом.

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

    • Tcp сервер на локальной машине, который бы принимал данные с сокета и перенаправлял их в файл
    • Форвард слушающего порта этого сервера на машину, с которой мы коннектимся по ssh
    • PROMPT_COMMAND в установках bash, который бы по завершению команды отправлял обновление истории на отфорварженный порт

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

    #!/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 для отправки данных на сервер.

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

    • Порт для netcat захардкожен, есть шанс нарваться на конфликт
    • Форвард порта дает возможность любому человеку со злыми намерениями заспамить вашу историю мусорными данными
    • Если вы создаете цепочку соединений (машина А-машина Б-машина В), то данные будут нормально передаваться с В на А, но в случае обрыва соединения между А и Б и установления нового соединения Б будет продолжать форвардить старый порт и данные с В не достигнут А, они будут сохраняться на Б

    Мой собственный .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
    ...
    

    Similar posts

    Ads
    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More

    Comments 23

      +2
      Пожалуй один совет: Если у Вас множество серверов, то Ansible Вам в помощь и не нужно придумывать велосипед.
        +1
        это может быть множество очень разных серверов очень разных проектов и людей, которые вот совсем не надо покрывать одним и тем же ансиблом
          +1
          Разные сервера, разных проектов и людей — категорически не рекомендовал бы объединять bash_history
            +1
            Пожалуйста обратите внимание, что косолидируется не та история, которая доступна команде history или используется по нажатию ctrl-r. Возможно я неудачно сформулировал, потому что я собираю лог комманд, который включает кроме команды время исполнения и рабочую директорию.
          +1
          Прошу прощения, надо было явно указать, что я испльзую ssh для диагностики проблем, а не для ручного конфигурирования серверов.
          0
          1. Не заметил нигде ничего про удаление ~/.ssh/control-socket-*. Не считаете это нужным или всё-таки как-то подчищаете за собой?
          2. "$ssh placeholder..." Какой сакральный смысл несёт placeholder? Теоретически, без этого тоже должно работать.


            0
            ~/.ssh/control-socket-* подчищается автоматически ssh-ем.

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

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

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

                Про placeholder тоже не могу сказать ничего более определенного. На какой ОС вы пробуете и какая версия вашего ssh?
                  0
                  OpenSSH_8.2p1 Ubuntu-4ubuntu0.1, OpenSSL 1.1.1f 31 Mar 2020
                    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:~$ 

            0
            А почему не что-то в этом роде?

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

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

                  Но если для вас такой вариант удобнее, то его безусловно можно использовать.
              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, а не разбрасывать по всем возможным файлам инициализирующим наш профиль.
                0
                а с putty есть вариант?
                  0
                  К сожалению с putty я не работаю.
                  0
                  Сюда бы еще и свой .vimrc прикрутить в комплекте :)
                    0
                    Это абсолютно реально сделать. Просто добавьте вот эту строчку
                    $ssh placeholder "cat >~/.vimrc" < .vimrc

                    перед запуском интерактивного баша.
                      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
                      }
                        0
                        Прошу прощения, вот так:
                        $ssh placeholder "cat >~/.vimrc" < ~/.vimrc

                        Надо явно указать, что .vimrc берется из корня домашней директории.
                    –1

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

                    Only users with full accounts can post comments. Log in, please.