Usando ssh e sed em um script python com os.system corretamente
Estou tentando executar um comando ssh dentro de um script python usando os.systempara adicionar um 0no final de uma string totalmente correspondente em um servidor remoto usando sshe sed.
Eu tenho um arquivo chamado nodelistem um servidor remoto que é uma lista semelhante a esta.
test-node-1
test-node-2
...
test-node-11
test-node-12
test-node-13
...
test-node-21
Quero usar o sed para fazer a seguinte modificação, quero pesquisar test-node-1e, quando uma correspondência completa for encontrada, quero adicionar um 0 no final, o arquivo deve ficar assim.
test-node-1 0
test-node-2
...
test-node-11
test-node-12
test-node-13
...
test-node-21
No entanto, quando executo o primeiro 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\"")
O resultado fica assim,
test-node-1 0
test-node-2
...
test-node-11 0
test-node-12 0
test-node-13 0
...
test-node-21
Tentei adicionar a \ b ao comando assim,
os.system(f"ssh -i ~/.ssh/my-ssh-key username@serverlocation \"sed -i '/\b{hostname}\b/s/$/ 0/' ~/{function}.txt\"")
O comando não funciona de todo.
Tenho que digitar manualmente o nome do nó em vez de usar uma variável como esta,
os.system(f"ssh -i ~/.ssh/my-ssh-key username@serverlocation \"sed -i '/\btest-node-1\b/s/$/ 0/' ~/{function}.txt\"")
para fazer meu comando funcionar.
O que há de errado com meu comando, por que não posso fazer o que quero?
Respostas
Este código tem sérios problemas de segurança; consertá-los requer uma reengenharia do zero. Vamos fazer isso aqui:
#!/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 você executar isso (exceto para subprocess.runno final, que exigiria uma chave SSH real, nome de host, etc), a saída será semelhante 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'
Saída correta / desejada; a '"'"'expressão engraçada é como se injeta com segurança uma aspa simples literal dentro de uma string entre aspas simples em um shell compatível com POSIX.
O que é diferente? Grande quantidade:
- Estamos gerando os comandos que queremos executar como arrays e permitindo que o Python faça o trabalho de converter esses arrays em strings quando necessário. Isso evita ataques de injeção de shell, uma classe muito comum de vulnerabilidade de segurança.
- Como estamos gerando listas nós mesmos, podemos mudar a forma como citamos cada uma: podemos usar strings f quando for apropriado, strings brutas quando for apropriado, etc.
- Não estamos passando
~para o servidor remoto: é redundante e desnecessário porque~é o local padrão para iniciar uma sessão SSH; e as precauções de segurança que estamos usando (para evitar que valores sejam analisados como código por um shell) evitam que tenha qualquer efeito (já que a substituição de~pelo valor ativo deHOMEnão é feita porsedsi só, mas pelo shell que o invoca ; porque não estamos chamando nenhum shell local, também precisamos usaros.path.expanduserpara fazer com que o~in~/.ssh/my-ssh-keyseja respeitado). - Como não estamos usando uma string bruta, precisamos dobrar as barras invertidas
\bpara garantir que sejam tratadas como literais em vez de sintáticas pelo Python. - De maneira crítica, nunca estamos transmitindo dados em um contexto em que possam ser analisados como código por qualquer shell, local ou remoto .