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(SupportsShouldProcess)] 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 ) begin { #Requires -RunAsAdministrator $ErrorActionPreference = "Stop" Import-NavModule -Service -Development } process { $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." } if ($PSCmdlet.ShouldProcess("$destUrl($($importData.ID))","Invoke-WebRequest DELETE")){ 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-Verbose "Destenation ($($serverInfo.SQLServerName)) ($($serverInfo.SQLDatabaseName)): $destUrl" if ($PSCmdlet.ShouldProcess("-Path $tempFob -DatabaseName $($serverInfo.SQLDatabaseName) -DatabaseServer $($serverInfo.SQLServerName)","Import-NAVApplicationObject")){ Import-NAVApplicationObject -Path $tempFob -DatabaseName $serverInfo.SQLDatabaseName -DatabaseServer $serverInfo.SQLServerName -ImportAction Overwrite -SynchronizeSchemaChanges No -SuppressBuildSearchIndex -Confirm:$false -ErrorAction Stop } if(($importData.tfsPatchLines | where { $_.Type -eq "MenuSuite" }).Count -gt 0){ Write-Host "MenuSuites detected in patch. Recompiling objects..." if ($PSCmdlet.ShouldProcess("MenuSuite","Compile-NAVApplicationObject")){ Compile-NAVApplicationObject -DatabaseName $serverInfo.SQLDatabaseName -DatabaseServer $serverInfo.SQLServerName ` -Recompile -Filter 'Type=MenuSuite' -SynchronizeSchemaChanges No -ErrorAction Continue } } if ($PSCmdlet.ShouldProcess($instanceName,"Sync-NAVTenant")){ $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 if ($PSCmdlet.ShouldProcess($destUrl,"Invoke-WebRequest")){ 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." } } end { if ($restart) { $instanceName, (Get-AdjacentInstances $instanceName) | % { Write-Host "Restarting instance $_" -ForegroundColor Cyan if ($PSCmdlet.ShouldProcess($_,"Restart-NAVServerInstance")){ Restart-NAVServerInstance $_ -ErrorAction Continue | Out-Null } } } } } <# .SYNOPSIS Installs all applicable patches from folder .DESCRIPTION Checks latest installed patch on instance, searches for patches in folder and if next patch can be installed then starts installation of patches one-by-one. If next patch to be installed has number that is not subsequent then process stops. .EXAMPLE Install-PatchMnogo -instanceName BC140 -folder "C:\Patches" -noSleep Install patches from folder 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-PatchMnogo { [CmdletBinding(SupportsShouldProcess)] param( # Destination BC instance where objects should be imported to. [Parameter(Mandatory = $True, ValueFromPipeline)] [string]$instanceName, # Path to the folder with patch archives. [Parameter(Mandatory = $true, ValueFromPipeline)] [string]$folder, # Level of the patch you interested in. Defaults to 1. [Parameter(Mandatory = $false)] [int]$level = 1, # 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, # Skip the 45s delay between Sync and Patch Data update request. Usefull when installing small patches in a batch. [Parameter(Mandatory = $false)] [switch]$noSleep ) begin { #Requires -RunAsAdministrator $ErrorActionPreference = "Stop" Import-NavModule -Service -Development $portDest = Get-NAVServerConfiguration $instanceName -KeyName ODataServicesPort $PatchInfo = Get-PatchInfo -instanceName $instanceName -port $portDest -level $level [int]$num = if (!$PatchInfo) { 0 } else { $PatchInfo.Number } Write-Verbose "Latest patch installed is $num" } process{ $num += 1 $found = @() $found = Get-ChildItem -Path "$folder\*" -Filter "*.zip" | ` % { if ($_.Name -match "_(1|2|3)_(?:0+)([0-9]+)" -and $matches[1] -eq $level -and $matches[2] -in $num..10000) { @{ Number = $matches[2]; Path = $_.FullName } } } if (!$found) { Write-Error "No patches found." } if ($found[0].Number -ne $num) { Write-Error "Next patch is not found. Latest installed is $num first found is $($found[0].Number)." } Write-Verbose "Found $($found.Count) patches on level $level" $num = $found[0].Number $found | % { if ($_.Number -ne $num) { Write-Warning "Processing has stopped because subsequent patch $num was not found in the folder." break; } if ($PSCmdlet.ShouldProcess("$($_.Path)","Install-Patch")) { Install-Patch -path $_.Path -instanceName $instanceName -restart:$restart -allowReinstall:$false -noSleep:$noSleep } $num++ } } } |