672 lines
42 KiB
PowerShell
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
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|