PSHVTag.psm1

Write-Verbose 'Importing from [C:\MyProjects\PSHVTag\PSHVTag\private]'
# .\PSHVTag\private\Convert-VMNoteTagsToObject.ps1
function Convert-VMNoteTagsToObject
{
    <#
    .SYNOPSIS
    Converts a Vm object to Vm object including tags
    
    .DESCRIPTION
    Converts a Hyper-V VM object including State, Status and Notes to a VMWithTag Object including the custom Tag information
    
    .PARAMETER VM
    A Hyper-V VM object including, State, Status and Notes.
    
    .EXAMPLE
    Get-VM -Name 'Test1' | Convert-VMNoteTagsToObject
    
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true, ValueFromPipeline)]
        [Microsoft.HyperV.PowerShell.VirtualMachine]
        $VM
    )
    $VM.Notes -Match '(\<Env\>(?<Environment>.+)\<\/Env\>|\<Service\>(?<Service>.+)\<\/Service\>|\<DependsOn\>(?<DependsOn>.+)\<\/DependsOn\>)+' |Out-Null
    #[VMWithTag]::new('PI-LAB-GW03',(New-Guid),'PI-LAB','Network',@('HY','AZ'))
    #$VmWithTag = [VMWithTag]::new($VM.Name, $Vm, ($Matches.Environment -split ','), ($Matches.Service -split ','), ($Matches.DependsOn -split ','))
    $VmWithTag = [VMWithTag]::new($VM.Name, $Vm, ($Matches.Environment -split ',' | Where-Object {$_ -ne ''}), ($Matches.Service -split ','| Where-Object {$_ -ne ''}), ($Matches.DependsOn -split ','| Where-Object {$_ -ne ''}))
    $VmWithTag
}
# .\PSHVTag\private\Get-ClonedObject.ps1

function Get-ClonedObject
{
    <#
    .SYNOPSIS
    Clones object on Binary level
    
    .DESCRIPTION
    Clones object on Binary level
    
    .PARAMETER DeepCopyObject
    Object to Clone
    
    .EXAMPLE
    $currentEdgeList = [hashtable] (Get-ClonedObject $edgeList)
    
    .NOTES
    Idea from http://stackoverflow.com/questions/7468707/deep-copy-a-dictionary-hashtable-in-powershell
    borrowed from http://stackoverflow.com/questions/8982782/does-anyone-have-a-dependency-graph-and-topological-sorting-code-snippet-for-pow
    #>

    param($DeepCopyObject)
    $memStream = new-object IO.MemoryStream
    $formatter = new-object Runtime.Serialization.Formatters.Binary.BinaryFormatter
    $formatter.Serialize($memStream, $DeepCopyObject)
    $memStream.Position = 0
    $formatter.Deserialize($memStream)
}
# .\PSHVTag\private\Get-EdgeHashtableFromVMNote.ps1
function Get-EdgeHashtableFromVMNote
{
    <#
    .SYNOPSIS
    Creates a hashtable with KeyProperty as Key and Value property as (an array of) Value(s)
    
    .DESCRIPTION
    Creates a hashtable with KeyProperty as Key and Value property as (an array of) Value(s)
    
    .PARAMETER VM
    A VMwithTag object
    
    .PARAMETER KeyProperty
    The property of the VM object which should be used as Key in the hashtable to create
    
    .PARAMETER ValueProperty
    The property of the VM object to use as value of the hashtable to create
    
    .EXAMPLE
    Get-EdgeHashtableFromVMNote -VM $VM -KeyProperty Service -ValueProperty DependsOn
        
    #>

    [CmdletBinding()]
    [OutputType([System.Collections.Hashtable])]
    param(

        # Vm object with tags
        [Parameter(Mandatory = $true)]
        [VMwithTag[]]
        $VM,
        #Property to use as Key
        [Parameter(Mandatory = $true)]
        [string]
        $KeyProperty,
        #Property to use as Value
        [Parameter(Mandatory = $true)]
        [string]
        $ValueProperty
    )

    $EdgeList = @{}
    foreach ($Node in $VM)
    {
        Write-Verbose -Message ('Processing VM ' + $Node.Name + ' Key Property: ' + $KeyProperty)
        Foreach ($Key in $Node.$KeyProperty)
        {
            Write-Verbose -Message ('Processing VM ' + $Node.Name + ' Key Property: ' + $KeyProperty + ' Key value: ' + $Key)
            If (!($EdgeList.ContainsKey($Key)))
            {
                Write-Verbose -Message ('Creating new Key for ' + $Key)
                $EdgeList.Add($Key, $null)                
            }
            Foreach ($Value in $Node.$ValueProperty)
            {
                Write-Verbose -Message ('Processing VM ' + $Node.Name + ' Key Property: ' + $Key + ' Value Property: ' + $Value)
                If ($EdgeList.Item($Key) -notcontains $Value)
                {
                    Write-Verbose -Message ('Adding ' + $Value + ' to Key Property ' + $Key + ' values')
                    [array]$EdgeList.Item($Key) += [string]$Value
                }
            }
        }
    }
    $EdgeList #.GetEnumerator() | Sort-Object -Property Name
}
# .\PSHVTag\private\Get-TopologicalSort.ps1
function Get-TopologicalSort
{
    <#
    .SYNOPSIS
    Sorts Keys of a Hashtable containing dependent Keys as Value array as a topology.
    
    .DESCRIPTION
    Sorts Keys of a Hashtable containing dependent Keys as Value array as a topology. And returns an orederd list.
    
    .PARAMETER edgeList
    A hashtable containing the edges from the Key to the values
    
    .EXAMPLE
    Get-TopologicalSort -edgeList @{ServiceA=@(ServiceB,ServiceC),ServiceB=@(ServiceC)}
    
    .NOTES
    Thanks to http://stackoverflow.com/questions/8982782/does-anyone-have-a-dependency-graph-and-topological-sorting-code-snippet-for-pow
    Input is a hashtable of @{ID = @(Depended,On,IDs);...}
    #>

    param(
        [Parameter(Mandatory = $true, Position = 0)]
        [hashtable] $edgeList
    )
  
    # Make sure we can use HashSet
    Add-Type -AssemblyName System.Core
  
    # Clone it so as to not alter original
    $currentEdgeList = [hashtable] (Get-ClonedObject $edgeList)
  
    # algorithm from http://en.wikipedia.org/wiki/Topological_sorting#Algorithms
    $topologicallySortedElements = New-Object System.Collections.ArrayList
    $setOfAllNodesWithNoIncomingEdges = New-Object System.Collections.Queue
  
    $fasterEdgeList = @{}
  
    # Keep track of all nodes in case they put it in as an edge destination but not source
    $allNodes = New-Object -TypeName System.Collections.Generic.HashSet[object] -ArgumentList (, [object[]] $currentEdgeList.Keys)
  
    foreach ($currentNode in $currentEdgeList.Keys)
    {
        $currentDestinationNodes = [array] $currentEdgeList[$currentNode]
        if ($currentDestinationNodes.Length -eq 0)
        {
            $setOfAllNodesWithNoIncomingEdges.Enqueue($currentNode)
        }
  
        foreach ($currentDestinationNode in $currentDestinationNodes)
        {
            if (!$allNodes.Contains($currentDestinationNode))
            {
                [void] $allNodes.Add($currentDestinationNode)
            }
        }
  
        # Take this time to convert them to a HashSet for faster operation
        If ($currentDestinationNodes -ne $null)
        {
            $currentDestinationNodes = New-Object -TypeName System.Collections.Generic.HashSet[object] -ArgumentList (, [object[]] $currentDestinationNodes )
        }    
        [void] $fasterEdgeList.Add($currentNode, $currentDestinationNodes)        
    }
  
    # Now let's reconcile by adding empty dependencies for source nodes they didn't tell us about
    foreach ($currentNode in $allNodes)
    {
        if (!$currentEdgeList.ContainsKey($currentNode))
        {
            [void] $currentEdgeList.Add($currentNode, (New-Object -TypeName System.Collections.Generic.HashSet[object]))
            $setOfAllNodesWithNoIncomingEdges.Enqueue($currentNode)
        }
    }
  
    $currentEdgeList = $fasterEdgeList
  
    while ($setOfAllNodesWithNoIncomingEdges.Count -gt 0)
    {        
        $currentNode = $setOfAllNodesWithNoIncomingEdges.Dequeue()
        [void] $currentEdgeList.Remove($currentNode)
        [void] $topologicallySortedElements.Add($currentNode)
  
        foreach ($currentEdgeSourceNode in $currentEdgeList.Keys)
        {
            $currentNodeDestinations = $currentEdgeList[$currentEdgeSourceNode]
            if ($null -ne $currentNodeDestinations -and $currentNodeDestinations.Contains($currentNode))
            {
                [void] $currentNodeDestinations.Remove($currentNode)
  
                if ($currentNodeDestinations.Count -eq 0)
                {
                    [void] $setOfAllNodesWithNoIncomingEdges.Enqueue($currentEdgeSourceNode)
                }                
            }
        }
    }
  
    if ($currentEdgeList.Count -gt 0)
    {
        throw "Graph has at least one cycle!"
    }
  
    return $topologicallySortedElements
}
# .\PSHVTag\private\Get-VMEnvironment.ps1
function Get-VMEnvironment
{
    <#
    .SYNOPSIS
    Creates a (array of) VMEnvironment object(s)
    
    .DESCRIPTION
    Creates a (array of) VMEnvironment object(s) based on the list of tagged VMs defnied by the parameter VM.
    
    .PARAMETER VM
    A (list of) VM object(s) with Tags from which to build the VMEnvironment object
    
    .EXAMPLE
    Get-VMEnvironment -VM (Get-VMWithTag | Convert-VMNoteTagsToObject)
    
    #>

    [CmdletBinding()]
    param(
        # VM with Tag object gathered by Convert-VMNoteTagsToObject
        [Parameter(Mandatory = $true)]
        [VMWithTag[]] 
        $VM
    )
    $EdgeList = (Get-EdgeHashtableFromVMNote -VM $VM -KeyProperty 'Environment' -ValueProperty 'Service')
    [array]$VMEnvironment = @()
    foreach ($Environment in $EdgeList.Keys)
    {
        $EnvironmentVM = $VM | Where-Object -Property Environment -eq $Environment
        $Service = Get-VMService -VM $EnvironmentVM
        $EnvironmentServiceEdgeList = Get-EdgeHashtableFromVMNote -VM $EnvironmentVM -KeyProperty 'Service' -ValueProperty 'DependsOn'
        $EnvironmentOrder = Get-TopologicalSort -edgeList $EnvironmentServiceEdgeList
        If (Compare-Object -ReferenceObject ($Service | Select-Object -ExpandProperty Name) -DifferenceObject $EnvironmentOrder)
        {
            Throw 'One or more required services are not provided by a VM'
        }
        [array]$VMEnvironment += [VMEnvironment]::new($Environment, $Service, $EnvironmentVM, $EnvironmentServiceEdgeList, $EnvironmentOrder)
    }
    $VMEnvironment
}
# .\PSHVTag\private\Get-VMService.ps1
function Get-VMService
{
    <#
    .SYNOPSIS
    Creates a (array of) VMService object(s)
    
    .DESCRIPTION
    Creates a (array of) VMService object(s) based on the list of tagged VMs defined by the parameter VM.
    
    .PARAMETER VM
    A (list of) VM object(s) with Tags from which to build the VMEnvironment object
    
    .EXAMPLE
    Get-VMService -VM (Get-VMWithTag | Convert-VMNoteTagsToObject)
    
    #>

    [CmdletBinding()]
    [OutputType([array])]
    param(
        # VM with Tag object gathered by Convert-VMNoteTagsToObject
        [Parameter(Mandatory = $true)]
        [VMWithTag[]] 
        $VM
    )
    $EdgeList = Get-EdgeHashtableFromVMNote -VM $VM -KeyProperty 'Service' -ValueProperty 'DependsOn'
    [array]$VMService = @()
    foreach ($Service in $EdgeList.Keys)
    {
        [array]$VmService += [VMService]::new($Service, ($VM | Where-Object -Property Service -eq $Service), $EdgeList.Item($Service))
    }
    $VMService
}
# .\PSHVTag\private\Get-VMWithTag.ps1
function Get-VMWithTag
{
    <#
    .SYNOPSIS
    Gets all VMs containing Tags on a given host
    
    .DESCRIPTION
    Gets all VMs containing Tags on a Hyper-V-Host given by the parameter Computername
    
    .PARAMETER Computername
    The Hyper-V-Host getting the VMs conatining Tags from
    
    .EXAMPLE
    Get-VMWithTag -Computername localhost
    
    #>

    [CmdletBinding()]
    param(
        # Specifies the VM Host.
        [Parameter(Mandatory = $false,
            Position = 0,
            ValueFromPipeline = $true,
            ValueFromPipelineByPropertyName = $true,
            HelpMessage = "Name of VM Host computer(s)")]
        [ValidateNotNullOrEmpty()]
        [string]
        $Computername = 'localhost'
    )
    Get-VM -ComputerName $Computername | Where-Object {$_.Notes -match '\<.*\>'}
}

Write-Verbose 'Importing from [C:\MyProjects\PSHVTag\PSHVTag\public]'
# .\PSHVTag\public\Get-VMTopology.ps1
function Get-VMTopology
{
    <#
    .SYNOPSIS
    Gets a VM Topology object for tagged VMs
    
    .DESCRIPTION
    Gets a VM Topology object for tagged VMs on the Hyper-V-Host defined by the paramater Computername
    
    .PARAMETER Computername
    A Hyper-V-Host with tagged VMs
    
    .EXAMPLE
    Get-VMTopology -Computername hyper-v-host.contoso.com
    
    #>

    [CmdletBinding()]
    param(
        # Parameter help description
        [Parameter(Mandatory = $false)]
        [string]
        $Computername = 'localhost'
    )
    $VM = Get-VMWithTag -Computername $Computername | ForEach-Object {Convert-VMNoteTagsToObject -VM $_}
    $Environment = Get-VMEnvironment -VM $VM
    $VMTopology = [VMTopology]::new($Computername, $VM, $Environment)
    $VMTopology
}
# .\PSHVTag\public\Get-VMTopologyGraph.ps1
#Requires -Modules PSGraph
function Get-VMTopologyGraph
{
    <#
    .SYNOPSIS
    Exports a Topological Graph
    
    .DESCRIPTION
    Exports a Topological Graph as JPEG and opens it
    
    .PARAMETER VMTopology
    A VM Topology object
    
    .EXAMPLE
    Get-VMTopology -VMTopology (Get-VMTopology)

    #>

    [CmdletBinding()]
    param(
        # VMTopology to show
        [Parameter(Mandatory = $true)]
        [VMTopology]
        $VMTopology
    )
    Import-Module PSGraph
    $Graph = graph "myGraph" {
        node -Default @{shape = 'box'}
        $subGraphID = 0
        ForEach ($Environment in $VMTopology.Environment)
        {
            subgraph $subGraphID -Attributes @{label = $Environment.Name} {
                Foreach ($Service in $Environment.Service)
                {
                    #Node ($Environment.Name + '|' + $Service.Name) @{label = $Service.Name}
                    Record -Name ($Environment.Name + '|' + $Service.Name) -Rows ((($VMTopology.Environment | Where-Object -Property Name -Value $Environment.Name -EQ).Service | Where-Object -Property Name -Value $Service.Name -EQ).VM.Name) -Label $Service.Name
                    ForEach ($DependsOn in $Service.DependsOn)
                    {
                        If ($DependsOn -ne '')
                        {
                            edge -from ($Environment.Name + '|' + $Service.Name)  -to ($Environment.Name + '|' + $DependsOn)
                        }
                    }      
                }
            }
            $subGraphID = $subGraphID + 1
        }
    } 
    $Graph
}
# .\PSHVTag\public\Set-VMTag.ps1
function Set-VMTag
{
    <#
    .SYNOPSIS
    Tag a VM
    
    .DESCRIPTION
    This function tags one or more VMs with the Environment, Service and DependsOn Tag
    
    .PARAMETER Environment
    The environment the VM belongs to
    
    .PARAMETER Service
    The service the VM provides
    
    .PARAMETER DependsOn
    The Services the VM depends on
    
    .PARAMETER VMName
    The name of the VM
    
    .PARAMETER VM
    The VM object
    
    .PARAMETER Computername
    The VM host
    
    .PARAMETER Force
    Overwrite existing values
    
    .EXAMPLE
    Set-VMTag -Environment 'LAB01' -Service 'Domain' -DependsOn @('Gateway','DHCP') -VMName 'DomainController01' -Force
    #>

    [CmdletBinding(SupportsShouldProcess = $true)]
    
    param(
        # Name of the VMEnvironment the VM belongs to
        [Parameter(Mandatory = $false)]
        [string[]]
        $Environment,
        # Name of the VMService the VM provides
        [Parameter(Mandatory = $false)]
        [string[]]
        $Service,
        # Name of the VMServices the VM depends on
        [Parameter(Mandatory = $false)]
        [string[]]
        $DependsOn,
        # The name of the VM
        [Parameter(ParameterSetName = 'Name', Mandatory = $true)]
        [string[]]
        $VMName,
        # The VM object
        [Parameter(ParameterSetName = 'VMObject', Mandatory = $true)]
        [Microsoft.HyperV.PowerShell.VirtualMachine[]]
        $VM,
        # Specifies the VM Host.
        [Parameter(Mandatory = $false)]
        [ValidateNotNullOrEmpty()]
        [string]
        $Computername = 'localhost',
        # Force overwrite of existing Tag
        [Parameter()]
        [switch]
        $Force
    )
    Begin
    {
        $TagPattern = '\<Env\>.*\<\/Env\>\<Service\>.*\<\/Service\>\<DependsOn\>.*\<\/DependsOn\>'
        $returnObject = [pscustomobject]@{ Success = @(); Error = @()} 
        If ($PSCmdlet.ParameterSetName -eq 'Name')
        {
            $VM = @()
            foreach ($Name in $VMName)
            {
                try
                {
                    $VM += Get-VM -ComputerName $Computername | Where-Object -Property Name -EQ $Name
                }
                catch
                {
                    Write-Verbose "Can not find a VM with the name $Name at the host $Computername"
                    $returnObject.Error += $Name    
                }
            }
        }
        If (!($Vm))
        {
            Throw 'Can not find any VM for these parameters'
        }
    }
    Process
    {
        foreach ($Node in $VM)
        {
            If ($Node.Notes -Match $TagPattern -and (-not $Force))
            {
                Write-Verbose -Message "VM: $($VM.Name) already has a Tag and the paramater -force is not used"
                $returnObject.Error += $Node.Name  
            }
            elseif (($Node.Notes -Match $TagPattern -and $Force) -or ($Node.Notes -notMatch $TagPattern))
            {
                $Tag = '<Env>' + ($Environment -join ',') + '</Env><Service>' + ($Service -join ',') + '</Service><DependsOn>' + ($DependsOn -join ',') + '</DependsOn>'
                if ($Node.Notes -Match $TagPattern)
                {
                    $NewNotes = $Node.Notes -replace $TagPattern, $Tag
                }
                Elseif ($null -ne $Node.Notes -and $Node.Notes -ne '')
                {
                    $NewNotes = $Node.Notes + "`r`n" + $Tag + "`r`n"
                }
                Else
                {
                    $NewNotes = $Tag + "`r`n"
                }
                Write-Verbose -Message "Setting Tag on VM $($VM.Name) to $NewNotes"
                if ($PSCmdlet.ShouldProcess(('Setting VM Notes of: ' + $Node.Name + ' to ' + $NewNotes)))
                {
                    Try
                    {
                        Set-Vm -VM $Node -Notes $NewNotes
                        $returnObject.Success += $Node.Name 
                    }
                    catch
                    {
                        Write-Verbose -Message "Could not set Tag on VM $($VM.Name) to $NewNotes"
                        $returnObject.Error += $Node.Name 
                    }
                }
            }
        }
    }
    End
    {
        $returnObject
    }
}
# .\PSHVTag\public\Start-VMService.ps1
function Start-VMService
{
    <#
    .SYNOPSIS
    Starts all VMs of a VM Service in a VM Topology
    
    .DESCRIPTION
    Starts all VMs of a VM Service in a VM Topology and all VMs of required Vm Services
    
    .PARAMETER ServiceName
    The name of the VM Service to start
    
    .PARAMETER EnvironmentName
    The name of the VM Environment the VM Service is in
    
    .PARAMETER Service
    VMService object of the VM Service to start
    
    .PARAMETER Environment
    VMEnvironment object of the VM Service to start
    
    .PARAMETER VMTopology
    The VMTopology containing all VM Services and Environments
    
    .PARAMETER Recurse
    Start all required VM Services before starting the current VM Service
    
    .PARAMETER AdditionalWaitTime
    Additional time to wait after all VMs of a service are started successfully

    .PARAMETER VMWaitFor
    Item to wait for to determine if a VM is started successfully (IPAddress or Heartbeat)
    
    .EXAMPLE
    Start-VMService -ServiceName Domain -Environment Lab -VMTopology (Get-VMTopology) -Recurse
    
    #>

    [CmdletBinding(DefaultParameterSetName = 'String', SupportsShouldProcess = $true)]
    [OutputType([Bool])]
    param(
        # Name of the VM Service to start
        [Parameter(ParameterSetName = 'String', Mandatory = $true)]
        [Parameter(ParameterSetName = 'Object-String', Mandatory = $true)]     
        [string]
        $ServiceName,
        # Name of the Environment of the service to start
        [Parameter(ParameterSetName = 'String', Mandatory = $true)]
        [Parameter(ParameterSetName = 'String-Object', Mandatory = $true)]
        [string]
        $EnvironmentName,
        # Name of the VM Service to start
        [Parameter(ParameterSetName = 'Object', Mandatory = $true)]
        [Parameter(ParameterSetName = 'String-Object', Mandatory = $true)]     
        [VMService]
        $Service,
        # Name of the Environment of the service to start
        [Parameter(ParameterSetName = 'Object', Mandatory = $true)]
        [Parameter(ParameterSetName = 'Object-String', Mandatory = $true)]     
        [VMEnvironment]
        $Environment,
        # VMTopology
        [Parameter(Mandatory = $true)]
        [VMTopology]
        $VMTopology,
        # Start VM Services recursively
        [Parameter()]
        [switch]
        $Recurse,
        # Additional seconds to wait after VM is started
        [Parameter()]
        [int]
        $AdditionalWaitTime = 20,
        # Wait For
        [Parameter()]
        [String]
        [ValidateSet('IPAddress', 'Heartbeat')]
        $VMWaitFor = 'IPAddress'
    )
    Begin
    {
        switch ($PsCmdlet.ParameterSetName)
        {
            'Object'
            { 
                $ServiceName = $Service.Name
                $EnvironmentName = $Environment.Name
            }
            'Object-String'
            {
                $EnvironmentName = $Environment.Name
            }
            'String-Object'
            {
                $ServiceName = $Service.Name
            }
        }
        $Service = ($VMTopology.Environment | where-object -property name -eq $EnvironmentName).Service | Where-Object Name -eq $ServiceName
        If (!($Service))
        {
            Throw ('Could not find any VM for the service ' + $ServiceName + ' in the environment ' + $EnvironmentName)
        }
    }
    Process
    {
        if ($Recurse)
        {
            foreach ($Dependency in $Service.DependsOn )
            {
                Write-Verbose ('Starting required service: ' + $Dependency)
                Start-VMService -ServiceName $Dependency -EnvironmentName $EnvironmentName -VMTopology $VMTopology -Recurse | Out-Null
            }
        }
        foreach ($Node in $Service.VM)
        {
            If ($Node.VM.State -ne 'Running')
            {
                if ($PSCmdlet.ShouldProcess(('Starting VM: ' + $Node.Name)))
                {
                    Start-VM -VM $Node.Vm
                    $State = Wait-VM -VM $Node.vm -For $VMWaitFor -Timeout 120 -Passthru
                    Start-Sleep -Seconds $AdditionalWaitTime
                    If (!($State.State -eq 'Running'))
                    {
                        Throw ('Could not start VM: ' + $Node.Name)
                    }
                }
            }            
        }
    }
    End
    {
        Write-Verbose ('Successfully started VM Service: ' + $ServiceName)
        $true
    }
}
# .\PSHVTag\public\Stop-VMService.ps1
function Stop-VMService
{
    <#
    .SYNOPSIS
    Stopps all VMs of a VM Service in a VM Topology
    
    .DESCRIPTION
    Stopps all VMs of a VM Service in a VM Topology and all VMs of required Vm Services
    
    .PARAMETER ServiceName
    The name of the VM Service to start
    
    .PARAMETER EnvironmentName
    The name of the VM Environment the VM Service is in
    
    .PARAMETER Service
    VMService object of the VM Service to stop
    
    .PARAMETER Environment
    VMEnvironment object of the VM Service to stop
    
    .PARAMETER VMTopology
    The VMTopology containing all VM Services and Environments
    
    .PARAMETER Recurse
    Stop all required VM Services after stopping the current VM Service
    
    .PARAMETER Force
    Force shutdown of VMs
    
    .EXAMPLE
    Stop-VMService -ServiceName Domain -EnvironmentName Lab -VMTopology (Get-VMTopology) -Recurse
    
    #>

    [CmdletBinding(DefaultParameterSetName = 'String', SupportsShouldProcess = $true)]
    [OutputType([Bool])]
    param(
        # Name of the VM Service to stop
        [Parameter(ParameterSetName = 'String', Mandatory = $true)]
        [Parameter(ParameterSetName = 'Object-String', Mandatory = $true)]     
        [string]
        $ServiceName,
        # Name of the Environment of the service to stop
        [Parameter(ParameterSetName = 'String', Mandatory = $true)]
        [Parameter(ParameterSetName = 'String-Object', Mandatory = $true)]
        [string]
        $EnvironmentName,
        # Name of the VM Service to stop
        [Parameter(ParameterSetName = 'Object', Mandatory = $true)]
        [Parameter(ParameterSetName = 'String-Object', Mandatory = $true)]     
        [VMService]
        $Service,
        # Name of the Environment of the service to stop
        [Parameter(ParameterSetName = 'Object', Mandatory = $true)]
        [Parameter(ParameterSetName = 'Object-String', Mandatory = $true)]     
        [VMEnvironment]
        $Environment,
        # VMTopology
        [Parameter(Mandatory = $true)]
        [VMTopology]
        $VMTopology,
        # Stop VM Services recursively
        [Parameter()]
        [switch]
        $Recurse,
        # Force stopping of VM
        [Parameter()]
        [switch]
        $Force
    )
    Begin
    {
        switch ($PsCmdlet.ParameterSetName)
        {
            'Object'
            { 
                $ServiceName = $Service.Name
                $EnvironmentName = $Environment.Name
            }
            'Object-String'
            {
                $EnvironmentName = $Environment.Name
            }
            'String-Object'
            {
                $ServiceName = $Service.Name
            }
        }
        $Service = ($VMTopology.Environment | where-object -property name -eq $EnvironmentName).Service | Where-Object Name -eq $ServiceName
        If (!($Service))
        {
            Throw ('Could not find the service ' + $ServiceName + ' in the environment ' + $EnvironmentName)
        }
    }
    Process
    {
        foreach ($Node in $Service.VM)
        {
            If ($Node.VM.State -ne 'Off')
            {
                if ($PSCmdlet.ShouldProcess(('Stopping VM: ' + $Node.Name)))
                {
                    If ($Force)
                    {
                        $State = Stop-VM -VM $Node.Vm -Passthru -Force
                    }
                    else
                    {
                        $State = Stop-VM -VM $Node.Vm -Passthru
                    }
                    If (!($State.State -eq 'Off'))
                    {
                        Throw ('Could not stop VM: ' + $Node.Name)
                    }
                }
            }            
        }
        if ($Recurse)
        {
            foreach ($Dependency in $Service.DependsOn )
            {
                Write-Verbose ('Stopping required service: ' + $Dependency)
                If ($Force)
                {
                    Stop-VMService -ServiceName $Dependency -EnvironmentName $EnvironmentName -VMTopology $VMTopology -Recurse -Force | Out-Null
                }
                else
                {
                    Stop-VMService -ServiceName $Dependency -EnvironmentName $EnvironmentName -VMTopology $VMTopology -Recurse | Out-Null
                }
            }
        }
    }
    End
    {
        Write-Verbose ('Successfully stopped VM Service: ' + $ServiceName)
        $true
    }
}
Write-Verbose 'Importing from [C:\MyProjects\PSHVTag\PSHVTag\classes]'
# .\PSHVTag\classes\1VMWithTag.ps1
# VM with Tag
class VMWithTag
{
    # Name of the VM
    [string] $Name
    # VM ID
    $VM
    # Environment
    [String[]] $Environment
    # Service provided by VM
    [String[]] $Service
    # DependsOn Services
    [String[]] $DependsOn

    # Constructor
    VMWithTag ([string] $name, $VM, [String[]]$Environment, [String[]] $Service, [String[]] $DependsOn)
    {
        $this.Name = $name
        $this.VM = $VM
        $this.Environment = $Environment
        $this.Service = $Service
        $this.DependsOn = $DependsOn
    }
}
# .\PSHVTag\classes\2VMService.ps1
class VMService
{
    [string]$Name
    [VMWithTag[]]$VM
    [array]$DependsOn
    VMService ([string]$Name, [VMWithTag[]]$VM, [array]$DependsOn)
    {
        $this.Name = $Name
        $this.VM = $VM
        $this.DependsOn = $DependsOn
    }
}
# .\PSHVTag\classes\3VMEnvironment.ps1
class VMEnvironment
{
    [string]$Name
    [VMService[]]$Service
    [VMWithTag[]] $VM
    [System.Collections.ArrayList]$EdgeList
    [Array]$Order
    
    VMEnvironment ([string] $Name, [VMService[]]$Service, [VMWithTag[]] $VM, [System.Collections.ArrayList]$EdgeList, [Array]$Order)
    {
        $this.Name = $Name
        $this.Service = $Service
        $this.VM = $VM
        $this.EdgeList = $EdgeList
        $this.Order = $Order
    }
}
# .\PSHVTag\classes\4VMTopology.ps1
class VMTopology
{
    [string] $Computername
    [VMWithTag[]] $VM
    [VMEnvironment[]] $Environment
    # Constructor
    VMTopology ([string] $Computername, [VMWithTag[]] $VM, [VMEnvironment[]] $Environment)
    {
        $this.Computername = $Computername
        $this.VM = $VM
        $this.Environment = $Environment
    }
}