Der Gedanke hinter dem PowerShell Script Framework (GitHub Projekt) ist, nur einen kleinen Teil des Scripts statisch auf dem System zu haben, der Rest soll dynamisch nachgeladen werden.

Durch dieses Vorgehen kann das je nach Bedarf ein nachgelagertes Script verändert oder sogar ganz neue Scripte hinzugefügt werden.

Im Detail sieht das Ganze dann so aus:

  1. Updater.ps1 wird gestartet
  2. Updater.ps1 lädt alle Files in seinem XML herunter
  3. Updater.ps1 startet das BaseScript.ps1 (in seinem XML Konfiguriert)
  4. BaseScript.ps1 startet alle Skripte in seinem XML nacheinander aus

Für den Download der weitere Daten ist ein WebServer von Nöten. Hier in meinem Beispiel verwende ich dazu diese WebSite, im Normalfall wird das aber ein interner IIS o.ä. sein.

Das Script Framework GitHub Projekt

GitHub: PS-ScriptFramework-AutoUpdate

PowerShell Script Framework with Auto Update

Das Script Framework im Detail

Updater.ps1

<#	
	.NOTES
    ===========================================================================
    Created by: Markus Kraus
    Organization: mycloudrevolution.com
    Personal Blog:  mycloudrevolution.com
    Twitter: @vMarkus_K
    ===========================================================================
	.DESCRIPTION
    This is a self updating PowerShell Script Framework.
    
    Module-Name: Updater
    Module Version: 1.2
#>
[CmdletBinding()]
Param(
  [Parameter(Mandatory=$False,Position=1)]
   [string] $Config = "Default",
  [Parameter(Mandatory=$False,Position=2)]
   [string] $IgnoreUpdate = $False
)  

#region: Clear Errors
$error.clear()
#endregion

#region: Global Definitions
$Validate = $True
$BaseDir = $env:windir + "\TEMP\PS\"
$LogFile = $BaseDir + "Output.txt"
$BaseURL = "http://mycloudrevolution.com/Projects/PS-Framework/"
$BaseXML = "Updater.xml"
$BasePS1 = "Updater.ps1"
Start-Transcript -Path $LogFile 
Write-Output "`nStarting with Config: $Config"
#endregion

#region: Check amd Create Path
if (!(Test-Path -path $BaseDir)) {
    try {
        New-Item -ItemType directory -Path $BaseDir
        }
    catch {
        # Error out if loading fails
        $Validate = $false
        Write-Error "`nERROR: Failed to Create $BaseDir"  
    }
}
#endregion

#region: Download BaseXML
try {
    $Url = $BaseURL + $BaseXML 
    Invoke-WebRequest $Url -OutFile ($BaseDir + $BaseXML) -ErrorAction Stop
    }
catch {
    # Error out if loading fails
    $Validate = $false
    Write-Error "`nERROR: Failed to Download $BaseXML"  
}
#endregion

#region: Download BasePS1
try {
    $Url = $BaseURL + $BasePS1
    Invoke-WebRequest $Url -OutFile ($BaseDir + $BasePS1) -ErrorAction Stop
    }
catch {
    # Error out if loading fails
    $Validate = $false
    Write-Error "`nERROR: Failed to Download $BasePS1"  
}
#endregion


#region: Check and Load BaseXML
$BaseXMLPath = $BaseDir + $BaseXML
If (Test-Path $BaseXMLPath  ) {
    try {$BaseXMLContent = [XML] (Get-Content $BaseXMLPath )} catch {$Validate = $false; Write-Error "`nERROR: Invalid $BaseXML"}
    } Else {
        # Error out if loading fails
        $Validate = $false
        Write-Error "`nERROR: Cannot load $BaseXML"
    }
#endregion

#region: Download Files from BaseXML
[Array] $BaseFiles =  $BaseXMLContent.Updater.Files.File
Foreach($BaseFile in $BaseFiles){
    try {
        $Url = $BaseURL + $BaseFile
        Invoke-WebRequest $Url -OutFile ($BaseDir + $BaseFile) -ErrorAction Stop
      }
    catch {
      # Error out if loading fails
       $Validate = $false
       Write-Error "`nERROR: Failed to Download $BaseFile"  
    }
}
#endregion

#region: Download Customer Files
if ($Config -ne "Default") {
    [Array] $ConfigFiles = @("Customer-" + $Config + ".xml"; "Customer-" + $Config + ".ps1")
    Foreach($ConfigFile in $ConfigFiles){
        try {
            $Url = $BaseURL + $ConfigFile
            Invoke-WebRequest $Url -OutFile ($BaseDir + $ConfigFile)
        }
        catch {
            # Error out if loading fails
            $Validate = $false
            Write-Error "`nERROR: Failed to Download $ConfigFile"  
        }
    }
 }
#endregion    

#region: Updater Check
$myFile = $MyInvocation.MyCommand.Path
$myFileHash = (Get-FileHash -Path $myFile -Algorithm MD5).Hash
$newFile = ($BaseDir + $BasePS1)
$newFileHash = (Get-FileHash -Path $newFile -Algorithm MD5).Hash

Write-Output "`nMy Updater File: $myFile `nMD5 Hash: $myFileHash"
Write-Output "`nNew Updater File: $newFile `nMD5 Hash: $newFileHash"

if ($myFileHash -ne $newFileHash -and $IgnoreUpdate -eq $False) {
    try {
        Copy-Item $newFile -Destination $($myFile)
        Write-Warning "Replacing local Updater.ps1 with Server Version. Exiting this Version and wait for next run..."
        Stop-Transcript
        Exit
    }
    catch {
            # Error out if replacing fails
            $Validate = $false
            Write-Error "`nERROR: Failed to Update Update.ps1"  
        } 
    }
    elseif ($myFileHash -ne $newFileHash -and $IgnoreUpdate -eq $true) {
        Write-Warning "Replacing local Updater.ps1 with Server Version skipped..."
    }
        elseif ($myFileHash -eq $newFileHash ) {
        Write-output "No Updater.ps1 update needed..."
    }
#endregion

#region: Start Base Script
if($Validate -eq $True) {
    $BaseScript = ($BaseXMLContent.Updater.Variable | Where-Object {$_.Name -eq "BaseScript"}).Value
    Write-Output "`nStarting $BaseDir$BaseScript"  
    Invoke-Expression ($BaseDir + $BaseScript)
    }
    else {
        Write-Warning "Starting $BaseDir$BaseScript skipped... Validation Error." 
    }
#endregion

#region: Finalize
Stop-Transcript

if($Validate -eq $false) {
    Write-Error "`nERROR: Validation Error(s) occured during Script!"
    }
#endregion

BaseScript.ps1

<#	
    .NOTES
    ===========================================================================
    Created by: Markus Kraus
    Organization: mycloudrevolution.com
    Personal Blog:  mycloudrevolution.com
    Twitter: @vMarkus_K
    ===========================================================================
    .DESCRIPTION
    This is a self updating PowerShell Script Framework.
    
    Module-Name: BaseScript
    Module Version: 1.1
#>

#region 1: Global Definitions
$Validate = $True
$BaseScriptXML = "BaseScript.xml"
#endregion

#region 2: Check and Load BaseXML
$BaseScriptXMLPath = $BaseDir + $BaseScriptXML
If (Test-Path $BaseXMLPath  ) {
    try {$BaseScriptXMLContent = [XML] (Get-Content $BaseScriptXMLPath )} catch {$Validate = $false; Write-Error "`nERROR: Invalid $BaseScriptXML"}
    } Else {
        # Error out if loading fails
        $Validate = $false
        Write-Error "`nERROR: Cannot load $BaseScriptXML"
    }
#endregion

#region 3: Base Script Invokes
if($Validate -eq $True) {
    [Array] $Scripts =  $BaseScriptXMLContent.BaseScript.Scripts.Script
    Foreach($Script in $Scripts){
        try {
            Write-Output "`nStarting $BaseDir$Script"  
            Invoke-Expression ($BaseDir + $Script)
        }
        catch {
            # Error out if loading fails
            $Validate = $false
            Write-Error "`nERROR: Failed to Start $Script in $BaseDir"  
            }
    }
}
#endregion

#region 4: Finalize
if($Validate -eq $false) {
    Write-Error "`nERROR: Validation Error(s) occured during Script!"
    }
#endregion

Das Script Framework in der Praxis

Dieses Vorgehen wurde ursprünglich entwickelt um auf einer größeren Anzhal von Kundensystemen ein extern aktualisierbares Script zur Lizenzerfassung laufen zu lassen.

In diesem Praxis beispiel sieht die XML für das BaseScript z.B. so aus:
BaseScript XML

  • Es wird als erstes die AD geprüft und vorbereitet
  • Es werden dann die Exchange SALS erfasst
  • Es werden final die Terminal Server User erfasst

Neue Versionen

17.08.2016

Updater.ps1 – Version 1.2

  • Neu: Selbst Update über Hash des lokalen und remote Files
  • Neu: Parameter zum deaktivieren des Selbst Update (-IgnoreUpdate:$True – Default ist $False)

myExample.ps1 – Version 1.1

  • Verbesserung: Statt einem neuen Eventlog Eintrag wird ein Popup angezeigt

BaseScript.ps1 – Version 1.1

  • Verbesserung: Nur Behebung von  Schönheitsfehlern