Update Dynamics NAV Licenses on Remote Hosts

To update Dynamics NAV licenses of multiple databases used to be a bit of a headache. We have customers running independent NAV instances in the hundreds using LS Retail POS. Imagine having to log into each machine manually:

  1. Connect to the remote host.
  2. Upload the Dynamics NAV license file to the remote host.
  3. Import the license file.
  4. Restart the Dynamics NAV Service Tiers.

Maybe you have multiple development / test machines which need a yearly license update?

In this blog post I demonstrate how you can update NAV licenses on multiple remote hosts all from the comfort of your own PowerShell prompt. Hopefully, along the way you’ll pick up some PowerShell tricks to make your life easier!

Update Dynamics NAV License Centrally on Multiple NAV Servers
Update Dynamics NAV License Centrally on Multiple NAV Servers

Before we get started there are a few prerequisites to this technique:

  • The remote hosts are on the same domain.
  • The user account running the PowerShell script has local admin on the host.

Connecting to a remote host with PowerShell

PowerShell comes with a utility called PowerShell Remoting. This allows us to connect to remote servers which have PowerShell Remoting enabled. Windows Server 2012 R2 and above have PowerShell Remoting enabled by default, but if we need to enable this manually we can run the following command on the host:

> Enable-PSRemoting -force

If the -force option is not used a prompt asking if we want to continue will occur.

Group Policy can be used to centrally enable PowerShell Remoting.

With Remoting enabled we can start a remote session using the Enter-PSSession cmdlet:

> Enter-PSSession -ComputerName <hostname>

Specifying a different set of credentials than the current Windows credentials can be done by adding the -Credential parameter:

> Enter-PSSession -ComputerName <hostname> -Credential <domain\user.name>

A password prompt will appear if we specify a user account.

The Enter-PSSession cmdlet is handy when we want to enter into an interactive session with a remote host, but it will come up short when we want to start automating our scripts.

New-PSSession is used to create a PowerShell session. This session can be a remote session and we can assign this session to a variable. This session variable can be used with the Invoke-Command cmdlet.

For example, we could create a remote session and get a list of services on the remote host with:

$session = New-PSSession -Computer <hostname>

Invoke-Command -Session $session -ScriptBlock { Get-Service }

In a scenario where we have a list of remote hosts to process, we will likely want to keep a note of any hosts that fail to respond. A simple method is to write any failures out to a text file. We can catch these failures in a try / catch block:

$RemoteHosts = "Host1", "Host2", "Host3"

foreach ($RemoteHost in $RemoteHosts) {
  try {
    $session = New-PSSession -Computer $RemoteHost

    Invoke-Command -Session $session -ScriptBlock { Get-Service }
  } catch {
    Add-Content -Path "C:\Temp\ErrorLog.txt" -Value ("Error creating remote session on: " + $RemoteHost)
  }
}

The Add-Content cmdlet will append values to the end of the file specified, creating the file if it doesn’t exist.

Send the local NAV license file to the host machine

I thought about a few options for getting the license file on the host machine:

  • Put the license file on a web server or fileshare and download from the host.
  • Copy the file through the Copy-Item Cmdlet using the -FromSession parameter.

But the most convenient in this scenario is to put the license data in a byte array and pass it into the Import-NAVServerLicense Cmdlet using the -LicenseData parameter.

To create a byte array from the NAV license file we can use the following code:

> [byte[]]$LicenseData = Get-Content -Path "C:\Temp\License.flf" -Encoding Byte

$LicenseData now contains the new NAV license, but we’ve created this variable in our local PowerShell session. Once we’ve created the remote session we will need to pass this variable in to the script block we are going to execute in the remote session. To do this, we can utilise script block parameters:

[byte[]] $LicenseData = Get-Content -Path "C:\Temp\License.flf" -Encoding Byte

$ScriptBlock = {
  Param($LicenseDataParam)
  # some code here...
}

$RemoteSession = New-PSSesssion -ComputerName <RemoteHost>

Invoke-Command -Session $RemoteSession -ScriptBlock $ScriptBlock -ArgumentList (,$LicenseData)

You may have noticed the -ArgumentList value looks a bit weird: (,$LicenseData). This is because the -ArgumentList parameter expects an array value, and will pass our byte array through as individual elements. That said, I’ve no idea why this syntax works… Stack Overflow to the rescue!

If you don’t use the (,$ArrayVar) syntax, expect an error message like this:

The license file is corrupt. Error Code: -200.
 + CategoryInfo : NotSpecified: (0:Int32) [Import-NAVServerLicense], FaultException`1
 + FullyQualifiedErrorId : MicrosoftDynamicsNavServer$DynamicsNAV110,Microsoft.Dynamics.Nav.Management.Cmdlets.ImportNavServerLicense
 + PSComputerName : REMOTEHOST

Importing the license into Dynamics NAV

So far we can create a remote session, move the license data to the remote host and execute script block on the remote host. Next we need to import the license into the remote Dynamics NAV instance. This is done using the Import-NAVServerLicense cmdlet:

> Import-NAVServerLicense <NAV Service Instance> -LicenseData <License Data>

The minimum parameters we need are the Dynamics NAV service instance name and the license data. We’ve already got the license data in the $LicenseData byte array, so lets look at getting a service to use.

To make this simple, I’m going to apply the license to any running services on the host. I’ve blogged about getting running services previously, here’s some PowerShell to return the service names of any running NAV services:

> Get-NAVServerInstance | Where-Object {$_.State -eq 'running'} | Select-Object -ExpandProperty "ServerInstance"

The following script will get a list of running Dynamics NAV services, apply the license using the first service found and then restart all Dynamics NAV services:

# Put license data in Byte array
[Byte[]]$LicenseData = Get-Content -Path "C:\Temp\License.flf" -Encoding Byte

# Get list of running NAV services
$NAVServices = Get-NAVServerInstance | Where-Object {$_.State -eq 'running'} | Select-Object -ExpandProperty "ServerInstance"

# If variable is an array get the first index
If ($NAVServices -is [array]) {
 $NAVService = $NAVServices[0]
} else {
 $NAVService = $NAVServices
}

# Check we have a NAV service before we try to import a license
if (-Not [string]::IsNullOrEmpty($NAVService)) {
 Import-NAVServerLicense $NAVService -LicenseData $LicenseData
}

# Restart the service(s)
Restart-Service -Name $NAVServices

Putting it all together: Update Dynamics NAV Licenses on remote machines

OK, we’ve run through all the parts. Lets put this all in to one script:

# Array of host machines
$RemoteHosts = "NAVSERV01", "NAVSERV02", "NAVSERV03"

# Text file with list of host machines
# $RemoteHosts = Get-Content -Path "C:\Temp\NAVHostsList.txt"

[Byte[]]$LicenseData = Get-Content -Path "C:\Temp\License.flf" -Encoding Byte

$ScriptBlock = {
 param ($LicenseData)

 Import-Module 'C:\Program Files\Microsoft Dynamics NAV\110\Service\NAVAdminTool.ps1'

 $NAVServices = Get-NAVServerInstance | Where-Object {$_.state -eq 'running'} | Select-Object -ExpandProperty "ServerInstance"

 # If variable is an array get the first index
 If ($NAVServices -is [array]) {
   $NAVService = $NAVServices[0]
 } else {
   $NAVService = $NAVServices
 }

# Check we have a NAV service before we try to import a license
 if (-Not [string]::IsNullOrEmpty($NAVService)) {
   Import-NAVServerLicense $NAVService -LicenseData $LicenseData

  # Restart the service(s)
   Restart-Service -Name $NAVServices
 }
}
foreach ($RemoteHost in $RemoteHosts) {
 try {
   $session = New-PSSession -Computer $RemoteHost

   Invoke-Command -Session $session -ScriptBlock $ScriptBlock -ArgumentList (,$LicenseData)

 } catch {
   Add-Content -Path "C:\Temp\ErrorLog.txt" -Value ("Error updating license on: " + $RemoteHost)
 }
}

Whilst this does the job, there is plenty of room for improvement. Error handling is very lightweight for a start. Issues connecting to a remote host will be logged in the ErrorLog file, but if any errors occur in the remote session you’ll only be able to view them from the console output. Also, this script only allows for a single version of NAV and assumes all services on a remote host are connected to the same database.

So, feel free to use and adapt this script.. but do so at your own risk.

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.