Download Windows Assessment and Deployment Kit (Windows ADK) with BITS cmdlets

*! Update: 2014-09-30:
Looking for the latest version of ADK 8.1, see my most recent post

*! Update: 2014-04-08:
Looking for the latest version of ADK 8.1, see my most recent post

*! Update: 2013-10-24:
Looking for the latest version of ADK 8.1, see my most recent post

While I was working on installing System Center Configuration Manager 2012 SP1, I had to download and install Windows Assessment and Deployment Kit (Windows ADK) as it’s a requirement for the OSD (Operating System Deployment) role.
Yes, the Windows ADK replaces the Windows AIK (Windows Automated Installation Kit) requirement on ConfigMgr 2012 RTM. As of SP1, ConfigMgr 2012 is able to deploy Windows 8 and Windows Server 2012 and thus requires ADK (WinPE 4.0,…)

The following link also helped me quickly find out what components of ADK are required for ConfigMgr 2012 SP1:…whereas the official documentation on the technet is: Prerequisites For Deploying Operating Systems in Configuration Manager

I first went to the download center to get the ADK using this link:
You can get the English version with this powershell oneliner:

iwr -URI '' -OutFile F:\adksetup.exe

The above file acts also as a web downloader if it doesn’t find all the 3GB of files in a adjacent folders named Installers.

I wanted to know where it gets all these files. The log file was really helpful

We can see that the link is translated to, that it’s the root path of all the ADK files.
However, we are not allowed to browse files and subfolders.

I decided to extract all the URLs like this:

gc  "C:\Users\local-user\AppData\Local\Temp\adk\Assessment and Deployment Kit_20130213161354.log" | Get-Matches -Pattern ".*\sto\s(?<URL>http://.*)" | % { $_.URL } | Out-File F:\ADK.url.txt -Append

Note: Get-Matches was written by Dr. Tobias Weltner and is available on

Downloading 3GB of files sounds like a job for BITS (Background Intelligent Transfer Service) cmdlets.
But I’ve first to deal with the 302 web response, which means that the content has been temporarily moved.
To see it with Powershell, I’ve had to force the maximum redirection to 0, i.e. none, as it’s set by default to 5.

iwr -URI -MaximumRedirection 0 -ErrorAction SilentlyContinue

Now the last thing I had to do is to capture the location where the content has been moved to.

Finally, here’s the function I wrote to download the ADK files using BITS cmdlets 🙂

#Requires -Version 3.0            
Function Get-ADKFiles {            
    [system.string]$TargetFolder = $null                       
Begin {            
    # Make sure we run as admin                                    
    $usercontext = [Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()                                    
    $IsAdmin = $usercontext.IsInRole(544)                                                                               
    if (-not($IsAdmin)) {                                    
        Write-Warning "Must run powerShell as Administrator to perform these actions"                                    
    $HT = @{}            
    $HT += @{ ErrorAction = 'Stop'}            
    # Validate target folder            
    try {            
        Get-Item $TargetFolder @HT | Out-Null            
    } catch {            
        Write-Warning -Message "The target folder specified as parameter does not exist"            
Process {            
    $adkGenericURL = (Invoke-WebRequest -Uri -MaximumRedirection 0 -ErrorAction SilentlyContinue)            
    # There's an expected error saying:            
    # The maximum redirection count has been exceeded.             
    # To increase the number of redirections allowed, supply a higher value to the -MaximumRedirection parameter.            
    # 302 = redirect as moved temporarily            
    if ($adkGenericURL.StatusCode -eq 302) {            
        # Currently set to            
        $MainURL = $adkGenericURL.Headers.Location            
        $AllURLs = DATA {                        
            ConvertFrom-StringData @'
        93=Application Compatibility Toolkit-x64_en-us.msi
        94=Application Compatibility Toolkit-x86_en-us.msi
        95=Assessments on Client-x86_en-us.msi
        96=Assessments on Server-x86_en-us.msi
        134=Kits Configuration Installer-x86_en-us.msi
        135=Microsoft Compatibility Monitor-x86_en-us.msi
        137=Toolkit Documentation-x86_en-us.msi
        138=User State Migration Tool-x86_en-us.msi
        139=Volume Activation Management Tool-x86_en-us.msi
        143=Windows Assessment Services - Client (AMD64 Architecture Specific, Client SKU)-x86_en-us.msi
        144=Windows Assessment Services - Client (AMD64 Architecture Specific, Server SKU)-x86_en-us.msi
        145=Windows Assessment Services - Client (Client SKU)-x86_en-us.msi
        146=Windows Assessment Services - Client (Server SKU)-x86_en-us.msi
        147=Windows Assessment Services - Client (X86 Architecture Specific, Client SKU)-x86_en-us.msi
        148=Windows Assessment Services-x86_en-us.msi
        149=Windows Assessment Toolkit (AMD64 Architecture Specific)-x86_en-us.msi
        150=Windows Assessment Toolkit (X86 Architecture Specific)-x86_en-us.msi
        151=Windows Assessment Toolkit-x86_en-us.msi
        152=Windows Deployment Customizations-x86_en-us.msi
        153=Windows Deployment Tools-x86_en-us.msi
        154=Windows PE x86 x64 wims-x86_en-us.msi
        155=Windows PE x86 x64-x86_en-us.msi
        156=Windows System Image Manager on amd64-x86_en-us.msi
        157=Windows System Image Manager on x86-x86_en-us.msi
        158=WPT Redistributables-x86_en-us.msi
        # Create target folders if required as BIT doesn't accept missing folders            
        If (-not(Test-Path (Join-Path -Path $TargetFolder -ChildPath Installers))) {            
            try {            
                New-Item -Path (Join-Path -Path $TargetFolder -ChildPath Installers) -ItemType Directory -Force @HT            
                # New-Item -Path $TargetFolder -ItemType Directory -Force -ErrorAction Stop            
            } catch {            
                Write-Warning -Message "Failed to create folder $($TargetFolder)/Installers"            
        # Create an job that will downlad our first file            
        $job = Start-BitsTransfer -Suspended -Source "$($MainURL)/$($AllURLs['0'])" -Asynchronous -Destination (Join-Path -Path $TargetFolder -ChildPath ($AllURLs['0']))             
        For ($i = 1 ; $i -lt $AllURLs.Count ; $i++) {            
            $URL = $Destination = $null            
            $URL = "$($MainURL)Installers/$($AllURLs[$i.ToString()])"            
            $Destination = Join-Path -Path (Join-Path -Path $TargetFolder -ChildPath Installers) -ChildPath (([URI]$URL).Segments[-1] -replace '%20'," ")            
            # Add-BitsFile            
            $newjob = Add-BitsFile -BitsJob $job -Source  $URL -Destination $Destination            
            Write-Progress -Activity "Adding file $($newjob.FilesTotal)" -Status "Percent completed: " -PercentComplete (($newjob.FilesTotal)*100/($AllURLs.Count))            
        # Begin the download and show us the job            
        Resume-BitsTransfer  -BitsJob $job -Asynchronous            
        while ($job.JobState -in @('Connecting','Transferring','Queued')) {            
            Write-Progress -activity "Downloading ADK files" -Status "Percent completed: " -PercentComplete ($job.BytesTransferred*100/$job.BytesTotal)            
        Switch($job.JobState) {            
         "Transferred" {            
                Complete-BitsTransfer -BitsJob $job            
         "Error" {            
                # List the errors.            
                $job | Format-List             
         default {            
                # Perform corrective action.            
End {}            
Get-ADKFiles -TargetFolder 'D:\Downloads\ADK'

New Active Directory cmdlets available on Windows Server 2012

When I read the PowerTip: Use a PowerShell Cmdlet to Obtain AD DS Replication Information this week end, I gave it a try and noticed that there are new cmdlets available in the Powershell Active Directory module 😀

On a Windows 2008 R2 server, I got the total count of cmdlets in this Active Directory module:

ipmo ActiveDirectory            
(Get-Command -Module ActiveDirectory).Count            

On a Windows 2012 server I did the same, except that the module wasn’t listed as the RSAT feature wasn’t enabled. First, I needed to load the feature:

Add-WindowsFeature -Name RSAT-AD-PowerShell -Restart:$false

To be able to use the Add-WindowsFeature cmdlet on Windows 2008 R2, you also first need to load the Server Manager module like this:

ipmo ServerManager

It isn’t required on Windows 2012 because of the module preloading feature.

Now I’m able to inventory the total count of cmdlets in the Active Directory module for Powershell on Windows Server 2012 by invoking the same command:

(Get-Command -Module ActiveDirectory).Count            

Cool, huge difference. Then I wanted to get the list of these new cmdlets.
On the Windows 2008 R2 server, I prepared a preformatted console output that I would be able to paste on the Windows Server 2012 like this:

Get-Command -Module ActiveDirectory | % { '"{0}",' -f $_.Name }

I copied/pasted the result in the ISE console, added the first line to define an array and replaced the last comma (@line 77) by a closing paranthese (@line 78).

Here’s the full code that was in the ISE tab:

$W2008R2ADcmdlets = @(            
Get-Command -Module ActiveDirectory | % {            
    if ($_.Name -in $W2008R2ADcmdlets) {            
        # $true            
    } else {            
        # $false            
} | Sort-Object -Property Name            

Executing the above code gives the 59 new cmdlets available in Windows Server 2012 😎

Winter Scripting Games (2013) Event 2

The Winter Scripting Games 2013 warmup is now closed but the Official

Bear in mind that:

The Official 2013 Scripting Games are now hosted by, and will kick off at the PowerShell Summit North America 2013. Stay tuned! We will be posting more information in April 2013.


Someone asked on the Scripting Games forums:

It appears that I’ve saved it in the following image:

as well as a text version:

    Covering Your Assets (Open Until Tuesday, February 12, 2013)

    Your organization is preparing to renew maintenance contracts on all of your server computers.
    You need to provide management with a report that details each server’s computer name, manufacturer,
    model, number of logical processors, and installed physical memory.
    Most of the servers do not have Windows PowerShell installed.
    None of the servers a separated from your client computer by a firewall.
    The server computer names are provided to you in a file named C:\Servers.txt, which contains one computer name per line.

    Minimum Requirements

    Optional Criteria
    Suppress error messages if a computer cannot be contacted

    Desired Output
    ComputerName Model Manufacturer LogicalProcs PhysicalRAM
    SERVER2 PowerEdge Dell 8 128 SERVER3 Proliant Compaq 2 4

Here the solution I submitted for this event:

$Cred = (Get-Credential)            
Get-content C:\Servers.txt | ForEach-Object -Process {            
    $Sys = $proc = $Mem = $WMIresult = $null            
    $WMIHT = @{            
        ErrorAction = 'Stop'             
        ComputerName = $_            
        Credential = $Cred            
    try {            
        $Sys  = Get-WmiObject -Class Win32_ComputerSystem @WMIHT            
        $Proc = Get-WmiObject -Class Win32_Processor      @WMIHT            
        $Mem  = Get-WmiObject -Class Win32_PhysicalMemory @WMIHT            
        $WMIresult = $true            
    } catch {            
        # Silent fail            
    if ($WMIresult) {            
        New-Object -TypeName PSObject -Property @{            
            ComputerName = $Sys.Name            
            Manufacturer = $Sys.Manufacturer            
            Model = $Sys.Model            
            LogicalProcs = @($Proc).Count            
            PhysicalRAM = '{0:n2} GB ' -f ($Mem | % { $_.Capacity/1Gb } | Measure-Object -Sum).Sum            
} | Select-Object -Property ComputerName,Manufacturer,Model,LogicalProcs,PhysicalRAM |            
Format-Table  -AutoSize

I typed the following code on my non-domain joined Windows 8 computer and targeted a 2003 server, a Windows 2008 R2 and a Windows Server 2012.

I’ve got the following console output:

I first store my domain credentials in a variable named $cred.

The second step is to get the content of the servers.txt file. I didn’t specify the ReadCount argument of the Get-Content cmdlet as it’s set by default to 1 which means that lines are submitted one by one through the pipeline. This allows to create a loop on each server using the Foreach-Object cmdlet.

Then I choose to reset every variable I intend to use in the foreach loop.

Now, let’s talk about the event description details that will dictate how to communicate with these servers and how to gather the requested data from them.

As servers don’t even have Powershell installed locally and as they can be running any Windows operating system, WMI is the best way to go. In the event description, it’s said that we shouldn’t care about the firewall. In real life we do and WMI must be explicitely allowed in firewall rules.

The next step is to identify the WMI classes that have the requested properties. If you don’t know them, you should search it on the web as it’s the easiest approach. You can do it with Powershell but it won’t be very straight-forward. If you type the following key words in a browser, “WMI class get manufacturer model”, you can land for example on this technet page, Collecting Information About Computers, which is a good starting point.

If you’ve found the name of the WMI class Win32_OperatingSystem, you should check the complete MSDN documentation on this page as well as check its properties with Powershell. Usually, just listing properties like this should be enough…

gwmi Win32_OperatingSystem | fl *

…but, sometimes it may be also a good idea to look at the current schema of the class.
The most human readable version can be obtained with the following trick:


You can read more on the subject on this Hey Scripting Guy blog post. If you’re looking for super combo tricks, you should have a look to Bartek Bielawski‘s favorites ones on this page.

Now that all the properties have been identified, the next step is to query WMI with the shortest code as possible. One way to achieve this is to use the splatting technique. Splatting consists in passing a predefined hashtable of parameters with their values to a cmdlet. A hashtable is defined by using the following notation @{}. If you’ve Powershell 3.0 and updated your help using this command:

Update-help -Force -UICulture $host.CurrentUICulture

…you can actually read the help about splatting written by Rohn Edwards, the winner of the Advanced Division of the 2012 Scripting Games 😀

Get-help about_Splatting

As the event description mentioned that we had to suppress error messages if a computer cannot be contacted , I could have used the -ErrorAction SilentlyContinue but I intentionally didn’t and still have a preference for the try/catch block.

Don Jones wrote about the Winter Scripting Games content and gave a highly valuable feedback about it where he also mentions how to use the -ErrorAction SilentlyContinue:

That’s it for today. Last word, enroll in the Scripting Games this year if you never did as it’s an excellent way of learning Powershell and have fun 😎

Toggle SmartScreen Settings

You may have noticed that the Action Center on Windows Server 2012 is prompting to turn on the SmartScreen feature

It can be set with the SmartScreenSettings.exe executable in GUI mode:

…and the following small piece of code allows to either query or set its status under Powershell:

# Requires -Version 3.0            
Function Get-SmartScreenSettingsStatus {            
Begin {}            
Process {            
    try {            
       $val = Get-ItemProperty -Path HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer -Name SmartScreenEnabled -ErrorAction Stop            
    } catch {            
    if ($val) {            
        'Smart screen settings is set to: {0}' -f $val.SmartScreenEnabled            
    } else {            
        'Smart screen settings is set to: Off (by default)'            
End {}            
Function Set-SmartScreenSettingsStatus {            
Begin {            
    # Make sure we run as admin                        
    $usercontext = [Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()                        
    $IsAdmin = $usercontext.IsInRole(544)                                                                   
    if (-not($IsAdmin)) {                        
        Write-Warning "Must run powerShell as Administrator to perform these actions"                        
Process {            
    try {            
        Set-ItemProperty -Path HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer -Name SmartScreenEnabled -ErrorAction Stop -Value $State -Force             
    } catch {            
        Write-Warning -Message "Failed to write registry value because $($_.Exception.Message)"            
End {}            

Windows 2012 Server Manager, behind the scene

When I first saw the server manager, I didn’t realize how different it’s from its predecessors. Of course, I’ve heard the motto ‘the power of many, the simplicity of one’ and that it’s using Powershell to achieve what it does and display. Being curious, I wondered how but didn’t find any technical article or any specific documentation explaining how it really works behind the scene.

I’ll try to uncover some of its hidden or let’s say not well known aspects. Of course, what I’ll present below is limited, incomplete, etc.
Even if the title of the blog post might look too much promising, I hope that you’ll find interesting what I’ve to share.

First, the visible part of the iceberg is located in the EventLog and is full of relevant information.

As you can see in the above image, there are logs related to the Server Manager activities.
This information isn’t really hidden and it’s actually mentioned in the Quick_Reference_SM_WS12.pdf

From the above images, we may also notice that:

  • it seems to use the configure-smremoting.exe excutable is used to get, enable and disable the ‘remote management’.

    On Windows 2008 R2, it used to be a powershell script that had the following arguments:
  • it uses the DCOM protocol to accesss locally the WMI repository. Why? Without overthinking it, I guess that this will ensure that the server manager still works event if you’ve disabled the remote management.
  • it seems to use Powershell workflows to manage multiple machine over remoting as the server manager has a dedicated powershell session:
  • it invokes methods on classes located in the WMI namespace root\microsoft\windows\servermanager
  • Let’s enumerate all the methods that can be invoked on this class:

    Get-CimClass -Namespace root\microsoft\windows\servermanager -Class MSFT_ServerManagerTasks |            
    Select -ExpandProperty CimClassMethods

    Let’s try to see how the GetServerFeature method should be invoked:

    (Get-CimClass -Namespace root\microsoft\windows\servermanager -Class MSFT_ServerManagerTasks).CimClassMethods  |             
    ? Name -eq "GetServerFeature" | fl *

    Let’s attempt to invoke the method without arguments and see what happens:

    Get-CimClass -Namespace root\microsoft\windows\servermanager -Class MSFT_ServerManagerTasks |            
    Invoke-CimMethod -MethodName GetServerFeature            

    I’ve received two Instances that holds features as the Batch size default was too small.

    If I double the batch size, I’ll only receive a single instance.

    Now to see the content of the objects in this instance, I just need to set the results in an array and show their ItemValue property like this:

    (Get-CimClass -Namespace root\microsoft\windows\servermanager -Class MSFT_ServerManagerTasks |  Invoke-CimMethod -MethodName GetServerFeature -Arguments @{BatchSize=512}).ItemValue

    The above image tells use that the instance array holds server features objects:

    But these objects are different from those that we can get using

    (Get-WindowsFeature)[0] | gm

    Or using:

    (Get-WmiObject win32_serverfeature)[0] | fl *

    What we actually found at this point is how the server manager retrieves the following data 😀

    # Role            
    (Get-CimClass -Namespace root\microsoft\windows\servermanager -Class MSFT_ServerManagerTasks |              
    Invoke-CimMethod -MethodName GetServerFeature -Arguments @{BatchSize=512}).ItemValue |             
    ? { $_.State -eq 1 -and $_.Type -eq 0 } |             
    Select DisplayName            
    # Role Service            
    (Get-CimClass -Namespace root\microsoft\windows\servermanager -Class MSFT_ServerManagerTasks |             
    Invoke-CimMethod -MethodName GetServerFeature -Arguments @{BatchSize=512}).ItemValue |             
    ? { $_.State -eq 1 -and $_.Type -eq 1 } |            
    Select DisplayName            
    # Feature            
    (Get-CimClass -Namespace root\microsoft\windows\servermanager -Class MSFT_ServerManagerTasks |            
    Invoke-CimMethod -MethodName GetServerFeature -Arguments @{BatchSize=512}).ItemValue |             
    ? { $_.State -eq 1 -and $_.Type -eq 2 } |             
    Select DisplayName            
    (Get-CimClass -Namespace root\microsoft\windows\servermanager -Class MSFT_ServerManagerTasks |             
    Invoke-CimMethod -MethodName GetServerFeature -Arguments @{BatchSize=512}).ItemValue |             
    ? { $_.State -eq 1 } |              

    This one was easy. Let’s try to get services using the same approach:

    (Get-CimClass -Namespace root\microsoft\windows\servermanager -Class MSFT_ServerManagerTasks).CimClassMethods |            
    ? Name -match GetServerServiceDetail |            
    Select -ExpandProperty Parameters            

    Let’s provide the two arguments and the services as an array of strings:

    (Get-CimClass -Namespace root\microsoft\windows\servermanager -Class MSFT_ServerManagerTasks |              
    Invoke-CimMethod -MethodName GetServerServiceDetail -Arguments @{            
    BatchSize = 512;services="wudfsvc","WSusCertServer"            

    To figure out how to pass a wildcard it’s more difficult:

    (Get-CimClass -Namespace root\microsoft\windows\servermanager -Class MSFT_ServerManagerTasks |            
    Invoke-CimMethod -MethodName GetServerServiceDetail -Arguments @{            
    BatchSize = 512;services=@("*")            

    Now, let’s try to figure out where the data on the main page come from.

    (Get-CimClass -Namespace root\microsoft\windows\servermanager -Class MSFT_ServerManagerTasks).CimClassMethods |            
    ? Name -eq "GetServerInventory" |             
    select -ExpandProperty Parameters            

    To retrieve the IP Address of the network card, I can do:

    ((Get-CimClass -Namespace root\microsoft\windows\servermanager -Class MSFT_ServerManagerTasks |              
    Invoke-CimMethod -MethodName GetServerInventory -Arguments @{            
    } -Verbose) | ? ParameterName -eq NetworkAdapters            
    ).ItemValue | ? ConnectionStatus            

    For the Operating system,

    (Get-CimClass -Namespace root\microsoft\windows\servermanager -Class MSFT_ServerManagerTasks |            
    Invoke-CimMethod -MethodName GetServerInventory -Arguments @{            

    For the server inventory,

    (Get-CimClass -Namespace root\microsoft\windows\servermanager -Class MSFT_ServerManagerTasks |             
    Invoke-CimMethod -MethodName GetServerInventory -Arguments @{            

    What information is missing? The NiC teaming status, the firewall state, the Remote Desktop, the time zone, the IE enhanced security configuration state and the total disk space? I’d like to know how the server manager gathers the rest of the information. Currently I don’t know and I can unfortunately only make assumptions 😦

    That’s the end for this post but there are still many many things to explore 🙂 …

    … as well as other information already published by Microsoft:

    Winter Scripting Games (2013) Event 1

    Teresa Wilson, the Scripting Wife, talks about the first event in the 2013 Winter Scripting Games and how she solved the scenario

    Here’s the one-liner I submitted:

    Get-Volume -CimSession (            
    New-CimSession -Credential (Get-Credential) -ComputerName my.WS.2012.FQDN -Authentication Kerberos            
    ) | ? DriveType -eq "Fixed" |            
    Select -Property DriveLetter,FileSystemLabel,            
    @{l="Percentage of free disk space";e={'{0:N2}'-f($_.SizeRemaining*100/$_.Size)}},            
    @{l="Free space (GB)";e={'{0:N2}'-f($_.SizeRemaining/1GB)}},            
    @{l='Total Size (GB)';e={'{0:N2}'-f($_.Size/1GB)}}

    My appproach was different. The event description stated that we could use WMI or CIM. I prefered the latter for two reasons.
    First they are new and shipped with Powershell 3:

    The scenario stated that:

    In order to support future abilities to connect to other computers,
    you have been asked to use WMI or CIM to retrieve this information.

    I’ve a Windows 8 computer member of a workgroup and a Windows 2012 server joined to an Active Directory domain. I knew that ‘Windows Remote Management’ is enabled by default on 2012 box, whereas WMI isn’t (mainly because of firewall rules). In other words, it was a great opportunity to use CIM cmdlets.

    To solve the scenario, I also knew that Windows 8 has also a brand new built-in storage module. But, as my Windows 8 computer isn’t domain joined, I’ve had to create a CIM session with the New-CimSession cmdlet to be able to connect to the remote server.

    Then the scenario mentioned that I’ve to display the amount of free space on each fixed-disk volume. The Get-Volume cmdlet returns an array of Microsoft.Management.Infrastructure.CimInstance#ROOT/Microsoft/Windows/Storage/MSFT_Volume objects as output that has a ‘DriveType’ property. To filter the output, I chose to use the new Powershell 3.0 syntax. Surrounding curly braces can be omitted when using simple filters with the Where-Object cmdlet that has ? for alias.

    The last step was about formatting. Teresa Wilson used the format-table cmdlet that has the ability to rename properties and change the display by calculating an expression. I prefered the Select-Object cmdlet to achieve this formatting as this cmdlet can actually create a new calculated property using the same syntax the Format-Table uses. I also used Select-Object as its output can be piped into any formatting cmdlet.

    Get Active Directory schema versions (update)

    This month the page on “How to determine the current Active Directory or Exchange Server schema version”, – –, has been updated to reflect the new version of Exchange. Unfortunately the KB article doesn’t provide guidance using powershell,…but the Hey Scripting Guy, Ed Wilson and Ashley McGlone published last year what was required and known at that time on “How to Find Active Directory Schema Update History by Using PowerShell

    Now, it’s my turn to publish an updated version of code that adds Exchange 2013, Windows Server 2012 and Lync Server 2013 😀

    #Requires -Version 2.0            
    Function Get-ADSchemaVersions {            
    Begin {            
        # First we load the Active Directory module if required                        
        if ((Get-Module -Name ActiveDirectory).Name -ne "ActiveDirectory") {                        
            Write-Verbose -Message "Attempting to load Active Directory module for Powershell"                        
            Import-Module -Name ActiveDirectory -ErrorAction SilentlyContinue                        
            if ( (Get-PSDrive -PSProvider ActiveDirectory -ErrorAction SilentlyContinue).Name -ne "AD") {                        
                Write-Warning -Message "Failed to load the ActiveDirectory Module"                  
        $KownSchema = DATA {            
        ConvertFrom-StringData @'
        13=Windows 2000 Server
        30=Windows Server 2003
        31=Windows Server 2003 R2
        44=Windows Server 2008
        47=Windows Server 2008 R2 
        56=Windows Server 2012 RTM
        4397=Exchange Server 2000 RTM
        4406=Exchange Server 2000 SP3
        6870=Exchange Server 2003 RTM
        6936=Exchange Server 2003 SP3
        10628=Exchange Server 2007 RTM
        10637=Exchange Server 2007 RTM
        11116=Exchange 2007 SP1
        14622=Exchange 2007 SP2 or Exchange 2010 RTM
        14726=Exchange 2010 SP1
        14732=Exchange 2010 SP2
        15137=Exchange 2013 RTM
        1006=LCS 2005
        1007=OCS 2007 R1
        1008=OCS 2007 R2
        1100=Lync Server 2010
        1150=Lync Server 2013
    Process {            
        try {            
            $SchemaPartition = (Get-ADRootDSE -ErrorAction Stop).NamingContexts | Where-Object {$_ -like "*Schema*"}             
        } catch {            
            Write-Warning -Message "Failed to find the AD naming context"            
        if ($SchemaPartition) {            
            # Get the version of AD schema            
            try {            
                $SchemaVersionAD = (Get-ADObject $SchemaPartition -Property objectVersion -ErrorAction Stop).objectVersion            
                New-Object -TypeName psobject -Property @{            
                    ProductName = "Active Directory"            
                    Version = $SchemaVersionAD            
                    Description = $KownSchema[$SchemaVersionAD.ToString()]            
            } catch {            
                Write-Warning -Message "Failed to query the AD schema version"            
            # Get the version of Exchange            
            try {            
                $SchemaVersionExchange = (Get-ADObject "CN=ms-Exch-Schema-Version-Pt,$SchemaPartition" -Property rangeUpper -ErrorAction Stop).rangeUpper            
                New-Object -TypeName psobject -Property @{            
                    ProductName = "Exchange"            
                    Version = $SchemaVersionExchange            
                    Description = $KownSchema[$SchemaVersionExchange.ToString()]            
            } catch {            
                Write-Warning -Message "Schema version for Exchange not found"            
            # Get the version of Lync            
            try {            
                $SchemaVersionLync = (Get-ADObject "CN=ms-RTC-SIP-SchemaVersion,$SchemaPartition" -Property rangeUpper -ErrorAction Stop).rangeUpper            
                New-Object -TypeName psobject -Property @{            
                    ProductName = "Lync"            
                    Version = $SchemaVersionLync            
                    Description = $KownSchema[$SchemaVersionLync.ToString()]            
            } catch {            
                Write-Warning -Message "Schema version for Lync not found"            
    End {}            
    } # end of function

    Here’s how to use the above function:

    Get-ADSchemaVersions | Select ProductName,Version,Description | ft -AutoSize

    This was the easy part. Now, let’s try to answer the following question: “I’d like to deploy System Center Configuration Manager 2012 SP1. How do I check if the Active Directory schema of the forest is ready?”

    Microsoft has published the following technet page for this purpose: Determine Whether to Extend the Active Directory Schema for Configuration Manager

    Even if the update of the schema is not strictly required, Powershell can help us check for additions of SMS v4 attributes. What are these new SMS V4 attributes ? The information is located in the file SMSSETUP\BIN\X64\ConfigMgr_ad_schema.ldf of the System Center 2012 ConfigMgr installation media.

    The following piece of code allows to quickly check for 3 of these new attributes:

    try {            
        ipmo -Name ActiveDirectory -ErrorAction Stop            
        $SchemaPartition = (Get-ADRootDSE -ErrorAction Stop).NamingContexts | Where-Object {$_ -like "*Schema*"}            
        Get-ADObject "CN=mS-SMS-Version,$SchemaPartition" -ErrorAction Stop            
        Get-ADObject "CN=mS-SMS-Capabilities,$SchemaPartition" -ErrorAction Stop            
        Get-ADObject "CN=mS-SMS-Source-Forest,$SchemaPartition" -ErrorAction Stop            
    } catch {            
        Write-Warning -Message "Failed to find SMS v4 new attributes because $($_.Exception.Message)"            

    As these attributes exist and are displayed in the above image, it means that my AD schema is ready for System Center 2012 Configuration Manager SP1 😎

    WSUS on Windows Server 2012 Core from scratch

    I don’t know anything about WSUS as I’m more a ConfigMgr guy but I wanted to evaluate a few things about WSUS a.k.a Update Services on Windows Server 2012:

    • Can it run on a Core version?
    • Can it be managed on Core version?

    The short answer is YES, it can run on a Core version as it’s a built-in role. YES, it can be managed on Core version but you should rather stick to Microsoft piece of advice “install it on core, manage it from a Windows 8 box with RSAT” (Remote Server Administration Tools). If you don’t have a Windows 8, you can also switch the server to the Minimal shell configuration that will allow you to launch MMC based snap-ins.

    So here’s my scenario. I want to build a VM running a Core version of Windows server 2012, install Updates Services, configure it and push updates to Windows 7 with Office 2010 computers.

    • Step1: Provision a new VM on the Hyper-V 2012 host
    • New-VM -Name "CO2" -MemoryStartupBytes 2GB -NewVHDPath CO2.vhdx -BootDevice CD -NewVHDSizeBytes 50GB -SwitchName Prod            
      # Check the controller            
      Get-VM -Name CO2 | Get-VMHardDiskDrive            
      # Check the type of HDD created            
      Get-VM -Name CO2 | Get-VMHardDiskDrive | Get-VHD            
      # Configure the BIOS            
      Get-VM -Name CO2 | Set-VMBios -EnableNumLock            
      # Increase the number of processors            
      Get-VM -Name CO2 | Set-VM -ProcessorCount 2            
      # Configure Dynamic memory            
      Get-VM -Name CO2 | Set-VMMemory -DynamicMemoryEnabled:$true -MaximumBytes 4096MB -MinimumBytes 786MB            
      # Check that I have DVD drive attached to the VM            
      Get-VM -Name CO2 | Get-VMDvdDrive            
      # Load a bootable ISO of WS 2012            
      Set-VMDvdDrive -Path D:\Downloads\SW_DVD5_Win_Svr_Std_and_DataCtr_2012_64Bit_English_Core_MLF_X18-27588.ISO -VMName CO2            
      # Configure VM starup/stop options            
      Get-vm -VMName CO2 | set-VM -AutomaticStartAction Nothing -AutomaticStopAction Shutdown            
      # Start the VM            
      Start-VM -Name CO2

    • Step2: Install Windows 2012 from the loaded DVD drive
    • Step 3: Perform some basic post-installation tasks
    • # Check the current IP configuration (no DHCP available)            
      Get-NetAdapter | ? Status -eq "Up" | Get-NetIPAddress | ft IPAddress            
      Get-NetAdapter | ? Status -eq "Up" | Get-NetIPAddress -AddressFamily IPV4            
      # As the IPAddress is APIPA and as I have only 1 adapter I can do:            
      Get-NetApater | ? Status -eq "Up" | New-NetIPAddress -IPAddress -DefaultGateway -PrefixLength 24            
      # Set the DNS server            
      Get-NetAdapter | ? Status -eq "Up" | Set-DnsClientServerAddress -ServerAddresses,            
      # Set a proxy            
      netsh --% winhttp set proxy "my.corp.proxy.fqdn:7070"            
      netsh --% winhttp show proxy            
      # Update the powershell help            
      Update-Help -Force -UICulture en-us
      # Enable remote desktop            
      Enable-NetFirewallRule -DisplayGroup "Remote Desktop"            
      Set-ItemProperty -Path 'HKLM:\SYSTEM\CurrentControlSet\Control\Terminal Server' -Name fDenyTSConnections -Value 0            
      # Get the current time zone            
      tzutil --% /g            
      # List all time zones            
      tzutil --% /l            
      # Set it            
      tzutil --% /s "Romance Standard Time"            
      # Verify            
      tzutil --% /g            
      # Change the date time            
      Get-Date -Date "01/26/2013 06:29:00 PM" | Set-Date
    • Step 4: Upgrade the VM integration tools
    • On the Hypervisor do:

      # Check the version of Integration services            
      Get-VM | ? State -eq "Running" | Select Name,State,Integrationservicesversion | ft -AutoSize            
      # Upgrade it to  6.2.9200.16433            
      Get-VM -Name CO2 | Get-VMDvdDrive | Set-VMDvdDrive -Path C:\Windows\System32\vmguest.iso            

      Inside the VM do:

      & D:\support\amd64\setup.exe

    • Step5: Configure Windows Udpate
    • # Configure regional and language options: control intl.cpl            
      Show-ControlPanelItem -Name Region            
      # Configure the timezone, date, or time: control timedate.cpl            
      Show-ControlPanelItem -Name "Date and Time"            
      # Disable Automatic Updates            
      New-ItemProperty -Path  'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Auto Update' -Name AUOptions -Type DWORD -Value 1            
      # Copy/paste function and invoke            

      Get-WUSettings can be found on

    • Step 6: Opt-in to Microsoft Update and install a subset a selected updates
    • # Opt-in to MU            
      $UpdateSvc = New-Object -ComObject Microsoft.Update.ServiceManager            
      $UpdateSvc.QueryServiceRegistration("7971f918-a847-4430-9279-4a52d1efe18d") | fl *            
      $UpdateSvc.Services | ? Name -eq "Microsoft Update"            
      # Scan for missing updates            
      $Session = New-Object -ComObject Microsoft.Update.Session            
      $Searcher = $Session.CreateUpdateSearcher()            
      $Criteria = "IsInstalled=0 and DeploymentAction='Installation' or IsPresent=1 and DeploymentAction='Uninstallation' or IsInstalled=1 and DeploymentAction='Installation' and RebootRequired=1 or IsInstalled=0 and DeploymentAction='Uninstallation' and RebootRequired=1"            
      $SearchResult = $Searcher.Search($Criteria)            
      # Display missing updates            
      $SearchResult.Updates | ft Title,AutoSelectOnWebSites            
      # Prepare to download            
      $updatesToDownload = New-Object -ComObject Microsoft.Update.UpdateColl            
      $downloader = $Session.CreateUpdateDownloader()            
      $updatesToInstall = New-Object -ComObject Microsoft.Update.UpdateColl            
      # Select items to be downloaded and installed            
      $SearchResult.Updates | Select-item -By Title,AutoSelectOnWebSites -Multi | % {$updatesToDownload.Add($_)}            

      # Download            
      $downloader.Updates = $updatesToDownload            
      $downresults = $downloader.Download()            
      # Install            
      $updatesToDownload | ? { -not($_.isInstalled) -and $_.isDownloaded} | % {$updatesToInstall.Add($_)}            
      $installer = $Session.CreateUpdateInstaller()            
      $installer.Updates = $updatesToInstall            
      $installationResult = $installer.Install()            
      # Show the result            

      As I run a Core version, there isn’t the built-in cmdlet Out-GridView with its nice PassThru parameter to allow me to select the udpates I want. If I try to use Out-GridView, I end up with the following error message:
      Out-GridView : To use the Out-GridView, install the Windows PowerShell ISE feature from Server Manager and restart this application.
      (Could not load file or assembly ‘Microsoft.PowerShell.GraphicalHost, Version=,Culture=neutral, PublicKeyToken=31bf3856ad364e35’ or one of its dependencies. The system cannot find the file specified.)

      The ISE feature is “removed” on Core version, i.e. “Disabled with Payload removed”. If you want to add back the ISE to be able to use Out-GridView, you can follow the steps on this page

      As you may have noticed, I didn’t need to use Out-GridView as Rob Campbell published an awsome Select-Item function on this page that does perfectly the job:

    • Step7: Add a second disk, initiliaze, partition and format it:
    • In the Hypervisor do:

      # Create a new dynamic VHDX disk            
      New-VHD -Dynamic -Path D:\VM\HDD\CO2_DATA.VHDX -SizeBytes 20GB            
      # Attach it as a iSCSI disk to the VM to store data            
      Get-VM -Name CO2 | Add-VMHardDiskDrive -ControllerType SCSI -Path D:\VM\HDD\CO2_DATA.vhdx            

      In the VM do:

      # Initialize the second harddisk (iSCSI)            
      Get-Disk | ft -AutoSize            

      Get-Disk -Number 1 | Initialize-Disk            
      Get-Disk -Number 1 | ft -AutoSize

      # Check volume letters            
      Get-Volume | ft -AutoSize                        

      # Change the CD drive letter            
      Set-CDRomLetter -NextAvailableLetter -Verbose            

      # Create a new primary partition            
      Get-Disk -Number 1 | New-Partition -UseMaximumSize -AssignDriveLetter   

      Get-Volume | ? { $_.DriveType -eq "Fixed" -and -not($_.FileSystem)}            
      # Format it            
      Get-Partition -DriveLetter D |  Format-Volume -FileSystem NTFS

      The Set-CDRomLetter cmdlet I used above isn’t a built-in cmdlet. It can be obtained on this page:

    • Step8: Change the computer name and join the domain
    • Add-Computer -DomainName -Server -Credential (Get-Credential) -NewName CO2            

      After joining the domain and rebooting the computer, I log on using a privileged account, change its user locale, logoff/on, review installed security updates and check firewall rules.

      Get-Command -Module International            
      Set-Culture en-GB            
      # Logoff            
      Get-NetFirewallRule | ? Enabled | ft Name,DisplayName,Action

      For the update history, see the comment in the following post:

    • Step9: Install the Update Services feature and it dependencies
    • You should not use the -IncludeAllSubFeature as the WSUS database subfeature are at the same level and you have to choose what kind of database you want:
      1. Feature WID Database has a conflict with the features Database.
      2. Feature Database has a conflict with the features WID Database.

      Instead do:

      Add-WindowsFeature -Name UpdateServices  -Restart:$false -IncludeManagementTools:$true

      2 warnings are issued but the 2nd is the most important:
      WARNING: Windows automatic updating is not enabled. To ensure that your newly-installed role or feature is automatically updated, turn on Windows Update.
      WARNING: Additional configuration may be required. Review the article Managing WSUS Using PowerShell at TechNet Library
      ( for more information on the recommended steps to perform WSUS installation using PowerShell.

    • Step10: Perform Update Services’ post-installation task
    • # Create a folder where WSUS will download its content            
      New-Item D:\WSUS.Content -Type Directory            
      & 'C:\Program Files\Update Services\Tools\WsusUtil.exe' --% postinstall CONTENT_DIR=D:\WSUS.Content            

    • Step11: Review installed features, test the WSUS and examin BPA results
    • Get-WindowsFeature | ? Installed

      If you didn’t perform the post-installation you’ll have the following:

      When it works you get:

      Launch a BPA analysis on Update Services and review the results:

      Get-BpaModel | ? Name -match "Update Services" | Invoke-BpaModel            
      Get-BpaModel | ? Name -match "Update Services" |                        
      Get-BpaResult | ? Resolution |                         
      fl -p ResultNumber,Severity,Category,Title,Problem,Resolution            

      Add some miscellaneous features that I will use later on

      # Add other features            
      Add-WindowsFeature -Name GPMC -Restart:$false -Verbose            
      Add-WindowsFeature -Name RSAT-AD-PowerShell -Restart:$false -Verbose

    • Step12: Configure WSUS
    • Currently, I have done all the steps mentioned on Getting started with WSUS in the Windows Server 8 beta: Installing the WSUS role using the new Server Manager

      We will now focus on configuring WSUS. Before continuing, here are some related good articles:

      # List all built-in cmdlets associated with WSUS            
      Get-Command -Module UpdateServices

      No comment. Humm, actually I’ve one. As you’ll see below, I don’t do an extensive use of these cmdlets except Get-WSUSserver as some of these cmdlets return a custom type of object instead of the raw .Net object.

      # View the configuration options            
      # Choose the upstream server            
      Set-WsusServerSynchronization -SyncFromMU            
      # Set the Proxy            
      $config = (Get-WsusServer).GetConfiguration()            
      $config.GetContentFromMU = $true            
      $config.UseProxy = $true            
      $config.ProxyName = "my.corp.proxy.fqdn"            
      $config.ProxyServerPort = 7070            
      # Choose Languages            
      $config.AllUpdateLanguagesEnabled = $false            
      $config.AllUpdateLanguagesDssEnabled = $false            
      # View enabled Languages            
      # Begin initial synchronization            
      $subscription = (Get-WsusServer).GetSubscription()            
      While ($subscription.GetSynchronizationStatus() -ne 'NotProcessing') {            
          Write-Host "waiting on sync"            
          Start-Sleep -Seconds 5            
      # View all products            
      (Get-WsusServer).GetUpdateCategories() | ft Title,Id            
      # Choose Products            
      $subscription = (Get-WsusServer).GetSubscription()            
      $products = (Get-WsusServer).GetUpdateCategories() | ? {$_.Id -in @(            
          '84f5f325-30d7-41c4-81d1-87a0e6535b66', # Office 2010            
          'bfe5b177-a086-47a0-b102-097e4fa1f807'  # Windows 7            
      $coll = New-Object -TypeName Microsoft.UpdateServices.Administration.UpdateCategoryCollection            
      $products | % { $coll.Add($_) }            
      # View all classifications            
      Get-WsusClassification | % {'{0}, # {1}' -f $_.Classification.Id,$_.Classification.Title }            
      # As the Title is based on             
      # Choose Classifications            
      $subscription = (Get-WsusServer).GetSubscription()            
      $classifications = (Get-WsusServer).GetUpdateClassifications() | ? {$_.Id -in @(            
          'e6cf1350-c01b-414d-a61f-263d14d133b4', # 'Critical Updates',            
          '0fa1201d-4330-4fa8-8ae9-b877473b6441', # "Security Updates",            
          '68c5b0a3-d1a6-4553-ae49-01d3a7827828'  # "Service Packs"            
      $coll = New-Object -TypeName Microsoft.UpdateServices.Administration.UpdateClassificationCollection            
      $classifications  | % { $coll.Add($_) }            
      # Check selected classifications            
      # Start a sync            
      # View it progress            
      # Wait for sync            
      $subscription = (Get-WsusServer).GetSubscription()            
      while ($subscription.GetSynchronizationProgress().ProcessedItems -ne $subscription.GetSynchronizationProgress().TotalItems) {            
          Write-Progress -PercentComplete (            
          ) -Activity "Sync"             
      # Make sure the default automatic approval rule is not enabled            
      # View synchronization schedule options            
      # Get Configuration state            
      # Get WSUS status            
    • Step13: Approve specific updates for a custom target group
    • Source:

      There are two default computer groups: All Computers and Unassigned Computers. By default, when each client computer first contacts the WSUS server, the server adds that client computer to both of these groups.

      You can create as many custom computer groups as you need to manage updates in your organization. As a best practice, create at least one computer group to test updates before you deploy them to other computers in your organization.

      # Create a new computer group            
      (Get-WsusServer).CreateComputerTargetGroup("Windows 7 x64")            

      # List all groups            
      # Find any update that has 'Windows 7' string mentioned            
      $allW7updates = (Get-WsusServer).SearchUpdates("Windows 7")            
      # Show only Windows 7 x64 security updates            
      $allW7updates |             
          ? { (-not($_.IsSuperseded)) -and ($_.Title -match "x64") -and ($_.UpdateClassificationTitle -eq "Security Updates")} |            
          ft Title,State,KnowledgebaseArticles,SecurityBulletins            
      # Define the group to which I will assign updates for install            
      $targetgroup = (Get-WsusServer).GetComputerTargetGroups() | ? Name -eq "Windows 7 x64"            
      # Approve all Windows 7 security udpates except those for .Net 4            
      $allW7updates | ? { (-not($_.IsSuperseded)) -and ($_.Title -match "x64") -and ($_.UpdateClassificationTitle -eq "Security Updates")} |            
      ? {$_.Title -notmatch ".NET Framework 4"} | ForEach-Object -Process {            
      # Look for Windows 7 SP1            
      (Get-WsusServer).SearchUpdates("Windows 7 Service Pack 1 for x64")            
      # Approve Windows 7 SP1            
      (Get-WsusServer).SearchUpdates("Windows 7 Service Pack 1 for x64") | ForEach-Object -Process {            
      # Approve all security udpates for Office 2010 (32bit)            
      (Get-WsusServer).SearchUpdates("Office 2010") |             
      ? { (-not($_.IsSuperseded)) -and ($_.Title -match "32-bit") -and ($_.UpdateClassificationTitle -eq "Security Updates")} |            
       ForEach-Object -Process {            
      # View Office 2010 SP1 32bit            
      (Get-WsusServer).SearchUpdates("Office 2010") | ? UpdateClassificationTitle -eq "Service Packs" | ? Title -eq "Service Pack 1 for Microsoft Office 2010 (KB2510690) 32-bit Edition"            
      # Approve Office 2010 SP1            
      (Get-WsusServer).SearchUpdates("Office 2010") |             
      ? UpdateClassificationTitle -eq "Service Packs" |             
      ? Title -eq "Service Pack 1 for Microsoft Office 2010 (KB2510690) 32-bit Edition" |             
      ForEach-Object -Process {            
      # View other critical updates!            
      (Get-WsusServer).SearchUpdates("Windows 7") |            
      ? UpdateClassificationTitle -eq "Critical Updates" |            
      ft Title            
      # Approve other critical updates!            
      (Get-WsusServer).SearchUpdates("Windows 7") |            
      ? UpdateClassificationTitle -eq "Critical Updates" | ? Title -in @(            
      'Update for Windows 7 for x64-based Systems (KB2749655)',            
      'Update for Windows 7 for x64-based Systems (KB2718704)') |             
      ForEach-Object -Process {            

    • Step14: Configure a Group policy for client computers

    • As you see above, I chose to disable automatic updates.
      This has 2 major consequences:

      • I won’t get the selfupdate of the WUA (Windows Update Agent). The latest version 7.6.7600.256 won’t be installed automatically on client computers.
      • Non interactive scan will fail. In other words, the following well-known command cannot be used.
        %systemroot%\system32\wuauclt.exe /detectnow

      To workaround the second issue, client computers need to perform an interactive scan. When they perfom it the first time, computers got registered automatically in the default built-in ‘Unassigned computers’ group.

      As I have just configured a new computer group policy and loaded the Group Policy Management (GPMC) feature, I can make it apply immediately on all client computers like this:

      Get-ADComputer -Filter 'SamAccountName -LIKE "PC*"' |             
      ForEach-Object -Process {            
          Invoke-GPUpdate -RandomDelayInMinutes 0 -Force -Target Computer -Computer "$($_.Name).my.corp.fqdn"            
    • Step15: Assign computers to their group and make them apply udpates
    • As I said previously, to see computers being registered in the default groups on the WSUS server, client computers should perform an interactive scan.
      Once they do, they can be assigned to their custom target group so that they can install the specific updates I selected for installation.

      # Assign all unassigned computers to their group 'Windows 7 x64' group	            
      (Get-WsusServer).GetComputerTargetGroup('b73ca6ed-5727-47f3-84de-015e03f6a88a').GetComputerTargets() |            
       ForEach-Object -Process {            
          ((Get-WsusServer).GetComputerTargetGroups() | ? Name -Match "Windows 7").AddComputerTarget($_)            

      To force computers perform an interactive scan, register for the first time on the WSUS server and install all missing udpates, I execute the following script on all clients.

      #Requires -Version 2.0
      Function Install-WSUSUpdates {
      Begin {
          $UpdateSvc = New-Object -ComObject Microsoft.Update.ServiceManager
          # Make sure we have a WSUS by default
          if (-not($UpdateSvc.Services | ? { $_.ServiceID -eq '3da21691-e39d-4da6-8a4b-b43877bcb1b7'}).IsDefaultAUService) {
              Write-Warning -Message "WSUS server service not detected"
      Process {
          # Create a session
          $Session = New-Object -ComObject Microsoft.Update.Session
          # Search for updates
          $Searcher = $Session.CreateUpdateSearcher()
          $Criteria = "IsInstalled=0 and DeploymentAction='Installation' or IsPresent=1 and DeploymentAction='Uninstallation' or IsInstalled=1 and DeploymentAction='Installation' and RebootRequired=1 or IsInstalled=0 and DeploymentAction='Uninstallation' and RebootRequired=1"
          Write-Verbose -Message "Searching for updates"
          $SearchResult = $Searcher.Search($Criteria)
          if ($SearchResult.ResultCode -eq 2) {
              # Show us on console what was found
              $SearchResult.Updates | ft Title,AutoSelectOnWebSites
              # Create collections
              $updatesToDownload = New-Object -ComObject Microsoft.Update.UpdateColl
              $downloader = $Session.CreateUpdateDownloader()
              $updatesToInstall = New-Object -ComObject Microsoft.Update.UpdateColl
              # Download all detected updates
              $SearchResult.Updates | % {$updatesToDownload.Add($_)}
              $downloader.Updates = $updatesToDownload
              Write-Verbose -Message "Downloading Updates"
              $downresults = $downloader.Download()
              if ($downresults.ResultCode -eq 2) {
                  Write-Verbose -Message 'Successfully downloaded'
                  $updatesToDownload | Where { -not($_.isInstalled) -and $_.isDownloaded} | % {$updatesToInstall.Add($_)}
                  # Install updates
                  $installer = $Session.CreateUpdateInstaller()
                  $installer.Updates = $updatesToInstall
                  Write-Verbose -Message "Installing Updates"
                  $installationResult = $installer.Install()
                  if ($installationResult) {
              } else {
                  Write-Warning -Message "Failed to downlad udpates"
          } else {
              Write-Warning -Message "Failed to search for updates"
      End {}
      # Call our function in verbose mode
      Install-WSUSUpdates -Verbose

      Voilà 😎
      You are of course free to choose the best way to launch this script. I’m using scheduled tasks in my environment.
      The above script can also be modified to install only Windows 7 SP1 as some .Net updates make it fail with the following error:
      Installation Failure: Windows failed to install the following update with error 0x80070643: Windows 7 Service Pack 1 for x64-based Systems (KB976932).