Classes/BcServerInstance.ps1
# TODO Methods: # - Create new server instance # - Create report for installed extensions # - Compare ServerInstance Configuration # - Update ServerInstance to higher platform # - Add switch 'enableEditing' to return a read only object by default. # - GetLicenseFile # - BC Application layer (installed apps report, mounted tenants) # - feature rename BCS by deleting and recreating the BCS. class BcServerInstance { [System.Object] $AppSettings hidden [System.Object] $_AppSettings hidden [string] $_SqlInstance hidden [string] $_DatabaseServer hidden [string] $_DatabaseInstance hidden [string] $_DatabaseName hidden [string] $_StartMode hidden [string] $_ServiceName hidden [string] $_ComputerName hidden [string] $_ServerInstance hidden [pscredential] $_ServiceAccount hidden [System.Management.ManagementBaseObject] $_BcService hidden [string] $_State hidden [string] $_Version hidden [int] $_ProcessID hidden [bool] $_isLocalService hidden [bool] $_remotePsEnabled # Powershell 5.1 lacks a way to add get/set for properties. # Using Add-Member in method Init() is a workaround. # Following properties are set in the Init() method: # [string] $SqlInstance # [string] $DatabaseServer # [string] $DatabaseInstance # [string] $DatabaseName # [string] $StartMode # [string] $ServiceName # Property not editable # [string] $ComputerName # Property not editable # [string] $ServiceAccount # Property not editable, use SetServerInstance($usernamm, $passwordAsSecureString) # [string] $ServerInstance # Property not editable # [string] $State # Property not editable, use method Start(), Stop() or Restart() # [version] $Version # Property not editable # [int] $ProcessID # Property not editable BcServerInstance( [string] $WinBcServiceName, [string] $ComputerName ){ # WinBcServiceName can be either the full Windows service name or only the bc server instance name. # E.g. 'MicrosoftDynamicsNavServer$MyBcInstance' or 'MyBcInstance' if($WinBcServiceName -notlike 'MicrosoftDynamicsNavServer$*'){ $WinBcServiceName = 'MicrosoftDynamicsNavServer${0}' -f $WinBcServiceName } $this._ServiceName = $WinBcServiceName $this._ComputerName = $ComputerName $this.AppSettings = New-Object BcAppSettings -ArgumentList @($this) # To support retreiving BC server instances from remote computers the ComputerName parameter should be set. # However, this property should not be set if the host and the target machine are the same and remote PS is not enabled. # Otherwise 'enable remote PowerShell' becomes a requirement to work on your local host. if ($this._ComputerName -eq 'localhost' -or $this._ComputerName -eq $env:COMPUTERNAME) { $this._isLocalService = $true } else { $this._isLocalService = $false } # If remote powershell is enabled on the localhost, use the computer parameter in invoke-command. # This will execute scriptblocks in a separate session, enabeling managing different BC platforms in one session. $this._remotePsEnabled = Test-RemotePowershell -ComputerName $this._ComputerName # In the Init the public properties are set. $this.Init() # Set the default visible properties of the object. # Additional parameters are available but hidden by default to keep the output clean. [string[]] $visible = 'ServerInstance', 'Version', 'State', 'ServiceAccount', 'SqlInstance', 'DatabaseName', 'ComputerName' [Management.Automation.PSMemberInfo[]] $psStandardMembers = New-Object ` System.Management.Automation.PSPropertySet('DefaultDisplayPropertySet',$visible) $this | Add-Member -MemberType MemberSet -Name PSStandardMembers -Value $psStandardMembers } ## START Initialisation and refresh methods hidden [void] Init(){ # Add readonly property ServiceName. $this | Add-Member -Name 'ServiceName' -MemberType ScriptProperty -Value { return $this._ServiceName } -SecondValue { 'Changing the Windows service name for the ServerInstance is not allowed.' | Write-Warning return } # Add readonly property ComputerName. $this | Add-Member -Name 'ComputerName' -MemberType ScriptProperty -Value { return $this._ComputerName } -SecondValue { 'Changing the ComputerName for the ServerInstance is not allowed.' | Write-Warning return } $this.InitWinService() $this.InitAppSettings() # Create visible properties for each BC appsettings hidden key-value pair. foreach($key in $this._AppSettings.keys){ $getter = [scriptblock]::Create(" `$this._ServerInstance._AppSettings.$key") $setter = [scriptblock]::Create(" Param([string] `$Value) `$this.SaveAppSetting('$Key', `$Value) `$this._ServerInstance.Refresh()") $this.AppSettings | Add-Member -Name $key -MemberType ScriptProperty -Value $getter -SecondValue $setter } # Add link between properties on root level and keys in BC appsettings. $this | Add-Member -Name 'SqlInstance' -MemberType ScriptProperty -Value { return $this._SqlInstance } -SecondValue { Param([string] $SqlInstance) if($SqlInstance -like '*\*'){ $this.AppSettings.DatabaseServer = $SqlInstance.Split('\')[0] $this.AppSettings.DatabaseInstance = $SqlInstance.Split('\')[1] } else { $this.AppSettings.DatabaseServer = $SqlInstance } } $this | Add-Member -Name 'DatabaseServer' -MemberType ScriptProperty -Value { return $this._DatabaseServer } -SecondValue { Param([string] $DatabaseServer) $this.AppSettings.DatabaseServer = $DatabaseServer } $this | Add-Member -Name 'DatabaseInstance' -MemberType ScriptProperty -Value { return $this._DatabaseInstance } -SecondValue { Param([string] $DatabaseInstance) $this.AppSettings.DatabaseInstance = $DatabaseInstance } $this | Add-Member -Name 'DatabaseName' -MemberType ScriptProperty -Value { return $this._DatabaseName } -SecondValue { Param([string] $DatabaseName) $this.AppSettings.DatabaseName = $DatabaseName } # Add readonly property ServerInstance. $this | Add-Member -Name 'ServerInstance' -MemberType ScriptProperty -Value { return $this._ServerInstance } -SecondValue { 'Renaming the ServerInstance is not implemented.' | Write-Warning return } $this | Add-Member -Name 'Name' -MemberType ScriptProperty -Value { return $this._ServerInstance } -SecondValue { 'Renaming the ServerInstance is not implemented.' | Write-Warning return } # Add readonly property ComputerName. $this | Add-Member -Name 'ProcessID' -MemberType ScriptProperty -Value { return $this._ProcessID } -SecondValue { 'Changing the Windows Service Process ID for the ServerInstance is not allowed.' | Write-Warning return } # Add readonly property Version. $this | Add-Member -Name 'Version' -MemberType ScriptProperty -Value { return [version] $this._Version } -SecondValue { 'Changing the Business Central version for the ServerInstance is not allowed.' | Write-Warning return } # Add property ServiceAccount with custom getter and setter. $this | Add-Member -Name 'ServiceAccount' -MemberType ScriptProperty -Value { return $this._ServiceAccount.UserName } -SecondValue { # ServiceAccount Credential can be set by updating the ServiceAccount property with a [pscredential] or # by calling the method SetServiceAccount($username, $PasswordAsSecureString). param([pscredential]$credential) $this.SetServiceAccount($credential) return } # Add property State with custom getter and setter. $this | Add-Member -Name 'State' -MemberType ScriptProperty -Value { return $this._State } -SecondValue { # State can be changed either by updating the State parameter or # by calling the method Start(), Stop() or Restart(). param( [ValidateSet('start', 'running', 'stop', 'stopped', 'Restart')] [string] $State ) switch ($State) { {$_ -eq 'Start' -or $_ -eq 'Running'}{ $this.Start()} {$_ -eq 'Stop' -or $_ -eq 'Stopped'}{ $this.Stop()} {$_ -eq 'Restart'}{ $this.Restart()} } return } # Add property StartMode with custom getter and setter. $this | Add-Member -Name 'StartMode' -MemberType ScriptProperty -Value { return $this._StartMode } -SecondValue { param( [ValidateSet('auto', 'automatic', 'manual', 'disabled')] [string] $StartMode ) $StartMode if($StartMode -eq 'Auto'){ $StartMode = 'Automatic' } $this._BcService.ChangeStartMode($StartMode) $this.InitWinService() return } } hidden [void] InitWinService(){ $this._BcService = $this.GetBcWinService() $this._ServerInstance = $this._BcService.Name.Split('$')[1] $this._State = $this._BcService.State $this._StartMode = $this._BcService.StartMode $this._ComputerName = $this._BcService.SystemName $this._ProcessID = $this._BcService.ProcessID $this._Version = $this.GetBcVersion() $this._ServiceAccount = New-Object System.Management.Automation.PSCredential ` ($this._BcService.StartName, (ConvertTo-SecureString 'dummypass' -AsPlainText -Force)) } hidden [void] InitAppSettings(){ $this._AppSettings = $this.AppSettings.GetBcAppSettings() $this._DatabaseServer = $this._AppSettings.DatabaseServer $this._DatabaseInstance = $this._AppSettings.DatabaseInstance $this._DatabaseName = $this._AppSettings.DatabaseName $this._SqlInstance = $this.GetSqlInstance() } [void] Refresh(){ $this.InitWinService() $this.InitAppSettings() } hidden [string] GetSqlInstance(){ if ([string]::IsNullOrEmpty($this._AppSettings.DatabaseInstance)){ $sqlInstanceName = $this._AppSettings.DatabaseServer } else { $sqlInstanceName = Join-Path $this._AppSettings.DatabaseServer $this._AppSettings.DatabaseInstance } return $sqlInstanceName } hidden [version] GetBcVersion(){ $regex = '.*"(?<ServicePath>.*?.exe)".*' $match = $this._BcService.PathName | Select-String -Pattern $regex $executablePath = $match.Matches[0].Groups['ServicePath'].Value [scriptblock] $scriptBlock = { param( [string] $executablePath ) try{ if((Test-Path $executablePath)){ [version] $executableVersion = (Get-Item $executablePath).VersionInfo.FileVersion } else { Write-Warning ('The Business Central installation is not found for Server Instance ''{0}'' on location: {1}' -f ($this.Service.Name.Split('$'))[1], $executablePath) return [version] '0.0.0.0' } } catch{ return $_ } return $executableVersion } $additionParams = @{} if(-not $this._isLocalService -or $this._remotePsEnabled -eq $true){ $additionParams = @{'ComputerName' = $this._ComputerName} } $executableVersion = Invoke-Command @additionParams -ScriptBlock $scriptBlock ` -ArgumentList @($executablePath) -ErrorAction Stop return $executableVersion } [void] ImportBcPsModules(){ $this.GetBcPsModules() | Import-Module -Scope Global -Force } [string[]] GetBcPsModules(){ [scriptblock] $scriptBlock = { param( [string] $installationFolder ) $modules = @( 'Microsoft.Dynamics.Nav.Management' 'Microsoft.Dynamics.Nav.Apps.Management' 'Microsoft.Dynamics.Nav.Apps.Tools' ) $modulePaths = @() # Check if the 'Management' sub-folder exists in the installation folder. $subFolder = Join-Path -Path $installationFolder -ChildPath "Management" if(Test-Path -Path $subFolder){ $baseFolder = $subFolder } else { $baseFolder = $installationFolder } foreach($module in $modules){ $modulePath = Join-Path -Path $baseFolder -ChildPath ($module + ".psd1") if (-not (Test-Path -Path $modulePath)) { $modulePath = Join-Path -Path $baseFolder -ChildPath ($module + ".psm1") } if (-not (Test-Path -Path $modulePath)) { if($module -eq 'Microsoft.Dynamics.Nav.Management'){ 'Module {0} not found on location {1}' -f $module, $modulePath | Write-Warning } continue } $modulePaths += $modulePath } return $modulePaths } $additionParams = @{} if(-not $this._isLocalService -or $this._remotePsEnabled -eq $true){ $additionParams = @{'ComputerName' = $this._ComputerName} } $installationFolder = $this.GetInstallationFolder() $modulePaths = Invoke-Command @additionParams -ScriptBlock $scriptBlock ` -ArgumentList @($installationFolder) -ErrorAction Stop return $modulePaths } ## END Initialisation and refresh methods ## START Windows Service Management hidden [System.Management.ManagementBaseObject] GetBcWinService(){ $service = [System.Management.ManagementBaseObject] $service = Get-WmiObject win32_service -ComputerName $this._ComputerName -ErrorAction Stop | Where-Object Name -eq $this._ServiceName if(-not $service){ $message = 'Windows service {0} for Business Central Server Instance {1} not found.' -f $this._ServiceName, $this._ServiceName.Split('$')[1] throw $message } return $service } [void] Start(){ $this._BcService = $this.GetBcWinService() if ($this._BcService.State -eq 'Running'){ 'Serverinstance {0} is already running on computer {1}.' -f $this.ServerInstance, $this.ComputerName | Write-Host return } if ($this._BcService.State -eq 'Pending'){ 'Serverinstance {0} is already starting on computer {1}.' -f $this.ServerInstance, $this.ComputerName | Write-Host return } [scriptblock] $scriptBlock = { param( [string[]] $BcModulesPath, [string] $ServerInstance ) try{ $BcModulesPath | Import-Module -Force Set-NAVServerInstance -ServerInstance $ServerInstance -Start -Confirm:$false # Start: Wait-BcServerInstanceMountingTenants # Waits for the the Service Instance to mount all attached tenants and executes Get-NavTenant in ForceSync mode $mounting = $false while($mounting -eq $false){ $tenants = Get-NAVTenant -ServerInstance $ServerInstance if('Mounting' -notin $tenants.State){ $mounting = $true } else { Start-Sleep -Seconds 10 } } # Invoke Get-NAVTenant with ForceRefresh to bring the tenants from state 'Mounted' to 'Operational'. (Get-NAVTenant -ServerInstance $ServerInstance).Id | ForEach-Object { Get-NAVTenant -ServerInstance $ServerInstance -Tenant $_ -ForceRefresh } # End: Wait-BcServerInstanceMountingTenants } catch{return $_} } 'Starting serverInstance {0} on computer {1}...' -f $this.ServerInstance, $this.ComputerName | Write-Host $additionParams = @{} if(-not $this._isLocalService -or $this._remotePsEnabled -eq $true){ $additionParams = @{'ComputerName' = $this._ComputerName} } Invoke-Command @additionParams -ScriptBlock $scriptBlock ` -ArgumentList @($this.GetBcPsModules(), $this.ServerInstance) -ErrorAction Stop $this.InitWinService() } [void] Stop(){ $this._BcService = $this.GetBcWinService() if ($this._BcService.State -eq 'Stopped'){ 'Serverinstance {0} is already stopped on computer {1}.' -f $this.ServerInstance, $this.ComputerName | Write-Host return } [scriptblock] $scriptBlock = { param( [string[]] $BcModulesPath, [string] $ServerInstance ) try{ $BcModulesPath | Import-Module -Force Set-NAVServerInstance -ServerInstance $ServerInstance -Stop } catch{return $_} } 'Stopping serverInstance {0} on computer {1}...' -f $this.ServerInstance, $this.ComputerName | Write-Host $additionParams = @{} if(-not $this._isLocalService -or $this._remotePsEnabled -eq $true){ $additionParams = @{'ComputerName' = $this._ComputerName} } Invoke-Command @additionParams -ScriptBlock $scriptBlock ` -ArgumentList @($this.GetBcPsModules(), $this.ServerInstance) -ErrorAction Stop $this.InitWinService() } [void] Restart(){ $this.Stop() $this.Start() } [string] GetInstallationFolder(){ $regex = '.*"(?<ServicePath>.*?.exe)".*' $match = $this._BcService.PathName | Select-String -Pattern $regex return Split-Path ($match.Matches[0].Groups['ServicePath'].Value) -Parent } [System.Object] GetComputerInfo(){ $result = Get-FpsComputerInfo -Computer $this._ComputerName return $result } ## END Windows Service Management ## START Service Account management [void] SetServiceAccount( [pscredential] $Credential ){ [scriptblock] $scriptBlock = { param( [string[]] $BcModulesPath, [string] $ServerInstance, [string] $previousServiceAccount, [pscredential] $Credential ) try{ $BcModulesPath | Import-Module -Force "Updating service account from '{0}' to '{1}' on ServerInstance {2}." -f $previousServiceAccount, $Credential.UserName, $ServerInstance | Write-Host Set-NAVServerInstance -ServerInstance $ServerInstance ` -ServiceAccount User -ServiceAccountCredential $Credential } catch{ return $_ } } $additionParams = @{} if(-not $this._isLocalService -or $this._remotePsEnabled -eq $true){ $additionParams = @{'ComputerName' = $this._ComputerName} } Invoke-Command @additionParams -ScriptBlock $scriptBlock ` -ArgumentList @($this.GetBcPsModules(), $this._ServerInstance, $this._ServiceAccount.UserName, $Credential) -ErrorAction Stop $this.InitWinService() } [void] SetServiceAccount([string] $username, [securestring] $password ){ [pscredential] $credential = New-Object System.Management.Automation.PSCredential ($userName, $password) $this.SetServiceAccount($credential) } [void] SetServiceAccount([string] $username,[string] $password){ [securestring] $password = ConvertTo-SecureString $password -AsPlainText -Force [pscredential] $credential = New-Object System.Management.Automation.PSCredential ($userName, $password) 'It is not recommended to store plain-text passwords in scripts.' | Write-Warning $this.SetServiceAccount($credential) } [void] SetServiceAccount( [string] $LocalAccount ){ if($LocalAccount -notin @('LocalService', 'LocalSystem', 'NetworkService')){ '{0} is not a valid option. Use one of the following options: LocalService, LocalSystem, NetworkService' -f $LocalAccount | Write-Warning return } [scriptblock] $scriptBlock = { param( [string[]] $BcModulesPath, [string] $ServerInstance, [string] $previousServiceAccount, [string] $LocalAccount ) try{ $BcModulesPath | Import-Module -Force "Updating service account from '{0}' to '{1}' on ServerInstance {2}." -f $previousServiceAccount, $LocalAccount, $ServerInstance | Write-Host Set-NAVServerInstance -ServerInstance $ServerInstance -ServiceAccount $LocalAccount } catch{ return $_ } } $additionParams = @{} if(-not $this._isLocalService -or $this._remotePsEnabled -eq $true){ $additionParams = @{'ComputerName' = $this._ComputerName} } Invoke-Command @additionParams -ScriptBlock $scriptBlock -ErrorAction Stop -ArgumentList @( $this.GetBcPsModules(), $this._ServerInstance, $this._ServiceAccount.UserName, $LocalAccount) $this.InitWinService() } ## END Service Account management } |