2014-09-26 01:01:01 +00:00
#!powershell
2018-04-04 01:28:10 +00:00
2018-07-17 21:29:05 +00:00
# Copyright: (c) 2014, Chris Hoffman <choffman@chathamfinancial.com>
2018-04-04 01:28:10 +00:00
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
# Requires -Module Ansible.ModuleUtils.Legacy
# Requires -Module Ansible.ModuleUtils.SID
2014-09-26 01:01:01 +00:00
2017-03-02 06:17:16 +00:00
$ErrorActionPreference = " Stop "
2014-09-26 01:01:01 +00:00
2017-03-02 06:17:16 +00:00
$params = Parse-Args $args -supports_check_mode $true
2017-03-15 01:53:31 +00:00
$check_mode = Get-AnsibleParam -obj $params -name '_ansible_check_mode' -type 'bool' -default $false
2017-06-27 00:11:43 +00:00
$dependencies = Get-AnsibleParam -obj $params -name 'dependencies' -type 'list' -default $null
2017-03-15 01:53:31 +00:00
$dependency_action = Get-AnsibleParam -obj $params -name 'dependency_action' -type 'str' -default 'set' -validateset 'add' , 'remove' , 'set'
$description = Get-AnsibleParam -obj $params -name 'description' -type 'str'
$desktop_interact = Get-AnsibleParam -obj $params -name 'desktop_interact' -type 'bool' -default $false
$display_name = Get-AnsibleParam -obj $params -name 'display_name' -type 'str'
$force_dependent_services = Get-AnsibleParam -obj $params -name 'force_dependent_services' -type 'bool' -default $false
$name = Get-AnsibleParam -obj $params -name 'name' -type 'str' -failifempty $true
$password = Get-AnsibleParam -obj $params -name 'password' -type 'str'
2017-11-02 23:55:03 +00:00
$path = Get-AnsibleParam -obj $params -name 'path'
2017-03-15 01:53:31 +00:00
$start_mode = Get-AnsibleParam -obj $params -name 'start_mode' -type 'str' -validateset 'auto' , 'manual' , 'disabled' , 'delayed'
2017-08-01 08:48:14 +00:00
$state = Get-AnsibleParam -obj $params -name 'state' -type 'str' -validateset 'started' , 'stopped' , 'restarted' , 'absent' , 'paused'
2017-03-15 01:53:31 +00:00
$username = Get-AnsibleParam -obj $params -name 'username' -type 'str'
2014-09-26 01:01:01 +00:00
2017-03-02 06:17:16 +00:00
$result = @ {
changed = $false
}
2018-04-04 01:28:10 +00:00
# parse the username to SID and back so we get the full username with domain in a way WMI understands
if ( $username -ne $null ) {
if ( $username -eq " LocalSystem " ) {
$username_sid = " S-1-5-18 "
} else {
$username_sid = Convert-ToSID -account_name $username
}
# the SYSTEM account is a special beast, Win32_Service Change requires StartName to be LocalSystem
# to specify LocalSystem/NT AUTHORITY\SYSTEM
if ( $username_sid -eq " S-1-5-18 " ) {
$username = " LocalSystem "
$password = $null
} else {
# Win32_Service, password must be "" and not $null when setting to LocalService or NetworkService
if ( $username_sid -in @ ( " S-1-5-19 " , " S-1-5-20 " ) ) {
$password = " "
}
$username = Convert-FromSID -sid $username_sid
}
2017-03-02 06:17:16 +00:00
}
if ( $password -ne $null -and $username -eq $null ) {
Fail-Json $result " The argument 'username' must be supplied with 'password' "
}
if ( $desktop_interact -eq $true -and ( -not ( $username -eq " LocalSystem " -or $username -eq $null ) ) ) {
Fail-Json $result " Can only set 'desktop_interact' to true when 'username' equals 'LocalSystem' "
}
2017-11-02 23:55:03 +00:00
if ( $path -ne $null ) {
$path = [ System.Environment ] :: ExpandEnvironmentVariables ( $path )
}
2017-03-02 06:17:16 +00:00
Function Get-ServiceInfo($name ) {
# Need to get new objects so we have the latest info
2018-04-04 01:28:10 +00:00
$svc = Get-Service | Where-Object { $_ . Name -eq $name -or $_ . DisplayName -eq $name }
$wmi_svc = Get-CimInstance -ClassName Win32_Service -Filter " name=' $( $svc . Name ) ' "
2017-03-02 06:17:16 +00:00
# Delayed start_mode is in reality Automatic (Delayed), need to check reg key for type
$delayed = Get-DelayedStatus -name $svc . Name
$actual_start_mode = $wmi_svc . StartMode . ToString ( ) . ToLower ( )
if ( $delayed -and $actual_start_mode -eq 'auto' ) {
$actual_start_mode = 'delayed'
}
2017-03-15 01:53:31 +00:00
$existing_dependencies = @ ( )
2017-03-02 06:17:16 +00:00
$existing_depended_by = @ ( )
if ( $svc . ServicesDependedOn . Count -gt 0 ) {
foreach ( $dependency in $svc . ServicesDependedOn . Name ) {
2017-03-15 01:53:31 +00:00
$existing_dependencies + = $dependency
2017-03-02 06:17:16 +00:00
}
2014-09-26 01:01:01 +00:00
}
2017-03-02 06:17:16 +00:00
if ( $svc . DependentServices . Count -gt 0 ) {
foreach ( $dependency in $svc . DependentServices . Name ) {
$existing_depended_by + = $dependency
}
}
2017-03-15 01:53:31 +00:00
$description = $wmi_svc . Description
if ( $description -eq $null ) {
$description = " "
}
2017-03-02 06:17:16 +00:00
$result . exists = $true
$result . name = $svc . Name
$result . display_name = $svc . DisplayName
$result . state = $svc . Status . ToString ( ) . ToLower ( )
$result . start_mode = $actual_start_mode
$result . path = $wmi_svc . PathName
2017-03-15 01:53:31 +00:00
$result . description = $description
2018-04-04 01:28:10 +00:00
$result . username = $wmi_svc . StartName
$result . desktop_interact = $wmi_svc . DesktopInteract
2017-03-15 01:53:31 +00:00
$result . dependencies = $existing_dependencies
2017-03-02 06:17:16 +00:00
$result . depended_by = $existing_depended_by
2017-08-01 08:48:14 +00:00
$result . can_pause_and_continue = $svc . CanPauseAndContinue
2014-09-26 01:01:01 +00:00
}
2017-03-02 06:17:16 +00:00
Function Get-WmiErrorMessage($return_value ) {
# These values are derived from https://msdn.microsoft.com/en-us/library/aa384901(v=vs.85).aspx
switch ( $return_value ) {
1 { " Not Supported: The request is not supported " }
2 { " Access Denied: The user did not have the necessary access " }
2017-03-15 01:53:31 +00:00
3 { " Dependent Services Running: The service cannot be stopped because other services that are running are dependent on it " }
4 { " Invalid Service Control: The requested control code is not valid, or it is unacceptable to the service " }
5 { " Service Cannot Accept Control: The requested control code cannot be sent to the service because the state of the service (Win32_BaseService.State property) is equal to 0, 1, or 2 " }
6 { " Service Not Active: The service has not been started " }
7 { " Service Request Timeout: The service did not respond to the start request in a timely fashion " }
2017-03-02 06:17:16 +00:00
8 { " Unknown Failure: Unknown failure when starting the service " }
2017-03-15 01:53:31 +00:00
9 { " Path Not Found: The directory path to the service executable file was not found " }
2017-03-02 06:17:16 +00:00
10 { " Service Already Running: The service is already running " }
11 { " Service Database Locked: The database to add a new service is locked " }
12 { " Service Dependency Deleted: A dependency this service relies on has been removed from the system " }
13 { " Service Dependency Failure: The service failed to find the service needed from a dependent service " }
2017-03-15 01:53:31 +00:00
14 { " Service Disabled: The service has been disabled from the system " }
15 { " Service Logon Failed: The service does not have the correct authentication to run on the system " }
2017-03-02 06:17:16 +00:00
16 { " Service Marked For Deletion: This service is being removed from the system " }
17 { " Service No Thread: The service has no execution thread " }
2017-03-15 01:53:31 +00:00
18 { " Status Circular Dependency: The service has circular dependencies when it starts " }
2017-03-02 06:17:16 +00:00
19 { " Status Duplicate Name: A service is running under the same name " }
2017-03-15 01:53:31 +00:00
20 { " Status Invalid Name: The service name has invalid characters " }
21 { " Status Invalid Parameter: Invalid parameters have been passed to the service " }
2017-03-02 06:17:16 +00:00
22 { " Status Invalid Service Account: The account under which this service runs is either invalid or lacks the permissions to run the service " }
23 { " Status Service Exists: The service exists in the database of services available from the system " }
24 { " Service Already Paused: The service is currently paused in the system " }
default { " Other Error " }
2014-09-26 01:01:01 +00:00
}
}
2017-03-02 06:17:16 +00:00
Function Get-DelayedStatus($name ) {
$delayed_key = " HKLM:\System\CurrentControlSet\Services\ $name "
try {
2018-04-04 01:28:10 +00:00
$delayed = ConvertTo-Bool ( ( Get-ItemProperty -LiteralPath $delayed_key ) . DelayedAutostart )
2017-03-02 06:17:16 +00:00
} catch {
$delayed = $false
}
$delayed
2014-09-26 01:01:01 +00:00
}
2017-03-02 06:17:16 +00:00
Function Set-ServiceStartMode($svc , $start_mode ) {
if ( $result . start_mode -ne $start_mode ) {
try {
$delayed_key = " HKLM:\System\CurrentControlSet\Services\ $( $svc . Name ) "
2017-03-15 01:53:31 +00:00
# Original start up type was auto (delayed) and we want auto, need to removed delayed key
if ( $start_mode -eq 'auto' -and $result . start_mode -eq 'delayed' ) {
2018-04-04 01:28:10 +00:00
Set-ItemProperty -LiteralPath $delayed_key -Name " DelayedAutostart " -Value 0 -WhatIf: $check_mode
2017-03-15 01:53:31 +00:00
# Original start up type was auto and we want auto (delayed), need to add delayed key
} elseif ( $start_mode -eq 'delayed' -and $result . start_mode -eq 'auto' ) {
2018-04-04 01:28:10 +00:00
Set-ItemProperty -LiteralPath $delayed_key -Name " DelayedAutostart " -Value 1 -WhatIf: $check_mode
2017-03-15 01:53:31 +00:00
# Original start up type was not auto or auto (delayed), need to change to auto and add delayed key
} elseif ( $start_mode -eq 'delayed' ) {
$svc | Set-Service -StartupType " auto " -WhatIf: $check_mode
2018-04-04 01:28:10 +00:00
Set-ItemProperty -LiteralPath $delayed_key -Name " DelayedAutostart " -Value 1 -WhatIf: $check_mode
2017-03-15 01:53:31 +00:00
# Original start up type was not what we were looking for, just change to that type
2017-03-02 06:17:16 +00:00
} else {
2017-03-15 01:53:31 +00:00
$svc | Set-Service -StartupType $start_mode -WhatIf: $check_mode
2017-03-02 06:17:16 +00:00
}
} catch {
Fail-Json $result $_ . Exception . Message
}
$result . changed = $true
}
2014-09-26 01:01:01 +00:00
}
2018-04-04 01:28:10 +00:00
Function Set-ServiceAccount($wmi_svc , $username_sid , $username , $password ) {
if ( $result . username -eq " LocalSystem " ) {
$actual_sid = " S-1-5-18 "
} else {
$actual_sid = Convert-ToSID -account_name $result . username
}
if ( $actual_sid -ne $username_sid ) {
$change_arguments = @ {
StartName = $username
StartPassword = $password
DesktopInteract = $result . desktop_interact
}
# need to disable desktop interact when not using the SYSTEM account
if ( $username_sid -ne " S-1-5-18 " ) {
$change_arguments . DesktopInteract = $false
}
2017-03-02 06:17:16 +00:00
#WMI.Change doesn't support -WhatIf, cannot fully test with check_mode
if ( -not $check_mode ) {
2018-04-04 01:28:10 +00:00
$return = $wmi_svc | Invoke-CimMethod -MethodName Change -Arguments $change_arguments
2017-03-02 06:17:16 +00:00
if ( $return . ReturnValue -ne 0 ) {
2018-04-04 01:28:10 +00:00
$error_msg = Get-WmiErrorMessage -return_value $result . ReturnValue
Fail-Json -obj $result -message " Failed to set service account to $( $username ) : $( $return . ReturnValue ) - $error_msg "
2017-03-02 06:17:16 +00:00
}
}
2014-09-26 01:01:01 +00:00
2017-03-02 06:17:16 +00:00
$result . changed = $true
2014-09-26 01:01:01 +00:00
}
2017-03-02 06:17:16 +00:00
}
Function Set-ServiceDesktopInteract($wmi_svc , $desktop_interact ) {
if ( $result . desktop_interact -ne $desktop_interact ) {
if ( -not $check_mode ) {
2018-04-04 01:28:10 +00:00
$return = $wmi_svc | Invoke-CimMethod -MethodName Change -Arguments @ { DesktopInteract = $desktop_interact }
2017-03-02 06:17:16 +00:00
if ( $return . ReturnValue -ne 0 ) {
2018-04-04 01:28:10 +00:00
$error_msg = Get-WmiErrorMessage -return_value $return . ReturnValue
Fail-Json -obj $result -message " Failed to set desktop interact $( $desktop_interact ) : $( $return . ReturnValue ) - $error_msg "
2017-03-02 06:17:16 +00:00
}
}
$result . changed = $true
2014-09-26 01:01:01 +00:00
}
}
2017-03-02 06:17:16 +00:00
Function Set-ServiceDisplayName($svc , $display_name ) {
if ( $result . display_name -ne $display_name ) {
try {
2017-03-15 01:53:31 +00:00
$svc | Set-Service -DisplayName $display_name -WhatIf: $check_mode
2017-03-02 06:17:16 +00:00
} catch {
Fail-Json $result $_ . Exception . Message
}
$result . changed = $true
}
2014-09-26 01:01:01 +00:00
}
2017-03-02 06:17:16 +00:00
Function Set-ServiceDescription($svc , $description ) {
if ( $result . description -ne $description ) {
2014-09-26 01:01:01 +00:00
try {
2017-03-15 01:53:31 +00:00
$svc | Set-Service -Description $description -WhatIf: $check_mode
2017-03-02 06:17:16 +00:00
} catch {
Fail-Json $result $_ . Exception . Message
2014-09-26 01:01:01 +00:00
}
2017-03-02 06:17:16 +00:00
$result . changed = $true
}
}
Function Set-ServicePath($name , $path ) {
if ( $result . path -ne $path ) {
try {
2018-04-04 01:28:10 +00:00
Set-ItemProperty -LiteralPath " HKLM:\System\CurrentControlSet\Services\ $name " -Name ImagePath -Value $path -WhatIf: $check_mode
2017-03-02 06:17:16 +00:00
} catch {
2014-09-26 01:01:01 +00:00
Fail-Json $result $_ . Exception . Message
}
2017-03-02 06:17:16 +00:00
$result . changed = $true
}
}
Function Set-ServiceDependencies($wmi_svc , $dependency_action , $dependencies ) {
$existing_dependencies = $result . dependencies
[ System.Collections.ArrayList ] $new_dependencies = @ ( )
if ( $dependency_action -eq 'set' ) {
2017-06-27 00:11:43 +00:00
foreach ( $dependency in $dependencies ) {
$new_dependencies . Add ( $dependency )
}
2017-03-02 06:17:16 +00:00
} else {
$new_dependencies = $existing_dependencies
foreach ( $dependency in $dependencies ) {
if ( $dependency_action -eq 'remove' ) {
if ( $new_dependencies -contains $dependency ) {
$new_dependencies . Remove ( $dependency )
}
} elseif ( $dependency_action -eq 'add' ) {
if ( $new_dependencies -notcontains $dependency ) {
$new_dependencies . Add ( $dependency )
}
}
}
}
$will_change = $false
foreach ( $dependency in $new_dependencies ) {
if ( $existing_dependencies -notcontains $dependency ) {
$will_change = $true
}
2014-09-26 01:01:01 +00:00
}
2017-03-02 06:17:16 +00:00
foreach ( $dependency in $existing_dependencies ) {
if ( $new_dependencies -notcontains $dependency ) {
$will_change = $true
}
}
if ( $will_change -eq $true ) {
if ( -not $check_mode ) {
2018-04-04 01:28:10 +00:00
$return = $wmi_svc | Invoke-CimMethod -MethodName Change -Arguments @ { ServiceDependencies = $new_dependencies }
2017-03-02 06:17:16 +00:00
if ( $return . ReturnValue -ne 0 ) {
2018-04-04 01:28:10 +00:00
$error_msg = Get-WmiErrorMessage -return_value $return . ReturnValue
$dep_string = $new_dependencies -join " , "
Fail-Json -obj $result -message " Failed to set service dependencies $( $dep_string ) : $( $return . ReturnValue ) - $error_msg "
2017-03-02 06:17:16 +00:00
}
}
$result . changed = $true
}
}
Function Set-ServiceState($svc , $wmi_svc , $state ) {
if ( $state -eq " started " -and $result . state -ne " running " ) {
2017-08-01 08:48:14 +00:00
if ( $result . state -eq " paused " ) {
try {
2018-04-04 01:28:10 +00:00
$svc | Resume-Service -WhatIf: $check_mode
2017-08-01 08:48:14 +00:00
} catch {
Fail-Json $result " failed to start service from paused state $( $svc . Name ) : $( $_ . Exception . Message ) "
}
} else {
try {
2018-04-04 01:28:10 +00:00
$svc | Start-Service -WhatIf: $check_mode
2017-08-01 08:48:14 +00:00
} catch {
Fail-Json $result $_ . Exception . Message
}
2014-09-26 01:01:01 +00:00
}
2017-03-02 06:17:16 +00:00
$result . changed = $true
}
if ( $state -eq " stopped " -and $result . state -ne " stopped " ) {
try {
2018-04-04 01:28:10 +00:00
$svc | Stop-Service -Force: $force_dependent_services -WhatIf: $check_mode
2017-03-02 06:17:16 +00:00
} catch {
2014-09-26 01:01:01 +00:00
Fail-Json $result $_ . Exception . Message
}
2017-03-02 06:17:16 +00:00
$result . changed = $true
2014-09-26 01:01:01 +00:00
}
2017-03-02 06:17:16 +00:00
if ( $state -eq " restarted " ) {
2014-09-26 01:01:01 +00:00
try {
2018-04-04 01:28:10 +00:00
$svc | Restart-Service -Force: $force_dependent_services -WhatIf: $check_mode
2017-03-02 06:17:16 +00:00
} catch {
Fail-Json $result $_ . Exception . Message
2014-09-26 01:01:01 +00:00
}
2017-03-02 06:17:16 +00:00
$result . changed = $true
}
2017-08-01 08:48:14 +00:00
if ( $state -eq " paused " -and $result . state -ne " paused " ) {
# check that we can actually pause the service
if ( $result . can_pause_and_continue -eq $false ) {
Fail-Json $result " failed to pause service $( $svc . Name ) : The service does not support pausing "
}
try {
2018-04-04 01:28:10 +00:00
$svc | Suspend-Service -WhatIf: $check_mode
2017-08-01 08:48:14 +00:00
} catch {
Fail-Json $result " failed to pause service $( $svc . Name ) : $( $_ . Exception . Message ) "
}
$result . changed = $true
}
2017-03-02 06:17:16 +00:00
if ( $state -eq " absent " ) {
try {
2018-04-04 01:28:10 +00:00
$svc | Stop-Service -Force: $force_dependent_services -WhatIf: $check_mode
2017-03-02 06:17:16 +00:00
} catch {
2014-09-26 01:01:01 +00:00
Fail-Json $result $_ . Exception . Message
}
2017-03-02 06:17:16 +00:00
if ( -not $check_mode ) {
2018-04-04 01:28:10 +00:00
$return = $wmi_svc | Invoke-CimMethod -MethodName Delete
2017-03-02 06:17:16 +00:00
if ( $return . ReturnValue -ne 0 ) {
2018-04-04 01:28:10 +00:00
$error_msg = Get-WmiErrorMessage -return_value $return . ReturnValue
Fail-Json -obj $result -message " Failed to delete service $( $svc . Name ) : $( $return . ReturnValue ) - $error_msg "
2017-03-02 06:17:16 +00:00
}
}
$result . changed = $true
}
}
Function Set-ServiceConfiguration($svc ) {
2018-04-04 01:28:10 +00:00
$wmi_svc = Get-CimInstance -ClassName Win32_Service -Filter " name=' $( $svc . Name ) ' "
2017-03-02 06:17:16 +00:00
Get-ServiceInfo -name $svc . Name
if ( $desktop_interact -eq $true -and ( -not ( $result . username -eq 'LocalSystem' -or $username -eq 'LocalSystem' ) ) ) {
2017-08-01 08:48:14 +00:00
Fail-Json $result " Can only set desktop_interact to true when service is run with/or 'username' equals 'LocalSystem' "
2017-03-02 06:17:16 +00:00
}
if ( $start_mode -ne $null ) {
Set-ServiceStartMode -svc $svc -start_mode $start_mode
}
if ( $username -ne $null ) {
2018-04-04 01:28:10 +00:00
Set-ServiceAccount -wmi_svc $wmi_svc -username_sid $username_sid -username $username -password $password
2017-03-02 06:17:16 +00:00
}
if ( $display_name -ne $null ) {
Set-ServiceDisplayName -svc $svc -display_name $display_name
}
if ( $desktop_interact -ne $null ) {
Set-ServiceDesktopInteract -wmi_svc $wmi_svc -desktop_interact $desktop_interact
}
if ( $description -ne $null ) {
Set-ServiceDescription -svc $svc -description $description
}
if ( $path -ne $null ) {
Set-ServicePath -name $svc . Name -path $path
}
if ( $dependencies -ne $null ) {
Set-ServiceDependencies -wmi_svc $wmi_svc -dependency_action $dependency_action -dependencies $dependencies
}
if ( $state -ne $null ) {
Set-ServiceState -svc $svc -wmi_svc $wmi_svc -state $state
}
}
2018-04-04 01:28:10 +00:00
# need to use Where-Object as -Name doesn't work with [] in the service name
# https://github.com/ansible/ansible/issues/37621
$svc = Get-Service | Where-Object { $_ . Name -eq $name -or $_ . DisplayName -eq $name }
2017-03-02 06:17:16 +00:00
if ( $svc ) {
Set-ServiceConfiguration -svc $svc
} else {
$result . exists = $false
if ( $state -ne 'absent' ) {
# Check if path is defined, if so create the service
if ( $path -ne $null ) {
try {
2017-03-15 01:53:31 +00:00
New-Service -Name $name -BinaryPathname $path -WhatIf: $check_mode
2017-03-02 06:17:16 +00:00
} catch {
Fail-Json $result $_ . Exception . Message
}
$result . changed = $true
2018-04-04 01:28:10 +00:00
$svc = Get-Service | Where-Object { $_ . Name -eq $name }
2017-03-02 06:17:16 +00:00
Set-ServiceConfiguration -svc $svc
} else {
# We will only reach here if the service is installed and the state is not absent
# Will check if any of the default actions are set and fail as we cannot action it
if ( $start_mode -ne $null -or
$state -ne $null -or
$username -ne $null -or
$password -ne $null -or
$display_name -ne $null -or
$description -ne $null -or
$desktop_interact -ne $false -or
$dependencies -ne $null -or
$dependency_action -ne 'set' ) {
Fail-Json $result " Service ' $name ' is not installed, need to set 'path' to create a new service "
}
}
}
}
# After making a change, let's get the service info again unless we deleted it
if ( $state -eq 'absent' ) {
# Recreate result so it doesn't have the extra meta data now that is has been deleted
$changed = $result . changed
$result = @ {
changed = $changed
exists = $false
2014-09-26 01:01:01 +00:00
}
2017-03-02 06:17:16 +00:00
} elseif ( $svc -ne $null ) {
Get-ServiceInfo -name $name
2014-09-26 01:01:01 +00:00
}
2018-04-04 01:28:10 +00:00
Exit-Json -obj $result