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:

Post 1: https://verticalagetechnologies.com/index.php/2020/03/04/citrix-microsoft-mdt-powershell-tools-the-beginning

Post 2: https://verticalagetechnologies.com/index.php/2020/03/16/building-multiple-citrix-images-with-one-microsoft-mdt-task-sequence

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:

  1. ‘First Run’ Powershell script (only ran once)
  2. Patch Tuesday arrives
  3. Run ‘Install/Update OSDbuilder’ Powershell script (scheduled task)
  4. Run ‘Ongoing Updates’ Powershell script (scheduled task)
  5. Run MDT Task Sequence
  6. 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:

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.

OSDBuilder

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.

Leave a Reply

Your email address will not be published. Required fields are marked *