The GroupPolicy module from Microsoft offers a great deal of functionality from a command line. In terms of regular maintenance or administration it is pretty hard to beat, especially if you have 100s or 1000s of GPOs. When you have such a large number, backing them up is critical and easy to accomplish with the Backup-GPO cmdlet. However, if you want to restore or import from a backup, you have to resort to the graphical Group Policy Managment Console. I couldn't find any way to manage GPO backups from PowerShell so I wrote my own.
ManageEngine ADManager Plus - Download Free Trial
Exclusive offer on ADManager Plus for US and UK regions. Claim now!
My function, Get-GPOBackup, will search a GPO backup folder and retrieve backup objects. All of this is done using the file system. Technically you don't need the Group Policy module to retrieve the backup information. However, if you want to restore anything then you of course need the Group Policy module loaded. The only parameter you need to specify is the path. Although the function is designed to use the value of a global variable, $GPBackupPath. So if you use this alot, set that variable. But perhaps before we get too far you'd like to see the function.
Function Get-GPOBackup { [cmdletbinding()] Param( [Parameter(Position=0,Mandatory=$False,HelpMessage="What is the path to the GPO backup folder?")] [ValidateNotNullOrEmpty()] [string]$Path=$global:GPBackupPath, [Parameter(Position=1)] [string]$Name, [switch]$Latest ) #validate $Path if (-Not $Path) { $Path=Read-Host "What is the path to the GPO backup folder?" } Try { Write-Verbose "Validating $Path" if (-Not (Test-Path $Path)) { Throw } } Catch { Write-Warning "Failed to find $Path" Break } #get each folder that looks like a GUID [regex]$regex="^(\{){0,1}[0-9a-fA-F]{8}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{12}(\}){0,1}$" Write-Verbose "Enumerating folders under $Path" #define an array to hold each backup object $Backups=@() #find all folders named with a GUID Get-ChildItem -Path $path | Where {$_.name -Match $regex -AND $_.PSIsContainer} | foreach { #import the Bkupinfo.xml file $file=Join-Path $_.FullName -ChildPath "bkUpinfo.xml" Write-Verbose "Importing $file" [xml]$data=Get-Content -Path $file #parse the xml file for data $GPO=$data.BackupInst.GPODisplayName."#cdata-section" $GPOGuid=$data.BackupInst.GPOGuid."#cdata-section" $ID=$data.BackupInst.ID."#cdata-section" $Comment=$data.BackupInst.comment."#cdata-section" #convert backup time to a date time object [datetime]$Backup=$data.BackupInst.BackupTime."#cdata-section" $Domain=$data.BackupInst.GPODomain."#cdata-section" #write a custom object to the pipeline $Backups+=New-Object -TypeName PSObject -Property @{ Name=$GPO Comment=$Comment #strip off the {} from the Backup ID GUID BackupID=$ID.Replace("{","").Replace("}","") #strip off the {} from the GPO GUID Guid=$GPOGuid.Replace("{","").Replace("}","") Backup=$Backup Domain=$Domain Path=$Path } } #foreach #if searching by GPO name, then filter and get just those GPOs if ($Name) { Write-Verbose "Filtering for GPO: $Name" $Backups=$Backups | where {$_.Name -like $Name} } Write-Verbose "Found $($Backups.Count) GPO Backups" #if -Latest then only write out the most current version of each GPO if ($Latest) { Write-Verbose "Getting Latest Backups" $grouped=$Backups | Sort-Object -Property GUID | Group-Object -Property GUID $grouped | Foreach { $_.Group | Sort-Object -Property Backup | Select-Object -Last 1 } } else { $Backups } Write-Verbose "Ending function" } #end function
The complete function has comment based help.
As I said, the -Path parameter is key. It is not mandatory in the attribute-sense, but I have code that prompts for a missing value and the path is tested before anything else is done.
#validate $Path if (-Not $Path) { $Path=Read-Host "What is the path to the GPO backup folder?" } Try { Write-Verbose "Validating $Path" if (-Not (Test-Path $Path)) { Throw } } Catch { Write-Warning "Failed to find $Path" Break }
GPO backups are stored under a GUID for each GPO, so I use a regular expression pattern to identify these. I'm making an assumption that the path you specify won't have any other folders with GUID-based names.
#get each folder that looks like a GUID [regex]$regex="^(\{){0,1}[0-9a-fA-F]{8}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{12}(\}){0,1}$"
Each backup consists of a few XML files. My function parses the XML and creates a custom object for each backup. These are added to a temporary array.
#define an array to hold each backup object $Backups=@() #find all folders named with a GUID Get-ChildItem -Path $path | Where {$_.name -Match $regex -AND $_.PSIsContainer} | foreach { #import the Bkupinfo.xml file $file=Join-Path $_.FullName -ChildPath "bkUpinfo.xml" Write-Verbose "Importing $file" [xml]$data=Get-Content -Path $file #parse the xml file for data $GPO=$data.BackupInst.GPODisplayName."#cdata-section" $GPOGuid=$data.BackupInst.GPOGuid."#cdata-section" $ID=$data.BackupInst.ID."#cdata-section" $Comment=$data.BackupInst.comment."#cdata-section" #convert backup time to a date time object [datetime]$Backup=$data.BackupInst.BackupTime."#cdata-section" $Domain=$data.BackupInst.GPODomain."#cdata-section" #write a custom object to the pipeline and add to the temporary array $Backups+=New-Object -TypeName PSObject -Property @{ Name=$GPO Comment=$Comment #strip off the {} from the Backup ID GUID BackupID=$ID.Replace("{","").Replace("}","") #strip off the {} from the GPO GUID Guid=$GPOGuid.Replace("{","").Replace("}","") Backup=$Backup Domain=$Domain Path=$Path } } #foreach
Now for the really fun part. The function supports a few other parameters that allow you to search for a GPO by name. So if you specify -Name, everything else is filtered out.
#if searching by GPO name, then filter and get just those GPOs if ($Name) { Write-Verbose "Filtering for GPO: $Name" $Backups=$Backups | where {$_.Name -like $Name} }
I also include a parameter to get the latest backup. I accomplish this by grouping the backup objects by their GPO GUID, sorting by backup date and selecting the latest one.
#if -Latest then only write out the most current version of each GPO if ($Latest) { Write-Verbose "Getting Latest Backups" $grouped=$Backups | Sort-Object -Property GUID | Group-Object -Property GUID $grouped | Foreach { $_.Group | Sort-Object -Property Backup | Select-Object -Last 1 } }
You should end up with an object like this for each GPO backup.
Backup : 5/2/2011 8:10:13 PM Name : WinRM Configuration BackupID : 5AAFD6DF-24C0-40EF-9DF1-AC9C279E010B Path : \\server01\backup\gpo Domain : jdhlab.local Guid : 38228a9f-f2ec-4b48-8829-4f3131e4f77c Comment : second backup
I tried to create an object so that if you wanted to restore a GPO you could with minimal effort. Unfortunately, the Import-GPO cmdlet doesn't have enough support for parameter binding. So if you want to restore a backup, you'll need to resort to a ForEach-Object command like this:
PS C:\> Get-GPOBackup \\coredc01\backup\gpo -name "Ex*Laptop" -latest | foreach {Import-GPO -TargetGuid $_.Guid -backupid $_.backupID -path $_.path} DisplayName : Executive Laptop DomainName : jdhlab.local Owner : JDHLAB\Domain Admins Id : af6f4a88-5e8a-49ce-84f7-f04677a7d80d GpoStatus : AllSettingsEnabled Description : configure C-level laptops CreationTime : 9/8/2010 8:36:10 AM ModificationTime : 5/23/2011 9:52:43 AM UserVersion : AD Version: 2, SysVol Version: 2 ComputerVersion : AD Version: 9, SysVol Version: 9 WmiFilter :
My function is in a stand-alone script so to use it, you either need to copy and paste the function into your profile or dot source the script. Even though I wrote it to help with imports, you could easily use it to manage a backup folder, deleting obsolete backups. The function is writing an object to the pipeline that you can use in whatever manner you can think of. I hope you'll let me know what you think and if there is anything you think is missing.
Download Get-GPOBackup.