Patch/Install-Release.ps1
<#
.SYNOPSIS The function performs upgrade of BC installation to new release .DESCRIPTION The function checks the database related to instance provided. Imports new objects and runs data upgrade procedures. It can temporary swap customer license in database with development license if it's needed for upgrade to succeed. Plugin scripts are available with the following names: BeforeImportPatch, AfterImportPatch, BeforeDataUpgrade, AfterDataUpgrade, BeforeUpgradeExtensions, AfterUpgradeExtensions. Plugin scripts should contain a call to Setup-UpgradeTask function and add an upgrade task before or after task mentioned in plugin name. .PARAMETER ServerInstance Specifies the Microsoft Dynaimcs Business Central Server instance that is used during upgrade .PARAMETER RapidStartPackageFile Specifies the path to the RapidStart package to be imported after the successfull upgrade .PARAMETER DevLicense Specifies the path to the development license. This license will be imported to the database before the upgrade. .PARAMETER CustLicense Specifies the path to the customer license. This license will be imported to the database when upgrade is finished. .PARAMETER NoSleep If that switch is ON then all delays will be skipped. Only use it to speed up testing process. .EXAMPLE Install-Release -ServerInstance prod -DevLicense "C:\License\DevLicense.flf" -PrimaryPatch "Patch_1_00015.zip" -SecondaryPatch "Patch_2_00006.zip" #> function Install-Release { [CmdletBinding()] param( [parameter(Mandatory=$true)] [string]$ServerInstance, [parameter(Mandatory=$false)] [string]$ReleaseFolder = (Get-Location), [parameter(Mandatory=$false)] [ArgumentCompleter({ param($Command, $Parameter, $WordToComplete, $CommandAst, $FakeBoundParams) $SearchPath = $FakeBoundParams.ReleaseFolder Get-ChildItem -Path "$SearchPath\*" -Directory -Filter ".*" | % { """$($_.Name)""" } })] [Alias("iml")] [string]$ReleaseSubFolder, [parameter(Mandatory=$false)] [string]$DevLicense, [parameter(Mandatory=$false)] [string]$CustLicense, [parameter(Mandatory=$false)] [string]$RapidStartPackageFile = "", [switch]$NoSleep ) #Requires -RunAsAdministrator $ErrorActionPreference = "Stop" if ($NoSleep) { Write-Warning "Only use NoSleep switch for testing purposes" } $PrimaryPatch = (Get-ChildItem -Path "$ReleaseFolder" -Filter "*_1_*.zip" | Select-Object -First 1).FullName if (!$PrimaryPatch) { Write-Error "Primary patch not found under $ReleaseFolder" } Write-Verbose "Primary patch > $PrimaryPatch" if ($ReleaseSubFolder) { $SecondaryPatch = (Get-ChildItem -Path (Join-Path $ReleaseFolder $ReleaseSubFolder) -Filter "*_2_*.zip" | Select-Object -First 1).FullName if (!$SecondaryPatch) { Write-Error "Secondary patch not found under $ReleaseFolder" } Write-Verbose "Secondary patch > $SecondaryPatch" } try { ($UpgradeTasksStatistics = Start-Upgrade ` -NAVServerInstance $ServerInstance ` -PrimaryPatch $PrimaryPatch ` -SecondaryPatch $SecondaryPatch ` -DevLicense $DevLicense ` -CustLicense $CustLicense ` -RapidStartPackageFile $RapidStartPackageFile ` -NoSleep:$NoSleep ` ) | %{ if($_.Status -eq 'Failed') { Write-Error $_."Error" } } } finally { # Uncomment the following line in order to have a better rendered view, in a separate window, on the Upgrade Tasks Statistics # $UpgradeTasksStatistics | Out-GridView $UpgradeTasksStatistics | ft } } <# .SYNOPSIS The function performs upgrade of BC installation to new release .DESCRIPTION The function checks the database related to instance provided. Imports new objects and runs data upgrade procedures. It can temporary swap customer license in database with development license if it's needed for upgrade to succeed. .PARAMETER NAVServerInstance Specifies the Microsoft Dynaimcs Business Central Server instance that is used during upgrade .PARAMETER PrimaryPatch File name of the patch archive to be installed first .PARAMETER SecondaryPatch File name of the patch archive to be installed second .PARAMETER RapidStartPackageFile Specifies the path to the RapidStart package to be imported after the successfull upgrade .PARAMETER DevLicense Specifies the path to the development license .PARAMETER CustLicense Specifies the path to the customer license .PARAMETER NoSleep If that switch is ON then all delays will be skipped. Only use it to speed up testing process. #> function Start-Upgrade { [CmdletBinding()] param ( [parameter(Mandatory=$true)] [string]$NAVServerInstance, [parameter(Mandatory=$true)] [string]$PrimaryPatch, [parameter(Mandatory=$false)] [string]$SecondaryPatch, [parameter(Mandatory=$false)] [string]$RapidStartPackageFile = "", [parameter(Mandatory=$false)] [string]$DevLicense = "", [parameter(Mandatory=$false)] [string]$CustLicense = "", [parameter(Mandatory=$false)] [switch]$NoSleep ) BEGIN { Write-Verbose "=========================================================================================" Write-Verbose ("Upgrade script starting at " + (Get-Date).ToLongTimeString() + "...") Write-Verbose "=========================================================================================" } PROCESS { $ErrorActionPreference = "Stop" $error.Clear(); $RootFolder = [System.IO.Path]::GetDirectoryName($PrimaryPatch) $SubFolder = if ($SecondaryPatch) { [System.IO.Path]::GetDirectoryName($SecondaryPatch) } else { "" } # Ensure the NAV Management Module is loaded Import-NavModule -Service -Development # Ensure the SQLPS PowerShell module is loaded Test-SqlServerLoaded #region Verify that the prerequisites required by the script are met # The NAV Server Instance exists if($null -eq (Get-NAVServerInstance $NAVServerInstance)) { Write-Error "The Microsoft Dynamics NAV Server instance $NAVServerInstance does not exist." return } $NavServerName = "localhost" $DatabaseName = (Get-NAVServerConfiguration -ServerInstance $NavServerInstance -AsXML).SelectSingleNode("configuration/appSettings/add[@key='DatabaseName']").value $DatabaseServer = (Get-NAVServerConfiguration -ServerInstance $NavServerInstance -AsXML).SelectSingleNode("configuration/appSettings/add[@key='DatabaseServer']").value $DatabaseInstance = (Get-NAVServerConfiguration -ServerInstance $NavServerInstance -AsXML).SelectSingleNode("configuration/appSettings/add[@key='DatabaseInstance']").value $DatabaseServerInstance = Get-SqlServerInstance -DatabaseServer $DatabaseServer -DatabaseInstance $DatabaseInstance $IsMultitenant = (Get-NAVServerConfiguration -ServerInstance $NavServerInstance -AsXML).SelectSingleNode("configuration/appSettings/add[@key='Multitenant']").value Write-Verbose "Upgrading > $DatabaseName @ $DatabaseServerInstance" # The NAV Database exists on the Sql Server Instance if(!(Test-NAVDatabaseOnSqlInstance -DatabaseServer $DatabaseServer -DatabaseInstance $DatabaseInstance -DatabaseName $DatabaseName)) { Write-Error "Database '$DatabaseName' does not exist on SQL Server instance '$DatabaseServerInstance'" return } #endregion # Initilize an empty list that will be populated with all the tasks that are executed part of Microsoft Dynamics NAV Data Upgrade process. # The list will include statistics regarding execution time, status and the associated script block $UpgradeTasks = [ordered]@{} #region Backup current license from the application part of the database (table '$ndo$dbproperty') , if it exists if($DevLicense -and !$CustLicense){ $CustLicense = [System.IO.Path]::GetTempFileName() . Setup-UpgradeTask ` -TaskName "Backup current application license" ` -ScriptBlock { Write-Verbose "Backup license to $CustLicense" Export-NAVLicense ` -DatabaseServer $DatabaseServerInstance ` -DatabaseName $DatabaseName ` -LicenseFilePath $CustLicense } | %{ $UpgradeTasks.Add($_.Statistics, $_.ScriptBlock) } } #endregion #region Import development license, and restart the server in order for the license to be loaded if($DevLicense){ . Setup-UpgradeTask ` -TaskName "Import dev license" ` -ScriptBlock { Import-NAVServerLicense -ServerInstance $NAVServerInstance -LicenseFile $DevLicense -Database NavDatabase -WarningAction SilentlyContinue Set-NAVServerInstance -ServerInstance $NAVServerInstance -Restart } | %{ $UpgradeTasks.Add($_.Statistics, $_.ScriptBlock) } } #endregion #region Synchronize the NAV database . Setup-UpgradeTask ` -TaskName "Synchronize schema for all tables" ` -ScriptBlock { Get-NAVTenant -ServerInstance $NavServerInstance | Sync-NAVTenant -ServerInstance $NavServerInstance -Mode Sync -force -ErrorAction Stop } | %{ $UpgradeTasks.Add($_.Statistics, $_.ScriptBlock) } #endregion #region Check for unsynchronized database changes. If there are any, abort the upgrade . Setup-UpgradeTask ` -TaskName "Check for unsynchronized database changes" ` -ScriptBlock { if(!$NoSleep) { Start-Sleep -Seconds 15 } if ($IsMultitenant.ToUpper() -eq "TRUE") { $NavTenants = Get-NAVTenant $NavServerInstance Foreach ($NavTenant in $NavTenants) { # Synchronize schema for all tables $CurrentNavTenantSettings = Get-NAVTenant -ServerInstance $NavServerInstance -Tenant $NavTenant.Id if ($CurrentNavTenantSettings.state -ne "Operational") { Throw "Tenant " + $NavTenant.Id + " is not operational, the upgrade is aborted. Run Sync-navtenant with mode 'CheckOnly' to get more information." return } } } else { # Synchronize schema for all tables $NavTenantSettings = Get-NAVTenant -ServerInstance $NavServerInstance -Tenant Default if ($NavTenantSettings.state -ne "Operational") { Throw "Tenant " + $NavTenant.Id + " is not operational, the upgrade is aborted. Run Sync-navtenant with mode 'CheckOnly' to get more information." return } } } | % { $UpgradeTasks.Add($_.Statistics, $_.ScriptBlock) } #endregion #TODO #Region Shutdown additional instances # . Setup-UpgradeTask ` # -TaskName "Shutdown additional instances" ` # -ScriptBlock { # $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 # } # } | %{ $UpgradeTasks.Add($_.Statistics, $_.ScriptBlock) } #endregion #Region Restart NAV Server Instance . Setup-UpgradeTask ` -TaskName "Restart NAV Server instance" ` -ScriptBlock { Set-NAVServerInstance $NavServerInstance -Start -Force -ErrorAction Stop } | % { $UpgradeTasks.Add($_.Statistics, $_.ScriptBlock) } #endregion #Region BeforeImportPatch $pluginName = "BeforeImportPatch" $pluginPath = Join-Path $RootFolder "$pluginName.ps1" if ($pluginPath -and (Test-Path $pluginPath -PathType Leaf)) { . ($pluginPath) } $pluginPath = "" #endregion #Region Import objects #Region Import primary objects . Setup-UpgradeTask ` -TaskName "Importing update objects" ` -ScriptBlock { if(!$NoSleep) { Start-Sleep -Seconds 60 } Install-Patch -instanceName $NavServerInstance -path $PrimaryPatch -allowReinstall -noSleep:$NoSleep } | % { $UpgradeTasks.Add($_.Statistics, $_.ScriptBlock) } #endregion #Region Import additional objects if ($SecondaryPatch -and (Test-Path $SecondaryPatch -PathType Leaf)) { #Region BeforeImportPatch $pluginName = "BeforeImportPatch" $pluginPath = Join-Path $SubFolder "$pluginName.ps1" if ($pluginPath -and (Test-Path $pluginPath -PathType Leaf)) { . ($pluginPath) } $pluginPath = "" #endregion . Setup-UpgradeTask ` -TaskName "Importing update objects for IML" ` -ScriptBlock { if(!$NoSleep) { Start-Sleep -Seconds 15 } Install-Patch -instanceName $NavServerInstance -path $SecondaryPatch -allowReinstall -noSleep:$NoSleep } | % { $UpgradeTasks.Add($_.Statistics, $_.ScriptBlock) } } #endregion #endregion #Region AfterImportPatch $pluginName = "AfterImportPatch" $pluginPath = Join-Path $RootFolder "$pluginName.ps1" if ($pluginPath -and (Test-Path $pluginPath -PathType Leaf)) { . ($pluginPath) } if ($SubFolder) { $pluginPath = Join-Path $SubFolder "$pluginName.ps1" if ($pluginPath -and (Test-Path $pluginPath -PathType Leaf)) { . ($pluginPath) } } $pluginPath = "" #endregion #Region Perform Schema Synchronization for all tenants . Setup-UpgradeTask ` -TaskName "Compile uncompiled objects" ` -ScriptBlock { if(!$NoSleep) { Start-Sleep -Seconds 10 } Compile-NAVApplicationObject -DatabaseServer $DatabaseServerInstance -DatabaseName $DatabaseName ` -NavServerInstance $NavServerInstance -NavServerName $NavServerName ` -SynchronizeSchemaChanges No -ErrorAction SilentlyContinue } | % { $UpgradeTasks.Add($_.Statistics, $_.ScriptBlock) } #endregion #Region Perform Schema Synchronization for all tenants . Setup-UpgradeTask ` -TaskName "Synchronize schema for all tables" ` -ScriptBlock { if(!$NoSleep) { Start-Sleep -Seconds 10 } Get-NAVTenant -ServerInstance $NavServerInstance | Sync-NAVTenant -ServerInstance $NavServerInstance -Mode Sync -force -ErrorAction Stop } | % { $UpgradeTasks.Add($_.Statistics, $_.ScriptBlock) } #endregion #Region MenuSuite Compilation . Setup-UpgradeTask ` -TaskName "MenuSuite objects compilation" ` -ScriptBlock { Compile-NAVApplicationObject -DatabaseServer $DatabaseServerInstance -DatabaseName $DatabaseName ` -Recompile -Filter 'Type=MenuSuite' ` -SynchronizeSchemaChanges No -ErrorAction Stop } | % { $UpgradeTasks.Add($_.Statistics, $_.ScriptBlock) } #endregion #Region BeforeDataUpgrade $pluginName = "BeforeDataUpgrade" $pluginPath = Join-Path $RootFolder "$pluginName.ps1" if ($pluginPath -and (Test-Path $pluginPath -PathType Leaf)) { . ($pluginPath) } if ($SubFolder) { $pluginPath = Join-Path $SubFolder "$pluginName.ps1" if ($pluginPath -and (Test-Path $pluginPath -PathType Leaf)) { . ($pluginPath) } } $pluginPath = "" #endregion #Region Invoke the Data Upgrade process . Setup-UpgradeTask ` -TaskName "Invoke data upgrade" ` -ScriptBlock { $NavTenants = Get-NAVTenant $NavServerInstance -ErrorAction Stop $NavTenants | % { $_.Id } | Invoke-NAVDataUpgrade -ServerInstance $NAVServerInstance -ErrorAction Stop } | %{ $UpgradeTasks.Add($_.Statistics, $_.ScriptBlock) } #endregion #Region AfterDataUpgrade $pluginName = "AfterDataUpgrade" $pluginPath = Join-Path $RootFolder "$pluginName.ps1" if ($pluginPath -and (Test-Path $pluginPath -PathType Leaf)) { . ($pluginPath) } if ($SubFolder) { $pluginPath = Join-Path $SubFolder "$pluginName.ps1" if ($pluginPath -and (Test-Path $pluginPath -PathType Leaf)) { . ($pluginPath) } } $pluginPath = "" #endregion #Region Delete Killme Objects . Setup-UpgradeTask ` -TaskName "Delete killme objects" ` -ScriptBlock { Delete-NAVApplicationObject -DatabaseServer $DatabaseServerInstance -DatabaseName $DatabaseName -NavServerName $NavServerName -NavServerInstance $NavServerInstance ` -NavServerManagementPort $NavManagementPort -Filter 'Name=@Kill*' -SynchronizeSchemaChanges No -Confirm:$false -ErrorAction Stop } | % { $UpgradeTasks.Add($_.Statistics, $_.ScriptBlock) } #endregion #region Delete Upgrade Objects . Setup-UpgradeTask ` -TaskName "Delete upgrade objects" ` -ScriptBlock { Delete-NAVApplicationObject -DatabaseServer $DatabaseServerInstance -DatabaseName $DatabaseName -NavServerName $NavServerName -NavServerInstance $NavServerInstance ` -NavServerManagementPort $NavManagementPort -Filter 'Version List=@*UPG*' -SynchronizeSchemaChanges No -Confirm:$false -ErrorAction Stop } | % { $UpgradeTasks.Add($_.Statistics, $_.ScriptBlock) } #endregion #region Synchronize schema for all killme tables . Setup-UpgradeTask ` -TaskName "Synchronize schema with force" ` -ScriptBlock { if(!$NoSleep) { Start-Sleep -Seconds 1 } Get-NAVTenant -ServerInstance $NavServerInstance | Sync-NAVTenant -ServerInstance $NavServerInstance -Mode Force -force -ErrorAction Stop } | % { $UpgradeTasks.Add($_.Statistics, $_.ScriptBlock) } #endregion #region Import customer license to database if($DevLicense){ . Setup-UpgradeTask ` -TaskName "Import customer license" ` -ScriptBlock { Write-Verbose "Restoring license from $CustLicense" Import-NAVServerLicense -ServerInstance $NAVServerInstance -LicenseFile $CustLicense -Database NavDatabase -WarningAction SilentlyContinue } | % { $UpgradeTasks.Add($_.Statistics, $_.ScriptBlock) } } #endregion #Region ServiceTierRestart . Setup-UpgradeTask ` -TaskName "Restarting the NAV Server Instance" ` -ScriptBlock { Set-NAVServerInstance $NavServerInstance -Restart -Force -ErrorAction Stop } | % { $UpgradeTasks.Add($_.Statistics, $_.ScriptBlock) } #endregion #Region BeforeUpgradeExtensions $pluginPath = Join-Path $ReleaseFolder "BeforeUpgradeExtensions.ps1" $pluginName = "BeforeUpgradeExtensions" $pluginPath = Join-Path $RootFolder "$pluginName.ps1" if ($pluginPath -and (Test-Path $pluginPath -PathType Leaf)) { . ($pluginPath) } if ($SubFolder) { $pluginPath = Join-Path $SubFolder "$pluginName.ps1" if ($pluginPath -and (Test-Path $pluginPath -PathType Leaf)) { . ($pluginPath) } } $pluginPath = "" #endregion #region Upgrade extensions $extensionFolder = (Join-Path $RootFolder "Extensions") if ($extensionFolder) { . Setup-UpgradeTask ` -TaskName "Upgrading extensions" ` -ScriptBlock { Upgrade-Extension -instanceName $NavServerInstance -path $extensionFolder } | % { $UpgradeTasks.Add($_.Statistics, $_.ScriptBlock) } } #endregion #Region AfterUpgradeExtensions $pluginName = "AfterUpgradeExtensions" $pluginPath = Join-Path $RootFolder "$pluginName.ps1" if ($pluginPath -and (Test-Path $pluginPath -PathType Leaf)) { . ($pluginPath) } if ($SubFolder) { $pluginPath = Join-Path $SubFolder "$pluginName.ps1" if ($pluginPath -and (Test-Path $pluginPath -PathType Leaf)) { . ($pluginPath) } } $pluginPath = "" #endregion #Region Optionally, run RapidStart package import if($RapidStartPackageFile) { . Setup-UpgradeTask ` -TaskName "RapidStart Package import" ` -ScriptBlock { Invoke-NAVRapidStartDataImport -ServerInstance $NAVServerInstance -RapidStartPackageFile $RapidStartPackageFile } | %{ $UpgradeTasks.Add($_.Statistics, $_.ScriptBlock) } } #endregion # Run the upgrade tasks and stop if an error has occurred foreach($UpgradeTask in $UpgradeTasks.GetEnumerator()) { Execute-UpgradeTask -currentTask ([ref]$UpgradeTask.Name) -scriptBlock $UpgradeTask.Value if($UpgradeTask.Name.Status -eq 'Failed') { Write-Host -ForegroundColor DarkCyan "------------------------------------NOTE-------------------------------------------------" Write-Host -ForegroundColor DarkCyan "The development license is loaded into the $DatabaseName database." Write-Host -ForegroundColor DarkCyan "Customer license saved to $CustomerLicenseBackup" Write-Host -ForegroundColor DarkCyan "-----------------------------------------------------------------------------------------" Write-Host -ForegroundColor Red "-----------------------------------------------------------------------------------------" Write-Host -ForegroundColor Red "The upgrade completed with errors." Write-Host -ForegroundColor Red "-----------------------------------------------------------------------------------------" return $UpgradeTasks.Keys } } Write-Host -ForegroundColor Green "-----------------------------------------------------------------------------------------" Write-Host -ForegroundColor Green "The upgrade completed successfully." Write-Host -ForegroundColor Green "You can start the Microsoft Dynamics Business Central Windows client on the upgraded database using $NavServerInstance." Write-Host -ForegroundColor Green "-----------------------------------------------------------------------------------------" if ($DevLicense -and !$CustLicense) { Write-Host -ForegroundColor DarkCyan "------------------------------------NOTE-------------------------------------------------" Write-Host -ForegroundColor DarkCyan "The development license is loaded into the $DatabaseName database." Write-Host -ForegroundColor DarkCyan "-----------------------------------------------------------------------------------------" } return $UpgradeTasks.Keys } END { Write-Verbose "=========================================================================================" Write-Verbose ("Upgrade script finished at " + (Get-Date).ToLongTimeString() + ".") Write-Verbose "=========================================================================================" } } function Setup-UpgradeTask([string]$TaskName,[scriptblock]$ScriptBlock) { $initTaskStatistics = New-Object PSObject Add-Member -InputObject $initTaskStatistics -MemberType NoteProperty -Name "Upgrade Task" -Value $TaskName Add-Member -InputObject $initTaskStatistics -MemberType NoteProperty -Name "Start Time" -Value "" Add-Member -InputObject $initTaskStatistics -MemberType NoteProperty -Name "Duration" -Value "" Add-Member -InputObject $initTaskStatistics -MemberType NoteProperty -Name "Status" -Value 'NotStarted' Add-Member -InputObject $initTaskStatistics -MemberType NoteProperty -Name "Error" -Value "" $taskContent = New-Object PSObject Add-Member -InputObject $taskContent -MemberType NoteProperty -Name "Statistics" -Value $initTaskStatistics Add-Member -InputObject $taskContent -MemberType NoteProperty -name "ScriptBlock" -Value $ScriptBlock return $taskContent } function Execute-UpgradeTask([PSObject][ref]$currentTask, [scriptblock]$scriptBlock) { Write-Host "Running Upgrade Task `"$($currentTask.'Upgrade Task')`"..." $startTime = Get-Date $currentTask.'Start Time' = $startTime.ToLongTimeString() try { . $scriptBlock | Out-Null $currentTask."Status" = 'Completed' } catch [Exception] { $currentTask."Status" = 'Failed' $currentTask."Error" = $_.Exception.Message + [Environment]::NewLine + "Script stack trace: " + [Environment]::NewLine + $_.ScriptStackTrace } finally { $duration = NEW-TIMESPAN -Start $startTime $durationFormat = '{0:00}:{1:00}:{2:00}.{3:000}' -f $duration.Hours, $duration.Minutes, $duration.Seconds, $duration.Milliseconds $currentTask.'Duration' = $durationFormat } } |