Сообщить об ошибке.

Использование ssh с модулем sh в Python

Ввод пароля в команды терминала

В этом разделе попытаемся подключиться к серверу по ssh и программно ввести пароль.

Примечание. Рекомендуется просто скопировать свой открытый ключ на сервер, чтобы не вводить пароль, но для целей демонстрации, пытаемся ввести пароль.

Чтобы взаимодействовать с процессом, необходимо назначить обратный вызов для STDOUT. Сигнатура обратного вызова, которую будем использовать, принимает объект queue.Queue в качестве второго аргумента, который будет использоваться для отправки STDIN обратно процессу.

from sh import ssh

def ssh_interact(line, stdin):
    line = line.strip()
    print(line)
    if line.endswith("password:"):
        stdin.put("correcthorsebatterystaple")

ssh("10.10.10.100", _out=ssh_interact)
# amoffat@10.10.10.100's password:

Если запустить код выше (заменив IP-адрес, к которому надо подключиться по SSH), заметите, что внутри обратного вызова ничего не выводится. Проблема связана с буферизацией STDOUT. По умолчанию модуль sh использует линейную буферизацию STDOUT, что означает, что ssh_interact() будет получать выходные данные только тогда, когда sh встретит новую строку в выходных данных. Это проблема, потому что запрос пароля не имеет новой строки:

Поскольку новая строка никогда не встречается, в обратный вызов ssh_interact ничего не отправляется. Итак, нужно изменить буферизацию STDOUT. Сделаем это с помощью специального аргумента _out_bufsize. Мы установим его в 0 для небуферизованного вывода:

from sh import ssh

def ssh_interact(line, stdin):
    line = line.strip()
    print(line)
    if line.endswith("password:"):
        stdin.put("correcthorsebatterystaple")

ssh("10.10.10.100", _out=ssh_interact, _out_bufsize=0)
# a
# m
# o
# f
# f
# a
# t
# @
# 1
# 0
# .
# ...
# ...
# r
# d
# :

Если запустить обновленную версию, то вылезет новая проблема. Это связано с тем, что фрагменты STDOUT, которые получает обратный вызов ssh_interact(), не буферизированы и, следовательно, являются отдельными символами, а не целыми строками. Сейчас необходимо объединить эти посимвольные данные в строку, чтобы проверить, был ли отправлен шаблон 'password:', означающий, что SSH готов к вводу.

Было бы разумно инкапсулировать переменную, которую будем использовать для агрегирования, в какое-то замыкание или класс, но для простоты воспользуемся global:

from sh import ssh
import sys

aggregated = ""
def ssh_interact(char, stdin):
    global aggregated
    sys.stdout.write(char.encode())
    sys.stdout.flush()
    aggregated += char
    if aggregated.endswith("password: "):
        stdin.put("correcthorsebatterystaple")

ssh("10.10.10.100", _out=ssh_interact, _out_bufsize=0)

Пример все еще не работает. Есть две проблемы: первая заключается в том, что пароль должен заканчиваться новой строкой, как если бы вы набрали его и нажали клавишу возврата. Это связано с тем, что SSH не знает, насколько длинный пароль, и буферизует STDIN.

Вторая проблема лежит глубже в SSH. SSH нуждается в TTY, прикрепленном к его STDIN для правильной работы. Это заставляет SSH поверить в то, что он взаимодействует с реальным пользователем в реальном сеансе терминала. Чтобы включить TTY, мы можем добавить специальный аргумент _tty_in. Также нужно использовать специальный аргумент _unify_ttys. Это говорит модулю sh, чтобы STDOUT и STDIN поступали с одного псевдотерминала, что является требованием SSH:

from sh import ssh
import sys

aggregated = ""
def ssh_interact(char, stdin):
    global aggregated
    sys.stdout.write(char.encode())
    sys.stdout.flush()
    aggregated += char
    if aggregated.endswith("password: "):
        stdin.put("correcthorsebatterystaple\n")

ssh("10.10.10.100", _out=ssh_interact, _out_bufsize=0, _tty_in=True, _unify_ttys=True)

Вот теперь скрипт удаленного входа работает!

Команда SSH Contrib

Вышеупомянутый процесс можно упростить, используя Contrib Commands. Команда SSH contrib выполняет всю настройку специальныхключевых аргументов и предоставляет простой, но мощный интерфейс для входа в систему с паролем SSH.

from sh.contrib import ssh

def ssh_interact(content, stdin):
    sys.stdout.write(content.cur_char)
    sys.stdout.flush()

# Автоматически входит в систему с паролем, а затем представляет 
# последующий контент для обратного вызова `ssh_interact()`
ssh("10.10.10.100", password="correcthorsebatterystaple", interact=ssh_interact)

Как вы на самом деле нужно использовать SSH?

Многие люди хотят научиться вводить пароль SSH с помощью сценария, т.к. постоянно встает необходимость выполнять удаленные команды на сервере. Вместо того, чтобы пытаться войти через SSH и затем посылать ввод команды для запуска через терминал, посмотрим, как можно сделать это по-другому.

Сначала откроем терминал и запустим команду ssh-copy-id yourservername. Будет предложено ввести пароль для сервера. После ввода пароля, можно подключиться к серверу по SSH без необходимости повторного ввода пароля. Это значительно упрощает работу с модулем sh.

Второе, что нужно сделать - это использовать возможность SSH передать команду для запуска серверу, к которому подключены по SSH. Вот как можно запустить команду ifconfig в терминале на сервере, не используя напрямую оболочку этого сервера:

$ ssh amoffat@10.10.10.100 ifconfig

Если перевести это в код, используя модуль sh, то получится:

import sh
print(sh.ssh("amoffat@10.10.10.100", "ifconfig"))

Можно сделать еще лучше, воспользовавшись преимуществами sh зашивать параметры в команду (например для привязки имени пользователя/ip сервера к объекту команды) :

import sh

my_server = sh.ssh.bake("amoffat@10.10.10.100")
print(my_server("ifconfig"))
print(my_server("whoami"))

Теперь есть многоразовый командный объект, который можно использовать для вызова удаленных команд. Но есть место для еще одного улучшения. Можно использовать функцию подкоманд sh, которая расширяет доступ к атрибутам в аргументы команд:

import sh

my_server = sh.ssh.bake("amoffat@10.10.10.100")
print(my_server.ifconfig())
print(my_server.whoami())