Windows 10 Offline Servicing

PowerShell-Scripte für die schnelle Bereitstellung von aktuell gepatchten Installationsmedien von Window 10 Build 1909. Die Formate .WIM und .ISO sind teils Microsoft- und teils allgemeinspezifisch.

Ziel #1 - Erstellen einer aktuell gepatchten Windows 10-Ausgabe

Ausgangslage: IT-Profressionals stehen möglicherweise vor der Aufgabe, das Client-Betriebssystem Windows 10 laufend auf einem aktuell gepatchten Level zu bringen oder zumindest ein sogenanntes "Referenz-Image" zyklisch auf die Endgeräte "auszurollen". Monatlich erscheinen für Windows 10 Sicherheitsverbesserungen und Stabilitätsupdates (siehe Update-Verlauf). Diese müssen aufgrund von Compliance-Richtlinien (ISO 27001, NIST, STIG, BSI) sowie aus regulatorischen Gründen (DS-GVO, UGB etc.) in Organisationen, Behörden, Konzernen, Vereinen etc. angewendet werden. Das Update-Prozedere kontinuierlich (PDCA) zu ser­vi­cie­ren, fällt aufgrund der Komplexität vielen Akteuren nicht leicht - angesichts der täglichen vielen Routinen und Projekt-Paketen von IT-Verantwortlichen. Ansätze zur Semi- oder Vollautomatisierung sind gefragt. Eine Semi-Automatisierung, wie die folgenden PowerShell-Scripte, unterstützt bei der Reduzierung der Komplexität, damit ein kontinuierlichen Patch-Management von Microsoft Windows-10-Betriebssystemen gewährleistet werden kann.

Vorteile der scriptbasierten Variante

  • Transparenz, keine Kosten: Quelloffen, frei verfügbar und skalierbar für Endanwender.

  • Standard: Abbilder werden zu 100-Prozent-Hersteller-Empfehlung und der Erfahrung der Authoren wie Johan Arwidmark und Alexander Scharmer "lege artis" erzeugt.

  • Aktualität: Die aktuellsten SSU- und LCU-Updates werden automatisch - mit Unterstützung des PowerShell-Moduls OSDSUS/OSDUpdate von David Segura - direkt Online von der Microsoft Update-Cataloge-Site bezogen und integriert.

  • Optimiert "Small Footprint": Die Abbilder sind nach den Richtlinien des Herstellers und der WIM-Technologie auf geringen "Overhead" und kleinen Dateigrößen optimiert.

  • Simplifiziert: Das PowerShell-Script Create-W10RefImagev1909.ps1 ist nur auf das notwendigste reduziert, um ein natürliches und aktuelles Windows 10 Image zu erzeugen. Es verändert keine kritischen Komponenten oder ersetzt diese. Es werden auch keine unnötigen Dienste oder Services aktiviert oder deaktiviert.

Voraussetzungen

Es wird eine Verbindung zum Internet benötigt. Das Herunterladen der PowerShell-Module sowie der Software-Patches erfolgt auf Ressourcen in der Cloud. Stellen Sie deshalb eine Internet-Konnektivität sicher.

Der PowerShell-Oneliner meldet die erfolgreiche Internet-Konnektivität zurück:

if (Test-Connection -Count 1 -Quiet 1.1.1.1) {Write-Output "Internet-Konnektivität erfolgreich hergestellt."}

Benutzen Sie eine Privileged Access Workstations (PAWs), wo Sie administrative Rechte besitzen um Programm-Installationen vornehmen zu können. Dies kann ein Notebook, ein Desktop-PC oder sonstiges Endgerät mit Windows 10 Home, Pro, Education, Enterprise (in deutscher Sprachvariante) sein.

Bereitstellungsumgebung für Offline-Patchen einrichten

Benutzen Sie nachfolgendes PowerShell-Script mit der Bezeichnung Install-SetupDeploymentv1909.ps1für die initiale Bereitstellung für das Offline-Patchen von Windows 10. Die Erstkonfiguration erstellt neben der Ordnerstruktur C:\SetupDeployment ebenfalls die Installation der Microsoft Windows ADK-Tools in der Version 1903.

Folgende Komponenten umfasst die Script-Installation auf dem PAW-Endgerät

  1. Windows Assessment Toolkit and the Windows Performance Toolkit to assess the quality and performance of systems or components.

  2. Deployment tools such as WinPE, Sysprep, and other tools that you can use to customize and deploy Windows 10 images.

  3. Windows Preinstallation Environment (PE).

  4. Download the Windows System Image Manager (WSIM) 1903 update (fix).

Empfehlung für den korrekten Aufruf des PS Install-SetupDeploymentv1909.ps1 ist:

powershell -ex bypass -co <pfad zum PS Script Install-SetupDeploymentv1909.ps1>

Die obige Befehlszeile in einer privilegierten Kommandozeile (CMD) unter Windows 10 aufrufen (z. B. als Administrator).

Nach dem Aufruf mit privilegierten Berechtigungen wird die Installation aller Komponenten automatisch durchgeführt. Dies kann je nach Ressourcen bzw. Leistung des Endgerätes zwischen 15 und 45 Minuten dauern - bitte haben Sie hier etwas Geduld.

Source-Code des Scripts Install-SetupDeploymentv1909.ps1

Install-SetupDeploymentv1909.ps1

# Check for elevation
If (-NOT ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole(`
    [Security.Principal.WindowsBuiltInRole] "Administrator"))
{
    Write-Warning "Oupps, you need to run this script from an elevated PowerShell prompt!`nPlease start the PowerShell prompt as an Administrator and re-run the script."
	Write-Warning "Aborting script..."
    Break
}

# Our Deployment Folder Structure
  $deploy_dir              = "$env:HOMEDRIVE\SetupDeployment"
  $deploy_DismPath         = "$env:HOMEDRIVE\Program Files (x86)\Windows Kits\10\Assessment and Deployment Kit\Deployment Tools\amd64\DISM"
  $deploy_OscdImgPath      = "$env:HOMEDRIVE\Program Files (x86)\Windows Kits\10\Assessment and Deployment Kit\Deployment Tools\amd64\Oscdimg"
  $deploy_src              = "$deploy_dir\src"
  $deploy_drivers          = "$deploy_dir\src\drivers"
  $deploy_ADKOffline       = "$deploy_dir\src\ADKOffline"
  $deploy_ADKWinPeOffline  = "$deploy_dir\src\ADKWinPeOffline"
  # $deploy_UpdatesDir       = "$deploy_dir\src\Updates"
  $deploy_logs             = "$deploy_dir\logs"
  $deploy_build            = "$deploy_dir\build"
  $deploy_mount            = "$deploy_dir\build\mount"
  $deploy_iso              = "$deploy_dir\ISO"
  $deploy_tmp              = "$deploy_dir\tmp"
  $deploy_WSIMUpdate       = "$deploy_dir\src\WSIM1903Update"

if (!(Test-Path -path $deploy_dir)) {New-Item $deploy_dir -Type Directory}
if (!(Test-Path -path $deploy_src)) {New-Item $deploy_src -Type Directory}
if (!(Test-Path -path $deploy_drivers)) {New-Item $deploy_drivers -Type Directory}
if (!(Test-Path -path $deploy_ADKOffline)) {New-Item $deploy_ADKOffline -Type Directory}
if (!(Test-Path -path $deploy_ADKWinPeOffline)) {New-Item $deploy_ADKWinPeOffline -Type Directory}
# if (!(Test-Path -path $deploy_UpdatesDir)) {New-Item $deploy_UpdatesDir -Type Directory}
# if (!(Test-Path -path $deploy_UpdatesDirLCU)) {New-Item $deploy_UpdatesDirLCU -Type Directory}
# if (!(Test-Path -path $deploy_UpdatesDirSSU)) {New-Item $deploy_UpdatesDirSSU -Type Directory}
if (!(Test-Path -path $deploy_WSIMUpdate)) {New-Item $deploy_WSIMUpdate -Type Directory}
if (!(Test-Path -path $deploy_logs)) {New-Item $deploy_logs -Type Directory}
if (!(Test-Path -path $deploy_build)) {New-Item $deploy_build -Type Directory}
if (!(Test-Path -path $deploy_mount)) {New-Item $deploy_mount -Type Directory}
if (!(Test-Path -path $deploy_tmp)) {New-Item $deploy_tmp -Type Directory}
if (!(Test-Path -path $deploy_iso)) {New-Item $deploy_iso -Type Directory}

# A Windows ADK for Windows 10, version 1909 will not be released. You can use the Windows ADK for Windows 10, version 1903 to deploy Windows 10, version 1909.
# https://docs.microsoft.com/en-us/windows-hardware/get-started/adk-install
# ADK Version 1903 Url and Install Options
# https://docs.microsoft.com/en-us/windows-hardware/get-started/adk-install
  $adk_url             = 'https://download.microsoft.com/download/B/E/6/BE63E3A5-5D1C-43E7-9875-DFA2B301EC70/adk/adksetup.exe'
  $adk_file            = 'adksetup.exe'
  $adk_features        = '*' # https://docs.microsoft.com/en-us/previous-versions/windows/it-pro/windows-8.1-and-8/dn621910(v=win.10)
  $adk_install_log     = "$deploy_logs\adksetup.log"
  $adk_reg_key         = "HKLM:\Software\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\{fb450356-9879-4b2e-8dc9-282709286661}"
  $adk_winPeUrl        = "https://download.microsoft.com/download/E/F/A/EFA17CF0-7140-4E92-AC0A-D89366EBD79E/adkwinpeaddons/adkwinpesetup.exe"
  $adk_winPeFile       = "adkwinpesetup.exe"
  $mdt_reg_key         = "HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall\{2E6CD7B9-9D00-4B04-882F-E6971BC9A763}"
  $mdt_fileURL         = "https://download.microsoft.com/download/3/3/9/339BE62D-B4B8-4956-B58D-73C4685FC492/MicrosoftDeploymentToolkit_x64.msi"
  $mdt_fileName        = "MicrosoftDeploymentToolkit_x64.msi"
  $adk_WinPeInstall_log= "C:\SetupDeployment\logs\ADKWinPEInstallation.log"

  Add-Type -AssemblyName "system.io.compression.filesystem"

Start-Sleep -Seconds 2  

if(-not (Test-Path -Path $adk_reg_key))
{
  # Offline Download ADK
  # $downADK = (New-Object System.Net.WebClient).DownloadFile($adk_url,"$deploy_src\$adk_file")
  # Invoke-WebRequest -uri $adk_url -OutFile $deploy_src\$adk_file -PassThru -Verbose
 Write-Host "`nDownload von $adk_url erfolgt ..." -ForeGroundColor Green
 Start-BitsTransfer -Source $adk_url -Destination "$deploy_src\$adk_file"
 Write-Host "`nInstallation von $deploy_src\$adk_file erfolgt ..." -ForeGroundColor Green
 Start-Process -FilePath "$deploy_src\$adk_file" -ArgumentList "/quiet /layout $deploy_ADKOffline /log `"$adk_install_log`"" -wait -Verbose
    [System.GC]::Collect()  
  
  # Offline Download WinPE-ADK
  # $downADKPE = (New-Object System.Net.WebClient).DownloadFile($adk_winPeUrl,"$deploy_src\$adk_winPeFile")
  # Invoke-WebRequest -UseBasicParsing -uri $adk_winPeUrl -OutFile $deploy_src\$adk_winPeFile -PassThru -Verbose
  Write-Host "`nDownload von $adk_winPeUrl erfolgt ..." -ForeGroundColor Green
  Start-BitsTransfer -Source $adk_winPeUrl -Destination "$deploy_src\$adk_winPeFile"
  Write-Host "`nInstallation von $deploy_src\$adk_winPeFile erfolgt ..." -ForeGroundColor Green
  Start-Process -FilePath "$deploy_src\$adk_winPeFile" -ArgumentList "/quiet /layout $deploy_ADKWinPeOffline /log `"$adk_WinPeInstall_log`"" -wait -Verbose
    [System.GC]::Collect()

  # Install ADK Offline-Repository
  Write-Host "`nInstallation von Windows ADK erfolgt ..." -ForeGroundColor Green
  Start-Process -FilePath "$deploy_ADKOffline\$adk_file" -ArgumentList "/quiet /norestart /log `"$adk_install_log`"" -wait -PassThru -Verbose
  
  # Install WinPE Offline-Repository
  Write-Host "`nInstallation von Windows PE erfolgt ..." -ForeGroundColor Green
  Start-Process -FilePath "$deploy_ADKWinPeOffline\$adk_winPeFile" -ArgumentList "/quiet /norestart /log `"$adk_WinPeInstall_log`"" -wait -PassThru -Verbose

  # Download and Install the Windows System Image Manager (WSIM) 1903 update
  $WSIMUpdateURL = "https://download.microsoft.com/download/4/0/F/40FD29FC-0E6D-46D0-8F7E-B033110D07D5/WSIM1903.zip"
  Write-Host "`nDownload von Windows System Image Manager (WSIM) 1903 update erfolgt ..." -ForeGroundColor Green
  Start-BitsTransfer -Source $WSIMUpdateURL -Destination "$deploy_src\WSIM1903Update\WSIM1903.zip"
  [io.compression.zipfile]::ExtractToDirectory("$deploy_src\WSIM1903Update\WSIM1903.zip", "$deploy_src/WSIM1903Update/") | Out-Null 
  Write-Host "`nInstallation von Windows System Image Manager (WSIM) 1903 update erfolgt ..." -ForeGroundColor Green
  Start-Process -FilePath "cmd.exe" -ArgumentList "/c $deploy_src\WSIM1903Update\UpdateWSIM.bat" -WindowStyle Hidden -WorkingDirectory "$deploy_src/WSIM1903Update/" -PassThru -Wait
}

# Install MDT 8456
if(-not (Test-Path -Path $mdt_reg_key)) {
    Write-Host "`nDownload von $mdt_fileName erfolgt ..." -ForeGroundColor Green
    Start-BitsTransfer -Source $mdt_fileURL -Destination "$deploy_src\$mdt_fileName"
    # Invoke-WebRequest -UseBasicParsing -uri $mdt_fileURL -OutFile $deploy_src\$mdt_fileName -PassThru -Verbose
    Write-Host "`nInstallation von $deploy_src\$mdt_fileName erfolgt ..." -ForeGroundColor Green
    Start-Process -FilePath "$deploy_src\$mdt_fileName" -ArgumentList "/passive /norestart /L*V `"$deploy_logs\mdt_install.log`"" -wait -PassThru -Verbose

    # Download and extract Sysinternals
    $SysinternalsURL = "https://download.sysinternals.com/files/SysinternalsSuite.zip"
    Write-Host "`nDownload von $SysinternalsURL erfolgt ..." -ForeGroundColor Green
    Start-BitsTransfer -Source $SysinternalsURL -Destination "$deploy_src\Sysinternals.zip"
    Write-Host "`nEntpacken von $deploy_src\Sysinternals.zip erfolgt ..." -ForeGroundColor Green
    [io.compression.zipfile]::ExtractToDirectory("$deploy_src\Sysinternals.zip", "$deploy_src/Sysinternals")

}

# Check if SSU OR LCU exists else get it
# if ((Get-ChildItem $deploy_UpdatesDirSSU -Filter *.msu | measure).Count -eq 0 `
#    -OR (Get-ChildItem $deploy_UpdatesDirLCU -Filter *.msu | measure).Count -eq 0) {
# Import DISM modules from Windows 10 ADK
Import-Module $deploy_DismPath -Force 

# Global Variables
& setx /M  "PATH" "$ENV:PATH;$deploy_DismPath"

# Get Latest Updates Windows 10 Version 1903
# https://docs.stealthpuppy.com/docs/latestupdate/usage/get-stack
# Install-PackageProvider -Name NuGet -Force -Confirm:$false
# Install-Module -Name LatestUpdate -Force -Confirm:$false -AllowClobber
# Import-Module -Name LatestUpdate -Force -PassThru

# SSU
# Get-LatestServicingStackUpdate -OperatingSystem Windows10 -Version 1903 | `
# Where-Object { $_.Architecture -eq "x64" -and $_.Note -notmatch "Server"} | `
#    Save-LatestUpdate -Path $deploy_UpdatesDirSSU

# LCU
# Get-LatestCumulativeUpdate -OperatingSystem Windows10 -Version 1903 | `
#    Where-Object { $_.Architecture -eq "x64" -and $_.Note -notmatch "Server"} | `
#        Save-LatestUpdate -Path $deploy_UpdatesDirLCU

# Unblock Files
# Get-ChildItem -Path $deploy_UpdatesDir -Filter *.msu -Recurse -Force | Unblock-File
# }

# Fix NTFS-permission
& icacls $deploy_dir /c /t /q /reset | Out-Null

# Fix MDT DeploymentShare permissions
# https://deploymentresearch.com/fixing-mdt-2013-update-1-deployment-share-permissions-using-powershell/

# ------------------------------------------------------------------------------------------

Video-Sequenz zum Ablauf des ersten Scripts.

Windows-Medien mit aktuellen SSU und LCU erstellen

Das PowerShell-Script Create-W10RefImagev1909.ps1 erstellt nach Durchlauf die aktuell gepatchte Windows-10-Version 1909 inklusive dem Feature .NET-Framework 2.0-3.5. Die Besonderheit ist die Reihenfolge in dem das Referenz-Image erstellt wird. Dies ist ein Abbild einer etablierten Herstellung eines sogenannten "Referenz"-Image wie es von Microsoft empfohlen wird.

1) Kopieren Sie das Script Create-W10RefImagev1909.ps1 innherhalb des Ordners C:\SetupDeployment\

Folgende Änderungen sollten Sie vor dem ersten Aufruf ggf. vornehmen:
$ISO =  [System.IO.Path]::Combine("$RootDir\ISO","<Name der ISO-Datei.iso>")

Kopieren Sie also vor dem Aufrufen des PowerShell-Scripts ihre gewünschte ISO-Datei in den Ordner C:\SetupDeployment\ISO und halten Sie den Namen durch die Änderung der Variable $ISO im Script fest. Ansonsten erscheint das folgende Dialogfenster und bittet Sie um Angabe eines ISO-Abbildes um erfolgreich fortzusetzen zu können.

Ausführen des Scripts unter privilegierten Benutzerrechten:

2) powershell -ex bypass -co <pfad zum Script Create-W10RefImagev1909.ps1>

Beispiel: powershell -ex bypass -co "C:\SetupDeployment\Create-W10RefIMagev1909.ps1"

Glossar zu SSU und LCU

SSU = Servicing Stack-updates/Wartungsstapel: SSUs verbessern die Zuverlässigkeit des Updatevorgangs. Dadurch wird das Risiko potenzieller Probleme beim Installieren des LCU und beim Anwenden von Microsoft-Sicherheitsfixes reduziert. Beim Warten auf Stack-Updates werden Korrekturen für den Wartungsstapel, die Komponente, die Windows-Updates installiert, bereitgestellt. Darüber hinaus enthält Sie den "komponentenbasierten Servicing Stack" (CBS), der eine wichtige zugrunde liegende Komponente für verschiedene Elemente der Windows-Bereitstellung ist, wie DISM, SFC, Ändern von Windows-Features oder-Rollen sowie Reparieren von Komponenten. Das CBS ist eine kleine Komponente, in der in der Regel keine Updates monatlich veröffentlicht werden.

LCU = Latest Cumulative Update: A tested, cumulative set of updates. They include both security and reliability updates that are packaged together and distributed over Windows Update, WSUS, System Center Configuration Manager and Microsoft Update Catalog for easy deployment. The Monthly Rollup is product specific, addresses both new security issues and nonsecurity issues in a single update and will proactively include updates that were released in the past. Security vulnerabilities are rated by their severity. The severity rating is indicated in the Microsoft security bulletin as critical, important, moderate, or low. This Monthly Rollup would be displayed under the title Security Monthly Quality Rollup when you download or install. This Monthly Rollup will be classified as an "Important" update on Windows Update and will automatically download and install if your Windows Update settings are configured to automatically download and install Important updates.

Why does a Windows 10 Cumulative Update Install Twice?

Wenn ein kumulatives Update (LCU) zweimal installiert wird, können Sie die Hintergründe hier in diesem Artikel recherchieren.

Empfohlene Reihenfolge beim Offline-Service-Management

Das Script unterstützt die folgende Hersteller-Empfehlung in den folgenden Punkten:

Source Code Create-W10RefImagev1909.ps1

Create-W10RefImagev1909.ps1
<#
.SYNOPSIS
  Dieses Script erstellt eine aktuell gepatchte ISO-Installations- und WIM-Datei von Windows 10 1909.
  Führe das Script unter privilegierten Benutzerrechten (Administrator) aus um einen einwandfreien
  Betrieb der Funktionen zu gewährleisten! 
.DESCRIPTION
  Für die schnelle Bereitstellung von Windows in Campus-Umgebungen sind aktuell gepatchte 
  ISO- und WIM-Dateien eine wesentliche Unterstützung. Die Weiterverarbeitung von WIM-Dateien sind
  in MDT oder SCM-Tools von Microsoft direkt zu importieren. 
  Die ISO-Datei kann mit Tools wie Rufus (https://rufus.ie/) auf einen USB-Stick übertragen werden.
.PARAMETER <Parameter_Name>
    No parameter input required.
.INPUTS
  None
.OUTPUTS
  WIM-file stored in C:\SetupDeployment\build\REFW10-001.wim
  ISO-file stored in C:\SetupDeployment\build\<W10_<DATE>.ISO
.NOTES
  Version:        2.0
  Author:         Alexander Scharmer
  Creation Date:  29.12.2019
  Purpose/Change: 
  Note #1       : To service a newer version of WinPE than the OS you are servicing from, for example service Windows 10 v1709 
                  from a Windows Server 2019 server, you need a newer DISM version.
                  Solution, simply install the latest Windows ADK 10, and use DISM from that version
  Note #2       : If your Windows OS already have a newer version of dism, uncomment the below line, and comment out line 11 and 12
                  $DISMFile = 'dism.exe'

.EXAMPLE
  PS> . \Create-W10RefImagev1909.ps1 
#>
Clear-Host
Set-ExecutionPolicy Bypass -Scope Process -Force
## Release handles on Registry Hive and free up memory
[System.GC]::Collect()

# Check for elevation
If (-NOT ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole(`
    [Security.Principal.WindowsBuiltInRole] "Administrator"))
{
    Write-Warning "Oupps, you need to run this script from an elevated PowerShell prompt!`nPlease start the PowerShell prompt as an Administrator and re-run the script."
	Write-Warning "Aborting script..."
    Break
    Pause
}

write-host "`nOffline-Patching Window 10 Image v2.0 - by Alexander Scharmer.`n" -ForegroundColor Yellow -BackgroundColor Black

$DISMFile = 'C:\Program Files (x86)\Windows Kits\10\Assessment and Deployment Kit\Deployment Tools\amd64\DISM\dism.exe'
If (!(Test-Path $DISMFile))
    {
     $scriptName = $MyInvocation.MyCommand.Name
     Write-Warning "DISM.exe bzw. Windows ADK nicht gefunden! Abbruch des Scripts $scriptName erfolgt!"
     [console]::beep(1000,500)
     Write-Host "`nINFO: Führen Sie das Script 'Install-SetupDeployment.ps1' vorher aus um alle Tools zu installieren und um die notwendige Ordnerstruktur zu erstellen." -ForegroundColor White -BackgroundColor Black
     Break 
    }
    else { Write-Host "`nDISM.exe in Windows ADK gefunden! Das Script wird fortgesetzt ..." -ForegroundColor Green }

$RootDir   = [System.IO.Path]::GetDirectoryName($MyInvocation.MyCommand.Path)
$ParentDir = [System.IO.Path]::GetDirectoryName($RootDir)

Set-StrictMode -Version Latest
function fGetUpdates {
     
        [CmdletBinding()]
        Param(
        [parameter(Position=0,Mandatory=$true)]
        [String]$arch = "x64",
        [parameter(Position=1,Mandatory=$true)]
        [ValidateSet("SSU","LCU")]
        [String]$SLCU,
        [parameter(Position=2,Mandatory=$true)]
        [ValidateSet("1909","1903","1809","1803")]
        [String]$BuildNummer
        )
        
        $ErrorActionPreference = "Stop"

        try{
            <#
                In diesem Block der Code, der eine Exception auslösen könnte
            #>
            if (!(Get-Module "OSDSUS")) 
                { 
                  write-host "Modul OSDSUS nicht geladen ... versuche es nachzuladen ..." -ForegroundColor DarkGreen
                  Import-Module OSDSUS -Force

                }

            if (!(Get-Module "OSDUpdate")) 
                { 
                  write-host "Modul OSDUpdate nicht geladen ... versuche es nachzuladen ..." -ForegroundColor DarkGreen
                  Import-Module OSDUpdate -Force

                }

            # $error[0].Exception.GetType().FullName
        }

        catch [System.IO.FileNotFoundException] 
            {
                <# Modul OSDSUS oder OSDUpdate wurde nicht gefunden weil nicht installiert #>
                write-host "Modul OSDSUS bzw. OSDUpdate nicht installiert  ... versuche es aus dem Internet zu laden und zu installieren  ..." -ForegroundColor DarkGreen
                Install-PackageProvider -Name NuGet -Force -Scope AllUsers -ForceBootstrap
                Install-Module OSDSUS -Scope AllUsers -Force -AllowClobber -SkipPublisherCheck -Confirm:$false
                Update-OSDSUS
                Import-Module OSDSUS -Force
                Install-Module OSDUpdate -Scope AllUsers -Force -AllowClobber -SkipPublisherCheck -Confirm:$false
                Import-Module OSDUpdate -Force
            }

        catch {
            <#
	        Hier findet die Fehlerbehandlung statt, z.B. das Schreiben eines Logs
	        Der letzte aufgezeichnete Fehler ist hier über die Variable $_ abrufbar,
	        einzelne Eigenschaften daher nach diesem Muster: $_.Exception.Message
            #>
            "`n==> Exception Handling durch nicht-terminierenden Fehler"
              }

        finally{
            <#
                Jede Anweisung in diesem Block wird immer ausgeführt, egal ob ein
	        Fehler aufgetreten ist oder nicht. Dieser Block ist optional.
            #>
            write-host "`n `nFinally-Block-OSD-Module: Erfolgreich Verarbeitung." -ForegroundColor Green
            if (Get-Module "OSDSUS") { Write-Host "Modul OSDSUS erfolgreich geladen." -ForegroundColor Yellow}
            if (Get-Module "OSDUpdate") { Write-Host "Modul OSDUpdate erfolgreich geladen." -ForegroundColor Yellow}
       
            # $error[0].Exception.GetType().FullName
            $error.Clear()
        }
        
        # Get Update-Information
        $updateInfo = Get-OSDUpdate | ? {$_.UpdateOS -eq 'Windows 10' -and $_.UpdateBuild -eq $BuildNummer -and $_.UpdateGroup -eq $SLCU -and $_.UpdateArch -eq $arch -and $_.IsLatestRevision -eq 'True'} 
        # Combine Path-Structure
        $path = [IO.Path]::Combine("$env:SystemDrive\SetupDeployment\Updates", $updateInfo."UpdateOS", $updateInfo."UpdateBuild", $updateInfo."Updategroup")
        # Create Folder-Structure for Updates
        New-Item $path -ItemType Directory -Force | Out-Null
        # Download or match Updates
        $updateInfo | Get-DownOSDUpdate -DownloadPath $path 
        sl $path | Out-Null
        set-content -path .\LatestFile.txt -value ([IO.Path]::Combine($path, $updateInfo."Title", $updateInfo."FileName")) -Force
}

fGetUpdates -arch x64 -SLCU SSU -BuildNummer 1909
fGetUpdates -arch x64 -SLCU LCU -BuildNummer 1909

#Auxiliary
$ISO =  [System.IO.Path]::Combine("$RootDir\ISO","SW_DVD9_Win_Pro_10_1909_64BIT_German_Pro_Ent_EDU_N_MLF_X22-17400.ISO")
$SSU   =  Get-Content -Path ([System.IO.Path]::Combine("$env:SystemDrive\SetupDeployment\Updates","Windows 10","1909","SSU","LatestFile.txt"))
$LCU   =  Get-Content -Path ([System.IO.Path]::Combine("$env:SystemDrive\SetupDeployment\Updates","Windows 10","1909","LCU","LatestFile.txt"))
$MountFolder    = [System.IO.Path]::Combine($RootDir,'build\Mount')
$RefImageFolder = [System.IO.Path]::Combine($RootDir,'build')
$TmpImage       = [System.IO.Path]::Combine("$RootDir\tmp",'tmp_install.wim')
$RefImage       = [System.IO.Path]::Combine($RefImageFolder,'REFW10-001.wim')

# Verify that the ISO and CU files existnote
if (!(Test-Path -path $ISO)) 
    {
    Write-Warning "`nINFO: Could not find Windows 10 ISO $ISO. Wähle eine gültige ISO-Datei im Dialogfeld aus ..."
    Write-Host    "INFO: Im Regelfall sucht das Script eine Windows-10-ISO-Datei im Pfad $ISO" -ForegroundColor Green
    Add-Type -AssemblyName System.Windows.Forms # https://docs.microsoft.com/en-us/dotnet/api/system.windows.forms.openfiledialog?redirectedfrom=MSDN&view=netframework-4.8
    $FileBrowser = New-Object System.Windows.Forms.OpenFileDialog -Property @{
        Title = "Wähle eine gültige Windows-10-ISO-Datei aus ..."
        InitialDirectory = [System.IO.Directory]::GetCurrentDirectory();
        Multiselect = $false
        Filter = "Image-Dateien (*.iso)|*.iso";
        CheckPathExists = 'True';
    } 
    [void]$FileBrowser.ShowDialog()
    $ISO = $FileBrowser.FileName
    }
if (!(Test-Path -path $SSU)) 
    {
        Write-Warning "Could not find servicing stack Update for Windows 10. Try to download ..."
        fGetUpdates -arch x64 -SLCU SSU -BuildNummer 1909
        
    }
if (!(Test-Path -path $LCU)) 
    {
        Write-Warning "Could not find Cumulative Update for Windows 10. Try to download ..."
        fGetUpdates -arch x64 -SLCU LCU -BuildNummer 1909
    }

# Mount the Windows 10 ISO
Write-Host "`nTry Mounting ISO $ISO ..." -ForegroundColor Green
Mount-DiskImage -ImagePath $ISO | Out-Null

$ISOImage = Get-DiskImage -ImagePath $ISO | Get-Volume
$ISODrive = [string]$ISOImage.DriveLetter+":"
$ImageName = (Get-WindowsImage -ImagePath "$ISODrive\Sources\install.wim" | ? { $_.imageindex -eq '1' }).ImageName.toString()

# Export the Windows 10 Standard index 1 to a new WIM
Write-Host "`nExporting image $ImageName from $ISODrive\Sources\install.wim ..." -ForegroundColor Green
Export-WindowsImage -SourceImagePath "$ISODrive\Sources\install.wim" -SourceIndex 1 -DestinationImagePath $TmpImage

# Mount the image
Write-Host "`nMounting WIM-File $TmpImage ..." -ForegroundColor Green
Mount-WindowsImage -ImagePath $TmpImage -Index 1 -Path $MountFolder

# Add the latest SSU to the Windows 10 Standard image
Write-Host "`nInstalling SSU $SSU ..." -ForegroundColor Green
Add-WindowsPackage -PackagePath $SSU -Path $MountFolder

# Add the latest CU (LCU) to the Windows 10 Standard image
Write-Host "Installing LCU (bitte um Geduld, dauert etwas) $LCU ..." -ForegroundColor Green
Add-WindowsPackage -PackagePath $LCU -Path $MountFolder

# Cleanup the image BEFORE installing .NET to prevent errors
# Using the /ResetBase switch with the /StartComponentCleanup parameter of DISM.exe on a running version of Windows removes all superseded versions of every component in the component store.
# https://docs.microsoft.com/en-us/windows-hardware/manufacture/desktop/clean-up-the-winsxs-folder#span-iddismexespanspan-iddismexespandismexe
Write-Host "`nCleaning up Image $MountFolder BEFORE installing .NET to prevent errors ..." -ForegroundColor Green

& $DISMFile /Image:$MountFolder /Cleanup-Image /StartComponentCleanup /ResetBase

# Add .NET Framework 3.5.1 to the Windows 10 Standard image
Write-Host "`nInstalling .Net Framework 3.5. to Offline-Image $MountFolder ..." -ForegroundColor Green
Add-WindowsCapability -Name NetFx3~~~~ –Source $ISODrive\sources\sxs\ -Path $MountFolder | Out-Null

# Re-apply latest CU (LCU) because of .NET changes
Write-Host "`nErneutes Applizieren von LCU - aufgrund von .NET 2.0-3.5-Package-Installation - erforderlich ..." -ForegroundColor Green
Add-WindowsPackage -PackagePath $LCU -Path $MountFolder

# Dismount the Windows 10 Standard image
Write-Host "`nDismounting WIM $MountFolder ..." -ForegroundColor Green
DisMount-WindowsImage -Path $MountFolder -Save

# Export the Windows 10 index to a new WIM (the export operation reduces the WIM size with about 100 MB or so)
# Optimize final image - https://docs.microsoft.com/en-us/windows-hardware/manufacture/desktop/oem-deployment-of-windows-10-for-desktop-editions
# Remove unused packages from your image, by exporting a copy of it.
Write-Host "`nExporting the WIM-File $TmpImage to $RefImage ..." -ForegroundColor Green
Export-WindowsImage -SourceImagePath $TmpImage -SourceIndex "1" -DestinationImagePath $RefImage

# Remove the uneeded temporary WIM-File.
Write-Host "`nDelete temporary WIM $TmpImage ..." -ForegroundColor Green
if (Test-Path -path $TmpImage) {Remove-Item -Path $TmpImage -Force}

# Copy REF-IMG to Media for ISO-Build
Write-Host "`nCopy REF-IMG to Media for ISO-Build ..." -ForegroundColor Green
if (!(Test-Path -path "$RefImageFolder\media")) {New-Item -path "$RefImageFolder\media" -ItemType Directory}
Copy-Item -Path "$ISODrive\*" -Destination "$RefImageFolder\media" -Force -Recurse
Copy-Item -Path $RefImage -Destination "$RefImageFolder\Media\sources\install.wim" -Force -Filter "*.wim" -Confirm:$false

# Create Windows ISO
$ISOFile = "$RefImageFolder" + "\" + ([string]"W10_$(get-date -Format d)").Replace(".","_") + ".ISO"
write-Host "`nKonstruiere Dateiname für ISO-Datei $ISOFile" -ForegroundColor Green
$ToolsPath = "C:\Program Files (x86)\Windows Kits\10\Assessment and Deployment Kit\Deployment Tools\amd64\Oscdimg"
$BootData='2#p0,e,b"{0}"#pEF,e,b"{1}"' -f "$ToolsPath\etfsboot.com","$ToolsPath\efisys_noprompt.bin"
$Proc = Start-Process -FilePath "$ToolsPath\oscdimg.exe" -ArgumentList @("-bootdata:$BootData",'-u2','-udfver102',"$RefImageFolder\media","$ISOFile") -PassThru -Wait 
if($Proc.ExitCode -ne 0)
{
    Throw "Failed to generate ISO with exitcode $($Proc.ExitCode)"
}
else {
    Write-Host "`nMedia $ISOFile creation has been successfully done!" -ForegroundColor Yellow -BackgroundColor Black
}

# Dismount the Windows 10 ISO
Write-Host "Dismounting ISO $ISO und finalisiere Script ..." -ForegroundColor Green
Dismount-DiskImage -ImagePath $ISO | Out-Null
Write-Host "Ende. Have a nice Day!"
# END OF SCRIPT

Video-Sequenz zum Ablauf des ersten Scripts.

Ausgabe der Medienformate .ISO und .WIM

Als Ergebnis werden die aktuell gepatchten Windows-10-1909-Builds als Abbilder in den Formaten .WIM und .ISO im Ordner C:\SetupDeployment\build ausgegeben.

Manuelles Erstellen eines startfähigen ISO-Abbildes von einem Ordner

Im Zusammenhang von Windows-Deployments werden auch Anforderungen gestellt, wo ein ISO-Abbild in einen Ordner kopiert wird, der Inhalt des Ordners aktualisiert wird (z. B. die Datei install.wim, oder das Einfügen einer Autounattend.xml-Datei oder auch Apps etc.) und anschließend wieder in das ISO-Format zurückgebracht werden soll. Microsoft liefert mit dem Windows ADK ein Kommandozeilen-basiertes Tool namens oscdimg.exe aus. Die 64-Bit-Variante von des Tools wird im Regelfall in den Standardordner C:\Program Files (x86)\Windows Kits\10\Assessment and Deployment Kit\Deployment Tools\amd64\Oscdimg\oscdimg.exe abgelegt. Dies kann unter Angabe verschiedener Parameter erneut ein ISO-Abbild generieren. Folgendes Beispiel soll dies skizzieren:

oscdimg.exe -u2 -udfver102 -bootdata:2#p0,e,bD:\Iso_Files\boot\etfsboot.com#pEF,e,bD:\Iso_Files\efi\microsoft\boot\efisys.bin D:\ISO_Files D:\W10-1903FAT.iso

Im Code-Block oben wird der Ordner D:\ISO_Files als bootfähiges (mithilfe der Dateien etfsboot.com und efisys.bin die in Windows ADK enthalten sind) Abbild im ISO-Format im Pfad D:\W10-1903FAT.iso erzeugt.

Einen startfähigen USB-Stick mit dem W10-ISO-Abbild erstellen

Das ISO-Abbild W10_<Erstell-Datum>.ISO kann mit einer vielzahl an Software-Tools auf einen USB-Stick übertragen werden. Damit kann ein Endgerät mithilfe des USB-Sticks gestartet (bootfähig) und Windows 10 mit aktuellen Patch-Level installiert werden. Der Author verwendet hier das Tool Rufus von Pete Batard.

.WIM-Abbild in Microsoft-Deployment-Toolkit (MDT) weiterverarbeiten

Abbilder im .WIM-Format können in MDT hervorragend dazu verwendet werden, um laufend aktualisierte Betriebssystem-Abbilder am Campus bereitzustellen. Hierbei können sowohl Microsoft Client- sowie Server-Betriebssysteme bereitgestellt werden.

Alternative Tools zur scriptbasierten Variante

NTLite ist ein Computerprogramm, mit dessen Hilfe alternative Installationsmedien von Windows 7, 8, 8.1 und 10 erstellt werden können. Mit NTLite können Addons, Treiber, Sprachpakete, Silent Installer und Updates in ein Installationsmedium integriert werden.

Last updated