PowerShell Module

PowerShell Module sind ein einfacher und definierter Weg, eine PowerShell Umgebung mit zusätzlichen Befehlen und Commandlets zu erweitern. Allerdings müssen die Namen der bereitgestellten Funktionen natürlich eindeutig sind. Eigene Module sind natürlich auch immer ein Weg, Code einfach zu recyclen. Es gibt aber noch andere Ansätze mit PowerShell modular zu arbeiten, die ich auf Powershell Modular beschrieben habe.

Grundlagen- Wo und wie

PowerShell Module sind einem PowerShell Script sehr ähnlich. Sie haben den gleichen Aufbau und können mit den bekannten Editoren erstellt werden und haben einige deutliche Vorteile:

  • Einbinden und Entfernen bei Bedarf
    Im Gegensatz zu Funktionen aus Skripten können Module zur Laufzeit einfach eingebunden aber auch wieder entladen werden
  • Funktionen exportiert
    Per Default werden alle Funktionen im Modul exportiert. Dies kann aber über "Export-ModuleMember" im Modul selbst gesteuert werden, so das auch "private Funktionen" möglich sind, die aus der Session nicht gefunden und aufgerufen werden können. "Privat" sind diese natürlich nur hinsichtlich des Code. Der Source-Code eines PSM1 ist natürlich einsehbar.
  • Lokale Variablen
    Alle in dem Modul verwendeten Variablen sind per Default nicht außerhalb sichtbar.

Interessant ist, dass die Funktionen z.B. z.B. eigene Variablen und Objekte verwenden können und diese zwischen den Aufrufen bestehen bleiben. Es ist damit zwar noch keine Klasse, weil ein Modul nur einmal eingebunden werden kann, aber für viele Zwecke wird oft nur ein "Objekt" verwendet.

Laden, Finden, Sharen

Wenn Sie ein Modul mit "Add-Module" hinzufügen und sie keinen absoluten Pfad mit angeben, dann sucht die PowerShell ein PSModulePath ab.

Beachten Sie, dass die PowerShell Classic (1-5) einen anderen Pfad haben als Powershell Code (6,7 und höher)

# PS5
PS C:\> $env:PSModulePath.Split(";")
C:\Users\user\OneDrive\Dokumente\WindowsPowerShell\Modules;
C:\Program Files\WindowsPowerShell\Modules;
C:\WINDOWS\system32\WindowsPowerShell\v1.0\Modules;
C:\Program Files\Common Files\Skype for Business Server 2019\Modules\;
C:\Program Files\Common Files\Skype for Business Online\Modules\;
C:\Program Files (x86)\Microsoft Azure Information Protection\Powershell

In meiner aktuellen PowerShell 7 (Jan 2021) war die Liste noch deutlich länger.

# PS7 
PS C:\> $env:psmodulepath.Split(";")
C:\\OneDrive\Dokumente\PowerShell\Modules;
C:\Program Files\PowerShell\Modules;c:\program files\powershell\7\Modules;
C:\Program Files\WindowsPowerShell\Modules;
C:\WINDOWS\system32\WindowsPowerShell\v1.0\Modules;
C:\Program Files\Common Files\Skype for Business Server 2019\Modules\;
C:\Program Files\Common Files\Skype for Business Online\Modules\;
C:\Program Files (x86)\Microsoft Azure Information Protection\Powershell

Interessant ist hier, dass ganz am Anfang ein Verzeichnis in meinem Home-Directory zu finden ist und wenn ich die Ordnerumleitung für OneDrive aktiviere, dann werden die Bibliotheken sogar über den Weg repliziert. Ich habe damit auf allen Clients immer den gleichen Stand. Das kann aber auch "größer" werden.

Auch bei der Abfrage der aktuellen Version gibt es zwei Commandlets, die unterschiedliche Daten liefern.

PS C:\> Get-Module powershellget -All | ft version,path

Version Path
------- ----
2.2.3   C:\Users\user\OneDrive\Dokumente\WindowsPowerShell\Modules\PowerShellGet\2.2.3\PSModule.psm1
2.2.5   C:\Users\user\OneDrive\Dokumente\WindowsPowerShell\Modules\PowerShellGet\2.2.5\PSModule.psm1

PS C:\> Get-InstalledModule powershellget -AllVersions | ft version,installedlocation

Version : 2.2.3
InstalledLocation : C:\Users\user\OneDrive\Dokumente\WindowsPowerShell\Modules\PowerShellGet\2.2.3

Version : 2.2.5
InstalledLocation : C:\Users\user\OneDrive\Dokumente\WindowsPowerShell\Modules\PowerShellGet\2.2.5

Die PowerShell beobachtet dabei die Daten in der folgenden Reihenfolge:

  • PSD1 - Manifest-Datei
  • PSM1 - Skriptmodul
  • DLL - Kompiliertes Modul

Das richtige Modul laden

Jeder Entwickler kann natürlich selbst ein "Modul" schreiben und veröffentlichen. Der Name ist allerdings der primäre Schlüssel und

Leider lädt die PowerShell nicht immer die "neueste Version" sondern die er nach der Path-Reihenfolge zuerst findet. Nur wenn Sie mit "import-module" auch eine Versionsnummer oder minimale Versionsnummer mit angeben, werden ältere Versionen übersprungen. Sie können aber auch absichtlich ältere oder neuere Versionen laden. Das geht sogar gleichzeitig in der gleichen Shell, wenn Sie mit einem Prefix arbeiten. Im Januar gab es z.B. zwei PowerShell-Module für Microsoft Teams. Version3.1.0 war aktuell aber 3.0.1-preview enthält das Commandlet "Get-CsUserCallingSettings". Wenn ich beide PowerShells nutzen will, kann ich diese auch parallel installieren und verbinden

import-module MicrosoftTeams -requiredversion 3.1.0
import-module MicrosoftTeams -requiredversion 3.0.1 -prefix 301

Connect-MicrosoftTeams
Connect-301MicrosoftTeams

Alle Commandlets der 3.0.1-Version haben nach dem Verb ein Prefix bekommen. Bei "Get-CsUserCallingSettings" ist das nun nicht so kritisch, denn das Commandlet gibt es in der 3.1.0 nicht und das es keinen Konflikt gibt, funktioniert es auch so.

Tipp: Wenn Sie in ihren Skripten ein Modul einbinden, dann verwenden Sie den Parameter "-MinimumVersion", damit sie keine zu alte Version instanzieren.

Ein Modul wird aber immer nur einmal geladen. Gerade wenn Sie Module entwickeln und ändern müssen Sie das Modul in der TestUmgebung immer erst entladen und neu laden oder mit "-force" laden. Bestehende Variablen im Modul werden dann aber natürlich zurück gesetzt.

Module können sogar verschachtelt werden. Schon fast wie wenn Sie ein bestehendes Module durch eigene Funktionen bereichern können. Wenn ein fertiges Modul ihnen zu viele Parameter hat, können Sie einfach ein eigenes Modul nutzen, welche das andere Modul einbindet und ihre lokal gültigen Parameter vorausfüllt. Dabei sind nur die Commandlets des Hauptmoduls erreichbar.

Wenn man gewissenhaft Module in sich abgeschlossen entwickelt, z.B. auch unabhängig von globalen Voraussetzungen, dann können Sie sich eine Bausteinsammlung aufbauen, die sie immer wieder verwenden können.

User oder Admin?

Damit stellt sich natürlich die Frage, wer Module auf einem Computer installiert und wie diese verwaltet werden. Grundsätzlich kann jeder Anwender selbst nicht nur PowerShell-Module entwickeln sondern auch in seinen persönlichen Bereich installieren. Über den PSModulePath werden die auch immer zuerst gefunden. Das ist auf einem Admin-PC oder Entwickler-PC genauso praktisch wie die Verwendung von Modulen auf Clients per Softwareverteilung. Allerdings kann das zu einer "Versionshölle" führen

Auf Servern und Job-Systemen spricht der Betrieb eher für eine zentrale Bereitstellung der Versionen. Hier sollte ich Module als Administrator besser zentral installieren und aktualisieren. Ein wenig lässt sich das mit der klassischen Softwareverteilung in "C:\Programme" und %userprofile\local\Appdata" vergleichen, wo Anwender eigene Apps installieren können.

Beim "Install-Module" kann ich den Scope mit geben. Mache ich das als Anwender, dann sehe ich aber ggfls. die Warnung.

PS C:\> Install-Module microsoftteams -Force -Scope allusers

Install-Module : Administrator rights are required to install modules in 'C:\Program Files\WindowsPowerShell\Modules'.
Log on to the computer with an account that has Administrator rights, and then try again, or install
'C:\Users\user\OneDrive\Dokumente\WindowsPowerShell\Modules' by adding "-Scope CurrentUser" to your command. You
can also try running the Windows PowerShell session with elevated rights (Run as Administrator).

Die Nutzung als "Anwender" oder Administrator wirkt sich auch auf den Befehlt "Get-InstalledModule" aus.

PS C:\> Get-InstalledModule | ft name,version,installedlocation

Name                                          Version          InstalledLocation
----                                          -------          -----------------
NuGet                                         1.3.3            C:\Program Files\WindowsPowerShell\Modules\NuGet\1.3.3
AzureAD                                       2.0.2.140        C:\Users\user\SkyDrive\Dokumente\WindowsPowerShell...
MicrosoftTeams                                3.1.0            C:\Users\user\SkyDrive\Dokumente\WindowsPowerShell...
PowerShellGet                                 2.2.5            C:\Users\user\SkyDrive\Dokumente\WindowsPowerShell...

Bei einer Auflistung bekomme ich noch die gleichen Informationen wenn ich das als "user" und einmal als "user als Admin (UAC)"starte. Wenn ich mich aber mit einem anderen Benutzer anmelde dann hat der natürlich ein eigenes Heimatverzeichnis, und entsprechend leerer ist es hier.

PS C:\> Get-InstalledModule | ft name,version,installedlocation

Name                                          Version          InstalledLocation
----                                          -------          -----------------
NuGet                                         1.3.3            C:\Program Files\WindowsPowerShell\Modules\NuGet\1.3.3

Wenn ich mir aber die Details zu Microsoft Teams anzeige, dann sehe ich erst alle parallel installierte Versionen.

PS C:\> Get-InstalledModule microsoftteams -AllVersions | ft name,version,installedlocation

Name           Version       InstalledLocation
----           -------       -----------------
MicrosoftTeams 2.5.1         C:\Users\user\OneDrive\Dokumente\WindowsPowerShell\Modules\MicrosoftTeams\2.5.1
MicrosoftTeams 3.0.1-preview C:\Users\user\OneDrive\Dokumente\WindowsPowerShell\Modules\MicrosoftTeams\3.0.1
MicrosoftTeams 3.1.0         C:\Users\user\OneDrive\Dokumente\WindowsPowerShell\Modules\MicrosoftTeams\3.1.0

Wenn ich allein auf meinem PC bin, dann kann ich die Module auch im eigenen Verzeichnis installieren. Ich sollte aber dann daran denken, wenn ich Powershell-Module z.B. in anderen Tasks oder Programmen wie PRTG nutzen, die nicht mit meinem Benutzeraccount laufen.

Daher bin ich doch eher ein Freund einer zentralen Bereitstellung, auch wenn für ein manuellesUpdate dann wieder Admin-Rechte erforderlich sind.

Libraries, Versionen und Updates

Mit "Install-Module" können sie ganz schnell neue Bausteine in ihre PowerShell einbinden. Die Module kommen aus dem Internet aus mehr oder minder vertrauenswürdigen Quellen wie z.B. https://www.powershellgallery.com/. Hier lohnt sich immer mal ein Blick. Siehe dazu auch PowerShell Gallery. Allerdings machen die Module kein automatisches Update und sie können sogar mehrere Versionen nebeneinander betreiben. Das füllt nicht nur die Festplatte auf sondern könnte auch ein Risiko durch alten fehlerhaften Code beinhalten. Ein Update ist aber auch nicht immer passend, wenn neuere Version Commandlets gestrichen oder verändert haben. Hinzu kommt, dass die Module je nach Powershell-Version, CPU-Version und Benutzer an unterschiedlichen Orten installiert werden, genau genommen müssten Sie eine Tabelle in der folgenden Art führen:

User LocalSystem CurrentUser
BIT-Version 32bit 64bit 32bit 64bit
PowerShell Version 5/6 7 5/6 7 5/6 7 5/6 7

Modulname

 

 

 

 

 

 

 

 

Modulname

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

Und das gilt nur für den aktuellen Benutzer:

Es ist nervig, wenn Sie ein Module als Benutzer aktualisieren wollen, was ein Administrator installiert hat. Dann muss auch das Update durch einen Administrator erfolgen.

Tipp: Installieren Sie Module zumindest auf Servern und gemeinsam genutzten Computern als "Administrator", damit alle dort angemeldeten Benutzer die gleichen Versionen nutzen.
Auf individuellen Systemen verzichte ich auf Admin-rechte und zentrale Installation und überlasse es dem Benutzer.

Folgendes Skript prüft alle installierten Module, ob es neuere Versionen gibt:

# Check installed module and show version 
$installedmodules = Get-InstalledModule

foreach ($currentmodul in $installedmodules) {
   Write-Host “Check: $($currentmodul.Name) – $($currentmodul.version)" -nonewline
   $latestAvail = Find-Module $currentmodul.Name
   if ($currentmodul.version -eq $latestAvail.version) {
      Write-host " Current Version installed" -foregroundcolor green
   }
   else {
      Write-host " Newer Version $($latestAvail.version) available” -foregroundcolor yellow
   }
}

Ein "Update-Module" installiert nur die neueste Version aber entfernt keine frühere Version. Es könnte ja Abhängigkeiten zu früheren Versionen geben.

Um Update habe ich mir ein Skript gebaut, welches ich bei Bedarf in der jeweiligen PowerShell ausführen. Ich versuche mich dazu aber schon auf "PowerShell 7 x64" zu beschränken und nutzt nur bei Inkompatibilitäten die andere Versionen. Das Skript prüft nicht auf Abhängigkeiten!

Write-Host "Sammle alle installierten Module"
$installedmodules = get-installedmodule

Write-Host "Starte Verarbeitung"
foreach ($module in $installedmodules) {
   write-host "Modul $($module.name)  -  Check for Updates and try update"
   update-module $module.name -Force
   write-host 
   $latestversion = get-installedmodule -name $module.name
   $allversions = get-installedmodule -name $module.name -allversions
   if ($allversions.count -ge 1) {
      write-host "  Anzahl installierter Versionen $($allversions.count)"
      foreach ($entry in $allversions) {
         if ($entry.version -ne $latestversion.version) {
            write-host "Uninstall Modul: $($entry.name)   Version: $($entry.version)" -nonewline
             $entry | uninstall-module -force
             write-host " DONE"
         }
         else {
            write-host "  Keep current Version $($latestversion.version)"
         }
      }
   }
   else {
      write-host " Only one Version $($latestversion.version)"
   }
}
write-host "done"

Die "Deinstallation" können Sie natürlich auch ganz einfach von Hand durchführen. Einfach das Verzeichnis löschen.

Beispiel einer PSM1-Datei

Es gibt viele Beispiele im Internet, die den Einsatz von Modulen zeigen. Ich möchte mich daher auf eine einfaches Modul als Muster beschränken:

# Modulsample to explain basic modules
[long]$script:test=0  # initialize Variable

function add-Modulsample {
   $script:test++
   write-host "Module1: Sub Plus-Modulsample done: Test=$script:test"
}

function get-Modulsample {
   $script:test
   write-host "Module1: Sub Get-Modulsample done: Test=$script:test"
}

function Set-Modulsample ($value ){
   $script:test = $value
   write-host "Module1: Sub Set-Modulsample done: Test=$script:test"
}

# Es ist ratsam am Ende eine Ausgabe zu generieren. Diese wir beim Laden des Moduls angezeigt
write-host "modulsample: Ready to use"

Diese Datei kann dann überall liegen und einfach mit "import-module <pfad\dati.psm1> importiert werden.

Auf ein paar Dinge möchte ich hier hinweisen.

  • Ausgabe am Anfang/Ende
    Wenn Sie außerhalb der Funktionen, die später als Commandlet agieren auch Code verwenden, dann wird dieser beim Laden des Module direkt ausgeführt. Es ist ratsam hier eine Bildschirmausgabe vorzusehen.
  • Initialisierung
    Sie können in dem Code aber auch schon erste Komponenten Initialisieren. Ich nutze dies im Beispiel für die Variable "Test", welche durch das Prefix "$Script:" innerhalb des Skripts als global definiert wird. Sie könnten hier aber auch z.B. SQL-Connections, PSSessions oder ADSI-Verbindungen aufbauen.
  • Variablen-Scope
    Sie sehen hier auch die Feinheiten beim Einsatz von Variablen. "Test" ist global bezüglich des Skripts, sonst könnte ich die Werte nicht in den Funktionen verändern. Die Variable ist aber nicht in der Session erreichbar, welche diese Modul importiert.

Experimentieren Sie einfach etwas mit Modulen und verändern Sie den Beispielcode, um das unterschiedliche Verhalten zu sehen. Vergessen Sie aber nicht nach Änderungen am Modul diese neu zu laden.

Module und Klassen

Noch sind Module einfach weitere PowerShell-befehle, die Sie nach dem Import nutzen können. Aber damit sind es noch keine Klassen, die mehrfach instanziert werden können und ihre internen Wert und Funktionen und nach extern erreichbare Properties und Methoden haben. Und genau das geht auch mit Modulen. Sie müssen Sie beim "Import-Module" oder New-Module einfach mit "-AsCustomObject" aufrufen. Damit werden aus den Funktionen entsprechende Methoden und exportierte Variablen werden zu Properties.

Hierbei zeigen sich aber zwei Probleme:

  • Methoden sind Funktionen mit Parametern
    Es müssen bei der Methode immer alle Parameter angegeben werden. Die bislang gekannte Flexibilität
  • Funktionsnamen "Verb-Noun" funktionieren nicht
    Zwar wird das Modul als Objekt mit der Methode mit dem Namen importiert aber ist nicht aufrufbar. Das ist ja auch nicht weiter schlimm, wenn man die Funktionen der Moduls dann einfach umbenennt.

Mit beiden Einschränkungen kann man natürlich leben auch wenn Sie nicht schön sind. Um aber nicht in Konflikt mit anderen Namen zu kommen, sollte so ein Modul dann immer mit dem Parameter "CustomObject" geladen werden.

Weitere Links

Advanced Automation using Windows PowerShell 2.0
http://channel9.msdn.com/Events/TechEd/NorthAmerica/2011/WSV406
Einstieg in Module ab 38. Minute

Windows PowerShell Modules - Patterns and Practices - Dan Harman
http://www.youtube.com/watch?v=yDmkXZVD55w
US TEC 2011 PowerShell Deep Dive conference, Windows PowerShell Program Manager Dan Harman

Die PowerShell Module 4:28Min
http://www.youtube.com/watch?v=Tmf7EFUXEhg&feature=related