Now that we’ve started down this “MDT” train, there’s no getting off until we discuss some of the more important parts. If you haven’t read the previous posts you can find them here:
Today we will be focusing on automating the procedures for always having an updated Windows OS ISO. This is especially important for automating master image builds b/c we can’t remove windows updates from the task sequence. Windows Updates can add hours into your build, so if we can decouple this from the process we would be saving considerable amounts of time.
For those not familiar with OSDBuilder it’s an awesome Powershell compilation created by David Segura that allows you to service your Windows Operating systems in an ‘offline’ fashion. Say you have a Server 2016 ISO that’s 4 years old. With OSDBuilder, you can ‘Import’ that ISO and apply all the necessary cumulative updates, in an automated offline fashion. So the next time you install Server 2016, it’s completely updated.
For me, this process looks something like this:
- ‘First Run’ Powershell script (only ran once)
- Patch Tuesday arrives
- Run ‘Install/Update OSDbuilder’ Powershell script (scheduled task)
- Run ‘Ongoing Updates’ Powershell script (scheduled task)
- Run MDT Task Sequence
- Rinse and Repeat (Steps 2-6)
As you can see you pretty much have a fully automated cycle that spits out a new #Citrix / #VMware Master Image with a fully updated OS.
Note: OSDbuilder follows WSUS release schedule, not Windows Updates. Read more here https://www.osdeploy.com/blog/microsoft-update-releases.
I’ll break this blog into 3 sections: Install/Update Module, First Run, Ongoing Updates. A big thank you to Julien for setting up the base code.
Install/Update Module
This section will install/update the necessary powershell modules. If the Module doesn’t exist at all, it will install it. Also, if the module finds an update, it will install the latest. It’s broken into two module sections, OSDBuilder and OSDSUS. OSDBuilder is the main module and OSDSUS is the module responsible for tracking the windows updates.
Latest release notes:
OSDSUS – https://osdsus.osdeploy.com/release
OSDBuilder – https://osdbuilder.osdeploy.com/release
#==========================================================================
#
# Automating Reference Image with OSDBuilder
#
# AUTHOR: Julian Mooren (https://citrixguyblog.com)
# DATE : 03.18.2019
#
# PowerShell Template by Dennis Span (http://dennisspan.com)
#
#==========================================================================
# define Error handling
# note: do not change these values
$global:ErrorActionPreference = "Stop"
if($verbose){ $global:VerbosePreference = "Continue" }
# FUNCTION DS_WriteLog
#==========================================================================
Function DS_WriteLog {
<#
.SYNOPSIS
Write text to this script's log file
.DESCRIPTION
Write text to this script's log file
.PARAMETER InformationType
This parameter contains the information type prefix. Possible prefixes and information types are:
I = Information
S = Success
W = Warning
E = Error
- = No status
.PARAMETER Text
This parameter contains the text (the line) you want to write to the log file. If text in the parameter is omitted, an empty line is written.
.PARAMETER LogFile
This parameter contains the full path, the file name and file extension to the log file (e.g. C:\Logs\MyApps\MylogFile.log)
.EXAMPLE
DS_WriteLog -$InformationType "I" -Text "Copy files to C:\Temp" -LogFile "C:\Logs\MylogFile.log"
Writes a line containing information to the log file
.Example
DS_WriteLog -$InformationType "E" -Text "An error occurred trying to copy files to C:\Temp (error: $($Error[0]))" -LogFile "C:\Logs\MylogFile.log"
Writes a line containing error information to the log file
.Example
DS_WriteLog -$InformationType "-" -Text "" -LogFile "C:\Logs\MylogFile.log"
Writes an empty line to the log file
#>
[CmdletBinding()]
Param(
[Parameter(Mandatory=$true, Position = 0)][ValidateSet("I","S","W","E","-",IgnoreCase = $True)][String]$InformationType,
[Parameter(Mandatory=$true, Position = 1)][AllowEmptyString()][String]$Text,
[Parameter(Mandatory=$true, Position = 2)][AllowEmptyString()][String]$LogFile
)
begin {
}
process {
$DateTime = (Get-Date -format dd-MM-yyyy) + " " + (Get-Date -format HH:mm:ss)
if ( $Text -eq "" ) {
Add-Content $LogFile -value ("") # Write an empty line
} Else {
Add-Content $LogFile -value ($DateTime + " " + $InformationType.ToUpper() + " - " + $Text)
}
}
end {
}
}
#==========================================================================
################
# Main section #
################
# Custom variables
$BaseLogDir = "E:\Logs"
$PackageName = "OSDBuilder"
# Global variables
$date = Get-Date -Format yyy-MM-dd-HHmm
$StartDir = $PSScriptRoot # the directory path of the script currently being executed
$LogDir = (Join-Path $BaseLogDir $PackageName).Replace(" ","_")
$LogFileName = "$PackageName-$date.log"
$LogFile = Join-path $LogDir $LogFileName
# Create the log directory if it does not exist
if (!(Test-Path $LogDir)) { New-Item -Path $LogDir -ItemType directory | Out-Null }
# Create new log file (overwrite existing one)
New-Item $LogFile -ItemType "file" -force | Out-Null
# ---------------------------------------------------------------------------------------------------------------------------
DS_WriteLog "I" "START SCRIPT - $Installationtype $PackageName" $LogFile
DS_WriteLog "-" "" $LogFile
#################################################
# Update OSDBuilder PoweShell Module #
#################################################
DS_WriteLog "I" "Looking for installed OSDBuilder Module..." $LogFile
try {
$Version = Get-ChildItem -Path "C:\Program Files\WindowsPowerShell\Modules\OSDBuilder" | Sort-Object LastAccessTime -Descending | Select-Object -First 1
DS_WriteLog "S" "OSDBuilder Module is installed - Version: $Version" $LogFile
} catch {
DS_WriteLog "E" "An error occurred while looking for the OSDBuilder PowerShell Module (error: $($error[0]))" $LogFile
Exit 1
}
DS_WriteLog "I" "Checking for newer OSDBuilder Module in the PowerShell Gallery..." $LogFile
try {
$NewBuild = Find-Module -Name OSDBuilder
DS_WriteLog "S" "The newest OSDBuilder Module is Version: $($NewBuild.Version)" $LogFile
} catch {
DS_WriteLog "E" "An error occurred while looking for the OSDBuilder PowerShell Module (error: $($error[0]))" $LogFile
Exit 1
}
if($Version.Name -lt $NewBuild.Version)
{
try {
DS_WriteLog "I" "Update is available. Update in progress...." $LogFile
OSDBuilder -Update
DS_WriteLog "S" "OSDBuilder Update completed succesfully to Version: $($NewBuild.Version)" $LogFile
} catch {
DS_WriteLog "E" "An error occurred while updating the OSDBuilder Module (error: $($error[0]))" $LogFile
Exit 1
}
}
else {DS_WriteLog "I" "Newest OSDBuilder is already installed." $LogFile}
DS_WriteLog "I" "Trying to Import the OSDBuilder Module..." $LogFile
try {
Import-Module -Name OSDBuilder -Force
DS_WriteLog "S" "Module got imported" $LogFile
} catch {
DS_WriteLog "E" "An error occurred while importing the OSDBuilder Module (error: $($error[0]))" $LogFile
Exit 1
}
DS_WriteLog "-" "" $LogFile
#################################################
# Update OSDSUS PoweShell Module #
#################################################
DS_WriteLog "I" "Looking for installed OSDSUS Module..." $LogFile
try {
$Version = Get-ChildItem -Path "C:\Program Files\WindowsPowerShell\Modules\OSDSUS" | Sort-Object LastAccessTime -Descending | Select-Object -First 1
DS_WriteLog "S" "OSDSUS Module is installed - Version: $Version" $LogFile
} catch {
DS_WriteLog "E" "An error occurred while looking for the OSDSUS PowerShell Module (error: $($error[0]))" $LogFile
Exit 1
}
DS_WriteLog "I" "Checking for newer OSDSUS Module in the PowerShell Gallery..." $LogFile
try {
$NewBuild = Find-Module -Name OSDSUS
DS_WriteLog "S" "The newest OSDSUS Module is Version: $($NewBuild.Version)" $LogFile
} catch {
DS_WriteLog "E" "An error occurred while looking for the OSDSUS PowerShell Module (error: $($error[0]))" $LogFile
Exit 1
}
if($Version.Name -lt $NewBuild.Version)
{
try {
DS_WriteLog "I" "Update is available. Update in progress...." $LogFile
Update-OSDSUS
DS_WriteLog "S" "OSDSUS Update completed succesfully to Version: $($NewBuild.Version)" $LogFile
} catch {
DS_WriteLog "E" "An error occurred while updating the OSDSUS Module (error: $($error[0]))" $LogFile
Exit 1
}
}
else {DS_WriteLog "I" "Newest OSDSUS is already installed." $LogFile}
DS_WriteLog "I" "Trying to Import the OSDSUS Module..." $LogFile
try {
Import-Module -Name OSDSUS -Force
DS_WriteLog "S" "Module got imported" $LogFile
} catch {
DS_WriteLog "E" "An error occurred while importing the OSDSUS Module (error: $($error[0]))" $LogFile
Exit 1
}
DS_WriteLog "-" "" $LogFile
# ---------------------------------------------------------------------------------------------------------------------------
DS_WriteLog "-" "" $LogFile
DS_WriteLog "I" "End of script" $LogFile
Get-Content $LogFile -Verbose
First Run
This section will talk about how to first get started by importing your OS Media into OSDBuilder. This section really only needs to be run once.
Here is what happens:
- Install/Update OSDbuilder
- Import Server 2016 ISO into OSDBuilder
- lives in the ‘OSImport’ directory
- Update OS with latest updates
- lives in the ‘OSMedia’ directory
The next time you run OSDBuilder, you don’t have to import the Media. You can simply run the commands in the ‘On-going Updates‘ section below. OSDBuilder will look in inside the ‘OSMedia’ folder, look for any updates from the last time it was run and apply the delta.
This code focuses on Server 2016. However, same code would apply for different Operating Systems.
# https://citrixguyblog.com/2019/03/19/osdbuilder-reference-image-on-steroids/
Import-Module -Name OSDBuilder
# Needs to be set for the rest of this to work
Get-OSBuilder -SetPath "E:\OSDBuilder"
# Create mount point for Windows Server 2016 iso
& subst F: \\Fileshare-p1\Files$\Citrix\Microsoft\2016-ISO
# Select index 2 (Standard w/Desktop Experience), go right to the updates, skip the ogv menu
Import-OSMedia -Verbose -ImageIndex 2 -SkipGrid
# Now we update the media
# the -SkipComponentCleanup argument is critical for us - without it, the PVS Target Device driver will break if installed during a task sequence
Get-OSMedia | Sort ModifiedTime -Descending | Select -First 1 | Update-OSMedia -Download -Execute -SkipComponentCleanup
# With the updates complete, we create a build task
New-OSBuildTask -TaskName 'Task' -CustomName 'Test' -EnableNetFx3 -EnableFeature
# Cleanup mount point for 2016 iso
& subst F: /D
Note: You’ll want to change your paths to match your needs. The “Get-OSBuilder -SetPath “E:\OSDBuilder” is where you want OSDBuilder to create it’s directory/file/folder structure. In my case I put it on another drive. You’ll also want o change the path for your ISO location. And also take note of the -SkipComponentCleanup. You’ll need this if you are using Citrix Provisioning Services.
On-going Updates
The ‘On-going Updates’ section Gets/Applies Windows Updates to the lasted updated ‘OSMedia’ directory. Meaning that you’ll only be applying delta updates to the previous one you ran. If you want to apply updates from scratch, that’s configurable as well.
#==========================================================================
#
# Automating Reference Image with OSDBuilder
#
# AUTHOR: Julian Mooren (https://citrixguyblog.com)
# DATE : 03.18.2019
#
# PowerShell Template by Dennis Span (http://dennisspan.com)
#
#==========================================================================
# define Error handling
# note: do not change these values
$global:ErrorActionPreference = "Stop"
if($verbose){ $global:VerbosePreference = "Continue" }
# FUNCTION DS_WriteLog
#==========================================================================
Function DS_WriteLog {
<#
.SYNOPSIS
Write text to this script's log file
.DESCRIPTION
Write text to this script's log file
.PARAMETER InformationType
This parameter contains the information type prefix. Possible prefixes and information types are:
I = Information
S = Success
W = Warning
E = Error
- = No status
.PARAMETER Text
This parameter contains the text (the line) you want to write to the log file. If text in the parameter is omitted, an empty line is written.
.PARAMETER LogFile
This parameter contains the full path, the file name and file extension to the log file (e.g. C:\Logs\MyApps\MylogFile.log)
.EXAMPLE
DS_WriteLog -$InformationType "I" -Text "Copy files to C:\Temp" -LogFile "C:\Logs\MylogFile.log"
Writes a line containing information to the log file
.Example
DS_WriteLog -$InformationType "E" -Text "An error occurred trying to copy files to C:\Temp (error: $($Error[0]))" -LogFile "C:\Logs\MylogFile.log"
Writes a line containing error information to the log file
.Example
DS_WriteLog -$InformationType "-" -Text "" -LogFile "C:\Logs\MylogFile.log"
Writes an empty line to the log file
#>
[CmdletBinding()]
Param(
[Parameter(Mandatory=$true, Position = 0)][ValidateSet("I","S","W","E","-",IgnoreCase = $True)][String]$InformationType,
[Parameter(Mandatory=$true, Position = 1)][AllowEmptyString()][String]$Text,
[Parameter(Mandatory=$true, Position = 2)][AllowEmptyString()][String]$LogFile
)
begin {
}
process {
$DateTime = (Get-Date -format dd-MM-yyyy) + " " + (Get-Date -format HH:mm:ss)
if ( $Text -eq "" ) {
Add-Content $LogFile -value ("") # Write an empty line
} Else {
Add-Content $LogFile -value ($DateTime + " " + $InformationType.ToUpper() + " - " + $Text)
}
}
end {
}
}
#==========================================================================
################
# Main section #
################
# Custom variables
$BaseLogDir = "E:\Logs"
$PackageName = "OSDBuilder"
# OSDBuilder variables
$OSDBuilderDir = "E:\OSDBuilder"
$TaskName = "CitrixVDA2" # Do not use the real name of the task file - Example: "OSBuild Build-031819.json" --> Build-031819
#MDT variables
$MDTShare = "E:\DeploymentShare"
# Global variables
$date = Get-Date -Format yyy-MM-dd-HHmm
$StartDir = $PSScriptRoot # the directory path of the script currently being executed
$LogDir = (Join-Path $BaseLogDir $PackageName).Replace(" ","_")
$LogFileName = "$PackageName-$date.log"
$LogFile = Join-path $LogDir $LogFileName
# Create the log directory if it does not exist
if (!(Test-Path $LogDir)) { New-Item -Path $LogDir -ItemType directory | Out-Null }
# Create new log file (overwrite existing one)
New-Item $LogFile -ItemType "file" -force | Out-Null
# ---------------------------------------------------------------------------------------------------------------------------
DS_WriteLog "I" "START SCRIPT - $Installationtype $PackageName" $LogFile
DS_WriteLog "-" "" $LogFile
#################################################
# Update of the OS-Media #
#################################################
DS_WriteLog "I" "Starting Update of OS-Media" $LogFile
try {
$StartDTM = (Get-Date)
Get-OSBuilder -SetPath $OSDBuilderDir
DS_WriteLog "S" "Set OSDBuider Path to $OSDBuilderDir" $LogFile
$OSMediaSource = Get-ChildItem -Path "$OSDBuilderDir\OSMedia" | Sort-Object LastAccessTime -Descending | Select-Object -First 1
Update-OSMedia -Name $($OSMediaSource.Name) -Download -Execute -SkipComponentCleanup
$EndDTM = (Get-Date)
DS_WriteLog "S" "Update-OSMedia completed succesfully" $LogFile
DS_WriteLog "I" "Elapsed Time: $(($EndDTM-$StartDTM).TotalMinutes) Minutes" $LogFile
} catch {
DS_WriteLog "E" "An error occurred while updating the OS-Media (error: $($error[0]))" $LogFile
Exit 1
}
#################################################
# Creation of the New-OSBuild #
#################################################
DS_WriteLog "I" "Creating New-OSBuild" $LogFile
try {
$StartDTM = (Get-Date)
New-OSBuild -ByTaskName $TaskName -Execute -SkipComponentCleanup
$EndDTM = (Get-Date)
DS_WriteLog "S" "OS-Media Creation for Task $TaskName completed succesfully" $LogFile
DS_WriteLog "I" "Elapsed Time: $(($EndDTM-$StartDTM).TotalMinutes) Minutes" $LogFile
} catch {
DS_WriteLog "E" "An error occurred while creating the OS-Media (error: $($error[0]))" $LogFile
Exit 1
}
#################################################
# Import the OS-Media to the MDT-Share #
#################################################
DS_WriteLog "I" "Searching for OS-Build Source Directory" $LogFile
try {
$OSBuildSource = Get-ChildItem -Path "$OSDBuilderDir\OSBuilds" | Sort-Object LastAccessTime -Descending | Select-Object -First 1
DS_WriteLog "S" "Found the latest OS-Build directory - $($OSBuildSource.FullName) " $LogFile
} catch {
DS_WriteLog "E" "An error occurred while searching the latest OS-Build directory (error: $($error[0]))" $LogFile
Exit 1
}
DS_WriteLog "I" "Importing Microsoft Deployment Toolkit PowerShell Module" $LogFile
try {
Import-Module "C:\Program Files\Microsoft Deployment Toolkit\Bin\MicrosoftDeploymentToolkit.psd1"
DS_WriteLog "S" "MDT PS Module got imported successfully" $LogFile
} catch {
DS_WriteLog "E" "An error occurred while importing the MDT PowerShell Module (error: $($error[0]))" $LogFile
Exit 1
}
DS_WriteLog "I" "Adding MDT Drive" $LogFile
try {
New-PSDrive -Name "DS001" -PSProvider "MDTProvider" –Root $MDTShare -Description "MDT Deployment Share"
DS_WriteLog "S" "Created MDT Drive" $LogFile
} catch {
DS_WriteLog "E" "An error occurred while creating the MDT Drive (error: $($error[0]))" $LogFile
Exit 1
}
DS_WriteLog "I" "Importing OS-Build to MDT" $LogFile
try {
$date = Get-Date -Format yyy-MM-dd-HHmm
New-Item -Path "DS001:\Operating Systems\2016\OSDBuilder-$date" -ItemType "Directory"
Import-MDTOperatingSystem -Path "DS001:\Operating Systems\2016\OSDBuilder-$date" -SourcePath "$($OSBuildSource.FullName)\OS" -DestinationFolder "OSDBuilder-2016-$date"
DS_WriteLog "S" "Imported latest OS-Build" $LogFile
} catch {
DS_WriteLog "E" "An error occurred while importing the OS-Build (error: $($error[0]))" $LogFile
Exit 1
}
try {
Remove-PSDrive -Name "DS001"
DS_WriteLog "S" "Removed the MDT Drive" $LogFile
} catch {
DS_WriteLog "E" "An error occurred while removing the MDT Drive (error: $($error[0]))" $LogFile
Exit 1
}
# ---------------------------------------------------------------------------------------------------------------------------
DS_WriteLog "-" "" $LogFile
DS_WriteLog "I" "End of script" $LogFile
You may have to edit these variables based upon your configuration.
Note that the ‘TaskName’ action is done in the ‘New-OSBuild’ section. This part can install any roles/features/languages that you may want to include that you don’t want to perform in your task sequence. If you just need Windows Updates, you can remove that whole ‘New-OSBuild’ section.
Also, be sure to modify the MDT import directory that matches how your structuring your Operating Systems.
Mine looks something like this:
If you need to check out the logs to diagnose or troubleshoot issues, everything gets logged to the ‘log directory’ you specify.
Summary
There you have it. You now have a way to Automate updating your Windows OS media by using OSDBuilder, which then gets imported into your MDT environment.
The next post I’ll go over the Task Sequence and how I’ve setup most of my installs.