Backing up Exchange Configurations, Pt. 2

Continuing the work on backing up Exchange configurations, I decided to simplify the script to back up the configurations to this:


$su = get-credential

$Target_Directory = 'C:\testfiles\Ex_ConfigExport'
$ExchangeServer = '006Exch-Srv1'

$SessionParams = 
   @{
     ConfigurationName = 'MicroSoft.Exchange'
     ConnectionURI     = "http://$ExchangeServer/powershell/"
     Authentication    = 'Kerberos'
     ErrorAction       = 'Stop'
     Credential        = $su
    }

$ExSession = New-PSSession @SessionParams

$Ex_Object_Folders = 
   @{
     Server =    @(
                    'MailboxServer',
                    'TransportServer',
                    'ClientAccessServer',
                    'UMServer'
                   )

     UM =        @(
                    'UMDialPlan'
                    'UMAutoAttendant'
                    'UMIPGateway'
                    'UMHuntGroup'
                   )

     Transport = @(
                    'TransportRule',
                    'JournalRule',
                    'SendConnector',
                    'ReceiveConnector',
                    'ForeignConnector',
                    'RoutingGroupConnector',
                    'AcceptedDomain',
                    'RemoteDomain'
                   )

     Database =   @(
                     'MailboxDatabase',
                     'PublicFolderDatabase',
                     'DatabaseAvailabilityGroup'
                   )

     Policy =     @(
                     'ActiveSyncMailboxPolicy',
                     'AddressBookPolicy',
                     'EmailAddressPolicy',
                     'ManagedFolderMailboxPolicy',
                     'OwaMailboxPolicy',
                     'RetentionPolicy',
                     'RetentionPolicyTag',
                     'RoleAssignmentPolicy',
                     'SharingPolicy',
                     'ThrottlingPolicy',
                     'UMMailboxPolicy'
                    )
    }
     
#Export set folder creation

$DirParams = @{
                ItemType    =  'Directory'
                Verbose     =  $true
                ErrorAction =  'Stop'
              }

Write-Verbose "Checking target root folder"
if ( -not ( Test-Path $Target_Directory ) )
  { New-Item  @DirParams -Path $Target_Directory }

$Export_Set = (get-date).tostring('yyyy-MM-dd_HH.mm.ss')

$Export_Set_Path = "$Target_Directory\$Export_Set"
Write-Verbose "Creating folder for this export set ($Export_Set)"
New-Item  @DirParams -Path $Export_Set_Path 

Write-Verbose "Exporting Exchange configuration objects"

foreach ($Ex_Object_Folder in $Ex_Object_Folders.keys)
   {
    $Ex_Object_Folder_Path = "$Export_Set_Path\$Ex_Object_Folder"
    New-Item @DirParams -Path $Ex_Object_Folder_Path

    foreach ( $Ex_Object_Type in $Ex_Object_Folders.$Ex_Object_Folder )
       {
        $Ex_Object_Type_Path = "$Ex_Object_Folder_Path\$Ex_Object_Type"
        New-Item @DirParams -Path $Ex_Object_Type_Path

        $SB = [ScriptBlock]::Create("Get-$Ex_Object_Type")
        Invoke-Command -ScriptBlock $SB -Session $ExSession |
          foreach {
             $_ | Export-Clixml "$Ex_Object_Type_Path\$($_.guid).clixml" -Verbose
           }
       }
   }
  Remove-PSSession $ExSession         
        

Again, right now this is in a testing phase and you’ll need to modify the server name and target directory, and you can remove the credential prompt and session parameter if it’s not necessary in your environment.

Running will create a set of exported configuration objects each time it’s run. Now I want to be able to compare one set to another to see what’s changed. This script will compare two of those configuration sets, and create objects that detail the objects and properties where it finds differences.



 <#
 .Synopsis
    Compare two export object sets
 .DESCRIPTION
    Compares deserialized objects from two directories of .clixml files.
 
 #>
     [CmdletBinding()]

     Param
     (
         # First export object set directory
         [Parameter(Mandatory=$true)]
         $ExportSetPath1,
 
         # Second export object set directory
         [Parameter(Mandatory=$true)]
         $ExportSetPath2
     )
 
     Begin
     {
       
      $CompareSetTimestamps = Get-Item $ExportSetPath1,$ExportSetPath2 |
       Select FullName,LastWriteTime |
       Sort LastWriteTime

      $ReferenceSetPath  = $CompareSetTimestamps[0].FullName
      $DifferenceSetPath = $CompareSetTimestamps[1].FullName

      $ExcludedProperties = @(
                              'PSShowComputerName',
                              'RunspaceId',
                              'OriginatingServer',
                              'WhenChanged'
                              )

      $ExcludeRegex = [regex]($ExcludedProperties -join '|')

     }
     Process
     {
     }
     End
     {
      $ReferenceSetFolders =
         Get-ChildItem $ReferenceSetPath -Directory

      Write-Verbose "Found $($ReferenceSetFolders.count) Export set folders"

      foreach ( $ReferenceSetFolder in $ReferenceSetFolders )
        { 
          Write-Verbose "***** Comparing $ReferenceSetFolder Objects*****`n"
 
          $ReferenceObjectTypeFolders =
           Get-ChildItem $ReferenceSetFolder.FullName -Directory

          foreach ( $ReferenceObjectTypeFolder in  $ReferenceObjectTypeFolders )
            {
              Write-Verbose "     *****Comparing  $ReferenceObjectTypeFolder Objects*****`n"
            
              $ReferenceObjectFiles = 
                Get-ChildItem $ReferenceObjectTypeFolder.FullName -File

              $DifferenceObjectTypeFolderPath = 
                       @(
                         $DifferenceSetPath,
                         $ReferenceSetFolder,
                         $ReferenceObjectTypeFolder
                         ) -join '\'

              $DifferenceObjectFiles = 
                Get-ChildItem $ReferenceObjectTypeFolder.FullName -File

              $AddedObjectFiles = 
                $DifferenceObjectFiles |
                Where { $ReferenceObjectFiles.name -notcontains $_.name }

              $DeletedObjectFiles = 
                $ReferenceObjectFiles |
                Where { $DifferenceObjectFiles.name -notcontains $_.name }

              $ComparedObjectFiles = 
              $ReferenceObjectFiles |
                Where { $DifferenceObjectFiles.name -Contains $_.name }

              foreach ( $ComparedObjectFile in $ComparedObjectFiles )
                {  
 
                  $DifferenceObjectFilePath = 
                    "$DifferenceObjectTypeFolderPath\$($ComparedObjectFile.Name)"

                  $DifferenceObject = Import-Clixml $DifferenceObjectFilePath

                  if ($DifferenceObject.WhenChanged -le $ComparedObjectFile.LastWriteTime)
                    { Continue }

                  Write-Verbose "   Change detected in $($ReferenceObjectTypeFolder.Name) $($DifferenceObject.Identity)"

                  $ReferenceObject = Import-Clixml $ComparedObjectFile.FullName

                  
                  Write-Debug "  Comparing properties of $($ReferenceObject.Identity)"
                  $Properties = 
                    $ReferenceObject.psobject.properties.name -notmatch $ExcludeRegex

                  foreach ( $Property in $Properties )
                    {
                      if ([string]$ReferenceObject.$Property -ne [string]$DifferenceObject.$Property)
                        {

                         Write-Verbose "    Found change in property $Property of $($ReferenceObjectTypeFolder.Name) $($ReferenceObject.Identity)`n"
                         Write-verbose "`n`nOld value = $($ReferenceObject.$Property)`n`nNew value = $($DifferenceObject.$Property)`n" 

                         [PSCustomObject]@{
                             Guid = $ReferenceObject.Guid
                             RefExportObjectPath = $ComparedObjectFile.FullName
                             DiffExportObjectPath = $DifferenceObjectFilePath
                             ObjectClass = $ReferenceObject.ObjectClass
                             Identity = $ReferenceObject.Identity
                             Property = $Property
                             RefWhenChanged = $ReferenceObject.WhenChanged
                             RefPropertyValue = $ReferenceObject.$Property
                             DiffWhenChanged = $DifferenceObject.WhenChanged
                             DiffPropertyValue = $DifferenceObject.$Property
                            }
                         
                        }

                  }#end property loop

              }#end object loop

           }#end  object type folder loop

      }#end export set folder loop

    }#end End block

For testing, I’m using this script to select the export sets to compare:


$Exportsets = Get-ChildItem C:\testfiles\Ex_ConfigExport -directory |
  select -expand fullname | 
  Out-GridView -OutputMode Multiple -Title "Select two export sets to compare."

If ($Exportsets.count -ne 2)
  {
    Write-Warning "Select two (and only two) export sets to compare"
    Return
  }

 $changes =  C:\scripts\Ex_Backup\Compare-ExportSets.ps1 $Exportsets[0] $Exportsets[1]

 $changes

I still need to address added and deleted objects between sets, but that’s relatively trivial compared to finding any changes in all the existing objects. This is not intended to provide a complete report, but to create a collection of objects containing the information necessary to produce it and provide enough information to easily investigate further. It’s also useful for doing ad-hoc comparisons of exports sets from arbitrary days.

In my environment it takes about 5.5 MB to save an export set to disk. These are plain-text files, and using NTFS compression on the folder will drop it down to about 3 MB, so for about 100MB of disk space I can keep about a month’s worth.

This is still obviously work in progress, and I welcome an comments or suggestions.

2 responses to “Backing up Exchange Configurations, Pt. 2

  1. This is as close as it gets to feature complete.
    Are you going to add a Restore-Config functionality down the line ?

Leave a comment