Freitag, 29. Januar 2021

Befehle und Scripts per ssh ausführen

Ich möchte über ssh (mit Powershell oder Perl) auf einem Linux Computer Befehle ausführen und dort Aktionen steuern.

Dazu braucht man ein paar Vorbereitungen, muss ein paar Eigenheiten beachten und ich möchte hier meine Erkenntnisse notieren.

Die ssh Einrichtung für Linux (fhem) habe ich hier schon mal beschrieben. Auf die Übertragung des public Key mit der Powershell gehe ich kurz ein. 

ssh in älteren Windows Versionen

Ich habe noch Windows Server 2012 im Einsatz, dort kann man OpenSSH nachinstallieren. Dabei gibt es zwei Dinge:

  1. der Pfad wird beim Setup nicht angepasst. Dies kann einfach durch Ergänzung der Pfadvariable am Anfang einer Sitzung/eines Script durchgeführt werden. $env:PATH += ""
  2. Die CMD in den alten Versionen unterstützt kein conpty - damit ist eine interaktive Arbeit mit ssh praktisch unmöglich. Befehlszeilen ausführen funktioniert aber sehr gut!
Die zweite Methode zerlegt den PATH in seine Bestandteile und fügt ihn neu zusammen. 

$env:path += ";C:\Program Files\OpenSSH-Win64\"
($env:path).split(";")+"C:\Program Files\OpenSSH-Win64\" -join ";"

Ohne Pfaderweiterung wird es schwieriger: will man z.B. "C:\Program Files\OpenSSH-Win64\ssh" ausführen (Leerzeichen im Pfad) braucht man den Call Operator(&) oder iex (Invoke-Expression).

Zugang einrichten

Eigenen ssh Key erstellen - wenn er noch nicht existiert (überschreiben wird abgefragt). Mit dem leicht modifiziertem Befehl für die Powershell (Standardpfad/Datei mit enter bestätigen):

ssh-keygen --% -t rsa -N ""

Die Besonderheit --% wird im nächsten Abschnitt erklärt.

Public Key kopieren - als Ersatz für das bei Windows nicht vorhandene Linux Tool"ssh-copy-id" habe ich auf GitHub ein Script abgelegt, die Verwendung ist analog, es funktioniert auch von Windows zu Windows.

wget -O ssh-copy-id.ps1 https://raw.githubusercontent.com/heinz-otto/Powershell/master/ssh-copy-id.ps1
$ziel = 'user@host' ; .\ssh-copy-id.ps1 $ziel

Hier der Inhalt als Vierzeiler. Quelle Windows Powershell -> Ziel Linux.

$ziel = 'user@host'
$apath = '.ssh'
$pkey = $(type $env:userprofile\.ssh\id_rsa.pub)
ssh $ziel "if ! grep -q `"`"$pkey`"`" $apath/authorized_keys; then mkdir -p $apath; echo $pkey >>$apath/authorized_keys;fi"

Anmerkung: Viele Einzeiler die im Netzt existieren, ersetzen einfach die Datei "authorized_keys" oder ergänzen sie ohne Prüfung. Hier wird geprüft ob der Pfad und die Datei oder der Key schon existiert, bei Bedarf wird der Pfad erzeugt, die Datei angelegt und der fehlende Key eingefügt. 

Zugang testen - remote einen Befehl ausführen:

ssh $ziel "ls -lha"

Powershell 

stop parsing

Das "Fenster" cmd und powershell funktionieren scheinbar identisch, im Detail aber völlig anders. Powershell versucht die gesamte Befehlszeile zu interpretieren! 

Dieser Befehl zur Erzeugung des ssh Keys funktioniert in der Windows CMD.

ssh-keygen -t rsa -N "" -f %USERPROFILE%\.ssh\id_rsa

Der gleiche Befehl in der Powershell bringt einen Fehler "Too many Arguments" - auch wenn man versucht den Aufruf an cmd.exe zu "übergeben".

Abhilfe schafft recht simpel das "stop parsing" Symbol "--%" nach dem eigentlichen Programmaufruf, wie oben schon verwendet. Alternativ müsste man ermitteln, welche Analyse Powershell vornimmt und diese durch Maskierung verhindern (aufwendig).

cmd.exe /c --% ssh-keygen -t rsa -N "" -f %USERPROFILE%\.ssh\id_rsa
ssh-keygen -t rsa -N '""' -f "${env:userprofile}\.ssh\id_rsa"

Mein Tipp: Einfach nach dem ssh Kommano das weitere parsen unterbinden und damit Kommandos wie gewohnt ausführen (funktioniert aber nicht mit Variablen):

ssh user@host --% "bash ScriptA1.sh /mnt/magenta /mnt/test"

Wichtige Anmerkung: Als äußere "Hochkommas" sind die doppelten zu verwenden. Das ist eventuell eine Eigenheit von der OpenSSH Implementierung in Windows? 

Nachtrag: Das stop parsing Symbol funktioniert nur, wenn man keine Variablen verwenden will!

Call Operator

Ich habe versucht ein mehrzeiliges bash Script an den ssh Aufruf zu übergeben, das ist mir mit dem "stop parsing" nicht gelungen. Aber mit dem & (Call Operator) funktioniert es. Es scheint mir so, als muss man hier eine 3er Aufteilung vornehmen: "ssh" "user@host" "Scripttext". 

Irgendwo hatte ich mal gelesen, der & Operator von Powershell will "die Argumente als Array". Zum Test habe ich eine ganz schön komplexen "Einzeiler", der ein magic paket verschickt (Wake on LAN)

$exe='ssh'
$ziel='user@host'

$script=@'
MAC=aa:bb:cc:11:22:33
Broadcast=255.255.255.255
PortNumber=9
echo -e $(echo $(printf 'f%.0s' {1..12}; printf "$(echo $MAC | sed 's/://g')%.0s" {1..16}) | sed -e 's/../\\x&/g') | nc -w1 -u -b $Broadcast $PortNumber
'@

& $exe $ziel $script.replace("`r`n","`n")

Das Script habe ich komplett in ein "single quoted here document" gepackt welches final von den Windows typischen Zeilenumbrüchen befreit wird. Es macht praktisch nicht viel Sinn ein WOL Script über Powershell und ssh auf einem linux Host zu starten, aber als eine Art Machbarkeitsstudie habe ich mal noch ein komplettes Script mit Parameter Übergabe auf GitHub abgelegt.

Perl Sub in FHEM

Entscheidend bei diesem Code ist die Verwendung eines 'single-quoted here documentes' und der system()  Aufruf mit vom Programmnamen getrennten Argumenten. Damit wird die "komplizierte" Zeile Code 1:1 an den ssh Aufruf als Argument  übergeben. 

Der eigentliche Remotebefehl wird aus zwei Teilen zu einem String zusammen gesetzt: Das "double quoted here document" im ersten Teil sorgt dafür, dass die Werte der Variablen übergeben werden können. 

Der Aufruf erfolgt mit {wol('user@host','aa:bb:cc:11:22:33','192.168.12.255','9')}.  Wird die Broadcast Addresse und/oder das Port weggelassen, werden Standardwerte gesetzt.

sub wol
{
my ($ziel,$mac,$bc,$port) = @_;
my $usage = 'use: wol \'user@host\',\'aa:bb:cc:11:22:33\'';
if (!defined $ziel or $ziel eq ''){return $usage}
if (!defined $mac or $mac eq '')  {return $usage}
if (!defined $bc)   {$bc  ='255.255.255.255'}
if (!defined $port) {$port='9'}

my $script = <<"EOF";
MAC=$mac
Broadcast=$bc
PortNumber=$port
EOF

$script .= <<'EOF';
echo -e $(echo $(printf 'f%.0s' {1..12}; printf "$(echo $MAC | sed 's/://g')%.0s" {1..16}) | sed -e 's/../\\x&/g') | nc -w1 -u -b $Broadcast $PortNumber
EOF

system('ssh',$ziel,$script);
}

Nachteil: Mit dieser Form des system Aufrufes bekommt man kein "Fork" hin, d.h. der Code wird blockierend ausgeführt. Die alternative "forkende" Form system("shellbefehl &") funktioniert mit diesem kompliziertem Code nicht. Ich muss mal noch schauen, ob ich da eine Alternative hinbekomme.

Nachtrag

Ich habe festgestellt, das man sorgsam bei der Übergabe der Parameter sein muss. Variablen muss man mit Hilfe von geschweiften Klammern auflösen, es braucht zusätzlich doppelte Anführungszeichen. Beispiel für ein funktionierenden Aufruf für ssh-keygen.

ssh-keygen -t rsa -N '""' -f "${ENV:UserProfile}\.ssh\id_willi"

ToDo ?

Code Block


Keine Kommentare:

Kommentar veröffentlichen