Functions/Sync-Entities.ps1
<#
.SYNOPSIS This function performs a sync of two lists of entities. .DESCRIPTION This function performs a sync of two lists of entities. This is a generic sync framework which can be used to sync most types of entities, depending on the script blocks which are provided. #> function Sync-Entities { [CmdletBinding(PositionalBinding=$false)] param ( # The entities which are currently present. [Parameter(Mandatory=$true)] [AllowEmptyCollection()] [Object[]]$current, # The entities which are expected to be present. [Parameter(Mandatory=$true)] [AllowEmptyCollection()] [Object[]]$expected, # The type of entity that is to be synced. # This identity is used in information, warning and error messages. [Parameter(Mandatory=$true)] [ValidateNotNullOrEmpty()] [String]$entityType, # The property of the entity to use as the key. # The value of this property must be not $null, and must be unique across all entities. [Parameter(Mandatory=$true)] [ValidateNotNullOrEmpty()] [String]$entityKeyProperty, # The name of the entity property used as its identity. # This identity is used in information, warning and error messages, and can be # different from $entityKeyProperty. [Parameter(Mandatory=$true)] [ValidateNotNullOrEmpty()] [String]$entityIdentityProperty, # Select the return type of this function. # This function can return the errors which occurred, which will be null if no errors occurred. # This function can also return the true/false result of whether the sync was successful. [Parameter(Mandatory=$false)] [ValidateSet("Errors", "Result")] [String]$returnType = "Result", # Select whether to create entities as part of the sync. [Parameter(Mandatory=$false)] [Boolean]$create = $false, # Select whether to update entities as part of the sync. [Parameter(Mandatory=$false)] [Boolean]$update = $false, # Select whether to delete/archive entities as part of the sync. [Parameter(Mandatory=$false)] [Boolean]$delete = $false, # Select whether the delete action deletes/archives entities. [Parameter(Mandatory=$false)] [ValidateSet("Archive", "Delete")] [String]$deleteAction = "Delete", # The script block used to un-archive an archived entity, if it exists. # Return values: # It should return "success" only if an archived entity was found as well as # successfully un-archived. # If no archived entity was found, return nothing. # Error handling: # This expression should throw an exception to indicate an error. [Parameter(Mandatory=$false)] [ValidateNotNull()] [ScriptBlock]$unArchiveEntity, # The script block used to create an entity. # Return values: # The script block should return "success" only if the entity was successfully created. # Error handling: # This expression should throw an exception to indicate an error. [Parameter(Mandatory=$true)] [ValidateNotNull()] [ScriptBlock]$createEntity, # The script block used to compare two entities for equality. # Return values: # $true if the two entities are equal, $false or $null otherwise. # Error handling: # None required. [Parameter(Mandatory=$true)] [ValidateNotNull()] [ScriptBlock]$compareEntities, # The expression used to update the properties of an entity. # Return values: # The script block should return "success" only if the entity was successfully updated. # Error handling: # This expression should throw an exception to indicate an error. [Parameter(Mandatory=$true)] [ValidateNotNull()] [ScriptBlock]$updateEntity, # The script block used to archive an entity. # Return values: # The script block should return "success" only if the entity was successfully archived. # Error handling: # This expression should throw an exception to indicate an error. [Parameter(Mandatory=$false)] [ValidateNotNull()] [ScriptBlock]$archiveEntity, # The script block used to delete an entity. # Return values: # The script block should return "success" only if the entity was successfully deleted. # Error handling: # This expression should throw an exception to indicate an error. [Parameter(Mandatory=$false)] [ValidateNotNull()] [ScriptBlock]$deleteEntity, # A list containing the objects which will be passed to the script blocks # as input parameters. [Parameter(Mandatory=$false)] [ValidateScript({ $_.length -gt 0 })] [Object[]]$parameterObjects, # A list of the names for the objects which will be passed to the script blocks # as input parameters. # The order of this list needs to correspond to the order of $parameterObjects. # These names do not need to be the same as the names of the objects passed to # $parameterObjects, as these are the names used exclusively in the # script blocks to refer to those objects. [Parameter(Mandatory=$false)] [ValidateScript({ $_.length -gt 0 -and ($_ | Where-Object { $_.GetType() -eq [String] }) -eq $_ })] [Object[]]$parameterNames ) # This function adds additional lines to the beginning of a script block # in order for it to be able to accept input parameters from this function. function Add-ScriptBlockInputParameters { param ( # The script block to update. [Parameter(Mandatory=$true)] [ValidateNotNull()] [ScriptBlock]$scriptBlock ) # Generate the code for the script block parameters $scriptBlockParametersCode = "param(" # Add the parameters if ($parameterNames) { foreach ($parameter in $parameterNames) { $scriptBlockParametersCode += "`$$($parameter)," } } # Add the entity as the last parameter $scriptBlockParametersCode += "`$entity)" # Add to script block and return Write-Verbose "Adding parameters $($scriptBlockParametersCode) to the script block" [ScriptBlock]::Create($scriptBlockParametersCode + $scriptBlock.ToString()) } # Create variable to store error messages $errorMessages = "" # Execute sync try { # Validate input parameters Write-Verbose "Validating `$parameterNames and `$parameterObjects." if ($parameterNames.length -ne $parameterObjects.length) { $errorMessages += "Length of `$parameterNames $($parameterNames.length) and `$parameterObjects $($parameterObjects.length) do not match." Write-Error $errorMessages throw $errorMessages } Write-Verbose "Validating parameter combinations for 'delete'." if ($delete) { if ($deleteAction -eq "Delete" -and [String]::IsNullOrWhiteSpace($deleteEntity)) { $errorMessages += "Delete is enabled and delete action is selected but expression to delete an entity has not been provided." Write-Error $errorMessages throw $errorMessages } elseif ($deleteAction -eq "Archive" -and [String]::IsNullOrWhiteSpace($archiveEntity)) { $errorMessages += "Delete is enabled and archive action is selected but expression to archive an entity has not been provided." Write-Error $errorMessages throw $errorMessages } } # Create hash table to help with syncing entities $entitiesHashTable = New-MergedObjectHashTable -CurrentObjects $current -ExpectedObjects $expected -KeyProperty $entityKeyProperty if ($null -eq $entitiesHashTable) { $errorMessages += "Failed to create merged hash table for syncing." Write-Error $errorMessages throw $errorMessages } # Run through hash table to sync entities $index = 0 foreach ($entity in $entitiesHashTable.Values) { # Provide updates on progress $index += 1 $syncProgress = "$($entityType) $($index)/$($entitiesHashTable.Count)" # Entity is to be created/un-archived if ($create -and !$entity.Current -and $entity.Expected) { # Retrieve the entity identity $entityIdentity = $entity.Expected.$entityIdentityProperty Write-Information "$($syncProgress): Creating/un-archiving $($entityType) '$($entityIdentity)'." # Un-archive entity if script block is provided if ($unArchiveEntity) { # Try to un-archive entity try { $scriptBlock = Add-ScriptBlockInputParameters $unArchiveEntity $argumentList = $parameterObjects + @($entity) $unArchiveResult = Invoke-Command -ScriptBlock $scriptBlock -ArgumentList $argumentList } catch { $errorMessages += "$($syncProgress): Error while un-archiving $($entityType) '$($entityIdentity)'. `r`n$($_.Exception.Message)`r`n" Write-Error $errorMessages continue } # Un-archive was successful if ($unArchiveResult -eq "success") { Write-Verbose "$($syncProgress): Un-archived $($entityType) '$($entityIdentity)'." continue } } # Try to create entity try { $scriptBlock = Add-ScriptBlockInputParameters $createEntity $argumentList = $parameterObjects + @($entity) $createResult = Invoke-Command -ScriptBlock $scriptBlock -ArgumentList $argumentList } catch { $errorMessages += "$($syncProgress): Error while creating $($entityType) '$($entityIdentity)'. `r`n$($_.Exception.Message)`r`n" Write-Error $errorMessages continue } # Create was successful if ($createResult -eq "success") { Write-Verbose "$($syncProgress): Created $($entityType) '$($entityIdentity)'." } else { Write-Error "$($syncProgress): Failed to create $($entityType) '$($entityIdentity)'." } } # Entity is to be updated elseif ($update -and $entity.Current -and $entity.Expected) { # Retrieve the entity identity $entityIdentity = $entity.Current.$entityIdentityProperty # Only update entity if it is different from the expected entity $scriptBlock = Add-ScriptBlockInputParameters $compareEntities $argumentList = $parameterObjects + @($entity) $comparisonResult = Invoke-Command -ScriptBlock $scriptBlock -ArgumentList $argumentList if ($comparisonResult) { Write-Information "$($syncProgress): $($entityType) '$($entityIdentity)' is up to date." continue } # Continue with updating the entity Write-Information "$($syncProgress): Updating $($entityType) '$($entityIdentity)'." # Try to update the entity try { $scriptBlock = Add-ScriptBlockInputParameters $updateEntity $argumentList = $parameterObjects + @($entity) $updateResult = Invoke-Command -ScriptBlock $scriptBlock -ArgumentList $argumentList } catch { $errorMessages += "$($syncProgress): Error while updating $($entityType) '$($entityIdentity)'. `r`n$($_.Exception.Message)`r`n" Write-Error $errorMessages continue } # Update was successful if ($updateResult -eq "success") { Write-Verbose "$($syncProgress): Updated $($entityType) '$($entityIdentity)'." } else { Write-Error "$($syncProgress): Failed to update $($entityType) '$($entityIdentity)'." } } # Entity is to be archived/deleted elseif ($delete -and $entity.Current -and !$entity.Expected) { # Retrieve the entity identity $entityIdentity = $entity.Current.$entityIdentityProperty # Archive only if action is selected and archive expression is provided if ($deleteAction -eq "Archive" -and $archiveEntity) { Write-Information "$($syncProgress): Archiving $($entityType) '$($entityIdentity)'." # Try to archive the entity try { $scriptBlock = Add-ScriptBlockInputParameters $archiveEntity $argumentList = $parameterObjects + @($entity) $archiveResult = Invoke-Command -ScriptBlock $scriptBlock -ArgumentList $argumentList } # Error while archiving the entity catch { $errorMessages += "$($syncProgress): Error while archiving $($entityType) '$($entityIdentity)'. `r`n$($_.Exception.Message)`r`n" Write-Error $errorMessages continue } # Archive was successful if ($archiveResult -eq "success") { Write-Verbose "$($syncProgress): Archived $($entityType) '$($entityIdentity)'." } else { Write-Error "$($syncProgress): Failed to archive $($entityType) '$($entityIdentity)'." } } else { Write-Information "$($syncProgress): Deleting $($entityType) '$($entityIdentity)'." # Try to delete the entity try { $scriptBlock = Add-ScriptBlockInputParameters $deleteEntity $argumentList = $parameterObjects + @($entity) $deleteResult = Invoke-Command -ScriptBlock $scriptBlock -ArgumentList $argumentList } # Error while deleting the entity catch { $errorMessages += "$($syncProgress): Error while deleting $($entityType) '$($entityIdentity)'. `r`n$($_.Exception.Message)`r`n" Write-Error $errorMessages continue } # Delete was successful if ($deleteResult -eq "success") { Write-Verbose "$($syncProgress): Deleted $($entityType) '$($entityIdentity)'." } else { Write-Error "$($syncProgress): Failed to delete $($entityType) '$($entityIdentity)'." } } } # No action is to be taken else { # Retrieve the entity identity if ($entity.Expected) { $entityIdentity = $entity.Expected.$entityIdentityProperty } else { $entityIdentity = $entity.Current.$entityIdentityProperty } # Output the information message Write-Information "$($syncProgress): No action to be taken for $($entityType) '$($entityIdentity)'." } } } catch { # An exception means that the sync failed $errorMessages = "An exception occurred during this function on line $($_.InvocationInfo.ScriptLineNumber): $($_.Exception.Message)`r`n" } # Return the errors if there were any if (![String]::IsNullOrWhiteSpace($errorMessages)) { if ($returnType -eq "Errors") { return $errorMessages } # Return false to indicate unsuccessful sync else { return $false } } # Successful sync if ($returnType -eq "Result") { return $true } } |