ansible-later/env_27/lib/python2.7/site-packages/ansible/modules/windows/win_scheduled_task.ps1
2019-04-11 13:00:36 +02:00

1151 lines
50 KiB
PowerShell

#!powershell
# Copyright: (c) 2015, Peter Mounce <public@neverrunwithscissors.com>
# Copyright: (c) 2015, Michael Perzel <michaelperzel@gmail.com>
# 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"
$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
$_remote_tmp = Get-AnsibleParam $params "_ansible_remote_tmp" -type "path" -default $env:TMP
$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"
$result = @{
changed = $false
}
if ($diff_mode) {
$result.diff = @{}
}
$task_enums = @"
public enum TASK_ACTION_TYPE // https://msdn.microsoft.com/en-us/library/windows/desktop/aa383553(v=vs.85).aspx
{
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
}
"@
$original_tmp = $env:TMP
$env:TMP = $_remote_tmp
Add-Type -TypeDefinition $task_enums
$env:TMP = $original_tmp
########################
### 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 Set-PropertyForComObject($com_object, $name, $arg, $value) {
$com_name = Convert-SnakeToPascalCase -snake $arg
try {
$com_object.$com_name = $value
} catch {
Fail-Json -obj $result -message "failed to set $name property '$com_name' to '$value': $($_.Exception.Message)"
}
}
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
if ($property_value -is [Hashtable]) {
foreach ($sub_property_arg in $property_value.Keys) {
$sub_com_name = Convert-SnakeToPascalCase -snake $sub_property_arg
$sub_property_value = $property_value.$sub_property_arg
[void]$diff_list.Add("+$com_name.$sub_com_name=$sub_property_value")
}
} else {
[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
if ($property_value -is [Hashtable]) {
foreach ($sub_property_arg in $property_value.Keys) {
$sub_com_name = Convert-SnakeToPascalCase -snake $sub_property_arg
$sub_property_value = $property_value.$sub_property_arg
[void]$diff_list.Add("+$com_name.$sub_com_name=$sub_property_value")
}
} else {
[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
$property_value = $new_property.$property_arg
$existing_value = $existing_property.$com_name
if ($property_value -is [Hashtable]) {
foreach ($sub_property_arg in $property_value.Keys) {
$sub_property_value = $property_value.$sub_property_arg
$sub_com_name = Convert-SnakeToPascalCase -snake $sub_property_arg
$sub_existing_value = $existing_property.$com_name.$sub_com_name
if ($sub_property_value -ne $null) {
[void]$diff_list.Add("+$com_name.$sub_com_name=$sub_property_value")
}
if ($sub_existing_value -ne $null) {
[void]$diff_list.Add("-$com_name.$sub_com_name=$sub_existing_value")
}
}
} else {
if ($property_value -ne $null) {
[void]$diff_list.Add("+$com_name=$property_value")
}
if ($existing_value -ne $null) {
[void]$diff_list.Add("-$com_name=$existing_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) {
$com_name = Convert-SnakeToPascalCase -snake $property_arg
$property_value = $new_property.$property_arg
$existing_value = $existing_property.$com_name
if ($property_value -is [Hashtable]) {
foreach ($sub_property_arg in $property_value.Keys) {
$sub_property_value = $property_value.$sub_property_arg
if ($sub_property_value -ne $null) {
$sub_com_name = Convert-SnakeToPascalCase -snake $sub_property_arg
$sub_existing_value = $existing_property.$com_name.$sub_com_name
if ($sub_property_value -cne $sub_existing_value) {
[void]$diff_list.Add("-$com_name.$sub_com_name=$sub_existing_value")
[void]$diff_list.Add("+$com_name.$sub_com_name=$sub_property_value")
}
}
}
} elseif ($property_value -ne $null -and $property_value -cne $existing_value) {
[void]$diff_list.Add("-$com_name=$existing_value")
[void]$diff_list.Add("+$com_name=$property_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 -is [Hashtable]) {
$com_name = Convert-SnakeToPascalCase -snake $property_arg
$new_object_property = $new_object.$com_name
foreach ($key in $new_value.Keys) {
$value = $new_value.$key
if ($value -ne $null) {
Set-PropertyForComObject -com_object $new_object_property -name $property_name -arg $key -value $value
}
}
} elseif ($new_value -ne $null) {
Set-PropertyForComObject -com_object $new_object -name $property_name -arg $property_arg -value $new_value
}
}
}
# 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()
}
$map = @{
[TASK_TRIGGER_TYPE2]::TASK_TRIGGER_BOOT = @{
mandatory = @()
optional = @('delay', 'enabled', 'end_boundary', 'execution_time_limit', 'start_boundary', 'repetition')
}
[TASK_TRIGGER_TYPE2]::TASK_TRIGGER_DAILY = @{
mandatory = @('start_boundary')
optional = @('days_interval', 'enabled', 'end_boundary', 'execution_time_limit', 'random_delay', 'repetition')
}
[TASK_TRIGGER_TYPE2]::TASK_TRIGGER_EVENT = @{
mandatory = @('subscription')
# TODO: ValueQueries is a COM object
optional = @('delay', 'enabled', 'end_boundary', 'execution_time_limit', 'start_boundary', 'repetition')
}
[TASK_TRIGGER_TYPE2]::TASK_TRIGGER_IDLE = @{
mandatory = @()
optional = @('enabled', 'end_boundary', 'execution_time_limit', 'start_boundary', 'repetition')
}
[TASK_TRIGGER_TYPE2]::TASK_TRIGGER_LOGON = @{
mandatory = @()
optional = @('delay', 'enabled', 'end_boundary', 'execution_time_limit', 'start_boundary', 'user_id', 'repetition')
}
[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', 'repetition')
}
[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', 'repetition')
}
[TASK_TRIGGER_TYPE2]::TASK_TRIGGER_REGISTRATION = @{
mandatory = @()
optional = @('delay', 'enabled', 'end_boundary', 'execution_time_limit', 'start_boundary', 'repetition')
}
[TASK_TRIGGER_TYPE2]::TASK_TRIGGER_TIME = @{
mandatory = @('start_boundary')
optional = @('enabled', 'end_boundary', 'execution_time_limit', 'random_delay', 'repetition')
}
[TASK_TRIGGER_TYPE2]::TASK_TRIGGER_WEEKLY = @{
mandatory = @('days_of_week', 'start_boundary')
optional = @('enabled', 'end_boundary', 'execution_time_limit', 'random_delay', 'weeks_interval', 'repetition')
}
[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', 'repetition')
}
}
$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
}
Function Test-XmlDurationFormat($key, $value) {
# validate value is in the Duration Data Type format
# PnYnMnDTnHnMnS
try {
$time_span = [System.Xml.XmlConvert]::ToTimeSpan($value)
return $time_span
} catch [System.FormatException] {
Fail-Json -obj $result -message "trigger option '$key' must be in the XML duration format but was '$value'"
}
}
######################################
### 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
}
# 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
}
# 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 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) {
if ($trigger.ContainsKey($property_name)) {
$time_span = $trigger.$property_name
Test-XmlDurationFormat -key $property_name -value $time_span
}
}
if ($trigger.ContainsKey("repetition")) {
$trigger.repetition = ConvertTo-HashtableFromPsCustomObject -object $trigger.repetition
$interval_timespan = $null
if ($trigger.repetition.ContainsKey("interval") -and $trigger.repetition.interval -ne $null) {
$interval_timespan = Test-XmlDurationFormat -key "interval" -value $trigger.repetition.interval
}
$duration_timespan = $null
if ($trigger.repetition.ContainsKey("duration") -and $trigger.repetition.duration -ne $null) {
$duration_timespan = Test-XmlDurationFormat -key "duration" -value $trigger.repetition.duration
}
if ($interval_timespan -ne $null -and $duration_timespan -ne $null -and $interval_timespan -gt $duration_timespan) {
Fail-Json -obj $result -message "trigger repetition option 'interval' value '$($trigger.repetition.interval)' must be less than or equal to 'duration' value '$($trigger.repetition.duration)'"
}
}
# 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