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:
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:
Questo ci chiederà di fornire un nome alla nostra chiave. Nominatela come preferite:
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
:
Verificare la nostra chiave SSH
Se volete controllare quali chiavi avete già, queste possono essere trovate nella .ssh
directory del tuo sistema:
Siamo alla ricerca di chiavi che iniziano con la seguente intestazione:
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:
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:
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:
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:
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 comeconn
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:
_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:
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:
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 seguente1
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:
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 specificareAutoAddPolicy()
. 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 metodoconnect()
permette una tonnellata di flessibilità attraverso una vasta gamma di argomenti opzionali. Mi è capitato di passarne alcuni qui: impostare look_for_keys aTrue
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à:
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:
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:
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()
:
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:
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()
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()
:
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:
Ecco l’output della nostra funzione di upload:
Ha funzionato! Non mi credete? Perché non controlliamo noi stessi eseguendo remote.execute_commands()
?
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:
Il codice sorgente completo per questo tutorial può essere trovato qui:
SiteGithubTwitter
Ingegnere con una continua crisi di identità. Rompe tutto prima di imparare le migliori pratiche. Completamente normale ed emotivamente stabile.