DSCResources/DSC_xWindowsFeature/DSC_xWindowsFeature.psm1

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

$modulePath = Join-Path -Path (Split-Path -Path (Split-Path -Path $PSScriptRoot -Parent) -Parent) -ChildPath 'Modules'

# Import the shared modules
Import-Module -Name (Join-Path -Path $modulePath `
    -ChildPath (Join-Path -Path 'xPSDesiredStateConfiguration.Common' `
        -ChildPath 'xPSDesiredStateConfiguration.Common.psm1'))

Import-Module -Name (Join-Path -Path $modulePath -ChildPath 'DscResource.Common')

# Import Localization Strings
$script:localizedData = Get-LocalizedData -DefaultUICulture 'en-US'

<#
    .SYNOPSIS
        Retrieves the status of the role or feature with the given name on the target machine.
 
    .PARAMETER Name
        Indicates the name of the role or feature that you want to ensure is added or removed.
        This is the same as the Name property from the Get-WindowsFeature cmdlet, and not the
        display name of the role or feature.
 
    .PARAMETER Credential
        Indicates the credential to use to add or remove the role or feature if needed.
 
    .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
        Indicates the name of the role or feature that you want to ensure is added or removed.
        This is the same as the Name property from the Get-WindowsFeature cmdlet, and not the
        display name of the role or feature.
 
    .PARAMETER Ensure
        Specifies whether the feature should be installed (Present) or uninstalled (Absent).
        Defaults to 'Present'.
 
    .PARAMETER IncludeAllSubFeature
        Set this property to $true to ensure the state of all required subfeatures with the
        state of the feature you specify with the Name property. The default value is $false.
 
    .PARAMETER Credential
        Indicates the credential to use to add or remove the role or feature if needed.
 
    .PARAMETER LogPath
        Indicates the path to a log file to log the operation.
        If not specified, 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
                Set-DscMachineRebootRequired
            }
        }
        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
                Set-DscMachineRebootRequired
            }
        }
        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
        Indicates the name of the role or feature that you want to ensure is added or removed.
        This is the same as the Name property from the Get-WindowsFeature cmdlet, and not the
        display name of the role or feature.
 
    .PARAMETER Ensure
        Specifies whether the feature should be installed (Present) or uninstalled (Absent).
        Defaults to 'Present'.
 
    .PARAMETER IncludeAllSubFeature
        Set this property to $true to ensure the state of all required subfeatures with the
        state of the feature you specify with the Name property. The default value is $false.
 
    .PARAMETER Credential
        Indicates the credential to use to add or remove the role or feature if needed.
 
    .PARAMETER LogPath
        Indicates the path to a log file to log the 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.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 $_.Exception.Message
        }
        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