diff --git a/lib/ansible/modules/windows/win_scheduled_task.ps1 b/lib/ansible/modules/windows/win_scheduled_task.ps1 index d3606a52584..be392e73651 100644 --- a/lib/ansible/modules/windows/win_scheduled_task.ps1 +++ b/lib/ansible/modules/windows/win_scheduled_task.ps1 @@ -1,254 +1,1138 @@ #!powershell # This file is part of Ansible -# + # Copyright 2015, Peter Mounce # Michael Perzel -# -# Ansible is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Ansible is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with Ansible. If not, see . +# Copyright (c) 2017 Ansible Project +# 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 $ErrorActionPreference = "Stop" -# WANT_JSON -# POWERSHELL_COMMON +$params = Parse-Args -arguments $args -supports_check_mode $true +$check_mode = Get-AnsibleParam -obj $params -name "_ansible_check_mode" -type "bool" -default $false +$diff_mode = Get-AnsibleParam -obj $params -name "_ansible_diff" -type "bool" -default $false + +$name = Get-AnsibleParam -obj $params -name "name" -type "str" -failifempty $true +$path = Get-AnsibleParam -obj $params -name "path" -type "str" -default "\" +$state = Get-AnsibleParam -obj $params -name "state" -type "str" -default "present" -validateset "absent", "present" + +# task actions, list of dicts [{path, arguments, working_directory}] +$actions = Get-AnsibleParam -obj $params -name "actions" -type "list" + +# task triggers, list of dicts [{ type, ... }] +$triggers = Get-AnsibleParam -obj $params -name "triggers" -type "list" + +# task Principal properties +$display_name = Get-AnsibleParam -obj $params -name "display_name" -type "str" +$group = Get-AnsibleParam -obj $params -name "group" -type "str" +$logon_type = Get-AnsibleParam -obj $params -name "logon_type" -type "str" -validateset "none","password","s4u","interactive_token","group","service_account","interactive_token_or_password" +$run_level = Get-AnsibleParam -obj $params -name "run_level" -type "str" -validateset "limited", "highest" -aliases "runlevel" +$username = Get-AnsibleParam -obj $params -name "username" -type "str" -aliases "user" +$password = Get-AnsibleParam -obj $params -name "password" -type "str" +$update_password = Get-AnsibleParam -obj $params -name "update_password" -type "bool" -default $true + +# task RegistrationInfo properties +$author = Get-AnsibleParam -obj $params -name "author" -type "str" +$date = Get-AnsibleParam -obj $params -name "date" -type "str" +$description = Get-AnsibleParam -obj $params -name "description" -type "str" +$source = Get-AnsibleParam -obj $params -name "source" -type "str" +$version = Get-AnsibleParam -obj $params -name "version" -type "str" + +# task Settings properties +$allow_demand_start = Get-AnsibleParam -obj $params -name "allow_demand_start" -type "bool" +$allow_hard_terminate = Get-AnsibleParam -obj $params -name "allow_hard_terminate" -type "bool" +$compatibility = Get-AnsibleParam -obj $params -name "compatibility" -type "int" # https://msdn.microsoft.com/en-us/library/windows/desktop/aa383486(v=vs.85).aspx +$delete_expired_task_after = Get-AnsibleParam -obj $params -name "delete_expired_task_after" -type "str" # time string PT... +$disallow_start_if_on_batteries = Get-AnsibleParam -obj $params -name "disallow_start_if_on_batteries" -type "bool" +$enabled = Get-AnsibleParam -obj $params -name "enabled" -type "bool" +$execution_time_limit = Get-AnsibleParam -obj $params -name "execution_time_limit" -type "str" # PT72H +$hidden = Get-AnsibleParam -obj $params -name "hidden" -type "bool" +# TODO: support for $idle_settings, needs to be created as a COM object +$multiple_instances = Get-AnsibleParam -obj $params -name "multiple_instances" -type "int" # https://msdn.microsoft.com/en-us/library/windows/desktop/aa383507(v=vs.85).aspx +# TODO: support for $network_settings, needs to be created as a COM object +$priority = Get-AnsibleParam -obj $params -name "priority" -type "int" # https://msdn.microsoft.com/en-us/library/windows/desktop/aa383512(v=vs.85).aspx +$restart_count = Get-AnsibleParam -obj $params -name "restart_count" -type "int" +$restart_interval = Get-AnsibleParam -obj $params -name "restart_interval" -type "str" # time string PT.. +$run_only_if_idle = Get-AnsibleParam -obj $params -name "run_only_if_idle" -type "bool" +$run_only_if_network_available = Get-AnsibleParam -obj $params -name "run_only_if_network_available" -type "bool" +$start_when_available = Get-AnsibleParam -obj $params -name "start_when_available" -type "bool" +$stop_if_going_on_batteries = Get-AnsibleParam -obj $params -name "stop_if_going_on_batteries" -type "bool" +$wake_to_run = Get-AnsibleParam -obj $params -name "wake_to_run" -type "bool" + +# deprecated action arguments - use actions instead +$old_arguments = Get-AnsibleParam -obj $params -name "arguments" -type "str" -aliases "argument" +$old_executable = Get-AnsibleParam -obj $params -name "executable" -type "path" -failifempty ($old_arguments -ne $null) -aliases "execute" + +# deprecated principal arguments - use logon_type instead +$store_password = Get-AnsibleParam -obj $params -name "store_password" -type "bool" + +# deprecated trigger arguments - use triggers instead +$old_days_of_week = Get-AnsibleParam -obj $params -name "days_of_week" -type "list" +$old_frequency = Get-AnsibleParam -obj $params -name "frequency" -type "str" +$old_time = Get-AnsibleParam -obj $params -name "time" -type "str" $result = @{ changed = $false } -function Invoke-TaskPathCheck { - [CmdletBinding(SupportsShouldProcess=$true)] - param($Path, [Switch]$Remove) - - $pathResults = @{ - PathExists = $null - RemovedPath = $false - } - - if ($path -ne "\") { - $trimmedPath = $Path.TrimEnd("\") - } - else { - $trimmedPath = $Path - } - - $scheduleObject = New-Object -ComObject Schedule.Service - $scheduleObject.Connect() - - try { - $targetFolder = $scheduleObject.GetFolder($trimmedPath) - $pathResults.PathExists = $true - } - catch { - $pathResults.PathExists = $false - } - - if ($Remove -and $pathResults.PathExists) { - $childFolders = $targetFolder.GetFolders($null) - $childTasks = $targetFolder.GetTasks($null) - - if ($childFolders.Count -eq 0 -and $childTasks.Count -eq 0) { - if ($PSCmdlet.ShouldProcess($trimmedPath, "Remove task path")) { - $rootFolder = $scheduleObject.GetFolder("\") - $rootFolder.DeleteFolder($trimmedPath, $null) - } - $pathResults.RemovedPath = $true - } - } - - return $pathResults +if ($diff_mode) { + $result.diff = @{} } -$params = Parse-Args $args -supports_check_mode $true -$check_mode = Get-AnsibleParam -obj $params -name "_ansible_check_mode" -type "bool" -default $false - -$arguments = Get-AnsibleParam -obj $params -name "arguments" -type "str" -aliases "argument" -$description = Get-AnsibleParam -obj $params -name "description" -type "str" -default "No description." -$enabled = Get-AnsibleParam -obj $params -name "enabled" -type "bool" -default $true -$path = Get-AnsibleParam -obj $params -name "path" -type "str" -default '\' - -# Required vars -$name = Get-AnsibleParam -obj $params -name "name" -type "str" -failifempty $true -$state = Get-AnsibleParam -obj $params -name "state" -type "str" -default "present" -validateset "present","absent" - -# Vars conditionally required -$present = $state -eq "present" -$executable = Get-AnsibleParam -obj $params -name "executable" -type "str" -aliases "execute" -failifempty $present -$frequency = Get-AnsibleParam -obj $params -name "frequency" -type "str" -validateset "once","daily","weekly" -failifempty $present -$time = Get-AnsibleParam -obj $params -name "time" -type "str" -failifempty $present - -$user = Get-AnsibleParam -obj $params -name "user" -default "$env:USERDOMAIN\$env:USERNAME" -type "str" -$password = Get-AnsibleParam -obj $params -name "password" -type "str" -$runlevel = Get-AnsibleParam -obj $params -name "runlevel" -default "limited" -type "str" -validateset "limited", "highest" -$store_password = Get-AnsibleParam -obj $params -name "store_password" -default $true -type "bool" - -$weekly = $frequency -eq "weekly" -$days_of_week = Get-AnsibleParam -obj $params -name "days_of_week" -type "str" -failifempty $weekly - - -try { - $task = Get-ScheduledTask | Where-Object {$_.TaskName -eq $name -and $_.TaskPath -eq $path} - - # Correlate task state to enable variable, used to calculate if state needs to be changed - $taskState = if ($task) { $task.State } else { $null } - if ($taskState -eq "Ready"){ - $taskState = $true - } - elseif($taskState -eq "Disabled"){ - $taskState = $false - } - else - { - $taskState = $null - } - - $measure = $task | measure - if ($measure.count -eq 1 ) { - $exists = $true - } - elseif ( ($measure.count -eq 0) -and ($state -eq "absent") ){ - # Nothing to do - $result.exists = $false - $result.msg = "Task does not exist" - - Exit-Json $result - } - elseif ($measure.count -eq 0){ - $exists = $false - } - else { - # This should never occur - Fail-Json $result "$($measure.count) scheduled tasks found" - } - - $result.exists = $exists - - if ($frequency){ - if ($frequency -eq "once") { - $trigger = New-ScheduledTaskTrigger -Once -At $time - } - elseif ($frequency -eq "daily") { - $trigger = New-ScheduledTaskTrigger -Daily -At $time - } - elseif ($frequency -eq "weekly"){ - $trigger = New-ScheduledTaskTrigger -Weekly -At $time -DaysOfWeek $days_of_week - } - else { - Fail-Json $result "frequency must be daily or weekly" - } - } - - if ( ($state -eq "absent") -and ($exists) ) { - Unregister-ScheduledTask -TaskName $name -Confirm:$false -WhatIf:$check_mode - $result.changed = $true - $result.msg = "Deleted task $name" - - # Remove task path if it exists - $pathResults = Invoke-TaskPathCheck -Path $path -Remove -WhatIf:$check_mode - - if ($pathResults.RemovedPath) { - $result.msg += " and task path $path removed" - } - - Exit-Json $result - } - elseif ( ($state -eq "absent") -and (-not $exists) ) { - $result.msg = "Task $name does not exist" - Exit-Json $result - } - - # Handle RunAs/RunLevel options for the task - - if ($store_password) { - # Specify direct credential and run-level values to add to Register-ScheduledTask - $registerRunOptionParams = @{ - User = $user - RunLevel = $runlevel - } - if ($password) { - $registerRunOptionParams.Password = $password - } - } - else { - # Create a ScheduledTaskPrincipal for the task to run under - $principal = New-ScheduledTaskPrincipal -UserId $user -LogonType S4U -RunLevel $runlevel -Id Author - $registerRunOptionParams = @{Principal = $principal} - } - - if ($enabled){ - $settings = New-ScheduledTaskSettingsSet - } - else { - $settings = New-ScheduledTaskSettingsSet -Disable - } - - if ($arguments) { - $action = New-ScheduledTaskAction -Execute $executable -Argument $arguments - } - else { - $action = New-ScheduledTaskAction -Execute $executable - } - - if ( ($state -eq "present") -and (-not $exists) ){ - # Check task path prior to registering - $pathResults = Invoke-TaskPathCheck -Path $path - - if (-not $check_mode) { - Register-ScheduledTask -Action $action -Trigger $trigger -TaskName $name -Description $description -TaskPath $path -Settings $settings @registerRunOptionParams - } - - $result.changed = $true - $result.msg = "Added new task $name" - - if (!$pathResults.PathExists) { - $result.msg += " and task path $path created" - } - } - elseif( ($state -eq "present") -and ($exists) ) { - # Check task path prior to registering - $pathResults = Invoke-TaskPathCheck -Path $path - - if ((!$store_password -and $task.Principal.LogonType -in @("S4U", "ServiceAccount")) -or ($store_password -and $task.Principal.LogonType -notin @("S4U", "Password") -and !$password)) { - $passwordStoreConsistent = $true - } - else { - $passwordStoreConsistent = $false - } - - if ($task.Description -eq $description -and $task.TaskName -eq $name -and $task.TaskPath -eq $path -and $task.Actions.Execute -eq $executable -and - $taskState -eq $enabled -and $task.Principal.UserId -eq $user -and $task.Principal.RunLevel -eq $runlevel -and $passwordStoreConsistent) { - # No change in the task - $result.msg = "No change in task $name" - } - else { - Unregister-ScheduledTask -TaskName $name -Confirm:$false -WhatIf:$check_mode - - if (-not $check_mode) { - $oldPathResults = Invoke-TaskPathCheck -Path $task.TaskPath -Remove - Register-ScheduledTask -Action $action -Trigger $trigger -TaskName $name -Description $description -TaskPath $path -Settings $settings @registerRunOptionParams - } - $result.changed = $true - $result.msg = "Updated task $name" - - if (!$pathResults.PathExists) { - $result.msg += " and task path $path created" - } - } - } - - Exit-Json $result -} -catch +Add-Type -TypeDefinition @" +public enum TASK_ACTION_TYPE // https://msdn.microsoft.com/en-us/library/windows/desktop/aa383553(v=vs.85).aspx { - Fail-Json $result $_.Exception.Message + TASK_ACTION_EXEC = 0, + // The below are not supported and are only kept for documentation purposes + TASK_ACTION_COM_HANDLER = 5, + TASK_ACTION_SEND_EMAIL = 6, + TASK_ACTION_SHOW_MESSAGE = 7 } + +public enum TASK_CREATION // https://msdn.microsoft.com/en-us/library/windows/desktop/aa382538(v=vs.85).aspx +{ + TASK_VALIDATE_ONLY = 0x1, + TASK_CREATE = 0x2, + TASK_UPDATE = 0x4, + TASK_CREATE_OR_UPDATE = 0x6, + TASK_DISABLE = 0x8, + TASK_DONT_ADD_PRINCIPAL_ACE = 0x10, + TASK_IGNORE_REGISTRATION_TRIGGERS = 0x20 +} + +public enum TASK_LOGON_TYPE // https://msdn.microsoft.com/en-us/library/windows/desktop/aa383566(v=vs.85).aspx +{ + TASK_LOGON_NONE = 0, + TASK_LOGON_PASSWORD = 1, + TASK_LOGON_S4U = 2, + TASK_LOGON_INTERACTIVE_TOKEN = 3, + TASK_LOGON_GROUP = 4, + TASK_LOGON_SERVICE_ACCOUNT = 5, + TASK_LOGON_INTERACTIVE_TOKEN_OR_PASSWORD = 6 +} + +public enum TASK_RUN_LEVEL // https://msdn.microsoft.com/en-us/library/windows/desktop/aa380747(v=vs.85).aspx +{ + TASK_RUNLEVEL_LUA = 0, + TASK_RUNLEVEL_HIGHEST = 1 +} + +public enum TASK_TRIGGER_TYPE2 // https://msdn.microsoft.com/en-us/library/windows/desktop/aa383915(v=vs.85).aspx +{ + TASK_TRIGGER_EVENT = 0, + TASK_TRIGGER_TIME = 1, + TASK_TRIGGER_DAILY = 2, + TASK_TRIGGER_WEEKLY = 3, + TASK_TRIGGER_MONTHLY = 4, + TASK_TRIGGER_MONTHLYDOW = 5, + TASK_TRIGGER_IDLE = 6, + TASK_TRIGGER_REGISTRATION = 7, + TASK_TRIGGER_BOOT = 8, + TASK_TRIGGER_LOGON = 9, + TASK_TRIGGER_SESSION_STATE_CHANGE = 11 +} +"@ + +######################## +### HELPER FUNCTIONS ### +######################## +Function ConvertTo-HashtableFromPsCustomObject($object) { + if ($object -is [Hashtable]) { + return ,$object + } + + $hashtable = @{} + $object | Get-Member -MemberType *Property | % { + $value = $object.$($_.Name) + if ($value -is [PSObject]) { + $value = ConvertTo-HashtableFromPsCustomObject -object $value + } + $hashtable.$($_.Name) = $value + } + + return ,$hashtable +} + +Function Convert-SnakeToPascalCase($snake) { + # very basic function to convert snake_case to PascalCase for use in COM + # objects + [regex]$regex = "_(\w)" + $pascal_case = $regex.Replace($snake, { $args[0].Value.Substring(1).ToUpper() }) + $capitalised = $pascal_case.Substring(0, 1).ToUpper() + $pascal_case.Substring(1) + + return $capitalised +} + +Function Compare-Properties($property_name, $parent_property, $map, $enum_map=$null) { + $changes = [System.Collections.ArrayList]@() + + # loop through the passed in map and compare values + # Name = The name of property in the COM object + # Value = The new value to compare the existing value with + foreach ($entry in $map.GetEnumerator()) { + $new_value = $entry.Value + + if ($new_value -ne $null) { + $property_name = $entry.Name + $existing_value = $parent_property.$property_name + if ($existing_value -cne $new_value) { + try { + $parent_property.$property_name = $new_value + } catch { + Fail-Json -obj $result -message "failed to set $property_name property '$property_name' to '$new_value': $($_.Exception.Message)" + } + + if ($enum_map -ne $null -and $enum_map.ContainsKey($property_name)) { + $enum = [type]$enum_map.$property_name + $existing_value = [Enum]::ToObject($enum, $existing_value) + $new_value = [Enum]::ToObject($enum, $new_value) + } + [void]$changes.Add("-$property_name=$existing_value`n+$property_name=$new_value") + } + } + } + + return ,$changes +} + +Function Compare-PropertyList { + Param( + $collection, # the collection COM object to manipulate, this must contains the Create method + [string]$property_name, # human friendly name of the property object, e.g. action/trigger + [Array]$new, # a list of new properties, passed in by Ansible + [Array]$existing, # a list of existing properties from the COM object collection + [Hashtable]$map, # metadata for the collection, see below for the structure + [string]$enum # the parent enum name for type value + ) + <## map metadata structure + { + collection type [TASK_ACTION_TYPE] for Actions or [TASK_TRIGGER_TYPE2] for Triggers { + mandatory = list of mandatory properties for this type, ansible input name not the COM name + optional = list of optional properties that could be set for this type + # maps the ansible input object name to the COM name, e.g. working_directory = WorkingDirectory + map = { + ansible input name = COM name + } + } + }##> + # used by both Actions and Triggers to compare the collections of that property + + $enum = [type]$enum + $changes = [System.Collections.ArrayList]@() + $new_count = $new.Count + $existing_count = $existing.Count + + for ($i = 0; $i -lt $new_count; $i++) { + if ($i -lt $existing_count) { + $existing_property = $existing[$i] + } else { + $existing_property = $null + } + $new_property = $new[$i] + + # get the type of the property, for action this is set automatically + if (-not $new_property.ContainsKey("type")) { + Fail-Json -obj $result -message "entry for $property_name must contain a type key" + } + $type = $new_property.type + $valid_types = $map.Keys + $property_map = $map.$type + + # now let's validate the args for the property + $mandatory_args = $property_map.mandatory + $optional_args = $property_map.optional + $total_args = $mandatory_args + $optional_args + + # validate the mandatory arguments + foreach ($mandatory_arg in $mandatory_args) { + if (-not $new_property.ContainsKey($mandatory_arg)) { + Fail-Json -obj $result -message "mandatory key '$mandatory_arg' for $($property_name) is not set, mandatory keys are '$($mandatory_args -join "', '")'" + } + } + # throw a warning if in invalid key was set + foreach ($entry in $new_property.GetEnumerator()) { + $key = $entry.Name + if ($key -notin $total_args -and $key -ne "type") { + Add-Warning -obj $result -message "key '$key' for $($property_name) entry is not valid and will be ignored, valid keys are '$($total_args -join "', '")'" + } + } + + # now we have validated the input and have gotten the metadata, let's + # get the diff string + if ($existing_property -eq $null) { + # we have more properties than before,just add to the new + # properties list + $diff_list = [System.Collections.ArrayList]@() + foreach ($property_arg in $total_args) { + if ($new_property.ContainsKey($property_arg)) { + $com_name = Convert-SnakeToPascalCase -snake $property_arg + $property_value = $new_property.$property_arg + [void]$diff_list.Add("+$com_name=$property_value") + } + } + + [void]$changes.Add("+$property_name[$i] = {`n +Type=$type`n $($diff_list -join ",`n ")`n+}") + } elseif ([Enum]::ToObject($enum, $existing_property.Type) -ne $type) { + # the types are different so we need to change + $diff_list = [System.Collections.ArrayList]@() + + if ($existing_property.Type -notin $valid_types) { + [void]$diff_list.Add("-UNKNOWN TYPE $($existing_property.Type)") + foreach ($property_args in $total_args) { + if ($new_property.ContainsKey($property_arg)) { + $com_name = Convert-SnakeToPascalCase -snake $property_arg + $property_value = $new_property.$property_arg + [void]$diff_list.Add("+$com_name=$property_value") + } + } + } else { + # we know the types of the existing property + $existing_type = [Enum]::ToObject([TASK_TRIGGER_TYPE2], $existing_property.Type) + [void]$diff_list.Add("-Type=$existing_type") + [void]$diff_list.Add("+Type=$type") + foreach ($property_arg in $total_args) { + $com_name = Convert-SnakeToPascalCase -snake $property_arg + $existing_value = $existing_property.$com_name + $new_value = $new_property.$property_arg + + if ($existing_value -ne $null) { + [void]$diff_list.Add("-$com_name=$existing_value") + } + if ($new_value -ne $null) { + [void]$diff_list.Add("+$com_name=$new_value") + } + } + } + + [void]$changes.Add("$property_name[$i] = {`n $($diff_list -join ",`n ")`n}") + } else { + # compare the properties of existing and new + $diff_list = [System.Collections.ArrayList]@() + + foreach ($property_arg in $total_args) { + $new_value = $new_property.$property_arg + $com_name = Convert-SnakeToPascalCase -snake $property_arg + $existing_value = $existing_property.$com_name + + if ($new_value -ne $null) { + if ($new_value -cne $existing_value) { + [void]$diff_list.Add("-$com_name=$existing_value") + [void]$diff_list.Add("+$com_name=$new_value") + } + } + } + + if ($diff_list.Count -gt 0) { + [void]$changes.Add("$property_name[$i] = {`n $($diff_list -join ",`n ")`n}") + } + } + + # finally rebuild the new property collection + $new_object = $collection.Create($type) + foreach ($property_arg in $total_args) { + $new_value = $new_property.$property_arg + if ($new_value -ne $null) { + $com_name = Convert-SnakeToPascalCase -snake $property_arg + try { + $new_object.$com_name = $new_value + } catch { + Fail-Json -obj $result -message "failed to set $property_name property '$com_name' to '$new_value': $($_.Exception.Message)" + } + } + } + } + + # if there were any extra properties not in the new list, create diff str + if ($existing_count -gt $new_count) { + for ($i = $new_count; $i -lt $existing_count; $i++) { + $diff_list = [System.Collections.ArrayList]@() + $existing_property = $existing[$i] + $existing_type = [Enum]::ToObject($enum, $existing_property.Type) + + if ($map.ContainsKey($existing_type)) { + $property_map = $map.$existing_type + $property_args = $property_map.mandatory + $property_map.optional + + foreach ($property_arg in $property_args) { + $com_name = Convert-SnakeToPascalCase -snake $property_arg + $existing_value = $existing_property.$com_name + if ($existing_value -ne $null) { + [void]$diff_list.Add("-$com_name=$existing_value") + } + } + } else { + [void]$diff_list.Add("-UNKNOWN TYPE $existing_type") + } + + [void]$changes.Add("-$property_name[$i] = {`n $($diff_list -join ",`n ")`n-}") + } + } + + return ,$changes +} + +Function Compare-Actions($task_definition) { + # compares the Actions property and returns a list of list of changed + # actions for use in a diff string + # ActionCollection - https://msdn.microsoft.com/en-us/library/windows/desktop/aa446804(v=vs.85).aspx + # Action - https://msdn.microsoft.com/en-us/library/windows/desktop/aa446803(v=vs.85).aspx + if ($actions -eq $null) { + return ,[System.Collections.ArrayList]@() + } + + $task_actions = $task_definition.Actions + $existing_count = $task_actions.Count + + # because we clear the actions and re-add them to keep the order, we need + # to convert the existing actions to a new list. + # The Item property in actions starts at 1 + $existing_actions = [System.Collections.ArrayList]@() + for ($i = 1; $i -le $existing_count; $i++) { + [void]$existing_actions.Add($task_actions.Item($i)) + } + if ($existing_count -gt 0) { + $task_actions.Clear() + } + + $map = @{ + [TASK_ACTION_TYPE]::TASK_ACTION_EXEC = @{ + mandatory = @('path') + optional = @('arguments', 'working_directory') + } + } + $changes = Compare-PropertyList -collection $task_actions -property_name "action" -new $actions -existing $existing_actions -map $map -enum TASK_ACTION_TYPE + + return ,$changes +} + +Function Compare-Principal($task_definition, $task_definition_xml) { + # compares the Principal property and returns a list of changed objects for + # use in a diff string + # https://msdn.microsoft.com/en-us/library/windows/desktop/aa382071(v=vs.85).aspx + $principal_map = @{ + DisplayName = $display_name + LogonType = $logon_type + RunLevel = $run_level + } + $enum_map = @{ + LogonType = "TASK_LOGON_TYPE" + RunLevel = "TASK_RUN_LEVEL" + } + $task_principal = $task_definition.Principal + $changes = Compare-Properties -property_name "Principal" -parent_property $task_principal -map $principal_map -enum_map $enum_map + + # Principal.UserId and GroupId only returns the username portion of the + # username, skipping the domain or server name. This makes the + # comparison process useless so we need to parse the task XML to get + # the actual sid/username. Depending on OS version this could be the SID + # or it could be the username, we need to handle that accordingly + $principal_username_sid = $task_definition_xml.Task.Principals.Principal.UserId + if ($principal_username_sid -ne $null -and $principal_username_sid -notmatch "^S-\d-\d+(-\d+){1,14}(-\d+){0,1}$") { + $principal_username_sid = Convert-ToSID -account_name $principal_username_sid + } + $principal_group_sid = $task_definition_xml.Task.Principals.Principal.GroupId + if ($principal_group_sid -ne $null -and $principal_group_sid -notmatch "^S-\d-\d+(-\d+){1,14}(-\d+){0,1}$") { + $principal_group_sid = Convert-ToSID -account_name $principal_group_sid + } + + if ($username_sid -ne $null) { + $new_user_name = Convert-FromSid -sid $username_sid + if ($principal_group_sid -ne $null) { + $existing_account_name = Convert-FromSid -sid $principal_group_sid + [void]$changes.Add("-GroupId=$existing_account_name`n+UserId=$new_user_name") + $task_principal.UserId = $new_user_name + $task_principal.GroupId = $null + } elseif ($principal_username_sid -eq $null) { + [void]$changes.Add("+UserId=$new_user_name") + $task_principal.UserId = $new_user_name + } elseif ($principal_username_sid -ne $username_sid) { + $existing_account_name = Convert-FromSid -sid $principal_username_sid + [void]$changes.Add("-UserId=$existing_account_name`n+UserId=$new_user_name") + $task_principal.UserId = $new_user_name + } + } + if ($group_sid -ne $null) { + $new_group_name = Convert-FromSid -sid $group_sid + if ($principal_username_sid -ne $null) { + $existing_account_name = Convert-FromSid -sid $principal_username_sid + [void]$changes.Add("-UserId=$existing_account_name`n+GroupId=$new_group_name") + $task_principal.UserId = $null + $task_principal.GroupId = $new_group_name + } elseif ($principal_group_sid -eq $null) { + [void]$changes.Add("+GroupId=$new_group_name") + $task_principal.GroupId = $new_group_name + } elseif ($principal_group_sid -ne $group_sid) { + $existing_account_name = Convert-FromSid -sid $principal_group_sid + [void]$changes.Add("-GroupId=$existing_account_name`n+GroupId=$new_group_name") + $task_principal.GroupId = $new_group_name + } + } + + return ,$changes +} + +Function Compare-RegistrationInfo($task_definition) { + # compares the RegistrationInfo property and returns a list of changed + # objects for use in a diff string + # https://msdn.microsoft.com/en-us/library/windows/desktop/aa382100(v=vs.85).aspx + $reg_info_map = @{ + Author = $author + Date = $date + Description = $description + Source = $source + Version = $version + } + $changes = Compare-Properties -property_name "RegistrationInfo" -parent_property $task_definition.RegistrationInfo -map $reg_info_map + + return ,$changes +} + +Function Compare-Settings($task_definition) { + # compares the task Settings property and returns a list of changed objects + # for use in a diff string + # https://msdn.microsoft.com/en-us/library/windows/desktop/aa383480(v=vs.85).aspx + $settings_map = @{ + AllowDemandStart = $allow_demand_start + AllowHardTerminate = $allow_hard_terminate + Compatibility = $compatibility + DeleteExpiredTaskAfter = $delete_expired_task_after + DisallowStartIfOnBatteries = $disallow_start_if_on_batteries + ExecutionTimeLimit = $execution_time_limit + Enabled = $enabled + Hidden = $hidden + # IdleSettings = $idle_settings # TODO: this takes in a COM object + MultipleInstances = $multiple_instances + # NetworkSettings = $network_settings # TODO: this takes in a COM object + Priority = $priority + RestartCount = $restart_count + RestartInterval = $restart_interval + RunOnlyIfIdle = $run_only_if_idle + RunOnlyIfNetworkAvailable = $run_only_if_network_available + StartWhenAvailable = $start_when_available + StopIfGoingOnBatteries = $stop_if_going_on_batteries + WakeToRun = $wake_to_run + } + $changes = Compare-Properties -property_name "Settings" -parent_property $task_definition.Settings -map $settings_map + + return ,$changes +} + +Function Compare-Triggers($task_definition) { + # compares the task Triggers property and returns a list of changed objects + # for use in a diff string + # TriggerCollection - https://msdn.microsoft.com/en-us/library/windows/desktop/aa383875(v=vs.85).aspx + # Trigger - https://msdn.microsoft.com/en-us/library/windows/desktop/aa383868(v=vs.85).aspx + if ($triggers -eq $null) { + return ,[System.Collections.ArrayList]@() + } + + $task_triggers = $task_definition.Triggers + $existing_count = $task_triggers.Count + + # because we clear the actions and re-add them to keep the order, we need + # to convert the existing actions to a new list. + # The Item property in actions starts at 1 + $existing_triggers = [System.Collections.ArrayList]@() + for ($i = 1; $i -le $existing_count; $i++) { + [void]$existing_triggers.Add($task_triggers.Item($i)) + } + if ($existing_count -gt 0) { + $task_triggers.Clear() + } + + # TODO: solve repetition, takes in a COM object + $map = @{ + [TASK_TRIGGER_TYPE2]::TASK_TRIGGER_BOOT = @{ + mandatory = @() + optional = @('enabled', 'end_boundary', 'execution_time_limit', 'start_boundary') + } + [TASK_TRIGGER_TYPE2]::TASK_TRIGGER_DAILY = @{ + mandatory = @('start_boundary') + optional = @('days_interval', 'enabled', 'end_boundary', 'execution_time_limit', 'random_delay') + } + [TASK_TRIGGER_TYPE2]::TASK_TRIGGER_EVENT = @{ + mandatory = @('subscription') + # TODO: ValueQueries is a COM object + optional = @('delay', 'enabled', 'end_boundary', 'execution_time_limit', 'start_boundary') + } + [TASK_TRIGGER_TYPE2]::TASK_TRIGGER_IDLE = @{ + mandatory = @() + optional = @('enabled', 'end_boundary', 'execution_time_limit', 'start_boundary') + } + [TASK_TRIGGER_TYPE2]::TASK_TRIGGER_LOGON = @{ + mandatory = @() + optional = @('delay', 'enabled', 'end_boundary', 'execution_time_limit', 'start_boundary', 'user_id') + } + [TASK_TRIGGER_TYPE2]::TASK_TRIGGER_MONTHLYDOW = @{ + mandatory = @('start_boundary') + optional = @('days_of_week', 'enabled', 'end_boundary', 'execution_time_limit', 'months_of_year', 'random_delay', 'run_on_last_week_of_month', 'weeks_of_month') + } + [TASK_TRIGGER_TYPE2]::TASK_TRIGGER_MONTHLY = @{ + mandatory = @('days_of_month', 'start_boundary') + optional = @('enabled', 'end_boundary', 'execution_time_limit', 'months_of_year', 'random_delay', 'run_on_last_day_of_month', 'start_boundary') + } + [TASK_TRIGGER_TYPE2]::TASK_TRIGGER_REGISTRATION = @{ + mandatory = @() + optional = @('delay', 'enabled', 'end_boundary', 'execution_time_limit', 'start_boundary') + } + [TASK_TRIGGER_TYPE2]::TASK_TRIGGER_TIME = @{ + mandatory = @('start_boundary') + optional = @('enabled', 'end_boundary', 'execution_time_limit', 'random_delay') + } + [TASK_TRIGGER_TYPE2]::TASK_TRIGGER_WEEKLY = @{ + mandatory = @('days_of_week', 'start_boundary') + optional = @('enabled', 'end_boundary', 'execution_time_limit', 'random_delay', 'weeks_interval') + } + [TASK_TRIGGER_TYPE2]::TASK_TRIGGER_SESSION_STATE_CHANGE = @{ + mandatory = @('days_of_week', 'start_boundary') + optional = @('delay', 'enabled', 'end_boundary', 'execution_time_limit', 'state_change', 'user_id') + } + } + $changes = Compare-PropertyList -collection $task_triggers -property_name "trigger" -new $triggers -existing $existing_triggers -map $map -enum TASK_TRIGGER_TYPE2 + + return ,$changes +} + +Function Test-TaskExists($task_folder, $name) { + # checks if a task exists in the TaskFolder COM object, returns null if the + # task does not exist, otherwise returns the RegisteredTask object + $task = $null + if ($task_folder) { + $raw_tasks = $task_folder.GetTasks(1) # 1 = TASK_ENUM_HIDDEN + + for ($i = 1; $i -le $raw_tasks.Count; $i++) { + if ($raw_tasks.Item($i).Name -eq $name) { + $task = $raw_tasks.Item($i) + break + } + } + } + + return $task +} + +###################################### +### VALIDATION/BUILDING OF OPTIONS ### +###################################### +# convert username and group to SID if set +$username_sid = $null +if ($username) { + $username_sid = Convert-ToSID -account_name $username +} +$group_sid = $null +if ($group) { + $group_sid = Convert-ToSID -account_name $group +} + +# Convert the older arguments to the newer format if required +if ($old_executable -ne $null) { + Add-DeprecationWarning -obj $result -message "executable option is deprecated, please use the actions list option instead" -version 2.7 + if ($actions -ne $null) { + Fail-Json -obj $result -message "actions and executable are mutually exclusive, use actions by itself instead" + } + + $new_action = @{ path = $old_executable } + if ($old_arguments -ne $null) { + Add-DeprecationWarning -obj $result -message "arguments option is deprecated, please use the actions list option instead" -version 2.7 + $new_action.arguments = $old_arguments + } + $actions = @($new_action) +} + +# validate store_password and logon_type +if ($logon_type -ne $null) { + $full_enum_name = "TASK_LOGON_$($logon_type.ToUpper())" + $logon_type = [TASK_LOGON_TYPE]::$full_enum_name +} +if ($store_password -ne $null) { + Add-DeprecationWarning -obj $result -message "store_password option is deprecated, please use logon_type: password instead" -version 2.7 + if ($logon_type -ne $null) { + Fail-Json -obj $result -message "logon_type and store_password are mutually exclusive, use logon_type=password instead" + } + if ($store_password -eq $true -and $password -ne $null) { + $logon_type = [TASK_LOGON_TYPE]::TASK_LOGON_PASSWORD + } +} + +# now validate the logon_type option with the other parameters +if ($username -ne $null -and $group -ne $null) { + Fail-Json -obj $result -message "username and group can not be set at the same time" +} +if ($logon_type -ne $null) { + if ($logon_type -eq [TASK_LOGON_TYPE]::TASK_LOGON_PASSWORD -and $password -eq $null) { + Fail-Json -obj $result -message "password must be set when logon_type=password" + } + if ($logon_type -eq [TASK_LOGON_TYPE]::TASK_LOGON_S4U -and $password -eq $null) { + Fail-Json -obj $result -message "password must be set when logon_type=s4u" + } + + if ($logon_type -eq [TASK_LOGON_TYPE]::TASK_LOGON_GROUP -and $group -eq $null) { + Fail-Json -obj $result -message "group must be set when logon_type=group" + } + + # SIDs == Local System, Local Service and Network Service + if ($logon_type -eq [TASK_LOGON_TYPE]::TASK_LOGON_SERVICE_ACCOUNT -and $username_sid -notin @("S-1-5-18", "S-1-5-19", "S-1-5-20")) { + Fail-Json -obj $result -message "username must be SYSTEM, LOCAL SERVICE or NETWORK SERVICE when logon_type=service_account" + } +} + +# convert the run_level to enum value +if ($run_level -ne $null) { + if ($run_level -eq "limited") { + $run_level = [TASK_RUN_LEVEL]::TASK_RUNLEVEL_LUA + } else { + $run_level = [TASK_RUN_LEVEL]::TASK_RUNLEVEL_HIGHEST + } +} + +# manually add the only support action type for each action - also convert PSCustomObject to Hashtable +for ($i = 0; $i -lt $actions.Count; $i++) { + $action = ConvertTo-HashtableFromPsCustomObject -object $actions[$i] + $action.type = [TASK_ACTION_TYPE]::TASK_ACTION_EXEC + if (-not $action.ContainsKey("path")) { + Fail-Json -obj $result -message "action entry must contain the key 'path'" + } + $actions[$i] = $action +} + +# convert deprecated trigger args to new format +$deprecated_trigger = $null +if ($old_frequency -ne $null) { + # once, daily, weekly + Add-DeprecationWarning -obj $result -message "" -version 2.7 + if ($triggers.Count -eq 0) { + $deprecated_trigger = @{type = $null} + switch ($frequency) { + once { $deprecated_trigger.type = [TASK_TRIGGER_TYPE2]::TASK_TRIGGER_TIME } + daily { $deprecated_trigger.type = [TASK_TRIGGER_TYPE2]::TASK_TRIGGER_DAILY } + weekly { $deprecated_trigger.type = [TASK_TRIGGER_TYPE2]::TASK_TRIGGER_WEEKLY } + } + } else { + Add-Warning -obj $result -message "the trigger list is already specified, ignoring the frequency option as it is deprecated" + } +} +if ($old_days_of_week -ne $null) { + Add-DeprecationWarning -obj $result -message "days_of_week is deprecated, use the triggers list with 'monthlydow' type" -version 2.7 + if ($triggers.Count -eq 0) { + $deprecated_trigger.days_of_week = $old_days_of_week + } else { + Add-Warning -obj $result -message "the trigger list is already specified, ignoring the days_of_week option as it is deprecated" + } +} +if ($old_time -ne $null) { + Add-DeprecationWarning -obj $result -message "old_time is deprecated, use the triggers list to specify the 'start_boundary'" -version 2.7 + if ($triggers.Count -eq 0) { + try { + $old_time_cast = [datetime]$old_time + } catch [System.InvalidCastException] { + Fail-Json -obj $result -message "failed to convert time '$old_time' to the DateTime format" + } + $deprecated_trigger.start_boundary = ($old_time_cast | Get-Date -Format s) + } else { + Add-Warning -obj $result -message "the trigger list is already specified, ignoring the time option as it is deprecated" + } +} +if ($deprecated_trigger -ne $null) { + $triggers += $deprecated_trgger +} + +# convert and validate the triggers - and convert PSCustomObject to Hashtable +for ($i = 0; $i -lt $triggers.Count; $i++) { + $trigger = ConvertTo-HashtableFromPsCustomObject -object $triggers[$i] + $valid_trigger_types = @('event', 'time', 'daily', 'weekly', 'monthly', 'monthlydow', 'idle', 'registration', 'boot', 'logon', 'session_state_change') + if (-not $trigger.ContainsKey("type")) { + Fail-Json -obj $result -message "a trigger entry must contain a key 'type' with a value of '$($valid_trigger_types -join "', '")'" + } + + $trigger_type = $trigger.type + if ($trigger_type -notin $valid_trigger_types) { + Fail-Json -obj $result -message "the specified trigger type '$trigger_type' is not valid, type must be a value of '$($valid_trigger_types -join "', '")'" + } + + $full_enum_name = "TASK_TRIGGER_$($trigger_type.ToUpper())" + $trigger_type = [TASK_TRIGGER_TYPE2]::$full_enum_name + $trigger.type = $trigger_type + + $date_properties = @('start_boundary', 'end_boundary') + foreach ($property_name in $date_properties) { + # validate the date is in the DateTime format + # yyyy-mm-ddThh:mm:ss + if ($trigger.ContainsKey($property_name)) { + $date_value = $trigger.$property_name + try { + $date = Get-Date -Date $date_value -Format s + # make sure we convert it to the full string format + $trigger.$property_name = $date.ToString() + } catch [System.Management.Automation.ParameterBindingException] { + Fail-Json -obj $result -message "trigger option '$property_name' must be in the format 'YYYY-MM-DDThh:mm:ss' format but was '$date_value'" + } + } + } + + $time_properties = @('execution_time_limit', 'delay', 'random_delay') + foreach ($property_name in $time_properties) { + # validate the duration is in the Duration Data Type format + # PnYnMnDTnHnMnS + if ($trigger.ContainsKey($property_name)) { + $time_span = $trigger.$property_name + try { + [void][System.Xml.XmlConvert]::ToTimeSpan($time_span) + } catch [System.FormatException] { + Fail-Json -obj $result -message "trigger option '$property_name' must be in the XML duration format but was '$time_span'" + } + } + } + + # convert out human readble text to the hex values for these properties + if ($trigger.ContainsKey("days_of_week")) { + $days = $trigger.days_of_week + if ($days -is [String]) { + $days = $days.Split(",").Trim() + } elseif ($days -isnot [Array]) { + $days = @($days) + } + + $day_value = 0 + foreach ($day in $days) { + # https://msdn.microsoft.com/en-us/library/windows/desktop/aa382057(v=vs.85).aspx + switch ($day) { + sunday { $day_value = $day_value -bor 0x01 } + monday { $day_value = $day_value -bor 0x02 } + tuesday { $day_value = $day_value -bor 0x04 } + wednesday { $day_value = $day_value -bor 0x08 } + thursday { $day_value = $day_value -bor 0x10 } + friday { $day_value = $day_value -bor 0x20 } + saturday { $day_value = $day_value -bor 0x40 } + default { Fail-Json -obj $result -message "invalid day of week '$day', check the spelling matches the full day name" } + } + } + if ($day_value -eq 0) { + $day_value = $null + } + + $trigger.days_of_week = $day_value + } + if ($trigger.ContainsKey("days_of_month")) { + $days = $trigger.days_of_month + if ($days -is [String]) { + $days = $days.Split(",").Trim() + } elseif ($days -isnot [Array]) { + $days = @($days) + } + + $day_value = 0 + foreach ($day in $days) { + # https://msdn.microsoft.com/en-us/library/windows/desktop/aa382063(v=vs.85).aspx + switch ($day) { + 1 { $day_value = $day_value -bor 0x01 } + 2 { $day_value = $day_value -bor 0x02 } + 3 { $day_value = $day_value -bor 0x04 } + 4 { $day_value = $day_value -bor 0x08 } + 5 { $day_value = $day_value -bor 0x10 } + 6 { $day_value = $day_value -bor 0x20 } + 7 { $day_value = $day_value -bor 0x40 } + 8 { $day_value = $day_value -bor 0x80 } + 9 { $day_value = $day_value -bor 0x100 } + 10 { $day_value = $day_value -bor 0x200 } + 11 { $day_value = $day_value -bor 0x400 } + 12 { $day_value = $day_value -bor 0x800 } + 13 { $day_value = $day_value -bor 0x1000 } + 14 { $day_value = $day_value -bor 0x2000 } + 15 { $day_value = $day_value -bor 0x4000 } + 16 { $day_value = $day_value -bor 0x8000 } + 17 { $day_value = $day_value -bor 0x10000 } + 18 { $day_value = $day_value -bor 0x20000 } + 19 { $day_value = $day_value -bor 0x40000 } + 20 { $day_value = $day_value -bor 0x80000 } + 21 { $day_value = $day_value -bor 0x100000 } + 22 { $day_value = $day_value -bor 0x200000 } + 23 { $day_value = $day_value -bor 0x400000 } + 24 { $day_value = $day_value -bor 0x800000 } + 25 { $day_value = $day_value -bor 0x1000000 } + 26 { $day_value = $day_value -bor 0x2000000 } + 27 { $day_value = $day_value -bor 0x4000000 } + 28 { $day_value = $day_value -bor 0x8000000 } + 29 { $day_value = $day_value -bor 0x10000000 } + 30 { $day_value = $day_value -bor 0x20000000 } + 31 { $day_value = $day_value -bor 0x40000000 } + default { Fail-Json -obj $result -message "invalid day of month '$day', please specify numbers from 1-31" } + } + } + if ($day_value -eq 0) { + $day_value = $null + } + $trigger.days_of_month = $day_value + } + if ($trigger.ContainsKey("weeks_of_month")) { + $weeks = $trigger.weeks_of_month + if ($weeks -is [String]) { + $weeks = $weeks.Split(",").Trim() + } elseif ($weeks -isnot [Array]) { + $weeks = @($weeks) + } + + $week_value = 0 + foreach ($week in $weeks) { + # https://msdn.microsoft.com/en-us/library/windows/desktop/aa382061(v=vs.85).aspx + switch ($week) { + 1 { $week_value = $week_value -bor 0x01 } + 2 { $week_value = $week_value -bor 0x02 } + 3 { $week_value = $week_value -bor 0x04 } + 4 { $week_value = $week_value -bor 0x08 } + default { Fail-Json -obj $result -message "invalid week of month '$week', please specify weeks from 1-4" } + } + + } + if ($week_value -eq 0) { + $week_value = $null + } + $trigger.weeks_of_month = $week_value + } + if ($trigger.ContainsKey("months_of_year")) { + $months = $trigger.months_of_year + if ($months -is [String]) { + $months = $months.Split(",").Trim() + } elseif ($months -isnot [Array]) { + $months = @($months) + } + + $month_value = 0 + foreach ($month in $months) { + # https://msdn.microsoft.com/en-us/library/windows/desktop/aa382064(v=vs.85).aspx + switch ($month) { + january { $month_value = $month_value -bor 0x01 } + february { $month_value = $month_value -bor 0x02 } + march { $month_value = $month_value -bor 0x04 } + april { $month_value = $month_value -bor 0x08 } + may { $month_value = $month_value -bor 0x10 } + june { $month_value = $month_value -bor 0x20 } + july { $month_value = $month_value -bor 0x40 } + august { $month_value = $month_value -bor 0x80 } + september { $month_value = $month_value -bor 0x100 } + october { $month_value = $month_value -bor 0x200 } + november { $month_value = $month_value -bor 0x400 } + december { $month_value = $month_value -bor 0x800 } + default { Fail-Json -obj $result -message "invalid month name '$month', please specify full month name" } + } + } + if ($month_value -eq 0) { + $month_value = $null + } + $trigger.months_of_year = $month_value + } + $triggers[$i] = $trigger +} + +# add \ to start of path if it is not already there +if (-not $path.StartsWith("\")) { + $path = "\$path" +} +# ensure path does not end with \ if more than 1 char +if ($path.EndsWith("\") -and $path.Length -ne 1) { + $path = $path.Substring(0, $path.Length - 1) +} + +######################## +### START CODE BLOCK ### +######################## +$service = New-Object -ComObject Schedule.Service +try { + $service.Connect() +} catch { + Fail-Json -obj $result -message "failed to connect to the task scheduler service: $($_.Exception.Message)" +} + +# check that the path for the task set exists, create if need be +try { + $task_folder = $service.GetFolder($path) +} catch { + $task_folder = $null +} + +# try and get the task at the path +$task = Test-TaskExists -task_folder $task_folder -name $name +$task_path = Join-Path -Path $path -ChildPath $name + +if ($state -eq "absent") { + if ($task -ne $null) { + if (-not $check_mode) { + try { + $task_folder.DeleteTask($name, 0) + } catch { + Fail-Json -obj $result -message "failed to delete task '$name' at path '$path': $($_.Exception.Message)" + } + } + if ($diff_mode) { + $result.diff.prepared = "-[Task]`n-$task_path`n" + } + $result.changed = $true + + # check if current folder has any more tasks + $other_tasks = $task_folder.GetTasks(1) # 1 = TASK_ENUM_HIDDEN + if ($other_tasks.Count -eq 0 -and $task_folder.Name -ne "\") { + try { + $task_folder.DeleteFolder($null, $null) + } catch { + Fail-Json -obj $result -message "failed to delete empty task folder '$path' after task deletion: $($_.Exception.Message)" + } + } + } +} else { + if ($task -eq $null) { + $create_diff_string = "+[Task]`n+$task_path`n`n" + # to create a bare minimum task we need 1 action + if ($actions -eq $null -or $actions.Count -eq 0) { + Fail-Json -obj $result -message "cannot create a task with no actions, set at least one action with a path to an executable" + } + + # Create a bare minimum task here, further properties will be set later on + $task_definition = $service.NewTask(0) + + # Set Actions info + # https://msdn.microsoft.com/en-us/library/windows/desktop/aa446803(v=vs.85).aspx + $create_diff_string += "[Actions]`n" + $task_actions = $task_definition.Actions + foreach ($action in $actions) { + $create_diff_string += "+action[0] = {`n +Type=$([TASK_ACTION_TYPE]::TASK_ACTION_EXEC),`n +Path=$($action.path)`n" + $task_action = $task_actions.Create([TASK_ACTION_TYPE]::TASK_ACTION_EXEC) + $task_action.Path = $action.path + if ($action.arguments -ne $null) { + $create_diff_string += " +Arguments=$($action.arguments)`n" + $task_action.Arguments = $action.arguments + } + if ($action.working_directory -ne $null) { + $create_diff_string += " +WorkingDirectory=$($action.working_directory)`n" + $task_action.WorkingDirectory = $action.working_directory + } + $create_diff_string += "+}`n" + } + + # Register the new task + # https://msdn.microsoft.com/en-us/library/windows/desktop/aa382577(v=vs.85).aspx + if ($check_mode) { + # Only validate the task in check mode + $task_creation_flags = [TASK_CREATION]::TASK_VALIDATE_ONLY + } else { + # Create the task but do not fire it as we still need to configure it further below + $task_creation_flags = [TASK_CREATION]::TASK_CREATE -bor [TASK_CREATION]::TASK_IGNORE_REGISTRATION_TRIGGERS + } + + # folder doesn't exist, need to create + if ($task_folder -eq $null) { + $task_folder = $service.GetFolder("\") + try { + if (-not $check_mode) { + $task_folder = $task_folder.CreateFolder($path) + } + } catch { + Fail-Json -obj $result -message "failed to create new folder at path '$path': $($_.Exception.Message)" + } + } + + try { + $task = $task_folder.RegisterTaskDefinition($name, $task_definition, $task_creation_flags, $null, $null, $null) + } catch { + Fail-Json -obj $result -message "failed to register new task definition: $($_.Exception.Message)" + } + if ($diff_mode) { + $result.diff.prepared = $create_diff_string + } + + $result.changed = $true + } + + # we cannot configure a task that was created above in check mode as it + # won't actually exist + if ($task) { + $task_definition = $task.Definition + $task_definition_xml = [xml]$task_definition.XmlText + + $action_changes = Compare-Actions -task_definition $task_definition + $principal_changed = Compare-Principal -task_definition $task_definition -task_definition_xml $task_definition_xml + $reg_info_changed = Compare-RegistrationInfo -task_definition $task_definition + $settings_changed = Compare-Settings -task_definition $task_definition + $trigger_changes = Compare-Triggers -task_definition $task_definition + + # compile the diffs into one list with headers + $task_diff = [System.Collections.ArrayList]@() + if ($action_changes.Count -gt 0) { + [void]$task_diff.Add("[Actions]") + foreach ($action_change in $action_changes) { + [void]$task_diff.Add($action_change) + } + [void]$task_diff.Add("`n") + } + if ($principal_changed.Count -gt 0) { + [void]$task_diff.Add("[Principal]") + foreach ($principal_change in $principal_changed) { + [void]$task_diff.Add($principal_change) + } + [void]$task_diff.Add("`n") + } + if ($reg_info_changed.Count -gt 0) { + [void]$task_diff.Add("[Registration Info]") + foreach ($reg_info_change in $reg_info_changed) { + [void]$task_diff.Add($reg_info_change) + } + [void]$task_diff.Add("`n") + } + if ($settings_changed.Count -gt 0) { + [void]$task_diff.Add("[Settings]") + foreach ($settings_change in $settings_changed) { + [void]$task_diff.add($settings_change) + } + [void]$task_diff.Add("`n") + } + if ($trigger_changes.Count -gt 0) { + [void]$task_diff.Add("[Triggers]") + foreach ($trigger_change in $trigger_changes) { + [void]$task_diff.Add("$trigger_change") + } + [void]$task_diff.Add("`n") + } + + if ($password -ne $null -and (($update_password -eq $true) -or ($task_diff.Count -gt 0))) { + # because we can't compare the passwords we just need to reset it + $register_username = $username + $register_password = $password + $register_logon_type = $task_principal.LogonType + } else { + # will inherit from the Principal property values + $register_username = $null + $register_password = $null + $register_logon_type = $null + } + + if ($task_diff.Count -gt 0 -or $register_password -ne $null) { + if ($check_mode) { + # Only validate the task in check mode + $task_creation_flags = [TASK_CREATION]::TASK_VALIDATE_ONLY + } else { + # Create the task + $task_creation_flags = [TASK_CREATION]::TASK_CREATE_OR_UPDATE + } + try { + $task_folder.RegisterTaskDefinition($name, $task_definition, $task_creation_flags, $register_username, $register_password, $register_logon_type) | Out-Null + } catch { + Fail-Json -obj $result -message "failed to modify scheduled task: $($_.Exception.Message)" + } + + $result.changed = $true + + if ($diff_mode) { + $changed_diff_text = $task_diff -join "`n" + if ($result.diff.prepared -ne $null) { + $diff_text = "$($result.diff.prepared)`n$changed_diff_text" + } else { + $diff_text = $changed_diff_text + } + $result.diff.prepared = $diff_text.Trim() + } + } + } +} + +Exit-Json -obj $result diff --git a/lib/ansible/modules/windows/win_scheduled_task.py b/lib/ansible/modules/windows/win_scheduled_task.py index f0100f98dd5..041d33355a3 100644 --- a/lib/ansible/modules/windows/win_scheduled_task.py +++ b/lib/ansible/modules/windows/win_scheduled_task.py @@ -1,22 +1,9 @@ #!/usr/bin/python # -*- coding: utf-8 -*- # This file is part of Ansible -# -# Ansible is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Ansible is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with Ansible. If not, see . -# this is a windows documentation stub. actual code lives in the .ps1 -# file of the same name +# Copyright (c) 2017 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'], @@ -26,138 +13,482 @@ ANSIBLE_METADATA = {'metadata_version': '1.1', DOCUMENTATION = r''' --- module: win_scheduled_task -author: "Peter Mounce" version_added: "2.0" short_description: Manage scheduled tasks description: - - Manage scheduled tasks +- Creates/modified or removes Windows scheduled tasks. notes: - - This module requires Windows Server 2012 or later. +- In Ansible 2.4 and earlier, this could only be run on Server 2012/Windows 8 + or newer. Since 2.5 this restriction has been lifted. +- The option names and structure for actions and triggers of a service follow + the C(RegisteredTask) naming standard and requirements, it would be useful to + read up on this guide if coming across any issues U(https://msdn.microsoft.com/en-us/library/windows/desktop/aa382542.aspx). options: + # module definition options name: - description: - - Name of the scheduled task + description: The name of the scheduled task without the path. required: true - description: - description: - - The description for the scheduled task - enabled: - description: - - Enable/disable the task - choices: - - yes - - no - default: yes - state: - description: - - State that the task should become - required: true - choices: - - present - - absent - user: - description: - - User to run the scheduled task as; defaults to the current user - default: DOMAIN\user - password: - description: - - Password for the user account to run the scheduled task as. This is required for running a task without the user being - logged in, excluding Windows built-in service accounts. This should be used for specifying credentials during initial - task creation, and changing stored user credentials, as setting this value will cause the task to be recreated. - version_added: "2.4" - runlevel: - description: - - The level of user rights used to run the task - default: limited - choices: - - limited - - highest - version_added: "2.4" - store_password: - description: - - Store the password for the user running the task. If C(false), the task will only have access to local resources. - default: true - version_added: "2.4" - executable: - description: - - Command the scheduled task should execute - aliases: [ execute ] - arguments: - description: - - Arguments to provide scheduled task action - aliases: [ argument ] - frequency: - description: - - The frequency of the command, not idempotent - choices: - - once - - daily - - weekly - time: - description: - - Time to execute scheduled task, not idempotent - days_of_week: - description: - - Days of the week to run a weekly task, not idempotent path: description: - - Task folder in which this task will be stored - creates a non-existent path when C(state) is C(present), - and removes an empty path when C(state) is C(absent) - default: '\' + - Task folder in which this task will be stored. + - Will create the folder when C(state=present) and the folder does not + already exist. + - Will remove the folder when C(state=absent) and there are no tasks left + in the folder. + default: \ + state: + description: + - When C(state=present) will ensure the task exists. + - When C(state=absent) will ensure the task does not exist. + choices: [ absent, present ] + default: present + + # Action options + actions: + description: + - A list of action to configure for the task. + - See suboptions for details on how to construct each list entry. + - When creating a task there MUST be at least one action but when deleting + a task this can be a null or an empty list. + - The ordering of this list is important, the module will ensure the order + is kept when modifying the task. + - This module only supports the C(ExecAction) type but can still delete the + older legacy types. + suboptions: + path: + description: + - The path to the executable for the ExecAction. + required: true + arguments: + description: + - An argument string to supply for the executable. + working_directory: + description: + - The working directory to run the executable from. + version_added: '2.5' + arguments: + description: + - Arguments to provide for a scheduled task action. + - DEPRECATED since 2.5, use the C(actions) option instead to specify a list + of actions to run. + - Will be removed in 2.7. + aliases: [ argument ] + executable: + description: + - The path to the executable to run for a scheduled task action. + - DEPRECATED since 2.5, use the C(actions) option instead to specify a list + of actions to run. + - Will be removed in 2.7. + + # Trigger options + triggers: + description: + - A list of triggers to configure for the task. + - See suboptions for details on how to construct each list entry. + - The ordering of this list is important, the module will ensure the order + is kept when modifying the task. + - There are multiple types of triggers, see U(https://msdn.microsoft.com/en-us/library/windows/desktop/aa383868.aspx) + for a list of trigger types and their options. + - The suboption options listed below are not required for all trigger + types, read the description for more details. + suboptions: + type: + description: + - The trigger type, this value controls what below options are + required. + required: true + choices: [ boot, daily, event, idle, logon, monthlydow, monthly, registration, time, weekly, session_state_change ] + enabled: + description: + - Whether to set the trigger to enabled or disabled + - Used in all trigger types. + type: bool + start_boundary: + description: + - The start time for the task, even if the trigger meets the other + start criteria, it won't start until this time is met. + - If you wish to run a task at 9am on a day you still need to specify + the date on which the trigger is activated, you can set any date even + ones in the past. + - Required when C(type) is C(daily), C(monthlydow), C(monthly), + C(time), C(weekly), (session_state_change). + - Optional for the rest of the trigger types. + - This is in ISO 8601 DateTime format C(YYYY-MM-DDThh:mm:ss). + end_boundary: + description: + - The end time for when the trigger is deactivated. + - This is in ISO 8601 DateTime format C(YYYY-MM-DDThh:mm:ss). + execution_time_limit: + description: + - The maximum amount of time that the task is allowed to run for. + - Optional for all the trigger types. + - Is in the ISO 8601 Duration format C(P[n]Y[n]M[n]DT[n]H[n]M[n]S). + delay: + description: + - The time to delay the task from running once the trigger has been + fired. + - Optional when C(type) is C(event), C(logon), C(registration), + C(session_state_change). + - Is in the ISO 8601 Duration format C(P[n]Y[n]M[n]DT[n]H[n]M[n]S). + random_delay: + description: + - The delay time that is randomly added to the start time of the + trigger. + - Optional when C(type) is C(daily), C(monthlydow), C(monthly), + C(time), C(weekly). + - Is in the ISO 8601 Duration format C(P[n]Y[n]M[n]DT[n]H[n]M[n]S). + subscription: + description: + - Only used and is required for C(type=event). + - The XML query string that identifies the event that fires the + trigger. + user_id: + description: + - The username that the trigger will target. + - Optional when C(type) is C(logon), C(session_state_change). + - Can be the username or SID of a user. + - When C(type=logon) and you want the trigger to fire when a user in a + group logs on, leave this as null and set C(group) to the group you + wish to trigger. + days_of_week: + description: + - The days of the week for the trigger. + - Can be a list or comma separated string of full day names e.g. monday + instead of mon. + - Required when C(type) is C(weekly), C(type=session_state_change). + - Optional when C(type=monthlydow). + days_of_month: + description: + - The days of the month from 1 to 31 for the triggers. + - If you wish to set the trigger for the last day of any month + use C(run_on_last_day_of_month). + - Can be a list or comma separated string of day numbers. + - Required when C(type=monthly). + weeks_of_month: + description: + - The weeks of the month for the trigger. + - Can be a list or comma separated string of the numbers 1 to 4 + representing the first to 4th week of the month. + - Optional when C(type=monthlydow). + months_of_year: + description: + - The months of the year for the trigger. + - Can be a list or comma separated string of full month names e.g. + march instead of mar. + - Optional when C(type) is C(monthlydow), C(monthly). + run_on_last_week_of_month: + description: + - Boolean value that sets whether the task runs on the last week of the + month. + - Optional when C(type) is C(monthlydow). + type: bool + run_on_last_day_of_month: + description: + - Boolean value that sets whether the task runs on the last day of the + month. + - Optional when C(type) is C(monthly). + type: bool + weeks_interval: + description: + - The interval of weeks to run on, e.g. C(1) means every week while + C(2) means every other week. + - Optional when C(type=weekly). + version_added: '2.5' + days_of_week: + description: + - Days of the week to run a weekly task. + - Specify a list or comma separate days in the full version, e.g. monday + instead of mon. + - DEPRECATED since 2.5, use the C(triggers) option list with the type of + C(monthlydow) or C(weekly). + - Will be removed in 2.7. + frequency: + description: + - The frequency of the task to run. + - DEPRECATED since 2.5, use the C(triggers) option list and specify the + type based on the frequency required. + - Will be removed in 2.7. + choices: [ daily, once, weekly ] + time: + description: + - The start time to execute the scheduled task. + - DEPRECATED since 2.5, use the C(triggers) option list and use the + C(start_boundary) option to set the start time. + - Will be removed in 2.7. + + # Principal options + display_name: + description: + - The name of the user/group that is displayed in the Task Scheduler UI. + version_added: '2.5' + group: + description: + - The group that will run the task. + - C(group) and C(username) are exclusive to each other and cannot be set + at the same time. + - C(logon_type) can either be not set or equal C(group). + version_added: '2.5' + logon_type: + description: + - The logon method that the task will run with. + - C(password) means the password will be stored and the task has access + to network resources. + - C(s4u) means the existing token will be used to run the task and no + password will be stored with the task. Means no network or encrypted + files access. + - C(interactive_token) means the user must already be logged on + interactively and will run in an existing interactive session. + - C(group) means that the task will run as a group. + - C(service_account) means that a service account like System, Local + Service or Network Service will run the task. + choices: [ none, password, s4u, interactive_token, group, service_account, token_or_password ] + version_added: '2.5' + run_level: + description: + - The level of user rights used to run the task. + - If not specified the task will be created with limited rights. + choices: [ limited, highest ] + version_added: '2.4' + aliases: [ runlevel ] + username: + description: + - The user to run the scheduled task as. + - Will default to the current user under an interactive token if not + specified during creation. + aliases: [ user ] + password: + description: + - The password for the user account to run the scheduled task as. + - This is required when running a task without the user being logged in, + excluding the builtin service accounts. + - If set, will always result in a change unless C(update_password) is set + to C(no) and no othr changes are required for the service. + version_added: '2.4' + update_password: + description: + - Whether to update the password even when not other changes have occured. + - When C(yes) will always result in a change when executing the module. + type: bool + default: 'yes' + version_added: '2.5' + store_password: + description: + - Whether to store the password for the user running the task. + - If C(no), the task will only have access to local resources. + - DEPRECATED since 2.5, use C(logon_type=password) to set whether to store + the password for the task. + - Will be removed in 2.7. + type: bool + default: 'yes' + version_added: '2.4' + + # RegistrationInfo options + author: + description: + - The author of the task. + version_added: '2.5' + date: + description: + - The date when the task was registered. + version_added: '2.5' + description: + description: + - The description of the task. + version_added: '2.5' + source: + description: + - The source of the task. + version_added: '2.5' + version: + description: + - The version number of the task. + version_added: '2.5' + + # Settings options + allow_demand_start: + description: + - Whether the task can be started by using either the Run command or the + Context menu. + type: bool + version_added: '2.5' + allow_hard_terminate: + description: + - Whether the task can be terminated by using TerminateProcess. + type: bool + version_added: '2.5' + compatibility: + description: + - The integer value with indicates which version of Task Scheduler a task + is compatible with. + - C(0) means the task is compatible with the AT command. + - C(1) means the task is compatible with Task Scheduler 1.0. + - C(2) means the task is compatible with Task Scheduler 2.0. + choices: [ 0, 1, 2 ] + version_added: '2.5' + delete_expired_task_after: + description: + - The amount of time that the Task Scheduler will wait before deleting the + task after it expires. + - A task expires after the end_boundary has been exceeded for all triggers + associated with the task. + - This is in ISO 8601 DateTime format C(YYYY-MM-DDThh:mm:ss). + version_added: '2.5' + disallow_start_if_on_batteries: + description: + - Whether the task will not be started if the computer is running on + battery power. + type: bool + version_added: '2.5' + enabled: + description: + - Whether the task is enabled, the task can only run when C(yes). + type: bool + version_added: '2.5' + execution_time_limit: + description: + - The amount of time allowed to complete the task. + - When not set, the time limit is infinite. + - This is in ISO 8601 DateTime format C(YYYY-MM-DDThh:mm:ss). + version_added: '2.5' + hidden: + description: + - Whether the task will be hidden in the UI. + type: bool + version_added: '2.5' + mutliple_instances: + description: + - An integer that indicates the behaviour when starting a task that is + already running. + - C(0) will start a new instance in parallel with existing instances of + that task. + - C(1) will wait until other instances of that task to finish running + before starting itself. + - C(2) will not start a new instance if another is running. + - C(3) will stop other instances of the task and start the new one. + choices: [ 0, 1, 2, 3 ] + version_added: '2.5' + priortiy: + description: + - The priority level (0-10) of the task. + - When creating a new task the default if C(7). + - See U(https://msdn.microsoft.com/en-us/library/windows/desktop/aa383512.aspx) + for details on the priority levels. + version_added: '2.5' + restart_count: + description: + - The number of times that the Task Scheduler will attempt to restart the + task. + version_added: '2.5' + restart_interval: + description: + - How long the Task Scheduler will attempt to restart the task. + - If this is set then C(restart_count) must also be set. + - The maximum allowed time is 31 days. + - The minimum allowed time is 1 minute. + - This is in ISO 8601 DateTime format C(YYYY-MM-DDThh:mm:ss). + version_added: '2.5' + run_only_if_idle: + description: + - Whether the task will run the task only if the computer is in an idle + state. + type: bool + version_added: '2.5' + run_only_if_network_available: + description: + - Whether the task will run only when a network is available. + type: bool + version_added: '2.5' + start_when_available: + description: + - Whether the task can start at any time after its scheduled time has + passed. + type: bool + version_added: '2.5' + stop_if_going_on_batteries: + description: + - Whether the task will be stopped if the computer begins to run on battery + power. + type: bool + version_added: '2.5' + wake_to_run: + description: + - Whether the task will wake the computer when it is time to run the task. + type: bool + version_added: '2.5' +author: +- Peter Mounce (@petemounce) +- Jordan Borean (@jborean93) ''' EXAMPLES = r''' -# Create a scheduled task to open a command prompt -- win_scheduled_task: +- name: create a task to open 2 command prompts as SYSTEM + win_scheduled_task: name: TaskName description: open command prompt - executable: cmd - arguments: -opt1 -opt2 - path: \example - time: 9am - frequency: daily + actions: + - path: cmd.exe + arguments: /c hostname + - path: cmd.exe + arguments: /c whoami + triggers: + - type: daily + start_boundary: 2017-10-09T09:00:00 + username: SYSTEM state: present enabled: yes - user: SYSTEM -- name: Create a task to run a PowerShell script as NETWORK SERVICE at the highest user rights level +- name: create task to run a PS script as NETWORK service on boot win_scheduled_task: name: TaskName2 description: Run a PowerShell script - executable: C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe - arguments: -ExecutionPolicy Unrestricted -NonInteractive -File C:\TestDir\Test.ps1 - time: 6pm - frequency: once + actions: + - path: C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe + arguments: -ExecutionPolicy Unrestricted -NonInteractive -File C:\TestDir\Test.ps1 + triggers: + - type: boot + username: NETWORK SERVICE + run_level: highest state: present - enabled: yes - user: NETWORK SERVICE - runlevel: highest -- name: Change the above task to run under a domain user account, storing credentials for the task +- name: change above task to run under a domain user account, storing the passwords win_scheduled_task: name: TaskName2 - description: Run a PowerShell script - executable: C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe - arguments: -ExecutionPolicy Unrestricted -NonInteractive -File C:\TestDir\Test.ps1 - time: 6pm - frequency: once - state: present - enabled: yes - user: DOMAIN\user - password: passwordGoesHere - runlevel: highest + username: DOMAIN\User + password: Password + logon_type: password -- name: Change the above task again, choosing not to store the password for the account +- name: change the above task again, choosing not to store the password win_scheduled_task: name: TaskName2 - description: Run a PowerShell script - executable: C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe - arguments: -ExecutionPolicy Unrestricted -NonInteractive -File C:\TestDir\Test.ps1 - time: 6pm - frequency: once - state: present - enabled: yes - user: DOMAIN\user - runlevel: highest - store_password: no + username: DOMAIN\User + logon_type: s4u + +- name: create task with multiple triggers + win_scheduled_task: + name: TriggerTask + path: \Custom + actions: + - path: cmd.exe + triggers: + - type: daily + - type: monthlydow + username: SYSTEM + +- name: set logon type to password but don't force update the password + win_scheduled_task: + name: TriggerTask + path: \Custom + actions: + - path: cmd.exe + username: Administrator + password: password + update_password: no + +- name: disable a task that already exists + win_scheduled_task: + name: TaskToDisable + enabled: no +''' + +RETURN = r''' ''' diff --git a/test/integration/targets/win_scheduled_task/defaults/main.yml b/test/integration/targets/win_scheduled_task/defaults/main.yml new file mode 100644 index 00000000000..f501c72fc50 --- /dev/null +++ b/test/integration/targets/win_scheduled_task/defaults/main.yml @@ -0,0 +1,5 @@ +--- +test_scheduled_task_name: Ansible Test +test_scheduled_task_path: \Ansible Test Folder +test_scheduled_task_user: MooCow +test_scheduled_task_pass: Password01 diff --git a/test/integration/targets/win_scheduled_task/library/test_task_stat.ps1 b/test/integration/targets/win_scheduled_task/library/test_task_stat.ps1 new file mode 100644 index 00000000000..19b515ef387 --- /dev/null +++ b/test/integration/targets/win_scheduled_task/library/test_task_stat.ps1 @@ -0,0 +1,278 @@ +#!powershell +#Requires -Module Ansible.ModuleUtils.Legacy +#Requires -Module Ansible.ModuleUtils.SID + +$params = Parse-Args -arguments $args +$path = Get-AnsibleParam -obj $params -name "path" -type "str" -failifempty $true +$name = Get-AnsibleParam -obj $params -name "name" -type "str" + +$result = @{ + changed = $false +} + +Add-Type -TypeDefinition @" +public enum TASK_ACTION_TYPE +{ + TASK_ACTION_EXEC = 0, + // The below are not supported and are only kept for documentation purposes + TASK_ACTION_COM_HANDLER = 5, + TASK_ACTION_SEND_EMAIL = 6, + TASK_ACTION_SHOW_MESSAGE = 7 +} + +public enum TASK_LOGON_TYPE +{ + TASK_LOGON_NONE = 0, + TASK_LOGON_PASSWORD = 1, + TASK_LOGON_S4U = 2, + TASK_LOGON_INTERACTIVE_TOKEN = 3, + TASK_LOGON_GROUP = 4, + TASK_LOGON_SERVICE_ACCOUNT = 5, + TASK_LOGON_INTERACTIVE_TOKEN_OR_PASSWORD = 6 +} + +public enum TASK_RUN_LEVEL +{ + TASK_RUNLEVEL_LUA = 0, + TASK_RUNLEVEL_HIGHEST = 1 +} + +public enum TASK_TRIGGER_TYPE2 +{ + TASK_TRIGGER_EVENT = 0, + TASK_TRIGGER_TIME = 1, + TASK_TRIGGER_DAILY = 2, + TASK_TRIGGER_WEEKLY = 3, + TASK_TRIGGER_MONTHLY = 4, + TASK_TRIGGER_MONTHLYDOW = 5, + TASK_TRIGGER_IDLE = 6, + TASK_TRIGGER_REGISTRATION = 7, + TASK_TRIGGER_BOOT = 8, + TASK_TRIGGER_LOGON = 9, + TASK_TRIGGER_SESSION_STATE_CHANGE = 11 +} +"@ + +Function Get-PropertyValue($task_property, $com, $property) { + $raw_value = $com.$property + + if ($raw_value -eq $null) { + return $null + } + + switch ($property) { + DaysOfWeek { + $value_list = @() + $map = @( + @{ day = "sunday"; bitwise = 0x01 } + @{ day = "monday"; bitwise = 0x02 } + @{ day = "tuesday"; bitwise = 0x04 } + @{ day = "wednesday"; bitwise = 0x08 } + @{ day = "thursday"; bitwise = 0x10 } + @{ day = "friday"; bitwise = 0x20 } + @{ day = "saturday"; bitwise = 0x40 } + ) + foreach ($entry in $map) { + $day = $entry.day + $bitwise = $entry.bitwise + if ($raw_value -band $bitwise) { + $value_list += $day + } + } + + $value = $value_list -join "," + break + } + DaysOfMonth { + $value_list = @() + $map = @( + @{ day = "1"; bitwise = 0x01 } + @{ day = "2"; bitwise = 0x02 } + @{ day = "3"; bitwise = 0x04 } + @{ day = "4"; bitwise = 0x08 } + @{ day = "5"; bitwise = 0x10 } + @{ day = "6"; bitwise = 0x20 } + @{ day = "7"; bitwise = 0x40 } + @{ day = "8"; bitwise = 0x80 } + @{ day = "9"; bitwise = 0x100 } + @{ day = "10"; bitwise = 0x200 } + @{ day = "11"; bitwise = 0x400 } + @{ day = "12"; bitwise = 0x800 } + @{ day = "13"; bitwise = 0x1000 } + @{ day = "14"; bitwise = 0x2000 } + @{ day = "15"; bitwise = 0x4000 } + @{ day = "16"; bitwise = 0x8000 } + @{ day = "17"; bitwise = 0x10000 } + @{ day = "18"; bitwise = 0x20000 } + @{ day = "19"; bitwise = 0x40000 } + @{ day = "20"; bitwise = 0x80000 } + @{ day = "21"; bitwise = 0x100000 } + @{ day = "22"; bitwise = 0x200000 } + @{ day = "23"; bitwise = 0x400000 } + @{ day = "24"; bitwise = 0x800000 } + @{ day = "25"; bitwise = 0x1000000 } + @{ day = "26"; bitwise = 0x2000000 } + @{ day = "27"; bitwise = 0x4000000 } + @{ day = "28"; bitwise = 0x8000000 } + @{ day = "29"; bitwise = 0x10000000 } + @{ day = "30"; bitwise = 0x20000000 } + @{ day = "31"; bitwise = 0x40000000 } + ) + + foreach ($entry in $map) { + $day = $entry.day + $bitwise = $entry.bitwise + if ($raw_value -band $bitwise) { + $value_list += $day + } + } + + $value = $value_list -join "," + break + } + WeeksOfMonth { + $value_list = @() + $map = @( + @{ week = "1"; bitwise = 0x01 } + @{ week = "2"; bitwise = 0x02 } + @{ week = "3"; bitwise = 0x04 } + @{ week = "4"; bitwise = 0x04 } + ) + + foreach ($entry in $map) { + $week = $entry.week + $bitwise = $entry.bitwise + if ($raw_value -band $bitwise) { + $value_list += $week + } + } + + $value = $value_list -join "," + break + } + MonthsOfYear { + $value_list = @() + $map = @( + @{ month = "january"; bitwise = 0x01 } + @{ month = "february"; bitwise = 0x02 } + @{ month = "march"; bitwise = 0x04 } + @{ month = "april"; bitwise = 0x08 } + @{ month = "may"; bitwise = 0x10 } + @{ month = "june"; bitwise = 0x20 } + @{ month = "july"; bitwise = 0x40 } + @{ month = "august"; bitwise = 0x80 } + @{ month = "september"; bitwise = 0x100 } + @{ month = "october"; bitwise = 0x200 } + @{ month = "november"; bitwise = 0x400 } + @{ month = "december"; bitwise = 0x800 } + ) + + foreach ($entry in $map) { + $month = $entry.month + $bitwise = $entry.bitwise + if ($raw_value -band $bitwise) { + $value_list += $month + } + } + + $value = $value_list -join "," + break + } + Type { + if ($task_property -eq "actions") { + $value = [Enum]::ToObject([TASK_ACTION_TYPE], $raw_value).ToString() + } elseif ($task_property -eq "triggers") { + $value = [Enum]::ToObject([TASK_TRIGGER_TYPE2], $raw_value).ToString() + } + break + } + RunLevel { + $value = [Enum]::ToObject([TASK_RUN_LEVEL], $raw_value).ToString() + break + } + LogonType { + $value = [Enum]::ToObject([TASK_LOGON_TYPE], $raw_value).ToString() + break + } + UserId { + $sid = Convert-ToSID -account_name $raw_value + $value = Convert-FromSid -sid $sid + } + GroupId { + $sid = Convert-ToSID -account_name $raw_value + $value = Convert-FromSid -sid $sid + } + default { + $value = $raw_value + break + } + } + + return ,$value +} + +$service = New-Object -ComObject Schedule.Service +$service.Connect() + +try { + $task_folder = $service.GetFolder($path) + $result.folder_exists = $true +} catch { + $result.folder_exists = $false + $task_folder = $null +} + +$folder_tasks = $task_folder.GetTasks(1) +$folder_task_names = @() +$folder_task_count = 0 +$task = $null +for ($i = 1; $i -le $folder_tasks.Count; $i++) { + $task_name = $folder_tasks.Item($i).Name + $folder_task_names += $task_name + $folder_task_count += 1 + + if ($name -ne $null -and $task_name -eq $name) { + $task = $folder_tasks.Item($i) + } +} +$result.folder_task_names = $folder_task_names +$result.folder_task_count = $folder_task_count + +if ($name -ne $null) { + if ($task -ne $null) { + $task_definition = $task.Definition + $result.task_exists = $true + $result.task = @{} + + $properties = @("principal", "registration_info", "settings") + $collection_properties = @("actions", "triggers") + + foreach ($property in $properties) { + $property_name = $property -replace "_" + $result.task.$property = @{} + $values = $task_definition.$property_name + Get-Member -InputObject $values -MemberType Property | % { + $result.task.$property.$($_.Name) = (Get-PropertyValue -task_property $property -com $values -property $_.Name) + } + } + + foreach ($property in $collection_properties) { + $result.task.$property = @() + $collection = $task_definition.$property + $collection_count = $collection.Count + for ($i = 1; $i -le $collection_count; $i++) { + $item = $collection.Item($i) + $item_info = @{} + + Get-Member -InputObject $item -MemberType Property | % { + $item_info.$($_.Name) = (Get-PropertyValue -task_property $property -com $item -property $_.Name) + } + $result.task.$property += $item_info + } + } + } else { + $result.task_exists = $false + } +} + +Exit-Json -obj $result diff --git a/test/integration/targets/win_scheduled_task/tasks/clean.yml b/test/integration/targets/win_scheduled_task/tasks/clean.yml new file mode 100644 index 00000000000..635339bf34e --- /dev/null +++ b/test/integration/targets/win_scheduled_task/tasks/clean.yml @@ -0,0 +1,16 @@ +# cleans up each test to ensure a blank slate +--- +- win_scheduled_task: + name: '{{item.name}}' + path: '{{item.path|default(omit)}}' + state: absent + with_items: + - name: Task # old tests + path: \Path + - name: '{{test_scheduled_task_name}}' + - name: '{{test_scheduled_task_name}}' + path: '{{test_scheduled_task_path}}' + +- win_user: + name: '{{test_scheduled_task_user}}' + state: absent diff --git a/test/integration/targets/win_scheduled_task/tasks/failures.yml b/test/integration/targets/win_scheduled_task/tasks/failures.yml new file mode 100644 index 00000000000..2b9e3e2184c --- /dev/null +++ b/test/integration/targets/win_scheduled_task/tasks/failures.yml @@ -0,0 +1,123 @@ +# test out the known failure cases to ensure we have decent error messages +--- +- name: fail create task without an action + win_scheduled_task: + name: '{{test_scheduled_task_name}}' + state: present + register: fail_create_without_action + failed_when: fail_create_without_action.msg != 'cannot create a task with no actions, set at least one action with a path to an executable' + +- name: fail both username and group are set + win_scheduled_task: + name: '{{test_scheduled_task_name}}' + state: present + username: '{{ansible_user}}' + group: '{{ansible_user}}' + register: fail_username_and_group + failed_when: fail_username_and_group.msg != 'username and group can not be set at the same time' + +- name: fail logon type password but no password set + win_scheduled_task: + name: '{{test_scheduled_task_name}}' + state: present + logon_type: password + register: fail_lt_password_not_set + failed_when: fail_lt_password_not_set.msg != 'password must be set when logon_type=password' + +- name: fail logon type s4u but no password set + win_scheduled_task: + name: '{{test_scheduled_task_name}}' + state: present + logon_type: s4u + register: fail_lt_s4u_not_set + failed_when: fail_lt_s4u_not_set.msg != 'password must be set when logon_type=s4u' + +- name: fail logon type group but no group set + win_scheduled_task: + name: '{{test_scheduled_task_name}}' + state: present + logon_type: group + register: fail_lt_group_not_set + failed_when: fail_lt_group_not_set.msg != 'group must be set when logon_type=group' + +- name: fail logon type service but non service user set + win_scheduled_task: + name: '{{test_scheduled_task_name}}' + state: present + logon_type: service_account + username: '{{ansible_user}}' + register: fail_lt_service_invalid_user + failed_when: fail_lt_service_invalid_user.msg != 'username must be SYSTEM, LOCAL SERVICE or NETWORK SERVICE when logon_type=service_account' + +- name: fail trigger with no type + win_scheduled_task: + name: '{{test_scheduled_task_name}}' + state: present + triggers: + - delay: test + register: fail_trigger_no_type + failed_when: fail_trigger_no_type.msg != "a trigger entry must contain a key 'type' with a value of 'event', 'time', 'daily', 'weekly', 'monthly', 'monthlydow', 'idle', 'registration', 'boot', 'logon', 'session_state_change'" + +- name: fail trigger with datetime in incorrect format + win_scheduled_task: + name: '{{test_scheduled_task_name}}' + state: present + triggers: + - type: time + start_boundary: fake + register: fail_trigger_invalid_datetime + failed_when: fail_trigger_invalid_datetime.msg != "trigger option 'start_boundary' must be in the format 'YYYY-MM-DDThh:mm:ss' format but was 'fake'" + +- name: fail trigger with duration in incorrect format + win_scheduled_task: + name: '{{test_scheduled_task_name}}' + state: present + triggers: + - type: boot + execution_time_limit: fake + register: fail_trigger_invalid_duration + failed_when: fail_trigger_invalid_duration.msg != "trigger option 'execution_time_limit' must be in the XML duration format but was 'fake'" + +- name: fail trigger option invalid day of the week + win_scheduled_task: + name: '{{test_scheduled_task_name}}' + state: present + triggers: + - type: weekly + start_boundary: '2000-01-01T00:00:01' + days_of_week: fakeday + register: fail_trigger_invalid_day_of_week + failed_when: fail_trigger_invalid_day_of_week.msg != "invalid day of week 'fakeday', check the spelling matches the full day name" + +- name: fail trigger option invalid day of the month + win_scheduled_task: + name: '{{test_scheduled_task_name}}' + state: present + triggers: + - type: monthly + start_boundary: '2000-01-01T00:00:01' + days_of_month: 35 + register: fail_trigger_invalid_day_of_month + failed_when: fail_trigger_invalid_day_of_month.msg != "invalid day of month '35', please specify numbers from 1-31" + +- name: fail trigger option invalid week of the month + win_scheduled_task: + name: '{{test_scheduled_task_name}}' + state: present + triggers: + - type: monthlydow + start_boundary: '2000-01-01T00:00:01' + weeks_of_month: 5 + register: fail_trigger_invalid_week_of_month + failed_when: fail_trigger_invalid_week_of_month.msg != "invalid week of month '5', please specify weeks from 1-4" + +- name: fail trigger option invalid month of the year + win_scheduled_task: + name: '{{test_scheduled_task_name}}' + state: present + triggers: + - type: monthlydow + start_boundary: '2000-01-01T00:00:01' + months_of_year: fakemonth + register: fail_trigger_invalid_month_of_year + failed_when: fail_trigger_invalid_month_of_year.msg != "invalid month name 'fakemonth', please specify full month name" diff --git a/test/integration/targets/win_scheduled_task/tasks/main.yml b/test/integration/targets/win_scheduled_task/tasks/main.yml index 05efe7c2016..4ff71fa5858 100644 --- a/test/integration/targets/win_scheduled_task/tasks/main.yml +++ b/test/integration/targets/win_scheduled_task/tasks/main.yml @@ -1,21 +1,38 @@ -# NOTE: The win_scheduled_task module only works on Win2012+ - -- name: Test Windows capabilities - raw: Get-Command New-ScheduledTask -ErrorAction SilentlyContinue; return $? - failed_when: no - register: new_scheduledtask - -- name: Only run tests when Windows is capable - when: new_scheduledtask.rc == 0 - block: +--- +- name: remove test tasks before test + include_tasks: clean.yml +- block: + # old tests, remove once new code is considered stable - name: Test in normal mode - include: tests.yml + include_tasks: tests.yml vars: in_check_mode: no - name: Test in check-mode - include: tests.yml + include_tasks: tests.yml vars: in_check_mode: yes check_mode: yes + + - include_tasks: clean.yml + + - name: Test failure scenarios + include: failures.yml + + - name: Test normal scenarios + include_tasks: new_tests.yml + + - include_tasks: clean.yml + + - name: Test principals + include_tasks: principals.yml + + - include_tasks: clean.yml + + - name: Test triggers + include_tasks: triggers.yml + + always: + - name: remove test tasks after test + include_tasks: clean.yml diff --git a/test/integration/targets/win_scheduled_task/tasks/new_tests.yml b/test/integration/targets/win_scheduled_task/tasks/new_tests.yml new file mode 100644 index 00000000000..9832d7d8166 --- /dev/null +++ b/test/integration/targets/win_scheduled_task/tasks/new_tests.yml @@ -0,0 +1,440 @@ +--- +- name: create task (check mode) + win_scheduled_task: + name: '{{test_scheduled_task_name}}' + state: present + actions: + - path: cmd.exe + arguments: /c echo hi + description: Original Description + register: create_task_check + check_mode: yes + +- name: get result of create task (check mode) + test_task_stat: + path: \ + name: '{{test_scheduled_task_name}}' + register: create_task_result_check + +- name: assert results of create task (check mode) + assert: + that: + - create_task_check|changed + - create_task_result_check.task_exists == False + +- name: create task + win_scheduled_task: + name: '{{test_scheduled_task_name}}' + state: present + actions: + - path: cmd.exe + arguments: /c echo hi + description: Original Description + register: create_task + +- name: get result of create task + test_task_stat: + path: \ + name: '{{test_scheduled_task_name}}' + register: create_task_result + +- name: assert results of create task + assert: + that: + - create_task|changed + - create_task_result.task_exists == True + - create_task_result.task.actions|count == 1 + - create_task_result.task.actions[0].Path == "cmd.exe" + - create_task_result.task.actions[0].Arguments == "/c echo hi" + - create_task_result.task.actions[0].WorkingDirectory == None + - create_task_result.task.registration_info.Description == "Original Description" + - create_task_result.task.triggers|count == 0 + +- name: create task (idempotent) + win_scheduled_task: + name: '{{test_scheduled_task_name}}' + state: present + actions: + - path: cmd.exe + arguments: /c echo hi + description: Original Description + register: create_task_again + +- name: assert results of create task (idempotent) + assert: + that: + - not create_task_again|changed + +- name: change task (check mode) + win_scheduled_task: + name: '{{test_scheduled_task_name}}' + state: present + author: Cow Inc. + description: Test for Ansible + allow_demand_start: no + restart_count: 5 + restart_interval: PT2H5M + register: change_task_check + check_mode: yes + +- name: get result of change task (check mode) + test_task_stat: + path: \ + name: '{{test_scheduled_task_name}}' + register: change_task_result_check + +- name: assert results of change task (check mode) + assert: + that: + - change_task_check|changed + - change_task_result_check.task.actions|count == 1 + - change_task_result_check.task.registration_info.Author == None + - change_task_result_check.task.registration_info.Description == "Original Description" + - change_task_result_check.task.settings.AllowDemandStart == true + - change_task_result_check.task.settings.RestartCount == 0 + - change_task_result_check.task.settings.RestartInterval == None + +- name: change task + win_scheduled_task: + name: '{{test_scheduled_task_name}}' + state: present + author: Cow Inc. + description: Test for Ansible + allow_demand_start: no + restart_count: 5 + restart_interval: PT1M + register: change_task + +- name: get result of change task + test_task_stat: + path: \ + name: '{{test_scheduled_task_name}}' + register: change_task_result + +- name: assert results of change task + assert: + that: + - change_task|changed + - change_task_result.task.actions|count == 1 + - change_task_result.task.registration_info.Author == "Cow Inc." + - change_task_result.task.registration_info.Description == "Test for Ansible" + - change_task_result.task.settings.AllowDemandStart == false + - change_task_result.task.settings.RestartCount == 5 + - change_task_result.task.settings.RestartInterval == "PT1M" + +- name: change task (idempotent) + win_scheduled_task: + name: '{{test_scheduled_task_name}}' + state: present + author: Cow Inc. + description: Test for Ansible + allow_demand_start: no + restart_count: 5 + restart_interval: PT1M + register: change_task_again + +- name: assert results of change task (idempotent) + assert: + that: + - not change_task_again|changed + +- name: add task action (check mode) + win_scheduled_task: + name: '{{test_scheduled_task_name}}' + state: present + actions: + - path: cmd.exe + arguments: /c echo hi + - path: powershell.exe + arguments: -File C:\ansible\script.ps1 + working_directory: C:\ansible + register: add_task_action_check + check_mode: yes + +- name: get result of add task action (check mode) + test_task_stat: + path: \ + name: '{{test_scheduled_task_name}}' + register: add_task_action_result_check + +- name: assert results of add task action (check mode) + assert: + that: + - add_task_action_check|changed + - add_task_action_result_check.task.actions|count == 1 + - add_task_action_result_check.task.actions[0].Path == "cmd.exe" + - add_task_action_result_check.task.actions[0].Arguments == "/c echo hi" + - add_task_action_result_check.task.actions[0].WorkingDirectory == None + +- name: add task action + win_scheduled_task: + name: '{{test_scheduled_task_name}}' + state: present + actions: + - path: cmd.exe + arguments: /c echo hi + - path: powershell.exe + arguments: -File C:\ansible\script.ps1 + working_directory: C:\ansible + register: add_task_action + +- name: get result of add task action + test_task_stat: + path: \ + name: '{{test_scheduled_task_name}}' + register: add_task_action_result + +- name: assert results of add task action + assert: + that: + - add_task_action|changed + - add_task_action_result.task.actions|count == 2 + - add_task_action_result.task.actions[0].Path == "cmd.exe" + - add_task_action_result.task.actions[0].Arguments == "/c echo hi" + - add_task_action_result.task.actions[0].WorkingDirectory == None + - add_task_action_result.task.actions[1].Path == "powershell.exe" + - add_task_action_result.task.actions[1].Arguments == "-File C:\\ansible\\script.ps1" + - add_task_action_result.task.actions[1].WorkingDirectory == "C:\\ansible" + +- name: add task action (idempotent) + win_scheduled_task: + name: '{{test_scheduled_task_name}}' + state: present + actions: + - path: cmd.exe + arguments: /c echo hi + - path: powershell.exe + arguments: -File C:\ansible\script.ps1 + working_directory: C:\ansible + register: add_task_action_again + +- name: assert results of add task action (idempotent) + assert: + that: + - not add_task_action_again|changed + +- name: remove task action (check mode) + win_scheduled_task: + name: '{{test_scheduled_task_name}}' + state: present + actions: + - path: powershell.exe + arguments: -File C:\ansible\script.ps1 + working_directory: C:\ansible + register: remove_task_action_check + check_mode: yes + +- name: get result of remove task action (check mode) + test_task_stat: + path: \ + name: '{{test_scheduled_task_name}}' + register: remove_task_action_result_check + +- name: assert results of remove task action (check mode) + assert: + that: + - remove_task_action_check|changed + - remove_task_action_result_check.task.actions|count == 2 + - remove_task_action_result_check.task.actions[0].Path == "cmd.exe" + - remove_task_action_result_check.task.actions[0].Arguments == "/c echo hi" + - remove_task_action_result_check.task.actions[0].WorkingDirectory == None + - remove_task_action_result_check.task.actions[1].Path == "powershell.exe" + - remove_task_action_result_check.task.actions[1].Arguments == "-File C:\\ansible\\script.ps1" + - remove_task_action_result_check.task.actions[1].WorkingDirectory == "C:\\ansible" + +- name: remove task action + win_scheduled_task: + name: '{{test_scheduled_task_name}}' + state: present + actions: + - path: powershell.exe + arguments: -File C:\ansible\script.ps1 + working_directory: C:\ansible + register: remove_task_action + +- name: get result of remove task action + test_task_stat: + path: \ + name: '{{test_scheduled_task_name}}' + register: remove_task_action_result + +- name: assert results of remove task action + assert: + that: + - remove_task_action|changed + - remove_task_action_result.task.actions|count == 1 + - remove_task_action_result.task.actions[0].Path == "powershell.exe" + - remove_task_action_result.task.actions[0].Arguments == "-File C:\\ansible\\script.ps1" + - remove_task_action_result.task.actions[0].WorkingDirectory == "C:\\ansible" + +- name: remove task action (idempontent) + win_scheduled_task: + name: '{{test_scheduled_task_name}}' + state: present + actions: + - path: powershell.exe + arguments: -File C:\ansible\script.ps1 + working_directory: C:\ansible + register: remove_task_action_again + +- name: assert results of remove task action (idempotent) + assert: + that: + - not remove_task_action_again|changed + +- name: remove task (check mode) + win_scheduled_task: + name: '{{test_scheduled_task_name}}' + state: absent + register: remove_task_check + check_mode: yes + +- name: get result of remove task (check mode) + test_task_stat: + path: \ + name: '{{test_scheduled_task_name}}' + register: remove_task_result_check + +- name: assert results of remove task (check mode) + assert: + that: + - remove_task_check|changed + - remove_task_result_check.task_exists == True + +- name: remove task + win_scheduled_task: + name: '{{test_scheduled_task_name}}' + state: absent + register: remove_task + +- name: get result of remove task + test_task_stat: + path: \ + name: '{{test_scheduled_task_name}}' + register: remove_task_result + +- name: assert results of remove task + assert: + that: + - remove_task|changed + - remove_task_result.task_exists == False + +- name: remove task (idempotent) + win_scheduled_task: + name: '{{test_scheduled_task_name}}' + state: absent + register: remove_task_again + +- name: assert results of remove task (idempotent) + assert: + that: + - not remove_task_again|changed + +- name: create sole task in folder (check mode) + win_scheduled_task: + name: '{{test_scheduled_task_name}}' + path: '{{test_scheduled_task_path}}' + actions: + - path: cmd.exe + register: create_sole_task_check + check_mode: yes + +- name: get result of create sole task in folder (check mode) + test_task_stat: + path: '{{test_scheduled_task_path}}' + name: '{{test_scheduled_task_name}}' + register: create_sole_task_result_check + +- name: assert results of create sole task in folder (check mode) + assert: + that: + - create_sole_task_check|changed + - create_sole_task_result_check.folder_exists == False + - create_sole_task_result_check.task_exists == False + +- name: create sole task in folder + win_scheduled_task: + name: '{{test_scheduled_task_name}}' + path: '{{test_scheduled_task_path}}' + actions: + - path: cmd.exe + register: create_sole_task + +- name: get result of create sole task in folder + test_task_stat: + path: '{{test_scheduled_task_path}}' + name: '{{test_scheduled_task_name}}' + register: create_sole_task_result + +- name: assert results of create sole task in folder + assert: + that: + - create_sole_task|changed + - create_sole_task_result.folder_exists == True + - create_sole_task_result.task_exists == True + +- name: create sole task in folder (idempotent) + win_scheduled_task: + name: '{{test_scheduled_task_name}}' + path: '{{test_scheduled_task_path}}' + actions: + - path: cmd.exe + register: create_sole_task_again + +- name: assert results of create sole task in folder (idempotent) + assert: + that: + - not create_sole_task_again|changed + +- name: remove sole task in folder (check mode) + win_scheduled_task: + name: '{{test_scheduled_task_name}}' + path: '{{test_scheduled_task_path}}' + state: absent + register: remove_sole_task_check + check_mode: yes + +- name: get result of remove sole task in folder (check mode) + test_task_stat: + path: '{{test_scheduled_task_path}}' + name: '{{test_scheduled_task_name}}' + register: remove_sole_task_result_check + +- name: assert results of remove sole task in folder (check mode) + assert: + that: + - remove_sole_task_check|changed + - remove_sole_task_result_check.folder_exists == True + - remove_sole_task_result_check.task_exists == True + +- name: remove sole task in folder + win_scheduled_task: + name: '{{test_scheduled_task_name}}' + path: '{{test_scheduled_task_path}}' + state: absent + register: remove_sole_task + +- name: get result of remove sole task in folder + test_task_stat: + path: '{{test_scheduled_task_path}}' + name: '{{test_scheduled_task_name}}' + register: remove_sole_task_result + +- name: assert results of remove sole task in folder + assert: + that: + - remove_sole_task|changed + - remove_sole_task_result.folder_exists == False + - remove_sole_task_result.task_exists == False + +- name: remove sole task in folder (idempotent) + win_scheduled_task: + name: '{{test_scheduled_task_name}}' + path: '{{test_scheduled_task_path}}' + state: absent + register: remove_sole_task_again + +- name: assert results of remove sole task in folder (idempotent) + assert: + that: + - not remove_sole_task_again|changed diff --git a/test/integration/targets/win_scheduled_task/tasks/principals.yml b/test/integration/targets/win_scheduled_task/tasks/principals.yml new file mode 100644 index 00000000000..2216eb9aa83 --- /dev/null +++ b/test/integration/targets/win_scheduled_task/tasks/principals.yml @@ -0,0 +1,436 @@ +--- +- name: create test user + win_user: + name: '{{test_scheduled_task_user}}' + password: '{{test_scheduled_task_pass}}' + state: present + groups: + - Administrators + +- name: task with password principal (check mode) + win_scheduled_task: + name: '{{test_scheduled_task_name}}' + state: present + username: '{{test_scheduled_task_user}}' + password: '{{test_scheduled_task_pass}}' + logon_type: password + update_password: no + actions: + - path: cmd.exe + register: task_with_password_check + check_mode: yes + +- name: get result of task with password principal (check mode) + test_task_stat: + path: \ + name: '{{test_scheduled_task_name}}' + register: task_with_password_result_check + +- name: assert results of task with password principal (check mode) + assert: + that: + - task_with_password_check|changed + - task_with_password_result_check.task_exists == False + +- name: task with password principal + win_scheduled_task: + name: '{{test_scheduled_task_name}}' + state: present + username: '{{test_scheduled_task_user}}' + password: '{{test_scheduled_task_pass}}' + logon_type: password + update_password: no + actions: + - path: cmd.exe + register: task_with_password + +- name: get result of task with password principal + test_task_stat: + path: \ + name: '{{test_scheduled_task_name}}' + register: task_with_password_result + +- name: assert results of task with password principal + assert: + that: + - task_with_password|changed + - task_with_password_result.task_exists == True + - task_with_password_result.task.principal.GroupId == None + - task_with_password_result.task.principal.LogonType == "TASK_LOGON_PASSWORD" + - task_with_password_result.task.principal.RunLevel == "TASK_RUNLEVEL_LUA" + - task_with_password_result.task.principal.UserId.endswith(test_scheduled_task_user) + +- name: task with password principal (idempotent) + win_scheduled_task: + name: '{{test_scheduled_task_name}}' + state: present + username: '{{test_scheduled_task_user}}' + password: '{{test_scheduled_task_pass}}' + logon_type: password + update_password: no + actions: + - path: cmd.exe + register: task_with_password_again + +- name: assert results of task with password principal (idempotent) + assert: + that: + - not task_with_password_again|changed + +- name: task with password principal force pass change + win_scheduled_task: + name: '{{test_scheduled_task_name}}' + state: present + username: '{{test_scheduled_task_user}}' + password: '{{test_scheduled_task_pass}}' + logon_type: password + update_password: yes + actions: + - path: cmd.exe + register: task_with_password_force_update + +- name: assert results of task with password principal force pass change + assert: + that: + - task_with_password_force_update|changed + +- name: task with s4u principal (check mode) + win_scheduled_task: + name: '{{test_scheduled_task_name}}' + state: present + username: '{{test_scheduled_task_user}}' + password: '{{test_scheduled_task_pass}}' + logon_type: s4u + update_password: no + actions: + - path: cmd.exe + register: task_with_s4u_check + check_mode: yes + +- name: get result of task with s4u principal (check mode) + test_task_stat: + path: \ + name: '{{test_scheduled_task_name}}' + register: task_with_s4u_result_check + +- name: assert results of task with s4u principal (check mode) + assert: + that: + - task_with_s4u_check|changed + - task_with_s4u_result_check.task_exists == True + - task_with_s4u_result_check.task.principal.GroupId == None + - task_with_s4u_result_check.task.principal.LogonType == "TASK_LOGON_PASSWORD" + - task_with_s4u_result_check.task.principal.RunLevel == "TASK_RUNLEVEL_LUA" + - task_with_s4u_result_check.task.principal.UserId.endswith(test_scheduled_task_user) + +- name: task with s4u principal + win_scheduled_task: + name: '{{test_scheduled_task_name}}' + state: present + username: '{{test_scheduled_task_user}}' + password: '{{test_scheduled_task_pass}}' + logon_type: s4u + update_password: no + actions: + - path: cmd.exe + register: task_with_s4u + +- name: get result of task with s4u principal + test_task_stat: + path: \ + name: '{{test_scheduled_task_name}}' + register: task_with_s4u_result + +- name: assert results of task with s4u principal + assert: + that: + - task_with_s4u|changed + - task_with_s4u_result.task_exists == True + - task_with_s4u_result.task.principal.GroupId == None + - task_with_s4u_result.task.principal.LogonType == "TASK_LOGON_S4U" + - task_with_s4u_result.task.principal.RunLevel == "TASK_RUNLEVEL_LUA" + - task_with_s4u_result.task.principal.UserId.endswith(test_scheduled_task_user) + +- name: task with s4u principal (idempotent) + win_scheduled_task: + name: '{{test_scheduled_task_name}}' + state: present + username: '{{test_scheduled_task_user}}' + password: '{{test_scheduled_task_pass}}' + logon_type: s4u + update_password: no + actions: + - path: cmd.exe + register: task_with_s4u_again + +- name: assert results of task with s4u principal (idempotent) + assert: + that: + - not task_with_s4u_again|changed + +- name: task with interactive principal (check mode) + win_scheduled_task: + name: '{{test_scheduled_task_name}}' + state: present + username: '{{test_scheduled_task_user}}' + logon_type: interactive_token + actions: + - path: cmd.exe + register: task_with_interactive_check + check_mode: yes + +- name: get result of task with interactive principal (check mode) + test_task_stat: + path: \ + name: '{{test_scheduled_task_name}}' + register: task_with_interactive_result_check + +- name: assert results of task with interactive principal (check mode) + assert: + that: + - task_with_interactive_check|changed + - task_with_interactive_result_check.task_exists == True + - task_with_interactive_result_check.task.principal.GroupId == None + - task_with_interactive_result_check.task.principal.LogonType == "TASK_LOGON_S4U" + - task_with_interactive_result_check.task.principal.RunLevel == "TASK_RUNLEVEL_LUA" + - task_with_interactive_result_check.task.principal.UserId.endswith(test_scheduled_task_user) + +- name: task with interactive principal + win_scheduled_task: + name: '{{test_scheduled_task_name}}' + state: present + username: '{{test_scheduled_task_user}}' + logon_type: interactive_token + actions: + - path: cmd.exe + register: task_with_interactive + +- name: get result of task with interactive principal + test_task_stat: + path: \ + name: '{{test_scheduled_task_name}}' + register: task_with_interactive_result + +- name: assert results of task with interactive principal + assert: + that: + - task_with_interactive|changed + - task_with_interactive_result.task_exists == True + - task_with_interactive_result.task.principal.GroupId == None + - task_with_interactive_result.task.principal.LogonType == "TASK_LOGON_INTERACTIVE_TOKEN" + - task_with_interactive_result.task.principal.RunLevel == "TASK_RUNLEVEL_LUA" + - task_with_interactive_result.task.principal.UserId.endswith(test_scheduled_task_user) + +- name: task with interactive principal (idempotent) + win_scheduled_task: + name: '{{test_scheduled_task_name}}' + state: present + username: '{{test_scheduled_task_user}}' + logon_type: interactive_token + actions: + - path: cmd.exe + register: task_with_interactive_again + +- name: assert results of task with interactive principal (idempotent) + assert: + that: + - not task_with_interactive_again|changed + +- name: task with group principal (check mode) + win_scheduled_task: + name: '{{test_scheduled_task_name}}' + state: present + group: Administrators + logon_type: group + actions: + - path: cmd.exe + register: task_with_group_check + check_mode: yes + +- name: get result of task with group principal (check mode) + test_task_stat: + path: \ + name: '{{test_scheduled_task_name}}' + register: task_with_group_result_check + +- name: assert results of task with group principal (check mode) + assert: + that: + - task_with_group_check|changed + - task_with_group_result_check.task_exists == True + - task_with_group_result_check.task.principal.GroupId == None + - task_with_group_result_check.task.principal.LogonType == "TASK_LOGON_INTERACTIVE_TOKEN" + - task_with_group_result_check.task.principal.RunLevel == "TASK_RUNLEVEL_LUA" + - task_with_group_result_check.task.principal.UserId.endswith(test_scheduled_task_user) + +- name: task with group principal + win_scheduled_task: + name: '{{test_scheduled_task_name}}' + state: present + group: Administrators + logon_type: group + actions: + - path: cmd.exe + register: task_with_group + +- name: get result of task with group principal + test_task_stat: + path: \ + name: '{{test_scheduled_task_name}}' + register: task_with_group_result + +- name: assert results of task with group principal + assert: + that: + - task_with_group|changed + - task_with_group_result.task_exists == True + - task_with_group_result.task.principal.GroupId == "BUILTIN\\Administrators" + - task_with_group_result.task.principal.LogonType == "TASK_LOGON_GROUP" + - task_with_group_result.task.principal.RunLevel == "TASK_RUNLEVEL_LUA" + - task_with_group_result.task.principal.UserId == None + +- name: task with group principal (idempotent) + win_scheduled_task: + name: '{{test_scheduled_task_name}}' + state: present + group: Administrators + logon_type: group + actions: + - path: cmd.exe + register: task_with_group_again + +- name: assert results of task with group principal (idempotent) + assert: + that: + - not task_with_group_again|changed + +- name: task with service account principal (check mode) + win_scheduled_task: + name: '{{test_scheduled_task_name}}' + state: present + username: System + logon_type: service_account + action: + - path: cmd.exe + register: task_with_service_check + check_mode: yes + +- name: get result of task with service account principal (check mode) + test_task_stat: + path: \ + name: '{{test_scheduled_task_name}}' + register: task_with_service_result_check + +- name: assert results of task with service account principal (check mode) + assert: + that: + - task_with_service_check|changed + - task_with_service_result_check.task_exists == True + - task_with_service_result_check.task.principal.GroupId == "BUILTIN\\Administrators" + - task_with_service_result_check.task.principal.LogonType == "TASK_LOGON_GROUP" + - task_with_service_result_check.task.principal.RunLevel == "TASK_RUNLEVEL_LUA" + - task_with_service_result_check.task.principal.UserId == None + +- name: task with service account principal + win_scheduled_task: + name: '{{test_scheduled_task_name}}' + state: present + username: System + logon_type: service_account + action: + - path: cmd.exe + register: task_with_service + +- name: get result of task with service account principal + test_task_stat: + path: \ + name: '{{test_scheduled_task_name}}' + register: task_with_service_result + +- name: assert results of task with service account principal + assert: + that: + - task_with_service|changed + - task_with_service_result.task_exists == True + - task_with_service_result.task.principal.GroupId == None + - task_with_service_result.task.principal.LogonType == "TASK_LOGON_SERVICE_ACCOUNT" + - task_with_service_result.task.principal.RunLevel == "TASK_RUNLEVEL_LUA" + - task_with_service_result.task.principal.UserId == "NT AUTHORITY\\SYSTEM" + +- name: task with service account principal (idempotent) + win_scheduled_task: + name: '{{test_scheduled_task_name}}' + state: present + username: System + logon_type: service_account + action: + - path: cmd.exe + register: task_with_service_again + +- name: assert results of task with service account principal (idempotent) + assert: + that: + - not task_with_service_again|changed + +- name: task with highest privilege (check mode) + win_scheduled_task: + name: '{{test_scheduled_task_name}}' + state: present + run_level: highest + username: System + logon_type: service_account + action: + - path: cmd.exe + register: task_with_highest_privilege_check + check_mode: yes + +- name: get result of task with highest privilege (check mode) + test_task_stat: + path: \ + name: '{{test_scheduled_task_name}}' + register: task_with_highest_privilege_result_check + +- name: assert results of task with highest privilege (check mode) + assert: + that: + - task_with_highest_privilege_check|changed + - task_with_highest_privilege_result_check.task.principal.RunLevel == "TASK_RUNLEVEL_LUA" + +- name: task with highest privilege + win_scheduled_task: + name: '{{test_scheduled_task_name}}' + state: present + run_level: highest + username: System + logon_type: service_account + action: + - path: cmd.exe + register: task_with_highest_privilege + +- name: get result of task with highest privilege + test_task_stat: + path: \ + name: '{{test_scheduled_task_name}}' + register: task_with_highest_privilege_result + +- name: assert results of task with highest privilege + assert: + that: + - task_with_highest_privilege|changed + - task_with_highest_privilege_result.task.principal.RunLevel == "TASK_RUNLEVEL_HIGHEST" + +- name: task with highest privilege (idempotent) + win_scheduled_task: + name: '{{test_scheduled_task_name}}' + state: present + run_level: highest + username: System + logon_type: service_account + action: + - path: cmd.exe + register: task_with_highest_privilege_again + +- name: assert results of task with highest privilege (idempotent) + assert: + that: + - not task_with_highest_privilege_again|changed diff --git a/test/integration/targets/win_scheduled_task/tasks/tests.yml b/test/integration/targets/win_scheduled_task/tasks/tests.yml index db62136b101..3641e9b64b1 100644 --- a/test/integration/targets/win_scheduled_task/tasks/tests.yml +++ b/test/integration/targets/win_scheduled_task/tasks/tests.yml @@ -1,3 +1,6 @@ +# these are the older tests that test out the deprecated args, keep here until +# the new changes are more bedded down +--- - name: Remove potentially leftover scheduled task win_scheduled_task: &wst_absent name: Ansible Test @@ -18,7 +21,6 @@ assert: that: - add_scheduled_task.changed == true - - add_scheduled_task.exists == false - name: Add scheduled task (again) @@ -29,14 +31,12 @@ assert: that: - add_scheduled_task_again.changed == false - - add_scheduled_task_again.exists == true when: not in_check_mode - name: Test add_scheduled_task_again (check-mode) assert: that: - add_scheduled_task_again.changed == true - - add_scheduled_task_again.exists == false when: in_check_mode @@ -56,7 +56,6 @@ assert: that: - disable_scheduled_task.changed == true - - disable_scheduled_task.exists == true - name: Disable scheduled task (again) @@ -69,7 +68,6 @@ assert: that: - disable_scheduled_task_again.changed == false - - disable_scheduled_task_again.exists == true - name: Enable scheduled task @@ -81,7 +79,6 @@ - assert: that: - enable_scheduled_task.changed == true - - enable_scheduled_task.exists == true - name: Enable scheduled task (again) win_scheduled_task: @@ -92,7 +89,6 @@ - assert: that: - enable_scheduled_task_again.changed == false - - enable_scheduled_task_again.exists == true - name: Remove scheduled task @@ -103,14 +99,12 @@ assert: that: - remove_scheduled_task.changed == true - - remove_scheduled_task.exists == true when: not in_check_mode - name: Test remove_scheduled_task (check-mode) assert: that: - remove_scheduled_task.changed == false - - remove_scheduled_task.exists == false when: in_check_mode @@ -122,7 +116,6 @@ assert: that: - remove_scheduled_task_again.changed == false - - remove_scheduled_task_again.exists == false # Test scheduled task path creation and removal @@ -157,7 +150,7 @@ - name: Test add_scheduled_task_new_path_1 assert: that: - - add_scheduled_task_new_path_1.msg == 'Added new task Ansible Test New Path 1 and task path \\non_existent_path\\ created' + - add_scheduled_task_new_path_1|changed - name: Add scheduled task new path 2 @@ -169,13 +162,13 @@ - name: Test add_scheduled_task_new_path_2 (normal mode) assert: that: - - add_scheduled_task_new_path_2.msg == 'Added new task Ansible Test New Path 2' + - add_scheduled_task_new_path_2|changed when: not in_check_mode - name: Test add_scheduled_task_new_path_2 (check-mode) assert: that: - - add_scheduled_task_new_path_2.msg == 'Added new task Ansible Test New Path 2 and task path \\non_existent_path\\ created' + - add_scheduled_task_new_path_2|changed when: in_check_mode @@ -186,13 +179,13 @@ - name: Test remove_scheduled_task_new_path_2 (normal mode) assert: that: - - remove_scheduled_task_new_path_2.msg == 'Deleted task Ansible Test New Path 2' + - remove_scheduled_task_new_path_2|changed when: not in_check_mode - name: Test remove_scheduled_task_new_path_2 (check-mode) assert: that: - - remove_scheduled_task_new_path_2.msg == 'Task does not exist' + - not remove_scheduled_task_new_path_2|changed when: in_check_mode @@ -203,13 +196,13 @@ - name: Test remove_scheduled_task_new_path_1 (normal mode) assert: that: - - remove_scheduled_task_new_path_1.msg == 'Deleted task Ansible Test New Path 1 and task path \\non_existent_path\\ removed' + - remove_scheduled_task_new_path_1|changed when: not in_check_mode - name: Test remove_scheduled_task_new_path_1 (check-mode) assert: that: - - remove_scheduled_task_new_path_1.msg == 'Task does not exist' + - not remove_scheduled_task_new_path_1|changed when: in_check_mode @@ -238,7 +231,6 @@ assert: that: - add_scheduled_task_run_options_1.changed == true - - add_scheduled_task_run_options_1.exists == false - name: Execute run options tests for normal mode only (expects scheduled task) @@ -255,7 +247,6 @@ assert: that: - change_scheduled_task_run_options_user.changed == true - - change_scheduled_task_run_options_user.exists == true - name: Change scheduled task run options user (again) @@ -268,7 +259,6 @@ assert: that: - change_scheduled_task_run_options_user_again.changed == false - - change_scheduled_task_run_options_user_again.exists == true - name: Change scheduled task run options run level @@ -282,7 +272,6 @@ assert: that: - change_scheduled_task_run_options_runlevel.changed == true - - change_scheduled_task_run_options_runlevel.exists == true - name: Change scheduled task run options run level (again) @@ -296,7 +285,6 @@ assert: that: - change_scheduled_task_run_options_runlevel_again.changed == false - - change_scheduled_task_run_options_runlevel_again.exists == true # Should ignore change as account being tested is a built-in service account @@ -312,7 +300,6 @@ assert: that: - change_scheduled_task_run_options_store_password.changed == false - - change_scheduled_task_run_options_store_password.exists == true - name: Remove scheduled task run options 1 @@ -323,12 +310,10 @@ assert: that: - remove_scheduled_task_run_options_1.changed == true - - remove_scheduled_task_run_options_1.exists == true when: not in_check_mode - name: Test remove_scheduled_task_run_options_1 (check-mode) assert: that: - remove_scheduled_task_run_options_1.changed == false - - remove_scheduled_task_run_options_1.exists == false when: in_check_mode diff --git a/test/integration/targets/win_scheduled_task/tasks/triggers.yml b/test/integration/targets/win_scheduled_task/tasks/triggers.yml new file mode 100644 index 00000000000..2a840dab76d --- /dev/null +++ b/test/integration/targets/win_scheduled_task/tasks/triggers.yml @@ -0,0 +1,635 @@ +--- +- name: create boot trigger (check mode) + win_scheduled_task: + name: '{{test_scheduled_task_name}}' + state: present + actions: + - path: cmd.exe + triggers: + - type: boot + register: trigger_boot_check + check_mode: yes + +- name: get result of create boot trigger (check mode) + test_task_stat: + path: \ + name: '{{test_scheduled_task_name}}' + register: trigger_boot_result_check + +- name: assert results of create boot trigger (check mode) + assert: + that: + - trigger_boot_check|changed + - trigger_boot_result_check.task_exists == False + +- name: create boot trigger + win_scheduled_task: + name: '{{test_scheduled_task_name}}' + state: present + actions: + - path: cmd.exe + triggers: + - type: boot + register: trigger_boot + +- name: get result of create boot trigger + test_task_stat: + path: \ + name: '{{test_scheduled_task_name}}' + register: trigger_boot_result + +- name: assert results of create boot trigger + assert: + that: + - trigger_boot|changed + - trigger_boot_result.task_exists == True + - trigger_boot_result.task.triggers|count == 1 + - trigger_boot_result.task.triggers[0].Type == "TASK_TRIGGER_BOOT" + - trigger_boot_result.task.triggers[0].Enabled == True + - trigger_boot_result.task.triggers[0].StartBoundary == None + - trigger_boot_result.task.triggers[0].EndBoundary == None + +- name: create boot trigger (idempotent) + win_scheduled_task: + name: '{{test_scheduled_task_name}}' + state: present + actions: + - path: cmd.exe + triggers: + - type: boot + register: trigger_boot_again + +- name: assert results of create boot trigger (idempotent) + assert: + that: + - not trigger_boot_again|changed + +- name: create daily trigger (check mode) + win_scheduled_task: + name: '{{test_scheduled_task_name}}' + state: present + actions: + - path: cmd.exe + triggers: + - type: daily + start_boundary: '2000-01-01T00:00:01' + register: trigger_daily_check + check_mode: yes + +- name: get result of create daily trigger (check mode) + test_task_stat: + path: \ + name: '{{test_scheduled_task_name}}' + register: trigger_daily_result_check + +- name: assert results of create daily trigger (check mode) + assert: + that: + - trigger_daily_check|changed + - trigger_daily_result_check.task_exists == True + - trigger_daily_result_check.task.triggers|count == 1 + - trigger_daily_result_check.task.triggers[0].Type == "TASK_TRIGGER_BOOT" + - trigger_daily_result_check.task.triggers[0].Enabled == True + - trigger_daily_result_check.task.triggers[0].StartBoundary == None + - trigger_daily_result_check.task.triggers[0].EndBoundary == None + +- name: create daily trigger + win_scheduled_task: + name: '{{test_scheduled_task_name}}' + state: present + actions: + - path: cmd.exe + triggers: + - type: daily + start_boundary: '2000-01-01T00:00:01' + register: trigger_daily + +- name: get result of create daily trigger + test_task_stat: + path: \ + name: '{{test_scheduled_task_name}}' + register: trigger_daily_result + +- name: assert results of create daily trigger + assert: + that: + - trigger_daily|changed + - trigger_daily_result.task_exists == True + - trigger_daily_result.task.triggers|count == 1 + - trigger_daily_result.task.triggers[0].Type == "TASK_TRIGGER_DAILY" + - trigger_daily_result.task.triggers[0].Enabled == True + - trigger_daily_result.task.triggers[0].StartBoundary == "2000-01-01T00:00:01" + - trigger_daily_result.task.triggers[0].EndBoundary == None + +- name: create daily trigger (idempotent) + win_scheduled_task: + name: '{{test_scheduled_task_name}}' + state: present + actions: + - path: cmd.exe + triggers: + - type: daily + start_boundary: '2000-01-01T00:00:01' + register: trigger_daily_again + +- name: assert results of create daily trigger (idempotent) + assert: + that: + - not trigger_daily_again|changed + +- name: create logon trigger (check mode) + win_scheduled_task: + name: '{{test_scheduled_task_name}}' + state: present + actions: + - path: cmd.exe + triggers: + - type: logon + register: trigger_logon_check + check_mode: yes + +- name: get result of create logon trigger (check mode) + test_task_stat: + path: \ + name: '{{test_scheduled_task_name}}' + register: trigger_logon_result_check + +- name: assert results of create logon trigger + assert: + that: + - trigger_logon_check|changed + - trigger_logon_result_check.task_exists == True + - trigger_logon_result_check.task.triggers|count == 1 + - trigger_logon_result_check.task.triggers[0].Type == "TASK_TRIGGER_DAILY" + - trigger_logon_result_check.task.triggers[0].Enabled == True + - trigger_logon_result_check.task.triggers[0].StartBoundary == "2000-01-01T00:00:01" + - trigger_logon_result_check.task.triggers[0].EndBoundary == None + +- name: create logon trigger + win_scheduled_task: + name: '{{test_scheduled_task_name}}' + state: present + actions: + - path: cmd.exe + triggers: + - type: logon + register: trigger_logon + +- name: get result of create logon trigger + test_task_stat: + path: \ + name: '{{test_scheduled_task_name}}' + register: trigger_logon_result + +- name: assert results of create logon trigger + assert: + that: + - trigger_logon|changed + - trigger_logon_result.task_exists == True + - trigger_logon_result.task.triggers|count == 1 + - trigger_logon_result.task.triggers[0].Type == "TASK_TRIGGER_LOGON" + - trigger_logon_result.task.triggers[0].Enabled == True + - trigger_logon_result.task.triggers[0].StartBoundary == None + - trigger_logon_result.task.triggers[0].EndBoundary == None + +- name: create logon trigger (idempotent) + win_scheduled_task: + name: '{{test_scheduled_task_name}}' + state: present + actions: + - path: cmd.exe + triggers: + - type: logon + register: trigger_logon_again + +- name: assert results of create logon trigger (idempotent) + assert: + that: + - not trigger_logon_again|changed + +- name: create monthly dow trigger (check mode) + win_scheduled_task: + name: '{{test_scheduled_task_name}}' + state: present + actions: + - path: cmd.exe + triggers: + - type: monthlydow + start_boundary: '2000-01-01T00:00:01' + weeks_of_month: 1,2 + days_of_week: [ "monday", "wednesday" ] + register: trigger_monthlydow_check + check_mode: yes + +- name: get result of create monthly dow trigger (check mode) + test_task_stat: + path: \ + name: '{{test_scheduled_task_name}}' + register: trigger_monthlydow_result_check + +- name: assert results of create monthly dow trigger (check mode) + assert: + that: + - trigger_monthlydow_check|changed + - trigger_monthlydow_result_check.task_exists == True + - trigger_monthlydow_result_check.task.triggers|count == 1 + - trigger_monthlydow_result_check.task.triggers[0].Type == "TASK_TRIGGER_LOGON" + - trigger_monthlydow_result_check.task.triggers[0].Enabled == True + - trigger_monthlydow_result_check.task.triggers[0].StartBoundary == None + - trigger_monthlydow_result_check.task.triggers[0].EndBoundary == None + +- name: create monthly dow trigger + win_scheduled_task: + name: '{{test_scheduled_task_name}}' + state: present + actions: + - path: cmd.exe + triggers: + - type: monthlydow + start_boundary: '2000-01-01T00:00:01' + weeks_of_month: 1,2 + days_of_week: [ "monday", "wednesday" ] + register: trigger_monthlydow + +- name: get result of create monthly dow trigger + test_task_stat: + path: \ + name: '{{test_scheduled_task_name}}' + register: trigger_monthlydow_result + +- name: assert results of create monthly dow trigger + assert: + that: + - trigger_monthlydow|changed + - trigger_monthlydow_result.task_exists == True + - trigger_monthlydow_result.task.triggers|count == 1 + - trigger_monthlydow_result.task.triggers[0].Type == "TASK_TRIGGER_MONTHLYDOW" + - trigger_monthlydow_result.task.triggers[0].Enabled == True + - trigger_monthlydow_result.task.triggers[0].StartBoundary == "2000-01-01T00:00:01" + - trigger_monthlydow_result.task.triggers[0].EndBoundary == None + - trigger_monthlydow_result.task.triggers[0].WeeksOfMonth == "1,2" + - trigger_monthlydow_result.task.triggers[0].DaysOfWeek == "monday,wednesday" + +- name: create monthly dow trigger (idempotent) + win_scheduled_task: + name: '{{test_scheduled_task_name}}' + state: present + actions: + - path: cmd.exe + triggers: + - type: monthlydow + start_boundary: '2000-01-01T00:00:01' + weeks_of_month: 1,2 + days_of_week: [ "monday", "wednesday" ] + register: trigger_monthlydow_again + +- name: assert results of create monthly dow trigger (idempotent) + assert: + that: + - not trigger_monthlydow_again|changed + +- name: create task with multiple triggers (check mode) + win_scheduled_task: + name: '{{test_scheduled_task_name}}' + state: present + actions: + - path: cmd.exe + triggers: + - type: monthly + days_of_month: 1,5,10,15,20,25,30 + run_on_last_day_of_month: true + start_boundary: '2000-01-01T00:00:01' + months_of_year: + - march + - may + - july + - type: time + start_boundary: '2000-01-01T00:00:01' + random_delay: PT10M5S + register: create_multiple_triggers_check + check_mode: yes + +- name: get result of create task with multiple triggers (check mode) + test_task_stat: + path: \ + name: '{{test_scheduled_task_name}}' + register: create_multiple_triggers_result_check + +- name: assert results of create task with multiple triggers (check mode) + assert: + that: + - create_multiple_triggers_check|changed + - create_multiple_triggers_result_check.task_exists == True + - create_multiple_triggers_result_check.task.triggers|count == 1 + - create_multiple_triggers_result_check.task.triggers[0].Type == "TASK_TRIGGER_MONTHLYDOW" + - create_multiple_triggers_result_check.task.triggers[0].Enabled == True + - create_multiple_triggers_result_check.task.triggers[0].StartBoundary == "2000-01-01T00:00:01" + - create_multiple_triggers_result_check.task.triggers[0].EndBoundary == None + - create_multiple_triggers_result_check.task.triggers[0].WeeksOfMonth == "1,2" + - create_multiple_triggers_result_check.task.triggers[0].DaysOfWeek == "monday,wednesday" + +- name: create task with multiple triggers + win_scheduled_task: + name: '{{test_scheduled_task_name}}' + state: present + actions: + - path: cmd.exe + triggers: + - type: monthly + days_of_month: 1,5,10,15,20,25,30 + run_on_last_day_of_month: true + start_boundary: '2000-01-01T00:00:01' + months_of_year: + - march + - may + - july + - type: time + start_boundary: '2000-01-01T00:00:01' + random_delay: PT10M5S + register: create_multiple_triggers + +- name: get result of create task with multiple triggers + test_task_stat: + path: \ + name: '{{test_scheduled_task_name}}' + register: create_multiple_triggers_result + +- name: assert results of create task with multiple triggers + assert: + that: + - create_multiple_triggers|changed + - create_multiple_triggers_result.task_exists == True + - create_multiple_triggers_result.task.triggers|count == 2 + - create_multiple_triggers_result.task.triggers[0].Type == "TASK_TRIGGER_MONTHLY" + - create_multiple_triggers_result.task.triggers[0].Enabled == True + - create_multiple_triggers_result.task.triggers[0].StartBoundary == "2000-01-01T00:00:01" + - create_multiple_triggers_result.task.triggers[0].EndBoundary == None + - create_multiple_triggers_result.task.triggers[0].DaysOfMonth == "1,5,10,15,20,25,30" + - create_multiple_triggers_result.task.triggers[0].MonthsOfYear == "march,may,july" + - create_multiple_triggers_result.task.triggers[0].RunOnLastDayOfMonth == True + - create_multiple_triggers_result.task.triggers[1].Type == "TASK_TRIGGER_TIME" + - create_multiple_triggers_result.task.triggers[1].Enabled == True + - create_multiple_triggers_result.task.triggers[1].StartBoundary == "2000-01-01T00:00:01" + - create_multiple_triggers_result.task.triggers[1].EndBoundary == None + - create_multiple_triggers_result.task.triggers[1].RandomDelay == "PT10M5S" + +- name: create task with multiple triggers (idempotent) + win_scheduled_task: + name: '{{test_scheduled_task_name}}' + state: present + actions: + - path: cmd.exe + triggers: + - type: monthly + days_of_month: 1,5,10,15,20,25,30 + run_on_last_day_of_month: true + start_boundary: '2000-01-01T00:00:01' + months_of_year: + - march + - may + - july + - type: time + start_boundary: '2000-01-01T00:00:01' + random_delay: PT10M5S + register: create_multiple_triggers_again + +- name: assert results of create task with multiple triggers (idempotent) + assert: + that: + - not create_multiple_triggers_again|changed + +- name: change task with multiple triggers (check mode) + win_scheduled_task: + name: '{{test_scheduled_task_name}}' + state: present + actions: + - path: cmd.exe + triggers: + - type: weekly + days_of_week: tuesday,friday + start_boundary: '2000-01-01T00:00:01' + - type: registration + enabled: no + register: change_multiple_triggers_check + check_mode: yes + +- name: get result of change task with multiple triggers (check mode) + test_task_stat: + path: \ + name: '{{test_scheduled_task_name}}' + register: change_multiple_triggers_result_check + +- name: assert results of change task with multiple triggers (check mode) + assert: + that: + - change_multiple_triggers_check|changed + - change_multiple_triggers_result_check.task_exists == True + - change_multiple_triggers_result_check.task.triggers|count == 2 + - change_multiple_triggers_result_check.task.triggers[0].Type == "TASK_TRIGGER_MONTHLY" + - change_multiple_triggers_result_check.task.triggers[0].Enabled == True + - change_multiple_triggers_result_check.task.triggers[0].StartBoundary == "2000-01-01T00:00:01" + - change_multiple_triggers_result_check.task.triggers[0].EndBoundary == None + - change_multiple_triggers_result_check.task.triggers[0].DaysOfMonth == "1,5,10,15,20,25,30" + - change_multiple_triggers_result_check.task.triggers[0].MonthsOfYear == "march,may,july" + - change_multiple_triggers_result_check.task.triggers[0].RunOnLastDayOfMonth == True + - change_multiple_triggers_result_check.task.triggers[1].Type == "TASK_TRIGGER_TIME" + - change_multiple_triggers_result_check.task.triggers[1].Enabled == True + - change_multiple_triggers_result_check.task.triggers[1].StartBoundary == "2000-01-01T00:00:01" + - change_multiple_triggers_result_check.task.triggers[1].EndBoundary == None + - change_multiple_triggers_result_check.task.triggers[1].RandomDelay == "PT10M5S" + +- name: change task with multiple triggers + win_scheduled_task: + name: '{{test_scheduled_task_name}}' + state: present + actions: + - path: cmd.exe + triggers: + - type: weekly + days_of_week: tuesday,friday + start_boundary: '2000-01-01T00:00:01' + - type: registration + enabled: no + register: change_multiple_triggers + +- name: get result of change task with multiple triggers + test_task_stat: + path: \ + name: '{{test_scheduled_task_name}}' + register: change_multiple_triggers_result + +- name: assert results of change task with multiple triggers + assert: + that: + - change_multiple_triggers|changed + - change_multiple_triggers_result.task_exists == True + - change_multiple_triggers_result.task.triggers|count == 2 + - change_multiple_triggers_result.task.triggers[0].Type == "TASK_TRIGGER_WEEKLY" + - change_multiple_triggers_result.task.triggers[0].Enabled == True + - change_multiple_triggers_result.task.triggers[0].StartBoundary == "2000-01-01T00:00:01" + - change_multiple_triggers_result.task.triggers[0].EndBoundary == None + - change_multiple_triggers_result.task.triggers[0].DaysOfWeek == "tuesday,friday" + - change_multiple_triggers_result.task.triggers[1].Type == "TASK_TRIGGER_REGISTRATION" + - change_multiple_triggers_result.task.triggers[1].Enabled == False + - change_multiple_triggers_result.task.triggers[1].StartBoundary == None + - change_multiple_triggers_result.task.triggers[1].EndBoundary == None + +- name: change task with multiple triggers (idempotent) + win_scheduled_task: + name: '{{test_scheduled_task_name}}' + state: present + actions: + - path: cmd.exe + triggers: + - type: weekly + days_of_week: tuesday,friday + start_boundary: '2000-01-01T00:00:01' + - type: registration + enabled: no + register: change_multiple_triggers_again + +- name: assert results of change task with multiple triggers (idempotent) + assert: + that: + - not change_multiple_triggers_again|changed + +- name: remove trigger from multiple triggers (check mode) + win_scheduled_task: + name: '{{test_scheduled_task_name}}' + state: present + actions: + - path: cmd.exe + triggers: + - type: registration + enabled: no + register: remove_single_trigger_check + check_mode: yes + +- name: get result of remove trigger from multiple triggers (check mode) + test_task_stat: + path: \ + name: '{{test_scheduled_task_name}}' + register: remove_single_trigger_result_check + +- name: assert results of remove trigger from multiple triggers (check mode) + assert: + that: + - remove_single_trigger_check|changed + - remove_single_trigger_result_check.task_exists == True + - remove_single_trigger_result_check.task.triggers|count == 2 + - remove_single_trigger_result_check.task.triggers[0].Type == "TASK_TRIGGER_WEEKLY" + - remove_single_trigger_result_check.task.triggers[0].Enabled == True + - remove_single_trigger_result_check.task.triggers[0].StartBoundary == "2000-01-01T00:00:01" + - remove_single_trigger_result_check.task.triggers[0].EndBoundary == None + - remove_single_trigger_result_check.task.triggers[0].DaysOfWeek == "tuesday,friday" + - remove_single_trigger_result_check.task.triggers[1].Type == "TASK_TRIGGER_REGISTRATION" + - remove_single_trigger_result_check.task.triggers[1].Enabled == False + - remove_single_trigger_result_check.task.triggers[1].StartBoundary == None + - remove_single_trigger_result_check.task.triggers[1].EndBoundary == None + +- name: remove trigger from multiple triggers + win_scheduled_task: + name: '{{test_scheduled_task_name}}' + state: present + actions: + - path: cmd.exe + triggers: + - type: registration + enabled: no + register: remove_single_trigger + +- name: get result of remove trigger from multiple triggers + test_task_stat: + path: \ + name: '{{test_scheduled_task_name}}' + register: remove_single_trigger_result + +- name: assert results of remove trigger from multiple triggers + assert: + that: + - remove_single_trigger|changed + - remove_single_trigger_result.task_exists == True + - remove_single_trigger_result.task.triggers|count == 1 + - remove_single_trigger_result.task.triggers[0].Type == "TASK_TRIGGER_REGISTRATION" + - remove_single_trigger_result.task.triggers[0].Enabled == False + - remove_single_trigger_result.task.triggers[0].StartBoundary == None + - remove_single_trigger_result.task.triggers[0].EndBoundary == None + +- name: remove trigger from multiple triggers (idempotent) + win_scheduled_task: + name: '{{test_scheduled_task_name}}' + state: present + actions: + - path: cmd.exe + triggers: + - type: registration + enabled: no + register: remove_single_trigger_again + +- name: assert results of remove trigger from multiple triggers (idempotent) + assert: + that: + - not remove_single_trigger_again|changed + +- name: remove all triggers (check mode) + win_scheduled_task: + name: '{{test_scheduled_task_name}}' + state: present + actions: + - path: cmd.exe + triggers: [] + register: remove_triggers_check + check_mode: yes + +- name: get result of remove all triggers (check mode) + test_task_stat: + path: \ + name: '{{test_scheduled_task_name}}' + register: remove_triggers_result_check + +- name: assert results of remove all triggers (check mode) + assert: + that: + - remove_triggers_check|changed + - remove_triggers_result_check.task_exists == True + - remove_triggers_result_check.task.triggers|count == 1 + - remove_triggers_result_check.task.triggers[0].Type == "TASK_TRIGGER_REGISTRATION" + - remove_triggers_result_check.task.triggers[0].Enabled == False + - remove_triggers_result_check.task.triggers[0].StartBoundary == None + - remove_triggers_result_check.task.triggers[0].EndBoundary == None + +- name: remove all triggers + win_scheduled_task: + name: '{{test_scheduled_task_name}}' + state: present + actions: + - path: cmd.exe + triggers: [] + register: remove_triggers + +- name: get result of remove all triggers + test_task_stat: + path: \ + name: '{{test_scheduled_task_name}}' + register: remove_triggers_result + +- name: assert results of remove all triggers + assert: + that: + - remove_triggers|changed + - remove_triggers_result.task_exists == True + - remove_triggers_result.task.triggers|count == 0 + +- name: remove all triggers (idempotent) + win_scheduled_task: + name: '{{test_scheduled_task_name}}' + state: present + actions: + - path: cmd.exe + triggers: [] + register: remove_triggers_again + +- name: assert results of remove all triggers (idempotent) + assert: + that: + - not remove_triggers_again|changed