392 lines
22 KiB
PowerShell
392 lines
22 KiB
PowerShell
#region Sync-RackpadObject
|
|
Function Sync-RackpadObject
|
|
{
|
|
<#
|
|
.SYNOPSIS
|
|
Performs an idempotent create or update of a single Rackpad object and records ownership within the managed state document.
|
|
|
|
.DESCRIPTION
|
|
This is the reconciliation primitive that every entity type flows through. The caller supplies the desired property set and
|
|
the matching object from the pre-loaded inventory, if one exists. The function then decides between three outcomes:
|
|
|
|
Created - No matching object exists, so the desired property set is posted to the collection route.
|
|
Updated - A matching object exists but one or more desired properties differ, so only the differing properties are patched.
|
|
Unchanged - A matching object exists and every desired property already matches, so no request is issued.
|
|
|
|
Because only differing properties are transmitted, repeated runs against an unchanged environment produce no write traffic
|
|
at all, which is what makes the overall process safe to schedule.
|
|
|
|
Objects that this function creates are recorded within the managed state document. Objects that already existed are observed
|
|
but never recorded, which is what keeps hand created objects outside the scope of pruning.
|
|
|
|
.PARAMETER Session
|
|
The session object produced by Connect-RackpadInstance.
|
|
|
|
.PARAMETER EntityType
|
|
The logical entity name used for state tracking and log messages, for example 'device' or 'port'.
|
|
|
|
.PARAMETER CollectionRoute
|
|
The collection route relative to /api, for example 'devices'.
|
|
|
|
.PARAMETER NaturalKey
|
|
The stable natural key that identifies this object within its entity type, for example a lab scoped hostname.
|
|
|
|
.PARAMETER DesiredProperty
|
|
An ordered dictionary describing the desired state of the object.
|
|
|
|
.PARAMETER ExistingObject
|
|
The matching object from the pre-loaded inventory, or null when no match exists.
|
|
|
|
.PARAMETER ManagedState
|
|
The state object produced by Get-RackpadManagedState.
|
|
|
|
.PARAMETER UpdateMethod
|
|
The method used to modify an existing object. PATCH transmits only the differing properties. PUT transmits the full payload.
|
|
|
|
.PARAMETER UpdateRoute
|
|
An explicit route used for update operations. Required for routes such as 'wifi/access-points/{deviceId}' where the update
|
|
identifier is not the object identifier.
|
|
|
|
.PARAMETER ImmutablePropertyName
|
|
Property names that are only valid during creation and that must never participate in the difference calculation.
|
|
|
|
.EXAMPLE
|
|
$SyncRackpadObjectParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary'
|
|
$SyncRackpadObjectParameters.Session = $RackpadSession
|
|
$SyncRackpadObjectParameters.EntityType = 'room'
|
|
$SyncRackpadObjectParameters.CollectionRoute = 'rooms'
|
|
$SyncRackpadObjectParameters.NaturalKey = 'server room'
|
|
$SyncRackpadObjectParameters.DesiredProperty = $RoomProperty
|
|
$SyncRackpadObjectParameters.ExistingObject = $ExistingRoom
|
|
$SyncRackpadObjectParameters.ManagedState = $ManagedState
|
|
|
|
$Result = Sync-RackpadObject @SyncRackpadObjectParameters
|
|
|
|
.NOTES
|
|
Author: Grace Solutions
|
|
Version: 2026.08.02.0000
|
|
#>
|
|
|
|
[CmdletBinding(SupportsShouldProcess=$True)]
|
|
Param
|
|
(
|
|
[Parameter(Mandatory=$True)]
|
|
[ValidateNotNullOrEmpty()]
|
|
[PSObject]$Session,
|
|
|
|
[Parameter(Mandatory=$True)]
|
|
[ValidateNotNullOrEmpty()]
|
|
[String]$EntityType,
|
|
|
|
[Parameter(Mandatory=$True)]
|
|
[ValidateNotNullOrEmpty()]
|
|
[String]$CollectionRoute,
|
|
|
|
[Parameter(Mandatory=$True)]
|
|
[ValidateNotNullOrEmpty()]
|
|
[String]$NaturalKey,
|
|
|
|
[Parameter(Mandatory=$True)]
|
|
[ValidateNotNullOrEmpty()]
|
|
[System.Collections.Specialized.OrderedDictionary]$DesiredProperty,
|
|
|
|
[Parameter(Mandatory=$False)]
|
|
[AllowNull()]
|
|
[PSObject]$ExistingObject,
|
|
|
|
[Parameter(Mandatory=$False)]
|
|
[AllowNull()]
|
|
[PSObject]$ManagedState,
|
|
|
|
[Parameter(Mandatory=$False)]
|
|
[ValidateSet('PATCH', 'PUT')]
|
|
[String]$UpdateMethod = 'PATCH',
|
|
|
|
[Parameter(Mandatory=$False)]
|
|
[AllowEmptyString()]
|
|
[String]$UpdateRoute = '',
|
|
|
|
[Parameter(Mandatory=$False)]
|
|
[AllowNull()]
|
|
[String[]]$ImmutablePropertyName
|
|
)
|
|
|
|
Try
|
|
{
|
|
#region Define the value normalization logic used by the difference calculation
|
|
[ScriptBlock]$ConvertToComparableValue = {
|
|
Param
|
|
(
|
|
[System.Object]$Value
|
|
)
|
|
|
|
$ComparableValue = ''
|
|
|
|
Switch ($True)
|
|
{
|
|
{($Null -ieq $Value)}
|
|
{
|
|
$ComparableValue = ''
|
|
}
|
|
|
|
{($Value -is [System.Boolean])}
|
|
{
|
|
$ComparableValue = "$($Value)".ToLowerInvariant()
|
|
}
|
|
|
|
{($Value -is [System.Array]) -or ($Value -is [System.Collections.IList])}
|
|
{
|
|
$ValueSegmentList = New-Object -TypeName 'System.Collections.Generic.List[String]'
|
|
|
|
ForEach ($ValueSegment In $Value)
|
|
{
|
|
$ValueSegmentList.Add("$($ValueSegment)".Trim())
|
|
}
|
|
|
|
$Null = $ValueSegmentList.Sort()
|
|
|
|
$ComparableValue = "$($ValueSegmentList -Join '|')"
|
|
}
|
|
|
|
{($Null -ine $Value) -and ($Value -isnot [System.Boolean]) -and ($Value -isnot [System.Array]) -and ($Value -isnot [System.Collections.IList])}
|
|
{
|
|
$ComparableValue = "$($Value)".Trim()
|
|
}
|
|
}
|
|
|
|
Write-Output -InputObject ("$($ComparableValue)")
|
|
}
|
|
#endregion
|
|
|
|
#region Register the natural key as observed so that it is excluded from pruning
|
|
$EntityTypeKey = "$($EntityType)".ToLowerInvariant()
|
|
$NaturalKeyValue = "$($NaturalKey)".ToLowerInvariant()
|
|
|
|
Switch ($Null -ine $ManagedState)
|
|
{
|
|
{($_ -eq $True)}
|
|
{
|
|
Switch ($ManagedState.ObservedTable.ContainsKey($EntityTypeKey))
|
|
{
|
|
{($_ -eq $False)}
|
|
{
|
|
$ManagedState.ObservedTable.Add($EntityTypeKey, (New-Object -TypeName 'System.Collections.Generic.HashSet[System.String]'))
|
|
}
|
|
}
|
|
|
|
$Null = $ManagedState.ObservedTable[$EntityTypeKey].Add($NaturalKeyValue)
|
|
}
|
|
}
|
|
#endregion
|
|
|
|
#region Establish the result object
|
|
$ResultProperties = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary'
|
|
$ResultProperties.EntityType = "$($EntityType)"
|
|
$ResultProperties.NaturalKey = "$($NaturalKey)"
|
|
$ResultProperties.Action = 'Unchanged'
|
|
$ResultProperties.Identifier = "$($ExistingObject.id)"
|
|
$ResultProperties.ChangedPropertyList = New-Object -TypeName 'System.Collections.Generic.List[String]'
|
|
$ResultProperties.Object = $ExistingObject
|
|
$ResultProperties.ErrorMessage = $Null
|
|
#endregion
|
|
|
|
#region Create the object when no match exists
|
|
Switch ($Null -ieq $ExistingObject)
|
|
{
|
|
{($_ -eq $True)}
|
|
{
|
|
$CreationRoute = "$($CollectionRoute)"
|
|
$CreationMethod = 'POST'
|
|
|
|
Switch (([String]::IsNullOrWhiteSpace($UpdateRoute) -eq $False) -and ($UpdateMethod -ieq 'PUT'))
|
|
{
|
|
{($_ -eq $True)}
|
|
{
|
|
$CreationRoute = "$($UpdateRoute)"
|
|
$CreationMethod = 'PUT'
|
|
}
|
|
}
|
|
|
|
Switch ($PSCmdlet.ShouldProcess("$($EntityType) `"$($NaturalKey)`"", "Create using $($CreationMethod) /api/$($CreationRoute)"))
|
|
{
|
|
{($_ -eq $False)}
|
|
{
|
|
$ResultProperties.Action = 'WhatIf'
|
|
|
|
ForEach ($DesiredPropertyEntry In $DesiredProperty.GetEnumerator())
|
|
{
|
|
$ResultProperties.ChangedPropertyList.Add("$($DesiredPropertyEntry.Key)")
|
|
}
|
|
|
|
Write-Output -InputObject (New-Object -TypeName 'System.Management.Automation.PSObject' -Property ($ResultProperties))
|
|
|
|
Return
|
|
}
|
|
}
|
|
|
|
$CreationResponse = Invoke-RackpadRequest -Session ($Session) -Route ($CreationRoute) -Method ($CreationMethod) -Body ($DesiredProperty)
|
|
|
|
Switch ($CreationResponse.IsSuccess)
|
|
{
|
|
{($_ -eq $True)}
|
|
{
|
|
$ResultProperties.Action = 'Created'
|
|
$ResultProperties.Object = $CreationResponse.Content
|
|
$ResultProperties.Identifier = "$($CreationResponse.Content.id)"
|
|
|
|
ForEach ($DesiredPropertyEntry In $DesiredProperty.GetEnumerator())
|
|
{
|
|
$ResultProperties.ChangedPropertyList.Add("$($DesiredPropertyEntry.Key)")
|
|
}
|
|
|
|
#region Record ownership so that this object becomes eligible for pruning later
|
|
Switch (($Null -ine $ManagedState) -and ([String]::IsNullOrWhiteSpace($ResultProperties.Identifier) -eq $False))
|
|
{
|
|
{($_ -eq $True)}
|
|
{
|
|
Switch ($ManagedState.EntryTable.ContainsKey($EntityTypeKey))
|
|
{
|
|
{($_ -eq $False)}
|
|
{
|
|
$ManagedState.EntryTable.Add($EntityTypeKey, (New-Object -TypeName 'System.Collections.Generic.Dictionary[[System.String], [System.String]]'))
|
|
}
|
|
}
|
|
|
|
$ManagedState.EntryTable[$EntityTypeKey][$NaturalKeyValue] = "$($ResultProperties.Identifier)"
|
|
}
|
|
}
|
|
#endregion
|
|
|
|
Write-Verbose -Message "[$([System.DateTime]::UtcNow.ToString('yyyy/MM/dd HH:mm:ss.FFF'))] - [INFO] - Created $($EntityType) `"$($NaturalKey)`". [Identifier: $($ResultProperties.Identifier)]"
|
|
}
|
|
|
|
{($_ -eq $False)}
|
|
{
|
|
$ResultProperties.Action = 'Failed'
|
|
$ResultProperties.ErrorMessage = "$($CreationResponse.StatusDescription) $($CreationResponse.ErrorMessage)".Trim()
|
|
|
|
Write-Warning -Message "[$([System.DateTime]::UtcNow.ToString('yyyy/MM/dd HH:mm:ss.FFF'))] - [WARNING] - Failed to create $($EntityType) `"$($NaturalKey)`". [Status Code: $($CreationResponse.StatusCode)] [$($ResultProperties.ErrorMessage)]"
|
|
}
|
|
}
|
|
|
|
Write-Output -InputObject (New-Object -TypeName 'System.Management.Automation.PSObject' -Property ($ResultProperties))
|
|
|
|
Return
|
|
}
|
|
}
|
|
#endregion
|
|
|
|
#region Calculate the difference between the desired and current property sets
|
|
$ChangedProperty = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary'
|
|
|
|
ForEach ($DesiredPropertyEntry In $DesiredProperty.GetEnumerator())
|
|
{
|
|
$PropertyName = "$($DesiredPropertyEntry.Key)"
|
|
|
|
$PropertyIsComparable = -Not (($Null -ine $ImmutablePropertyName) -and ($PropertyName -iin $ImmutablePropertyName))
|
|
|
|
Switch ($PropertyIsComparable)
|
|
{
|
|
{($_ -eq $True)}
|
|
{
|
|
$DesiredComparableValue = $ConvertToComparableValue.InvokeReturnAsIs($DesiredPropertyEntry.Value)
|
|
$CurrentComparableValue = $ConvertToComparableValue.InvokeReturnAsIs($ExistingObject.$($PropertyName))
|
|
|
|
Switch ($DesiredComparableValue -ine $CurrentComparableValue)
|
|
{
|
|
{($_ -eq $True)}
|
|
{
|
|
$ChangedProperty.$($PropertyName) = $DesiredPropertyEntry.Value
|
|
|
|
$ResultProperties.ChangedPropertyList.Add("$($PropertyName)")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
#endregion
|
|
|
|
#region Update the object when at least one property differs
|
|
Switch ($ChangedProperty.Count -gt 0)
|
|
{
|
|
{($_ -eq $False)}
|
|
{
|
|
Write-Verbose -Message "[$([System.DateTime]::UtcNow.ToString('yyyy/MM/dd HH:mm:ss.FFF'))] - [INFO] - No changes are required for $($EntityType) `"$($NaturalKey)`". [Identifier: $($ResultProperties.Identifier)]"
|
|
|
|
Write-Output -InputObject (New-Object -TypeName 'System.Management.Automation.PSObject' -Property ($ResultProperties))
|
|
|
|
Return
|
|
}
|
|
}
|
|
|
|
$ResolvedUpdateRoute = "$($CollectionRoute)/$($ExistingObject.id)"
|
|
|
|
Switch ([String]::IsNullOrWhiteSpace($UpdateRoute) -eq $False)
|
|
{
|
|
{($_ -eq $True)}
|
|
{
|
|
$ResolvedUpdateRoute = "$($UpdateRoute)"
|
|
}
|
|
}
|
|
|
|
$UpdatePayload = $ChangedProperty
|
|
|
|
Switch ($UpdateMethod -ieq 'PUT')
|
|
{
|
|
{($_ -eq $True)}
|
|
{
|
|
$UpdatePayload = $DesiredProperty
|
|
}
|
|
}
|
|
|
|
Switch ($PSCmdlet.ShouldProcess("$($EntityType) `"$($NaturalKey)`"", "Update $($ResultProperties.ChangedPropertyList -Join ', ') using $($UpdateMethod) /api/$($ResolvedUpdateRoute)"))
|
|
{
|
|
{($_ -eq $False)}
|
|
{
|
|
$ResultProperties.Action = 'WhatIf'
|
|
|
|
Write-Output -InputObject (New-Object -TypeName 'System.Management.Automation.PSObject' -Property ($ResultProperties))
|
|
|
|
Return
|
|
}
|
|
}
|
|
|
|
$UpdateResponse = Invoke-RackpadRequest -Session ($Session) -Route ($ResolvedUpdateRoute) -Method ($UpdateMethod) -Body ($UpdatePayload)
|
|
|
|
Switch ($UpdateResponse.IsSuccess)
|
|
{
|
|
{($_ -eq $True)}
|
|
{
|
|
$ResultProperties.Action = 'Updated'
|
|
$ResultProperties.Object = $UpdateResponse.Content
|
|
|
|
Switch ([String]::IsNullOrWhiteSpace("$($UpdateResponse.Content.id)") -eq $False)
|
|
{
|
|
{($_ -eq $True)}
|
|
{
|
|
$ResultProperties.Identifier = "$($UpdateResponse.Content.id)"
|
|
}
|
|
}
|
|
|
|
Write-Verbose -Message "[$([System.DateTime]::UtcNow.ToString('yyyy/MM/dd HH:mm:ss.FFF'))] - [INFO] - Updated $($EntityType) `"$($NaturalKey)`". [Identifier: $($ResultProperties.Identifier)] [Changed: $($ResultProperties.ChangedPropertyList -Join ', ')]"
|
|
}
|
|
|
|
{($_ -eq $False)}
|
|
{
|
|
$ResultProperties.Action = 'Failed'
|
|
$ResultProperties.ErrorMessage = "$($UpdateResponse.StatusDescription) $($UpdateResponse.ErrorMessage)".Trim()
|
|
|
|
Write-Warning -Message "[$([System.DateTime]::UtcNow.ToString('yyyy/MM/dd HH:mm:ss.FFF'))] - [WARNING] - Failed to update $($EntityType) `"$($NaturalKey)`". [Status Code: $($UpdateResponse.StatusCode)] [$($ResultProperties.ErrorMessage)]"
|
|
}
|
|
}
|
|
#endregion
|
|
|
|
Write-Output -InputObject (New-Object -TypeName 'System.Management.Automation.PSObject' -Property ($ResultProperties))
|
|
}
|
|
Catch
|
|
{
|
|
Throw
|
|
}
|
|
}
|
|
#endregion
|