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>
245 lines
14 KiB
PowerShell
245 lines
14 KiB
PowerShell
#region Get-HyperVStorageLocation
|
|
Function Get-HyperVStorageLocation
|
|
{
|
|
<#
|
|
.SYNOPSIS
|
|
Dynamically determines where virtual machines and virtual hard disks should be stored on the hypervisor.
|
|
|
|
.DESCRIPTION
|
|
The paths that are configured on the hypervisor are preferred, but only when the volume that hosts them is a fixed volume that has enough free space available.
|
|
When those paths are unsuitable, the fixed volume with the most free space available is selected instead.
|
|
|
|
.PARAMETER RootDirectory
|
|
An optional folder path that overrides the automatic determination.
|
|
|
|
.PARAMETER MinimumFreeSpaceGB
|
|
The amount of free space, in gigabytes, that a volume must have available before it is considered suitable.
|
|
|
|
.PARAMETER CreateDirectories
|
|
Create the resulting directories when they do not exist.
|
|
|
|
.PARAMETER ContinueOnError
|
|
Ignore failures.
|
|
|
|
.EXAMPLE
|
|
$GetHyperVStorageLocationResult = Get-HyperVStorageLocation -MinimumFreeSpaceGB 64 -CreateDirectories -Verbose
|
|
|
|
Write-Output -InputObject ($GetHyperVStorageLocationResult.VirtualHardDiskDirectory.FullName)
|
|
|
|
.NOTES
|
|
The hypervisor default paths are typically located on the system volume, which is rarely the most appropriate location for a lab.
|
|
|
|
.LINK
|
|
https://learn.microsoft.com/en-us/powershell/module/hyper-v/get-vmhost
|
|
#>
|
|
|
|
[CmdletBinding()]
|
|
Param
|
|
(
|
|
[Parameter(Mandatory=$False)]
|
|
[AllowNull()]
|
|
[Alias('RD')]
|
|
[System.IO.DirectoryInfo]$RootDirectory,
|
|
|
|
[Parameter(Mandatory=$False)]
|
|
[ValidateRange(1, 4096)]
|
|
[Alias('MFS')]
|
|
[System.Int32]$MinimumFreeSpaceGB,
|
|
|
|
[Parameter(Mandatory=$False)]
|
|
[Alias('CD')]
|
|
[Switch]$CreateDirectories,
|
|
|
|
[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)
|
|
{
|
|
{($MinimumFreeSpaceGB -le 0)}
|
|
{
|
|
[System.Int32]$MinimumFreeSpaceGB = 64
|
|
}
|
|
}
|
|
#endregion
|
|
|
|
$OutputObjectProperties = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary'
|
|
$OutputObjectProperties.RootDirectory = $Null
|
|
$OutputObjectProperties.VirtualMachineDirectory = $Null
|
|
$OutputObjectProperties.VirtualHardDiskDirectory = $Null
|
|
$OutputObjectProperties.VolumeName = $Null
|
|
$OutputObjectProperties.VolumeFreeSpaceGB = 0
|
|
$OutputObjectProperties.WasAutomaticallyDetermined = $False
|
|
|
|
[System.Int64]$MinimumFreeSpaceBytes = $MinimumFreeSpaceGB * 1GB
|
|
|
|
$VolumeList = Get-CIMInstance -Namespace 'Root\CIMv2' -ClassName 'Win32_LogicalDisk' -Property @('*') | Where-Object {($_.DriveType -eq 3) -and ($_.FreeSpace -gt 0)} | Sort-Object -Property @('FreeSpace') -Descending
|
|
|
|
ForEach ($Volume In $VolumeList)
|
|
{
|
|
$WriteLogMessage.Invoke(0, @("Detected the fixed volume `"$($Volume.DeviceID)`". [Size: $([System.Math]::Round($Volume.Size / 1GB, 2)) GB] [Free Space: $([System.Math]::Round($Volume.FreeSpace / 1GB, 2)) GB] [File System: $($Volume.FileSystem)]"))
|
|
}
|
|
|
|
#region Determine the root directory
|
|
Switch ($True)
|
|
{
|
|
{($Null -ine $RootDirectory) -and ([System.String]::IsNullOrEmpty($RootDirectory.FullName) -eq $False)}
|
|
{
|
|
$WriteLogMessage.Invoke(0, @("The virtual machine root directory was explicitly specified. [Path: $($RootDirectory.FullName)]"))
|
|
|
|
$OutputObjectProperties.RootDirectory = $RootDirectory
|
|
}
|
|
|
|
Default
|
|
{
|
|
$OutputObjectProperties.WasAutomaticallyDetermined = $True
|
|
|
|
$VMHostDetails = Try {Hyper-V\Get-VMHost -ErrorAction SilentlyContinue} Catch {$Null}
|
|
|
|
[Boolean]$UseHypervisorDefaultPath = $False
|
|
|
|
Switch ($Null -ine $VMHostDetails)
|
|
{
|
|
{($_ -eq $True)}
|
|
{
|
|
$WriteLogMessage.Invoke(0, @("Hypervisor Virtual Machine Path: $($VMHostDetails.VirtualMachinePath)", "Hypervisor Virtual Hard Disk Path: $($VMHostDetails.VirtualHardDiskPath)"))
|
|
|
|
[System.String]$HypervisorVolumeName = Try {[System.IO.Path]::GetPathRoot($VMHostDetails.VirtualHardDiskPath).TrimEnd([System.IO.Path]::DirectorySeparatorChar)} Catch {$Null}
|
|
|
|
$HypervisorVolume = $VolumeList | Where-Object {($_.DeviceID -ieq $HypervisorVolumeName)} | Select-Object -First 1
|
|
|
|
$UseHypervisorDefaultPath = (($Null -ine $HypervisorVolume) -and ($HypervisorVolume.FreeSpace -ge $MinimumFreeSpaceBytes))
|
|
}
|
|
}
|
|
|
|
Switch ($UseHypervisorDefaultPath)
|
|
{
|
|
{($_ -eq $True)}
|
|
{
|
|
$WriteLogMessage.Invoke(0, @("The hypervisor default storage location will be used. [Volume: $($HypervisorVolume.DeviceID)] [Free Space: $([System.Math]::Round($HypervisorVolume.FreeSpace / 1GB, 2)) GB]"))
|
|
|
|
$OutputObjectProperties.RootDirectory = [System.IO.DirectoryInfo][System.IO.Path]::Combine($VMHostDetails.VirtualMachinePath)
|
|
$OutputObjectProperties.VirtualMachineDirectory = [System.IO.DirectoryInfo][System.IO.Path]::Combine($VMHostDetails.VirtualMachinePath)
|
|
$OutputObjectProperties.VirtualHardDiskDirectory = [System.IO.DirectoryInfo][System.IO.Path]::Combine($VMHostDetails.VirtualHardDiskPath)
|
|
$OutputObjectProperties.VolumeName = $HypervisorVolume.DeviceID
|
|
$OutputObjectProperties.VolumeFreeSpaceGB = [System.Math]::Round($HypervisorVolume.FreeSpace / 1GB, 2)
|
|
}
|
|
|
|
{($_ -eq $False)}
|
|
{
|
|
$SelectedVolume = $VolumeList | Where-Object {($_.FreeSpace -ge $MinimumFreeSpaceBytes)} | Select-Object -First 1
|
|
|
|
Switch ($Null -ieq $SelectedVolume)
|
|
{
|
|
{($_ -eq $True)}
|
|
{
|
|
Throw "A fixed volume with at least $($MinimumFreeSpaceGB) GB of free space available could not be located."
|
|
}
|
|
}
|
|
|
|
$WriteLogMessage.Invoke(0, @("The fixed volume with the most free space available will be used. [Volume: $($SelectedVolume.DeviceID)] [Free Space: $([System.Math]::Round($SelectedVolume.FreeSpace / 1GB, 2)) GB]"))
|
|
|
|
$OutputObjectProperties.RootDirectory = [System.IO.DirectoryInfo][System.IO.Path]::Combine("$($SelectedVolume.DeviceID)$([System.IO.Path]::DirectorySeparatorChar)", 'Virtualization', 'Hyper-V')
|
|
$OutputObjectProperties.VolumeName = $SelectedVolume.DeviceID
|
|
$OutputObjectProperties.VolumeFreeSpaceGB = [System.Math]::Round($SelectedVolume.FreeSpace / 1GB, 2)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
#endregion
|
|
|
|
#region Determine the remaining directories
|
|
Switch ($Null -ieq $OutputObjectProperties.VirtualMachineDirectory)
|
|
{
|
|
{($_ -eq $True)}
|
|
{
|
|
$OutputObjectProperties.VirtualMachineDirectory = [System.IO.DirectoryInfo][System.IO.Path]::Combine($OutputObjectProperties.RootDirectory.FullName, 'Virtual Machines')
|
|
$OutputObjectProperties.VirtualHardDiskDirectory = [System.IO.DirectoryInfo][System.IO.Path]::Combine($OutputObjectProperties.RootDirectory.FullName, 'Virtual Hard Disks')
|
|
}
|
|
}
|
|
|
|
Switch ($Null -ieq $OutputObjectProperties.VolumeName)
|
|
{
|
|
{($_ -eq $True)}
|
|
{
|
|
[System.String]$RootVolumeName = Try {[System.IO.Path]::GetPathRoot($OutputObjectProperties.RootDirectory.FullName).TrimEnd([System.IO.Path]::DirectorySeparatorChar)} Catch {$Null}
|
|
|
|
$RootVolume = $VolumeList | Where-Object {($_.DeviceID -ieq $RootVolumeName)} | Select-Object -First 1
|
|
|
|
Switch ($Null -ine $RootVolume)
|
|
{
|
|
{($_ -eq $True)}
|
|
{
|
|
$OutputObjectProperties.VolumeName = $RootVolume.DeviceID
|
|
$OutputObjectProperties.VolumeFreeSpaceGB = [System.Math]::Round($RootVolume.FreeSpace / 1GB, 2)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
#endregion
|
|
|
|
#region Create the directories
|
|
Switch ($CreateDirectories.IsPresent)
|
|
{
|
|
{($_ -eq $True)}
|
|
{
|
|
$DirectoryList = New-Object -TypeName 'System.Collections.Generic.List[System.IO.DirectoryInfo]'
|
|
$DirectoryList.Add($OutputObjectProperties.VirtualMachineDirectory)
|
|
$DirectoryList.Add($OutputObjectProperties.VirtualHardDiskDirectory)
|
|
|
|
For ($DirectoryListIndex = 0; $DirectoryListIndex -lt $DirectoryList.Count; $DirectoryListIndex++)
|
|
{
|
|
$Directory = $DirectoryList[$DirectoryListIndex]
|
|
|
|
Switch ([System.IO.Directory]::Exists($Directory.FullName))
|
|
{
|
|
{($_ -eq $False)}
|
|
{
|
|
$WriteLogMessage.Invoke(0, @("Attempting to create the non-existing directory. Please Wait... [Path: $($Directory.FullName)]"))
|
|
|
|
$Null = [System.IO.Directory]::CreateDirectory($Directory.FullName)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
#endregion
|
|
|
|
$WriteLogMessage.Invoke(0, @("Virtual Machine Directory: $($OutputObjectProperties.VirtualMachineDirectory.FullName)", "Virtual Hard Disk Directory: $($OutputObjectProperties.VirtualHardDiskDirectory.FullName)"))
|
|
|
|
$OutputObject = New-Object -TypeName 'System.Management.Automation.PSObject' -Property ($OutputObjectProperties)
|
|
|
|
Write-Output -InputObject ($OutputObject)
|
|
}
|
|
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
|