Windows PowerShellErstellen eines besseren Inventurprogramms

Don Jones

Inhalt

Objekte sind immer gleich
Flexible Ausgabe
Flexible Eingabe
Wie verhält es sich mit Fehlern?
Gewährleisten optimalen Nutzens

In der letzten Ausgabe dieser Rubrik wurde eine Funktion erstellt, mit der Service Pack-Inventarinformationen von mehreren Remotecomputern abgerufen werden konnten. Obwohl es sich dabei um ein äußerst praktisches Tool handelt, das recht nützlich sein kann, bin ich auch auf den Prozess eingegangen, den ich zur Entwicklung des Tools verwendet habe. In dieser Ausgabe möchte ich Ihnen den von mir verwendeten Prozess zeigen, damit Sie besser in der Lage sind, eigene Funktionen zu erstellen.

Die in der letzten Ausgabe erstellte Funktion sieht folgendermaßen aus:

Function Get-SPInventory {
  PROCESS {
    $wmi = Get-WmiObject Win32_OperatingSystem  
      –comp $_ | 
    Select CSName,BuildNumber,
      ServicePackMajorVersion
    Write-Output $wmi
  }
}

Sie können sie wie folgt verwenden:

Get-Content c:\computernames.txt | 
Get-SPInventory

Dies funktioniert gut, wenn die Textdatei einen Computernamen pro Zeile enthält.

Eine spezielle Schwachstelle dieser Funktion besteht darin, dass sie auf die Rückgabe von Daten aus einer einzigen WMI-Klasse (Windows Management Instrumentation, Windows-Verwaltungsinstrumentation) beschränkt ist. Wie ist jedoch vorzugehen, wenn auch die BIOS-Seriennummer zurückgegeben werden soll?

Objekte sind immer gleich

Das Problem besteht darin, dass die Funktion die Win32_OperatingSystem-Klasse aus WMI abruft und dieses Objekt einfach ausgibt. Die Win32_OperatingSystem-Klasse ist wie alle Objekte in Bezug auf die Daten, die sie enthält, unveränderlich – sie beinhaltet keine BIOS-Seriennummer und kann dies auch nie. Solange Sie lediglich Win32_OperatingSystem-Objekte ausgeben, kann die Ausgabe nie eine Seriennummer enthalten.

Sie können noch so sehr in WMI stöbern – Sie werden keine einzige Objektklasse finden, die sowohl BIOS-Seriennummern als auch Service Pack-Informationen enthält. Daher kann die Funktion nicht einfach eine WMI-Klasse ausgeben. Stattdessen muss sie ein benutzerdefiniertes Objekt ausgeben, das Sie dynamisch erstellen und das alle benötigten Daten enthält.

Um ein neues, leeres Objekt ohne Eigenschaften zu erstellen, führen Sie einfach den folgenden Code aus:

$obj = New-Object PSObject

Das neue Objekt wird in der Variablen „$obj“ gespeichert, und Sie können ihm beliebige Daten hinzufügen. Nachdem Sie alle erforderlichen Daten hinzugefügt haben, wird es zur Ausgabe der Funktion.

Innerhalb der Funktion wurden die Win32_OperatingSystem-Informationen abgerufen und in der Variablen „$wmi“ gespeichert. Die Verwendung dieser Informationen ist so einfach wie das Verweisen auf Eigenschaften von $wmi. Um beispielsweise die BuildNumber-Eigenschaft abzurufen, verwenden Sie folgenden Code:

$wmi.BuildNumber

Um dem benutzerdefinierten Objekt eine Eigenschaft hinzuzufügen, müssen Sie das Objekt (das in der Variablen „$obj“ gespeichert ist) an Add-Member weiterleiten. Sie teilen Add-Member den Typ der Eigenschaft, die Sie hinzufügen möchten (immer eine NoteProperty), den Namen der hinzuzufügenden Eigenschaft und den Wert, den die Eigenschaft aufweisen soll, folgendermaßen mit:

$obj | Add-Member NoteProperty BuildNumber 
($wmi.BuildNumber)

Sie können diesen Vorgang auch für den Namen des Computersystems und die Service Pack-Version durchführen:

$obj | Add-Member NotePropertyCSName 
  ($wmi.CSName)
$obj | Add-Member NotePropertySPVersion 
  ($wmi.ServicePackMajorVersion)

Durch Umschließen dieses Codes ergibt sich eine neue Funktion (siehe Abbildung 1). Beachten Sie, dass die Write-Output-Zeile so geändert wurde, dass anstelle des ursprünglichen $wmi-Objekts das benutzerdefinierte Objekt ausgegeben wird.

Abbildung 1 Ein benutzerdefiniertes Objekt

Function Get-SPInventory {
  PROCESS {
    $wmi = Get-WmiObject Win32_       OperatingSystem –comp $_ | 
       Select CSName,BuildNumber,ServicePack         MajorVersion
    $obj = New-Object PSObject
    $obj | Add-Member NoteProperty 
      BuildNumber ($wmi.BuildNumber)
    $obj | Add-Member NoteProperty CSName      ($wmi.CSName) 
    $obj | Add-Member NoteProperty SPVersion 
      ($wmi.ServicePackMajorVersion)
    Write-Output $obj
  }
}

Jetzt müssen Sie die BIOS-Seriennummer abrufen. Nach einigem Suchen stoßen Sie auf die Win32_BIOS-Klasse, die über eine SerialNumber-Eigenschaft verfügt. (Abbildung 2 zeigt einen Teil der Seite der Onlinedokumentation.) Daher müssen Sie einfach die Win32_BIOS-Klasse abfragen und dem benutzerdefinierten Objekt die SerialNumber-Eigenschaft hinzufügen, und schon sind Sie fertig! Die überarbeitete Funktion ist in Abbildung 3 zu sehen.

fig02.gif

Abbildung 2 Dokumentationsseite für die Win32_BIOS-Klasse (zum Vergrößern auf das Bild klicken)

Abbildung 3 Mit SerialNumber-Eigenschaft

Function Get-SPInventory {
  PROCESS {
    $wmi = Get-WmiObject Win32_      OperatingSystem –comp $_ |    
      Select CSName,BuildNumber,ServicePack        MajorVersion
    $obj = New-Object PSObject
    $obj | Add-Member NoteProperty 
      BuildNumber ($wmi.BuildNumber)
    $obj | Add-Member NoteProperty CSName      ($wmi.CSName)
    $obj | Add-Member NoteProperty SPVersion 
      ($wmi.ServicePackMajorVersion)

    $wmi = Get-WmiObject Win32_BIOS –comp $_ | 
      Select SerialNumber
    $obj | Add-Member NoteProperty BIOSSerial 
      ($wmi.SerialNumber)
    Write-Output $obj
  }
}

Flexible Ausgabe

Durch Verwendung eines benutzerdefinierten Objekts für die Ausgabe haben Sie verschiedene potenzielle Verwendungszwecke für diese Funktion geschaffen. Um beispielsweise nur Computer mit Windows Server 2008 einzubeziehen, können Sie den folgenden Code verwenden:

Gc c:\computernames.txt | Get-SPInventory | 
Where { $_.BuildNumber –eq 6001 }

Alternativ können Sie mithilfe des folgenden Codes eine HTML-Datei mit einer Liste aller Windows Vista-Computer erstellen, auf denen Service Pack 1 nicht installiert ist:

Gc c:\computernames.txt | Get-SPInventory | 
Where { $_.BuildNumber –eq 6000 } | 
ConvertTo-HTML | Out-File c:\VistaInventory.html

Die Möglichkeiten sind endlos – XML, CSV, Tabellen, Listen, HTML, sortiert, gefiltert, gruppiert und so weiter. Die integrierten Cmdlets von Windows PowerShell wurden für die Funktion in Verbindung mit Objekten entworfen. Durch Erstellen von Objekten für die Ausgabe können Sie das gesamte Funktionsspektrum von Windows PowerShell nutzen – ohne zusätzliche Arbeit!

Flexible Eingabe

Leider ist die Funktion in Bezug auf die Eingabe nicht genauso flexibel. Diese Einschränkung ist teilweise darauf zurückzuführen, wie Funktionen in Version 1 von Windows PowerShell entworfen wurden – mit Version 2 werden Skript-Cmdlets eingeführt, die eine viel größere Flexibilität bieten.

Die Funktion erwartet als Pipelineeingabe einfache Zeichenfolgenobjekte. Sie würde nicht funktionieren, wenn Sie wie folgt den Get-Content-Befehl beispielsweise durch ein Cmdlet zum Abrufen aller Computernamen aus Active Directory ersetzen:

Get-QADComputer | Get-SPInventory

In diesem Fall wird durch den Get-QADComputer-Befehl (Teil eines kostenlosen Satzes von Cmdlets zur Active Directory-Verwaltung, den Sie unter quest.com/powershell herunterladen können) anstelle von einfachen Zeichenfolgenobjekten ein Objekt mit einer Name-Eigenschaft zurückgegeben. Damit dieser Befehl funktioniert, müssten Sie die Funktion wie in Abbildung 4 dargestellt anpassen. Beachten Sie, dass die Änderungen rot hervorgehoben sind.

Abbildung 4 Das Endprodukt

Function Get-SPInventory {
  PROCESS {
    $wmi = Get-WmiObject Win32_OperatingSystem –<span class="clsRed" xmlns="http://www.w3.org/1999/xhtml">comp 
    $_.Name</span> | Select 
      CSName,BuildNumber,
        ServicePackMajorVersion
    $obj = New-Object PSObject
    $obj | Add-Member NoteProperty 
      BuildNumber ($wmi.BuildNumber)
    $obj | Add-Member NoteProperty CSName 
      ($wmi.CSName)
    $obj | Add-Member NoteProperty SPVersion  
      ($wmi.ServicePackMajorVersion)

    $wmi = Get-WmiObject Win32_BIOS –comp <span class="clsRed" xmlns="http://www.w3.org/1999/xhtml">$_.Name |</span> 
      Select SerialNumber
    $obj | Add-Member NoteProperty BIOSSerial 
      ($wmi.SerialNumber)
    Write-Output $obj
  }
}

Statt das gesamte Pipelineobjekt an den Parameter „–computerName“ zu übergeben, arbeitet die Funktion jetzt mit der Name-Eigenschaft des Pipelineobjekts. Diese kleine Änderung ist im Hinblick auf die Flexibilität die Mühe wert – mithilfe von Get-QADComputer können Sie die Eingabe beispielsweise auf Computer in einer bestimmten Organisationseinheit oder auf Computer beschränken, die andere Kriterien auf Grundlage von Active Directory-Attributen erfüllen.

Wie verhält es sich mit Fehlern?

Es ist unvermeidlich, dass diese Funktion schließlich auf einen Computer trifft, zu dem sie keine Verbindung herstellen kann oder darf und so weiter. In ihrer derzeitigen Form zeigt die Funktion im Windows PowerShell-Konsolenfenster eine Fehlermeldung mit rotem Text an und fährt dann mit dem nächsten Computer fort.

Möglicherweise wird genau das von Ihnen gewünscht. Vielleicht ziehen Sie es aber vor, diese Fehlermeldungen durch die Funktion unterdrücken zu lassen. Dies lässt sich einfach erreichen, indem Sie die folgende Zeile am Anfang des PROCESS-Skriptblocks der Funktion hinzufügen:

$ErrorActionPreference = "SilentlyContinue"

Andererseits möchten Sie vielleicht einen komplexeren Ansatz verwenden und ein Fehlerprotokoll erstellen, in dem die Namen der Computer aufgelistet werden, die nicht erreichbar waren. Windows PowerShell ist hierzu in der Lage, aber Sie müssen sich bis zum nächsten Monat gedulden, um zu erfahren, wie Sie dazu vorgehen.

Cmdlet des Monats: Export-Alias und Import-Alias

Die folgende Methode eignet sich hervorragend, um benutzerdefinierte Aliase freizugeben, die Sie erstellt haben, oder um Ihre benutzerdefinierten Aliase bei jedem Start der Shell zu laden. Nachdem Sie alle gewünschten Aliase erstellt haben, exportieren Sie sie wie folgt in eine Datei:

Export-Alias c:\aliases.xml

Um diese Aliase dann wieder in die Shell zu laden, führen Sie den folgenden Befehl aus:

Import-Alias c:\aliases.xml

Sie können diesen zweiten Befehl in Ihr Windows PowerShell-Profilskript einfügen, damit er bei jedem Start der Shell ausgeführt wird. Sie können die Datei auch auf einer Netzwerkfreigabe speichern, damit sie für die anderen Administratoren, mit denen Sie arbeiten, leicht verfügbar ist.

Gewährleisten optimalen Nutzens

Bisher haben Sie diese Funktion möglicherweise in eine .ps1-Datei eingegeben und das Skript ausgeführt. Das ist zwar alles schön und gut, bringt aber einige Probleme im Hinblick auf die Benutzerfreundlichkeit mit sich. Ein Problem besteht darin, dass Sie jedes Mal, wenn Sie die Funktion verwenden möchten, Folgendes tun müssen: die Skriptdatei öffnen, die Zeile ändern, über die die Funktion aufgerufen wird (um die jeweils benötigten Ausgabe- oder Sortierbefehle oder andere Befehle hinzuzufügen), sowie das Skript speichern und ausführen. Es wäre viel einfacher, wenn die Funktion selbst direkt im Konsolenfenster der Shell verfügbar wäre – fast so wie ein Cmdlet.

Hierfür gibt es zwei Möglichkeiten. Die erste besteht darin, die Funktion einfach in ein Windows PowerShell-Profilskript zu kopieren, d. h. in eine der vier Skriptdateien, die von der Shell bei jedem Start automatisch ausgeführt werden (sofern sie vorhanden sind). In der Kurzanleitung, die mit Windows PowerShell installiert wird, sind die vier Speicherorte aufgeführt.

Ich verwende in der Regel die Datei namens „profile.ps1“, die im Ordner „WindowsPowerShell“ (keine Leerzeichen) im Ordner „Dokumente“ (oder auf Windows XP- und Windows Server 2003-Computern im Ordner „Eigene Dateien“) gespeichert werden muss. Sobald die Funktion in Ihrem Profil enthalten ist, ist sie immer dann, wenn Sie in der Shell arbeiten, direkt über die Eingabeaufforderung verfügbar.

Die zweite Option besteht darin, die Funktion – nur die Funktion und keinen anderen Code – in ein Skript einzufügen und dieses Skript per Dot Sourcing in die Shell einzubinden. Wenn Sie ein Skript ausführen, erstellt Windows PowerShell in der Regel einen neuen Bereich für das Skript. Alle Vorgänge im Skript finden innerhalb dieses Bereichs statt, beispielsweise die Definition von Funktionen. Wenn die Ausführung des Skripts beendet ist, wird der Bereich verworfen, und alles in diesem Bereich geht verloren.

Wenn Sie über eine Skriptdatei verfügen, die nur die Get-SPInventory-Funktion enthält, sollten Sie daher bedenken, dass durch Ausführen dieses Skripts ein neuer Bereich erstellt, die Funktion definiert und der Bereich verworfen wird und die Funktion dann verloren geht. Offensichtlich ist das für Sie nicht sehr nützlich.

Dagegen wird durch Dot Sourcing ein Skript ohne Erstellung eines neuen Bereichs ausgeführt. Wenn Sie Get-SPInventory in eine Datei namens „MyFunction.ps1“ eingefügt haben, können Sie sie folgendermaßen per Dot Sourcing einbinden:

PS C:\> . c:\functions\myfunction

Achten Sie auf den einen Punkt und das Leerzeichen vor dem Pfad und Dateinamen des Skripts. Dadurch wird die Shell angewiesen, das Skript im aktuellen Bereich auszuführen, was bedeutet, dass der gesamte Inhalt des Skripts auch nach Beendigung der Skriptausführung erhalten bleibt. Das Ergebnis ist, dass das Get-SPInventory-Skript jetzt in der Shell definiert ist und Sie es beinahe wie ein Cmdlet direkt über die Befehlszeile verwenden können.

Don Jones ist Mitbegründer von Concentrated Technology und Autor zahlreicher IT-Bücher. Lesen Sie seine wöchentlichen Windows PowerShell-Tipps, oder kontaktieren Sie ihn unter ConcentratedTech.com.