DscResources/MSFT_WindowsFeature/MSFT_WindowsFeature.psm1

# Global needed to indicate if a restart is required
[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidGlobalVars', '')]
param ()

$errorActionPreference = 'Stop'
Set-StrictMode -Version 'Latest'

Import-Module -Name (Join-Path -Path (Split-Path -Path $PSScriptRoot -Parent) `
                               -ChildPath 'CommonResourceHelper.psm1')

# Localized messages for verbose and error messages in this resource
$script:localizedData = Get-LocalizedData -ResourceName 'MSFT_WindowsFeature'

<#
    .SYNOPSIS
        Retrieves the status of the role or feature with the given name on the target machine.
 
    .PARAMETER Name
        The name of the role or feature to retrieve
 
    .PARAMETER Credential
        The credential (if required) to retrieve the role or feature.
        Optional.
 
    .NOTES
        If the specified role or feature does not contain any subfeatures then
        IncludeAllSubFeature will be set to $false. If the specified feature contains one
        or more subfeatures then IncludeAllSubFeature will be set to $true only if all the
        subfeatures are installed. Otherwise, IncludeAllSubFeature will be set to $false.
#>

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

       [Parameter()]
       [ValidateNotNullOrEmpty()]
       [System.Management.Automation.PSCredential]
       [System.Management.Automation.Credential()]
       $Credential
    )

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

    Import-ServerManager

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

    $isWinServer2008R2SP1 = Test-IsWinServer2008R2SP1
    if ($isWinServer2008R2SP1 -and $PSBoundParameters.ContainsKey('Credential'))
    {
        $feature = Invoke-Command -ScriptBlock { Get-WindowsFeature -Name $Name } `
                                  -ComputerName . `
                                  -Credential $Credential `
    }
    else
    {
        $feature = Get-WindowsFeature @PSBoundParameters
    }

    Assert-SingleInstanceOfFeature -Feature $feature -Name $Name

    $includeAllSubFeature = $true

    if ($feature.SubFeatures.Count -eq 0)
    {
        $includeAllSubFeature = $false
    }
    else
    {
        foreach ($currentSubFeatureName in $feature.SubFeatures)
        {
            $getWindowsFeatureParameters = @{
                Name = $currentSubFeatureName
            }

            if ($PSBoundParameters.ContainsKey('Credential'))
            {
               $getWindowsFeatureParameters['Credential'] = $Credential
            }

            if ($isWinServer2008R2SP1 -and $PSBoundParameters.ContainsKey('Credential'))
            {
                <#
                    Calling Get-WindowsFeature through Invoke-Command to start a new process with
                    the given credential since Get-WindowsFeature doesn't support the Credential
                    attribute on this server.
                #>

                $subFeature = Invoke-Command -ScriptBlock { Get-WindowsFeature -Name $currentSubFeatureName } `
                                             -ComputerName . `
                                             -Credential $Credential `
            }
            else
            {
                $subFeature = Get-WindowsFeature @getWindowsFeatureParameters
            }

            Assert-SingleInstanceOfFeature -Feature $subFeature -Name $currentSubFeatureName

            if (-not $subFeature.Installed)
            {
                $includeAllSubFeature = $false
                break
            }
        }
    }

    if ($feature.Installed)
    {
        $ensureResult = 'Present'
    }
    else
    {
        $ensureResult = 'Absent'
    }

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

    # Add all feature properties to the hash table
    return @{
        Name = $Name
        DisplayName = $feature.DisplayName
        Ensure = $ensureResult
        IncludeAllSubFeature = $includeAllSubFeature
    }
}

<#
    .SYNOPSIS
        Installs or uninstalls the role or feature with the given name on the target machine
        with the option of installing or uninstalling all subfeatures as well.
 
    .PARAMETER Name
        The name of the role or feature to install or uninstall.
 
    .PARAMETER Ensure
        Specifies whether the role or feature should be installed ('Present')
        or uninstalled ('Absent').
        By default this is set to Present.
 
    .PARAMETER IncludeAllSubFeature
        Specifies whether or not all subfeatures should be installed or uninstalled with
        the specified role or feature. Default is false.
        If this property is true and Ensure is set to Present, all subfeatures will be installed.
        If this property is false and Ensure is set to Present, subfeatures will not be installed or uninstalled.
        If Ensure is set to Absent, all subfeatures will be uninstalled.
 
    .PARAMETER Credential
        The credential (if required) to install or uninstall the role or feature.
        Optional.
 
    .PARAMETER LogPath
        The custom path to the log file to log this operation.
        If not passed in, the default log path will be used (%windir%\logs\ServerManager.log).
#>

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

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

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

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

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

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

    Import-ServerManager

    $isWinServer2008R2SP1 = Test-IsWinServer2008R2SP1

    if ($Ensure -eq 'Present')
    {
        $addWindowsFeatureParameters = @{
            Name = $Name
            IncludeAllSubFeature = $IncludeAllSubFeature
        }

        if ($PSBoundParameters.ContainsKey('LogPath'))
        {
           $addWindowsFeatureParameters['LogPath'] = $LogPath
        }

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

        if ($isWinServer2008R2SP1 -and $PSBoundParameters.ContainsKey('Credential'))
        {
            <#
                Calling Add-WindowsFeature through Invoke-Command to start a new process with
                the given credential since Add-WindowsFeature doesn't support the Credential
                attribute on this server.
            #>

            $feature = Invoke-Command -ScriptBlock { Add-WindowsFeature @addWindowsFeatureParameters } `
                                      -ComputerName . `
                                      -Credential $Credential
        }
        else
        {
            if ($PSBoundParameters.ContainsKey('Credential'))
            {
               $addWindowsFeatureParameters['Credential'] = $Credential
            }

            $feature = Add-WindowsFeature @addWindowsFeatureParameters
        }

        if ($null -ne $feature -and $feature.Success)
        {
            Write-Verbose -Message ($script:localizedData.InstallSuccess -f $Name)

            # Check if reboot is required, if so notify the Local Configuration Manager.
            if ($feature.RestartNeeded -eq 'Yes')
            {
                Write-Verbose -Message $script:localizedData.RestartNeeded
                $global:DSCMachineStatus = 1
            }
        }
        else
        {
            New-InvalidOperationException -Message ($script:localizedData.FeatureInstallationFailureError -f $Name)
        }
    }
    # Ensure = 'Absent'
    else
    {
        $removeWindowsFeatureParameters = @{
            Name = $Name
        }

        if ($PSBoundParameters.ContainsKey('LogPath'))
        {
           $removeWindowsFeatureParameters['LogPath'] = $LogPath
        }

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

        if ($isWinServer2008R2SP1 -and $PSBoundParameters.ContainsKey('Credential'))
        {
            <#
                Calling Remove-WindowsFeature through Invoke-Command to start a new process with
                the given credential since Remove-WindowsFeature doesn't support the Credential
                attribute on this server.
            #>

            $feature = Invoke-Command -ScriptBlock { Remove-WindowsFeature @removeWindowsFeatureParameters } `
                                      -ComputerName . `
                                      -Credential $Credential
        }
        else
        {
            if ($PSBoundParameters.ContainsKey('Credential'))
            {
               $addWindowsFeatureParameters['Credential'] = $Credential
            }

            $feature = Remove-WindowsFeature @removeWindowsFeatureParameters
        }

        if ($null -ne $feature -and $feature.Success)
        {
            Write-Verbose ($script:localizedData.UninstallSuccess -f $Name)

            # Check if reboot is required, if so notify the Local Configuration Manager.
            if ($feature.RestartNeeded -eq 'Yes')
            {
                Write-Verbose -Message $script:localizedData.RestartNeeded
                $global:DSCMachineStatus = 1
            }
        }
        else
        {
            New-InvalidOperationException -Message ($script:localizedData.FeatureUninstallationFailureError -f $Name)
        }
    }

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

<#
    .SYNOPSIS
        Tests if the role or feature with the given name is in the desired state.
 
    .PARAMETER Name
        The name of the role or feature to test the state of.
 
    .PARAMETER Ensure
        Specifies whether the role or feature should be installed ('Present')
        or uninstalled ('Absent').
        By default this is set to Present.
 
    .PARAMETER IncludeAllSubFeature
        Specifies whether or not the installation state of all subfeatures should be tested with
        the specified role or feature. Default is false.
        -If this property is set to true and Ensure is set to Present,
            each subfeature will be tested to ensure it is installed.
        -If this property is set to true and Ensure is set to Absent,
            each subfeature will be tested to ensure it is uninstalled.
        -If this property is false, subfeatures will not be tested.
 
    .PARAMETER Credential
        The Credential (if required) to test the status of the role or feature.
        Optional.
 
    .PARAMETER LogPath
        The path to the log file to log this operation.
        Not used in Test-TargetResource.
 
#>

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

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

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

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

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

    )

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

    Import-ServerManager

    $testTargetResourceResult = $false

    $getWindowsFeatureParameters = @{
        Name = $Name
    }

    if ($PSBoundParameters.ContainsKey('Credential'))
    {
       $getWindowsFeatureParameters['Credential'] = $Credential
    }

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

    $isWinServer2008R2SP1 = Test-IsWinServer2008R2SP1
    if ($isWinServer2008R2SP1 -and $PSBoundParameters.ContainsKey('Credential'))
    {
        <#
            Calling Get-WindowsFeature through Invoke-Command to start a new process with
            the given credential since Get-WindowsFeature doesn't support the Credential
            attribute on this server.
        #>

        $feature = Invoke-Command -ScriptBlock { Get-WindowsFeature -Name $Name } `
                                  -ComputerName . `
                                  -Credential $Credential
    }
    else
    {
        $feature = Get-WindowsFeature @getWindowsFeatureParameters
    }

    Assert-SingleInstanceOfFeature -Feature $feature -Name $Name

    # Check if the feature is in the requested Ensure state.
    if (($Ensure -eq 'Present' -and $feature.Installed -eq $true) -or `
        ($Ensure -eq 'Absent' -and $feature.Installed -eq $false))
    {
        $testTargetResourceResult = $true

        if ($IncludeAllSubFeature)
        {
            # Check if each subfeature is in the requested state.
            foreach ($currentSubFeatureName in $feature.SubFeatures)
            {
                $getWindowsFeatureParameters['Name'] = $currentSubFeatureName

                if ($isWinServer2008R2SP1 -and $PSBoundParameters.ContainsKey('Credential'))
                {
                    <#
                        Calling Get-WindowsFeature through Invoke-Command to start a new process with
                        the given credential since Get-WindowsFeature doesn't support the Credential
                        attribute on this server.
                    #>

                    $subFeature = Invoke-Command -ScriptBlock { Get-WindowsFeature -Name $currentSubFeatureName } `
                                                 -ComputerName . `
                                                 -Credential $Credential
                }
                else
                {
                    $subFeature = Get-WindowsFeature @getWindowsFeatureParameters
                }

                Assert-SingleInstanceOfFeature -Feature $subFeature -Name $currentSubFeatureName

                if (-not $subFeature.Installed -and $Ensure -eq 'Present')
                {
                    $testTargetResourceResult = $false
                    break
                }

                if ($subFeature.Installed -and $Ensure -eq 'Absent')
                {
                    $testTargetResourceResult = $false
                    break
                }
            }
        }
    }
    else
    {
        # Ensure is not in the correct state
        $testTargetResourceResult = $false
    }

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

    return $testTargetResourceResult
}


<#
    .SYNOPSIS
        Asserts that a single instance of the given role or feature exists.
 
    .PARAMETER Feature
        The role or feature object to check.
 
    .PARAMETER Name
        The name of the role or feature to include in any error messages that are thrown.
        (Not used to assert validity of the feature).
#>

function Assert-SingleInstanceOfFeature
{
    [CmdletBinding()]
    param
    (
        [Parameter()]
        [System.Management.Automation.PSObject]
        $Feature,

        [Parameter()]
        [System.String]
        $Name
    )

    if ($null -eq $Feature)
    {
        New-InvalidOperationException -Message ($script:localizedData.FeatureNotFoundError -f $Name)
    }

    if ($Feature -is [Array] -and $Feature.Count -gt 1)
    {
        New-InvalidOperationException -Message ($script:localizedData.MultipleFeatureInstancesError -f $Name)
    }
}

<#
    .SYNOPSIS
        Sets up the ServerManager module on the target node.
        Throws an error if not on a machine running Windows Server.
#>

function Import-ServerManager
{
    param
    ()

    <#
        Enable ServerManager-PSH-Cmdlets feature if OS is WS2008R2 Core.
        Datacenter = 12, Standard = 13, Enterprise = 14
    #>

    $serverCoreOSCodes = @( 12, 13, 14 )

    $operatingSystem = Get-CimInstance -Class 'Win32_OperatingSystem'

    # Check if this operating system needs an update to the ServerManager cmdlets
    if ($operatingSystem.Version.StartsWith('6.1.') -and `
        $serverCoreOSCodes -contains $operatingSystem.OperatingSystemSKU)
    {
        Write-Verbose -Message $script:localizedData.EnableServerManagerPSHCmdletsFeature

        <#
            ServerManager-PSH-Cmdlets has a depndency on Powershell 2 update: MicrosoftWindowsPowerShell,
            so enabling the MicrosoftWindowsPowerShell update.
        #>

        $null = Dism\online\enable-feature\FeatureName:MicrosoftWindowsPowerShell
        $null = Dism\online\enable-feature\FeatureName:ServerManager-PSH-Cmdlets
    }

    try
    {
        Import-Module -Name 'ServerManager' -ErrorAction Stop
    }
    catch [System.Management.Automation.RuntimeException]
    {
        if ($_.Exception.Message -like "*Some or all identity references could not be translated*")
        {
            Write-Verbose -Message $script:localizedData.IdentityNotFoundMessage
        }
        else
        {
            Write-Verbose -Message $script:localizedData.ServerManagerModuleNotFoundMessage
            New-InvalidOperationException -Message $script:localizedData.SkuNotSupported
        }
    }
    catch
    {
        Write-Verbose -Message $script:localizedData.ServerManagerModuleNotFoundMessage
        New-InvalidOperationException -Message $script:localizedData.SkuNotSupported
    }
}

<#
    .SYNOPSIS
        Tests if the machine is a Windows Server 2008 R2 SP1 machine.
 
    .NOTES
        Since Assert-PrequisitesValid ensures that ServerManager is available on the machine,
        this function only checks the OS version.
#>

function Test-IsWinServer2008R2SP1
{
    param
    ()

    return ([Environment]::OSVersion.Version.ToString().Contains('6.1.'))
}

Export-ModuleMember -Function *-TargetResource