Usando ssh y sed dentro de un script de python con os.system correctamente

Sep 12 2020

Estoy tratando de ejecutar un comando ssh dentro de una secuencia de comandos de Python usando os.systempara agregar 0al final de una cadena totalmente coincidente en un servidor remoto usando sshy sed.

Tengo un archivo llamado nodelisten un servidor remoto que es una lista que se ve así.

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

Quiero usar sed para realizar la siguiente modificación, quiero buscar test-node-1, y cuando se encuentra una coincidencia completa quiero agregar un 0 al final, el archivo debe terminar luciendo así.

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

Sin embargo, cuando ejecuto el primer 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\"")

El resultado se vuelve así,

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

Intenté agregar a \ b al comando de esta manera,

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

El comando no funciona en absoluto.

Tengo que escribir manualmente el nombre del nodo en lugar de usar una variable como esa,

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

para hacer que mi comando funcione.

¿Qué pasa con mi comando? ¿Por qué no puedo hacer lo que quiero que haga?

Respuestas

5 CharlesDuffy Sep 13 2020 at 01:31

Este código tiene serios problemas de seguridad; arreglarlos requiere una reingeniería desde cero. Hagámoslo aquí:

#!/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 ejecuta esto (excepto subprocess.runal final, que requeriría una clave SSH real, nombre de host, etc.), la salida se ve así:

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'

Esa es la salida correcta / deseada; el '"'"'modismo divertido es cómo se inyecta de forma segura una comilla simple literal dentro de una cadena entre comillas simples en un shell compatible con POSIX.


¿Qué es diferente? Un montón:

  • Estamos generando los comandos que queremos ejecutar como matrices y dejando que Python haga el trabajo de convertir esas matrices en cadenas cuando sea necesario. Esto evita los ataques de inyección de shell, una clase muy común de vulnerabilidad de seguridad.
  • Debido a que estamos generando listas nosotros mismos, podemos cambiar la forma en que citamos cada una: podemos usar f-strings cuando sea apropiado hacerlo, strings sin procesar cuando sea apropiado, etc.
  • No estamos pasando ~al servidor remoto: es redundante e innecesario porque ~es el lugar predeterminado para que comience una sesión SSH; y las precauciones de seguridad que estamos usando (para evitar que los valores sean analizados como código por un shell) evitan que tenga algún efecto (ya que el reemplazo de ~por el valor activo de HOMEno se realiza por sedsí mismo, sino por el shell que lo invoca ; porque nosotros no estamos invocando cualquier shell local en todo, también necesitamos utilizar os.path.expanduserpara hacer que el ~de ~/.ssh/my-ssh-keyser honrado).
  • Debido a que no estamos usando una cadena sin formato, necesitamos duplicar las barras invertidas \bpara asegurarnos de que Python las trate como literales en lugar de sintácticas.
  • Fundamentalmente, nunca pasamos datos en un contexto en el que cualquier shell, ya sea local o remoto, pueda analizarlos como código .