DSCResources/ArcGIS_InstallPatch/ArcGIS_InstallPatch.psm1

<#
    .SYNOPSIS
        Installs a given component of the ArcGIS Enterprise Stack.
    .PARAMETER Ensure
        Indicates if the Component is to be installed or uninstalled if not present. Take the values Present or Absent.
        - "Present" ensures that component is installed, if not already installed.
        - "Absent" ensures that component is uninstalled or removed, if installed.
    .PARAMETER Name
        Name of ArcGIS Enterprise Component to be installed.
    .PARAMETER PatchesDir
        Path to Installer for patches for the Component - Can be a Physical Location or Network Share Address.
    .PARAMETER PatchInstallOrder
        Array of Patch Installer file names to specify the Installation order of Patch and the patches to install
    .PARAMETER Version
        Version of the Component being Installed.
#>


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

        [parameter(Mandatory = $true)]
        [System.String]
        $PatchesDir,

        [parameter(Mandatory = $false)]
        [System.Array]
        $PatchInstallOrder,

        [parameter(Mandatory = $true)]
        [System.String]
        $Version,

        [parameter(Mandatory = $false)]
        [System.String]
        $ProductId,

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

    Import-Module $PSScriptRoot\..\..\ArcGISUtility.psm1 -Verbose:$false

    $null
}

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

        [parameter(Mandatory = $true)]
        [System.String]
        $PatchesDir,

        [parameter(Mandatory = $false)]
        [System.Array]
        $PatchInstallOrder,

        [parameter(Mandatory = $true)]
        [System.String]
        $Version,

        [parameter(Mandatory = $false)]
        [System.String]
        $ProductId,

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

    Import-Module $PSScriptRoot\..\..\ArcGISUtility.psm1 -Verbose:$false

    if($Ensure -eq 'Present') {
        # test & install patches
        Write-Verbose "Installing Patches"
        if ($PatchesDir) {
            if($PatchInstallOrder.Length -gt 0){
                foreach ($Patch in $PatchInstallOrder) {
                    $PatchFileName = Split-Path $Patch -leaf
                    $PatchLocation = (Join-Path $PatchesDir $PatchFileName)
                    Write-Verbose " > PatchFile : $PatchFileName | Fullname : $($PatchLocation)"
                    if (Test-PatchInstalled -mspPath $PatchLocation) {
                        Write-Verbose " > Patch installed - no Action required"
                    }else{
                        Write-Verbose " > Patch not installed - installing"
                        if(Install-Patch -mspPath $PatchLocation -Verbose){
                            Write-Verbose " > Patch installed - successfully"
                        }else{
                            Write-Verbose " > Patch installation failed"
                        }
                    }
                }
            }else{
                $files = Get-ChildItem "$PatchesDir"        
                Foreach ($file in $files) {
                    Write-Verbose " > PatchFile : $file | Fullname : $($file.Fullname)"
                    if (Test-PatchInstalled -mspPath $($file.FullName)) {
                        Write-Verbose " > Patch installed - no Action required"
                    } else {
                        Write-Verbose " > Patch not installed - installing"
                        if(Install-Patch -mspPath $file.FullName -Verbose){
                            Write-Verbose " > Patch installed - successfully"
                        }else{
                            Write-Verbose " > Patch installation failed"
                        }
                    }
                }
            }
        }
    }
    elseif($Ensure -eq 'Absent') {
        #Uninstall Patch
    }
    Write-Verbose "In Set-Resource for $Name"
}

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

        [parameter(Mandatory = $true)]
        [System.String]
        $PatchesDir,

        [parameter(Mandatory = $false)]
        [System.Array]
        $PatchInstallOrder,

        [parameter(Mandatory = $true)]
        [System.String]
        $Version,

        [parameter(Mandatory = $false)]
        [System.String]
        $ProductId,

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

    Import-Module $PSScriptRoot\..\..\ArcGISUtility.psm1 -Verbose:$false

    $result = $false
    
    $ComponentName = $Name
    if($Name -ieq "ArcGIS Pro"){
        $ComponentName = 'Pro'
    }elseif($Name -ieq "ArcGIS Desktop"){
        $ComponentName = 'Desktop'
    }elseif($Name -ieq "ArcGIS License Manager"){
        $ComponentName = 'LicenseManager'
    }elseif($Name -ieq "ArcGIS for Server"){
        $ComponentName = 'Server'
    }elseif($Name -ieq "Web Styles"){
        $ComponentName = 'WebStyles'
    }elseif($Name -ieq "DataStore"){
        $ComponentName = 'DataStore'
    }elseif($Name -ieq "GeoEvent"){
        $ComponentName = 'GeoEvent'
    }elseif($Name -ieq "Notebook Server"){
        $ComponentName = 'NotebookServer'
    }elseif($Name -ieq "Mission Server"){
        $ComponentName = 'MissionServer'
    }elseif($Name -ieq "Workflow Manager Server"){
        $ComponentName = 'WorkflowManagerServer'
    }elseif($Name -ieq "Workflow Manager WebApp"){
        $ComponentName = 'WorkflowManagerWebApp'
    }elseif($Name -ieq "Insights"){
        $ComponentName = 'Insights'
    }elseif($Name -ieq "WebAdaptor"){
        $ComponentName = 'WebAdaptor'
    }

    if(-not($ProductId)){
        $trueName = Get-ArcGISProductName -Name $ComponentName -Version $Version
        
        $InstallObject = (Get-ArcGISProductDetails -ProductName $trueName)
        if($Name -ieq 'WebAdaptor'){
            if($InstallObject.Length -gt 1){
                Write-Verbose "Multiple Instances of Web Adaptor are already installed - $($InstallObject.Version)"
            }
            foreach($wa in $InstallObject){
                $result = Test-Install -Name 'WebAdaptor' -Version $Version -ProductId $wa.IdentifyingNumber.TrimStart("{").TrimEnd("}") -Verbose
                if($result -ieq $True){
                    Write-Verbose "Found Web Adaptor Installed for Version $Version"
                    break
                }else{
                    $result = $False
                }
            }
        }else{
            Write-Verbose "Installed Version $($InstallObject.Version)"
            $result = Test-Install -Name $ComponentName -Version $Version
        }
    }else{
        $result = Test-Install -Name $ComponentName -ProductId $ProductId
    }
   
    #test for installed patches
    if($result -and $PatchesDir) {
        if($PatchInstallOrder.Length -gt 0){
            foreach ($Patch in $PatchInstallOrder) {
                $PatchFileName = Split-Path $Patch -leaf
                $PatchLocation = (Join-Path $PatchesDir $PatchFileName)
                Write-Verbose " > PatchFile : $PatchFileName | Fullname : $($PatchLocation)"
                if (Test-PatchInstalled -mspPath $PatchLocation) {
                    Write-Verbose " > Patch installed"
                }else{
                    Write-Verbose " > Patch not installed"
                    $result = $false
                }
            }
        }else{
            $files = Get-ChildItem "$PatchesDir"        
            Foreach ($file in $files) {
                Write-Verbose " > PatchFile : $file | Fullname : $($file.Fullname)"
                if (Test-PatchInstalled -mspPath $($file.FullName)) {
                    Write-Verbose " > Patch installed"
                } else {
                    Write-Verbose " > Patch not installed"
                    $result = $false
                }
            }
        }
    }

    if($Ensure -ieq 'Present') {
           $result   
    }
    elseif($Ensure -ieq 'Absent') {        
        (-not($result))
    }
}

Function Test-PatchInstalled {
        
    [OutputType([System.Boolean])]
    Param(
        # The path to the patch file
        [System.String]
        $MSPPath
    )

    $Test = $False

    # Confirm the Patch file exists, let upstream handle the error
    If ( -Not (Test-Path -PathType Leaf -Path $MSPPath) ) {
        Write-Warning -Message "The Patch File $MSPPath is not accessible"
        Return $Test
    }
    
    # Extract the QFE-ID from the *.msp
    $Patch_QFE_ID = Get-MSPQFEID -PatchNamePath $MSPPath
    $Test_QFE_ID = "$Patch_QFE_ID"

    If ( [String]::IsNullOrEmpty($Test_QFE_ID) ) {
        Write-Warning -Message "Unable to extract the QFE-ID from the Patch file $MSPPath"
        Return $Test
    }

    # A list of Registry Paths to check
    $RegPaths = @(
        "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*" ,
        "HKLM:\SOFTWARE\ESRI\Portal for ArcGIS\Updates\*" ,
        "HKLM:\SOFTWARE\ESRI\ArcGIS Data Store\Updates\*" ,
        "HKLM:\SOFTWARE\ESRI\ArcGIS Insights\Updates\*" ,
        "HKLM:\SOFTWARE\ESRI\Server10.3\Updates\*" ,
        "HKLM:\SOFTWARE\ESRI\Server10.4\Updates\*" ,
        "HKLM:\SOFTWARE\ESRI\Server10.5\Updates\*" ,
        "HKLM:\SOFTWARE\ESRI\Server10.6\Updates\*" ,
        "HKLM:\SOFTWARE\ESRI\Server10.7\Updates\*" ,
        "HKLM:\SOFTWARE\ESRI\Server10.8\Updates\*" ,
        "HKLM:\SOFTWARE\ESRI\Server10.9\Updates\*" ,
        "HKLM:\SOFTWARE\ESRI\GeoEvent10.6\Server\Updates\*",
        "HKLM:\SOFTWARE\ESRI\GeoEvent10.7\Server\Updates\*",
        "HKLM:\SOFTWARE\ESRI\GeoEvent10.8\Server\Updates\*",
        "HKLM:\SOFTWARE\ESRI\GeoEvent10.9\Server\Updates\*",
        "HKLM:\SOFTWARE\ESRI\ArcGISPro\Updates\*" ,
        "HKLM:\SOFTWARE\WOW6432Node\ESRI\Desktop10.4\Updates\*" ,
        "HKLM:\SOFTWARE\WOW6432Node\ESRI\Desktop10.5\Updates\*" ,
        "HKLM:\SOFTWARE\WOW6432Node\ESRI\Desktop10.6\Updates\*" ,
        "HKLM:\SOFTWARE\WOW6432Node\ESRI\Desktop10.7\Updates\*",
        "HKLM:\SOFTWARE\WOW6432Node\ESRI\Desktop10.8\Updates\*",
        "HKLM:\SOFTWARE\WOW6432Node\ESRI\ArcGIS Web Adaptor (IIS) 10.8.1\Updates\*"
    )
    
    ForEach ( $RegPath in $RegPaths ) {
           
        If ( Test-Path -PathType Container -Path $RegPath ) {
        
            # Search the Registry path for all 'QFE_ID' Objects
            $Reg_QFE_IDs = Get-ItemProperty $RegPath | Sort-Object -Property QFE_ID | Select-Object QFE_ID

            ForEach ( $Reg_QFE_ID in $Reg_QFE_IDs ) {

                If ( [String]::IsNullOrEmpty($Reg_QFE_ID.QFE_ID) ) {
                    Continue
                }
            
                Write-Verbose -Message "Comparing QFE ID $Test_QFE_ID against ID $($Reg_QFE_ID.QFE_ID)"
                
                If ( $( $Reg_QFE_ID.QFE_ID ) -ieq $Test_QFE_ID ) {
                
                    # The patch is installed, skip further processing
                    Write-Verbose -Message "Patch already installed: $MSPPath - $Test_QFE_ID"
                    $Test = $True
                    Return $Test

                }

            }

        } Else {

            Continue

        }

    }

    Return $Test

}

function Install-Patch{
    [OutputType([System.Boolean])]
    param
    (
        [System.String]
        $mspPath
    )

    if(Test-Path $mspPath){
        $arguments = "/update "+ '"' + $mspPath +'"' + " /quiet"
        Write-Verbose $arguments
        try {
            $PatchInstallProc = Start-Process -FilePath msiexec.exe -ArgumentList $Arguments -Wait -Verbose -PassThru
            if($PatchInstallProc.ExitCode -ne 0){
                Write-Verbose "Error while installing patch :- exited with status code $($PatchInstallProc.ExitCode)"
                return $false
            }else{
                Write-Verbose "Patch Installation successful."
                return $true
            }
        } catch {
            Write-Verbose "Error in Install-Patch :-$_"
            return $false
        }
    }else{
        Write-Verbose "Patch '$mspPath' path doesn't exist"
        return $false
    }
}

# http://www.andreasnick.com/85-reading-out-an-msp-product-code-with-powershell.html
<#
.SYNOPSIS
    Get the Patch Code from an Microsoft Installer Patch MSP
.DESCRIPTION
    Get a Patch Code from an Microsoft Installer Patch MSP (Andreas Nick 2015)
.NOTES
    $NULL for an error
.LINK
.RETURNVALUE
  [String] Product Code
.PARAMETER
  [IO.FileInfo] Path to the msp file
#>

function Get-MSPqfeID {
    param (
        [IO.FileInfo] $patchnamepath
          
    )
    try {
        $wi = New-Object -com WindowsInstaller.Installer
        $mspdb = $wi.GetType().InvokeMember("OpenDatabase", "InvokeMethod", $Null, $wi, $($patchnamepath.FullName, 32))
        $su = $mspdb.GetType().InvokeMember("SummaryInformation", "GetProperty", $Null, $mspdb, $Null)
        #$pc = $su.GetType().InvokeMember("PropertyCount", "GetProperty", $Null, $su, $Null)

        [String] $qfeID = $su.GetType().InvokeMember("Property", "GetProperty", $Null, $su, 3)
        return $qfeID
    }
    catch {
        Write-Output -InputObject $_.Exception.Message
        return $NULL
    }
}

Export-ModuleMember -Function *-TargetResource