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=0',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=0',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=0',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.
Achtung: will man auf end triggern und den Termin überprüfen (z.B. Inhalt von summary) dann darf  hideOlderThan nicht auf 0 stehen! Man kann sich leicht mit 1 oder 2 (in sec) behelfen. Nach dem end Event ist der Termin aus der Liste verschwunden und nicht mehr lesbar! 

Noch ein paar Codebeispiele

toDo

Tipp: Die Abfrage der Alarmzeit funktioniert z.B: mit
get TestKalender events filter:mode=="alarm" format:full limit:count=1,from=0
Will man die Tage bis zum nächsten Event wissen (hier einfach der nächste, man kann den Filter natürlich anders setzen). In einer verschachtelten zweiten Abfrage wird dann noch 1 in "morgen" und 0 in "heute" gewandelt.
{my $day = int((fhem('get '.$name.' events format:custom="$t1" limit:from=0,count=1',1) + 86399 - time)/86400);
$day?eval{$day>1?$day:"morgen"}:"heute"}
Will man z.B. die Geburtstage aus dem Google Kontakten in einen ical Kalender kopieren, habe ich hier ein Google Script gefunden. Direkt hat man leider dafür keine ical Adresse verfügbar.