InformationTechnologyOperationsModule.psm1
################################################################################## # # # Copyright 2023 Ryan E. Anderson # # # # Licensed under the Apache License, Version 2.0 (the "License"); # # you may not use this file except in compliance with the License. # # You may obtain a copy of the License at # # # # http://www.apache.org/licenses/LICENSE-2.0 # # # # Unless required by applicable law or agreed to in writing, software # # distributed under the License is distributed on an "AS IS" BASIS, # # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # # See the License for the specific language governing permissions and # # limitations under the License. # # # ################################################################################## ###################################################### # # # InformationTechnologyOperationsModule v0.1.0 # # # # By Ryan E. Anderson # # # # Copyright (C) 2023 Ryan E. Anderson # # # ###################################################### Set-StrictMode -Version Latest function Invoke-ShouldContinue { [CmdletBinding()] param ( [Parameter(Mandatory)] [System.Management.Automation.PSCmdlet] $Context, [Parameter(Mandatory)] [string] $Query, [Parameter(Mandatory)] [string] $Caption ) $Context.ShouldContinue($Query, $Caption) } function Invoke-ShouldProcess { [CmdletBinding(SupportsShouldProcess)] param ( [Parameter(Mandatory)] [System.Management.Automation.PSCmdlet] $Context, [Parameter(Mandatory)] [string] $Target, [Parameter(Mandatory)] [string] $Action ) $Context.ShouldProcess($Target, $Action) } <# .SYNOPSIS This function starts virtual machines hosted on an instance of Hyper-V. .DESCRIPTION This function starts virtual machines hosted on an instance of Hyper-V after evaluating memory usage. This function supports wildcards. .PARAMETER Name This is a list of names that identify virtual machines. .PARAMETER Threshold The total memory capacity is decreased by this percentage, to a threshold, beyond which a VM cannot start. This value must be between 0.05 and 0.95. This parameter has a default value of 0.20, which is 20%. .PARAMETER AsJob This is a switch that can be used to start VMs as background jobs. .PARAMETER Force This is a switch that can be used to force the initialization of virtual machines after determining that resource usage could exceed a reasonable amount. .EXAMPLE # Start virtual machines using an exact match. Start-InformationTechnologyOperationVirtualMachine -Name OS-EN-US-User .EXAMPLE # Start virtual machines using a wildcard. Start-InformationTechnologyOperationVirtualMachine -Name Windows* .EXAMPLE # Start virtual machines using names separated by commas. Start-InformationTechnologyOperationVirtualMachine -Name 'Windows*','Linux*','OS-EN-US-User' .EXAMPLE # Start virtual machines with a threshold that is specified by a user. Start-InformationTechnologyOperationVirtualMachine -Name Windows* -Threshold 0.50 .EXAMPLE # Bypass the prompt so that execution can stop immediately after determining that memory usage for a VM would exceed a threshold. Start-InformationTechnologyOperationVirtualMachine -Name Windows* -Force .EXAMPLE # Start a VM as a background job. Start-InformationTechnologyOperationVirtualMachine -Name 'Ubuntu-22.10' -Threshold 0.55 -AsJob -Force .NOTES Names are not case-sensitive. Names will not be trimmed during processing. #> function Start-InformationTechnologyOperationVirtualMachine { [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'High')] param ( [Parameter(Position = 0, Mandatory, ValueFromPipelineByPropertyName)] [string[]] $Name, [Parameter(Position = 1, Mandatory = $false, ValueFromPipelineByPropertyName)] [ValidateRange(0.05, 0.95)] [float] $Threshold = 0.20, [Parameter(Position = 2, Mandatory = $false, ValueFromPipelineByPropertyName)] [switch] $AsJob, [Parameter(Position = 3, Mandatory = $false, ValueFromPipelineByPropertyName)] [switch] $Force ) begin { [int]$lineCount = 0 $title = 'InformationTechnologyOperationsModule v0.1.0' $author = 'By Ryan E. Anderson' $copyrightText = 'Copyright (C) 2023 Ryan E. Anderson' $authorLength = $author.Length $copyrightTextLength = $copyrightText.Length $titleLength = $title.Length $lengths = @($authorLength, $copyrightTextLength, $titleLength) $maximumLength = ($lengths | Measure-Object -Maximum).Maximum $horizontalBorderLength = $maximumLength + 10 # m spaces on either side times n sides = 5 * 2 = 10 $horizontalBorder = '#' * $horizontalBorderLength $verticalBorderPosition = $horizontalBorderLength - 1 $emptyRowFormat = '{0}{1,' + $verticalBorderPosition + '}' Write-Information -MessageData $horizontalBorder -InformationAction Continue Write-Information -MessageData ([string]::Format($emptyRowFormat, '#', '#')) -InformationAction Continue Write-Information -MessageData ([string]::Format('{0,-5}{1}{2,' + ($maximumLength - $titleLength + 5) + '}', '#', $title, '#')) -InformationAction Continue Write-Information -MessageData ([string]::Format($emptyRowFormat, '#', '#')) -InformationAction Continue Write-Information -MessageData ([string]::Format('{0,-5}{1}{2,' + ($maximumLength - $authorLength + 5) + '}', '#', $author, '#')) -InformationAction Continue Write-Information -MessageData ([string]::Format($emptyRowFormat, '#', '#')) -InformationAction Continue Write-Information -MessageData ([string]::Format('{0,-5}{1}{2,' + ($maximumLength - $copyrightTextLength + 5) + '}', '#', $copyrightText, '#')) -InformationAction Continue Write-Information -MessageData ([string]::Format($emptyRowFormat, '#', '#')) -InformationAction Continue Write-Information -MessageData ($horizontalBorder + [Environment]::NewLine) -InformationAction Continue } process { try { # Don't consider $Confirm when executing with force and manually setting $ConfirmPreference because it might not be set. if ($Force) { $ConfirmPreference = 'None' } $statusVirtualMachineManagementService = (Get-Service -Name 'vmms').Status $statusVirtualMachineComputeService = (Get-Service -Name 'vmcompute').Status Write-Information -MessageData ([string]::Format("({0}) The status of the Hyper-V 'vmms' service is {1}...", ++$lineCount, $statusVirtualMachineManagementService)) -InformationAction Continue Write-Information -MessageData ([string]::Format("({0}) The status of the Hyper-V 'vmcompute' service is {1}...", ++$lineCount, $statusVirtualMachineComputeService)) -InformationAction Continue if ($statusVirtualMachineManagementService -eq 'Running' -and $statusVirtualMachineComputeService -eq 'Running') { Write-Information -MessageData ([string]::Format('({0}) Evaluating system memory...', ++$lineCount)) -InformationAction Continue [float]$totalMemoryUsage = 0 [float]$memoryCapacityOfAllRunningVirtualMachines = 0 Get-VM -ErrorAction Stop | Where-Object { $_.State -eq 'Running' } | ForEach-Object { $memoryCapacityOfAllRunningVirtualMachines += $_.MemoryAssigned } # Assign the total memory capacity of all virtual machines that are currently running. $operatingSystem = Get-CimInstance -ClassName 'CIM_OperatingSystem' | Select-Object TotalVisibleMemorySize, FreePhysicalMemory [float]$memoryCapacityOfCurrentSystem = [Math]::Round($operatingSystem.TotalVisibleMemorySize / 1mb, 2) # Convert capacity to GB. [float]$memoryUsedByCurrentSystem = [Math]::Round($memoryCapacityOfCurrentSystem - $operatingSystem.FreePhysicalMemory / 1mb, 2) # Convert capacity to GB. [float]$memoryThreshold = [Math]::Round($memoryCapacityOfCurrentSystem - $Threshold * $memoryCapacityOfCurrentSystem, 2) # Use a percentage decrease for the threshold to ensure that memory usage does not exceed a reasonable amount. $totalMemoryUsage += $memoryUsedByCurrentSystem Write-Information -MessageData ([string]::Format('({0}) The capacity of physical memory is {1} GB.', ++$lineCount, $memoryCapacityOfCurrentSystem)) -InformationAction Continue Write-Information -MessageData ([string]::Format('({0}) The current usage of physical memory is {1} GB.', ++$lineCount, $memoryUsedByCurrentSystem)) -InformationAction Continue [float]$memoryRatio = [Math]::Round($memoryUsedByCurrentSystem / $memoryCapacityOfCurrentSystem, 4) Write-Information -MessageData ([string]::Format('({0}) The ratio of used physical memory to total physical memory is {1} or {2}%.', ++$lineCount, $memoryRatio, 100 * $memoryRatio)) -InformationAction Continue Write-Information -MessageData ([string]::Format('({0}) The memory threshold for this system is {1} GB, which is {2}% of all memory that is available.', ++$lineCount, $memoryThreshold, 100 - [Math]::Round(100 * $Threshold, 2))) -InformationAction Continue Write-Information -MessageData ([string]::Format('({0}) The memory capacity of all virtual machines that are currently running is {1} GB (This is startup or allocated memory.).', ++$lineCount, [Math]::Round($memoryCapacityOfAllRunningVirtualMachines / 1gb, 2))) -InformationAction Continue Write-Information -MessageData ([string]::Format("({0}) Evaluating each entry from the provided list of names as either the name of a single VM instance or a wildcard prefix followed by '*' for a set of VMs (The name of a VM is not case-sensitive, and white space will not be trimmed.)...", ++$lineCount)) -InformationAction Continue Write-Information -MessageData ([string]::Format('({0}) The provided list of names is {1}.', ++$lineCount, [string]::Join(',', $Name))) -InformationAction Continue :outer for ($i = 0; $i -lt $Name.Count; $i++) { $virtualMachineName = $Name[$i] $virtualMachineInstances = New-Object 'System.Collections.Generic.List[System.Object]' $temporaryVirtualMachineInstances = Get-VM -Name $virtualMachineName -ErrorAction SilentlyContinue | Where-Object { $_.State -eq 'Off' } | Select-Object if ($temporaryVirtualMachineInstances -is 'System.Object[]') { $virtualMachineInstances.AddRange($temporaryVirtualMachineInstances) } elseif ($temporaryVirtualMachineInstances -is 'Microsoft.HyperV.PowerShell.VirtualMachine') { $virtualMachineInstances.Add($temporaryVirtualMachineInstances) } else { Write-Information -MessageData ([string]::Format("({0}) A VM named '{1}' could not be started. If a machine named '{1}' exists, then make sure that it is not already in a running state.", ++$lineCount, $virtualMachineName)) -InformationAction Continue continue } for ($j = 0; $j -lt $virtualMachineInstances.Count; $j++) { $virtualMachineInstance = $virtualMachineInstances[$j] $virtualMachineInstanceName = $virtualMachineInstance.VMName Write-Information -MessageData ([string]::Format("({0}) Checking memory for '{1}'...", ++$lineCount, $virtualMachineInstanceName)) -InformationAction Continue [float]$virtualMachineInstanceStartupMemory = [Math]::Round($virtualMachineInstance.MemoryStartup / 1gb, 2) [float]$temporaryMemory = $totalMemoryUsage + $virtualMachineInstanceStartupMemory Write-Information -MessageData ([string]::Format("({0}) The startup memory for '{1}' is {2} GB.", ++$lineCount, $virtualMachineInstanceName, $virtualMachineInstanceStartupMemory)) -InformationAction Continue Write-Information -MessageData ([string]::Format("({0}) The total memory after starting '{1}' will be {2} GB.", ++$lineCount, $virtualMachineInstanceName, $temporaryMemory)) -InformationAction Continue if ($Force -or (Invoke-ShouldProcess -Context $PSCmdlet -Target $virtualMachineInstanceName -Action 'Initialize VMs')) { if ($temporaryMemory -gt $memoryThreshold) { Write-Information -MessageData ([string]::Format("({0}) The VM named '{1}' could not be started because doing so would cause the total usage to exceed a reasonable amount: {2} GB > {3} GB.", ++$lineCount, $virtualMachineInstanceName, $temporaryMemory, $memoryThreshold)) -InformationAction Continue if (-not $Force -and -not (Invoke-ShouldContinue -Context $PSCmdlet -Query 'Do you want to continue initializing the current set of VMs?' -Caption 'Continue VM Initialization')) { break outer } } Write-Information -MessageData ([string]::Format("({0}) Starting '{1}' with total memory usage at {2} GB...", ++$lineCount, $virtualMachineInstanceName, $totalMemoryUsage)) -InformationAction Continue Start-VM -Name $virtualMachineInstanceName -AsJob:$AsJob $totalMemoryUsage += $virtualMachineInstanceStartupMemory Write-Information -MessageData ([string]::Format("({0}) The VM '{1}' was started; total memory usage is now at {2} GB.", ++$lineCount, $virtualMachineInstanceName, $totalMemoryUsage)) -InformationAction Continue } } } } else { Write-Information -MessageData ([string]::Format('({0}) Hyper-V initialization did not succeed because a Hyper-V service is not running...', ++$lineCount)) -InformationAction Continue } } catch { Write-Warning ([string]::Format('({0}) {1}', ++$lineCount, $_.Exception)) Write-Information -MessageData ([string]::Format('({0}) An unexpected error that could not be resolved to a specific classification occurred.', ++$lineCount)) -InformationAction Continue } } end { Write-Information -MessageData ([string]::Format('({0}) The task of starting VMs that are hosted on the current Hyper-V instance has completed.', ++$lineCount)) -InformationAction Continue } } Set-Alias sitovm Start-InformationTechnologyOperationVirtualMachine Export-ModuleMember -Function Start-InformationTechnologyOperationVirtualMachine -Alias sitovm |