Added Ansible.Service util and win_service_info (#67367)
* Added Ansible.Service util and win_service_info * Fix up util test * Sigh forgot to update the test and fix sanity * Try to make tests more robust * That didn't work, just check the username * Betraying Queen and country with this doc fix * More changes for compat * More OS compatibility
This commit is contained in:
parent
b041d96762
commit
2d9328cb0f
10 changed files with 3015 additions and 0 deletions
1341
lib/ansible/module_utils/csharp/Ansible.Service.cs
Normal file
1341
lib/ansible/module_utils/csharp/Ansible.Service.cs
Normal file
File diff suppressed because it is too large
Load diff
207
lib/ansible/modules/windows/win_service_info.ps1
Normal file
207
lib/ansible/modules/windows/win_service_info.ps1
Normal file
|
@ -0,0 +1,207 @@
|
|||
#!powershell
|
||||
|
||||
# Copyright: (c) 2020, Ansible Project
|
||||
# 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.Service
|
||||
|
||||
$spec = @{
|
||||
options = @{
|
||||
name = @{ type = "str" }
|
||||
}
|
||||
supports_check_mode = $true
|
||||
}
|
||||
|
||||
$module = [Ansible.Basic.AnsibleModule]::Create($args, $spec)
|
||||
|
||||
$name = $module.Params.name
|
||||
|
||||
$module.Result.exists = $false
|
||||
$module.Result.services = @(foreach ($rawService in (Get-Service -Name $name -ErrorAction SilentlyContinue)) {
|
||||
try {
|
||||
$service = New-Object -TypeName Ansible.Service.Service -ArgumentList @(
|
||||
$rawService.Name, [Ansible.Service.ServiceRights]'EnumerateDependents, QueryConfig, QueryStatus'
|
||||
)
|
||||
} catch [Ansible.Service.ServiceManagerException] {
|
||||
# ERROR_ACCESS_DENIED, ignore the service and continue on.
|
||||
if ($_.Exception.InnerException -and $_.Exception.InnerException.NativeErrorCode -eq 5) {
|
||||
$module.Warn("Failed to access service '$($rawService.Name) to get more info, ignoring")
|
||||
continue
|
||||
}
|
||||
|
||||
throw
|
||||
}
|
||||
$module.Result.exists = $true
|
||||
|
||||
$controlsAccepted = @($service.ControlsAccepted.ToString() -split ',' | ForEach-Object -Process {
|
||||
switch ($_.Trim()) {
|
||||
Stop { 'stop' }
|
||||
PauseContinue { 'pause_continue' }
|
||||
Shutdown { 'shutdown' }
|
||||
ParamChange { 'param_change' }
|
||||
NetbindChange { 'netbind_change' }
|
||||
HardwareProfileChange { 'hardware_profile_change' }
|
||||
PowerEvent { 'power_event' }
|
||||
SessionChange { 'session_change' }
|
||||
PreShutdown { 'pre_shutdown' }
|
||||
}
|
||||
})
|
||||
|
||||
$rawFailureActions = $service.FailureActions
|
||||
$failureActions = @(foreach ($action in $rawFailureActions.Actions) {
|
||||
[Ordered]@{
|
||||
type = switch ($action.Type) {
|
||||
None { 'none' }
|
||||
Reboot { 'reboot' }
|
||||
Restart { 'restart' }
|
||||
RunCommand { 'run_command' }
|
||||
}
|
||||
delay_ms = $action.Delay
|
||||
}
|
||||
})
|
||||
|
||||
# LaunchProtection is only valid in Windows 8.1 (2012 R2) or above.
|
||||
$launchProtection = 'none'
|
||||
if ($service.LaunchProtection) {
|
||||
$launchProtection = switch ($service.LaunchProtection) {
|
||||
None { 'none' }
|
||||
Windows { 'windows' }
|
||||
WindowsLight { 'windows_light' }
|
||||
AntimalwareLight { 'antimalware_light' }
|
||||
}
|
||||
}
|
||||
|
||||
$serviceFlags = @($service.ServiceFlags.ToString() -split ',' | ForEach-Object -Process {
|
||||
switch ($_.Trim()) {
|
||||
RunsInSystemProcess { 'runs_in_system_process' }
|
||||
}
|
||||
})
|
||||
|
||||
# The ServiceType value can contain other flags which are represented by other properties, this strips them out
|
||||
# so we don't include them in the service_type return value.
|
||||
$serviceType = [uint32]$service.ServiceType -band -bnot [uint32][Ansible.Service.ServiceType]::InteractiveProcess
|
||||
$serviceType = $serviceType -band -bnot [uint32][Ansible.Service.ServiceType]::UserServiceInstance
|
||||
$serviceType = switch (([Ansible.Service.ServiceType]$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' }
|
||||
}
|
||||
|
||||
$startType = switch ($service.StartType) {
|
||||
BootStart { 'boot_start' }
|
||||
SystemStart { 'system_start' }
|
||||
AutoStart { 'auto' }
|
||||
DemandStart { 'manual' }
|
||||
Disabled { 'disabled' }
|
||||
AutoStartDelayed { 'delayed' }
|
||||
}
|
||||
|
||||
$state = switch ($service.State) {
|
||||
Stopped { 'stopped' }
|
||||
StartPending { 'start_pending' }
|
||||
StopPending { 'stop_pending' }
|
||||
Running { 'started' }
|
||||
ContinuePending { 'continue_pending' }
|
||||
PausePending { 'pause_pending' }
|
||||
paused { 'paused' }
|
||||
}
|
||||
|
||||
$triggers = @(foreach ($trigger in $service.Triggers) {
|
||||
[Ordered]@{
|
||||
action = switch($trigger.Action) {
|
||||
ServiceStart { 'start_service' }
|
||||
ServiceStop { 'stop_service' }
|
||||
}
|
||||
type = switch($trigger.Type) {
|
||||
DeviceInterfaceArrival { 'device_interface_arrival' }
|
||||
IpAddressAvailability { 'ip_address_availability' }
|
||||
DomainJoin { 'domain_join' }
|
||||
FirewallPortEvent { 'firewall_port_event' }
|
||||
GroupPolicy { 'group_policy' }
|
||||
NetworkEndpoint { 'network_endpoint' }
|
||||
Custom { 'custom' }
|
||||
}
|
||||
sub_type = switch($trigger.SubType.ToString()) {
|
||||
([Ansible.Service.Trigger]::NAMED_PIPE_EVENT_GUID) { 'named_pipe_event' }
|
||||
([Ansible.Service.Trigger]::RPC_INTERFACE_EVENT_GUID) { 'rpc_interface_event' }
|
||||
([Ansible.Service.Trigger]::DOMAIN_JOIN_GUID) { 'domain_join' }
|
||||
([Ansible.Service.Trigger]::DOMAIN_LEAVE_GUID) { 'domain_leave' }
|
||||
([Ansible.Service.Trigger]::FIREWALL_PORT_OPEN_GUID) { 'firewall_port_open' }
|
||||
([Ansible.Service.Trigger]::FIREWALL_PORT_CLOSE_GUID) { 'firewall_port_close' }
|
||||
([Ansible.Service.Trigger]::MACHINE_POLICY_PRESENT_GUID) { 'machine_policy_present' }
|
||||
([Ansible.Service.Trigger]::USER_POLICY_PRESENT_GUID) { 'user_policy_present' }
|
||||
([Ansible.Service.Trigger]::NETWORK_MANAGER_FIRST_IP_ADDRESS_ARRIVAL_GUID) { 'network_first_ip_arrival' }
|
||||
([Ansible.Service.Trigger]::NETWORK_MANAGER_LAST_IP_ADDRESS_REMOVAL_GUID) { 'network_last_ip_removal' }
|
||||
default { 'custom' }
|
||||
}
|
||||
sub_type_guid = $trigger.SubType.ToString()
|
||||
data_items = @(foreach ($dataItem in $trigger.DataItems) {
|
||||
$dataValue = $dataItem.Data
|
||||
|
||||
# We only need to convert byte and byte[] to a Base64 string, the rest can be serialised as is.
|
||||
if ($dataValue -is [byte]) {
|
||||
$dataValue = [byte[]]@($dataValue)
|
||||
}
|
||||
|
||||
if ($dataValue -is [byte[]]) {
|
||||
$dataValue = [System.Convert]::ToBase64String($dataValue)
|
||||
}
|
||||
|
||||
[Ordered]@{
|
||||
type = switch ($dataItem.Type) {
|
||||
Binary { 'binary' }
|
||||
String { 'string' }
|
||||
Level { 'level' }
|
||||
KeywordAny { 'keyword_any' }
|
||||
KeywordAll { 'keyword_all' }
|
||||
}
|
||||
data = $dataValue
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
# These should closely reflect the options for win_service
|
||||
[Ordered]@{
|
||||
checkpoint = $service.Checkpoint
|
||||
controls_accepted = $controlsAccepted
|
||||
dependencies = $service.DependentOn
|
||||
dependency_of = $service.DependedBy
|
||||
description = $service.Description
|
||||
desktop_interact = $service.ServiceType.HasFlag([Ansible.Service.ServiceType]::InteractiveProcess)
|
||||
display_name = $service.DisplayName
|
||||
error_control = $service.ErrorControl.ToString().ToLowerInvariant()
|
||||
failure_actions = $failureActions
|
||||
failure_actions_on_non_crash_failure = $service.FailureActionsOnNonCrashFailures
|
||||
failure_command = $rawFailureActions.Command
|
||||
failure_reboot_msg = $rawFailureActions.RebootMsg
|
||||
failure_reset_period_sec = $rawFailureActions.ResetPeriod
|
||||
launch_protection = $launchProtection
|
||||
load_order_group = $service.LoadOrderGroup
|
||||
name = $service.ServiceName
|
||||
path = $service.Path
|
||||
pre_shutdown_timeout_ms = $service.PreShutdownTimeout
|
||||
preferred_node = $service.PreferredNode
|
||||
process_id = $service.ProcessId
|
||||
required_privileges = $service.RequiredPrivileges
|
||||
service_exit_code = $service.ServiceExitCode
|
||||
service_flags = $serviceFlags
|
||||
service_type = $serviceType
|
||||
sid_info = $service.ServiceSidInfo.ToString().ToLowerInvariant()
|
||||
start_mode = $startType
|
||||
state = $state
|
||||
triggers = $triggers
|
||||
username = $service.Account.Value
|
||||
wait_hint_ms = $service.WaitHint
|
||||
win32_exit_code = $service.Win32ExitCode
|
||||
}
|
||||
})
|
||||
|
||||
$module.ExitJson()
|
294
lib/ansible/modules/windows/win_service_info.py
Normal file
294
lib/ansible/modules/windows/win_service_info.py
Normal file
|
@ -0,0 +1,294 @@
|
|||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright: (c) 2020, Ansible Project
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
ANSIBLE_METADATA = {'metadata_version': '1.1',
|
||||
'status': ['preview'],
|
||||
'supported_by': 'community'}
|
||||
|
||||
DOCUMENTATION = r'''
|
||||
---
|
||||
module: win_service_info
|
||||
version_added: '2.10'
|
||||
short_description: Gather information about Windows services
|
||||
description:
|
||||
- Gather information about all or a specific installed Windows service(s).
|
||||
options:
|
||||
name:
|
||||
description:
|
||||
- If specified, this is used to match the C(name) or C(display_name) of the Windows service to get the info for.
|
||||
- Can be a wildcard to match multiple services but the wildcard will only be matched on the C(name) of the service
|
||||
and not C(display_name).
|
||||
- If omitted then all services will returned.
|
||||
type: str
|
||||
seealso:
|
||||
- module: win_service
|
||||
author:
|
||||
- Jordan Borean (@jborean93)
|
||||
'''
|
||||
|
||||
EXAMPLES = r'''
|
||||
- name: Get info for all installed services
|
||||
win_service_info:
|
||||
register: service_info
|
||||
|
||||
- name: Get info for a single service
|
||||
win_service_info:
|
||||
name: WinRM
|
||||
register: service_info
|
||||
|
||||
- name: Get info for a service using its display name
|
||||
win_service_info:
|
||||
name: Windows Remote Management (WS-Management)
|
||||
|
||||
- name: Find all services that start with 'win'
|
||||
win_service_info:
|
||||
name: win*
|
||||
'''
|
||||
|
||||
RETURN = r'''
|
||||
exists:
|
||||
description: Whether any services were found based on the criteria specified.
|
||||
returned: always
|
||||
type: bool
|
||||
sample: true
|
||||
services:
|
||||
description:
|
||||
- A list of service(s) that were found based on the criteria.
|
||||
- Will be an empty list if no services were found.
|
||||
returned: always
|
||||
type: list
|
||||
elements: dict
|
||||
contains:
|
||||
checkpoint:
|
||||
description:
|
||||
- A check-point value that the service increments periodically to report its progress.
|
||||
type: int
|
||||
sample: 0
|
||||
controls_accepted:
|
||||
description:
|
||||
- A list of controls that the service can accept.
|
||||
- Common controls are C(stop), C(pause_continue), C(shutdown).
|
||||
type: list
|
||||
elements: str
|
||||
sample: ['stop', 'shutdown']
|
||||
dependencies:
|
||||
description:
|
||||
- A list of services by their C(name) that this service is dependent on.
|
||||
type: list
|
||||
elements: str
|
||||
sample: ['HTTP', 'RPCSS']
|
||||
dependency_of:
|
||||
description:
|
||||
- A list of services by their C(name) that depend on this service.
|
||||
type: list
|
||||
elements: str
|
||||
sample: ['upnphost', 'WMPNetworkSvc']
|
||||
description:
|
||||
description:
|
||||
- The description of the service.
|
||||
type: str
|
||||
sample: Example description of the Windows service.
|
||||
desktop_interact:
|
||||
description:
|
||||
- Whether the service can interact with the desktop, only valid for services running as C(SYSTEM).
|
||||
type: bool
|
||||
sample: false
|
||||
display_name:
|
||||
description:
|
||||
- The display name to be used by SCM to identify the service.
|
||||
type: str
|
||||
sample: Windows Remote Management (WS-Management)
|
||||
error_control:
|
||||
description:
|
||||
- The action to take if a service fails to start.
|
||||
- Common values are C(critical), C(ignore), C(normal), C(severe).
|
||||
type: str
|
||||
sample: normal
|
||||
failure_actions:
|
||||
description:
|
||||
- A list of failure actions to run in the event of a failure.
|
||||
type: list
|
||||
elements: dict
|
||||
contains:
|
||||
delay_ms:
|
||||
description:
|
||||
- The time to wait, in milliseconds, before performing the specified action.
|
||||
type: int
|
||||
sample: 120000
|
||||
type:
|
||||
description:
|
||||
- The action that will be performed.
|
||||
- Common values are C(none), C(reboot), C(restart), C(run_command).
|
||||
type: str
|
||||
sample: run_command
|
||||
failure_action_on_non_crash_failure:
|
||||
description:
|
||||
- Controls when failure actions are fired based on how the service was stopped.
|
||||
type: bool
|
||||
sample: false
|
||||
failure_command:
|
||||
description:
|
||||
- The command line that will be run when a C(run_command) failure action is fired.
|
||||
type: str
|
||||
sample: runme.exe
|
||||
failure_reboot_msg:
|
||||
description:
|
||||
- The message to be broadcast to server users before rebooting when a C(reboot) failure action is fired.
|
||||
type: str
|
||||
sample: Service failed, rebooting host.
|
||||
failure_reset_period_sec:
|
||||
description:
|
||||
- The time, in seconds, after which to reset the failure count to zero.
|
||||
type: int
|
||||
sample: 86400
|
||||
launch_protection:
|
||||
description:
|
||||
- The protection type of the service.
|
||||
- Common values are C(none), C(windows), C(windows_light), or C(antimalware_light).
|
||||
type: str
|
||||
sample: none
|
||||
load_order_group:
|
||||
description:
|
||||
- The name of the load ordering group to which the service belongs.
|
||||
- Will be an empty string if it does not belong to any group.
|
||||
type: str
|
||||
sample: My group
|
||||
name:
|
||||
description:
|
||||
- The name of the service.
|
||||
type: str
|
||||
sample: WinRM
|
||||
path:
|
||||
description:
|
||||
- The path to the service binary and any arguments used when starting the service.
|
||||
- The binary part can be quoted to ensure any spaces in path are not treated as arguments.
|
||||
type: str
|
||||
sample: 'C:\Windows\System32\svchost.exe -k netsvcs -p'
|
||||
pre_shutdown_timeout_ms:
|
||||
description:
|
||||
- The preshutdown timeout out value in milliseconds.
|
||||
type: int
|
||||
sample: 10000
|
||||
preferred_node:
|
||||
description:
|
||||
- The node number for the preferred node.
|
||||
- This will be C(null) if the Windows host has no NUMA configuration.
|
||||
type: int
|
||||
sample: 0
|
||||
process_id:
|
||||
description:
|
||||
- The process identifier of the running service.
|
||||
type: int
|
||||
sample: 5135
|
||||
required_privileges:
|
||||
description:
|
||||
- A list of privileges that the service requires and will run with
|
||||
type: list
|
||||
elements: str
|
||||
sample: ['SeBackupPrivilege', 'SeRestorePrivilege']
|
||||
service_exit_code:
|
||||
description:
|
||||
- A service-specific error code that is set while the service is starting or stopping.
|
||||
type: int
|
||||
sample: 0
|
||||
service_flags:
|
||||
description:
|
||||
- Shows more information about the behaviour of a running service.
|
||||
- Currently the only flag that can be set is C(runs_in_system_process).
|
||||
type: list
|
||||
elements: str
|
||||
sample: [ 'runs_in_system_process' ]
|
||||
service_type:
|
||||
description:
|
||||
- The type of service.
|
||||
- Common types are C(win32_own_process), C(win32_share_process), C(user_own_process), C(user_share_process),
|
||||
C(kernel_driver).
|
||||
type: str
|
||||
sample: win32_own_process
|
||||
sid_info:
|
||||
description:
|
||||
- The behavior of how the service's access token is generated and how to add the service SID to the token.
|
||||
- Common values are C(none), C(restricted), or C(unrestricted).
|
||||
type: str
|
||||
sample: none
|
||||
start_mode:
|
||||
description:
|
||||
- When the service is set to start.
|
||||
- Common values are C(auto), C(manual), C(disabled), C(delayed).
|
||||
type: str
|
||||
sample: auto
|
||||
state:
|
||||
description:
|
||||
- The current running state of the service.
|
||||
- Common values are C(stopped), C(start_pending), C(stop_pending), C(started), C(continue_pending),
|
||||
C(pause_pending), C(paused).
|
||||
type: str
|
||||
sample: started
|
||||
triggers:
|
||||
description:
|
||||
- A list of triggers defined for the service.
|
||||
type: list
|
||||
elements: dict
|
||||
contains:
|
||||
action:
|
||||
description:
|
||||
- The action to perform once triggered, can be C(start_service) or C(stop_service).
|
||||
type: str
|
||||
sample: start_service
|
||||
data_items:
|
||||
description:
|
||||
- A list of trigger data items that contain trigger specific data.
|
||||
- A trigger can contain 0 or multiple data items.
|
||||
type: list
|
||||
elements: dict
|
||||
contains:
|
||||
data:
|
||||
description:
|
||||
- The trigger data item value.
|
||||
- Can be a string, list of string, int, or base64 string of binary data.
|
||||
type: complex
|
||||
sample: named pipe
|
||||
type:
|
||||
description:
|
||||
- The type of C(data) for the trigger.
|
||||
- Common values are C(string), C(binary), C(level), C(keyword_any), or C(keyword_all).
|
||||
type: str
|
||||
sample: string
|
||||
sub_type:
|
||||
description:
|
||||
- The trigger event sub type that is specific to each C(type).
|
||||
- Common values are C(named_pipe_event), C(domain_join), C(domain_leave), C(firewall_port_open), and others.
|
||||
type: str
|
||||
sample:
|
||||
sub_type_guid:
|
||||
description:
|
||||
- The guid which represents the trigger sub type.
|
||||
type: str
|
||||
sample: 1ce20aba-9851-4421-9430-1ddeb766e809
|
||||
type:
|
||||
description:
|
||||
- The trigger event type.
|
||||
- Common values are C(custom), C(rpc_interface_event), C(domain_join), C(group_policy), and others.
|
||||
type: str
|
||||
sample: domain_join
|
||||
username:
|
||||
description:
|
||||
- The username used to run the service.
|
||||
- Can be null for user services and certain driver services.
|
||||
type: str
|
||||
sample: NT AUTHORITY\SYSTEM
|
||||
wait_hint_ms:
|
||||
description:
|
||||
- The estimated time in milliseconds required for a pending start, stop, pause,or continue operations.
|
||||
type: int
|
||||
sample: 0
|
||||
win32_exitcode:
|
||||
description:
|
||||
- The error code returned from the service binary once it has stopped.
|
||||
- When set to C(1066) then a service specific error is returned on C(service_exit_code).
|
||||
type: int
|
||||
sample: 0
|
||||
'''
|
|
@ -0,0 +1,937 @@
|
|||
#!powershell
|
||||
|
||||
#AnsibleRequires -CSharpUtil Ansible.Basic
|
||||
#AnsibleRequires -CSharpUtil Ansible.Service
|
||||
#Requires -Module Ansible.ModuleUtils.ArgvParser
|
||||
#Requires -Module Ansible.ModuleUtils.CommandUtil
|
||||
|
||||
$module = [Ansible.Basic.AnsibleModule]::Create($args, @{})
|
||||
|
||||
$path = "$env:SystemRoot\System32\svchost.exe"
|
||||
|
||||
Function Assert-Equals {
|
||||
param(
|
||||
[Parameter(Mandatory=$true, ValueFromPipeline=$true)][AllowNull()]$Actual,
|
||||
[Parameter(Mandatory=$true, Position=0)][AllowNull()]$Expected
|
||||
)
|
||||
|
||||
$matched = $false
|
||||
if ($Actual -is [System.Collections.ArrayList] -or $Actual -is [Array] -or $Actual -is [System.Collections.IList]) {
|
||||
$Actual.Count | Assert-Equals -Expected $Expected.Count
|
||||
for ($i = 0; $i -lt $Actual.Count; $i++) {
|
||||
$actualValue = $Actual[$i]
|
||||
$expectedValue = $Expected[$i]
|
||||
Assert-Equals -Actual $actualValue -Expected $expectedValue
|
||||
}
|
||||
$matched = $true
|
||||
} else {
|
||||
$matched = $Actual -ceq $Expected
|
||||
}
|
||||
|
||||
if (-not $matched) {
|
||||
if ($Actual -is [PSObject]) {
|
||||
$Actual = $Actual.ToString()
|
||||
}
|
||||
|
||||
$call_stack = (Get-PSCallStack)[1]
|
||||
$module.Result.test = $test
|
||||
$module.Result.actual = $Actual
|
||||
$module.Result.expected = $Expected
|
||||
$module.Result.line = $call_stack.ScriptLineNumber
|
||||
$module.Result.method = $call_stack.Position.Text
|
||||
|
||||
$module.FailJson("AssertionError: actual != expected")
|
||||
}
|
||||
}
|
||||
|
||||
Function Invoke-Sc {
|
||||
[CmdletBinding()]
|
||||
param (
|
||||
[Parameter(Mandatory=$true)]
|
||||
[String]
|
||||
$Action,
|
||||
|
||||
[Parameter(Mandatory=$true)]
|
||||
[String]
|
||||
$Name,
|
||||
|
||||
[Object]
|
||||
$Arguments
|
||||
)
|
||||
|
||||
$commandArgs = [System.Collections.Generic.List[String]]@("sc.exe", $Action, $Name)
|
||||
if ($null -ne $Arguments) {
|
||||
if ($Arguments -is [System.Collections.IDictionary]) {
|
||||
foreach ($arg in $Arguments.GetEnumerator()) {
|
||||
$commandArgs.Add("$($arg.Key)=")
|
||||
$commandArgs.Add($arg.Value)
|
||||
}
|
||||
} else {
|
||||
foreach ($arg in $Arguments) {
|
||||
$commandArgs.Add($arg)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$command = Argv-ToString -arguments $commandArgs
|
||||
|
||||
$res = Run-Command -command $command
|
||||
if ($res.rc -ne 0) {
|
||||
$module.Result.rc = $res.rc
|
||||
$module.Result.stdout = $res.stdout
|
||||
$module.Result.stderr = $res.stderr
|
||||
$module.FailJson("Failed to invoke sc with: $command")
|
||||
}
|
||||
|
||||
$info = @{ Name = $Name }
|
||||
|
||||
if ($Action -eq 'qtriggerinfo') {
|
||||
# qtriggerinfo is in a different format which requires some manual parsing from the norm.
|
||||
$info.Triggers = [System.Collections.Generic.List[PSObject]]@()
|
||||
}
|
||||
|
||||
$currentKey = $null
|
||||
$qtriggerSection = @{}
|
||||
$res.stdout -split "`r`n" | Foreach-Object -Process {
|
||||
$line = $_.Trim()
|
||||
|
||||
if ($Action -eq 'qtriggerinfo' -and $line -in @('START SERVICE', 'STOP SERVICE')) {
|
||||
if ($qtriggerSection.Count -gt 0) {
|
||||
$info.Triggers.Add([PSCustomObject]$qtriggerSection)
|
||||
$qtriggerSection = @{}
|
||||
}
|
||||
|
||||
$qtriggerSection = @{
|
||||
Action = $line
|
||||
}
|
||||
}
|
||||
|
||||
if (-not $line -or (-not $line.Contains(':') -and $null -eq $currentKey)) {
|
||||
return
|
||||
}
|
||||
|
||||
$lineSplit = $line.Split(':', 2)
|
||||
if ($lineSplit.Length -eq 2) {
|
||||
$k = $lineSplit[0].Trim()
|
||||
if (-not $k) {
|
||||
$k = $currentKey
|
||||
}
|
||||
|
||||
$v = $lineSplit[1].Trim()
|
||||
} else {
|
||||
$k = $currentKey
|
||||
$v = $line
|
||||
}
|
||||
|
||||
if ($qtriggerSection.Count -gt 0) {
|
||||
if ($k -eq 'DATA') {
|
||||
$qtriggerSection.Data.Add($v)
|
||||
} else {
|
||||
$qtriggerSection.Type = $k
|
||||
$qtriggerSection.SubType = $v
|
||||
$qtriggerSection.Data = [System.Collections.Generic.List[String]]@()
|
||||
}
|
||||
} else {
|
||||
if ($info.ContainsKey($k)) {
|
||||
if ($info[$k] -isnot [System.Collections.Generic.List[String]]) {
|
||||
$info[$k] = [System.Collections.Generic.List[String]]@($info[$k])
|
||||
}
|
||||
$info[$k].Add($v)
|
||||
} else {
|
||||
$currentKey = $k
|
||||
$info[$k] = $v
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($qtriggerSection.Count -gt 0) {
|
||||
$info.Triggers.Add([PSCustomObject]$qtriggerSection)
|
||||
}
|
||||
|
||||
[PSCustomObject]$info
|
||||
}
|
||||
|
||||
$tests = [Ordered]@{
|
||||
"Props on service created by New-Service" = {
|
||||
$actual = New-Object -TypeName Ansible.Service.Service -ArgumentList $serviceName
|
||||
|
||||
$actual.ServiceName | Assert-Equals -Expected $serviceName
|
||||
$actual.ServiceType | Assert-Equals -Expected ([Ansible.Service.ServiceType]::Win32OwnProcess)
|
||||
$actual.StartType | Assert-Equals -Expected ([Ansible.Service.ServiceStartType]::DemandStart)
|
||||
$actual.ErrorControl | Assert-Equals -Expected ([Ansible.Service.ErrorControl]::Normal)
|
||||
$actual.Path | Assert-Equals -Expected ('"{0}"' -f $path)
|
||||
$actual.LoadOrderGroup | Assert-Equals -Expected ""
|
||||
$actual.DependentOn.Count | Assert-Equals -Expected 0
|
||||
$actual.Account | Assert-Equals -Expected (
|
||||
[System.Security.Principal.SecurityIdentifier]'S-1-5-18').Translate([System.Security.Principal.NTAccount]
|
||||
)
|
||||
$actual.DisplayName | Assert-Equals -Expected $serviceName
|
||||
$actual.Description | Assert-Equals -Expected $null
|
||||
$actual.FailureActions.ResetPeriod | Assert-Equals -Expected 0
|
||||
$actual.FailureActions.RebootMsg | Assert-Equals -Expected $null
|
||||
$actual.FailureActions.Command | Assert-Equals -Expected $null
|
||||
$actual.FailureActions.Actions.Count | Assert-Equals -Expected 0
|
||||
$actual.FailureActionsOnNonCrashFailures | Assert-Equals -Expected $false
|
||||
$actual.ServiceSidInfo | Assert-Equals -Expected ([Ansible.Service.ServiceSidInfo]::None)
|
||||
$actual.RequiredPrivileges.Count | Assert-Equals -Expected 0
|
||||
# Cannot test default values as it differs per OS version
|
||||
$null -ne $actual.PreShutdownTimeout | Assert-Equals -Expected $true
|
||||
$actual.Triggers.Count | Assert-Equals -Expected 0
|
||||
$actual.PreferredNode | Assert-Equals -Expected $null
|
||||
if ([Environment]::OSVersion.Version -ge [Version]'6.3') {
|
||||
$actual.LaunchProtection | Assert-Equals -Expected ([Ansible.Service.LaunchProtection]::None)
|
||||
} else {
|
||||
$actual.LaunchProtection | Assert-Equals -Expected $null
|
||||
}
|
||||
$actual.State | Assert-Equals -Expected ([Ansible.Service.ServiceStatus]::Stopped)
|
||||
$actual.Win32ExitCode | Assert-Equals -Expected 1077 # ERROR_SERVICE_NEVER_STARTED
|
||||
$actual.ServiceExitCode | Assert-Equals -Expected 0
|
||||
$actual.Checkpoint | Assert-Equals -Expected 0
|
||||
$actual.WaitHint | Assert-Equals -Expected 0
|
||||
$actual.ProcessId | Assert-Equals -Expected 0
|
||||
$actual.ServiceFlags | Assert-Equals -Expected ([Ansible.Service.ServiceFlags]::None)
|
||||
$actual.DependedBy.Count | Assert-Equals 0
|
||||
}
|
||||
|
||||
"Service creation through util" = {
|
||||
$testName = "$($serviceName)_2"
|
||||
$actual = [Ansible.Service.Service]::Create($testName, '"{0}"' -f $path)
|
||||
|
||||
try {
|
||||
$cmdletService = Get-Service -Name $testName -ErrorAction SilentlyContinue
|
||||
$null -ne $cmdletService | Assert-Equals -Expected $true
|
||||
|
||||
$actual.ServiceName | Assert-Equals -Expected $testName
|
||||
$actual.ServiceType | Assert-Equals -Expected ([Ansible.Service.ServiceType]::Win32OwnProcess)
|
||||
$actual.StartType | Assert-Equals -Expected ([Ansible.Service.ServiceStartType]::DemandStart)
|
||||
$actual.ErrorControl | Assert-Equals -Expected ([Ansible.Service.ErrorControl]::Normal)
|
||||
$actual.Path | Assert-Equals -Expected ('"{0}"' -f $path)
|
||||
$actual.LoadOrderGroup | Assert-Equals -Expected ""
|
||||
$actual.DependentOn.Count | Assert-Equals -Expected 0
|
||||
$actual.Account | Assert-Equals -Expected (
|
||||
[System.Security.Principal.SecurityIdentifier]'S-1-5-18').Translate([System.Security.Principal.NTAccount]
|
||||
)
|
||||
$actual.DisplayName | Assert-Equals -Expected $testName
|
||||
$actual.Description | Assert-Equals -Expected $null
|
||||
$actual.FailureActions.ResetPeriod | Assert-Equals -Expected 0
|
||||
$actual.FailureActions.RebootMsg | Assert-Equals -Expected $null
|
||||
$actual.FailureActions.Command | Assert-Equals -Expected $null
|
||||
$actual.FailureActions.Actions.Count | Assert-Equals -Expected 0
|
||||
$actual.FailureActionsOnNonCrashFailures | Assert-Equals -Expected $false
|
||||
$actual.ServiceSidInfo | Assert-Equals -Expected ([Ansible.Service.ServiceSidInfo]::None)
|
||||
$actual.RequiredPrivileges.Count | Assert-Equals -Expected 0
|
||||
$null -ne $actual.PreShutdownTimeout | Assert-Equals -Expected $true
|
||||
$actual.Triggers.Count | Assert-Equals -Expected 0
|
||||
$actual.PreferredNode | Assert-Equals -Expected $null
|
||||
if ([Environment]::OSVersion.Version -ge [Version]'6.3') {
|
||||
$actual.LaunchProtection | Assert-Equals -Expected ([Ansible.Service.LaunchProtection]::None)
|
||||
} else {
|
||||
$actual.LaunchProtection | Assert-Equals -Expected $null
|
||||
}
|
||||
$actual.State | Assert-Equals -Expected ([Ansible.Service.ServiceStatus]::Stopped)
|
||||
$actual.Win32ExitCode | Assert-Equals -Expected 1077 # ERROR_SERVICE_NEVER_STARTED
|
||||
$actual.ServiceExitCode | Assert-Equals -Expected 0
|
||||
$actual.Checkpoint | Assert-Equals -Expected 0
|
||||
$actual.WaitHint | Assert-Equals -Expected 0
|
||||
$actual.ProcessId | Assert-Equals -Expected 0
|
||||
$actual.ServiceFlags | Assert-Equals -Expected ([Ansible.Service.ServiceFlags]::None)
|
||||
$actual.DependedBy.Count | Assert-Equals 0
|
||||
} finally {
|
||||
$actual.Delete()
|
||||
}
|
||||
}
|
||||
|
||||
"Fail to open non-existing service" = {
|
||||
$failed = $false
|
||||
try {
|
||||
$null = New-Object -TypeName Ansible.Service.Service -ArgumentList 'fake_service'
|
||||
} catch [Ansible.Service.ServiceManagerException] {
|
||||
# 1060 == ERROR_SERVICE_DOES_NOT_EXIST
|
||||
$_.Exception.Message -like '*Win32ErrorCode 1060 - 0x00000424*' | Assert-Equals -Expected $true
|
||||
$failed = $true
|
||||
}
|
||||
|
||||
$failed | Assert-Equals -Expected $true
|
||||
}
|
||||
|
||||
"Open with specific access rights" = {
|
||||
$service = New-Object -TypeName Ansible.Service.Service -ArgumentList @(
|
||||
$serviceName, [Ansible.Service.ServiceRights]'QueryConfig, QueryStatus'
|
||||
)
|
||||
|
||||
# QueryStatus can get the status
|
||||
$service.State | Assert-Equals -Expected ([Ansible.Service.ServiceStatus]::Stopped)
|
||||
|
||||
# Should fail to get the config because we did not request that right
|
||||
$failed = $false
|
||||
try {
|
||||
$service.Path = 'fail'
|
||||
} catch [Ansible.Service.ServiceManagerException] {
|
||||
# 5 == ERROR_ACCESS_DENIED
|
||||
$_.Exception.Message -like '*Win32ErrorCode 5 - 0x00000005*' | Assert-Equals -Expected $true
|
||||
$failed = $true
|
||||
}
|
||||
|
||||
$failed | Assert-Equals -Expected $true
|
||||
|
||||
}
|
||||
|
||||
"Modfiy ServiceType" = {
|
||||
$service = New-Object -TypeName Ansible.Service.Service -ArgumentList $serviceName
|
||||
$service.ServiceType = [Ansible.Service.ServiceType]::Win32ShareProcess
|
||||
|
||||
$actual = Invoke-Sc -Action qc -Name $serviceName
|
||||
$service.ServiceType | Assert-Equals -Expected ([Ansible.Service.ServiceType]::Win32ShareProcess)
|
||||
$actual.TYPE | Assert-Equals -Expected "20 WIN32_SHARE_PROCESS"
|
||||
|
||||
$null = Invoke-Sc -Action config -Name $serviceName -Arguments @{type="own"}
|
||||
$service.Refresh()
|
||||
$service.ServiceType | Assert-Equals -Expected ([Ansible.Service.ServiceType]::Win32OwnProcess)
|
||||
}
|
||||
|
||||
"Create desktop interactive service" = {
|
||||
$service = New-Object -Typename Ansible.Service.Service -ArgumentList $serviceName
|
||||
$service.ServiceType = [Ansible.Service.ServiceType]'Win32OwnProcess, InteractiveProcess'
|
||||
|
||||
$actual = Invoke-Sc -Action qc -Name $serviceName
|
||||
$actual.TYPE | Assert-Equals -Expected "110 WIN32_OWN_PROCESS (interactive)"
|
||||
$service.ServiceType | Assert-Equals -Expected ([Ansible.Service.ServiceType]'Win32OwnProcess, InteractiveProcess')
|
||||
|
||||
# Change back from interactive process
|
||||
$service.ServiceType = [Ansible.Service.ServiceType]::Win32OwnProcess
|
||||
|
||||
$actual = Invoke-Sc -Action qc -Name $serviceName
|
||||
$actual.TYPE | Assert-Equals -Expected "10 WIN32_OWN_PROCESS"
|
||||
$service.ServiceType | Assert-Equals -Expected ([Ansible.Service.ServiceType]::Win32OwnProcess)
|
||||
|
||||
$service.Account = [System.Security.Principal.SecurityIdentifier]'S-1-5-20'
|
||||
|
||||
$failed = $false
|
||||
try {
|
||||
$service.ServiceType = [Ansible.Service.ServiceType]'Win32OwnProcess, InteractiveProcess'
|
||||
} catch [Ansible.Service.ServiceManagerException] {
|
||||
$failed = $true
|
||||
$_.Exception.NativeErrorCode | Assert-Equals -Expected 87 # ERROR_INVALID_PARAMETER
|
||||
}
|
||||
$failed | Assert-Equals -Expected $true
|
||||
|
||||
$actual = Invoke-Sc -Action qc -Name $serviceName
|
||||
$actual.TYPE | Assert-Equals -Expected "10 WIN32_OWN_PROCESS"
|
||||
}
|
||||
|
||||
"Modify StartType" = {
|
||||
$service = New-Object -TypeName Ansible.Service.Service -ArgumentList $serviceName
|
||||
$service.StartType = [Ansible.Service.ServiceStartType]::Disabled
|
||||
|
||||
$actual = Invoke-Sc -Action qc -Name $serviceName
|
||||
$service.StartType | Assert-Equals -Expected ([Ansible.Service.ServiceStartType]::Disabled)
|
||||
$actual.START_TYPE | Assert-Equals -Expected "4 DISABLED"
|
||||
|
||||
$null = Invoke-Sc -Action config -Name $serviceName -Arguments @{start="demand"}
|
||||
$service.Refresh()
|
||||
$service.StartType | Assert-Equals -Expected ([Ansible.Service.ServiceStartType]::DemandStart)
|
||||
}
|
||||
|
||||
"Modify StartType auto delayed" = {
|
||||
# Delayed start type is a modifier of the AutoStart type. It uses a separate config entry to define and this
|
||||
# makes sure the util does that correctly from various types and back.
|
||||
$service = New-Object -TypeName Ansible.Service.Service -ArgumentList $serviceName
|
||||
$service.StartType = [Ansible.Service.ServiceStartType]::Disabled # Start from Disabled
|
||||
|
||||
# Disabled -> Auto Start Delayed
|
||||
$service.StartType = [Ansible.Service.ServiceStartType]::AutoStartDelayed
|
||||
|
||||
$actual = Invoke-Sc -Action qc -Name $serviceName
|
||||
$service.StartType | Assert-Equals -Expected ([Ansible.Service.ServiceStartType]::AutoStartDelayed)
|
||||
$actual.START_TYPE | Assert-Equals -Expected "2 AUTO_START (DELAYED)"
|
||||
|
||||
# Auto Start Delayed -> Auto Start
|
||||
$service.StartType = [Ansible.Service.ServiceStartType]::AutoStart
|
||||
|
||||
$actual = Invoke-Sc -Action qc -Name $serviceName
|
||||
$service.StartType | Assert-Equals -Expected ([Ansible.Service.ServiceStartType]::AutoStart)
|
||||
$actual.START_TYPE | Assert-Equals -Expected "2 AUTO_START"
|
||||
|
||||
# Auto Start -> Auto Start Delayed
|
||||
$service.StartType = [Ansible.Service.ServiceStartType]::AutoStartDelayed
|
||||
|
||||
$actual = Invoke-Sc -Action qc -Name $serviceName
|
||||
$service.StartType | Assert-Equals -Expected ([Ansible.Service.ServiceStartType]::AutoStartDelayed)
|
||||
$actual.START_TYPE | Assert-Equals -Expected "2 AUTO_START (DELAYED)"
|
||||
|
||||
# Auto Start Delayed -> Manual
|
||||
$service.StartType = [Ansible.Service.ServiceStartType]::DemandStart
|
||||
|
||||
$actual = Invoke-Sc -Action qc -Name $serviceName
|
||||
$service.StartType | Assert-Equals -Expected ([Ansible.Service.ServiceStartType]::DemandStart)
|
||||
$actual.START_TYPE | Assert-Equals -Expected "3 DEMAND_START"
|
||||
}
|
||||
|
||||
"Modify ErrorControl" = {
|
||||
$service = New-Object -TypeName Ansible.Service.Service -ArgumentList $serviceName
|
||||
$service.ErrorControl = [Ansible.Service.ErrorControl]::Severe
|
||||
|
||||
$actual = Invoke-Sc -Action qc -Name $serviceName
|
||||
$service.ErrorControl | Assert-Equals -Expected ([Ansible.Service.ErrorControl]::Severe)
|
||||
$actual.ERROR_CONTROL | Assert-Equals -Expected "2 SEVERE"
|
||||
|
||||
$null = Invoke-Sc -Action config -Name $serviceName -Arguments @{error="ignore"}
|
||||
$service.Refresh()
|
||||
$service.ErrorControl | Assert-Equals -Expected ([Ansible.Service.ErrorControl]::Ignore)
|
||||
}
|
||||
|
||||
"Modify Path" = {
|
||||
$service = New-Object -TypeName Ansible.Service.Service -ArgumentList $serviceName
|
||||
$service.Path = "Fake path"
|
||||
|
||||
$actual = Invoke-Sc -Action qc -Name $serviceName
|
||||
$service.Path | Assert-Equals -Expected "Fake path"
|
||||
$actual.BINARY_PATH_NAME | Assert-Equals -Expected "Fake path"
|
||||
|
||||
$null = Invoke-Sc -Action config -Name $serviceName -Arguments @{binpath="other fake path"}
|
||||
$service.Refresh()
|
||||
$service.Path | Assert-Equals -Expected "other fake path"
|
||||
}
|
||||
|
||||
"Modify LoadOrderGroup" = {
|
||||
$service = New-Object -TypeName Ansible.Service.Service -ArgumentList $serviceName
|
||||
$service.LoadOrderGroup = "my group"
|
||||
|
||||
$actual = Invoke-Sc -Action qc -Name $serviceName
|
||||
$service.LoadOrderGroup | Assert-Equals -Expected "my group"
|
||||
$actual.LOAD_ORDER_GROUP | Assert-Equals -Expected "my group"
|
||||
|
||||
$null = Invoke-Sc -Action config -Name $serviceName -Arguments @{group=""}
|
||||
$service.Refresh()
|
||||
$service.LoadOrderGroup | Assert-Equals -Expected ""
|
||||
}
|
||||
|
||||
"Modify DependentOn" = {
|
||||
$service = New-Object -TypeName Ansible.Service.Service -ArgumentList $serviceName
|
||||
$service.DependentOn = @("HTTP", "WinRM")
|
||||
|
||||
$actual = Invoke-Sc -Action qc -Name $serviceName
|
||||
@(,$service.DependentOn) | Assert-Equals -Expected @("HTTP", "WinRM")
|
||||
@(,$actual.DEPENDENCIES) | Assert-Equals -Expected @("HTTP", "WinRM")
|
||||
|
||||
$null = Invoke-Sc -Action config -Name $serviceName -Arguments @{depend=""}
|
||||
$service.Refresh()
|
||||
$service.DependentOn.Count | Assert-Equals -Expected 0
|
||||
}
|
||||
|
||||
"Modify Account - service account" = {
|
||||
$systemSid = [System.Security.Principal.SecurityIdentifier]'S-1-5-18'
|
||||
$systemName =$systemSid.Translate([System.Security.Principal.NTAccount])
|
||||
$localSid = [System.Security.Principal.SecurityIdentifier]'S-1-5-19'
|
||||
$localName = $localSid.Translate([System.Security.Principal.NTAccount])
|
||||
$networkSid = [System.Security.Principal.SecurityIdentifier]'S-1-5-20'
|
||||
$networkName = $networkSid.Translate([System.Security.Principal.NTAccount])
|
||||
|
||||
$service = New-Object -TypeName Ansible.Service.Service -ArgumentList $serviceName
|
||||
$service.Account = $networkSid
|
||||
|
||||
$actual = Invoke-Sc -Action qc -Name $serviceName
|
||||
$service.Account | Assert-Equals -Expected $networkName
|
||||
$actual.SERVICE_START_NAME | Assert-Equals -Expected $networkName.Value
|
||||
|
||||
$null = Invoke-Sc -Action config -Name $serviceName -Arguments @{obj=$localName.Value}
|
||||
$service.Refresh()
|
||||
$service.Account | Assert-Equals -Expected $localName
|
||||
|
||||
$service.Account = $systemSid
|
||||
$actual = Invoke-Sc -Action qc -Name $serviceName
|
||||
$service.Account | Assert-Equals -Expected $systemName
|
||||
$actual.SERVICE_START_NAME | Assert-Equals -Expected "LocalSystem"
|
||||
}
|
||||
|
||||
"Modify Account - user" = {
|
||||
$currentSid = [System.Security.Principal.WindowsIdentity]::GetCurrent().User
|
||||
|
||||
$service = New-Object -TypeName Ansible.Service.Service -ArgumentList $serviceName
|
||||
$service.Account = $currentSid
|
||||
$service.Password = 'password'
|
||||
|
||||
$actual = Invoke-Sc -Action qc -Name $serviceName
|
||||
|
||||
# When running tests in CI this seems to become .\Administrator
|
||||
if ($service.Account.Value.StartsWith('.\')) {
|
||||
$username = $service.Account.Value.Substring(2, $service.Account.Value.Length - 2)
|
||||
$actualSid = ([System.Security.Principal.NTAccount]"$env:COMPUTERNAME\$username").Translate(
|
||||
[System.Security.Principal.SecurityIdentifier]
|
||||
)
|
||||
} else {
|
||||
$actualSid = $service.Account.Translate([System.Security.Principal.SecurityIdentifier])
|
||||
}
|
||||
$actualSid.Value | Assert-Equals -Expected $currentSid.Value
|
||||
$actual.SERVICE_START_NAME | Assert-Equals -Expected $service.Account.Value
|
||||
|
||||
# Go back to SYSTEM from account
|
||||
$systemSid = [System.Security.Principal.SecurityIdentifier]'S-1-5-18'
|
||||
$service.Account = $systemSid
|
||||
|
||||
$actual = Invoke-Sc -Action qc -Name $serviceName
|
||||
$service.Account | Assert-Equals -Expected $systemSid.Translate([System.Security.Principal.NTAccount])
|
||||
$actual.SERVICE_START_NAME | Assert-Equals -Expected "LocalSystem"
|
||||
}
|
||||
|
||||
"Modify Account - virtual account" = {
|
||||
$account = [System.Security.Principal.NTAccount]"NT SERVICE\$serviceName"
|
||||
|
||||
$service = New-Object -TypeName Ansible.Service.Service -ArgumentList $serviceName
|
||||
$service.Account = $account
|
||||
|
||||
$actual = Invoke-Sc -Action qc -Name $serviceName
|
||||
$service.Account | Assert-Equals -Expected $account
|
||||
$actual.SERVICE_START_NAME | Assert-Equals -Expected $account.Value
|
||||
}
|
||||
|
||||
"Modify Account - gMSA" = {
|
||||
# This cannot be tested through CI, only done on manual tests.
|
||||
return
|
||||
|
||||
$gmsaName = [System.Security.Principal.NTAccount]'gMSA$@DOMAIN.LOCAL' # Make sure this is UPN.
|
||||
$gmsaSid = $gmsaName.Translate([System.Security.Principal.SecurityIdentifier])
|
||||
$gmsaNetlogon = $gmsaSid.Translate([System.Security.Principal.NTAccount])
|
||||
|
||||
$service = New-Object -TypeName Ansible.Service.Service -ArgumentList $serviceName
|
||||
$service.Account = $gmsaName
|
||||
|
||||
$actual = Invoke-Sc -Action qc -Name $serviceName
|
||||
$service.Account | Assert-Equals -Expected $gmsaName
|
||||
$actual.SERVICE_START_NAME | Assert-Equals -Expected $gmsaName
|
||||
|
||||
# Go from gMSA to account and back to verify the Password doesn't matter.
|
||||
$currentUser = [System.Security.Principal.WindowsIdentity]::GetCurrent().User
|
||||
$service.Account = $currentUser
|
||||
$service.Password = 'fake password'
|
||||
$service.Password = 'fake password2'
|
||||
|
||||
# Now test in the Netlogon format.
|
||||
$service.Account = $gmsaSid
|
||||
|
||||
$actual = Invoke-Sc -Action qc -Name $serviceName
|
||||
$service.Account | Assert-Equals -Expected $gmsaNetlogon
|
||||
$actual.SERVICE_START_NAME | Assert-Equals -Expected $gmsaNetlogon.Value
|
||||
}
|
||||
|
||||
"Modify DisplayName" = {
|
||||
$service = New-Object -TypeName Ansible.Service.Service -ArgumentList $serviceName
|
||||
$service.DisplayName = "Custom Service Name"
|
||||
|
||||
$actual = Invoke-Sc -Action qc -Name $serviceName
|
||||
$service.DisplayName | Assert-Equals -Expected "Custom Service Name"
|
||||
$actual.DISPLAY_NAME | Assert-Equals -Expected "Custom Service Name"
|
||||
|
||||
$null = Invoke-Sc -Action config -Name $serviceName -Arguments @{displayname="New Service Name"}
|
||||
$service.Refresh()
|
||||
$service.DisplayName | Assert-Equals -Expected "New Service Name"
|
||||
}
|
||||
|
||||
"Modify Description" = {
|
||||
$service = New-Object -TypeName Ansible.Service.Service -ArgumentList $serviceName
|
||||
$service.Description = "My custom service description"
|
||||
|
||||
$actual = Invoke-Sc -Action qdescription -Name $serviceName
|
||||
$service.Description | Assert-Equals -Expected "My custom service description"
|
||||
$actual.DESCRIPTION | Assert-Equals -Expected "My custom service description"
|
||||
|
||||
$null = Invoke-Sc -Action description -Name $serviceName -Arguments @(,"new description")
|
||||
$service.Description | Assert-Equals -Expected "new description"
|
||||
|
||||
$service.Description = $null
|
||||
|
||||
$actual = Invoke-Sc -Action qdescription -Name $serviceName
|
||||
$service.Description | Assert-Equals -Expected $null
|
||||
$actual.DESCRIPTION | Assert-Equals -Expected ""
|
||||
}
|
||||
|
||||
"Modify FailureActions" = {
|
||||
$newAction = [Ansible.Service.FailureActions]@{
|
||||
ResetPeriod = 86400
|
||||
RebootMsg = 'Reboot msg'
|
||||
Command = 'Command line'
|
||||
Actions = @(
|
||||
[Ansible.Service.Action]@{Type = [Ansible.Service.FailureAction]::RunCommand; Delay = 1000},
|
||||
[Ansible.Service.Action]@{Type = [Ansible.Service.FailureAction]::RunCommand; Delay = 2000},
|
||||
[Ansible.Service.Action]@{Type = [Ansible.Service.FailureAction]::Restart; Delay = 1000},
|
||||
[Ansible.Service.Action]@{Type = [Ansible.Service.FailureAction]::Reboot; Delay = 1000}
|
||||
)
|
||||
}
|
||||
$service = New-Object -TypeName Ansible.Service.Service -ArgumentList $serviceName
|
||||
$service.FailureActions = $newAction
|
||||
|
||||
$actual = Invoke-Sc -Action qfailure -Name $serviceName
|
||||
$actual.'RESET_PERIOD (in seconds)' | Assert-Equals -Expected 86400
|
||||
$actual.REBOOT_MESSAGE | Assert-Equals -Expected 'Reboot msg'
|
||||
$actual.COMMAND_LINE | Assert-Equals -Expected 'Command line'
|
||||
$actual.FAILURE_ACTIONS.Count | Assert-Equals -Expected 4
|
||||
$actual.FAILURE_ACTIONS[0] | Assert-Equals -Expected "RUN PROCESS -- Delay = 1000 milliseconds."
|
||||
$actual.FAILURE_ACTIONS[1] | Assert-Equals -Expected "RUN PROCESS -- Delay = 2000 milliseconds."
|
||||
$actual.FAILURE_ACTIONS[2] | Assert-Equals -Expected "RESTART -- Delay = 1000 milliseconds."
|
||||
$actual.FAILURE_ACTIONS[3] | Assert-Equals -Expected "REBOOT -- Delay = 1000 milliseconds."
|
||||
$service.FailureActions.Actions.Count | Assert-Equals -Expected 4
|
||||
|
||||
# Test that we can change individual settings and it doesn't change all
|
||||
$service.FailureActions = [Ansible.Service.FailureActions]@{ResetPeriod = 172800}
|
||||
|
||||
$actual = Invoke-Sc -Action qfailure -Name $serviceName
|
||||
$actual.'RESET_PERIOD (in seconds)' | Assert-Equals -Expected 172800
|
||||
$actual.REBOOT_MESSAGE | Assert-Equals -Expected 'Reboot msg'
|
||||
$actual.COMMAND_LINE | Assert-Equals -Expected 'Command line'
|
||||
$actual.FAILURE_ACTIONS.Count | Assert-Equals -Expected 4
|
||||
$service.FailureActions.Actions.Count | Assert-Equals -Expected 4
|
||||
|
||||
$service.FailureActions = [Ansible.Service.FailureActions]@{RebootMsg = "New reboot msg"}
|
||||
|
||||
$actual = Invoke-Sc -Action qfailure -Name $serviceName
|
||||
$actual.'RESET_PERIOD (in seconds)' | Assert-Equals -Expected 172800
|
||||
$actual.REBOOT_MESSAGE | Assert-Equals -Expected 'New reboot msg'
|
||||
$actual.COMMAND_LINE | Assert-Equals -Expected 'Command line'
|
||||
$actual.FAILURE_ACTIONS.Count | Assert-Equals -Expected 4
|
||||
$service.FailureActions.Actions.Count | Assert-Equals -Expected 4
|
||||
|
||||
$service.FailureActions = [Ansible.Service.FailureActions]@{Command = "New command line"}
|
||||
|
||||
$actual = Invoke-Sc -Action qfailure -Name $serviceName
|
||||
$actual.'RESET_PERIOD (in seconds)' | Assert-Equals -Expected 172800
|
||||
$actual.REBOOT_MESSAGE | Assert-Equals -Expected 'New reboot msg'
|
||||
$actual.COMMAND_LINE | Assert-Equals -Expected 'New command line'
|
||||
$actual.FAILURE_ACTIONS.Count | Assert-Equals -Expected 4
|
||||
$service.FailureActions.Actions.Count | Assert-Equals -Expected 4
|
||||
|
||||
# Test setting both ResetPeriod and Actions together
|
||||
$service.FailureActions = [Ansible.Service.FailureActions]@{
|
||||
ResetPeriod = 86400
|
||||
Actions = @(
|
||||
[Ansible.Service.Action]@{Type = [Ansible.Service.FailureAction]::RunCommand; Delay = 5000},
|
||||
[Ansible.Service.Action]@{Type = [Ansible.Service.FailureAction]::None; Delay = 0}
|
||||
)
|
||||
}
|
||||
|
||||
$actual = Invoke-Sc -Action qfailure -Name $serviceName
|
||||
$actual.'RESET_PERIOD (in seconds)' | Assert-Equals -Expected 86400
|
||||
$actual.REBOOT_MESSAGE | Assert-Equals -Expected 'New reboot msg'
|
||||
$actual.COMMAND_LINE | Assert-Equals -Expected 'New command line'
|
||||
# sc.exe does not show the None action it just ends the list, so we verify from get_FailureActions
|
||||
$actual.FAILURE_ACTIONS | Assert-Equals -Expected "RUN PROCESS -- Delay = 5000 milliseconds."
|
||||
$service.FailureActions.Actions.Count | Assert-Equals -Expected 2
|
||||
$service.FailureActions.Actions[1].Type | Assert-Equals -Expected ([Ansible.Service.FailureAction]::None)
|
||||
|
||||
# Test setting just Actions without ResetPeriod
|
||||
$service.FailureActions = [Ansible.Service.FailureActions]@{
|
||||
Actions = [Ansible.Service.Action]@{Type = [Ansible.Service.FailureAction]::RunCommand; Delay = 10000}
|
||||
}
|
||||
$actual = Invoke-Sc -Action qfailure -Name $serviceName
|
||||
$actual.'RESET_PERIOD (in seconds)' | Assert-Equals -Expected 86400
|
||||
$actual.REBOOT_MESSAGE | Assert-Equals -Expected 'New reboot msg'
|
||||
$actual.COMMAND_LINE | Assert-Equals -Expected 'New command line'
|
||||
$actual.FAILURE_ACTIONS | Assert-Equals -Expected "RUN PROCESS -- Delay = 10000 milliseconds."
|
||||
$service.FailureActions.Actions.Count | Assert-Equals -Expected 1
|
||||
|
||||
# Test removing all actions
|
||||
$service.FailureActions = [Ansible.Service.FailureActions]@{
|
||||
Actions = @()
|
||||
}
|
||||
$actual = Invoke-Sc -Action qfailure -Name $serviceName
|
||||
$actual.'RESET_PERIOD (in seconds)' | Assert-Equals -Expected 0 # ChangeServiceConfig2W resets this back to 0.
|
||||
$actual.REBOOT_MESSAGE | Assert-Equals -Expected 'New reboot msg'
|
||||
$actual.COMMAND_LINE | Assert-Equals -Expected 'New command line'
|
||||
$actual.PSObject.Properties.Name.Contains('FAILURE_ACTIONS') | Assert-Equals -Expected $false
|
||||
$service.FailureActions.Actions.Count | Assert-Equals -Expected 0
|
||||
|
||||
# Test that we are reading the right values
|
||||
$null = Invoke-Sc -Action failure -Name $serviceName -Arguments @{
|
||||
reset = 172800
|
||||
reboot = "sc reboot msg"
|
||||
command = "sc command line"
|
||||
actions = "run/5000/reboot/800"
|
||||
}
|
||||
|
||||
$actual = $service.FailureActions
|
||||
$actual.ResetPeriod | Assert-Equals -Expected 172800
|
||||
$actual.RebootMsg | Assert-Equals -Expected "sc reboot msg"
|
||||
$actual.Command | Assert-Equals -Expected "sc command line"
|
||||
$actual.Actions.Count | Assert-Equals -Expected 2
|
||||
$actual.Actions[0].Type | Assert-Equals -Expected ([Ansible.Service.FailureAction]::RunCommand)
|
||||
$actual.Actions[0].Delay | Assert-Equals -Expected 5000
|
||||
$actual.Actions[1].Type | Assert-Equals -Expected ([Ansible.Service.FailureAction]::Reboot)
|
||||
$actual.Actions[1].Delay | Assert-Equals -Expected 800
|
||||
}
|
||||
|
||||
"Modify FailureActionsOnNonCrashFailures" = {
|
||||
$service = New-Object -TypeName Ansible.Service.Service -ArgumentList $serviceName
|
||||
$service.FailureActionsOnNonCrashFailures = $true
|
||||
|
||||
$actual = Invoke-Sc -Action qfailureflag -Name $serviceName
|
||||
$service.FailureActionsOnNonCrashFailures | Assert-Equals -Expected $true
|
||||
$actual.FAILURE_ACTIONS_ON_NONCRASH_FAILURES | Assert-Equals -Expected "TRUE"
|
||||
|
||||
$null = Invoke-Sc -Action failureflag -Name $serviceName -Arguments @(,0)
|
||||
$service.FailureActionsOnNonCrashFailures | Assert-Equals -Expected $false
|
||||
}
|
||||
|
||||
"Modify ServiceSidInfo" = {
|
||||
$service = New-Object -TypeName Ansible.Service.Service -ArgumentList $serviceName
|
||||
$service.ServiceSidInfo = [Ansible.Service.ServiceSidInfo]::None
|
||||
|
||||
$actual = Invoke-Sc -Action qsidtype -Name $serviceName
|
||||
$service.ServiceSidInfo | Assert-Equals -Expected ([Ansible.Service.ServiceSidInfo]::None)
|
||||
$actual.SERVICE_SID_TYPE | Assert-Equals -Expected 'NONE'
|
||||
|
||||
$null = Invoke-Sc -Action sidtype -Name $serviceName -Arguments @(,'unrestricted')
|
||||
$service.ServiceSidInfo | Assert-Equals -Expected ([Ansible.Service.ServiceSidInfo]::Unrestricted)
|
||||
|
||||
$service.ServiceSidInfo = [Ansible.Service.ServiceSidInfo]::Restricted
|
||||
|
||||
$actual = Invoke-Sc -Action qsidtype -Name $serviceName
|
||||
$service.ServiceSidInfo | Assert-Equals -Expected ([Ansible.Service.ServiceSidInfo]::Restricted)
|
||||
$actual.SERVICE_SID_TYPE | Assert-Equals -Expected 'RESTRICTED'
|
||||
}
|
||||
|
||||
"Modify RequiredPrivileges" = {
|
||||
$service = New-Object -TypeName Ansible.Service.Service -ArgumentList $serviceName
|
||||
$service.RequiredPrivileges = @("SeBackupPrivilege", "SeTcbPrivilege")
|
||||
|
||||
$actual = Invoke-Sc -Action qprivs -Name $serviceName
|
||||
,$service.RequiredPrivileges | Assert-Equals -Expected @("SeBackupPrivilege", "SeTcbPrivilege")
|
||||
,$actual.PRIVILEGES | Assert-Equals -Expected @("SeBackupPrivilege", "SeTcbPrivilege")
|
||||
|
||||
# Ensure setting to $null is the same as an empty array
|
||||
$service.RequiredPrivileges = $null
|
||||
|
||||
$actual = Invoke-Sc -Action qprivs -Name $serviceName
|
||||
,$service.RequiredPrivileges | Assert-Equals -Expected @()
|
||||
,$actual.PRIVILEGES | Assert-Equals -Expected @()
|
||||
|
||||
$service.RequiredPrivileges = @("SeBackupPrivilege", "SeTcbPrivilege")
|
||||
$service.RequiredPrivileges = @()
|
||||
|
||||
$actual = Invoke-Sc -Action qprivs -Name $serviceName
|
||||
,$service.RequiredPrivileges | Assert-Equals -Expected @()
|
||||
,$actual.PRIVILEGES | Assert-Equals -Expected @()
|
||||
|
||||
$null = Invoke-Sc -Action privs -Name $serviceName -Arguments @(,"SeCreateTokenPrivilege/SeRestorePrivilege")
|
||||
,$service.RequiredPrivileges | Assert-Equals -Expected @("SeCreateTokenPrivilege", "SeRestorePrivilege")
|
||||
}
|
||||
|
||||
"Modify PreShutdownTimeout" = {
|
||||
$service = New-Object -TypeName Ansible.Service.Service -ArgumentList $serviceName
|
||||
$service.PreShutdownTimeout = 60000
|
||||
|
||||
# sc.exe doesn't seem to have a query argument for this, just get it from the registry
|
||||
$actual = (
|
||||
Get-ItemProperty -LiteralPath "HKLM:\SYSTEM\CurrentControlSet\Services\$serviceName" -Name PreshutdownTimeout
|
||||
).PreshutdownTimeout
|
||||
$actual | Assert-Equals -Expected 60000
|
||||
}
|
||||
|
||||
"Modify Triggers" = {
|
||||
$service = [Ansible.Service.Service]$serviceName
|
||||
$service.Triggers = @(
|
||||
[Ansible.Service.Trigger]@{
|
||||
Type = [Ansible.Service.TriggerType]::DomainJoin
|
||||
Action = [Ansible.Service.TriggerAction]::ServiceStop
|
||||
SubType = [Guid][Ansible.Service.Trigger]::DOMAIN_JOIN_GUID
|
||||
},
|
||||
[Ansible.Service.Trigger]@{
|
||||
Type = [Ansible.Service.TriggerType]::NetworkEndpoint
|
||||
Action = [Ansible.Service.TriggerAction]::ServiceStart
|
||||
SubType = [Guid][Ansible.Service.Trigger]::NAMED_PIPE_EVENT_GUID
|
||||
DataItems = [Ansible.Service.TriggerItem]@{
|
||||
Type = [Ansible.Service.TriggerDataType]::String
|
||||
Data = 'my named pipe'
|
||||
}
|
||||
},
|
||||
[Ansible.Service.Trigger]@{
|
||||
Type = [Ansible.Service.TriggerType]::NetworkEndpoint
|
||||
Action = [Ansible.Service.TriggerAction]::ServiceStart
|
||||
SubType = [Guid][Ansible.Service.Trigger]::NAMED_PIPE_EVENT_GUID
|
||||
DataItems = [Ansible.Service.TriggerItem]@{
|
||||
Type = [Ansible.Service.TriggerDataType]::String
|
||||
Data = 'my named pipe 2'
|
||||
}
|
||||
},
|
||||
[Ansible.Service.Trigger]@{
|
||||
Type = [Ansible.Service.TriggerType]::Custom
|
||||
Action = [Ansible.Service.TriggerAction]::ServiceStart
|
||||
SubType = [Guid]'9bf04e57-05dc-4914-9ed9-84bf992db88c'
|
||||
DataItems = @(
|
||||
[Ansible.Service.TriggerItem]@{
|
||||
Type = [Ansible.Service.TriggerDataType]::Binary
|
||||
Data = [byte[]]@(1, 2, 3, 4)
|
||||
},
|
||||
[Ansible.Service.TriggerItem]@{
|
||||
Type = [Ansible.Service.TriggerDataType]::Binary
|
||||
Data = [byte[]]@(5, 6, 7, 8, 9)
|
||||
}
|
||||
)
|
||||
}
|
||||
[Ansible.Service.Trigger]@{
|
||||
Type = [Ansible.Service.TriggerType]::Custom
|
||||
Action = [Ansible.Service.TriggerAction]::ServiceStart
|
||||
SubType = [Guid]'9fbcfc7e-7581-4d46-913b-53bb15c80c51'
|
||||
DataItems = @(
|
||||
[Ansible.Service.TriggerItem]@{
|
||||
Type = [Ansible.Service.TriggerDataType]::String
|
||||
Data = 'entry 1'
|
||||
},
|
||||
[Ansible.Service.TriggerItem]@{
|
||||
Type = [Ansible.Service.TriggerDataType]::String
|
||||
Data = 'entry 2'
|
||||
}
|
||||
)
|
||||
},
|
||||
[Ansible.Service.Trigger]@{
|
||||
Type = [Ansible.Service.TriggerType]::FirewallPortEvent
|
||||
Action = [Ansible.Service.TriggerAction]::ServiceStop
|
||||
SubType = [Guid][Ansible.Service.Trigger]::FIREWALL_PORT_CLOSE_GUID
|
||||
DataItems = [Ansible.Service.TriggerItem]@{
|
||||
Type = [Ansible.Service.TriggerDataType]::String
|
||||
Data = [System.Collections.Generic.List[String]]@("1234", "tcp", "imagepath", "servicename")
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
$actual = Invoke-Sc -Action qtriggerinfo -Name $serviceName
|
||||
|
||||
$actual.Triggers.Count | Assert-Equals -Expected 6
|
||||
$actual.Triggers[0].Type | Assert-Equals -Expected 'DOMAIN JOINED STATUS'
|
||||
$actual.Triggers[0].Action | Assert-Equals -Expected 'STOP SERVICE'
|
||||
$actual.Triggers[0].SubType | Assert-Equals -Expected "$([Ansible.Service.Trigger]::DOMAIN_JOIN_GUID) [DOMAIN JOINED]"
|
||||
$actual.Triggers[0].Data.Count | Assert-Equals -Expected 0
|
||||
|
||||
$actual.Triggers[1].Type | Assert-Equals -Expected 'NETWORK EVENT'
|
||||
$actual.Triggers[1].Action | Assert-Equals -Expected 'START SERVICE'
|
||||
$actual.Triggers[1].SubType | Assert-Equals -Expected "$([Ansible.Service.Trigger]::NAMED_PIPE_EVENT_GUID) [NAMED PIPE EVENT]"
|
||||
$actual.Triggers[1].Data.Count | Assert-Equals -Expected 1
|
||||
$actual.Triggers[1].Data[0] | Assert-Equals -Expected 'my named pipe'
|
||||
|
||||
$actual.Triggers[2].Type | Assert-Equals -Expected 'NETWORK EVENT'
|
||||
$actual.Triggers[2].Action | Assert-Equals -Expected 'START SERVICE'
|
||||
$actual.Triggers[2].SubType | Assert-Equals -Expected "$([Ansible.Service.Trigger]::NAMED_PIPE_EVENT_GUID) [NAMED PIPE EVENT]"
|
||||
$actual.Triggers[2].Data.Count | Assert-Equals -Expected 1
|
||||
$actual.Triggers[2].Data[0] | Assert-Equals -Expected 'my named pipe 2'
|
||||
|
||||
$actual.Triggers[3].Type | Assert-Equals -Expected 'CUSTOM'
|
||||
$actual.Triggers[3].Action | Assert-Equals -Expected 'START SERVICE'
|
||||
$actual.Triggers[3].SubType | Assert-Equals -Expected '9bf04e57-05dc-4914-9ed9-84bf992db88c [ETW PROVIDER UUID]'
|
||||
$actual.Triggers[3].Data.Count | Assert-Equals -Expected 2
|
||||
$actual.Triggers[3].Data[0] | Assert-Equals -Expected '01 02 03 04'
|
||||
$actual.Triggers[3].Data[1] | Assert-Equals -Expected '05 06 07 08 09'
|
||||
|
||||
$actual.Triggers[4].Type | Assert-Equals -Expected 'CUSTOM'
|
||||
$actual.Triggers[4].Action | Assert-Equals -Expected 'START SERVICE'
|
||||
$actual.Triggers[4].SubType | Assert-Equals -Expected '9fbcfc7e-7581-4d46-913b-53bb15c80c51 [ETW PROVIDER UUID]'
|
||||
$actual.Triggers[4].Data.Count | Assert-Equals -Expected 2
|
||||
$actual.Triggers[4].Data[0] | Assert-Equals -Expected "entry 1"
|
||||
$actual.Triggers[4].Data[1] | Assert-Equals -Expected "entry 2"
|
||||
|
||||
$actual.Triggers[5].Type | Assert-Equals -Expected 'FIREWALL PORT EVENT'
|
||||
$actual.Triggers[5].Action | Assert-Equals -Expected 'STOP SERVICE'
|
||||
$actual.Triggers[5].SubType | Assert-Equals -Expected "$([Ansible.Service.Trigger]::FIREWALL_PORT_CLOSE_GUID) [PORT CLOSE]"
|
||||
$actual.Triggers[5].Data.Count | Assert-Equals -Expected 1
|
||||
$actual.Triggers[5].Data[0] | Assert-Equals -Expected '1234;tcp;imagepath;servicename'
|
||||
|
||||
# Remove trigger with $null
|
||||
$service.Triggers = $null
|
||||
|
||||
$actual = Invoke-Sc -Action qtriggerinfo -Name $serviceName
|
||||
$actual.Triggers.Count | Assert-Equals -Expected 0
|
||||
|
||||
# Add a single trigger
|
||||
$service.Triggers = [Ansible.Service.Trigger]@{
|
||||
Type = [Ansible.Service.TriggerType]::GroupPolicy
|
||||
Action = [Ansible.Service.TriggerAction]::ServiceStart
|
||||
SubType = [Guid][Ansible.Service.Trigger]::MACHINE_POLICY_PRESENT_GUID
|
||||
}
|
||||
|
||||
$actual = Invoke-Sc -Action qtriggerinfo -Name $serviceName
|
||||
$actual.Triggers.Count | Assert-Equals -Expected 1
|
||||
$actual.Triggers[0].Type | Assert-Equals -Expected 'GROUP POLICY'
|
||||
$actual.Triggers[0].Action | Assert-Equals -Expected 'START SERVICE'
|
||||
$actual.Triggers[0].SubType | Assert-Equals -Expected "$([Ansible.Service.Trigger]::MACHINE_POLICY_PRESENT_GUID) [MACHINE POLICY PRESENT]"
|
||||
$actual.Triggers[0].Data.Count | Assert-Equals -Expected 0
|
||||
|
||||
# Remove trigger with empty list
|
||||
$service.Triggers = @()
|
||||
|
||||
$actual = Invoke-Sc -Action qtriggerinfo -Name $serviceName
|
||||
$actual.Triggers.Count | Assert-Equals -Expected 0
|
||||
|
||||
# Add triggers through sc and check we get the values correctly
|
||||
$null = Invoke-Sc -Action triggerinfo -Name $serviceName -Arguments @(
|
||||
'start/namedpipe/abc',
|
||||
'start/namedpipe/def',
|
||||
'start/custom/d4497e12-ac36-4823-af61-92db0dbd4a76/11223344/aabbccdd',
|
||||
'start/strcustom/435a1742-22c5-4234-9db3-e32dafde695c/11223344/aabbccdd',
|
||||
'stop/portclose/1234;tcp;imagepath;servicename',
|
||||
'stop/networkoff'
|
||||
)
|
||||
|
||||
$actual = $service.Triggers
|
||||
$actual.Count | Assert-Equals -Expected 6
|
||||
|
||||
$actual[0].Type | Assert-Equals -Expected ([Ansible.Service.TriggerType]::NetworkEndpoint)
|
||||
$actual[0].Action | Assert-Equals -Expected ([Ansible.Service.TriggerAction]::ServiceStart)
|
||||
$actual[0].SubType = [Guid][Ansible.Service.Trigger]::NAMED_PIPE_EVENT_GUID
|
||||
$actual[0].DataItems.Count | Assert-Equals -Expected 1
|
||||
$actual[0].DataItems[0].Type | Assert-Equals -Expected ([Ansible.Service.TriggerDataType]::String)
|
||||
$actual[0].DataItems[0].Data | Assert-Equals -Expected 'abc'
|
||||
|
||||
$actual[1].Type | Assert-Equals -Expected ([Ansible.Service.TriggerType]::NetworkEndpoint)
|
||||
$actual[1].Action | Assert-Equals -Expected ([Ansible.Service.TriggerAction]::ServiceStart)
|
||||
$actual[1].SubType = [Guid][Ansible.Service.Trigger]::NAMED_PIPE_EVENT_GUID
|
||||
$actual[1].DataItems.Count | Assert-Equals -Expected 1
|
||||
$actual[1].DataItems[0].Type | Assert-Equals -Expected ([Ansible.Service.TriggerDataType]::String)
|
||||
$actual[1].DataItems[0].Data | Assert-Equals -Expected 'def'
|
||||
|
||||
$actual[2].Type | Assert-Equals -Expected ([Ansible.Service.TriggerType]::Custom)
|
||||
$actual[2].Action | Assert-Equals -Expected ([Ansible.Service.TriggerAction]::ServiceStart)
|
||||
$actual[2].SubType = [Guid]'d4497e12-ac36-4823-af61-92db0dbd4a76'
|
||||
$actual[2].DataItems.Count | Assert-Equals -Expected 2
|
||||
$actual[2].DataItems[0].Type | Assert-Equals -Expected ([Ansible.Service.TriggerDataType]::Binary)
|
||||
,$actual[2].DataItems[0].Data | Assert-Equals -Expected ([byte[]]@(17, 34, 51, 68))
|
||||
$actual[2].DataItems[1].Type | Assert-Equals -Expected ([Ansible.Service.TriggerDataType]::Binary)
|
||||
,$actual[2].DataItems[1].Data | Assert-Equals -Expected ([byte[]]@(170, 187, 204, 221))
|
||||
|
||||
$actual[3].Type | Assert-Equals -Expected ([Ansible.Service.TriggerType]::Custom)
|
||||
$actual[3].Action | Assert-Equals -Expected ([Ansible.Service.TriggerAction]::ServiceStart)
|
||||
$actual[3].SubType = [Guid]'435a1742-22c5-4234-9db3-e32dafde695c'
|
||||
$actual[3].DataItems.Count | Assert-Equals -Expected 2
|
||||
$actual[3].DataItems[0].Type | Assert-Equals -Expected ([Ansible.Service.TriggerDataType]::String)
|
||||
$actual[3].DataItems[0].Data | Assert-Equals -Expected '11223344'
|
||||
$actual[3].DataItems[1].Type | Assert-Equals -Expected ([Ansible.Service.TriggerDataType]::String)
|
||||
$actual[3].DataItems[1].Data | Assert-Equals -Expected 'aabbccdd'
|
||||
|
||||
$actual[4].Type | Assert-Equals -Expected ([Ansible.Service.TriggerType]::FirewallPortEvent)
|
||||
$actual[4].Action | Assert-Equals -Expected ([Ansible.Service.TriggerAction]::ServiceStop)
|
||||
$actual[4].SubType = [Guid][Ansible.Service.Trigger]::FIREWALL_PORT_CLOSE_GUID
|
||||
$actual[4].DataItems.Count | Assert-Equals -Expected 1
|
||||
$actual[4].DataItems[0].Type | Assert-Equals -Expected ([Ansible.Service.TriggerDataType]::String)
|
||||
,$actual[4].DataItems[0].Data | Assert-Equals -Expected @('1234', 'tcp', 'imagepath', 'servicename')
|
||||
|
||||
$actual[5].Type | Assert-Equals -Expected ([Ansible.Service.TriggerType]::IpAddressAvailability)
|
||||
$actual[5].Action | Assert-Equals -Expected ([Ansible.Service.TriggerAction]::ServiceStop)
|
||||
$actual[5].SubType = [Guid][Ansible.Service.Trigger]::NETWORK_MANAGER_LAST_IP_ADDRESS_REMOVAL_GUID
|
||||
$actual[5].DataItems.Count | Assert-Equals -Expected 0
|
||||
}
|
||||
|
||||
# Cannot test PreferredNode as we can't guarantee CI is set up with NUMA support.
|
||||
# Cannot test LaunchProtection as once set we cannot remove unless rebooting
|
||||
}
|
||||
|
||||
# setup and teardown should favour native tools to create and delete the service and not the util we are testing.
|
||||
foreach ($testImpl in $tests.GetEnumerator()) {
|
||||
$serviceName = "ansible_$([System.IO.Path]::GetRandomFileName())"
|
||||
$null = New-Service -Name $serviceName -BinaryPathName ('"{0}"' -f $path) -StartupType Manual
|
||||
|
||||
try {
|
||||
$test = $testImpl.Key
|
||||
&$testImpl.Value
|
||||
} finally {
|
||||
$null = Invoke-Sc -Action delete -Name $serviceName
|
||||
}
|
||||
}
|
||||
|
||||
$module.Result.data = "success"
|
||||
$module.ExitJson()
|
|
@ -82,3 +82,12 @@
|
|||
assert:
|
||||
that:
|
||||
- ansible_privilege_test.data == "success"
|
||||
|
||||
- name: test Ansible.Service.cs
|
||||
ansible_service_tests:
|
||||
register: ansible_service_test
|
||||
|
||||
- name: assert test Ansible.Service.cs
|
||||
assert:
|
||||
that:
|
||||
- ansible_service_test.data == "success"
|
||||
|
|
2
test/integration/targets/win_service_info/aliases
Normal file
2
test/integration/targets/win_service_info/aliases
Normal file
|
@ -0,0 +1,2 @@
|
|||
shippable/windows/group7
|
||||
shippable/windows/smoketest
|
11
test/integration/targets/win_service_info/defaults/main.yml
Normal file
11
test/integration/targets/win_service_info/defaults/main.yml
Normal file
|
@ -0,0 +1,11 @@
|
|||
---
|
||||
test_path: '{{ remote_tmp_dir }}\win_service_info .ÅÑŚÌβŁÈ [$!@^&test(;)]'
|
||||
service_url: https://ansible-ci-files.s3.amazonaws.com/test/integration/targets/win_service/SleepService.exe
|
||||
|
||||
service_name1: ansible_service_info_test
|
||||
service_name2: ansible_service_info_test2
|
||||
service_name3: ansible_service_info_other
|
||||
service_names:
|
||||
- '{{ service_name1 }}'
|
||||
- '{{ service_name2 }}'
|
||||
- '{{ service_name3 }}'
|
|
@ -0,0 +1,6 @@
|
|||
---
|
||||
- name: remove test service
|
||||
win_service:
|
||||
name: '{{ item }}'
|
||||
state: absent
|
||||
loop: '{{ service_names }}'
|
2
test/integration/targets/win_service_info/meta/main.yml
Normal file
2
test/integration/targets/win_service_info/meta/main.yml
Normal file
|
@ -0,0 +1,2 @@
|
|||
dependencies:
|
||||
- setup_remote_tmp_dir
|
206
test/integration/targets/win_service_info/tasks/main.yml
Normal file
206
test/integration/targets/win_service_info/tasks/main.yml
Normal file
|
@ -0,0 +1,206 @@
|
|||
---
|
||||
- name: ensure test directory exists
|
||||
win_file:
|
||||
path: '{{ test_path }}'
|
||||
state: directory
|
||||
|
||||
- name: download test binary for services
|
||||
win_get_url:
|
||||
url: '{{ service_url }}'
|
||||
dest: '{{ test_path }}\SleepService.exe'
|
||||
|
||||
- name: create test service
|
||||
win_service:
|
||||
name: '{{ item }}'
|
||||
path: '"{{ test_path }}\SleepService.exe"'
|
||||
state: stopped
|
||||
loop: '{{ service_names }}'
|
||||
notify: remove test service
|
||||
|
||||
- name: test we can get info for all services
|
||||
win_service_info:
|
||||
register: all_actual
|
||||
check_mode: yes # tests that this will run in check mode
|
||||
|
||||
- name: assert test we can get info for all services
|
||||
assert:
|
||||
that:
|
||||
- not all_actual is changed
|
||||
- all_actual.exists
|
||||
- all_actual.services | length > 0
|
||||
|
||||
- name: test info on a missing service
|
||||
win_service_info:
|
||||
name: ansible_service_info_missing
|
||||
register: missing_service
|
||||
|
||||
- name: assert test info on a missing service
|
||||
assert:
|
||||
that:
|
||||
- not missing_service is changed
|
||||
- not missing_service.exists
|
||||
|
||||
- name: test info on a single service
|
||||
win_service_info:
|
||||
name: '{{ service_name1 }}'
|
||||
register: specific_service
|
||||
|
||||
- name: assert test info on single service
|
||||
assert:
|
||||
that:
|
||||
- not specific_service is changed
|
||||
- specific_service.exists
|
||||
- specific_service.services | length == 1
|
||||
- specific_service.services[0].checkpoint == 0
|
||||
- specific_service.services[0].controls_accepted == []
|
||||
- specific_service.services[0].dependencies == []
|
||||
- specific_service.services[0].dependency_of == []
|
||||
- specific_service.services[0].description == None
|
||||
- specific_service.services[0].desktop_interact == False
|
||||
- specific_service.services[0].display_name == service_name1
|
||||
- specific_service.services[0].error_control == 'normal'
|
||||
- specific_service.services[0].failure_actions == []
|
||||
- specific_service.services[0].failure_actions_on_non_crash_failure == False
|
||||
- specific_service.services[0].failure_command == None
|
||||
- specific_service.services[0].failure_reboot_msg == None
|
||||
- specific_service.services[0].failure_reset_period_sec == 0
|
||||
- specific_service.services[0].launch_protection == 'none'
|
||||
- specific_service.services[0].load_order_group == ""
|
||||
- specific_service.services[0].name == service_name1
|
||||
- specific_service.services[0].path == '"' ~ test_path + '\\SleepService.exe"'
|
||||
- specific_service.services[0].pre_shutdown_timeout_ms is defined # Looks like the default for New-Service differs per OS version
|
||||
- specific_service.services[0].preferred_node == None
|
||||
- specific_service.services[0].process_id == 0
|
||||
- specific_service.services[0].required_privileges == []
|
||||
- specific_service.services[0].service_exit_code == 0
|
||||
- specific_service.services[0].service_flags == []
|
||||
- specific_service.services[0].service_type == 'win32_own_process'
|
||||
- specific_service.services[0].sid_info == 'none'
|
||||
- specific_service.services[0].start_mode == 'auto'
|
||||
- specific_service.services[0].state == 'stopped'
|
||||
- specific_service.services[0].triggers == []
|
||||
- specific_service.services[0].username == 'NT AUTHORITY\SYSTEM'
|
||||
- specific_service.services[0].wait_hint_ms == 0
|
||||
- specific_service.services[0].win32_exit_code == 1077
|
||||
|
||||
- name: test info on services matching wildcard
|
||||
win_service_info:
|
||||
name: ansible_service_info_t* # should match service_name 1 and 2, but not 3
|
||||
register: wildcard_service
|
||||
|
||||
- name: assert test info on services matching wildcard
|
||||
assert:
|
||||
that:
|
||||
- not wildcard_service is changed
|
||||
- wildcard_service.exists
|
||||
- wildcard_service.services | length == 2
|
||||
- wildcard_service.services[0].name == service_name1
|
||||
- wildcard_service.services[1].name == service_name2
|
||||
|
||||
- name: modify service1 to depend on service 2
|
||||
win_service:
|
||||
name: '{{ service_name1 }}'
|
||||
state: stopped
|
||||
dependencies:
|
||||
- '{{ service_name2 }}'
|
||||
|
||||
- name: edit basic settings for service 2
|
||||
win_service:
|
||||
dependencies:
|
||||
- '{{ service_name3 }}'
|
||||
description: Service description
|
||||
display_name: Ansible Service Display Name
|
||||
name: '{{ service_name2 }}'
|
||||
state: stopped
|
||||
|
||||
# TODO: move this back into the above once win_service supports them
|
||||
- name: edit complex settings for service 2
|
||||
win_command: sc.exe {{ item.action }} {{ service_name2 }} {{ item.args }}
|
||||
with_items:
|
||||
- action: config
|
||||
args: type= share type= interact error= ignore group= "My group" start= delayed-auto
|
||||
- action: failure
|
||||
args: reset= 86400 reboot= "Reboot msg" command= "Command line" actions= run/500/run/600/restart/700/reboot/800
|
||||
- action: failureflag
|
||||
args: 1
|
||||
- action: sidtype
|
||||
args: unrestricted
|
||||
- action: privs
|
||||
args: SeBackupPrivilege/SeRestorePrivilege
|
||||
- action: triggerinfo
|
||||
args: start/namedpipe/abc start/namedpipe/def start/custom/0e0682e2-9951-4e6d-a36a-a0047e616f28/11223344/aabbccdd start/strcustom/c2961e88-c1f4-4d97-b581-219c852e1c7d/11223344/aabbccdd start/portopen/1234;tcp;imagepath;servicename
|
||||
|
||||
- name: get info of advanced service using display name
|
||||
win_service_info:
|
||||
name: Ansible Service Display Name
|
||||
register: adv_service
|
||||
|
||||
- name: assert get info of advanced service using display_name
|
||||
assert:
|
||||
that:
|
||||
- not adv_service is changed
|
||||
- adv_service.exists
|
||||
- adv_service.services | length == 1
|
||||
- adv_service.services[0].dependencies == [service_name3]
|
||||
- adv_service.services[0].dependency_of == [service_name1]
|
||||
- adv_service.services[0].description == 'Service description'
|
||||
- adv_service.services[0].desktop_interact == True
|
||||
- adv_service.services[0].error_control == 'ignore'
|
||||
- adv_service.services[0].failure_actions | length == 4
|
||||
- adv_service.services[0].failure_actions[0].delay_ms == 500
|
||||
- adv_service.services[0].failure_actions[0].type == 'run_command'
|
||||
- adv_service.services[0].failure_actions[1].delay_ms == 600
|
||||
- adv_service.services[0].failure_actions[1].type == 'run_command'
|
||||
- adv_service.services[0].failure_actions[2].delay_ms == 700
|
||||
- adv_service.services[0].failure_actions[2].type == 'restart'
|
||||
- adv_service.services[0].failure_actions[3].delay_ms == 800
|
||||
- adv_service.services[0].failure_actions[3].type == 'reboot'
|
||||
- adv_service.services[0].failure_actions_on_non_crash_failure == True
|
||||
- adv_service.services[0].failure_command == 'Command line'
|
||||
- adv_service.services[0].failure_reboot_msg == 'Reboot msg'
|
||||
- adv_service.services[0].failure_reset_period_sec == 86400
|
||||
- adv_service.services[0].load_order_group == 'My group'
|
||||
- adv_service.services[0].required_privileges == ['SeBackupPrivilege', 'SeRestorePrivilege']
|
||||
- adv_service.services[0].service_type == 'win32_share_process'
|
||||
- adv_service.services[0].sid_info == 'unrestricted'
|
||||
- adv_service.services[0].start_mode == 'delayed'
|
||||
- adv_service.services[0].triggers | length == 5
|
||||
- adv_service.services[0].triggers[0].action == 'start_service'
|
||||
- adv_service.services[0].triggers[0].data_items | length == 1
|
||||
- adv_service.services[0].triggers[0].data_items[0].data == 'abc'
|
||||
- adv_service.services[0].triggers[0].data_items[0].type == 'string'
|
||||
- adv_service.services[0].triggers[0].sub_type == 'named_pipe_event'
|
||||
- adv_service.services[0].triggers[0].sub_type_guid == '1f81d131-3fac-4537-9e0c-7e7b0c2f4b55'
|
||||
- adv_service.services[0].triggers[0].type == 'network_endpoint'
|
||||
- adv_service.services[0].triggers[1].action == 'start_service'
|
||||
- adv_service.services[0].triggers[1].data_items | length == 1
|
||||
- adv_service.services[0].triggers[1].data_items[0].data == 'def'
|
||||
- adv_service.services[0].triggers[1].data_items[0].type == 'string'
|
||||
- adv_service.services[0].triggers[1].sub_type == 'named_pipe_event'
|
||||
- adv_service.services[0].triggers[1].sub_type_guid == '1f81d131-3fac-4537-9e0c-7e7b0c2f4b55'
|
||||
- adv_service.services[0].triggers[1].type == 'network_endpoint'
|
||||
- adv_service.services[0].triggers[2].action == 'start_service'
|
||||
- adv_service.services[0].triggers[2].data_items | length == 2
|
||||
- adv_service.services[0].triggers[2].data_items[0].data == 'ESIzRA=='
|
||||
- adv_service.services[0].triggers[2].data_items[0].type == 'binary'
|
||||
- adv_service.services[0].triggers[2].data_items[1].data == 'qrvM3Q=='
|
||||
- adv_service.services[0].triggers[2].data_items[1].type == 'binary'
|
||||
- adv_service.services[0].triggers[2].sub_type == 'custom'
|
||||
- adv_service.services[0].triggers[2].sub_type_guid == '0e0682e2-9951-4e6d-a36a-a0047e616f28'
|
||||
- adv_service.services[0].triggers[2].type == 'custom'
|
||||
- adv_service.services[0].triggers[3].action == 'start_service'
|
||||
- adv_service.services[0].triggers[3].data_items | length == 2
|
||||
- adv_service.services[0].triggers[3].data_items[0].data == '11223344'
|
||||
- adv_service.services[0].triggers[3].data_items[0].type == 'string'
|
||||
- adv_service.services[0].triggers[3].data_items[1].data == 'aabbccdd'
|
||||
- adv_service.services[0].triggers[3].data_items[1].type == 'string'
|
||||
- adv_service.services[0].triggers[3].sub_type == 'custom'
|
||||
- adv_service.services[0].triggers[3].sub_type_guid == 'c2961e88-c1f4-4d97-b581-219c852e1c7d'
|
||||
- adv_service.services[0].triggers[3].type == 'custom'
|
||||
- adv_service.services[0].triggers[4].action == 'start_service'
|
||||
- adv_service.services[0].triggers[4].data_items | length == 1
|
||||
- adv_service.services[0].triggers[4].data_items[0].data == ['1234', 'tcp', 'imagepath', 'servicename']
|
||||
- adv_service.services[0].triggers[4].data_items[0].type == 'string'
|
||||
- adv_service.services[0].triggers[4].sub_type == 'firewall_port_open'
|
||||
- adv_service.services[0].triggers[4].sub_type_guid == 'b7569e07-8421-4ee0-ad10-86915afdad09'
|
||||
- adv_service.services[0].triggers[4].type == 'firewall_port_event'
|
Loading…
Reference in a new issue