DSCResources/MSFT_SPFarm/MSFT_SPFarm.psm1

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

        [Parameter()]
        [ValidateSet("Present","Absent")]
        [System.String]
        $Ensure = "Present",

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

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

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

        [Parameter()]
        [System.Management.Automation.PSCredential]
        $InstallAccount,

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

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

        [Parameter(Mandatory = $true)]
        [System.Boolean]
        $RunCentralAdmin,

        [Parameter()]
        [System.UInt32]
        $CentralAdministrationPort,

        [Parameter()]
        [System.String]
        [ValidateSet("NTLM","Kerberos")]
        $CentralAdministrationAuth,

        [Parameter()]
        [System.String]
        [ValidateSet("Application",
                     "ApplicationWithSearch",
                     "Custom",
                     "DistributedCache",
                     "Search",
                     "SingleServer",
                     "SingleServerFarm",
                     "WebFrontEnd",
                     "WebFrontEndWithDistributedCache")]
        $ServerRole
    )

    Write-Verbose -Message "Getting the settings of the current local SharePoint Farm (if any)"

    if ($PSBoundParameters.ContainsKey("CentralAdministrationPort"))
    {
        if ($CentralAdministrationPort -notin 1..65535)
        {
            throw ("An invalid value for CentralAdministrationPort is specified: " + `
                   "$CentralAdministrationPort")
        }
    }

    if ($Ensure -eq "Absent")
    {
        throw ("SharePointDsc does not support removing a server from a farm, please set the " + `
               "ensure property to 'present'")
    }

    $installedVersion = Get-SPDSCInstalledProductVersion
    switch ($installedVersion.FileMajorPart)
    {
        15 {
            Write-Verbose -Message "Detected installation of SharePoint 2013"
        }
        16 {
            if($installedVersion.ProductBuildPart.ToString().Length -eq 4)
            {
                Write-Verbose -Message "Detected installation of SharePoint 2016"
            }
            else
            {
                Write-Verbose -Message "Detected installation of SharePoint 2019"
            }
        }
        default {
            throw ("Detected an unsupported major version of SharePoint. SharePointDsc only " + `
                   "supports SharePoint 2013, 2016 or 2019.")
        }
    }

    if (($PSBoundParameters.ContainsKey("ServerRole") -eq $true) `
        -and $installedVersion.FileMajorPart -ne 16)
    {
        throw [Exception] "Server role is only supported in SharePoint 2016 and 2019."
    }

    if (($PSBoundParameters.ContainsKey("ServerRole") -eq $true) `
        -and $installedVersion.FileMajorPart -eq 16 `
        -and $installedVersion.FileBuildPart -lt 4456 `
        -and ($ServerRole -eq "ApplicationWithSearch" `
             -or $ServerRole -eq "WebFrontEndWithDistributedCache"))
    {
        throw [Exception] ("ServerRole values of 'ApplicationWithSearch' or " + `
                           "'WebFrontEndWithDistributedCache' require the SharePoint 2016 " + `
                           "Feature Pack 1 to be installed. See " + `
                           "https://support.microsoft.com/en-au/kb/3127940")
    }


    # Determine if a connection to a farm already exists
    $majorVersion = $installedVersion.FileMajorPart
    $regPath = "hklm:SOFTWARE\Microsoft\Shared Tools\Web Server Extensions\$majorVersion.0\Secure\ConfigDB"
    $dsnValue = Get-SPDSCRegistryKey -Key $regPath -Value "dsn" -ErrorAction SilentlyContinue

    if ($null -ne $dsnValue)
    {
        # This node has already been connected to a farm
        $result = Invoke-SPDSCCommand -Credential $InstallAccount `
                                  -Arguments $PSBoundParameters `
                                  -ScriptBlock {
            $params = $args[0]

            try
            {
                $spFarm = Get-SPFarm
            }
            catch
            {
                Write-Verbose -Message "Unable to detect local farm."
                return $null
            }

            if ($null -eq $spFarm)
            {
                return $null
            }

            $configDb = Get-SPDatabase | Where-Object -FilterScript {
                $_.Name -eq $spFarm.Name -and $_.Type -eq "Configuration Database"
            }
            $centralAdminSite = Get-SPWebApplication -IncludeCentralAdministration `
                                | Where-Object -FilterScript {
                $_.IsAdministrationWebApplication -eq $true
            }

            if ($params.FarmAccount.UserName -eq $spFarm.DefaultServiceAccount.Name)
            {
                $farmAccount = $params.FarmAccount
            }
            else
            {
                $farmAccount = $spFarm.DefaultServiceAccount.Name
            }

            $centralAdminSite = Get-SPWebApplication -IncludeCentralAdministration `
                                | Where-Object -FilterScript {
                                    $_.IsAdministrationWebApplication -eq $true
                                }

            $centralAdminProvisioned = $false
            $ca = Get-SPServiceInstance -Server $env:ComputerName `
                  | Where-Object -Filterscript {
                        $_.TypeName -eq "Central Administration" -and $_.Status -eq "Online"
                    }
            if ($null -ne $ca)
            {
                $centralAdminProvisioned = $true
            }

            if ($centralAdminSite.IisSettings[0].DisableKerberos -eq $false)
            {
                $centralAdminAuth = "Kerberos"
            }
            else
            {
                $centralAdminAuth = "NTLM"
            }

            $returnValue = @{
                IsSingleInstance = "Yes"
                FarmConfigDatabaseName = $spFarm.Name
                DatabaseServer = $configDb.NormalizedDataSource
                FarmAccount = $farmAccount # Need to return this as a credential to match the type expected
                InstallAccount = $null
                Passphrase = $null
                AdminContentDatabaseName = $centralAdminSite.ContentDatabases[0].Name
                RunCentralAdmin = $centralAdminProvisioned
                CentralAdministrationPort = (New-Object -TypeName System.Uri $centralAdminSite.Url).Port
                CentralAdministrationAuth = $centralAdminAuth
            }
            $installedVersion = Get-SPDSCInstalledProductVersion
            if($installedVersion.FileMajorPart -eq 16)
            {
                $server = Get-SPServer -Identity $env:COMPUTERNAME -ErrorAction SilentlyContinue
                if($null -ne $server -and $null -ne $server.Role)
                {
                    $returnValue.Add("ServerRole", $server.Role)
                }
                else
                {
                    $domain = (Get-CimInstance -ClassName Win32_ComputerSystem).Domain
                    $currentServer = "$($env:COMPUTERNAME).$domain"

                    $server = Get-SPServer -Identity $currentServer -ErrorAction SilentlyContinue
                    if($null -ne $server -and $null -ne $server.Role)
                    {
                        $returnValue.Add("ServerRole", $server.Role)
                    }
                }
            }
            return $returnValue
        }

        if ($null -eq $result)
        {
            # The node is currently connected to a farm but was unable to retrieve the values
            # of current farm settings, most likely due to connectivity issues with the SQL box
            Write-Verbose -Message ("This server appears to be connected to a farm already, " + `
                                    "but the configuration database is currently unable to be " + `
                                    "accessed. Values returned from the get method will be " + `
                                    "incomplete, however the 'Ensure' property should be " + `
                                    "considered correct")
            return @{
                IsSingleInstance = "Yes"
                FarmConfigDatabaseName = $null
                DatabaseServer = $null
                FarmAccount = $null
                InstallAccount = $null
                Passphrase = $null
                AdminContentDatabaseName = $null
                RunCentralAdmin = $null
                CentralAdministrationPort = $null
                CentralAdministrationAuth = $null
                Ensure = "Present"
            }
        }
        else
        {
            $result.Add("Ensure", "Present")
            return $result
        }
    }
    else
    {
        # This node has never been connected to a farm, return the null return object
        return @{
            IsSingleInstance = "Yes"
            FarmConfigDatabaseName = $null
            DatabaseServer = $null
            FarmAccount = $null
            InstallAccount = $null
            Passphrase = $null
            AdminContentDatabaseName = $null
            RunCentralAdmin = $null
            CentralAdministrationPort = $null
            CentralAdministrationAuth = $null
            Ensure = "Absent"
        }
    }
}

function Set-TargetResource
{
    # Supressing the global variable use to allow passing DSC the reboot message
    [CmdletBinding()]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidGlobalVars", "")]
    param
    (
        [Parameter(Mandatory = $true)]
        [ValidateSet('Yes')]
        [String]
        $IsSingleInstance,

        [Parameter()]
        [ValidateSet("Present","Absent")]
        [System.String]
        $Ensure = "Present",

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

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

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

        [Parameter()]
        [System.Management.Automation.PSCredential]
        $InstallAccount,

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

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

        [Parameter(Mandatory = $true)]
        [System.Boolean]
        $RunCentralAdmin,

        [Parameter()]
        [System.UInt32]
        $CentralAdministrationPort,

        [Parameter()]
        [System.String]
        [ValidateSet("NTLM","Kerberos")]
        $CentralAdministrationAuth,

        [Parameter()]
        [System.String]
        [ValidateSet("Application",
                     "ApplicationWithSearch",
                     "Custom",
                     "DistributedCache",
                     "Search",
                     "SingleServer",
                     "SingleServerFarm",
                     "WebFrontEnd",
                     "WebFrontEndWithDistributedCache")]
        $ServerRole
    )

    Write-Verbose -Message "Setting local SP Farm settings"

    if ($Ensure -eq "Absent")
    {
        throw ("SharePointDsc does not support removing a server from a farm, please set the " + `
               "ensure property to 'present'")
    }

    $CurrentValues = Get-TargetResource @PSBoundParameters

    # Set default values to ensure they are passed to Invoke-SPDSCCommand
    if (-not $PSBoundParameters.ContainsKey("CentralAdministrationPort"))
    {
        $PSBoundParameters.Add("CentralAdministrationPort", 9999)
    }

    if (-not $PSBoundParameters.ContainsKey("CentralAdministrationAuth"))
    {
        $PSBoundParameters.Add("CentralAdministrationAuth", "NTLM")
    }

    if ($CurrentValues.Ensure -eq "Present")
    {
        if ($CurrentValues.RunCentralAdmin -ne $RunCentralAdmin)
        {
            Invoke-SPDSCCommand -Credential $InstallAccount `
                                -Arguments $PSBoundParameters `
                                -ScriptBlock {
                $params = $args[0]

                # Provision central administration
                if ($params.RunCentralAdmin -eq $true)
                {
                    $serviceInstance = Get-SPServiceInstance -Server $env:COMPUTERNAME `
                                            | Where-Object -FilterScript {
                                                $_.TypeName -eq "Central Administration"
                                            }
                    if ($null -eq $serviceInstance)
                    {
                        $domain = (Get-CimInstance -ClassName Win32_ComputerSystem).Domain
                        $fqdn = "$($env:COMPUTERNAME).$domain"
                        $serviceInstance = Get-SPServiceInstance -Server $fqdn `
                                            | Where-Object -FilterScript {
                                                $_.TypeName -eq "Central Administration"
                                            }
                    }
                    if ($null -eq $serviceInstance)
                    {
                        throw [Exception] "Unable to locate Central Admin service instance on this server"
                    }
                    Start-SPServiceInstance -Identity $serviceInstance
                }
                else
                {
                    # Unprovision central administration
                    $serviceInstance = Get-SPServiceInstance -Server $env:COMPUTERNAME `
                                                | Where-Object -FilterScript {
                                                    $_.TypeName -eq "Central Administration"
                                                }
                    if ($null -eq $serviceInstance)
                    {
                        $domain = (Get-CimInstance -ClassName Win32_ComputerSystem).Domain
                        $fqdn = "$($env:COMPUTERNAME).$domain"
                        $serviceInstance = Get-SPServiceInstance -Server $fqdn `
                                            | Where-Object -FilterScript {
                                                $_.TypeName -eq "Central Administration"
                                            }
                    }
                    if ($null -eq $serviceInstance)
                    {
                        throw "Unable to locate Central Admin service instance on this server"
                    }
                    Stop-SPServiceInstance -Identity $serviceInstance
                }
            }
        }
        if ($CurrentValues.CentralAdministrationPort -ne $CentralAdministrationPort)
        {
            Invoke-SPDSCCommand -Credential $InstallAccount `
                                -Arguments $PSBoundParameters `
                                -ScriptBlock {
                $params = $args[0]

                Set-SPCentralAdministration -Port $params.CentralAdministrationPort
            }
        }
        return
    }
    else
    {
        $actionResult = Invoke-SPDSCCommand -Credential $InstallAccount `
                                            -Arguments @($PSBoundParameters, $PSScriptRoot) `
                                            -ScriptBlock {
            $params = $args[0]
            $scriptRoot = $args[1]

            $modulePath = "..\..\Modules\SharePointDsc.Farm\SPFarm.psm1"
            Import-Module -Name (Join-Path -Path $scriptRoot -ChildPath $modulePath -Resolve)

            $sqlInstanceStatus = Get-SPDSCSQLInstanceStatus -SQLServer $params.DatabaseServer `

            if ($sqlInstanceStatus.MaxDOPCorrect -ne $true)
            {
                throw "The MaxDOP setting is incorrect. Please correct before continuing."
            }

            $dbStatus = Get-SPDSCConfigDBStatus -SQLServer $params.DatabaseServer `
                                                -Database $params.FarmConfigDatabaseName

            while ($dbStatus.Locked -eq $true)
            {
                Write-Verbose -Message ("[$([DateTime]::Now.ToShortTimeString())] The configuration " + `
                                        "database is currently being provisioned by a remote " + `
                                        "server, this server will wait for this to complete")
                Start-Sleep -Seconds 30
                $dbStatus = Get-SPDSCConfigDBStatus -SQLServer $params.DatabaseServer `
                                                    -Database $params.FarmConfigDatabaseName
            }

            if ($dbStatus.ValidPermissions -eq $false)
            {
                throw "The current user does not have sufficient permissions to SQL Server"
                return
            }

            $executeArgs = @{
                DatabaseServer = $params.DatabaseServer
                DatabaseName = $params.FarmConfigDatabaseName
                Passphrase = $params.Passphrase.Password
                SkipRegisterAsDistributedCacheHost = $true
            }

            $installedVersion = Get-SPDSCInstalledProductVersion
            switch($installedVersion.FileMajorPart)
            {
                15 {
                    Write-Verbose -Message "Detected Version: SharePoint 2013"
                }
                16 {
                    if ($params.ContainsKey("ServerRole") -eq $true)
                    {
                        if($installedVersion.ProductBuildPart.ToString().Length -eq 4)
                        {
                            Write-Verbose -Message ("Detected Version: SharePoint 2016 - " + `
                                                    "configuring server as $($params.ServerRole)")
                        }
                        else
                        {
                            Write-Verbose -Message ("Detected Version: SharePoint 2019 - " + `
                                                    "configuring server as $($params.ServerRole)")
                        }
                        $executeArgs.Add("LocalServerRole", $params.ServerRole)
                    }
                    else
                    {
                        if($installedVersion.ProductBuildPart.ToString().Length -eq 4)
                        {
                            Write-Verbose -Message ("Detected Version: SharePoint 2016 - no server " + `
                                                    "role provided, configuring server without a " + `
                                                    "specific role")
                        }
                        else
                        {
                            Write-Verbose -Message ("Detected Version: SharePoint 2019 - no server " + `
                                                    "role provided, configuring server without a " + `
                                                    "specific role")
                        }
                        $executeArgs.Add("ServerRoleOptional", $true)
                    }
                }
                Default {
                    throw [Exception] ("An unknown version of SharePoint (Major version $_) " + `
                                       "was detected. Only versions 15 (SharePoint 2013) and" + `
                                       "16 (SharePoint 2016 or SharePoint 2019) are supported.")
                }
            }

            if ($dbStatus.DatabaseExists -eq $true)
            {
                Write-Verbose -Message ("The SharePoint config database " + `
                                        "'$($params.FarmConfigDatabaseName)' already exists, so " + `
                                        "this server will join the farm.")
                $createFarm = $false
            }
            elseif ($dbStatus.DatabaseExists -eq $false -and $params.RunCentralAdmin -eq $false)
            {
                # Only allow the farm to be created by a server that will run central admin
                # to avoid a ghost CA site appearing on this server and causing issues
                Write-Verbose -Message ("The SharePoint config database " + `
                                        "'$($params.FarmConfigDatabaseName)' does not exist, but " + `
                                        "this server will not be running the central admin " + `
                                        "website, so it will wait to join the farm rather than " + `
                                        "create one.")
                $createFarm = $false
            }
            else
            {
                Write-Verbose -Message ("The SharePoint config database " + `
                                        "'$($params.FarmConfigDatabaseName)' does not exist, so " + `
                                        "this server will create the farm.")
                $createFarm = $true
            }

            $farmAction = ""
            if ($createFarm -eq $false)
            {
                # The database exists, so attempt to join the farm to the server


                # Remove the server role optional attribute as it is only used when creating
                # a new farm
                if ($executeArgs.ContainsKey("ServerRoleOptional") -eq $true)
                {
                    $executeArgs.Remove("ServerRoleOptional")
                }

                Write-Verbose -Message ("The server will attempt to join the farm now once every " + `
                                        "60 seconds for the next 15 minutes.")
                $loopCount = 0
                $connectedToFarm = $false
                $lastException = $null
                while ($connectedToFarm -eq $false -and $loopCount -lt 15)
                {
                    try
                    {
                        Connect-SPConfigurationDatabase @executeArgs | Out-Null
                        $connectedToFarm = $true
                    }
                    catch
                    {
                        $lastException = $_.Exception
                        Write-Verbose -Message ("$([DateTime]::Now.ToShortTimeString()) - An error " + `
                                                "occured joining config database " + `
                                                "'$($params.FarmConfigDatabaseName)' on " + `
                                                "'$($params.DatabaseServer)'. This resource will " + `
                                                "wait and retry automatically for up to 15 minutes. " + `
                                                "(waited $loopCount of 15 minutes)")
                        $loopCount++
                        Start-Sleep -Seconds 60
                    }
                }

                if ($connectedToFarm -eq $false)
                {
                    Write-Verbose -Message ("Unable to join config database. Throwing exception.")
                    throw $lastException
                    return
                }
                $farmAction = "JoinedFarm"
            }
            else
            {
                Add-SPDscConfigDBLock -SQLServer $params.DatabaseServer `
                                    -Database $params.FarmConfigDatabaseName

                try
                {
                    $executeArgs += @{
                        FarmCredentials = $params.FarmAccount
                        AdministrationContentDatabaseName = $params.AdminContentDatabaseName
                    }

                    New-SPConfigurationDatabase @executeArgs

                    $farmAction = "CreatedFarm"
                }
                finally
                {
                    Remove-SPDscConfigDBLock -SQLServer $params.DatabaseServer `
                                            -Database $params.FarmConfigDatabaseName
                }
            }

            # Run common tasks for a new server
            Install-SPHelpCollection -All | Out-Null
            Initialize-SPResourceSecurity | Out-Null
            Install-SPService | Out-Null
            Install-SPFeature -AllExistingFeatures -Force | Out-Null

            # Provision central administration
            if ($params.RunCentralAdmin -eq $true)
            {
                $centralAdminSite = Get-SPWebApplication -IncludeCentralAdministration `
                                    | Where-Object -FilterScript {
                                        $_.IsAdministrationWebApplication -eq $true
                                    }


                $centralAdminProvisioned = $false
                if ((New-Object -TypeName System.Uri $centralAdminSite.Url).Port -eq $params.CentralAdministrationPort)
                {
                    $centralAdminProvisioned = $true
                }

                if ($centralAdminProvisioned -eq $false)
                {
                    New-SPCentralAdministration -Port $params.CentralAdministrationPort `
                                                -WindowsAuthProvider $params.CentralAdministrationAuth
                }
                else
                {
                    $serviceInstance = Get-SPServiceInstance -Server $env:COMPUTERNAME `
                                            | Where-Object -FilterScript {
                                                $_.TypeName -eq "Central Administration"
                                            }
                    if ($null -eq $serviceInstance)
                    {
                        $domain = (Get-CimInstance -ClassName Win32_ComputerSystem).Domain
                        $fqdn = "$($env:COMPUTERNAME).$domain"
                        $serviceInstance = Get-SPServiceInstance -Server $fqdn `
                                            | Where-Object -FilterScript {
                                                $_.TypeName -eq "Central Administration"
                                            }
                    }
                    if ($null -eq $serviceInstance)
                    {
                        throw [Exception] "Unable to locate Central Admin service instance on this server"
                    }
                    Start-SPServiceInstance -Identity $serviceInstance
                }
            }

            Install-SPApplicationContent | Out-Null

            return $farmAction
        }

        if ($actionResult -eq "JoinedFarm")
        {
            Write-Verbose -Message "Starting timer service"
            Start-Service -Name sptimerv4

            Write-Verbose -Message ("Pausing for 5 minutes to allow the timer service to " + `
                                    "fully provision the server")
            Start-Sleep -Seconds 300
            Write-Verbose -Message ("Join farm complete. Restarting computer to allow " + `
                                    "configuration to continue")

            $global:DSCMachineStatus = 1
        }
    }
}

function Test-TargetResource
{
    [CmdletBinding()]
    [OutputType([System.Boolean])]
    param
    (
        [Parameter(Mandatory = $true)]
        [ValidateSet('Yes')]
        [String]
        $IsSingleInstance,

        [Parameter()]
        [ValidateSet("Present","Absent")]
        [System.String]
        $Ensure = "Present",

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

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

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

        [Parameter()]
        [System.Management.Automation.PSCredential]
        $InstallAccount,

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

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

        [Parameter(Mandatory = $true)]
        [System.Boolean]
        $RunCentralAdmin,

        [Parameter()]
        [System.UInt32]
        $CentralAdministrationPort,

        [Parameter()]
        [System.String]
        [ValidateSet("NTLM","Kerberos")]
        $CentralAdministrationAuth,

        [Parameter()]
        [System.String]
        [ValidateSet("Application",
                     "ApplicationWithSearch",
                     "Custom",
                     "DistributedCache",
                     "Search",
                     "SingleServer",
                     "SingleServerFarm",
                     "WebFrontEnd",
                     "WebFrontEndWithDistributedCache")]
        $ServerRole
    )

    Write-Verbose -Message "Testing local SP Farm settings"

    $PSBoundParameters.Ensure = $Ensure

    $CurrentValues = Get-TargetResource @PSBoundParameters

    return Test-SPDscParameterState -CurrentValues $CurrentValues `
                                    -DesiredValues $PSBoundParameters `
                                    -ValuesToCheck @("Ensure",
                                                     "RunCentralAdmin",
                                                     "CentralAdministrationPort")
}

Export-ModuleMember -Function *-TargetResource