DSCResources/MSFT_SqlRSSetup/MSFT_SqlRSSetup.psm1

$script:resourceModulePath = Split-Path -Path (Split-Path -Path $PSScriptRoot -Parent) -Parent
$script:modulesFolderPath = Join-Path -Path $script:resourceModulePath -ChildPath 'Modules'

$script:localizationModulePath = Join-Path -Path $script:modulesFolderPath -ChildPath 'DscResource.LocalizationHelper'
Import-Module -Name (Join-Path -Path $script:localizationModulePath -ChildPath 'DscResource.LocalizationHelper.psm1')

$script:resourceHelperModulePath = Join-Path -Path $script:modulesFolderPath -ChildPath 'DscResource.Common'
Import-Module -Name (Join-Path -Path $script:resourceHelperModulePath -ChildPath 'DscResource.Common.psm1')

$script:localizedData = Get-LocalizedData -ResourceName 'MSFT_SqlRSSetup'

<#
    .SYNOPSIS
        Returns the current state of the Microsoft SQL Server Reporting Service
        instance.
 
    .PARAMETER InstanceName
        Name of the Microsoft SQL Server Reporting Service instance to installed.
        This can only be set to 'SSRS'. { 'SSRS' }
 
    .PARAMETER IAcceptLicenseTerms
        Accept licens terms. This must be set to 'Yes'. { 'Yes' }
 
    .PARAMETER SourcePath
        The path to the installation media file to be used for installation,
        e.g an UNC path to a shared resource. Environment variables can be used
        in the path.
 
    .NOTES
        The following properties are always returning $null because it's currently
        unknown how to return that information.
          - ProductKey
          - Edition
 
        The following properties always return $null on purpose. This could be
        changed in the future.
          - Action
          - SourceCredential
          - ForceRestart
          - EditionUpgrade
          - VersionUpgrade
          - LogPath
 
#>

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

        [Parameter(Mandatory = $true)]
        [ValidateSet('Yes')]
        [System.String]
        $IAcceptLicenseTerms,

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

    $returnObject = @{
        InstanceName = $null
        IAcceptLicenseTerms = $IAcceptLicenseTerms
        SourcePath = $SourcePath
        Action = $null
        SourceCredential = $null
        ProductKey = $null
        ForceRestart = $false
        EditionUpgrade = $false
        VersionUpgrade = $false
        Edition = $null
        LogPath = $null
        InstallFolder = $null
        ErrorDumpDirectory = $null
        CurrentVersion = $null
        ServiceName = $null
    }

    $InstanceName = $InstanceName.ToUpper()

    $getRegistryPropertyValueParameters = @{
        Path = 'HKLM:\SOFTWARE\Microsoft\Microsoft SQL Server\Instance Names\RS'
        Name = $InstanceName
    }

    $reportingServiceInstanceId = Get-RegistryPropertyValue @getRegistryPropertyValueParameters
    if ($reportingServiceInstanceId)
    {
        Write-Verbose -Message (
            $script:localizedData.FoundInstance -f $InstanceName
        )

        # InstanceName
        $returnObject['InstanceName'] = $InstanceName

        # InstallFolder
        $getRegistryPropertyValueParameters = @{
            Path = 'HKLM:\SOFTWARE\Microsoft\Microsoft SQL Server\SSRS\Setup'
            Name = 'InstallRootDirectory'
        }

        $returnObject['InstallFolder'] = Get-RegistryPropertyValue @getRegistryPropertyValueParameters

        # ServiceName
        $getRegistryPropertyValueParameters['Name'] = 'ServiceName'

        $returnObject['ServiceName'] = Get-RegistryPropertyValue @getRegistryPropertyValueParameters

        # ErrorDumpDirectory
        $getRegistryPropertyValueParameters = @{
            Path = 'HKLM:\SOFTWARE\Microsoft\Microsoft SQL Server\SSRS\CPE'
            Name = 'ErrorDumpDir'
        }

        $returnObject['ErrorDumpDirectory'] = Get-RegistryPropertyValue @getRegistryPropertyValueParameters

        # CurrentVersion
        $getPackageParameters = @{
            Name = 'Microsoft SQL Server Reporting Services'
            ProviderName = 'Programs'
            ErrorAction = 'SilentlyContinue'
            # Get-Package returns a lot of excessive information that we don't need.
            Verbose = $false
        }

        $reportingServicesPackage = Get-Package @getPackageParameters
        if ($reportingServicesPackage)
        {
            Write-Verbose -Message (
                $script:localizedData.VersionFound -f $reportingServicesPackage.Version
            )

            $returnObject['CurrentVersion'] = $reportingServicesPackage.Version
        }
        else
        {
            Write-Warning -Message $script:localizedData.PackageNotFound
        }
    }
    else
    {
        Write-Verbose -Message (
            $script:localizedData.InstanceNotFound -f $InstanceName
        )
    }

    return $returnObject
}

<#
    .SYNOPSIS
        Installs the the Microsoft SQL Server Reporting Service instance.
 
    .PARAMETER InstanceName
        Name of the Microsoft SQL Server Reporting Service instance to installed.
        This can only be set to 'SSRS'. { 'SSRS' }
 
    .PARAMETER IAcceptLicenseTerms
        Accept licens terms. This must be set to 'Yes'. { 'Yes' }
 
    .PARAMETER SourcePath
        The path to the installation media file to be used for installation,
        e.g an UNC path to a shared resource. Environment variables can be used
        in the path.
 
    .PARAMETER Action
        The action to be performed. Default value is 'Install' which performs
        either install or upgrade.
        { *Install* | Uninstall }
 
    .PARAMETER SourceCredential
        Credentials used to access the path set in the parameter 'SourcePath'.
 
    .PARAMETER SuppressRestart
        Suppresses any attempts to restart.
 
    .PARAMETER ProductKey
        Sets the custom license key, e.g. '12345-12345-12345-12345-12345'.
 
    .PARAMETER ForceRestart
        Forces a restart after installation is finished.
 
    .PARAMETER EditionUpgrade
        Upgrades the edition of the installed product. Requires that either the
        ProductKey or the Edition parameter is also assigned. Default is $false.
 
    .PARAMETER VersionUpgrade
        Upgrades installed product version, if the major product version of the
        source executable is higher than the major current version. Requires that
        either the ProductKey or the Edition parameter is also assigned. Default
        is $false.
 
        Not used in Set-TargetResource. The default is that the installation
        does upgrade. This variable is only used in Test-TargetResource to return
        false if the major version is different.
 
    .PARAMETER Edition
        Sets the custom free edition.
        { 'Development' | 'Evaluation' | 'ExpressAdvanced' }
 
    .PARAMETER LogPath
        Specifies the setup log file location, e.g. 'log.txt'. By default, log
        files are created under %TEMP%.
 
    .PARAMETER InstallFolder
        Sets the install folder, e.g. 'C:\Program Files\SSRS'. Default value is
        'C:\Program Files\Microsoft SQL Server Reporting Services'.
 
    .PARAMETER SetupProcessTimeout
        The timeout, in seconds, to wait for the setup process to finish.
        Default value is 7200 seconds (2 hours). If the setup process does not
        finish before this time, and error will be thrown.
#>

function Set-TargetResource
{
    <#
        Suppressing this rule because $global:DSCMachineStatus is used to trigger
        a Restart, either by force or when there are pending changes.
    #>

    [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidGlobalVars', '')]
    <#
        Suppressing this rule because $global:DSCMachineStatus is only set,
        never used (by design of Desired State Configuration).
    #>

    [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '', Scope='Function', Target='DSCMachineStatus')]
    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory = $true)]
        [ValidateSet('SSRS')]
        [System.String]
        $InstanceName,

        [Parameter(Mandatory = $true)]
        [ValidateSet('Yes')]
        [System.String]
        $IAcceptLicenseTerms,

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

        [Parameter()]
        [ValidateSet('Install','Uninstall')]
        [System.String]
        $Action = 'Install',

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

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

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

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

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

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

        [Parameter()]
        [ValidateSet('Development','Evaluation','ExpressAdvanced')]
        [System.String]
        $Edition,

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

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

        [Parameter()]
        [System.UInt32]
        $SetupProcessTimeout = 7200
    )

    # Must either choose ProductKey or Edition, not both.
    if ($Action -eq 'Install' -and $PSBoundParameters.ContainsKey('Edition') -and $PSBoundParameters.ContainsKey('ProductKey'))
    {
        $errorMessage = $script:localizedData.EditionInvalidParameter
        New-InvalidArgumentException -ArgumentName 'Edition, ProductKey' -Message $errorMessage
    }

    # Must either choose ProductKey or Edition, not none.
    if ($Action -eq 'Install' -and -not $PSBoundParameters.ContainsKey('Edition') -and -not $PSBoundParameters.ContainsKey('ProductKey'))
    {
        $errorMessage = $script:localizedData.EditionMissingParameter
        New-InvalidArgumentException -ArgumentName 'Edition, ProductKey' -Message $errorMessage
    }

    if (-not (Test-Path -Path $SourcePath) -or (Get-Item -Path $SourcePath).Extension -ne '.exe')
    {
        $errorMessage = $script:localizedData.SourcePathNotFound -f $SourcePath
        New-InvalidArgumentException -ArgumentName 'SourcePath' -Message $errorMessage
    }

    $InstanceName = $InstanceName.ToUpper()

    $SourcePath = [Environment]::ExpandEnvironmentVariables($SourcePath)

    $parametersToEvaluateTrailingSlash = @(
        'SourcePath',
        'InstallFolder'
    )

    # Making sure paths are correct.
    foreach ($parameterName in $parametersToEvaluateTrailingSlash)
    {
        if ($PSBoundParameters.ContainsKey($parameterName))
        {
            $parameterValue = Get-Variable -Name $parameterName -ValueOnly
            $formattedPath = Format-Path -Path $parameterValue -TrailingSlash
            Set-Variable -Name $parameterName -Value $formattedPath
        }
    }

    if ($SourceCredential)
    {
        $executableParentFolder = Split-Path -Path $SourcePath -Parent
        $executableFileName = Split-Path -Path $SourcePath -Leaf

        $invokeInstallationMediaCopyParameters = @{
            SourcePath = $executableParentFolder
            SourceCredential = $SourceCredential
            PassThru = $true
        }

        $newExecutableParentFolder = Invoke-InstallationMediaCopy @invokeInstallationMediaCopyParameters

        # Switch SourcePath to point to the new local location.
        $SourcePath = Join-Path -Path $newExecutableParentFolder -ChildPath $executableFileName
    }

    Write-Verbose -Message ($script:localizedData.UsingExecutable -f $SourcePath)

    $setupArguments = @{
        Quiet = [System.Management.Automation.SwitchParameter] $true
    }

    if ($Action -eq 'Install')
    {
        $setupArguments += @{
            IAcceptLicenseTerms = [System.Management.Automation.SwitchParameter] $true
        }
    }
    else
    {
        $setupArguments += @{
            'uninstall' =  [System.Management.Automation.SwitchParameter] $true
        }
    }

    <#
        This is a list of parameters that are allowed to be translated into
        arguments.
    #>

    $allowedParametersAsArguments = @(
        'ProductKey'
        'SuppressRestart'
        'EditionUpgrade'
        'Edition'
        'LogPath'
        'InstallFolder'
    )

    $argumentParameters = $PSBoundParameters.Keys | Where-Object -FilterScript {
        $_ -in $allowedParametersAsArguments
    }

    <#
        Handle translation between parameter name and argument name.
        Also making sure using the correct casing, e.g. 'log' and not 'Log'.
    #>

    switch ($argumentParameters)
    {
        'ProductKey'
        {
            $setupArguments += @{
                'PID' = $ProductKey
            }
        }

        'SuppressRestart'
        {
            $setupArguments += @{
                'norestart' = [System.Management.Automation.SwitchParameter] $true
            }
        }

        'EditionUpgrade'
        {
            $setupArguments += @{
                'EditionUpgrade' = [System.Management.Automation.SwitchParameter] $true
            }
        }

        'Edition'
        {
            $setupArguments += @{
                'Edition' = Convert-EditionName -Name $Edition
            }
        }

        'LogPath'
        {
            $setupArguments += @{
                'log' = $LogPath
            }
        }

        default
        {
            $setupArguments += @{
                $_ = Get-Variable -Name $_ -ValueOnly
            }
        }
    }

    # Build the argument string to be passed to setup
    $argumentString = ''
    foreach ($currentSetupArgument in $setupArguments.GetEnumerator())
    {
        # Arrays are handled specially
        if ($currentSetupArgument.Value -is [System.Management.Automation.SwitchParameter])
        {
            $argumentString += '/{0}' -f $currentSetupArgument.Key
        }
        else
        {
            $argumentString += '/{0}={1}' -f $currentSetupArgument.Key, $currentSetupArgument.Value
        }

        # Add a space between arguments.
        $argumentString += ' '
    }

    # Trim whitespace at start and end of string.
    $argumentString = $argumentString.Trim()

    # Save the arguments for the log output
    $logOutput = $argumentString

    # Replace sensitive values for verbose output
    if ($PSBoundParameters.ContainsKey('ProductKey'))
    {
        $logOutput = $logOutput -replace $ProductKey, '*****-*****-*****-*****-*****'
    }

    Write-Verbose -Message ($script:localizedData.SetupArguments -f $logOutput)

    <#
        This handles when PsDscRunAsCredential is set, or running
        as the SYSTEM account.
    #>


    $startProcessParameters = @{
        FilePath = $SourcePath
        ArgumentList = $argumentString
        Timeout = $SetupProcessTimeout
    }

    $processExitCode = Start-SqlSetupProcess @startProcessParameters

    Write-Verbose -Message ($script:localizedData.SetupExitMessage -f $processExitCode)

    if ($processExitCode -eq 0)
    {
        Write-Verbose -Message ($script:localizedData.SetupSuccessful -f $script:localizedData.$Action)
    }
    elseif ($processExitCode -eq 3010)
    {
        Write-Warning -Message ($script:localizedData.SetupSuccessfulRestartRequired -f $script:localizedData.$Action)

        $global:DSCMachineStatus = 1
    }
    else
    {
        if ($PSBoundParameters.ContainsKey('LogPath'))
        {
            $errorMessage = $script:localizedData.SetupFailedWithLog -f $LogPath
        }
        else
        {
            $errorMessage = $script:localizedData.SetupFailed
        }

        New-InvalidResultException -Message $errorMessage
    }

    <#
        If ForceRestart is set it will always restart, and override SuppressRestart.
        If SuppressRestart is set it will always override any pending restart.
    #>

    if ($ForceRestart)
    {
        $global:DSCMachineStatus = 1
    }
    elseif ($global:DSCMachineStatus -eq 1 -and $SuppressRestart)
    {
        # Suppressing restart to make sure the node is not restarted.
        $global:DSCMachineStatus = 0

        Write-Verbose -Message $script:localizedData.SuppressRestart
    }
    elseif (-not $SuppressRestart -and (Test-PendingRestart))
    {
        $global:DSCMachineStatus = 1
    }

    if ($global:DSCMachineStatus -eq 1)
    {
        Write-Verbose -Message $script:localizedData.Restart
    }
}

<#
    .SYNOPSIS
        Tests if the Microsoft SQL Server Reporting Service instance is installed.
 
    .PARAMETER InstanceName
        Name of the Microsoft SQL Server Reporting Service instance to installed.
        This can only be set to 'SSRS'. { 'SSRS' }
 
    .PARAMETER IAcceptLicenseTerms
        Accept licens terms. This must be set to 'Yes'. { 'Yes' }
 
    .PARAMETER SourcePath
        The path to the installation media file to be used for installation,
        e.g an UNC path to a shared resource. Environment variables can be used
        in the path.
 
    .PARAMETER Action
        The action to be performed. Default value is 'Install' which performs
        either install or upgrade.
        { *Install* | Uninstall }
 
    .PARAMETER SourceCredential
        Credentials used to access the path set in the parameter 'SourcePath'.
 
    .PARAMETER SuppressRestart
        Suppresses any attempts to restart.
 
    .PARAMETER ProductKey
        Sets the custom license key, e.g. '12345-12345-12345-12345-12345'.
 
    .PARAMETER ForceRestart
        Forces a restart after installation is finished.
 
    .PARAMETER EditionUpgrade
        Upgrades the edition of the installed product. Requires that either the
        ProductKey or the Edition parameter is also assigned. Default is $false.
 
    .PARAMETER VersionUpgrade
        Upgrades installed product version, if the major product version of the
        source executable is higher than the major current version. Requires that
        either the ProductKey or the Edition parameter is also assigned. Default
        is $false.
 
    .PARAMETER Edition
        Sets the custom free edition.
        { 'Development' | 'Evaluation' | 'ExpressAdvanced' }
 
    .PARAMETER LogPath
        Specifies the setup log file location, e.g. 'log.txt'. By default, log
        files are created under %TEMP%.
 
    .PARAMETER InstallFolder
        Sets the install folder, e.g. 'C:\Program Files\SSRS'. Default value is
        'C:\Program Files\Microsoft SQL Server Reporting Services'.
 
    .PARAMETER SetupProcessTimeout
        The timeout, in seconds, to wait for the setup process to finish.
        Default value is 7200 seconds (2 hours). If the setup process does not
        finish before this time, and error will be thrown.
#>

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

        [Parameter(Mandatory = $true)]
        [ValidateSet('Yes')]
        [System.String]
        $IAcceptLicenseTerms,

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

        [Parameter()]
        [ValidateSet('Install','Uninstall')]
        [System.String]
        $Action = 'Install',

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

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

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

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

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

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

        [Parameter()]
        [ValidateSet('Development','Evaluation','ExpressAdvanced')]
        [System.String]
        $Edition,

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

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

        [Parameter()]
        [System.UInt32]
        $SetupProcessTimeout = 7200
    )

    Write-Verbose -Message (
        $script:localizedData.TestingConfiguration
    )

    $getTargetResourceParameters = @{
        InstanceName = $InstanceName
        IAcceptLicenseTerms = $IAcceptLicenseTerms
        SourcePath = $SourcePath
    }

    $getTargetResourceResult = Get-TargetResource @getTargetResourceParameters

    $returnValue = $false

    <#
        We determine if the Microsoft SQL Server Reporting Service instance is
        installed if the instance name is found in the registry.
    #>

    if ($Action -eq 'Install')
    {
        $fileVersion = Get-FileProductVersion -Path $SourcePath

        if ($getTargetResourceResult.InstanceName)
        {
            $installedVersion = [System.Version] $getTargetResourceResult.CurrentVersion

            # The major version is evaluated if VersionUpgrade is set to $true
            if (-not $VersionUpgrade -or ($VersionUpgrade -and $installedVersion -ge $fileVersion))
            {
                $returnValue = $true
            }
            else
            {
                Write-Verbose -Message (
                    $script:localizedData.WrongVersionFound `
                        -f $fileVersion.ToString(), $installedVersion.ToString()
                )
            }
        }
        else
        {
            Write-Verbose -Message (
                $script:localizedData.MissingVersion `
                    -f $fileVersion.ToString()
            )
        }
    }

    if ($Action -eq 'Uninstall' -and $null -eq $getTargetResourceResult.InstanceName)
    {
        $returnValue = $true
    }

    return $returnValue
}

<#
    .SYNOPSIS
        Converts between the edition names used by the resource and the
        installation media.
 
    .PARAMETER Name
        The edition name to convert.
 
    .OUTPUTS
        Returns the equivalent name of what was provided in the parameter Name.
        For example, if Name is set to 'Dev', the cmdlet returns 'Development'.
        If Name is set to 'Development', the cmdlet returns 'Dev'.
#>

function Convert-EditionName
{
    [CmdletBinding()]
    [OutputType([System.String])]
    param
    (
        [Parameter(Mandatory = $true)]
        [System.String]
        $Name
    )

    switch ($Name)
    {
        # Resource edition names
        'Development'
        {
            $convertEditionNameResult = 'Dev'
        }

        'Evaluation'
        {
            $convertEditionNameResult = 'Eval'
        }

        'ExpressAdvanced'
        {
            $convertEditionNameResult = 'ExprAdv'
        }

        # Installation media edition names
        'Dev'
        {
            $convertEditionNameResult = 'Development'
        }

        'Eval'
        {
            $convertEditionNameResult = 'Evaluation'
        }

        'ExprAdv'
        {
            $convertEditionNameResult = 'ExpressAdvanced'
        }
    }

    return $convertEditionNameResult
}

<#
    .SYNOPSIS
        Gets the product version of a executable.
 
    .PARAMETER Path
        The path to the executable to return product version for.
 
    .OUTPUTS
        Returns the product version as [System.Version] type.
#>

function Get-FileProductVersion
{
    [CmdletBinding()]
    [OutputType([System.Version])]
    param
    (
        [Parameter(Mandatory = $true)]
        [System.String]
        $Path
    )

    return [System.Version] (Get-Item -Path $Path).VersionInfo.ProductVersion
}