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 # 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) $this.Init() # Set the default visible properties of the object. [string[]] $visible = 'ServerInstance', 'Version', 'State', 'ServiceAccount', 'SqlInstance', 'DatabaseName' [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 } # 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 $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 } $executableVersion = Invoke-Command -ComputerName $this._ComputerName -ScriptBlock $scriptBlock ` -ArgumentList @($executablePath) -ErrorAction Stop return $executableVersion } [void] ImportBcPsModules(){ Import-BcModule -ServerInstance $this.ServerInstance -Force } ## 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( $WinService ) try{ Start-Service $WinService -WarningAction SilentlyContinue -ErrorAction Stop } catch{return $_} } 'Starting serverInstance {0} on computer {1}...' -f $this.ServerInstance, $this.ComputerName | Write-Host Invoke-Command -ComputerName $this.ComputerName -ScriptBlock $scriptBlock ` -ArgumentList @($this._ServiceName) -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( $WinService ) try{ Stop-Service $WinService -WarningAction SilentlyContinue -ErrorAction Stop } catch{return $_} } 'Stopping serverInstance {0} on computer {1}...' -f $this.ServerInstance, $this.ComputerName | Write-Host Invoke-Command -ComputerName $this.ComputerName -ScriptBlock $scriptBlock ` -ArgumentList @($this._ServiceName) -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] $ServerInstance, [string] $previousServiceAccount, [pscredential] $Credential, [switch] $NetworkService ) try{ if(-not (Get-Module -Name FpsBcDeployment -ListAvailable)){ Install-Module FpsBcDeployment -Force } Import-Module FpsBcDeployment Import-BcModule -ServerInstance $ServerInstance "Updating service account from '{0}' to '{1}' on ServerInstance {2}." -f $previousServiceAccount.UserName, $Credential.UserName, $ServerInstance | Write-Host Set-NAVServerInstance -ServerInstance $ServerInstance ` -ServiceAccount User -ServiceAccountCredential $Credential } catch{ return $_ } } Invoke-Command -ComputerName $this._ComputerName -ScriptBlock $scriptBlock ` -ArgumentList @($this._ServerInstance, $this._ServiceAccount, $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] $ServerInstance, [string] $previousServiceAccount, [string] $LocalAccount ) try{ if(-not (Get-Module -Name FpsBcDeployment -ListAvailable)){ Install-Module FpsBcDeployment -Force } Import-Module FpsBcDeployment Import-BcModule -ServerInstance $ServerInstance "Updating service account from '{0}' to '{1}' on ServerInstance {2}." -f $previousServiceAccount, $LocalAccount, $ServerInstance | Write-Host Set-NAVServerInstance -ServerInstance $ServerInstance -ServiceAccount $LocalAccount } catch{ return $_ } } Invoke-Command -ComputerName $this._ComputerName -ScriptBlock $scriptBlock ` -ArgumentList @($this._ServerInstance, $this._ServiceAccount.UserName, $LocalAccount) -ErrorAction Stop $this.InitWinService() } ## END Service Account management } |