Public/Stop-HyperVVMService.ps1
|
function Stop-HyperVVMService { [CmdletBinding(SupportsShouldProcess, DefaultParameterSetName = 'ByTopology')] [OutputType([PSCustomObject])] param( [Parameter(Mandatory)] [string] $ServiceName, [Parameter(Mandatory)] [string] $EnvironmentName, [Parameter(ParameterSetName = 'ByTopology', Mandatory)] [HyperVVMTopology] $Topology, [Parameter(ParameterSetName = 'ByComputerName')] [ValidateNotNullOrEmpty()] [string] $ComputerName = 'localhost', [Parameter()] [switch] $Recurse, [Parameter()] [switch] $Force, [Parameter()] [bool] $WaitForVM = $true, [Parameter()] [int] $WaitTimeoutSeconds = 120, # Tracks services stopped in this call-chain so the dependency-safety check ignores them. [Parameter(DontShow)] [System.Collections.Generic.HashSet[string]] $StoppedServiceNames, # False when recursing upward through dependents; prevents cascading down into their own DependsOn chains. [Parameter(DontShow)] [bool] $StopDependencies = $true ) if (-not $StoppedServiceNames) { $StoppedServiceNames = [System.Collections.Generic.HashSet[string]]::new( [System.StringComparer]::OrdinalIgnoreCase) } if ($PSCmdlet.ParameterSetName -eq 'ByComputerName') { $Topology = Get-HyperVVMTopology -ComputerName $ComputerName } $effectiveComputerName = $Topology.ComputerName $result = [PSCustomObject]@{ Success = @(); Failed = $null } $vmEnv = $Topology.Environment | Where-Object Name -eq $EnvironmentName if (-not $vmEnv) { throw "Environment '$EnvironmentName' not found in topology." } $service = $vmEnv.Service | Where-Object Name -eq $ServiceName if (-not $service) { throw "Service '$ServiceName' not found in environment '$EnvironmentName'." } # Step 1: stop dependents first (services that DependsOn this one and still have running VMs) if ($Recurse) { $dependents = $vmEnv.Service | Where-Object { $_.DependsOn -contains $ServiceName -and -not $StoppedServiceNames.Contains($_.Name) -and ($_.VM | Where-Object { $_.State -ne 'Off' }) } foreach ($dep in $dependents) { $depParams = @{ ServiceName = $dep.Name EnvironmentName = $EnvironmentName Topology = $Topology Recurse = $true Force = $Force WaitForVM = $WaitForVM WaitTimeoutSeconds = $WaitTimeoutSeconds StoppedServiceNames = $StoppedServiceNames StopDependencies = $false } $depResult = Stop-HyperVVMService @depParams $result.Success += $depResult.Success if ($depResult.Failed) { $result.Failed = $depResult.Failed return $result } } } # Step 2: stop this service's VMs foreach ($vmObj in $service.VM) { if ($vmObj.State -eq 'Off') { Write-Verbose "VM '$($vmObj.Name)' is already stopped, skipping." continue } if ($PSCmdlet.ShouldProcess($vmObj.Name, 'Stop VM')) { try { $stopSplat = @{ Name = $vmObj.Name; ComputerName = $effectiveComputerName } if ($Force) { $stopSplat.Force = $true } Stop-VM @stopSplat if ($WaitForVM) { Wait-HyperVVMOff -Name $vmObj.Name -ComputerName $effectiveComputerName -TimeoutSeconds $WaitTimeoutSeconds } $result.Success += $vmObj.Name } catch { $result.Failed = [PSCustomObject]@{ VMName = $vmObj.Name; Error = $_.ToString() } return $result } } } $StoppedServiceNames.Add($ServiceName) | Out-Null # Step 3: with -Recurse, stop dependencies (this service's DependsOn) if no other running service still needs them if ($Recurse -and $StopDependencies) { foreach ($depName in $service.DependsOn) { if ($StoppedServiceNames.Contains($depName)) { continue } $otherRunningDependents = $vmEnv.Service | Where-Object { $_.Name -ne $ServiceName -and -not $StoppedServiceNames.Contains($_.Name) -and $_.DependsOn -contains $depName -and ($_.VM | Where-Object { $_.State -ne 'Off' }) } if ($otherRunningDependents) { Write-Verbose "Skipping '$depName': still needed by $($otherRunningDependents.Name -join ', ')" continue } $depParams = @{ ServiceName = $depName EnvironmentName = $EnvironmentName Topology = $Topology Recurse = $true Force = $Force WaitForVM = $WaitForVM WaitTimeoutSeconds = $WaitTimeoutSeconds StoppedServiceNames = $StoppedServiceNames StopDependencies = $true } $depResult = Stop-HyperVVMService @depParams $result.Success += $depResult.Success if ($depResult.Failed) { $result.Failed = $depResult.Failed return $result } } } return $result } |