Functions/LicenseManagement/Set-BcLicense.ps1

<#
    .Synopsis
    Imports a Business Central license into the Business Central database.
 
    .Description
    The function exports the current installed license from the BC ServerInstance, and reads the license details from the new license file.
    The new license is compared with the current license. If there are any discrepancies, the function will display warning messages indicating the issues.
    The function can be forced to complete the license import even if some validation have not passed succesfully by using the Force switch.
 
    .Example
    # Set-BcLicense -ServerInstance 'BC210' -LicensePath 'c:\temp\myLicense.bclicense' -Database 'NavDatabase' -RestartServerInstance
#>


function Set-BcLicense {
    [Alias('Import-BcLicense')]
    [CmdletBinding(SupportsShouldProcess)]
    param(
        # The name of the BC ServerInstance used to import the new license file.
        [Parameter(
            Mandatory=$true,
            Position = 0,
            ValueFromPipeline=$true, 
            ValueFromPipelineByPropertyName = $true)]
        [string] $ServerInstance,
        
        # The LicensePath parameter specifies the path to the Business Central license file.
        [Parameter(
            Mandatory=$true,
            Position = 1,
            ValueFromPipeline=$true, 
            ValueFromPipelineByPropertyName = $true)]
        [string] $LicensePath,

        <#
        Specifies the database into which to import the license file. You can use string or integer value according to the following:
        Default or 0: The default value. Overrides the license file currently in use.
        Master or 1: Forces the license file to be global.
        NavDatabase or 2: Forces the license file to be local and stored in the Business Central database that is used by the specified Business Central Server instance.
        Tenant or 3: Forces the license file to be local and stored in the Business Central database that is used by the tenant that is specified in the Tenant parameter.
        Possible values: Default, Master, NavDatabase, Tenant
        #>

        [Parameter(
            Mandatory=$true,
            Position = 2,
            ValueFromPipeline=$true, 
            ValueFromPipelineByPropertyName = $true)]
        [ValidateSet('Default','Master','NavDatabase', 'Tenant')]
        [string] $Database,
        
        # The computer where the ServerInstance is running. Default is the local system.
        [Parameter(
            Position = 3,
            ValueFromPipeline=$true, 
            ValueFromPipelineByPropertyName = $true)]
        [string] $Computer = $env:COMPUTERNAME,
        
        # Specifies the ID of the tenant that the license is stored in.
        # This parameter is required unless the specified service instance is not configured to run multiple tenants.
        [Parameter(
            Position = 4,
            ValueFromPipeline=$true, 
            ValueFromPipelineByPropertyName = $true)]
        [string] $Tenant,

        # Restarts the BC ServerInstance after the new license is imported.
        [Parameter(ValueFromPipelineByPropertyName = $true)]
        [switch] $RestartServerInstance,

        # When Force is set the failed license detail validations become informative instead of skipping the license import and-
        # the user confirmation will be disabled.
        [Parameter(ValueFromPipelineByPropertyName = $true)]
        [switch] $Force
    )

    begin {
        $TaskStartTime = Write-StartProcessLine "Updating Business Central Software License." 
    }
    process{
        # Get the ServerInstance object.
        $serverInstanceObj = Get-BCServerInstance $ServerInstance -Computer $Computer
        if(!$serverInstanceObj){
            'Could not find the ServerInstance {0}.' -f $ServerInstance | Write-Warning
            return
        }

        # Validate if ServerInstance is running
        if($serverInstanceObj.state -ne 'running'){
            'Cannot import license from ServerInstance {0} because the service is not started. Start the service and try-again.' -f 
                $ServerInstance | Write-Warning
            return
        }

        # Validate if tenant parameter is specified if the ServerInstance is configured to be multi-tenant
        if($serverInstanceObj.AppSettings.Multitenant -eq 'True' -and !$Tenant){
            $message = @()
            $message += 'The BC ServerInstance is configured to be multi-tenant and the tenant parameter is not specified.'
            $message += 'Importing license skipped. Specify the tenant to import the BC license.'
            Write-Warning ($message | Out-String)
            return
        }

        # Export the current installed license from the BC ServerInstance.
        $additionalParams = @{}
        if(-not [string]::IsNullOrEmpty($Tenant)){
            $additionalParams = @{'Tenant' = $Tenant}
        }
        try{
            $currentLicense = Get-BcLicense @additionalParams -ServerInstance $ServerInstance -Computer $Computer -ErrorAction Stop
        } 
        catch {
            $errorMessage = $_.Exception.Message
            if($errorMessage -match "Your program license has expired"){
                Write-Warning "The current license in the Business Central database is expired and could not be exported. Skipping additional license detail validations."
            } else {
                Write-Warning 'Cannot export the current license from the ServerInstance. Skipping additional license detail validations.'
            }
        }

        # Read the license details from the to install license.
        $newLicense = Get-BcLicense -LicensePath $LicensePath -ErrorAction Stop
        if(!$newLicense){
            'Could not read license file from path {0}. Importing license skipped.' -f $LicensePath | Write-Warning
            return
        }

        $message = ''
        if($currentLicense){
            $message += '*** Database license file details ***{0}' -f ($currentLicense | Out-String)
        }
        $message += '*** To install license file details *** {0}' -f ($newLicense | Out-String)
        $message | Write-Host

        # Start validations on the license details.
        $bcPlatformVersion = [version] $ServerInstanceObj.Version
        
        # Validate if license file extension is correct
        $supportedLicenseExtension = @('.flf', '.bclicense')

        if($bcPlatformVersion.Major -le 16 -or
          ($bcPlatformVersion.Major -eq 17 -and $bcPlatformVersion.Minor -lt 12) -or
          ($bcPlatformVersion.Major -eq 18 -and $bcPlatformVersion.Minor -lt 7 ) -or
          ($bcPlatformVersion.Major -eq 19 -and $bcPlatformVersion.Minor -lt 1)){
            $supportedLicenseExtension = @('.flf')
        } 
        elseif($bcPlatformVersion.Major -ge 21){
            $supportedLicenseExtension = @('.bclicense')
        }
        if(-not (Get-Item -Path $LicensePath).Extension -in $supportedLicenseExtension){
            'Supplied license is not a valid Business Central license. Expected a file with the following extension: {0}.' -f
                ($supportedLicenseExtension -join ' or ') | Write-Warning
            return
        }

        # Validate if the Microsoft account number is the same.
        if($currentLicense -and $newLicense.AccountNumber -ne $currentLicense.AccountNumber){    
            $failedValidation = $true

            $message = @()
            $message += 'The supplied license and the license in the database are licensed to a different VOICE accounts.'
            $message += 'Supplied license VOICE Account Number: {0}, Licensed To: {1}.' -f 
                            $newLicense.VOICEAccountNumber, 
                            $newLicense.LicensedTo
            $message += "License in database VOICE Account Number: {0}, Licensed To: {1}.`n" -f 
                            $currentLicense.VOICEAccountNumber, 
                            $currentLicense.LicensedTo
            Write-Warning ($message | Out-String)
        } else {
            Write-Verbose 'The VOICE Account Number matches between the installed and the to install licenses.'
        }

        # Validate if the new license file is newer than the installed license.
        if($currentLicense -and $NewLicense.CreatedDate -le $CurrentLicense.CreatedDate -and
        [int]$NewLicense.ProductVersion -le [int] $currentLicense.ProductVersion)
        {
            $failedValidation = $true
            Write-Host "Supplied license creation data is older or equal to the creation date of the active license in the database." -ForegroundColor Cyan
        }
        if($currentLicense -and $NewLicense.CreatedDate -gt $CurrentLicense.CreatedDate)
        {
            Write-Verbose 'Supplied license file is more recent than the active license in the database.'
        }

        # Validate if the product version is correct
        if([int] $NewLicense.ProductVersion -lt $bcPlatformVersion.Major -or
           [int] $NewLicense.ProductVersion -gt $bcPlatformVersion.Major){
            
            $failedValidation = $true
            'This license (BC{0}) is not compatible with this version of Business Central (BC{1}).' -f 
                $NewLicense.ProductVersion, $bcPlatformVersion.Major | Write-Warning
        }

        # Write warning to host.
        if($failedValidation -and !$Force){
            Write-Warning 'Importing license skipped due a failed validation. Use the -Force switch if you want to import the license file anyway.'
            return
        }
        # End validations on license details.

        # Start installing the license file.
        # Scriptblock is used to load BC modules into a separate PS session and to add compatibility for Computer param.
        [scriptblock] $scriptBlock = {
            param(
                [string[]] $BcModulesPath,  
                [string] $ServerInstance,
                [string] $Database,
                [byte[]] $LicenseData,
                [string] $Tenant
            )
            try{
                $BcModulesPath | Import-Module -Force

                $params = @{
                    'ServerInstance' = $ServerInstance
                    'LicenseData'    = $LicenseData
                    'Database'       = $Database
                    'Tenant'         = $Tenant
                    'Confirm'        = $false
                }
                Import-NAVServerLicense @params
            } catch{
                return $_
            }
        }

        $additionParams = @{}
        if(-not $serverInstanceObj._isLocalService){
            $additionParams += @{'ComputerName' = $serverInstanceObj.ComputerName}
        }

        if(-not $Tenant){
            $Tenant = 'default'
        }

        $licenseData = ([Byte[]]$(Get-Content -Path $LicensePath -Encoding Byte))

        # Execute scriptblock to import the BC license
        $target = 'ServerInstance ''{0}'', tenant ''{1}'' on computer ''{2}''' -f $ServerInstance, $Tenant, $Computer
        $operation = 'import Business Central license'
        if($PSCmdlet.ShouldProcess($target, $operation)){
                'Importing license into the database...' | Write-Host
                Invoke-Command @additionParams -ScriptBlock $scriptBlock -ErrorAction Stop -ArgumentList @(
                                    $serverInstanceObj.GetBcPsModules(), 
                                    $serverInstanceObj.ServerInstance, 
                                    $Database,
                                    $licenseData,
                                    $Tenant)        
        }
        # End installing the license file.
        $target = 'ServerInstance ''{0}'' on computer ''{1}''' -f $ServerInstance, $Computer
        $operation = 'restart ServerInstance'
        if($RestartServerInstance -and $PSCmdlet.ShouldProcess($target, $operation)){
            'Restarting ServerInstance {0} to activate license...' -f $ServerInstance | Write-Host
            $serverInstanceObj.Restart()
        }
    }
    end {
        Write-EndProcessLine $TaskStartTime
    }
}

Export-ModuleMember -Function Set-BcLicense -Alias Import-BcLicense