Extending the new Invoke-GPUpdate cmdlet

One of the most wanted feature has been added to Windows 8 / Windows 2012: a way to force a remote group policy update.

Forget everything you find on this page: http://www.windowsecurity.com/articles/How-Force-Remote-Group-Policy-Processing.html

The Microsoft GPTeam already introducted this feature.
http://blogs.technet.com/b/grouppolicy/archive/2012/11/27/group-policy-in-windows-server-2012-using-remote-gpupdate.aspx

Cool, let’s give it a try: Force a Remote Group Policy Refresh (GPUpdate)

As mentioned in the help of the cmdlet, it works perfectly if you run in on Windows 2012 server, you can target Windows 7 and Windows 8 computers and if the following Firewall exception have been added to client computers

  • Remote Scheduled Tasks Management (RPC)
  • Remote Scheduled Tasks Management (RPC-ERMAP)
  • Windows Management Instrumentation (WMI-IN)

Source: Invoke-GPUpdate http://technet.microsoft.com/en-us/library/hh967455.aspx
Note: to use this cmdlet, you need to import the ‘GroupPolicy’ module that is available only if the ‘GPMC’ feature has been enabled on the server.

# Step 1: Add first the 'Group Policy Management' feature            
Add-WindowsFeature -Name  GPMC -Restart:$false            
# Step 2: Load the GPO module            
Import-module GroupPolicy

Wait, there are other limitations and gotchas.

  • When the gpupdate command runs, it’s not necessarily hidden
  • There are no ‘Credential’ parameter associated with the Invoke-GPUpdate cmdlet to pass alternate credentials. You need to run it with an account that has admin privileges on the target remote computers
  • This cmdlet doesn’t exist on a Windows 7 computer

Let’s have a look at what the original Invoke-GPUpdate cmdlet does behind the scene.
The Invoke-GPupdate cmdlet actually creates a scheduled task on the target remote computer that runs only once the gpupdate executable and deletes itself as it isn’t rescheduled.
GPUpdate task

As a proof of concept I’ve tried to achieve the same behavior as the original Invoke-GPUpdate cmdlet but only over PS remoting.
Here’s how:

#Requires -Version 2.0

Function Invoke-GPUpdateTask {
[cmdletbinding()]
Param(
           
    [parameter(mandatory=$false)]            
    [Switch]$Boot=$false,

     [Parameter(Position=0,ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true)]            
     [Alias("CN","__SERVER","IPAddress")]            
     [string[]]$ComputerName=$Env:Computername,            

    [parameter()]            
    [Alias('RunAs')]            
    [System.Management.Automation.Credential()]$Credential = [System.Management.Automation.PSCredential]::Empty,   
    
    [parameter(mandatory=$false)]            
    [ValidateSet('Sync','Force')]
    [String]$RefreshType=$null,

    [parameter(mandatory=$false)]            
    [Switch]$LogOff=$false,

    [parameter(mandatory=$false)]            
    [ValidateRange(0,44640)]            
    [int32]$RandomDelayInMinutes = 10,

    [parameter(mandatory=$false)]            
    [ValidateSet('User','Computer')]
    [System.String]$Target=$null
)
Begin {
    $HT = @{}
    if ($Credential) {
        $HT += @{Credential = $Credential}
    }
$str = @'
#Requires -Version 2.0

Function Add-GPUpdateTask {
Param(
    [parameter(mandatory=$false)]            
    [Switch]$LogOff,

    [parameter(mandatory=$false)]            
    [Switch]$Boot,

    [parameter(mandatory=$false)]            
    [ValidateSet('Sync','Force')]
    [String]$RefreshType=$null,

    [parameter(mandatory=$false)]            
    [ValidateRange(0,44640)]            
    [int]$RandomDelayInMinutes=10,
    
    [parameter(mandatory=$false)]            
    [ValidateSet('User','Computer')]
    [System.String]$Target
)

Begin {
    # Make sure we run as admin     
    # Creating tasks require admin privileges                               
    $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"                                    
        break                        
    }              
    if ([System.Environment]::OSVersion.Version -lt [system.version]'6.0') {
        Write-Warning 'Can only run this on Vista or later based computers'
        break
    }
    # Make sure the computer is a member of a domain
    if (-not(Get-WmiObject -Query 'Select * from Win32_ComputerSystem' -ErrorAction SilentlyContinue).PartOfDomain) {
        Write-Warning 'Can only run this on a computer member of ActiveDirectory domain'
        break
    }

    $now = (Get-Date).ToUniversalTime()
    # http://msdn.microsoft.com/en-us/library/az4se3k1.aspx
    # $StartDate = $now.GetDateTimeFormats('u') -replace "\s","T"
    if ($RandomDelayInMinutes -eq 0) {
        $TrigType = "RegistrationTrigger"
    } else {
        $TrigType = "TimeTrigger"
        $StartDate = @"
      <StartBoundary>$($now.ToString("yyyy-MM-ddTHH:mm:ssZ"))</StartBoundary>    
"@    
    }

    # Differ the end date by 90 seconds what they call the random offset in the cmdlet help documentation
    $EndDate = $now.AddSeconds((($RandomDelayInMinutes*60)+90)).ToString("yyyy-MM-ddTHH:mm:ssZ")

    # The modulus operator only works on numbers as well and returns the remainder from a division operation of the right to the left operand.
    if ($RandomDelayInMinutes -eq 0) {
        $RandomDelay = @"
      <Delay>PT0S</Delay>
"@        
    } else {
        Switch ($RandomDelayInMinutes) {
            {($_ % 1440) -eq 0} { $Delay1 = "P" + ($_/1440) + "D" ; break }
            {($_ % 60) -eq 0  } { $Delay1 = "PT" + ($_/60) + "H" ; break   }
            default { $Delay1 = "PT" + ($_*60) + 'S'}
        }
      $RandomDelay = @"
      <RandomDelay>$Delay1</RandomDelay>        
"@      
    }
    # Define a helper function that will fill-in the blanks in our here-string XML template
    Function Get-xmlTemplate {
    param(
    $StartDate,
    $EndDate,
    $Delay,
    $UserID,
    $IdleSettings,
    $Arguments,
    $TriggerType
    )
    # Define an XML template
    $xml= @"
<?xml version="1.0" encoding="UTF-16"?>
<Task version="1.2" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">
  <RegistrationInfo>
    <Author>SYSTEM</Author>
  </RegistrationInfo>
  <Triggers>
    <$TriggerType id="GPUpdate Trigger">
      $StartDate
      <EndBoundary>$EndDate</EndBoundary>
      <Enabled>true</Enabled>
      $Delay
    </$TriggerType>
  </Triggers>
  <Principals>
    <Principal id="Author">
      $UserIDxmlpart
      <RunLevel>LeastPrivilege</RunLevel>
    </Principal>
  </Principals>
  <Settings>
    <MultipleInstancesPolicy>IgnoreNew</MultipleInstancesPolicy>
    <DisallowStartIfOnBatteries>false</DisallowStartIfOnBatteries>
    <StopIfGoingOnBatteries>false</StopIfGoingOnBatteries>
    <AllowHardTerminate>true</AllowHardTerminate>
    <StartWhenAvailable>true</StartWhenAvailable>
    <RunOnlyIfNetworkAvailable>false</RunOnlyIfNetworkAvailable>
    <IdleSettings>
      $ComputerIdleSettings
      <StopOnIdleEnd>false</StopOnIdleEnd>
      <RestartOnIdle>false</RestartOnIdle>
    </IdleSettings>
    <AllowStartOnDemand>true</AllowStartOnDemand>
    <Enabled>true</Enabled>
    <Hidden>false</Hidden>
    <RunOnlyIfIdle>false</RunOnlyIfIdle>
    <WakeToRun>false</WakeToRun>
    <ExecutionTimeLimit>P3D</ExecutionTimeLimit>
    <DeleteExpiredTaskAfter>PT0S</DeleteExpiredTaskAfter>
    <Priority>6</Priority>
  </Settings>
  <Actions Context="Author">
    <Exec id="GPUpdate">
      <Command>gpupdate.exe</Command>
      <Arguments>$gpupdatearguments</Arguments>
    </Exec>
  </Actions>
</Task>
"@
    return $xml
    } # End of Function

    # Define a second helper function that will be responsible for creating a task
    Function Register-GPUpdateTask {
    Param(
        [string]$XmlTaskDefinition,
        $TaskName = 'GPUpdate-V2',
        $LogonType    
    )
    Begin {
        $TASK_CREATE_OR_UPDATE_FLAG = 0x6            
        $TaskService = New-Object -com schedule.service
        try {
            $TaskService.Connect() | Out-Null
        } catch {
            Write-Warning "Failed to initialize the schedule service COM object"
            return
        }
    } 
    Process {
        # http://msdn.microsoft.com/en-us/magazine/cc163350.aspx
       try {
            $folder = $TaskService.GetFolder('\Microsoft\Windows\GroupPolicy')
        } catch {
            try {
                $folder = $TaskService.GetFolder('\').CreateFolder('\Microsoft\Windows\GroupPolicy',"D:(A;;FA;;;BA)(A;;FA;;;SY)")
            } catch {
                Write-Warning "Failed to create task folder \Microsoft\Windows\GroupPolicy because $($_.Exception.Message)"
            }
        }
        if ($folder) {
            try {
                $taskdefinition = (New-Object -ComObject schedule.service).NewTask($null)
                $taskdefinition.xmlText = $XmlTaskDefinition
                # Create the task
                $folder.RegisterTaskDefinition($TaskName,$taskdefinition,$TASK_CREATE_OR_UPDATE_FLAG,$null,$null,$LogonType) |Out-Null
            } catch {
                Write-Warning "Failed to register task because $($_.Exception.Message)"
            }
        }
    } 
    End {}
    } # End of function
   
}
Process {
    # Define an array to add strings based on the main function parameters
    $commandargs = @()
    if ($Boot)   { $commandargs += '/Boot'   }
    if ($LogOff) { $commandargs += '/LogOff' }
    if ($RefreshType) { $commandargs += "/$($RefreshType)"}
    Switch ($Target) {
        User {
            $commandargs += '/Target:User'
            # To get the currenly logged on users accounts we lock at the owner of the explorer.exe
            $explorerprocesses = @(Get-WmiObject -Query "Select * FROM Win32_Process WHERE Name='explorer.exe'" -ErrorAction SilentlyContinue)
            # http://msdn.microsoft.com/en-us/library/windows/desktop/aa394102%28v=vs.85%29.aspx
            # The Domain property can be a FQDN
            $Domain = ((Get-WmiObject -Query 'Select * from Win32_ComputerSystem' -ErrorAction SilentlyContinue).Domain -split "\.")[0]
            $loggedonUsers = @()
            if ($explorerprocesses.Count -eq 0)
            {
               # No user logged on
            } else {
                $explorerprocesses | ForEach-Object -Process {
                    # Avoid invoke-GPUpdate on local accounts
                    if ($_.GetOwner().Domain -imatch $Domain) {
                        $loggedonUsers += $_.GetOwner().Domain + "\" + $_.GetOwner().User
                    }
                }
            }
            
            # Concatenate
            $gpupdatearguments = [string]::Join(" ",$commandargs)
          
            # Run a refresh for all domain users
            $loggedonUsers | ForEach-Object -Process {
                $UserIDxmlpart = @"
                      <UserId>$_</UserId>
                      <LogonType>InteractiveToken</LogonType>
"@

                # Fill-in the XML template
                if ($StartDate -ne  $null) {                
                    $xmldef = Get-xmlTemplate -StartDate $StartDate -EndDate $EndDate -Delay $RandomDelay -UserID $UserIDxmlpart  -Arguments $gpupdatearguments -TriggerType $TrigType
                } else {
                    $xmldef = Get-xmlTemplate -EndDate $EndDate -Delay $RandomDelay -UserID $UserIDxmlpart  -Arguments $gpupdatearguments -TriggerType $TrigType
                }
                $Name =  ($_ -split "\\")[1] + '@' + ($_ -split "\\")[0]
                Register-GPUpdateTask -XmlTaskDefinition $xmldef.ToString() -TaskName "GPUpdate-V2 ($Name)" -LogonType 3
            } # End of foreach
        }
        Computer {
            $commandargs += '/Target:Computer'
            $ComputerIdleSettings = @"
          <Duration>PT10M</Duration>
          <WaitTimeout>PT1H</WaitTimeout>
"@
            $UserIDxmlpart = @"
          <UserId>S-1-5-20</UserId>
"@
        # Concatenate
        $gpupdatearguments = [string]::Join(" ",$commandargs)
        
        # Fill-in the XML template
        if ($StartDate -ne $null) {                        
            $xmldef = Get-xmlTemplate -StartDate $StartDate -EndDate $EndDate -Delay $RandomDelay -UserID $UserIDxmlpart -IdleSettings $ComputerIdleSettings -Arguments $gpupdatearguments -TriggerType $TrigType
        } else {
            $xmldef = Get-xmlTemplate -EndDate $EndDate -Delay $RandomDelay -UserID $UserIDxmlpart -IdleSettings $ComputerIdleSettings -Arguments $gpupdatearguments -TriggerType $TrigType
        }
        # Register the task
        Register-GPUpdateTask -XmlTaskDefinition $xmldef.ToString() -LogonType 5
        }
        Default {
            # Target wasn't specified -> do both Computer and User with a recall of the function
            Add-GPUpdateTask -Target Computer @PSBoundParameters
            Add-GPUpdateTask -Target User @PSBoundParameters
        }
    }


}
End {}
}

'@

} 
Process {
        # Loop for each computername object
        $ComputerName | ForEach-Object -Process {
            $Computer  = $_
            # Based on function parameters, add strings to the array
            $commandargs = @()
            if ($Boot)   { $commandargs += '-Boot'    }
            if ($LogOff) { $commandargs += '-LogOff'  }
            if ($RefreshType) { $commandargs += "-RefreshType $($RefreshType)"}
            if ($Target) { $commandargs += "-Target $($Target)"}
            $commandargs += "-RandomDelayInMinutes $($RandomDelayInMinutes)"
            # Create a second string that contains our one-liner
            $newstr = "Add-GPUpdateTask " + [string]::Join(" ",$commandargs)  
            $sb = [scriptblock]::Create(($str + $newstr))
            try {
                Invoke-Command -ComputerName $Computer -ScriptBlock $sb -ErrorAction Stop @HT
            } catch {
                Write-Warning -Message "Failed to invoke command on remote computer $Computer because $($_.Exception.Message)"
            }
        }
}
End {}

<#
    .SYNOPSIS   
        Refresh Group Policies of remote computers and users
             
    .DESCRIPTION   
        Schedule a remote Group Policy refresh (gpupdate) on the specified computer.
        
    .PARAMETER Computername
            Specifies the name of the computer for which to schedule a Group Policy refresh.
            If the computer name is not specified the computer, on which the Invoke-GPUpdate cmdlet was run, will have the Group Policy settings refreshed.

    .PARAMETER Credential
        Alternate credentials to use to access remote computers
        
    .PARAMETER Boot
        Causes a computer restart after the Group Policy settings are applied. This is required for those Group Policy client side extensions (CSEs) that do not process Group Policy on a background update cycle, but do process Group Policy at computer startup, for example, per-computer Software Installation policy settings.
        This parameter has no effect if there are no CSEs called that require a restart.        
    
    .PARAMETER RefreshType
        Can only be set to 'Force','Sync' or null.
        'Force' reapplies all policy settings. By default, Group Policy is only refreshed when policy settings have changed.       
        'Sync' causes the next foreground Group Policy application to be done synchronously. Foreground Group Policy applications occur at computer startup and user logon. You can specify this for the user, computer or both using the Target parameter.
        On a client computer, by default, Group Policy processes synchronously at computer startup and asynchronously at user logon.
        On a server, by default Group Policy processes synchronously at computer startup and at user logon.

    .PARAMETER Target
        Specifies that only user or computer policy settings are refreshed. 
        By default, both user and computer policy settings are refreshed. You can specify one of two allowable values for this parameter:
        -- User
        -- Computer
        If the target parameter is not specified both user and computer policy settings will be refreshed.

    .PARAMETER LogOff
        Causes a logoff after the policy settings have been updated. 
        This is required for those Group Policy client-side extensions (CSEs) that do not process Group Policy on a background update cycle but do process Group Policy when a user logs on. Examples include per-user Software Installation policy settings and the Folder Redirection extension.
        This parameter has no effect if there are no CSEs called that require a logoff.

    .PARAMETER RandomDelayInMinutes
        Specifies the delay, in minutes that Task Scheduler will wait, with a random factor added to lower the network load, before running a scheduled Group Policy refresh.
        You can specify a delay in from 0 minutes to a maximum of 44640 minutes (31 days):
        -- A value of 0 will cause the Group Policy refresh to run as soon as the gpupdate task has been scheduled.
        —A value in the range of 1 to the maximum value of 44640 minutes cause the Group Policy refresh to delay the specified number of minutes plus a random offset before starting the Group Policy refresh. 

    .EXAMPLE 
        Invoke-GPUpdateTask -RefreshType Force
                     
        Description 
        ----------- 
        Forces a refresh of the computer policies as well as user policies for any logged on domain user

    .EXAMPLE
        $Workstations = Get-Content Workstations.txt
        $Workstations | Invoke-GPUpdateTask -RefreshType Force -Target Computer
            
        Description
        -----------
        Forces a refresh of the computer policies on a collection of workstations
            
    .EXAMPLE
        (Get-Content Workstations.txt) | Invoke-GPUpdateTask -RefreshType Force -Target User -LogOff -Credential (Get-Credential)
            
        Description
        -----------
        Forces a refresh of the user policies on a collection of workstations. Also uses alternate administrator credentials provided.                                            
        Logs off the users only if a client-side extensions (CSEs) require it

#>

}

I’ve seen some issues with the task scheduler engine:

  • The XML property DeleteExpiredTaskAfter set to 30 seconds not being honored for example.
  • The task engine reaches the end date, says “Task Scheduler launched “{00000000-0000-0000-0000-000000000000}” instance of task “\Microsoft\Windows\GroupPolicy\GPUpdate-V2″ due to a time trigger condition.” and then just deletes the task w/o launching the gpupdate process. Weird!
  • This MSDN page says

    The default value is 7. The minimum and maximum values are set by the priorityType simple type. Priority levels 7 and 8 are used for background tasks, and priority levels 4, 5, and 6 are used for interactive tasks.

    For both a computer and a user GPUpdate task, the priority is always 6.

Currently the above PoC code runs w/o any issue on a Windows 7 box when I do:

# Do both a computer and user refresh            
Invoke-GPUpdateTask -Computer RemotePC -RefreshType Force -RandomDelayInMinutes 0            
            
# Do only a computer refresh            
Invoke-GPUpdateTask -Computer RemotePC -Target Computer -RefreshType Force -RandomDelayInMinutes 0

Voilà 😎 Far from being perfect … for version 1.0

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s