1119 lines
38 KiB
PowerShell
1119 lines
38 KiB
PowerShell
#!powershell
|
|
|
|
# Copyright: (c) 2014, Chris Hoffman <choffman@chathamfinancial.com>
|
|
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
|
|
|
#AnsibleRequires -CSharpUtil Ansible.Basic
|
|
#AnsibleRequires -CSharpUtil ansible_collections.ansible.windows.plugins.module_utils.SCManager
|
|
|
|
# Need to be able to specify values larger than [Int32]::MaxValue for certain time based options.
|
|
$uint32Type = [Func[[Object], [UInt32]]] { [UInt32]$args[0] }
|
|
$spec = @{
|
|
options = @{
|
|
dependencies = @{ type = 'list'; elements = 'str' }
|
|
dependency_action = @{ type = 'str'; default = 'set'; choices = 'add', 'remove', 'set' }
|
|
description = @{ type = 'str' }
|
|
desktop_interact = @{ type = 'bool'; default = $false }
|
|
display_name = @{ type = 'str' }
|
|
error_control = @{ type = 'str'; choices = 'critical', 'ignore', 'normal', 'severe' }
|
|
failure_actions = @{
|
|
type = 'list'
|
|
elements = 'dict'
|
|
options = @{
|
|
delay_ms = @{ aliases = , 'delay'; type = $uint32Type; default = 0 }
|
|
type = @{ type = 'str'; choices = 'none', 'reboot', 'restart', 'run_command'; required = $true }
|
|
}
|
|
}
|
|
failure_actions_on_non_crash_failure = @{ type = 'bool' }
|
|
failure_command = @{ type = 'str' }
|
|
failure_reboot_msg = @{ type = 'str' }
|
|
failure_reset_period_sec = @{ aliases = , 'failure_reset_period'; type = $uint32Type }
|
|
force_dependent_services = @{ type = 'bool'; default = $false }
|
|
load_order_group = @{ type = 'str' }
|
|
name = @{ type = 'str'; required = $true }
|
|
password = @{ type = 'str'; no_log = $true }
|
|
path = @{ type = 'str'; }
|
|
pre_shutdown_timeout_ms = @{ aliases = , 'pre_shutdown_timeout'; type = $uint32Type }
|
|
required_privileges = @{ type = 'list'; elements = 'str' }
|
|
service_type = @{
|
|
type = 'str'
|
|
choices = 'win32_own_process', 'win32_share_process', 'user_own_process', 'user_share_process'
|
|
}
|
|
sid_info = @{ type = 'str'; choices = 'none', 'restricted', 'unrestricted' }
|
|
start_mode = @{ type = 'str'; choices = 'auto', 'manual', 'disabled', 'delayed' }
|
|
state = @{ type = 'str'; choices = 'started', 'stopped', 'restarted', 'absent', 'paused' }
|
|
update_password = @{ type = 'str'; choices = 'always', 'on_create' }
|
|
username = @{ type = 'str' }
|
|
}
|
|
required_by = @{
|
|
password = @('username')
|
|
}
|
|
supports_check_mode = $true
|
|
}
|
|
|
|
$module = [Ansible.Basic.AnsibleModule]::Create($args, $spec)
|
|
|
|
$dependencies = $module.Params.dependencies
|
|
$dependencyAction = $module.Params.dependency_action
|
|
$description = $module.Params.description
|
|
$desktopInteract = $module.Params.desktop_interact
|
|
$displayName = $module.Params.display_name
|
|
$errorControl = $module.Params.error_control
|
|
$failureActions = $module.Params.failure_actions
|
|
$failureActionsOnNonCrashFailure = $module.Params.failure_actions_on_non_crash_failure
|
|
$failureCommand = $module.Params.failure_command
|
|
$failureRebootMsg = $module.Params.failure_reboot_msg
|
|
$failureResetPeriodSec = $module.Params.failure_reset_period_sec
|
|
$forceDependentServices = $module.Params.force_dependent_services
|
|
$loadOrderGroup = $module.Params.load_order_group
|
|
$name = $module.Params.name
|
|
$password = $module.Params.password
|
|
$path = $module.Params.path
|
|
$preShutdownTimeoutMs = $module.Params.pre_shutdown_timeout_ms
|
|
$requiredPrivileges = $module.Params.required_privileges
|
|
$serviceType = $module.Params.service_type
|
|
$sidInfo = $module.Params.sid_info
|
|
$startMode = $module.Params.start_mode
|
|
$state = $module.Params.state
|
|
$updatePassword = $module.Params.update_password
|
|
$username = $module.Params.username
|
|
|
|
$module.Result.exists = $false
|
|
|
|
if ($null -ne $path) {
|
|
$path = [System.Environment]::ExpandEnvironmentVariables($path)
|
|
}
|
|
|
|
Function ConvertTo-SecurityIdentifier {
|
|
[CmdletBinding()]
|
|
[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingEmptyCatchBlock', '',
|
|
Justification = 'Ignore a SID conversion eror and try alternative, we do not care about the error')]
|
|
param (
|
|
[Parameter(Mandatory = $true)]
|
|
[String]$Name
|
|
)
|
|
|
|
# First check if the account is already a SID
|
|
try {
|
|
New-Object -TypeName System.Security.Principal.SecurityIdentifier -ArgumentList $Name
|
|
return
|
|
}
|
|
catch [ArgumentException] {}
|
|
|
|
# win_service allows LocalSystem as a way of specifying SYSTEM as that is what the GUI shows. This name cannot be
|
|
# translated so we have a manual check for it.
|
|
if ($Name -eq 'LocalSystem') {
|
|
[System.Security.Principal.SecurityIdentifier]'S-1-5-18'
|
|
return
|
|
}
|
|
|
|
# Handle cases when referencing a local user like .\account.
|
|
if ($Name.Contains('\')) {
|
|
$nameSplit = $Name.Split('\', 2)
|
|
if ($nameSplit[0] -eq '.') {
|
|
$domain = $env:COMPUTERNAME
|
|
}
|
|
else {
|
|
$domain = $nameSplit[0]
|
|
}
|
|
$username = $nameSplit[1]
|
|
|
|
$ntAccount = New-Object -TypeName System.Security.Principal.NTAccount -ArgumentList $domain, $username
|
|
}
|
|
else {
|
|
$ntAccount = New-Object -TypeName System.Security.Principal.NTAccount -ArgumentList $Name
|
|
}
|
|
|
|
$ntAccount.Translate([System.Security.Principal.SecurityIdentifier])
|
|
}
|
|
|
|
Function ConvertTo-ServiceFailureActionsDiff {
|
|
[CmdletBinding()]
|
|
param (
|
|
[Parameter(Mandatory = $true)]
|
|
[AllowEmptyCollection()]
|
|
[Object[]]
|
|
$FailureActions
|
|
)
|
|
|
|
foreach ($action in $FailureActions) {
|
|
[Ordered]@{
|
|
type = switch ($action.Type) {
|
|
None { 'none' }
|
|
Reboot { 'reboot' }
|
|
Restart { 'restart' }
|
|
RunCommand { 'run_command' }
|
|
}
|
|
delay_ms = $action.Delay
|
|
}
|
|
}
|
|
}
|
|
|
|
Function ConvertTo-ServiceStartModeDiff {
|
|
[CmdletBinding()]
|
|
param (
|
|
[Parameter(Mandatory = $true)]
|
|
[Ansible.Windows.SCManager.ServiceStartType]
|
|
$StartMode
|
|
)
|
|
|
|
# Convert the enum ServiceStartType to the value for a diff.
|
|
switch ($StartMode) {
|
|
BootStart { 'boot' }
|
|
SystemStart { 'system' }
|
|
AutoStart { 'auto' }
|
|
DemandStart { 'manual' }
|
|
Disabled { 'disabled' }
|
|
AutoStartDelayed { 'delayed' }
|
|
}
|
|
}
|
|
|
|
Function ConvertTo-ServiceStateDiff {
|
|
[CmdletBinding()]
|
|
param (
|
|
[Parameter(Mandatory = $true)]
|
|
[Ansible.Windows.SCManager.ServiceStatus]
|
|
$State
|
|
)
|
|
|
|
# Converts the enum ServiceStatus to the value for a diff.
|
|
switch ($State) {
|
|
Stopped { 'stopped' }
|
|
StartPending { 'start_pending' }
|
|
StopPending { 'stop_pending' }
|
|
Running { 'started' }
|
|
ContinuePending { 'continue_pending' }
|
|
PausePending { 'pause_pending' }
|
|
Paused { 'paused' }
|
|
}
|
|
}
|
|
|
|
Function ConvertTo-ServiceTypeDiff {
|
|
[CmdletBinding()]
|
|
param (
|
|
[Parameter(Mandatory = $true)]
|
|
[Ansible.Windows.SCManager.ServiceType]
|
|
$ServiceType
|
|
)
|
|
|
|
# Converts the enum ServiceType to the valud for a diff.
|
|
$ServiceType = [uint32]$ServiceType -band -bnot [uint32][Ansible.Windows.SCManager.ServiceType]::InteractiveProcess
|
|
$ServiceType = $ServiceType -band -bnot [uint32][Ansible.Windows.SCManager.ServiceType]::UserServiceInstance
|
|
switch ($ServiceType.ToString()) {
|
|
KernelDriver { 'kernel_driver' }
|
|
FileSystemDriver { 'file_system_driver' }
|
|
Adapter { 'adapter' }
|
|
RecognizerDriver { 'recognizer_driver' }
|
|
Win32OwnProcess { 'win32_own_process' }
|
|
Win32ShareProcess { 'win32_share_process' }
|
|
UserOwnprocess { 'user_own_process' }
|
|
UserShareProcess { 'user_share_process' }
|
|
PkgService { 'pkg_service' }
|
|
}
|
|
}
|
|
|
|
Function Get-ServiceDiff {
|
|
[CmdletBinding()]
|
|
param (
|
|
[Ansible.Windows.SCManager.Service]
|
|
$Service
|
|
)
|
|
|
|
if (-not $Service) {
|
|
""
|
|
}
|
|
else {
|
|
$diff = @{
|
|
dependencies = $Service.DependentOn
|
|
description = $Service.Description
|
|
desktop_interact = $Service.ServiceType.HasFlag(
|
|
[Ansible.Windows.SCManager.ServiceType]::InteractiveProcess)
|
|
display_name = $Service.DisplayName
|
|
error_control = $Service.ErrorControl.ToString().ToLowerInvariant()
|
|
failure_actions = @(ConvertTo-ServiceFailureActionsDiff -FailureActions $Service.FailureActions.Actions)
|
|
failure_actions_on_non_crash_failure = $Service.FailureActionsOnNonCrashFailures
|
|
failure_command = $Service.FailureActions.Command
|
|
failure_reboot_msg = $Service.FailureActions.RebootMsg
|
|
failure_reset_period_sec = $Service.FailureActions.ResetPeriod
|
|
load_order_group = $Service.LoadOrderGroup
|
|
name = $Service.ServiceName
|
|
path = $Service.Path
|
|
pre_shutdown_timeout_ms = $Service.PreShutdownTimeout
|
|
required_privileges = $Service.RequiredPrivileges
|
|
service_type = ConvertTo-ServiceTypeDiff -ServiceType $Service.ServiceType
|
|
sid_info = $Service.ServiceSidInfo.ToString().ToLowerInvariant()
|
|
start_mode = ConvertTo-ServiceStartModeDiff -StartMode $Service.StartType
|
|
state = ConvertTo-ServiceStateDiff -State $Service.State
|
|
}
|
|
|
|
# Only set the username/password diff if we have the proper type.
|
|
if (
|
|
-not (
|
|
$Service.ServiceType.HasFlag([Ansible.Windows.SCManager.ServiceType]::KernelDriver) -or
|
|
$service.ServiceType.HasFlag([Ansible.Windows.SCManager.ServiceType]::FileSystemDriver)
|
|
)
|
|
) {
|
|
$diff.username = $Service.Account.Value
|
|
$diff.password = 'REDACTED'
|
|
}
|
|
|
|
$diff
|
|
}
|
|
}
|
|
|
|
Function Get-ServiceFromName {
|
|
[CmdletBinding()]
|
|
param (
|
|
[Parameter(Mandatory = $true)]
|
|
[String]
|
|
$Name
|
|
)
|
|
|
|
# Get-Service -Name * doesn't work if the name has a wildcard char like '[]'. Use Where-Object to achieve the same
|
|
# result just in a slightly more expensive way.
|
|
$service = Get-Service | Where-Object { $_.ServiceName -eq $Name -or $_.DisplayName -eq $Name }
|
|
|
|
# https://github.com/ansible-collections/ansible.windows/issues/115
|
|
# Get-Service without a name will not output driver services whereas -Name does. Fallback to checking with -Name
|
|
# if we couldn't find a match from above.
|
|
if ($service) {
|
|
return $service
|
|
}
|
|
Get-Service -Name $Name -ErrorAction SilentlyContinue
|
|
}
|
|
|
|
Function Set-ServiceAccount {
|
|
[CmdletBinding()]
|
|
param (
|
|
[String]
|
|
$Username,
|
|
|
|
[String]
|
|
$Password,
|
|
|
|
[Switch]
|
|
$UpdatePassword,
|
|
|
|
[Ansible.Basic.AnsibleModule]
|
|
$Module,
|
|
|
|
[Ansible.Windows.SCManager.Service]
|
|
$Service
|
|
)
|
|
|
|
$Module.Diff.after.username = $Username
|
|
$Module.Diff.after.password = 'REDACTED'
|
|
if ($null -ne $Service -and $Service.Account) {
|
|
$desiredSid = $null
|
|
if ($Username) {
|
|
$desiredSid = ConvertTo-SecurityIdentifier -Name $Username
|
|
$Module.Diff.after.username = $desiredSid.Translate([System.Security.Principal.NTAccount]).Value
|
|
}
|
|
$actualSid = ConvertTo-SecurityIdentifier -Name $Service.Account.Value
|
|
|
|
if ($null -eq $desiredSid) {
|
|
$Module.Diff.after.username = $Service.Account.Value
|
|
}
|
|
elseif (-not $desiredSid.Equals($actualSid)) {
|
|
# We need to remove the desktop interact flag if we are changing from SYSTEM to another account
|
|
$systemSid = ConvertTo-SecurityIdentifier -Name 'LocalSystem'
|
|
if (-not $desiredSid.Equals($systemSid)) {
|
|
Set-ServiceType -DesktopInteract $false -Username $username -Module $Module -Service $Service
|
|
}
|
|
|
|
if (-not $Module.CheckMode) {
|
|
$Service.Account = $desiredSid
|
|
}
|
|
$UpdatePassword = $true # Always set the password if changing the account.
|
|
|
|
$Module.Result.changed = $true
|
|
}
|
|
|
|
# We cannot compare the password, so always change it based on the update_password module option.
|
|
if ($UpdatePassword -and $Password) {
|
|
$Module.Diff.after.password = 'CHANGED REDACTED'
|
|
|
|
if (-not $Module.CheckMode -and $Service) {
|
|
$Service.Password = $Password
|
|
}
|
|
$Module.Result.changed = $true
|
|
}
|
|
}
|
|
}
|
|
|
|
Function Set-ServiceDependency {
|
|
[CmdletBinding()]
|
|
param (
|
|
[String[]]
|
|
$Dependencies,
|
|
|
|
[String]
|
|
$DependencyAction,
|
|
|
|
[Ansible.Basic.AnsibleModule]
|
|
$Module,
|
|
|
|
[Ansible.Windows.SCManager.Service]
|
|
$Service
|
|
)
|
|
|
|
$Module.Diff.after.dependencies = $Dependencies
|
|
if ($null -ne $Service) {
|
|
$Module.Diff.after.dependencies = $Service.DependentOn
|
|
|
|
if ($null -ne $Dependencies) {
|
|
$existingDependencies = [String[]]$Service.DependentOn
|
|
[String[]]$addedDependents = @()
|
|
[String[]]$removedDependents = @()
|
|
|
|
if ($DependencyAction -eq 'add') {
|
|
$addedDependents = [Linq.Enumerable]::Except($Dependencies, $existingDependencies)
|
|
}
|
|
elseif ($DependencyAction -eq 'remove') {
|
|
$removedDependents = [Linq.Enumerable]::Intersect($Dependencies, $existingDependencies)
|
|
}
|
|
else {
|
|
$addedDependents = [Linq.Enumerable]::Except($Dependencies, $existingDependencies)
|
|
$removedDependents = [Linq.Enumerable]::Except($existingDependencies, $Dependencies)
|
|
}
|
|
|
|
if ($addedDependents -or $removedDependents) {
|
|
$newDependents = $Service.DependentOn
|
|
foreach ($toRemove in $removedDependents) {
|
|
$null = $newDependents.Remove($toRemove)
|
|
}
|
|
foreach ($toAdd in $addedDependents) {
|
|
$newDependents.Add($toAdd)
|
|
}
|
|
$Module.Diff.after.dependencies = $newDependents
|
|
|
|
if (-not $Module.CheckMode) {
|
|
$Service.DependentOn = $newDependents
|
|
}
|
|
$Module.Result.changed = $true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Function Set-ServiceDescription {
|
|
[CmdletBinding()]
|
|
param (
|
|
# [String] - Cannot set so we can pass $null in (empty string is delete while $null is preserve).
|
|
$Description,
|
|
|
|
[Ansible.Basic.AnsibleModule]
|
|
$Module,
|
|
|
|
[Ansible.Windows.SCManager.Service]
|
|
$Service
|
|
)
|
|
|
|
$Module.Diff.after.description = $Description
|
|
if ($null -ne $Service) {
|
|
if ($null -eq $Description) {
|
|
$Module.Diff.after.description = $Service.Description
|
|
}
|
|
else {
|
|
if (-not $Description) {
|
|
$Description = $null
|
|
}
|
|
|
|
if ($Description -cne $Service.Description) {
|
|
if (-not $Module.CheckMode) {
|
|
$Service.Description = $Description
|
|
}
|
|
$Module.Result.changed = $true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Function Set-ServiceDisplayName {
|
|
[CmdletBinding()]
|
|
param (
|
|
[String]
|
|
$DisplayName,
|
|
|
|
[Ansible.Basic.AnsibleModule]
|
|
$Module,
|
|
|
|
[Ansible.Windows.SCManager.Service]
|
|
$Service
|
|
)
|
|
|
|
$Module.Diff.after.display_name = $DisplayName
|
|
if ($null -ne $Service) {
|
|
if (-not $DisplayName) {
|
|
$Module.Diff.after.display_name = $Service.DisplayName
|
|
}
|
|
elseif ($DisplayName -cne $Service.DisplayName) {
|
|
if (-not $Module.CheckMode) {
|
|
$Service.DisplayName = $DisplayName
|
|
}
|
|
$Module.Result.changed = $true
|
|
}
|
|
}
|
|
}
|
|
|
|
Function Set-ServiceErrorControl {
|
|
[CmdletBinding()]
|
|
param (
|
|
[String]
|
|
$ErrorControl,
|
|
|
|
[Ansible.Basic.AnsibleModule]
|
|
$Module,
|
|
|
|
[Ansible.Windows.SCManager.Service]
|
|
$Service
|
|
)
|
|
|
|
$Module.Diff.after.error_control = $ErrorControl
|
|
if ($null -ne $Service) {
|
|
if (-not $ErrorControl) {
|
|
$Module.Diff.after.error_control = $Service.ErrorControl.ToString().ToLowerInvariant()
|
|
}
|
|
elseif ([Ansible.Windows.SCManager.ErrorControl]$ErrorControl -ne $Service.ErrorControl) {
|
|
if (-not $Module.CheckMode) {
|
|
$Service.ErrorControl = $ErrorControl
|
|
}
|
|
$Module.Result.changed = $true
|
|
}
|
|
}
|
|
}
|
|
|
|
Function Set-ServiceFailureAction {
|
|
[CmdletBinding()]
|
|
param (
|
|
[Object[]]
|
|
$Actions,
|
|
|
|
# [Bool] - Allow passing in $null
|
|
$ActionOnNonCrashFailure,
|
|
|
|
# [String]
|
|
$Command,
|
|
|
|
# [String]
|
|
$RebootMsg,
|
|
|
|
# [UInt32]
|
|
$ResetPeriod,
|
|
|
|
[Ansible.Basic.AnsibleModule]
|
|
$Module,
|
|
|
|
[Ansible.Windows.SCManager.Service]
|
|
$Service
|
|
)
|
|
|
|
$Module.Diff.after.failure_actions = $Actions
|
|
$Module.Diff.after.failure_actions_on_non_crash_failure = $ActionOnCrashFailure
|
|
$Module.Diff.after.failure_command = $Command
|
|
$Module.Diff.after.failure_reboot_msg = $RebootMsg
|
|
$Module.Diff.after.failure_reset_period_sec = $ResetPeriod
|
|
|
|
if ($null -ne $Service) {
|
|
if ($null -eq $ActionOnNonCrashFailure) {
|
|
$Module.Diff.after.failure_actions_on_non_crash_failure = $Service.FailureActionsOnNonCrashFailures
|
|
}
|
|
elseif ($ActionOnNonCrashFailure -ne $Service.FailureActionsOnNonCrashFailures) {
|
|
if (-not $Module.CheckMode) {
|
|
$Service.FailureActionsOnNonCrashFailures = $ActionOnNonCrashFailure
|
|
}
|
|
$Module.Result.changed = $true
|
|
}
|
|
|
|
$newFailures = New-Object -TypeName Ansible.Windows.SCManager.FailureActions
|
|
$changed = $false
|
|
|
|
if ($null -eq $Actions) {
|
|
$diff = @(ConvertTo-ServiceFailureActionsDiff -FailureActions $Service.FailureActions.Actions)
|
|
$Module.Diff.after.failure_actions = $diff
|
|
}
|
|
else {
|
|
$newActions = [System.Collections.Generic.List[Ansible.Windows.SCManager.Action]]@()
|
|
foreach ($action in $Actions) {
|
|
$newActions.Add([Ansible.Windows.SCManager.Action]@{
|
|
Type = switch ($action.type) {
|
|
none { 'None' }
|
|
reboot { 'Reboot' }
|
|
restart { 'Restart' }
|
|
run_command { 'RunCommand' }
|
|
}
|
|
Delay = $action.delay_ms
|
|
})
|
|
}
|
|
|
|
$changed = $newActions.Count -ne $Service.FailureActions.Actions.Count
|
|
if (-not $changed) {
|
|
for ($i = 0; $i -lt $newActions.Count; $i++) {
|
|
$existing = $Service.FailureActions.Actions[$i]
|
|
$new = $newActions[$i]
|
|
|
|
if ($new.Type -ne $existing.Type -or $new.Delay -ne $existing.Delay) {
|
|
$changed = $true
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
if ($changed) {
|
|
$newFailures.Actions = $newActions
|
|
}
|
|
}
|
|
|
|
if ($null -eq $Command) {
|
|
$Module.Diff.after.failure_command = $Service.FailureActions.Command
|
|
}
|
|
else {
|
|
if (-not $Command) {
|
|
# Empty string resets the value but we still want idempotency.
|
|
$Command = $null
|
|
}
|
|
|
|
if ($Command -cne $Service.FailureActions.Command) {
|
|
$newFailures.Command = $Command
|
|
$changed = $true
|
|
}
|
|
}
|
|
|
|
if ($null -eq $RebootMsg) {
|
|
$Module.Diff.after.failure_reboot_msg = $Service.FailureActions.RebootMsg
|
|
}
|
|
else {
|
|
if (-not $RebootMsg) {
|
|
# See Command
|
|
$RebootMsg = $null
|
|
}
|
|
|
|
if ($RebootMsg -cne $Service.FailureActions.RebootMsg) {
|
|
$newFailures.RebootMsg = $RebootMsg
|
|
$changed = $true
|
|
}
|
|
}
|
|
|
|
if ($null -eq $ResetPeriod) {
|
|
$Module.Diff.after.failure_reset_period_sec = $Service.FailureActions.ResetPeriod
|
|
}
|
|
elseif ($ResetPeriod -ne $Service.FailureActions.ResetPeriod) {
|
|
$newFailures.ResetPeriod = $ResetPeriod
|
|
$changed = $true
|
|
}
|
|
|
|
if ($changed) {
|
|
if (-not $Module.CheckMode) {
|
|
$Service.FailureActions = $newFailures
|
|
}
|
|
$Module.Result.changed = $true
|
|
}
|
|
}
|
|
}
|
|
|
|
Function Set-ServiceLoadOrderGroup {
|
|
[CmdletBinding()]
|
|
param (
|
|
# [String] - Cannot set so we can pass $null in (empty string is delete while $null is preserve).
|
|
$LoadOrderGroup,
|
|
|
|
[Ansible.Basic.AnsibleModule]
|
|
$Module,
|
|
|
|
[Ansible.Windows.SCManager.Service]
|
|
$Service
|
|
)
|
|
|
|
$Module.Diff.after.load_order_group = $LoadOrderGroup
|
|
if ($null -ne $Service) {
|
|
if ($null -eq $LoadOrderGroup) {
|
|
$Module.Diff.after.load_order_group = $Service.LoadOrderGroup
|
|
}
|
|
else {
|
|
if ($LoadOrderGroup -cne $Service.LoadOrderGroup) {
|
|
if (-not $Module.CheckMode) {
|
|
$Service.LoadOrderGroup = $LoadOrderGroup
|
|
}
|
|
$Module.Result.changed = $true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Function Set-ServicePath {
|
|
[CmdletBinding()]
|
|
param (
|
|
[String]
|
|
[AllowNull()]
|
|
$Path,
|
|
|
|
[Ansible.Basic.AnsibleModule]
|
|
$Module,
|
|
|
|
[Ansible.Windows.SCManager.Service]
|
|
$Service
|
|
)
|
|
|
|
$Module.Diff.after.path = $Path
|
|
if ($null -ne $Service) {
|
|
if (-not $Path) {
|
|
$Module.Diff.after.path = $Service.Path
|
|
}
|
|
elseif ($Path -cne $Service.Path) {
|
|
if (-not $Module.CheckMode) {
|
|
$Service.Path = $Path
|
|
}
|
|
$Module.Result.changed = $true
|
|
}
|
|
}
|
|
}
|
|
|
|
Function Set-ServicePreShutdownTimeout {
|
|
[CmdletBinding()]
|
|
param (
|
|
# [UInt32] - Need to be able to pass $Null in
|
|
$PreShutdownTimeoutMs,
|
|
|
|
[Ansible.Basic.AnsibleModule]
|
|
$Module,
|
|
|
|
[Ansible.Windows.SCManager.Service]
|
|
$Service
|
|
)
|
|
|
|
$Module.Diff.after.pre_shutdown_timeout_ms = $PreShutdownTimeoutMs
|
|
if ($null -ne $Service) {
|
|
if ($null -eq $PreShutdownTimeoutMs) {
|
|
$Module.Diff.after.pre_shutdown_timeout_ms = $Service.PreShutdownTimeout
|
|
}
|
|
elseif ($PreShutdownTimeoutMs -ne $Service.PreShutdownTimeout) {
|
|
if (-not $Module.CheckMode) {
|
|
$Service.PreShutdownTimeout = $PreShutdownTimeoutMs
|
|
}
|
|
$Module.Result.changed = $true
|
|
}
|
|
}
|
|
}
|
|
|
|
Function Set-ServiceRequiredPrivilege {
|
|
[CmdletBinding()]
|
|
param (
|
|
[String[]]
|
|
$RequiredPrivileges,
|
|
|
|
[Ansible.Basic.AnsibleModule]
|
|
$Module,
|
|
|
|
[Ansible.Windows.SCManager.Service]
|
|
$Service
|
|
)
|
|
|
|
$Module.Diff.after.required_privileges = $RequiredPrivileges
|
|
if ($null -ne $Service) {
|
|
if ($null -eq $RequiredPrivileges) {
|
|
$Module.Diff.after.required_privileges = $Service.RequiredPrivileges
|
|
}
|
|
else {
|
|
$existingPrivis = $Service.RequiredPrivileges.ToArray()
|
|
$extraPrivs = [String[]][Linq.Enumerable]::Except($RequiredPrivileges, $existingPrivis)
|
|
$missingPrivs = [String[]][Linq.Enumerable]::Except($existingPrivis, $RequiredPrivileges)
|
|
|
|
if ($extraPrivs -or $missingPrivs) {
|
|
if (-not $Module.CheckMode) {
|
|
$Service.RequiredPrivileges = $RequiredPrivileges
|
|
}
|
|
$Module.Result.changed = $true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Function Set-ServiceSidInfo {
|
|
[CmdletBinding()]
|
|
param (
|
|
[String]
|
|
$SidInfo,
|
|
|
|
[Ansible.Basic.AnsibleModule]
|
|
$Module,
|
|
|
|
[Ansible.Windows.SCManager.Service]
|
|
$Service
|
|
)
|
|
|
|
$Module.Diff.after.sid_info = $SidInfo
|
|
if ($null -ne $Service) {
|
|
if (-not $SidInfo) {
|
|
$Module.Diff.after.sid_info = $Service.ServiceSidInfo.ToString().ToLowerInvariant()
|
|
}
|
|
elseif ([Ansible.Windows.SCManager.ServiceSidInfo]$SidInfo -ne $Service.ServiceSidInfo) {
|
|
if (-not $Module.CheckMode) {
|
|
$Service.ServiceSidInfo = $SidInfo
|
|
}
|
|
$Module.Result.changed = $true
|
|
}
|
|
}
|
|
}
|
|
|
|
Function Set-ServiceStartMode {
|
|
[CmdletBinding()]
|
|
param (
|
|
[String]
|
|
$StartMode,
|
|
|
|
[Ansible.Basic.AnsibleModule]
|
|
$Module,
|
|
|
|
[Ansible.Windows.SCManager.Service]
|
|
$Service
|
|
)
|
|
|
|
$Module.Diff.after.start_mode = $StartMode
|
|
if ($null -ne $Service) {
|
|
if (-not $StartMode) {
|
|
$Module.Diff.after.start_mode = ConvertTo-ServiceStartModeDiff -StartMode $Service.StartType
|
|
}
|
|
else {
|
|
$stateMap = @{
|
|
auto = [Ansible.Windows.SCManager.ServiceStartType]::AutoStart
|
|
delayed = [Ansible.Windows.SCManager.ServiceStartType]::AutoStartDelayed
|
|
disabled = [Ansible.Windows.SCManager.ServiceStartType]::Disabled
|
|
manual = [Ansible.Windows.SCManager.ServiceStartType]::DemandStart
|
|
}
|
|
|
|
foreach ($kvp in $stateMap.GetEnumerator()) {
|
|
if ($StartMode -ne $kvp.Key) {
|
|
continue
|
|
}
|
|
|
|
if ($Service.StartType -ne $kvp.Value) {
|
|
if (-not $Module.CheckMode) {
|
|
$Service.StartType = $kvp.Value
|
|
}
|
|
|
|
$Module.Result.changed = $true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Function Set-ServiceState {
|
|
[CmdletBinding()]
|
|
param (
|
|
[String]
|
|
$State,
|
|
|
|
[Switch]
|
|
$ForceDependentServices,
|
|
|
|
[Ansible.Basic.AnsibleModule]
|
|
$Module,
|
|
|
|
[Ansible.Windows.SCManager.Service]
|
|
$Service
|
|
)
|
|
|
|
$Module.Diff.after.state = $State
|
|
if ($null -ne $Service) {
|
|
$stateEnum = [Ansible.Windows.SCManager.ServiceStatus]
|
|
|
|
# Due to *-Service cmdlets in win ps struggling with wildcard chars like [ we need to pipe in an actual service
|
|
# object to the state changing cmdlets.
|
|
$winService = Get-ServiceFromName -Name $Service.ServiceName
|
|
|
|
if (-not $State) {
|
|
$Module.Diff.after.state = ConvertTo-ServiceStateDiff -State $Service.State
|
|
|
|
}
|
|
elseif ($State -eq 'started' -and $Service.State -ne $stateEnum::Running) {
|
|
if ($Service.State -eq $stateEnum::Paused) {
|
|
try {
|
|
$winService | Resume-Service -WhatIf:$Module.CheckMode
|
|
}
|
|
catch {
|
|
$msg = "failed to start service from paused state $($Service.ServiceName): $($_.Exception.Message)"
|
|
$Module.FailJson($msg, $_)
|
|
}
|
|
}
|
|
else {
|
|
$winService | Start-Service -WhatIf:$Module.CheckMode
|
|
}
|
|
$Module.Result.changed = $true
|
|
|
|
}
|
|
elseif ($State -eq 'stopped' -and $Service.State -ne $stateEnum::Stopped) {
|
|
$winService | Stop-Service -Force:$ForceDependentServices.IsPresent -WhatIf:$Module.CheckMode
|
|
$Module.Result.changed = $true
|
|
|
|
}
|
|
elseif ($State -eq 'restarted') {
|
|
$winService | Restart-Service -Force:$ForceDependentServices.IsPresent -WhatIf:$Module.CheckMode
|
|
$Module.Result.changed = $true
|
|
|
|
}
|
|
elseif ($State -eq 'paused' -and $Service.State -ne $stateEnum::Paused) {
|
|
if (-not $Service.ControlsAccepted.HasFlag([Ansible.Windows.SCManager.ControlsAccepted]::PauseContinue)) {
|
|
$Module.FailJson("failed to pause service $($Service.ServiceName): The service does not support pausing")
|
|
}
|
|
|
|
try {
|
|
$winService | Suspend-Service -WhatIf:$Module.CheckMode
|
|
}
|
|
catch {
|
|
$Module.FailJson("failed to pause service $($Service.ServiceName): $($_.Exception.Message)", $_)
|
|
}
|
|
$Module.Result.changed = $true
|
|
}
|
|
}
|
|
}
|
|
|
|
Function Set-ServiceType {
|
|
[CmdletBinding()]
|
|
param (
|
|
[String]
|
|
$ServiceType,
|
|
|
|
[Boolean]
|
|
$DesktopInteract,
|
|
|
|
[String]
|
|
$Username,
|
|
|
|
[Ansible.Basic.AnsibleModule]
|
|
$Module,
|
|
|
|
[Ansible.Windows.SCManager.Service]
|
|
$Service
|
|
)
|
|
|
|
$Module.Diff.after.desktop_interact = $DesktopInteract
|
|
$Module.Diff.after.service_type = $ServiceType
|
|
if ($null -ne $Service) {
|
|
[Ansible.Windows.SCManager.ServiceType]$desiredType = switch ($ServiceType) {
|
|
kernel_driver { [Ansible.Windows.SCManager.ServiceType]::KernelDriver }
|
|
file_system_driver { [Ansible.Windows.SCManager.ServiceType]::FileSystemDriver }
|
|
adapter { [Ansible.Windows.SCManager.ServiceType]::Adapter }
|
|
recognizer_driver { [Ansible.Windows.SCManager.ServiceType]::RecognizerDriver }
|
|
win32_own_process { [Ansible.Windows.SCManager.ServiceType]::Win32OwnProcess }
|
|
win32_share_process { [Ansible.Windows.SCManager.ServiceType]::Win32ShareProcess }
|
|
user_own_process { [Ansible.Windows.SCManager.ServiceType]::UserOwnprocess }
|
|
user_share_process { [Ansible.Windows.SCManager.ServiceType]::UserShareProcess }
|
|
default { $Service.ServiceType }
|
|
}
|
|
$Module.Diff.after.service_type = ConvertTo-ServiceTypeDiff -ServiceType $desiredType
|
|
|
|
$interactive = [uint32][Ansible.Windows.SCManager.ServiceType]::InteractiveProcess
|
|
if ($DesktopInteract) {
|
|
if ($Username) {
|
|
$actualAccount = ConvertTo-SecurityIdentifier -Name $Username
|
|
}
|
|
else {
|
|
$actualAccount = $Service.Account.Translate([System.Security.Principal.SecurityIdentifier])
|
|
}
|
|
$systemSid = ConvertTo-SecurityIdentifier -Name 'LocalSystem'
|
|
|
|
if (-not $actualAccount.Equals($systemSid)) {
|
|
$Module.FailJson("Can only set 'desktop_interact' to true when 'username' equals 'SYSTEM'")
|
|
}
|
|
|
|
$desiredType = [uint32]$desiredType -bor $interactive
|
|
}
|
|
else {
|
|
$desiredType = [uint32]$desiredType -band -bnot $interactive
|
|
}
|
|
|
|
if ($desiredType -ne $Service.ServiceType) {
|
|
if (-not $Module.CheckMode) {
|
|
$Service.ServiceType = $desiredType
|
|
}
|
|
$Module.Result.changed = $true
|
|
}
|
|
}
|
|
}
|
|
|
|
# We don't need the full gauntlet of service rights for this module. Only request the ones that are needed in case
|
|
# we come across a more restricted service.
|
|
# https://github.com/ansible-collections/ansible.windows/issues/118
|
|
$rights = [Ansible.Windows.SCManager.ServiceRights]'QueryConfig, QueryStatus, EnumerateDependents'
|
|
|
|
if ($failureActions) {
|
|
# If setting an SC_ACTION_RESTART failure action the handle needs to be opened with start rights.
|
|
$rights = $rights -bor [Ansible.Windows.SCManager.ServiceRights]::Start
|
|
}
|
|
|
|
if ($state -eq 'absent') {
|
|
$rights = $rights -bor [Ansible.Windows.SCManager.ServiceRights]::Delete
|
|
}
|
|
else {
|
|
$rights = $rights -bor [Ansible.Windows.SCManager.ServiceRights]::ChangeConfig
|
|
}
|
|
|
|
$service = Get-ServiceFromName -Name $name | ForEach-Object {
|
|
New-Object -TypeName Ansible.Windows.SCManager.Service -ArgumentList @(
|
|
$_.ServiceName, $rights
|
|
)
|
|
}
|
|
$module.Diff.before = Get-ServiceDiff -Service $service
|
|
|
|
if ($state -eq 'absent') {
|
|
if ($service) {
|
|
$winService = Get-ServiceFromName -Name $service.ServiceName
|
|
$winService | Stop-Service -Force:$forceDependentServices -WhatIf:$module.CheckMode
|
|
if (-not $module.CheckMode) {
|
|
$service.Delete()
|
|
}
|
|
|
|
$module.Result.changed = $true
|
|
}
|
|
|
|
$module.Diff.after = ""
|
|
}
|
|
else {
|
|
# win_service can be used as a way to get info from an existing service by not setting any of these parameters.
|
|
# This should be deprecated in the future in favour of win_service_info to simplify this module.
|
|
$detectChanges = (
|
|
$null -ne $service -or
|
|
$null -ne $description -or
|
|
$null -ne $displayName -or
|
|
$null -ne $dependencies -or
|
|
$dependencyAction -ne 'set' -or
|
|
$desktopInteract -eq $true -or
|
|
$null -ne $errorControl -or
|
|
$null -ne $failureActions -or
|
|
$null -ne $failureActionsOnNonCrashFailure -or
|
|
$null -ne $failureCommand -or
|
|
$null -ne $failureRebootMsg -or
|
|
$null -ne $failureResetPeriodSec -or
|
|
$null -ne $loadOrderGroup -or
|
|
$null -ne $path -or
|
|
$null -ne $preShutdownTimeoutMs -or
|
|
$null -ne $requiredPrivileges -or
|
|
$null -ne $serviceType -or
|
|
$null -ne $sidInfo -or
|
|
$null -ne $startMode -or
|
|
$null -ne $state -or
|
|
$null -ne $username -or
|
|
$null -ne $password
|
|
)
|
|
|
|
if ($detectChanges) {
|
|
$updatePassword = switch ($updatePassword) {
|
|
always { $true }
|
|
on_create { $false }
|
|
default {
|
|
# TODO: add deprecation warning for a change to always as the default.
|
|
$false
|
|
}
|
|
}
|
|
|
|
if (-not $service) {
|
|
$updatePassword = $true
|
|
|
|
if ($null -ne $path) {
|
|
$null = New-Service -Name $name -BinaryPathName $path -WhatIf:$module.CheckMode
|
|
if (-not $module.CheckMode) {
|
|
$service = New-Object -TypeName Ansible.Windows.SCManager.Service -ArgumentList $name
|
|
}
|
|
$module.Result.changed = $true
|
|
}
|
|
else {
|
|
$module.FailJson("Service '$name' is not installed, need to set 'path' to create a new service")
|
|
}
|
|
}
|
|
|
|
# Each component is set in the Set-Service* cmdlets below
|
|
$module.Diff.after = @{
|
|
name = if ($service) { $service.ServiceName } else { $name }
|
|
}
|
|
|
|
$common = @{Service = $service; Module = $module }
|
|
|
|
# The ServiceStartName for these types of services aren't the account it runs on so running this will fail
|
|
# to convert it to an account. While this could be configured in the future for now we just ignore the values.
|
|
if ( -not (
|
|
$service.ServiceType.HasFlag([Ansible.Windows.SCManager.ServiceType]::KernelDriver) -or
|
|
$service.ServiceType.HasFlag([Ansible.Windows.SCManager.ServiceType]::FileSystemDriver)
|
|
)) {
|
|
Set-ServiceAccount -Username $username -Password $password -UpdatePassword:$updatePassword @common
|
|
}
|
|
|
|
Set-ServiceDependency -Dependencies $dependencies -DependencyAction $dependencyAction @common
|
|
Set-ServiceDescription -Description $description @common
|
|
Set-ServiceDisplayName -DisplayName $displayName @common
|
|
Set-ServiceErrorControl -ErrorControl $errorControl @common
|
|
|
|
$failureParams = @{
|
|
Actions = $failureActions
|
|
ActionOnNonCrashFailure = $failureActionsOnNonCrashFailure
|
|
Command = $failureCommand
|
|
RebootMsg = $failureRebootMsg
|
|
ResetPeriod = $failureResetPeriodSec
|
|
}
|
|
Set-ServiceFailureAction @failureParams @common
|
|
|
|
Set-ServiceLoadOrderGroup -LoadOrderGroup $loadOrderGroup @common
|
|
Set-ServicePath -Path $path @common
|
|
Set-ServicePreShutdownTimeout -PreShutdownTimeoutMs $preShutdownTimeoutMs @common
|
|
Set-ServiceRequiredPrivilege -RequiredPrivileges $requiredPrivileges @common
|
|
Set-ServiceSidInfo -SidInfo $sidInfo @common
|
|
Set-ServiceStartMode -StartMode $startMode @common
|
|
Set-ServiceType -ServiceType $serviceType -DesktopInteract $desktopInteract -Username $username @common
|
|
|
|
# This should always be set last in case one of the config options above changes
|
|
Set-ServiceState -State $state -ForceDependentServices:$forceDependentServices @common
|
|
}
|
|
else {
|
|
# TODO: deprecate this scenario as state != 'absent' should require one of the params in $detectChanges.
|
|
$module.Diff.after = $module.Diff.before
|
|
}
|
|
}
|
|
|
|
# The after diff already contains the pre-formatted return information. This should be deprecated with no new
|
|
# fields returned as this is just a backwards compatibility nightmare.
|
|
if ($module.Diff.after) {
|
|
$after = $module.Diff.after
|
|
|
|
$module.Result.exists = $true
|
|
|
|
$module.Result.dependencies = $after.dependencies
|
|
$module.Result.description = $after.description
|
|
$module.Result.desktop_interact = $after.desktop_interact
|
|
$module.Result.display_name = $after.display_name
|
|
$module.Result.name = $after.name
|
|
$module.Result.path = $after.path
|
|
$module.Result.start_mode = $after.start_mode
|
|
|
|
# Backwards compat, win_service used to just return the raw StartName value of the service and not a normalised
|
|
# name that the diff has. The only case this matters if the username is SYSTEM which we correct to LocalSystem.
|
|
$module.Result.username = $after.username
|
|
if ($after.username) {
|
|
$userSid = ConvertTo-SecurityIdentifier -Name $after.username
|
|
$systemSid = ConvertTo-SecurityIdentifier -Name 'LocalSystem'
|
|
if ($userSid.Equals($systemSid)) {
|
|
$module.Result.username = 'LocalSystem'
|
|
}
|
|
}
|
|
|
|
# Backwards compat, returned state was just a lowercase state of (Get-Service).State, whereas the diff matches
|
|
# the output of win_service_info, i.e. 'continue_pending' becomes 'continuepending'. Also started needs to become
|
|
# running to match the original value here.
|
|
$module.Result.state = $after.state.Replace('_', '')
|
|
if ($module.Result.state -in @('restarted', 'started')) {
|
|
$module.Result.state = 'running'
|
|
}
|
|
|
|
# This isn't something we return in the diff as we cannot set this value.
|
|
$module.Result.can_pause_and_continue = $null
|
|
$module.Result.depended_by = @()
|
|
if ($service) {
|
|
$service.Refresh() # Always ensure we are working with the latest info available.
|
|
$module.Result.depended_by = $service.DependedBy
|
|
$module.Result.can_pause_and_continue = $service.ControlsAccepted.HasFlag(
|
|
[Ansible.Windows.SCManager.ControlsAccepted]::PauseContinue
|
|
)
|
|
}
|
|
}
|
|
|
|
$module.ExitJson()
|