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

Technical Architect | DevOps Evangelist | Software Developer | Microsoft, NVIDIA, Citrix and Desktop Virtualisation (VDI) Specialist/Expert | Rapper | Improvisor | Comedian | Property Investor | Kayaking enthusiast at J House Consulting
Jeremy Saunders is the Problem Terminator. He is a highly respected IT Professional with over 35 years’ experience in the industry. Using his exceptional design and problem solving skills with precise methodologies applied at both technical and business levels he is always focused on achieving the best business outcomes. He worked as an independent consultant until September 2017, when he took up a full time role at BHP, one of the largest and most innovative global mining companies. With a diverse skill set, high ethical standards, and attention to detail, coupled with a friendly nature and great sense of humour, Jeremy aligns to industry and vendor best practices, which puts him amongst the leaders of his field. He is intensely passionate about solving technology problems for his organisation, their customers and the tech community, to improve the user experience, reliability and operational support. Views and IP shared on this site belong to Jeremy.
Jeremy Saunders
Jeremy Saunders

Previous post:

Next post: