Dienstag, 19. März 2019

Precence und OpenWrt - der 3. Versuch

Am Wochenende habe ich meinen OpenWrt Router WRT1900ACS mit neuer Firmware versorgt und dabei fiel dann relativ schnell auf, da war doch noch was...
Die beiden Varianten um festzustellen, ob bestimmte Geräte im Wlan angemeldet sind, die ich im Juni bzw. August 2018 schon mal getestet habe, haben einen ganz entscheidenden Nachteil: Sie überleben das Firmware Update nicht!
Ich hatte die Variante aus August zwar als Provisorium ohne Probleme laufen, aber so richtig zufrieden war ich damit sowieso nicht.
Der ssh Zugang zum OpenWrt Router überlebt das Firmware Update!
Deswegen eine neue Version. Ich beschreibe der Vollständigkeit halber an dieser Stelle auch noch einmal in Kurzfassung die Einrichtung des passwortlosen ssh Zuganges von fhem zum OpenWrt Router. Die Langfassung gab es ja schon in 2017.

Vorbemerkung

Das Tool ssh-copy-id kann nicht mit den speziellen Pfaden von dropbear (ssh OpenWrt) umgehen, deshalb kommt mein "Einzeiler" aus dem Artikel zum Einsatz.
Ich weiß, dass man prinzipiell die ssh Keys auch vom aktiven User auf den User fhem kopieren kann. Bei sicherheitsrelevanten Themen bin ich lieber für "ordentliches" und vielleicht etwas aufwendigeres Vorgehen.

ssh Zugang mit Public Key einrichten

Achtung: In zwei Zeilen muss am Anfang der username und der hostname eingetragen werden. Meine Codeboxen sind editierbar, man kann es hier direkt tun und die Zeile anschließend kopieren. Der "Einzeiler" zum ssh Key kopieren ist etwas lang!
sudo cp /etc/passwd /etc/passwd.sav
sudo sed -i -e 's/fhem:.*/fhem:x:999:20::\/opt\/fhem:\/bin\/bash/' /etc/passwd
sudo passwd fhem             # Passwort vergeben und bestätigen
su fhem                      # Als fhem einloggen, es startet eine neue session!
# prüfen ob fhem schon einen ssh Key hat, wenn nicht einen erzeugen
ls -lha /opt/fhem/.ssh/id_rsa        # ssh-keygen -t rsa 
# Remote User und Hostname/IP eintragen!
cat /opt/fhem/.ssh/id_rsa.pub | ssh <user>@<remote-system> "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"
ssh <user>@<remote-system>   # Einmal testen - es startet eine neue session!
exit                         # aus der ssh Test session vom Remotehost!
exit                         # aus der Anmeldung von fhem!  
sudo cp /etc/passwd.sav /etc/passwd

FHEM

Die Einrichtung in FHEM besteht aus einem Script und einem Presence Device.

Script

Meine Script liegen jetzt alle auch auf GitHub, wer das aktuelle Script einfach laden will kann das wie hier gezeigt tun, fhem braucht selbst nur Lese Rechte. Im Script muss normal nichts konfiguriert werden.
Dieser Befehl lädt das Script ohne Umwege ins FHEM Verzeichnis:
sudo wget -O /opt/fhem/GetMacPresence.sh https://raw.githubusercontent.com/heinz-otto/scripts/master/Bash/GetMacPresence.sh
Ich habe keinen anderen Benutzer in OpenWrt angelegt, sollte man aber vielleicht tun.
Das Script liefert lediglich, wie vom Modul gefordert, 0 oder 1 zurück, MAC Adresse und Routername werden als Parameter übergeben.
MAC=$1
host=$2
user="root"
ssh $user@$host '
    if (
      for m in $(
                 for w in $(iwinfo |grep -oE "wlan\d-\d|wlan\d")
                 do
                   iwinfo $w assoclist | grep -o -E "([[:xdigit:]]{1,2}:){5}[[:xdigit:]]{1,2}"
                 done
                )
      do
        [ "$m" = '"$MAC"' ] && exit 0
      done
      exit 1
    )
  then
    echo 1
  else
    echo 0
  fi
'

Presence Device

Das Beispiel zeigt die einfachste Verwendung, neben der zu suchenden MAC Adresse wird noch der Name des OpenWrt Routers angegeben.
define WL_Dev1 PRESENCE shellscript "bash GetHostPresent.sh 11:22:33:AA:BB:CC wrt1900"

Erfahrung

Ein Problem tritt relativ schnell zu Tage: Man kann offenbar dieses Script nicht mehrfach starten, dann führt irgendeine Komponente zum Abbruch.

Dienstag, 5. März 2019

Eine weitere FHEM Instanz auf gleicher Hardware

Ein kleines HowTo für eine zweite FHEM Instanz.

1. Variante: Codeshare

Es gibt bei dieser Variante keine separaten Pfade - alles läuft in einem Pfad!
Lediglich eine neue fhem.cfg wird verwendet. Ein Update wirkt sich auf alle Instanzen aus!
Nur 3 Zeilen für die FHEM Kommandozeile:
  • Die erste Zeile holt eine Standard fhem.cfg vom SVN und speichert sie als fhem2.cfg. Die Original fhem.cfg wird nicht überschrieben!
  • Die zweite Zeile modifiziert diese fhem2.cfg:
    • Port 8093 statt 8083
    • ändert Namen für Logfile und Statefile sowie eventTypes, damit läuft diese Instanz zunächst unabhängig.
    • initialUsbCheck wird deaktiviert 
  • Die dritte Zeile startet diese neue Instanz, sie ist unter http://<hostname>:8093 erreichbar.
"wget -qO fhem2.cfg https://svn.fhem.de/fhem/trunk/fhem/fhem.cfg"
"sed -i -e 's/\/fhem/\/fhem2/;;s/\/eventTypes/\/eventTypes2/;;s/8083/8093/;;$aattr initialUsbCheck disable 1' fhem2.cfg"
"perl /opt/fhem/fhem.pl /opt/fhem/fhem2.cfg"

Achtung: Obwohl die Konfiguration separat ist, verwendet sie den gleichen Pfad wie die erste Instanz. Update, gleiche Namen innerhalb von Definitionen (Logdateien usw.) wirken sich auf beide Instanzen aus!

Ein shutdown der zweiten Instanz beendet diese, ein shutdown der ersten Instanz beendet beide Instanzen!

Mit einem notify in der ersten Instanz, kann man die zweite Instanz automatisch nach dem Start der ersten Instanz starten:
define n_StartFhem2 notify global:INITIALIZED "perl /opt/fhem/fhem.pl /opt/fhem/fhem2.cfg"

Damit läuft auch ein restart ohne Probleme: beide Instanzen werden nacheinander neu gestartet.
Beide Instanzen verwenden unterschiedliche Prozessorthreads/CPUs.

2. Variante: Rettungsweste

Wenn FHEM nicht mehr startet muss man Fehler suchen. Dazu ist es eigentlich nicht schlecht, mal eben "schnell" zu prüfen: Läuft es generell "ohne alles", was sagt das Log?
Der Ausgangszustand ist hier: FHEM läuft nicht! Wir bewegen uns im Terminal!

Erstmal prüfen, ob FHEM wirklich nicht läuft.
ps -aux|grep fhem
Prinzipiell passiert mit diesen Zeilen, das Gleiche wie in Variante 1, außer:
  • Nur der Name für den Statefile wird geändert, damit der Inhalt des originalen Statefile erhalten bleibt. 
  • Der Logfilename bleibt erhalten, man startet zwar ein  FHEM mit leerer cfg, hat aber den originalen Logfile im Zugriff.
n=4
cd /opt/fhem
sudo wget -qO fhem$n.cfg https://svn.fhem.de/fhem/trunk/fhem/fhem.cfg
sed -i "s/\/fhem.save/\/fhem$n.save/" fhem$n.cfg
perl fhem.pl fhem$n.cfg

Damit sieht/kann man:
  • ob FHEM startet (keine Problem mit dem Betriebssystem),
  • wie gewohnt das Logfile lesen. 

3. Variante: Völlig separat

In dieser Variante wird folgendes eingerichtet:
  • neuer User, 
  • neues Port, 
  • der existierende /opt/fhem Pfad wird in einen neuen Pfad /opt/fhemN kopiert, 
  • neuer zweiter FHEM Service, 
  • neue leere fhem.cfg wird verwendet.
Ein installiertes FHEM  mit Systemd ist Voraussetzung!

Das Script / die Befehle alle im sudo Kontext (sudo su) ausführen!
#!/bin/bash
# Nummer/Username und Port vergeben
n=3
fPort=8103

# Dienst User anlegen, Gruppen zuordnen
adduser fhem$n --home /opt/fhem$n --ingroup dialout --disabled-login --gecos ""
usermod -G audio,davfs2 fhem$n

# aktiven Pfad kopieren
cp -r /opt/fhem/* /opt/fhem$n/

# fhem Service kopieren, anpassen und anlegen
cp /etc/systemd/system/fhem.service /etc/systemd/system/fhem$n.service
sed -i -e "s/User=fhem/User=fhem$n/;s/\/fhem/\/fhem$n/;s/fhem.cfg/fhem$n.cfg/" /etc/systemd/system/fhem$n.service
systemctl --system daemon-reload
systemctl enable fhem$n

# Leere cfg holen und anpassen
wget -qO /opt/fhem$n/fhem.cfg https://svn.fhem.de/fhem/trunk/fhem/fhem.cfg
sed -i -e "s/8083/$fPort/;\$aattr initialUsbCheck disable 1" /opt/fhem$n/fhem.cfg

#Besitzer setzen
chown -R fhem$n: /opt/fhem$n/

# Service starten
systemctl start fhem$n

Achtung, das kopiert und modifiziert die Version von fhem.cfg und fhem.service vom Entwicklungsstand heute! Wenn sich in Zukunft etwas ändert, muss dieser Code angepasst werden!

Mittwoch, 20. Februar 2019

FHEM HTTP Client

Der eingebaute Client Modus in der fhem.pl funktioniert nur über die Telnet Schnittstelle von FHEM, diese wird aber per default nicht gar nicht mehr definiert.
Ich habe mal in 3 Varianten einen FHEM Client gebaut: als Bash-, Powershell- und Perlscript.

Der FHEM Client verfügt einheitlich über folgende Möglichkeiten:
  • Angabe komplette URL oder nur Portnummer (lokaler Zugriff) [http://<Username>:<Password>@<hostName>:]<portNummer>
    • Zugriff über Standard WEB sofort nach FHEM Installation möglich, csrf Token wird verwendet.
    • Angabe von Basic Auth in der URL möglich.
  • FHEM Befehle als:
    • Argument(e) (analog fhem.pl)
    • Dateiname der Befehlsdatei mit Codeschnipseln (z.B. ganz oder teilweise fhem.cfg).
    • Zeilen über die Pipeline (cat Befehlsdatei | fhemcl 8083)
    • Mehrzeilige Definitionen (Zeilenende mit "\")werden verarbeitet.
  • Ausgabe der FHEM Antwort, z.B. bei list Befehlen, analog zum Client Modus in fhem.pl
  • Kurze Hinweise zur Verwendung
Getestet habe ich die Bash und Perl Scripts unter Raspbian, das Powershell Script unter Windows 10. Mit Sicherheit ist es noch nicht völlig frei von bugs! Verwendung also auf eigene Gefahr!

Offene Punkte/derzeit bekannte Bugs


Allgemeiner Ablauf


  1. Es wird zunächst das erste Argument getestet, dies muss vorhanden sein. Es erfolgt ein Test ob nur die Portnummer angegeben wurde, diese wird um localhost erweitert ansonsten wird die url ohne weitere Prüfung übernommen. 
  2. Bei Powershell wird aus username:password noch ein extra "Credential" String gemacht, da Invoke-Webrequest die url nicht einfach so verarbeitet wie curl.
  3. Der csrfToken wird extrahiert und gespeichert.
  4. Dann wird ein Array mit den FHEM Befehlen gebildet, dazu wird 
    • die Pipeline getestet und gelesen
    • das zweite Argument gelesen und auf Dateiname getestet
    • entweder die Datei oder weitere Argumente eingelesen 
  5. Zum Schluss wird eine Schleife über das Befehlsarray abgearbeitet, die Befehle url-encoded  und an den FHEM Server übergeben und die Antwort ausgewertet.
  6. In der Antwort wird nur der Block <pre> </pre> ohne HTML Tags ausgegeben.  

Die Scripts

Bash Variante
#!/bin/bash
# Heinz-Otto Klas 2019
# send commands to FHEM over HTTP
# if no Argument, show usage

if [ $# -eq 0 ]
then
     echo 'fhemcl Usage'
     echo 'fhemcl [http://<hostName>:]<portNummer> "FHEM command1" "FHEM command2"'
     echo 'fhemcl [http://<hostName>:]<portNummer> filename'
     echo 'echo -e "set Aktor01 toggle" | fhemcl [http://<hostName>:]<portNumber>'
     exit 1
fi

# split the first Argument
IFS=:
arr=($1)

# if only one then use as portNumber
# or use it as url
IFS=
if [ ${#arr[@]} -eq 1 ]
then
    if [[ `echo "$1" | grep -E ^[[:digit:]]+$` ]]
    then
        hosturl=http://localhost:$1
    else
        echo "$1 is not a Portnumber"
        exit 1
    fi
else
    hosturl=$1
fi

# get Token 
token=$(curl -s -D - "$hosturl/fhem?XHR=1" | awk '/X-FHEM-csrfToken/{print $2}')

# reading FHEM command, from Pipe, File or Arguments 
# Check to see if a pipe exists on stdin.
cmdarray=()
if [ -p /dev/stdin ]; then
        echo "Data was piped to this script!"
        # If we want to read the input line by line
        while IFS= read -r line; do
              cmdarray+=("${line}")
        done
else
        # Checking the 2 parameter: filename exist or simple commands
        if [ -f "$2" ]; then
            echo "Reading File: ${2}"
            readarray -t cmdarray < ${2}
        else
        echo "Reading further parameters"
        for ((a=2; a<=${#}; a++)); do
            echo "command specified: ${!a}"
            cmdarray+=("${!a}")
        done
        fi
fi

# loop over all lines stepping up. For stepping down (i=${#cmdarray[*]}; i>0; i--)
for ((i=0; i<${#cmdarray[*]}; i++));do 
    # concat def lines with ending \ to the next line
    cmd=${cmdarray[i]}
    while [ ${cmd:${#cmd}-2:1} = '\' ];do 
          ((i++))
          cmd=${cmd::-2}$'\n'${cmdarray[i]}
    done
    echo "proceeding Line $i : "${cmd}
    # urlencode loop over String
    cmdu=''
    for ((pos=0;pos<${#cmd};pos++)); do
        c=${cmd:$pos:1}
        [[ "$c" =~ [a-zA-Z0-9\.\~\_\-] ]] || printf -v c '%%%02X' "'$c"
        cmdu+="$c"
    done
    cmd=$cmdu
    # send command to FHEM and filter the output (tested with list...).
    # give only lines between, including the two Tags back, then remove all HTML Tags 
    curl -s --data "fwcsrf=$token" $hosturl/fhem?cmd=$cmd | sed -n '/<pre>/,/<\/pre>/p' |sed 's/<[^>]*>//g'
done

Perl Variante
#!/usr/bin/env perl
# Heinz-Otto Klas 2019
# send commands to FHEM over HTTP
# if no Argument, show usage

use strict;
use warnings;
use URI::Escape;
use LWP::UserAgent;

my $token;
my $hosturl;
my $fhemcmd;

 if ( not @ARGV ) {
     print 'fhemcl Usage',"\n";
     print 'fhemcl [http://<hostName>:]<portNummer> "FHEM command1" "FHEM command2"',"\n";
     print 'fhemcl [http://<hostName>:]<portNummer> filename',"\n";
     print 'echo -e "set Aktor01 toggle" | fhemcl [http://<hostName>:]<portNumber>',"\n";
     exit;
 }

if ($ARGV[0] !~ m/:/) {
   if ($ARGV[0] eq ($ARGV[0]+0)) { # isnumber?
       $hosturl = "http://localhost:$ARGV[0]";
   }
   else {
       print "$ARGV[0] is not a Portnumber";
       exit(1);
   }
}
else {
    $hosturl = $ARGV[0];
}

# get token 
my $ua = new LWP::UserAgent;
my $url = "$hosturl/fhem?XHR=1/";
my $resp = $ua->get($url);
   $token = $resp->header('X-FHEM-CsrfToken');

my @cmdarray ;

# test the pipe and read 
if (-p STDIN) {
   while(<STDIN>) {
       chomp($_);
       push(@cmdarray,$_);
   }
}
# second Argument is file or command?
if ($ARGV[1] and -e $ARGV[1]) {
    open(DATA, '<', $ARGV[1]);
    while(<DATA>) {
       s/\r[\n]*/\n/gm;      #remove any \r 
       chomp($_);
       push(@cmdarray,$_);
    }
    close(DATA);
}
else {
    for(my $i=1; $i < int(@ARGV); $i++) {
    push(@cmdarray, $ARGV[$i]);
    }
}
#execute commands and print response from FHEMWEB 
for(my $i = 0; $i < @cmdarray; $i++) {
    # concat def lines with ending \ to the next line
    my $cmd = $cmdarray[$i];
    while ($cmd =~ m/\\$/) {
        $i++;
        $cmd = substr($cmd,0, -1)."\n".$cmdarray[$i];
    };
    # url encode the cmd
    $fhemcmd = uri_escape($cmd);
    print "proceeding line $i : $fhemcmd\n";
    $url = "$hosturl/fhem?cmd=$fhemcmd&fwcsrf=$token";
    $resp = $ua->get($url)->content;
    # only between the lines <pre></pre> and remove any HTML Tag
    #funktioniert noch nicht sauber bei massenimport
    my @resparray = split("\n", $resp);
    foreach my $zeile(@resparray){
        if ($zeile !~ /<[^>]*>/ or $zeile =~ /pre>/ or $zeile =~ /NAME/) {
           $zeile =~ s/<[^>]*>//g;
           print "$zeile\n" ;
        }
    }
}

Powershell Variante
<#
.SYNOPSIS
    This Script is a FHEM Client for HTTP
.DESCRIPTION
    FHEM commands could given over the Pipe, Arguments or File.
.EXAMPLE
    fhemcl [http://<hostName>:]<portNummer> "FHEM command1" "FHEM command2"
    fhemcl [http://<hostName>:]<portNummer> filename
    echo "FHEM command"|fhemcl [http://<hostName>:]<portNummer>
.NOTES
    put every FHEM command line in ""
#>
#region Params
param(
    [Parameter(Mandatory=$true,Position=0,HelpMessage="-first 'Portnumber or URL'")]
    [String]$first,
    [Parameter(ValueFromPipeline=$true,ValueFromRemainingArguments=$true)]
    [String[]]$sec
)
#endregion 

# if only one element the use as portNumber
# or use as hosturl
$arr = $first -split ':'
if ($arr.Length -eq 1){
   if ($first -match '^\d+$') {$hosturl="http://localhost:$first"}
       else {
           write-output "is not a Portnumber"
           exit
            }
 } else {$hosturl=$first}
# url contains usernam@password?
if ($arr.Length -eq 4){
     $username = $($arr[1] -split'//')[1]
     $password = $($arr[2] -split '@')[0]
     $base64AuthInfo = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes(("{0}:{1}" -f $username,$password)))
     $headers = @{
     Authorization=("Basic {0}" -f $base64AuthInfo)
     }

     # cut the account from hosturl 
     $hosturl=$arr[0] + "://"+$($arr[2] -split '@')[1] +":" + $arr[3]
}
# get Token
$token = Invoke-WebRequest -UseBasicParsing -Headers $headers -Uri "$hosturl/fhem?XHR=1" | %{$_.Headers["X-FHEM-csrfToken"]}

# reading commands from Pipe, File or Arguments 
# clear cmdarray and save the Pipeline,
# $input contains all lines from pipeline, $sec contains the last line
$cmdarray=@()
foreach ($cmd2 in $input){$cmdarray += $cmd2}
if ($cmdarray.length -eq 0) {
     if((Test-Path $sec) -And ($sec.Length -eq 1)) {$cmdarray = Get-Content $sec} 
     else {foreach ($cmd2 in $sec){$cmdarray += $cmd2}}
}
# send all commands to FHEM
# there is still an error message with Basic Auth and commands like set Aktor01 ..  e.g. list is without any error.

for ($i=0; $i -lt $cmdarray.Length; $i++) {
   # concat def lines with ending \ to the next line
   $cmd = $cmdarray[$i]
   while($cmd.EndsWith('\')) {$cmd=$cmd.Remove($cmd.Length - 1,1) + "`n" + $cmdarray[$i+1];$i++}
   write-output "proceeding line $($i+1) : $cmd"
   # url encode
   $cmd=[System.Uri]::EscapeDataString($cmd)
   $web = Invoke-WebRequest -Uri "$hosturl/fhem?cmd=$cmd&fwcsrf=$token" -Headers $headers
   if ($web.content.IndexOf("<pre>") -ne -1) {$web.content.Substring($web.content.IndexOf("<pre>"),$web.content.IndexOf("</pre>")-$web.content.IndexOf("<pre>")) -replace '<[^>]+>',''}
}

Verwendung

Ich habe alle Scripte auf GitHub abgelegt und halte sie dort auch aktuell.
Am einfachsten kann man sich die Scripts per Download auf das System holen, das geht ziemlich einheitlich (auch unter Windows/Powershell) mit wget:
wget -OutFile fhemcl.ps1 https://raw.githubusercontent.com/heinz-otto/fhemcl/master/fhemcl.ps1
wget -O fhemcl.pl https://raw.githubusercontent.com/heinz-otto/fhemcl/master/fhemcl.pl
wget -O fhemcl.sh https://raw.githubusercontent.com/heinz-otto/fhemcl/master/fhemcl.sh
Anmerkung. -OutFile anstatt -O braucht man auf älteren Windows Systemen. Bei Windows 10 reicht -O.

Benchmark

Ich habe mal Schleifen mit 10 gleichen Befehlen (set Aktor01 toggle) abgesetzt um zu sehen ob es große Unterschiede bei den realisierten Scripts gibt.

time for ((i=0; i<10; i++));do perl fhemcl.pl http://raspib:8083 "set Aktor01 toggle";done
time for ((i=0; i<10; i++));do bash fhemcl.sh http://raspib:8083 "set Aktor01 toggle";done
Measure-Command {for ($i=1; $i -le 10; $i++) {.\fhemcl.ps1 http://raspib:8083 "set Aktor01 toggle"}}
Die Bash und Powershell Variante läuft etwa gleich schnell ab und braucht für die 10 Durchläufe etwas über 3 sec. Die Perl Variante benötigt je nach Platform (Pi1/Pi2/Pi3) 2 bis 10 mal solange.

Code Block


Mittwoch, 30. Januar 2019

VHD - was geht, was nicht?

VHD erzeugen und verwenden

VHDs sind Container Dateien, die ein Festplattenimage enthalten. Ich setze sie der neben Virtualisierung gerne ein, um alte Datenpartitionen zu erhalten oder separate Festplatten zu simulieren. Um sie schnell mal zu erzeugen und auch für "normale" Benutzer bereitzustellen, gibt es verschiedenen Möglichkeiten.

In jedem aktuellen Windows System vorhanden:
Explorer: Man kann VHD Dateien mounten/dismounten - einfach per Doppelklick oder rechter Maustaste. Das funktioniert auch mit normalen Benutzerrechten.
Alle anderen Möglichkeiten brauchen Adminrechte.
diskpart/Datenträgerverwaltung: Mit beiden Tools kann man VHD Dateien erzeugen und mounten/dismounten.
Die Powershell kennt zwei Sets an Cmdlets.
(Dismount, Get, Mount)-DiskImage
Diese Cmdlets sind wahrscheinlich in jeder Windows Version vorhanden. Sie funktionieren mit VHD und ISO Dateien. Für die Verwendung von VHD Dateien braucht man Administrator Rechte.
*-VHD, *-VHDSet, *-VHDSnapshot
Ein neues, umfangreiches Set an Cmdlets welches erst mit der Hyper-V Rolle installiert wird. Mit denen kann man VHDs auch erstellen und umfangreich manipulieren. Doku.

Automatische Bereitstellung.

Zur Laufzeit kann man VHD Dateien leicht interaktiv einbinden und auswerfen, aber kann man VHD Dateien als HDD Ersatz auch beim Systemstart bereitstellen?
Das geht per Taskplaner und diskpart Script.

Praktisches Beispiel

Schritt für Schritt entsteht so eine dynamische VHD Datei mit 10 GB und einer Partition.
Achtung: Die Powershellversion funktioniert nur auf einem Hyper-V "aktiviertem" System:
$VDisk = "D:\VHD\TestPS.vhdx"
New-VHD -Path $VDisk -Dynamic -SizeBytes 10GB
$Disk = Mount-VHD -Path $VDisk -Passthru|Get-Disk
$Disk|Initialize-Disk 
$Disk|New-Partition -UseMaximumSize -AssignDriveLetter|Format-Volume -NewFileSystemLabel "Meine VHD"
Dismount-VHD -Path $VDisk
Schritt für Schritt in diskpart
create vdisk file=D:\VHD\TestDP.vhdx type=expandable maximum=10240
select vdisk file=D:\VHD\TestDP.vhdx 
attach vdisk
create partition primary
format FS=NTFS LABEL="MEINE VHD" QUICK
assign
detach vdisk
Alle diskpart Befehle kann man in eine Textdatei packen, und diese dann als Parameter übergeben.
diskpart /s Scriptdatei
Für die automatische Bereitstellung braucht man nun bloß noch ein paar Befehle. Für Powershell ist das Script ein Einzeiler
Mount-VHD -Path "D:\VHD\TestPS.vhdx"
# Oder so
Mount-DiskImage "D:\VHD\TestPS.vhdx"
Um es mit diskpart automatisch zu erledigen, braucht man wieder ein kurzes Script. Dieses startet man dann wie oben.
select vdisk file="D:\VHD\TestDP.vhdx"
attach vdisk
exit

Das Vorgehen bis hierher hat eventuell ein Problem: Die Zuweisung eines Laufwerkbuchstabens. Das passiert im Zweifelsfall einfach nicht. Wenn dem System die VHD nicht bekannt ist, wird sie eventuell ohne Laufwerksbuchstaben bereitgestellt. Bei dem Diskpart Script kann man das einbauen, der Mount-VHD Befehl kennt nur die Option -NoDriveLetter, bei Mount-DiskImage hat man gar keine Option.

Im Taskplaner (Aufgabenplanung) kann man eine Task für den Systemstart einrichten, damit steht die VHD nach dem Start für alle Benutzer zur Verfügung. So geht es:
Registerkarte Allgemein
Wichtig: Benutzer SYSTEM verwenden und den "Mit höchsten Privilegien" Haken setzen.
Registerkarte Trigger
"beim Start" auswählen.
Registerkarte Aktion
Hier in zwei getrennten Boxen den Programmnamen und die Argumente eintragen:
powershell
# entweder als Befehlsblock
-Command Mount-DiskImage "D:\VHD\TestPS.vhdx"
# alternativ als Scriptfile
-ExecutionPolicy Bypass -File "C:\Tools\Scripts\mountvhd.ps1"

diskpart
/s "C:\Tools\Scripts\mountvhd.txt"
Mit "Ausführen" sollte man die Aufgabe nach dem Erstellen direkt testen!

Um den Laufwerksbuchstaben der gemounteten VHD Datei zu ermitteln, muss man eine Kette von Cmdlets bemühen (Doku):
(Get-DiskImage -ImagePath $VDisk |Get-Disk|Get-Partition|Get-Volume).DriveLetter

Montag, 7. Januar 2019

Kalender in FHEM - auf bestimmte Termine reagieren

Die Grundlage schaffen - ein Calendar Device

Für dieses Beispiel: ein Google Kalender. Als Grundlage brauchen wir die "Privatadresse" im iCal Format (Google Kalender Einstellungen/<Kalendername>/Kalendereinstellungen).
Diese URL wird einfach in die Calendar Definition eingesetzt, das Aktualisierungsintervall setze ich auf einen Tag. Man kann jederzeit ein reload des Kalenders durchführen. Man muss bedenken, dass vor allem viele Serientermine, eine nicht unerheblich Zeit beim reload beanspruchen. Das kann schnell mal mehrere Minuten dauern. In der Grundeinstellung erfolgt das blockierend. Je nach Kalender, kann also nach dem define die Oberfläche für ein paar Minuten "stehen"!
define TestKalender Calendar ical url https://calendar.google.com/calendar/ical/xxx/basic.ics 86400
Um zu testen, ob der Kalender richtig gelesen wird, kann man ihn jetzt einfach abfragen. Eine direkte Anzeige der Termine in Readings erfolgt nämlich nicht.

Abfragen machen

Die Abfrage der Termine ist nicht offensichtlich, es gibt aber einige Beispiele in der englischen Doku. Als Ausgangspunkt für mein Beispiel nehme ich mal die Weboberfläche. In der zweiten Auswahlbox in der get Zeile wählt man einfach events aus.
Als Ergebnis bekommt man in einer Box eine Liste der Kalendereinträge (events), ich habe heute (6.1.) drei Einträge drin. Wie man sieht, auch vergangene.
04.01.2019 08:00 4h Sprechstunde
06.01.2019 08:00 14h Heute frei
07.01.2019 08:00 11h Sprechstunde
In der leeren Box kann man weiter format und filter Angaben machen.

Das Ziel in diesem Beispiel soll sein, auf einen ganz bestimmten Eintrag im Kalender zur Startzeit des Termins etwas auszulösen: z.B. den Server an den Tagen wo Sprechstunde ist, kurz vor Arbeitsbeginn zu starten.

Event auswählen

Dazu erzeugt man am Besten einen Termin in naher Zukunft und schaut sich mit dem Eventmonitor die Events an:
2019-01-06 20:00:00 Calendar TestKalender changed: 79vs3fq7siulo1hskdn4gtht7kgooglecom start
2019-01-06 20:00:00 Calendar TestKalender start: 79vs3fq7siulo1hskdn4gtht7kgooglecom 
2019-01-06 20:00:00 Calendar TestKalender modeUpcoming: 7fcbh4r7snu7iqovask7l8oq9qgooglecom
2019-01-06 20:00:00 Calendar TestKalender modeAlarmOrStart: 44318rlssm81janveuga0olanpgooglecom;79vs3fq7siulo1hskdn4gtht7kgooglecom
2019-01-06 20:00:00 Calendar TestKalender modeChanged: 79vs3fq7siulo1hskdn4gtht7kgooglecom
2019-01-06 20:00:00 Calendar TestKalender modeStart: 44318rlssm81janveuga0olanpgooglecom;79vs3fq7siulo1hskdn4gtht7kgooglecom
2019-01-06 20:00:00 Calendar TestKalender modeStarted: 79vs3fq7siulo1hskdn4gtht7kgooglecom
2019-01-06 20:00:00 Calendar TestKalender triggered
2019-01-06 20:00:00 Calendar TestKalender nextWakeup: 2019-01-06 20:05:00
2019-01-06 20:05:00 Calendar TestKalender changed: 79vs3fq7siulo1hskdn4gtht7kgooglecom end
2019-01-06 20:05:00 Calendar TestKalender end: 79vs3fq7siulo1hskdn4gtht7kgooglecom 
2019-01-06 20:05:00 Calendar TestKalender modeAlarmOrStart: 44318rlssm81janveuga0olanpgooglecom
2019-01-06 20:05:00 Calendar TestKalender modeStart: 44318rlssm81janveuga0olanpgooglecom
2019-01-06 20:05:00 Calendar TestKalender modeStarted: 
2019-01-06 20:05:00 Calendar TestKalender modeEnd: 26aq76ljdgjfmo444466faml09googlecom;79vs3fq7siulo1hskdn4gtht7kgooglecom
2019-01-06 20:05:00 Calendar TestKalender modeEnded: 79vs3fq7siulo1hskdn4gtht7kgooglecom
2019-01-06 20:05:00 Calendar TestKalender triggered
2019-01-06 20:05:00 Calendar TestKalender nextWakeup: 2019-01-06 22:00:00
Man sieht jeweils 9 Events beim Start des Termines und 9 Events beim Ende. Allerdings keine Information über den "Lesbaren" Inhalt des Termines, diese kann man mittels der uid ($EVTPART1) auslesen. Es gibt zwei Events, jeweils den ersten und zweiten, die für einen trigger interessant sind.
TestKalender:changed:.*
TestKalender:changed:.*start
TestKalender:start:.*
Beim ersten regExp müsste/könnte man mit $EVTPART2 im Code abfragen ob es start oder end war. Bei den beiden anderen wird eindeutig auf start|end|alarm getriggert.

Exakte Abfrage auf einen Termin

Jetzt kann man im Code noch exakt abfragen, ob es sich um den richtigen Kalendereintrag handelt.
fhem('get '.$NAME.' events filter:uid=="'.$EVTPART1.'",field(summary)=~"(?i)sprechstunde" limit:count=1,from=+0d',1)
Die eigentliche Aktion ist damit recht simpel. Wenn die Abfrage des Events exakt genug ist, liefert sie nur bei Übereinstimmung ein Ergebnis. Damit sind alle notwendigen Komponenten komplett.
{fhem("set Server on") if defined fhem('get '.$NAME.' events filter:uid=="'.$EVTPART1.'",field(summary)=~"(?i)sprechstunde|notdienst" limit:count=1,from=+0d',1)}

Starte das Gerät zum Zeitpunkt

Ein notify, welches zu Beginn eines Kalenderevents, der den Begriff "Sprechstunde" in der Terminbeschreibung enthält, den Server startet:
define n_TestKalender notify TestKalender:changed:.*start {\
fhem("set Server on") if defined fhem('get '.$NAME.' events filter:uid=="'.$EVTPART1.'",field(summary)=~"(?i)sprechstunde" limit:count=1,from=+0d',1)\
}
Die Sache hat noch einen Schönheitsfehler: Die Sprechstunde beginnt zwar um 8:00 Uhr aber es wäre gut, wenn der Server  schon gestartet ist, wenn alle Mitarbeiter den Dienst beginnen. Dazu brauchen wir den Event nicht exakt zum Termin sondern vorher. Das kann das Calendar Modul erledigen. Mit diesem Attribute wird ein zusätzlicher alarm Event eine Stunde (3600 sec) vorm Termin in Abhängigkeit der Terminbeschreibung erzeugt.
attr TestKalender onCreateEvent { $e->{alarm}= $e->{start}-3600 if($e->{summary} =~ m/Sprechstunde/i)}

Starte das Gerät vor dem Zeitpunkt

Der Trigger im notify muss lediglich von start auf alarm geändert werden, der Ausführungsteil bleibt  identisch.
defmod n_TestKalender notify TestKalender:changed:.*alarm {}
Der Event für start und end bleibt erhalten, zu diesem Zeitpunkt kann man andere Aktionen ausführen.

Noch ein paar zusätzliche Tipps und Infos

Serientermine einer Serie (z.B. jeden Mittwoch 8:00) haben alle die gleiche UID. Bei einem Einzeltermin hätte man im notify über die uid den konkreten Zugriff auf genau den Termin, beim Serientermin erscheinen alle Termine der Serie, auch vergangene! Diese uid bleibt auch erhalten wenn man mal einen Termin der Serie modifiziert (Beschreibung, Zeit usw.)
Die Modi (alarm|start|end|upcoming) sind transient und geben quasi Auskunft über den aktuellen Status eines Termins (get <Kalender> events filter:mode=="<modus>").
  • upcoming - der Termin liegt in der Zukunft
  • end - der Termin ist abgelaufen und liegt in der Vergangenheit
  • start - der Termin ist gerade aktiv
  • alarm - die Alarmphase ist aktiv, also der Event alarm ist vorüber, der Event start noch nicht erreicht.

Noch ein paar Codebeispiele

toDo
Code Block

Code Block

Dienstag, 4. Dezember 2018

Diskpart vs Powershell

Um Festplattenpartitionen zu behandeln - was sollte man nehmen - Diskpart oder Powershell?

Achtung beim Probieren! der clean Befehl innerhalb diskpart wird ohne Nachfrage ausgeführt und löscht die aktive disk!
Die Powershell Befehle fragen zwar nach, Standardantwort ist aber Yes. Also auch hier Vorsicht!

Diskpart lässt sich zwar auch scripten, aber etwas ungewöhnlich. Mit Powershell lässt sich viel mehr machen. Aber die Befehle sind auch zum Teil komplexer.

Ich fange hier einfach mal eine Tabelle, die ich vervollständigen kann:

list disk get-disk|sort number
sel disk 0
list part
Get-Partition -disknumber 0
sel disk 1
clean
Clear-Disk -Number 1 -RemoveData -RemoveOEM
list volume Get-Volume
sel disk 1
(clean)
convert gpt
Initialize-Disk -Number 1
sel disk 1
(clean)
convert mbr
Initialize-Disk -Number 1 -PartitionStyle MBR
sel disk 5
attributes disk clear readonly
Set-Disk -Number 5 -IsReadonly $False






Das geht so richtig gut nur in Powershell

Wo liegt das Laufwerk E?
Get-Partition -DriveLetter E|select DiskNumber,PartitionNumber,DriveLetter,Offset,Size,Type|format-table

Welche Festplatte hat welche Laufwerke?
Get-Volume|Get-Partition |select DiskNumber,PartitionNumber,DriveLetter,Offset,Size,Type|sort DiskNumber,DriveLetter|Format-Table

Erzeuge neues Volume auf Disk 1 mit definiertem DriveLetter
New-Partition -Disknumber 1 -DriveLetter W -UseMaximumSize |Format-Volume

Ein Volume umbenennen
Set-Volume -DriveLetter W -NewFileSystemLabel "Data1"

Sonntag, 18. November 2018

Windows Image

Die Windows Installation (so wie ist) auf eine neue Festplatte ziehen - geht auch mit Boardmitteln.

Achtung! Diese Anleitung enthält Befehle, die bei falscher Anwendung Daten löschen können! Bitte unbedingt zwischen Copy & Paste auch den Kopf verwenden!

Was wird benötigt?
  • Ein Startmedium mit Windows PE: USB Stick oder CD (ISO)
  • Einen genügend großen Zwischenspeicher: USB Festplatte oder Netzlaufwerk,
  • ein paar Tools von Windows und
  • gewusst wie.

Ablauf

  1. Den Computer mit Windows PE starten (Man kann Quell und Ziel Laufwerk anschließen)
  2. Die Quell Laufwerk mit dism in ein image "aufzeichnen".
  3. Optional: Umbauen und wieder mit WinPE starten.
  4. Das Ziel Laufwerk partitionieren.
  5. Das Image auf das Ziel Laufwerk "anwenden".
  6. Startsystem wieder herstellen
Es ist also ziemlich simpel, die Befehle und Schritte sind im Prinzip im zweiten Link beschrieben. Ich will dies hier aber mit etwas Erfahrungen und Tipps ausbauen.

Windows PE

Es gibt Windows PE in 32 und 64 bit Version. Obwohl WinPE mit einem einfachen CMD Fenster startet, kann man von dort die meisten (portablen) Programme und Tools einfach starten. Aber - die 64 bit Version hat kein 32 bit Subsystem, mann kann also immer nur Programme der gleichen Architektur starten!
Ich habe darauf verzichtet WinPE aufwendig anzupassen. Ich habe einfach ein paar Scripte erstellt, einen Teil habe ich auf dem USB Stick bereitgestellt und Andere einfach auf einem zentralen Netzwerklaufwerk abgelegt.
Es genügt ein kleiner USB2 Stick, WinPE ist nicht groß (ca. 300 MB ohne Anpassung, ca 450 MB mit Powershell) und die Ladezeiten sind nicht sehr lang.
An meinem System lagen die Zeiten zwischen 25 und 30 sec, der Unterschied USB2 und USB3 sowie Powershell ging quasi in der "Nachdenkzeit" des Reset und BIOS unter.

Damit man mit der Tastatur klar kommt braucht man ein Startscript.
wpeutil SetKeyboardLayout 0407:00000407
start cmd
Die Tastatur ist sonst mit englischem Layout und die Änderung mit SetKeyboardLayout wirkt erst im neuen Fenster.
Um eine Netzwerkverbindung herzustellen ist dies sinnvoll. Dieses Zeile enthält auch einen Trick:
Hat man ein Linux SMB Share, welches mit anonymen Zugriff konfiguriert ist, kann sich WinPE nicht ohne Konto verbinden (anonymer Zugriff ist auch in Windows seit Version 10 1709 abgeschaltet).
Bei WinPE hilft es einfach einen Benutzer anzugeben den es nicht gibt: net use ist zufrieden und Linux ignoriert es.
net use * \\server1\Shares /user:willi password 
net use * \\server1\Sicherung  
Natürlich kann man die paar Zeilen auch in ein CMD Script packen und auf dem USB Stick bereitstellen.
Die beiden net use Befehle erzeugen ein Laufwerk z: und y: - das Laufwerk x: ist das Systemlaufwerk von WinPE.

Image aufzeichnen

Zunächst  muss man ermitteln, welche Laufwerksbuchstaben WinPE für die eingehängten Festplatten vergeben hat. Dazu gibt es viele Möglichkeiten, ziemlich eindeutig und gezielt funktioniert es mit diskpart. Am Prompt: DISKPART> gibt man folgende Reihenfolge ein:
list disk
sel disk 0
detail disk
Der erste Befehl zeigt alle eingebauten Festplatten.
Der zweite Befehl selektiert die Quellfestplatte, die Ziffer ist also entsprechend anzupassen.
Der dritte Befehl zeigt alle Details der gewählten Disk inklusive der dort enthaltenen Volumes und Laufwerks Buchstaben.
Tipp: Auch gleich die Nummer für das Zielaufwerk merken, die wird im nächsten Abschnitt benötigt.
Ein Beispiel zum Aufzeichnen des Images von Laufwerk C
dism /capture-image /imagefile:y:\wim\backup.wim /capturedir:c: /name:C-WinOS
Man kann recht einfach ein Beispielscript im Laufwerk z: vorbereiten und mit notepad (läuft unter WinPE) anpassen und dann ausführen. So spart man sich viel Tipparbeit. Die meiste Zeit wird die Bandbreite zum Ziellaufwerk sehr gut genutzt, die Geschwindigkeit für die Aufzeichnung hängt also vom Umfang, Geschwindigkeit Quelle und Netzlaufwerk (oder USB Laufwerk) ab.

Ziel Laufwerk partitionieren

Das Ziellaufwerk wird in der Regel neu partitioniert und kann dabei auch völlig neu strukturiert werden! Beispiel:
War das Quellsystem eine MBR Partition kann die Ziel Platte ein GPT System werden (falls der PC UEFI unterstützt).
Achtung! Wird jetzt die falsche Disk verwendet, werden ohne Nachfrage Daten gelöscht!
Die Empfehlung von Microsoft (man findet Unterschiedliche) für eine GPT Partitionsstruktur aus meinem zweiten Link sieht als diskpart Script wie folgt aus:
rem == CreatePartitions-UEFI.txt ==
rem == These commands are used with DiskPart to
rem    create four partitions
rem    for a UEFI/GPT-based PC.
rem    Adjust the partition sizes to fill the drive
rem    as necessary. ==
select disk 0
clean
convert gpt
rem == 1. System partition =========================
create partition efi size=100
rem    ** NOTE: For Advanced Format 4Kn drives,
rem               change this value to size = 260 ** 
format quick fs=fat32 label="System"
assign letter="S"
rem == 2. Microsoft Reserved (MSR) partition =======
create partition msr size=16
rem == 3. Windows partition ========================
rem ==    a. Create the Windows partition ==========
create partition primary 
rem ==    b. Create space for the recovery tools ===
shrink minimum=500
rem       ** NOTE: Update this size to match the
rem                size of the recovery tools 
rem                (winre.wim)                    **
rem ==    c. Prepare the Windows partition ========= 
format quick fs=ntfs label="Windows"
assign letter="W"
rem === 4. Recovery tools partition ================
create partition primary
format quick fs=ntfs label="Recovery tools"
assign letter="R"
set id="de94bba4-06d1-4d40-a16a-bfd50179d6ac"
gpt attributes=0x8000000000000001
list volume
exit
Bevor jetzt das Script mit dieser Befehlszeile angewendet wird, ist wie im vorherigen Abschnitt bitte genau zu prüfen auf welche Disk mit welcher Nummer das Ziel geschrieben werden soll! Das Script kann man einfach mit Notepad ändern und speichern.
DiskPart /s Z:\Scripts\CreatePartitions-UEFI.txt

Image anwenden

Für den jetzigen Schritt sind eigentlich alle Vorbereitungen getroffen und man kann direkt loslegen. Ich habe hier das Script zur Anwendung mal etwas vor dem "Doppelklick" entschärft.
@if "%1" == "" goto usage 
dism /Apply-Image /ImageFile:%1 /Index:1 /ApplyDir:%2
goto end
:usage
@echo Nichts passiert: Parameter fehlen, bitte so verwenden
@echo apply y:\wim\backup.wim W:\
:end

Startsystem erzeugen

Jetzt muss man mindestens noch die neue Platte "start-fähig" machen. Dazu genügt in der Regel ein Befehl. Wieder als ungefährliches Script:
@if "%1" == "" goto usage 
bcdboot %1 /s %2
goto end
:usage
@echo Nichts passiert: Parameter fehlen, bitte so verwenden
@echo makeboot W:\Windows S:
:end
Jetzt kann man mit dem Befehl
wpeutil shutdown
das System herunterfahren, den USB Stick und die Quellplatte entfernen und das System sollte sich mit der neuen Platte einfach starten lassen.

Recovery System erzeugen

Eigentlich fehlt noch etwas, das Recovery System. Vielleicht war es auf der Quellplatte schon nicht vorhanden, aber bei einer Windows Installation gehört es dazu. Solange die Laufwerke aus Schritt 4 noch vorhanden sind, sollte dieses Script das Recovery System einrichten.
@if "%1" == "" goto usage 
md %1
copy %2\System32\Recovery\winre.wim %1\winre.wim
%2\System32\reagentc /setreimage /path %1 /target %2
goto end
:usage
@echo MakeRecovery R:\Recovery\WindowsRE W:\Windows
:end
Man kann das Recovery System auch später im laufenden System einrichten.

Windows PE bekommen

Windows PE kann man aus unterschiedlichsten Quellen verwenden und "bauen". Ich habe einfach das aktuelle Windows ADK heruntergeladen, man braucht die Deployment Tools und das Windows PE AddOn (Details im ersten Link)
Das "Kommandocenter" für die Umgebung öffnet man nach der Installation mit "Windows + Bereit (tippen)" + rechte Maustaste "Als Administrator ausführen".
Hier braucht man nur zwei Befehle, Laufwerk mit 600-800 MB freiem Platz und einen freien USB Stick. Die Laufwerksbuchstaben und Pfade müssen angepasst  werden.
Achtung, der USB Stick wird (nach Nachfrage) neu formatiert!
copype amd64 D:\WinPE_amd64
MakeWinPEMedia /UFD D:\WinPE_amd64 Q:
Man kann natürlich auch ein ISO File erstellen, z.B. für Test in Hyper-V.
MakeWinPEMedia /ISO D:\WinPE_amd64 D:\ISO\WinPE_amd64.iso

Anmerkung für Dual Boot x86 und amd64:
Ich hatte hier schon mal einen dualen Windows Setup Stick gebaut. Ob die Anleitung mit aktuellem ADK funktioniert habe ich nicht getestet.

Probleme lösen

Ich hatte unklare, nicht reproduzierbare Probleme mit der 32 bit Version von WinPE und dism /Capture-Image. Ich habe daraufhin konsequent alles mit der 64 bit Version getestet. Ob die 32 bit Version ein generelles Problem hat kann ich nicht sagen.
Abbruch von dism
Beim Prozess "Aufzeichnen" hatte ich neben zu wenig Platz auf dem Netzlaufwerk Probleme die mit spartanischen Fehlermeldungen von dism abgebrochen wurden. dism schreibt aber normal eine Logdatei nach \Windows\Logs\DISM.
War der Vorgang erfolgreich sollte man die übrigens löschen, sonst ist sie beim nächsten Mal und einer Fehlersuche unnötig groß.
Durch die Hinweise im Log konnte ich die Fälle lösen.
  • chkdsk /f half beim Fehler mit dem Volumebit auf dem Laufwerk.
  • Löschen einer Datei im User Ordner \OneDrive half, weil dism offenbar eine "kaputte" Datei erst aus der Cloud komplett synchronisieren wollte.
  • Ein kleine verdächtige Datei im Browsercache des Users wurde vom Virenscanner erkannt. Ich hatte zu dem Zeitpunkt nicht WinPE gestartet sondern das normale System laufen und die Platte zusätzlich angesteckt.
Ich war durchaus froh über diese "Fehler" die sonst mit auf die neue Platte gewandert wären.
Der Virenscanner im laufenden System wird den Aufzeichnungsprozess verlangsamen! Aber man sollte durchaus erstmal das System auf der originalen Platte gründlich scannen!

Neue Platte startet nicht
Trotz identischem Vorgehen, hatte ich ein UEFI System, welches zunächst nicht starten wollte. Nachdem ich etwas konfus probiert und untersucht habe, war die Lösung ziemlich einfach:
  • Einfach solange in den Fehler starten (3 mal) bis Windows von sich aus in die Startoptionen startet. 
  • Dort einmal die Option: "Abgesichert mit Kommando Fenster" auswählen, anmelden und dann neu starten. Windows repariert so offenbar sein Startsystem automatisch.

Recovery System funktioniert nicht
Die obigen Befehle funktionieren scheinbar alle einwandfrei, aber im fertigen System meldet der Befehl
reagentc /info
das Recovery System als nicht verfügbar.
Der Befehl zum Aktivieren bringt eine Fehlermeldung
reagentc /enable
REAGENTC.EXE: Startkonfigurationsdaten können nicht aktualisiert werden.
Nach langer erfolgloser Suche habe ich einfach das probiert:
del c:\Windows\System32\Recovery\ReAgent.xml
Danach funktionierte /enable einwandfrei.

Zum Schluss

Ich bin oft für spartanische Lösungen, mit dem was gerade da ist. Windows hat seit langem schon alles an Board um eine Systeminstallation (die Systempartition) zu sichern und auf eine neue Festplatte zu bringen. Man braucht keine "bunten" Tools, die im Zweifelsfall auch bloß Dinge machen die man nicht versteht und die Fehlerhaft sein können.
Was ich hier aufgeschrieben habe funktioniert Schritt für Schritt "zu Fuß" und wenn man will mit Batch- oder Powershellscripts. Die c't hat vor Jahren schon mal ein Script für dism & Co vorgestellt welches noch weiter geht. Es erstellt eine Sicherung, die sich mit Setup wieder installieren lässt. (WIMage)
Diese Methode hat gegenüber dem üblicherweise verwendeten sektorbasiertem 1:1 Image durchaus Vorteile:
  • Dateisystem wird quasi auf Fehler überprüft
  • Quell und Zielsystem können unterschiedlich sein:
    • BIOS / UEFI
    • Partitionsgröße - man muss z.B. nicht vorher zeitaufwendig defragmentieren und verkleinern um ein System von einer großen Partition auf eine kleiner SSD zu migrieren
    • Partitionsanordnung
  • man könnte Ordner aus dem Aufzeichnungsprozess ausschließen (WimScript.ini - [ExclusionList])
  • unterschiedliches Dateisystem - muss ich noch untersuchen?
  • Zielsystem ist anschließend erstmal defragmentiert.

Nützliche Tools für WinPE

Die Anleitung zur Integration von Powershell findet man im dritten Link. Die Startzeit vergrößert sich dadurch etwas. 
Ein aktueller OpenSource Dateimanager mit 32 und 64 bit: DoubleCommander  
Ansonsten laufen relativ viele OpenSource Programme ohne Probleme.

Wichtige Quellen

Microsoft: Boot to Windows PE
Microsoft Aufzeichnen und Anwenden von wim Images
Microsoft WinPE mit Powershell