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 } } } } |