ps/Modules/Alkami.PowerShell.Configuration/Public/Update-SystemPortReservations.ps1
2023-05-30 22:51:22 -07:00

534 lines
23 KiB
PowerShell

function Update-SystemPortReservations {
<#
.SYNOPSIS
Updates the system port reservations if they haven't been set yet.
This script is intended to automate me out of a job. This script is for developer environment setup and exceptional override
.PARAMETER AddRange
Supply an array of value-tuples for creating one or more ranges.
Value tuple is defined as an array of two integers: StartPort, NumerOfPorts
Example input: Update-SystemPortReservations -AddRange @(50000,30)
Example input: Update-SystemPortReservations -AddRange @(@(50000,30),@(12345,2))
.PARAMETER RemoveRange
Supply an array of value-tuples for removing one or more ranges.
Value tuple is defined as an array of two integers: StartPort, NumerOfPorts
Example input: Update-SystemPortReservations -RemoveRange @(@(50000,30),@(12345,2))
#>
[CmdletBinding()]
param (
[Parameter(Mandatory = $false)]
[Alias("Create")]
$AddRange = $null,
[Parameter(Mandatory = $false)]
[Alias("Destroy")]
$RemoveRange = $null
)
$logLead = (Get-LogLeadName)
$validationErrors = @()
$wasRunningProcessNames = @()
Write-Host "$logLead : Ensure basic system registrations have occurred - IPListens"
Set-DefaultNetshIPListens
Write-Host "$logLead : Ensure basic system registrations have occurred - URL ACLs"
Set-DefaultNetshURLACLS
#region internal-private functions to make life easier
function Get-EnvironmentVariableNameForRange {
<#
.SYNOPSIS
This function is internal only and is used to consistently return the variable name
This function should not be a standalone function as it only applies to this specific file
.PARAMETER rangeStart
This parameter is the Start of the range.
.PARAMETER NumberOfPorts
The number of ports in the range
.PARAMETER End
The End port in the range
#>
[CmdletBinding(DefaultParameterSetName = 'NumberOfPorts')]
param (
[Parameter(Mandatory = $false)]
$Start,
[Parameter(Mandatory = $true, ParameterSetName = 'NumberOfPorts')]
[ValidateNotNullOrEmpty()]
[Alias("Range")]
[int]$NumberOfPorts,
[Parameter(Mandatory = $true, ParameterSetName = 'EndPorts')]
[ValidateNotNullOrEmpty()]
[Alias("EndPort")]
[int]$End
)
if ($Start -le 0) {
throw "$logLead : Start port value must be greater than 0"
}
if ($PSCmdlet.ParameterSetName -eq 'NumberOfPorts') {
if ($NumberOfPorts -eq $Start) {
throw "$logLead : NumberOfPorts can not equal the parameter for Start"
}
if ($NumberOfPorts -le 0) {
throw "$logLead : NumberOfPorts value must be greater than 0"
}
$End = $NumberOfPorts + $Start - 1
}
if ($PSCmdlet.ParameterSetName -eq 'EndPorts') {
if ($End -le $Start) {
throw "$logLead : End port value must be larger than Start port value"
}
if ($End -le 0) {
throw "$logLead : End port value must be greater than 0"
}
}
return "ALKAMI.SRE.EXCLUDED_PORT_RANGE_CONFIGURED.$Start.$End"
}
function Stop-AnyProcessesInPortRange {
<#
.SYNOPSIS
Stop any processes that are using these ports. Return the names so we can try to restart any services.
.PARAMETER Start
This parameter is the Start of the range.
.PARAMETER NumberOfPorts
The number of ports in the range
.PARAMETER End
The End port in the range
#>
[CmdletBinding(DefaultParameterSetName = 'NumberOfPorts')]
param (
[Parameter(Mandatory = $false)]
$Start,
[Parameter(Mandatory = $true, ParameterSetName = 'NumberOfPorts')]
[ValidateNotNullOrEmpty()]
[Alias("Range")]
[int]$NumberOfPorts,
[Parameter(Mandatory = $true, ParameterSetName = 'EndPorts')]
[ValidateNotNullOrEmpty()]
[Alias("EndPort")]
[int]$End
)
$runningProcessNames = @()
if ($Start -le 0) {
throw "$logLead : Start port value must be greater than 0"
}
if ($PSCmdlet.ParameterSetName -eq 'NumberOfPorts') {
if ($NumberOfPorts -eq $Start) {
throw "$logLead : NumberOfPorts can not equal the parameter for Start"
}
if ($NumberOfPorts -le 0) {
throw "$logLead : NumberOfPorts value must be greater than 0"
}
$End = $NumberOfPorts + $Start - 1
}
if ($PSCmdlet.ParameterSetName -eq 'EndPorts') {
if ($End -le $Start) {
throw "$logLead : End port value must be larger than Start port value"
}
if ($End -le 0) {
throw "$logLead : End port value must be greater than 0"
}
}
$allBoundProcessPorts = (Get-NetTCPConnection).LocalPort | Sort-Object -Unique
Write-Host "$logLead : Looking for ports between [$Start] and [$End]"
$runningProcessPorts = @($allBoundProcessPorts.Where({$_ -ge $Start -and $_ -le $End}))
if (Test-IsCollectionNullOrEmpty $runningProcessPorts) {
Write-Host "$logLead : Found no actively used ports, nothing to stop"
return $runningProcessNames
}
Write-Host "$logLead : Found [$($runningProcessPorts.Length)] actively used ports"
# We found some processes, let's kill 'em
foreach($processPort in $runningProcessPorts) {
# It's possible we already killed a process that was holding more than one port open
$netTcpConnection = Get-NetTCPConnection -LocalPort $processPort -ErrorAction Ignore
if ($null -ne $netTcpConnection) {
$process = Get-Process -Id ($netTcpConnection).OwningProcess -ErrorAction Ignore
if ($null -eq $process) {
# The process was already killed a moment ago. This is normal.
continue
}
$processName = $process.Name
$processPath = $process.Path
# Test to ensure this is not the System process, and that it has a path
if ([string]::IsNullOrEmpty($processPath)) {
Write-Host "$logLead : [$processName] was running on process [$processPort] with no path. Skipping"
continue
}
Write-Warning "$logLead : Found [$processName] running and keeping [$processPort] open. Killing this process by force."
Write-Host "$logLead : [$processName] was running at path [$processPath]. Will try to restart at the end if this was a service"
$runningProcessNames += $processName
$processId = $process.Id
Stop-Process -Id $processId -Force
}
}
return $runningProcessNames
}
#endregion internal-private functions to make life easier
Write-Host "$logLead : Updating system port reservations"
<#
Once a range is committed to this table, please do not edit it except under extreme duress
If you need to add "a longer port" you would want to just make a new grouping
Example: the "standard default" is 50000 + 30 ports (to 50029) so if we wanted instead
50000 + 60, what you would do here is @{ Start = 50030; NumberOfPorts = 30; }
Plus the other fields as indicated below.
These ranges once set are "set in stone"
There are machine level environment variables that get set as well
So if you want to remove something, you have to ensure you call that
#>
$createRanges = @(
@{ Start = 50000; NumberOfPorts = 30; CollidingReservations = @(); }
@{ Start = 12345; NumberOfPorts = 2; CollidingReservations = @(); }
)
# Be very careful what you do here.
# If you MUST delete one from the above table, move it here.
$destroyExistingRanges = @(
)
#region process the inputs
if (!(Test-IsCollectionNullOrEmpty $AddRange)) {
# $AddRange was supplied with some set of values. Let's evaluate those for validness.
$createRanges = @()
if (($AddRange.Length -eq 2) -and ($AddRange[0] -is [int])) {
$createRanges += @{ Start = $AddRange[0]; NumberOfPorts = $AddRange[1]; CollidingReservations = @(); }
} else {
foreach($range in $AddRange) {
if (($range.Length -eq 2) -and ($range[0] -is [int]) -and ($range[1] -is [int])) {
$createRanges += @{ Start = $range[0]; NumberOfPorts = $range[1]; CollidingReservations = @(); }
} else {
$validationErrors += "$logLead : Could not parse the input parameter for AddRange. Should be an array of two ints, or an array of arrays of int pairs."
}
}
}
} elseif (!(Test-IsCollectionNullOrEmpty $RemoveRange)) {
# There was no create range input, but there was a remove range input
# Therefore we only want to remove the ranges supplied
$createRanges = @()
}
if (!(Test-IsCollectionNullOrEmpty $RemoveRange)) {
# $RemoveRange was supplied with some set of values. Let's evaluate those for validness.
$destroyExistingRanges = @()
if (($RemoveRange.Length -eq 2) -and ($RemoveRange[0] -is [int])) {
$destroyExistingRanges += @{ Start = $RemoveRange[0]; NumberOfPorts = $RemoveRange[1]; }
} else {
foreach($range in $RemoveRange) {
if (($range.Length -eq 2) -and ($range[0] -is [int]) -and ($range[1] -is [int])) {
$destroyExistingRanges += @{ Start = $range[0]; NumberOfPorts = $range[1]; }
} else {
$validationErrors += "$logLead : Could not parse the input parameter for RemoveRange. Should be an array of two ints, or an array of arrays of int pairs."
}
}
}
}
if (!(Test-IsCollectionNullOrEmpty $validationErrors)) {
#Some errors were found. Stop now.
foreach($validationError in $validationErrors) {
Write-Error $validationError
}
throw "$logLead : Please resolve errors in input and try again"
}
#endregion process the inputs
#region fast abort if create-only and all ranges exist
# Start with true only if we have values to test, otherwise start with false
$allRangeMachineEnvVarsExist = !(Test-IsCollectionNullOrEmpty $createRanges)
if (Test-IsCollectionNullOrEmpty $destroyExistingRanges) {
foreach($range in $createRanges) {
if ($null -eq (Get-EnvironmentVariable -Name (Get-EnvironmentVariableNameForRange -Start $range.Start -NumberOfPorts $range.NumberOfPorts) -StoreName Machine)) {
# $null means the var does not exist
$allRangeMachineEnvVarsExist = $false
break
}
}
}
if ($allRangeMachineEnvVarsExist) {
Write-Host "$logLead : All the requested ranges seem to exist. Nothing to do."
return
}
#endregion fast abort if create-only and all ranges exist
<#
__ __ __ ____ _ _
( \/ ) /__\ (_ _)( )_( )
) ( /(__)\ )( ) _ (
(_/\/\_)(__)(__)(__) (_) (_)
,_ . .
|_ ,-. | | ,-. . , , ,-.
| | | | | | | |/|/ `-.
| `-' `' `' `-' ' ' `-'
'
There are two locations below that use this set of calculations.
I included this enormous block of text so that I can keep track of the
Logical math taking place. There's 3 complex logic sets to monitor
So I want to make sure that my logic is sound
It is possible to have overlapping ranges that are problematic
We need to remove some conditions if they already exist
In some cases they may be our own cause, but hopefully we already
Resolved those with the removals above.
In the examples below we want to consider a $range in $createRanges
Range: Start 100 NumberOfPorts 100
DesiredRangeStart: 100 (DRS)
DesiredRangeEnd: 199 (DRE)
Non-conflicting conditions to handle:
Defined range matches our request
How can this happen? This script runs on a machine that has
already been configured before the environment variables were added
RangeStart: 100 (RS)
RangeEnd: 199 (RE)
RS -eq DRS -and RE -eq DRE
Action to be taken in this case:
Create the environment variable on this machine, continue loop
Conflicting conditions:
Conflict #1: "hangs over the beginning"
99 - 101 already exists as a reserved set of ports
RangeStart: 99 (RS)
RangeEnd: 101 (RE)
RS -lt DRS -and RE -gt DRS -and RE -lt DRE
99 -lt 100 -and 101 -gt 100 -and 101 -lt 199
Conflict #2: "hangs over the end"
198 - 201 already exists as a reserved set of ports
RangeStart: 198 (RS)
RangeEnd: 201 (RE)
RS -gt DRS -and RS -lt DRE -and RE -gt DRE
198 -gt 100 -and 198 -lt 199 -and 201 -gt 199
Conflict #3: "narrow condition"
140 - 160 already exists as a reserved set of ports
RangeStart: 140 (RS)
RangeEnd: 160 (RE)
RS -gt DRS -and RE -lt DRE
140 -gt 100 -and 160 -lt 199
As a visualization:
Desired: |--------------------|
Conf 1: |-----|
Conf 2: |-----|
Conf 3: |------|
#>
#region validate the create ranges don't overlap
foreach($range in $createRanges) {
$desiredRangeStart = $range.Start
$desiredRangeEnd = $range.Start + $range.NumberOfPorts - 1
$otherRanges = $createRanges.Where({!(($_.Start -eq $range.Start) -and ($_.NumberOfPorts -eq $range.NumberOfPorts))})
foreach($otherRange in $otherRanges) {
$rangeStart = $otherRange.Start
$rangeEnd = $otherRange.Start + $otherRange.NumberOfPorts - 1
# As a visualization:
# Desired: |--------------------|
# Conf 1: |-----|
# Conf 2: |-----|
# Conf 3: |------|
$conflictCondition1 = ($rangeStart -le $desiredRangeStart) -and ($rangeEnd -gt $desiredRangeStart) -and ($rangeEnd -lt $desiredRangeEnd)
$conflictCondition2 = ($rangeStart -gt $desiredRangeStart) -and ($rangeStart -lt $desiredRangeEnd) -and ($rangeEnd -ge $desiredRangeEnd)
$conflictCondition3 = ($rangeStart -gt $desiredRangeStart) -and ($rangeEnd -lt $desiredRangeEnd)
if ($conflictCondition1 -or $conflictCondition2 -or $conflictCondition3) {
$validationErrors += "$logLead : Range crossover exception. Create range with Start $($range.Start) and NumberOfPorts $($range.NumberOfPorts) has a range conflict with the create range with values Start $($otherRange.Start) NumberOfPorts $($otherRange.NumberOfPorts)."
}
}
}
if (!(Test-IsCollectionNullOrEmpty $validationErrors)) {
#Some errors were found. Stop now.
foreach($validationError in $validationErrors) {
Write-Error $validationError
}
throw "$logLead : Please resolve errors in input and try again"
}
#endregion validate the ranges don't overlap
$existingRanges = @(Get-NetshExcludedPortRanges)
#region ensure the delete ranges already exist to be deleted
foreach($range in $destroyExistingRanges) {
$desiredRangeStart = $range.Start
$desiredRangeEnd = $range.Start + $range.NumberOfPorts - 1
$activeDeleteRanges = $existingRanges.Where({!(($_.Start -eq $desiredRangeStart) -and ($_.End -eq $desiredRangeEnd))})
if (Test-IsCollectionNullOrEmpty $activeDeleteRanges) {
$validationErrors += "$logLead : There are no existing ranges to remove for Start $($range.Start) NumberOfPorts $($range.NumberOfPorts) FinalPort $($desiredRangeEnd)"
}
}
if (!(Test-IsCollectionNullOrEmpty $validationErrors)) {
#Some errors were found. Stop now.
foreach($validationError in $validationErrors) {
Write-Error $validationError
}
throw "$logLead : Please resolve errors in input and try again"
}
#endregion ensure the delete ranges already exist to be deleted
#region destroy before creating
if (!(Test-IsCollectionNullOrEmpty $destroyExistingRanges)) {
# Destroying before you create saves you a lot of hassle
# It does mean reading the system state twice, but that should be ok for what we are doing
foreach($range in $destroyExistingRanges) {
$wasRunningProcessNames += @(Stop-AnyProcessesInPortRange -Start $range.Start -NumberOfPorts $range.NumberOfPorts)
Write-Host "$logLead : Ready to delete range with Start $($range.Start) NumberOfPorts $($range.NumberOfPorts)"
# The function returns a true-false value if it succeeds or fails
if (Remove-NetshExcludedPortRange -Start $range.Start -NumberOfPorts $range.NumberOfPorts) {
Remove-EnvironmentVariable -Name (Get-EnvironmentVariableNameForRange -Start $range.Start -NumberOfPorts $range.NumberOfPorts) -StoreName Machine
}
}
if (Test-IsCollectionNullOrEmpty $createRanges) {
# If we only came here to destroy things, let's get out of here!
Write-Host "$logLead : All ranges destroyed, nothing to create. Done"
return
}
# Since we just nuked the ranges, let's just recreate from the system to be sure we got 'em all
$existingRanges = (Get-NetshExcludedPortRanges)
}
#endregion destroy before creating
#region look for existing non-reserved ranges that need to be destroyed
foreach($range in $createRanges) {
$desiredRangeStart = $range.Start
$desiredRangeEnd = $range.Start + $range.NumberOfPorts - 1
$envVarName = (Get-EnvironmentVariableNameForRange -Start $range.Start -NumberOfPorts $range.NumberOfPorts)
if ($null -ne (Get-EnvironmentVariable $envVarName)) {
Write-Verbose "$logLead : non-reserved destroy range check : Environment range already set for Start $($range.Start) NumberOfPorts $($range.NumberOfPorts), skipping"
continue
}
foreach($existingRange in $existingRanges) {
$rangeStart = $existingRange.Start
$rangeEnd = $existingRange.End
# As a visualization:
# Desired: |--------------------|
# Non-Conf: |--------------------|
# Conf 1: |-----|
# Conf 2: |-----|
# Conf 3: |------|
$nonconflictCondition = ($rangeStart -eq $desiredRangeStart) -and ($rangeEnd -eq $desiredRangeEnd)
if ($nonconflictCondition) {
Set-EnvironmentVariable -Name $envVarName -Value $true -StoreName Machine
Write-Host "$logLead : Found an already existing range for Start $($range.Start) NumberOfPorts $($range.NumberOfPorts). Setting Machine Environment Variable and continuing."
continue
}
$conflictCondition1 = ($rangeStart -le $desiredRangeStart) -and ($rangeEnd -gt $desiredRangeStart) -and ($rangeEnd -lt $desiredRangeEnd)
$conflictCondition2 = ($rangeStart -gt $desiredRangeStart) -and ($rangeStart -lt $desiredRangeEnd) -and ($rangeEnd -ge $desiredRangeEnd)
$conflictCondition3 = ($rangeStart -gt $desiredRangeStart) -and ($rangeEnd -lt $desiredRangeEnd)
if ($conflictCondition1 -or $conflictCondition2 -or $conflictCondition3) {
$range.CollidingReservations += @{ Start = $rangeStart; End = $rangeEnd; }
}
}
}
#endregion look for existing non-reserved ranges that need to be destroyed
#region remove excess regions so we can create the ones we need to create
$haveDeleteRanges = @($createRanges.Where({!(Test-IsCollectionNullOrEmpty $_.CollidingReservations)}))
if (!(Test-IsCollectionNullOrEmpty $haveDeleteRanges)) {
$allBoundProcessPorts = (Get-NetTCPConnection).LocalPort | Sort-Object -Unique
foreach($range in $haveDeleteRanges) {
foreach ($reservation in $range.CollidingReservations) {
# Remove the existing reservations
# There may be a port conflict because something may have the port in use.
# Now we get to see if ANY other application is using that port right now,
# And try to force-kill them
# We can't clear the port range until these processes are stopped.
# We have to repeat the process before we create each range, as well, just in case.
$wasRunningProcessNames += @(Stop-AnyProcessesInPortRange -Start $reservation.Start -End $reservation.End)
$portCount = $reservation.End - $reservation.Start + 1
# TODO: Add support for SupportsShouldProcess
Write-Host "$logLead : Removing conflicting port reservation to enable properly configuring requested range. Removing Start [$($reservation.Start)] NumberOfPorts [$($portCount)]"
# The function returns a true-false value if it succeeds or fails
if (Remove-NetshExcludedPortRange -Start $reservation.Start -NumberOfPorts $portCount) {
Remove-EnvironmentVariable (Get-EnvironmentVariableNameForRange -Start $reservation.Start -NumberOfPorts $portCount)
}
}
}
}
#endregion remove excess regions so we can create the ones we need to create
#region now the moment we've all been waiting for
# all the overlapping/conflicting ranges should now be deleted
foreach($range in $createRanges) {
$envVarName = (Get-EnvironmentVariableNameForRange -Start $range.Start -NumberOfPorts $range.NumberOfPorts)
if ($null -ne (Get-EnvironmentVariable $envVarName)) {
Write-Host "$logLead : Environment range already set for Start $($range.Start) NumberOfPorts $($range.NumberOfPorts), skipping"
continue
}
# Ensure that the ports we want to create don't have anything using them
# This is usually where things go hairy if at all
$wasRunningProcessNames += @(Stop-AnyProcessesInPortRange -Start $range.Start -NumberOfPorts $range.NumberOfPorts)
# The function returns a true-false value if it succeeds or fails
if (Add-NetshExcludedPortRange -Start $range.Start -NumberOfPorts $range.NumberOfPorts) {
Write-Host "$logLead : Created excluded port range with Start $($range.Start) NumberOfPorts $($range.NumberOfPorts)"
Set-EnvironmentVariable -Name $envVarName -Value $true -StoreName Machine
}
}
#endregion now the moment we've all been waiting for
#region try to restart services once. If it doesn't work, give up
foreach($processName in $wasRunningProcessNames) {
Write-Host "$logLead : We stopped [$processName] to do the port configuration, trying to start it if it was a service"
# Start-AlkamiService doesn't care if it wasn't actually a service if we SilentlyContinue
Start-AlkamiService -ServiceName $processName -ErrorAction SilentlyContinue
}
#endregion try to restart services once. If it doesn't work, give up
}