DSCResources/MSFT_xCluster/MSFT_xCluster.psm1

<#
    .SYNOPSIS
        Returns the current state of the failover cluster.
 
    .PARAMETER Name
        Name of the failover cluster.
 
    .PARAMETER StaticIPAddress
        Static IP Address of the failover cluster.
 
    .PARAMETER DomainAdministratorCredential
        Credential used to create the failover cluster in Active Directory.
#>

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

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

        [Parameter(Mandatory = $true)]
        [System.Management.Automation.PSCredential]
        $DomainAdministratorCredential
    )

    $computerInformation = Get-WmiObject -Class Win32_ComputerSystem
    if (($null -eq $computerInformation) -or ($null -eq $computerInformation.Domain))
    {
        throw 'Can''t find machine''s domain name'
    }

    try
    {
        ($oldToken, $context, $newToken) = Set-ImpersonateAs -Credential $DomainAdministratorCredential

        $cluster = Get-Cluster -Name $Name -Domain $computerInformation.Domain
        if ($null -eq $cluster)
        {
            throw "Can't find the cluster $Name"
        }

        $address = Get-ClusterGroup -Cluster $Name -Name 'Cluster IP Address' | Get-ClusterParameter -Name 'Address'
    }
    finally
    {
        if ($context)
        {
            $context.Undo()
            $context.Dispose()
            Close-UserToken -Token $newToken
        }
    }

    @{
        Name                          = $Name
        StaticIPAddress               = $address.Value
        DomainAdministratorCredential = $DomainAdministratorCredential
    }
}

<#
    .SYNOPSIS
        Creates the failover cluster and adds a node to the failover cluster.
 
    .PARAMETER Name
        Name of the failover cluster.
 
    .PARAMETER StaticIPAddress
        Static IP Address of the failover cluster.
 
    .PARAMETER DomainAdministratorCredential
        Credential used to create the failover cluster in Active Directory.
 
    .NOTES
        If the cluster does not exist, it will be created in the domain and the
        static IP address will be assigned to the cluster.
        When the cluster exist (either it was created or already existed), it
        will add the target node ($env:COMPUTERNAME) to the cluster.
        If the target node already is a member of the failover cluster but has
        status down, it will be removed and then added again to the failover
        cluster.
#>

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

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

        [Parameter(Mandatory = $true)]
        [System.Management.Automation.PSCredential]
        $DomainAdministratorCredential
    )

    $bCreate = $true

    Write-Verbose -Message "Checking if Cluster $Name is present ..."

    $computerInformation = Get-WmiObject -Class Win32_ComputerSystem
    if (($null -eq $computerInformation) -or ($null -eq $computerInformation.Domain))
    {
        throw 'Can''t find machine''s domain name'
    }

    try
    {
        $cluster = Get-Cluster -Name $Name -Domain $computerInformation.Domain

        if ($cluster)
        {
            $bCreate = $false
        }
    }
    catch
    {
        $bCreate = $true
    }

    try
    {
        ($oldToken, $context, $newToken) = Set-ImpersonateAs -Credential $DomainAdministratorCredential

        if ($bCreate)
        {
            Write-Verbose -Message "Cluster $Name is NOT present"

            New-Cluster -Name $Name -Node $env:COMPUTERNAME -StaticAddress $StaticIPAddress -NoStorage -Force -ErrorAction Stop

            if ( -not (Get-Cluster))
            {
                throw 'Cluster creation failed. Please verify output of ''Get-Cluster'' command'
            }

            Write-Verbose -Message "Created Cluster $Name"
        }
        else
        {
            Write-Verbose -Message "Add node to Cluster $Name ..."

            Write-Verbose -Message "Add-ClusterNode $env:COMPUTERNAME to cluster $Name"

            $list = Get-ClusterNode -Cluster $Name
            foreach ($node in $list)
            {
                if ($node.Name -eq $env:COMPUTERNAME)
                {
                    if ($node.State -eq 'Down')
                    {
                        Write-Verbose -Message "Node $env:COMPUTERNAME was down, need remove it from the list."

                        Remove-ClusterNode -Name $env:COMPUTERNAME -Cluster $Name -Force
                    }
                }
            }

            Add-ClusterNode -Name $env:COMPUTERNAME -Cluster $Name -NoStorage

            Write-Verbose -Message "Added node to Cluster $Name"
        }
    }
    finally
    {
        if ($context)
        {
            $context.Undo()
            $context.Dispose()
            Close-UserToken -Token $newToken
        }
    }
}

<#
    .SYNOPSIS
        Test the failover cluster exist and that the node is a member of the
        failover cluster.
 
    .PARAMETER Name
        Name of the failover cluster.
 
    .PARAMETER StaticIPAddress
        Static IP Address of the failover cluster.
 
    .PARAMETER DomainAdministratorCredential
        Credential used to create the failover cluster in Active Directory.
 
    .NOTES
        The code will check the following in order:
 
          1. Is target node a member of the Active Directory domain?
          2. Does the failover cluster exist in the Active Directory domain?
          3. Is the target node a member of the failover cluster?
          4. Does the cluster node have the status UP?
 
        If the first return false an error will be thrown. If either of the
        other return $false, then the cluster will be created, if it does not
        exist and then the node will be added to the failover cluster.
#>

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

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

        [Parameter(Mandatory = $true)]
        [System.Management.Automation.PSCredential]
        $DomainAdministratorCredential
    )

    $returnValue = $false

    Write-Verbose -Message "Checking if Cluster $Name is present ..."

    $ComputerInfo = Get-WmiObject -Class Win32_ComputerSystem
    if (($null -eq $ComputerInfo) -or ($null -eq $ComputerInfo.Domain))
    {
        throw "Can't find machine's domain name"
    }

    try
    {
        ($oldToken, $context, $newToken) = Set-ImpersonateAs -Credential $DomainAdministratorCredential

        $cluster = Get-Cluster -Name $Name -Domain $ComputerInfo.Domain

        Write-Verbose -Message "Cluster $Name is present"

        if ($cluster)
        {
            Write-Verbose -Message "Checking if the node is in cluster $Name ..."

            $allNodes = Get-ClusterNode -Cluster $Name

            foreach ($node in $allNodes)
            {
                if ($node.Name -eq $env:COMPUTERNAME)
                {
                    if ($node.State -eq 'Up')
                    {
                        $returnValue = $true
                    }
                    else
                    {
                        Write-Verbose -Message "Node is in cluster $Name but is NOT up, treat as NOT in cluster."
                    }

                    break
                }
            }

            if ($returnValue)
            {
                Write-Verbose -Message "Node is in cluster $Name"
            }
            else
            {
                Write-Verbose -Message "Node is NOT in cluster $Name"
            }
        }
    }
    catch
    {
        Write-Verbose -Message "Cluster $Name is NOT present with Error $_.Message"
    }
    finally
    {
        if ($context)
        {
            $context.Undo()
            $context.Dispose()

            Close-UserToken -Token $newToken
        }
    }

    $returnValue
}

<#
    .SYNOPSIS
        Loads and returns a reference to the impersonation library.
 
#>

function Get-ImpersonateLib
{
    if ($script:ImpersonateLib)
    {
        return $script:ImpersonateLib
    }

    $sig = @'
[DllImport("advapi32.dll", SetLastError = true)]
public static extern bool LogonUser(string lpszUsername, string lpszDomain, string lpszPassword, int dwLogonType, int dwLogonProvider, ref IntPtr phToken);
 
[DllImport("kernel32.dll")]
public static extern Boolean CloseHandle(IntPtr hObject);
'@

    $script:ImpersonateLib = Add-Type -PassThru -Namespace 'Lib.Impersonation' -Name ImpersonationLib -MemberDefinition $sig

    return $script:ImpersonateLib
}

<#
    .SYNOPSIS
        Starts to impersonate the credentials provided in parameter Credential on the current user context.
 
    .PARAMETER Credential
        The credentials that should be impersonated.
 
    .OUTPUTS
        Returns three values.
 
        First value: The current user token before impersonation.
        Second value: The impersonation context returned when impersonation is started.
        Third value: The impersonated user token.
 
    .NOTES
        LogonUser function
        https://msdn.microsoft.com/en-us/library/windows/desktop/aa378184(v=vs.85).aspx
 
        WindowsIdentity.Impersonate Method ()
        https://msdn.microsoft.com/en-us/library/w070t6ka(v=vs.110).aspx
#>

function Set-ImpersonateAs
{
    param
    (
        [Parameter(Mandatory = $true)]
        [System.Management.Automation.PSCredential]
        $Credential
    )

    [IntPtr] $userToken = [Security.Principal.WindowsIdentity]::GetCurrent().Token
    $userToken
    $ImpersonateLib = Get-ImpersonateLib

    $bLogin = $ImpersonateLib::LogonUser($Credential.GetNetworkCredential().UserName, $Credential.GetNetworkCredential().Domain, $Credential.GetNetworkCredential().Password,
        9, 0, [ref]$userToken)

    if ($bLogin)
    {
        $Identity = New-Object Security.Principal.WindowsIdentity $userToken
        $context = $Identity.Impersonate()
    }
    else
    {
        throw "Can't Logon as User $($Credential.GetNetworkCredential().UserName)."
    }

    $context, $userToken
}

<#
    .SYNOPSIS
        Closes a (impersonation) user token.
 
    .PARAMETER Token
        The user token to close.
 
    .NOTES
        CloseHandle function
        https://msdn.microsoft.com/en-us/library/windows/desktop/ms724211(v=vs.85).aspx
#>

function Close-UserToken
{
    param
    (
        [Parameter(Mandatory = $true)]
        [System.IntPtr]
        $Token
    )

    $ImpersonateLib = Get-ImpersonateLib

    $bLogin = $ImpersonateLib::CloseHandle($Token)
    if (-not $bLogin)
    {
        throw 'Can''t close token'
    }
}