Tag: PowerShell

  • Uninstall all dependant apps in Business Central On-premise

    Uninstall all dependent apps

    Ever tried to uninstall a Business Central app in PowerShell, and failed due to dependencies on that app? I hit that problem and decided to share a script to detect and uninstall all dependant apps.

    I was looking to publish an app from VS Code on a copy of a customers database so I could debug and develop locally and needed to uninstall the app first before I could deploy. The problem was, this app was a dependency for several other apps created for the customer.

    What I found is we can get a full list of apps installed with the Get-NavAppInfo cmdlet, then for each app iterate through the dependencies until we find our app:

    https://gist.github.com/DanKinsella/b3b4a534fe23204c35affc56b296d3c2.js

    The script prints out a list of app names and versions which can be used for reinstalling them after. 

    Hope this is useful for someone else.

  • Removing Business Central Docker containers with PowerShell

    Removing Business Central Docker containers with PowerShell

    Yesterday I bumped into an intermittent issue on our Jenkins CI server where some Business Central containers where not getting removed after use. This led me to find a way of removing Business Central Docker containers with PowerShell, and a topic for a blog post. The issue seems to be with a process keeping the NavContainerHelper container folder open, which is stopping the script from removing it.. anyway, that’s not what this post is about.

    As a temporary work-around while I get to the root cause of the issue, I decided to build the containers with a unique name and setup a nightly cleanup job to remove any surplus containers on the build server.

    To do this, I first need a list of containers to remove. I used the docker ps command, formatting the result a a table to make it easier to use in PowerShell:

    $containers = docker ps -a --filter "name=bc*" --format "{{.Names}}"

    Filtering

    I was using the prefix “bc” on my container names, so I’ve selected this as my filter “name=bc*”. You could also filter on the image using the ancestor filter. For example:

    $containers = docker ps -a --filter "ancestor=mcr.microsoft.com/businesscentral/sandbox:gb-ltsc2019"

    Unfortunately I couldn’t get the ancestor filter to work with a more generic image name (i.e. mcr.microsoft.com/businesscentral/sandbox) which limited it’s usefulness in my scenario.

    There is also the label filter which is useful. The Business Central images come with a number of labels which we can retrieve by querying our containers. For example:

    PS C:\WINDOWS\system32> docker ps -a --format "{{.Labels}}"
    country=gb,tag=0.0.9.97,nav=,osversion=10.0.17763.914,platform=15.0.37865.39262,created=201912201932,cu=update31,eula=https://go.microsoft.com/fwlink/?linkid=86
    1843,legal=http://go.microsoft.com/fwlink/?LinkId=837447,maintainer=Dynamics SMB,version=15.1.37881.39313
    country=W1,legal=http://go.microsoft.com/fwlink/?LinkId=826604,maintainer=Dynamics SMB,nav=2018,tag=0.0.9.2,version=11.0.19394.0,created=201903101911,cu=rtm,eul
    a=https://go.microsoft.com/fwlink/?linkid=861843,osversion=10.0.17763.316
    cu=rtm,eula=https://go.microsoft.com/fwlink/?linkid=861843,legal=http://go.microsoft.com/fwlink/?LinkId=826604,maintainer=Dynamics SMB,nav=2018,country=gb,creat
    ed=201903102009,osversion=10.0.17763.316,tag=0.0.9.2,version=11.0.19394.0

    The above output shows a list of label key/value pairs being used by containers on my machine (I’ve only got Business Central and NAV containers). One label common to all my containers is “maintainer=Dynamics SMB”, which we could use in our filtering as follows:

    docker ps -a --filter "label=maintainer=Dynamics SMB"

    Formatting the output

    After running the script (with –format “{{.Names}}”), $containers will look something like this:

    bccontainer1
    bccontainer2
    bccontainer3

    I only want the container name so I’m only requesting this one field in the format parameter. If I wanted more information I could simply list out the additional fields required. For example:

    $containers = docker ps -a --format "{{.Names}} {{.ID}} {{.Image}}"

    With my list of container names I can now loop through and invoke the Remove-NavContainer Cmdlet on each name:

    $containers = docker ps -a --filter "name=c*" --format "table {{.Names}}"
    
    foreach ($container in $containers) {
        Write-Host 'Removing ' $container
        try {
          Remove-NavContainer -containerName $container
        }
        catch {
          Write-Host 'Could not remove ' $container -f Red
        }
    }

    As I still had problems with the NavContainerHelper folder being locked, the script was still failing on some containers (Jenkins restart required) so I added a try-catch to make sure the script at least attempts to remove all containers.

    That’s it, dirty hack temporary fix complete!

  • How-to: Run LS Central in a Docker container – Part 2

    Docker containers

    In my previous blog post How-to: Run LS Central in a Docker container I showed how you can run LS Central on Docker with some manual steps to get the client and server components installed.

    For this blog post I’m going to show a more reusable solution where you can install the client components via a script passed into the container.

    To make the Business Central Docker images configurable, the designers decided to incorporate a set of Powershell script files which can be substituted at build time to override the standard setup process. The standard scripts are found in the containers C:\Run directory:

    Business Central container run contents
    The AdditionalSetup.ps1 script file, which is empty by default, when overwritten will execute after the main setup has completed. This is where we can provide code to install additional components such as client and service add-ins.

    When you place a script with the same name into the containers C:\Run\my directory, Docker will use this version of the file instead of the standard version. As we saw in my previous blog post the New-NavContainer Cmdlet’s -myScripts parameter is used to copy files from the Docker host into the containers C:\Run\my directory.

    I’ve created an AdditionalSetup.ps1 file with the following content:

    Write-Host "Installing LS Client Components.."
    
    & "C:\Run\my\LS Central 13.04.00.852 Client Components.exe" /silent
    
    Write-Host "Installing LS Service Components.."
    
    & "C:\Run\my\LS Central 13.04.00.852 Service Components.exe" /silent
    
    Write-Host "Remove database backup.."
    
    Remove-Item -Path 'C:\Run\my\*' -Include *.bak

    Note: The reason I’m not cleaning up the installer files is because I was getting an access denied error. If anyone knows why I can delete the database backup but not the .exe files please let me know in the comments!

    For this example I’ve created a folder on the Docker host machine with the following content:

    LS Central install files

     

     

     

     

    Now I can run a script to build the container, using my setup files and additional setup script:

    $imageName = "mcr.microsoft.com/businesscentral/onprem:1810-cu3"
    $navcredential = New-Object System.Management.Automation.PSCredential -argumentList "admin", (ConvertTo-SecureString -String "admin" -AsPlainText -Force)
    New-NavContainer -accept_eula `
                        -containerName "LSDEMO2" `
                        -Auth NavUserPassword `
                        -imageName $imageName `
                        -Credential $navcredential `
                        -licenseFile "C:\Temp\LS\BC13License.flf" `
                        -updateHosts `
                        -alwaysPull `
                        -additionalParameters @('--env bakfile="c:\run\my\w1-ls-central-13-04-release.bak"') `
                        -myScripts @(`
                                    "C:\Temp\LS\w1-ls-central-13-04-release.bak", `
                                    "C:\Temp\LS\LS Central 13.04.00.852 Client Components.exe", `
                                    "C:\Temp\LS\LS Central 13.04.00.852 Service Components.exe", `
                                    "C:\Temp\LS\AdditionalSetup.ps1"`
                                    ) `
                        -memoryLimit 8GB `
                        -accept_outdated `
                        -doNotExportObjectsToText
  • How-to: Run LS Central in a Docker container

    Microsoft have been releasing Business Central (formerly Dynamics NAV) as Docker images for a few years now. These have been great for testing and learning the new developer tools and trying out new functionality, but in real life many of us don’t use vanilla Business Central. You, like me, probably need an ISV solution and it’s demo data before Docker is useful on customer projects and demos.

    This blog post shows one way you can get LS Central by LS Retail running in a Docker container.

    LS Central releases contain a demo .bak file which we’ll use to replace the default .bak file that comes with Business Central. We’ll also need the client and server add-in files to deploy to the container.

    Note it’s important that any installer packages can run in unattended/silent mode as Windows Server Core based containers do not have a GUI to handle any user interaction. One way to check this is to run the .exe with the /? parameter and see if it prints out any information. LS Central installers use the /silent parameter:

    PS C:\Temp\LS> & '.\LS Central 13.04.00.852 Client Components.exe' /?
    
    ---------------------------
    Setup
    ---------------------------
    The Setup program accepts optional command line parameters.
    
    
    /HELP, /?
    
    Shows this information.
    
    /SP-
    
    Disables the This will install... Do you wish to continue? prompt at the beginning of Setup.
    
    /SILENT, /VERYSILENT
    
    Instructs Setup to be silent or very silent.
    
    /SUPPRESSMSGBOXES
    
    Instructs Setup to suppress message boxes.
    
    /LOG
    
    Causes Setup to create a log file in the user's TEMP directory.
    
    /LOG="filename"
    
    Same as /LOG, except it allows you to specify a fixed path/filename to use for the log file.
    
    /NOCANCEL
    
    Prevents the user from cancelling during the installation process.
    
    /NORESTART
    
    Prevents Setup from restarting the system following a successful installation, or after a Preparing to Install failure that requests a restart.
    
    /RESTARTEXITCODE=exit code
    
    Specifies a custom exit code that Setup is to return when the system needs to be restarted.
    
    /CLOSEAPPLICATIONS
    
    Instructs Setup to close applications using files that need to be updated.
    
    /NOCLOSEAPPLICATIONS
    
    Prevents Setup from closing applications using files that need to be updated.
    
    /RESTARTAPPLICATIONS
    
    Instructs Setup to restart applications.
    
    /NORESTARTAPPLICATIONS
    
    Prevents Setup from restarting applications.
    
    /LOADINF="filename"
    
    Instructs Setup to load the settings from the specified file after having checked the command line.
    
    /SAVEINF="filename"
    
    Instructs Setup to save installation settings to the specified file.
    
    /LANG=language
    
    Specifies the internal name of the language to use.
    
    /DIR="x:\dirname"
    
    Overrides the default directory name.
    
    /GROUP="folder name"
    
    Overrides the default folder name.
    
    /NOICONS
    
    Instructs Setup to initially check the Don't create a Start Menu folder check box.
    
    /TYPE=type name
    
    Overrides the default setup type.
    
    /COMPONENTS="comma separated list of component names"
    
    Overrides the default component settings.
    
    /TASKS="comma separated list of task names"
    
    Specifies a list of tasks that should be initially selected.
    
    /MERGETASKS="comma separated list of task names"
    
    Like the /TASKS parameter, except the specified tasks will be merged with the set of tasks that would have otherwise been selected by default.
    
    /PASSWORD=password
    
    Specifies the password to use.
    
    
    For more detailed information, please visit http://www.jrsoftware.org/ishelp/index.php?topic=setupcmdline
    ---------------------------
    OK   
    ---------------------------
    
    

    Of course if the installer is only adding DLLs to the add-ins folder then you could also get these files from another machine and copy them into the container. Have a look at the docker cp command documentation to see how to copy files into a container.

    We’re going to use the Create-NavContainer Cmdlet from the NavContainerHelper PowerShell module to build the container and use the LS demo database. We can use the -myScripts parameter to copy the LS components into the container, and then install them individually using the shell desktop shortcut the NavContainerHelper module creates.

    I used a script from Freddy’s Blog and adapted to suit. The steps look like this, adjust as required:

    $imageName = "mcr.microsoft.com/businesscentral/onprem:cu3"
    $navcredential = New-Object System.Management.Automation.PSCredential -argumentList "admin", (ConvertTo-SecureString -String "admin" -AsPlainText -Force)
    New-NavContainer -accept_eula `
    -containerName "LSDEMO" `
    -Auth NavUserPassword `
    -imageName $imageName `
    -Credential $navcredential `
    -licenseFile "https://www.dropbox.com/<blanked out>/Licence.flf?dl=1" `
    -myScripts @("C:\Temp\LS\w1-ls-central-13-04-release.bak", "C:\Temp\LS\LS Central 13.04.00.852 Client Components.exe", "C:\Temp\LS\LS Central 13.04.00.852 Service Components.exe") `
    -additionalParameters @('--env bakfile="c:\run\my\w1-ls-central-13-04-release.bak"') `
    -useBestContainerOS `
    -includeCSide `
    -updateHosts `
    -enableSymbolLoading
    

    In the above PowerShell script which I ran from PowerShell ISE, I copy the demo database and LS component installers into the containers C:\run\my directory using the -myScripts parameter, and then replace the database used during installation using the -additionalParameters parameter.

    Note: you must match the correct Business Central image for your demo database. LS Central 13.04 is based on Business Central On-prem CU3, check the release notes for the version you need and adjust he image name in the script above.

    So far so good, we have a running container but if we try and use the system we’ll quickly bump into a missing component error. Next we’ll need to install the LS components.

    The NavContainerHelper Module has conveniently left a command line shortcut on my desktop:

    We can use this to install our LS components which we loaded into the container c:\Run\my directory earlier:

    We now have LS Central running in our Docker container.

    If you want to use the LS Windows Client POS you’ll also need to copy the LS client components into local RTC add-ins folder created by NavContainerHelper. Assuming the container name is LSDEMO, the local add-in folder can be found on the Docker host machine here:

    C:\ProgramData\NavContainerHelper\Extensions\LSDEMO\Program Files\130\RoleTailored Client\Add-ins

    Enjoy!

    See Part 2 to automate creation further:

    How-to: Run LS Central in a Docker container – Part 2

     

  • Business Central: AL Compiler

    AL Compiler

    The Business Central AL Compiler

    When you start looking into build automation, one of the first things you’ll need to figure out is how to build an AL project without Visual Studio Code. This blog post serves as a brief introduction to finding the AL compiler and how to run it from the command line.

    Where to find the AL compiler

    The Business Central AL compiler is shipped inside the AL Language extension (vsix) file. The easiest way I find to get the correct compiler version is to create a docker container using the Business Central image version required and extract the VSIX file. The container will provide a HTTP download link to the AL Language extension, but I prefer to copy the VSIX file to the local file system from the containers C:\Run directory using the docker cp command.

    The VSIX file is essentially a zip archive, so with 7zip installed I can extract the contents of the AL Language vsix file as-is, but you can also change the file extension to zip so the built in Windows zip tool can recognise the file. Of course we’ll want to script all this for automation, so as an example the following PowerShell can be used:

    Copy-Item C:\Temp\*.vsix -Destination C:\Temp\alc.zip
    
    Expand-Archive C:\Temp\alc.zip -DesintationPath C:\Temp\alc -Force
    
    $CompilerPath = 'C:\Temp\alc\extension\bin\alc.exe'

    The Expand-Archive Cmdlet requires the zip extension, so I first copy the vsix file and give the new file the zip extension.

    Once the archive has been extracted you can find the AL compiler (alc.exe) in the \extension\bin directory:

    alc.exe
    AL compiler (alc.exe)

    Run the AL compiler from the command line

    If we run the alc.exe application with the /? parameter,  the parameters supported by the AL compiler are printed to the screen:

    Microsoft (R) AL Compiler version 2.1.1.13845
    Copyright (C) Microsoft Corporation. All rights reserved
    
    AL Compiler Options
    
    - PROJECT DIRECTORY -
    /project: Specify the project directory.
    
    - OUTPUT FILE -
    /out: Specify the output package file name (default: the name is generated from the project manifest as __.app).
    
    - ERRORS AND WARNINGS -
    /warnaserror[+|-] Report all warnings as errors.
    /nowarn: Turn off specific warning messages.
    /errorlog: Specify a file to log all compiler and analyzer diagnostics.
    /ruleset: Specify a ruleset file that alters severity of specific diagnostics.
    
    - SETTINGS -
    /packagecachepath: Specify the cache location for the symbols.
    /target: Specify the compilation target.
    /features: List of feature flags.
    
    - MISCELLANEOUS -
    /parallel[+|-] Concurrent build. (Short form /p[+|-])

    The minimum required parameters are:

    • /project – to specify the AL project workspace root.
    • /packagecachepath – to specify the location of the symbol files and any dependent app files.

    So for example we could run the following:

    > alc.exe /project:C:\Temp\AL\TestProject /packagecachepath:C:\Temp\AL\TestProject\symbols

    If successful the built app file will be placed in the workspace root folder. Error and warning messages will be displayed in the console output.

    How-to get the symbol app files?

    So far so good, but if you you want to introduce build automation you’ll need a way of getting the latest symbol files for the compiler to reference.

    When using Visual Studio Code to build projects, you’ve probably noticed that the symbol files are downloaded from the Business Central service’s developer end-point. We can achieve the same result programmatically using the PowerShell Cmdlet Invoke-WebRequest.

    The following script serves as an example (the credential code came from here):

    $user = 'admin'
    $password = 'admin'
    $containerName = 'BCLATEST'
    $versionText = '13.0.0.0'
    $symbolPath = 'C:\Temp\AL\TestApp\symbols'
    
    $pair = "$($user):$($password)"
    
    $encodedCreds = [System.Convert]::ToBase64String([System.Text.Encoding]::ASCII.GetBytes($pair))
    
    $basicAuthValue = "Basic $encodedCreds"
    
    $Headers = @{
    Authorization = $basicAuthValue
    }
    
    $SystemSymURL = 'http://{0}:7049/NAV/dev/packages?publisher=Microsoft&appName=System&versionText={1}' -f $containerName, $versionText
    $AppSymURL = 'http://{0}:7049/NAV/dev/packages?publisher=Microsoft&appName=Application&versionText={1}' -f $containerName, $versionText
    
    Invoke-WebRequest $SystemSymURL -OutFile "$symbolPath\system.app" -Headers $Headers
    
    Invoke-WebRequest $AppSymURL -OutFile "$symbolPath\application.app" -Headers $Headers

    As an aside, the version I’ve used above for the versionText parameter (13.0.0.0) is now outdated as the Business Central April release is versioned 14, however, using version 13.0.0.0 still currently appears to download the correct symbols even on the April ’19 release.

    Thanks for reading,

    Dan

  • Add User in Dynamics NAV and Business Central On-premise with Powershell

    If you have access to an account with local administrator privileges to a Windows box hosting the Dynamics NAV or Business Central service, you can add your own user very easily with Powershell.

    Note: This post is about the on-premise version of Business Central (formerly Dynamics NAV), to add a user to Business Central Cloud/SaaS see here: Add User in Business Central Cloud / SaaS

    This functionality has been available for quite sometime in Dynamics NAV and continues to be applicable to Business Central On-premise… so it surprises me how often I get asked for a NAV user by technical staff. I’ll now be forwarding this blog post when asked!

    Another use for this of course (amongst other things) is when you need to restore a database which doesn’t contain a login you have access to. In the past we had to clear out the User related tables in SQL so NAV would give us access on first log on.

    There are two Cmdlets we need:

    New-NAVServerUser – Used to create the NAV user

    New-NAVServerUserPermissionSet – Used to assign a Permission Set to the user.

    As of Business Central CU03, the Cmdlets still have “NAV” in their name.

    So for example, using the Business Central or Dynamics NAV Administration Shell (run as administrator) we can add a new Windows user to NAV/BC and assign the SUPER Permission Set as follows:

    
    > New-NAVServerUser ServiceInstanceName -WindowsAccount DOMAIN\User.Name
    > New-NAVServerUserPermissionSet 
    ServiceInstanceName -WindowsAccount DOMAIN\User.Name -PermissionSetId SUPER

    If you want to add your currently logged on Windows user you can use the whoami command:

    
    > New-NAVServerUser ServiceInstanceName -WindowsAccount $(whoami)
    > New-NAVServerUserPermissionSet 
    ServiceInstanceName -WindowsAccount $(whoami) -PermissionSetId SUPER
  • 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.

  • Remove Docker Images with PowerShell

    Update: Since Docker 1.13 (API version 1.25) the prune command has been available. You can remove all unused images with > docker image prune –all

    If like me, you’ve been experimenting with Docker since Microsoft made the Dynamics NAV images available, you’ll probably notice you’re using up a fair bit of hard drive space. So how do we remove Docker Images?

    Dynamics NAV Docker Images
    Dynamics NAV Docker Images

    The image above is a tad misleading however, as Docker shares images saving space. The microsoft/windowsservercore image for example is around 10GB, and is reused by all the other Dynamics NAV images. The size you see in the screenshot above is the total size of the image if you were to save an archive using the docker save command. Still, my Docker folder was somewhere around 40-50GB in size.

    So today I decided to have a clean up and remove all the images from my machine. I could have removed each image one by one using docker rmi:

    > docker rmi ee0
    > docker rmi bfe
    > docker rmi 7fc

    Docker tip: When referencing Docker Image or Containers in Docker commands, for brevity, you can use the first x number of characters of the Image or Container ID. Where x is the minimum number needed to identify a unique ID on your system. The first three characters will be fine in most cases.

    But.. repetition is a computers job right?

    To automate this we’ll need to get a list of our Docker Image IDs and pass them to the docker rmi command.

    To get a list of Image IDs on our system, we can use the docker images command with the -q option:

    Get Docker Image IDs
    Get Docker Image IDs

    We can then take the list of image IDs and iterate through them using a PowerShell foreach statement:

    $images = docker images -q
    
    foreach ($image in $images) {
     docker rmi $image -f
    }

    What about filtering?

    So far so good.. we can remove Docker images with a PowerShell script. But what if we want to keep the base image? It’s around 12GB, so we probably don’t want to download it again next time we pull a NAV image.

    The docker images command takes the repository name as an optional parameter. We can use this to filter on our NAV images:

    > docker images microsoft/dynamcis-nav

    List NAV Docker Images
    List NAV Docker Images

    Keeping the base image

    The problem is, unless we’ve explicitly requested the base image it doesn’t show up in our image list. This means it will get removed with the last NAV image referencing it. To get around this we just need to do a pull request for the base image.

    To understand this concept, it’s important to know that Docker images are built in layers. Instead of building an entire image from scratch every time, when creating an image you can base it on an existing image. All Dynamics NAV images are built on an image called microsoft/nav-docker. Docker images can be built from a Dockerfile. We can view the Dockerfile for microsoft/nav-docker and see that this image is currently based on the microsoft/dotnet-framework:4.7-windowsservercore image.

    So back to the pull request:

    > docker pull microsoft/nav-docker

    Cant Pull nav-docker image
    Cant Pull nav-docker Image

    Microsoft has restricted access, or just not published this image. We can use the image that microsoft/nav-docker is based on instead:

    > docker pull microsoft/dotnet-framework:4.7-windowsservercore

    Pull microsoft/dotnet-framework:4.7-windowsservercore
    Pull microsoft/dotnet-framework:4.7-windowsservercore

    Running this command didn’t download the image again as Docker found it locally. However, it will now list the microsoft/dotnet-framework:4.7-windowsservercore image alongside our NAV images:

    Docker Images with a base image available
    Docker Images with a base image available

    As we can see from the size attributes, the 4.7-windowsservercore image makes up the bulk of the NAV image size.

    To remove all the microsoft/dynamics-nav images but leave the microsoft/dotnet-framework image for later use, we can run the following PowerShell script:

    $images = docker images -q microsoft/dynamics-nav
    
    foreach ($image in $images) {
     docker rmi $image -f
    }

    Remove Docker Images.. except the most recent

    To have a look at another filtering option lets explore another scenario. What if we want to keep the most recently pulled NAV image, but remove Docker images previous to this?

    The docker images command has a –filter option. We can use this to filter on the image created date. It’s important to note that this is based on the time the images were created on your system (when it was pulled), not when the vendor created or published the image.

    Using a before filter we can get a list of all image IDs that where pulled before microsoft/dynamics-nav:latest:

    > docker images microsoft/dynamics-nav --filter "before=microsoft/dynamics-nav:latest"
    $images = docker images -q microsoft/dynamics-nav --filter "before=microsoft/dynamics-nav:latest"
    
    foreach ($image in $images) {
     docker rmi $image -f
    }

    Putting the two commands together

    So far we’ve used a foreach loop to iterate through the list of image IDs we get back from the docker images command. I think this approach makes the script easier to read, but we can also use this list directly with the docker rmi command on the same line:

    > docker rmi (docker images -q microsoft/dynamics-nav --filter "since=microsoft/dynamics-nav:latest")

    In the example above I’ve used the since filter to remove all NAV images after the “latest” image.

    Now I’m left with just the latest Dynamics NAV image and the dotnet-framework image it’s based on.

    Dynamics NAV latest Docker image
    Dynamics NAV latest

    OK, well thanks for reading. If you want to look at more of the filtering options available check out this link: https://docs.docker.com/engine/reference/commandline/images/#filtering”

  • Restart Dynamics NAV Services with Powershell

    In this blog post I explain how you can restart Dynamics NAV services with Powershell, while also exploring a few of the available Cmdlets for selecting services and properties.

    Restarting a NAV service

    Restart-Service is a standard Powershell Cmdlet that will stop and then start Windows services. If the service is already stopped, then it will simply start the service.

    Restarting the default NAV 2017 service can be done as follows:

    PS C:\Windows\System32>Restart-Service 'MicrosoftDynamicsNavServer$DynamicsNAV100'

    The unnamed parameter ‘MicrosoftDynamicsNavServer$DynamicsNAV100’ is the service name, this is always the Dynamics NAV Server instance name prefixed with MicrosoftDynamicsNavServer$.

    NAV Service Properties
    The properties of the default NAV 2017 service

    Restarting multiple NAV services

    Restarting multiple services can be done simply by using the * wildcard in the service name parameter:

    PS C:\Windows\System32>Restart-Service 'MicrosoftDynamicsNavServer*'

    The above command will restart every service with a name beginning with MicrosoftDynamicsNavServer.

    So far the services selected for restart have been very indiscriminate, we’ve simply restarted all NAV services on the machine… What if we need to be a bit more selective?

    Get-Service provides us with the ability to get objects representing Windows services. To get all services on your machine run Get-Service with no parameters:

    PS C:\Windows\System32>Get-Service

    The above command will print out a list of services and there statuses:

    Output of the Get-Service Cmdlet

    The same wildcard used for the Restart-Service Cmdlet can be used with Get-Service using the -Name parameter:

    PS C:\Windows\system32> Get-Service -Name 'MicrosoftDynamicsNavServer*'

    The benefit of using Get-Service however, is the ability to pipe the output to the Where-Object Cmdlet:

    PS C:\Windows\system32> Get-Service -Name 'MicrosoftDynamicsNavServer*' | Where-Object {$_.Status -eq "Running"}

    We now have a list of services starting with the name “MicrosoftDynamicsNavServer”, with a status of running:

    Restart Dynamics NAV Service with Powershell
    Get-Service running services

    To keep things consistent, lets delegate all the filtering to Where-Object:

    PS C:\Windows\system32> Get-Service | Where-Object {$_.Status -eq "Running" -and $_.Name -like 'MicrosoftDynamicsNavServer*'}

    Get running NAV services with Get-Service and Where-Object

    The output of Where-Object can be piped into Restart-Service.. so by piping Get-Service into Where-Object into Restart-Service, we can now restart all currently running NAV services with the following:

    PS C:\Windows\system32> Get-Service | Where-Object {$_.Status -eq "Running" -and $_.Name -like 'MicrosoftDynamicsNavServer*'} | Restart-Service

    Get-NAVServiceInstance

    As always, there are many ways to skin a cat.. We could also approach this problem from another direction. The Get-NavServerInstance Cmdlet is available in the NavAdminTool.ps1 script file found in the NAV service folder. The script file will need to be loaded into your Powershell environement using the Import-Module Cmdlet:

    PS C:\Windows\system32> Import-Module 'C:\Program Files\Microsoft Dynamics NAV\100\Service\NavAdminTool.ps1'

    If you get the following error:

    Execution policy error

    You can change the execution policy using the Execution-Policy Cmdlet. In the following image you can see that I get the current execution policy for reference and then after changing the execution policy, I am able to run the Import-Module Cmdlet:

    Powershell change execution policy

    For security reasons it’s good practice to change the execution policy back:

    PS C:\Windows\system32> Set-ExecutionPolicy -ExecutionPolicy Restricted

    Get-NavServerInstance returns a list of NAV services, but the output is not compatable with the Restart-Service Cmdlet:

    Output of Get-NavServerInstance

    We can however pipe the output into the Select-Object Cmdlet which we can use to get the service names as string objects:
    PS C:\Windows\system32> Get-NavServerInstance | Select-Object -ExpandProperty "ServerInstance"

    Get-NavServerInstance ServerInstance Property

    The string objects can then be piped directly into the Restart-Service Cmdlet:

    PS C:\Windows\system32> Get-NavServerInstance | Select-Object -Expand "ServerInstance" | Restart-Service

    Or we can pipe into Get-Service first, followed by Where-Object for more filtering options:

    PS C:\Windows\system32> Get-NavServerInstance | Select-Object -ExpandProperty "ServerInstance" | Get-Service | Where-Object {$_.Status -eq 'running'} | Restart-Service