Fork me on GitHub
2/8/2009

Копирование файлов (paramiko и sftp)

Зачастую возникает задача копирования файлов между хостами. Если вы пишите шелл-скрипт, то чаще всего эта задача решается при помощи sftp либо rsync. Для rsync я не встречал хорошего Python-инструмента, а вот для sftp (и ssh) есть отличная библиотека paramiko. О ней и пойдет сегодня речь.

ssh -- это хорошо спроектированный многоуровневый инструмент, поэтому для эффективной работы не плохо бы представлять, хотя бы в общих чертах, как он работает. Про что я буду рассказывать: как скопировать файлы с удаленной машины. Порядок действий примерно таков:

  1. Проверить, что хост, к которому мы подключаемся тот, за который себя выдает. Дело в том, что при первом соединении по ssh, ssh-клиент показывает вам отпечаток (fingerprint) открытого ключа сервера. По-хорошему, вы должны по другому каналу связи выяснить у администратора, действительно ли такой отпечаток у удаленного хоста. В дальнейшем, ssh при каждом коннекте запрашивает ключ удаленной стороны и сравнивает с уже сохраненным. Обычно список сохраненных открытых ключей хранится в ~/.ssh/known_hosts.

  2. Осуществить аутентификацию по ключу. ssh подерживает различные методы аутентификации. Нас интересует аутентификация по ключу. Т.е. создается пара ключей: открытый и закрытый -- открытый ключ помещается на удаленную сторону, закрытый -- хранится на на нашей стороне (обычно -- в ~/.ssh/id_dsa или ~/.ssh/id_rsa, в зависимости от типа ключа -- RSA или DSA). В дальнейшем, при соединении ssh не спрашивает пароль аккаунта на удаленной стороне, а проверяет наличие на удаленной стороне открытого ключа, комплементарного локальному закрытому. Если закрытый ключ запаролен -- он будет спрашивать пароль закрытого ключа. Но для наших целей хватит и безпарольного ключа ;-)

  3. Далее, установить канал связи и скопировать с удаленного хоста определенные файлы.

Поехали.

Ключ хоста

Итак, нам нужно раздобыть список сохраненных ключей и найти нужный нам ключ по имени хоста:

import os
import paramiko

def get_host_key(host):
    hostkeytype = None
    hostkey = None
    # try to load host key from known hosts
    try:
        host_keys = paramiko.util.load_host_keys(
            os.path.expanduser("~/.ssh/known_hosts"))
    except IOError:
        host_keys = {}
    if host in host_keys:
        hostkeytype = host_keys[host].keys()[0]
        hostkey = host_keys[host][hostkeytype]
    return hostkeytype, hostkey

Здесь мы используем os.path.expanduser для того, чтобы ~ "развернуть" в домашний каталог текущего пользователя. Далее, при помощи paramiko.util.load_host_keys парсим этот файл, и если находим нужный нам хост, узнаем тип его ключа (RSA или DSA) и собственно сам ключ.

Закрытый ключ пользователя

Вторым пунктом стоит аутентификация по ключу. Если нам не указали где искать ключ, мы должны посмотреть в обычном месте (~/.ssh/id_rsa и ~/.ssh/id_dsa) и загрузить ключ:

def get_private_key(keyfile=None):
    key = None
    keytype = None
    if keyfile is None:
        keyfiles = [os.path.expanduser('~/.ssh/id_%s' % keytype)
                    for keytype in ('dsa', 'rsa')]
    else:
        keyfiles = [keyfile,]
    for kf in keyfiles:
        try:
            key = paramiko.RSAKey.from_private_key_file(kf)
            keytype = 'ssh-rsa'
        except (IOError, paramiko.SSHException), e:
            try:
                key = paramiko.DSSKey.from_private_key_file(kf)
                keytype = 'ssh-dsa'
            except (IOError, paramiko.SSHException), e:
                pass
    if key is None:
        raise paramiko.SSHException('No rsa or dsa keys are available')        
return keytype, key

Здесь единственный нюанс -- paramiko для RSA и DSA ключей использует отдельные классы, а спрашивать о типе ключа пользователя нам не хочется, так что мы пытаемся вначале загрузить ключ как RSA, если не удается -- как DSA, ну а если и так не получается, то тогда всё же вызывать исключение.

Копируем файлы

Теперь собираем всё вместе и копируем файлы с удаленной стороны:

def get_remote_file(user, host, path, pkeyfile=None):
    hostkeytype, hostkey = get_host_key(host)
    userkeytype, userkey = get_private_key(pkeyfile)
    t = paramiko.Transport((host, 22))
    t.connect(hostkey=hostkey, username=user, pkey=userkey)
    sftp = paramiko.SFTPClient.from_transport(t)
    sftp.get(path, os.path.basename(path))
    # вместо sftp.get можно использовать sftp.open,
    # и дальше как с обычным файлом, но только не забудьте
    # закрыть транспорт...
    t.close()

Полный пример, как обычно, на hg.pyo

Это малая часть того, что умеет Paramiko. Надеюсь, я привлек ваше внимание к этой отличной библиотеке и вы запишите http://www.lag.net/paramiko/ в свои закладки, или зафолловите robey/paramiko на github ;-)

P.S. Еще есть рецепт 576810, чем то похожий на мой пример, но мне он не нравится, ни стилем, ни кодом.

Комментарии

Все статьи