Worklet: Automox OS Upgrade to Latest Windows10 OS version

The following Worklet will ensure all of your devices are always on the most current release of Windows 10. Since the current version is 2004, that is the version this Worklet will upgrade you.

This Worklet supports multiple languages:

DISCLAIMER: THIS WILL AUTOMATICALLY REBOOT THE DEVICE WHEN THE UPGRADE IS COMPLETE WITHOUT USER NOTIFICATION

Thanks to @aescolastico for creating this!

Evaluation:


if ((Test-Path $iso) -eq $true)
    {Remove-Item $iso
}

$osversion = (Get-Item "HKLM:SOFTWARE\Microsoft\Windows NT\CurrentVersion").GetValue('ReleaseID')

if (($osversion -lt "2004")) 
	{exit 1
		}
else 
	{exit 0
		}

Remediation:

function Get-Win10ISOLink {
    <#
    .SYNOPSIS
        This function generates a fresh download link for a Windows 10 ISO
    .NOTES
        Version:        1.6
        Author:         Andy Escolastico
        Creation Date:  10/11/2019
    #>
    [CmdletBinding()]
    param (
        [Parameter(Mandatory=$false)] 
        [ValidateSet("64-bit", "32-bit")]
        [String] $Architecture = (Get-WmiObject Win32_OperatingSystem).OSArchitecture,
        [Parameter(Mandatory=$false)] 
        [ValidateSet("fr-dz", "es-ar", "en-au", "nl-be", "fr-be", "es-bo", "bs-ba", "pt-br", "en-ca", "fr-ca", "cs-cz", "es-cl", "es-co", "es-cr", "sr-latn-me", "en-cy", "da-dk", "de-de", "es-ec", "et-ee", "en-eg", "es-sv", "es-es", "fr-fr", "es-gt", "en-gulf", "es-hn", "en-hk", "hr-hr", "en-in", "id-id", "en-ie", "is-is", "it-it", "en-jo", "lv-lv", "en-lb", "lt-lt", "hu-hu", "en-my", "en-mt", "es-mx", "fr-ma", "nl-nl", "en-nz", "es-ni", "en-ng", "nb-no", "de-at", "en-pk", "es-pa", "es-py", "es-pe", "en-ph", "pl-pl", "pt-pt", "es-pr", "es-do", "ro-md", "ro-ro", "en-sa", "de-ch", "en-sg", "sl-si", "sk-sk", "en-za", "sr-latn-rs", "en-lk", "fr-ch", "fi-fi", "sv-se", "fr-tn", "tr-tr", "en-gb", "en-us", "es-uy", "es-ve", "vi-vn", "el-gr", "ru-by", "bg-bg", "ru-kz", "ru-ru", "uk-ua", "he-il", "ar-iq", "ar-sa", "ar-ly", "ar-eg", "ar-gulf", "th-th", "ko-kr", "zh-cn", "zh-tw", "ja-jp", "zh-hk")]
        [String] $Locale = (Get-WinSystemLocale).Name,
        [Parameter(Mandatory=$false)]
        [ValidateSet("Arabic", "Brazilian Portuguese", "Bulgarian", "Chinese (Simplified)", "Chinese (Traditional)", "Croatian", "Czech", "Danish", "Dutch", "English", "English International", "Estonian", "Finnish", "French", "French Canadian", "German", "Greek", "Hebrew", "Hungarian", "Italian", "Japanese", "Korean", "Latvian", "Lithuanian", "Norwegian", "Polish", "Portuguese", "Romanian", "Russian", "Serbian Latin", "Slovak", "Slovenian", "Spanish", "Spanish (Mexico)", "Swedish", "Thai", "Turkish", "Ukrainian")]
        [String] $Language = "English",
        [Parameter(Mandatory=$false)] 
        [ValidateSet("1909", "Latest")]
        [String] $Version = "Latest"
    )
    
    # prefered architecture
    if ($Architecture -eq "64-bit"){ $archID = "x64" } else { $archID = "x32" }
    
    # prefered prodID
    if ($Version -eq "Latest") {
        # grabs latest id
        $response = Invoke-WebRequest -UserAgent $userAgent -WebSession $session -Uri "https://www.microsoft.com/$Locale/software-download/windows10ISO" -UseBasicParsing
        $prodID = ([regex]::Match((($response).RawContent), 'product-info-content.*option value="(.*)">Windows 10')).captures.groups[1].value
    } else{
        # uses hard-coded id
        $prodID = "1429"
    } 

    # variables you might not want to change (unless msft changes their schema)
    $pgeIDs = @("a8f8f489-4c7f-463a-9ca6-5cff94d8d041", "cfa9e580-a81e-4a4b-a846-7b21bf4e2e5b")
    $actIDs = @("getskuinformationbyproductedition", "getproductdownloadlinksbysku")
    $hstParam = "www.microsoft.com"
    $segParam = "software-download"
    $sdvParam = "2"
    $verID = "Windows10ISO"

    # used to spoof a non-windows web request
    $userAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36 Edge/18.18362"

    # used to maintain session in subsequent requests
    $sessionID = [GUID]::NewGuid()

    # builds session request url 
    $uri = "https://www.microsoft.com/" + $Locale + "/api/controls/contentinclude/html"
    $uri += "?pageId=" + $pgeIDs[0]
    $uri += "&host=" + $hstParam
    $uri += "&segments=" + $segParam + "," + $verID
    $uri += "&query="
    $uri += "&action=" + $actIDs[0]
    $uri += "&sessionId=" + $sessionID
    $uri += "&productEditionId=" + $prodID
    $uri += "&sdvParam=" + $sdvParam

    # requests user session
    $response = Invoke-WebRequest -UserAgent $userAgent -WebSession $session -Uri $uri -UseBasicParsing

    # prefered skuid
    if ($Version -eq "Latest") {
        # grabs latest id
        $skuIDs = (($response.RawContent) -replace "&quot;" -replace '</div><script language=.*' -replace  '</select></div>.*' -split '<option value="' -replace '">.*' -replace '{' -replace '}'| Select-String -pattern 'id:') -replace 'id:' -replace 'language:' -replace '\s' | ConvertFrom-String -PropertyNames SkuID, Language -Delimiter ','
        $skuID = $skuIDs | Where-Object {$_.Language -eq "$Language"} | Select-Object -ExpandProperty SkuID
    }
    else{
        # uses hard-coded id
        $skuID = "9029"
    } 

    # builds link request url
    $uri = "https://www.microsoft.com/" + $Locale + "/api/controls/contentinclude/html"
    $uri += "?pageId=" + $pgeIDs[1]
    $uri += "&host=" + $hstParam
    $uri += "&segments=" + $segParam + "," + $verID
    $uri += "&query="
    $uri += "&action=" + $actIDs[1]
    $uri += "&sessionId=" + $sessionID
    $uri += "&skuId=" + $skuID
    $uri += "&lang=" + $Language
    $uri += "&sdvParam=" + $sdvParam

    # requests link data
    $response = Invoke-WebRequest -UserAgent $userAgent -WebSession $session -Uri $uri -UseBasicParsing

    # parses response data 
    $raw = ($response.Links).href
    $clean = $raw.Replace('amp;','')

    # stores download link
    $dlLink = $clean | Where-Object {$_ -like "*$archID*"}

    # outputs download link
    Write-Output $dlLink
}

function Start-Win10UpgradeISO {
    <#
    .SYNOPSIS
        Downloads the latest Windows 10 ISO, mounts it, and runs it silently.
    .NOTES
        Version:        1.1
        Author:         Andy Escolastico
        Creation Date:  02/11/2020
        
        Version 1.0 (2020-02-11)
        Version 1.1 (2020-06-03) - Added handling for case where drive letter was not mounted.                
        Version 1.2 (2020-06-03) - Added ISO download functionality
    #>
    [CmdletBinding()]
    param (
        #THIS FLAG DOES NOT WORK FOR THIS FUNCTION
        [Parameter(Mandatory=$false)] 
        [Boolean] $Reboot = $true,
        [Parameter(Mandatory=$false)] 
        [ValidateSet("64-bit", "32-bit")]
        [String] $Architecture = (Get-WmiObject Win32_OperatingSystem).OSArchitecture,
        [Parameter(Mandatory=$false)] 
        [String] [String] $DLPath = (Get-Location).Path + "\" +"Win10_" + $Architecture + ".iso",
        [Parameter(Mandatory=$false)] 
        [String] $LogPath = (Get-Location).Path 
    )

    Write-Verbose "Attempting to generate a $Architecture windows 10 iso download link" -Verbose
    try {
        $DLLink = Get-Win10ISOLink -Architecture $Architecture
    }
    catch {
        throw "Failed to generate windows 10 iso download link."
    }
    
    Write-Verbose "Attempting to download windows 10 iso to '$DLPath'" -Verbose
    try {
        (New-Object System.Net.WebClient).DownloadFile($DLLink, "$DLPath")
    }
    catch {
        throw "Failed to download ISO at path specified."
    }
    
    $ISOPath = $DLPath
    
    if (Test-Path $ISOPath) {
        $DriveLetter = (Mount-DiskImage -ImagePath $ISOPath | Get-Volume).DriveLetter
    } else {
        throw "ISO could not be found under $($ISOPath)."
    }
    
    Write-Warning "The Upgrade will commence shortly. Your PC will be rebooted soon. Please save any work you do not want to lose."
    
    if ($DriveLetter) {
        if ($Reboot -eq $true){
            Invoke-Expression "$($DriveLetter):\setup.exe /auto Upgrade /quiet /Compat IgnoreWarning /DynamicUpdate disable /copylogs $LogPath"
        } else{
            Invoke-Expression "$($DriveLetter):\setup.exe /auto Upgrade /quiet /NoReboot /NoRestartUI /NoRestart /Compat IgnoreWarning /DynamicUpdate disable /copylogs $LogPath"
        }    
    } else {
        throw "ISO could not be mounted on this system."
    }

}
New-Alias -Name "Start-Win10FeatureUpdate" -Value "Start-Win10UpgradeISO" -ea 0

function Start-Win10UpgradeWUA {
    <#
    .SYNOPSIS
        This function downloads the Windows update assistant tool and runs it silently.
    .NOTES
        Version:        1.0
        Author:         Andy Escolastico
        Creation Date:  05/10/2020
    #>
    [CmdletBinding()]
    param (
        [Parameter(Mandatory=$false)] 
        [Boolean] $Reboot = $true,
        #THIS FLAG DOES NOT WORK FOR THIS FUNCTION
        [Parameter(Mandatory=$false)] 
        [String] $DLPath = (Get-Location).Path,
        [Parameter(Mandatory=$false)] 
        [String] $LogPath = (Get-Location).Path
    )
    if(!(Test-Path -Path $DLPath)){$null = New-Item -ItemType directory -Path $DLPath -Force}   
    if(!(Test-Path -Path $LogPath)){$null = New-Item -ItemType directory -Path $LogPath -Force}      
    $DLLink = "https://go.microsoft.com/fwlink/?LinkID=799445"
    $PackagePath = "$DLPath\Win10_WUA.exe"
    $LogPath = "$LogPath\Win10_WUA.log"
    (New-Object System.Net.WebClient).DownloadFile($DLLink, "$PackagePath")
    Write-Host "The Upgrade will commence shortly. Your PC will be rebooted. Please save any work you do not want to lose."
    if ($Reboot -eq $true){
        Invoke-Expression "$PackagePath /copylogs $LogPath /auto upgrade /dynamicupdate /compat ignorewarning enable /skipeula /quietinstall"
    } else{
        Invoke-Expression "$PackagePath /NoReboot /NoRestartUI /NoRestart /copylogs $LogPath /auto upgrade /dynamicupdate /compat ignorewarning enable /skipeula /quietinstall"
    }
}

function Start-Win10UpgradeCAB{
    <#
    .SYNOPSIS
        This function downloads the feature enablement package cab file and runs it silently using dism.exe.
    .NOTES
        Version:        1.0
        Author:         Andy Escolastico
        Creation Date:  06/11/2020
    #>
    [CmdletBinding()]
    param (
        [Parameter(Mandatory=$false)] 
        [ValidateSet("1909")]
        [String] $Version = "1909",
        [Parameter(Mandatory=$false)] 
        [Boolean] $Reboot = $true,
        [Parameter(Mandatory=$false)]
        [String] $DLPath = (Get-Location).Path,
        [Parameter(Mandatory=$false)] 
        [String] $LogPath = (Get-Location).Path
    )
    if(!(Test-Path -Path $DLPath)){$null = New-Item -ItemType directory -Path $DLPath -Force}   
    if(!(Test-Path -Path $LogPath)){$null = New-Item -ItemType directory -Path $LogPath -Force}    
    if($Version -eq "1909"){
        $DLLink = 'http://b1.download.windowsupdate.com/d/upgr/2019/11/windows10.0-kb4517245-x64_4250e1db7bc9468236c967c2c15f04b755b3d3a9.cab'
    }
    $PackagePath = "$DLPath\Win10_CAB.cab"
    $LogPath = "$LogPath\Win10_CAB.log"
    (New-Object System.Net.WebClient).DownloadFile($DLLink, "$PackagePath")
    if ($Reboot -eq $true){
        Invoke-Expression "DISM.exe /Online /Add-Package /Quiet /PackagePath:$PackagePath /LogPath:$LogPath"
    } else{
        Invoke-Expression "DISM.exe /Online /Add-Package /Quiet /NoRestart /PackagePath:$PackagePath /LogPath:$LogPath"
    }
}

Start-Win10FeatureUpdate -DLPath 'C:\Windows\Temp\Windows10.iso' -LogPath 'C:\Windows\Temp\WindowsUpgradeLogs'
3 Likes

sweet , the top check still refers to the 1909 iso? should that not be variable too?

@Maikel The 1909 was for removing the iso after the install. This is in the evaluation so it will remove it after remediation is complete and we auto re-evaulate. This is actually not needed anymore so I removed it.

2 Likes

What if there is an update in between this one and the one that the device is presently on? Will it step up to the one in between and then this one or just go straight to the latest?
This sounds wonderful, just want to make sure I understand it correctly. My patch window is tomorrow, and I’d like to give this a go on a test group.

The last line

[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Ssl3, [Net.SecurityProtocolType]::Tls, [Net.SecurityProtocolType]::Tls11, [Net.SecurityProtocolType]::Tls12; (new-object Net.WebClient).DownloadString('https://raw.githubusercontent.com/RFAInc/windows10-iso/master/win10-iso-functions.ps1') | Invoke-Expression; Start-Win10FeatureUpdate -DLPath 'C:\Windows\Temp\Windows10.iso' -LogPath 'C:\Windows\Temp\WindowsUpgradeLogs'

actually downloads the entire script from GitHub and runs the function. Its a cool technique for giving clients scripts that you can update after the fact.

Meaning you could replace the entirety of the remediation block with just that last line. With this setup, you get the latest code updates, but you have less control over what gets deployed. I wouldn’t recommend it. I would advise doing what you guys normally do, where you paste the actual code, and run the function. Right now you’re halfway there. You’re still just downloading the code directly from github and invoking that, instead of running what’s above it. ATM they are the same but that could change if we push new code.

It also means that if a device fails to download the script from github, the whole thing will fail. Despite the fact that you have the code downloaded from Automox servers already.

This is what the last line would look like instead:

Start-Win10FeatureUpdate -DLPath 'C:\Windows\Temp\Windows10.iso' -LogPath 'C:\Windows\Temp\WindowsUpgradeLogs'

Apologies if that explanation isnt very clear.

2 Likes

trying to upgrade win10 1909 and i get the following error:

Mount-DiskImage : The I/O operation has been aborted because of either a thread exit or an application request. At line:150 char:25 + $DriveLetter = (Mount-DiskImage -ImagePath $ISOPath | Get-Vol … + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + CategoryInfo : NotSpecified: (MSFT_DiskImage …torageType = 1):ROOT/Microsoft/…/MSFT_DiskImage) [Mou nt-DiskImage], CimException + FullyQualifiedErrorId : HRESULT 0x800703e3,Mount-DiskImage ISO could not be mounted on this system. At line:164 char:9 + throw “ISO could not be mounted on this system.” + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + CategoryInfo : OperationStopped: (ISO could not be mounted on this system.:String) [], RuntimeException + FullyQualifiedErrorId : ISO could not be mounted on this system. Error: 0x800703E3, -2147023901 Error Source: FACILITY_WIN32 Error Message: FACILITY_WIN32 is not currently supported by this translator.

Thanks @aescolastico, Nic just updated the last line as you defined in this update!

1 Like

We had a few other customers with a similar issue, and we were also able to reproduce in our own testing. Our issue turned out to not be an actual issue with mounting the iso properly, but the drive letter was not returned. Here is what fixed it for me:

In the “Start-Win10UpgradeISO” function

Try replacing this:

if (Test-Path $ISOPath) {
        $DriveLetter = (Mount-DiskImage -ImagePath $ISOPath | Get-Volume).DriveLetter

With this:

   if (Test-Path $ISOPath) {
        $DriveLetter = (Mount-DiskImage -ImagePath $ISOPath | Get-Volume).DriveLetter
        $DriveLetter = (Get-DiskImage -ImagePath $ISOPath |Get-Volume).DriveLetter
        if (!$DriveLetter){
            Mount-DiskImage -ImagePath $ISOPath -StorageType ISO
            } 

Hey I noticed a silly bug in my original code the other day.
The two lines that have:
if ($Version = “Latest”)

Are not using correct comparison syntax. Instead it’s assigning a value to the variable $Version.

It should be “-eq” instead of “=“. I’ve since update the code on GitHub. This would cause the script to always install the latest and never actually install version 1909. Apologies for not seeing this earlier. I would recommend updating the code in your worklet.

1 Like

Thanks for catching that @aescolastico - I’ve changed those two lines; let me know if they look ok now.

Looks right.

@Nic You guys ever find out if there’s a way to suppress reboots with the new ISOs?

1 Like

Not that I know of. Let me ping @awhitman and see if he’s found a way for that.

I can’t get this to work reliably at all. Ran it on 10 desktops and only 2-3 actually worked. Others just sit at pending and don’t ever do anything. My office has gigabit internet and computers running 7th gen i7s or newer so its not that.

This is a huge part of us investing in Automox and if we can’t get it to work reliably, that’s a big problem.

Hi SteveLord. You may want to open a help request with support for assistance.

If the Worklet is starting, but fails to run properly, as a test try running the scripts locally. Run the evaluation code to verify its return, then if it returns a non-zero, test the remediation code block locally on one of your devices where it is failing.
Test tips: Use an elevated ISE (x86) instance to run your tests. This will mimic the way the PS script will run via Automox. If you use a proxy, try running ISE as system rather than as a user to verify the download is working properly (as Automox agent would be making its internet connection as system rather thank your user).

Yeah I’ve done that before and it worked that day/ate up a bunch of time. Just seems like it isn’t reliable enough to be consistent. Guess I’ll carve time out for that again.

Whole point of me using this system is so I do not have to manually touch each machine and run these over and over. Otherwise I would just run Microsoft’s update assistant or ISO myself. Granted, I am not saying it isn’t part Microsoft’s fault either with their awful update architecture.

The success rate really depends on a lot of unpredictable factors that vary from environment to environment. I’m sure there are plenty of edge cases this script fails to address. But maybe you’ll have better luck with the function that uses the Update Assistant instead of an ISO.
Clone the Worklet and replace the last line in the remediation code with:

Start-Win10UpgradeWUA -DLPath ‘C:\Windows\Temp’ -LogPath ‘C:\Windows\Temp’

1 Like

@awhitman @d.mccleskey @Nic

Gentlemen, I’m new to the Automox world, and I’m not very familiar with PowerShell, so please forgive me for my ignorance. At the risk of being flamed: I’ve noticed 2 things.

  1. Is the first line of the evaluation code missing on this page? It starts on line 2, and the code for the 1909 Automatic Upgrade is very similar, except the first line. Is it okay to leave it out and copy exactly as shown above?

  2. On the blog post related to this update, step 4 says:
    “Copy and paste the Evaluation code scripts…”

As a newbie, I was thrown by this for a while. I didn’t know for sure what the purpose of the remediation code above was. I also didn’t see a way to let anyone know that it seemed incorrect, so here I am. Based on the 1909 blog post, I think it should actually say:
“Copy and paste the Evaluation and Remediation code scripts…”

Thank you so much for your time and graciousness.

~TB

1 Like

We are currently using this for updates, but have been running into an issue with the 2004 update and drive mappings. Is there a way to use this same worklet but only get them to the 1909 version until the drive mappings issue is fixed?

Hi @WGraham,
I had a problem with mounting too, and added an alternative mounting snippet above. Have you tried that?

Hi d.mccleskey,
I may have misspoke. The script works just fine and mounts the ISO perfect and does the update. The issue I am running into is when computers that are upgraded to 2004, they lose all network drives after being updated. That is why i was hoping for something similar but upgrades the computer to 1909 instead.