Patch/Install-Patch.ps1

<#
.SYNOPSIS
    Installs patch to BC database
.DESCRIPTION
    Imports fob file and data about patch and included objects' versions to BC database specified in the settings of BC instance provided as a parameter.
.EXAMPLE
    Install-Patch -instanceName n721 -path 'C:\N.7.2.1_1_00000003.zip'
    Install patch from file
.EXAMPLE
    Install-Patch -instanceName n721 -path "C:\7.2 BC_1_00000001.zip"
    Run script by its full path.
.EXAMPLE
    "n721", "prod" | Install-Patch -path "C:\7.2 BC_1_00000001.zip"
    Install patch to multiple instances
.EXAMPLE
    Get-ChildItem c:\temp\* -Include "*.zip" | Install-Patch -instanceName n721
    Install all patches from folder
.EXAMPLE
    Get-ChildItem -Path "c:\patches\*" | `
        % { if ($_.Name -match "(?:0+)([0-9]+)" -and $matches[1] -in 9..31) { $_ } } | `
            Install-Patch -instanceName prod
.NOTES
    Instances from destenation parameter must be configured to accept NTLM auth and have API endpoint enabled.
    Must be run as administrator because it requires to tun Sync-NavTenant cmdlet.
#>

function Install-Patch {
    [CmdletBinding()]
    param(
        # Destination BC instance where objects should be imported to.
        [Parameter(Mandatory = $True, ValueFromPipeline)]
        [string]$instanceName,
        # Path to patch archive.
        [Parameter(Mandatory = $true, ValueFromPipeline)]
        [string]$path,
        # When this switch is present all instances set to the same database will be restarted after last patch is imported.
        [Parameter(Mandatory = $false)]
        [switch]$restart,
        # When this switch is present current patch will be checked with the one being imported and if they are match, then current will be deleted and reimported.
        [Parameter(Mandatory = $false)]
        [switch]$allowReinstall,
        # Skip the 45s delay between Sync and Patch Data update request. Usefull when installing small patches in a batch.
        [Parameter(Mandatory = $false)]
        [switch]$noSleep
    )
    process {
        #Requires -RunAsAdministrator
        $ErrorActionPreference = "Stop"
        Import-NavModule -Service -Development

        $portDest = Get-NAVServerConfiguration $instanceName -KeyName ODataServicesPort
        $bcServerDest = "localhost"

        $temparchive = "$Env:TEMP\$([System.IO.Path]::GetFileNameWithoutExtension($path))\"
        Write-Verbose $temparchive
        Expand-Archive $path -DestinationPath $temparchive -Force
        $tempFob = Get-ChildItem $temparchive -Include "*.fob" -Recurse
        $tempJson = Get-ChildItem $temparchive -Include "*.json" -Recurse

        Write-Host "Installing patch $path"
        $destBaseUrl = "http://$bcServerDest`:$portDest/$instanceName/api/v1.0/companies"
        $company = (Invoke-RestMethod -Uri ($destBaseUrl) -UseDefaultCredentials -ErrorAction Stop).value[0].id
        $serverInfo = (Invoke-RestMethod -Uri ("$destBaseUrl($company)/tfsInfos") -UseDefaultCredentials).value[0]
        $destUrl = "$destBaseUrl($company)/tfsPatches"
        # Validity checks
        $importData = Get-Content $tempJson | ConvertFrom-Json
        $current = (Invoke-RestMethod -Uri "$destUrl`?`$filter=Level eq $($importData.Level)&`$top=1&`$orderby=Number desc" -UseDefaultCredentials -Method Get -ContentType "application/json").value

        Write-Verbose ($current | Format-List | Out-String)
        if ([int]$current.Number -ne 0) {
            if ([int]$current.Number + 1 -ne [int]$importData.Number) {
                if ($allowReinstall) {
                    if ([int]$current.Number -ne [int]$importData.Number) {
                        throw "Patch being imported has number $($importData.Number) and latest installed has number $($current.Number). With -reinstall parameter it is only allowed to install current patch or next one. Import cancelled."
                    }
                    Invoke-WebRequest -Uri "$destUrl($($importData.ID))" -UseDefaultCredentials -Method Delete -ContentType "application/json" | Out-Null
                }
                else {
                    if ([int]$current.Number -eq [int]$importData.Number) {
                        throw "Patch $($importData.Number) being imported is already installed if you want to reinstall patch then use parameter -reinstall. Import cancelled."
                    }
                    else {
                        throw "Patch being imported has number $($importData.Number) and latest installed has number $($current.Number). Install missing patches first. Import cancelled."
                    }
                }
            }
        }

        # Import objects
        Write-Host "Destenation ($($serverInfo.SQLServerName)) ($($serverInfo.SQLDatabaseName)): $destUrl" -ForegroundColor Green
        Import-NAVApplicationObject -Path $tempFob -DatabaseName $serverInfo.SQLDatabaseName -DatabaseServer $serverInfo.SQLServerName -ImportAction Overwrite -SynchronizeSchemaChanges No -SuppressBuildSearchIndex -Confirm:$false -ErrorAction Stop
        Sync-NAVTenant -ServerInstance $instanceName -Mode Sync -Force -ErrorAction SilentlyContinue -ErrorVariable syncError

        # Update version data
        if (-not $noSleep) { Start-Sleep -Seconds 45 } # Wait to avoid (503) Server Unavailable

        Invoke-WebRequest -Uri $destUrl -UseDefaultCredentials -Method Post -ContentType "application/json" -InFile $tempJson | Out-Null

        if ($syncError) {
            Write-Error "Patch ($([System.IO.Path]::GetFileName($path))) installed but schema was not synced due to error:
                    $syncError"

        }
        else {
            Write-Host "Patch ($([System.IO.Path]::GetFileName($path))) installed successfully." -ForegroundColor Cyan
        }
    }
    end {
        if ($restart) {
            $current = Get-NAVApplication $instanceName
            Get-NAVServerInstance | Where-Object -Property State -EQ -Value Running | % {
                $app = Get-NAVApplication $_.ServerInstance
                if ($app.'Database server' -eq $current.'Database server' -and $app.'Database name' -eq $current.'Database name') { $_ }
            } | % {
                Write-Host "Restarting instance $($_.ServerInstance)" -ForegroundColor Cyan
                Restart-NAVServerInstance $_.ServerInstance -ErrorAction Continue | Out-Null
            }
        }
    }
}