351 lines
15 KiB
PowerShell
351 lines
15 KiB
PowerShell
|
function Install-AlkamiService {
|
||
|
<#
|
||
|
.SYNOPSIS
|
||
|
Installs an Alkami Windows Service
|
||
|
|
||
|
.DESCRIPTION
|
||
|
Installs an Alkami Windows Service.
|
||
|
Services installed via this function are not installed to a Service Fabric or other mesh implementation.
|
||
|
This installer does not create k8s or Lambdas or anything that is not a Windows Service.
|
||
|
|
||
|
If the service is found under the specified name and path already registered, this function will exit early and do nothing.
|
||
|
|
||
|
.PARAMETER ServicePath
|
||
|
The path to the service folder or service file. If to a folder, assumes that the file matches the path name.
|
||
|
Ex: C:\ProgramData\chocolatey\lib\Alkami.Services.Subscriptions.Host as a param would then find the file that matches Alkami.Services.Subscriptions.Host.exe under this path
|
||
|
|
||
|
.PARAMETER AssemblyInfo
|
||
|
The expected name of the service. This overrides the default option of matching the path ID.
|
||
|
|
||
|
.PARAMETER DisplayName
|
||
|
The expected display name of the service.
|
||
|
|
||
|
.PARAMETER IsDatabaseAccessRequired
|
||
|
Does this need to access a database?
|
||
|
|
||
|
.PARAMETER StartOnInstall
|
||
|
Should the program start on install? Defaults to true, must be used to not-start on install
|
||
|
|
||
|
.PARAMETER StartTimeout
|
||
|
The StartTimeout to be passed to Start-AlkamiService. Defaults to 60
|
||
|
|
||
|
.PARAMETER SetNewRelicConfiguration
|
||
|
Should the New Relic configuration be applied to this service?
|
||
|
|
||
|
.PARAMETER StartType
|
||
|
Options are "Manual","Delayed","Automatic","Disabled","AutomaticDelayedStart"
|
||
|
See also the switches: StartDelayed, StartDisabled, StartAutomatically
|
||
|
|
||
|
.EXAMPLE
|
||
|
Install-AlkamiService C:\ProgramData\chocolatey\lib\Alkami.Services.Subscriptions.Host
|
||
|
#>
|
||
|
[CmdletBinding(DefaultParameterSetName = 'StartSpecified')]
|
||
|
[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidDefaultValueSwitchParameter', '', Scope='Function', Justification='Negated-action switch names are non-intuitive. It should take hoops to disable.')]
|
||
|
Param(
|
||
|
[Parameter(Mandatory = $true, Position = 0)]
|
||
|
[Alias("Path")]
|
||
|
[string]$ServicePath,
|
||
|
[Parameter(Mandatory = $false, Position = 1)]
|
||
|
[Alias("Name")]
|
||
|
[string]$AssemblyInfo,
|
||
|
[string]$DisplayName,
|
||
|
[Alias("UseDBUser")]
|
||
|
[switch]$IsDatabaseAccessRequired,
|
||
|
# Set to true for the dev experience. Expect the pipeline to set to false.
|
||
|
[switch]$StartOnInstall = $true,
|
||
|
[int]$StartTimeout = 60,
|
||
|
[switch]$SetNewRelicConfiguration,
|
||
|
|
||
|
[Parameter(Mandatory = $false, Position = 2)]
|
||
|
[Parameter(ParameterSetName = "StartSpecified")]
|
||
|
[ValidateSet("Manual","Delayed","Automatic","Disabled","AutomaticDelayedStart","")]
|
||
|
[string]$StartType = "",
|
||
|
[Alias("AutoStart")]
|
||
|
[Parameter(ParameterSetName = "StartAutomatically")]
|
||
|
[switch]$StartAutomatically,
|
||
|
[Alias("Disabled")]
|
||
|
[Parameter(ParameterSetName = "StartDisabled")]
|
||
|
[switch]$StartDisabled,
|
||
|
[Alias("Manual")]
|
||
|
[Parameter(ParameterSetName = "StartManual")]
|
||
|
[switch]$StartManual,
|
||
|
[Alias("Delayed")]
|
||
|
[Parameter(ParameterSetName = "StartDelayed")]
|
||
|
[switch]$StartDelayed
|
||
|
)
|
||
|
|
||
|
$delayedParameterConstant = "delayed-auto"
|
||
|
$delayedConstant = "AutomaticDelayedStart"
|
||
|
$disabledParameterConstant = "disabled"
|
||
|
$disabledConstant = "Disabled"
|
||
|
$manualParameterConstant = "demand"
|
||
|
$manualConstant = "Manual"
|
||
|
$automaticParameterConstant = "autostart"
|
||
|
$automaticConstant = "Automatic"
|
||
|
|
||
|
$logLead = Get-LogLeadName
|
||
|
|
||
|
$tier0Services = Get-ServicesByTier -Tier 0
|
||
|
|
||
|
$StartOnInstallConfigSetting = (Get-ConfigSetting "Alkami.Installer.StartOnInstall")
|
||
|
if (![string]::IsNullOrWhiteSpace($StartOnInstallConfigSetting) -and ([bool]::TryParse($StartOnInstallConfigSetting,[ref]$StartOnInstallConfigSetting))) {
|
||
|
if ($StartOnInstall -ne $StartOnInstallConfigSetting) {
|
||
|
Write-Warning "$logLead : StartOnInstall flag conflicts with machine config [$StartOnInstallConfigSetting]"
|
||
|
}
|
||
|
$StartOnInstall = $StartOnInstallConfigSetting
|
||
|
}
|
||
|
|
||
|
# Tier 0 is hard coded to Automatic - Delayed -- SRE-17490
|
||
|
if($tier0Services -Contains $AssemblyInfo ){
|
||
|
$StartType = $delayedConstant
|
||
|
# Skip these checks for tier0
|
||
|
} else {
|
||
|
if ([string]::IsNullOrWhiteSpace($StartType)) {
|
||
|
$serviceStartupModeAppSettingKey = "Alkami.Installer.ServiceStartupMode"
|
||
|
$defaultInstallerMode = Get-AppSetting $serviceStartupModeAppSettingKey
|
||
|
if (![string]::IsNullOrWhiteSpace($defaultInstallerMode)) {
|
||
|
if ([string]::Equals($defaultInstallerMode,"OFF",[StringComparison]::CurrentCultureIgnoreCase)){
|
||
|
$StartType = $disabledConstant
|
||
|
} elseif ([string]::Equals($defaultInstallerMode,"ON",[StringComparison]::CurrentCultureIgnoreCase)){
|
||
|
$StartType = $manualConstant
|
||
|
} elseif ([string]::Equals($defaultInstallerMode,$automaticConstant,[StringComparison]::CurrentCultureIgnoreCase)){
|
||
|
$StartType = $automaticConstant
|
||
|
} else {
|
||
|
Write-Warning "$logLead : Can not parse appSetting [$serviceStartupModeAppSettingKey] with value [$defaultInstallerMode] (acceptable: on, off, $automaticConstant)"
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if ([string]::IsNullOrWhiteSpace($StartType)) {
|
||
|
$StartType = $manualConstant
|
||
|
}
|
||
|
|
||
|
# Allow the user to type either or on AutomaticDelayedStart or Disabled
|
||
|
# The value here is used in this lookup sequence and later with Set-Service -StartupType
|
||
|
if ($StartDelayed -or ($StartType -eq "Delayed")) {
|
||
|
$StartType = $delayedConstant
|
||
|
}
|
||
|
|
||
|
if ($StartAutomatically) {
|
||
|
$StartType = $automaticConstant
|
||
|
}
|
||
|
|
||
|
if ($StartDisabled) {
|
||
|
$StartType = $disabledConstant
|
||
|
}
|
||
|
|
||
|
if ($StartManual) {
|
||
|
$StartType = $manualConstant
|
||
|
}
|
||
|
}
|
||
|
|
||
|
# Hash lookup - quicker than a switch
|
||
|
$serviceInstallType = @{
|
||
|
Manual = $manualParameterConstant;
|
||
|
Disabled = $disabledParameterConstant;
|
||
|
Automatic = $automaticParameterConstant;
|
||
|
AutomaticDelayedStart = $delayedParameterConstant;
|
||
|
}.$StartType
|
||
|
|
||
|
<#
|
||
|
# This is gated behind the pipeline, so we have no need to test right now.
|
||
|
if (!(Test-IsDeveloperMachine) -and (Test-IsWebServer)) {
|
||
|
Write-Warning "$loglead : Can not install microservices on the web tier"
|
||
|
return
|
||
|
}
|
||
|
#>
|
||
|
if (!(Test-Path $ServicePath)) {
|
||
|
Write-Warning "$logLead : Path passed in does not represent a valid path: [$ServicePath]. Can not install something which does not exist."
|
||
|
return
|
||
|
}
|
||
|
|
||
|
# Ensure it doesn't end with exe (yet)
|
||
|
if ($AssemblyInfo.EndsWith(".exe")) {
|
||
|
$AssemblyInfo = $AssemblyInfo.Remove($AssemblyInfo.LastIndexOf(".exe"))
|
||
|
}
|
||
|
|
||
|
$ServicePath = (Resolve-Path $ServicePath)
|
||
|
$folderPath = $ServicePath
|
||
|
$serviceAlreadyExists = $false
|
||
|
|
||
|
$stopWatch = [System.Diagnostics.Stopwatch]::StartNew()
|
||
|
|
||
|
$serviceAccount = (Get-MachineConfigServiceAccount -IsDatabaseAccessRequired:$IsDatabaseAccessRequired)
|
||
|
|
||
|
$item = (Get-Item $ServicePath)
|
||
|
if ($item.PSIsContainer) {
|
||
|
# path was a folder
|
||
|
$leafName = (Split-Path -Path $ServicePath -Leaf)
|
||
|
if ($leafName -match 'tools') {
|
||
|
$ServicePath = (Split-Path -Path $ServicePath -Parent)
|
||
|
$leafName = (Split-Path -Path $ServicePath -Leaf)
|
||
|
}
|
||
|
|
||
|
# We still don't have this value, let's pretend it's the folder name then
|
||
|
if ([string]::IsNullOrWhiteSpace($AssemblyInfo)) {
|
||
|
$AssemblyInfo = $leafName
|
||
|
}
|
||
|
|
||
|
$exeName = "$AssemblyInfo.exe"
|
||
|
|
||
|
Write-Host "$logLead : looking for [$exeName] in [$ServicePath]"
|
||
|
|
||
|
$exeItems = @()
|
||
|
# Start with @("app","lib","tools") then do the naive scan
|
||
|
foreach($subpath in @("app","lib","tools")) {
|
||
|
$expectedSubpath = Join-Path -Path $ServicePath -ChildPath $subpath
|
||
|
$expectedSubpathExe = Join-Path -Path $expectedSubpath -ChildPath $exeName
|
||
|
if (Test-Path -Path $expectedSubpathExe)
|
||
|
{
|
||
|
$exeItems = @(Get-Item -Path $expectedSubpathExe)
|
||
|
}
|
||
|
else {
|
||
|
Write-Host "Tried to find an item in a subpath [$expectedSubpath] that doesn't exist. This is fine."
|
||
|
}
|
||
|
|
||
|
if (-not (Test-IsCollectionNullOrEmpty -Collection $exeItems)) {
|
||
|
Write-Host "Found an exe in [$expectedSubpath] at [$expectedSubpathExe]"
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
# If we still didn't find anything, fall back to the "naive" pattern
|
||
|
if (Test-IsCollectionNullOrEmpty -Collection $exeItems) {
|
||
|
Write-Host "$logLead : Looking for files via naive-lookup path"
|
||
|
$exeItems = Get-ChildItem -Path (Join-Path $ServicePath $exeName) -Recurse
|
||
|
}
|
||
|
|
||
|
# If we are in naive and find the same .exe multiple times, throw
|
||
|
if ($exeItems.Count -gt 1) {
|
||
|
$additionalDetailMessage = ", can not continue as we don't know which to use"
|
||
|
if ($ErrorActionPreference -ne 'Stop') {
|
||
|
$additionalDetailMessage = ", going to try to install the first one."
|
||
|
}
|
||
|
|
||
|
Write-Error "$logLead : More than one EXE available$additionalDetailMessage"
|
||
|
Write-Host "$logLead : $([string].Join(',',$exeItems.FullName))"
|
||
|
}
|
||
|
|
||
|
$exeItem = $exeItems | Select-Object -First 1 # there should only be one exe with the [package name].exe inside the folders
|
||
|
|
||
|
if ($null -eq $exeItem) {
|
||
|
$stopWatch.Stop()
|
||
|
Write-Warning "$logLead : Path passed in does not contain an exe with the filename: [$exeName]. Can not install a non-existent microservice in [$($stopWatch.Elapsed)]."
|
||
|
return
|
||
|
}
|
||
|
|
||
|
$ServicePath = $exeItem.FullName
|
||
|
} else {
|
||
|
# The $item was not a .PSIsContainer so it was a file
|
||
|
# That means we want the folderPath to be the folder of the file
|
||
|
$folderPath = (Split-Path -Path $ServicePath -Parent)
|
||
|
|
||
|
# We still don't have this value, let's pretend it's the folder name then
|
||
|
if ([string]::IsNullOrWhiteSpace($AssemblyInfo)) {
|
||
|
$AssemblyInfo = (Split-Path -Path $ServicePath -Leaf).Replace(".exe","")
|
||
|
}
|
||
|
}
|
||
|
|
||
|
$exeName = (Split-Path -Path $ServicePath -Leaf)
|
||
|
|
||
|
$serviceAlreadyExists = $null -ne (Get-Service -Name $AssemblyInfo -ErrorAction Ignore)
|
||
|
|
||
|
# If the service already existed we don't have to install it, but we still do the other things
|
||
|
if (!$serviceAlreadyExists) {
|
||
|
# Do the thing here to install the MS.
|
||
|
Write-Host "$logLead : Installing [$exeName] from $ServicePath"
|
||
|
|
||
|
$argumentList = @()
|
||
|
|
||
|
$argumentList += "create"
|
||
|
|
||
|
# SRE-17892 - If given a display-name (which only happens in New-AppTierWindowsServices) use that for the "service name"
|
||
|
# This is useful for Nag and Radium as the service name does not match what is expected
|
||
|
if (![string]::IsNullOrWhiteSpace($DisplayName)) {
|
||
|
$argumentList += "$DisplayName"
|
||
|
# HARD PIVOT
|
||
|
# We are now using DisplayName for AssemblyInfo. Before it was used for service lookup by file only
|
||
|
# Because we are changing how it is registered in _selective_ cases, we are going to change
|
||
|
# the rest of the file on how we use it
|
||
|
Write-Host "$logLead : Changing Install-AlkamiService to use [$DisplayName] for `$AssemblyInfo in-script - This notice is for SRE benefit."
|
||
|
$AssemblyInfo = $DisplayName
|
||
|
} else {
|
||
|
$argumentList += "$AssemblyInfo"
|
||
|
}
|
||
|
$argumentList += "binpath=$ServicePath"
|
||
|
$argumentList += "start=$serviceInstallType"
|
||
|
|
||
|
if (![string]::IsNullOrWhiteSpace($DisplayName)) {
|
||
|
$argumentList += "DisplayName=$DisplayName"
|
||
|
}
|
||
|
|
||
|
Write-Host $argumentList
|
||
|
|
||
|
Invoke-SCExe -Arguments $argumentList
|
||
|
Write-Host "$logLead : Successfully finished [$ServicePath] registration of service"
|
||
|
|
||
|
Write-Host "$logLead : 150ms sleep for [$ServicePath] for flushing"
|
||
|
Start-Sleep -Milliseconds 150
|
||
|
} else {
|
||
|
Stop-AlkamiService $AssemblyInfo
|
||
|
|
||
|
$splat = @{
|
||
|
ServiceName = $AssemblyInfo
|
||
|
StartupType = $StartType
|
||
|
}
|
||
|
|
||
|
if (![string]::IsNullOrWhiteSpace($DisplayName)) {
|
||
|
$splat.DisplayName = $DisplayName
|
||
|
}
|
||
|
# Powershell 5.1 doesn't support using Set-Service to set the start type to Automatic-Delayed. So we do it with SC.exe
|
||
|
if($splat.StartupType -eq $delayedConstant){
|
||
|
$params = @("config", $AssemblyInfo, "start=delayed-auto")
|
||
|
|
||
|
Invoke-SCExe -Arguments $params
|
||
|
$splat.Remove('StartupType')
|
||
|
}
|
||
|
|
||
|
# Call this regardless to set the DisplayName
|
||
|
Set-Service @splat
|
||
|
}
|
||
|
|
||
|
if ($null -ne (Get-Service $AssemblyInfo)) {
|
||
|
Write-Host "$logLead : Attempt to Set-ChocolateyPackageNewRelicState"
|
||
|
$packageName = (Get-ChocoPackageFromPath $ServicePath)
|
||
|
|
||
|
if (![string]::IsNullOrWhiteSpace($packageName)) {
|
||
|
Write-Verbose "$logLead : Set-ChocolateyPackageNewRelicState -Name `"$packageName`" -Enabled $SetNewRelicConfiguration"
|
||
|
Set-ChocolateyPackageNewRelicState -Name $packageName -Enabled $SetNewRelicConfiguration
|
||
|
}
|
||
|
} else {
|
||
|
throw "$loglead : Newly registered service could not be found."
|
||
|
}
|
||
|
|
||
|
# We always call this, whether we just installed it or it already existed
|
||
|
# The reason for this is two-fold.
|
||
|
# 1. In case we are correcting a bad install.
|
||
|
# 2. Resets the gMSA "password already rolled" issue that we use "fixlogins" to resolve
|
||
|
# 2a. This wouldn't resolve "the service got installed 3 months ago" need for "fixlogins", just reinstall
|
||
|
Set-WindowsServiceExecutionAccount -ServiceName $AssemblyInfo -ServiceUser $serviceAccount -IsGMSAAccount
|
||
|
|
||
|
Set-ServiceRecoveryOneRestart -ServiceName $AssemblyInfo
|
||
|
|
||
|
if ($StartOnInstall) {
|
||
|
try {
|
||
|
Start-AlkamiService -ServiceName $AssemblyInfo -Timeout $StartTimeout
|
||
|
} catch {
|
||
|
Write-Warning "$logLead : Service could not be started. See logs above."
|
||
|
if ($serviceAlreadyExists) {
|
||
|
throw $_
|
||
|
} else {
|
||
|
Write-Warning "$logLead : Continuing with install so files will be present post chocolatey install. This is not reporting as an error due to the way chocolatey works during installs, where if it fails it rolls back, but we've already registered the service with Windows. The alternative is to uninstall on failure and let the package be removed."
|
||
|
}
|
||
|
}
|
||
|
} else {
|
||
|
Write-Host "$logLead : Service not started, flag to start was not present or was not set to true"
|
||
|
}
|
||
|
|
||
|
$stopWatch.Stop()
|
||
|
|
||
|
Write-Host "$logLead : [$AssemblyInfo] installed at [$ServicePath] in [$($stopWatch.Elapsed)]"
|
||
|
}
|