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

            if ([string]::IsNullOrWhiteSpace( $vmswitch.Ensure ))
            {
                $vmswitch.Ensure = 'Present'
            }

            $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 ($vmswitch.Ensure -ne 'Absent')
                {
                    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
                    $vmswitch.Ensure -ne 'Absent' -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)

            if ($vmswitch.Ensure -ne 'Absent')
            {
                # 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 =
                    {
                        #the rule 'PSUseDeclaredVarsMoreThanAssignments' is triggered by the result variable even if it is used.
                        [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '')]
                        $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

            if ([string]::IsNullOrWhiteSpace( $vmmachine.Ensure ))
            {
                $vmmachine.Ensure = 'Present'
            }

            # 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

            if ($vmmachine.Ensure -eq 'Absent')
            {
                xVMHyperV $vmName
                {
                    Ensure  = 'Absent'
                    Name    = $vmName
                    VhdPath = $strVHDPath   # required parameter
                }

                # continue with next VM
                continue
            }

            # 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
}