Functions/Install-BcApp.ps1
<#
.Synopsis Installs Microsoft Dynamics 365 Business Central AL Extension(s) on a Busines Central environment. .Description This script can install AL extension(s). It's compatible with single- and multi-tenant installations. The script is executed in two phases: Installation initialization and the installation itself. Initialization: - Creates a log file. - Imports required PowerShell modules. - Starts the server instance if not started yet. - Sorts the to-install apps in the right installation order. - Reads required parameters from the BC server instance. - Gathers and displays general environment details. - Displays apps to-install and current published apps. - Updates Business Central software license. - Validates environment preconditions. - Asks user confirmation to start installation. Installation: - Starts installation for each app in the right installation order. - Deploys the app in every tenant unless other specified. 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) - Restarts BC server tier. .Example Install-BcApp -ServerInstance 'BC150Test' -SoftwarePath (Join-Path $PSScriptRoot 'Extensions') -ModulePath (Join-Path $PSScriptRoot 'PowerShell') .Example Install-BcApp -ServerInstance 'BC150Test' -SoftwarePath 'D:\Folder\Software' -ModulePath 'D:\PowerShell\Modules' .Example Install-BcApp ` -ServerInstance 'BC150Test' ` -Tenant 'tenant1' ` -SoftwarePath (Join-Path $PSScriptRoot 'Extensions') ` -ModulePath (Join-Path $PSScriptRoot 'PowerShell') ` -BaseAppId @('d809cdc6-44fd-485c-b9ed-2841276bbf32', 'a5fcb2f3-fce3-47a5-976a-e837599ae46d') ` -ErrorAction Continue ` -SkipVerification # This will deploy the app files from the folder Extensions relative to the script root path. # The tenant is specified. This indicates the environment is Multi-Tenant AND the app(s) should ONLY be deployed in tenant1. # The BaseAppId is changed to the BaseAppId of 4PS Construct W1 and 4PS Construct NL, the 4PS base apps. #> function Install-BcApp { [CmdletBinding()] Param( # Specifies the name of a Business Central Server Instance. E.g. 'BC150' or 'NST150Test' [Parameter(Mandatory=$true)] [string] $ServerInstance, # Path to the folder that contains the app file(s) to install. [Parameter(Mandatory=$true)] [string] $SoftwarePath, # Path to the PowerShell deployment module folder. e.g. c:\powershell\modules. # In this folder should the modules be pressent: # 'FpsDeployment\FpsBcDeployment.psd1' # 'FpsGeneral\FpsGeneral.psd1' [string] $ModulePath, # The customers Business Central software license file. E.g. 'C:\License\CustomerLicense.flf' # Important note for multi-tenant environments: When there are multiple tenants mounted on the serverinstance # and with the $Tenants parameter there are zero, two or more tenants specified; the license will be installed in # multiple tenants. If every tenant has it's own license, don't use this functionality. Upload it seperately. [string] $LicensePath, # Tenant parameter can only be used with a Business Central multi tenant installation. # If no tenant is specified and the environment is multi-tenant the app will be published in all tenants. # E.g. 'tenant1' or @('tenant1', 'tenant2') to specify multiple tenants. [string[]] $Tenants, # Use $AppScope to set the default scope for new installed apps. # If there is a previous version of an app installed the scope of this app will be used. # Global: The extension is published for all tenants on the server. # Tenant: The extension is published into the per-tenant scope. [string] $AppScope, # For Microsoft Partners with their own base app, supply the BaseAppId. Default value is the Microsoft Base App. [string[]] $BaseAppId = @('437dbf0e-84ff-417a-965d-ed2bb9650972'), # Location to write the logfile to. E.g. 'C:\BcInstallation\Log' # Default location is '?:\ProgramData\4ps\bcdeployment'. [string] $LogFilePath = (Join-Path -Path $env:ProgramData -ChildPath '4ps\bcdeployment'), # When an extension is updated all dependend extensions will be uninstalled during the upgrade. # Default the dependend apps will be reïnstalled after the installation completed. # With $SkipAutoInstallChildApps on $true the dependend apps will not be reïnstalled. [switch] $SkipAutoInstallChildApps, # For every 1 MB appfile (runtime) size roughtly 35 MB free memory is required for the serverinstance to compile the app. # For every 1 MB appfile (full) seze roughly 180 MB free memory is required. # This scripts validates if there is enough free memory to compile the largest to-install app. # This validation can be disabled by setting $SkipMemoryCheck to $true. [switch] $SkipMemoryCheck, # Default after the script initialisation the user is asked to continue or not. # This question can be disabled by setting $SkipConfirmation to $true. [switch] $SkipConfirmation, # Forces the deployment to run without verifying the authenticode signature. # Not recommendend in production environments. [switch] $SkipVerification, # Default the Business Central Service is restarted after installation. # To disable the service restart after the installation set $SkipServiceRestart to $true. [switch] $SkipServiceRestart, # Default the previous app will be unpublish after a succesfull installation. # To keep the previous app published in the database set $KeepPreviousApp to $true. [switch] $KeepPreviousApp, # Forces the installation of the license file in all tenants. [switch] $ForceLicense ) " ____ _____ _____ _ _ | _ \ / ____| | __ \ | | | | | |_) | | | | | | ___ _ __ | | ___ _ _ _ __ ___ ___ _ __ | |_ | _ <| | | | | |/ _ \ '_ \| |/ _ \| | | | '_ `` _ \ / _ \ '_ \| __| | |_) | |____ | |__| | __/ |_) | | (_) | |_| | | | | | | __/ | | | |_ |____/ \_____| |_____/ \___| .__/|_|\___/ \__, |_| |_| |_|\___|_| |_|\__| | | __/ | 4PS v{0} |_| |___/ " -f $MyInvocation.MyCommand.Module.Version | Write-Host # Validate PowerShell version if (-not $PSVersionTable.PSVersion.Major -ge '5') { $Message = 'Powershell 5.0 or higher required to continue. Current version is {0}.{1}' -f ` $PSVersionTable.PSVersion.Major, $PSVersionTable.PSVersion.Minor throw $Message } 'PowerShell version {0} is compatible.' -f $PSVersionTable.PSVersion.ToString() | Write-Host # Validate if PowerShell is started as administrator $WindowsIdentity = ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()) $Elevated = ($WindowsIdentity.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) if (-not $Elevated) { $Message = 'You need administrative privileges to deploy Business Central extensions. ' $Message += 'Start the script in a Powershell session launched as administrator.' throw $Message } 'Script is executed as administrator.' | Write-Host # Validate if script is executed in a 64 bit process. if(-not [Environment]::Is64BitProcess){ $Message = 'This script needs to be executed as a 64 bit process. Current process is x86 (32bit).' throw $Message } 'Session is 64 bit.' | Write-Host try{ Stop-Transcript }catch{} # Create logfile $LogFile = Join-Path -Path $LogFilePath -ChildPath ('{0}_{1}.log' -f (Get-Date).ToString('yyyy-MM-dd_HH.mm.ss'), $ServerInstance) New-Item -Path $LogFile -ItemType File -Force | Out-Null # Remove old logfiles (keep max 10 logfiles per ServerInstance) $Regex = '.*_{0}\.log' -f $ServerInstance $LogFiles = Get-ChildItem $LogFilePath -File -Filter '*.log' | Where-Object -Property Name -Match $Regex if($LogFiles.Count -gt 10){ $Exclude = $LogFiles | Sort-Object -Property CreationTime -Descending | Select-Object -First 10 Get-ChildItem (Split-Path $LogFile -Parent) -Exclude $Exclude | Remove-Item -Recurse -Force -ErrorAction SilentlyContinue } Start-Transcript -path $LogFile -append ### Import required modules 'Importing required PowerShell modules...' | Write-host if($ModulePath){ $GeneralModulePath = (Join-Path $ModulePath 'FpsGeneral\FpsGeneral.psd1') Import-Module $GeneralModulePath -DisableNameChecking -Global -Force } elseif(-not (Get-Module FpsBcDeployment) -or -not (Get-Module FpsGeneral)){ $Message = 'Please import PowerShell module FpsBcDeployment and FpsGeneral before executing this cmdlet, or supply the path to the module.' throw $Message } # Validate if the NST exists Check-ServiceNotExistsError -ServerInstance $ServerInstance -ErrorAction Stop # Import Business Central Modules Import-BcModule ` -ServerInstance $ServerInstance ` -ManagementModule ` -AppManagementModule ` -Force $TotalTime = Write-StartProcessLine -StartLogText 'Microsoft Dynamics 365 Business Central AL Extension(s) deployment on a Busines Central environment.' $TaskStartTime = Write-StartProcessLine -StartLogText 'Validating Business Central server instance state.' # Validate if NST is running if((Get-NAVServerInstance -ServerInstance $ServerInstance).State -ne 'Running'){ 'ServerInstance {0} is not running. Starting service...' -f $ServerInstance | Write-Host Set-NAVServerInstance -ServerInstance $ServerInstance -Start -ErrorAction Stop } else { 'ServerInstance {0} is running.' -f $ServerInstance | Write-Host } Wait-BcServerInstanceMountingTenants ` -ServerInstance $ServerInstance Write-EndProcessLine -TaskStartTime $TaskStartTime ### Get installation details $TaskStartTime = Write-StartProcessLine -StartLogText 'Retreiving installation details.' # Get system info $OperatingSystem = Get-WmiObject -Class Win32_OperatingSystem $TotalMemory = [math]::round($OperatingSystem.TotalVisibleMemorySize/1024/1024) $FreeMemory = [math]::round(($OperatingSystem.FreePhysicalMemory/1024/1024), 1) $Processor = Get-WmiObject Win32_Processor $OsInfo = @("`n") $OsInfo += 'Operating System : {0} {1}' -f $OperatingSystem.Caption, $OperatingSystem.Version $OsInfo += 'PowerShell Version : {0}' -f $PSVersionTable.PSVersion.ToString() $OsInfo += 'Total Memory : {0} GB' -f $TotalMemory $OsInfo += 'Free Memory : {0} GB' -f $FreeMemory $OsInfo += 'CPU Name : {0}' -f $Processor.Name $OsInfo += 'CPU Cores : {0}' -f $Processor.NumberOfCores $OsInfo += 'CPU Threads : {0}' -f $Processor.NumberOfLogicalProcessors $OsInfo += 'CPU MaxClockSpeed : {0}' -f $Processor.MaxClockSpeed $OsInfo += "`n" # Get appfiles in right installation order $Apps = Get-AppDependencyOrder -Path $SoftwarePath -ErrorAction Stop # Get tenants if(-not $Tenants){ $Tenants = (Get-NAVTenant -ServerInstance $ServerInstance).Id } # Write environment installation to host $Title = 'Installation Details' $Body = "`n" $Body += '*** Operating System ***{0}' -f ($OsInfo | Out-String) $Body += '*** Business Central environment ***{0}' -f (Get-BCServerInstance -ServerInstance $ServerInstance | Out-String) $Body += "*** Targeted Tenants ***{0}" -f (Get-NAVTenant -ServerInstance $ServerInstance | ` Select-Object -Property Id, State, AllowAppDatabaseWrite, DatabaseName, DatabaseServer, TenantDataVersion | ` Where-Object -Property Id -in $Tenants | Out-String) $Body += "*** Apps to deploy ***`n{0}`n" -f ($Apps | Select-Object -Property Name, Publisher, Version | Out-String) $Body += "*** Current published apps ***`n" $Tenants | ForEach-Object { $Body += "Apps published in tenant {0}:`n{1}`n" -f $_, (Get-NavAppInfo ` -ServerInstance $ServerInstance ` -Tenant $_ ` -TenantSpecificProperties | Sort-Object -Property Name, Version | ` Select-Object -Property Name, Publisher, IsPublished, SyncState, NeedsUpgrade, IsInstalled, ExtensionDataVersion, Version, AppId | ` Format-Table | Out-String) } $Body += "*** Bound Parameters ***`n{0}" -f ($PSCmdlet.MyInvocation.BoundParameters | Out-String) Write-host (Write-DebugInfoToOutput -MessageTitle $Title -MessageBody $Body) Write-EndProcessLine -TaskStartTime $TaskStartTime ### Validations on preconditions before installation $TaskStartTime = Write-StartProcessLine -StartLogText "Validating environment." # Validate if enough free memory is available. # Publishing large extensions in Business Central consumes a lot of memory, because the whole application is compiled on publishing. if(-not $SkipMemoryCheck){ # Get the filesize of the largest app file $FileSize = ($Apps | Sort-Object -Property Length -Descending | Select-Object -First 1).Length # Calculate minimal free memory in GB. Every 1 MB appfile equals roughly 180 MB free memory, 1 MB equals ~35 MB for runtime apps. $MinimalFreeMemory = [math]::round($FileSize /1MB, 2) * 35 $MinimalFreeMemory = [math]::round($MinimalFreeMemory * 1MB /1GB, 1) if([decimal] $FreeMemory -lt [decimal] $MinimalFreeMemory){ $Message = 'Not enough free memory available. System has {0} GB of the {1} GB memory available. ' -f $FreeMemory, $TotalMemory $Message += 'Atleast {0} GB free memory is required' -f $MinimalFreeMemory throw $Message } '{0} GB free memory is above the minimal threshold of {1} GB.' -f $FreeMemory, $MinimalFreeMemory | Write-Host } # Validate if tenants are valid $MountedTenants = Get-NAVTenant -ServerInstance $ServerInstance $Tenants | ForEach-Object { if($_ -notin $MountedTenants.Id){ $Message = "Supplied tenant '{0}' is not mounted on the ServerInstance '{1}'." -f $_, $ServerInstance throw $Message } $TenantState = ($MountedTenants | Where-Object -property Id -eq $_).State if($TenantState -ne 'Operational'){ $Message = "The tenant '{0}' doesn't have the status 'Operational'. The tenant status is {1}.`n" -f $_, $TenantState $Message += "Please make the tenant operational before starting the deployment." throw $Message } } 'Supplied tenant(s) are mountend and operational on ServerInstance {0}' -f $ServerInstance | Write-Host Write-EndProcessLine -TaskStartTime $TaskStartTime ### Update Business Central Software License $TaskStartTime = Write-StartProcessLine -StartLogText "Updating Business Central Software License." $ImportLicense = $false # Validate if LicensePath is set, the filepath is valid and the file extension is .flf. If all true read the file. if($LicensePath){ if((Test-Path $LicensePath)){ if((Get-Item -Path $LicensePath).Extension -eq '.flf' ){ $NewLicense = Get-LicenseDetails -RawLicense (Get-Content $LicensePath -Raw) $Message = @('') $Message += "*** Supplied license file details ***`n" $Message += 'License Path : {0}' -f $LicensePath $Message += 'Account Number : {0}' -f $NewLicense.AccountNumber $Message += 'Licensed To : {0}' -f $NewLicense.LicensedTo $Message += 'Created Date : {0}' -f $NewLicense.CreatedDate.ToShortDateString() if(-not [string]::IsNullOrEmpty($NewLicense.ExpireDate)){ $Message += 'Expire Date : {0}' -f $NewLicense.ExpireDate.ToShortDateString() } $Message += '' $Message | Write-Host $ElapsedTime = ((Get-Date) - $NewLicense.CreatedDate) if($ElapsedTime.Days -gt 60){ $Message = 'The supplied license is created {0} days ago. It is recommended to use a more recent license.' -f $ElapsedTime.Days Write-Warning $Message } } else { throw 'Supplied license is not a Business Central license. Expected a path to a .flf license file.' } } else { throw ('Could not find the Business Central license file on path: {0}.' -f $LicensePath) } } else { 'Import new license skipped. No path to the Business Central Software License file is set.' | Write-Host } if($Tenants.Count -ge 2 -and (Test-Path Variable:\NewLicense) -eq $true){ $Message = @() $Message += 'Installation is initiated for multiple tenants and ONE license file is specified.' $Message += 'The specified license file will be imported in EVERY tenant database.' $Message += 'If the tenants have separate licenses: Upload them seperatly and do not use the upload license functionalitiy of this cmdlet OR' $Message += 'start the deployment for every tenant seperatly with each their own license file. ' $Message += 'To install the same license in all tenants set the -ForceLicense switch.' if ($ForceLicense){ Write-Warning ($Message | Out-String) } else { throw ($Message | Out-String) } } foreach($Tenant in $Tenants){ # Read the active license from the ServerInstance. $CurrentLicense = Get-LicenseDetails -RawLicense (Export-NAVServerLicenseInformation ` -ServerInstance $ServerInstance ` -Tenant $Tenant) $Message = @('') $Message += "*** Database license file details ***`n" $Message += 'Tenant : {0}' -f $Tenant $Message += 'Account Number : {0}' -f $CurrentLicense.AccountNumber $Message += 'Licensed To : {0}' -f $CurrentLicense.LicensedTo $Message += 'Created Date : {0}' -f $CurrentLicense.CreatedDate.ToShortDateString() if(-not [string]::IsNullOrEmpty($NewLicense.ExpireDate)){ $Message += 'Expire Date : {0}' -f $CurrentLicense.ExpireDate.ToShortDateString() } $Message += '' $Message | Write-Host if(-not $NewLicense){ $ElapsedTime = ((Get-Date) - $CurrentLicense.CreatedDate) if($ElapsedTime.Days -gt 60){ $Message = 'The license in the database is created {0} days ago. It is recommended to use a more recent license.' -f $ElapsedTime.Days Write-Warning $Message } } if($NewLicense){ if($NewLicense.AccountNumber -ne $CurrentLicense.AccountNumber){ $Message = @() $Message += 'Supplied license and license in the database are licensed to a different VOICE accounts.' $Message += 'Supplied license account number: {0}, Licensed To: {1}.' -f $NewLicense.AccountNumber, $NewLicense.LicensedTo $Message += 'License in database account number: {0}, Licensed To: {1}.' -f $CurrentLicense.AccountNumber, $CurrentLicense.LicensedTo if ($ForceLicense){ Write-Warning ($Message | Out-String) } else { throw ($Message | Out-String) } } if($NewLicense.CreatedDate -lt $CurrentLicense.CreatedDate) { $Message = 'Suplied license is older than the active license in the database.' if ($ForceLicense){ Write-Warning $Message } else { throw $Message } } if($NewLicense.CreatedDate -eq $CurrentLicense.CreatedDate) { 'License is the same as the active license in the database.' | Write-Host if (-not $ForceLicense){ 'Import new license skipped.' | Write-Host continue } } if($NewLicense.CreatedDate -gt $CurrentLicense.CreatedDate) { 'Supplied license file is more recent than the active license in the database' | Write-Host } 'Importing license into the database...' | Write-Host $ImportLicense = $true if($Tenant -eq 'default'){ $LicenseDatabase = 'NavDatabase' } else { $LicenseDatabase = 'Tenant' } Import-NAVServerLicense ` -Tenant $Tenant ` -ServerInstance $ServerInstance ` -LicenseFile $LicensePath ` -Database $LicenseDatabase ` -WarningAction SilentlyContinue } # End if $NewLicense } # End foreach tenant if($ImportLicense){ 'Restarting ServerInstance {0} to activate license...' -f $ServerInstance | Write-Host Set-NavServerInstance ` -ServerInstance $ServerInstance ` -Restart Wait-BcServerInstanceMountingTenants ` -ServerInstance $ServerInstance } Write-EndProcessLine -TaskStartTime $TaskStartTime $Message = 'This script does NOT make a SQL backup from the Business Central database. ' $Message += 'Please make sure you have a valid backup before continuing.' Write-Warning -Message $Message if(-not $SkipConfirmation){ 'All validations have passed. Do you want to continue the installation?' | Write-Host -ForegroundColor Green Show-MessageBox ` -MessageType InShell ` -Button OKCancel ` -Message $Title ` -ErrorMsg 'User canceled installation.' ` -Throw } " _____ _ _ _ _ _ |_ _| | | | | | | | (_) | | _ __ ___| |_ __ _| | | __ _| |_ _ ___ _ __ | | | '_ \/ __| __/ _`` | | |/ _`` | __| |/ _ \| '_ \ _| |_| | | \__ \ || (_| | | | (_| | |_| | (_) | | | | |_____|_| |_|___/\__\__,_|_|_|\__,_|\__|_|\___/|_| |_| Starts here " | Write-Host ### Execute installation script $TaskStartTime = Write-StartProcessLine -StartLogText "Installing Business Central Extensions." # Loop through all the app files and install them. Appfiles are already sorted on right installation order. foreach ($AppFilePath in $Apps.Path) { # Set parameters for new app version $NewApp = Get-NAVAppInfo -Path $AppFilePath $AppGuid = $NewApp.AppId.Value.Guid $AppVersion = $NewApp.Version.Tostring() $AppName = $NewApp.Name Write-host (Write-DebugInfoToOutput -MessageTitle ('Installation: {0} v{1}' -f $AppName, $AppVersion)) -ForegroundColor Green # Installation for a base app has additional installation steps compared to a regular extension. if($AppGuid -in $BaseAppId){ 'Extension {0} is marked as BaseApp.' -f $AppName | Write-Host $AppIsBaseApp = $true } else { 'Extension is not marked as BaseApp' | Write-Host $AppIsBaseApp = $false } foreach($Tenant in $Tenants){ $SkipPublish = $false $SkipUnInstall = $false $SkipSync = $false $SkipDataUpgrade = $false $SkipInstall = $false $SkipUnpublish = $false ### START *** App file installation initialization *** $AppDeployStartTime = Write-StartProcessLine -StartLogText ("*** Installation started for app {0} on tenant {1} ***" -f $AppName, $Tenant) $SubTaskStartTime = Write-StartProcessLine -StartLogText ('Scan for earlier published apps for app {0} in tenant {1}' -f $AppName, $Tenant) 'Location app file: {0}' -f $AppFilePath | Write-Host # Set parameters for the current published app if there is already a version of the app installed. $CurrentApp = '' $CurrentAppVersion = '' $CurrentAppName = '' $CurrentApp = Get-NAVAppInfo ` -ServerInstance $ServerInstance ` -Id $AppGuid ` -Tenant $Tenant ` -TenantSpecificProperties $CurrentApp | ForEach-Object { # Validate if supplied app file contains a newer app than the current published apps. if ([version] $_.Version -gt [version] $AppVersion){ $Message = "Version {0} of the current published app is higher or equal to the version {1} of the supplied app file:" -f $_.Version.Tostring(), $AppVersion $Message | Write-Warning } } # If multiple versions of the same app are published, select one as current app. if($CurrentApp.Count -gt 1){ 'Multiple ({0}) versions of app {1} are already published. Selecting appropriate version as previous version.' -f $CurrentApp.Count, $AppName | Write-Host # Select the app that is currently installed if (($CurrentApp | Where-Object -Property IsInstalled -eq $true)){ $CurrentApp = $CurrentApp | Where-Object -Property IsInstalled -eq $true } # Select the version that correspond with the current tenant data version as current app. elseif($CurrentApp[0].ExtensionDataVersion -in $CurrentApp.Version){ $CurrentApp = $CurrentApp | Where-Object -Property Version -eq $CurrentApp[0].ExtensionDataVersion } # If neither are present, select the oldest version of the app else { $CurrentApp = $CurrentApp | Sort-Object -Property Version | Select-Object -First 1 } } if ($CurrentApp) { $CurrentAppVersion = $CurrentApp.Version.Tostring() $CurrentAppName = $CurrentApp.Name 'Previous version of app {0} found:{1}' -f ` $AppName, ($CurrentApp | Select-Object -Property Name, Publisher, Version | Out-String) | Write-Host if([version] $AppVersion -lt [version] $CurrentAppVersion){ $Message = 'The supplied app version {0} is lower than the already install app version {1}. Skipping installation.' -f ` $AppVersion, $CurrentAppVersion if($AppIsBaseApp){ throw $Message } else { Write-Warning $Message $SkipPublish = $true $SkipUnInstall = $true $SkipSync = $true $SkipDataUpgrade = $true $SkipInstall = $true $SkipUnpublish = $true } } if ([version] $AppVersion -eq [version] $CurrentAppVersion){ $SkipPublish = $true $SkipUnpublish = $true 'SkipPublish and SkipUnpublish are set to true,' | Write-Host ' version {0} of the supplied app is equal to the currently installed app.' -f $AppVersion | Write-Host } } else { 'SkipUnpublish set to true,' | Write-Host ' no previous published app {0} is found.' -f $AppName | Write-Host $SkipUnpublish = $true } if($KeepPreviousApp){ 'SkipUnpublish set to true, because switch KeepPreviousApp is enabled.' | Write-Host $SkipUnpublish = $true } Write-EndProcessLine -TaskStartTime $SubTaskStartTime # Get app scope if not set. $SubTaskStartTime = Write-StartProcessLine -StartLogText 'Set app scope.' 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 } Write-EndProcessLine -TaskStartTime $SubTaskStartTime # Get the (child) apps that depend on the app that is being installed, to reinstall them after the installation. $SubTaskStartTime = Write-StartProcessLine -StartLogText ('Scanning for installed apps that depend on the to-install app {0}.' -f $AppName) $InstalledDependendApps = @() $PublishedApps = Get-NAVAppInfo ` -ServerInstance $ServerInstance ` -Tenant $Tenant ` -TenantSpecificProperties # Check every published app if the guid of the to-install app is pressent in the dependencies. foreach ($PublishedApp in $PublishedApps){ $AppDependencies = Get-NAVAppInfo ` -ServerInstance $ServerInstance ` -Id $PublishedApp.AppId.Value.Guid ` -Tenant $Tenant ` -TenantSpecificProperties ` -Version $PublishedApp.Version # Exclude not installed child apps, they don't need to be reinstalled. if ($AppDependencies.IsInstalled -ne $True) { continue } foreach($Dependency in $AppDependencies.Dependencies){ if($Dependency.AppId.Guid -eq $AppGuid){ $InstalledDependendApps += $PublishedApp } } } if ($InstalledDependendApps.Count -ge 1){ "The following apps depend on the app {0} that is being installed and will be {1} during the installation: `n{2}" -f ` $AppName, $(if($SkipAutoInstallChildApps){'uninstalled'} else {'re-installed'}), ($InstalledDependendApps.Name | Out-String) | Write-Host } else { 'No installed child-apps found.' | Write-Host } Write-EndProcessLine -TaskStartTime $SubTaskStartTime ### END *** App file installation initialization *** ### START *** App file installation *** $SubTaskStartTime = Write-StartProcessLine -StartLogText 'Publishing extension.' if(-not $SkipPublish){ 'Publishing {0} App, Guid: {1}, version {2} on server instance {3}.' -f $AppName, $AppGuid, $AppVersion, $ServerInstance | Write-Host switch ($AppScope) { 'Tenant' { "Publish-NAVApp -ServerInstance '{0}' -Path '{1}' -Tenant '{2}' -Scope '{3}' -SkipVerification:{4}" -f $ServerInstance, $AppFilePath, $Tenant, $AppScope, $SkipVerification | Write-Host Publish-NAVApp ` -ServerInstance $ServerInstance ` -Path $AppFilePath ` -Tenant $Tenant ` -Scope $AppScope ` -SkipVerification:$SkipVerification } 'Global' { "Publish-NAVApp -ServerInstance '{0}' -Path '{1}' -Scope '{2}' -SkipVerification:{3}" -f $ServerInstance, $AppFilePath, $AppScope, $SkipVerification | Write-Host Publish-NAVApp ` -ServerInstance $ServerInstance ` -Path $AppFilePath ` -Scope $AppScope ` -SkipVerification:$SkipVerification } } } else { 'Skipped Publish-NAVApp.' | Write-Host } Write-EndProcessLine -TaskStartTime $SubTaskStartTime $SubTaskStartTime = Write-StartProcessLine -StartLogText 'Determine which next installation steps are required.' # Update $NewApp with tenant specific installation status. $NewApp = Get-NAVAppInfo ` -ServerInstance $ServerInstance ` -Id $AppGuid ` -Version $AppVersion ` -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 $SkipSync = $true } else { 'Sync-NAVApp should be executed. Syncstate is {0}' -f $NewApp.SyncState | Write-host } # Check if Start-NAVAppDataUpgrade should be executed. if([version] $NewApp.ExtensionDataVersion -eq [version] $AppVersion){ 'SkipDataUpgrade is set to true, extensions data already upgraded.' | Write-Host $SkipDataUpgrade = $true } elseif([string]::IsNullOrEmpty($NewApp.ExtensionDataVersion)){ 'SkipDataUpgrade is set to true, extensionDataVersion is empty.' | Write-Host ' It is the first time this extension is installed.' | Write-Host $SkipDataUpgrade = $true } else { 'Start-NAVAppDataUpgrade should be executed.' | Write-Host ' Extension dataversion in tenant {0} is {1} and should be upgraded to {2}.' -f $Tenant, $NewApp.ExtensionDataVersion.ToString(), $AppVersion | Write-host } # 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 $SkipUnInstall = $true } elseif ($CurrentApp.IsInstalled -eq $false) { 'SkipUnInstall is set to true, previous extension is not installed.' | Write-Host $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 $SkipUnInstall = $true } else { 'Previous app will be uninstalled due to a pending table sync or dataupgrade.' | Write-Host } # 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 $SkipInstall = $true } elseif($SkipDataUpgrade -eq $false){ 'SkipInstall is set to true, Extension will be installed during the dataupgrade.' | Write-Host $SkipInstall = $true } Write-EndProcessLine -TaskStartTime $SubTaskStartTime $SubTaskStartTime = Write-StartProcessLine -StartLogText 'Uninstalling previous app.' # Uninstall current app if applicable. Uninstall app is only required in some cases of table or data sync. if(-not $SkipUnInstall){ 'Uninstalling {0} App, version {1}.' -f $CurrentAppName, $CurrentAppVersion | Write-Host "Uninstall-NAVApp -ServerInstance '{0}' -Name '{1}' -Version '{2}' -Tenant '{3}' -Force" -f $ServerInstance, $CurrentAppName, $CurrentAppVersion, $Tenant | Write-Host Uninstall-NAVApp ` -ServerInstance $ServerInstance ` -Name $CurrentAppName ` -Version $CurrentAppVersion ` -Tenant $Tenant ` -Force } else { 'Skipped Uninstall-NAVApp.' | Write-Host } Write-EndProcessLine -TaskStartTime $SubTaskStartTime $SubTaskStartTime = Write-StartProcessLine -StartLogText 'Syncing database schema for new app.' if(-not $SkipSync){ 'Execute sync {0} App on server instance {1}' -f $AppName, $ServerInstance | Write-Host "Sync-NAVApp -ServerInstance '{0}' -Name '{1}' -Version '{2}' -Mode '{3}' -Tenant '{4}'" -f $ServerInstance, $AppName, $AppVersion, 'Add', $Tenant | Write-Host Sync-NAVApp ` -ServerInstance $ServerInstance ` -Name $AppName ` -Version $AppVersion ` -Mode Add ` -Tenant $Tenant } else { 'Skipped Sync-NAVApp.' | Write-Host } Write-EndProcessLine -TaskStartTime $SubTaskStartTime $SubTaskStartTime = Write-StartProcessLine -StartLogText 'Execute data upgrade for new app.' if(-not $SkipDataUpgrade){ 'Execute data upgrade for {0} App' -f $AppName | Write-Host 'Note: Install-NAVApp is included in Start-NAVAppDataUpgrade' | Write-Host "Start-NAVAppDataUpgrade -ServerInstance '{0}' -Name '{1}' -Version '{2}' -Tenant '{3}'" -f $ServerInstance, $AppName, $AppVersion, $Tenant | Write-Host Start-NAVAppDataUpgrade ` -ServerInstance $ServerInstance ` -Name $AppName ` -Version $AppVersion ` -Tenant $Tenant } else { 'Skipped Start-NAVAppDataUpgrade.' | Write-Host } Write-EndProcessLine -TaskStartTime $SubTaskStartTime $SubTaskStartTime = Write-StartProcessLine -StartLogText 'Installing app if not yet installed.' if(-not $SkipInstall){ 'Installing {0} App' -f $AppName | Write-Host "Install-NAVApp -ServerInstance '{0}' -Name '{1}' -Version '{2}' -Tenant '{3}' -Force" -f $ServerInstance, $AppName, $AppVersion, $Tenant Install-NAVApp ` -ServerInstance $ServerInstance ` -Name $AppName ` -Version $AppVersion ` -Tenant $Tenant ` -Force } else { 'Skipped Install-NAVApp.' | Write-Host } Write-EndProcessLine -TaskStartTime $SubTaskStartTime if ($AppIsBaseApp){ $SubTaskStartTime = Write-StartProcessLine -StartLogText '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 $AppGuid -and $_.Version -eq $AppVersion} $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 $AppName, $AppVersion throw $Message } # Update the ApplicationVersion in the application database (the application version displayed in the webclient) if(([version] $AppVersion).Major -ge '17'){ [string] $nAppVersion = '{0}.{1}.0.0' -f ([version] $AppVersion).Major, ([version] $AppVersion).Minor } else { [string] $nAppVersion = $AppVersion } $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 $AppName, $ApplicationVersion, $nAppVersion | Write-Host Set-NAVApplication ` -ServerInstance $ServerInstance ` -ApplicationVersion $nAppVersion ` -Force # The dataupgrade needs to be executed for each tenant. # If not the other tenant state becomes 'OperationalDataUpgradePending' (Get-NAVTenant -ServerInstance $ServerInstance).Id | ForEach-Object { 'Run the data upgrade for tenant {0} on ServerInstance {1}.' -f $_, $ServerInstance | Write-Host ' Sync-NAVTenant' | Write-Host Sync-NAVTenant ` -ServerInstance $ServerInstance ` -Mode Sync ` -Tenant $_ ` -Force ' Start-NAVDataUpgrade' | Write-Host $dataUpgradeParams = @{ ServerInstance = $ServerInstance 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 } } else { 'ApplicationVersion is already {0}.' -f $ApplicationVersion | Write-Host } Write-EndProcessLine -TaskStartTime $SubTaskStartTime } # Unpublish the previous app if it is not installed in any tenant anymore. $SubTaskStartTime = Write-StartProcessLine -StartLogText 'Unpublishing previous app version.' if ($SkipUnpublish -eq $false) { if(Get-NAVAppTenant -ServerInstance $ServerInstance -Id $CurrentApp.AppId.Value -Version $CurrentAppVersion){ 'Previous app {0} version {1} is not unpublished because it is still in use by another tenant.' -f $CurrentAppName, $CurrentAppVersion | Write-Host } else { 'Unpublishing {0} App, version {1}' -f $CurrentAppName, $CurrentAppVersion | Write-Host "Unpublish-NAVApp -ServerInstance '{0}' -Name '{1}' -Version '{2}'" -f $ServerInstance, $CurrentAppName, $CurrentAppVersion | Write-Host Unpublish-NAVApp ` -ServerInstance $ServerInstance ` -Name $CurrentAppName ` -Version $CurrentAppVersion } } else { 'Skipped Unpublish-NAVApp.' | Write-Host } Write-EndProcessLine -TaskStartTime $SubTaskStartTime # Install uninstalled childapps again if AutoInstallChildApps is true) $SubTaskStartTime = Write-StartProcessLine -StartLogText 'Reinstalling child-apps.' if (-not $SkipAutoInstallChildApps -and $InstalledDependendApps.Count -ge 1 -and -not $SkipUnInstall){ 'Installing ChildApps.' | Write-Host # Get the right installation order $InstalledDependendApps = Get-AppDependencyOrder -Apps $InstalledDependendApps -ServerInstance $ServerInstance foreach ($ChildApp in $InstalledDependendApps){ # Skip the child-app installation if the app is included in the to-install apps, # except if the to-install app is a lower version. if ($ChildApp.AppId.Value.Guid -in $Apps.AppId.Value.Guid -and $ChildApp.Version -le ($Apps | Where-Object {$_.AppId.Value.Guid -eq $ChildApp.AppId.Value.Guid}).Version ){ ' Skipped install for child app {0} because the app is included in the to-install appfiles' -f $ChildApp.Name | Write-Host continue } if ($AppIsBaseApp){ ' Recompiling child app {0} against the new base app {1}.' -f $ChildApp.Name, $App.Name | Write-Host Repair-NAVApp ` -ServerInstance $ServerInstance ` -Name $ChildApp.Name ` -Version $ChildApp.Version | Out-Null } ' Installing app {0}' -f $ChildApp.Name | Write-Host Install-NAVApp ` -ServerInstance $ServerInstance ` -Name $ChildApp.Name ` -Version $ChildApp.Version ` -Tenant $Tenant ` -Force } } else { 'Skipped installing child apps.' | Write-Host } Write-EndProcessLine -TaskStartTime $SubTaskStartTime 'Total deploy time app {0} for tenant {1}:' -f $AppName, $Tenant | Write-Host Write-EndProcessLine -TaskStartTime $AppDeployStartTime } # End foreach tenant } # End foreach app file 'Total deploy time Business Central Extension(s):' | Write-Host Write-EndProcessLine -TaskStartTime $TaskStartTime $TaskStartTime = Write-StartProcessLine -StartLogText ('Restarting ServerInstance {0}.' -f $ServerInstance) if($SkipServiceRestart -eq $false){ Set-NavServerInstance ` -ServerInstance $ServerInstance ` -Restart Wait-BcServerInstanceMountingTenants ` -ServerInstance $ServerInstance } else { 'Skipped restarting ServiceInstance because switch SkipServiceRestart is set.' } Write-EndProcessLine -TaskStartTime $TaskStartTime $TaskStartTime = Write-StartProcessLine -StartLogText ('Writing summary.') $Tenants | ForEach-Object { $Message = (Get-NavAppInfo ` -ServerInstance $ServerInstance ` -Tenant $_ ` -TenantSpecificProperties | Sort-Object -Property Name, Version | ` Select-Object -Property Name, Publisher, IsPublished, SyncState, NeedsUpgrade, IsInstalled, ExtensionDataVersion, Version, AppId | ` Format-Table | Out-String) Write-host (Write-DebugInfoToOutput -MessageTitle ('Extension states for tenant {0}' -f $_) -MessageBody $Message) } Write-EndProcessLine -TaskStartTime $TaskStartTime 'Total execution time:' | Write-Host Write-EndProcessLine -TaskStartTime $TotalTime 'Installation completed.' | Write-Host try{ Stop-Transcript }catch{} } Export-ModuleMember -Function Install-BcApp |