Samstag, 7. Juli 2018

Etwas Powershell für ssh

Alle Hostverbindungen sind in lnk Dateien definiert und jetzt wäre es cool wenn man diese Definitionen vielfach weiterverwendet.
In Ergänzung zu meinem letzten Artikel will ich hier ein paar Bausteine aufzeigen und die Gedanken dahinter und den Syntax kurz erklären.

Zunächst mal ein paar grundlegende Variablen und Objekte damit der Code universell bleibt.
# Varianten für Zielpfad
#$HomePath = "$env:HOMEDRIVE$env:HOMEPATH\Desktop"
$HomePath = "$env:onedrive\ssh Hosts"
# Filter
$Filter = "*.lnk"
$FArg = "@"

# Shell Object zum Zugriff auf Eigenschaften von lnk
$shell = New-Object -COM WScript.Shell
Kurz und kompakt ist die Variante mit konsequenter Ausnutzung von Pipes.

  • GCI liefert alle Dateien mit der Endung lnk in einem Pfad.
  • Für jedes Element wird ein Shell Objekt erzeugt und im Element Arguments nochmal gefiltert. Im Beispiel auf das Zeichen @ um wirklich nur die lnk Dateien mit user@host zu behandeln. Im Element Targetpath steht der Befehl komplett mit Pfad.
  • Für alle gefilterten Elemente wird ein neuer Prozess mit cmd gestartet und einfach das Kommando aus der lnk Datei aufgeführt.

Ergo werden alle vorhandenen "ssh" Hostdefinitionen in separaten Fenstern geöffnet.
# Variante mit Pipes
Get-childItem $HomePath -filter $Filter |
  ForEach-Object {$shell.CreateShortcut($_.FullName)}|where Arguments -match $FArg|
    ForEach-Object {
       Start-Process "cmd.exe" "/c $($_.Targetpath) $($_.Arguments)"
    }
Die beiden Elemente Targetpath und Arguments liegen als eine Art Objectreferenz vor. Damit wirklich der Inhalt als String verwendet wird muss dies im Process Aufruf mit $() aufgelöst werden.
Es geht auch ohne Pipes, dann werden alle Übergänge in Variablen gespeichert, der Ablauf ist gleich.
# Variante ohne Pipes
$Files = Get-childItem $HomePath -filter $Filter
ForEach ($File in $Files) {
   $lnkfile = $shell.CreateShortcut($File.FullName) 
   if ($($lnkfile.Arguments) -match $FArg ) {
      Start-Process "cmd.exe" "/c $($lnkfile.Targetpath) $($lnkfile.Arguments)"
   }
}
Für andere Fälle kann man auch einfach ein Array mit Hosts erstellen und lässt eine ForEach Schleife über dieses Array laufen.
$Hosts = @("192.168.178.80","192.168.178.81","192.168.178.82","192.168.178.83")
ForEach ($h in $Hosts) {}
Um den Code leserlich und modular zu halten, kann man zu Beginn alles in Variablen packen. Die Unterbringung von Befehlssquenzen in Variablen ist nicht ganz trivial. Damit man gut testen kann und nicht immer den ganzen Code verändern muss, packe ich  auch das erste cmd Argument in eine String Variable $cs.
#Absoluter Pfad zum Public Key
$Keyfile = "$env:HOMEDRIVE$env:HOMEPATH\.ssh\id_rsa.pub"
$cs = "/C "  # Schließt das cmd Fenster nach Abarbeitung
#$cs = "/K "  # Lässt das cmd Fenster offen - nur für Tests wichtig!
$sshTarget = "pi@$h"
Die eigentliche Codezeile ( Ersatz ssh-copy-id) enthält zwei in sich geschachtelte Argumentelisten. Zunächst der Fenstertyp für cmd.exe in $cs.
Die Kombination aus $cs, $C1 und $sshTarget wäre für sich ausführbar, Ergebnis ist eine Shellkonsole.
Danach kommt die Argumentenliste (Befehle) die in der Shellkonsole ausgeführt werden sollen. Diese muss komplett in " " verpackt werden. Innerhalb dieser Argumentenliste werden wieder Variablen mit dem $ Zeichen im Namen gebildet. Diese Namen haben nichts mit der aufrufenden Powershell zu tun.
Damit Powershell die Zeichen $ und " nicht auflöst, muss man sie mit Backticks (`) schützen. Wenn man die Befehlsfolge an den Stellen aufteilt, wo man sie auch separat testen könnte, bleibt sie lesbar und ist gut zu dokumentieren. Der Code funktioniert, im eigentlichen Artikel gibt es eventuell eine aktuelle Variante.
$C1 = "type $Keyfile | ssh " 
$C2 = " akey='.ssh/authorized_keys'; okey='keee.tmp'; cat | tr -d '\r' >`$okey;" # Variablen erzeugen, Text aus der Pipe ohne CR speichern
$C3 = " umask 077; test -d .ssh || mkdir .ssh;" # Berechtigung setzen, Verzeichnis prüfen sonst anlegen
$C4 = " test -f `$akey || cat `$okey >`$akey;" # Datei prüfen sonst direkt anlegen
$C5 = " grep -q `"`"`$(cat `$okey)`"`" `$akey  || cat `$okey >>`$akey;rm `$okey" # Prüfen ob Key schon vorhanden sonst hinzufügen 
Der grep Befehl bekommt einen String übergeben der in sich durch " geschützt sein muss. Damit die Shell die " nicht auflöst müssen sie in der Shell durch Verdopplung geschützt werden, damit Powershell die " im String nicht auflöst müssen sie durch ` geschützt werden.
Jetzt fügen wir alles zusammen und testen das Ergebnis als Ausgabe. Hier erfolgt jetzt das oben erwähnte Einpacken der Shellbefehle in " . Man kann $C2 bis $C5 flexibel weglassen und einzeln testen.
$Argument = $($cs + $C1 + $sshTarget + " `"" + $C2 + $C3 + $C4 + $C5 + "`"")
Write-Output $Argument
Wenn alles gut aussieht kann man den Befehl testen.
Start-Process "cmd.exe" $Argument
Jetzt kann man alles zusammenpacken und auf Knopfdruck entweder alle Hosts auf einmal in getrennten ssh Fenstern öffnen oder gleich alle Hosts mit einem Vorgang mit den eigenen Public Keys versorgen.
Hier eine komplette Version mit ein paar Optionen, die durch Kommentarzeichen noch aus- oder eingeschaltet werden müssen.
# Varianten für Zielpfad
#$HomePath = "$env:HOMEDRIVE$env:HOMEPATH\Desktop"
$HomePath = "$env:onedrive\ssh Hosts"

# eigener Public Key
$Keyfile = "$env:HOMEDRIVE$env:HOMEPATH\.ssh\id_rsa.pub"
# Filter 
$Filter = "*.lnk"
$FArg = "pi@"
# cmd Fenstertyp bestimmen
$cs = "/C"  # Schließt das cmd Fenster nach Abarbeitung
#$cs = "/K"  # Lässt das cmd Fenster offen - nur für Tests wichtig!

# Kommando für cmd Aufruf in Einzelteilen
$C1 = " type $Keyfile | ssh " 
$C2 = " akey='.ssh/authorized_keys'; okey='keee.tmp'; cat | tr -d '\r' >`$okey;" # Variablen erzeugen, Text aus der Pipe ohne CR speichern
$C3 = " umask 077; test -d .ssh || mkdir .ssh;" # Berechtigung setzen, Verzeichnis prüfen sonst anlegen
$C4 = " test -f `$akey || cat `$okey >`$akey;" # Datei prüfen sonst direkt anlegen
$C5 = " grep -q `"`"`$(cat `$okey)`"`" `$akey  || cat `$okey >>`$akey;rm `$okey" # Prüfen ob Key schon vorhanden sonst hinzufügen 

# Die Alternativen Zeilen für OpenWrt Router
#$FArg = "root@"
#$C2 = " akey='/etc/dropbear/authorized_keys'; okey='keee.tmp'; cat | tr -d '\r' >`$okey;"

# Shell Object zum Zugriff auf Eigenschaften von lnk
$shell = New-Object -COM WScript.Shell
# Variante mit Pipes
Get-childItem $HomePath -filter $Filter |
  ForEach-Object {$shell.CreateShortcut($_.FullName)}|where Arguments -match $FArg|
    ForEach-Object {
       $sshTarget = $_.Arguments
       # Entweder nur die Public Keys übertragen 
       $Argument = $($cs + $C1 + $sshTarget + " `"" + $C2 + $C3 + $C4 + $C5 + "`"")
       # oder nur die Terminalsession öffnen
       #$Argument = "$cs $($_.Targetpath) $($_.Arguments)"
        
       Write-Output $Argument # Zum Test
       #Start-Process "cmd.exe" $Argument # eigentlicher Befehl 
    }

Keine Kommentare:

Kommentar veröffentlichen