Files
GraceSolutions bbdfbe3d8d Complete
2026-08-02 21:28:37 -04:00

1175 lines
86 KiB
PowerShell

#region Publish-RackpadDesiredState
Function Publish-RackpadDesiredState
{
<#
.SYNOPSIS
Applies a desired state graph to a Rackpad instance in dependency order.
.DESCRIPTION
Publication proceeds in phases so that every referenced object exists before the object that references it is written. Each
phase resolves natural keys to Rackpad identifiers using lookup tables that are seeded from the pre-loaded inventory and then
extended as objects are created.
Phase order.
01 Lab The lab is created when it does not already exist
02 VLANs VLANs are written first because ports, subnets, and SSIDs reference them
03 Rooms Rooms are written before racks
04 Racks Racks reference rooms
05 Physical devices Devices placed within a rack or a room, which have no device parent
06 Virtual devices Devices parented to a host, which requires the host to already exist
07 Virtual switches Bridges hosted by a physical device
08 Ports Ports reference their device, their VLAN, and optionally a virtual switch
09 Subnets Subnets reference VLANs
10 IP zones Address ranges that serve each room and rack, declared in the settings document
11 IP assignments Assignments reference a subnet and a device
12 Cable links Links reference two ports
13 Wireless Controllers, SSIDs, access points, radios, and client associations
DHCP scopes are never written. Every operation honors the common WhatIf parameter.
.PARAMETER Session
The session object produced by Connect-RackpadInstance.
.PARAMETER DesiredState
The desired state graph produced by ConvertTo-RackpadDesiredState.
.PARAMETER Inventory
The pre-loaded inventory produced by Get-RackpadInventory.
.PARAMETER ManagedState
The state object produced by Get-RackpadManagedState.
.PARAMETER LabID
The identifier of the lab that will receive the desired state.
.PARAMETER Options
The processing options object taken from the instance configuration.
.EXAMPLE
$PublicationSummary = Publish-RackpadDesiredState -Session $RackpadSession -DesiredState $DesiredState -Inventory $RackpadInventory -ManagedState $ManagedState -LabID $LabID -Options $InstanceConfiguration.Options
.NOTES
Author: Grace Solutions
Version: 2026.08.02.0000
#>
[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '', Justification = 'Every write is delegated to Sync-RackpadObject, which calls ShouldProcess. SupportsShouldProcess is declared here so that WhatIf propagates down the call chain.')]
[CmdletBinding(SupportsShouldProcess=$True)]
Param
(
[Parameter(Mandatory=$True)]
[ValidateNotNullOrEmpty()]
[PSObject]$Session,
[Parameter(Mandatory=$True)]
[ValidateNotNullOrEmpty()]
[PSObject]$DesiredState,
[Parameter(Mandatory=$True)]
[ValidateNotNullOrEmpty()]
[PSObject]$Inventory,
[Parameter(Mandatory=$True)]
[ValidateNotNullOrEmpty()]
[PSObject]$ManagedState,
[Parameter(Mandatory=$True)]
[ValidateNotNullOrEmpty()]
[String]$LabID,
[Parameter(Mandatory=$True)]
[ValidateNotNullOrEmpty()]
[PSObject]$Options
)
Try
{
$SyncResultList = New-Object -TypeName 'System.Collections.Generic.List[PSObject]'
#region Define the identifier lookup tables
$RoomIdentifierTable = New-Object -TypeName 'System.Collections.Generic.Dictionary[[System.String], [System.String]]'
$RackIdentifierTable = New-Object -TypeName 'System.Collections.Generic.Dictionary[[System.String], [System.String]]'
$DeviceIdentifierTable = New-Object -TypeName 'System.Collections.Generic.Dictionary[[System.String], [System.String]]'
$VirtualSwitchIdentifierTable = New-Object -TypeName 'System.Collections.Generic.Dictionary[[System.String], [System.String]]'
$PortIdentifierTable = New-Object -TypeName 'System.Collections.Generic.Dictionary[[System.String], [System.String]]'
$VLANIdentifierTable = New-Object -TypeName 'System.Collections.Generic.Dictionary[[System.String], [System.String]]'
$SubnetIdentifierTable = New-Object -TypeName 'System.Collections.Generic.Dictionary[[System.String], [System.String]]'
$SSIDIdentifierTable = New-Object -TypeName 'System.Collections.Generic.Dictionary[[System.String], [System.String]]'
$WirelessControllerIdentifierTable = New-Object -TypeName 'System.Collections.Generic.Dictionary[[System.String], [System.String]]'
$WirelessRadioIdentifierTable = New-Object -TypeName 'System.Collections.Generic.Dictionary[[System.String], [System.String]]'
ForEach ($ExistingRoom In $Inventory.RoomList) {$RoomIdentifierTable["$($ExistingRoom.name)".ToLowerInvariant()] = "$($ExistingRoom.id)"}
ForEach ($ExistingRack In $Inventory.RackList) {$RackIdentifierTable["$($ExistingRack.name)".ToLowerInvariant()] = "$($ExistingRack.id)"}
ForEach ($ExistingDevice In $Inventory.DeviceList) {$DeviceIdentifierTable["$($ExistingDevice.hostname)".ToLowerInvariant()] = "$($ExistingDevice.id)"}
ForEach ($ExistingVLAN In $Inventory.VLANList) {$VLANIdentifierTable["$($ExistingVLAN.vlanId)"] = "$($ExistingVLAN.id)"}
ForEach ($ExistingSubnet In $Inventory.SubnetList) {$SubnetIdentifierTable["$($ExistingSubnet.cidr)".ToLowerInvariant()] = "$($ExistingSubnet.id)"}
ForEach ($ExistingSSID In $Inventory.WirelessSSIDList) {$SSIDIdentifierTable["$($ExistingSSID.name)".ToLowerInvariant()] = "$($ExistingSSID.id)"}
ForEach ($ExistingController In $Inventory.WirelessControllerList) {$WirelessControllerIdentifierTable["$($ExistingController.name)".ToLowerInvariant()] = "$($ExistingController.id)"}
ForEach ($ExistingVirtualSwitch In $Inventory.VirtualSwitchList)
{
$HostDevice = $Inventory.DeviceList | Where-Object {("$($_.id)" -ieq "$($ExistingVirtualSwitch.hostDeviceId)")} | Select-Object -First 1
$VirtualSwitchIdentifierTable["$($HostDevice.hostname)|$($ExistingVirtualSwitch.name)".ToLowerInvariant()] = "$($ExistingVirtualSwitch.id)"
}
ForEach ($ExistingPort In $Inventory.PortList)
{
$PortDevice = $Inventory.DeviceList | Where-Object {("$($_.id)" -ieq "$($ExistingPort.deviceId)")} | Select-Object -First 1
$PortIdentifierTable["$($PortDevice.hostname)|$($ExistingPort.name)".ToLowerInvariant()] = "$($ExistingPort.id)"
}
ForEach ($ExistingRadio In $Inventory.WirelessRadioList)
{
$RadioDevice = $Inventory.DeviceList | Where-Object {("$($_.id)" -ieq "$($ExistingRadio.apDeviceId)")} | Select-Object -First 1
$WirelessRadioIdentifierTable["$($RadioDevice.hostname)|$($ExistingRadio.slotName)".ToLowerInvariant()] = "$($ExistingRadio.id)"
}
#endregion
#region Define the subnet containment logic used to attach IP assignments
[ScriptBlock]$TestIPAddressWithinCIDR = {
Param
(
[String]$IPAddress,
[String]$CIDR
)
$IsWithinCIDR = $False
Switch (([String]::IsNullOrWhiteSpace($IPAddress) -eq $False) -and ("$($CIDR)".Contains('/') -eq $True))
{
{($_ -eq $True)}
{
$IsWithinCIDR = Try {[Boolean](Test-SubnetMember -SubjectIPAddress "$($IPAddress)" -ObjectIPAddress "$($CIDR)" -ErrorAction Stop)} Catch {$False}
}
}
Write-Output -InputObject ($IsWithinCIDR)
}
#endregion
#region Define the phase progress reporting logic
$PhaseNumber = 0
$TotalPhaseCount = 13
[ScriptBlock]$ReportPhaseProgress = {
Param
(
[String]$PhaseName,
[Int]$ItemCount
)
$WriteProgressParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary'
$WriteProgressParameters.Activity = "Publishing the desired state to lab `"$($DesiredState.LabName)`""
$WriteProgressParameters.Status = "Phase $($PhaseNumber) of $($TotalPhaseCount): $($PhaseName) ($($ItemCount) object(s))"
$WriteProgressParameters.PercentComplete = [System.Math]::Round(($PhaseNumber / $TotalPhaseCount) * 100, 2)
$WriteProgressParameters.Id = 40
$Null = Write-Progress @WriteProgressParameters
Write-Verbose -Message "[$([System.DateTime]::UtcNow.ToString('yyyy/MM/dd HH:mm:ss.FFF'))] - [INFO] - Phase $($PhaseNumber) of $($TotalPhaseCount): $($PhaseName). [$($ItemCount) object(s)]"
}
#endregion
#region Phase 02 - VLANs
$PhaseNumber = 2
$Null = $ReportPhaseProgress.InvokeReturnAsIs('VLANs', $DesiredState.VLANList.Count)
ForEach ($DesiredVLAN In $DesiredState.VLANList)
{
$ExistingVLAN = $Inventory.VLANList | Where-Object {([Int]"0$($_.vlanId)" -eq [Int]"0$($DesiredVLAN.VLANID)")} | Select-Object -First 1
$VLANProperty = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary'
$VLANProperty.labId = "$($LabID)"
$VLANProperty.vlanId = [Int]"$($DesiredVLAN.VLANID)"
$VLANProperty.name = "$($DesiredVLAN.Name)"
$VLANProperty.description = "$($DesiredVLAN.Description)"
$VLANProperty.color = "$($DesiredVLAN.Color)"
$SyncResult = Sync-RackpadObject -Session ($Session) -EntityType 'vlan' -CollectionRoute 'vlans' -NaturalKey "$($LabID)|$($DesiredVLAN.VLANID)" -DesiredProperty ($VLANProperty) -ExistingObject ($ExistingVLAN) -ManagedState ($ManagedState) -ImmutablePropertyName @('labId')
$SyncResultList.Add($SyncResult)
Switch ([String]::IsNullOrWhiteSpace($SyncResult.Identifier) -eq $False)
{
{($_ -eq $True)}
{
$VLANIdentifierTable["$($DesiredVLAN.VLANID)"] = "$($SyncResult.Identifier)"
}
}
}
#endregion
#region Phase 03 - Rooms
$PhaseNumber = 3
$Null = $ReportPhaseProgress.InvokeReturnAsIs('Rooms', $DesiredState.RoomList.Count)
ForEach ($DesiredRoom In $DesiredState.RoomList)
{
$ExistingRoom = $Inventory.RoomList | Where-Object {("$($_.name)" -ieq "$($DesiredRoom.Name)")} | Select-Object -First 1
$RoomProperty = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary'
$RoomProperty.labId = "$($LabID)"
$RoomProperty.name = "$($DesiredRoom.Name)"
$RoomProperty.description = "$($DesiredRoom.Description)"
$RoomProperty.location = "$($DesiredRoom.Location)"
$RoomProperty.notes = "$($DesiredRoom.Notes)"
$SyncResult = Sync-RackpadObject -Session ($Session) -EntityType 'room' -CollectionRoute 'rooms' -NaturalKey "$($LabID)|$($DesiredRoom.Name)" -DesiredProperty ($RoomProperty) -ExistingObject ($ExistingRoom) -ManagedState ($ManagedState) -ImmutablePropertyName @('labId')
$SyncResultList.Add($SyncResult)
Switch ([String]::IsNullOrWhiteSpace($SyncResult.Identifier) -eq $False)
{
{($_ -eq $True)}
{
$RoomIdentifierTable["$($DesiredRoom.Name)".ToLowerInvariant()] = "$($SyncResult.Identifier)"
}
}
}
#endregion
#region Phase 04 - Racks
$PhaseNumber = 4
$Null = $ReportPhaseProgress.InvokeReturnAsIs('Racks', $DesiredState.RackList.Count)
ForEach ($DesiredRack In $DesiredState.RackList)
{
$ExistingRack = $Inventory.RackList | Where-Object {("$($_.name)" -ieq "$($DesiredRack.Name)")} | Select-Object -First 1
$RackProperty = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary'
$RackProperty.labId = "$($LabID)"
$RackProperty.name = "$($DesiredRack.Name)"
$RackProperty.totalU = [Int]"$($DesiredRack.TotalU)"
$RackProperty.description = "$($DesiredRack.Description)"
$RackProperty.location = "$($DesiredRack.Location)"
$RackProperty.notes = "$($DesiredRack.Notes)"
$RackProperty.roomId = $Null
Switch ($RoomIdentifierTable.ContainsKey("$($DesiredRack.RoomName)".ToLowerInvariant()))
{
{($_ -eq $True)}
{
$RackProperty.roomId = "$($RoomIdentifierTable["$($DesiredRack.RoomName)".ToLowerInvariant()])"
}
}
$SyncResult = Sync-RackpadObject -Session ($Session) -EntityType 'rack' -CollectionRoute 'racks' -NaturalKey "$($LabID)|$($DesiredRack.Name)" -DesiredProperty ($RackProperty) -ExistingObject ($ExistingRack) -ManagedState ($ManagedState) -ImmutablePropertyName @('labId')
$SyncResultList.Add($SyncResult)
Switch ([String]::IsNullOrWhiteSpace($SyncResult.Identifier) -eq $False)
{
{($_ -eq $True)}
{
$RackIdentifierTable["$($DesiredRack.Name)".ToLowerInvariant()] = "$($SyncResult.Identifier)"
}
}
}
#endregion
#region Define the rack unit allocator
# The service rejects a rack placement that carries no start unit, but a broad placement rule cannot name a unit for
# every device it matches. Anything left unpositioned is therefore assigned the highest free block that fits.
#
# Assignment is sticky. A device that already holds a position keeps it, because recomputing a position on every run
# would move equipment around in the elevation and would report a change forever. Only genuinely unpositioned devices
# are allocated, and only into space that nothing else claims.
$RackOccupancyTable = New-Object -TypeName 'System.Collections.Generic.Dictionary[[System.String], [System.Collections.Generic.HashSet[System.Int32]]]'
$RackHeightTable = New-Object -TypeName 'System.Collections.Generic.Dictionary[[System.String], [System.Int32]]'
ForEach ($ExistingRack In $Inventory.RackList)
{
$RackHeightTable["$($ExistingRack.id)"] = [Int]"0$($ExistingRack.totalU)"
}
ForEach ($DesiredRack In $DesiredState.RackList)
{
Switch ($RackIdentifierTable.ContainsKey("$($DesiredRack.Name)".ToLowerInvariant()))
{
{($_ -eq $True)}
{
$RackHeightTable["$($RackIdentifierTable["$($DesiredRack.Name)".ToLowerInvariant()])"] = [Int]"$($DesiredRack.TotalU)"
}
}
}
[ScriptBlock]$ReserveRackUnit = {
Param
(
[String]$RackIdentifier,
[Int]$StartUnit,
[Int]$UnitHeight
)
Switch ($RackOccupancyTable.ContainsKey("$($RackIdentifier)"))
{
{($_ -eq $False)}
{
$RackOccupancyTable["$($RackIdentifier)"] = New-Object -TypeName 'System.Collections.Generic.HashSet[System.Int32]'
}
}
For ($UnitOffset = 0; $UnitOffset -lt [System.Math]::Max(1, $UnitHeight); $UnitOffset++)
{
$Null = $RackOccupancyTable["$($RackIdentifier)"].Add($StartUnit - $UnitOffset)
}
}
#region Seed occupancy from what the lab already holds, so allocation never lands on top of existing equipment
ForEach ($ExistingDevice In $Inventory.DeviceList)
{
Switch (([String]::IsNullOrWhiteSpace("$($ExistingDevice.rackId)") -eq $False) -and ([Int]"0$($ExistingDevice.startU)" -gt 0))
{
{($_ -eq $True)}
{
$Null = $ReserveRackUnit.InvokeReturnAsIs("$($ExistingDevice.rackId)", [Int]"$($ExistingDevice.startU)", [Int]"0$($ExistingDevice.heightU)")
}
}
}
#endregion
#region Reserve every declared position before anything is allocated
# A device whose rack unit was named in a placement rule owns that unit regardless of where it happens to fall in the
# processing order. Reserving the declared positions in a prior pass is what stops an automatically allocated device
# from taking a unit that a later, pinned device is going to ask for, which the service would reject as an overlap.
ForEach ($DesiredDevice In $DesiredState.DeviceList)
{
$DeclaredUnitIsUsable = ("$($DesiredDevice.Placement)" -ieq 'rack') -and ([String]::IsNullOrWhiteSpace($DesiredDevice.StartU) -eq $False) -and ($RackIdentifierTable.ContainsKey("$($DesiredDevice.RackName)".ToLowerInvariant()) -eq $True)
Switch ($DeclaredUnitIsUsable)
{
{($_ -eq $True)}
{
$Null = $ReserveRackUnit.InvokeReturnAsIs("$($RackIdentifierTable["$($DesiredDevice.RackName)".ToLowerInvariant()])", [Int]"$($DesiredDevice.StartU)", [Int]"0$($DesiredDevice.HeightU)")
}
}
}
#endregion
[ScriptBlock]$AllocateRackUnit = {
Param
(
[String]$RackIdentifier,
[Int]$UnitHeight
)
$AllocatedUnit = 0
$RackTotalUnits = 42
Switch ($RackHeightTable.ContainsKey("$($RackIdentifier)"))
{
{($_ -eq $True)}
{
$RackTotalUnits = $RackHeightTable["$($RackIdentifier)"]
}
}
Switch ($RackOccupancyTable.ContainsKey("$($RackIdentifier)"))
{
{($_ -eq $False)}
{
$RackOccupancyTable["$($RackIdentifier)"] = New-Object -TypeName 'System.Collections.Generic.HashSet[System.Int32]'
}
}
$EffectiveHeight = [System.Math]::Max(1, $UnitHeight)
:CandidateUnitLoop For ($CandidateUnit = $RackTotalUnits; $CandidateUnit -ge $EffectiveHeight; $CandidateUnit--)
{
$BlockIsFree = $True
For ($UnitOffset = 0; $UnitOffset -lt $EffectiveHeight; $UnitOffset++)
{
Switch ($RackOccupancyTable["$($RackIdentifier)"].Contains($CandidateUnit - $UnitOffset))
{
{($_ -eq $True)}
{
$BlockIsFree = $False
}
}
}
Switch ($BlockIsFree)
{
{($_ -eq $True)}
{
$AllocatedUnit = $CandidateUnit
Break CandidateUnitLoop
}
}
}
Write-Output -InputObject ($AllocatedUnit)
}
#endregion
#region Phases 05 and 06 - Devices are written in two passes so that hosts exist before their guests
$DevicePassTable = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary'
$DevicePassTable.'Physical devices' = 5
$DevicePassTable.'Virtual and attached devices' = 6
ForEach ($DevicePassEntry In $DevicePassTable.GetEnumerator())
{
$PhaseNumber = [Int]"$($DevicePassEntry.Value)"
$DevicePassList = New-Object -TypeName 'System.Collections.Generic.List[PSObject]'
ForEach ($DesiredDevice In $DesiredState.DeviceList)
{
$DeviceHasParent = [String]::IsNullOrWhiteSpace($DesiredDevice.ParentHostname) -eq $False
Switch ((($PhaseNumber -eq 5) -and ($DeviceHasParent -eq $False)) -or (($PhaseNumber -eq 6) -and ($DeviceHasParent -eq $True)))
{
{($_ -eq $True)}
{
$DevicePassList.Add($DesiredDevice)
}
}
}
$Null = $ReportPhaseProgress.InvokeReturnAsIs("$($DevicePassEntry.Key)", $DevicePassList.Count)
$DevicePassListCount = $DevicePassList.Count
For ($DevicePassIndex = 0; $DevicePassIndex -lt $DevicePassListCount; $DevicePassIndex++)
{
$DesiredDevice = $DevicePassList[$DevicePassIndex]
$ExistingDevice = $Inventory.DeviceList | Where-Object {("$($_.hostname)" -ieq "$($DesiredDevice.Hostname)")} | Select-Object -First 1
$DeviceIsRackMounted = "$($DesiredDevice.Placement)" -ieq 'rack'
$DeviceProperty = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary'
$DeviceProperty.labId = "$($LabID)"
$DeviceProperty.hostname = "$($DesiredDevice.Hostname)"
$DeviceProperty.displayName = "$($DesiredDevice.DisplayName)"
$DeviceProperty.deviceType = "$($DesiredDevice.DeviceType)"
$DeviceProperty.manufacturer = "$($DesiredDevice.Manufacturer)"
$DeviceProperty.model = "$($DesiredDevice.Model)"
$DeviceProperty.serial = "$($DesiredDevice.SerialNumber)"
$DeviceProperty.managementIp = "$($DesiredDevice.ManagementIP)"
$DeviceProperty.macAddress = "$($DesiredDevice.MACAddress)"
$DeviceProperty.status = "$($DesiredDevice.Status)"
$DeviceProperty.placement = "$($DesiredDevice.Placement)"
$DeviceProperty.networkMode = "$($DesiredDevice.NetworkMode)"
$DeviceProperty.specs = "$($DesiredDevice.Specifications)"
$DeviceProperty.notes = "$($DesiredDevice.Notes)"
$DeviceProperty.tags = $DesiredDevice.TagList.ToArray()
$DeviceProperty.cpuCores = $DesiredDevice.CPUCoreCount
$DeviceProperty.memoryGb = $DesiredDevice.MemoryGB
$DeviceProperty.storageGb = $DesiredDevice.StorageGB
$DeviceProperty.rackId = $Null
$DeviceProperty.roomId = $Null
$DeviceProperty.parentDeviceId = $Null
#region Rack geometry keys are omitted entirely unless the device is rack mounted
# The service supplies its own defaults for startU, heightU, face, and rackSlot, so transmitting a
# null would be answered with that default and every subsequent run would report a difference that
# can never be satisfied. Omitting the keys is what the user interface itself does.
Switch ($DeviceIsRackMounted)
{
{($_ -eq $True)}
{
$DeviceProperty.startU = $Null
$DeviceProperty.heightU = [Int]"0$($DesiredDevice.HeightU)"
$DeviceProperty.face = "$($DesiredDevice.Face)"
$DeviceProperty.rackSlot = "$($DesiredDevice.RackSlot)"
}
}
#endregion
Switch ($DeviceIsRackMounted)
{
{($_ -eq $True)}
{
Switch ($RackIdentifierTable.ContainsKey("$($DesiredDevice.RackName)".ToLowerInvariant()))
{
{($_ -eq $True)}
{
$DeviceProperty.rackId = "$($RackIdentifierTable["$($DesiredDevice.RackName)".ToLowerInvariant()])"
}
}
#region Resolve the rack unit, preferring what was declared, then what the device already holds, then a fresh allocation
$ResolvedStartUnit = 0
Switch ([String]::IsNullOrWhiteSpace($DesiredDevice.StartU) -eq $False)
{
{($_ -eq $True)}
{
$ResolvedStartUnit = [Int]"$($DesiredDevice.StartU)"
}
}
Switch (($ResolvedStartUnit -le 0) -and ([Int]"0$($ExistingDevice.startU)" -gt 0) -and ("$($ExistingDevice.rackId)" -ieq "$($DeviceProperty.rackId)"))
{
{($_ -eq $True)}
{
$ResolvedStartUnit = [Int]"$($ExistingDevice.startU)"
}
}
Switch (($ResolvedStartUnit -le 0) -and ([String]::IsNullOrWhiteSpace($DeviceProperty.rackId) -eq $False))
{
{($_ -eq $True)}
{
$ResolvedStartUnit = [Int]$AllocateRackUnit.InvokeReturnAsIs("$($DeviceProperty.rackId)", [Int]"0$($DesiredDevice.HeightU)")
Switch ($ResolvedStartUnit -gt 0)
{
{($_ -eq $True)}
{
Write-Verbose -Message "[$([System.DateTime]::UtcNow.ToString('yyyy/MM/dd HH:mm:ss.FFF'))] - [INFO] - `"$($DesiredDevice.Hostname)`" was assigned rack unit $($ResolvedStartUnit), which was the highest free block that fits."
}
}
}
}
#region A rack with no room left falls back to loose room placement rather than failing the write
Switch ($ResolvedStartUnit -gt 0)
{
{($_ -eq $True)}
{
$DeviceProperty.startU = $ResolvedStartUnit
$Null = $ReserveRackUnit.InvokeReturnAsIs("$($DeviceProperty.rackId)", $ResolvedStartUnit, [Int]"0$($DesiredDevice.HeightU)")
}
{($_ -eq $False)}
{
Write-Warning -Message "[$([System.DateTime]::UtcNow.ToString('yyyy/MM/dd HH:mm:ss.FFF'))] - [WARNING] - `"$($DesiredDevice.Hostname)`" could not be given a rack unit in `"$($DesiredDevice.RackName)`" and will be placed loose in the room instead. [Reason: The rack has no free block of $([System.Math]::Max(1, [Int]"0$($DesiredDevice.HeightU)")) unit(s).]"
$DeviceProperty.placement = 'room'
$DeviceProperty.rackId = $Null
$DeviceProperty.Remove('startU')
$DeviceProperty.Remove('heightU')
$DeviceProperty.Remove('face')
$DeviceProperty.Remove('rackSlot')
}
}
#endregion
#endregion
}
{($_ -eq $False)}
{
Switch ($RoomIdentifierTable.ContainsKey("$($DesiredDevice.RoomName)".ToLowerInvariant()))
{
{($_ -eq $True)}
{
$DeviceProperty.roomId = "$($RoomIdentifierTable["$($DesiredDevice.RoomName)".ToLowerInvariant()])"
}
}
}
}
Switch ($DeviceIdentifierTable.ContainsKey("$($DesiredDevice.ParentHostname)".ToLowerInvariant()))
{
{($_ -eq $True)}
{
$DeviceProperty.parentDeviceId = "$($DeviceIdentifierTable["$($DesiredDevice.ParentHostname)".ToLowerInvariant()])"
}
}
$SyncResult = Sync-RackpadObject -Session ($Session) -EntityType 'device' -CollectionRoute 'devices' -NaturalKey "$($LabID)|$($DesiredDevice.Hostname)" -DesiredProperty ($DeviceProperty) -ExistingObject ($ExistingDevice) -ManagedState ($ManagedState) -ImmutablePropertyName @('labId')
$SyncResultList.Add($SyncResult)
Switch ([String]::IsNullOrWhiteSpace($SyncResult.Identifier) -eq $False)
{
{($_ -eq $True)}
{
$DeviceIdentifierTable["$($DesiredDevice.Hostname)".ToLowerInvariant()] = "$($SyncResult.Identifier)"
}
}
}
}
#endregion
#region Phase 07 - Virtual switches
$PhaseNumber = 7
$Null = $ReportPhaseProgress.InvokeReturnAsIs('Virtual switches', $DesiredState.VirtualSwitchList.Count)
ForEach ($DesiredVirtualSwitch In $DesiredState.VirtualSwitchList)
{
$HostDeviceIdentifier = ''
Switch ($DeviceIdentifierTable.ContainsKey("$($DesiredVirtualSwitch.HostHostname)".ToLowerInvariant()))
{
{($_ -eq $True)}
{
$HostDeviceIdentifier = "$($DeviceIdentifierTable["$($DesiredVirtualSwitch.HostHostname)".ToLowerInvariant()])"
}
}
Switch ([String]::IsNullOrWhiteSpace($HostDeviceIdentifier) -eq $False)
{
{($_ -eq $True)}
{
$ExistingVirtualSwitch = $Inventory.VirtualSwitchList | Where-Object {("$($_.hostDeviceId)" -ieq "$($HostDeviceIdentifier)") -and ("$($_.name)" -ieq "$($DesiredVirtualSwitch.Name)")} | Select-Object -First 1
$VirtualSwitchProperty = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary'
$VirtualSwitchProperty.hostDeviceId = "$($HostDeviceIdentifier)"
$VirtualSwitchProperty.name = "$($DesiredVirtualSwitch.Name)"
$VirtualSwitchProperty.kind = "$($DesiredVirtualSwitch.Kind)"
$VirtualSwitchProperty.notes = "$($DesiredVirtualSwitch.Notes)"
$SyncResult = Sync-RackpadObject -Session ($Session) -EntityType 'virtual-switch' -CollectionRoute 'virtual-switches' -NaturalKey "$($DesiredVirtualSwitch.HostHostname)|$($DesiredVirtualSwitch.Name)" -DesiredProperty ($VirtualSwitchProperty) -ExistingObject ($ExistingVirtualSwitch) -ManagedState ($ManagedState) -ImmutablePropertyName @('hostDeviceId')
$SyncResultList.Add($SyncResult)
Switch ([String]::IsNullOrWhiteSpace($SyncResult.Identifier) -eq $False)
{
{($_ -eq $True)}
{
$VirtualSwitchIdentifierTable["$($DesiredVirtualSwitch.HostHostname)|$($DesiredVirtualSwitch.Name)".ToLowerInvariant()] = "$($SyncResult.Identifier)"
}
}
}
{($_ -eq $False)}
{
Write-Warning -Message "[$([System.DateTime]::UtcNow.ToString('yyyy/MM/dd HH:mm:ss.FFF'))] - [WARNING] - Skipping virtual switch `"$($DesiredVirtualSwitch.Name)`". [Reason: The host device `"$($DesiredVirtualSwitch.HostHostname)`" could not be resolved.]"
}
}
}
#endregion
#region Phase 08 - Ports
$PhaseNumber = 8
$Null = $ReportPhaseProgress.InvokeReturnAsIs('Ports', $DesiredState.PortList.Count)
$DesiredPortListCount = $DesiredState.PortList.Count
For ($DesiredPortIndex = 0; $DesiredPortIndex -lt $DesiredPortListCount; $DesiredPortIndex++)
{
$DesiredPort = $DesiredState.PortList[$DesiredPortIndex]
$PortDeviceIdentifier = ''
Switch ($DeviceIdentifierTable.ContainsKey("$($DesiredPort.DeviceHostname)".ToLowerInvariant()))
{
{($_ -eq $True)}
{
$PortDeviceIdentifier = "$($DeviceIdentifierTable["$($DesiredPort.DeviceHostname)".ToLowerInvariant()])"
}
}
Switch ([String]::IsNullOrWhiteSpace($PortDeviceIdentifier) -eq $False)
{
{($_ -eq $True)}
{
$ExistingPort = $Inventory.PortList | Where-Object {("$($_.deviceId)" -ieq "$($PortDeviceIdentifier)") -and ("$($_.name)" -ieq "$($DesiredPort.Name)")} | Select-Object -First 1
$PortProperty = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary'
$PortProperty.deviceId = "$($PortDeviceIdentifier)"
$PortProperty.name = "$($DesiredPort.Name)"
$PortProperty.position = [Int]"0$($DesiredPort.Position)"
$PortProperty.kind = "$($DesiredPort.Kind)"
$PortProperty.speed = "$($DesiredPort.Speed)"
$PortProperty.mode = "$($DesiredPort.Mode)"
$PortProperty.linkState = "$($DesiredPort.LinkState)"
$PortProperty.face = "$($DesiredPort.Face)"
$PortProperty.description = "$($DesiredPort.Description)"
$PortProperty.macAddress = "$($DesiredPort.MACAddress)"
$PortProperty.vlanId = $Null
$PortProperty.virtualSwitchId = $Null
Switch ($VLANIdentifierTable.ContainsKey("$($DesiredPort.VLANID)"))
{
{($_ -eq $True)}
{
$PortProperty.vlanId = "$($VLANIdentifierTable["$($DesiredPort.VLANID)"])"
}
}
Switch ($VirtualSwitchIdentifierTable.ContainsKey("$($DesiredPort.VirtualSwitchHostHostname)|$($DesiredPort.VirtualSwitchName)".ToLowerInvariant()))
{
{($_ -eq $True)}
{
$PortProperty.virtualSwitchId = "$($VirtualSwitchIdentifierTable["$($DesiredPort.VirtualSwitchHostHostname)|$($DesiredPort.VirtualSwitchName)".ToLowerInvariant()])"
}
}
$SyncResult = Sync-RackpadObject -Session ($Session) -EntityType 'port' -CollectionRoute 'ports' -NaturalKey "$($DesiredPort.DeviceHostname)|$($DesiredPort.Name)" -DesiredProperty ($PortProperty) -ExistingObject ($ExistingPort) -ManagedState ($ManagedState) -ImmutablePropertyName @('deviceId')
$SyncResultList.Add($SyncResult)
Switch ([String]::IsNullOrWhiteSpace($SyncResult.Identifier) -eq $False)
{
{($_ -eq $True)}
{
$PortIdentifierTable["$($DesiredPort.DeviceHostname)|$($DesiredPort.Name)".ToLowerInvariant()] = "$($SyncResult.Identifier)"
}
}
}
}
}
#endregion
#region Phase 09 - Subnets
$PhaseNumber = 9
$Null = $ReportPhaseProgress.InvokeReturnAsIs('Subnets', $DesiredState.SubnetList.Count)
ForEach ($DesiredSubnet In $DesiredState.SubnetList)
{
$ExistingSubnet = $Inventory.SubnetList | Where-Object {("$($_.cidr)" -ieq "$($DesiredSubnet.CIDR)")} | Select-Object -First 1
$SubnetProperty = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary'
$SubnetProperty.labId = "$($LabID)"
$SubnetProperty.cidr = "$($DesiredSubnet.CIDR)"
$SubnetProperty.name = "$($DesiredSubnet.Name)"
$SubnetProperty.description = "$($DesiredSubnet.Description)"
$SubnetProperty.gateway = "$($DesiredSubnet.Gateway)"
$SubnetProperty.dnsServers = $DesiredSubnet.DNSServerList.ToArray()
$SubnetProperty.vlanId = $Null
Switch ($VLANIdentifierTable.ContainsKey("$($DesiredSubnet.VLANID)"))
{
{($_ -eq $True)}
{
$SubnetProperty.vlanId = "$($VLANIdentifierTable["$($DesiredSubnet.VLANID)"])"
}
}
$SyncResult = Sync-RackpadObject -Session ($Session) -EntityType 'subnet' -CollectionRoute 'subnets' -NaturalKey "$($LabID)|$($DesiredSubnet.CIDR)" -DesiredProperty ($SubnetProperty) -ExistingObject ($ExistingSubnet) -ManagedState ($ManagedState) -ImmutablePropertyName @('labId')
$SyncResultList.Add($SyncResult)
Switch ([String]::IsNullOrWhiteSpace($SyncResult.Identifier) -eq $False)
{
{($_ -eq $True)}
{
$SubnetIdentifierTable["$($DesiredSubnet.CIDR)".ToLowerInvariant()] = "$($SyncResult.Identifier)"
}
}
}
#endregion
#region Phase 10 - IP zones, which are the address ranges that serve each room and rack
$PhaseNumber = 10
$Null = $ReportPhaseProgress.InvokeReturnAsIs('IP zones', $DesiredState.IPZoneList.Count)
ForEach ($DesiredIPZone In $DesiredState.IPZoneList)
{
Switch ($SubnetIdentifierTable.ContainsKey("$($DesiredIPZone.SubnetCIDR)".ToLowerInvariant()))
{
{($_ -eq $True)}
{
$ZoneSubnetIdentifier = "$($SubnetIdentifierTable["$($DesiredIPZone.SubnetCIDR)".ToLowerInvariant()])"
$ExistingIPZone = $Inventory.IPZoneList | Where-Object {("$($_.subnetId)" -ieq "$($ZoneSubnetIdentifier)") -and ("$($_.startIp)" -ieq "$($DesiredIPZone.StartIP)") -and ("$($_.endIp)" -ieq "$($DesiredIPZone.EndIP)")} | Select-Object -First 1
$IPZoneProperty = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary'
$IPZoneProperty.subnetId = "$($ZoneSubnetIdentifier)"
$IPZoneProperty.kind = "$($DesiredIPZone.Kind)"
$IPZoneProperty.startIp = "$($DesiredIPZone.StartIP)"
$IPZoneProperty.endIp = "$($DesiredIPZone.EndIP)"
$IPZoneProperty.description = "$($DesiredIPZone.Description)"
$SyncResult = Sync-RackpadObject -Session ($Session) -EntityType 'ip-zone' -CollectionRoute 'ip-zones' -NaturalKey "$($DesiredIPZone.SubnetCIDR)|$($DesiredIPZone.StartIP)|$($DesiredIPZone.EndIP)" -DesiredProperty ($IPZoneProperty) -ExistingObject ($ExistingIPZone) -ManagedState ($ManagedState) -ImmutablePropertyName @('subnetId')
$SyncResultList.Add($SyncResult)
}
{($_ -eq $False)}
{
Write-Warning -Message "[$([System.DateTime]::UtcNow.ToString('yyyy/MM/dd HH:mm:ss.FFF'))] - [WARNING] - Skipping the IP range $($DesiredIPZone.StartIP) to $($DesiredIPZone.EndIP). [Reason: The subnet `"$($DesiredIPZone.SubnetCIDR)`" was not discovered by any source.]"
}
}
}
#endregion
#region Phase 11 - IP assignments
$PhaseNumber = 11
$Null = $ReportPhaseProgress.InvokeReturnAsIs('IP assignments', $DesiredState.IPAssignmentList.Count)
$ObservedIPAssignmentKeySet = New-Object -TypeName 'System.Collections.Generic.HashSet[System.String]'
ForEach ($DesiredIPAssignment In $DesiredState.IPAssignmentList)
{
#region Resolve the subnet that contains this address
$ResolvedSubnetCIDR = ''
ForEach ($DesiredSubnet In $DesiredState.SubnetList)
{
Switch (($TestIPAddressWithinCIDR.InvokeReturnAsIs("$($DesiredIPAssignment.IPAddress)", "$($DesiredSubnet.CIDR)") -eq $True) -and ([String]::IsNullOrWhiteSpace($ResolvedSubnetCIDR) -eq $True))
{
{($_ -eq $True)}
{
$ResolvedSubnetCIDR = "$($DesiredSubnet.CIDR)"
}
}
}
#endregion
$AssignmentIsResolvable = ($SubnetIdentifierTable.ContainsKey("$($ResolvedSubnetCIDR)".ToLowerInvariant()) -eq $True) -and ($DeviceIdentifierTable.ContainsKey("$($DesiredIPAssignment.DeviceHostname)".ToLowerInvariant()) -eq $True)
$AssignmentNaturalKey = "$($ResolvedSubnetCIDR)|$($DesiredIPAssignment.IPAddress)".ToLowerInvariant()
Switch (($AssignmentIsResolvable -eq $True) -and ($ObservedIPAssignmentKeySet.Add($AssignmentNaturalKey) -eq $True))
{
{($_ -eq $True)}
{
$SubnetIdentifier = "$($SubnetIdentifierTable["$($ResolvedSubnetCIDR)".ToLowerInvariant()])"
$ExistingIPAssignment = $Inventory.IPAssignmentList | Where-Object {("$($_.subnetId)" -ieq "$($SubnetIdentifier)") -and ("$($_.ipAddress)" -ieq "$($DesiredIPAssignment.IPAddress)")} | Select-Object -First 1
#region The address that a subnet uses as its gateway cannot be documented as an ordinary endpoint
# The service rejects that combination and asks for an interface, reserved, or infrastructure
# classification instead, which is reasonable: a routed edge holding its own gateway address is
# infrastructure rather than a host that happens to sit on the network.
$ResolvedAssignmentType = "$($DesiredIPAssignment.AssignmentType)"
ForEach ($DesiredSubnet In $DesiredState.SubnetList)
{
Switch (("$($DesiredSubnet.CIDR)" -ieq "$($ResolvedSubnetCIDR)") -and ("$($DesiredSubnet.Gateway)" -ieq "$($DesiredIPAssignment.IPAddress)") -and ([String]::IsNullOrWhiteSpace($DesiredSubnet.Gateway) -eq $False))
{
{($_ -eq $True)}
{
$ResolvedAssignmentType = 'infrastructure'
}
}
}
#endregion
$IPAssignmentProperty = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary'
$IPAssignmentProperty.subnetId = "$($SubnetIdentifier)"
$IPAssignmentProperty.ipAddress = "$($DesiredIPAssignment.IPAddress)"
$IPAssignmentProperty.assignmentType = "$($ResolvedAssignmentType)"
$IPAssignmentProperty.allocationMode = "$($DesiredIPAssignment.AllocationMode)"
$IPAssignmentProperty.deviceId = "$($DeviceIdentifierTable["$($DesiredIPAssignment.DeviceHostname)".ToLowerInvariant()])"
$IPAssignmentProperty.hostname = "$($DesiredIPAssignment.Hostname)"
$IPAssignmentProperty.description = "$($DesiredIPAssignment.Description)"
$IPAssignmentProperty.dhcpScopeId = $Null
$SyncResult = Sync-RackpadObject -Session ($Session) -EntityType 'ip-assignment' -CollectionRoute 'ip-assignments' -NaturalKey "$($AssignmentNaturalKey)" -DesiredProperty ($IPAssignmentProperty) -ExistingObject ($ExistingIPAssignment) -ManagedState ($ManagedState) -ImmutablePropertyName @('subnetId')
$SyncResultList.Add($SyncResult)
}
}
}
#endregion
#region Phase 12 - Cable links
$PhaseNumber = 12
$Null = $ReportPhaseProgress.InvokeReturnAsIs('Cable links', $DesiredState.PortLinkList.Count)
$ObservedPortLinkKeySet = New-Object -TypeName 'System.Collections.Generic.HashSet[System.String]'
ForEach ($DesiredPortLink In $DesiredState.PortLinkList)
{
$FromPortKey = "$($DesiredPortLink.FromDeviceHostname)|$($DesiredPortLink.FromPortName)".ToLowerInvariant()
$ToPortKey = "$($DesiredPortLink.ToDeviceHostname)|$($DesiredPortLink.ToPortName)".ToLowerInvariant()
$LinkIsResolvable = ($PortIdentifierTable.ContainsKey($FromPortKey) -eq $True) -and ($PortIdentifierTable.ContainsKey($ToPortKey) -eq $True)
$LinkNaturalKey = (@("$($FromPortKey)", "$($ToPortKey)") | Sort-Object) -Join '<->'
Switch (($LinkIsResolvable -eq $True) -and ($ObservedPortLinkKeySet.Add($LinkNaturalKey) -eq $True))
{
{($_ -eq $True)}
{
$FromPortIdentifier = "$($PortIdentifierTable[$FromPortKey])"
$ToPortIdentifier = "$($PortIdentifierTable[$ToPortKey])"
$ExistingPortLink = $Inventory.PortLinkList | Where-Object {(("$($_.fromPortId)" -ieq "$($FromPortIdentifier)") -and ("$($_.toPortId)" -ieq "$($ToPortIdentifier)")) -or (("$($_.fromPortId)" -ieq "$($ToPortIdentifier)") -and ("$($_.toPortId)" -ieq "$($FromPortIdentifier)"))} | Select-Object -First 1
$PortLinkProperty = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary'
$PortLinkProperty.fromPortId = "$($FromPortIdentifier)"
$PortLinkProperty.toPortId = "$($ToPortIdentifier)"
$PortLinkProperty.cableType = "$($DesiredPortLink.CableType)"
$PortLinkProperty.cableLength = "$($DesiredPortLink.CableLength)"
$PortLinkProperty.color = "$($DesiredPortLink.Color)"
$PortLinkProperty.notes = "$($DesiredPortLink.Notes)"
$SyncResult = Sync-RackpadObject -Session ($Session) -EntityType 'port-link' -CollectionRoute 'port-links' -NaturalKey "$($LinkNaturalKey)" -DesiredProperty ($PortLinkProperty) -ExistingObject ($ExistingPortLink) -ManagedState ($ManagedState) -ImmutablePropertyName @('fromPortId', 'toPortId')
$SyncResultList.Add($SyncResult)
}
}
}
#endregion
#region Phase 13 - Wireless
$PhaseNumber = 13
Switch ($Options.ManageWireless)
{
{($_ -eq $True)}
{
$Null = $ReportPhaseProgress.InvokeReturnAsIs('Wireless', ($DesiredState.WirelessControllerList.Count + $DesiredState.WirelessSSIDList.Count + $DesiredState.WirelessAccessPointList.Count + $DesiredState.WirelessRadioList.Count + $DesiredState.WirelessAssociationList.Count))
#region Controllers
ForEach ($DesiredController In $DesiredState.WirelessControllerList)
{
$ExistingController = $Inventory.WirelessControllerList | Where-Object {("$($_.name)" -ieq "$($DesiredController.Name)")} | Select-Object -First 1
$ControllerProperty = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary'
$ControllerProperty.labId = "$($LabID)"
$ControllerProperty.name = "$($DesiredController.Name)"
$ControllerProperty.vendor = "$($DesiredController.Vendor)"
$ControllerProperty.model = "$($DesiredController.Model)"
$ControllerProperty.managementIp = "$($DesiredController.ManagementIP)"
$ControllerProperty.notes = "$($DesiredController.Notes)"
$ControllerProperty.deviceId = $Null
Switch ($DeviceIdentifierTable.ContainsKey("$($DesiredController.DeviceHostname)".ToLowerInvariant()))
{
{($_ -eq $True)}
{
$ControllerProperty.deviceId = "$($DeviceIdentifierTable["$($DesiredController.DeviceHostname)".ToLowerInvariant()])"
}
}
$SyncResult = Sync-RackpadObject -Session ($Session) -EntityType 'wifi-controller' -CollectionRoute 'wifi/controllers' -NaturalKey "$($LabID)|$($DesiredController.Name)" -DesiredProperty ($ControllerProperty) -ExistingObject ($ExistingController) -ManagedState ($ManagedState) -ImmutablePropertyName @('labId')
$SyncResultList.Add($SyncResult)
Switch ([String]::IsNullOrWhiteSpace($SyncResult.Identifier) -eq $False)
{
{($_ -eq $True)}
{
$WirelessControllerIdentifierTable["$($DesiredController.Name)".ToLowerInvariant()] = "$($SyncResult.Identifier)"
}
}
}
#endregion
#region SSIDs
ForEach ($DesiredSSID In $DesiredState.WirelessSSIDList)
{
$ExistingSSID = $Inventory.WirelessSSIDList | Where-Object {("$($_.name)" -ieq "$($DesiredSSID.Name)")} | Select-Object -First 1
$SSIDProperty = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary'
$SSIDProperty.labId = "$($LabID)"
$SSIDProperty.name = "$($DesiredSSID.Name)"
$SSIDProperty.purpose = "$($DesiredSSID.Purpose)"
$SSIDProperty.security = "$($DesiredSSID.Security)"
$SSIDProperty.hidden = [Boolean]$DesiredSSID.Hidden
$SSIDProperty.color = "$($DesiredSSID.Color)"
$SSIDProperty.vlanId = $Null
Switch ($VLANIdentifierTable.ContainsKey("$($DesiredSSID.VLANID)"))
{
{($_ -eq $True)}
{
$SSIDProperty.vlanId = "$($VLANIdentifierTable["$($DesiredSSID.VLANID)"])"
}
}
$SyncResult = Sync-RackpadObject -Session ($Session) -EntityType 'wifi-ssid' -CollectionRoute 'wifi/ssids' -NaturalKey "$($LabID)|$($DesiredSSID.Name)" -DesiredProperty ($SSIDProperty) -ExistingObject ($ExistingSSID) -ManagedState ($ManagedState) -ImmutablePropertyName @('labId')
$SyncResultList.Add($SyncResult)
Switch ([String]::IsNullOrWhiteSpace($SyncResult.Identifier) -eq $False)
{
{($_ -eq $True)}
{
$SSIDIdentifierTable["$($DesiredSSID.Name)".ToLowerInvariant()] = "$($SyncResult.Identifier)"
}
}
}
#endregion
#region Access points are addressed by device identifier and are always written using PUT
ForEach ($DesiredAccessPoint In $DesiredState.WirelessAccessPointList)
{
Switch ($DeviceIdentifierTable.ContainsKey("$($DesiredAccessPoint.DeviceHostname)".ToLowerInvariant()))
{
{($_ -eq $True)}
{
$AccessPointDeviceIdentifier = "$($DeviceIdentifierTable["$($DesiredAccessPoint.DeviceHostname)".ToLowerInvariant()])"
$ExistingAccessPoint = $Inventory.WirelessAccessPointList | Where-Object {("$($_.deviceId)" -ieq "$($AccessPointDeviceIdentifier)")} | Select-Object -First 1
$AccessPointProperty = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary'
$AccessPointProperty.controllerId = $Null
$AccessPointProperty.location = "$($DesiredAccessPoint.Location)"
$AccessPointProperty.firmwareVersion = "$($DesiredAccessPoint.FirmwareVersion)"
$AccessPointProperty.notes = "$($DesiredAccessPoint.Notes)"
Switch ($WirelessControllerIdentifierTable.ContainsKey("$($DesiredAccessPoint.ControllerName)".ToLowerInvariant()))
{
{($_ -eq $True)}
{
$AccessPointProperty.controllerId = "$($WirelessControllerIdentifierTable["$($DesiredAccessPoint.ControllerName)".ToLowerInvariant()])"
}
}
$SyncResult = Sync-RackpadObject -Session ($Session) -EntityType 'wifi-access-point' -CollectionRoute 'wifi/access-points' -NaturalKey "$($DesiredAccessPoint.DeviceHostname)" -DesiredProperty ($AccessPointProperty) -ExistingObject ($ExistingAccessPoint) -ManagedState ($ManagedState) -UpdateMethod 'PUT' -UpdateRoute "wifi/access-points/$($AccessPointDeviceIdentifier)"
$SyncResultList.Add($SyncResult)
}
}
}
#endregion
#region Radios
ForEach ($DesiredRadio In $DesiredState.WirelessRadioList)
{
Switch ($DeviceIdentifierTable.ContainsKey("$($DesiredRadio.AccessPointHostname)".ToLowerInvariant()))
{
{($_ -eq $True)}
{
$RadioDeviceIdentifier = "$($DeviceIdentifierTable["$($DesiredRadio.AccessPointHostname)".ToLowerInvariant()])"
$ExistingRadio = $Inventory.WirelessRadioList | Where-Object {("$($_.apDeviceId)" -ieq "$($RadioDeviceIdentifier)") -and ("$($_.slotName)" -ieq "$($DesiredRadio.SlotName)")} | Select-Object -First 1
$RadioSSIDIdentifierList = New-Object -TypeName 'System.Collections.Generic.List[String]'
ForEach ($RadioSSIDName In $DesiredRadio.SSIDNameList)
{
Switch ($SSIDIdentifierTable.ContainsKey("$($RadioSSIDName)".ToLowerInvariant()))
{
{($_ -eq $True)}
{
$RadioSSIDIdentifierList.Add("$($SSIDIdentifierTable["$($RadioSSIDName)".ToLowerInvariant()])")
}
}
}
$RadioProperty = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary'
$RadioProperty.apDeviceId = "$($RadioDeviceIdentifier)"
$RadioProperty.slotName = "$($DesiredRadio.SlotName)"
$RadioProperty.band = "$($DesiredRadio.Band)"
$RadioProperty.channel = "$($DesiredRadio.Channel)"
$RadioProperty.channelWidth = "$($DesiredRadio.ChannelWidth)"
$RadioProperty.txPower = "$($DesiredRadio.TransmitPower)"
$RadioProperty.ssidIds = $RadioSSIDIdentifierList.ToArray()
$RadioProperty.notes = "$($DesiredRadio.Notes)"
$SyncResult = Sync-RackpadObject -Session ($Session) -EntityType 'wifi-radio' -CollectionRoute 'wifi/radios' -NaturalKey "$($DesiredRadio.AccessPointHostname)|$($DesiredRadio.SlotName)" -DesiredProperty ($RadioProperty) -ExistingObject ($ExistingRadio) -ManagedState ($ManagedState) -ImmutablePropertyName @('apDeviceId')
$SyncResultList.Add($SyncResult)
Switch ([String]::IsNullOrWhiteSpace($SyncResult.Identifier) -eq $False)
{
{($_ -eq $True)}
{
$WirelessRadioIdentifierTable["$($DesiredRadio.AccessPointHostname)|$($DesiredRadio.SlotName)".ToLowerInvariant()] = "$($SyncResult.Identifier)"
}
}
}
}
}
#endregion
#region Client associations are addressed by the client device identifier, which is also carried within the payload
ForEach ($DesiredAssociation In $DesiredState.WirelessAssociationList)
{
$ClientDeviceKey = "$($DesiredAssociation.ClientHostname)".ToLowerInvariant()
$AccessPointKey = "$($DesiredAssociation.AccessPointHostname)".ToLowerInvariant()
$AssociationIsResolvable = ($DeviceIdentifierTable.ContainsKey($AccessPointKey) -eq $True) -and ($DeviceIdentifierTable.ContainsKey($ClientDeviceKey) -eq $True)
Switch ($AssociationIsResolvable)
{
{($_ -eq $True)}
{
$ClientDeviceIdentifier = "$($DeviceIdentifierTable[$ClientDeviceKey])"
$ExistingAssociation = $Inventory.WirelessAssociationList | Where-Object {("$($_.clientDeviceId)" -ieq "$($ClientDeviceIdentifier)")} | Select-Object -First 1
$AssociationProperty = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary'
$AssociationProperty.clientDeviceId = "$($ClientDeviceIdentifier)"
$AssociationProperty.apDeviceId = "$($DeviceIdentifierTable[$AccessPointKey])"
$AssociationProperty.radioId = $Null
$AssociationProperty.ssidId = $Null
$AssociationProperty.band = "$($DesiredAssociation.Band)"
$AssociationProperty.channel = "$($DesiredAssociation.Channel)"
$AssociationProperty.signalDbm = $DesiredAssociation.SignalDbm
$AssociationProperty.lastSeen = $Null
$AssociationProperty.lastRoamAt = $Null
$AssociationProperty.notes = "$($DesiredAssociation.Notes)"
Switch ($WirelessRadioIdentifierTable.ContainsKey("$($DesiredAssociation.AccessPointHostname)|$($DesiredAssociation.RadioSlotName)".ToLowerInvariant()))
{
{($_ -eq $True)}
{
$AssociationProperty.radioId = "$($WirelessRadioIdentifierTable["$($DesiredAssociation.AccessPointHostname)|$($DesiredAssociation.RadioSlotName)".ToLowerInvariant()])"
}
}
Switch ($SSIDIdentifierTable.ContainsKey("$($DesiredAssociation.SSIDName)".ToLowerInvariant()))
{
{($_ -eq $True)}
{
$AssociationProperty.ssidId = "$($SSIDIdentifierTable["$($DesiredAssociation.SSIDName)".ToLowerInvariant()])"
}
}
Switch ($Null -ine $DesiredAssociation.LastSeen)
{
{($_ -eq $True)}
{
$AssociationProperty.lastSeen = $DesiredAssociation.LastSeen.ToString('o')
}
}
$SyncResult = Sync-RackpadObject -Session ($Session) -EntityType 'wifi-association' -CollectionRoute 'wifi/associations' -NaturalKey "$($DesiredAssociation.ClientHostname)" -DesiredProperty ($AssociationProperty) -ExistingObject ($ExistingAssociation) -ManagedState ($ManagedState) -UpdateMethod 'PUT' -UpdateRoute "wifi/associations/$($ClientDeviceIdentifier)"
$SyncResultList.Add($SyncResult)
}
}
}
#endregion
}
{($_ -eq $False)}
{
Write-Verbose -Message "[$([System.DateTime]::UtcNow.ToString('yyyy/MM/dd HH:mm:ss.FFF'))] - [INFO] - Phase $($PhaseNumber) of $($TotalPhaseCount): Wireless was skipped because wireless management is disabled."
}
}
#endregion
$Null = Write-Progress -Activity "Publishing the desired state to lab `"$($DesiredState.LabName)`"" -Id 40 -Completed
#region Summarize the outcome by entity type
$SummaryList = New-Object -TypeName 'System.Collections.Generic.List[PSObject]'
ForEach ($SummaryGroup In ($SyncResultList | Group-Object -Property 'EntityType' | Sort-Object -Property 'Name'))
{
$SummaryProperties = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary'
$SummaryProperties.EntityType = "$($SummaryGroup.Name)"
$SummaryProperties.Created = ($SummaryGroup.Group | Where-Object {($_.Action -ieq 'Created')} | Measure-Object).Count
$SummaryProperties.Updated = ($SummaryGroup.Group | Where-Object {($_.Action -ieq 'Updated')} | Measure-Object).Count
$SummaryProperties.Unchanged = ($SummaryGroup.Group | Where-Object {($_.Action -ieq 'Unchanged')} | Measure-Object).Count
$SummaryProperties.WhatIf = ($SummaryGroup.Group | Where-Object {($_.Action -ieq 'WhatIf')} | Measure-Object).Count
$SummaryProperties.Failed = ($SummaryGroup.Group | Where-Object {($_.Action -ieq 'Failed')} | Measure-Object).Count
$SummaryList.Add((New-Object -TypeName 'System.Management.Automation.PSObject' -Property ($SummaryProperties)))
}
#endregion
$PublicationProperties = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary'
$PublicationProperties.LabID = "$($LabID)"
$PublicationProperties.LabName = "$($DesiredState.LabName)"
$PublicationProperties.CompletedAt = [System.DateTime]::UtcNow
$PublicationProperties.SummaryList = $SummaryList
$PublicationProperties.ResultList = $SyncResultList
Write-Output -InputObject (New-Object -TypeName 'System.Management.Automation.PSObject' -Property ($PublicationProperties))
}
Catch
{
Throw
}
}
#endregion