Articles

SSH & SCP em Python com Paramiko

Posted on
19 min. lido
Janeiro 03

SSH SCP em Python com Paramiko

Os prestadores de serviços de gestão de nuvens fizeram um assassinato a partir de serviços geridos em pacotes bem embalados durante anos. Quer sejam bases de dados ou corretores de mensagens, os programadores como nós não parecem ter qualquer problema em pagar um pouco mais para ter as coisas tratadas. Mas espere, não somos tipicamente as últimas pessoas a optar por menos optimização e menos controlo? Por que razão é esta a altura em que decidimos o contrário? Se tivesse de fazer um palpite, apostaria que é em parte porque os DevOps do lado do servidor são uma porcaria.

Como um programador, configurar ou depurar um VPS é normalmente um trabalho que não é contabilizado, e não é particularmente gratificante. Na melhor das hipóteses, a sua aplicação irá provavelmente acabar por funcionar da mesma forma que o seu ambiente local. Como poderíamos tornar melhor esta parte inevitável dos nossos trabalhos? Bem, poderíamos automatizá-la.

Paramiko e SCP são duas bibliotecas Python que podemos utilizar em conjunto para automatizar tarefas que gostaríamos de executar num anfitrião remoto, tais como reiniciar serviços, fazer actualizações, ou agarrar ficheiros de registo. Vamos dar uma vista de olhos ao aspecto da scripting com estas bibliotecas. Aviso justo: há uma quantidade considerável de código neste tutorial, o que tende a deixar-me excitado o suficiente para coagir outros a entrarem nos meus episódios maníacos de codificação-tutorial. Se começar a sentir-se perdido, o repo completo pode ser encontrado aqui:

hackersandslackers/paramiko-tutorial
📡🐍SSH & SCP em Python com Paramiko. Contribuir para o desenvolvimento do hackersandslackers/paramiko-tutorial através da criação de uma conta no GitHub.
GitHubhackersandslackers

Paramiko inclina-se fortemente para o lado “in-the-weeds” das bibliotecas Python. Se estiver à procura de algo fácil que possa simplesmente fazer o trabalho, a pyinfra é supostamente uma óptima (e fácil) alternativa.

Configurar chaves SSH

Para autenticar uma ligação SSH, precisamos de configurar uma chave SSH RSA privada (não confundir com OpenSSH). Podemos gerar uma chave usando o seguinte comando:

$ ssh-keygen -t rsa
Gerar uma chave RSA

Isto irá levar-nos a fornecer um nome para a nossa chave. Dê-lhe o nome que quiser:

Generating a public/private rsa key pair.Enter the file in which you wish to save they key (i.e., /home/username/.ssh/id_rsa):
RSA prompt

Next, ser-lhe-á pedido que forneça uma palavra-passe (sinta-se à vontade para deixar isto em branco).

Agora que temos a nossa chave, precisamos de a copiar para o nosso anfitrião remoto. A forma mais fácil de o fazer é usando ssh-copy-id:

$ ssh-copy-id -i ~/.ssh/mykey username@my_remote_host.org
Copy key to remote host

Verificando a nossa chave SSH

Se quiser verificar que chaves já tem, estes podem ser encontrados no directório .ssh do seu sistema:

$ cd ~/.ssh
Check /.ssh directory

Procuramos chaves que comecem com o seguinte cabeçalho:

-----BEGIN RSA PRIVATE KEY-----...-----END RSA PRIVATE KEY-----
id_rsa

Feel free to do the same on your FPS.

Iniciando o nosso Script

Liberemos instalar as nossas bibliotecas. Acenda o ambiente virtual que preferir e deixe-o rasgar:

$ pip3 install paramiko scp
Instalar paramiko & scp

p>Apenas mais uma coisa antes de escrevermos algum código Python significativo! Crie um ficheiro de configuração para guardar as variáveis que precisamos de ligar ao nosso anfitrião. Aqui estão os barebones do que precisamos para entrar no nosso servidor:

  • Host: O endereço IP ou URL do hospedeiro remoto que estamos a tentar aceder.
  • Nome de utilizador: Este é o nome de utilizador que usa para SSH no seu servidor.
  • Senha (opcional): Se especificou uma frase-senha quando criou a sua chave ssh, especifique isso aqui. Lembre-se que a sua palavra-chave SSH não é a mesma que a palavra-chave do seu utilizador.
  • chave SSH: O caminho do ficheiro da chave que criámos anteriormente. No OSX, estas vivem na pasta ~/.ssh do seu sistema. A chave SSH que estamos a apontar deve ter uma chave de acompanhamento com uma extensão de ficheiro .pub. Esta é a nossa chave pública; se estava a segui-la anteriormente, esta já deveria ter sido gerada para si.

Se estiver a tentar carregar ou descarregar ficheiros do seu anfitrião remoto, terá de incluir mais duas variáveis:

  • Caminho Remoto: O caminho para o directório remoto que procuramos para as transferências de ficheiros. Podemos carregar coisas para esta pasta ou descarregar o seu conteúdo.
  • Caminho Local: A mesma ideia que a anterior, mas o inverso. Para nossa conveniência, o caminho local que vamos utilizar é simplesmente /data, e contém imagens de gifs fox gifs.

Agora temos tudo o que precisamos para fazer um respeitável ficheiro config.py:

"""Remote host configuration."""from os import environ, pathfrom dotenv import load_dotenv# Load environment variables from .envbasedir = path.abspath(path.dirname(__file__))load_dotenv(path.join(basedir, '.env'))# Read environment variableshost = environ.get('REMOTE_HOST')user = environ.get('REMOTE_USERNAME')ssh_key_filepath = environ.get('SSH_KEY')remote_path = environ.get('REMOTE_PATH')local_file_directory = 'data'
config.py

Criar um Cliente SSH

Vamos criar uma classe chamada RemoteClient para lidar com as interacções que vamos ter com o nosso anfitrião remoto. Antes de ficarmos demasiado extravagantes, vamos começar por instanciar a classe RemoteClient com as variáveis que criámos em config.py:

"""Client to handle connections and actions executed against a remote host."""class RemoteClient: """Client to interact with a remote host via SSH & SCP.""" def __init__(self, host, user, ssh_key_filepath, remote_path): self.host = host self.user = user self.ssh_key_filepath = ssh_key_filepath self.remote_path = remote_path
client.py

Nada impressionante até ao momento: apenas definimos algumas variáveis e passámo-las para uma classe inútil. Vamos dar um pontapé de saída às coisas sem deixar o nosso construtor:

"""Client to handle connections and actions executed against a remote host."""from paramiko import SSHClient, AutoAddPolicy, RSAKeyfrom paramiko.auth_handler import AuthenticationException, SSHExceptionclass RemoteClient: """Client to interact with a remote host via SSH & SCP.""" def __init__(self, host, user, ssh_key_filepath, remote_path): self.host = host self.user = user self.ssh_key_filepath = ssh_key_filepath self.remote_path = remote_path self.client = None self.scp = None self.conn = None self._upload_ssh_key()
client.py

Adicionámos três coisas novas a serem instanciadas com a nossa classe:

  • self.client: self.client acabará por servir como objecção de ligação na nossa classe, semelhante à forma como lidou com terminologia como conn em bibliotecas de bases de dados. A nossa ligação será None até que nos conectemos explicitamente ao nosso anfitrião remoto.
  • self.scp: semelhante a self.client, mas trata exclusivamente de ligações para transferência de ficheiros.
  • self.__upload_ssh_key() não é uma variável, mas sim uma função a ser executada automaticamente sempre que o nosso cliente for instanciado. Chamando __upload_ssh_key() está a dizer ao nosso objecto RemoteClient para verificar as chaves locais ssh imediatamente após a criação, para que possamos tentar passá-las ao nosso anfitrião remoto. Caso contrário, não seríamos de todo capazes de estabelecer uma ligação.

Uploading SSH Keys to a Remote Host

Chegámos à secção deste exercício onde precisamos de derrubar algum código de caldeira devastadoramente inglório. Isto é tipicamente onde indivíduos emocionalmente inferiores sucumbem à pura obscuridade da compreensão das chaves SSH e da manutenção das ligações. Não se enganem: autenticar e gerir ligações a qualquer coisa programática é extremamente aborrecido… a não ser que o vosso guia turístico seja por acaso um encantador orador, servindo como vosso amoroso protector através de uma obscuridade perigosa. Algumas pessoas chamam a este post um tutorial. Pretendo chamar-lhe art.

RemoteCliente começará com dois métodos privados: __get_ssh_key() e __upload_ssh_key(). O primeiro irá buscar uma chave pública armazenada localmente, e se tiver sucesso, o segundo irá entregar esta chave pública ao nosso anfitrião remoto como um ramo de acesso de oliveira. Quando uma chave pública criada localmente existir numa máquina remota, essa máquina confiará para sempre nos nossos pedidos para nos ligarmos a ela: não são necessárias palavras-passe. Vamos incluir o registo adequado ao longo do caminho, para o caso de termos algum problema:

"""Client to handle connections and actions executed against a remote host."""from os import systemfrom paramiko import SSHClient, AutoAddPolicy, RSAKeyfrom paramiko.auth_handler import AuthenticationException, SSHExceptionfrom scp import SCPClient, SCPExceptionfrom .log import loggerclass RemoteClient: """Client to interact with a remote host via SSH & SCP.""" ... def _get_ssh_key(self): """ Fetch locally stored SSH key. """ try: self.ssh_key = RSAKey.from_private_key_file(self.ssh_key_filepath) logger.info(f'Found SSH key at self {self.ssh_key_filepath}') except SSHException as error: logger.error(error) return self.ssh_key def _upload_ssh_key(self): try: system(f'ssh-copy-id -i {self.ssh_key_filepath} {self.user}@{self.host}>/dev/null 2>&1') system(f'ssh-copy-id -i {self.ssh_key_filepath}.pub {self.user}@{self.host}>/dev/null 2>&1') logger.info(f'{self.ssh_key_filepath} uploaded to {self.host}') except FileNotFoundError as error: logger.error(error)
client.py

_get_ssh_key() é bastante simples: verifica que existe uma chave SSH no caminho que especificámos na nossa configuração para ser utilizada para ligação ao nosso anfitrião. Se o ficheiro existir de facto, definimos alegremente a nossa variável self.ssh_key, para que esta chave possa ser carregada e utilizada pelo nosso cliente a partir daqui. Paramiko fornece-nos um submódulo chamado RSAKey para tratar facilmente todas as coisas relacionadas com a chave RSA, como analisar um ficheiro de chave privada numa autenticação de ligação utilizável. É o que obtemos aqui:

 RSAKey.from_private_key_file(self.ssh_key_filepath)
Ler chave RSA a partir de ficheiro local.

Se a nossa chave RSA fosse um disparate incompreensível em vez de uma chave real, a SSHException de Paramiko teria apanhado isto e levantado uma excepção logo no início, explicando apenas isso. A utilização adequada do tratamento de erros de uma biblioteca tira muitas das conjecturas do “que correu mal”, especialmente em casos como quando há potencial para numerosas incógnitas num espaço de nicho com que nenhum de nós se mete frequentemente.

_upload_ssh_key() é onde conseguimos enfiar a nossa chave SSH pela garganta abaixo do nosso servidor remoto enquanto gritamos, “LOOK! PODES CONFIAR EM MIM PARA SEMPRE AGORA”! Para o conseguir, vou um pouco “old school” passando comandos de bash através de os.system de Python. A menos que alguém me faça tomar consciência de uma abordagem mais limpa nos comentários, vou assumir que esta é a forma mais má de lidar com a passagem de chaves para um servidor remoto.

A forma padrão não-python de passar chaves a um anfitrião tem este aspecto:

ssh-copy-id -i ~/.ssh/mykey user@host
Pass SSH key to remote host

Isto é precisamente o que realizamos na nossa função em Python, que se parece com isto:

system(f'ssh-copy-id -i {self.ssh_key_filepath} \ {self.user}@{self.host}>/dev/null 2>&1')

Suponho que não me vai deixar escapar que /dev/null 2>&1 mordido por si? Muito bem. Se tiver de saber, aqui está um tipo no StackOverflow a explicar melhor do que eu:

> é para redireccionar /dev/null é um buraco negro onde quaisquer dados enviados, serão descartados. 2 é o descritor de ficheiro para erro padrão. > é para redireccionar. & é o símbolo para o descritor de ficheiro (sem ele, o seguinte 1 seria considerado um nome de ficheiro). 1 é o descritor de ficheiro para Standard O.

Então estamos basicamente a dizer ao nosso servidor remoto que lhe estamos a dar algo, e é tudo do tipo “onde ponho esta coisa”, ao qual respondemos “em nenhum lugar físico no espaço, pois este não é um objecto, mas sim um símbolo eterno da nossa amizade”. O nosso anfitrião remoto é então inundado de gratidão e emoção, porque sim, os computadores têm emoções, mas não podemos ser incomodados por isso neste momento.

Conectar ao nosso cliente

Adicionaremos um método ao nosso cliente chamado connect() para lidar com a ligação ao nosso anfitrião:

...class RemoteClient: """Client to interact with a remote host via SSH & SCP.""" ... def _connect(self): """Open connection to remote host.""" if self.conn is None: try: self.client = SSHClient() self.client.load_system_host_keys() self.client.set_missing_host_key_policy(AutoAddPolicy()) self.client.connect( self.host, username=self.user, key_filename=self.ssh_key_filepath, look_for_keys=True, timeout=5000 ) self.scp = SCPClient(self.client.get_transport()) except AuthenticationException as error: logger.error(f'Authentication failed: \ did you remember to create an SSH key? {error}') raise error return self.client
client.py

p>Dividamos isto:

  • client = SSHClient() prepara o cenário para a criação de um objecto representando o nosso cliente SSH. As seguintes linhas irão configurar este objecto para o tornar mais útil.
  • load_system_host_keys() instrui o nosso cliente a procurar todos os anfitriões a que nos ligámos no passado, olhando para o ficheiro de hosts conhecidos_do nosso sistema e encontrando as chaves SSH que o nosso anfitrião está à espera. Nunca nos ligámos ao nosso anfitrião no passado, por isso precisamos de especificar explicitamente a nossa chave SSH.
  • set_missing_host_key_policy() diz a Paramiko o que fazer no caso de um par de chaves desconhecido. Isto é esperar uma “política” incorporada à Paramiko, à qual vamos especificar AutoAddPolicy(). Definir a nossa política para “auto-add” significa que se tentarmos ligar a um anfitrião não reconhecido, o Paramiko adicionará automaticamente a chave em falta localmente.
  • connect() é o método mais importante do SSHClient (como pode imaginar). Somos finalmente capazes de passar o nosso anfitrião, utilizador, e chave SSH para alcançar o que todos temos estado à espera: uma gloriosa ligação SSH no nosso servidor! O método connect() permite uma tonelada de flexibilidade através de uma vasta gama de argumentos opcionais de palavras-chave também. Acontece que eu passo alguns aqui: definir look_for_keys para True dá a Paramiko permissão para procurar na nossa pasta ~/.ssh para descobrir chaves SSH por si só, e definir o timeout fechará automaticamente as ligações que provavelmente esqueceremos de fechar. Poderíamos até passar variáveis para coisas como porta e senha, se tivéssemos escolhido ligar ao nosso anfitrião desta forma.

Desligar

Devemos fechar as ligações ao nosso anfitrião remoto sempre que acabarmos de as utilizar. Não o fazer pode não ser necessariamente desastroso, mas já tive alguns casos em que ligações penduradas suficientes acabariam por maximizar o tráfego de entrada na porta 22. Independentemente de o seu caso de utilização poder considerar um reinício como um desastre ou um ligeiro inconveniente, vamos apenas fechar as nossas malditas ligações como adultos como se estivéssemos a limpar o rabo depois de fazer cocó. Não importa a higiene da sua ligação, defendo a definição de uma variável de tempo limite (como vimos anteriormente). Seja como for, voilà:

class RemoteClient: ... def disconnect(self): """Close ssh connection.""" if self.client: self.client.close() if self.scp: self.scp.close()
client.py

Fun fact: setting self.client.close() sets actually self.client a igualar None, o que é útil em casos em que se queira verificar se uma ligação já está aberta.

Executar Comandos Unix

Temos agora uma maravilhosa classe Python que pode encontrar chaves RSA, ligar, e desligar. Falta-lhe a capacidade de fazer, bem, qualquer coisa útil.

Podemos corrigir isto e finalmente começar a fazer “coisas” com um método completamente novo para executar comandos, que eu apropriadamente apelidarei execute_commands() (isso é correcto, “comandos” como em potencialmente mais do que um, tocaremos nisso dentro de momentos). O trabalho de perna de tudo isto é feito pelo método interno do cliente Paramiko exec_command(), que aceita uma única string como um comando e o executa:

class RemoteClient: ... def execute_commands(self, commands): """ Execute multiple commands in succession. :param commands: List of unix commands as strings. :type commands: List """ self.conn = self._connect() for cmd in commands: stdin, stdout, stderr = self.client.exec_command(cmd) stdout.channel.recv_exit_status() response = stdout.readlines() for line in response: logger.info(f'INPUT: {cmd} | OUTPUT: {line}')
client.py

A função que acabámos de criar execute_commands() espera que uma lista de cordas seja executada como comandos. Isso é parcialmente por conveniência, mas também porque o Paramiko não executa quaisquer alterações de “estado” (como mudar directórios) entre comandos, por isso cada comando que passamos ao Paramiko deve assumir que estamos a trabalhar a partir da raiz do nosso servidor. Tomei a liberdade de passar três desses comandos como so:

remote.execute_commands()
__init__.py

Posso ver o conteúdo de um directório encadeando cd path/to/dir && ls, mas correr cd path/to/dir seguido por ls não resultaria em nada porque ls a segunda vez devolve a lista de ficheiros na raiz do nosso servidor.

notará devolve três valores em vez de um: isto pode ser útil para ver que input produziu que output. Por exemplo, aqui estão os registos completos do exemplo que forneci onde passei três comandos para remote.execute_commands():

2020-01-02 23:20:16.103 | paramiko_tutorial.client:execute_cmd:95 - INPUT: cd /var/www/ && ls | OUTPUT: django2020-01-02 23:20:16.103 | paramiko_tutorial.client:execute_cmd:95 - INPUT: cd /var/www/ && ls | OUTPUT: ghost2020-01-02 23:20:16.103 | paramiko_tutorial.client:execute_cmd:95 - INPUT: cd /var/www/ && ls | OUTPUT: hackers-hbs2020-01-02 23:20:16.103 | paramiko_tutorial.client:execute_cmd:95 - INPUT: cd /var/www/ && ls | OUTPUT: html2020-01-02 23:20:16.104 | paramiko_tutorial.client:execute_cmd:95 - INPUT: cd /var/www/ && ls | OUTPUT: hustlers2020-01-02 23:20:16.104 | paramiko_tutorial.client:execute_cmd:95 - INPUT: cd /var/www/ && ls | OUTPUT: pizza2020-01-02 23:20:16.104 | paramiko_tutorial.client:execute_cmd:95 - INPUT: cd /var/www/ && ls | OUTPUT: toddbirchard2020-01-02 23:20:16.196 | paramiko_tutorial.client:execute_cmd:95 - INPUT: tail /var/log/nginx/access.log | OUTPUT: 134.209.37.49 - - "GET / HTTP/2.0" 404 139 "-" "Mozilla/5.0 (compatible; NetcraftSurveyAgent/1.0; [email protected])"2020-01-02 23:20:16.196 | paramiko_tutorial.client:execute_cmd:95 - INPUT: tail /var/log/nginx/access.log | OUTPUT: 54.36.148.187 - - "GET /robots.txt HTTP/1.1" 404 149 "-" "Mozilla/5.0 (compatible; AhrefsBot/6.1; +http://ahrefs.com/robot/)"2020-01-02 23:20:16.196 | paramiko_tutorial.client:execute_cmd:95 - INPUT: tail /var/log/nginx/access.log | OUTPUT: 54.36.150.104 - - "GET / HTTP/1.1" 404 139 "-" "Mozilla/5.0 (compatible; AhrefsBot/6.1; +http://ahrefs.com/robot/)"2020-01-02 23:20:16.196 | paramiko_tutorial.client:execute_cmd:95 - INPUT: tail /var/log/nginx/access.log | OUTPUT: 46.229.168.146 - - "GET /robots.txt HTTP/1.1" 200 92 "-" "Mozilla/5.0 (compatible; SemrushBot/6~bl; +http://www.semrush.com/bot.html)"2020-01-02 23:20:16.197 | paramiko_tutorial.client:execute_cmd:95 - INPUT: tail /var/log/nginx/access.log | OUTPUT: 46.229.168.153 - - "GET /the-art-of-technical-documentation/ HTTP/1.1" 200 9472 "-" "Mozilla/5.0 (compatible; SemrushBot/6~bl; +http://www.semrush.com/bot.html)"2020-01-02 23:20:16.197 | paramiko_tutorial.client:execute_cmd:95 - INPUT: tail /var/log/nginx/access.log | OUTPUT: 157.55.39.171 - - "GET /robots.txt HTTP/1.1" 200 94 "-" "Mozilla/5.0 (compatible; bingbot/2.0; +http://www.bing.com/bingbot.htm)"2020-01-02 23:20:16.197 | paramiko_tutorial.client:execute_cmd:95 - INPUT: tail /var/log/nginx/access.log | OUTPUT: 40.77.167.220 - - "GET / HTTP/1.1" 200 3791 "-" "Mozilla/5.0 (compatible; bingbot/2.0; +http://www.bing.com/bingbot.htm)"2020-01-02 23:20:16.197 | paramiko_tutorial.client:execute_cmd:95 - INPUT: tail /var/log/nginx/access.log | OUTPUT: 54.36.150.64 - - "GET /the-ruin-of-feeling-powerless/ HTTP/2.0" 200 9605 "-" "Mozilla/5.0 (compatible; AhrefsBot/6.1; +http://ahrefs.com/robot/)"2020-01-02 23:20:16.197 | paramiko_tutorial.client:execute_cmd:95 - INPUT: tail /var/log/nginx/access.log | OUTPUT: 54.36.150.187 - - "GET /bigquery-and-sql-databases/ HTTP/2.0" 404 146 "-" "Mozilla/5.0 (compatible; AhrefsBot/6.1; +http://ahrefs.com/robot/)"2020-01-02 23:20:16.197 | paramiko_tutorial.client:execute_cmd:95 - INPUT: tail /var/log/nginx/access.log | OUTPUT: 46.229.168.149 - - "GET /robots.txt HTTP/1.1" 502 182 "-" "Mozilla/5.0 (compatible; SemrushBot/6~bl; +http://www.semrush.com/bot.html)"2020-01-02 23:20:16.322 | paramiko_tutorial.client:execute_cmd:95 - INPUT: ps aux | grep node | OUTPUT: ghost 1354 39.2 3.4 1223844 140172 ? Sl 04:20 0:05 /usr/bin/node current/index.js2020-01-02 23:20:16.323 | paramiko_tutorial.client:execute_cmd:95 - INPUT: ps aux | grep node | OUTPUT: ghost 1375 36.8 3.3 1217696 135548 ? Sl 04:20 0:04 /usr/bin/node current/index.js2020-01-02 23:20:16.323 | paramiko_tutorial.client:execute_cmd:95 - INPUT: ps aux | grep node | OUTPUT: ghost 1395 46.8 3.6 1229824 147384 ? Sl 04:20 0:06 /usr/bin/node current/index.js2020-01-02 23:20:16.323 | paramiko_tutorial.client:execute_cmd:95 - INPUT: ps aux | grep node | OUTPUT: ghost 1410 37.7 3.2 1216320 132912 ? Sl 04:20 0:04 /usr/bin/node current/index.js2020-01-02 23:20:16.323 | paramiko_tutorial.client:execute_cmd:95 - INPUT: ps aux | grep node | OUTPUT: root 1848 0.0 0.0 13312 3164 ? Ss 04:20 0:00 bash -c ps aux | grep node2020-01-02 23:20:16.323 | paramiko_tutorial.client:execute_cmd:95 - INPUT: ps aux | grep node | OUTPUT: root 1850 0.0 0.0 14856 1104 ? S 04:20 0:00 grep node
Output

algumas coisas bonitas aqui. Agora podem ver que sites estão no meu servidor, que bots me estão a enviar spam, e quantos processos de nós estou a executar.

Não quero perder muito mais tempo com a arte de executar comandos, mas vale a pena mencionar a presença do porquê de chamarmos stdout.channel.recv_exit_status() após cada comando. Esperando por recv_exit_status() para voltar após executar client.exec_command() força os nossos comandos a serem executados em sincronia, caso contrário é provável que a nossa máquina remota não esteja prestes a decifrar comandos tão rapidamente como os transmitimos.

Carregar (e descarregar) ficheiros via SCP

SCP refere-se tanto ao protocolo de cópia de ficheiros para máquinas remotas (protocolo de cópia segura) como à biblioteca Python, que utiliza isto. Já instalámos a biblioteca SCP, por isso importe essa merda.

As bibliotecas SCP e Paramiko complementam-se mutuamente para tornar o carregamento via SCP super fácil. SCPClient() cria um objecto que espera “transporte” do Paramiko, que fornecemos com self.conn.get_transport(). Criando uma ligação SCP porgybacks fora do nosso cliente SSH em termos de sintaxe, mas estas ligações são separadas. É possível fechar uma ligação SSH e deixar uma ligação SCP aberta, por isso não faça isso. Abra uma ligação SCP como esta:

self.scp = SCPClient(self.client.get_transport())
Abrir uma ligação SCP.

Carregar um único ficheiro é aborrecido, por isso vamos em vez disso carregar um directório inteiro de ficheiros. bulk_upload() aceita uma lista de caminhos de ficheiros, e depois chama __upload_single_file()

class RemoteClient: ... def bulk_upload(self, files): """ Upload multiple files to a remote directory. :param files: List of paths to local files. :type files: List """ self.conn = self._connect() uploads = logger.info(f'Finished uploading {len(uploads)} files to {self.remote_path} on {self.host}')
client.py

O nosso método espera receber duas cordas: a primeira é o caminho local para o nosso ficheiro, e a segunda é o caminho do directório remoto que gostaríamos de carregar para.

SCP’s put() o método irá carregar um ficheiro local para o nosso anfitrião remoto. Isto substituirá os ficheiros existentes com o mesmo nome, caso estes existam no destino que especificamos. É tudo o que é preciso!

Download Files

A contrapartida do método put() é o método get() do SCP:

class RemoteClient: ... def download_file(self, file): """Download file from remote host.""" if self.conn is None: self.conn = self.connect() self.scp.get(file)
cliente.py

Nossa Grande Bela Escrita

Temos agora uma classe Python doente para lidar com SSH e SCP com um hospedeiro remoto… vamos pô-lo a trabalhar! O seguinte trecho é uma forma rápida de testar o que construímos até agora. Em suma, este script procura uma pasta local cheia de ficheiros (no meu caso, preenchi a pasta com fox gifs 🦊).

Verifica como é fácil criar um main.py que lide com tarefas complexas em máquinas remotas graças à nossa classe RemoteClient:

"""Perform tasks against a remote host."""from config import ( host, user, ssh_key_filepath, local_file_directory, remote_path)from .files import fetch_local_filesfrom .client import RemoteClientdef main(): """Initialize remote host client and execute actions.""" remote = RemoteClient(host, user, ssh_key_filepath, remote_path) upload_files_to_remote(remote) execute_command_on_remote(remote) remote.disconnect()def upload_files_to_remote(remote): """Upload files to remote via SCP.""" local_files = fetch_local_files(local_file_directory) remote.bulk_upload(local_files)def execute_command_on_remote(remote): """Execute UNIX command on the remote host.""" remote.execute_cmd('cd /var/www/ghost ls')
__init__.py

aqui está a saída da nossa função de carregamento:

2020-01-03 00:00:27.215 | paramiko_tutorial.client:__upload_single_file:85 - Uploaded data/fox1.gif to /uploads/2020-01-03 00:00:27.985 | paramiko_tutorial.client:__upload_single_file:85 - Uploaded data/fox2.gif to /uploads/2020-01-03 00:00:30.015 | paramiko_tutorial.client:__upload_single_file:85 - Uploaded data/fox3.gif to /uploads/2020-01-03 00:00:30.015 | paramiko_tutorial.client:bulk_upload:73 - Finished uploading 3 files to /uploads/ on 149.433.117.1425
Foxes uploaded successfully

Funcionou! Não acredita em mim? Porque não verificamos por nós próprios, correndo remote.execute_commands()?

2020-01-03 00:08:55.955 | paramiko_tutorial.client:execute_commands:96 - INPUT: cd /uploads/ && ls | OUTPUT: fox1.gif2020-01-03 00:08:55.955 | paramiko_tutorial.client:execute_commands:96 - INPUT: cd /uploads/ && ls | OUTPUT: fox2.gif2020-01-03 00:08:55.956 | paramiko_tutorial.client:execute_commands:96 - INPUT: cd /uploads/ && ls | OUTPUT: fox3.gif
Saída de cd /var/www/ && ls

p>a Directamente da boca da raposa.

Take It And Run With It

É aqui que gostaria de tirar um momento para agradecer a todos vós, e pedir desculpa por ainda estarem aqui. Fiz um juramento a mim mesmo para parar de colocar tutoriais com mais de duas mil palavras, e este está à procura de empurrar cinco mil palavras de disparate. Vou trabalhar nesse sentido. Ano novo, novo me.

Para sua conveniência, carreguei a fonte deste tutorial no Github. Sinta-se à vontade para pegar nisto e correr com ele! Para encerrar, deixo-vos com a carne e as batatas da classe Cliente que reunimos:

"""Client to handle connections and actions executed against a remote host."""from os import systemfrom paramiko import SSHClient, AutoAddPolicy, RSAKeyfrom paramiko.auth_handler import AuthenticationException, SSHExceptionfrom scp import SCPClient, SCPExceptionfrom .log import loggerclass RemoteClient: """Client to interact with a remote host via SSH & SCP.""" def __init__(self, host, user, ssh_key_filepath, remote_path): self.host = host self.user = user self.ssh_key_filepath = ssh_key_filepath self.remote_path = remote_path self.client = None self.scp = None self.conn = None self._upload_ssh_key() @logger.catch def _get_ssh_key(self): """ Fetch locally stored SSH key.""" try: self.ssh_key = RSAKey.from_private_key_file(self.ssh_key_filepath) logger.info(f'Found SSH key at self {self.ssh_key_filepath}') except SSHException as error: logger.error(error) return self.ssh_key @logger.catch def _upload_ssh_key(self): try: system(f'ssh-copy-id -i {self.ssh_key_filepath} {self.user}@{self.host}>/dev/null 2>&1') system(f'ssh-copy-id -i {self.ssh_key_filepath}.pub {self.user}@{self.host}>/dev/null 2>&1') logger.info(f'{self.ssh_key_filepath} uploaded to {self.host}') except FileNotFoundError as error: logger.error(error) @logger.catch def _connect(self): """Open connection to remote host. """ if self.conn is None: try: self.client = SSHClient() self.client.load_system_host_keys() self.client.set_missing_host_key_policy(AutoAddPolicy()) self.client.connect( self.host, username=self.user, key_filename=self.ssh_key_filepath, look_for_keys=True, timeout=5000 ) self.scp = SCPClient(self.client.get_transport()) except AuthenticationException as error: logger.error(f'Authentication failed: did you remember to create an SSH key? {error}') raise error return self.client def disconnect(self): """Close ssh connection.""" if self.client: self.client.close() if self.scp: self.scp.close() @logger.catch def bulk_upload(self, files): """ Upload multiple files to a remote directory. :param files: List of paths to local files. :type files: List """ self.conn = self._connect() uploads = logger.info(f'Finished uploading {len(uploads)} files to {self.remote_path} on {self.host}') def _upload_single_file(self, file): """Upload a single file to a remote directory.""" upload = None try: self.scp.put( file, recursive=True, remote_path=self.remote_path ) upload = file except SCPException as error: logger.error(error) raise error finally: logger.info(f'Uploaded {file} to {self.remote_path}') return upload def download_file(self, file): """Download file from remote host.""" self.conn = self._connect() self.scp.get(file) @logger.catch def execute_commands(self, commands): """ Execute multiple commands in succession. :param commands: List of unix commands as strings. :type commands: List """ self.conn = self._connect() for cmd in commands: stdin, stdout, stderr = self.client.exec_command(cmd) stdout.channel.recv_exit_status() response = stdout.readlines() for line in response: logger.info(f'INPUT: {cmd} | OUTPUT: {line}')
client.py

O código fonte completo para este tutorial pode ser encontrado aqui:

hackersandslackers/paramiko-tutorial
📡🐍SSH & SCP em Python com Paramiko. Contribuir para o desenvolvimento do hackersandslackers/paramiko-tutorial através da criação de uma conta no GitHub.
GitHubhackersandslackers

Todd Birchard's avatar's avatar
Todd Birchard 123 Posts
New York City

SiteGithubTwitter

Engineer com uma crise de identidade em curso. Quebra tudo antes de aprender as melhores práticas. Completamente normal e emocionalmente estável.

Deixe uma resposta

O seu endereço de email não será publicado. Campos obrigatórios marcados com *