Database Upgrade with Release Pipeline

Published on 11 December 2023 at 07:00

Introduction

In a previous post, I discussed upgrading the Microsoft Dynamics 365 Business Central database to a desired version. In earlier discussions, I highlighted the versatility of Azure DevOps Release Pipelines for various operations. It occurred to me: why not integrate these two operations into a meaningful whole?

With SaaS solutions, users seamlessly upgrade to the latest product version, often without requiring significant partner intervention, as users can perform the upgrade independently. However, not all users opt for SaaS solutions; some prefer OnPrem, requiring partners to periodically update their product version. Many users find themselves "stuck" on a specific product version over time, a situation I consider a significant missed opportunity.

In this blog post, I aim to share my approach to achieving a fully automated method for upgrading the database to the latest product version. Admittedly, I've implemented this process from version 23.0 to 23.1, focusing initially on Dev databases. The plan is to conduct further testing on scripts and gradually deploy them to user databases, catering to those users ready to embrace the latest product version.

 

Prerequisites

  • Download link of latest version
  • Find out which app needs to be upgraded/installed
  • Setup configuration to be used for installing the new version of the product
  • And finaly, I have created a variable group that will be used in the release pipeline

 

I had to split the upgrade operation into two scripts due to the challenge of the backup of instances using Get-NAVServerConfiguration and Get-NAVWebServerInstance. This involved uninstalling the existing product version, installing the new product version, and restoring instances from the backup configuration. I encountered difficulty importing one version of NavAdminTool.ps1 first and then importing the new version through the same script.

As a result, I decided to divide the upgrade into two distinct operations:

  1. Upgrade Preparation
  2. Upgrade Operation

 

Let's explore what needs to be done in the upgrade preparation step.

 

PrepareUpgrade.ps1

In this step, the following actions are performed:

  1. Download the latest version of the product.
  2. Backup instances using the Get-NAVServerConfiguration command.
  3. Backup web server instances using the Get-NAVWebServerInstance command.
  4. Uninstall-NAVApp and Unpublish-NAVApp for all apps, followed by Stop-NAVServerInstance and Remove-NAVServerInstance.
  5. Uninstall operation.
  6. Install the downloaded version of the product from the first step using the quiet and Wait parameters.

 


  $TenantId = 'default'
  $ArtifactsDirectory = (Join-Path -Path $env:System_ArtifactsDirectory -ChildPath("$env:Release_PrimaryArtifactSourceAlias\$env:ArtifactName\"))
  
  Measure-Command {
      Measure-Command {
          if (!(Test-Path -Path $env:DOWNLOAD_PATH)) {
              New-Item -ItemType Directory -Path $env:DOWNLOAD_PATH
          }
          $zipFilePath = Join-Path -Path $env:DOWNLOAD_PATH -ChildPath "file.zip"
          Get-ChildItem -Path $env:DOWNLOAD_PATH -Recurse | Remove-Item -Force -Recurse
          Invoke-WebRequest -Uri $env:BC_DOWNLOAD_URL -OutFile $zipFilePath
          Expand-Archive -Path $zipFilePath -DestinationPath $env:DOWNLOAD_PATH -Force
          Remove-Item -Path $zipFilePath -Force
      } | ForEach-Object { Write-Host "`n##[section]Downloading latest version: $([int]$_.TotalSeconds) seconds" }
  
      $folders = Get-ChildItem -Path "C:\Program Files\Microsoft Dynamics 365 Business Central\" -Directory
      $numericFolders = $folders | Where-Object { $_.Name -match '^\d+$' }
      $highestFolder = $numericFolders | Sort-Object -Property Name -Descending | Select-Object -First 1
      $navAdminToolPath = Join-Path -Path $highestFolder.FullName -ChildPath "Service\NavAdminTool.ps1"
      Import-Module $navAdminToolPath
      $configFilePath = Join-Path -Path $ArtifactsDirectory -ChildPath $env:SETUP_CONFIG
      [xml]$setupConfig = Get-Content -Path $configFilePath
      $NavServiceInstanceName = $setupConfig.Configuration.Parameter | Where-Object { $_.Id -eq 'NavServiceInstanceName' } | Select-Object -ExpandProperty Value
      Stop-NAVServerInstance -ServerInstance $NavServiceInstanceName -Force
      $configs = @{}
      $webConfigs = @{}
      $failedInstances = @()
      $runningInstances = Get-NAVServerInstance | Where-Object { $_.State -eq 'Running' } | ForEach-Object { $_.ServerInstance.Split('$')[1] }
      foreach ($instance in $runningInstances) {
          try {
              $config = Get-NAVServerConfiguration -ServerInstance $instance -AsXml
              $configs[$instance] = $config
          }
          catch {
              $failedInstances += $instance
          }
      }
      $configs | Export-Clixml -Path (Join-Path -Path $ArtifactsDirectory -ChildPath $env:Configs)
      $webConfigs = Get-NAVWebServerInstance
      $webConfigs | Export-Clixml -Path (Join-Path -Path $ArtifactsDirectory -ChildPath $env:WebConfigs)
      $runningInstances = $runningInstances | Where-Object { $_ -notin $failedInstances }
      Measure-Command {
          foreach ($instance in $runningInstances) {
              Get-NAVAppInfo -ServerInstance $instance -Tenant $TenantId | ForEach-Object { Uninstall-NAVApp -ServerInstance $instance -Tenant $TenantId -Name $_.Name -Version $_.Version -Force } 
              Get-NAVAppInfo -ServerInstance $instance -SymbolsOnly | ForEach-Object { Unpublish-NAVApp -ServerInstance $instance -Name $_.Name -Version $_.Version } 
              Stop-NAVServerInstance -ServerInstance $instance
              Remove-NAVServerInstance -ServerInstance $instance -Force
          }
          $webConfigs | ForEach-Object {
              $webServerInstance = $_.WebServerInstance
              Remove-NAVWebServerInstance -WebServerInstance $webServerInstance
          }
      } | ForEach-Object { Write-Host "`n##[section]Removing Old Server Instances: $([int]$_.TotalSeconds) seconds" }
      
      Measure-Command {
          $setupFilePath = Join-Path $env:DOWNLOAD_PATH "setup.exe"
          Start-Process -FilePath $setupFilePath -ArgumentList "/uninstall", "/quiet" -Wait
          Start-Process -FilePath $setupFilePath -ArgumentList "/config `"$configFilePath`" /quiet" -Wait
          Start-Sleep -Seconds 60
      } | ForEach-Object { Write-Host "`n##[section]Reinstalling the product: $([int]$_.TotalSeconds) seconds" }
  } | ForEach-Object { Write-Host "`n##[section]Total time: $([int]$_.TotalSeconds) seconds" }

 

If you observe the commands in this step and the subsequent ones, you'll notice that they are standard commands from the official product upgrade documentation, applicable from version 15 and later, but on steroids :)

 

And that concludes this step; afterward, the database, or multiple databases, are ready for the upgrade. This is because backups are created for all instances for which configuration retrieval is possible.

 

 

UpgradeDatabase.ps1

 

In this step, a slightly more extensive code section is implemented to perform the following tasks:

  1. Prepare Microsoft Apps for upgrade/installation by copying them to a separate folder dedicated to upgrading all databases.
  2. Create server instances from the backup using the New-NAVServerInstance and Set-NAVServerConfiguration commands.
  3. Upgrade Microsoft Apps.
  4. Upgrade Third-Party Apps.
  5. Upgrade/Install Control Add-Ins.
  6. Create web server instances from the backup using the New-NAVWebServerInstance command.

 

As mentioned earlier, these are essentially the same steps outlined on the official website but automated.

 

Let's take a look at what is inside that script. At the very beginning, there are some helper functions designed to assist in upgrading/installing apps in the correct order.

 


  function Get-OrderedApps {
      param(
          [Parameter(Mandatory = $true)]
          [string] $Path
      )
  
      $DependenciesFirst = @()
  
      function AddAnApp {
          Param(
              $anApp,
              [ref]$DependenciesFirst
          )
          $alreadyAdded = $DependenciesFirst.Value | Where-Object { $_.AppId -eq $anApp.AppId -and $_.Version -eq $anApp.Version }
          if (-not ($alreadyAdded)) {
              AddDependencies -anApp $anApp -DependenciesFirst $DependenciesFirst
              $DependenciesFirst.Value += $anApp
          }
      }
  
      function AddDependency {
          Param(
              $dependency,
              [ref]$DependenciesFirst
          )
          $dependentApp = $null
          if ($dependency.AppId -eq '00000000-0000-0000-0000-000000000000') {
              # AppId: 00000000-0000-0000-0000-000000000000 Name: Application Publisher: Microsoft
              $dependentApp = $AllApps | Where-Object { $_.Name -eq 'Application' }
          }
          else {
              $dependentApp = $AllApps | Where-Object { $_.AppId -eq $dependency.AppId }
          }
          if ($dependentApp) {
              @($dependentApp) | ForEach-Object { AddAnApp -AnApp $_ -DependenciesFirst $DependenciesFirst }
          }
      }
  
      function AddDependencies {
          Param(
              $anApp,
              [ref]$DependenciesFirst
          )
          if (($anApp) -and ($anApp.Dependencies)) {
              $anApp.Dependencies | ForEach-Object { AddDependency -Dependency $_ -DependenciesFirst $DependenciesFirst }
          }
      }
  
      $AllAppFiles = Get-ChildItem -Path $Path -Filter "*.app" -Recurse
      $AllApps = @()
      foreach ($AppFile in $AllAppFiles) {
          $App = Get-NAVAppInfo -Path $AppFile.FullName
          $AllApps += [PSCustomObject]@{
              AppId        = $App.AppId
              Version      = $App.Version
              Name         = $App.Name
              Publisher    = $App.Publisher
              Dependencies = $App.Dependencies
              Path         = $AppFile.FullName
              ProcessOrder = 0
          }
      }
  
      $AllApps | ForEach-Object { AddAnApp -AnApp $_ -DependenciesFirst ([ref]$DependenciesFirst) }
  
      $counter = 1
      $DependenciesFirst | ForEach-Object -Process {
          $_.ProcessOrder = $counter
          $counter++
      }
      return $DependenciesFirst
  }


  function UpgradeApps {
      [CmdletBinding()]
      param(            
          [string] $Service,
          [string] $Tenant,
          [Parameter(Mandatory = $true)]
          [array] $Apps
      )
  
      $Apps = $Apps | Where-Object { $null -ne $_.AppId }
  
      $attempt = 0
      while ($true) {
          $attempt++
          $Apps | Sort-Object -Property ProcessOrder | ForEach-Object {
              try {
                  Publish-NAVApp -Path $_.Path -ServerInstance $Service -SkipVerification
                  if ($_.Publisher -notlike 'Microsoft') {
                      Repair-NAVApp -ServerInstance $Service -AppId $_.AppId -Version $_.Version 
                  }
              }
              catch {
              }
          }
  
          Sync-NAVTenant -ServerInstance $Service -Force -Mode ForceSync -Tenant $Tenant
  
          $Apps | Sort-Object -Property ProcessOrder | ForEach-Object {
              try {
                  Sync-NAVApp -ServerInstance $Service -Force -Mode ForceSync -AppId $_.AppId -Version $_.Version
              }
              catch {
              }
          }
      
          $Apps | Sort-Object -Property ProcessOrder | ForEach-Object {
              $CurrentApp = $_
              try {
                  Start-NAVAppDataUpgrade -ServerInstance $Service -AppId $_.AppId -Version $_.Version -SkipVersionCheck -ErrorAction Stop
                  Write-Host "`n##[section]Published and upgraded:" $_.Name 'Version:' $_.Version -ForegroundColor Green
              }
              catch {
                  try {
                      Install-NAVApp -ServerInstance $Service -AppId $CurrentApp.AppId -Version $CurrentApp.Version 
                      Write-Host "`n##[section]Installed:" $CurrentApp.Name 'Version:' $CurrentApp.Version -ForegroundColor Green
                  }
                  catch {
                      Write-Host "`n##[section]Error:" $CurrentApp.Name 'Version:' $CurrentApp.Version -ForegroundColor Green
                  }
              }
          }
          if ($Error.Count -eq 0) {
              $AppVersions = @()
              foreach ($App in $Apps) {
                  $AppVersions += Get-NAVAppInfo -ServerInstance $Service -Id $App.AppId
              }
              foreach ($AppVersion in $AppVersions) {
                  if ($Apps | Where-Object { $_.AppId -eq $AppVersion.AppId -and $_.Version -gt $AppVersion.Version }) {
                      Unpublish-NAVApp -AppId $AppVersion.AppId -ServerInstance $Service -Version $AppVersion.Version
                  }
              }
              break
          }
  
          if ($attempt -gt 10) {
              Write-Host "`n##[Error]Error: Upgrade failed" -ForegroundColor Red
              if ($Error.Count -gt 0) {
                  foreach ($error in $Error) {
                      Write-Host "`n##[error]:" $($error.Exception.Message)
                  }
              }
              throw "`n##[Error]Error: Upgrade failed"
          }
          $global:Error.Clear()
      }
  }

 

Following that, there is a section dedicated to copying Microsoft apps from the DVD folder. However, considering the multitude of apps in the DVD folder, our focus is solely on those listed in the MicrosoftApps.xml file.


  $MicrosoftAppsDir = Join-Path -Path $env:DOWNLOAD_PATH -ChildPath "Microsoft Apps"
  if (!(Test-Path -Path $MicrosoftAppsDir)) {
      New-Item -ItemType Directory -Path $MicrosoftAppsDir
  }
  $configFilePath = Join-Path -Path $ArtifactsDirectory -ChildPath $env:SETUP_CONFIG
  [xml]$setupConfig = Get-Content -Path $configFilePath
  $targetPathX64 = $setupConfig.Configuration.Parameter | Where-Object { $_.Id -eq 'TargetPathX64' } | Select-Object -ExpandProperty Value
  $NavServiceInstanceName = $setupConfig.Configuration.Parameter | Where-Object { $_.Id -eq 'NavServiceInstanceName' } | Select-Object -ExpandProperty Value
  $navAdminToolPath = Join-Path -Path $targetPathX64 -ChildPath "Service\NavAdminTool.ps1"
  Import-Module $navAdminToolPath -Force
  Stop-NAVServerInstance -ServerInstance $NavServiceInstanceName -Force
  $serviceName = "MicrosoftDynamicsNavServer`$$NavServiceInstanceName"
  sc.exe config $serviceName start= disabled
  $configs = Import-Clixml -Path (Join-Path -Path $ArtifactsDirectory -ChildPath $env:Configs)
  $license = Join-Path -Path $ArtifactsDirectory -ChildPath ($env:LicenseFile)
  $settings = (Get-Content (Join-Path -Path $ArtifactsDirectory -ChildPath ($env:ConfigFile)) -Encoding UTF8 | ConvertFrom-Json)
  $AddinsFolder = Join-Path -Path $targetPathX64 -ChildPath "Service\Add-ins"
  $subdirectories = Get-ChildItem -Path $AddinsFolder -Directory
  $zipFile = Join-Path -Path $env:DOWNLOAD_PATH -ChildPath "\Applications\BaseApp\Source\Base Application.Source.zip"
  $extractPath = Join-Path -Path $env:DOWNLOAD_PATH -ChildPath "\Applications\BaseApp\Source"
  Expand-Archive -Path $zipFile -DestinationPath $extractPath -Force
  $appJsonFile = Join-Path -Path $extractPath -ChildPath "app.json"
  $appJson = Get-Content -Path $appJsonFile -Raw | ConvertFrom-Json
  $NewBCVersion = $appJson.version
  $webConfigs = Import-Clixml -Path (Join-Path -Path $ArtifactsDirectory -ChildPath $env:WebConfigs)
  
  Measure-Command {
      $apps = Import-Clixml -Path (Join-Path -Path $ArtifactsDirectory -ChildPath $env:MicrosoftApps)
      $DvdFolderPath = $env:DOWNLOAD_PATH
      $AllAppFiles = Get-ChildItem -Path $DvdFolderPath -Filter "*.app" -Recurse
      foreach ($AppFile in $AllAppFiles) {
          $App = Get-NAVAppInfo -Path $AppFile.FullName
          $AppExists = $apps | Where-Object { $_.AppId -eq $App.AppId }
          if ($AppExists) {
              $DestinationPath = Join-Path $MicrosoftAppsDir $AppFile.Name
              Copy-Item -Path $AppFile.FullName -Destination $DestinationPath -Force
          }
      }
} | ForEach-Object { Write-Host "`n##[command]Copying Microsoft Apps: $([int]$_.TotalSeconds) seconds" -ForegroundColor Green }

 

Afterwards, there is a section dedicated to creating server instances from the backup.


$config = [xml]$configs[$instance]
$DatabaseServer = $config.configuration.appSettings.add | Where-Object { $_.key -eq 'DatabaseServer' } | Select-Object -ExpandProperty value
$ApplicationDatabase = $config.configuration.appSettings.add | Where-Object { $_.key -eq 'DatabaseName' } | Select-Object -ExpandProperty value
$ClientServicesCredentialType = $config.configuration.appSettings.add | Where-Object { $_.key -eq 'ClientServicesCredentialType' } | Select-Object -ExpandProperty value
Measure-Command {
    $ManagementServicesPort = $config.configuration.appSettings.add | Where-Object { $_.key -eq 'ManagementServicesPort' } | Select-Object -ExpandProperty value
    New-NAVServerInstance -ManagementServicesPort $ManagementServicesPort -ServerInstance $instance
    $serviceName = "MicrosoftDynamicsNavServer`$$instance"
    sc.exe config $serviceName obj= "$env:SERVICE_ACCOUNT" password= "$env:SERVICE_ACCOUNT_PASSWORD"
    $config.configuration.appSettings.add | ForEach-Object {
        $key = $_.key
        $value = $_.value
        if ($key -ne 'ServerInstance') {
            Set-NAVServerConfiguration -ServerInstance $instance -KeyName $key -KeyValue $value -Force -Applyto ConfigFile
        }
    }
} | ForEach-Object { Write-Host "`n##[command]Creating Server Instance for" $instance "from backup: $([int]$_.TotalSeconds) seconds" -ForegroundColor Green }

 

I studied the command:

New-NAVServerConfiguration

 

On the official documentation, it states that you can achieve the following:

 

Get-NAVServerConfiguration bc -AsXml | New-NAVServerConfiguration myinstance

 

It would be ideal if I could create a service configuration from an XML config. However, I couldn't figure out how to do that. Therefore, I wrote a section of code that sets the configuration step by step through a loop.

 

Next comes the section for upgrading Microsoft Apps using the helper functions defined at the beginning of the script.


Measure-Command {
    if ($ClientServicesCredentialType -eq 'NavUserPassword' -and $UpgradedDatabases -notcontains $ApplicationDatabase) {
        Invoke-NAVApplicationDatabaseConversion -DatabaseName $ApplicationDatabase -DatabaseServer $DatabaseServer -Force
        Set-NavServerConfiguration -ServerInstance $instance -KeyName "EnableTaskScheduler" -KeyValue false 
        Restart-NAVServerInstance -ServerInstance $instance
        Import-NAVServerLicense -ServerInstance $instance -LicenseFile $license 
        Restart-NAVServerInstance -ServerInstance $instance
        $MicrosoftApps = Get-OrderedApps -Path $MicrosoftAppsDir
        UpgradeApps -Apps $MicrosoftApps -Service $instance -Tenant $TenantId
        Restart-NAVServerInstance -ServerInstance $instance -Force
    }
} | ForEach-Object { Write-Host "`n##[command]Upgrading Microsoft Apps for" $instance ": $([int]$_.TotalSeconds) seconds" -ForegroundColor Green }

 

Afterward, there is an identical section for upgrading Third-Party Apps, with the distinction that in this step, the latest versions of the apps are downloaded.


Measure-Command {
  $ThirdPartyAppsDir = Join-Path -Path $env:DOWNLOAD_PATH -ChildPath ("Third party Apps\$instance")
  if (!(Test-Path -Path $ThirdPartyAppsDir)) {
      New-Item -ItemType Directory -Path $ThirdPartyAppsDir
  }
  Get-ChildItem -Path $ThirdPartyAppsDir -Recurse | Remove-Item -Force -Recurse
  if ($ClientServicesCredentialType -eq 'NavUserPassword' -and $UpgradedDatabases -notcontains $ApplicationDatabase) {
      $AppIds = @()
      foreach ($app in $settings.Apps) {
          foreach ($config in $app.configurations) {
              if ($config.serverInstance -eq $instance) {
                  $AppIds += $app.id
              }
          }
      }
      if ($instance -like '*TEST*') {
          $branchName = "refs/heads/uat"
      }
      elseif ($instance -like '*PROD*') {
          $branchName = "refs/heads/main"
      }
      else {
          $branchName = $env:BRANCH_NAME
      }
      foreach ($appId in $AppIds) {
          $RepositorySettings = (Get-Content -Path (Join-Path $ArtifactsDirectory -ChildPath($env:RepositoryConfigFile)) -Encoding UTF8 | ConvertFrom-Json)
          $RepositorySettings = $RepositorySettings.Apps | Where-Object { $_.id -eq $appId }
          $repositoryName = $RepositorySettings.repositoryName
          $token = [System.Convert]::ToBase64String([System.Text.Encoding]::ASCII.GetBytes(":$($env:PAT)"))
          $url = "https://dev.azure.com/$($env:OrgName)/$($env:ProjectName)/_apis/git/repositories/$($repositoryName)?api-version=4.1"
          $response = Invoke-RestMethod -Uri $url -Headers @{Authorization = "Basic $token" }
          $response = $response | ConvertTo-Json
          $resultJson = $response | ConvertFrom-Json
          $repositoryid = $resultJson.id
          $url = "https://dev.azure.com/$($env:OrgName)/$($env:ProjectName)/_apis/build/builds?branchName=$($branchName)&repositoryId=$($repositoryId)&repositoryType=$($env:repositoryType)&api-version=7.0"
          $response = Invoke-RestMethod -Uri $url -Headers @{Authorization = "Basic $token" }
          $response = $response | ConvertTo-Json
          $resultJson = $response | ConvertFrom-Json
          $Build = $resultJson.value | Where-Object { $_.result -eq 'succeeded' }
          $BuildId = ($Build | Select-Object -ExpandProperty "id" | Select-Object -First 1)
          $url = "https://dev.azure.com/$($env:OrgName)/$($env:ProjectName)/_apis/build/builds/$($BuildID)/artifacts?artifactName=$($env:ArtifactName)&api-version=7.0"    
          $response = Invoke-RestMethod -Uri $url -Headers @{Authorization = "Basic $token" }
          $downloadUrl = $response.resource.downloadUrl
          $OutFile = $repositoryName + '.zip'
          Invoke-WebRequest -Uri $downloadUrl -Headers @{Authorization = "Basic $token" } -outFile $OutFile
          Expand-Archive -LiteralPath $OutFile -DestinationPath $repositoryName -Force
          $sourcepath = Join-Path (Get-Location) -ChildPath("$repositoryName\$env:ArtifactName\")
          Get-ChildItem -Path $sourcepath -Filter *.app -File -Recurse | ForEach-Object {
              if ($_.Name -notmatch 'Test') {
                  Copy-Item -Path $_.FullName -Destination $ThirdPartyAppsDir
              }
          }
      }
      $ThirdPartyApps = Get-OrderedApps -Path $ThirdPartyAppsDir
      if ($ThirdPartyApps -ne $null) {
          UpgradeApps -Apps $ThirdPartyApps -Service $instance -Tenant $TenantId
      }
      Restart-NAVServerInstance -ServerInstance $instance -Force
  }
} | ForEach-Object { Write-Host "`n##[command]Upgrading Third Party Apps for" $instance ": $([int]$_.TotalSeconds) seconds" -ForegroundColor Green }

 

Following that, there is a section for upgrading/installing Control Add-Ins.


Measure-Command {
  if ($ClientServicesCredentialType -eq 'NavUserPassword' -and $UpgradedDatabases -notcontains $ApplicationDatabase) {
      $existingAddIns = Get-NAVAddIn -ServerInstance $instance
      foreach ($subdirectory in $subdirectories) {
          $zipFiles = Get-ChildItem -Path $subdirectory.FullName -Filter *.zip
          foreach ($zipFile in $zipFiles) {
              $addinName = $zipFile.BaseName
              $dllFile = Get-ChildItem -Path $subdirectory.FullName -Filter "$addinName.dll"
              if ($dllFile) {
                  $assemblyFullName = [System.Reflection.AssemblyName]::GetAssemblyName($dllFile.FullName).FullName
                  $publicKeyToken = ($assemblyFullName -split 'PublicKeyToken=')[1]
                  $resourceFile = $zipFile.FullName
                  $existingAddIn = $existingAddIns | Where-Object { $_.AddInName -eq $addinName }
                  if ($existingAddIn) {
                      Set-NAVAddIn -ServerInstance $instance -AddinName $addinName -PublicKeyToken $publicKeyToken -ResourceFile $resourceFile -Force
                  }
                  else {
                      New-NAVAddIn -ServerInstance $instance -AddinName $addinName -PublicKeyToken $publicKeyToken -ResourceFile $resourceFile -Force
                  }
              }
          }
      }
  }
} | ForEach-Object { Write-Host "`n##[command]Installing Add-ins for" $instance ": $([int]$_.TotalSeconds) seconds" -ForegroundColor Green }

 

Then comes the final section of the script:

Start-NAVDataUpgrade -ServerInstance $NewBcServerInstance -FunctionExecutionMode Serial -Tenant $TenantId

 

and the creation of web server instances from the backup.


Measure-Command {
  if ($ClientServicesCredentialType -eq 'NavUserPassword' -and $UpgradedDatabases -notcontains $ApplicationDatabase) {
      Set-NAVApplication -ServerInstance $instance -ApplicationVersion $NewBCVersion -Force
      Sync-NAVTenant -ServerInstance $instance -Mode Sync -Tenant $TenantId -Force
      Start-NAVDataUpgrade -ServerInstance $instance -FunctionExecutionMode Serial -Tenant $TenantId -ContinueOnError -Force
      Start-Sleep -Seconds 10
      Get-NAVDataUpgrade -ServerInstance $instance
      $state = Get-NAVAppTenant -ServerInstance $instance
      while ($true) {
          Start-Sleep -Seconds 10
          $state = Get-NAVAppTenant -ServerInstance $instance
          if ($state.State -eq "Operational") {
              break
          }
      }
      Set-NAVServerConfiguration -ServerInstance $instance -KeyName SolutionVersionExtension -KeyValue "437dbf0e-84ff-417a-965d-ed2bb9650972" -ApplyTo All -Force 
      Set-NavServerConfiguration -ServerInstance $instance -KeyName "EnableTaskScheduler" -KeyValue true -Force 
      Restart-NAVServerInstance -ServerInstance $instance -Force
  }
} | ForEach-Object { Write-Host "`n##[command]Upgrading Application Database for" $instance ": $([int]$_.TotalSeconds) seconds" -ForegroundColor Green }

Measure-Command {
  $webConfigs | ForEach-Object {
      New-NAVWebServerInstance -Server $_.Server -ServerInstance $_.ServerInstance -WebServerInstance $_.ServerInstance -AddFirewallException -ClientServicesCredentialType $_.ClientServicesCredentialType -ClientServicesPort $_.ClientServicesPort -DnsIdentity $_.DnsIdentity
  }
} | ForEach-Object { Write-Host "`n##[command]Creating Web Server Instance for" $instance ": $([int]$_.TotalSeconds) seconds" -ForegroundColor Green }

$UpgradedDatabases += $ApplicationDatabase 
Stop-NAVServerInstance -ServerInstance $instance -Force
    
  

 

You may notice that I am storing the databases in a list for which an upgrade has already been performed. This is because it is possible to run multiple instances for the same database, perhaps due to NavUserPassword or Windows login, or for some other reason. In this case, the upgrade is executed only for databases that have not been upgraded previously.

 

Additionally, while testing the script with multiple databases or instances, I overkilled the server with the script. Therefore, after upgrading a specific database, I decided to stop that service. Towards the end of the script, I restart all the necessary services.


  Measure-Command {
    foreach ($instance in $configs.Keys) {
        Start-NAVServerInstance -ServerInstance $instance
    }
  } | ForEach-Object { Write-Host "`n##[command]Starting all instances: $([int]$_.TotalSeconds) seconds" -ForegroundColor Green }
          
} | ForEach-Object { Write-Host "`n##[command]Total time taken: $([int]$_.TotalSeconds) seconds" -ForegroundColor Green }
    
  

 

 

Conclusion

Why limit the latest product versions to just SaaS when, with a bit of effort, you can automate upgrades for OnPrem databases as well?

 

I tested the script from version 23.0 to version 23.1 in Dev environments, and I must say it's quite efficient. Upgrading four databases on that server took less than an hour.

 

 

Of course, this isn't a recommendation for such an approach, as many things can go wrong, but...

If something go wrong, a manual upgrade is the next step, which is something done anyway. Therefore, with this approach, there isn't much to lose, only time and manual effort can be saved :)

During this kind of upgrade, or manual upgrade, it's essential to create a backup of the database. I think there's no need to emphasize that.


Add comment

Comments

There are no comments yet.