Using AdvaniaGIT in Visual Studio Code

It has become obvious that the future of AL programming is in Visual Studio Code.

Microsoft has made a decision to ship all their releases as Docker Containers.

The result of this is a development machine that does not have any NAV version installed.  I wanted to go through the installation and configuration of a new NAV on Docker development machine.

Here is what I did.

I installed Windows Server 2016 with Containers.  The other option was to use Windows 10 and install Docker as explained here.

After installing and fully updating the operating system I downloaded and installed Visual Studo Code.

After installation Visual Studio Code detects that I need to install Git.

I selected Download Git and was taken to the Git download page.

I downloaded and installed Git with default settings.

To be able to run NAV Development and NAV Client I need to install prerequisite components.  I copied the Prerequisite Components folder from my NAV 2018 DVD and installed some of them…

Let’s hook Visual Studio Code to our NAV 2018 repository and install AdvaniaGIT.  I first make sure to always run Visual Studio Code with administrative privileges.

Now that we have our AdvaniaGIT installed and configured we can start our development.  Let’s start our C/AL classic development.  Where this video ends you can continue development as described in my previous posts on AdvaniaGIT.  AdvaniaGIT also supports NAV 2016 and NAV 2017.

Since we are running NAV 2018 we can and should be using AL language and the Extension 2.0 model.  Let’s see how to use our repository structure, our already build Docker container and Visual Studio Code to start our first AL project.

So as you can see by watching these short videos it is easy to start developing both in C/AL and AL using AdvaniaGIT and Visual Studio Code.

My next task is to update my G/L Source Names extension to V2.  I will be using these tools for the job.  More to come soon…

Introducing AdvaniaGIT – SCM for Dynamics NAV

Almost two years ago we in Advania decided to start using GIT as Source Control Management (SCM).  We brought Kamil up to Iceland and we kicked off.  In Sorens session on NAVTechDays last year we demoed SourceTree as the GIT client for NAV SCM.

Everything we in Advania are doing with SCM is available on GitHub.  It is our hope that we can get as many users and companies to use and contribute to this solution.

Over the next coming days and weeks I will be writing here about this tool.  I will also be using the GitHub Wiki for some of the information.

Installing AdvaniaGIT will create a folder structure on your local drive. You can select any of the local drive installed. We suggest that the AdvaniaGIT\Workspace folder should be excluded from Windows Defender and that also goes for any GIT folder used.

Refer to the README.md file inside every subfolder for more details about each subfolder usage.

Inside the Data subfolder we store the module settings in JSON files.

  • BranchSettings.json is automatically managed by the module and used to link GIT branches to local NAV environments.
  • BuildSettings.json contains incremented values that will be used when building new environments.
  • GITSettings.json contains machine settings for the module.
  • NAVVersions.json contains information about locally installed NAV.
  • RemoteSettings.json contains settings for the Remote Management module. Not used by GIT in any way.
  • TenantSettings.json contains settings for each tenant running on a remote server that is managed using the Remote Management module. Not used by GIT in any way.

In the GIT repository folder we require a setup.json file. When the scripts are executed settings from the GIT branch (setup.json) and settings from the machine (GITSettings.json) are merged to a single settings object. If same settings exist in both files the one in the GIT branch will be used.

Installing the module will add custom actions to SourceTree and a command file (StartPowerShell.cmd) to your Windows directory. SourceTree will execute this command file with parameters telling the module what to do. The command file will execute Scripts\Start-CustomAction.ps1 with the same parameters. All custom actions within the Scripts\CustomActions subfolder can be executed.

For teams we suggest using a FTP server for backups and CRONUS text files.

My next blog post will be on the installation and update of AdvaniaGIT.  Stay tuned…

Using NetTcpPortSharing for NAV Servers

I just came back from three weeks vacation yesterday.  During my vacation I had made a decision to implement Tcp Port Sharing for the Instance Administration tool used in Advania Azure.

Early last year I published a function that uses the sc.exe to modify a NAV Service startup type.  When a NAV Service is installed and configured in setup, the startup type is Automatic (Delayed Start).  However, create a new service with Powershell New-NavServerIntance and the statup type is Automatic without the (Delayed Start).

To enable Tcp Port Sharing that same sc.exe function is needed.  Interestingly, after I had finished the task and was reading NAV blogs I saw that Waldo just published a powershell function on his blog to do just this.

The script lines I used and added to my Instance Administration powershell scripts are based on my fist sc.exe function but not using the function it self.  Now when a new NAV service is created by the tool the startup type is modified and if so selected by the deployment settings, the Tcp Port Sharing is also activated.

By default, the Tcp Port Sharing service is disabled.
servicedisabled

The startup type should be changed to Manual.  This can be done manually or by an administrative powershell script.

[code lang=”powershell”]#Set Startup Mode for NetTcpPortSharing to Manual
$command = ‘sc.exe \\$Computer config "NetTcpPortSharing" start= demand’
$Output = Invoke-Expression -Command $Command -ErrorAction Stop
if($LASTEXITCODE -ne 0){
Write-Error "$Computer : Failed to set NetTcpPortSharing to manual start. More details: $Output"
}
[/code]

Similar script is used to update the existing NAV Services to both delayed start and Tcp Port Sharing dependency.

[code lang=”powershell”]

#Stop NAV Server Instances
Get-NAVServerInstance | Set-NAVServerInstance -Stop
#Update Startup Type and Dependency on NAV Server Instances
Get-NAVServerInstance | foreach {
$Service = $_.ServerInstance
Write-Host "Working on service $Service"
$Computer = ‘LOCALHOST’
$command = ‘sc.exe \\$Computer config "$Service" start= delayed-auto’
$Output = Invoke-Expression -Command $Command -ErrorAction Stop
if($LASTEXITCODE -ne 0){
Write-Error "$Computer : Failed to set $Service to delayed start. More details: $Output"
}
$command = ‘sc.exe \\$Computer config "$Service" depend= NetTcpPortSharing/HTTP’
$Output = Invoke-Expression -Command $Command -ErrorAction Stop
if($LASTEXITCODE -ne 0){
Write-Error "$Computer : Failed to set $Service TcpPortSharing. More details: $Output" -foregroundcolor red
}

}
#Start NAV Server Instances
Get-NAVServerInstance | Set-NAVServerInstance -Start
[/code]

It should be obvious that the above script can also use the Set-ServiceStartupMode from my blog and the Enable-NAVServerInstancePortSharing function on Waldo’s blog. That would be a cleaner code and more in line with what we would like to see.

Again quoting Waldo from his previous blog, “When you’re using a dedicated service account, things might become a slight more difficult”.  That is exactly my case, I am using a dedicated service account.

After enabling Tcp Port Sharing and updating the services they would not start.  Event Viewer revealed the reason.

Server instance: CRONUS
The service MicrosoftDynamicsNavServer$CRONUS failed to start. This could be caused by a configuration error. Detailed error information:System.ServiceModel.CommunicationException: The service endpoint failed to listen on the URI ‘net.tcp://mynavserver.dynamics.is:7046/CRONUS/Service’ because access was denied. Verify that the current user is granted access in the appropriate allowAccounts section of SMSvcHost.exe.config. —> System.ComponentModel.Win32Exception: Access is denied

So I started to ask Bing what I could do.  Microsoft MSDN states:

When a net.tcp binding enables port sharing (by setting portSharingEnabled =true on the transport binding element), it implicitly allows an external process (namely the SMSvcHost.exe, which hosts the Net.TCP Port Sharing Service) to manage the TCP socket on its behalf.

Hence, I need to add the Sid of my NAV Service Account to the SMSvcHost.exe.config file.  I could do this manually, but I am a programmer!

Another powershell script was born.  This one could also be converted to a function.  Before executing the script make sure to update the user and domain in the top of the script.  Be smart and execute this function before updating the NAV Services with the script above.

[code lang=”powershell”]
#Modify User and Domain to fit your environment
$UserToAdd = ‘srvNAV’
$UserDomainToAdd = ‘DYNAMICS’

#Initial Values
$UserSidFound = ‘false’
$ConfigurationSet = ‘false’

#Net.Tcp Port Sharing Service Name
$ServiceName = ‘NetTcpPortSharing’

#Get SID for the Service User
$UserSid = ([wmi] "win32_userAccount.Domain=’$UserDomainToAdd’,Name=’$UserToAdd’").SID

#Get Path for SMSvcHost.exe.config file
$SMSvcHostPath = (Get-WmiObject win32_service | ?{$_.Name -like $ServiceName} ).PathName
$SMSvcHostPathConfig = $SMSvcHostPath + ‘.config’

Write-Host "Reading XML from $SMSvcHostPathConfig"
#Read Config file
$xmlDoc = [xml] (Get-Content $SMSvcHostPathConfig)

Write-Host "Looking for access permission for $UserSid"
#Loop through allowed accounts and search for the service user Sid
$allowAccounts = Select-Xml "configuration/system.serviceModel.activation/net.tcp/allowAccounts/add" $xmlDoc
$allowAccounts | ForEach-Object {
$ConfiguredSid = $_.Node.Attributes.Item(0).Value
if ($ConfiguredSid -eq $UserSid) {$UserSidFound = ‘true’}
$ConfigurationSet = ‘true’
Write-Host "Found SID $ConfiguredSid"
}

#Act if Access Configuration is not enabled
if ($ConfigurationSet -eq ‘false’) {Write-Host "Access permission not configured"
$config = [xml] ‘<system.serviceModel.activation>
<net.tcp listenBacklog="10" maxPendingConnections="100" maxPendingAccepts="2" receiveTimeout="00:00:10" teredoEnabled="false">
<allowAccounts>
<add securityIdentifier="S-1-5-18"/>
<add securityIdentifier="S-1-5-19"/>
<add securityIdentifier="S-1-5-20"/>
<add securityIdentifier="S-1-5-32-544" />
</allowAccounts>
</net.tcp>
<net.pipe maxPendingConnections="100" maxPendingAccepts="2" receiveTimeout="00:00:10">
<allowAccounts>
<add securityIdentifier="S-1-5-18"/>
<add securityIdentifier="S-1-5-19"/>
<add securityIdentifier="S-1-5-20"/>
<add securityIdentifier="S-1-5-32-544" />
</allowAccounts>
</net.pipe>
<diagnostics performanceCountersEnabled="true" />
</system.serviceModel.activation>’

$configurationNode = $xmlDoc.DocumentElement
$newConfig = $xmlDoc.ImportNode($config.DocumentElement, $true)
$configurationNode.AppendChild($newConfig)

$allowAccounts = Select-Xml "configuration/system.serviceModel.activation/net.tcp/allowAccounts/add" $xmlDoc
$allowAccounts | ForEach-Object {
$ConfiguredSid = $_.Node.Attributes.Item(0).Value
Write-Host "Found SID $ConfiguredSid"
if ($ConfiguredSid -eq $UserSid) {$UserSidFound = ‘true’}
$ConfigurationSet = ‘true’
}

}

#Add Service User Sid if needed
if ($UserSidFound -ne ‘true’) {
$nettcp = $xmlDoc.SelectSingleNode("configuration/system.serviceModel.activation/net.tcp/allowAccounts")
$addNode = $xmlDoc.CreateElement(‘add’)
$secIden = $xmlDoc.CreateAttribute(‘securityIdentifier’)
$secIden.Value = $UserSid
$addNode.Attributes.Append($secIden)

$nettcp.AppendChild($addNode)
$xmlDoc.Save($SMSvcHostPathConfig)
Write-Host "Configuration Updated"
#Restart Service if running
if ((Get-Service NetTcpPortSharing).Status -eq "Running") {Restart-Service NetTcpPortSharing -Force}
}

[/code]

This script will search for the SMSvcHost.exe.config file, load it and check to see if the NAV Service User is already allowed access.  If not then the config file is updated and saved.  This script must be executed with administrative privileges.

Perhaps this should be what I started with, but the question; why do we need this, should be answered.

First, modifying the startup mode to delayed start is done to make sure that all the required networking and database processes have been started before the NAV Service starts.  This is very important if the SQL Server is running on the same server.  On a dedicated NAV Service server this is not as important but still recommended.

Secondly, accessing a NAV Service in most cases requires changes to a firewall.  Either to open a specific port or setting up a NAT from a public interface.  To minimize the number of ports used also minimizes the networking setup and maintenance.  If different network permissions or network access is required I recommend using separate ports for the NAV Services.

Building a clean database – remove not licensed objects

I just got a question from a client;

Gunnar,
Do you have a “King Kong” license that will allow you to delete any object?  It appears our development license does not have the rights to some of the newer LS Retail objects and I need to create a CRONUS database with just our stuff.

Well, I don’t have a “King Kong” license.  That is only for Microsoft.

There is a way to solve this dilemma.  It will take a few steps.

Start with we have two databases, one with the data we need (LSRetail), another with the application we need (CRONUS).

After the process is completed the LSRetail database will not be usable as a standalone database, so make a copy if you need one.  A new database will be created, CRONUS_APP.  To clean up it is safe to delete both these databases.

The following powershell script has two options.  Option 1 is to have the company data imported into the CRONUS database in the end.  This option requires a server instance running on the CRONUS database.  Option 2 is to create a new database with SQL Management Studio and merge the CRONUS application and the LSRetail data into that one.

[code lang=”powershell”]
$CronusDatabaseName = "CRONUS" # Database with destination Application
$CRONUSServerInstance = "DynamicsNAV80" # Instance for destination Application if using option 1
$LSRetailDatabaseName = "LSRETAIL" # LS Retail Demo Database, database with company data
$EmptyDatabaseName = "CRONUS WITH COMPANYDATA" # Create a new empty database using SQL Management Studio if using option 2
$SQLServerName = "SQL2014"
$SQLServerInstance = "NAVDEMO" # Set blank for default instance

$AppDatabaseName = $CronusDatabaseName + "_APP"
$ServiceAccount = $env:USERDOMAIN + "\" + $env:USERNAME
$ServerInstance = "UPGRADE"
$NavDataFile = (Join-Path $env:TEMP "NAVmerge.navdata")

$SelectOption = "2"

#Export Application from CRONUS Database to Application Database
Export-NAVApplication -DatabaseServer $SQLServerName -DatabaseInstance $SQLServerInstance -DatabaseName $CronusDatabaseName -DestinationDatabaseName $AppDatabaseName -ServiceAccount $ServiceAccount -Force

#Setup a temporary Server Instance for the new database
Get-Credential | New-NAVServerInstance -ServerInstance $ServerInstance -ManagementServicesPort 33555 -ClientServicesPort 33556 -SOAPServicesPort 33557 -ODataServicesPort 33558 -DatabaseInstance $SQLServerInstance -DatabaseServer $SQLServerName -DatabaseName $AppDatabaseName -ServiceAccount User -Force
Set-NAVServerConfiguration -ServerInstance $ServerInstance -KeyName "Multitenant" -KeyValue "true" -Force
Set-NAVServerInstance -ServerInstance $ServerInstance -Start -Force

#Prepare LSRetailDatabase for new configuration
Remove-NAVApplication -DatabaseInstance $SQLServerInstance -DatabaseServer $SQLServerName -DatabaseName $LSRetailDatabaseName -Force

#Mount and Sync LSRetailDatabase as a tenant
Mount-NAVTenant -ServerInstance $ServerInstance -DatabaseInstance $SQLServerInstance -DatabaseServer $SQLServerName -DatabaseName $LSRetailDatabaseName -Id DEFAULT -OverwriteTenantIdInDatabase -AllowAppDatabaseWrite -Force
Sync-NAVTenant -ServerInstance $ServerInstance -Tenant DEFAULT -Mode ForceSync -Force

if (Test-Path $NavDataFile)
{
Remove-Item -Path $NavDataFile -Force
}

#Option 1, Copy Company data to the original CRONUS database. Requies a service running on the CRONUS database
if ($SelectOption -eq "1")
{
Export-NAVData -ServerInstance $ServerInstance -Tenant DEFAULT -AllCompanies -FilePath $NavDataFile -Force
Import-NAVData -ServerInstance $CRONUSServerInstance -FilePath $NavDataFile -AllCompanies -Force
}
#Option 2, Import into the new empty database created by SQL Management Studio
if ($SelectOption -eq "2")
{

Export-NAVData -ServerInstance $ServerInstance -Tenant DEFAULT -AllCompanies -FilePath $NavDataFile -IncludeApplication -IncludeApplicationData -IncludeGlobalData -Force
if ($SQLServerInstance -eq "")
{
Import-NAVData -DatabaseServer $SQLServerName -DatabaseName $EmptyDatabaseName -FilePath $NavDataFile -AllCompanies -IncludeApplicationData -IncludeGlobalData -IncludeApplication

}
else
{
Import-NAVData -DatabaseServer ($SQLServerName + "\" + $SQLServerInstance) -DatabaseName $EmptyDatabaseName -FilePath $NavDataFile -AllCompanies -IncludeApplicationData -IncludeGlobalData -IncludeApplication
}

}

Set-NAVServerInstance -ServerInstance $ServerInstance -Stop -Force
Remove-NAVServerInstance -ServerInstance $ServerInstance -Force

if (Test-Path $NavDataFile)
{
Remove-Item -Path $NavDataFile -Force
}
[/code]

To walk you through what happens;

  • Application from CRONUS is exported into CRONUS_APP database
  • New Service Instance is created for CRONUS_APP database
  • Service Instance is changed to Multi Tenant and started
  • Application is removed from LSRetail database
  • LSRetail database is mounted as a tenant for CRONUS_APP database
  • LSRetail database structure is force-synched to CRONUS_APP application
  • Data from CRONUS_APP and LSRetail tenant is exported to NAVData file
  • NAVData file is imported into an empty database or the existing CRONUS database

 

Apply a Cumulative Update with Powershell

Microsoft has changed the way they ship Cumulative Updates.  Now we download the whole DVD image along with the application changes.

This post is intended as additional information for New Developer Tools for Dynamics NAV 2013 R2 and Install Client and Server update with PowerShell posts.

To update a code that is built on a RTM version or an older CU version I use the Merge-NAVToNewCU script package.  I create a temporary folder and export three set of objects.  One file for the original unchanged objects (38457Objects.txt), One file for the new CU objects (40938Objects.txt) and finally the modified version (38457customizedObjects.txt).  When I export I normally skip the MenuSuites that I don’t have permission to import.

Microsoft sometimes adds new fields to tables, fields that are outside of my permission to insert.  Therefore, when I have completed the compare and have my new customized object (40938customizedObjects.txt) I start by importing the object fob file directly from the Microsoft Dynamics NAV Application folder found in the CU package.  I accept the default action, replace most objects and merge tables.  This will create all the new fields needed by the update.  After I am able to import the new customized objects text file and compile.

[code lang=”powershell”]$rootFolderName = $PSScriptRoot
$oldVersion = ‘38457’
$newVersion = ‘40938’

Import-Module "${env:ProgramFiles(x86)}\Microsoft Dynamics NAV\80\RoleTailored Client\Microsoft.Dynamics.Nav.Model.Tools.psd1" -force
Import-Module (Join-Path $rootFolderName ‘Merge-NAVVersionListString script.ps1’) -force

$diffFolderName = (Join-Path $rootFolderName ($oldVersion + ‘to’ + $newVersion + ‘diff’))
$oldObjects = (Join-Path $rootFolderName ($oldVersion + ‘objects.txt’))
$newObjects = (Join-Path $rootFolderName ($newVersion + ‘objects.txt’))
$newFolder = (Join-Path $rootFolderName ($newVersion + ‘update’))
$customObjects = (Join-Path $rootFolderName ($oldVersion + ‘customizedobjects.txt’))
$newCustomizedObjects = (Join-Path $rootFolderName ($newVersion + ‘customizedobjects.txt’))
$newCustomizedFolder = (Join-Path $rootFolderName ($newVersion + ‘customized’))
if (!(Test-Path $diffFolderName))
{
mkdir $diffFolderName
}
if (!(Test-Path $newFolder))
{
mkdir $newFolder
}
if (!(Test-Path $newCustomizedFolder))
{
mkdir $newCustomizedFolder
}
Write-Host "Comparing customized and original…"
Compare-NAVApplicationObject -Original $oldObjects -Modified $customObjects -Delta $diffFolderName | Where-Object CompareResult -eq ‘Identical’ | foreach { Remove-Item (Join-Path $diffFolderName ($_.ObjectType.substring(0,3) + $_.Id + ‘.delta’)) }
Write-Host "Splitting new objects…"
Split-NAVApplicationObjectFile $newObjects $newFolder
Write-Host "Removing unchanged new objects…"
Get-ChildItem -Path $newFolder | foreach { if (!(Test-Path ((Join-Path $diffFolderName $_.BaseName) + ‘.delta’))) { Remove-Item $_.FullName } }
Write-Host "Updating new objects…"
Update-NAVApplicationObject -Target $newFolder -Delta $diffFolderName -Result $newCustomizedFolder -DateTimeProperty FromModified -ModifiedProperty FromModified -VersionListProperty FromModified -DocumentationConflict ModifiedFirst
Write-Host "Updating customized object version list…"
Get-ChildItem -Path (Join-Path $newCustomizedFolder ‘*.txt’)| foreach { if (Test-Path (Join-Path $newFolder $_.Name)) {Set-NAVApplicationObjectProperty -Target $_.FullName -VersionListProperty (Merge-NAVVersionListString -source (Get-NAVApplicationObjectProperty -Source $_.FullName).VersionList -target (Get-NAVApplicationObjectProperty -Source (Join-Path $newFolder $_.Name)).VersionList) }}
Write-Host "Joining customized object to a single file…"
Join-NAVApplicationObjectFile -Source (Join-Path $newCustomizedFolder ‘*.txt’) -Destination $newCustomizedObjects
Write-Host "If you have conflicts then you need to manually fix conflicting code changes"
[/code]

I have also created a set of scripts to update the Binaries.  For some time now Microsoft has not shipped the binary upgrade folders with the CU package.  Now they deliver the whole DVD, including the new updated Demo Database.  The scripts therefore must copy the files from the DVD instead of copying from the binary upgrade folders.

When copying from the DVD we must make sure that we don’t overwrite the configuration files for the server and the web client. This is the server update script. All server on the computer will be stopped, updated and restarted.

[code lang=”powershell”]$NAVDVDFilePath = ‘\\STORAGE\NAV 2015\NAV.8.0.40938.IS.DVD’
$NotToCopy = @(‘Tenants.config’,’CustomSettings.config’)
Write-Verbose "Copying NAV Server Update…"
$ClientKBFolder = Join-Path $NAVDVDFilePath ‘ServiceTier\program files\Microsoft Dynamics NAV\80\Service’
$navInstallationDirectory = Join-Path ${env:ProgramFiles} ‘Microsoft Dynamics NAV\80\Service’
if (Test-Path $navInstallationDirectory)
{
Import-Module (Join-Path $ClientKBFolder ‘Microsoft.Dynamics.Nav.Management.dll’) -DisableNameChecking | Out-Null
$RunningInstances = Get-NAVServerInstance | Where-Object { $_.State -eq "Running" }
Write-Verbose "Stopping Server Instances…"
Get-NAVServerInstance | Where-Object { $_.State -eq "Running" } | Set-NAVServerInstance -Stop
Start-Sleep -s 5

Write-Verbose "Running file copy command…"
Get-ChildItem -Path $ClientKBFolder | % {Copy-Item $_.FullName $navInstallationDirectory -Recurse -Force -Exclude $NotToCopy}

Write-Verbose "Done updating files…"
foreach ($RunningInstance in $RunningInstances)
{
$InstanceName = $RunningInstance.ServerInstance.ToString()
Write-Verbose "Starting Server Instance $InstanceName"
Get-NAVServerInstance -ServerInstance $InstanceName | Set-NAVServerInstance -Start
}
}[/code]

This is the web server update script

[code lang=”powershell”]$NAVDVDFilePath = ‘\\STORAGE\NAV 2015\NAV.8.0.40938.IS.DVD’
$NotToCopy = (‘web.config’,’instanceweb.config’,’Header.png’,’About.png’,’Splash.png’)
function Copy-LatestFile
{
[CmdletBinding()]
param (
[parameter(Mandatory=$true)]
[string]$SourceDirectoryPath,
[parameter(Mandatory=$true)]
[string]$DestinationDirectoryPath
)
Write-Verbose "Running file copy command…"
$sourcefiles = Get-ChildItem $SourceDirectoryPath -Recurse
$destfiles = Get-ChildItem $DestinationDirectoryPath -Recurse
foreach ($sourcefile in $sourcefiles)
{
$copyfile = $false
foreach($destfile in $destfiles)
{
if ($destfile.Name -eq $sourcefile.Name)
{
$copyfile = $true
break
}
}
if (!($copyfile) -or ($NotToCopy -match $sourcefile.BaseName))
{
write-verbose "not copying $sourcefile…"
}
else
{
write-verbose "copying $sourcefile…"
Copy-Item $sourcefile.FullName $destfile.FullName -Force
}
}
}
Write-Verbose "Copying Web Client Update…"
$ClientKBFolder = Join-Path $NAVDVDFilePath ‘WebClient\Microsoft Dynamics NAV\80\Web Client’
$navInstallationDirectory = Join-Path ${env:ProgramFiles} ‘Microsoft Dynamics NAV\80\Web Client’
if (Test-Path $navInstallationDirectory)
{
Copy-LatestFile -SourceDirectoryPath $ClientKBFolder -DestinationDirectoryPath $navInstallationDirectory
}
[/code]

And finally the client update script.

[code lang=”powershell”]$NAVDVDFilePath = ‘\\STORAGE\NAV 2015\NAV.8.0.40938.IS.DVD’
$NotToCopy = (‘Header.png’,’About.png’,’Splash.png’)
function Copy-LatestFile
{
[CmdletBinding()]
param (
[parameter(Mandatory=$true)]
[string]$SourceDirectoryPath,
[parameter(Mandatory=$true)]
[string]$DestinationDirectoryPath
)

Write-Verbose "Running file copy command…"
$sourcefiles = Get-ChildItem $SourceDirectoryPath -Recurse -File
$destfiles = Get-ChildItem $DestinationDirectoryPath -Recurse -File

foreach ($sourcefile in $sourcefiles)
{
$copyfile = $false
foreach($destfile in $destfiles)
{
if ($destfile.Name -eq $sourcefile.Name)
{
$copyfile = $true
break
}
}
if (!($copyfile) -or ($NotToCopy -match $sourcefile.BaseName))
{
write-verbose "not copying $sourcefile…"
}
else
{
write-verbose "copying $sourcefile…"
Copy-Item $sourcefile.FullName $destfile.FullName -Force
}
}
}

Write-Verbose "Copying RTC Update…"
$ClientKBFolder = Join-Path $NAVDVDFilePath ‘RoleTailoredClient\program files\Microsoft Dynamics NAV\80\RoleTailored Client’
$navInstallationDirectory = Join-Path ${env:ProgramFiles(x86)} ‘Microsoft Dynamics NAV\80\RoleTailored Client’
if (Test-Path $navInstallationDirectory)
{
Copy-LatestFile -SourceDirectoryPath $ClientKBFolder -DestinationDirectoryPath $navInstallationDirectory
}
Write-Verbose "Copying Office 14 Update…"
$ClientKBFolder = Join-Path $NAVDVDFilePath ‘Outlook\program files\Microsoft Dynamics NAV\80\OutlookAddin’
$navInstallationDirectory = Join-Path ${env:ProgramFiles(x86)} ‘Microsoft Office\Office14’
if (Test-Path $navInstallationDirectory)
{
Copy-LatestFile -SourceDirectoryPath $ClientKBFolder -DestinationDirectoryPath $navInstallationDirectory
}
Write-Verbose "Copying Office 15 Update…"
$ClientKBFolder = Join-Path $NAVDVDFilePath ‘Outlook\program files\Microsoft Dynamics NAV\80\OutlookAddin’
$navInstallationDirectory = Join-Path ${env:ProgramFiles(x86)} ‘Microsoft Office\Office15’
if (Test-Path $navInstallationDirectory)
{
Copy-LatestFile -SourceDirectoryPath $ClientKBFolder -DestinationDirectoryPath $navInstallationDirectory
}
[/code]

The ClickOnce distribution is always based on the client folder and the script in Install Client and Server update with PowerShell can be used for that purpose.

I am using these binary update scripts in the Instance and Tenant Administration tool and I normally maintain the ClickOnce distribution from there.

ServerManagement

 

New Developer Tools for Dynamics NAV 2013 R2

Cumulative update 9 for Dynamics NAV 2013 R2 has been released.  This new version includes a powershell tool package for developers.

You can read about these tools in the NAV Team Blog, on Soren’s Blog and on Waldo’s Blog.

With every CU package we get an upgrade for the application.  I have a customer that is already running CU8 and I want to install CU9.  This time I will do that with these new powershell tools.

First step is to install the new CU9 binaries.  This I will do with the powershell scripts from here.  This will update windows client files, web client files, server files, outlook integration and click once deployment.  On my developement machine I needed to copy the RTC client files from the CU9 DVD.  The new model tools where not included in the KB RTC folder.

Then to export the Dynamic NAV objects.

I use my CU8 version demo database and export all objects to cu8objects.txt.  I export all objects from the customized database to cu8customizedobjects.txt.  I finally use my CU9 version demo database and export all objects to cu9objects.txt.

Open PoweShell ISE and navigate to the folder with the object files.

Next I import the new model tools.

I create a folder called cu8tocu9diff and execute

I see in my cu8tocu9diff folder that I have 4.036 objects and 3.972 of them are not needed.  I deleted all the files from cu8tocu9diff folder and executed

The result is that I only have the 64 delta files needed in the cu8tocu9diff folder.

Now I need the same 64 objects from the customized database.  I begin by splitting the exported object file into a new folder.

I now have all 4.036 objects in the cu8customized folder.  I delete all unneeded files by executing

I am now in a place to update the customized objects with the cu9 changes.

When I updated the customized objects with these 64 changes I got objects with a new version list.  The version list needs to be merged and for that I use a script that was written by NAV MVP Kamil Sacek.  The script is attached here as Merge-NAVVersionListString script and his scripts are available on CodePlex.  After copying the script to my base folder I need to import the new functions.

And to merge the version list for the new objects I use

I have now 64 updated object that I can import into my customized database and compile.  First I want to join all these 64 files into one object file.

When I imported the new customized object I got an error.  Error because I had new fields in tables that I did not have permission to create.  To solve this I begin by importing the URObjects.IS.37221.fob file that is included in the cumulative update package.  I accept the default action so all new fields should be merged into the existing tables.

Scripts are available to export and import NAV objects so this whole process can be scripted and automated.

Unblock downloaded files

In the process of downloading a file from an insecure location Windows blocks the file.

I have had this bothering to often.  For example when I downloaded an add-in and hit an error.

And more recently I downloaded a knowledge base packed and installed with my PowerShell script just to find out that every file in the download had been blocked.

Once again, PowerShell came to the rescue.  Opened Windows PowerShell ISE in Administration Mode and execute the below script for the needed folders.

[code lang=”ps”]$folder = ‘C:\PUB’
#$folder = ‘C:\Program Files\Microsoft Dynamics NAV’
#$folder = ‘C:\Program Files (x86)\Microsoft Dynamics NAV’
$files = Get-ChildItem -Path $folder -Recurse
foreach ($file in $files)
{
Write-Host "Unblocking file $file…"
Unblock-File -Path $file.FullName
}
[/code]

Install Client and Server update with PowerShell

It is time to share with you the scripts that I am using to install and update the NAV application in the company network.  In the attached ZIP packages you will find a lot of files.

In the root folder I keep the settings files.  These files must be customized.  The folders are as follows;

  • Edge3-Internet – my internet facing server.  Here I install ClickOnce, WebClient and NAV Service.
  • Fjarvinna-Domain – my remote desktop server.  Here I install KB updates remotely.
  • Localhost-Domain – my intranet server.  Here I install NAV Service for the domain.
  • NAVCmdLets – my collection of PowerShell scripts for NAV.  Some a copied from the NAV DVD.
  • Install-KBLocally – scripts to install a KB package to the local computer.

If we first look at the Edge3-Internet folder, here I have scripts to remotely install NAV on a server.  I usually create a ClickOnce container in the folder ‘c:\inebpub\ClickOnce’ and keep all my ClickOnce subfolders in there.  In the Set-MachineSettings.ps1 file I set the machine name.

Next, in Fjarvinna-Domain folder I only have scripts to upload and install a KB package.  Also in here I have the Set-MachineSettings.ps1 to define the machine name.

The Localhost-Domain folder includes scripts to install NAV Service and NAV Users to the localhost.

Then there are the functions that I use in the above scripts.  In the NAVCmdLets folder I have a collection of scripts to handle ClickOnce, KB install and some SQL administration tasks.

Finally the Install-KBLocally folder.  As the name suggest, the contained scripts are used to install a KB package on the localhost.  I usually create a folder that contains this folder, the NAVCmdLets folder and for example the KB2955941 folder.  The KB2955941 can be downloaded from Microsoft.  I usually delete all sub folders except ADCS, NST, OUTLOOK, RTC and WEB CLIENT after I add this folder to the update package.

Also, in my KB package I have two CMD files.  One to install to a 64bit machine (default) and the other to a 32bit machine.  If you put this package on your network and modify the CMD files to point to the correct file share you should be able to use this for all the computers on the domain.  The KB package without the Microsoft binaries is attached below.  Make sure to right-click on the install CMD file and run as administrator for a succesful install.

If you are running an old operating system, you might need KB2506146 or KB2506143 installed before using the KB package.

With your network administration tools you should be able to use some of these scripts to install a KB package on all your domain computers to make sure that everyone is running the latest version recommended by Microsoft.

NAVPowerShellScripts KB

NAV Service Startup Type in PowerShell scripting

I have spent some time creating PowerShell scripts to solve all my installation and upgrade tasks.  I am now able to install Dynamics NAV on both local and remote computers.  I can install databases, services, clickonce distribution and web clients.  And my final task last week was to be able to install knowledge base package in a few minutes that upgrades services, client, webclient and clickonce.  Likely to create a blog about that later.

As a part of this process I have created several PowerShell scripts and functions.  I will find a time to share this with you over the next weeks.

For now I would like to share one function.

When you install a NAV Service Instance from the DVD the service startup is set to “Automatic (Delayed Start)”.

NAVServiceStartupType

There is a reason for this.  I have seen servers unable to finish the startup process if the service startup type is set to “Automatic” only.  There is a period of time needed before the service can start after the computer startup.

The problem I am facing with my scripts is that the command New-NAVServerInstance will leave the service in “Automatic” startup mode.  I wanted to change this so I created a function.

[code lang=”powershell”]# Set Service Startup Mode to Automatic (delayed)
function Set-ServiceStartupModeRemotely
{
[CmdletBinding()]
param (
[parameter(Mandatory=$true)]
[string] $ServiceInstance,

[parameter(Mandatory=$true)]
[System.Management.Automation.Runspaces.PSSession] $Session
)
PROCESS {

Invoke-Command -Session $Session -ScriptBlock {
param([string] $ServiceInstance, [string] $verbosePreference)
$VerbosePreference = $verbosePreference
Set-ExecutionPolicy -ExecutionPolicy RemoteSigned
$Service = ‘MicrosoftDynamicsNavServer$’ + $ServiceInstance
$Computer = ‘LOCALHOST’
$command = ‘sc.exe \\$Computer config "$Service" start= delayed-auto’
$Output = Invoke-Expression -Command $Command -ErrorAction Stop
if($LASTEXITCODE -ne 0){
Write-Host "$Computer : Failed to set $Service to delayed start.
More details: $Output" -foregroundcolor red
} else {
Write-Host "$Computer : Successfully changed $Service service
to delayed start" -foregroundcolor green
}

} -ArgumentList
$ServiceInstance,

$VerbosePreference

return $ManifestList
}
}
Export-ModuleMember -Function Set-ServiceStartupModeRemotely[/code]

I added this function to my New-Instance script and call the function after the service has started.

WARNING: Waiting for service ‘Microsoft Dynamics NAV Server [NAV71_KAPPI] (MicrosoftDynamicsNavServer$NAV71_KAPPI)’ to start…

LOCALHOST : Successfully changed MicrosoftDynamicsNavServer$NAV71_KAPPI service to delayed start

Create Instance, Web Client and ClickOnce with PowerShell

In my earlier posts I showed how to create the databases and how to install Dynamics NAV on the server machines.

The next step is to create an instance for every application and mount the default tenant.  The default tenant is the only one that will be able to change the application data.

To finish this demonstration I will show how I mount an empty tenant database and create a new company with clickonce installation for windows client and an active web client.

For each installation machine I create a separate folder. Within that folder I create a settings file, Set-MachineSettings.ps1

[code lang=”powershell”]$NAV_RemoteMachineAddress = ‘kl4msas37’
$ClientServicesCredentialType = ‘Windows’
[/code]

On my out-facing machine the client credential type is ‘NavUserPassword’. My second settings file is for the instance, Set-Instancesettings.ps1

[code lang=”powershell”]$dbNamePrefix = ‘NAV71_L01_’
$ServiceInstance = $dbNamePrefix + ‘APP’
$DatabaseName = $ServiceInstance
$SQL_DatabaseInstance = ”
$SQL_RemoteMachineAddress = ‘SQLSERVER’
$ManagementServicesPort = 7100
$NAV_WindowsServiceDomain = ‘KAPPI’
$NAV_WindowsServiceAccount = ‘NAVSERVICE’
$NAV_WindowsServiceAccountPassword = ‘NavPass@Word’
$ClientServicesPort = $ManagementServicesPort + 1
$SOAPServicesPort = $ClientServicesPort + 1
$ODataServicesPort = $SOAPServicesPort + 1
$ClickOnceWebSitePort = 80
$DefTenant = $dbNamePrefix + ‘DEF’
$DemoName = ‘www.kappi.is’
$OtherTenants = ”[/code]

With these setting I can create the instance remotly and change the settings to multitenant.

[code lang=”powershell”]Set-StrictMode -Version 2.0

Import-Module ‘N:\NAV2013R2\NAVDVD\WindowsPowerShellScripts\Cloud\NAVRemoteAdministrationSamples\NAVRemoteAdministrationSamples.psm1’ -DisableNameChecking
Import-Module ‘N:\NAV2013R2\NAVDVD\WindowsPowerShellScripts\Cloud\NAVRemoteAdministration\Misc\Import-NAVAdministrationModuleRemotely.ps1’ -DisableNameChecking

# Import settings
$PSScriptRootV2 = Split-Path $MyInvocation.MyCommand.Definition -Parent
. (Join-Path $PSScriptRootV2 ‘..\Set-DeploySettings.ps1’)
. (Join-Path $PSScriptRootV2 ‘Set-MachineSettings.ps1’)
. (Join-Path $PSScriptRootV2 ‘..\Set-InstanceSettings.ps1’)

#New-NavAdminSession
[int]$currentMemoryLimitPerPSSessionInMB = Get-MaxMemoryPerShellRemotely -RemoteMachineAddress $NAV_RemoteMachineAddress
$requiredMemoryLimitPerSessionInMB = 1024
if(($currentMemoryLimitPerPSSessionInMB -ne 0) -and ($currentMemoryLimitPerPSSessionInMB -lt $requiredMemoryLimitPerSessionInMB))
{
Set-MaxMemoryPerShellRemotely -Value $requiredMemoryLimitPerSessionInMB -RemoteMachineAddress $NAV_RemoteMachineAddress
}

Write-Verbose "Creating remote PS session on $NAV_RemoteMachineAddress…"
$Session = New-PSSession -ComputerName $NAV_RemoteMachineAddress
Write-Verbose "Done creating remote PS session on $NAV_RemoteMachineAddress."

try {
# Import the module into the remote PS session
Invoke-Command -Session $Session -ScriptBlock {
param([string] $ModulePath, [string] $verbosePreference)
$VerbosePreference = $verbosePreference
Set-ExecutionPolicy -ExecutionPolicy RemoteSigned
Import-Module $ModulePath
Add-PSSnapin "Microsoft.Dynamics.Nav.Management" -ErrorAction SilentlyContinue
} -ArgumentList (Join-Path $NAV_AdminRemoteDirectory ‘NAVAdministration.psm1’), $VerbosePreference
Write-Verbose "================================================================================"
Write-Verbose ("Deploy-NewInstance starting at " + (Get-Date).ToLongTimeString() + "…")
Write-Verbose "================================================================================"

New-NAVServerInstanceRemotely -ServiceInstance $ServiceInstance -DatabaseName $DatabaseName -DatabaseInstance $SQL_DatabaseInstance -DatabaseServer $SQL_RemoteMachineAddress -ManagementServicesPort $ManagementServicesPort -ClientServicesPort $ClientServicesPort -SOAPServicesPort $SOAPServicesPort -ODataServicesPort $ODataServicesPort -RemoteMachineShortName $NAV_WindowsServiceDomain -ServiceAccount $NAV_WindowsServiceAccount -ServiceAccountPassword $NAV_WindowsServiceAccountPassword -Session $Session
Set-NAVServerConfigurationRemotely -KeyName "ClientServicesCredentialType" -KeyValue $ClientServicesCredentialType -ServerInstance $ServiceInstance -Session $Session
Set-NAVServerConfigurationRemotely -KeyName "MultiTenant" -KeyValue "true" -ServerInstance $ServiceInstance -Session $Session
Set-NAVServerConfigurationRemotely -KeyName "DatabaseName" -KeyValue "" -ServerInstance $ServiceInstance -Session $Session
Set-NAVServerConfigurationRemotely -KeyName ‘NASServicesStartupCodeunit’ -KeyValue ‘450’ -ServerInstance $ServiceInstance -Session $Session
Set-NAVServerConfigurationRemotely -KeyName ‘NASServicesStartupMethod’ -KeyValue ” -ServerInstance $ServiceInstance -Session $Session
Set-NAVServerConfigurationRemotely -KeyName ‘NASServicesStartupArgument’ -KeyValue ‘JOBQUEUE’ -ServerInstance $ServiceInstance -Session $Session

$fullAccountName = $NAV_WindowsServiceDomain + "\" + $NAV_WindowsServiceAccount

# Install certificates
[System.Security.SecureString]$clientServicesPfxPasswordAsSecureString = ConvertTo-SecureString $ClientServicesPfxPassword -AsPlainText -Force
if ($ClientServicesPfxFile -ne $null)
{
Install-ClientServicesCertificate
-ServiceInstance $ServiceInstance

-ServiceAccount $fullAccountName
-ClientServicesPfxFile $ClientServicesPfxFile

-ClientServicesPfxPassword $clientServicesPfxPasswordAsSecureString
-Session $Session
}

Start-ServiceRemotely -ServiceName ("MicrosoftDynamicsNavServer$" + $ServiceInstance) -Session $Session

Write-Verbose "================================================================================"
Write-Verbose ("Deploy-NewInstance finished at " + (Get-Date).ToLongTimeString() + ".")
Write-Verbose "================================================================================"

Write-Output "The NAV Server virtual machine is: $NAV_RemoteMachineAddress"
Write-Output "The NAV Server Instance is: $ServiceInstance"
Write-Output "The NAV Server account credentials: $NAV_WindowsServiceDomain\$NAV_WindowsServiceAccount/$NAV_WindowsServiceAccountPassword"

}
catch [System.Exception] {
Remove-NAVAdminSession -Session $Session
throw $_.Exception
}

finally
{
Remove-NAVAdminSession -Session $Session
}

[/code]

I also activate the JOBQUEUE NAS function by default.
I use the CRONUS database as the default tenant and that is the only one with write access to the application database. My next step is to mount the application and the default tenant and add required users to the default tenant.

[code lang="powershell"]Set-StrictMode -Version 2.0

Import-Module 'N:\NAV2013R2\NAVDVD\WindowsPowerShellScripts\Cloud\NAVRemoteAdministrationSamples\NAVRemoteAdministrationSamples.psm1' -DisableNameChecking
Import-Module 'N:\NAV2013R2\NAVDVD\WindowsPowerShellScripts\Cloud\NAVRemoteAdministration\Misc\Import-NAVAdministrationModuleRemotely.ps1' -DisableNameChecking

# Import settings
$PSScriptRootV2 = Split-Path $MyInvocation.MyCommand.Definition -Parent
. (Join-Path $PSScriptRootV2 '..\Set-DeploySettings.ps1')
. (Join-Path $PSScriptRootV2 'Set-MachineSettings.ps1')
. (Join-Path $PSScriptRootV2 '..\Set-InstanceSettings.ps1')

#New-NavAdminSession
[int]$currentMemoryLimitPerPSSessionInMB = Get-MaxMemoryPerShellRemotely -RemoteMachineAddress $NAV_RemoteMachineAddress
$requiredMemoryLimitPerSessionInMB = 1024
if(($currentMemoryLimitPerPSSessionInMB -ne 0) -and ($currentMemoryLimitPerPSSessionInMB -lt $requiredMemoryLimitPerSessionInMB))
{
Set-MaxMemoryPerShellRemotely -Value $requiredMemoryLimitPerSessionInMB -RemoteMachineAddress $NAV_RemoteMachineAddress
}

Write-Verbose "Creating remote PS session on $NAV_RemoteMachineAddress..."
$Session = New-PSSession -ComputerName $NAV_RemoteMachineAddress
Write-Verbose "Done creating remote PS session on $NAV_RemoteMachineAddress."

try {
# Import the module into the remote PS session
Invoke-Command -Session $Session -ScriptBlock {
param([string] $ModulePath, [string] $verbosePreference)
$VerbosePreference = $verbosePreference
Set-ExecutionPolicy -ExecutionPolicy RemoteSigned
Import-Module $ModulePath
Add-PSSnapin "Microsoft.Dynamics.Nav.Management" -ErrorAction SilentlyContinue
} -ArgumentList (Join-Path $NAV_AdminRemoteDirectory 'NAVAdministration.psm1'), $VerbosePreference

Write-Verbose "Mount Application to $ServiceInstance..."
Invoke-Command -Session $session -ScriptBlock

{
param([string]$ServiceInstance, [string]$DatabaseServer, [string]$DatabaseInstance, [string]$DatabaseName)
Mount-NAVApplication -ServerInstance $ServiceInstance -DatabaseServer $DatabaseServer -DatabaseInstance $DatabaseInstance -DatabaseName $DatabaseName

}
-ArgumentList $ServiceInstance, $SQL_RemoteMachineAddress, $SQL_DatabaseInstance, $DatabaseName

Write-Verbose "Mount Default Tenant to $ServiceInstance..."
Invoke-Command -Session $session -ScriptBlock

{
param([string]$ServiceInstance, [string]$DatabaseServer, [string]$DatabaseInstance, [string]$DatabaseName, [string]$AlternateId)
Mount-NAVTenant -ServerInstance $ServiceInstance -DatabaseServer $DatabaseServer -DatabaseInstance $DatabaseInstance -DatabaseName $DatabaseName -Id Default -OverwriteTenantIdInDatabase -AllowAppDatabaseWrite -AlternateId $AlternateId

}
-ArgumentList $ServiceInstance, $SQL_RemoteMachineAddress, $SQL_DatabaseInstance, $DefTenant, $DemoName

[System.Security.SecureString]$NAVPasswordAsSecureString = ConvertTo-SecureString $NAVAdminPassword -AsPlainText -Force
New-NAVServerUserRemotely -UserName $NAVAdminUserName -Password $NAVPasswordAsSecureString -ServerInstance $ServiceInstance -Tenant Default -Session $Session
New-NAVServerUserPermissionSetRemotely -UserName $NAVAdminUserName -PermissionSetId "SUPER" -ServerInstance $ServiceInstance -Tenant Default -Session $Session

$fullUserName = $NAV_WindowsServiceDomain + "\" + $NAV_WindowsServiceAccount
New-NAVServerUserRemotely -WindowsAccount $fullUserName -ServerInstance $ServiceInstance -Tenant Default -Session $Session
New-NAVServerUserPermissionSetRemotely -UserName $fullUserName -PermissionSetId "SUPER" -ServerInstance $ServiceInstance -Tenant Default -Session $Session

$fullUserName = $NAV_WindowsServiceDomain + "\kappi"
New-NAVServerUserRemotely -WindowsAccount $fullUserName -ServerInstance $ServiceInstance -Tenant Default -Session $Session
New-NAVServerUserPermissionSetRemotely -UserName $fullUserName -PermissionSetId "SUPER" -ServerInstance $ServiceInstance -Tenant Default -Session $Session

if ($OtherTenants -ne '') {
foreach ($tenant in $OtherTenants)
{
$tenantdbName = $dbNamePrefix + $tenant
Write-Verbose "Mount Tenant $tenant to $ServiceInstance..."
Invoke-Command -Session $session -ScriptBlock

{
param([string]$ServiceInstance, [string]$DatabaseServer, [string]$DatabaseInstance, [string]$TenantId, [string]$DatabaseName)
Mount-NAVTenant -ServerInstance $ServiceInstance -DatabaseServer $DatabaseServer -DatabaseInstance $DatabaseInstance -DatabaseName $DatabaseName -Id $TenantId -OverwriteTenantIdInDatabase

}
-ArgumentList $ServiceInstance, $SQL_RemoteMachineAddress, $SQL_DatabaseInstance, $tenant, $tenantdbName

}
}

Write-Verbose "Done Mounting Tenants to Server Instance $ServiceInstance on $NAV_RemoteMachineAddress."

}
catch [System.Exception] {
Remove-NAVAdminSession -Session $Session
throw $_.Exception
}

finally
{
Remove-NAVAdminSession -Session $Session
}

[/code]

If something when wrong I can use this script to remote the instance and try again.

[code lang="powershell"]Set-StrictMode -Version 2.0

Import-Module 'N:\NAV2013R2\NAVDVD\WindowsPowerShellScripts\Cloud\NAVRemoteAdministrationSamples\NAVRemoteAdministrationSamples.psm1' -DisableNameChecking
Import-Module 'N:\NAV2013R2\NAVDVD\WindowsPowerShellScripts\Cloud\NAVRemoteAdministration\Misc\Import-NAVAdministrationModuleRemotely.ps1' -DisableNameChecking

# Import settings
$PSScriptRootV2 = Split-Path $MyInvocation.MyCommand.Definition -Parent
. (Join-Path $PSScriptRootV2 '..\Set-DeploySettings.ps1')
. (Join-Path $PSScriptRootV2 'Set-MachineSettings.ps1')
. (Join-Path $PSScriptRootV2 '..\Set-InstanceSettings.ps1')

#New-NavAdminSession
[int]$currentMemoryLimitPerPSSessionInMB = Get-MaxMemoryPerShellRemotely -RemoteMachineAddress $NAV_RemoteMachineAddress
$requiredMemoryLimitPerSessionInMB = 1024
if(($currentMemoryLimitPerPSSessionInMB -ne 0) -and ($currentMemoryLimitPerPSSessionInMB -lt $requiredMemoryLimitPerSessionInMB))
{
Set-MaxMemoryPerShellRemotely -Value $requiredMemoryLimitPerSessionInMB -RemoteMachineAddress $NAV_RemoteMachineAddress
}

Write-Verbose "Creating remote PS session on $NAV_RemoteMachineAddress..."
$Session = New-PSSession -ComputerName $NAV_RemoteMachineAddress
Write-Verbose "Done creating remote PS session on $NAV_RemoteMachineAddress."

try {
# Import the module into the remote PS session
Invoke-Command -Session $Session -ScriptBlock {
param([string] $ModulePath, [string] $verbosePreference)
$VerbosePreference = $verbosePreference
Set-ExecutionPolicy -ExecutionPolicy RemoteSigned
Import-Module $ModulePath
Add-PSSnapin "Microsoft.Dynamics.Nav.Management" -ErrorAction SilentlyContinue
} -ArgumentList (Join-Path $NAV_AdminRemoteDirectory 'NAVAdministration.psm1'), $VerbosePreference

Write-Verbose "Removing Server Instance $ServiceInstance..."
Invoke-Command -Session $session -ScriptBlock

{
param([string]$ServiceInstance)
Remove-NAVServerInstance -ServerInstance $ServiceInstance -Force

}
-ArgumentList $ServiceInstance

Write-Verbose "Done Removing Server Instance $ServiceInstance on $NAV_RemoteMachineAddress."

}
catch [System.Exception] {
Remove-NAVAdminSession -Session $Session
throw $_.Exception
}

finally
{
Remove-NAVAdminSession -Session $Session
}

[/code]

All the framework is installed and running. Now it is time to prepare the system for my first client. I create a folder for each client and in that folder I put the Set-TenantSettings.ps1 file.

[code lang="powershell"]# Select Instance
. (Join-Path $PSScriptRootV2 '..\Set-InstanceSettings.ps1')

# Install on machines
$InstallOnMachines = ('PUBLICSERVER','LOCALSERVER','DEVSERVER')

# Next available Tenant
$TenantId = '01'
# Domain name for ClickOnce and Web Client
$AlternateIds = @("microsoft.kappi.is")
$EndpointIdentifier = 'microsoft.kappi.is'

# New Company Name
$NewCompanyName = 'Microsoft'

# Activate NAS
$EnableNAS = '$true'

# Web Client Path
$WebServerInstance = 'NAV71_MS'

# ClickOnce Client Application Name - No need to change
$applicationName = "Microsoft Dynamics NAV 2013 R2 for $NewCompanyName"

# First Super User
$ClientUserName = 'Gunnar'
$ClientUserPassword = 'gunnar@dynamics.is'
[/code]

Now I am all set to mount my first tenant and create the first company. This script will also create the users needed for the tenant.

[code lang="powershell"]Set-StrictMode -Version 2.0

Import-Module 'N:\NAV2013R2\NAVDVD\WindowsPowerShellScripts\Cloud\NAVRemoteAdministrationSamples\NAVRemoteAdministrationSamples.psm1' -DisableNameChecking
Import-Module 'N:\NAV2013R2\NAVDVD\WindowsPowerShellScripts\Cloud\NAVRemoteAdministration\Misc\Import-NAVAdministrationModuleRemotely.ps1' -DisableNameChecking

# Import settings
$PSScriptRootV2 = Split-Path $MyInvocation.MyCommand.Definition -Parent
. (Join-Path $PSScriptRootV2 '..\Set-DeploySettings.ps1')
. (Join-Path $PSScriptRootV2 'Set-TenantSettings.ps1')

$LoopCount = 0

# Loop Machines
foreach ($InstallOnMachine in $InstallOnMachines)
{
$MachineSettingsPath = '..\' + $InstallOnMachine + '\Set-MachineSettings.ps1'
. (Join-Path $PSScriptRootV2 $MachineSettingsPath)
$LoopCount = $LoopCount + 1

#New-NavAdminSession
[int]$currentMemoryLimitPerPSSessionInMB = Get-MaxMemoryPerShellRemotely -RemoteMachineAddress $NAV_RemoteMachineAddress
$requiredMemoryLimitPerSessionInMB = 1024
if(($currentMemoryLimitPerPSSessionInMB -ne 0) -and ($currentMemoryLimitPerPSSessionInMB -lt $requiredMemoryLimitPerSessionInMB))
{
Set-MaxMemoryPerShellRemotely -Value $requiredMemoryLimitPerSessionInMB -RemoteMachineAddress $NAV_RemoteMachineAddress
}

Write-Verbose "Creating remote PS session on $NAV_RemoteMachineAddress..."
$Session = New-PSSession -ComputerName $NAV_RemoteMachineAddress
Write-Verbose "Done creating remote PS session on $NAV_RemoteMachineAddress."

try {
# Import the module into the remote PS session
Invoke-Command -Session $Session -ScriptBlock {
param([string] $ModulePath, [string] $verbosePreference)
$VerbosePreference = $verbosePreference
Set-ExecutionPolicy -ExecutionPolicy RemoteSigned
Import-Module $ModulePath
Add-PSSnapin "Microsoft.Dynamics.Nav.Management" -ErrorAction SilentlyContinue
} -ArgumentList (Join-Path $NAV_AdminRemoteDirectory 'NAVAdministration.psm1'), $VerbosePreference

$DatabaseName = $dbNamePrefix + $TenantId
Invoke-Command -Session $Session -ScriptBlock

{
param([string]$TenandId, [string]$ServiceInstance, [string]$DatabaseName, [string]$DatabaseInstance, [string]$DatabaseServer, [string]$AlternateId, [string]$DefaultCompanyName, [string]$EnableNAS, [int]$LoopCount)
if ($EnableNAS -eq ‘$true’)
{
Mount-NAVTenant -Id $TenandId -ServerInstance $ServiceInstance -DatabaseName $DatabaseName -DatabaseServer $DatabaseServer -DatabaseInstance $DatabaseInstance -AlternateId $AlternateId -DefaultCompany $DefaultCompanyName -NasServicesEnabled -OverwriteTenantIdInDatabase
}
else
{
Mount-NAVTenant -Id $TenandId -ServerInstance $ServiceInstance -DatabaseName $DatabaseName -DatabaseServer $DatabaseServer -DatabaseInstance $DatabaseInstance -AlternateId $AlternateId -DefaultCompany $DefaultCompanyName -OverwriteTenantIdInDatabase
}
if ($LoopCount -eq 1)
{
New-NAVCompany -ServerInstance $ServiceInstance -Tenant $TenandId -CompanyName $DefaultCompanyName
}
}
-ArgumentList $TenantId, $ServiceInstance, $DatabaseName, $SQL_DatabaseInstance, $SQL_RemoteMachineAddress, $AlternateIds, $NewCompanyName, $EnableNAS, $LoopCount

if ($LoopCount -eq 1)
{
[System.Security.SecureString]$NAVPasswordAsSecureString = ConvertTo-SecureString $NAVAdminPassword -AsPlainText -Force
New-NAVServerUserRemotely -UserName $NAVAdminUserName -Password $NAVPasswordAsSecureString -ServerInstance $ServiceInstance -Tenant $TenantId -Session $Session -ChangePasswordAtNextLogOn $false
New-NAVServerUserPermissionSetRemotely -UserName $NAVAdminUserName -PermissionSetId "SUPER" -ServerInstance $ServiceInstance -Tenant $TenantId -Session $Session

$fullUserName = $NAV_WindowsServiceDomain + "\" + $NAV_WindowsServiceAccount
New-NAVServerUserRemotely -WindowsAccount $fullUserName -ServerInstance $ServiceInstance -Tenant $TenantId -Session $Session
New-NAVServerUserPermissionSetRemotely -UserName $fullUserName -PermissionSetId "SUPER" -ServerInstance $ServiceInstance -Tenant $TenantId -Session $Session

[System.Security.SecureString]$ClientUserPasswordAsSecureString = ConvertTo-SecureString $ClientUserPassword -AsPlainText -Force
New-NAVServerUserRemotely -UserName $ClientUserName -Password $ClientUserPasswordAsSecureString -ServerInstance $ServiceInstance -Tenant $TenantId -Session $Session -ChangePasswordAtNextLogOn $true
New-NAVServerUserPermissionSetRemotely -UserName $NAVAdminUserName -PermissionSetId "SUPER" -ServerInstance $ServiceInstance -Tenant $TenantId -Session $Session
}
}
catch [System.Exception] {
Remove-NAVAdminSession -Session $Session
throw $_.Exception
}

finally
{
Remove-NAVAdminSession -Session $Session
}

}[/code]

All set and now to create the client distribution with clickonce.

[code lang="powershell"]Set-StrictMode -Version 2.0

Import-Module 'N:\NAV2013R2\NAVDVD\WindowsPowerShellScripts\Cloud\NAVRemoteAdministrationSamples\NAVRemoteAdministrationSamples.psm1' -DisableNameChecking
Import-Module 'N:\NAV2013R2\NAVDVD\WindowsPowerShellScripts\Cloud\NAVRemoteAdministration\Misc\Import-NAVAdministrationModuleRemotely.ps1' -DisableNameChecking
Import-Module 'N:\NAV2013R2\NAVDVD\WindowsPowerShellScripts\Cloud\NAVRemoteAdministration\ClickOnce\Test-MageExePrerequisite.ps1' -DisableNameChecking
Import-Module 'N:\NAV2013R2\NAVDVD\WindowsPowerShellScripts\Cloud\NAVRemoteAdministration\ClickOnce\Get-MageExeLocation.ps1' -DisableNameChecking
Import-Module 'N:\NAV2013R2\NAVDVD\WindowsPowerShellScripts\Cloud\NAVRemoteAdministration\ClickOnce\Set-ManifestSignatureRemotely.ps1' -DisableNameChecking
Import-Module 'N:\NAV2013R2\NAVDVD\WindowsPowerShellScripts\Cloud\NAVRemoteAdministration\ClickOnce\New-ClickOnceWebSiteRemotely.ps1' -DisableNameChecking

# Import settings
$PSScriptRootV2 = Split-Path $MyInvocation.MyCommand.Definition -Parent
. (Join-Path $PSScriptRootV2 '..\Set-DeploySettings.ps1')
. (Join-Path $PSScriptRootV2 '..\PUBLICSERVER\Set-MachineSettings.ps1')
. (Join-Path $PSScriptRootV2 '..\Set-InstanceSettings.ps1')
. (Join-Path $PSScriptRootV2 'Set-TenantSettings')

#New-NavAdminSession
[int]$currentMemoryLimitPerPSSessionInMB = Get-MaxMemoryPerShellRemotely -RemoteMachineAddress $NAV_RemoteMachineAddress
$requiredMemoryLimitPerSessionInMB = 1024
if(($currentMemoryLimitPerPSSessionInMB -ne 0) -and ($currentMemoryLimitPerPSSessionInMB -lt $requiredMemoryLimitPerSessionInMB))
{
Set-MaxMemoryPerShellRemotely -Value $requiredMemoryLimitPerSessionInMB -RemoteMachineAddress $NAV_RemoteMachineAddress
}

Write-Verbose "Creating remote PS session on $NAV_RemoteMachineAddress..."
$Session = New-PSSession -ComputerName $NAV_RemoteMachineAddress
Write-Verbose "Done creating remote PS session on $NAV_RemoteMachineAddress."

try {
# Import the module into the remote PS session
Invoke-Command -Session $Session -ScriptBlock {
param([string] $ModulePath, [string] $verbosePreference)
$VerbosePreference = $verbosePreference
Set-ExecutionPolicy -ExecutionPolicy RemoteSigned
Import-Module $ModulePath
Add-PSSnapin "Microsoft.Dynamics.Nav.Management" -ErrorAction SilentlyContinue
} -ArgumentList (Join-Path $NAV_AdminRemoteDirectory 'NAVAdministration.psm1'), $VerbosePreference

# Create a web site that holds a ClickOnce deployment of the Windows client
Write-Verbose "Creating a web site that holds a ClickOnce deployment of the Windows client on $NAV_RemoteMachineAddress..."
[System.Security.SecureString]$clickOnceCodeSigningPfxPasswordAsSecureString = $null
if ($ClickOnceCodeSigningPfxPassword)
{
$clickOnceCodeSigningPfxPasswordAsSecureString = ConvertTo-SecureString $ClickOnceCodeSigningPfxPassword -AsPlainText -Force
}
$Port = $ClickOnceWebSitePort
$clickOnceDeploymentId = "ClickOnce_${ServiceInstance}_at_${EndpointIdentifier}"
$clickOnceDirectory = Join-Path 'C:\inetpub' $clickOnceDeploymentId
$webSiteUrl = ("http://" + $EndpointIdentifier + ":" + $Port)

Write-Verbose "Creating new ClickOnce web site for server instance $ServiceInstance at address $webSiteUrl..."

# Create and populate the directory on the remote machine
[xml]$clientUserSettings = New-NAVClientUserSettings

-Session $Session
-Server $EndpointIdentifier

-ServerInstance $ServiceInstance
-ClientServicesPort $ClientServicesPort

-DnsIdentity $dnsIdentity
Edit-NAVClientUserSettings -ClientUserSettings $ClientUserSettings  -KeyName ‘ClientServicesCredentialType’ -NewValue $ClientServicesCredentialType
Edit-NAVClientUserSettings -ClientUserSettings $ClientUserSettings  -KeyName ‘ServicesCertificateValidationEnabled’ -NewValue false
Edit-NAVClientUserSettings -ClientUserSettings $ClientUserSettings  -KeyName ‘TenantId’ -NewValue $TenantId

Invoke-Command -Session $session -ScriptBlock {
param([string]$ClickOnceDirectory,[xml]$ClientUserSettings)
New-ClickOnceDirectory
-ClickOnceDirectory $ClickOnceDirectory

-ClientUserSettings $ClientUserSettings
} -ArgumentList $clickOnceDirectory,$ClientUserSettings

# Copy the mage.exe file to the remote machine
if (!(Test-MageExePrerequisite))
{
Write-Error ‘Mage.exe not found or wrong version. Install the Windows SDK from http://www.microsoft.com/en-us/download/details.aspx?displaylang=en&id=8279, and set up the global variable MageExeFile ($Global:MageFileExe) to point to the executable, probably C:\Program Files\Microsoft SDKs\Windows\v7.1\Bin\NETFX 4.0 Tools\mage.exe.’
return $null
}
$localMageExeFile = Get-MageExeLocation
$remoteMageExeFile = Join-Path $NAV_RemoteFolder (Split-Path -Leaf $localMageExeFile)
Copy-FileToRemoteMachine -SourceFile $localMageExeFile -DestinationFile $remoteMageExeFile -Session $session

# Adjust the application manifest (Microsoft.Dynamics.Nav.Client.exe.manifest)
$clickOnceApplicationFilesDirectory = Join-Path $clickOnceDirectory ‘Deployment\ApplicationFiles’
$applicationManifestFile = Join-Path $clickOnceApplicationFilesDirectory ‘Microsoft.Dynamics.Nav.Client.exe.manifest’
$applicationIdentityName = "$clickOnceDeploymentId application identity" # any unique value will do
$applicationIdentityVersion = "1.0.0.0" # any version will do; it is not tied to the product version
Invoke-Command -Session $session -ScriptBlock {
param([string]$ApplicationManifestFile,[string]$ApplicationIdentityName,[string]$ApplicationIdentityVersion,[string]$ApplicationFilesDirectory,[string]$RemoteMageExeFile)

# Note: be careful to run "mage -update" before you change settings, because "mage -update" sometimes resets some of the other settings
Set-ApplicationManifestFileList
-ApplicationManifestFile $ApplicationManifestFile

-ApplicationFilesDirectory $ApplicationFilesDirectory
-MageExeLocation $RemoteMageExeFile
Set-ApplicationManifestApplicationIdentity

-ApplicationManifestFile $ApplicationManifestFile
-ApplicationIdentityName $ApplicationIdentityName

-ApplicationIdentityVersion $ApplicationIdentityVersion
} -ArgumentList $applicationManifestFile,$applicationIdentityName,$applicationIdentityVersion,$ClickOnceApplicationFilesDirectory,$remoteMageExeFile

# Sign the application manifest
if ($ClickOnceCodeSigningPfxFile)
{
Set-ManifestSignatureRemotely
-ManifestFile $applicationManifestFile

-CodeSigningPfxFile $ClickOnceCodeSigningPfxFile
-CodeSigningPfxPassword $clickOnceCodeSigningPfxPasswordAsSecureString

-MageExeLocation $localMageExeFile
-Session $Session
}

# Adjust the deployment manifest (Microsoft.Dynamics.Nav.Client.application)
$deploymentManifestFile = Join-Path $clickOnceDirectory 'Deployment\Microsoft.Dynamics.Nav.Client.application'
$deploymentIdentityName = "$clickOnceDeploymentId deployment identity" # any unique value will do
$deploymentIdentityVersion = "1.0.0.0" # any version will do; it is not tied to the product version
$deploymentManifestUrl = ($webSiteUrl + "/Deployment/Microsoft.Dynamics.Nav.Client.application")
$applicationManifestUrl = ($webSiteUrl + "/Deployment/ApplicationFiles/Microsoft.Dynamics.Nav.Client.exe.manifest")
Invoke-Command -Session $session -ScriptBlock {
param([string]$DeploymentManifestFile,[string]$ApplicationManifestFile,[string]$DeploymentIdentityName,[string]$DeploymentIdentityVersion,[string]$ApplicationPublisher,[string]$ApplicationName,[string]$DeploymentManifestUrl,[string]$ApplicationManifestUrl,[string]$RemoteMageExeFile)
# Note: be careful to run "mage -update" before you change settings, because "mage -update" sometimes resets some of the other settings
Set-DeploymentManifestApplicationReference

-DeploymentManifestFile $DeploymentManifestFile
-ApplicationManifestFile $ApplicationManifestFile

-ApplicationManifestUrl $ApplicationManifestUrl
-MageExeLocation $RemoteMageExeFile
Set-DeploymentManifestSettings

-DeploymentManifestFile $DeploymentManifestFile
-DeploymentIdentityName $DeploymentIdentityName

-DeploymentIdentityVersion $DeploymentIdentityVersion
-ApplicationPublisher $ApplicationPublisher

-ApplicationName $ApplicationName
-DeploymentManifestUrl $DeploymentManifestUrl
} -ArgumentList $deploymentManifestFile,$applicationManifestFile,$deploymentIdentityName,$deploymentIdentityVersion,$ApplicationPublisher,$ApplicationName,$deploymentManifestUrl,$applicationManifestUrl,$remoteMageExeFile

# Sign the deployment manifest
if ($ClickOnceCodeSigningPfxFile)
{
Set-ManifestSignatureRemotely

-ManifestFile $deploymentManifestFile
-CodeSigningPfxFile $ClickOnceCodeSigningPfxFile

-CodeSigningPfxPassword $clickOnceCodeSigningPfxPasswordAsSecureString
-MageExeLocation $localMageExeFile

-Session $Session
}

# Create the actual web site
Invoke-Command -Session $session -ScriptBlock {
param([string]$WebSiteName,[int]$Port,[string]$PhysicalPath, [string]$EndpointIdentifier, [string]$AdminRemoteDirectory)
Write-Verbose "Creating new web site $WebSiteName on port $Port, pointing to $PhysicalPath…"

# Create the web site
$website = New-Website -Name $WebSiteName -Port $Port -PhysicalPath $PhysicalPath -HostHeader $EndpointIdentifier -Force

# Put a web.config file in the root folder, which will tell IIS which .html file to open
$sourceFile = Join-Path $AdminRemoteDirectory ‘ClickOnce\Resources\root_web.config’
$targetFile = Join-Path $PhysicalPath ‘web.config’
Copy-Item $sourceFile -destination $targetFile

# Put a web.config file in the Deployment folder, which will tell IIS to allow downloading of .config files etc.
$sourceFile = Join-Path $AdminRemoteDirectory ‘ClickOnce\Resources\deployment_web.config’
$targetFile = Join-Path $PhysicalPath ‘Deployment\web.config’
Copy-Item $sourceFile -destination $targetFile

# Open firewall on the given port
New-FirewallPortAllowRule -RuleName $WebSiteName -Port $Port

Write-Verbose "Done creating new web site $WebSiteName on port $Port, pointing to $PhysicalPath."

} -ArgumentList $clickOnceDeploymentId,$Port,$clickOnceDirectory, $EndpointIdentifier, $NAV_AdminRemoteDirectory

Write-Verbose "Done creating new ClickOnce web site for server instance $ServiceInstance at address $webSiteUrl."
Write-Output "The Windows Client can be downloaded via ClickOnce at: $webSiteUrl"

}
catch [System.Exception] {
Remove-NAVAdminSession -Session $Session
throw $_.Exception
}

finally
{
Remove-NAVAdminSession -Session $Session
}

[/code]

And finally the web client.

[code lang=”powershell”]Set-StrictMode -Version 2.0

Import-Module ‘N:\NAV2013R2\NAVDVD\WindowsPowerShellScripts\Cloud\NAVRemoteAdministrationSamples\NAVRemoteAdministrationSamples.psm1’ -DisableNameChecking
Import-Module ‘N:\NAV2013R2\NAVDVD\WindowsPowerShellScripts\Cloud\NAVRemoteAdministration\Misc\Import-NAVAdministrationModuleRemotely.ps1’ -DisableNameChecking

# Import settings
$PSScriptRootV2 = Split-Path $MyInvocation.MyCommand.Definition -Parent
. (Join-Path $PSScriptRootV2 ‘..\Set-DeploySettings.ps1’)
. (Join-Path $PSScriptRootV2 ‘..\PUBLICSERVER\Set-MachineSettings.ps1’)
. (Join-Path $PSScriptRootV2 ‘..\Set-InstanceSettings.ps1’)
. (Join-Path $PSScriptRootV2 ‘Set-TenantSettings’)

#New-NavAdminSession
[int]$currentMemoryLimitPerPSSessionInMB = Get-MaxMemoryPerShellRemotely -RemoteMachineAddress $NAV_RemoteMachineAddress
$requiredMemoryLimitPerSessionInMB = 1024
if(($currentMemoryLimitPerPSSessionInMB -ne 0) -and ($currentMemoryLimitPerPSSessionInMB -lt $requiredMemoryLimitPerSessionInMB))
{
Set-MaxMemoryPerShellRemotely -Value $requiredMemoryLimitPerSessionInMB -RemoteMachineAddress $NAV_RemoteMachineAddress
}

Write-Verbose "Creating remote PS session on $NAV_RemoteMachineAddress…"
$Session = New-PSSession -ComputerName $NAV_RemoteMachineAddress
Write-Verbose "Done creating remote PS session on $NAV_RemoteMachineAddress."

try {
# Import the module into the remote PS session
Invoke-Command -Session $Session -ScriptBlock {
param([string] $ModulePath, [string] $verbosePreference)
$VerbosePreference = $verbosePreference
Set-ExecutionPolicy -ExecutionPolicy RemoteSigned
Import-Module $ModulePath
Add-PSSnapin "Microsoft.Dynamics.Nav.Management" -ErrorAction SilentlyContinue
} -ArgumentList (Join-Path $NAV_AdminRemoteDirectory ‘NAVAdministration.psm1’), $VerbosePreference

Invoke-Command -Session $Session -ScriptBlock
{
param([string]$ClientServicesCredentialType, [int]$ClientServicesPort, [string]$Company, [string]$DnsIdentity, [string]$Server, [string]$ServerInstance, [string]$WebServerInstance, [string]$language, [string]$regionFormat, [string]$TenantId)

Write-Host "Creating Web Site " $WebServerInstance " ..."
New-NAVWebServerInstance -ClientServicesCredentialType $ClientServicesCredentialType -ClientServicesPort $ClientServicesPort -Company $Company -DnsIdentity $DnsIdentity -Server $Server -ServerInstance $ServerInstance -WebServerInstance $WebServerInstance -Language $language -RegionFormat $regionFormat

}
-ArgumentList $ClientServicesCredentialType, $ClientServicesPort, $NewCompanyName, $DnsIdentity, $ServerDNSName, $ServiceInstance, $WebServerInstance, $WebServerLanguage, $WebServerRegionFormat, $TenantId
}
catch [System.Exception] {
Remove-NAVAdminSession -Session $Session
throw $_.Exception
}

finally
{
Remove-NAVAdminSession -Session $Session
}

[/code]

I am using the URL rewrite method in IIS and that has to be enabled. See this post from Microsoft on how to do that.

The next task is to create the required scripts that will install the client and the server updates and update the clickonce distribution.  This will be posted later.