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.
    Plugins can be placed in either root folder (ReleaseFolder) or in dot-folders (ReleaseSubFolder).
 
    Supported subfolders:
    * Extensions - should contain *.app files which will be automatically installed/upgraded on the instance
    * .name - folders started with the dot will be proposed as value for ReleaseSubFolder parameter, there might be several such folders but only one should be selected for installation
.PARAMETER ServerInstance
    Specifies the Microsoft Dynaimcs Business Central Server instance that is used during upgrade
.PARAMETER ReleaseFolder
    Path to folder where all release data stored.
    There must be a zip file in this folder named like "XXX_1_NNNNNNN.zip". This file will be theated like a main patch and will be instlled in process.
.PARAMETER ReleaseSubFolder
    The name of subfolder in release folder which will be installe along with the release. By default this folder's name should start with the dot,
    then it will be hadnled by autocompletion. But you can provide any subfolder name manually.
    There must be a zip file in this folder named like "XXX_2_NNNNNNN.zip". This file will be theated like a supplimentary patch and will be instlled after a main patch in process.
.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 RapidStartPackageFile
    Specifies the path to the RapidStart package to be imported after the successfull upgrade
.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" -ReleaseFolder "c:\Release2021.01" -ReleaseSubFolder ".Customer01"
#>

function Install-Release
{
    [alias("inrls")]
    [CmdletBinding()]
    param(
        [parameter(Mandatory=$true)]
        [ArgumentCompleter({ Import-NavModule -Admin; Get-NAVServerInstance | % { $_.ServerInstance -replace ".*\$", "" } })]
        [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,
        [switch]$Offline,
        [switch]$Force
    )
    #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) {
        if (!$Force) {
            $decision = $Host.UI.PromptForChoice('Warning! Primary patch not found!', "There is no patch in $ReleaseFolder. Are you sure you want to proceed and install only IML patch?", @('&Yes'; '&No'), 1)
            if ($decision -eq 1) {
                return
            }
        } else {
            Write-Warning "Primary patch not found under $ReleaseFolder. Execution forced without primary patch."
        }
        $PrimaryPatch = Join-Path $ReleaseFolder "\" # that trailing slash is important for GetDirectoryName
    } else {
        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 $(Join-Path $ReleaseFolder $ReleaseSubFolder)"
        }
        Write-Verbose "Secondary patch > $SecondaryPatch"
    }

    try
    {
        ($UpgradeTasksStatistics = Start-Upgrade `
            -NAVServerInstance $ServerInstance `
            -PrimaryPatch $PrimaryPatch `
            -SecondaryPatch $SecondaryPatch `
            -DevLicense $DevLicense `
            -CustLicense $CustLicense `
            -RapidStartPackageFile $RapidStartPackageFile `
            -NoSleep:$NoSleep `
            -offline:$offline `
        ) |  %{ 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,
            [string]$SecondaryPatch,
            [string]$RapidStartPackageFile = "",
            [string]$DevLicense = "",
            [string]$CustLicense = "",
            [switch]$NoSleep,
            [switch]$offline
        )
    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
        $AdditionalInstances = Get-AdjacentInstances $NavServerInstance

        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 {

                    $ProgressPreferenceBack = $ProgressPreference; $ProgressPreference = "SilentlyContinue"
                    Get-NAVTenant -ServerInstance $NavServerInstance | Sync-NAVTenant -ServerInstance $NavServerInstance -Mode Sync -force -ErrorAction Stop
                    $ProgressPreference = $ProgressPreferenceBack;

                 } | %{ $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

        #Region Shutdown additional instances
        if ($AdditionalInstances) {
            . Setup-UpgradeTask `
                -TaskName "Stop additional instances" `
                -ScriptBlock {

                    $AdditionalInstances | % { Write-Verbose "Stopping instance $_" }
                    $AdditionalInstances | Stop-NAVServerInstance -ErrorAction Stop | 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
        if ($PrimaryPatch -and (Test-Path $PrimaryPatch -PathType Leaf)) {
            . Setup-UpgradeTask `
                -TaskName "Importing update objects" `
                -ScriptBlock {

                    if(!$NoSleep) { Start-Sleep -Seconds 60 }
                    Install-Patch -instanceName $NavServerInstance -path $PrimaryPatch -allowReinstall -allowRelease -noSleep:$NoSleep -offline:$offline

                } | % { $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 -allowRelease -noSleep:$NoSleep -offline:$offline

                } | % { $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 }
                    $ProgressPreferenceBack = $ProgressPreference; $ProgressPreference = "SilentlyContinue"
                    Get-NAVTenant -ServerInstance $NavServerInstance | Sync-NAVTenant -ServerInstance $NavServerInstance -Mode Sync -force -ErrorAction Stop
                    $ProgressPreference = $ProgressPreferenceBack;

                } | % { $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 }
                    $ProgressPreferenceBack = $ProgressPreference; $ProgressPreference = "SilentlyContinue"
                    Get-NAVTenant -ServerInstance $NavServerInstance | Sync-NAVTenant -ServerInstance $NavServerInstance -Mode Force -force -ErrorAction Stop
                    $ProgressPreference = $ProgressPreferenceBack;

                } | % { $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 (Test-Path $extensionFolder -PathType Container ) {
            . Setup-UpgradeTask `
                -TaskName "Upgrading extensions" `
                -ScriptBlock {

                        Upgrade-Extension -instanceName $NavServerInstance -path $extensionFolder -offline:$offline

                    } | % { $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

        #Region AdditionalServiceTierStart
        if ($AdditionalInstances) {
            . Setup-UpgradeTask `
                -TaskName "Start additional instances" `
                -ScriptBlock {

                            $AdditionalInstances | Write-Verbose
                            $AdditionalInstances | Start-NAVServerInstance -Force -ErrorAction Continue

                    } | % { $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
    }
}

Export-ModuleMember -Alias "insrls" -Function "Install-Release"