Articles

SSH & SCP in Python con Paramiko

Posted on
19 min read
January 03

SSH SCP in Python con Paramiko

I fornitori di cloud hanno fatto una fortuna con servizi gestiti ben confezionati per anni. Che si tratti di database o di message broker, gli sviluppatori come noi non sembrano avere problemi a pagare un po’ di più per avere le cose sotto controllo. Ma aspettate, non siamo di solito le ultime persone che optano per meno ottimizzazione e meno controllo? Perché questo è il momento in cui decidiamo diversamente? Se dovessi fare un’ipotesi, scommetterei che è in parte perché il DevOps lato server fa un po’ schifo.

Come sviluppatore, configurare o fare il debug di un VPS è di solito un lavoro che non viene considerato, e non è particolarmente gratificante. Nel migliore dei casi, la vostra applicazione finirà probabilmente per funzionare come il vostro ambiente locale. Come potremmo migliorare questa parte inevitabile del nostro lavoro? Beh, potremmo automatizzarla.

Paramiko e SCP sono due librerie Python che possiamo usare insieme per automatizzare i compiti che vorremmo eseguire su un host remoto, come riavviare i servizi, fare aggiornamenti o prendere i file di log. Daremo un’occhiata a come appare lo scripting con queste librerie. Avviso: c’è una notevole quantità di codice in questo tutorial, il che tende a rendermi abbastanza eccitato da costringere gli altri nei miei episodi maniacali di tutorial sul codice. Se iniziate a sentirvi persi, il repo completo può essere trovato qui:

hackersandslackers/paramiko-tutorial
📡🐍SSH & SCP in Python con Paramiko. Contribuisci allo sviluppo di hackersandslackers/paramiko-tutorial creando un account su GitHub.

GitHubhackersandslackers

Paramiko punta molto sul lato “in-the-weeds” delle librerie Python. Se state cercando qualcosa di facile che possa semplicemente fare il lavoro, pyinfra è presumibilmente una grande (e facile) alternativa.

Impostare le chiavi SSH

Per autenticare una connessione SSH, dobbiamo impostare una chiave privata RSA SSH (da non confondere con OpenSSH). Possiamo generare una chiave usando il seguente comando:

$ ssh-keygen -t rsa
Genera una chiave RSA

Questo ci chiederà di fornire un nome alla nostra chiave. Nominatela come preferite:

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

In seguito, vi verrà richiesto di fornire una password (sentitevi liberi di lasciarla in bianco).

Ora che abbiamo la nostra chiave, dobbiamo copiarla sul nostro host remoto. Il modo più semplice per farlo è usando ssh-copy-id:

$ ssh-copy-id -i ~/.ssh/mykey username@my_remote_host.org
Copia la chiave sull’host remoto

Verificare la nostra chiave SSH

Se volete controllare quali chiavi avete già, queste possono essere trovate nella .ssh directory del tuo sistema:

$ cd ~/.ssh
Controlla la directory /.ssh directory

Siamo alla ricerca di chiavi che iniziano con la seguente intestazione:

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

Siate liberi di fare lo stesso sul vostro FPS.

Avviare il nostro script

Installiamo le nostre librerie. Accendi l’ambiente virtuale che preferisci e lasciali andare:

$ pip3 install paramiko scp
Installiamo paramiko & scp

Un’ultima cosa prima di scrivere del codice Python significativo! Creare un file di configurazione per contenere le variabili di cui avremo bisogno per connetterci al nostro host. Ecco le basi di ciò che ci serve per entrare nel nostro server:

  • Host: L’indirizzo IP o l’URL dell’host remoto a cui stiamo cercando di accedere.
  • Nome utente: Questo è il nome utente che si usa per accedere al server SSH.
  • Passphrase (opzionale): Se avete specificato una passphrase quando avete creato la vostra chiave ssh, specificatela qui. Ricorda che la passphrase della tua chiave SSH non è la stessa della password del tuo utente.
  • Chiave SSH: Il percorso del file della chiave che abbiamo creato in precedenza. Su OSX, queste si trovano nella cartella ~/.ssh del tuo sistema. La chiave SSH che stiamo prendendo di mira deve avere una chiave di accompagnamento con estensione .pub. Questa è la nostra chiave pubblica; se hai seguito in precedenza, dovrebbe essere già stata generata per te.

Se stai cercando di caricare o scaricare file dal tuo host remoto, dovrai includere altre due variabili:

  • Remote Path: Il percorso della cartella remota che stiamo cercando di indirizzare per i trasferimenti di file. Possiamo sia caricare cose in questa cartella che scaricarne il contenuto.
  • Percorso locale: Stessa idea di cui sopra, ma al contrario. Per nostra comodità, il percorso locale che useremo è semplicemente /data, e contiene immagini di simpatiche gif di volpi.

Ora abbiamo tutto il necessario per creare un file config.py rispettabile:

"""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

Creazione di un client SSH

Creeremo una classe chiamata RemoteClient per gestire le interazioni che avremo con il nostro host remoto. Prima di diventare troppo sofisticati, iniziamo istanziando la classe RemoteClient con le variabili che abbiamo creato in 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

Niente di impressionante finora: abbiamo solo impostato alcune variabili e passate in una classe inutile. Facciamo un salto di qualità senza lasciare il nostro costruttore:

"""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

Abbiamo aggiunto tre cose nuove da istanziare con la nostra classe:

  • self.client: self.client servirà in definitiva come oggetto di connessione nella nostra classe, in modo simile a come avete trattato la terminologia come conn nelle librerie di database. La nostra connessione sarà None finché non ci connetteremo esplicitamente al nostro host remoto.
  • self.scp: simile a self.client, ma gestisce esclusivamente le connessioni per il trasferimento di file.
  • self.__upload_ssh_key() non è una variabile, ma piuttosto una funzione da eseguire automaticamente ogni volta che il nostro client viene istanziato. Chiamare __upload_ssh_key() sta dicendo al nostro oggetto RemoteClient di controllare le chiavi ssh locali immediatamente dopo la creazione in modo da poterle passare al nostro host remoto. Altrimenti, non saremmo affatto in grado di stabilire una connessione.

Scaricare le chiavi SSH ad un host remoto

Abbiamo raggiunto la sezione di questo esercizio dove abbiamo bisogno di buttare giù un po’ di devastante e inglorioso codice boilerplate. Questo è tipicamente il punto in cui individui emotivamente inferiori soccombono alla pura oscurità noiosa di comprendere le chiavi SSH e mantenere le connessioni. Non commettete errori: autenticare e gestire le connessioni a qualsiasi cosa programmaticamente è estremamente noioso… a meno che la vostra guida turistica non sia un incantevole scrittore di parole, che serve come vostro amorevole protettore attraverso una pericolosa oscurità. Alcune persone chiamano questo post un tutorial. Io intendo chiamarlo arte.

RemoteClient inizierà con due metodi privati: __get_ssh_key() e __upload_ssh_key(). Il primo recupererà una chiave pubblica memorizzata localmente e, in caso di successo, il secondo consegnerà questa chiave pubblica al nostro host remoto come ramo d’ulivo di accesso. Una volta che una chiave pubblica creata localmente esiste su una macchina remota, quella macchina si fiderà per sempre delle nostre richieste di connessione: non servono password. Includeremo un’adeguata registrazione lungo la strada, nel caso in cui ci imbattessimo in qualche 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() è abbastanza semplice: verifica che esista una chiave SSH nel percorso che abbiamo specificato nella nostra configurazione da utilizzare per la connessione al nostro host. Se il file esiste, impostiamo felicemente la nostra variabile self.ssh_key, così questa chiave può essere caricata e utilizzata dal nostro client da qui in avanti. Paramiko ci fornisce un sottomodulo chiamato RSAKey per gestire facilmente tutte le cose relative alle chiavi RSA, come l’analisi di un file di chiave privata in un’autenticazione di connessione utilizzabile. Ecco cosa otteniamo qui:

 RSAKey.from_private_key_file(self.ssh_key_filepath)
Lettura chiave RSA da file locale.

Se la nostra chiave RSA fosse una incomprensibile sciocchezza invece di una vera chiave, la SSHException di Paramiko l’avrebbe colta e avrebbe sollevato un’eccezione all’inizio spiegando proprio questo. Utilizzare correttamente la gestione degli errori di una libreria toglie un sacco di congetture su “cosa è andato storto”, specialmente in casi come quello in cui ci sono potenzialmente numerose incognite in uno spazio di nicchia con cui nessuno di noi scherza spesso.

_upload_ssh_key() è dove possiamo infilare la nostra chiave SSH nella gola del nostro server remoto mentre gridiamo: “LOOK! ORA PUOI FIDARTI DI ME PER SEMPRE!” Per fare questo, vado un po’ alla “vecchia scuola” passando i comandi bash attraverso il os.system di Python. A meno che qualcuno non mi metta al corrente di un approccio più pulito nei commenti, assumerò che questo sia il modo più cazzuto di gestire il passaggio di chiavi ad un server remoto.

Il modo standard non Python di passare le chiavi ad un host assomiglia a questo:

ssh-copy-id -i ~/.ssh/mykey user@host
Passa la chiave SSH all’host remoto

Questo è esattamente ciò che realizziamo nella nostra funzione in Python, che si presenta così:

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

Immagino che non mi lascerete sfuggire questo /dev/null 2>&1 pezzo? Bene. Se volete saperlo, ecco un tizio su StackOverflow che lo spiega meglio di me:

> è per il redirect /dev/null è un buco nero dove qualsiasi dato inviato, verrà scartato. 2 è il descrittore di file per l’errore standard. > è per il redirect. & è il simbolo del descrittore di file (senza di esso, il seguente 1 sarebbe considerato un nome di file). 1 è il descrittore di file per Standard O.

Quindi stiamo fondamentalmente dicendo al nostro server remoto che gli stiamo dando qualcosa, e lui è tutto un “dove metto questa cosa”, a cui noi rispondiamo “da nessuna parte nello spazio fisico, poiché questo non è un oggetto, ma piuttosto un simbolo eterno della nostra amicizia. Il nostro ospite remoto è allora inondato di gratitudine ed emozione, perché sì, i computer hanno emozioni, ma non possiamo essere disturbati da questo adesso.

Connettersi al nostro client

Aggiungeremo un metodo al nostro client chiamato connect() per gestire la connessione al nostro host:

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

Distruggiamo questo:

  • client = SSHClient() imposta la fase di creazione di un oggetto che rappresenta il nostro client SSH. Le righe seguenti configureranno questo oggetto per renderlo più utile.
  • load_system_host_keys() istruisce il nostro client a cercare tutti gli host a cui ci siamo connessi in passato guardando il file known_hosts del nostro sistema e trovando le chiavi SSH che il nostro host si aspetta. Non ci siamo mai connessi al nostro host in passato, quindi abbiamo bisogno di specificare esplicitamente la nostra chiave SSH.
  • set_missing_host_key_policy() dice a Paramiko cosa fare in caso di una coppia di chiavi sconosciute. Questo si aspetta una “policy” incorporata in Paramiko, alla quale andremo a specificare AutoAddPolicy(). Impostare la nostra politica su “auto-add” significa che se tentiamo di connetterci ad un host non riconosciuto, Paramiko aggiungerà automaticamente la chiave mancante localmente.
  • connect() è il metodo più importante di SSHClient (come si può immaginare). Siamo finalmente in grado di passare il nostro host, utente e chiave SSH per ottenere ciò che tutti stavamo aspettando: una gloriosa connessione SSH al nostro server! Il metodo connect() permette una tonnellata di flessibilità attraverso una vasta gamma di argomenti opzionali. Mi è capitato di passarne alcuni qui: impostare look_for_keys a True dà a Paramiko il permesso di cercare nella nostra cartella ~/.ssh per scoprire le chiavi SSH da solo, e impostare timeout chiuderà automaticamente le connessioni che probabilmente dimenticheremo di chiudere. Potremmo anche passare variabili per cose come la porta e la password, se avessimo scelto di connetterci al nostro host in questo modo.

Disconnessione

Dovremmo chiudere le connessioni al nostro host remoto quando abbiamo finito di usarle. Non farlo potrebbe non essere necessariamente disastroso, ma ho avuto alcuni casi in cui un numero sufficiente di connessioni sospese avrebbe finito per massimizzare il traffico in entrata sulla porta 22. Indipendentemente dal fatto che il vostro caso d’uso possa considerare un riavvio come un disastro o un lieve inconveniente, chiudiamo le nostre dannate connessioni da adulti come se ci stessimo pulendo il sedere dopo aver fatto la cacca. Indipendentemente dall’igiene della vostra connessione, io sostengo l’impostazione di una variabile di timeout (come abbiamo visto prima). Comunque, voilà:

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

Fatto divertente: l’impostazione self.client.close() imposta effettivamente self.client all’uguale None, che è utile nei casi in cui si potrebbe voler controllare se una connessione è già aperta.

Eseguire i comandi Unix

Ora abbiamo una meravigliosa classe Python che può trovare chiavi RSA, connettersi e disconnettersi. Manca la capacità di fare, beh, qualcosa di utile.

Possiamo rimediare a questo e finalmente iniziare a fare “cose” con un nuovissimo metodo per eseguire comandi, che soprannominerò giustamente execute_commands() (esatto, “comandi” come potenzialmente più di uno, ne parleremo tra un momento). Il lavoro di tutto questo è fatto dal metodo exec_command() integrato nel client Paramiko, che accetta una singola stringa come comando e la esegue:

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

La funzione che abbiamo appena creato execute_commands() si aspetta una lista di stringhe da eseguire come comandi. Questo è in parte per comodità, ma è anche perché Paramiko non eseguirà alcun cambiamento di “stato” (come cambiare directory) tra i comandi, quindi ogni comando che passiamo a Paramiko dovrebbe assumere che stiamo lavorando dalla root del nostro server. Mi sono preso la libertà di passare tre comandi come segue:

remote.execute_commands()
__init__.py

Posso visualizzare il contenuto di una directory concatenando cd path/to/dir && ls, ma eseguendo cd path/to/dir seguito da ls si otterrebbe il nulla perché ls la seconda volta restituisce la lista dei file nella root del nostro server.

Si noterà che client.exec_command(cmd) restituisce tre valori invece di uno: questo può essere utile per vedere quale input ha prodotto quale output. Per esempio, ecco i log completi per l’esempio che ho fornito dove ho passato tre comandi a 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

Qualche bella cosa qui. Ora potete vedere quali siti sono sul mio server, quali bot mi stanno spammando, e quanti processi di nodo sto eseguendo.

Non voglio perdere altro tempo sull’arte di eseguire i comandi, ma vale la pena menzionare la presenza del perché chiamiamo stdout.channel.recv_exit_status() dopo ogni comando. Aspettare che recv_exit_status() ritorni dopo l’esecuzione di client.exec_command() costringe i nostri comandi ad essere eseguiti in modo sincrono, altrimenti è probabile che la nostra macchina remota non sia in grado di decifrare i comandi alla stessa velocità con cui li passiamo.

Scaricare (e scaricare) file via SCP

SCP si riferisce sia al protocollo per copiare file su macchine remote (secure copy protocol) sia alla libreria Python che lo utilizza. Abbiamo già installato la libreria SCP, quindi importa quella roba.

Le librerie SCP e Paramiko si completano a vicenda per rendere l’upload via SCP super facile. SCPClient() crea un oggetto che si aspetta il “trasporto” da Paramiko, che noi forniamo con self.conn.get_transport(). La creazione di una connessione SCP si appoggia al nostro client SSH in termini di sintassi, ma queste connessioni sono separate. È possibile chiudere una connessione SSH e lasciare aperta una connessione SCP, quindi non fatelo. Aprite una connessione SCP in questo modo:

self.scp = SCPClient(self.client.get_transport())
Aprite una connessione SCP.

Scaricare un singolo file è noioso, quindi carichiamo invece un’intera directory di file. bulk_upload() accetta una lista di percorsi di file, e poi chiama __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

Il nostro metodo si aspetta di ricevere due stringhe: la prima è il percorso locale del nostro file, e la seconda è il percorso della directory remota su cui vorremmo caricare.

Il metodo put() di SCP caricherà un file locale sul nostro host remoto. Questo sostituirà i file esistenti con lo stesso nome, se esistono nella destinazione che abbiamo specificato. Questo è tutto ciò che serve!

Scaricare i file

La controparte di SCP put() è il metodo get():

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)
client.py

Il nostro bello script

Ora abbiamo una classe Python malata per gestire SSH e SCP con un host remoto… mettiamola al lavoro! Il seguente snippet è un modo veloce per testare ciò che abbiamo costruito finora. In breve, questo script cerca una cartella locale piena di file (nel mio caso, ho riempito la cartella con le fox gif 🦊).

Guardate come è facile creare un main.py che gestisce compiti complessi su macchine remote grazie alla nostra 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

Ecco l’output della nostra funzione di upload:

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

Ha funzionato! Non mi credete? Perché non controlliamo noi stessi eseguendo 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
Output di cd /var/www/ && ls

Ecco fatto. Direttamente dalla bocca della volpe.

Take It And Run With It

Questo è il momento in cui vorrei prendermi un momento per ringraziare tutti voi, e scusarmi che siete ancora qui. Ho fatto un giuramento a me stesso di non pubblicare più tutorial lunghi più di duemila parole, e questo sta cercando di superare le cinquemila parole di sciocchezze. Ci lavorerò. Anno nuovo, io nuovo.

Per vostra comodità, ho caricato il sorgente di questo tutorial su Github. Sentitevi liberi di prenderlo e usarlo! Per concludere, vi lascio con la carne e le patate della classe Client che abbiamo messo insieme:

"""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

Il codice sorgente completo per questo tutorial può essere trovato qui:

hackersandslackers/paramiko-tutorial
📡🐍SSH & SCP in Python con Paramiko. Contribuisci allo sviluppo di hackersandslackers/paramiko-tutorial creando un account su GitHub.

GitHubhackersandslackers

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

SiteGithubTwitter

Ingegnere con una continua crisi di identità. Rompe tutto prima di imparare le migliori pratiche. Completamente normale ed emotivamente stabile.

Lascia un commento

Il tuo indirizzo email non sarà pubblicato. I campi obbligatori sono contrassegnati *