Dienstag, 31. Juli 2018

Import static Leases in OpenWrt

OpenWrt kann mit speziellen Kommandos (uci) in der Shell konfiguriert werden.
Ich habe mir immer gewünscht, statische DHCP Reservierungen einfach durch Textdateien zu konfigurieren. OpenWrt kann zwar wenigsten in der Weboberfläche die MAC Adressen per Copy&Paste einfügen, aber ich hätte es gern komfortabler.
Wer spielen will, der macht zunächst nichts kaputt. Die Konfigurationsänderungen werden zunächst nur temporär ausgeführt.
Die dhcp Konfiguration anzeigen:
uci show dhcp
uci show dhcp | grep '@host\[0\]'
uci get dhcp.@host[0].ip
Die Host Einträge in der /etc/config/dhcp stehen dort am Ende und sind nicht indiziert. Das uci indiziert sie "fließend" für uns und einen direkten Zugriff:
  • [0] ist der erste, 
  • [-1] der letzte Eintrag.

Vorbemerkung

Ich habe ein paar Scripts als heredoc erstellt, die kann man einfach per copy&paste ins Terminal fallen lassen. Damit die $ Zeichen nicht als Variablen interpretiert werden, muss EOF in einfache ' ' gesetzt werden!
Die uci arbeitet temporär, man kann sich mit uci changes den Erfolg anzeigen lassen. Mit uci revert dhcp kann man alles immer wieder rückgängig machen.
Erst ein uci commit schafft Tatsachen.

Hosts hinzufügen

Will man einen neuen Host eintragen geht das mit folgendem Beispiel. Ein neuer Eintrag wird angefügt und ist damit der Letzte, dieser wird um die Werte ip und mac ergänzt:
script="addhost.sh"
cat <<'EOF' >$script
#!/bin/sh
if [ $# -eq 3 ]
  then
    echo "name=$1 ip=$2 mac=$3"
  else
    echo "Fehler: Bitte name ip und mac angeben."
    exit
fi
uci add dhcp host
uci set dhcp.@host[-1].name=$1
uci set dhcp.@host[-1].ip=$2
uci set dhcp.@host[-1].mac=$3
EOF
chmod +x $script
Jetzt kann man eine existierende Lease Zeile aus dem Browser kopieren und als Reservierung einfügen
./addhost.sh HPF43909586B94 192.168.56.111 F4:39:09:58:6B:94
Mit einer Schleife kann man Zeilenweise den stdin lesen und und pro Zeile einen neuen Hosteintrag erstellen. Damit die static Leases lesbar bleiben sollte ein Hostname mit in die Tabelle.
Read Host per Line -> rhpl.sh
script="rhpl.sh"
cat <<'EOF' >$script
#!/bin/sh
while read line; do
./addhost.sh $line
done
EOF
chmod +x $script
Dazu eine Datei mit Wertepaaren -> wp.txt
host1 192.168.1.1 aa:bb:cc:dd:ee:d1
host2 192.168.1.2 aa:bb:cc:dd:ee:d2
host3 192.168.1.3 aa:bb:cc:dd:ee:d3
Das Ganze mit der Pipe verknüpft und fertig ist der Import!
./rhpl.sh <wp.txt
Nachtrag 2021: Hat man mehrere Dateien, kann man diese gemeinsam lesen, die MAC Adressen in Großbuchstaben wandeln, sortieren, Doubletten entfernen und einlesen
cat leases.txt wpo.txt |awk '{ print( $1,$2,toupper($3) ) }'|sort|uniq|./rhpl.sh
Noch ist nichts aktiv!

Hosts löschen

Will man vor dem Import aufräumen, kann man mit einem (nicht perfekten) Einzeiler erst einmal alle Hosts löschen:
while uci delete dhcp.@host[0]; do :;done
Die Schleife läuft einmal zu viel, deswegen gibt es einen Fehler. Aber alle Hosts sind gelöscht.

Host auslesen

Damit man die Liste der Werte Paar nicht komplette per Hand erstellen muss, kann man existierende Daten auslesen. Die entstandenen Dateien kann man bei Bedarf bearbeiten und anschließend importieren.

Static Leases auslesen

Um alle Hosts in eine Datei zu exportieren, braucht man eine formatierte Ausgabe
printf "$(uci get dhcp.@host[-1].name) $(uci get dhcp.@host[-1].ip) $(uci get dhcp.@host[-1].mac)\n"
Und ein Array mit den vorhandenen Indizes. Ein doppelter "Eierkuchen" liefert uns dies aus dem show Befehl.
echo $(uci show dhcp | grep -oE "host\[\d+\].ip"|grep -oE '\d+')
Eine simple Schleife liefert uns die Wertepaare als Ausgabe.
script="wrlp.sh"
cat <<'EOF' > $script
#!/bin/sh
for i in $(uci show dhcp | grep -oE "host\[\d+\].ip"|grep -oE '\d+'); do
  printf "$(uci get dhcp.@host[$i].name) $(uci get dhcp.@host[$i].ip) $(uci get dhcp.@host[$i].mac)\n"
done
EOF
chmod +x $script
Die schreibt man einfach in eine Datei.
./wrlp.sh >wpo.txt

Aktuelle Leases auslesen

Nachtrag 2021: Ich habe noch eine bessere Variante zum Auslesen der aktiven Leases gefunden, lasse aber den alten Code bewusst noch stehen.
cat /tmp/dhcp.leases| awk '$4 != "*" { print( $4,$3,$2 ) }'
Um einfach mal alle existierenden Leases auszulesen genügt ein Einzeiler. Allerdings ist das Ausgabeformat anders als im Beispiel oben: mac IP Host (Darstellung im Browser Host IP mac)
cat /tmp/dhcp.leases |grep -oE '([a-f0-9]{2}:){5}[a-f0-9]{2}\s[0-9]{1,3}.[0-9]{1,3}.[0-9]{1,3}.[0-9]{1,3}\s[A-Z,a-z,0-9,-,_]*'
Das Script macht ein match auf mac Adresse, IP Adresse und String (Hostname)

Ich habe noch eine vereinfachte Form "experimentell" gefunden (\s.*\s passt auf IP Adresse und Hostname - warum? Der Rest wird abgeschnitten).
Aus der Ausgabe kann man noch per Pipe mit grep die Zeilen mit unbekannten Hostnamen (*) entfernen und mit awk formatieren.

grep -oE '([a-f0-9]{2}:){5}[a-f0-9]{2}\s.*\s' /tmp/dhcp.leases|grep -v "*"|awk '{print $3,$2,$1}'

Finale

Das uci System arbeitet temporär, will man seine Änderungen fest machen, muss man diese speichern und das jeweilige System(dhcp) neu starten. 
Achtung: Scheinbar wird seit einiger Zeit nicht mehr dnsmasq sondern odhcpd für den DHCP Service verwendet. Offenbar genügt aber ein commit Befehl den restart & reload kann man sparen?
uci commit dhcp
/etc/init.d/dnsmasq restart
luci-reload

Die uci Schnittstelle ist auch über JSON erreichbar.
wiki.teltonika.lt

Ersatz für ssh-copy-id

Manchmal kommt es vor, das auf einem System das Tool ssh-copy-id nicht vorhanden ist. Dann gibt es zwar einfache Alternativen, aber wenn man nicht genau weiß worauf es ankommt, wird es schnell kompliziert. Ich mache hier raus mal einen kurzen Artikel, weil ich auch ein paar Besonderheiten der Shell gelernt habe. 
Für die Verwendung unter Windows habe ich ein Powershellscript auf GitHub abgelegt.

Die einzelnen Schritte

Was macht ssh-copy-id bzw. was muss man berücksichtigen?
Alles in allem geht es darum, den Public Key von System 1 (lokal) nach System 2 (remote) zu übertragen um sich zukünftig vom System 1 am System 2 vereinfachten anzumelden und Befehle auszuführen.
  • lokalen Public Key an das Remotesystem senden.
  • im Remotesystem schauen ob der Pfad .ssh schon existiert, 
    • wenn nicht: Pfad anlegen.
  • in diesem Pfad schauen ob schon eine Datei .ssh/id_rsa.pub existiert, 
    • wenn nicht: mit dem Public Key erzeugen.
    • wenn schon vorhanden: prüfen ob der public Key schon enthalten ist,
      • wenn nicht: Den Public Key hinzufügen.
Wer Hemmungen hat den Programmcode zu verwenden, kann die oben genannten Schritte auch manuell mit ls und nano (Public Key einfach mit copy&paste übertragen) abarbeiten.
 Schon die erste Aufgabe ist nicht trivial, hier wird mit der Pipe gearbeitet und in Abhängigkeit der Shell stehen dort unterschiedliche Subsysteme zur Verfügung. Variablen können nicht einfach transportiert werden.
Hinweise:

  • Ich habe die Codestücke zur Erklärung ohne Maskierung geschrieben. So funktionieren sie aus der Windows cmd Konsole bzw. direkt in der Shell. Wie weiter unten erläutert muss $ und " maskiert werden!
  • der letzte Befehl (cat bzw. echo) ist nur zum Test ob es funktioniert hat.
Am Einfachsten geht es in eine Datei.
cat .ssh/id_rsa.pub | ssh user@host "cat >kee.tmp;cat kee.tmp"
In Windows sind für Linux oft schädliche Zeilumbrüche (CR/LF) enthalten, dies Variante filtert die CR aus.
type .ssh\id_rsa.pub | ssh user@host "cat | tr -d '\r' >kee.tmp;cat kee.tmp"
Nach einigen Versuchen glaube ich mittlerweile, dass es mit diesem Konstrukt auch zuverlässig in eine Variable ($pub) kommt:
cat .ssh/id_rsa.pub | ssh user@host "pub=$(cat );echo $pub"
type .ssh\id_rsa.pub | ssh user@host "pub=$(cat | tr -d '\r');echo $pub"
Ob das Verzeichnis existiert ist schnell getestet, das ODER führt den Befehl danach nur bei false aus. Vorher wird mit dem umask Befehl die richtige Berechtigung für den neuen Pfad sichergestellt.
umask 077;test -d .ssh || mkdir .ssh
Auf ähnliche Art wird die Datei getestet und bei false einfach erzeugt:
test -f .ssh/authorized_keys || cat kee.tmp >.ssh/authorized_keys
test -f .ssh/authorized_keys || echo $pub >.ssh/authorized_keys
Danach wird der Inhalt der Datei mit grep auf Vorhandensein des Key geprüft und bei false wird der public Key angehängt. Bei der ersten Variante wird die tmp datei gelöscht.
Da Leerzeichen im Public Key sind, braucht grep den Suchstring in "ein zwei drei"!
grep -q "$(cat kee.tmp)" .ssh/authorized_keys  || cat kee.tmp >>.ssh/authorized_keys;rm kee.tmp
grep -q "$(echo $pub)" .ssh/authorized_keys  || echo $pub >>.ssh/authorized_keys

Die Einzeiler

Da die Dateinamen im Code öfters gebraucht werden, definiere ich sie am Anfang als Variable. Jetzt steht noch zusätzlich die Aufgabe den Einzeiler so zu schreiben, dass alle Sonderzeichen von der Shell richtig behandelt werden. Die folgenden Varianten haben als Ziel ein Linux System! Startet man den Befehl aus der Linux Shell muss mit dem Backslash \ maskiert werden:
cat .ssh/id_rsa.pub | ssh user@host "p='.ssh';akey=\$p'/authorized_keys'; okey='kee.tmp'; cat >\$okey; umask 077; test -d \$p || mkdir \$p; test -f \$akey || cat \$okey >\$akey; grep -q \"\$(cat \$okey)\" \$akey  || cat \$okey >>\$akey;rm \$okey"
cat .ssh/id_rsa.pub | ssh user@host "p='.ssh';akey=\$p'/authorized_keys'; pub=\$(cat ); umask 077; test -d \$p || mkdir \$p; test -f \$akey || echo \$pub >\$akey; grep -q \"\$(echo \$pub)\" \$akey || echo \$pub >>\$akey"
Startet man man den Einzeiler aus der cmd von Windows, müssen lediglich die " verdoppelt werden:
type .ssh\id_rsa.pub | ssh user@host "p='.ssh';akey=$p'/authorized_keys'; pub=$(cat | tr -d '\r'); umask 077; test -d $p || mkdir $p; test -f $akey || echo $pub >$akey; grep -q ""$(echo $pub)"" $akey || echo $pub >>$akey"
Baut man den Befehl in Powershell zusammen muss mit dem Backtick ` maskiert werden.

Der Pfad .ssh gilt für die meistens Standard Systeme mit Benutzeranmeldung. Bei vielen Spezialsystemen (Zielsystem) muss der Pfad angepasst werden!
z.B. /etc/dropbear bei OpenWrt.
cat .ssh/id_rsa.pub | ssh user@host "p='/etc/dropbear';akey=\$p'/authorized_keys'; pub=\$(cat ); umask 077; test -d \$p || mkdir \$p; test -f \$akey || echo \$pub >\$akey; grep -q \"\$(echo \$pub)\" \$akey || echo \$pub >>\$akey"
Die nächste Variante hat als Ziel ein Windows System!
ziel='user@host';pkey=$(cat ~/.ssh/id_rsa.pub);ssh $ziel "findstr /c:\"$pkey\" .ssh\authorized_keys ||mkdir .ssh ||echo $pkey >>.ssh\authorized_keys"

Maskierung von Sonderzeichen
Windows: Nur " muss verdoppelt werden -> ""
Linux: $ und " müssen mit \ maskiert werden -> \$ \"
Powershell: $ und " müssen mit ` maskiert werden -> `$ `"

Mittwoch, 18. Juli 2018

Kalender in FHEM einbinden

Die älter Bereitstellung des Link habe ich bereits hier gezeigt. In der neuen Form des Google Kalenders gibt es den grünen iCal Button nicht mehr, der Link steht jetzt unter Einstellungen/<Kalendername>/Kalendereinstellungen und dort weit unten in der Box:
Privatadresse im iCal Format
Darunter steht der wichtige Hinweis:
Mit dieser Adresse können Sie von anderen Anwendungen aus auf den Kalender zugreifen, ohne ihn öffentlich zu machen.

Das Kalender Modul in FHEM ist aktuell überarbeitet und die Funktionen sind erweitert und geändert worden. Mittlerweile weiß ich, dass es keine gute Lösung ist ein Modul einzusetzen, welches auf einem anderen Modul aufsetzt aber völlig getrennt von dem entwickelt wird. Man schafft unnötig Abhängigkeiten.
Bei der Einbindung eines Kalenders in FHEM sollte man sich über den praktischen eigenen Umgang mit dem Kalender und dem Aktualisierungsintervall (Standard 1 h) Gedanken machen. Ein Abfallkalender der sich das ganze Jahr praktisch nicht ändert muss entweder nie oder höchsten 1 mal Tag aktualisiert werden.

1. Beispiel: 

Signalisierung der Abfalltonne, Aktualisierung einmal am Tag
define AbfallKalender Calendar ical url https://... 86400
Dann brauchen wir ein Gerät wo die aktuelle Tonne drin steht, im einfachsten Fall ein Dummy
define Tonne dummy
Und ein Timer der einmal am Tag den Kalender ausliest und das Ergebnis ablegt.
define a_Tonne at *12:00:00 set Tonne {(my $evt=fhem('get AbfallKalender events format:custom="$S" limit:from=1d,to=1d');;;;$evt?$evt:"0")}
Die Funktion:
  • Am Mittag den Ganztagestermin für morgen auslesen -> limit:from=1d,to=1d
  • Nur den Textinhalt des Eintrages lesen -> format:custom="$S"
  • $evt?$evt:"0" -> Falls kein Termin gefunden wird, wird der dann leere String durch eine 0 ersetzt.
  • Den resultierenden Text in den Dummy Tonne schreiben.
Damit kann man am Vortag des Termines ein Aktion auslösen: z.B. Nachmittag den Hinweis geben: die Tonne muss morgen raus. Und am Morgen des Termines kann man nochmal den Hinweis geben: die Tonne muss heute raus. Der Zeitpunkt im Timer entscheidet über das Auftauchen und Verschwinden des Inhaltes von der "Tonne".
Hinweis:
Bei Perl Code innerhalb von einem at Kommando müssen die ; verdoppelt werden, im define müssen sie auch verdoppelt werden, deshalb ;;;; !
Bei Perl Code im set Befehl muss eine zusätzliche Klammer () stehen, sonst wird der Perlausdruck als String gesehen: set Tonne {3+5} ergibt {3+5} im Dummy, set Tonne {(3+5)} ergibt 8 im Dummy.

2. Beispiel: 

Im Kalender stehen FHEM Device Namen (z.B. Aktoren), die Terminzeiten sind die Schaltzeiten.
Das Beispiel gilt für Einzeltermine (eigene uid). Serientermine haben eine uid für alle Termine, hier muss man weiter filtern(limit:from ...).
Der Kalender mit stündlicher Aktualisierung:
define TestKalender Calendar ical url https://... 
Dann wiederum ein Dummy:
define Urlaub dummy
Und ein notify, welches exakt zu Beginn (start) und Ende (end) des Einzeltermines den Dummy auf on oder off setzt. (Code für die Raw Def)
define n_TestKalender notify TestKalender:changed:.* {\
  my $cmd ='on';;\
  $cmd = 'off' if ($EVTPART2 eq 'end');;\
  my $actor = fhem('get '.$NAME.' events format:custom="$S" filter:uid=="'.$EVTPART1.'" limit:count=1');;\
  fhem("set $actor $cmd");;\
}

Funktion
Das Calendar Modul wirft zum Zeitpunkt des Termines ein paar Events, davon spricht das notify genau auf diese Beiden an:
2018-07-16 16:25:00 Calendar TestKalender changed: 123456googlecom start
...
2018-07-16 16:30:00 Calendar TestKalender changed: 123456googlecom end
Der Event hat drei Teile:
  • $EVTPART0 ist uninteressant, 
  • $EVTPART1 enthält die ID des Eintrages und 
  • $EVTPART2 "start" bzw. "end".

Mit Hilfe der ID wird der Kalendertext gelesen und mit den umgewandelten "on" (start) und "off" (end) Befehlen das Device in FHEM gesetzt.
Warum sieht der Befehl so kompliziert aus?

  • Der uid/format String muss im String die doppelten Anführungszeichen enthalten! Damit dies funktioniert, muss der gesamte Befehl in ' ' gesetzt werden. Innerhalb werden dann zwar " akzeptiert aber keine Variablen mehr aufgelöst. Diese muss man hier mit Verkettung/concatenation einbauen.

Weiter unten habe ich eine (für den Anfänger) besser lesbare Variante eingebaut.

Komplexen Code auslagern

Für komplexeren Code sollte man alles in die 99_myUtils auslagern und im notify lediglich die wichtigen Parameter übergeben:
define n_TestKalender notify TestKalender:changed:.* {KalenderSub($EVTPART1,$EVTPART2,$NAME)} 
Das folgende Beispiel filtert das Ereignis (Devicenamen) "Urlaub" aus dem Kalender und setzt das gleichlautendes Device bei Start und Ende auf ja /nein. Alle anderen Events werden zwar getriggert aber verworfen.
Die sub kann man beliebig komplex gestaltet. Damit der get Befehl lesbar bleibt, habe ich alles in extra Variablen gepackt. (Code für die 99_myUtils.pm)
sub KalenderSub ($$$)
   {
     my ($uid,$cmd,$cname) = @_;
     #Logging der Paramter bei Bedarf;     #Log 1, "uid: $uid | Start/End: $cmd | cal: $cname";
     my $format = '"$S"';
     my $dev = 'Urlaub';
     $uid='"'.$uid.'"';
     $cmd = 'ja' if ($cmd eq 'start');
     $cmd = 'nein' if ($cmd eq 'end');
     
     my $actor = fhem("get $cname events format:custom=$format filter:uid==$uid limit:count=1");
     # Unterschiedliche Abfragen auf ist exakt oder enthält möglich;
     #fhem("set $actor $cmd") if ($actor eq $dev);
     if ($actor =~ /$dev/) {fhem("set $dev $cmd")};
}

Tipp

Der Calendar Aufruf wird mit Level 3 geloggt, wenn man das nicht möchte hängt man an den Befehlsaufruf einfach noch ",1" an.
fhem("Befehl",1)

Weitere Infos:
Forum
Wiki

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 
    }