OSDCloud #4 – OOBE customization

Introduction

Let’s continue our OSDCloud journey with more customization. In the previous post we were discussed the different options to use zero touch installation (ZTI). At the end of this process, we are landing in the out-of-box-experience (OOBE) phase. What capabilities do we have for a Windows installation from here?

In the following Microsoft article, you can read more details about the different configuration phases.

We are now in the OOBE phase, where we can start (Shift + F10) two important functions:

  • Start-OOBEDeploy: this function comes from the origin OSD module.
  • Start-AutopilotOOBE: this one has to be imported separately.

Both functions are running based on a JSON configuration file. Which are in the folder C:\ProgramData\OSDeploy:

OOBEDeploy – Evaluate

Before you are testing the automatisation, you can have some manual tests.

Addcapability

This function will let you add any Windows Capability as needed by gridView, or matching a string. Like Add-WindowsCapability PS command.

RemoveApps

This function will provisioned packages by matching a string, or displaying a GridView if you do not add a string to the command line. Like Remove-AppXProvisionedPackage -Online -AllUsers PS command.

RSAT

If you are looking to add some Remote Server Administration Tools, then this function will either match a string, or display a GridView to select from.

UpdateWindows

This function will fully update Windows including Microsoft Defender. It’s using in the backend the PSWindowsUpdate PS commandlet.

UpdateDrivers

This function will update hardware drivers from Microsoft.

OOBEDeploy – Automate

This JSON file can be imported during WinPE image creation. But then, we are again, too static.

So, I am building up this configuration file into the OSDCloud logic in the GitHub scripts. One example looks like that:

$OOBEDeployJson = @'
{
    "AddNetFX3":  {
                      "IsPresent":  true
                  },
    "Autopilot":  {
                      "IsPresent":  false
                  },
    "RemoveAppx":  [
                    "MicrosoftTeams",
                    "Microsoft.BingWeather",
                    "Microsoft.BingNews",
                    "Microsoft.GamingApp",
                    "Microsoft.GetHelp",
                    "Microsoft.Getstarted",
                    "Microsoft.Messaging",
                    "Microsoft.MicrosoftOfficeHub",
                    "Microsoft.MicrosoftSolitaireCollection",
                    "Microsoft.MicrosoftStickyNotes",
                    "Microsoft.MSPaint",
                    "Microsoft.People",
                    "Microsoft.PowerAutomateDesktop",
                    "Microsoft.StorePurchaseApp",
                    "Microsoft.Todos",
                    "microsoft.windowscommunicationsapps",
                    "Microsoft.WindowsFeedbackHub",
                    "Microsoft.WindowsMaps",
                    "Microsoft.WindowsSoundRecorder",
                    "Microsoft.Xbox.TCUI",
                    "Microsoft.XboxGameOverlay",
                    "Microsoft.XboxGamingOverlay",
                    "Microsoft.XboxIdentityProvider",
                    "Microsoft.XboxSpeechToTextOverlay",
                    "Microsoft.YourPhone",
                    "Microsoft.ZuneMusic",
                    "Microsoft.ZuneVideo"
                   ],
    "UpdateDrivers":  {
                          "IsPresent":  true
                      },
    "UpdateWindows":  {
                          "IsPresent":  true
                      }
}
'@

And this file can be saved then into this target folder. So, the OSDCloud process will grab this file automatically during the flow:

If (!(Test-Path "C:\ProgramData\OSDeploy")) {
    New-Item "C:\ProgramData\OSDeploy" -ItemType Directory -Force | Out-Null
}
$OOBEDeployJson | Out-File -FilePath "C:\ProgramData\OSDeploy\OSDeploy.OOBEDeploy.json" -Encoding ascii -Force

AutopilotOOBE

This module comes from David Segura as well and it’s published in PowerShell Gallery.

This part is a bit tricky. At least, at the beginning, it was for me. You can bring more Autopilot profile into the WinPE creation process. So far this opportunity is great. But our customers have always more than one OSDCloud profile.

Customer starts the process, chooses one OSDCloud profile, but how can we handle the requirement, which AP profile should applied when. I didn’t find here a solution.

Either, I am building the JSON file again, like for OOBEDeploy module. One example you can find here:

    $AutopilotOOBEJson = @"
    {
        "Assign":  {
                        "IsPresent":  true
                    },
        "GroupTag":  "$AssignedComputerName",
        "AddToGroup": "GroupX",
        "Hidden":  [
                        "AssignedComputerName",
                        "AssignedUser",
                        "PostAction",
                        "Assign"
                    ],
        "PostAction":  "Quit",
        "Run":  "NetworkingWireless",
        "Docs":  "https://google.com/",
        "Title":  "Manual Autopilot Register"
    }
"@

Naming Convention

The second challenge here is, the computer name convention. Everybody knows the limitations to set the computer name in Autopilot profile. Using AutopilotOOBE, the computer name can be defined as you want and pass the parameters to the well know PS module, Get-WindowsAutopilotInfo with the parameter -Online.

Write-Host -ForegroundColor Green "Define Computername:"
$Serial = Get-WmiObject Win32_bios | Select-Object -ExpandProperty SerialNumber
$TargetComputername = $Serial.Substring(4,3)

$AssignedComputerName = "AkosCloud-$TargetComputername"

Then, simply store this JSON near the OOBEDeploy one:

    If (!(Test-Path "C:\ProgramData\OSDeploy")) {
        New-Item "C:\ProgramData\OSDeploy" -ItemType Directory -Force | Out-Null
    }
    $AutopilotOOBEJson | Out-File -FilePath "C:\ProgramData\OSDeploy\OSDeploy.AutopilotOOBE.json" -Encoding ascii -Force 

When we have built our Autopilot registration puzzle parts together, the AutopilotOOBE GUI looks like:

E’Voila, we have our defined AssignedComputerName, GroupTag and AddToGroup values, which are coming not directly from an AzureAD Autopilot profile.

Two things to mention here:

  • On my virtual machine, I don’t have a qualified TPM chip –> shows red, that the registration would fail. It make a good opportunity for customers who are migrating older hardware models.
  • For GroupTag and AddToGroup, we have the ability to define more values. It could be choose in the GUI from a dropdown list.

Then we are handing over these defined parameters to the well know script. Our machine will be imported to Intune AND (!) assigned to the the correct AP profile.

Crazy things! 😉

But wait, one thing is missing here: our AzureAD credentials to have the correct privilege to do these tasks:

For this job, our customers are defining always a custom Intune role, which has these permissions:

Autopilot App Registration

Hmm… having a user interaction during the flow?! Can we automatise it?

Sure, we can. David has wrote in his blog a very well detailed guideline, how you can do it.

Would I suggest? It depends! Because until this OSD part, the process is running like we have wished silently and autonomous. What about if somebody gets this stick and starts rolling out other devices which we don’t want to. OK, we have restriction policies and other Intune / AzureAD settings to prevent it. But in my opinion, “break” the process for an authentication, makes in some cases (!) sense.

Puzzle is coming together

Our OSDCloud logic can be extended, what we have learned today. Put these parts together, store it in your online storage (we saw the case for GitHub in the previous post) and then start the journey. 😉

My puzzle parts are in a OOBETasks.CMD file, which has to be execute manually in the OOBE phase. I didn’t found a way, how this batch file can be kicked out automatically in this unattend phase. If somebody has an idea, I would be very happy to hear it. #CommunityPower

So, the process order is like that:

  • Update OSD module
  • Define parameters for Start-OSDCloud
  • Define OOBEDeploy configuration
  • Define AutopilotOOBE configuration
  • Define the execution order in a batch file
  • Restart computer

Batch file looks in my case so:

Write-Host -ForegroundColor Green "Create C:\Windows\System32\OOBETasks.CMD"
$OOBETasksCMD = @'
PowerShell -NoL -Com Set-ExecutionPolicy RemoteSigned -Force
Set Path = %PATH%;C:\Program Files\WindowsPowerShell\Scripts
Start /Wait PowerShell -NoL -C Install-Module AutopilotOOBE -Force -Verbose
Start /Wait PowerShell -NoL -C Install-Module OSD -Force -Verbose
Start /Wait PowerShell -NoL -C Start-AutopilotOOBE
Start /Wait PowerShell -NoL -C Start-OOBEDeploy
Start /Wait PowerShell -NoL -C Restart-Computer -Force
'@
$OOBETasksCMD | Out-File -FilePath 'C:\Windows\System32\OOBETasks.CMD' -Encoding ascii -Force

Log Files

Logfiles can be found for these OOBE tasks in $env:Windir\Temp.

Summary

Actually, I see only benefits using OSDCloud:

  • You are more flexible for Autopilot profile attributes. You can configure your own AutopilotOOBE.json files. So, we don’t have the Microsoft limitation, at least not for the computer naming convention.
  • At the beginning of the process, you can define which target OS do you want to install. So, we are not independent from the OEM delivery. Like we are/were multiple times: customer has ordered Windows 11, but it has been delivered Windows 10. In these cases, we have to care about the in-place upgrade process.
  • With the bloatwares are the same story. Some orders cannot be done correctly and only during Autopilot, you are recognising something is not OK. Here is the same, you can configure your own OOBEDeploy.json file per use cases/test scenarios.

Flexibility, modularity, dynamic using, automate are the key words here (and always) for me. 🙂

After these OOBE phase configs, you are ready to ship the device to the customers AND/OR you can start the pre-provisioning process from that point.

They are receiving an up-to-date OS and driver set, without the unwanted Appx’, installed required capabilities and Autopilot registered and profile assigned. Would you have still any wish in this OSD stage phase to have more job done?

Please let me know here as a comment or reach me out anytime via Twitter.

Happy #OSDClouding!

11 Comments


  1. Hi,
    You can try to add a setupcomplete.cmd script during the winpe phase that runs your OOBE scripts. It will run after reboot, before OOBE but on the real OS.
    Or you can change unattend.xml to run the script during OOBE phase.

    Reply

    1. Brilliant idea! Have you managed to kick off oobe scripts this way without manually start oobedeploy? Really like the zti deployment but it would be nice to automatically install netfx and add/ remove appx during OOBE without manually starting the scripts.

      Reply

    1. If i am not mistaken; you can place a C:\Windows\Setup\Scripts\OOBE.cmd file which will get executed post-OOBE. Did not test that yet though…

      C:\Setup\Scripts\SetupComplete.cmd will only start after the user sees the desktop…

      Reply

      1. OOBE.cmd location doesn’t matter. I am placing into C:\Windows\System32, because after Shift+F10 I am already in this folder.
        Regarding SetupComplete.cmd, it starts before OOBE phase comes up.

        Reply

  2. C:\Windows\Setup\Scripts\OOBE.cmd really is a special location, that file will always run automatically in enterprise/pro editions! See also: https://www.ntlite.com/community/index.php?threads/tutorial-create-perfect-windows-post-setup-using-oobe-cmd.1750/

    But it seems to run even before the real OOBE (before the last reboot actually), and thus it has limitations (not all powershell stuff is supported). I am searching for a way to auto-start every script we need in the OOBE phase under the temporary defaultuser0 user. This way the AutopilotOOBE window can popup without having to use Shift+F10 at all.

    Reply

  3. Great articles! Can you please explain these parts…?

    When you press Shift-F10, do you have access to Start-OOBEDeploy without doing anything?

    And will it just grap C:\ProgramData\OSDeploy\OSDeploy.OOBEDeploy.json if I created that file?

    Do you run “iex (irm sandbox.cloudosd.com) ?

    Reply

    1. Hi Henrik, you can put the your scripts together in the OOBE.cmd which you can start after Shift+F10. And yes, OSDeploy.AutopilotOOBE.json and OSDeploy.OOBEDeploy.json are your best friends for the customization.
      Nope, running “iex (irm sandbox.osdcloud.com)” is not required.

      Reply

  4. Hi Akos,

    If i run Start-OOBEDeploy in OOBE with Schedule Task and SYSTEM (from you scripts) Start-OOBEDeploy starts running but then quits not updating windows….

    If i run it manually from powershell i OOBE it works without any problem. Do you know how to solve this?

    Reply

    1. Hi Johan

      Did you check this log file?
      $Transcript = “$((Get-Date).ToString(‘yyyy-MM-dd-HHmmss’))-OOBEDeploy.log”
      Start-Transcript -Path (Join-Path “$env:SystemRoot\Temp” $Transcript) -ErrorAction Ignore

      Do you use a custom JSON file?
      $ProgramDataOSDeploy = “$env:ProgramData\OSDeploy”
      $JsonPath = “$ProgramDataOSDeploy\OSDeploy.OOBEDeploy.json”

      Cheers, Ákos

      Reply

Leave a Reply

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