DSCResources/MSFT_xVMHyperV/MSFT_xVMHyperV.psm1

$script:dscResourceCommonModulePath = Join-Path -Path $PSScriptRoot -ChildPath '../../Modules/DscResource.Common'
$script:hyperVDscCommonModulePath = Join-Path -Path $PSScriptRoot -ChildPath '../../Modules/HyperVDsc.Common'

Import-Module -Name $script:dscResourceCommonModulePath
Import-Module -Name $script:hyperVDscCommonModulePath

$script:localizedData = Get-LocalizedData -DefaultUICulture 'en-US'

function Get-TargetResource
{
    [CmdletBinding()]
    [OutputType([System.Collections.Hashtable])]
    param
    (
        [Parameter(Mandatory = $true)]
        [System.String]
        $Name,

        [Parameter(Mandatory = $true)]
        [System.String]
        $VhdPath
    )

    Write-Verbose -Message ($script:localizedData.QueryingVM -f $Name)

    # Check if Hyper-V module is present for Hyper-V cmdlets
    if (!(Get-Module -ListAvailable -Name Hyper-V))
    {
        throw ($script:localizedData.RoleMissingError -f 'Hyper-V')
    }

    $vmobj = Get-VM -Name $Name -ErrorAction SilentlyContinue

    # Check if 1 or 0 VM with name = $name exist
    if ($vmobj.count -gt 1)
    {
        throw ($script:localizedData.MoreThanOneVMExistsError -f $Name)
    }

    <#
        Retrieve the Vhd hierarchy to ensure we enumerate snapshots/differencing disks
        Fixes #28
    #>

    if ($null -ne $vmobj)
    {
        $vhdChain = @(Get-VhdHierarchy -VhdPath ($vmObj.HardDrives[0].Path))
    }

    $vmSecureBootState = $false
    if ($vmobj.Generation -eq 2)
    {
        # Retrieve secure boot status (can only be enabled on Generation 2 VMs) and convert to a boolean.
        $vmSecureBootState = ($vmobj | Get-VMFirmware).SecureBoot -eq 'On'
    }

    $guestServiceId = 'Microsoft:{0}\6C09BB55-D683-4DA0-8931-C9BF705F6480' -f $vmObj.Id

    $macAddress = @()
    $switchName = @()
    $ipAddress = @()

    foreach ($networkAdapter in $vmobj.NetworkAdapters)
    {
        $macAddress += $networkAdapter.MacAddress

        if (-Not ([System.String]::IsNullOrEmpty($networkAdapter.SwitchName)))
        {
            $switchName += $networkAdapter.SwitchName
        }

        if ($networkAdapter.IPAddresses.Count -ge 1)
        {
            $ipAddress += $networkAdapter.IPAddresses
        }
    }

    @{
        Name                        = $Name
        # Return the Vhd specified if it exists in the Vhd chain
        VhdPath                     = if ($vhdChain -contains $VhdPath)
        {
            $VhdPath
        }
        else
        {
            $null
        }
        SwitchName                  = $switchName
        State                       = $vmobj.State
        Path                        = $vmobj.Path
        Generation                  = $vmobj.Generation
        SecureBoot                  = $vmSecureBootState
        StartupMemory               = $vmobj.MemoryStartup
        MinimumMemory               = $vmobj.MemoryMinimum
        MaximumMemory               = $vmobj.MemoryMaximum
        MACAddress                  = $macAddress
        ProcessorCount              = $vmobj.ProcessorCount
        Ensure                      = if ($vmobj)
        {
            'Present'
        }
        else
        {
            'Absent'
        }
        ID                          = $vmobj.Id
        Status                      = $vmobj.Status
        CPUUsage                    = $vmobj.CPUUsage
        MemoryAssigned              = $vmobj.MemoryAssigned
        Uptime                      = $vmobj.Uptime
        CreationTime                = $vmobj.CreationTime
        HasDynamicMemory            = $vmobj.DynamicMemoryEnabled
        NetworkAdapters             = $ipAddress
        EnableGuestService          = ($vmobj | Get-VMIntegrationService | Where-Object -FilterScript { $_.Id -eq $guestServiceId }).Enabled
        AutomaticCheckpointsEnabled = $vmobj.AutomaticCheckpointsEnabled
    }
}

function Set-TargetResource
{
    [CmdletBinding()]
    param
    (
        # Name of the VM
        [Parameter(Mandatory = $true)]
        [System.String]
        $Name,

        # VHD associated with the VM
        [Parameter(Mandatory = $true)]
        [System.String]
        $VhdPath,

        # Virtual switch associated with the VM
        [Parameter()]
        [System.String[]]
        $SwitchName,

        # State of the VM
        [Parameter()]
        [ValidateSet('Running', 'Paused', 'Off')]
        [System.String]
        $State,

        # Folder where the VM data will be stored
        [Parameter()]
        [System.String]
        $Path,

        # Virtual machine generation
        [Parameter()]
        [ValidateRange(1, 2)]
        [System.UInt32]
        $Generation = 1,

        # Startup RAM for the VM
        [Parameter()]
        [ValidateRange(32MB, 65536MB)]
        [System.UInt64]
        $StartupMemory,

        # Minimum RAM for the VM. This enables dynamic memory
        [Parameter()]
        [ValidateRange(32MB, 65536MB)]
        [System.UInt64]
        $MinimumMemory,

        # Maximum RAM for the VM. This enables dynamic memory
        [Parameter()]
        [ValidateRange(32MB, 1048576MB)]
        [System.UInt64]
        $MaximumMemory,

        # MAC address of the VM
        [Parameter()]
        [System.String[]]
        $MACAddress,

        # Processor count for the VM
        [Parameter()]
        [System.UInt32]
        $ProcessorCount,

        # Waits for VM to get valid IP address
        [Parameter()]
        [System.Boolean]
        $WaitForIP,

        # If specified, shutdowns and restarts the VM as needed for property changes
        [Parameter()]
        [System.Boolean]
        $RestartIfNeeded,

        # Should the VM be created or deleted
        [Parameter()]
        [ValidateSet('Present', 'Absent')]
        [System.String]
        $Ensure = 'Present',

        [Parameter()]
        [System.String]
        $Notes,

        # Enable secure boot for Generation 2 VMs
        [Parameter()]
        [System.Boolean]
        $SecureBoot = $true,

        # Enable Guest Services
        [Parameter()]
        [System.Boolean]
        $EnableGuestService = $false,

        # Enable AutomaticCheckpoints
        [Parameter()]
        [System.Boolean]
        $AutomaticCheckpointsEnabled
    )

    # Check if Hyper-V module is present for Hyper-V cmdlets
    if (!(Get-Module -ListAvailable -Name Hyper-V))
    {
        throw ($script:localizedData.RoleMissingError -f 'Hyper-V')
    }

    # Check if AutomaticCheckpointsEnabled is set in configuration
    if ($PSBoundParameters.ContainsKey('AutomaticCheckpointsEnabled'))
    {
        <#
            Check if AutomaticCheckpoints are supported
            If AutomaticCheckpoints are supported, parameter exists on Set-VM
        #>

        if (-Not (Get-Command -Name Set-VM -Module Hyper-V).Parameters.ContainsKey('AutomaticCheckpointsEnabled'))
        {
            throw ($script:localizedData.AutomaticCheckpointsUnsupported)
        }
    }

    Write-Verbose -Message ($script:localizedData.CheckingVMExists -f $Name)
    $vmObj = Get-VM -Name $Name -ErrorAction SilentlyContinue

    # VM already exists
    if ($vmObj)
    {
        Write-Verbose -Message ($script:localizedData.VMExists -f $Name)

        # If VM shouldn't be there, stop it and remove it
        if ($Ensure -eq 'Absent')
        {
            Write-Verbose -Message ($script:localizedData.VMPropertyShouldBe -f 'Ensure', $Ensure, 'Present')
            Get-VM $Name | Stop-VM -Force -Passthru -WarningAction SilentlyContinue | Remove-VM -Force
            Write-Verbose -Message ($script:localizedData.VMPropertySet -f 'Ensure', $Ensure)
        }

        <#
            If VM is present, check its state, startup memory, minimum memory, maximum memory,processor count, automatic checkpoint and mac address
            One cannot set the VM's vhdpath, path, generation and switchName after creation
        #>

        else
        {
            # If state has been specified and the VM is not in right state, set it to right state
            if ($State -and ($vmObj.State -ne $State))
            {
                Write-Verbose -Message ($script:localizedData.VMPropertyShouldBe -f 'State', $State, $vmObj.State)
                Set-VMState -Name $Name -State $State -WaitForIP $WaitForIP
                Write-Verbose -Message ($script:localizedData.VMPropertySet -f 'State', $State)
            }

            $changeProperty = @{ }
            # If the VM does not have the right startup memory
            if ($PSBoundParameters.ContainsKey('StartupMemory') -and ($vmObj.MemoryStartup -ne $StartupMemory))
            {
                Write-Verbose -Message ($script:localizedData.VMPropertyShouldBe -f 'MemoryStartup', $StartupMemory, $vmObj.MemoryStartup)
                $changeProperty['MemoryStartup'] = $StartupMemory
            }
            elseif ($PSBoundParameters.ContainsKey('MinimumMemory') -and ($vmObj.MemoryStartup -lt $MinimumMemory))
            {
                Write-Verbose -Message ($script:localizedData.AdjustingLessThanMemoryWarning -f 'StartupMemory', $vmObj.MemoryStartup, 'MinimumMemory', $MinimumMemory)
                $changeProperty['MemoryStartup'] = $MinimumMemory
            }
            elseif ($PSBoundParameters.ContainsKey('MaximumMemory') -and ($vmObj.MemoryStartup -gt $MaximumMemory))
            {
                Write-Verbose -Message ($script:localizedData.AdjustingGreaterThanMemoryWarning -f 'StartupMemory', $vmObj.MemoryStartup, 'MaximumMemory', $MaximumMemory)
                $changeProperty['MemoryStartup'] = $MaximumMemory
            }

            # If the VM does not have the right minimum or maximum memory, stop the VM, set the right memory, start the VM
            if ($PSBoundParameters.ContainsKey('MinimumMemory') -or $PSBoundParameters.ContainsKey('MaximumMemory'))
            {
                $changeProperty['DynamicMemory'] = $true
                $changeProperty['StaticMemory'] = $false

                if ($PSBoundParameters.ContainsKey('MinimumMemory') -and ($vmObj.Memoryminimum -ne $MinimumMemory))
                {
                    Write-Verbose -Message ($script:localizedData.VMPropertyShouldBe -f 'MinimumMemory', $MinimumMemory, $vmObj.MemoryMinimum)
                    $changeProperty['MemoryMinimum'] = $MinimumMemory
                }
                if ($PSBoundParameters.ContainsKey('MaximumMemory') -and ($vmObj.Memorymaximum -ne $MaximumMemory))
                {
                    Write-Verbose -Message ($script:localizedData.VMPropertyShouldBe -f 'MaximumMemory', $MaximumMemory, $vmObj.MemoryMaximum)
                    $changeProperty['MemoryMaximum'] = $MaximumMemory
                }
            }

            # If the VM does not have the right processor count, stop the VM, set the right memory, start the VM
            if ($PSBoundParameters.ContainsKey('ProcessorCount') -and ($vmObj.ProcessorCount -ne $ProcessorCount))
            {
                Write-Verbose -Message ($script:localizedData.VMPropertyShouldBe -f 'ProcessorCount', $ProcessorCount, $vmObj.ProcessorCount)
                $changeProperty['ProcessorCount'] = $ProcessorCount
            }

            # Stop the VM, set the right properties, start the VM only if there are properties to change
            if ($changeProperty.Count -gt 0)
            {
                Set-VMProperty -Name $Name -VMCommand 'Set-VM' -ChangeProperty $changeProperty -WaitForIP $WaitForIP -RestartIfNeeded $RestartIfNeeded
                Write-Verbose -Message ($script:localizedData.VMPropertiesUpdated -f $Name)
            }

            <#
                Special cases to disable dynamic memory:
                - If startup, minimum and maximum memory are specified with equal values or
                - If only startup memory is specified, but neither minimum nor maximum
            #>

            if ( ($PSBoundParameters.ContainsKey('StartupMemory') -and
                    ($StartupMemory -eq $MinimumMemory) -and
                    ($StartupMemory -eq $MaximumMemory)
                ) -or
                ( $PSBoundParameters.ContainsKey('StartupMemory') -and
                    (-not $PSBoundParameters.ContainsKey('MinimumMemory')) -and
                    (-not $PSBoundParameters.ContainsKey('MaximumMemory'))
                )
            )
            {
                # Refresh VM properties
                $vmObj = Get-VM -Name $Name -ErrorAction SilentlyContinue
                if ($vmObj.DynamicMemoryEnabled)
                {
                    Write-Verbose -Message ($script:localizedData.VMPropertyShouldBe -f 'DynamicMemoryEnabled', $false, $vmObj.DynamicMemoryEnabled)
                    $setVMPropertyParams = @{
                        VMName          = $Name
                        VMCommand       = 'Set-VM'
                        ChangeProperty  = @{
                            StaticMemory  = $true
                            DynamicMemory = $false
                        }
                        WaitForIP       = $WaitForIP
                        RestartIfNeeded = $RestartIfNeeded
                    }
                    Set-VMProperty @setVMPropertyParams
                    Write-Verbose -Message ($script:localizedData.VMPropertiesUpdated -f $Name)
                }
            }

            # Set VM network switches. This can be done while the VM is running.
            for ($i = 0; $i -lt $SwitchName.Count; $i++)
            {
                $switch = $SwitchName[$i]
                $nic = $vmObj.NetworkAdapters[$i]
                if ($nic)
                {
                    # We cannot change the MAC address whilst the VM is running.. This is changed later
                    if ($nic.SwitchName -ne $switch)
                    {
                        Write-Verbose -Message ($script:localizedData.VMPropertyShouldBe -f 'NIC', $switch, $nic.SwitchName)
                        $nic | Connect-VMNetworkAdapter -SwitchName $switch
                        Write-Verbose -Message ($script:localizedData.VMPropertySet -f 'NIC', $switch)
                    }
                }
                else
                {
                    Write-Verbose -Message ($script:localizedData.VMPropertyShouldBe -f 'NIC', $switch, '<missing>')
                    if ($MACAddress -and (-not [System.String]::IsNullOrEmpty($MACAddress[$i])))
                    {
                        Add-VMNetworkAdapter -VMName $Name -SwitchName $switch -StaticMacAddress $MACAddress[$i]
                        Write-Verbose -Message ($script:localizedData.VMPropertySet -f 'NIC', $switch)
                    }
                    else
                    {
                        Add-VMNetworkAdapter -VMName $Name -SwitchName $switch
                        Write-Verbose -Message ($script:localizedData.VMPropertySet -f 'NIC', $switch)
                    }
                    # Refresh the NICs after we've added one
                    $vmObj = Get-VM -Name $Name -ErrorAction SilentlyContinue
                }
            }

            # If the VM does not have the right MACAddress, stop the VM, set the right MACAddress, start the VM
            for ($i = 0; $i -lt $MACAddress.Count; $i++)
            {
                $address = $MACAddress[$i]
                $nic = $vmObj.NetworkAdapters[$i]
                if ($nic.MacAddress -ne $address)
                {
                    Write-Verbose -Message ($script:localizedData.VMPropertyShouldBe -f 'MACAddress', $address, $nic.MacAddress)
                    Set-VMMACAddress -Name $Name -NICIndex $i -MACAddress $address -WaitForIP $WaitForIP -RestartIfNeeded $RestartIfNeeded
                }
            }

            if ($Generation -eq 2)
            {
                # Retrive the current secure boot state
                $vmSecureBoot = Test-VMSecureBoot -Name $Name
                if ($SecureBoot -ne $vmSecureBoot)
                {
                    Write-Verbose -Message ($script:localizedData.VMPropertyShouldBe -f 'SecureBoot', $SecureBoot, $vmSecureBoot)

                    if (-not $SecureBoot)
                    {
                        $enableSecureBoot = 'On'
                    }
                    else
                    {
                        $enableSecureBoot = 'Off'
                    }

                    # Cannot change the secure boot state whilst the VM is powered on.
                    $setVMPropertyParams = @{
                        VMName          = $Name
                        VMCommand       = 'Set-VMFirmware'
                        ChangeProperty  = @{
                            EnableSecureBoot = $enableSecureBoot
                        }
                        RestartIfNeeded = $RestartIfNeeded
                    }
                    Set-VMProperty @setVMPropertyParams
                    Write-Verbose -Message ($script:localizedData.VMPropertySet -f 'SecureBoot', $SecureBoot)
                }
            }

            if ($Notes -ne $null)
            {
                # If the VM notes do not match the desire notes, update them. This can be done while the VM is running.
                if ($vmObj.Notes -ne $Notes)
                {
                    Set-Vm -Name $Name -Notes $Notes
                }
            }

            # If the VM doesn't have Guest Service Interface correctly configured, update it.
            $guestServiceId = 'Microsoft:{0}\6C09BB55-D683-4DA0-8931-C9BF705F6480' -f $vmObj.Id

            $guestService = $vmObj | Get-VMIntegrationService | Where-Object -FilterScript { $_.Id -eq $guestServiceId }
            if ($guestService.Enabled -eq $false -and $EnableGuestService)
            {
                Write-Verbose -Message ($script:localizedData.VMPropertyShouldBe -f 'EnableGuestService', $EnableGuestService, $guestService.Enabled)
                $guestService | Enable-VMIntegrationService
                Write-Verbose -Message ($script:localizedData.VMPropertySet -f 'EnableGuestService', $EnableGuestService)
            }
            elseif ($guestService.Enabled -and -not $EnableGuestService)
            {
                Write-Verbose -Message ($script:localizedData.VMPropertyShouldBe -f 'EnableGuestService', $EnableGuestService, $guestService.Enabled)
                $guestService | Disable-VMIntegrationService
                Write-Verbose -Message ($script:localizedData.VMPropertySet -f 'EnableGuestService', $EnableGuestService)
            }

            # If AutomaticCheckpointsEnabled is set in configuration
            if ($PSBoundParameters.ContainsKey('AutomaticCheckpointsEnabled'))
            {
                if ($vmObj.AutomaticCheckpointsEnabled -ne $AutomaticCheckpointsEnabled)
                {
                    Set-VM -Name $Name -AutomaticCheckpointsEnabled $AutomaticCheckpointsEnabled
                }
            }
        }
    }

    # VM is not present, create one
    else
    {
        Write-Verbose -Message ($script:localizedData.VMDoesNotExist -f $Name)
        if ($Ensure -eq 'Present')
        {
            Write-Verbose -Message ($script:localizedData.CreatingVM -f $Name)

            $parameters = @{ }
            $parameters['Name'] = $Name
            $parameters['VHDPath'] = $VhdPath
            $parameters['Generation'] = $Generation

            # Optional parameters
            if ($SwitchName)
            {
                $parameters['SwitchName'] = $SwitchName[0]
            }
            if ($Path)
            {
                $parameters['Path'] = $Path
            }
            $defaultStartupMemory = 512MB
            if ($PSBoundParameters.ContainsKey('StartupMemory'))
            {
                $parameters['MemoryStartupBytes'] = $StartupMemory
            }
            elseif ($PSBoundParameters.ContainsKey('MinimumMemory') -and ($defaultStartupMemory -lt $MinimumMemory))
            {
                $parameters['MemoryStartupBytes'] = $MinimumMemory
            }
            elseif ($PSBoundParameters.ContainsKey('MaximumMemory') -and ($defaultStartupMemory -gt $MaximumMemory))
            {
                $parameters['MemoryStartupBytes'] = $MaximumMemory
            }
            $null = New-VM @parameters

            $parameters = @{ }
            $parameters['Name'] = $Name
            $parameters['StaticMemory'] = $true
            $parameters['DynamicMemory'] = $false
            if ($PSBoundParameters.ContainsKey('MinimumMemory') -or $PSBoundParameters.ContainsKey('MaximumMemory'))
            {
                $parameters['DynamicMemory'] = $true
                $parameters['StaticMemory'] = $false
                if ($PSBoundParameters.ContainsKey('MinimumMemory'))
                {
                    $parameters['MemoryMinimumBytes'] = $MinimumMemory
                }
                if ($PSBoundParameters.ContainsKey('MaximumMemory'))
                {
                    $parameters['MemoryMaximumBytes'] = $MaximumMemory
                }
            }

            if ($Notes)
            {
                $parameters['Notes'] = $Notes
            }

            if ($PSBoundParameters.ContainsKey('ProcessorCount'))
            {
                $parameters['ProcessorCount'] = $ProcessorCount
            }

            # If AutomaticCheckpointsEnabled is set in configuration
            if ($PSBoundParameters.ContainsKey('AutomaticCheckpointsEnabled'))
            {
                $parameters['AutomaticCheckpointsEnabled'] = $AutomaticCheckpointsEnabled
            }

            $null = Set-VM @parameters

            # Special case: Disable dynamic memory if startup, minimum and maximum memory are equal
            if ($PSBoundParameters.ContainsKey('StartupMemory') -and
                ($StartupMemory -eq $MinimumMemory) -and
                ($StartupMemory -eq $MaximumMemory))
            {
                Set-VMMemory -VMName $Name -DynamicMemoryEnabled $false
            }

            # There's always a NIC added with New-VM
            if ($MACAddress)
            {
                Set-VMNetworkAdapter -VMName $Name -StaticMacAddress $MACAddress[0]
            }

            # Add additional NICs
            for ($i = 1; $i -lt $SwitchName.Count; $i++)
            {
                $addVMNetworkAdapterParams = @{
                    VMName     = $Name
                    SwitchName = $SwitchName[$i]
                }
                if ($MACAddress -and (-not [System.String]::IsNullOrEmpty($MACAddress[$i])))
                {
                    $addVMNetworkAdapterParams['StaticMacAddress'] = $MACAddress[$i]
                }
                Add-VMNetworkAdapter @addVMNetworkAdapterParams
                Write-Verbose -Message ($script:localizedData.VMPropertySet -f 'NIC', $SwitchName[$i])
            }

            if ($Generation -eq 2)
            {
                <#
                    Secure boot is only applicable to Generation 2 VMs and it defaults to on.
                    Therefore, we only need to explicitly set it to off if specified.
                #>

                if ($SecureBoot -eq $false)
                {
                    Set-VMFirmware -VMName $Name -EnableSecureBoot Off
                }
            }

            if ($EnableGuestService)
            {
                $guestServiceId = 'Microsoft:{0}\6C09BB55-D683-4DA0-8931-C9BF705F6480' -f (Get-VM -Name $Name).Id
                Get-VMIntegrationService -VMName $Name | Where-Object -FilterScript { $_.Id -eq $guestServiceId } | Enable-VMIntegrationService
            }

            Write-Verbose -Message ($script:localizedData.VMCreated -f $Name)

            if ($State)
            {
                Set-VMState -Name $Name -State $State -WaitForIP $WaitForIP
                Write-Verbose -Message ($script:localizedData.VMPropertySet -f 'State', $State)
            }

        }
    }
}

function Test-TargetResource
{
    [CmdletBinding()]
    [OutputType([System.Boolean])]
    param
    (
        # Name of the VM
        [Parameter(Mandatory = $true)]
        [System.String]
        $Name,

        # VHD associated with the VM
        [Parameter(Mandatory = $true)]
        [System.String]
        $VhdPath,

        # Virtual switch associated with the VM
        [Parameter()]
        [System.String[]]
        $SwitchName,

        # State of the VM
        [Parameter()]
        [ValidateSet('Running', 'Paused', 'Off')]
        [System.String]
        $State,

        # Folder where the VM data will be stored
        [Parameter()]
        [System.String]
        $Path,

        # Virtual machine generation
        [Parameter()]
        [ValidateRange(1, 2)]
        [System.UInt32]
        $Generation = 1,

        # Startup RAM for the VM
        [Parameter()]
        [ValidateRange(32MB, 65536MB)]
        [System.UInt64]
        $StartupMemory,

        # Minimum RAM for the VM. This enables dynamic memory
        [Parameter()]
        [ValidateRange(32MB, 65536MB)]
        [System.UInt64]
        $MinimumMemory,

        # Maximum RAM for the VM. This enables dynamic memory
        [Parameter()]
        [ValidateRange(32MB, 1048576MB)]
        [System.UInt64]
        $MaximumMemory,

        # MAC address of the VM
        [Parameter()]
        [System.String[]]
        $MACAddress,

        # Processor count for the VM
        [Parameter()]
        [System.UInt32]
        $ProcessorCount,

        # Waits for VM to get valid IP address
        [Parameter()]
        [System.Boolean]
        $WaitForIP,

        # If specified, shutdowns and restarts the VM as needed for property changes
        [Parameter()]
        [System.Boolean]
        $RestartIfNeeded,

        # Should the VM be created or deleted
        [Parameter()]
        [ValidateSet('Present', 'Absent')]
        [System.String]
        $Ensure = 'Present',

        [Parameter()]
        [System.String]
        $Notes,

        # Enable secure boot for Generation 2 VMs
        [Parameter()]
        [System.Boolean]
        $SecureBoot = $true,

        [Parameter()]
        [System.Boolean]
        $EnableGuestService = $false,

        # Enable AutomaticCheckpoints
        [Parameter()]
        [System.Boolean]
        $AutomaticCheckpointsEnabled
    )

    # Check if Hyper-V module is present for Hyper-V cmdlets
    if (!(Get-Module -ListAvailable -Name Hyper-V))
    {
        throw ($script:localizedData.RoleMissingError -f 'Hyper-V')
    }

    # Check if 1 or 0 VM with name = $name exist
    if ((Get-VM -Name $Name -ErrorAction SilentlyContinue).count -gt 1)
    {
        throw ($script:localizedData.MoreThanOneVMExistsError -f $Name)
    }

    # Check if AutomaticCheckpointsEnabled is set in configuration
    if ($PSBoundParameters.ContainsKey('AutomaticCheckpointsEnabled'))
    {
        <#
            Check if AutomaticCheckpoints are supported
            If AutomaticCheckpoints are supported, parameter exists on Set-VM
        #>

        if (-Not (Get-Command -Name Set-VM -Module Hyper-V).Parameters.ContainsKey('AutomaticCheckpointsEnabled'))
        {
            throw ($script:localizedData.AutomaticCheckpointsUnsupported)
        }
    }

    try
    {
        $vmObj = Get-VM -Name $Name -ErrorAction Stop
        if ($Ensure -eq 'Present')
        {
            # Check if $VhdPath exist
            if (!(Test-Path $VhdPath))
            {
                throw ($script:localizedData.VhdPathDoesNotExistError -f $VhdPath)
            }

            # Check if Minimum memory is less than StartUpmemory
            if ($PSBoundParameters.ContainsKey('StartupMemory') -and
                $PSBoundParameters.ContainsKey('MinimumMemory') -and
                ($MinimumMemory -gt $StartupMemory))
            {
                throw ($script:localizedData.MinMemGreaterThanStartupMemError -f $MinimumMemory, $StartupMemory)
            }

            # Check if Minimum memory is greater than Maximummemory
            if ($PSBoundParameters.ContainsKey('MaximumMemory') -and
                $PSBoundParameters.ContainsKey('MinimumMemory') -and
                ($MinimumMemory -gt $MaximumMemory))
            {
                throw ($script:localizedData.MinMemGreaterThanMaxMemError -f $MinimumMemory, $MaximumMemory)
            }

            # Check if Startup memory is greater than Maximummemory
            if ($PSBoundParameters.ContainsKey('MaximumMemory') -and
                $PSBoundParameters.ContainsKey('StartupMemory') -and
                ($StartupMemory -gt $MaximumMemory))
            {
                throw ($script:localizedData.StartUpMemGreaterThanMaxMemError -f $StartupMemory, $MaximumMemory)
            }

            <#
                VM Generation has no direct relation to the virtual hard disk format and cannot be changed
                after the virtual machine has been created. Generation 2 VMs do not support .VHD files.
            #>

            if (($Generation -eq 2) -and ($VhdPath.Split('.')[-1] -eq 'vhd'))
            {
                throw ($script:localizedData.VhdUnsupportedOnGen2VMError)
            }

            # Check if $Path exist
            if ($Path -and !(Test-Path -Path $Path))
            {
                throw ($script:localizedData.PathDoesNotExistError -f $Path)
            }

            $vhdChain = @(Get-VhdHierarchy -VhdPath ($vmObj.HardDrives[0].Path))
            if ($vhdChain -notcontains $VhdPath)
            {
                Write-Verbose -Message ($script:localizedData.VMPropertyShouldBe -f 'VhdPath', $VhdPath, ($vhdChain -join ','))
                return $false
            }

            if ($state -and ($vmObj.State -ne $State))
            {
                return $false
            }

            if ($PSBoundParameters.ContainsKey('StartupMemory') -and
                ($vmObj.MemoryStartup -ne $StartupMemory))
            {
                return $false
            }

            if ($PSBoundParameters.ContainsKey('MaximumMemory') -and
                ($vmObj.MemoryMaximum -ne $MaximumMemory))
            {
                return $false
            }

            if ($PSBoundParameters.ContainsKey('MinimumMemory') -and
                ($vmObj.MemoryMinimum -ne $MinimumMemory))
            {
                return $false
            }

            # If startup memory but neither minimum nor maximum memory specified, dynamic memory should be disabled
            if ($PSBoundParameters.ContainsKey('StartupMemory') -and
                ( -not $PSBoundParameters.ContainsKey('MinimumMemory')) -and
                ( -not $PSBoundParameters.ContainsKey('MaximumMemory')) -and
                $vmobj.DynamicMemoryEnabled)
            {
                return $false
            }

            # If startup, minimum and maximum memory are specified with the same values, dynamic memory should be disabled
            if ($PSBoundParameters.ContainsKey('StartupMemory') -and
                $PSBoundParameters.ContainsKey('MinimumMemory') -and
                $PSBoundParameters.ContainsKey('MaximumMemory') -and
                ($StartupMemory -eq $MinimumMemory) -and
                ($StartupMemory -eq $MaximumMemory) -and
                $vmobj.DynamicMemoryEnabled)
            {
                return $false
            }

            if ($vmObj.HardDrives.Path -notcontains $VhdPath)
            {
                return $false
            }

            for ($i = 0; $i -lt $SwitchName.Count; $i++)
            {
                if ($vmObj.NetworkAdapters[$i].SwitchName -ne $SwitchName[$i])
                {
                    Write-Verbose -Message ($script:localizedData.VMPropertyShouldBe -f 'SwitchName', $SwitchName[$i], $vmObj.NetworkAdapters[$i].SwitchName)
                    return $false
                }
            }

            for ($i = 0; $i -lt $MACAddress.Count; $i++)
            {
                if ($vmObj.NetworkAdapters[$i].MACAddress -ne $MACAddress[$i])
                {
                    Write-Verbose -Message ($script:localizedData.VMPropertyShouldBe -f 'MACAddress', $MACAddress[$i], $vmObj.NetworkAdapters[$i].MACAddress)
                    return $false
                }
            }

            # $Generation always exists, only check if parameter has been explicitly specified
            if ($PSBoundParameters.ContainsKey('Generation') -and ($Generation -ne $vmObj.Generation))
            {
                return $false
            }

            if ($PSBoundParameters.ContainsKey('ProcessorCount') -and ($vmObj.ProcessorCount -ne $ProcessorCount))
            {
                return $false
            }

            if ($vmObj.Generation -eq 2)
            {
                $vmSecureBoot = Test-VMSecureBoot -Name $Name
                if ($SecureBoot -ne $vmSecureBoot)
                {
                    Write-Verbose -Message ($script:localizedData.VMPropertyShouldBe -f 'SecureBoot', $SecureBoot, $vmSecureBoot)
                    return $false
                }
            }

            $guestServiceId = 'Microsoft:{0}\6C09BB55-D683-4DA0-8931-C9BF705F6480' -f $vmObj.Id
            $guestService = $vmObj | Get-VMIntegrationService | Where-Object -FilterScript { $_.Id -eq $guestServiceId }
            if ($guestService.Enabled -ne $EnableGuestService)
            {
                Write-Verbose -Message ($script:localizedData.VMPropertyShouldBe -f 'EnableGuestService', $EnableGuestService, $guestService.Enabled)
                return $false
            }

            # If AutomaticCheckpointsEnabled is set in configuration
            if ($PSBoundParameters.ContainsKey('AutomaticCheckpointsEnabled'))
            {
                if ($vmObj.AutomaticCheckpointsEnabled -ne $AutomaticCheckpointsEnabled)
                {
                    Write-Verbose -Message ($script:localizedData.VMPropertyShouldBe -f 'AutomaticCheckpointsEnabled', $AutomaticCheckpointsEnabled, $vmObj.AutomaticCheckpointsEnabled)
                    return $false
                }
            }

            return $true
        }
        else
        {
            return $false
        }
    }
    catch [System.Management.Automation.ActionPreferenceStopException]
    {
        ($Ensure -eq 'Absent')
    }
}

#region Helper function

# Returns VM VHDs, including snapshots and differencing disks
function Get-VhdHierarchy
{
    param
    (
        [Parameter(Mandatory = $true)]
        [System.String]
        $VhdPath
    )

    $vmVhdPath = Get-VHD -Path $VhdPath
    Write-Output -InputObject $vmVhdPath.Path
    while (-not [System.String]::IsNullOrEmpty($vmVhdPath.ParentPath))
    {
        $vmVhdPath.ParentPath
        $vmVhdPath = (Get-VHD -Path $vmVhdPath.ParentPath)
    }
}

<#
    The 'Set-VMProperty' method cannot be used as it cannot deal with piped
    command in it's current implementation
#>

function Set-VMMACAddress
{
    param
    (
        [Parameter(Mandatory = $true)]
        [System.String]
        $Name,

        [Parameter(Mandatory = $true)]
        [System.String]
        $MACAddress,

        [Parameter(Mandatory = $true)]
        [Int]
        $NICIndex,

        [Parameter()]
        [System.Boolean]
        $WaitForIP,

        [Parameter()]
        [System.Boolean]
        $RestartIfNeeded
    )
    $vmObj = Get-VM -Name $Name
    $originalState = $vmObj.state
    if ($originalState -ne 'Off' -and $RestartIfNeeded)
    {
        Set-VMState -Name $Name -State Off
        $vmObj.NetworkAdapters[$NICIndex] | Set-VMNetworkAdapter -StaticMacAddress $MACAddress

        # Can not move a off VM to paused, but only to running state
        if ($originalState -eq 'Running')
        {
            Set-VMState -Name $Name -State Running -WaitForIP $WaitForIP
        }

        # Cannot make a paused VM to go back to Paused state after turning Off
        if ($originalState -eq 'Paused')
        {
            Write-Warning -Message ($script:localizedData.VMStateWillBeOffWarning -f $Name)
        }
    }
    elseif ($originalState -eq 'Off')
    {
        $vmObj.NetworkAdapters[$NICIndex] | Set-VMNetworkAdapter -StaticMacAddress $MACAddress
        Write-Verbose -Message ($script:localizedData.VMPropertySet -f 'MACAddress', $MACAddress)
    }
    else
    {
        Write-Error -Message ($script:localizedData.CannotUpdatePropertiesOnlineError -f $Name, $vmObj.State)
    }
}

function Test-VMSecureBoot
{
    param
    (
        [Parameter(Mandatory = $true)]
        [System.String]
        $Name
    )
    $vm = Get-VM -Name $Name
    return (Get-VMFirmware -VM $vm).SecureBoot -eq 'On'
}

#endregion

Export-ModuleMember -Function *-TargetResource