For this post to make any sense check out https://osdbuilder.osdeploy.com/ for the details on OSDBuilder.

Since discovering the OSDBuilder tool from @SeguraOSD and being massively impressed by how well documented it is, I’ve wanted to automate the process. The tool is already pain-free to use, but the idea of just grabbing updated media from a NAS whenever patches have been released sounds great to me.

For myself, I just need a pretty stock image with just one customisation, enabling .NET Framework, this means that the script to fully automate the process is fairly simple.

If you exclude the Jenkins portion of this post, you can simply run the PowerShell script and have the updated ISOs, I’m using Jenkins because I have some other ideas to improve the process and it seemed like the right tool for it. In the current implementation, you could quite simply use Task Scheduler instead.

Pre-requirements

There are a few prerequisites for you to implement this method:

  • A Windows 10 or Windows Server installation to configure as a Jenkins Build agent.
  • The Windows 10 ADK installed on the server where you plan to build the ISOs.
  • A Jenkins server install, this can be on Windows or a Linux. The supported operating systems for Jenkins are fairly extensive – https://jenkins.io/download/.

The PowerShell Script

Step 1. Clearing down the folders from previous run

In the lab, I have a Windows Server 2019 install specifically for running these automated tasks and have OSDBuilder set to use “E:\OSDBuilder”. I plan to clear these folders down after the ISOs have been generated so this step is not strictly needed but could come in handy in the case of a failed build.

$directories = "E:\OSDBuilder\OSImport","E:\OSDBuilder\OSBuilds","E:\OSDBuilder\OSMedia","E:\OSDBuilder\Mount"
foreach($directory in $directories){ Get-ChildItem -Path $directory | Remove-Item -Recurse -Force -Verbose }

When OSDBuilder runs the WIM that is being worked on generally gets exported in to the AppData of the user account running the cmdlets, when the script is ran by the Jenkins build agent these temporary WIMs get created in “C:\Windows\Temp”, so we will also need to filter for WIMs there and remove them.

Get-ChildItem -Path "C:\Windows\Temp" -Filter *.wim | Remove-Item -Force -Verbose

In the actual script I have these steps grouped in a function as they are called more than once.

Create Paths

Step 2. Installing or updating OSDBuilder

Keeping OSDBuilder and OSDSUS up to date is important. The WSUS Update Catalogs, that OSDBuilder relies on, come from OSDSUS.

I would recommend installing OSDBuilder on the machine where you plan to run in it before using this script, this is simply so you can test functionality and get the basics of how it works understood.

## Install Modules ## 

Install-Module -Name OSDBuilder -Force
Import-Module -Name OSDBuilder -Force

## Initialise OSDBuilder and check some config ##

Get-OSDBuilder -SetHome E:\OSDBuilder
Get-OSDBuilder -CreatePaths

if(!(Test-Path -Path "E:\WindowsISOCache")){New-Item -Path "E:\" -ItemType Directory -Name WindowsISOCache}

Get-DownOSDBuilder -ContentDownload ‘OneDriveSetup Production’

The commands above will install/update OSDBuilder and OSDSUS to the latest releases available, set the home directory and create the folder structure needed. The last command downloads the latest version of the OneDrive Setup executable, OSDBuilder injects this into the image later on in the process.

Step 3. Mounting the ISO and running the update process

This is the meat and potatoes of the script, here we mount the ISO, import the media, update it and then package it back up as an ISO. This bit takes a while, but OSDBuilder is nice and verbose so you can see what’s happening the whole way through. Example below:

Sample Output
## Mount the reference image ##

$ReferenceISO = "E:\ReferenceImages\Windows10.iso"
Mount-DiskImage -ImagePath $ReferenceISO -Verbose

## Import the media, update it and run a OSBuild for each index ## 

$indexes = "Windows 10 Enterprise","Windows 10 Pro"
foreach($index in $indexes) { Import-OSMedia -ImageName $index -SkipGrid -Update -BuildNetFX }

## Eject the reference image ##

Dismount-DiskImage -ImagePath $ReferenceISO -Verbose

This will leave us with the freshly updated and edited Source media in “E:\OSDBuilder\OSBuilds” but I would like to create the ISOs. This makes sense for my future workflow but you can use the WIMs in the OS folder and import them into MDT or Configuration Manager.

I’m no PowerShell scripting wizard so just wrote the snippet below to copy the ISOs (renamed with the version information) to another folder as a cache.

## Copy the new ISOs to a different folder and rename with version. ##

$folders = Get-ChildItem -Path "E:\OSDBuilder\OSBuilds"

foreach($item in $folders){
    
    ## Create ISOs ##

    New-OSBMediaISO -FullName $item.FullName

    ## Get the ISO and work out name ##

    $iso = Get-ChildItem -Path "E:\OSDBuilder\OSBuilds\$($item.Name)\ISO"

    if(!(Test-Path -Path "E:\WindowsISOCache\$($item.Name).iso")){
        
        ## Copy the ISO over if its not already there ##

        Write-Host "## INFO ## ISO for this version not found, copying to the cache"

        Copy-Item -Path $iso.FullName -Destination "E:\WindowsISOCache\$($item.Name).iso" -Verbose
    
    
    }else{

        ## Do nothing if it exists ##

        Write-Host "## INFO ## ISO already exists in the cache. Skipping...."

    }

}

Step 4. Cleanup

All that’s left to do is remove all the data created. The process for an Enterprise and a Professional build of Windows consumes 30 to 40 GBs of space and I’d like to keep that down in the lab. I’ve done this by running the same code as the start of the script so no need to copy here.

Jenkins Implementation

I’m not going to set up Jenkins here as there are plenty of guides for that all over the web and I’m no expert on the matter. I’ve chosen to run my install on Ubuntu Server 18.04 and for that reason will need to set up a Windows Server with the Jenkins Build agent.

Windows Agent setup

Before setting up the agent in Jenkins, we need to enable a setting in the Global Security settings section. Inbound TCP Agent Protocol/4 (TLS encryption) needs to be enabled, this enables the Windows agent to launch the setup config and connect to the master.

Enable incoming ports in Jenkins

To add the agent go to Manage Nodes, then New Node and give it a name. Select Permanent Agent, then press OK.

Example New Node

You will then get a new page of settings to configure. The setting in the screenshot below are what I have picked.

Remote root directory – C:\Jenkins

Launch method – Launch agent by connecting it to the master

Label – OSDBuilder

Agent Configuration

To link the agent to the master server simply log in to your Windows Server and log in to the Jenkins web console. Select the agent and Launch the Java applet to setup the connection.

Run the downloaded app and you should get the screen below.

To make life easier you can install this app as a service so it connects to the master when the server boots. Click File and Install as service.

If you run into any issues setting up the service, then running the install command from an elevated prompt seems to work best for me.

cd C:\Jenkins
jenkins-slave.exe install
net start jenkinsslave-C__Jenkins

The agent should now be registered with Jenkins and we can start setting up the Jenkins Build.

Jenkins PowerShell Plugin

There is a very handy Jenkins PowerShell plugin that lets you add the script directly through the web UI, it can be found here: https://plugins.jenkins.io/powershell/

@adbertram has a great guide on installing PowerShell plugin if you start from the link below it covers everything needed. https://adamtheautomator.com/jenkins-powershell/#enabling-the-powershell-plugin

This guide also goes through setting up a build with parameters, which I’m not using. I’ll go through the exact configuration I have in the next step.

The Job

Select New Item and enter a name. We will be using a freestyle project. Click OK.

This opens the settings for this build. First lets tick Restrict where this project can be run and set the Label Expression to OSDBuilder. You should get a hint that the label is serviced by 1 node.

Jenkins can do scheduled jobs, so I have set a cron schedule to run on the second Wednesday of each month.

0 4 8-14 * 3
Would last have run at Wednesday, February 12, 2020 4:00:54 AM UTC; would next run at Wednesday, March 11, 2020 4:00:54 AM UTC

Now add a build step, select PowerShell and copy in the code below. This step is to install the module and update OSDSUS. I have this in a separate step so that it’s a different PowerShell session, I have had some funky problems with OSDBuilder when running in the same console session when I have run Update-OSDSUS. This eliminates the problem.

## Install latest versions of the modules
Install-Module -Name OSDBuilder -Force
Update-OSDSUS

Add another step and copy in the code below, changing the variables as needed to match your environment.

function ClearDownSpace {

    ## Clear down OSDBuilder Folders ##

    $directories = "E:\OSDBuilder\OSImport","E:\OSDBuilder\OSBuilds","E:\OSDBuilder\OSMedia","E:\OSDBuilder\Mount"

    foreach($directory in $directories){

        Get-ChildItem -Path $directory | Remove-Item -Recurse -Force -Verbose

    }

    ## Clear down WIMs in Windows Temp ##

    Get-ChildItem -Path "C:\Windows\Temp" -Filter *.wim | Remove-Item -Force -Verbose

}

## Clean up working directories ## 

ClearDownSpace

## Install Modules ## 

Install-Module -Name OSDBuilder -Force
Import-Module -Name OSDBuilder -Force

## Initialise OSDBuilder and check some config ##

Get-OSDBuilder -SetHome E:\OSDBuilder
Get-OSDBuilder -CreatePaths

if(!(Test-Path -Path "E:\WindowsISOCache")){New-Item -Path "E:\" -ItemType Directory -Name WindowsISOCache}

Get-DownOSDBuilder -ContentDownload ‘OneDriveSetup Production’

## Mount the reference image ##

$ReferenceISO = "E:\ReferenceImages\Windows10.iso"
Mount-DiskImage -ImagePath $ReferenceISO -Verbose

## Import the media, update it and run a OSBuild for each index ## 

$indexes = "Windows 10 Enterprise","Windows 10 Pro"
foreach($index in $indexes) { Import-OSMedia -ImageName $index -SkipGrid -Update -BuildNetFX }

## Eject the reference image ##

Dismount-DiskImage -ImagePath $ReferenceISO -Verbose

## Copy the new ISOs to a different folder and rename with version. ##

$folders = Get-ChildItem -Path "E:\OSDBuilder\OSBuilds"

foreach($item in $folders){
    
    ## Create ISOs ##

    New-OSBMediaISO -FullName $item.FullName

    ## Get the ISO and work out name ##

    $iso = Get-ChildItem -Path "E:\OSDBuilder\OSBuilds\$($item.Name)\ISO"

    if(!(Test-Path -Path "E:\WindowsISOCache\$($item.Name).iso")){
        
        ## Copy the ISO over if its not already there ##

        Write-Host "## INFO ## ISO for this version not found, copying to the cache"

        Copy-Item -Path $iso.FullName -Destination "E:\WindowsISOCache\$($item.Name).iso" -Verbose
    
    
    }else{

        ## Do nothing if it exists ##

        Write-Host "## INFO ## ISO already exists in the cache. Skipping...."

    }

}

## Clean up working directories ## 

ClearDownSpace

After saving the settings for the job the page will take you back to the job page. Click Build Now to start the job. You can view the output by clicking to the right of the build in the Build History section and selecting Console Output.

The result

Once the build has finished you can check the folder where you outputted the ISOs, for me that’s “E:\WindowsISOCache”.

I need both an Enterprise and a Professional patched image so the build process takes just shy of 3 hours in the lab, I’m sure on faster hardware timings will vary.

Conclusion

That’s it!

Thanks for taking the time to read through this post, this is the first proper one I’ve done so I would greatly appreciate some feedback. If you have any questions about anything explained, please free to ask.

I plan to automate this process further, so watch this space 🙂

The script can be found on my GitHub: https://github.com/samhep/autoWindowsImageServicingJenkins

Join the conversation

1 Comment

Leave a comment

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