bc3dd6c535
Deploys a fully preconfigured OPNsense appliance into a Hyper-V lab in a single execution, re-running safely because every stage detects the current state before it acts. Main script: - Hyper-V platform detection and installation, exiting 3010 only when the hypervisor itself needs a restart - Random /20 block selection out of a private base network, divided into /24 networks whose VLAN tag is the third octet of their own network address - Zone based roles, with five server zones paired by index to five client zones, plus Management, Infrastructure, DMZ, Storage and Guest - Generated OPNsense config.xml delivered on a FAT32 VHDX at conf/config.xml - Generation 2 virtual machine with secure boot disabled and a LAN trunk carrying VLANs 1-4094 - Marker scoped teardown via RemoveExistingDeployment Toolkit functions: - Save-ToolkitModule, Install-HyperVPlatform, Test-PendingReboot - Expand-CompressedFile, Get-OPNSenseInstallationMedia - Get-HyperVStorageLocation, Get-HostUpstreamDNSConfiguration - New-RandomPassword, New-OPNSensePasswordHash - New-OPNSenseNetworkPlan, New-OPNSenseConfigurationDocument, Save-OPNSenseConfigurationDocument, New-OPNSenseConfigurationDisk - Initialize-OPNSenseVirtualSwitch, New-OPNSenseVirtualMachine, Remove-OPNSenseDeployment Configuration document covers interfaces, VLANs, Kea DHCPv4 scopes with PXE options, Unbound, outbound NAT, six firewall aliases and an ordered rule set that grants management full reach, allows the jump hosts over well known management ports, forces name resolution to approved resolvers, and pairs the client and server zones. Bundles 7-Zip, because the tar.exe included with Windows cannot read a raw bzip2 stream, and BCrypt.Net-Next for the appliance password hash. docs: Add readme with execution flow and generated per function reference docs: Add design specification under .ai/specification Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
261 lines
14 KiB
PowerShell
261 lines
14 KiB
PowerShell
#region Save-ToolkitModule
|
|
Function Save-ToolkitModule
|
|
{
|
|
<#
|
|
.SYNOPSIS
|
|
Idempotently saves one or more powershell modules into the toolkit module directory.
|
|
|
|
.DESCRIPTION
|
|
The module is only downloaded when it is not already present within the destination directory, or when the requested version is newer than the version that is already present.
|
|
This allows the parent script to be executed repeatedly without incurring repeated downloads.
|
|
|
|
.PARAMETER Name
|
|
One or more powershell module name(s) to save.
|
|
|
|
.PARAMETER Destination
|
|
A valid folder path. If the folder does not exist, it will be created. This is typically the "Toolkit\Modules" directory.
|
|
|
|
.PARAMETER RequiredVersion
|
|
An optional specific module version to save. When omitted, the latest available version will be saved.
|
|
|
|
.PARAMETER Repository
|
|
The powershell repository to retrieve the module(s) from.
|
|
|
|
.PARAMETER Force
|
|
Save the module(s) even if they are already present within the destination directory.
|
|
|
|
.PARAMETER ContinueOnError
|
|
Ignore failures.
|
|
|
|
.EXAMPLE
|
|
$SaveToolkitModuleParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary'
|
|
$SaveToolkitModuleParameters.Name = New-Object -TypeName 'System.Collections.Generic.List[System.String]'
|
|
$SaveToolkitModuleParameters.Name.Add('Indented.Net.IP')
|
|
$SaveToolkitModuleParameters.Name.Add('Indented.Net.Dns')
|
|
$SaveToolkitModuleParameters.Destination = $ModulesDirectory.FullName
|
|
$SaveToolkitModuleParameters.Verbose = $True
|
|
|
|
$SaveToolkitModuleResult = Save-ToolkitModule @SaveToolkitModuleParameters
|
|
|
|
Write-Output -InputObject ($SaveToolkitModuleResult)
|
|
|
|
.NOTES
|
|
The saved module(s) are imported by the toolkit during the next execution of the parent script. Any module that is saved during the current execution is imported by this function so that it becomes immediately usable.
|
|
|
|
.LINK
|
|
https://learn.microsoft.com/en-us/powershell/module/powershellget/save-module
|
|
#>
|
|
|
|
[CmdletBinding()]
|
|
Param
|
|
(
|
|
[Parameter(Mandatory=$True)]
|
|
[ValidateNotNullOrEmpty()]
|
|
[Alias('N')]
|
|
[System.String[]]$Name,
|
|
|
|
[Parameter(Mandatory=$True)]
|
|
[ValidateNotNullOrEmpty()]
|
|
[Alias('D', 'Path')]
|
|
[System.IO.DirectoryInfo]$Destination,
|
|
|
|
[Parameter(Mandatory=$False)]
|
|
[AllowEmptyString()]
|
|
[AllowNull()]
|
|
[Alias('RV')]
|
|
[System.String]$RequiredVersion,
|
|
|
|
[Parameter(Mandatory=$False)]
|
|
[ValidateNotNullOrEmpty()]
|
|
[Alias('R')]
|
|
[System.String]$Repository,
|
|
|
|
[Parameter(Mandatory=$False)]
|
|
[Alias('F')]
|
|
[Switch]$Force,
|
|
|
|
[Parameter(Mandatory=$False)]
|
|
[Alias('COE')]
|
|
[Switch]$ContinueOnError
|
|
)
|
|
|
|
Try
|
|
{
|
|
[System.String]$CmdletName = $MyInvocation.MyCommand.Name
|
|
|
|
$WriteLogMessage.Invoke(0, @("Function `'$($CmdletName)`' is beginning. Please Wait..."))
|
|
|
|
#region Set default parameter value(s)
|
|
Switch ($True)
|
|
{
|
|
{([System.String]::IsNullOrEmpty($Repository) -eq $True) -or ([System.String]::IsNullOrWhiteSpace($Repository) -eq $True)}
|
|
{
|
|
[System.String]$Repository = 'PSGallery'
|
|
}
|
|
}
|
|
#endregion
|
|
|
|
#region Ensure that the destination directory exists
|
|
Switch ([System.IO.Directory]::Exists($Destination.FullName))
|
|
{
|
|
{($_ -eq $False)}
|
|
{
|
|
$WriteLogMessage.Invoke(0, @("Attempting to create the non-existing module destination directory. Please Wait... [Path: $($Destination.FullName)]"))
|
|
|
|
$Null = [System.IO.Directory]::CreateDirectory($Destination.FullName)
|
|
}
|
|
}
|
|
#endregion
|
|
|
|
#region Ensure that a secure transport protocol is available for the repository connection
|
|
Switch ([System.Net.ServicePointManager]::SecurityProtocol -band [System.Net.SecurityProtocolType]::Tls12)
|
|
{
|
|
{($_ -ne [System.Net.SecurityProtocolType]::Tls12)}
|
|
{
|
|
$WriteLogMessage.Invoke(0, @("Attempting to enable the TLS 1.2 security protocol for the current process. Please Wait..."))
|
|
|
|
[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor [System.Net.SecurityProtocolType]::Tls12
|
|
}
|
|
}
|
|
#endregion
|
|
|
|
$ModuleObjectList = New-Object -TypeName 'System.Collections.Generic.List[System.Management.Automation.PSObject]'
|
|
|
|
$NameListCount = ($Name | Measure-Object).Count
|
|
|
|
For ($NameListIndex = 0; $NameListIndex -lt $NameListCount; $NameListIndex++)
|
|
{
|
|
[System.String]$ModuleName = $Name[$NameListIndex]
|
|
|
|
$ModuleObjectProperties = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary'
|
|
$ModuleObjectProperties.Name = $ModuleName
|
|
$ModuleObjectProperties.Version = $Null
|
|
$ModuleObjectProperties.Path = $Null
|
|
$ModuleObjectProperties.WasAlreadyPresent = $False
|
|
$ModuleObjectProperties.WasSaved = $False
|
|
|
|
$ModuleDirectory = [System.IO.DirectoryInfo][System.IO.Path]::Combine($Destination.FullName, $ModuleName)
|
|
|
|
$AvailableModuleList = Try {Get-Module -Name ($ModuleDirectory.FullName) -ListAvailable -ErrorAction SilentlyContinue | Sort-Object -Property @('Version') -Descending} Catch {$Null}
|
|
|
|
$AvailableModule = $AvailableModuleList | Select-Object -First 1
|
|
|
|
$ModuleObjectProperties.WasAlreadyPresent = ($Null -ine $AvailableModule)
|
|
|
|
Switch (($ModuleObjectProperties.WasAlreadyPresent -eq $True) -and ($Force.IsPresent -eq $False))
|
|
{
|
|
{($_ -eq $True)}
|
|
{
|
|
$WriteLogMessage.Invoke(0, @("Skipping the download of the powershell module `"$($ModuleName)`". [Reason: Version $($AvailableModule.Version.ToString()) is already present within the toolkit.]"))
|
|
}
|
|
|
|
{($_ -eq $False)}
|
|
{
|
|
$WriteLogMessage.Invoke(0, @("Attempting to save the powershell module `"$($ModuleName)`" from the `"$($Repository)`" repository. Please Wait... [Destination: $($Destination.FullName)]"))
|
|
|
|
#region Ensure that the package provider and repository prerequisites are satisfied
|
|
$PackageProvider = Try {Get-PackageProvider -Name 'NuGet' -ErrorAction SilentlyContinue} Catch {$Null}
|
|
|
|
Switch ($Null -ieq $PackageProvider)
|
|
{
|
|
{($_ -eq $True)}
|
|
{
|
|
$WriteLogMessage.Invoke(0, @("Attempting to install the `"NuGet`" package provider. Please Wait..."))
|
|
|
|
$Null = Install-PackageProvider -Name 'NuGet' -Scope 'CurrentUser' -Force -ErrorAction Stop
|
|
}
|
|
}
|
|
|
|
$PackageRepository = Try {Get-PSRepository -Name ($Repository) -ErrorAction SilentlyContinue} Catch {$Null}
|
|
|
|
Switch (($Null -ine $PackageRepository) -and ($PackageRepository.InstallationPolicy -ine 'Trusted'))
|
|
{
|
|
{($_ -eq $True)}
|
|
{
|
|
$WriteLogMessage.Invoke(0, @("Attempting to temporarily trust the `"$($Repository)`" repository. Please Wait..."))
|
|
|
|
$Null = Set-PSRepository -Name ($Repository) -InstallationPolicy 'Trusted' -ErrorAction SilentlyContinue
|
|
}
|
|
}
|
|
#endregion
|
|
|
|
$SaveModuleParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary'
|
|
$SaveModuleParameters.Name = $ModuleName
|
|
$SaveModuleParameters.Path = $Destination.FullName
|
|
$SaveModuleParameters.Repository = $Repository
|
|
$SaveModuleParameters.Force = $True
|
|
$SaveModuleParameters.ErrorAction = 'Stop'
|
|
$SaveModuleParameters.Verbose = $False
|
|
$SaveModuleParameters.Confirm = $False
|
|
|
|
Switch ($True)
|
|
{
|
|
{([System.String]::IsNullOrEmpty($RequiredVersion) -eq $False) -and ([System.String]::IsNullOrWhiteSpace($RequiredVersion) -eq $False)}
|
|
{
|
|
$SaveModuleParameters.RequiredVersion = $RequiredVersion
|
|
}
|
|
}
|
|
|
|
$Null = Save-Module @SaveModuleParameters
|
|
|
|
$ModuleObjectProperties.WasSaved = $True
|
|
|
|
$AvailableModule = Try {Get-Module -Name ($ModuleDirectory.FullName) -ListAvailable -ErrorAction SilentlyContinue | Sort-Object -Property @('Version') -Descending | Select-Object -First 1} Catch {$Null}
|
|
}
|
|
}
|
|
|
|
Switch ($Null -ine $AvailableModule)
|
|
{
|
|
{($_ -eq $True)}
|
|
{
|
|
$ModuleObjectProperties.Version = $AvailableModule.Version
|
|
$ModuleObjectProperties.Path = [System.IO.DirectoryInfo][System.IO.Path]::Combine($AvailableModule.ModuleBase)
|
|
|
|
Switch ($Null -ieq (Get-Module -Name ($ModuleName) -ErrorAction SilentlyContinue))
|
|
{
|
|
{($_ -eq $True)}
|
|
{
|
|
$WriteLogMessage.Invoke(0, @("Attempting to import the powershell module `"$($ModuleName)`" [Version: $($AvailableModule.Version.ToString())]. Please Wait... [Path: $($AvailableModule.ModuleBase)]"))
|
|
|
|
$Null = Import-Module -Name ($AvailableModule.Path) -Global -DisableNameChecking -Force -Verbose:$False -ErrorAction Stop
|
|
}
|
|
}
|
|
}
|
|
|
|
{($_ -eq $False)}
|
|
{
|
|
$WriteLogMessage.Invoke(2, @("The powershell module `"$($ModuleName)`" could not be located within `"$($Destination.FullName)`" after the save attempt."))
|
|
}
|
|
}
|
|
|
|
$ModuleObject = New-Object -TypeName 'System.Management.Automation.PSObject' -Property ($ModuleObjectProperties)
|
|
|
|
$ModuleObjectList.Add($ModuleObject)
|
|
}
|
|
|
|
Write-Output -InputObject ($ModuleObjectList.ToArray())
|
|
}
|
|
Catch
|
|
{
|
|
$ErrorRecord = $_
|
|
|
|
Switch ($ContinueOnError.IsPresent)
|
|
{
|
|
{($_ -eq $True)}
|
|
{
|
|
$WriteLogMessage.Invoke(2, @("[Message: $($ErrorRecord.Exception.Message)] [LineNumber: $($ErrorRecord.InvocationInfo.ScriptLineNumber)] [Code: $($ErrorRecord.InvocationInfo.Line.Trim())]"))
|
|
}
|
|
|
|
{($_ -eq $False)}
|
|
{
|
|
Throw
|
|
}
|
|
}
|
|
}
|
|
Finally
|
|
{
|
|
$WriteLogMessage.Invoke(0, @("Function `'$($CmdletName)`' is completed."))
|
|
}
|
|
}
|
|
#endregion
|