Files
Invoke-RackpadPopulation/Invoke-RackpadPopulation.ps1
T
GraceSolutions bbdfbe3d8d Complete
2026-08-02 21:28:37 -04:00

672 lines
42 KiB
PowerShell

#Requires -Version 5
<#
.SYNOPSIS
Idempotently populates one or more Rackpad labs from live infrastructure.
.DESCRIPTION
This script reconciles a Rackpad lab against a desired state that is assembled from two kinds of input.
The live sources are authoritative and each one owns its own domain.
Omada owns switches, access points, switch ports, cables, and wireless.
Proxmox owns hypervisor hosts, bridges, guests, and guest network interfaces.
Hyper-V owns the same shapes as Proxmox and can run alongside it.
OPNsense owns VLANs, subnets, and the routed edge.
The settings document only supplies what no API can know, which is the physical layout consisting of rooms, racks, and rack unit
placement, together with the address ranges that serve them. It cannot create devices, VLANs, subnets, or SSIDs. Rooms and racks
are optional; leave them out and discovered equipment stays loose within the lab.
The run is safe to repeat. Objects are matched using stable natural keys, only differing properties are transmitted, and a run
against an unchanged environment produces no write traffic at all.
Pruning is limited to objects that a previous run of this script created. Those objects are recorded within a state document, so
anything created by hand within the Rackpad user interface is never deleted.
DHCP is intentionally out of scope. No DHCP scope is created, updated, or deleted, and IP assignments always use the static
allocation mode.
.PARAMETER ConfigurationPath
The path of the settings document. Defaults to a document named after this script within the Settings directory beside it, so
Invoke-RackpadPopulation.ps1 reads Settings\Invoke-RackpadPopulation.xml.
.PARAMETER InstanceName
One or more instance names from the configuration document. When omitted, every enabled instance is processed.
.PARAMETER SkipPrune
Suppresses the pruning phase for this run regardless of what the configuration document requests.
.PARAMETER LogDirectory
A valid folder path. If the folder does not exist, it will be created. This parameter can also be specified by the alias "LogPath".
.PARAMETER ContinueOnError
Ignore failures.
.EXAMPLE
Preview every change without writing anything.
powershell.exe -ExecutionPolicy Bypass -NoProfile -NoLogo -File "%FolderPathContainingScript%\Invoke-RackpadPopulation.ps1" -WhatIf -Verbose
.EXAMPLE
Populate every enabled instance.
powershell.exe -ExecutionPolicy Bypass -NoProfile -NoLogo -File "%FolderPathContainingScript%\Invoke-RackpadPopulation.ps1" -Verbose
.EXAMPLE
Populate a single instance using an alternate configuration document and leave orphaned objects in place.
powershell.exe -ExecutionPolicy Bypass -NoProfile -NoLogo -File "%FolderPathContainingScript%\Invoke-RackpadPopulation.ps1" -ConfigurationPath "D:\Settings\Alternate.xml" -InstanceName "Primary" -SkipPrune -Verbose
.NOTES
Author: Grace Solutions
Version: 2026.08.02.0000
Exit codes.
0 Every enabled instance was reconciled successfully.
1000 At least one instance completed with warnings, which means one or more objects failed to write.
2000 An unhandled failure occurred.
6000 The toolkit failed to load.
.LINK
https://github.com/Grace-Solutions/Invoke-RackpadPopulation
#>
[CmdletBinding(SupportsShouldProcess=$True)]
Param
(
[Parameter(Mandatory=$False)]
[ValidateNotNullOrEmpty()]
[Alias('Config', 'ConfigPath')]
[System.IO.FileInfo]$ConfigurationPath,
[Parameter(Mandatory=$False)]
[ValidateNotNullOrEmpty()]
[Alias('Instance')]
[String[]]$InstanceName,
[Parameter(Mandatory=$False)]
[Switch]$SkipPrune,
[Parameter(Mandatory=$False)]
[ValidateNotNullOrEmpty()]
[Alias('LogDir', 'LogPath')]
[System.IO.DirectoryInfo]$LogDirectory,
[Parameter(Mandatory=$False)]
[Switch]$ContinueOnError
)
Function Test-ProcessElevationStatus
{
$Identity = [System.Security.Principal.WindowsIdentity]::GetCurrent()
$Principal = New-Object -TypeName 'System.Security.Principal.WindowsPrincipal' -ArgumentList ($Identity)
$Result = $Principal.IsInRole([System.Security.Principal.WindowsBuiltInRole]::Administrator)
Write-Output -InputObject ($Result)
}
Switch (Test-ProcessElevationStatus)
{
Default
{
Try
{
#region Define Default Action Preferences
$Script:InformationPreference = 'Continue'
$Script:DebugPreference = 'SilentlyContinue'
$Script:ErrorActionPreference = 'Stop'
$Script:WarningPreference = 'Continue'
#region Honor the common Verbose parameter rather than overriding it, so that per object detail from the toolkit functions is visible on demand
Switch ($PSBoundParameters.ContainsKey('Verbose'))
{
{($_ -eq $True)}
{
$Script:VerbosePreference = 'Continue'
}
{($_ -eq $False)}
{
$Script:VerbosePreference = 'SilentlyContinue'
}
}
#endregion
$Script:ConfirmPreference = 'None'
#endregion
#region Set the default exit code for the script (By default, the script will exit with an exit code of 0)
[System.Environment]::ExitCode = 0
#endregion
#region Initialize Toolkit (This operation loads functions, modules, and variables into the current session, so if you do not see a variable defined below, it is because it is defined in the Toolkit)
Try
{
[System.IO.FileInfo]$ToolkitScriptPath = [System.IO.Path]::Combine("$([System.IO.Path]::GetDirectoryName($MyInvocation.MyCommand.Definition))", 'Toolkit', 'Toolkit.ps1')
. "$($ToolkitScriptPath.FullName)" -CallingScriptInvocationInfo ($MyInvocation) -CallingScriptParameterSetName ($PSCmdlet.ParameterSetName)
}
Catch
{
[System.Environment]::ExitCode = 6000
Throw
}
#endregion
#region Set default parameter values
Switch ($True)
{
{($Null -ieq $ConfigurationPath)}
{
[System.IO.DirectoryInfo]$SettingsDirectory = [System.IO.Path]::Combine("$($CallingScriptDirectory.FullName)", 'Settings')
[System.IO.FileInfo]$ConfigurationPath = [System.IO.Path]::Combine("$($SettingsDirectory.FullName)", "$($CallingScriptPath.BaseName).xml")
}
}
#endregion
#region Perform Script Actions
$WriteLogMessage.Invoke(0, @("Configuration Document: $($ConfigurationPath.FullName)"))
#region Define the deletion order used during pruning so that dependent objects are removed first
$PruneOrder = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary'
$PruneOrder.'ip-assignment' = 'ip-assignments'
$PruneOrder.'ip-zone' = 'ip-zones'
$PruneOrder.'port-link' = 'port-links'
$PruneOrder.'wifi-association' = 'wifi/associations'
$PruneOrder.'wifi-radio' = 'wifi/radios'
$PruneOrder.'port' = 'ports'
$PruneOrder.'wifi-ssid' = 'wifi/ssids'
$PruneOrder.'virtual-switch' = 'virtual-switches'
$PruneOrder.'device' = 'devices'
$PruneOrder.'subnet' = 'subnets'
$PruneOrder.'vlan' = 'vlans'
$PruneOrder.'rack' = 'racks'
$PruneOrder.'room' = 'rooms'
$PruneOrder.'wifi-controller' = 'wifi/controllers'
#endregion
#region Load the configuration document
$GetRackpadPopulationConfigurationParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary'
$GetRackpadPopulationConfigurationParameters.Path = $ConfigurationPath
Switch (($Null -ine $InstanceName) -and ($InstanceName.Count -gt 0))
{
{($_ -eq $True)}
{
$GetRackpadPopulationConfigurationParameters.InstanceName = $InstanceName
}
}
$InstanceConfigurationList = Get-RackpadPopulationConfiguration @GetRackpadPopulationConfigurationParameters
$InstanceConfigurationListCount = $InstanceConfigurationList.Count
$WriteLogMessage.Invoke(0, @("$($InstanceConfigurationListCount) enabled instance(s) will be processed."))
#endregion
#region Process every enabled instance
$InstanceOutcomeList = New-Object -TypeName 'System.Collections.Generic.List[PSObject]'
For ($InstanceConfigurationIndex = 0; $InstanceConfigurationIndex -lt $InstanceConfigurationListCount; $InstanceConfigurationIndex++)
{
$InstanceConfiguration = $InstanceConfigurationList[$InstanceConfigurationIndex]
$WriteProgressParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary'
$WriteProgressParameters.Activity = 'Reconciling Rackpad instances'
$WriteProgressParameters.Status = "Processing `"$($InstanceConfiguration.Name)`" ($($InstanceConfigurationIndex + 1) of $($InstanceConfigurationListCount))"
$WriteProgressParameters.PercentComplete = [System.Math]::Round((($InstanceConfigurationIndex + 1) / $InstanceConfigurationListCount) * 100, 2)
$WriteProgressParameters.Id = 1
$Null = Write-Progress @WriteProgressParameters
$InstanceOutcomeProperties = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary'
$InstanceOutcomeProperties.InstanceName = "$($InstanceConfiguration.Name)"
$InstanceOutcomeProperties.LabName = "$($InstanceConfiguration.Lab.Name)"
$InstanceOutcomeProperties.LabID = ''
$InstanceOutcomeProperties.Succeeded = $False
$InstanceOutcomeProperties.FailureCount = 0
$InstanceOutcomeProperties.PrunedCount = 0
$InstanceOutcomeProperties.SummaryList = New-Object -TypeName 'System.Collections.Generic.List[PSObject]'
$InstanceOutcomeProperties.ErrorMessage = $Null
Try
{
$WriteLogMessage.Invoke(0, @("Attempting to reconcile the instance `"$($InstanceConfiguration.Name)`". Please Wait..."))
#region Authenticate
$ConnectRackpadInstanceParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary'
$ConnectRackpadInstanceParameters.BaseURL = $InstanceConfiguration.Rackpad.BaseURL
$ConnectRackpadInstanceParameters.Username = $InstanceConfiguration.Rackpad.Username
$ConnectRackpadInstanceParameters.Password = $InstanceConfiguration.Rackpad.Password
$ConnectRackpadInstanceParameters.SkipCertificateCheck = $InstanceConfiguration.Rackpad.SkipCertificateCheck
$RackpadSession = Connect-RackpadInstance @ConnectRackpadInstanceParameters
#endregion
#region Phase 01 - Resolve or create the lab
$LabResponse = Invoke-RackpadRequest -Session ($RackpadSession) -Route 'labs' -Method 'GET'
$ExistingLab = @($LabResponse.Content) | Where-Object {("$($_.name)" -ieq "$($InstanceConfiguration.Lab.Name)")} | Select-Object -First 1
Switch ($Null -ieq $ExistingLab)
{
{($_ -eq $True)}
{
Switch ($InstanceConfiguration.Lab.CreateIfMissing)
{
{($_ -eq $False)}
{
Throw "The lab `"$($InstanceConfiguration.Lab.Name)`" does not exist and the configuration document does not permit creating it."
}
{($_ -eq $True)}
{
$LabProperty = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary'
$LabProperty.name = "$($InstanceConfiguration.Lab.Name)"
$LabProperty.description = "$($InstanceConfiguration.Lab.Description)"
$LabProperty.location = "$($InstanceConfiguration.Lab.Location)"
Switch ($PSCmdlet.ShouldProcess("lab `"$($InstanceConfiguration.Lab.Name)`"", 'Create using POST /api/labs'))
{
{($_ -eq $True)}
{
$LabCreationResponse = Invoke-RackpadRequest -Session ($RackpadSession) -Route 'labs' -Method 'POST' -Body ($LabProperty)
Switch ($LabCreationResponse.IsSuccess)
{
{($_ -eq $False)}
{
Throw "The lab `"$($InstanceConfiguration.Lab.Name)`" could not be created. [Status Code: $($LabCreationResponse.StatusCode)] [$($LabCreationResponse.ErrorMessage)]"
}
}
$ExistingLab = $LabCreationResponse.Content
$WriteLogMessage.Invoke(0, @("Created the lab `"$($ExistingLab.name)`". [Identifier: $($ExistingLab.id)]"))
}
{($_ -eq $False)}
{
Throw "The lab `"$($InstanceConfiguration.Lab.Name)`" does not exist. Run without WhatIf to create it before previewing the remaining phases."
}
}
}
}
}
}
$LabID = "$($ExistingLab.id)"
$InstanceOutcomeProperties.LabID = "$($LabID)"
$WriteLogMessage.Invoke(0, @("Target Lab: $($ExistingLab.name) [Identifier: $($LabID)]"))
#endregion
#region Load the current inventory and the managed object state
$RackpadInventory = Get-RackpadInventory -Session ($RackpadSession) -LabID ($LabID)
$SanitizedInstanceName = ("$($InstanceConfiguration.Name)" -ireplace '[^0-9A-Za-z\.\-_]', '_')
#region The state document lives beside the script so that the solution stays self contained and portable
[System.IO.DirectoryInfo]$StateDirectory = [System.IO.Path]::Combine("$($CallingScriptDirectory.FullName)", 'Data')
Switch ([String]::IsNullOrWhiteSpace($InstanceConfiguration.Options.StateDirectory) -eq $False)
{
{($_ -eq $True)}
{
[System.IO.DirectoryInfo]$StateDirectory = "$($InstanceConfiguration.Options.StateDirectory)"
}
}
#endregion
[System.IO.FileInfo]$ManagedStatePath = [System.IO.Path]::Combine("$($StateDirectory.FullName)", "$($SanitizedInstanceName).state.json")
$ManagedState = Get-RackpadManagedState -Path ($ManagedStatePath) -InstanceName ($InstanceConfiguration.Name) -LabID ($LabID)
#endregion
#region Collect every enabled source
$OmadaInventory = $Null
$OPNsenseInventory = $Null
$PublicIPInformation = $Null
$HyperVInventory = $Null
$ProxmoxInventoryList = New-Object -TypeName 'System.Collections.Generic.List[PSObject]'
Switch (($InstanceConfiguration.Sources.Omada.Enabled -eq $True) -and ($Null -ine $InstanceConfiguration.Sources.Omada.BaseURL))
{
{($_ -eq $True)}
{
Try
{
$ConnectOmadaControllerParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary'
$ConnectOmadaControllerParameters.BaseURL = $InstanceConfiguration.Sources.Omada.BaseURL
$ConnectOmadaControllerParameters.ClientID = $InstanceConfiguration.Sources.Omada.ClientID
$ConnectOmadaControllerParameters.ClientSecret = $InstanceConfiguration.Sources.Omada.ClientSecret
$ConnectOmadaControllerParameters.SkipCertificateCheck = $InstanceConfiguration.Sources.Omada.SkipCertificateCheck
Switch (([String]::IsNullOrWhiteSpace($InstanceConfiguration.Sources.Omada.Username) -eq $False) -and ($InstanceConfiguration.Sources.Omada.PreferControllerInterface -eq $True))
{
{($_ -eq $True)}
{
$OmadaSecurePassword = ConvertTo-SecureString -String ($InstanceConfiguration.Sources.Omada.Password) -AsPlainText -Force
$ConnectOmadaControllerParameters.Credential = New-Object -TypeName 'System.Management.Automation.PSCredential' -ArgumentList ($InstanceConfiguration.Sources.Omada.Username), ($OmadaSecurePassword)
}
}
$OmadaContext = Connect-OmadaController @ConnectOmadaControllerParameters
$GetOmadaInventoryParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary'
$GetOmadaInventoryParameters.Context = $OmadaContext
Switch ($InstanceConfiguration.Sources.Omada.SiteNameList.Count -gt 0)
{
{($_ -eq $True)}
{
$GetOmadaInventoryParameters.SiteName = $InstanceConfiguration.Sources.Omada.SiteNameList.ToArray()
}
}
$OmadaInventory = Get-OmadaInventory @GetOmadaInventoryParameters
}
Catch
{
$WriteLogMessage.Invoke(2, @("The Omada collection failed and will be skipped. Message: $($_.Exception.Message)"))
}
}
}
Switch (($InstanceConfiguration.Sources.Proxmox.Enabled -eq $True) -and ($InstanceConfiguration.Sources.Proxmox.EndpointList.Count -gt 0))
{
{($_ -eq $True)}
{
#region Each endpoint is collected separately, because separate clusters cannot see one another
ForEach ($ProxmoxEndpoint In $InstanceConfiguration.Sources.Proxmox.EndpointList)
{
Try
{
$GetProxmoxInventoryParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary'
$GetProxmoxInventoryParameters.BaseURL = $ProxmoxEndpoint.BaseURL
$GetProxmoxInventoryParameters.TokenID = $ProxmoxEndpoint.TokenID
$GetProxmoxInventoryParameters.TokenSecret = $ProxmoxEndpoint.TokenSecret
$GetProxmoxInventoryParameters.SkipCertificateCheck = $ProxmoxEndpoint.SkipCertificateCheck
$GetProxmoxInventoryParameters.IncludeTemplates = $ProxmoxEndpoint.IncludeTemplates
$ProxmoxEndpointInventory = Get-ProxmoxInventory @GetProxmoxInventoryParameters
#region A declared name wins over whatever the endpoint reports, which is how a standalone host gets a meaningful label
Switch ([String]::IsNullOrWhiteSpace($ProxmoxEndpoint.Name) -eq $False)
{
{($_ -eq $True)}
{
$ProxmoxEndpointInventory.ClusterName = "$($ProxmoxEndpoint.Name)"
}
}
#endregion
$ProxmoxInventoryList.Add($ProxmoxEndpointInventory)
}
Catch
{
$WriteLogMessage.Invoke(2, @("The Proxmox collection for endpoint `"$($ProxmoxEndpoint.Name)$($ProxmoxEndpoint.BaseURL)`" failed and will be skipped. Message: $($_.Exception.Message)"))
}
}
#endregion
}
}
Switch (($InstanceConfiguration.Sources.HyperV.Enabled -eq $True) -and ($InstanceConfiguration.Sources.HyperV.HostNameList.Count -gt 0))
{
{($_ -eq $True)}
{
Try
{
$GetHyperVInventoryParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary'
$GetHyperVInventoryParameters.ComputerName = $InstanceConfiguration.Sources.HyperV.HostNameList.ToArray()
$GetHyperVInventoryParameters.ClusterName = $InstanceConfiguration.Sources.HyperV.ClusterName
$GetHyperVInventoryParameters.UseDCOM = $InstanceConfiguration.Sources.HyperV.UseDCOM
$GetHyperVInventoryParameters.UseGuestIntrospection = $InstanceConfiguration.Sources.HyperV.UseGuestIntrospection
Switch ([String]::IsNullOrWhiteSpace($InstanceConfiguration.Sources.HyperV.Username) -eq $False)
{
{($_ -eq $True)}
{
$HyperVSecurePassword = ConvertTo-SecureString -String ($InstanceConfiguration.Sources.HyperV.Password) -AsPlainText -Force
$GetHyperVInventoryParameters.Credential = New-Object -TypeName 'System.Management.Automation.PSCredential' -ArgumentList ($InstanceConfiguration.Sources.HyperV.Username), ($HyperVSecurePassword)
}
}
Switch ([String]::IsNullOrWhiteSpace($InstanceConfiguration.Sources.HyperV.GuestUsername) -eq $False)
{
{($_ -eq $True)}
{
$HyperVGuestSecurePassword = ConvertTo-SecureString -String ($InstanceConfiguration.Sources.HyperV.GuestPassword) -AsPlainText -Force
$GetHyperVInventoryParameters.GuestCredential = New-Object -TypeName 'System.Management.Automation.PSCredential' -ArgumentList ($InstanceConfiguration.Sources.HyperV.GuestUsername), ($HyperVGuestSecurePassword)
}
}
$HyperVInventory = Get-HyperVInventory @GetHyperVInventoryParameters
}
Catch
{
$WriteLogMessage.Invoke(2, @("The Hyper-V collection failed and will be skipped. Message: $($_.Exception.Message)"))
}
}
}
Switch (($InstanceConfiguration.Sources.OPNsense.Enabled -eq $True) -and ($Null -ine $InstanceConfiguration.Sources.OPNsense.BaseURL))
{
{($_ -eq $True)}
{
Try
{
$GetOPNsenseInventoryParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary'
$GetOPNsenseInventoryParameters.BaseURL = $InstanceConfiguration.Sources.OPNsense.BaseURL
$GetOPNsenseInventoryParameters.APIKey = $InstanceConfiguration.Sources.OPNsense.APIKey
$GetOPNsenseInventoryParameters.APISecret = $InstanceConfiguration.Sources.OPNsense.APISecret
$GetOPNsenseInventoryParameters.SkipCertificateCheck = $InstanceConfiguration.Sources.OPNsense.SkipCertificateCheck
$OPNsenseInventory = Get-OPNsenseInventory @GetOPNsenseInventoryParameters
}
Catch
{
$WriteLogMessage.Invoke(2, @("The OPNsense collection failed and will be skipped. Message: $($_.Exception.Message)"))
}
}
}
#endregion
#region Resolve the public address and enrich it with the internet service provider detail
Switch ($InstanceConfiguration.Networking.PublicIPEnabled)
{
{($_ -eq $True)}
{
Try
{
$PublicIPInformation = Get-PublicIPInformation -ProviderURL ($InstanceConfiguration.Networking.PublicIPProviderURL)
}
Catch
{
$WriteLogMessage.Invoke(2, @("The public IP lookup failed and will be skipped. Message: $($_.Exception.Message)"))
}
}
}
#endregion
#region Assemble the desired state graph
$ConvertToRackpadDesiredStateParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary'
$ConvertToRackpadDesiredStateParameters.Configuration = $InstanceConfiguration
$ConvertToRackpadDesiredStateParameters.OmadaInventory = $OmadaInventory
$ConvertToRackpadDesiredStateParameters.ProxmoxInventory = $ProxmoxInventoryList.ToArray()
$ConvertToRackpadDesiredStateParameters.HyperVInventory = $HyperVInventory
$ConvertToRackpadDesiredStateParameters.OPNsenseInventory = $OPNsenseInventory
$ConvertToRackpadDesiredStateParameters.PublicIPInformation = $PublicIPInformation
$DesiredState = ConvertTo-RackpadDesiredState @ConvertToRackpadDesiredStateParameters
#endregion
#region Publish the desired state
$PublishRackpadDesiredStateParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary'
$PublishRackpadDesiredStateParameters.Session = $RackpadSession
$PublishRackpadDesiredStateParameters.DesiredState = $DesiredState
$PublishRackpadDesiredStateParameters.Inventory = $RackpadInventory
$PublishRackpadDesiredStateParameters.ManagedState = $ManagedState
$PublishRackpadDesiredStateParameters.LabID = $LabID
$PublishRackpadDesiredStateParameters.Options = $InstanceConfiguration.Options
$PublicationResult = Publish-RackpadDesiredState @PublishRackpadDesiredStateParameters
ForEach ($SummaryEntry In $PublicationResult.SummaryList)
{
$InstanceOutcomeProperties.SummaryList.Add($SummaryEntry)
$WriteLogMessage.Invoke(0, @("$($SummaryEntry.EntityType): Created $($SummaryEntry.Created), Updated $($SummaryEntry.Updated), Unchanged $($SummaryEntry.Unchanged), WhatIf $($SummaryEntry.WhatIf), Failed $($SummaryEntry.Failed)"))
$InstanceOutcomeProperties.FailureCount = $InstanceOutcomeProperties.FailureCount + $SummaryEntry.Failed
}
#endregion
#region Prune objects that a previous run created and that are no longer wanted
$PruningIsRequested = ($InstanceConfiguration.Options.PruneManagedObjects -eq $True) -and ($SkipPrune.IsPresent -eq $False)
Switch ($PruningIsRequested)
{
{($_ -eq $True)}
{
$PruneResultList = Remove-RackpadOrphanedObject -Session ($RackpadSession) -ManagedState ($ManagedState) -PruneOrder ($PruneOrder)
$InstanceOutcomeProperties.PrunedCount = ($PruneResultList | Where-Object {($_.Action -ieq 'Deleted')} | Measure-Object).Count
$InstanceOutcomeProperties.FailureCount = $InstanceOutcomeProperties.FailureCount + ($PruneResultList | Where-Object {($_.Action -ieq 'Failed')} | Measure-Object).Count
}
{($_ -eq $False)}
{
$WriteLogMessage.Invoke(0, @('Pruning was skipped for this run.'))
}
}
#endregion
#region Persist the managed object state
$Null = Save-RackpadManagedState -ManagedState ($ManagedState)
#endregion
$InstanceOutcomeProperties.Succeeded = $True
}
Catch
{
$InstanceOutcomeProperties.ErrorMessage = "$($_.Exception.Message)"
$WriteLogMessage.Invoke(3, @("The instance `"$($InstanceConfiguration.Name)`" could not be reconciled. Message: $($_.Exception.Message)"))
Switch ($ContinueOnError.IsPresent)
{
{($_ -eq $False)}
{
Throw
}
}
}
Finally
{
$InstanceOutcomeList.Add((New-Object -TypeName 'System.Management.Automation.PSObject' -Property ($InstanceOutcomeProperties)))
}
}
$Null = Write-Progress -Activity 'Reconciling Rackpad instances' -Id 1 -Completed
#endregion
#region Determine the final exit code
$TotalFailureCount = ($InstanceOutcomeList | Measure-Object -Property 'FailureCount' -Sum).Sum
$UnsuccessfulInstanceCount = ($InstanceOutcomeList | Where-Object {($_.Succeeded -eq $False)} | Measure-Object).Count
Switch ($True)
{
{($UnsuccessfulInstanceCount -gt 0)}
{
[System.Environment]::ExitCode = 2000
}
{($UnsuccessfulInstanceCount -eq 0) -and ($TotalFailureCount -gt 0)}
{
[System.Environment]::ExitCode = 1000
}
}
$WriteLogMessage.Invoke(0, @("$($InstanceOutcomeList.Count) instance(s) were processed with $($TotalFailureCount) object level failure(s)."))
#endregion
Write-Output -InputObject ($InstanceOutcomeList)
#endregion
}
Catch
{
#region Perform error handling actions
$ErrorHandlingDefinition.Invoke($Error[0], 2, $ContinueOnError.IsPresent)
#endregion
}
Finally
{
#region Perform finalization actions
$FinalizationActions.Invoke()
#endregion
}
}
{($_ -eq $False)}
{
[System.IO.FileInfo]$ScriptPath = "$($MyInvocation.MyCommand.Definition)"
$CurrentExecutionPolicy = 'Bypass'
$ArgumentList = New-Object -TypeName 'System.Collections.Generic.List[String]'
$ArgumentList.Add("-ExecutionPolicy $($CurrentExecutionPolicy)")
$ArgumentList.Add('-NonInteractive')
$ArgumentList.Add('-NoProfile')
$ArgumentList.Add('-NoLogo')
$ArgumentList.Add('-NoExit')
$ArgumentList.Add('-Command')
$ArgumentList.Add("`"`& {. `"$($ScriptPath.FullName)`"")
$MyInvocation.UnboundArguments.GetEnumerator() | ForEach-Object {$ArgumentList.Add("-$($_.Key) @($($_.Value | ForEach-Object {`"$($_)`"}))")}
$PSBoundParameters.GetEnumerator() | ForEach-Object {$ArgumentList.Add("-$($_.Key) @($($_.Value | ForEach-Object {`"$($_)`"}))")}
$ArgumentListTargetIndex = $ArgumentList.Count - 1
$ArgumentListIndexItem = $ArgumentList[$ArgumentListTargetIndex]
$Null = $ArgumentList.RemoveAt($ArgumentListTargetIndex)
$Null = $ArgumentList.Insert($ArgumentListTargetIndex, ($ArgumentListIndexItem + ';'))
$ArgumentList.Add("[System.Environment]::Exit((`$LASTEXITCODE -Bor [Int](-Not `$? -And -Not `$LASTEXITCODE)))}`"")
$ScriptInterpreterList = New-Object -TypeName 'System.Collections.Generic.List[System.String]'
$ScriptInterpreterList.Add('powershell.exe')
$ScriptInterpreterList.Add('pwsh.exe')
:ScriptInterpreterListLoop ForEach ($ScriptInterpreter In $ScriptInterpreterList)
{
$ScriptInterpreterObject = Try {Get-Command -Name ($ScriptInterpreter) -ErrorAction SilentlyContinue} Catch {$Null}
Switch ($Null -ine $ScriptInterpreterObject)
{
{($_ -eq $True)}
{
$Null = Start-Process -FilePath ($ScriptInterpreterObject.Path) -WorkingDirectory "$($Env:Temp.TrimEnd('\'))" -ArgumentList ($ArgumentList.ToArray()) -WindowStyle Normal -Verb RunAs -PassThru
Break ScriptInterpreterListLoop
}
}
}
}
}