I find it amazing how many Active Directory environments I review that do not have an ADMX Central Store set up. It’s been a best practice since the release of Windows Vista/2008 some 7 years ago now. What I find is that there tends to be ADMX sprawl across management servers and even the workstations of the IT Pros, which creates challenges when determining where to edit certain GPOs from. This is just down to lack of understanding and perhaps even laziness.
This PowerShell script will create the ADMX Central Store for you by copying the ADMX files from several source locations, such as a master source on an Administrative share and/or several management servers, including IT Pro workstations.
I use to do this via a batch script using xcopy, but the batch script needed some re-work before I was prepared to share it, so I took this opportunity to re-write it using PowerShell.
The script has 3 variables:
- $MasterReferenceLocation – This is the location where you may store your ADMX master files, or 3rd party ADMX files. If you use a relative path, the script will prepend the script path to create an absolute path.
- $languages – This is an array of languages you use so that we copy across the relevant ADML files, such as “en-us” for example. Setting this to an * (asterix) will copy the ADML files from ALL language folders.
- $SourceServers – This is an array of servers and workstations that you want to use to build the ADMX Central Store. They are typically the servers and workstations that contain the latest versions of ADMX files, as well as the customised and 3rd party ones you’re currently referencing in any GPOs.
The screen shot below shows the output of running the script for the first time, using a master reference location and one source server. You’ll note that I’ve joined together two screen shots, one from the start of the script, and the other from the end, as I didn’t see the need to show a further 320 files being copied.
The screen shot below shows the output of running the script again. This time adding more source servers. You’ll note that it only copies newer files.
The screen shot below shows the output of running the script yet again. This time you can see that there are no more files to be added from the source locations, confirming that the central store is complete, containing the newest ADMX files.
The following screen shot shows that it creates the PolicyDefinitions folder under the SYSVOL\<domainname>\Policies folder.
The following screen shot shows the contents of the PolicyDefinitions folder. You can see the ADMX files and the language folders that contain the ADML files.
The following screen shot shows that once you’ve got a central store in place; the GPMC will immediately use it for all ADMX files.
Here is the CreateADMXCentralStore.ps1 script:
<# This script will create your ADMX Central Store by using a master source and the local store on existing management servers. Script Name: CreateADMXCentralStore.ps1 Release 1.3 Modified by Jeremy@jhouseconsulting.com 23rd February 2014 Written by Jeremy@jhouseconsulting.com 14th February 2014 Notes: - I've found that some ADML files are more language generic. For Example: The OpsMgs (SCOM) HealthService.adml is located under the "EN" folder instead of the "en-us" folder. - I've found that some ADML files are accompanied by a dll. For Example: The OpsMgr (SCOM) HealthService.adml also has a HealthServiceADML.Dll. I've not been able to find any information on this, so have made sure this script copies across any existing dlls that accompany the ADML. ADMX Central Store references: - For further information refer to Managing Group Policy ADMX Files Step-by-Step Guide: http://msdn.microsoft.com/en-us/library/bb530196.aspx - How to create a Central Store for Group Policy Administrative Templates in Window Vista http://support.microsoft.com/kb/929841 Compare-Object cmdlet limitations: - The output of the compare-object cmdlet may be incorrect if you're comparing collections of more than 11 elements. To address this issue we set the SyncWindow parameter to half the size of the smaller object. http://dmitrysotnikov.wordpress.com/2008/06/06/compare-object-gotcha/ Copy-Item cmdlet limitations: - The Copy-Item cmdlet is quite limiting in its behavior. There is no "overwrite if newer", or "keep newest version" parameter. If the destination file exists, it will not be overwritten unless you use the -force paratemeter. So to work around this I've added a check to compare the lastwritetime property of the source and destination files to decide on which one is the newer file. Get-ChildItem cmdlet confusion: - The Include parameter is effective only when the command includes the "-recurse" parameter OR the path leads to the contents of a directory such as C:\Windows\* #> #------------------------------------------------------------- # Set this to the location where your ADMX master files are kept. # If you use a relative path, the script will prepend the script # path to create an absolute path. $MasterReferenceLocation = "ADMXCentralStore\Used" # Set array to the language so that we copy across the relevant # ADML files. Note that but setting this to an * (asterix), it # will copy the ADML files from all language folders. $languages = @("EN","en-us") # Set this to the servers that you want to use to build the ADMX # Central Store. They are typically the servers that contain the # latest versions of ADMX files, as well as the customised and # 3rd party ones you're currently using in any GPOs. $SourceServers = @("dc01","ctx01","adm01") #------------------------------------------------------------- # Get the current domain name $FQDN = [System.DirectoryServices.ActiveDirectory.Domain]::GetCurrentDomain().name If (!($MasterReferenceLocation.Contains(':\')) -AND !($MasterReferenceLocation.Contains('\\'))) { $ScriptPath = (Split-Path -Path ((Get-Variable -Name MyInvocation).Value).MyCommand.Path) If (!($MasterReferenceLocation.StartsWith('\'))) { $MasterReferenceLocation = $ScriptPath + "\" + $MasterReferenceLocation } Else { $MasterReferenceLocation = $ScriptPath + $MasterReferenceLocation } } # We can either prepend of append the $MasterReferenceLocation to the $SourceServers # array. If we append it, we should then reverse the array so that it's processed first. $SourceServers = ,$MasterReferenceLocation + $SourceServers #$SourceServers += $MasterReferenceLocation #[array]::Reverse($SourceServers) write-host -ForegroundColor green "`nCreating or adding to the ADMX Central Store..." [string]$t = "\\$FQDN\SYSVOL\$FQDN\Policies\PolicyDefinitions" If (-not(Test-Path -Path "$t")) { write-host -ForegroundColor green "`n`tCreating the '$t' folder..." New-Item -Path "$t" -ItemType Directory | out-Null } else { write-host -ForegroundColor yellow "`n`tThe '$t' folder already exists." } $target = Get-ChildItem $t | Where {$_.psIsContainer -eq $false} ForEach ($SourceServer in $SourceServers ) { If ($SourceServer.Contains('\')) { [string]$s = $SourceServer } else { If ($SourceServer -ne ($env:computername)) { [string]$s = "\\" + $SourceServer + "\admin$\PolicyDefinitions" } else { [string]$s = "$($env:systemroot)\PolicyDefinitions" } } write-host -ForegroundColor green "`n`tProcessing source files from $SourceServer..." If (Test-Path -Path $s) { $source = Get-ChildItem $s | Where {$_.psIsContainer -eq $false} If (($languages -eq "*") -OR ($languages -contains "*")) { $languages = @() $folders = Get-ChildItem $s | Where {$_.psIsContainer -eq $true} ForEach ($folder in $folders) { $languages += $folder.name If (-not(Test-Path -Path "$t\$($folder.name)")) { write-host -ForegroundColor green "`t- Creating the '$t\$($folder.name)' folder..." New-Item -Path "$t\$($folder.name)" -ItemType Directory | out-Null } else { write-host -ForegroundColor yellow "`t- The '$t\$($folder.name)' folder already exists." } } } else { ForEach ($language in $languages) { If (-not(Test-Path -Path "$t\$language")) { write-host -ForegroundColor green "`t- Creating the '$t\$language' folder..." New-Item -Path "$t\$language" -ItemType Directory | out-Null } else { #write-host -ForegroundColor yellow "`t- The '$t\$language' folder already exists." } } } # Set the SyncWindow to half the size of the smaller object $TargetCount = ($target | Measure-object).Count $SourceCount = ($source | Measure-object).Count If ($TargetCount -le $SourceCount) { $SyncWindow = $TargetCount / 2 } Else { $SyncWindow = $SourceCount / 2 } If ($SyncWindow -gt 5) { # Use the modulus operator to divide it by 2 to determine if it's an # odd or even number. An even number will not have a remainer of 0, # whilst an odd number has a remainder of 0.5, so we use the [int] # DataType to round it down to a A 32-bit signed whole number. If (($SyncWindow % 2) -ne 0) { $SyncWindow = [int]$SyncWindow } } Else { $SyncWindow = 5 } If ($TargetCount -eq 0) { # If there are no files in the target folder, the Compare-Object cmdlet # will fail with the following error: # Cannot bind argument to parameter 'DifferenceObject' because it is null. # To work around this issue we create a starter file, re-create the # target object and then delete the starter file. Now we have a difference # object that is not null. New-Item $t\StarterFile.txt -type file | out-Null $target = Get-ChildItem $t | Where {$_.psIsContainer -eq $false} Remove-Item $t\StarterFile.txt | out-Null } $results = @(Compare-Object -ReferenceObject $source -DifferenceObject $target -SyncWindow $SyncWindow |Where-Object { $_.SideIndicator -eq '<=' } ) If (($results | Measure-object).Count -ne 0) { write-host -ForegroundColor green "`t- Processing results from $SourceServer..." foreach($result in $results) { If (!($result.InputObject.PSIsContainer)) { #$SourceADMXFile = "$($result.InputObject.FullName)" $SourceADMXFile = "$($result.InputObject.DirectoryName)\$($result.InputObject.BaseName).admx" $ADMLFilePresent = $False ForEach ($language in $languages) { $SourceADMLFile = "$($result.InputObject.DirectoryName)\$language\$($result.InputObject.BaseName).adml" $SourceADMLLibraryFile = "$($result.InputObject.DirectoryName)\$language\$($result.InputObject.BaseName)ADML.dll" If (Test-Path -Path $SourceADMLFile) { $ADMLFilePresent = $True $DestinationADMLFile = "$t\$language\$($result.InputObject.BaseName).adml" $DestinationADMLLibraryFile = "$t\$language\$($result.InputObject.BaseName)ADML.dll" if (Test-Path -Path $DestinationADMLFile) { $SourceADMLFileTime = [datetime](Get-ItemProperty -Path $SourceADMLFile -Name LastWriteTime).lastwritetime $DestinationADMLFileTime = [datetime](Get-ItemProperty -Path $DestinationADMLFile -Name LastWriteTime).lastwritetime If ($SourceADMLFileTime -gt $DestinationADMLFileTime ) { write-host -ForegroundColor green "`t- Overwriting from source: $SourceADMLFile" copy-item "$SourceADMLFile" -destination "$t\$language" -force } else { write-host -ForegroundColor yellow "`t`t- Destination file is newer: $SourceADMLFile" } } else { write-host -ForegroundColor green "`t`t- Copying from source: $SourceADMLFile" copy-item "$SourceADMLFile" -destination "$t\$language" } # Copy a matching ADML library file if present. If ($ADMLFilePresent -AND (Test-Path -Path $SourceADMLLibraryFile)) { if (Test-Path -Path $DestinationADMLLibraryFile) { $SourceADMLLibraryFileTime = [datetime](Get-ItemProperty -Path $SourceADMLLibraryFile -Name LastWriteTime).lastwritetime $DestinationADMLLibraryFileTime = [datetime](Get-ItemProperty -Path $DestinationADMLLibraryFile -Name LastWriteTime).lastwritetime If ($SourceADMLLibraryFileTime -gt $DestinationADMLLibraryFileTime ) { write-host -ForegroundColor green "`t- Overwriting from source: $SourceADMLLibraryFile" copy-item "$SourceADMLLibraryFile" -destination "$t\$language" -force } else { write-host -ForegroundColor yellow "`t`t- Destination file is newer: $SourceADMLLibraryFile" } } else { write-host -ForegroundColor green "`t`t- Copying from source: $SourceADMLLibraryFile" copy-item "$SourceADMLLibraryFile" -destination "$t\$language" } } } } # Only copy the ADMX if an ADML is present. If ($ADMLFilePresent) { $DestinationADMXFile = "$t\$($result.InputObject.BaseName).admx" if (Test-Path -Path $DestinationADMXFile) { $SourceADMXFileTime = [datetime](Get-ItemProperty -Path $SourceADMXFile -Name LastWriteTime).lastwritetime $DestinationADMXFileTime = [datetime](Get-ItemProperty -Path $DestinationADMXFile -Name LastWriteTime).lastwritetime If ($SourceADMXFileTime -gt $DestinationADMXFileTime ) { write-host -ForegroundColor green "`t`t- Overwriting from source: $SourceADMXFile" copy-item "$SourceADMXFile" -destination "$t" -force } else { write-host -ForegroundColor yellow "`t`t- Destination file is newer: $SourceADMXFile" } } else { write-host -ForegroundColor green "`t`t- Copying from source: $SourceADMXFile" copy-item "$SourceADMXFile" -destination "$t" } } else { write-host -ForegroundColor yellow "`t- No matching ADML file was found for: $SourceADMXFile" } } } } else { write-host -ForegroundColor yellow "`t- No files to be added from $SourceServer." } } else { write-host -ForegroundColor red "`t- The $SourceServer location does not exist." } } write-host -ForegroundColor green "`nSummary:" $TotalADMX = (Get-ChildItem $t | Where {$_.psIsContainer -eq $false}| Measure-object).Count write-host -ForegroundColor green "- Total ADMX files in '$t': $TotalADMX" $folders = Get-ChildItem $t | Where {$_.psIsContainer -eq $true} ForEach ($folder in $folders) { $language = $folder.name $TotalADML = (Get-ChildItem "$t\$language\*" -include *.adml | Measure-object).Count If ($TotalADML -ne 0) { write-host -ForegroundColor green "- Total ADML files in '$t\$language': $TotalADML" $TotalADMLDLL = (Get-ChildItem "$t\$language\*" -include *adml.dll | Measure-object).Count If ($TotalADMLDLL -ne 0) { write-host -ForegroundColor green "- Total ADML dll files in '$t\$language': $TotalADMLDLL" } } } write-host -ForegroundColor green "`nFinished."
References:
- Microsoft MSDN Library: For further information refer to Managing Group Policy ADMX Files Step-by-Step Guide
- Microsoft TechNet Article: How to create a Central Store for Group Policy Administrative Templates in Window Vista
Enjoy!