#region Remove-RackpadOrphanedObject Function Remove-RackpadOrphanedObject { <# .SYNOPSIS Deletes objects that this solution previously created and that are no longer present within the desired state. .DESCRIPTION Pruning is deliberately narrow. Only objects recorded within the managed state document are eligible, which means that any object created by hand within the Rackpad user interface is never touched. An entry becomes eligible when it exists within the state document but was not registered as observed during the current run. Deletion is performed in the supplied order so that dependent objects are removed before the objects that they reference. A status code of 404 is treated as success because the target object has already been removed, which keeps the operation idempotent. .PARAMETER Session The session object produced by Connect-RackpadInstance. .PARAMETER ManagedState The state object produced by Get-RackpadManagedState. .PARAMETER PruneOrder An ordered dictionary mapping entity type to collection route. Entity types are processed in the order supplied. .EXAMPLE $PruneOrder = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' $PruneOrder.'ip-assignment' = 'ip-assignments' $PruneOrder.'port-link' = 'port-links' $PruneOrder.'port' = 'ports' $PruneResultList = Remove-RackpadOrphanedObject -Session $RackpadSession -ManagedState $ManagedState -PruneOrder $PruneOrder .NOTES Author: Grace Solutions Version: 2026.08.02.0000 #> [CmdletBinding(SupportsShouldProcess=$True)] Param ( [Parameter(Mandatory=$True)] [ValidateNotNullOrEmpty()] [PSObject]$Session, [Parameter(Mandatory=$True)] [ValidateNotNullOrEmpty()] [PSObject]$ManagedState, [Parameter(Mandatory=$True)] [ValidateNotNullOrEmpty()] [System.Collections.Specialized.OrderedDictionary]$PruneOrder ) Try { $PruneResultList = New-Object -TypeName 'System.Collections.Generic.List[PSObject]' #region Build the list of orphaned entries in the supplied deletion order $OrphanedEntryList = New-Object -TypeName 'System.Collections.Generic.List[PSObject]' ForEach ($PruneOrderEntry In $PruneOrder.GetEnumerator()) { $EntityTypeKey = "$($PruneOrderEntry.Key)".ToLowerInvariant() Switch ($ManagedState.EntryTable.ContainsKey($EntityTypeKey)) { {($_ -eq $True)} { $ObservedKeySet = New-Object -TypeName 'System.Collections.Generic.HashSet[System.String]' Switch ($ManagedState.ObservedTable.ContainsKey($EntityTypeKey)) { {($_ -eq $True)} { $ObservedKeySet = $ManagedState.ObservedTable[$EntityTypeKey] } } ForEach ($RecordedEntry In $ManagedState.EntryTable[$EntityTypeKey].GetEnumerator()) { Switch ($ObservedKeySet.Contains("$($RecordedEntry.Key)")) { {($_ -eq $False)} { $OrphanedEntryProperties = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' $OrphanedEntryProperties.EntityType = "$($EntityTypeKey)" $OrphanedEntryProperties.CollectionRoute = "$($PruneOrderEntry.Value)" $OrphanedEntryProperties.NaturalKey = "$($RecordedEntry.Key)" $OrphanedEntryProperties.Identifier = "$($RecordedEntry.Value)" $OrphanedEntryList.Add((New-Object -TypeName 'System.Management.Automation.PSObject' -Property ($OrphanedEntryProperties))) } } } } } } #endregion Write-Verbose -Message "[$([System.DateTime]::UtcNow.ToString('yyyy/MM/dd HH:mm:ss.FFF'))] - [INFO] - $($OrphanedEntryList.Count) previously created object(s) are eligible for pruning." #region Delete each orphaned entry $OrphanedEntryListCount = $OrphanedEntryList.Count For ($OrphanedEntryIndex = 0; $OrphanedEntryIndex -lt $OrphanedEntryListCount; $OrphanedEntryIndex++) { $OrphanedEntry = $OrphanedEntryList[$OrphanedEntryIndex] $WriteProgressParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' $WriteProgressParameters.Activity = 'Pruning previously created Rackpad objects' $WriteProgressParameters.Status = "Removing $($OrphanedEntry.EntityType) `"$($OrphanedEntry.NaturalKey)`" ($($OrphanedEntryIndex + 1) of $($OrphanedEntryListCount))" $WriteProgressParameters.PercentComplete = [System.Math]::Round((($OrphanedEntryIndex + 1) / $OrphanedEntryListCount) * 100, 2) $WriteProgressParameters.Id = 30 $Null = Write-Progress @WriteProgressParameters $DeletionRoute = "$($OrphanedEntry.CollectionRoute)/$($OrphanedEntry.Identifier)" $PruneResultProperties = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' $PruneResultProperties.EntityType = "$($OrphanedEntry.EntityType)" $PruneResultProperties.NaturalKey = "$($OrphanedEntry.NaturalKey)" $PruneResultProperties.Identifier = "$($OrphanedEntry.Identifier)" $PruneResultProperties.Action = 'Deleted' $PruneResultProperties.ErrorMessage = $Null Switch ($PSCmdlet.ShouldProcess("$($OrphanedEntry.EntityType) `"$($OrphanedEntry.NaturalKey)`"", "Delete using DELETE /api/$($DeletionRoute)")) { {($_ -eq $True)} { $DeletionResponse = Invoke-RackpadRequest -Session ($Session) -Route ($DeletionRoute) -Method 'DELETE' -AcceptableStatusCodeList @('404') Switch ($DeletionResponse.IsSuccess) { {($_ -eq $True)} { $Null = $ManagedState.EntryTable["$($OrphanedEntry.EntityType)"].Remove("$($OrphanedEntry.NaturalKey)") Write-Verbose -Message "[$([System.DateTime]::UtcNow.ToString('yyyy/MM/dd HH:mm:ss.FFF'))] - [INFO] - Pruned $($OrphanedEntry.EntityType) `"$($OrphanedEntry.NaturalKey)`". [Identifier: $($OrphanedEntry.Identifier)]" } {($_ -eq $False)} { $PruneResultProperties.Action = 'Failed' $PruneResultProperties.ErrorMessage = "$($DeletionResponse.StatusDescription) $($DeletionResponse.ErrorMessage)".Trim() Write-Warning -Message "[$([System.DateTime]::UtcNow.ToString('yyyy/MM/dd HH:mm:ss.FFF'))] - [WARNING] - Failed to prune $($OrphanedEntry.EntityType) `"$($OrphanedEntry.NaturalKey)`". [Status Code: $($DeletionResponse.StatusCode)] [$($PruneResultProperties.ErrorMessage)]" } } } {($_ -eq $False)} { $PruneResultProperties.Action = 'WhatIf' } } $PruneResultList.Add((New-Object -TypeName 'System.Management.Automation.PSObject' -Property ($PruneResultProperties))) } $Null = Write-Progress -Activity 'Pruning previously created Rackpad objects' -Id 30 -Completed #endregion Write-Output -InputObject ($PruneResultList) } Catch { Throw } } #endregion