SSH Verbindungen aufbauen, Befehle ausführen und protokollieren

Wenn du dir nicht sicher bist, in welchem der anderen Foren du die Frage stellen sollst, dann bist du hier im Forum für allgemeine Fragen sicher richtig.
Antworten
lithium213
User
Beiträge: 6
Registriert: Sonntag 8. März 2020, 10:40

Hallo zusammen,

ich bin zwar seit über 15 Jahren Informatiker, aber ein totaler Programmier-Neuling und möchte Folgendes erreichen:

Ich möchte ein Programm schreiben, welches sich auf verschiedenen Rechnern über SSH anmeldet, dort ein paar Befehle ausführt und das Ganze protokolliert.
Für einen Rechner (mit dem ganzen Code in einer Python Datei) funktioniert das schon recht gut, allerdings möchte ich die Zielrechner und die auszuführenden Befehle aus einer separaten Datei auslesen und habe aktuell mühe, die Befehle so einzulesen, dass diese korrekt ausgeführt werden.

Ich habe dazu folgende Dateien erstellt:

Hosts.txt (IP, Port)

Code: Alles auswählen

192.168.1.200,22
192.168.1.201,21021
192.168.2.10,21000
Commands.txt (Remote Befehle)

Code: Alles auswählen

hostname
cat /etc/hosts
ifconfig
connect.py (Die Funktion um via SSH zu verbinden, die Befehle abzusetzen und pro Verbindung eine Protokolldatei zu erstellen)

Code: Alles auswählen

import paramiko
from datetime import datetime

now = datetime.now()  # current date and time
timestamp = now.strftime("%m/%d/%Y, %H:%M:%S ")


def run_ssh(hostname, port, username, password, remote_commands):
    ssh = paramiko.SSHClient()
    ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
    ssh.connect(hostname=hostname, port=port, username=username, password=password)

    for command in remote_commands:
        stdin, stdout, stderr = ssh.exec_command(command)
        with open(hostname + ".log", "a") as logfile:
            logfile.write(timestamp + command + "\n")
        for line in stdout.readlines():
            with open(hostname + ".log", "a") as logfile:
                logfile.write(timestamp + line)
    print("Closing SSH Session")
    ssh.close()
run.py (das eigentliche Programm, welches die Zielrechner und die Befehle einlesen, und die Funktion "run_ssh" aus der Datei connect.py ausführen soll)

Code: Alles auswählen

import connect
import csv

with open("hosts.txt") as file:
    reader = csv.reader(file)
    targets = list(reader)

username = "user"
password = "password"
remote_commands = [
    "hostname",
    "cat /etc/hosts",
    "ifconfig",
]

for address in targets:
    hostname = (address[0])
    port = (address[1])
    print("Connecting to " + hostname + " on Port: " + port)
    connect.run_ssh(hostname, port, username, password, remote_commands)


So funktioniert es mit den Befehlen. Die Frage ist nun, wie ich die Variable remote_commands aus der Datei commands.txt einlesen muss, damit das sich gleich verhält, wie mit

Code: Alles auswählen

remote_commands = [
    "hostname",
    "cat /etc/hosts",
    "ifconfig",
]

Wenn ich das so versuche

Code: Alles auswählen

remote_commands=open("commands.txt","r")

print(remote_commands)
erhalte ich folgende Ausgabe:

Code: Alles auswählen

<_io.TextIOWrapper name='commands.txt' mode='r' encoding='cp1252'>
Auch wenn ihr andere Anregungen habt, bin ich froh um euren Input!
Danke und schöne Grüsse aus Zürich
__deets__
User
Beiträge: 14545
Registriert: Mittwoch 14. Oktober 2015, 14:29

Du musst nach dem öffnen ja auch noch lesen, damit du an den Inhalt kommst.

Und auf einer allgemeineren Ebene die Frage: solche Tools gibt es doch schon. Zb fabric, auch in Python geschrieben. Ansible, puppet, Chef etc. Wäre es nicht besser, die zu nutzen?
lithium213
User
Beiträge: 6
Registriert: Sonntag 8. März 2020, 10:40

__deets__ hat geschrieben: Sonntag 8. März 2020, 11:26 Du musst nach dem öffnen ja auch noch lesen, damit du an den Inhalt kommst.

Und auf einer allgemeineren Ebene die Frage: solche Tools gibt es doch schon. Zb fabric, auch in Python geschrieben. Ansible, puppet, Chef etc. Wäre es nicht besser, die zu nutzen?
Danke für die rasche Antwort! Die Tools schau ich mir gleich mal an, ich muss ja nicht das Rad neu erfinden. Trotzdem möchte ich mir Python aneignen.

Ich habe gerade festgestellt, dass mit

Code: Alles auswählen

commands_file = open("commands.txt")
remote_commands = commands_file.read()

print(remote_commands)
in der SSH Sitzung jeder Buchstabe einzeln ausgeführt wird... das sieht im Log dann so aus:

Code: Alles auswählen

03/08/2020, 11:42:17 h
03/08/2020, 11:42:17 o
03/08/2020, 11:42:17 s
03/08/2020, 11:42:17 t
03/08/2020, 11:42:17 n
03/08/2020, 11:42:17 a
03/08/2020, 11:42:17 m
03/08/2020, 11:42:17 e
03/08/2020, 11:42:17 
ich müsste die Befehle also noch in Anführungszeichen kriegen... weisst du da weiter?
nezzcarth
User
Beiträge: 1764
Registriert: Samstag 16. April 2011, 12:47

Ein Vorteil von Python ist der interaktive Interpreter. Wenn du dein Beispiel dort ausprobierst und dir den Inhalt von "remote_commands" ansiehst, siehst du sofort das Problem: Es ist ein einziger langer String.

Du solltest die Datei so einlesen:

Code: Alles auswählen

with open('commands.txt', 'r') as file: 
    remote_commands = [command.rstrip() for command in file] 
Für Quoting, etc.. kannst du dir mal das shlex Modul ansehen (https://docs.python.org/3/library/shlex.html).
Sirius3
User
Beiträge: 18272
Registriert: Sonntag 21. Oktober 2012, 17:20

Eine Datei run.py und connect.py sind zu viel. Das passt alles in eine Datei, die vielleicht auch noch einen sprechenden Namen bekommen könnte. Das was in run.py steht, sollte auch noch in Funktionen wandern. Strings setzt man nicht mit + zusammen, sondern benutzt Stringformatierung.

Code: Alles auswählen

for hostname, port in targets:
    print(f"Connecting to {hostname} on Port: {port}")
    connect.run_ssh(hostname, port, username, password, remote_commands)
now wird nur einmal beim Start ermittelt. Gerade bei vielen Hosts können schon einige Minuten vergehen, so dass die Zeit bis ein Command ausgeführt wird von der log-Zeit deutlich abweicht. Das Dateiformat sollte %Y-%m-%d sein, sonst kann man leicht Monat und Tag verwechseln.
lithium213
User
Beiträge: 6
Registriert: Sonntag 8. März 2020, 10:40

Danke für eure Inputs! Ich habe erst vor zwei Tagen mithilfe eines Youtube Tutorials mit Python angefangen, und bin sehr dankbar um euer Feedback!

Das "f" hier ist für die Stringformatierung notwendig, richtig?

Code: Alles auswählen

 print(f"Connecting to {hostname} on Port: {port}")
Ich habe nun alles in eine Datei gepackt. Die sieht jetzt so aus und funktioniert einwandfrei.
Gibt es noch anderes zu optimieren?

Code: Alles auswählen

import paramiko
import csv
from datetime import datetime


now = datetime.now()  # current date and time
timestamp = now.strftime("%Y-%m-%d, %H:%M:%S ")
username = "user"
password = "password"

with open("hosts.txt") as file:
    reader = csv.reader(file)
    targets = list(reader)

with open('commands.txt', 'r') as file:
    remote_commands = [command.rstrip() for command in file]

def run_ssh(hostname, port, username, password, remote_commands):
    ssh = paramiko.SSHClient()
    ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
    ssh.connect(hostname=hostname, port=port, username=username, password=password)

    for command in remote_commands:
        stdin, stdout, stderr = ssh.exec_command(command)
        with open(hostname + ".log", "a") as logfile:
            logfile.write(timestamp + command + "\n")
        for line in stdout.readlines():
            with open(hostname + ".log", "a") as logfile:
                logfile.write(timestamp + line)
    print("Closing SSH Session")
    ssh.close()


for hostname, port in targets:
    print(f"Connecting to {hostname} on Port: {port}")
    run_ssh(hostname, port, username, password, remote_commands)
__deets__
User
Beiträge: 14545
Registriert: Mittwoch 14. Oktober 2015, 14:29

Du solltest auf Modul-Ebene keinen ausgefuehrten Code stehen haben. Sondern das alles in Funktionen stecken, und die mit dem ueblichen

Code: Alles auswählen

if __name__ == '__main__':
     main() # die Funktion muss man natuerlich definieren
guard ganz am Ende schuetzen. Somit verhinderst du ungewolltes ausfuehren beim importieren und musst weniger Sorgfalt walten lassen bei Abhaengigkeiten - wuerde der Code im letzten Teil nach oben gestellt, waere run_ssh noch nicht definiert. Durch packen in Funktionen verhindert man sowas, behaelt mehr Uebersicht, etc.
lithium213
User
Beiträge: 6
Registriert: Sonntag 8. März 2020, 10:40

Also wenn ich das richtig verstanden habe, wäre es so korrekt.
Jetzt würde beim Import gar nichts passieren, auch das Einlesen der beiden Dateien gehört eigentlich in die "execute" Funktion... ?

Gruss

Code: Alles auswählen

import paramiko
import csv
from datetime import datetime


now = datetime.now()  # current date and time
timestamp = now.strftime("%Y-%m-%d, %H:%M:%S ")
username = "admin"
password = "password"



def run_ssh(hostname, port, username, password, remote_commands):
    ssh = paramiko.SSHClient()
    ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
    ssh.connect(hostname=hostname, port=port, username=username, password=password)

    for command in remote_commands:
        stdin, stdout, stderr = ssh.exec_command(command)
        with open(hostname + ".log", "a") as logfile:
            logfile.write(timestamp + command + "\n")
        for line in stdout.readlines():
            with open(hostname + ".log", "a") as logfile:
                logfile.write(timestamp + line)
    print("Closing SSH Session")
    ssh.close()

def execute():
    with open("hosts.txt") as file:
        reader = csv.reader(file)
        targets = list(reader)

    with open('commands.txt', 'r') as file:
        remote_commands = [command.rstrip() for command in file]

    for hostname, port in targets:
        print(f"Connecting to {hostname} on Port: {port}")
        run_ssh(hostname, port, username, password, remote_commands)

if __name__ == '__main__':
    execute()
Zuletzt geändert von lithium213 am Sonntag 8. März 2020, 16:14, insgesamt 1-mal geändert.
__deets__
User
Beiträge: 14545
Registriert: Mittwoch 14. Oktober 2015, 14:29

Das ist definitiv schon besser. Wobei ich username und password GROSS schreiben wuerde, weil es Konstanten sind. Und now und timestamp erstens in eine Zeile stecken, und zweitens nur an der Stelle wo sie auch gebraucht werden einmal berechnen wuerde. Ggf. in execute und dann als Argument uebergeben.
lithium213
User
Beiträge: 6
Registriert: Sonntag 8. März 2020, 10:40

Hallo zusammen,

Ich habe noch eine Frage zu meinem Programm oben. Verbindet das Programm für jeden via SSH auszuführenden Befehl neu, oder werden die Befehle so alle nacheinander und innerhalb einer SSH Sitzung ausgeführt?
Ich habe das Problem, dass ein Gerätetyp nach der Anmeldung noch nicht bereit ist und ich eine Verzögerung einbauen muss.

Vielen Dank und schöne Grüsse
__deets__
User
Beiträge: 14545
Registriert: Mittwoch 14. Oktober 2015, 14:29

Das ist mE alles in einer Session, darum heisst die ja so - und du machst nur eine auf.
Jankie
User
Beiträge: 592
Registriert: Mittwoch 26. September 2018, 14:06

Das hier passiert alles in einer Session:

Code: Alles auswählen

    for command in remote_commands:
        stdin, stdout, stderr = ssh.exec_command(command)
        with open(hostname + ".log", "a") as logfile:
            logfile.write(timestamp + command + "\n")
        for line in stdout.readlines():
            with open(hostname + ".log", "a") as logfile:
                logfile.write(timestamp + line)
    print("Closing SSH Session")
lithium213
User
Beiträge: 6
Registriert: Sonntag 8. März 2020, 10:40

Alles klar,
wo müsste ich dann die Verzögerung (time.sleep(5))einbauen, damit nach der Anmeldung über SSH zuerst eine Pause eingelegt wird, bevor die Befehle ausgeführt werden?
Sirius3
User
Beiträge: 18272
Registriert: Sonntag 21. Oktober 2012, 17:20

Nicht für jede Zeile die log-Datei neu öffnen:

Code: Alles auswählen

    with open(hostname + ".log", "a") as logfile:
        for command in remote_commands:
            stdin, stdout, stderr = ssh.exec_command(command)
            logfile.write(timestamp + command + "\n")
            for line in stdout:
                logfile.write(timestamp + line)
Jankie
User
Beiträge: 592
Registriert: Mittwoch 26. September 2018, 14:06

Zwischen das connect und der for-Schleife wo die Befehle ausgeführt werden.
Antworten