Usare correttamente ssh e sed all'interno di uno script python con os.system

Sep 12 2020

Sto cercando di eseguire un comando ssh all'interno di uno script python usando os.systemper aggiungere un 0alla fine di una stringa completamente abbinata in un server remoto usando sshe sed.

Ho un file chiamato nodelistin un server remoto che è un elenco simile a questo.

test-node-1
test-node-2
...
test-node-11
test-node-12
test-node-13
...
test-node-21

Voglio usare sed per apportare la seguente modifica, voglio cercare test-node-1, e quando viene trovata una corrispondenza completa voglio aggiungere uno 0 alla fine, il file deve avere questo aspetto.

test-node-1 0
test-node-2
...
test-node-11
test-node-12
test-node-13
...
test-node-21

Tuttavia, quando eseguo il primo comando,

hostname = 'test-node-1'
function = 'nodelist'

os.system(f"ssh -i ~/.ssh/my-ssh-key username@serverlocation \"sed -i '/{hostname}/s/$/ 0/' ~/{function}.txt\"")

Il risultato diventa così,

test-node-1 0
test-node-2
...
test-node-11 0
test-node-12 0
test-node-13 0
...
test-node-21

Ho provato ad aggiungere un \ b al comando in questo modo,

os.system(f"ssh -i ~/.ssh/my-ssh-key username@serverlocation \"sed -i '/\b{hostname}\b/s/$/ 0/' ~/{function}.txt\"")

Il comando non funziona affatto.

Devo digitare manualmente il nome del nodo invece di usare una variabile in questo modo,

os.system(f"ssh -i ~/.ssh/my-ssh-key username@serverlocation \"sed -i '/\btest-node-1\b/s/$/ 0/' ~/{function}.txt\"")

per far funzionare il mio comando.

Cosa c'è di sbagliato nel mio comando, perché non posso fare quello che voglio che faccia?

Risposte

5 CharlesDuffy Sep 13 2020 at 01:31

Questo codice presenta seri problemi di sicurezza; risolverli richiede una riprogettazione da zero. Facciamolo qui:

#!/usr/bin/env python3
import os.path
import shlex  # note, quote is only here in Python 3.x; in 2.x it was in the pipes module
import subprocess
import sys

# can set these from a loop if you choose, of course
username = "whoever"
serverlocation = "whereever"
hostname = 'test-node-1'
function = 'somename'

desired_cmd = ['sed', '-i',
               f'/\\b{hostname}\\b/s/$/ 0/',
               f'{function}.txt']
desired_cmd_str = ' '.join(shlex.quote(word) for word in desired_cmd)
print(f"Remote command: {desired_cmd_str}", file=sys.stderr)

# could just pass the below direct to subprocess.run, but let's log what we're doing:
ssh_cmd = ['ssh', '-i', os.path.expanduser('~/.ssh/my-ssh-key'),
           f"{username}@{serverlocation}", desired_cmd_str]
ssh_cmd_str = ' '.join(shlex.quote(word) for word in ssh_cmd)
print(f"Local command: {ssh_cmd_str}", file=sys.stderr)  # log equivalent shell command
subprocess.run(ssh_cmd) # but locally, run without a shell

Se lo esegui (ad eccezione di subprocess.runalla fine, che richiederebbe una chiave SSH reale, un nome host, ecc.), L'output sarà simile a:

Remote command: sed -i '/\btest-node-1\b/s/$/ 0/' somename.txt
Local command: ssh -i /home/yourname/.ssh/my-ssh-key whoever@whereever 'sed -i '"'"'/\btest-node-1\b/s/$/ 0/'"'"' somename.txt'

Questo è l'output corretto / desiderato; l' '"'"'idioma divertente è come si inserisce in modo sicuro una virgoletta singola letterale all'interno di una stringa con virgolette singole in una shell conforme a POSIX.


Cosa c'è di diverso? Molte:

  • Stiamo generando i comandi che vogliamo eseguire come array e lasciando che Python si occupi della conversione di quegli array in stringhe dove necessario. Ciò evita attacchi di shell injection, una classe molto comune di vulnerabilità di sicurezza.
  • Poiché stiamo generando elenchi noi stessi, possiamo cambiare il modo in cui citiamo ciascuno di essi: possiamo usare stringhe f quando è appropriato farlo, stringhe grezze quando è appropriato, ecc.
  • Non stiamo passando ~al server remoto: è ridondante e non necessario perché ~è il luogo predefinito per l'avvio di una sessione SSH; e le precauzioni di sicurezza che stiamo usando (per evitare che i valori vengano analizzati come codice da una shell) impediscono che abbia alcun effetto (poiché la sostituzione di ~con il valore attivo di HOMEnon viene eseguita da sedsola, ma dalla shell che lo invoca ; poiché non stiamo invocando alcuna shell locale, abbiamo anche bisogno di usare os.path.expanduserper far sì che la ~in ~/.ssh/my-ssh-keysia onorata).
  • Poiché non stiamo usando una stringa grezza, dobbiamo raddoppiare le barre rovesciate \bper assicurarci che siano trattate come letterali piuttosto che sintattiche da Python.
  • Fondamentalmente, non stiamo mai passando i dati in un contesto in cui potrebbero essere analizzati come codice da qualsiasi shell, locale o remota .