DscResources/HyperV/HyperV.schema.psm1

# see https://github.com/dsccommunity/xHyper-V
configuration HyperV
{
    param
    (
        [Parameter()]
        [ValidateSet('Server', 'Client')]
        $HostOS = 'Server',

        [Parameter()]
        [Boolean]
        $EnableEnhancedSessionMode = $false,

        [Parameter()]
        [String]
        $VirtualHardDiskPath,
        
        [Parameter()]
        [String]
        $VirtualMachinePath, 

        [Parameter()]
        [Hashtable[]]
        $VMSwitches,

        [Parameter()]
        [Hashtable[]]
        $VMMachines
    )

    Import-DscResource -ModuleName PSDesiredStateConfiguration
    Import-DscResource -ModuleName 'xHyper-V'

    [string]$dependsOnHostOS = $null

    if( $HostOS -eq 'Server' )
    {
        WindowsFeature 'Hyper-V-WinSrv'
        {
            Name   = 'Hyper-V'
            Ensure = 'Present'
        }
    
        WindowsFeature 'Hyper-V-Powershell-WinSrv'
        {
            Name        = 'Hyper-V-Powershell'
            Ensure      = 'Present'
            DependsOn   = '[WindowsFeature]Hyper-V-WinSrv'
        }

        $dependsOnHostOS = '[WindowsFeature]Hyper-V-Powershell-WinSrv'
    }
    elseif( $HostOS -eq 'Client' )
    {
        WindowsOptionalFeature 'Hyper-V-Win10'
        {
            Name                 = 'Microsoft-Hyper-V-All'
            NoWindowsUpdateCheck = $true
            RemoveFilesOnDisable = $true
            Ensure               = 'Enable'
        }

        $dependsOnHostOS = '[WindowsOptionalFeature]Hyper-V-Win10'
    }

    #######################################################################
    #region Host Settings

    $configHyperV = @{
        IsSingleInstance          = 'Yes'
        EnableEnhancedSessionMode = $EnableEnhancedSessionMode
    }

    if( -not [string]::IsNullOrWhiteSpace($VirtualHardDiskPath) )
    {
        $configHyperV.VirtualHardDiskPath = $VirtualHardDiskPath
    }
    if( -not [string]::IsNullOrWhiteSpace($VirtualMachinePath) )
    {
        $configHyperV.VirtualMachinePath = $VirtualMachinePath
    }

    (Get-DscSplattedResource -ResourceName xVMHost -ExecutionName 'config_HyperVHost' -Properties $configHyperV -NoInvoke).Invoke( $configHyperV )
    #endregion

    #######################################################################
    #region Virtual switches
    if( $null -ne $VMSwitches )
    {
        $natSwitch = ''

        foreach( $vmswitch in $VMSwitches )
        {
            # Remove case sensitivity of ordered Dictionary or Hashtables
            $vmswitch = @{} + $vmswitch

            $netName         = $vmswitch.Name
            $netAddressSpace = $vmswitch.AddressSpace
            $netIpAddress    = $vmswitch.IpAddress
            $netGateway      = $vmswitch.Gateway
            $netCategory     = $vmswitch.NetworkCategory
            $netInterfaceMetric = $vmswitch.InterfaceMetric

            $vmSwitch.Remove('AddressSpace')
            $vmSwitch.Remove('IpAddress')
            $vmSwitch.Remove('Gateway')
            $vmSwitch.Remove('NetworkCategory')
            $vmSwitch.Remove('InterfaceMetric')

            if( $vmswitch.Type -eq 'NAT' )
            {
                if( -not [string]::IsNullOrWhiteSpace($natSwitch) )
                {
                    throw "ERROR: Only one NAT switch is supported."
                }

                if( [string]::IsNullOrWhiteSpace($netAddressSpace) -or [string]::IsNullOrWhiteSpace($netIpAddress) )
                {
                    throw "ERROR: A NAT switch requires the 'AddressSpace' and 'IpAddress' attribute."
                }

                $natSwitch     = $vmswitch.Name
                $vmswitch.Type = 'Internal'
            }
            elseif( $vmswitch.Type -eq 'Private' -and
                    (-not [string]::IsNullOrWhiteSpace($netAddressSpace) -or
                     -not [string]::IsNullOrWhiteSpace($netIpAddress) -or
                     -not [string]::IsNullOrWhiteSpace($netGateway) -or
                     -not [string]::IsNullOrWhiteSpace($netCategory) -or
                     -not [string]::IsNullOrWhiteSpace($netInterfaceMetric) ))
            {
                throw "ERROR: A private switch doesn't support 'AddressSpace', 'IpAddress', 'Gateway', 'NetworkCategory' or 'InterfaceMetric' attribute."
            }

            $vmswitch.DependsOn = $dependsOnHostOS
            $executionName = $vmswitch.Name -replace '[().:\s]', '_'
            (Get-DscSplattedResource -ResourceName xVMSwitch -ExecutionName "vmswitch_$executionName" -Properties $vmswitch -NoInvoke).Invoke( $vmswitch )

            # enable Net Adressspace, IpAddress and NAT switch
            if( -not [string]::IsNullOrWhiteSpace($netIpAddress) -or -not [string]::IsNullOrWhiteSpace($netCategory) )
            {
                # Default is adapter auto configuration with 255.255.0.0 -> 16
                [int]$netPrefixLength = 16;

                if( -not [string]::IsNullOrWhiteSpace($netAddressSpace) )
                {
                    $prefixSelect = $netAddressSpace | Select-String '\d+\.\d+\.\d+\.\d+/(\d+)'

                    if( $null -eq $prefixSelect )
                    {
                        throw "ERROR: Invalid format of attribute 'AddressSpace'."
                    }

                    $netPrefixLength = $prefixSelect.Matches.Groups[1].Value
                }

                if( -not [string]::IsNullOrWhiteSpace($netIpAddress) -and -not ($netIpAddress -match '\d+\.\d+\.\d+\.\d+') )
                {
                    throw "ERROR: Invalid format of attribute 'IpAddress'."
                }

                if( -not [string]::IsNullOrWhiteSpace($netGateway) -and -not ($netGateway -match '\d+\.\d+\.\d+\.\d+') )
                {
                    throw "ERROR: Invalid format of attribute 'Gateway'."
                }

                if( -not [string]::IsNullOrWhiteSpace($netCategory) -and -not ($netCategory -match '^(Public|Private|DomainAuthenticated)$') )
                {
                    throw "ERROR: Invalid value of attribute 'NetworkCategory'."
                }

                Script "vmnet_$executionName"
                {
                    TestScript = 
                    {
                        [boolean]$result = $true
                        $netAdapter = Get-NetAdapter -ErrorAction SilentlyContinue | Where-Object { $_.Name -match $using:netName }                    

                        if( $null -eq $netAdapter )
                        {
                            $result = $false
                            Write-Verbose "NetAdapter containing switch name '$using:netName' not found."
                        }
                        elseif( -not [string]::IsNullOrWhiteSpace( $using:netGateway ) )
                        {
                            $netIpConfig = Get-NetIPConfiguration -InterfaceIndex $netAdapter.InterfaceIndex

                            if( $null -eq $netIpConfig -or $netIpConfig.IPv4DefaultGateway -ne $using:netGateway )
                            {
                                Write-Verbose "NetAdapter '$using:netName' has not the expected default gateway ($using:netGateway)."
                            }
                        }

                        if( -not [string]::IsNullOrWhiteSpace( $using:netIpAddress ) )
                        {
                            $netIpAddr  = Get-NetIPAddress -IPAddress $using:netIpAddress -ErrorAction SilentlyContinue

                            if( $null -eq $netIpAddr -or
                                $netIpAddr.InterfaceIndex -ne $netAdapter.InterfaceIndex -or
                                $netIpAddr.PrefixLength -ne $using:netPrefixLength )
                            {
                                $result = $false
                                Write-Verbose "NetAdapter '$using:netName' has not the expected IP configuration ($using:netIpAddress/$using:netPrefixLength)."
                            }
                        }

                        # check network category
                        if( -not [string]::IsNullOrWhiteSpace( $using:netCategory ) )
                        {
                            $netProfile = Get-NetConnectionProfile -InterfaceIndex $netAdapter.InterfaceIndex
           
                            if ($null -eq $netProfile -or $netProfile.NetworkCategory -ne $using:netCategory )
                            {
                                $result = $false
                                Write-Verbose "NetAdapter '$using:netName' has not the expected network category ($using:netCategory)."
                            }
                        }
                        
                        # check NAT switch
                        if( $using:netName -eq $using:natSwitch )
                        {
                            $netNat = Get-NetNat -Name $using:netName -ErrorAction SilentlyContinue

                            if( ($null -eq $netNat -or
                                $netNat.InternalIPInterfaceAddressPrefix -ne $using:netAddressSpace) )
                            {
                                $result = $false
                                Write-Verbose "NetNAT '$using:netName' not found or has not the expected configuration."
                            }
                        }

                        $result
                        return $result
                    }
                    SetScript = 
                    {
                        $netAdapter = Get-NetAdapter | Where-Object { $_.Name -match $using:netName }                    

                        if( $null -eq $netAdapter )
                        {
                            Write-Error "ERROR: NetAdapter containing switch name '$using:netName' not found."
                            return
                        }

                        if( -not [string]::IsNullOrWhiteSpace($using:netIpAddress) )
                        {
                            # remove existing configuration
                            Remove-NetIPAddress -IPAddress $using:netIpAddress -Confirm:$false -ErrorAction SilentlyContinue

                            # Remove all routes including the default gateway
                            Remove-NetRoute -InterfaceIndex $netAdapter.InterfaceIndex

                            Write-Verbose "Create IP Address '$using:netIpAddress'..."
                            $ipAddrParams = @{
                                IPAddress      = $using:netIpAddress
                                PrefixLength   = $using:netPrefixLength
                                InterfaceIndex = $netAdapter.InterfaceIndex
                            }
                            if( -not [string]::IsNullOrWhiteSpace( $using:netGateway ) )
                            {
                                $ipAddrParams.DefaultGateway = $using:netGateway
                            }
                            New-NetIPAddress @ipAddrParams
                        }

                        # set network category
                        if( -not [string]::IsNullOrWhiteSpace( $using:netCategory ) )
                        {
                            if( $using:netCategory -eq 'DomainAuthenticated')
                            {
                                Write-Verbose "Set NetworkCategory of NetAdapter '$using:netName' to '$using:netCategory ' is not supported. The computer automatically sets this value when the network is authenticated to a domain controller."
        
                                # Workaround if the computer is domain joined -> Restart NLA service to restart the network location check
                                # see https://newsignature.com/articles/network-location-awareness-service-can-ruin-day-fix/
                                Write-Verbose "Restarting NLA service to reinitialize the network location check..."
                                Restart-Service nlasvc -Force
                                Start-Sleep 5
        
                                $netProfile = Get-NetConnectionProfile -InterfaceIndex $netAdapter.InterfaceIndex
            
                                Write-Verbose "Current NetworkCategory is now: $($netProfile.NetworkCategory)"
                
                                if( $netProfile.NetworkCategory -ne $using:netCategory )
                                {
                                    Write-Error "NetAdapter '$using:netName' is not '$using:netCategory'."
                                } 
                            }
                            else
                            {
                                Write-Verbose "Set NetworkCategory of NetAdapter '$using:netName' to '$using:netCategory '."
                                Set-NetConnectionProfile -InterfaceIndex $netAdapter.InterfaceIndex -NetworkCategory $using:netCategory                             
                            }
                        }
                        
                        if( $using:netName -eq $using:natSwitch )
                        {
                            Remove-NetNat $using:netName -Confirm:$false -ErrorAction SilentlyContinue

                            Write-Verbose "Create NetNat '$using:netName'..."
                            New-NetNat -Name $using:netName -InternalIPInterfaceAddressPrefix $using:netAddressSpace
                        }
                    }
                    GetScript = { return @{result = 'N/A' } }
                    DependsOn = "[xVMSwitch]vmswitch_$executionName"
                }
            }

            if( $null -ne $netInterfaceMetric -and $netInterfaceMetric -gt 0 )
            {
                Script "vmnetInterfaceMetric_$executionName"
                {
                    TestScript = 
                    {
                        $netIf = Get-NetIpInterface | Where-Object { $_.InterfaceAlias -match $using:netName }                    
                        if( $null -eq $netIf )
                        {
                            Write-Verbose "NetAdapter containing switch name '$using:netName' not found."
                            return $false
                        }
    
                        [boolean]$result = $true
                        $netIf | ForEach-Object { Write-Verbose "InterfaceMetric $($_.AddressFamily): $($_.InterfaceMetric)"; 
                                                  if( $_.InterfaceMetric -ne $using:netInterfaceMetric ) { $result = $false }; }
    
                        Write-Verbose "Expected Interface Metric: $using:netInterfaceMetric"
                        return $result
                    }
                    SetScript = 
                    {
                        $netIf = Get-NetIpInterface | Where-Object { $_.InterfaceAlias -match $using:netName }                    
                        if( $null -eq $netIf )
                        {
                            Write-Error "NetAdapter containing switch name '$using:netName' not found."
                        }
                        else
                        {
                            $netIf | ForEach-Object { Write-Verbose "Set $($_.AddressFamily) InterfaceMetric to $($using:netInterfaceMetric)"; 
                                                      $_ | Set-NetIpInterface -InterfaceMetric $using:netInterfaceMetric }
                        }
                    }
                    GetScript = { return @{result = 'N/A' } }
                }
            }    
        }
    }
    #endregion

    #######################################################################
    #region Virtual machines
    if( $null -ne $VMMachines )
    {
        foreach( $vmmachine in $VMMachines )
        {
            $vmName  = $vmmachine.Name

            # VM state will be handled as last step
            $vmState = $vmmachine.State
            $vmmachine.Remove( 'State' )

            $strVMDepend = "[xVMHyperV]$vmName"
            $strVHDPath  = "$($vmmachine.Path)\$vmName\Disks"
            $iMemorySize = ($vmmachine.StartupMemory / 1MB) * 1MB

            # save the additional disk and drives definitions
            $arrDisks  = @()
            $arrDrives = @()
            if( $vmmachine.Disks -is [System.Array] )
            {
                for( $i = 1; $i -lt $vmmachine.Disks.Count; ++$i )
                {
                    $arrDisks += $vmmachine.Disks[$i]
                }
            }
            foreach( $drive in $vmmachine.Drives )
            {
                $arrDrives += $drive
            }

            # create the OS disk
            $disk = $vmmachine.Disks[0]
            if( $disk.Path.Length -lt 1 ) { $disk.Path = $strVHDPath }
            $strVHDName = "$($vmName)_$($disk.Name).vhdx"
            $strVHDFile = "$($disk.Path)\$strVHDName"
            $folderVHD  = "$($vmName)_DiskPath_$($disk.Name)"
            $strVHD     = "$($vmName)_Disk_$($disk.Name)"
            File $folderVHD
            {
                Ensure          = 'Present'
                Type            = 'Directory'
                DestinationPath = $disk.Path
                DependsOn       = $dependsOnHostOS
            }

            if( $disk.Contains( 'CopyFrom' ) )
            {
                $strVHDDepend = "[FILE]$strVHD"
                File $strVHD
                {
                    Ensure          = 'Present'
                    Type            = 'File'
                    SourcePath      = $disk.CopyFrom
                    DestinationPath = $strVHDFile
                    DependsOn       = "[File]$folderVHD"
                }
            }
            else
            {
                $iDiskSize    = ($disk.Size / 1GB) * 1GB
                $strVHDDepend = "[xVHD]$strVHD"
                xVHD $strVHD
                {
                    Name             = $strVHDName
                    Path             = $strVHDPath
                    MaximumSizeBytes = $iDiskSize
                    Generation       = 'Vhdx'
                    Type             = 'Dynamic'
                    DependsOn        = "[File]$folderVHD"
                }
            }

            # copy files before VM starts at first time
            if( $null -ne $disk.CopyOnce )
            {
                [int]$i = 0

                foreach( $copyStep in $disk.CopyOnce )
                {       
                    $i++
                    $scriptCopyOnce = "$($vmName)_Disk_$($disk.Name)_CopyOnce_$i"
                    $sources        = @() + $copyStep.Sources
                    $targetDir      = $copyStep.Destination
                    $excludes       = @() + $copyStep.Excludes
                    $prepareScripts = @() + $copyStep.PrepareScripts

                    if( $sources.Count -eq 0 -or [string]::IsNullOrWhiteSpace($sources[0]) )
                    {
                        throw "ERROR: Missing CopyOnce sources at disk '$($disk.Name)' of VM '$vmName'."
                    }
                    if( [string]::IsNullOrWhiteSpace($copyStep.Destination) )   
                    {
                        throw "ERROR: Missing CopyOnce destination at disk '$($disk.Name)' of VM '$vmName'."
                    }

                    Script $scriptCopyOnce
                    {
                        TestScript = 
                        {
                            # run only before creation of VM
                            if( $null -eq (Get-VM -Name $using:vmName -ErrorAction SilentlyContinue) )
                            {
                                Write-Verbose "VM '$using:vmName' not found. Performing CopyOnce action is possible."
                                return $false
                            }

                            Write-Verbose "The destination VM '$using:vmName' was found and no action is required."
                            return $true
                        }
                        SetScript = 
                        {
                            # reset readonly flags of VHDX file
                            Write-Verbose "Reset readonly file attribute of VHDX '$using:strVHDFile'..."  
                            Set-ItemProperty -Path $using:strVHDFile -Name IsReadOnly -Value $false -ErrorAction Stop

                            Write-Verbose "Mounting VHDX '$using:strVHDFile' on host computer '$env:COMPUTERNAME'..."  
                            $mountedDisk = Mount-VHD -Path $using:strVHDFile -Passthru -ErrorAction Stop
                            try
                            {
                                # execute prepare scripts
                                foreach( $script in $using:prepareScripts )
                                {
                                    Write-Verbose "Executing scriptblock: $script"  
                                    $exec = [ScriptBlock]::Create($script)
                                    Invoke-Command -ScriptBlock $exec -ErrorAction Stop
                                }

                                [String]$driveLetter = ($mountedDisk | Get-Disk | Get-Partition | Where-Object {$_.Type -eq "Basic"} | Select-Object -ExpandProperty DriveLetter) + ":"
                                $driveLetter = $driveLetter -replace '\s',''
                                Write-Verbose "VHDX '$using:strVHDFile' mounted as drive '$driveLetter'"

                                [String]$targetPath = "$driveLetter\$using:targetDir"
                                
                                Start-Sleep -Seconds 2  #Optional
                                Get-PSDrive | Out-Null  #refresh PsDrive, Optional

                                Write-Verbose "Copy files from host source directories $($using:sources -join ', ') to mounted VHDX '$targetPath'..."  
                                New-Item -Path $targetPath -ItemType Directory -Force
                                Copy-Item -Path $using:sources -Destination $targetPath -Exclude $using:excludes -Recurse -Force -Verbose -ErrorAction Stop                            
                            }
                            finally
                            {
                                Start-Sleep -Seconds 2 #Optional
                                Dismount-VHD $mountedDisk.Path
                            }
                        }
                        GetScript = { return @{result = 'N/A' } }
                        DependsOn = $strVHDDepend
                    }
    
                    $strVHDDepend = "[Script]$scriptCopyOnce"
                }
            }
            
            # set computername in unattended.xml
            $scriptSetComputerName = "$($vmName)_Disk_$($disk.Name)_SetComputerName"

            Script $scriptSetComputerName
            {
                TestScript = {
                    # run only before creation of VM
                    if( $null -eq (Get-VM -Name $using:vmName -ErrorAction SilentlyContinue) )
                    {
                        Write-Verbose "The destination object was found and no action is required."
                        return $false
                    }                    

                    Write-Verbose "VM '$using:vmName' not found. Performing SetComputerName action is possible."
                    return $true
                }
                SetScript = {
                    # reset readonly flags of VHDX file
                    Write-Verbose "Reset readonly file attribute of VHDX '$using:strVHDFile'..."  
                    Set-ItemProperty -Path $using:strVHDFile -Name IsReadOnly -Value $false -ErrorAction Stop

                    Write-Verbose "Mounting VHDX '$using:strVHDFile' on host computer '$env:COMPUTERNAME'..."  
                    $mountedDisk = Mount-VHD -Path $using:strVHDFile -Passthru -ErrorAction Stop
                    try
                    {
                        [String]$driveLetter = ($mountedDisk | Get-Disk | Get-Partition | Where-Object {$_.Type -eq "Basic"} | Select-Object -ExpandProperty DriveLetter) + ":"
                        $driveLetter = $driveLetter -replace '\s',''
                        Write-Verbose "VHDX '$using:strVHDFile' mounted as drive '$driveLetter'"
                        
                        Start-Sleep -Seconds 2  #Optional
                        Get-PSDrive | Out-Null  #refresh PsDrive, Optional

                        # path unattend.xml on several search paths
                        $unattendXmlPathList = @( "$driveLetter\unattend.xml", "$driveLetter\Windows\Panther\unattend.xml" )

                        foreach( $unattendXmlPath in $unattendXmlPathList )
                        {
                            if( Test-Path -Path $unattendXmlPath )
                            {
                                Write-Verbose "Set computername in '$unattendXmlPath' to '$using:vmName' on mounted VHDX '$using:strVHDFile'."  

                                [xml]$unattendXml = Get-Content -Path $unattendXmlPath

                                $ns = New-Object System.Xml.XmlNamespaceManager($unattendXml.NameTable)
                                $ns.AddNamespace("ns","urn:schemas-microsoft-com:unattend")

                                # set computername
                                $computerNameXml = $unattendXml.SelectSingleNode('//ns:component[@name="Microsoft-Windows-Shell-Setup"]/ns:ComputerName', $ns)
                                $computerNameXml.InnerText = $using:vmName

                                # reset readonly flags of target file
                                Set-ItemProperty -Path $unattendXmlPath -Name IsReadOnly -Value $false
                                
                                $unattendXml.Save( $unattendXmlPath )
                            }
                            else
                            {
                                Write-Verbose "File '$unattendXmlPath' not found on mounted VHDX '$using:strVHDFile'. Skipping set of computername."  
                            }
                        }
                    }
                    finally
                    {
                        Start-Sleep -Seconds 2 #Optional
                        Dismount-VHD $mountedDisk.Path
                    }
                }
                GetScript = { return @{result = 'N/A' } }
                DependsOn = $strVHDDepend
            }

            $strVHDDepend = "[Script]$scriptSetComputerName"

            # save specific network adapter definition
            $networkAdapters = $vmmachine.NetworkAdapters

            # save additional settings
            $checkpointType              = $vmmachine.CheckpointType
            $automaticCheckpointsEnabled = $vmmachine.AutomaticCheckpointsEnabled
            $automaticStartAction        = $vmmachine.AutomaticStartAction
            $automaticStartDelay         = $vmmachine.AutomaticStartDelay
            $automaticStopAction         = $vmmachine.AutomaticStopAction

            # save security settings
            $tpmEnabled     = $vmmachine.TpmEnabled
            $encryptTraffic = $vmmachine.EncryptStateAndVmMigrationTraffic

            # save integration services
            $enableTimeSyncService = $vmmachine.EnableTimeSyncService

            # remove all additional settings
            $vmmachine.Remove( 'Disks' )
            $vmmachine.Remove( 'Drives' )
            $vmmachine.Remove( 'NetworkAdapters' )
            $vmmachine.Remove( 'CheckpointType' )
            $vmmachine.Remove( 'AutomaticCheckpointsEnabled' )
            $vmmachine.Remove( 'AutomaticStartAction' )
            $vmmachine.Remove( 'AutomaticStartDelay' )
            $vmmachine.Remove( 'AutomaticStopAction' )
            $vmmachine.Remove( 'TpmEnabled' )
            $vmmachine.Remove( 'EncryptStateAndVmMigrationTraffic' )
            $vmmachine.Remove( 'EnableTimeSyncService' )

            # create the virtual machine
            $vmmachine.DependsOn          = $strVHDDepend
            $vmmachine.VhdPath            = $strVHDFile
            $vmmachine.Generation         = 2
            $vmmachine.StartupMemory      = $iMemorySize

            # remove all separators from MAC Address
            if( $vmmachine.MacAddress -ne $null )
            {
                $macAddrList = [System.Collections.ArrayList]@()

                if( $vmmachine.MacAddress -is [array] )
                {
                    foreach( $macAddr in $vmmachine.MacAddress )
                    {
                        $macAddrList.Add( ($macAddr -replace '[-:\s]','') )
                    }
                }
                else
                {
                    $macAddrList.Add( ($vmmachine.MacAddress -replace '[-:\s]','') )
                }
                $vmmachine.MacAddress = $macAddrList
            }

            (Get-DscSplattedResource -ResourceName xVMHyperV -ExecutionName $vmName -Properties $vmmachine -NoInvoke).Invoke( $vmmachine )
            
            $strVMdepends = "[xVMHyperV]$vmName"

            # additional VM settings
            if( $null -ne $checkpointType -or
                $null -ne $automaticCheckpointsEnabled -or
                $null -ne $automaticStartAction -or
                $null -ne $automaticStartDelay -or
                $null -ne $automaticStopAction )
            {
                $execName = "additionalProp_$vmName"

                Script $execName
                {
                    TestScript = {
                        [boolean]$status = $true
                        $vmProp = Get-VM -VMName $using:vmName | Select-Object CheckpointType, AutomaticStartAction, AutomaticStartDelay, AutomaticStopAction, AutomaticCheckpointsEnabled
                        
                        if( $null -ne $vmProp ) 
                        {
                            Write-Verbose "VM settings of '$using:vmName':`n$vmProp"

                            if( ($null -ne $using:checkpointType              -and $vmProp.CheckpointType -ne $using:checkpointType) -or
                                ($null -ne $using:automaticStartAction        -and $vmProp.AutomaticStartAction -ne $using:automaticStartAction) -or
                                ($null -ne $using:automaticStartDelay         -and $vmProp.AutomaticStartDelay -ne $using:automaticStartDelay) -or
                                ($null -ne $using:automaticStopAction         -and $vmProp.AutomaticStopAction -ne $using:automaticStopAction) -or
                                ($null -ne $using:automaticCheckpointsEnabled -and $vmProp.AutomaticCheckpointsEnabled -ne $using:automaticCheckpointsEnabled) )
                            {
                                $status = $false
                            }
                        }
                        else
                        {
                            Write-Verbose "VM settings not available."
                            $status = $false
                        }
                        return $status
                    }
                    SetScript = {
                        $vmProps = @{
                            VMName = $using:vmName
                        }

                        if( $null -ne $using:checkpointType )
                        {
                            $vmProps.CheckpointType = $using:checkpointType
                        }
                        if( $null -ne $using:automaticStartAction )
                        {
                            $vmProps.AutomaticStartAction = $using:automaticStartAction
                        }
                        if( $null -ne $using:automaticStartDelay )
                        {
                            $vmProps.AutomaticStartDelay = $using:automaticStartDelay
                        }
                        if( $null -ne $using:automaticStopAction )
                        {
                            $vmProps.AutomaticStopAction = $using:automaticStopAction
                        }
                        if( $null -ne $using:automaticCheckpointsEnabled )
                        {
                            $vmProps.AutomaticCheckpointsEnabled = $using:automaticCheckpointsEnabled
                        }

                        Set-VM @vmProps
                    }
                    GetScript = { return @{result = 'N/A' } }
                    DependsOn = $strVMdepends
                }

                $strVMdepends = "[Script]$execName"
            }

            # security VM settings
            if( $null -ne $tpmEnabled -or $null -ne $encryptTraffic )
            {
                $execName = "securityProp_$vmName"

                Script $execName
                {
                    TestScript = {
                        [boolean]$status = $true
                        $vmSec = Get-VMSecurity -VMName $using:vmName | Select-Object TpmEnabled, BindToHostTpm, EncryptStateAndVmMigrationTraffic, VirtualizationBasedSecurityOptOut, Shielded
                        
                        if( $null -ne $vmSec ) 
                        {
                            Write-Verbose "Security Settings of '$using:vmName':`n$vmSec"

                            if( ($null -ne $using:tpmEnabled -and $vmSec.TpmEnabled -ne $using:tpmEnabled) )
                            {
                                $status = $false
                            }

                            if( ($null -ne $using:encryptTraffic -and $vmSec.EncryptStateAndVmMigrationTraffic -ne $using:encryptTraffic) )
                            {
                                $status = $false
                            }
                        }
                        else
                        {
                            Write-Verbose "VM security settings not available."
                            $status = $false
                        }
                        return $status
                    }
                    SetScript = {
                        if( $null -ne $using:encryptTraffic )
                        {
                            Set-VMSecurity -VMName $using:vmName -EncryptStateAndVmMigrationTraffic $encryptTraffic
                        }

                        if( $null -ne $using:tpmEnabled )
                        {
                            if( $using:tpmEnabled -eq $true )
                            {
                                # create a new KeyProtectore if necessary
                                $key = Get-VMKeyProtector -VMName $using:vmName
                                if( $null -eq $key -or $key.Count -lt 10 )
                                {
                                    Set-VMKeyProtector -VMName $using:vmName -NewLocalKeyProtector
                                }
                                Enable-VMTPM -VMName $using:vmName
                            }
                            elseif( $using:tpmEnabled -eq $false )
                            {
                                Disable-VMTPM -VMName $using:vmName
                            }
                        }
                    }
                    GetScript = { return @{result = 'N/A' } }
                    DependsOn = $strVMdepends
                }

                $strVMdepends = "[Script]$execName"
            }

            # setup integration services
            #
            # Services have localized names, so we must use the last part of the id to identify the service
            #
            # Name Id
            # ---- --
            # Gastdienstschnittstelle Microsoft:F9C25F23-DB30-403D-A1CD-31FE241DFD54\6C09BB55-D683-4DA0-8931-C9BF705F6480
            # Takt Microsoft:F9C25F23-DB30-403D-A1CD-31FE241DFD54\84EAAE65-2F2E-45F5-9BB5-0E857DC8EB47
            # Austausch von Schlüsselwertepaaren Microsoft:F9C25F23-DB30-403D-A1CD-31FE241DFD54\2A34B1C2-FD73-4043-8A5B-DD2159BC743F
            # Herunterfahren Microsoft:F9C25F23-DB30-403D-A1CD-31FE241DFD54\9F8233AC-BE49-4C79-8EE3-E7E1985B2077
            # Zeitsynchronisierung Microsoft:F9C25F23-DB30-403D-A1CD-31FE241DFD54\2497F4DE-E9FA-4204-80E4-4B75C46419C0
            # VSS Microsoft:F9C25F23-DB30-403D-A1CD-31FE241DFD54\5CED1297-4598-4915-A5FC-AD21BB4D02A4
            #
            # Name Id
            # ---- --
            # Guest Service Interface Microsoft:700D77D9-74C3-4B2B-AE4D-D7B7089F9B2F\6C09BB55-D683-4DA0-8931-C9BF705F6480
            # Heartbeat Microsoft:700D77D9-74C3-4B2B-AE4D-D7B7089F9B2F\84EAAE65-2F2E-45F5-9BB5-0E857DC8EB47
            # Key-Value Pair Exchange Microsoft:700D77D9-74C3-4B2B-AE4D-D7B7089F9B2F\2A34B1C2-FD73-4043-8A5B-DD2159BC743F
            # Shutdown Microsoft:700D77D9-74C3-4B2B-AE4D-D7B7089F9B2F\9F8233AC-BE49-4C79-8EE3-E7E1985B2077
            # Time Synchronization Microsoft:700D77D9-74C3-4B2B-AE4D-D7B7089F9B2F\2497F4DE-E9FA-4204-80E4-4B75C46419C0
            # VSS Microsoft:700D77D9-74C3-4B2B-AE4D-D7B7089F9B2F\5CED1297-4598-4915-A5FC-AD21BB4D02A4

            if( $null -ne $enableTimeSyncService )
            {
                [boolean]$enableTimeSync = [System.Convert]::ToBoolean($enableTimeSyncService)

                $execName = "timeSyncService_$vmName"

                Script $execName
                {
                    TestScript = {
                        $timeSyncSvc = Get-VMIntegrationService -VMName $using:vmName | Where-Object { $_.Id -match '2497F4DE-E9FA-4204-80E4-4B75C46419C0$' } | Select-Object Name, Id, Enabled

                        Write-Verbose "VM: $using:vmName - time sync service: expected: $using:enableTimeSync - current: $timeSyncSvc"

                        if( $timeSyncSvc.Enabled -eq $using:enableTimeSync )
                        {
                            return $true
                        }
                        return $false
                    }
                    SetScript = {
                        $timeSyncSvc = Get-VMIntegrationService -VMName $using:vmName | Where-Object { $_.Id -match '2497F4DE-E9FA-4204-80E4-4B75C46419C0$' }
                        
                        if( $using:enableTimeSync -eq $true )
                        {
                            $timeSyncSvc | Enable-VMIntegrationService
                        }
                        else
                        {
                            $timeSyncSvc | Disable-VMIntegrationService 
                        }
                    }
                    GetScript = { return @{result = 'N/A' } }
                    DependsOn = $strVMdepends
                }

                $strVMdepends = "[Script]$execName"
            }

            # Network adapter fix
            # DSC Hyper-V Resource creates at least one network interface
            # If no switch and no MAC address is specified we delete this addional network adapter
            if( [string]::IsNullOrEmpty($vmmachine.MacAddress) -and [string]::IsNullOrEmpty($vmmachine.SwitchName) )
            {
                $execName = "removeUnusedNetAdapter_$vmName"

                Script $execName
                {
                    TestScript = {
                        $netAdapt = Get-VMNetworkAdapter -VMName $using:vmName | Where-Object { $_.Connected -eq $false -and $_.MacAddress -eq '000000000000' -and $_.DeviceNaming -eq 'Off' }

                        if( $null -ne $netAdapt )
                        {
                            Write-Verbose "VM: $using:vmName - unused network adapter found: $netAdapt"
                            return $false
                        }
                        else
                        {
                            Write-Verbose "VM: $using:vmName - no unused network adapter found."
                            return $true                            
                        }
                    }
                    SetScript = {
                        $netAdapt = Get-VMNetworkAdapter -VMName $using:vmName | Where-Object { $_.Connected -eq $false -and $_.MacAddress -eq '000000000000' -and $_.DeviceNaming -eq 'Off' }
                        $netAdapt | Remove-VMNetworkAdapter
                    }
                    GetScript = { return @{result = 'N/A' } }
                    DependsOn = $strVMdepends
                }

                $strVMdepends = "[Script]$execName"
            }

            # create specific network adapter
            foreach( $netAdapter in $networkAdapters )
            {
                # Remove Case Sensitivity of ordered Dictionary or Hashtables
                $netAdapter = @{}+$netAdapter

                $netAdapterName = $netAdapter.Name

                $netAdapter.Id        = "$($vmName)_$($netAdapterName)"
                $netAdapter.VMName    = $vmName
                $netAdapter.Ensure    = 'Present'
                $netAdapter.DependsOn = $strVMdepends

                if( $netAdapter.MacAddress -ne $null )
                {
                    # remove all separators from MAC Address
                    $netAdapter.MacAddress = $netAdapter.MacAddress -replace '[-:\s]',''
                }
    
                if( $netAdapter.NetworkSetting -ne $null )
                {
                    $netSetting = xNetworkSettings
                    {
                        IpAddress      = $netAdapter.NetworkSetting.IpAddress
                        Subnet         = $netAdapter.NetworkSetting.Subnet
                        DefaultGateway = $netAdapter.NetworkSetting.DefaultGateway
                        DnsServer      = $netAdapter.NetworkSetting.DnsServer
                    }

                    $netAdapter.NetworkSetting = $netSetting
                }
                # Disabled until Pull Request #189 is merged into xHyper-V DSC resource
                # see https://github.com/dsccommunity/xHyper-V/pull/189
                # elseif( $null -eq $netAdapter.IgnoreNetworkSetting )
                # {
                # # if no network settings are specified -> enable IgnoreNetworkSetting as default
                # $netAdapter.IgnoreNetworkSetting = $true
                # }

                # extract additional attributed
                $dhcpGuard   = $netAdapter.DhcpGuard
                $routerGuard = $netAdapter.RouterGuard

                $netAdapter.Remove( 'DhcpGuard' )
                $netAdapter.Remove( 'RouterGuard' )

                $execName = "netadapter_$($netAdapter.Id)"

                (Get-DscSplattedResource -ResourceName xVMNetworkAdapter -ExecutionName $execName -Properties $netAdapter -NoInvoke).Invoke( $netAdapter )

                Script "$($execName)_properties"
                {
                    TestScript = {
                        $netAdapter = Get-VMNetworkAdapter -VmName $using:vmName -Name $using:netAdapterName
                        
                        if( $null -ne $netAdapter ) 
                        {
                            $result = $true

                            Write-Verbose "Networkadapter '$using:netAdapterName': DeviceNaming is '$($netAdapter.DeviceNaming)' (expected: On)"

                            if( $netAdapter.DeviceNaming -ne 'On' )
                            {
                                $result = $false
                            }

                            Write-Verbose "Networkadapter '$using:netAdapterName': DhcpGuard is '$($netAdapter.DhcpGuard)' (expected: $using:dhcpGuard)"

                            if( -not [string]::IsNullOrWhiteSpace($using:dhcpGuard) -and $netAdapter.DhcpGuard -ne $using:dhcpGuard )
                            {
                                $result = $false
                            }

                            Write-Verbose "Networkadapter '$using:netAdapterName': RouterGuard is '$($netAdapter.RouterGuard)' (expected: $using:routerGuard)"

                            if( -not [string]::IsNullOrWhiteSpace($using:routerGuard) -and $netAdapter.RouterGuard -ne $using:routerGuard )
                            {
                                $result = $false
                            }

                            return $result
                        }
                        else
                        {
                            Write-Verbose "Networkadapter '$using:netAdapterName' not found."
                        }

                        return $false
                    }
                    SetScript = {
                        $params = @{
                            DeviceNaming = 'On'
                        }

                        if( -not [string]::IsNullOrWhiteSpace($using:dhcpGuard) )
                        {
                            $params.DhcpGuard = $using:dhcpGuard
                        }

                        if( -not [string]::IsNullOrWhiteSpace($using:routerGuard) )
                        {
                            $params.RouterGuard = $using:routerGuard
                        }

                        Write-Verbose "VM $($using:vmName): Set properties of network adapter $($using:netAdapterName)`n$($params | Out-String)"

                        Get-VMNetworkAdapter -VmName $using:vmName -Name $using:netAdapterName | Set-VMNetworkAdapter @params                        
                    }
                    GetScript = { return @{result = 'N/A' } }
                    DependsOn = "[xVMNetworkAdapter]$execName"
                }
            }

            # create further disks
            [string]$strHddDepends = $strVMDepend
            [int]$iController = 0
            [int]$iLocation   = 1
            foreach( $disk in $arrDisks )
            {
                # copy files before VM starts at first time
                if( $null -ne $disk.CopyOnce )
                {
                    throw "ERROR: CopyOnce is only supported on first disk of VM '$vmName'."
                }
                
                # create the VHD file
                if( [string]::IsNullOrWhiteSpace( $disk.Path ) )
                { 
                    $disk.Path = $strVHDPath
                }
                $strVHDName  = "$($vmName)_$($disk.Name).vhdx"
                $strVHDFile  = "$($disk.Path)\$strVHDName"
                $folderVHD   = "$($vmName)_DiskPath_$($disk.Name)"
                $strVHD      = "$($vmName)_Disk_$($disk.Name)"

                File $folderVHD
                {
                    Ensure          = 'Present'
                    Type            = 'Directory'
                    DestinationPath = $disk.Path
                    DependsOn       = $strHddDepends
                }
                if( $disk.Contains( 'CopyFrom' ) )
                {
                    $strVHDDepend = "[FILE]$strVHD"
                    File $strVHD
                    {
                        Ensure          = 'Present'
                        Type            = 'File'
                        SourcePath      = $disk.CopyFrom
                        DestinationPath = $strVHDFile
                        DependsOn       = "[File]$folderVHD"
                    }
                }
                else
                {
                    $iDiskSize    = ($disk.Size / 1GB) * 1GB
                    $strVHDDepend = "[xVHD]$strVHD"
                    xVHD $strVHD
                    {
                        Name             = $strVHDName
                        Path             = $disk.Path
                        MaximumSizeBytes = $iDiskSize
                        Generation       = 'Vhdx'
                        Type             = 'Dynamic'
                        DependsOn        = "[File]$folderVHD"
                    }
                }

                # attach the VHD file to the virtual machine
                $executionName = "$($vmName)_DiskAttach_$($disk.Name)"
                xVMHardDiskDrive $executionName
                {
                    Ensure             = 'Present'
                    VMName             = $vmName
                    Path               = $strVHDFile
                    ControllerType     = 'SCSI'
                    ControllerNumber   = $iController
                    ControllerLocation = $iLocation
                    DependsOn          = $strVMDepend, $strVHDDepend
                }

                ++$iLocation
                $strHddDepends = "[xVMHardDiskDrive]$executionName"
            }

            # create virtual drives and mount ISO files
            [string]$strDvdDepends = $strHddDepends
            $iLocation = 10
            foreach( $drive in $arrDrives )
            {
                $executionName = "$($vmName)_Drive_$($drive.Name)"

                if( $drive.Path.Length )
                {
                    xVMDvdDrive $executionName
                    {
                        Ensure             = 'Present'
                        VMName             = $vmName
                        Path               = $drive.Path
                        ControllerNumber   = $iController
                        ControllerLocation = $iLocation
                        DependsOn          = $strDvdDepends
                    }
                }
                else
                {
                    xVMDvdDrive $executionName
                    {
                        Ensure             = 'Present'
                        VMName             = $vmName
                        ControllerNumber   = $iController
                        ControllerLocation = $iLocation
                        DependsOn          = $strDvdDepends
                    }
                }

                ++$iLocation
                $strDvdDepends = "[xVMDvdDrive]$executionName"
            }

            # change boot order: first hdd -> system drive, last dvd -> OS install
            --$iLocation
            $scriptBootOrderName = "$($vmName)_ChangeBootOrder"
            Script $scriptBootOrderName
            {
                TestScript = {
                    $strPrefix  = "[$using:vmName]:"

                    $arrDVDDrives = Get-VMDvdDrive $using:vmName
                    if( ($null -eq $arrDVDDrives) -or ($arrDVDDrives.Count -lt 1) )
                    {
                        Write-Verbose "$strPrefix No DVD drives found. Boot order left unchanged."
                        return $true
                    }

                    $arrBoot = (Get-VMFirmware $using:vmName).BootOrder
                    if( ($null -eq $arrBoot) -or ($arrBoot.Count -lt 1) )
                    {
                        Write-Verbose "$strPrefix Can't get informations about the boot devices!"
                        return $false
                    }

                    # first boot device from type File is OK - skip this
                    [int]$bootIndex = 0

                    if ($arrBoot[0].BootType -eq 'File')
                    {
                        $bootIndex = 1    
                    }

                    if( $arrBoot.Count -eq (1 + $bootIndex) )
                    {
                        Write-Verbose "$strPrefix Only 1 boot device found. Boot order left unchanged."
                        return $true
                    }

                    $devBootHDD = $arrBoot[$bootIndex].Device
                    if( $null -eq $devBootHDD )
                    {
                        Write-Verbose "$strPrefix First boot device is null!"
                        return $false
                    }

                    $devBootDVD = $arrBoot[$bootIndex + 1].Device
                    if( $null -eq $devBootDVD )
                    {
                        Write-Verbose "$strPrefix Second boot device is null!"
                        return $false
                    }

                    $strMsg = "$strPrefix First boot device is set to: $($devBootHDD.ControllerType) $($devBootHDD.ControllerNumber):$($devBootHDD.ControllerLocation)"
                    if( [string]::IsNullOrEmpty($devBootHDD.Path) -eq $false ) { $strMsg += " ($($devBootHDD.Path))" }
                    Write-Verbose $strMsg

                    $strMsg = "$strPrefix Second boot device is set to: $($devBootDVD.ControllerType) $($devBootDVD.ControllerNumber):$($devBootDVD.ControllerLocation)"
                    if( [string]::IsNullOrEmpty($devBootDVD.Path) -eq $false ) { $strMsg += " ($($devBootDVD.Path))" }
                    Write-Verbose $strMsg

                    if( ($devBootHDD.ControllerNumber   -eq 0) -and
                        ($devBootHDD.ControllerLocation -eq 0) -and
                        ($devBootDVD.ControllerNumber   -eq $using:iController) -and
                        ($devBootDVD.ControllerLocation -eq $using:iLocation) )
                    {
                        Write-Verbose "$strPrefix The boot order is OK."
                        return $true
                    }
                    else
                    {
                        Write-Verbose "$strPrefix The boot order has to be modified."
                        return $false
                    }
                }
                SetScript = {
                    $strPrefix    = "[" + $using:vmName + "]:"
                    $arrHDDrives  = Get-VMHardDiskDrive $using:vmName
                    $arrDVDDrives = Get-VMDvdDrive $using:vmName

                    if( ($null -eq $arrHDDrives) -or ($arrHDDrives.Count -lt 1) )
                    {
                        Write-Error "$strPrefix Can't get informations about the hard disks!"
                        return $false
                    }

                    if( ($null -eq $arrDVDDrives) -or ($arrDVDDrives.Count -lt 1) )
                    {
                        Write-Verbose "$strPrefix No DVD drives found. Boot order left unchanged."
                        return $true
                    }

                    Set-VMFirmware $using:vmName -BootOrder $arrHDDrives[0], $arrDVDDrives[-1]
                }
                GetScript = { return @{result = 'N/A' } }
                DependsOn = $strDvdDepends
            }

            # check VM state as last step
            if( [string]::IsNullOrWhiteSpace($vmState) -eq $false )
            {
                Script "$($vmName)_State"
                {
                    TestScript = {
                        $vmObj = Get-VM -Name $using:vmName -ErrorAction Stop

                        if( $null -ne $vmObj -and $vmObj.State -eq $using:vmState )
                        {
                            return $true
                        }                    

                        return $false
                    }
                    SetScript = {
                        $vmObj = Get-VM -Name $using:vmName -ErrorAction Stop

                        if( $null -ne $vmObj -and $vmObj.State -ne $using:vmState )
                        {
                            if( $using:vmState -eq 'Running' )
                            {
                                Write-Verbose "[$using:vmName]: Starting VM '$using:vmState'"
                                Start-VM -Name $using:vmName
                            }
                            elseif( $using:vmState -eq 'Off' )
                            {
                                Write-Verbose "[$using:vmName]: Stopping VM '$using:vmState'"
                                Stop-VM -Name $using:vmName
                            }
                            elseif( $using:vmState -eq 'Paused' )
                            {
                                Write-Verbose "[$using:vmName]: Suspend VM '$using:vmState'"
                                Suspend-VM -Name $using:vmName
                            }
                        }
                    }
                    GetScript = { return @{result = 'N/A' } }
                    DependsOn = "[Script]$scriptBootOrderName"
                }
            }
        }
    }
    #endregion
}