Dr. Watson und seine Erben Release 1 STE 26.07.2016 Inhaltsverzeichnis 1 . . . . . 1 2 3 4 4 5 2 WER goes Datenbank 2.1 Ausgangssituation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2.2 Refaktoring . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7 7 8 3 WER darstellen 3.1 Ausgangssituation . . . . . . . 3.2 Grafische Mockups/WireFrames 3.3 Umsetzung in PrimalForms . . 3.4 PS und DB-Anbindung . . . . . 3.5 Exportieren nach Excel . . . . . 4 5 Vorgeschichte 1.1 Error Report Dateien *.wer . . . . . . . . . . . . . . 1.2 Konfiguration von Windows Error Reporting (WER) . 1.3 Windows Error Reporting (WER) Tools . . . . . . . . 1.4 Windows PowerShell Error Reporting (WER) Cmdlets 1.5 Logfiles anstatt *.wer Dateien nutzen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 15 15 16 17 20 23 Implementierung 4.1 Runde 1 . . . . . . . . . . . . . . . . . . 4.2 Runde 2 . . . . . . . . . . . . . . . . . . 4.3 Runde 3 . . . . . . . . . . . . . . . . . . 4.4 Runde 4 - Optimierung . . . . . . . . . . 4.5 Runde 5 - Ändern von Daten in tbl_report . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 25 26 29 31 34 38 Grafische Auswertung von EventLogs 5.1 Ausgangssituation . . . . . . . . 5.2 Ist-Analyse . . . . . . . . . . . . 5.3 Soll-Analyse . . . . . . . . . . . 5.4 Umsetzung . . . . . . . . . . . . 5.5 Fazit . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 39 39 40 40 41 41 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6 Aufgabe 43 7 Glossar 47 i ii KAPITEL 1 Vorgeschichte Bis Windows XP und Server2003 war Dr. Watson das Programm zur Crash-Analyse. Ab Windows Vista ist nun Windows Error Reporting (WER) für das Hardware und Software Problemreporting, an die Stelle von Dr. Watson getreten. Hier muss man aber beachten, das Programme die mit dem .NET Framework entwickelt worden sind meist keine Error-Reports erzeugen! Windows Error Reporting (WER) ist für 2 Aufgaben zuständig. Die Fehlerberichtsfunktion ermöglicht es Anwendungsfehler, Kernel-Fehler, nicht mehr reagierende Anwendungen und andere anwendungsspezifische Problemberichte über einen Service an Microsoft zu senden. Problematik von WER-Report notes_on_wer Werden die Problemberichte an Microsoft gesendet, dann wird in der Datenbank bei Microsoft nach Lösungen für das aufgetretene Problem gesucht und es werden Lösungen vorschlagen, wenn welche gefunden wurden. Das Senden dieser Berichte kann seitens des Systemadministrators verhindert werden. Version=1 EventType=APPCRASH EventTime=130467955473684710 ReportType=2 Consent=1 UploadTime=130467955480403784 ReportIdentifier=4a222852-efdd-11e3-8281-78dd08b4da52 IntegratorReportIdentifier=4a222851-efdd-11e3-8281-78dd08b4da52 WOW64=1 NsAppName=daVinci.exe Response.BucketId=4f5befa6735ab77e0d40a74ab08834b6 Response.BucketTable=1 Response.LegacyBucketId=73313869235 Response.type=4 Sig[0].Name=Anwendungsname Sig[0].Value=daVinci.exe Sig[1].Name=Anwendungsversion Sig[1].Value=6.0.2.79 .... .... Die Probleme werden anhand einer so genannten Bucket-ID Identifiziert. Die Bucket-ID (oder auch fault Bucket ID) wird aus der Version des Programms, das abgestürzt ist und anderen technischen Informationen erzeugt. Dies schafft 1 Dr. Watson und seine Erben, Release 1 eine einzigartige Signatur für den Absturz. Die Bucket-ID erscheint in den Details des Windows-Crash-Dialogs und den Ereignisprotokollen. Diese Bucket-ID ist wichtig für Microsoft um das Problem eindeutig erkennen zu können. 1.1 Error Report Dateien *.wer Windows Error Reporting (WER) sammelt auch die Reportdateien mit der Endung “.wer”, an verschiedenen Orten auf dem Computer. # Per user archived Windows Error Reports $env:USERPROFILE\AppData\Local\Microsoft\Windows\WER\ReportArchive # Per user queued Windows Error Reports $env:USERPROFILE\AppData\Local\Microsoft\Windows\WER\ReportQueue # System archived Windows Error Reports $env:ALLUSERSPROFILE\Microsoft\Windows\WER\ReportArchive # System queued Windows Error Reports $env:ALLUSERSPROFILE\Microsoft\Windows\WER\ReportQueue Möchte man alle WER Reports erfassen, so muss man in jedem User Profil auf dem Rechner nach den *.wer Dateien suchen und alle genannten Ordner “abklappern”. # liste mit allen Windows Error Reporting User-Pfaden dieses Rechner erstellen $WerReportPfade = Get-ChildItem $env:SystemDrive\users | Where-Object ` {$_.PSIsContainer} | ForEach-Object { $Path = $_.Fullname # Testen ob der richtige Unter-Pfad im User Ordner vorhanden ist If(Test-Path (Join-Path -Path $Path -ChildPath 'AppData\Local\Microsoft\Windows\WER\ReportArchive') ) { # Pfad ausgeben (Join-Path -Path $Path -ChildPath 'AppData\Local\Microsoft\Windows\WER\ReportArchive') } # Testen ob der richtige Unter-Pfad im User Ordner vorhanden ist 2 Kapitel 1. Vorgeschichte Dr. Watson und seine Erben, Release 1 If(Test-Path (Join-Path -Path $Path -ChildPath ` 'AppData\Local\Microsoft\Windows\WER\ReportQueue') ) { # Pfad ausgeben (Join-Path -Path $Path -ChildPath 'AppData\Local\Microsoft\Windows\WER\ReportQueue') } } # Das System hat noch 2 Windows Error Reporting andere Pfade wo .wer Dateien liegen # diese werden zur liste zugefügt # System archived Windows Error Reports $WerReportPfade += "$env:ALLUSERSPROFILE\Microsoft\Windows\WER\ReportArchive" # System queued Windows Error Reports $WerReportPfade += "$env:ALLUSERSPROFILE\Microsoft\Windows\WER\ReportQueue" # leeres Array erstellen in dem die Dateipfade gesammelt werden $WerDateien = [System.Collections.ArrayList]@() # nun holen wir aus allen Ordnern alle .wer Dateien ForEach($Path in $WerReportPfade) { $WerDateien.AddRange((Get-ChildItem -Path $Path -Filter '*.wer' ` -Recurse | Select-Object -ExpandProperty Fullname)) } # jeden einzelnen Windows Error Reporting Bericht mit Windows PoweShell Anzeigen ForEach($WerDatei in $WerDateien) { # Pfad anzeigen: $WerDatei #inhalt der datei anzeigen Get-Content -Path $WerDatei # Trennlinie "---------------------------------------------" } 1.2 Konfiguration von Windows Error Reporting (WER) Windows Error Reporting (WER) kann auf verschiedene Weisen konfiguriert werden. So kann man z.B. Abschalten das Problemberichte an Microsoft deaktivieren oder die Orte ändern, an denen die *.wer Dateien abgelegt werden. Einen guten Artikel hat Herr Sommergut von WindowsPro verfasst: Problembericht senden: Windows Error Reporting konfigurieren oder deaktivieren http://www.windowspro.de/wolfgang-sommergut/problembericht-senden-windowsfehlerberichterstattung-konfigurieren-deaktivieren Herr Sommergut beschreibt auch das man die WER Reportdateien über Gruppenrichtlinien so konfigurieren kann, das Fehlerberichte an einen internen Server anstatt an Microsoft gesendet werden. Damit wird eine Funktion des Corporate Windows Error Reporting (CER) genutzt. Man kann damit ein NTFS-Share oder ein SSL-Verbindung nutzen, um die Reports an den eigenen Server zu senden. Weitere Dokumentation, wie man das Windows Error Reporting (WER) mit Group Policies oder Registry Schlüsseln einstellen kann, findet man in der Microsoft Dokumentation im Developer Center: Windows Error Reporting (WER) http://msdn.microsoft.com/en-us/library/windows/desktop/bb513641%28v=vs.85%29.aspx 1.2. Konfiguration von Windows Error Reporting (WER) 3 Dr. Watson und seine Erben, Release 1 Collecting User-Mode Dumps http://msdn.microsoft.com/en-us/library/windows/desktop/bb787181%28v=vs.85%29.aspx Den Speicherort von den *.wer Dateien stellt man z.B. mit den folgenden Schlüsseln in der Windows Registry um: HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\Windows Error Reporting\LocalDumps\DumpFolder HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\Windows Error Reporting\LocalDumps\DumpType 1.3 Windows Error Reporting (WER) Tools NirView http://www.nirsoft.net/utils/app_crash_view.html notes_on_wer Ein gutes Tool zum Verarbeiten von den Windows Error Reporting Dateien, hat Nir Sofer hergestellt. Dieses Tool nennt sich AppCrashView und kann auch mit der Windows PowerShell eingesetzt werden. Da es die *.wer Dateien in CSV oder XML umwandeln kann und von der Kommandozeile aus, gesteuert werden kann. 1.4 Windows PowerShell Error Reporting (WER) Cmdlets In Windows 8 und Server 2012 R2 gibt es nun auch 3 sehr bescheidene Windows PowerShell Cmdlets um das Reporting an oder abzuschalten. 4 Kapitel 1. Vorgeschichte Dr. Watson und seine Erben, Release 1 Windows Error Reporting us/library/jj590827.aspx (WER) Cmdlets in Windows PowerShell http://technet.microsoft.com/en- • Disable-WindowsErrorReporting schaltet das Error Reporting an Microsoft ab. • Enable-WindowsErrorReporting schaltet das Error Reporting an Microsoft wieder ein. • Get-WindowsErrorReporting zeigt an ob das Errorreporting ein oder ausgeschaltet ist. 1.5 Logfiles anstatt *.wer Dateien nutzen Zentrale Verwaltng von Errors http://www.faq-o-matic.net/2011/03/28/eventlog-monitoring-via-smtp-zentral-verwaltet-und-kostenfrei/ http://www.msxfaq.de/tools/logparser.htm Die *.wer Dateien zu sammeln und mit der Windows PowerShell zu analysieren, ist aber eigentlich der falsche Ansatz. Immer wenn ein Programm crasht, dann wird das in den Ereignisprotokollen(Eventlog) von Windows verzeichnet. Windows PowerShell kann hervorragend mit den Windows Ereignisprotokollen (Eventlog) von Windows umgehen, da es für diese Aufgabe spezielle Cmdlets hat. Ein abgestürzte Applikation, die einen Eintrag im Applikations-Ereignisprotokoll mit der ID 1000 und der ReportQuelle “Application Error” hinterlässt, könnte mit folgenden Befehl abgefragt werden. Get-EventLog -LogName Application -InstanceId 1000 -Source 'Application Error' Das Reporting selbst hinterlässt auch Einträge im Applikations-Ereignisprotokoll mit der ID 1001 und der ReportQuelle ” Windows Error Reporting”. Get-EventLog -LogName Application -InstanceId 1001 -Source 'Windows Error Reporting' Einen schönen Artikel darüber hat Aaron Paul Rykhus geschrieben: Finding useful crash data and Windows Error Reporting (WER) http://blogs.technet.com/b/arykhus/archive/2008/12/11/finding-useful-crash-data-and-windows-errorreporting-wer.aspx 1.5. Logfiles anstatt *.wer Dateien nutzen 5 Dr. Watson und seine Erben, Release 1 6 Kapitel 1. Vorgeschichte KAPITEL 2 WER goes Datenbank 2.1 Ausgangssituation Bisher existierten in Ihrer Firma Powershell-Skripte, die eine Ausgabe verschiedenster Informationen der WERReports über die Konsole möglich machen. Diese Informationen beziehen sich aber nur auf den jeweiligen Einzel-PC. Eine Aggregation von Informationen über alle Computer ist nicht möglich. Der Leiter der IT-Abteilung beschließt daher, ihr Skript des letzten Jahres als Ausgangspunkt für eine Erweiterung zu nehmen. Die Daten sollen nun in einer Mysql-Datenbank gespeichert werden. Zur Information erhalten Sie vom IT-Leiter noch das folgende PS-Skript Aufgabe Analysieren Sie das dargestellte Skript werreports_10.ps1. Notieren Sie sich Bemerkungen. 7 Dr. Watson und seine Erben, Release 1 2.2 Refaktoring Die oben dargestellten Überlegungen führen zu Veränderungen des Quellcodes sowie zu einem ER-Modell, welche die Datenhaltung widerspiegeln könnte. Aufgabe Erstellen Sie ein ER-Modell bzw. Klassendiagramm. 2.2.1 Umsetzung MySQL-Datenbank Nachdem die Daten nun in einer objektorientierten Struktur vorliegen, müsen diese nun anschließend in Datenbanktabellen gespeichert werden. Das zuvor erstellte SQL-Skript erzeugt eine leere Datenbankstruktur. Aufgabe Diskutieren Sie nochmals die Vor- und Nachteile des oben dargestellten Datenmodells figure/klasse_11/datamodell_1.png 8 Kapitel 2. WER goes Datenbank Dr. Watson und seine Erben, Release 1 2.2.2 Klassen Da die Daten in Annäherung an das Datenmodell gehalten werden sollen, bietet es sich an, die Zsammenhänge mit Hilfe von Klassen zu modellieren. Die many-Beziehung in der Klasse User zu den Reports wird mit Hilfe einer ArrayList abgebildet. Die Klasse Computer erhält einen Konstruktor, der beim Erzeugen eines Objektes gleich die Werte für den Computer holt. Dabei wird die bisher vorhandene Methode getOtherData/SetComputer wiederverwendet. Auch die Klasse Report erhält über einen Konstruktor die erforderlichen Daten. Die Klasse User erzeugt sich im Konstruktor einen neuen Computer und weist ihn der Referenz ps zu. class User { [string]$Name $Reports = [System.Collections.ArrayList]::New() [Computer]$pc User([string]$uname) { $this.Name = $uname $this.pc = [Computer]::new() } } class Report { [string]$ReportID [int]$ReportType [string]$EventType [string]$EventTime [string]$BucketID [string]$Appname Report($repid, $repType, $evType, $evTime, $buckId, $appnam) { $this.ReportID = $repid $this.ReportType = $repType $this.EventType = $evTime $this.EventTime = $evTime $this.BucketID = $buckId $this.Appname = $appnam } } class Computer { [string]$mac [string]$OpSys [string]$Name Computer() { $macadresse = get-wmiobject -class "Win32_NetworkAdapterConfiguration" | Where {$_.IpEnabled -Ma $this.mac = $macadresse[0].MacAddress $this.OpSys = (Get-WmiObject Win32_OperatingSystem).Name $this.Name = (Get-WmiObject Win32_OperatingSystem).CSName } 2.2. Refaktoring 9 Dr. Watson und seine Erben, Release 1 } 2.2.3 Anpassungen bestehender Funktionen Die Funktionen GetReportInnerData sowie GetWERPath müssen nicht geändert werden. GetUsers Die Funktion muss nun User-Objekte liefern. Sie erwartet eine ArrayList, welche sie mit USer-Objekten füllt. function GetUsers($Benutzer) { <# .Synopsis Erstellt aus dem Ordnernamen unterhalb von C:\Users eigenständige User-Objekte und weist Sie einer ArrayList zu. #> $tempUser = Get-ChildItem "C:\Users" | Select-Object Name foreach($username in $tempUser) { [User]$u = [User]::New($username.Name) $Benutzer.Add($u) } } GetReportData Neben der verwendeten ArrayList für das Halten der User-Objekte wird die Zuweisung der Reports durch die Verwendung der Report-Klasse ersetzt. Am Ende hat man eine komplettes Objektmodell #Wegen Problemen mit Rückgabe von ArrayListen wird eine eigene erzeugt und der aufgerufenen Funktion function GetReportData($Benutzer) { GetUsers $Benutzer foreach($_user in $Benutzer) { $paths = GetWERPath $_user.Name; foreach($_path in $paths) # $_users hier stehen alle benuter vom Rechner und werden n { if($_path -ne $null) { $_reportid = GetReportInnerData $_path "ReportIdentifier"; $_reporttype = GetReportInnerData $_path "ReportType"; $_eventtype = GetReportInnerData $_path "EventType"; # z.B AppCrash $_eventtime = GetReportInnerData $_path "EventTime"; $_bucketid = GetReportInnerData $_path "Response.BucketId"; $_appname = GetReportInnerData $_path "AppName"; [Report]$rep = [Report]::new($_reportid, $_reporttype, $_eventtype, $ $_user.Reports += $rep } 10 Kapitel 2. WER goes Datenbank Dr. Watson und seine Erben, Release 1 } } return $Benutzer } 2.2.4 Erweiterungen um datenbankspezifische Funktionen Aus Vereinfachungsgründen werden die Datenbank-Funktionen mit in das Skript geschrieben. Es wäre besser, diese in eine eigene Datei auszulagern und diese in das Hauptskript mit einzubinden. Zunächst wird die dll referenziert und Objekte wie Connection und Command erzeugt. Die Funktionen open() und close() dienen zum Kapseln der Open() und Vlose()-Methode des Connection-Objektes. Die Funktion saveData erhält die ArrayList aller Benutzerobjekte. Sie öffnet einmalig die Connection und geht dann in einer Schleife alle Userobjkete durch. Mit Hilfe von setUser wird dann zunächst eine USer in die Datenbank angelegt (siehe später mehr) und anschließend die dazugehörigen Report-Objekte gespeichert. Die setUser-Funktion operiert mit dem replace-Befehl von mysql. Dieser stellt sicher, dass bereits bestehende UserDatensätze nicht neu hinzugefügt werden, sondern lediglich Änderungen umgesetzt werden (replace = delete und insert). setReports erhält ein User-Objekt übergeben und geht mit Hilfe einer Schleife die Reports-Liste des User durch durch. Sie erzeugt jeweils ein Insert-Statement. Das sie Zugriff auf die User-Eigenschaften hat, kann sie die MAC-Adresse des Computers sowie den Usernamen mit in das Statement aufnehmen. Die setComputer-Funktion funktioniert analog zur setUser-Funktion. #mysql_model.ps1 #Datenbankverbindung öffnen und Datenübertragung vorbereiten #Bibliothek zur Datenbankeinbindung einbinden [void][system.reflection.Assembly]::LoadFrom("C:\Program Files (x86)\MySQL\MySQL Connector Net #Skriptweite Variablen deklarieren #Connectionvariable $connstring = "Server=localhost;Uid=watson;Pwd='watson';Database=watson_11FI3" $con = New-Object Mysql.Data.MysqlClient.MysqlConnection; #Commandobjekt $command = New-Object MySql.Data.MySqlClient.MySqlCommand; function open() { #Funktion zum Öffnen der Datenbankverbindung try { $script:con.ConnectionString = $connstring; $con.Open(); Write-Debug "Datenbankverbindung geöffnet" } catch { Write-Debug "Achtung Fehler: $_.ExceptionMessage" Write-Debug "Daten müssen später übertragen werden" } } function close() 2.2. Refaktoring 11 Dr. Watson und seine Erben, Release 1 { #Funktion zum Schliessen der Datenbankverbindung try { $con.Close(); Write-Debug "Datenbankverbindung geschlossen" } catch { Write-Debug "Achtung Fehler: $_.ExceptionMessage" Write-Debug "Daten müssen später übertragen werden" } } function saveData($Users) { open foreach($u in $Users) { write-debug $u.Name setUser $u setReports $u } close } function setUser($user) { $name = $user.Name [string]$sql = "replace into User(Anmeldename) values ('$name');" $command.CommandText = $sql $command.Connection = $con; $command.ExecuteNonQuery() } function setReports($u) { $m = $u.pc.mac $uname = $u.Name foreach($rep in $u.Reports) { $repid = $rep.ReportID $repType = $rep.ReportType $evTime = $rep.EventTime $evType = $rep.EventType $buckID = $rep.BucketID $app = $rep.Appname [string]$sql = "insert into report(ReportID, Appname, EventTime, BucketID, ReportType, User, Co $sql += "values('$repid', '$app', '$evTime', '$buckID', '$repType', '$uname', '$m');" $command.CommandText = $sql 12 Kapitel 2. WER goes Datenbank Dr. Watson und seine Erben, Release 1 $command.Connection = $con; $command.ExecuteNonQuery() } } function setComputer([Computer]$c) { $m = $c.mac $sys = $c.OpSys $n = $c.Name [string]$sql = "replace into computer(MAC, OSName, HostName) values ('$m', '$sys','$n') #[string]$sql = "Replace into computer values ($c.'mac', $c.'OpSys',$c.'Name');" $command.CommandText = $sql $command.Connection = $con; open $command.ExecuteNonQuery() close } 2.2.5 Ablaufsteuerung Aufgrund der objektorientierten Struktur der Daten ergibt sich eine logische Abfolge des Einfügens der Daten. Die User-Objekte enthalten nicht nur die Daten des ausgelesenen Benutzers, sondern referenzieren den jeweiligen Computer sowie alle Report-Objekte, die der jeweilige User auf dem Rechner hatte. Aufgrund der Foreign-KeyBeziehungen zwischen den Tabellen muss sichergestellt werden, dass neue User und Computer zunächst als Erstes in die DB geschrieben werden. Um sich aufwendiges Abfragen zu sparen (ist der DS schon vorhanden ?), wird beim User und Computer ein replace-Staement verwendet. Danach können die Report-Datensätze mit den Informationen geschrieben werden. Dieser Vorgang muss für jedes User-Objekt in der Users-Liste durchgeführt werden. Da davon auszugehen ist, dass das Skript immer nur auf einem Computer läuft, müssen die Computerdaten nur einmal geschrieben werden. Folgende Abbildung veranschaulicht den Prozess des Einfügens. 2.2. Refaktoring 13 Dr. Watson und seine Erben, Release 1 Zum Ende des Skriptes erfolgt deshalb der Aufruf der Hauptfunktionen. Zunächst holt man sich aller User mit zugehörigen Reports. Anschließend speichert/ersetzt man einmalig den Datensatz in der Tabelle Computer. Zuletzt speichert saveData() alle User sowie die dazugehörigen Reports. $Benutzer = [System.Collections.ArrayList]::New() GetReportData $Benutzer # untenstehende Routine könnte auch in der setUSer-Methode für jeden User ausgeführt werden # da das Skript auf nur auf einem Rechner läuft, kann man sich die wiederholten # Statements aber sparen setComputer ($Benutzer[0]).pc # und jetzt der ganze Rest saveData $Benutzer 14 Kapitel 2. WER goes Datenbank KAPITEL 3 WER darstellen 3.1 Ausgangssituation In ihrer Firma existieren Skripte, die Windows Error Reports sammeln und in einer MySQL-Datenbank abspeichern. Nachdem mit dem Speichern der Daten der Sammelvorgang abgeschlossen ist, wird nun vom IT-Leiter der Wunsch geäußert, diese Daten in einer einfachen Form abfragen und nach bestimmten Kriterien auswerten zu können. Da dieses Projekt keine hohe Priorität in der Firma hat, können keine Programmierer mit dieser Aufgabe betraut werden. Sie erhalten deshalb vom IT-Leiter die Aufgabe, für diese Auswertung eine einfache grafische Lösung zu programmieren. Zur Information erhalten Sie vom IT-Leiter noch das folgende Datenmodell. Aufgabe Analysieren Sie das folgende Datenmodell 15 Dr. Watson und seine Erben, Release 1 Aufgabe Überlegen Sie sich mit Hilfe eines Mockups, wie diese “einfache grafische Lösung” aussehen könnte. gehen Sie dabei von folgender Fragestellung aus: Was könnte der IT-Leiter mit dieser Anwendung vorhaben; wie könnte er sie benutzen ? 3.2 Grafische Mockups/WireFrames Mit “Mockup” wird die Technik beschrieben, mit einfachen Hilfsmitteln eine Vorstellung von einer GUI-Anwendung zu erhalten. Es können damit Bedienkonzepte dargestellt und diskutiert werden. Ein bekanntes und für unsere Zwecke ausreichendes Tool ist das Programm PENCIL, welches Sie unter 16 Kapitel 3. WER darstellen Dr. Watson und seine Erben, Release 1 http://pencil.evolus.vn/ downloaden können. Eine damit erzeugte GUI könnte wie folgt aussehen: GUI GUI 3.3 Umsetzung in PrimalForms PrimalForms Community Edition ist ein Werkzeug, mit dem man grafische Oberflächen (WinForm) für die Powershell zusammenklicken kann. Neben den grafischen Elementen und deren Eigenschaften können auch die auf diese Komponenten wirkenden Ereignisse (z.B. Click) definiert werden. PrimalForms generiert daraus Quellcode, der dann in der Powershell ausgeführt werden kann. 3.3. Umsetzung in PrimalForms 17 Dr. Watson und seine Erben, Release 1 Mit Hilfe des Buttons ExportToPowershell kann der das Formular beschreibende Quellcode als Powershell-Skript gespeichert werden. Download Quellcode #Generated Form Function function GenerateForm { ######################################################################## # Code Generated By: SAPIEN Technologies PrimalForms (Community Edition) v1.0.10.0 # Generated On: 26.11.2014 21:24 # Generated By: Karl ######################################################################## #region Import the Assemblies [reflection.assembly]::loadwithpartialname("System.Windows.Forms") | Out-Null [reflection.assembly]::loadwithpartialname("System.Drawing") | Out-Null #endregion #region Generated Form Objects $form1 = New-Object System.Windows.Forms.Form $lblAusgabe = New-Object System.Windows.Forms.Label $txtNameEingabe = New-Object System.Windows.Forms.TextBox $Hello = New-Object System.Windows.Forms.Button $InitialFormWindowState = New-Object System.Windows.Forms.FormWindowState #endregion Generated Form Objects #---------------------------------------------#Generated Event Script Blocks #---------------------------------------------#Provide Custom Code for events specified in PrimalForms. $Hello_OnClick= { #TODO: Place custom script here } $OnLoadForm_StateCorrection= {#Correct the initial state of the form to prevent the .Net maximized form issue $form1.WindowState = $InitialFormWindowState } 18 Kapitel 3. WER darstellen Dr. Watson und seine Erben, Release 1 #---------------------------------------------#region Generated Form Code $System_Drawing_Size = New-Object System.Drawing.Size $System_Drawing_Size.Height = 328 $System_Drawing_Size.Width = 395 $form1.ClientSize = $System_Drawing_Size $form1.DataBindings.DefaultDataSourceUpdateMode = 0 $form1.Name = "form1" $form1.Text = "Primal Form" $lblAusgabe.DataBindings.DefaultDataSourceUpdateMode = 0 $System_Drawing_Point = New-Object System.Drawing.Point $System_Drawing_Point.X = 199 $System_Drawing_Point.Y = 120 $lblAusgabe.Location = $System_Drawing_Point $lblAusgabe.Name = "lblAusgabe" $System_Drawing_Size = New-Object System.Drawing.Size $System_Drawing_Size.Height = 24 $System_Drawing_Size.Width = 137 $lblAusgabe.Size = $System_Drawing_Size $lblAusgabe.TabIndex = 2 $lblAusgabe.Text = "label1" $form1.Controls.Add($lblAusgabe) $txtNameEingabe.DataBindings.DefaultDataSourceUpdateMode = 0 $System_Drawing_Point = New-Object System.Drawing.Point $System_Drawing_Point.X = 43 $System_Drawing_Point.Y = 125 $txtNameEingabe.Location = $System_Drawing_Point $txtNameEingabe.Name = "txtNameEingabe" $System_Drawing_Size = New-Object System.Drawing.Size $System_Drawing_Size.Height = 20 $System_Drawing_Size.Width = 109 $txtNameEingabe.Size = $System_Drawing_Size $txtNameEingabe.TabIndex = 1 $form1.Controls.Add($txtNameEingabe) $Hello.DataBindings.DefaultDataSourceUpdateMode = 0 $System_Drawing_Point = New-Object System.Drawing.Point $System_Drawing_Point.X = 99 $System_Drawing_Point.Y = 178 $Hello.Location = $System_Drawing_Point $Hello.Name = "Hello" $System_Drawing_Size = New-Object System.Drawing.Size $System_Drawing_Size.Height = 64 $System_Drawing_Size.Width = 173 $Hello.Size = $System_Drawing_Size $Hello.TabIndex = 0 $Hello.Text = "btnHello" $Hello.UseVisualStyleBackColor = $True $Hello.add_Click($Hello_OnClick) $form1.Controls.Add($Hello) 3.3. Umsetzung in PrimalForms 19 Dr. Watson und seine Erben, Release 1 #endregion Generated Form Code #Save the initial state of the form $InitialFormWindowState = $form1.WindowState #Init the OnLoad event to correct the initial state of the form $form1.add_Load($OnLoadForm_StateCorrection) #Show the Form $form1.ShowDialog()| Out-Null } #End Function #Call the Function GenerateForm 3.4 PS und DB-Anbindung Lediglich für den SQL-Server bietet Microsoft eine Sammlung von Commandlets zum Umgang mit dem DatenbankServer an. Bei anderen Datenbank-Systemen kann man aber mit Hilfe des .NET-Frameworks und der von den Herstellern angebotenen Datenbanktreibern eine Zusammenarbeit bewerkstelligen. Dazu sind aber grundlegende Verständnisse des Datenbankzugriffes unter .NET notwendig. 3.4.1 ADO .NET ADO .NET stellt das grundlegende Denkmodell von Microsoft zum Datenbankzugriff dar. Es besteht aus einer ganzen Reihe von durch Objekten gekapselten Zugriffsverfahren, die durch die jeweiligen Datenbank-Treiber (Provider) zur Verfügung gestellt werden. Die wichtigsten werden kurz vorgestellt. 20 Kapitel 3. WER darstellen Dr. Watson und seine Erben, Release 1 figure/kd_ado_net_modell.jpg Damit werden für den lesenden und schreibenden Zugriff auf die Daten einer Datenbank unterschiedliche Objketmodelle seitens von .NET verwendet. 3.4.2 Herstellen einer Verbindung Link http://vwiki.co.uk/MySQL_and_PowerShell In jedem Falle ist ein Verbindungsaufbau zum Datenbankserver notwendig. Es müssen zumindest Username/Passwort, Rechnername des Datenbankservers sowie der Datenbankname übergeben werden. Function con_db () { [void][system.reflection.Assembly]::LoadFrom("C:\Program Files (x86)\MySQL\MySQL Connector Ne $connstring = "Server=localhost;Uid=root;Pwd='XXXXX';Database=XXXXXX"; $con = New-Object Mysql.Data.MysqlClient.MysqlConnection; $con.ConnectionString = $connstring; return $con; } Die Verbindungsdaten werden in .NET in einem Objekt gespeichert; dieses kann dann von anderen Teilen des Skriptes verwendet werden. 3.4. PS und DB-Anbindung 21 Dr. Watson und seine Erben, Release 1 3.4.3 Auslesen von Daten Der SELECT-Befehl kann mit Hilfe verschiedener Objekte abgewickelt werden. Dies sind DataReader bzw. DataTables. DataReader function mysql_select($query) { $cmd = New-Object MySql.Data.MySqlClient.MySqlCommand; $cmd.CommandText = $query $results = $cmd.ExecuteReader() $cmd.Dispose() while ($results.Read()) { //Zugriff über die Spalten eines Datensatzes for ($i= 0; $i -lt $results.FieldCount; $i++) { write-output $results.GetValue($i).ToString() } //So würde es auch gehen write-output %results["feld1"] write-output $results["feld2"] } $select_string = "SELECT feld1, feld2 FROM table;" mysql_select $select_string Problematische Elemente des DataReaders sind: • Der Reader kann nur über eine Schleife durchlaufen werden; die Daten müssen deshalb zur weiteren Verwendung irgendwie gespeichert werden. • der Zugriff auf die Feldwerte erfolgt innerhalb der Schleife über einen Indexwert, was nicht besonders komfortabel ist DataSet function Execute-MySQLQuery([string]$query, $con) { # Create SQL command $cmd = New-Object MySql.Data.MySqlClient.MySqlCommand($query, $con) # Create data adapter from query command $dataAdapter = New-Object MySql.Data.MySqlClient.MySqlDataAdapter($cmd) # Create dataset $dataSet = New-Object System.Data.DataSet # Fill dataset from data adapter, with name "data" $dataAdapter.Fill($dataSet, "data") $cmd.Dispose() # Returns an array of results return $dataSet.Tables["data"] } $query = "SELECT feld1, feld2 FROM table;" $result = Execute-MySQLQuery $query $con 22 Kapitel 3. WER darstellen Dr. Watson und seine Erben, Release 1 Write-Host ("Found " + $result.rows.count + " rows...") $result | Format-Table ExecuteScalar Falls nur die Rückgabe eines einzigen Wertes erwartet wird (count, avg, etc.) kann auch mit der ExecuteScalar gearbeitet werden. function Execute-MySQLScalar($query, $con) { # Create command object $cmd = $con.CreateCommand() # Load query into object $cmd.CommandText = $query # Execute command $cmd.ExecuteScalar() } Execute-MSQLScalar "$select count(*) from table;" $con 3.4.4 Einfügen/Ändern/Löschen von Daten Zum Ändern/Löschen und Einfügen von Daten muss die ExecuteNonQuery()-Methode des Command-Objektes verwendet werden. Function Executebefehl ($String) { $con = con_db; $con.Open(); $command = New-Object MySql.Data.MySqlClient.MySqlCommand; $command.CommandText = $String; #command muss die connection kennen $command.Connection = $con; #ExecuteNonQuery führt den Befehl auf der DB aus und gibt das $command.ExecuteNonQuery(); $con.Close(); } $SQLstring = "insert into server (BS, SerialNr, CPU_Temp, CPU_Workload, UpTime, HDUsage, Process_Coun #$SQLstring; Executebefehl $SQLstring; 3.5 Exportieren nach Excel Zum Export der jeweiligen Inhalte des DataGrids kann recht elegant ein bekanntes Konzept der Powershell verwendet werden, nämlich das der Pipeline. 3.5. Exportieren nach Excel 23 Dr. Watson und seine Erben, Release 1 Während ein DataGrid-Objekt zunächst einmal nicht einfach nach Excel exportiert werden kann, ist es der darunter liegenden DataTable des DataSet-Objektes durchaus möglich, seine Inhalte in eine csv-Datei zu speichern. Der entsprechende Code in der control.ps1 ist dann nur noch ein Einzeiler: function exportToExcel { $DataSet.Tables[0] | export-csv c:\temp\flam_stein\file.csv -notypeinformation -UseCulture } 24 Kapitel 3. WER darstellen KAPITEL 4 Implementierung Aufgrund der sich steteig ändernden GUI ist es nicht ratsam, den GUI-Quellcode von Primalforms mit der eigenen Logik zu verbinden. Die verschiedenen Bestandteile des Skriptes sollten in eigenen Dateien (.ps1) gespeichert werden, um sie vor Überschreiben zu schützen und eine Wiederverwendbarkeit zu erreichen. Für unser Beispiel ist zunächst folgende Vorgehensweise sinnvoll. • View.ps1: Handelt die GUI sowie den Aufrufhandler für die eigenen Funktionen • Model.ps1: Regelt alle Zugriffe auf die Datenbank • Control.ps1: Vermittelt zwischen dem Quellcode von View.ps1 und Model.ps1 Implementiert die notwendige Verarbeitungslogik Mit dieser Dreiteilung wird ein klassischs Muster der Programmierung umgesetzt, das sog. MVC-Muster. Prinzipiell geht es darum, die unterschiedlichen Aufgabenbereiche eines Programmes zu unterscheiden und damit auch austauschbar zu machen. ======> View.ps1 ======> Control.ps1 <====== Model.ps1 <====== 25 Dr. Watson und seine Erben, Release 1 4.1 Runde 1 26 Kapitel 4. Implementierung Dr. Watson und seine Erben, Release 1 In der ersten Runde geht es darum, eine Verbindung mit dem Datenbankserver herzustellen und das DataGrid des Hauptfensters mit den Daten der Tabelle tblReports zu füllen. 4.1.1 View Beim Starten des Formulars soll im DataGrid eine Liste der letzten 20 Einträge gezeigt werden. Das von PF generiete Skript muss im Form_Load-Ereignis einen Datenbankabruf vornehmen. Der Eventhandler des Formulars ruft deshalb im Controler eine entsprechende Funktion get_reports auf, die die Daten aus der Datenbank holt (r1_model.ps1). Der Controler bindet anschließend die Ergebnisse an das DataGrid der View. ... $handler_form1_Load= { Write-Debug "in Handler_form_load" getReports ... 4.1.2 Controler Der Controler leitet den Aufruf der View an das Model weiter und sorgt anschließend für das Anzeigen der Daten im DataGrid des View-Skriptes. Dazu wird dessen Fähigkeit genutzt, sich per DataSource-Eigenschaften an die “Tabellen” des DataSets zu binden . .\r1_model.ps1 function getReports { getReportModel updateReport } .... function updateReport() { $dataGridWER.DataSource = $null; $dataGridWER.DataSource = $DataSet.Tables[0]; $dataGridWER.DataBind $form1.refresh() } 4.1.3 Model Im Model werden alle ausgeführten Datenbankzugriffe implementiert. Die entsprechend notwendigen Objekte wie Connection, Command, DataSet und DataAdapter werden erzeugt und mit den daten gefüllt. Zum Füllen des DataGrids wird mit Hilfe eines DataAdapters ein DataSet erstellt. Dies stellt sozusagen eine “virtuelle” Tabelle im Speicher dar und kann von vielen Steuerlementen des .NET-Frameworks genutzt werden. Funktionalitäten wie das Öffnen und Schließen von Datenbankverbindungen werden ausgelagert, da sie in Zukunft noch häufiger genutzt werden. #Erste Einrichtung der Datenbankverbindung #$connstring = "Server=10.161.8.17;Uid='root';Pwd='steinam';Database=dr_watson"; $connstring = "Server=localhost;Uid='root';Pwd='patricia1234';Database=drwatson"; 4.1. Runde 1 27 Dr. Watson und seine Erben, Release 1 $con = New-Object Mysql.Data.MysqlClient.MysqlConnection; $con.ConnectionString = $connstring; $DataSet = New-Object System.Data.DataSet ############################################################################ #Neu: Dataadapter muss bereits hier erzeugt werden, da bei Änderungen etc. auf diesen zurückgegriffen #muss, damit das dann später benutzte Commandobjekt darauf zugreifen kann. #Siehe: file:///C:/Fp/OpenBooks/C%20Sharp/Visual%20C%20Sharp%202012/1997_35_001.html#dodtp44f9b504-b6 $SqlAdapter = New-Object MySql.Data.MySqlClient.MySqlDataAdapter #Verbindung öffnen function verbindungOeffnen() { $con.Open(); Write-Debug "Datenbankverbindung geöffnet" } #Verbindung schließen function verbindungschliessen() { $con.Close(); Write-Debug "Datenbankverbindung geschlossen" } #Computerdaten besorgen function getReportModel() { #Verbindung öffnen verbindungOeffnen #SQL-Statement eingeben $SqlQuery = "select * from tbl_report" #Commandobjekt anlegen und Connectionobjekt sowie Abfrage zuordnen $SqlCmd = New-Object MySql.Data.MySqlClient.MySqlCommand; $SqlCmd.CommandText = $SqlQuery $SqlCmd.Connection = $con #Datenadapter instantiieren und Commandreferenz zuweisen $SqlAdapter.SelectCommand = $SqlCmd #Dataset leeren, sonst werden die alten Daten noch angezeigt $DataSet.Reset() #Dataset instantiieren und füllen lassen $SqlAdapter.Fill($DataSet) #Verbindung schließen verbindungSchliessen } 28 Kapitel 4. Implementierung Dr. Watson und seine Erben, Release 1 4.2 Runde 2 Nach dem Füllen des DataGrids sollen die ComboBoxen zur Auswahl der User und Rechner mit den Werten aus den entsprechenden Tabellen gefüllt werden. 4.2.1 View Da das Füllen der ComboBoxen bereits beim Start erfolgen soll, muss die Form_Load-Handlermethode des Views um entsprechende Aufrufe von Contrer-Funktionen ergänzt werden. $handler_form1_Load= { Write-Debug "in Handler_form_load" getReports #neuer Code getCboUser getCboHosts } 4.2.2 Controler Der Controler ruft eine Methode des Models auf. Seine Daten erhält er von dieser Methode in Form eines Arrays. Diese werden über die updateCboUser-Funktion in die Items-Liste der ComboBox übertragen. function getCboUser() { # Aufruf einer Funktion des Models $UserList = getUserModel updateCboUser $UserList 4.2. Runde 2 29 Dr. Watson und seine Erben, Release 1 } function updateCbouser($list) { foreach($user in $list) { $cboUser.Items.Add($user) } } 4.2.3 Model Im Model werden die Daten mit Hilfe eines SQL-Statements und einem DataReader aus der entsprechenden Tabelle gelesen. Die Daten des Readers werden in ein Array kopiert und an die aufrufende Funktion des Controlers zurückgegeben. Diese Vorgehensweise wird für das Füllen beider ComboBoxen angewendet. Anstellen des Umwandelns in einen Array hätte man auch den DataReader direkt zum Controler zurückgeben können. function getUserModel { $Userlist = @(); verbindungOeffnen #SQL-Statement eingeben $SqlQuery = "select Anmeldename from tbl_user;" #Commandobjekt anlegen und Connectionobjekt sowie Abfrage zuordnen $SqlCmd = New-Object MySql.Data.MySqlClient.MySqlCommand; $SqlCmd.CommandText = $SqlQuery $SqlCmd.Connection = $con #Datenadaptar instantiieren und Commandreferenz zuweisen $reader = $SqlCmd.ExecuteReader(); while($reader.Read()) { Write-Host $reader["Anmeldename"] #$Userlist.Add($reader["Anmeldename"] = $reader["Anmeldename"]) $Userlist += $reader["Anmeldename"] } verbindungschliessen return $Userlist } 30 Kapitel 4. Implementierung Dr. Watson und seine Erben, Release 1 4.3 Runde 3 Im DataGrid sollen nun nach der Auswahl der jeweiligen ComboBox die Daten angezeigt werden. Da hierzu grundsätzlich keine neuen Features hinzukommen, müsste dies leicht zu implementieren sein. 4.3.1 View Im GUI-Teil sind die On_Cklick()-Handler der beiden Buttons zu bearbeiten. Der Wert des markierten Eintrags der ComboBoxen ist an den Controler weiterzugeben. #r3_view.ps1 $btnSelectUser_OnClick= { #TODO: Code for dataGrid for selected User getReportsForUser $cboUser.SelectedItem } ... $btnSelectRechner_OnClick= { getReportsForRechner $cboRechner.SelectedItem } 4.3.2 Controler Der Controler nimmt den Wert der ComboBoxen entgegen und reicht ihn an die Model-Funktionen weiter. Da diesmal wieder mit DataSets gearbeitet wird, sorgt er zum Schluss noch für das Updaten des DataGrids 4.3. Runde 3 31 Dr. Watson und seine Erben, Release 1 #r3_control.ps1 function getReportsForUser($selectedUser) { getReportsForUserModel $selectedUser updatereport } function getReportsForRechner($selectedRechner) { getReportsForRechnerModel $selectedRechner updatereport } 4.3.3 Model Im Model werden die Daten mit Hilfe einer SQL-Query geholt, in ein DataSet verpackt und an das Controler-Skript zur weiteren Verarbeitung zurückgegeben. Beim SQL-Statement ist darauf zu achten, dass für die Ausgabe der Rechner ein INNER JOIN-Statement zu wählen ist, da die Tabellen tbl_reports und tbl_computer über die MAC-Adressen verknüpft sind - die Auswahl der ComboBox bezieht sich allerdings auf den Hostnamen. Die Zuordnung des übergebenen Rechner- bzw. Usernamens in den SQL-String erfolgt mit Hilfe der ‘” “’ Schreibweise #r3_model.ps1 function getReportsForUserModel($user) { verbindungOeffnen $SqlQuery = "select * from tbl_report where Anmeldename = '" + $user +"';" #Commandobjekt anlegen und Connectionobjekt sowie Abfrage zuordnen $SqlCmd = New-Object MySql.Data.MySqlClient.MySqlCommand; $SqlCmd.CommandText = $SqlQuery $SqlCmd.Connection = $con #Datenadapter instantiieren und Commandreferenz zuweisen $SqlAdapter.SelectCommand = $SqlCmd #Dataset leeren, sonst werden die alten Daten noch angezeigt $DataSet.Reset() #Dataset instantiieren und füllen lassen $SqlAdapter.Fill($DataSet) verbindungschliessen } function getReportsForRechnerModel($Rechner) { verbindungOeffnen #Ein equi join gibt falsche Ergebnisse #$SqlQuery = "select r.ReportID, r.Appname, r.Eventtime, #r.Anmeldename, r.MAC, r.ReportType " #$SqlQuery += "from tbl_report r,tbl_computer c " #$SqlQuery += "where r.MAC = c.MAC and c.Hostname = '" + $Rechner +"';" 32 Kapitel 4. Implementierung Dr. Watson und seine Erben, Release 1 #der inner join machts richtig $SqlQuery ="select r.ReportID, r.Appname, r.Eventtime, r.Anmeldename, r.MAC, r.ReportType " $SqlQuery += "from tbl_report r inner join tbl_computer c " $SqlQuery += "on r.MAC = c.MAC " $SqlQuery += "where c.Hostname = '" + $Rechner +"';" #Commandobjekt anlegen und Connectionobjekt sowie Abfrage zuordnen $SqlCmd = New-Object MySql.Data.MySqlClient.MySqlCommand; $SqlCmd.CommandText = $SqlQuery $SqlCmd.Connection = $con #Datenadapter instantiieren und Commandreferenz zuweisen $SqlAdapter.SelectCommand = $SqlCmd #Dataset leeren, sonst werden die alten Daten noch angezeigt $DataSet.Reset() #Dataset instantiieren und füllen lassen $SqlAdapter.Fill($DataSet) verbindungschliessen } 4.3. Runde 3 33 Dr. Watson und seine Erben, Release 1 4.4 Runde 4 - Optimierung Nachdem das Skript lauffähig ist, wird in einer Zwischenrunde zunächst redundanter Code refaktorisiert. Unter Refaktorisierung versteht man das Umgestaltung von Quellcode zur Vermeidung von redundanten Code-Teilen bzw. generell eine Verbesserung der Code-Strukturen im Hinblick auf die Erweiterung von Skripten. 4.4.1 Model Im Model-Skript fallen sofort die Funktionen zum Befüllen der KomboBoxen sowie die Funktionen zum Befüllen des DataGrids auf. Beide unterscheiden sich eigentlich nur in der Formulierung des SQL-Statements sowie in der Art des Parameters. Refaktorisierung der KomboBoxen In den beiden Funktionen zum Holen der Daten für die KomboBoxen User und Rechner ist die gleiche Logik implementiert. Durch Parametrisierung der Funktion und Einbau einer Fallabfrage kann die Aufgabe mit Hilfe einer einzigen Funktion erledigt werden. #r4_model.ps1 function getCboModel ($comboBox) { $SqlQuery = "" $FeldName = "" switch($comboBox) { "User" { #SQL-Statement zum Befüllen der ComboBox cboUser $SqlQuery = "select Anmeldename from tbl_user;" $Feldname = "Anmeldename"; break } 34 Kapitel 4. Implementierung Dr. Watson und seine Erben, Release 1 "Rechner" { #SQL-Staement zum Befüllen der ComboBox $SqlQuery = "select Hostname from tbl_computer;" $Feldname = "Hostname"; break } } $list = @(); verbindungOeffnen #Commandobjekt anlegen und Connectionobjekt sowie Abfrage zuordnen $SqlCmd = New-Object MySql.Data.MySqlClient.MySqlCommand; $SqlCmd.CommandText = $SqlQuery $SqlCmd.Connection = $con #Datenadaptar instantiieren und Commandreferenz zuweisen $reader = $SqlCmd.ExecuteReader(); while($reader.Read()) { $list += $reader[$Feldname] } verbindungschliessen return $list } Das Befüllen des DataGrids wird zur Zeit durch 3 verschiedene Methoden ausgeführt • getReportModel() • getReportsForUserModel($user) • getReportsForRechnerModel($Rechner) Diese Methoden sind gleich ! Lediglich der verwendete SQL-String unterscheidet sich. Man kann deswegen lediglich eine dieser 3 Methoden (getReportModel()) weiterverwenden; der jeweils notwendige SQL-String wird in den Controler-Methoden generiert und an die Methode übergeben(getReportModel($SqlQuery). r4_model.ps1 function getReportModel($SqlQuery) { #Verbindung öffnen verbindungOeffnen #Commandobjekt anlegen und Connectionobjekt sowie Abfrage zuordnen $SqlCmd = New-Object MySql.Data.MySqlClient.MySqlCommand; $SqlCmd.CommandText = $SqlQuery $SqlCmd.Connection = $con #Datenadapter instantiieren und Commandreferenz zuweisen $SqlAdapter.SelectCommand = $SqlCmd #Dataset leeren, sonst werden die alten Daten noch angezeigt $DataSet.Reset() #Dataset instantiieren und füllen lassen $SqlAdapter.Fill($DataSet) 4.4. Runde 4 - Optimierung 35 Dr. Watson und seine Erben, Release 1 #Verbindung schließen verbindungSchliessen } 4.4.2 Control Die beiden Funktionen getCbouser sowie getCboHost könnten in einer Funktion zusammengefasst werden, wenn man den jeweiligen ComboBox-typ als Parameter übergibt. r4_control.ps1 .... function getCbo($type) { $list = getcboModel $type if($type -eq "Rechner") { updateCboHost $list } else { updateCboUser $List } } Für den Aufruf zum Befüllen des DataGrids im Model werden die SQL-Statements aus den Model in die bereits bestehenden Funktionen des Controlers ausgelagert. #r4_control.ps1 function getReports { getReportModel "select * from tbl_report" updateReport } function getReportsForUser($selectedUser) { $SqlQuery = "select * from tbl_report where Anmeldename = '" + $selectedUser +"';" getReportModel $SqlQuery updatereport } function getReportsForRechner($selectedRechner) { $SqlQuery $SqlQuery $SqlQuery $SqlQuery ="select r.ReportID, r.Appname, r.Eventtime, r.Anmeldename, r.MAC, r.ReportType " += "from tbl_report r inner join tbl_computer c " += "on r.MAC = c.MAC " += "where c.Hostname = '" + $selectedRechner +"';" getReportModel $sqlQuery updatereport 36 Kapitel 4. Implementierung Dr. Watson und seine Erben, Release 1 } 4.4.3 View Der Aufruf der *getCbo.. * - Methoden in der View müssen wie folgt geschrieben werden. r4_view.ps1 ..... $handler_form1_Load= { getReports #Erstes Befüllen des DataGrids #getCboUser #getCboHosts getcbo "Rechner" #Aufruf der refaktorisierten Methode und Übergabe getcbo "User" #ds Parameters für die jeweils zu füllende KomboBox ..... 4.4.4 Fazit Durch die Refaktorisierung wurde der Quellcode im Controler etwas mehr; es konnten jedoch einige Funktionen eingespart werden. Im Model wurden erhebliche Zeilen Quellcode eingespart, welches zur besseren Verständlichkeit des Quellcode beiträgt. 4.4. Runde 4 - Optimierung 37 Dr. Watson und seine Erben, Release 1 4.5 Runde 5 - Ändern von Daten in tbl_report Die Verwendung von Objekten des .NET-Frameworks erhöhen zwar den Komplexitätsgrad des Skriptes; sie machen aber manche Aufgaben realtiv einfach, wie beispielsweise das Ändern von Einträgen im DataGrid und das Abspeichern dieser Änderungen in der Datenbank. Durch die Verwendung des DataAdapter-Konzeptes zum Befüllen des DataGrids, stellt .NET quasi eine logische Tabelle im Arbeitsspeicher zur Verfügung. Der Adapter hat damit das Wissen um den Aufbau der Tabelle und kann dieses Wissen auch zum Ändern und Löschen von Einträgen in dieser Tabelle verwenden. Das verwendete DataAdapterObjekt muss dazu einem sog. CommandBuilder übergeben werden, der die ‘lästige’ Arbeit des Formulierens von SQL INSERT-, UPDATE und DELETE-Anweisungen für den DataAdapter übernimmt. function updateDatenbankModel() { verbindungOeffnen $commandbuilder = new-object MySql.Data.MySqlClient.MySqlCommandBuilder $SqlAdapter $SqlAdapter.UpdateCommand = $commandbuilder.GetUpdateCommand() $SqlAdapter.InsertCommand = $commandbuilder.GetInsertCommand() $SqlAdapter.DeleteCommand = $commandbuilder.GetDeleteCommand() $null = $SqlAdapter.Update($DataSet) verbindungSchliessen } 38 Kapitel 4. Implementierung KAPITEL 5 Grafische Auswertung von EventLogs 5.1 Ausgangssituation Fehler werden nicht nur mit Hilfe von WER-Reports abgebildet, sondern sie werden normalerweise in das sog. Eventlog eines Rechners geschrieben. Sie sind nach verschiedenen Kategorien sortiert und können mit Hilfe der Ereignisanzeige kontrolliert werden. 39 Dr. Watson und seine Erben, Release 1 5.2 Ist-Analyse Die grafische Oberfläche der Ereignisanzeige lässt vielfältige Ansichten zu; eine grafische Darstellung über größere Zeiträume ist allerdings nicht möglich. Hier setzt jetzt der Wunsch des IT-Leiter ein, der gerne wissen möchte, wie häufig ein bestimmter Fehler über einen bestimmten Zeitraum erfolgt. Ein erstes Mockup (Prototyp) der grafischen Benutzeroberfläche könnte er sich wie folgt vorstellen. Im linken Teil der GUI wählt man die gewünschte EventID, den Log-Typ und den Zeitraum aus. Mit dem Klick auf den Button Continue soll im Eventlog überprüft werden, ob für den entsprechenden Event Daten vorhanden sind. Während in einer Detailsicht die Textinformationen angezeit werden, soll in einer grafischen Visualisierung die Anzahl der Ereignisse pro Tag als Liniendiagramm abgebildet werden. 5.3 Soll-Analyse Bei der Realisierung der Vorgaben stellen sich schnell einige kritische Punkte heraus. • Wie erstellt man mit Hilfe der Powershell eine grafische Oberfläche • Wie erzeugt man die grafischen Daten und integriert sie in die GUI • Wie gewinnt man die Daten aus der grafischen Oberfläche heraus 40 Kapitel 5. Grafische Auswertung von EventLogs Dr. Watson und seine Erben, Release 1 5.3.1 GUI-Oberfläche Powershell ist in der Lage, die GUI-Frameworks von .NET anzusprechen. Ein recht leichtes GUT-Framework ist die WinForm-Library 5.3.2 Grafische Darstellung 5.3.3 Datengewinnung und Übergabe 5.4 Umsetzung 5.4.1 GUI 5.4.2 Logik 5.4.3 Excel 5.4.4 Grafische Ausgabe 5.5 Fazit 5.4. Umsetzung 41 Dr. Watson und seine Erben, Release 1 42 Kapitel 5. Grafische Auswertung von EventLogs KAPITEL 6 Aufgabe Sie planen ein Verwaltungstool für die UserAnlage innerhalb ihres ActiveDirectory. Die bisherige Vorgehensweise ist wie folgt: • Es wird eine csv-datei produziert, die von weiteren Poweshell-Skripten abgearbeitet wird. Aus dieser csv-Datei werden dann die entsprechenden Container im AD erzeugt • Das Editieren der csv-Datei war bisher recht aufwändig und fehleranfällig. Sie planen deshalb, die Inhalte der csv-Datei in einer Datenbank zu speichern und Änderungen/Anpassungen der Werte mit Hilfe einer GUI nur innerhalb der Datenbank vorzunehmen. • Zuletzt wird aus den Datensätzen der Tabelle wiederum eine csv-Datei erzeugt. • Die Datenbank mit der Tabelle data ist bereits vorhanden. • Der Systembetreuer hat mit Ihnen bereits ein Konzept einer GUI erarbeitet. 43 Dr. Watson und seine Erben, Release 1 Folgende Idee steckt hinter dieser GUI. 44 Kapitel 6. Aufgabe Dr. Watson und seine Erben, Release 1 Datei GUI Aufgabe Eine GUI-Beschreibung mit Hilfe von PrimalForms liegt vor, wenngleich aber noch nicht alle Ereignishandler definiert wurden. Benutzen sie diese Datei als Ausgangspunkt für ihr Programm. 45 Dr. Watson und seine Erben, Release 1 46 Kapitel 6. Aufgabe KAPITEL 7 Glossar Command Repräsentiert im .NET-Framework den SQL-Befehl, der auf eine Datenbank abgegeben wird. Er braucht dazu noch mindestens ein Connection-Objekt $cmd = New-Object MySql.Data.MySqlClient.MySqlCommand; $cmd.CommandText = $query $results = $cmd.ExecuteReader() Connection Objekt des .NET-Frameworks, welches die Verbindungsinformationen zu einer Datenbank aufnimmt. Er hat häufig eine genau definierte Syntax. Dazu gehören Username/Passwort/Hostname/Datenbankname/Port $connstring = "Server=localhost;Uid=root;Pwd='XXXXX';Database=XXXXXX"; $con = New-Object Mysql.Data.MysqlClient.MysqlConnection; $con.ConnectionString = $connstring; DataReader .NET-Klasse, die Daten eines Command-Objektes aufnimmt. Sie muss über eine Schleife in eine andere Datenstruktur, meist ein Array, übertragen werden, das sie nur forward-only zu lesen ist. PrimalForms Desktop-Programm der Firma Sapien, welches auf einfache Weise GUI-Oberfl#ächen (WinForm) für Powershell erstellt. Leider nicht mehr erhältlich. Provider .NET-Bezeichnung für einen spezifischen Datenbanktreiber eines Herstellers Refaktorisierung Umwandlung bestehenden Quellcodes zur - besseren Verständlichkeit - besseren Weiterverwendbarkeit 47 Dr. Watson und seine Erben, Release 1 48 Kapitel 7. Glossar Stichwortverzeichnis C Command, 47 Connection, 47 D DataReader, 47 P PrimalForms, 47 Provider, 47 R Refaktorisierung, 47 49