Utiliser correctement ssh et sed dans un script python avec os.system
J'essaie d'exécuter une commande ssh dans un script python en utilisant os.systempour ajouter un 0à la fin d'une chaîne entièrement assortie dans un serveur distant en utilisant sshet sed.
J'ai un fichier appelé nodelistdans un serveur distant qui est une liste qui ressemble à ceci.
test-node-1
test-node-2
...
test-node-11
test-node-12
test-node-13
...
test-node-21
Je veux utiliser sed pour apporter la modification suivante, je veux rechercher test-node-1, et quand une correspondance complète est trouvée, je veux ajouter un 0 à la fin, le fichier doit finir par ressembler à ceci.
test-node-1 0
test-node-2
...
test-node-11
test-node-12
test-node-13
...
test-node-21
Cependant, lorsque j'exécute la première commande,
hostname = 'test-node-1'
function = 'nodelist'
os.system(f"ssh -i ~/.ssh/my-ssh-key username@serverlocation \"sed -i '/{hostname}/s/$/ 0/' ~/{function}.txt\"")
Le résultat devient comme ça,
test-node-1 0
test-node-2
...
test-node-11 0
test-node-12 0
test-node-13 0
...
test-node-21
J'ai essayé d'ajouter un \ b à la commande comme ceci,
os.system(f"ssh -i ~/.ssh/my-ssh-key username@serverlocation \"sed -i '/\b{hostname}\b/s/$/ 0/' ~/{function}.txt\"")
La commande ne fonctionne pas du tout.
Je dois taper manuellement le nom du nœud au lieu d'utiliser une variable comme celle-ci,
os.system(f"ssh -i ~/.ssh/my-ssh-key username@serverlocation \"sed -i '/\btest-node-1\b/s/$/ 0/' ~/{function}.txt\"")
pour faire fonctionner ma commande.
Quel est le problème avec ma commande, pourquoi ne puis-je pas faire ce que je veux qu'elle fasse?
Réponses
Ce code présente de sérieux problèmes de sécurité; les réparer nécessite de le repenser à partir de zéro. Faisons ça ici:
#!/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
Si vous exécutez ceci (sauf pour le subprocess.runà la fin, qui nécessiterait une vraie clé SSH, un nom d'hôte, etc.), la sortie ressemble à:
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'
C'est la sortie correcte / souhaitée; l' '"'"'idiome amusant est la façon dont on injecte en toute sécurité un guillemet simple littéral dans une chaîne entre guillemets simples dans un shell compatible POSIX.
Qu'est ce qui est different? Beaucoup:
- Nous générons les commandes que nous voulons exécuter en tant que tableaux et laissons Python faire le travail de conversion de ces tableaux en chaînes si nécessaire. Cela évite les attaques par injection shell, une classe très courante de vulnérabilité de sécurité.
- Parce que nous générons nous-mêmes des listes, nous pouvons changer la façon dont nous citons chacune d'elles: nous pouvons utiliser des f-strings quand c'est approprié de le faire, des chaînes brutes quand c'est approprié, etc.
- Nous ne passons pas
~au serveur distant: c'est redondant et inutile car~c'est l'endroit par défaut pour qu'une session SSH démarre; et les précautions de sécurité que nous utilisons (pour empêcher que les valeurs soient analysées comme du code par un shell) l'empêchent d'avoir un effet (car le remplacement de~par la valeur active deHOMEn'est pas effectué parsedlui-même, mais par le shell qui l'invoque ; parce que nous n'invoquons aucun shell local, nous devions également l'utiliseros.path.expanduserpour que le~in~/.ssh/my-ssh-keysoit honoré) - Comme nous n'utilisons pas de chaîne brute, nous devons doubler les barres obliques inverses
\bpour nous assurer qu'elles sont traitées comme littérales plutôt que syntaxiques par Python. - De manière critique, nous ne transmettons jamais de données dans un contexte où elles pourraient être analysées en tant que code par n'importe quel shell, local ou distant .