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 BC140 -path 'C:\N.7.2.1_1_00000003.zip' Install patch from file to instance BC140 .EXAMPLE "BC140_1", "BC140_2" | Install-Patch -path "C:\7.2 BC_1_00000001.zip" Install patch to multiple instances (BC140_1 and BC140_2) .EXAMPLE Get-ChildItem "c:\temp\*" -Include "*.zip" | Install-Patch -instanceName BC140 Install all patches from folder c:\temp to instance BC140 .EXAMPLE Get-ChildItem -Path "c:\patches\*" | ` % { if ($_.Name -match "_(1|2|3)_(?:0+)([0-9]+)" -and $matches[1] -eq 1 -and $matches[2] -in 9..31) { $_ } } | ` Install-Patch -instanceName BC140 Install patches 9 through 31 for layer 1 from folder c:\patches to instance BC140 .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 $ProgressPreferenceBack = $ProgressPreference; $ProgressPreference = "SilentlyContinue"; Sync-NAVTenant -ServerInstance $instanceName -Mode Sync -Force -ErrorAction SilentlyContinue -ErrorVariable syncError $ProgressPreference = $ProgressPreferenceBack; # 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 } } } } |