Functions/AppManagement/Install-BcSingleApp.ps1
<#
.Synopsis Installs a single Microsoft Dynamics 365 Business Central AL Extension on a Busines Central environment. .Description This script can install an AL extension Installation: o Publish-NavApp o Determine the next installation procedure o Uninstall-NavApp (previous version) o Sync-NavApp o Start-NavAppDataUpgrade or Install-NavApp o Set application version in database (if the app is a base app). o Unpublish-NavApp (previous app) o Restarts BC server tier. .Example Install-BcSingleApp -ServerInstance 'BC210Test' AppFilePath 'c:\MyExtension.app' -tenant 'default' .Example $installAppArgs = @{ 'ServerInstance' = $ServerInstance 'AppFilePath' = 'c:\MyExtension.app' 'Tenant' = 'default' 'AppScope' = 'Global' 'PublishOnly' = $true 'SkipVerification' = $SkipVerification } Install-BcSingleApp @installAppArgs #> function Install-BcSingleApp { param( [string] $ServerInstance, [string] $AppFilePath, [string] $Tenant, [ValidateSet('Global', 'Tenant')] [string] $AppScope, [switch] $PublishOnly, [switch] $IsBaseApp, [switch] $KeepPreviousApp, [switch] $SkipVerification ) $newApp = Get-NAVAppInfo -Path $AppFilePath $newApp | Add-Member -NotePropertyName 'Guid' ` -NotePropertyValue $newApp.AppId.Value.Guid Write-Title ('Installation: {0} v{1}' -f $newApp.Name, $newApp.Version.Tostring()) # Start tracking installation time. $appDeployStartTime = "Installation started on tenant {0}" -f $Tenant | Write-StartTask 'Location app file: {0}' -f $AppFilePath | Write-Host # Set variables for the installation. $skipPublish = $false $skipUnInstall = $false $skipSync = $false $skipDataUpgrade = $false $skipInstall = $false $skipUnpublish = $false # Set hashtable installReport to keep track off the duration of # the installation steps and to write a report after the app installation. $report = @{ Publish = @{ Title = 'Publishing new app'} UnInstall = @{ Title = 'Uninstalling previous app' } TableSync = @{ Title = 'Table Synchronisation new app' } DataUpgrade = @{ Title = 'Dataupgrade and installation new app'} Install = @{ Title = 'Installation new app'} Unpublish = @{ Title = 'Unpublishing previous app'} } # Set forground color for Write-Host $c = @{ 'ForegroundColor' = 'Cyan' } # Check for previous version of the app. 'Scan for earlier published apps for app {0} in tenant {1}.' -f $newApp.Name, $Tenant | Write-Host @c $currentApp = Get-NAVAppInfo ` -ServerInstance $ServerInstance ` -Id $newApp.Guid ` -Tenant $Tenant ` -TenantSpecificProperties $currentApp | Add-Member -NotePropertyName 'Guid' ` -NotePropertyValue $currentApp.AppId.Value.Guid # Validate if supplied app file contains a newer app than the current published apps. foreach($app in $currentApp){ if ($app.Version -gt $newApp.Version){ $msg = "The supplied app with version {0} is lower " $msg += "than the already deployed app with version {1}.`n " $msg += 'Skipping installation for app {2}.' $msg = $msg -f $app.Version.Tostring(), $newApp.Version.Tostring(), $app.Name if($IsBaseApp){ throw $msg } Write-Warning $msg return } } # If multiple versions of the same app are published, select one as current app. if($currentApp -is [array] -and $currentApp.Count -gt 1){ 'Multiple ({0}) versions of app {1} are already published.' -f $currentApp.Count, $newApp.Name | Write-Host $installedApp = $currentApp | Where-Object -Property IsInstalled -eq $true $dataVersionMatch = $currentApp | Where-Object Version -eq $currentApp[0].ExtensionDataVersion # Select the app that is currently installed if($installedApp){ $currentApp = $installedApp } # Select the version that correspond with the current tenant data version as current app. elseif($dataVersionMatch){ $currentApp = $dataVersionMatch } # If neither are present, select the oldest version of the app else{ $currentApp = ($currentApp | Sort-Object -Property Version)[0] } } if(-not $currentApp){ $msg = 'SkipUnpublish is set to true because there are no other ' $msg + 'published versions found for app {0}.' -f $newApp.Name | Write-Host $skipUnpublish = $true } else { # Write current app details to host. 'Previous version of the app found: {0} - {1}' -f $currentApp.Name, $currentApp.Version.ToString() | Write-Host # Skip publish if the supplied app is already published. if ($newApp.Version -eq $currentApp.Version){ $msg = 'SkipPublish and SkipUnpublish are set to true because the version ' $msg + '{0} of the supplied app is equal to the app in the database.' -f $newApp.Version.ToString() | Write-Host $skipPublish = $true $skipUnpublish = $true } } if($KeepPreviousApp){ $msg = 'SkipUnpublish is set to true, because the switch KeepPreviousApp ' $msg + 'is enabled.' | Write-Host $skipUnpublish = $true } # Get app scope if not set. if ($currentApp) { $AppScope = $currentApp.Scope 'The extension scope is set to the same level as the previous app: {0}' -f $currentApp.Scope | Write-Host } elseif (-not $AppScope) { 'The extension scope is set to the default value: Tenant' | Write-Host $AppScope = 'Tenant' } else { 'Appscope is set as installation parameter: {0}' -f $AppScope | Write-Host } ### END *** App file installation initialization *** ### START *** App file installation *** #-------------------------------Publish App--------------------------------- $null = Write-StartTask 'Publishing extension.' $report.Publish['StartTime'] = Get-Date if($skipPublish){ 'Skipped Publish-NAVApp.' | Write-Host } else { 'Publishing {0} App, Guid: {1}, version {2} on server instance {3}.' -f $newApp.Name, $newApp.Guid, $newApp.Version.ToString(), $ServerInstance | Write-Host $message = "Publish-NAVApp -ServerInstance '{0}' -Path '{1}' -Scope '{2}' -SkipVerification:{3}" -f $ServerInstance, $AppFilePath, $AppScope, $skipVerification $arguments = @{ 'ServerInstance' = $ServerInstance 'Path' = $AppFilePath 'Scope' = $AppScope 'SkipVerification' = $skipVerification } if($AppScope -eq 'Tenant'){ $arguments += @{ 'Tenant' = $Tenant } $message += " -Tenant '{0}'" -f $Tenant } $message | Write-Host -ForegroundColor Gray Publish-NavApp @arguments } $report.Publish['Duration'] = (Get-Date) - $report.Publish.StartTime #--------------------------Calculate next steps----------------------------- $null = Write-StartTask 'Determine which next installation steps are required.' # Update $newApp with tenant specific installation status. $newApp = Get-NAVAppInfo ` -ServerInstance $ServerInstance ` -Id $newApp.Guid ` -Version $newApp.Version ` -Tenant $Tenant ` -TenantSpecificProperties # Check if Sync-NAVApp should be executed. if($newApp.SyncState -eq 'Synced'){ 'SkipSync is set to true, table definition is already in sync with SQL tables.' | Write-Host @c $skipSync = $true } else { 'Sync-NAVApp should be executed. Syncstate is {0}' -f $newApp.SyncState | Write-host @c } # Check if Start-NAVAppDataUpgrade should be executed. if($newApp.ExtensionDataVersion -eq $newApp.Version){ 'SkipDataUpgrade is set to true, extensions data already upgraded.' | Write-Host @c $skipDataUpgrade = $true } elseif([string]::IsNullOrEmpty($newApp.ExtensionDataVersion)){ 'SkipDataUpgrade is set to true, extensionDataVersion is empty.' | Write-Host @c ' It is the first time this extension is installed.' | Write-Host @c $skipDataUpgrade = $true } else { 'Start-NAVAppDataUpgrade should be executed.' | Write-Host @c ' Extension dataversion in tenant {0} is {1} and should be upgraded to {2}.' -f $Tenant, $newApp.ExtensionDataVersion.ToString(), $newApp.Version.ToString() | Write-host @c } # Check if there is a previous app that should be uninstalled. if([string]::IsNullOrEmpty($currentApp)){ 'SkipUnInstall is set to true, no previous extension found.' | Write-Host @c $skipUnInstall = $true } elseif ($currentApp.IsInstalled -eq $false) { 'SkipUnInstall is set to true, previous extension is not installed.' | Write-Host @c $skipUnInstall = $true } elseif($currentApp.IsInstalled -eq $true -and $skipSync -eq $true -and $skipDataUpgrade -eq $true){ 'SkipUnInstall is set to true, there is no table sync or dataupgrade planned for the extension.' | Write-Host @c $skipUnInstall = $true } else { 'Previous app will be uninstalled due to a pending table sync or dataupgrade.' | Write-Host @c } # Check if Install-NavApp should be executed. # Only use Install-NavApp if the new app has not been installed before or if the app is already in sync and upgraded. if($newApp.IsInstalled -eq $true){ 'SkipInstall is set to true, extension is already installed.' | Write-Host @c $skipInstall = $true } elseif($skipDataUpgrade -eq $false){ 'SkipInstall is set to true, Extension will be installed during the dataupgrade.' | Write-Host @c $skipInstall = $true } # When publish only is enabled the sync, dataupgrade and installation should be skipped. if($PublishOnly){ 'skipSync, skipDataUpgrade and skipInstall are set to false because PublishOnly is enabled.' | Write-Host @c $skipSync = $true $skipDataUpgrade = $true $skipInstall = $true } # Write next steps to host $msg = @() $msg += ' skipUnInstall = {0}' -f $skipUnInstall $msg += ' skipSync = {0}' -f $skipSync $msg += ' skipDataUpgrade = {0}' -f $skipDataUpgrade $msg += ' skipInstall = {0}' -f $skipInstall $msg += ' skipUnpublish = {0}' -f $skipUnpublish $msg | Write-Host #------------------------Uninstalling previous app-------------------------- $null = Write-StartTask 'Uninstalling previous app.' $report.Uninstall['StartTime'] = Get-Date # Uninstall current app if applicable. Uninstall app is only required in some cases of table or data sync. if($skipUnInstall){ 'Skipped Uninstall-NAVApp.' | Write-Host } else { 'Uninstalling {0} App, version {1}.' -f $currentApp.Name, $currentApp.Version.ToString() | Write-Host "Uninstall-NAVApp -ServerInstance '{0}' -Name '{1}' -Version '{2}' -Tenant '{3}' -Force" -f $ServerInstance, $currentApp.Name, $currentApp.Version, $Tenant | Write-Host -ForegroundColor Gray Uninstall-NAVApp ` -ServerInstance $ServerInstance ` -Name $currentApp.Name ` -Version $currentApp.Version ` -Tenant $Tenant ` -Force } $report.Uninstall['Duration'] = (Get-Date) - $report.Uninstall.StartTime #-------------------------------Table sync---------------------------------- $null = Write-StartTask 'Syncing database schema for new app.' $report.TableSync['StartTime'] = Get-Date if($skipSync){ 'Skipped Sync-NAVApp.' | Write-Host } else { 'Execute sync {0} App on server instance {1}' -f $newApp.Name, $ServerInstance | Write-Host "Sync-NAVApp -ServerInstance '{0}' -Name '{1}' -Version '{2}' -Mode '{3}' -Tenant '{4}'" -f $ServerInstance, $newApp.Name, $newApp.Version.ToString(), 'Add', $Tenant | Write-Host -ForegroundColor Gray Sync-NAVApp ` -ServerInstance $ServerInstance ` -Name $newApp.Name ` -Version $newApp.Version ` -Mode Add ` -Tenant $Tenant } $report.TableSync['Duration'] = (Get-Date) - $report.TableSync.StartTime #------------------------------Data Upgrade--------------------------------- $null = Write-StartTask 'Execute data upgrade for new app.' $report.DataUpgrade['StartTime'] = Get-Date if($skipDataUpgrade){ 'Skipped Start-NAVAppDataUpgrade.' | Write-Host } else { 'Execute data upgrade for {0} App' -f $newApp.Name | Write-Host 'Note: Install-NAVApp is included in Start-NAVAppDataUpgrade' | Write-Host "Start-NAVAppDataUpgrade -ServerInstance '{0}' -Name '{1}' -Version '{2}' -Tenant '{3}'" -f $ServerInstance, $newApp.Name, $newApp.Version.ToString(), $Tenant | Write-Host -ForegroundColor Gray Start-NAVAppDataUpgrade ` -ServerInstance $ServerInstance ` -Name $newApp.Name ` -Version $newApp.Version ` -Tenant $Tenant } $report.DataUpgrade['Duration'] = (Get-Date) - $report.DataUpgrade.StartTime #-------------------------------Install App--------------------------------- $null = Write-StartTask 'Installing app if not yet installed.' $report.Install['StartTime'] = Get-Date if($skipInstall){ 'Skipped Install-NAVApp.' | Write-Host } else { 'Installing {0} App' -f $newApp.Name | Write-Host "Install-NAVApp -ServerInstance '{0}' -Name '{1}' -Version '{2}' -Tenant '{3}' -Force" -f $ServerInstance, $newApp.Name, $newApp.Version.ToString(), $Tenant | Write-Host -ForegroundColor Gray Install-NAVApp ` -ServerInstance $ServerInstance ` -Name $newApp.Name ` -Version $newApp.Version ` -Tenant $Tenant ` -Force } $report.Install['Duration'] = (Get-Date) - $report.Install.StartTime #--------------------------Set base app version----------------------------- if ($AppIsBaseApp){ $null = Write-StartTask 'Updating the application version in the database for base app.' $BaseAppStatus = Get-NavAppInfo -ServerInstance $ServerInstance -Tenant $Tenant -TenantSpecificProperties $BaseAppStatus = $BaseAppStatus | Where-Object { $_.AppId.Value.Guid -eq $newApp.Guid -and $_.Version -eq $newApp.Version } $BaseAppStatus | Select-Object -Property AppId, Name, Version, SyncState, NeedsUpgrade, IsInstalled | Write-host # Only set higher application version if installation of the baseapp is successful. if($BaseAppStatus.IsInstalled -eq $false){ $message = 'Updating the base app {0} version {1} failed. Installation canceled.' -f $newApp.Name, $newApp.Version throw $message } # Update the ApplicationVersion in the application database (the application version displayed in the webclient) if($newApp.Version.Major -ge '17'){ [string] $nAppVersion = '{0}.{1}.0.0' -f $newApp.Version.Major, $newApp.Version.Minor } else { [string] $nAppVersion = $newApp.Version } $ApplicationVersion = (Get-NAVApplication -ServerInstance $ServerInstance).ApplicationVersion if ([version] $nAppVersion -gt [version] $ApplicationVersion){ 'Update {0} app version number from {1} to {2} in the application database.' -f $newApp.Name, $ApplicationVersion, $nAppVersion | Write-Host Set-NAVApplication ` -ServerInstance $ServerInstance ` -ApplicationVersion $nAppVersion ` -Force Invoke-BcDataUpgrade -ServerInstance $ServerInstance } else { 'ApplicationVersion is already {0}.' -f $ApplicationVersion | Write-Host } } #-------------------------Unpublish previous app---------------------------- $null = Write-StartTask 'Unpublishing previous app version.' $report.Unpublish['StartTime'] = Get-Date # Unpublish the previous app if it is not installed in any tenant anymore. if ($skipUnpublish) { 'Skipped Unpublish-NAVApp.' | Write-Host } else { $appUsedBy = Get-NAVAppTenant -ServerInstance $ServerInstance ` -Id $currentApp.AppId.Value ` -Version $currentApp.Version if($appUsedBy){ 'Previous app {0} version {1} is not unpublished because it is still in use by another tenant.' -f $currentApp.Name, $currentApp.Version.ToString() | Write-Host } else { 'Unpublishing {0} App, version {1}' -f $currentApp.Name, $currentApp.Version | Write-Host "Unpublish-NAVApp -ServerInstance '{0}' -Name '{1}' -Version '{2}'" -f $ServerInstance, $currentApp.Name, $currentApp.Version.ToString() | Write-Host -ForegroundColor Gray Unpublish-NAVApp ` -ServerInstance $ServerInstance ` -Name $currentApp.Name ` -Version $currentApp.Version } } $report.Unpublish['Duration'] = [DateTime] (Get-Date) - $report.Unpublish.StartTime # Write app instalation report to host 'Total deploy time for app {0} on tenant {1} is {2} seconds.' -f $newApp.Name, $Tenant, ((Get-Date) - $appDeployStartTime).Seconds | Write-Host foreach($key in $report.Keys){ ' {0} seconds for {1}' -f $report.$key.Duration.Seconds, $report.$key.title | Write-Host } Write-EndTask $appDeployStartTime } Export-ModuleMember -Function Install-BcSingleApp function Invoke-BcDataUpgrade { param( [string] $ServerInstance, [string[]] $Tenants ) if(-not $Tenants){ # The dataupgrade needs to be executed for each tenant. # If not the other tenant state becomes 'OperationalDataUpgradePending' $tenants = (Get-NAVTenant -ServerInstance $ServerInstance).Id } foreach ($tenant in $tenants){ 'Run the data upgrade for tenant {0} on ServerInstance {1}.' -f $tenant, $ServerInstance | Write-Host ' Sync-NAVTenant in Sync mode' | Write-Host Sync-NAVTenant ` -ServerInstance $ServerInstance ` -Mode Sync ` -Tenant $tenant ` -Force ' Start-NAVDataUpgrade' | Write-Host $dataUpgradeParams = @{ ServerInstance = $ServerInstance Tenant = $tenant FunctionExecutionMode = 'Serial' SkipUserSessionCheck = $true SkipAppVersionCheck = $true Force = $true } if (([version] $AppVersion).Major -lt '17'){ $dataUpgradeParams["SkipCompanyInitialization"] = $true } Start-NAVDataUpgrade @dataUpgradeParams ' Get-NAVDataUpgrade' | Write-Host $dataUpgradeState = 'InProgress' while ($DataUpgradeState -eq 'InProgress'){ ' Dataupgrade status: {0}' -f $dataUpgradeState | Write-Host $dataUpgradeState = (Get-NAVDataUpgrade -ServerInstance $ServerInstance -Tenant $tenant).state Start-Sleep -Seconds 3 } ' Dataupgrade status: {0}' -f $dataUpgradeState | Write-Host } } |