Script to Create the ADMX Central Store

by Jeremy Saunders on February 25, 2014

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.

CreateADMXCentralStore - 1st Run Script Output

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.

CreateADMXCentralStore - 2nd Run Script Output

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.

CreateADMXCentralStore - 3rd Run Script Output

The following screen shot shows that it creates the PolicyDefinitions folder under the SYSVOL\<domainname>\Policies folder. 

SYSVOL - 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.

SYSVOL - Policies - PolicyDefinitions Folder

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.

GPO - retrieving administrative templates from central store

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:

Enjoy!

Jeremy Saunders

Jeremy Saunders

Independent Consultant | Contractor | Microsoft & Citrix Specialist | Desktop Virtualization Specialist at J House Consulting
Jeremy is a highly respected, IT Professional, with over 30 years’ experience in the industry. He is an independent IT consultant providing expertise to enterprise, corporate, higher education and government clients. His skill set, high ethical standards, integrity, morals and attention to detail, coupled with his friendly nature and exceptional design and problem solving skills, makes him one of the most highly respected and sought after Microsoft and Citrix technical resources in Australia. His alignment with industry and vendor best practices puts him amongst the leaders of his field.
Jeremy Saunders
Jeremy Saunders
Jeremy Saunders
  • CarlWebster

    Do the existing ADM, ADMX files that exist in SYSVOL need to be removed once the Central Store is created and populated?

    For reference: http://blogs.technet.com/b/askpfeplat/archive/2011/12/12/how-to-implement-the-central-store-for-group-policy-admin-templates-completely-hint-remove-those-adm-files.aspx

    • Hi Webster,

      There will be no existing ADMX files in SYSVOL. If you don’t have a Central Store they will only be referenced from the local C:WindowsPolicyDefinitions folder where you run the GPMC from.

      As for ADM files…absolutely not. The script you referenced is good from a reporting point of view, but how do you know that the contents of an ADMX file are going to provide legacy settings that are in an old style ADM file, and therefore not break or interfere with your GPO settings if you were to edit a GPO after the change? Whilst you may be able to follow that script to help you remove the default Microsoft ADM files, I would proceed with caution, and have therefore not documented or included that in this article and script. This script is all about creation of a Central Store and does not interfere with existing GPO settings, as they are the ADMX templates already being referenced. There may also be some ADM files that you want to keep or have not been replaced with ADMX files, or you want to turn into GPPs. There is no wham bam thank you ma’am script that’s going to do that for you. The “safe” clean-up/removal of ADM files is for another article 🙂

      Cheers,

      Jeremy

  • Pingback: Create the Central Store in AD using Powershell | Chris Spanougakis MVP Technology Issues()

  • Pingback: Active Directory Health Check, Audit and Remediation Scripts()

Previous post:

Next post: