DSCResources/MSFT_SPInstall/MSFT_SPInstall.psm1

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

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

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

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

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

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

    Write-Verbose -Message "Getting install status of SharePoint"

    Write-Verbose -Message "Check if Binary folder exists"
    if (-not(Test-Path -Path $BinaryDir))
    {
        $message = "Specified path cannot be found: {$BinaryDir}"
        Add-SPDscEvent -Message $message `
            -EntryType 'Error' `
            -EventID 100 `
            -Source $MyInvocation.MyCommand.Source
        throw $message
    }

    $InstallerPath = Join-Path -Path $BinaryDir -ChildPath "setup.exe"
    if (-not(Test-Path -Path $InstallerPath))
    {
        $message = "Setup.exe cannot be found in {$BinaryDir}"
        Add-SPDscEvent -Message $message `
            -EntryType 'Error' `
            -EventID 100 `
            -Source $MyInvocation.MyCommand.Source
        throw $message
    }

    Write-Verbose -Message "Checking file status of $InstallerPath"
    $checkBlockedFile = $true
    if (Split-Path -Path $InstallerPath -IsAbsolute)
    {
        $driveLetter = (Split-Path -Path $InstallerPath -Qualifier).TrimEnd(":")
        Write-Verbose -Message "BinaryDir refers to drive $driveLetter"

        $volume = Get-Volume -DriveLetter $driveLetter -ErrorAction SilentlyContinue
        if ($null -ne $volume)
        {
            if ($volume.DriveType -ne "CD-ROM")
            {
                Write-Verbose -Message "Volume is a fixed drive: Perform Blocked File test"
            }
            else
            {
                Write-Verbose -Message "Volume is a CD-ROM drive: Skipping Blocked File test"
                $checkBlockedFile = $false
            }
        }
        else
        {
            Write-Verbose -Message "Volume not found. Unable to determine the type. Continuing."
        }
    }

    if ($checkBlockedFile -eq $true)
    {
        Write-Verbose -Message "Checking status now"
        try
        {
            $zone = Get-Item -Path $InstallerPath -Stream "Zone.Identifier" -EA SilentlyContinue
        }
        catch
        {
            Write-Verbose -Message 'Encountered error while reading file stream. Ignoring file stream.'
        }
        if ($null -ne $zone)
        {
            $message = ("Setup file is blocked! Please use 'Unblock-File -Path $InstallerPath' " + `
                    "to unblock the file before continuing.")
            Add-SPDscEvent -Message $message `
                -EntryType 'Error' `
                -EventID 100 `
                -Source $MyInvocation.MyCommand.Source
            throw $message
        }
        Write-Verbose -Message "File not blocked, continuing."
    }

    $x86Path = "HKLM:\Software\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*"
    $installedItemsX86 = Get-ItemProperty -Path $x86Path | Select-Object -Property DisplayName

    $x64Path = "HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall\*"
    $installedItemsX64 = Get-ItemProperty -Path $x64Path | Select-Object -Property DisplayName

    $installedItems = $installedItemsX86 + $installedItemsX64
    $installedItems = $installedItems.DisplayName | Select-Object -Unique
    $spInstall = $installedItems | Where-Object -FilterScript {
        $_ -match "^Microsoft SharePoint Server (2013|2016|2019|Subscription Edition)$"
    }

    if ($spInstall)
    {
        return @{
            IsSingleInstance = "Yes"
            BinaryDir        = $BinaryDir
            ProductKey       = $ProductKey
            InstallPath      = $InstallPath
            DataPath         = $DataPath
            Ensure           = "Present"
        }
    }
    else
    {
        return @{
            IsSingleInstance = "Yes"
            BinaryDir        = $BinaryDir
            ProductKey       = $ProductKey
            InstallPath      = $InstallPath
            DataPath         = $DataPath
            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(Mandatory = $true)]
        [System.String]
        $BinaryDir,

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

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

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

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

    Write-Verbose -Message "Setting install status of SharePoint"

    if ($Ensure -eq "Absent")
    {
        $message = ("SharePointDsc does not support uninstalling SharePoint or " + `
                "its prerequisites. Please remove this manually.")
        Add-SPDscEvent -Message $message `
            -EntryType 'Error' `
            -EventID 100 `
            -Source $MyInvocation.MyCommand.Source
        throw $message
    }

    Write-Verbose -Message "Check if Binary folder exists"
    if (-not(Test-Path -Path $BinaryDir))
    {
        $message = "Specified path cannot be found: {$BinaryDir}"
        Add-SPDscEvent -Message $message `
            -EntryType 'Error' `
            -EventID 100 `
            -Source $MyInvocation.MyCommand.Source
        throw $message
    }

    $InstallerPath = Join-Path -Path $BinaryDir -ChildPath "setup.exe"
    if (-not(Test-Path -Path $InstallerPath))
    {
        $message = "Setup.exe cannot be found in {$BinaryDir}"
        Add-SPDscEvent -Message $message `
            -EntryType 'Error' `
            -EventID 100 `
            -Source $MyInvocation.MyCommand.Source
        throw $message
    }

    $majorVersion = Get-SPDscAssemblyVersion -PathToAssembly $InstallerPath
    $buildVersion = Get-SPDscBuildVersion -PathToAssembly $InstallerPath
    if ($majorVersion -eq 15)
    {
        $svrsetupDll = Join-Path -Path $BinaryDir -ChildPath "updates\svrsetup.dll"
        $checkDotNet = $true
        if (Test-Path -Path $svrsetupDll)
        {
            $svrsetupDllFileInfo = Get-ItemProperty -Path $svrsetupDll
            $fileVersion = $svrsetupDllFileInfo.VersionInfo.FileVersion
            if ($fileVersion -ge "15.0.4709.1000")
            {
                $checkDotNet = $false
            }
        }

        if ($checkDotNet -eq $true)
        {
            $ndpKey = "HKLM:\SOFTWARE\Microsoft\NET Framework Setup\NDP\v4"
            $dotNet46Installed = $false
            if (Test-Path -Path $ndpKey)
            {
                $dotNetv4Keys = Get-ChildItem -Path $ndpKey
                foreach ($dotnetInstance in $dotNetv4Keys)
                {
                    if ($dotnetInstance.GetValue("Release") -ge 390000)
                    {
                        $dotNet46Installed = $true
                        break
                    }
                }
            }

            if ($dotNet46Installed -eq $true)
            {
                $message = ("A known issue prevents installation of SharePoint 2013 on " + `
                        "servers that have .NET 4.6 already installed. See details " + `
                        "at https://support.microsoft.com/en-us/kb/3087184")
                Add-SPDscEvent -Message $message `
                    -EntryType 'Error' `
                    -EventID 100 `
                    -Source $MyInvocation.MyCommand.Source
                throw $message
            }
        }
    }

    Write-Verbose -Message "Checking file status of $InstallerPath"
    $checkBlockedFile = $true
    if (Split-Path -Path $InstallerPath -IsAbsolute)
    {
        $driveLetter = (Split-Path -Path $InstallerPath -Qualifier).TrimEnd(":")
        Write-Verbose -Message "BinaryDir refers to drive $driveLetter"

        $volume = Get-Volume -DriveLetter $driveLetter -ErrorAction SilentlyContinue
        if ($null -ne $volume)
        {
            if ($volume.DriveType -ne "CD-ROM")
            {
                Write-Verbose -Message "Volume is a fixed drive: Perform Blocked File test"
            }
            else
            {
                Write-Verbose -Message "Volume is a CD-ROM drive: Skipping Blocked File test"
                $checkBlockedFile = $false
            }
        }
        else
        {
            Write-Verbose -Message "Volume not found. Unable to determine the type. Continuing."
        }
    }

    if ($checkBlockedFile -eq $true)
    {
        Write-Verbose -Message "Checking status now"
        try
        {
            $zone = Get-Item -Path $InstallerPath -Stream "Zone.Identifier" -EA SilentlyContinue
        }
        catch
        {
            Write-Verbose -Message 'Encountered error while reading file stream. Ignoring file stream.'
        }
        if ($null -ne $zone)
        {
            $message = ("Setup file is blocked! Please use 'Unblock-File -Path $InstallerPath' " + `
                    "to unblock the file before continuing.")
            Add-SPDscEvent -Message $message `
                -EntryType 'Error' `
                -EventID 100 `
                -Source $MyInvocation.MyCommand.Source
            throw $message
        }
        Write-Verbose -Message "File not blocked, continuing."
    }

    Write-Verbose -Message "Checking if Path is an UNC path"
    $uncInstall = $false
    if ($BinaryDir.StartsWith("\\"))
    {
        Write-Verbose -Message ("Specified BinaryDir is an UNC path. Adding servername to Local " +
            "Intranet Zone")

        $uncInstall = $true

        if ($BinaryDir -match "\\\\(.*?)\\.*")
        {
            $serverName = $Matches[1]
        }
        else
        {
            $message = "Cannot extract servername from UNC path. Check if it is in the correct format."
            Add-SPDscEvent -Message $message `
                -EntryType 'Error' `
                -EventID 100 `
                -Source $MyInvocation.MyCommand.Source
            throw $message
        }

        Set-SPDscZoneMap -Server $serverName
    }

    Write-Verbose -Message "Writing install config file"

    $configPath = Join-Path -Path $env:temp -ChildPath "SPInstallConfig.xml"

    $configData = "<Configuration>
    <Package Id=`"sts`">
        <Setting Id=`"LAUNCHEDFROMSETUPSTS`" Value=`"Yes`"/>
    </Package>
 
    <Package Id=`"spswfe`">
        <Setting Id=`"SETUPCALLED`" Value=`"1`"/>
    </Package>
 
    <Logging Type=`"verbose`" Path=`"%temp%`" Template=`"SharePoint Server Setup(*).log`"/>
    <PIDKEY Value=`"$ProductKey`" />
    <Display Level=`"none`" CompletionNotice=`"no`" />
"


    if ($PSBoundParameters.ContainsKey("InstallPath") -eq $true)
    {
        $configData += " <INSTALLLOCATION Value=`"$InstallPath`" />
"

    }
    if ($PSBoundParameters.ContainsKey("DataPath") -eq $true)
    {
        $configData += " <DATADIR Value=`"$DataPath`"/>
"

    }
    $configData += " <Setting Id=`"SERVERROLE`" Value=`"APPLICATION`"/>
    <Setting Id=`"USINGUIINSTALLMODE`" Value=`"0`"/>
    <Setting Id=`"SETUP_REBOOT`" Value=`"Never`" />
    <Setting Id=`"SETUPTYPE`" Value=`"CLEAN_INSTALL`"/>
</Configuration>"


    $configData | Out-File -FilePath $configPath

    Write-Verbose -Message "Beginning installation of SharePoint"

    $setupExe = Join-Path -Path $BinaryDir -ChildPath "setup.exe"

    $args = "/config `"$configPath`""
    if ($majorVersion -eq 16 -and $buildVersion -ge 13000)
    {
        $args += ' /IAcceptTheLicenseTerms'
    }

    Write-Verbose -Message "Calling the SharePoint installer"
    Write-Verbose -Message "Args for SharePoint installer are: $args"
    $setup = Start-Process -FilePath $setupExe `
        -ArgumentList $args `
        -Wait `
        -PassThru

    if ($uncInstall -eq $true)
    {
        Write-Verbose -Message "Removing added path from the Local Intranet Zone"
        Remove-SPDscZoneMap -ServerName $serverName
    }

    # Exit codes: https://docs.microsoft.com/en-us/windows/desktop/msi/error-codes
    switch ($setup.ExitCode)
    {
        0
        {
            Write-Verbose -Message "SharePoint binary installation complete"
            $global:DSCMachineStatus = 1
        }
        3010
        {
            Write-Verbose -Message "SharePoint binary installation complete, but reboot is required"
            $global:DSCMachineStatus = 1
        }
        30066
        {
            $pr1 = ("HKLM:\Software\Microsoft\Windows\CurrentVersion\" + `
                    "Component Based Servicing\RebootPending")
            $pr2 = ("HKLM:\Software\Microsoft\Windows\CurrentVersion\" + `
                    "WindowsUpdate\Auto Update\RebootRequired")
            $pr3 = "HKLM:\SYSTEM\CurrentControlSet\Control\Session Manager"
            if (    ($null -ne (Get-Item -Path $pr1 -ErrorAction SilentlyContinue)) `
                    -or ($null -ne (Get-Item -Path $pr2 -ErrorAction SilentlyContinue)) `
                    -or ((Get-Item -Path $pr3 | Get-ItemProperty).PendingFileRenameOperations.count -gt 0) `
            )
            {

                Write-Verbose -Message ("SPInstall has detected the server has pending " + `
                        "a reboot. Flagging to the DSC engine that the " + `
                        "server should reboot before continuing.")
                $global:DSCMachineStatus = 1
            }
            else
            {
                $message = ("SharePoint installation has failed due to an issue with prerequisites " + `
                        "not being installed correctly. Please review the setup logs.")
                Add-SPDscEvent -Message $message `
                    -EntryType 'Error' `
                    -EventID 100 `
                    -Source $MyInvocation.MyCommand.Source
                throw $message
            }
        }
        30203
        {
            $message = "SharePoint install failed, license terms are not accepted."
            Add-SPDscEvent -Message $message `
                -EntryType 'Error' `
                -EventID 100 `
                -Source $MyInvocation.MyCommand.Source
            throw $message
        }
        Default
        {
            $message = "SharePoint install failed, exit code was $($setup.ExitCode)"
            Add-SPDscEvent -Message $message `
                -EntryType 'Error' `
                -EventID 100 `
                -Source $MyInvocation.MyCommand.Source
            throw $message
        }
    }
}


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

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

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

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

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

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

    Write-Verbose -Message "Testing install status of SharePoint"

    $PSBoundParameters.Ensure = $Ensure

    if ($Ensure -eq "Absent")
    {
        $message = ("SharePointDsc does not support uninstalling SharePoint or " + `
                "its prerequisites. Please remove this manually.")
        Add-SPDscEvent -Message $message `
            -EntryType 'Error' `
            -EventID 100 `
            -Source $MyInvocation.MyCommand.Source
        throw $message
    }

    $CurrentValues = Get-TargetResource @PSBoundParameters

    Write-Verbose -Message "Current Values: $(Convert-SPDscHashtableToString -Hashtable $CurrentValues)"
    Write-Verbose -Message "Target Values: $(Convert-SPDscHashtableToString -Hashtable $PSBoundParameters)"

    $result = Test-SPDscParameterState -CurrentValues $CurrentValues `
        -Source $($MyInvocation.MyCommand.Source) `
        -DesiredValues $PSBoundParameters `
        -ValuesToCheck @("Ensure")

    Write-Verbose -Message "Test-TargetResource returned $result"

    return $result
}

function Export-TargetResource
{
    param (
        [Parameter()]
        [System.String]
        $ProductKey = "XXXXX-XXXXX-XXXXX-XXXXX-XXXXX",

        [Parameter()]
        [System.String]
        $BinaryLocation = "\\<location>"
    )

    $VerbosePreference = "SilentlyContinue"
    Add-ConfigurationDataEntry -Node "NonNodeData" -Key "FullInstallation" -Value "`$False" -Description "Specifies whether or not the DSC configuration script will install the SharePoint Prerequisites and Binaries;"
    $Content = " if (`$ConfigurationData.NonNodeData.FullInstallation)`r`n"
    $Content += " {`r`n"
    $Content += " SPInstall BinaryInstallation" + "`r`n {`r`n"

    if ([System.String]::IsNullOrEmpty($BinaryLocation))
    {
        $BinaryLocation = "\\<location>"
    }
    Add-ConfigurationDataEntry -Node "NonNodeData" -Key "SPInstallationBinaryPath" -Value $BinaryLocation -Description "Location of the SharePoint Binaries (local path or network share);"
    $Content += " BinaryDir = `$ConfigurationData.NonNodeData.SPInstallationBinaryPath;`r`n"
    if ([System.String]::IsNullOrEmpty($ProductKey))
    {
        $ProductKey = "xxxxx-xxxxx-xxxxx-xxxxx"
    }
    Add-ConfigurationDataEntry -Node "NonNodeData" -Key "SPProductKey" -Value $ProductKey -Description "SharePoint Product Key"
    $Content += " ProductKey = `$ConfigurationData.NonNodeData.SPProductKey;`r`n"
    $Content += " Ensure = `"Present`";`r`n"
    $Content += " IsSingleInstance = `"Yes`";`r`n"
    $Content += " PSDscRunAsCredential = `$Creds" + ($Global:spFarmAccount.Username.Split('\'))[1].Replace("-", "_").Replace(".", "_") + ";`r`n"
    $Content += " }`r`n"
    $Content += " }`r`n"

    return $Content
}

Export-ModuleMember -Function *-TargetResource