1179 lines
52 KiB
PowerShell
1179 lines
52 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"
|
|
|
|
Function ConvertTo-Hashtable {
|
|
param([Object]$Value)
|
|
|
|
if ($null -eq $Value) {
|
|
return $null
|
|
}
|
|
$value_type = $Value.GetType()
|
|
if ($value_type.IsGenericType) {
|
|
$value_type = $value_type.GetGenericTypeDefinition()
|
|
}
|
|
if ($value_type -eq [System.Collections.Generic.Dictionary`2]) {
|
|
$new_value = @{}
|
|
foreach ($kv in $Value.GetEnumerator()) {
|
|
$new_value.Add($kv.Key, (ConvertTo-Hashtable -Value $kv.Value))
|
|
}
|
|
return ,$new_value
|
|
} elseif ($value_type -eq [System.Collections.ArrayList]) {
|
|
for ($i = 0; $i -lt $Value.Count; $i++) {
|
|
$Value[$i] = ConvertTo-Hashtable -Value $Value[$i]
|
|
}
|
|
return ,$Value.ToArray()
|
|
} else {
|
|
return ,$Value
|
|
}
|
|
}
|
|
|
|
$params = Parse-Args -arguments $args -supports_check_mode $true
|
|
|
|
# FUTURE: remove this once exec_wrapper has this behaviour inbuilt with the new
|
|
# json changes in the exec_wrapper.
|
|
# Currently ConvertFrom-Json creates a PSObject for the deserialized JSON and the
|
|
# exec_wrapper converts all dicts as Hashtable. Unfortunately it doesn't
|
|
# convert any dict in lists leaving to some confusing behaviour. We manually
|
|
# use JavaScriptSerializer to ensure we have the type of objects to simply the
|
|
# code in the module when it comes to type checking
|
|
$params_json = ConvertTo-Json -InputObject $params -Depth 99 -Compress
|
|
|
|
Add-Type -AssemblyName System.Web.Extensions
|
|
$json = New-Object -TypeName System.Web.Script.Serialization.JavaScriptSerializer
|
|
$json.MaxJsonLength = [Int32]::MaxValue
|
|
$json.RecursionLimit = [Int32]::MaxValue
|
|
$params = ConvertTo-Hashtable -Value ($json.Deserialize($params_json, [System.Collections.Generic.Dictionary`2[[String], [Object]]]))
|
|
|
|
$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 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 ($kv in $property_value.GetEnumerator()) {
|
|
$sub_com_name = Convert-SnakeToPascalCase -snake $kv.Key
|
|
$sub_property_value = $kv.Value
|
|
[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 ($kv in $property_value.GetEnumerator()) {
|
|
$sub_com_name = Convert-SnakeToPascalCase -snake $kv.Key
|
|
$sub_property_value = $kv.Value
|
|
[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 ($kv in $property_value.GetEnumerator()) {
|
|
$sub_property_value = $kv.Value
|
|
$sub_com_name = Convert-SnakeToPascalCase -snake $kv.Key
|
|
$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 ($kv in $property_value.GetEnumerator()) {
|
|
$sub_property_value = $kv.Value
|
|
|
|
if ($sub_property_value -ne $null) {
|
|
$sub_com_name = Convert-SnakeToPascalCase -snake $kv.Key
|
|
$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 ($kv in $new_value.GetEnumerator()) {
|
|
$value = $kv.Value
|
|
if ($value -ne $null) {
|
|
Set-PropertyForComObject -com_object $new_object_property -name $property_name -arg $kv.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 = $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 = $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")) {
|
|
if ($trigger.repetition -is [Array]) {
|
|
Add-DeprecationWarning -obj $result -message "repetition is a list, should be defined as a dict" -version "2.12"
|
|
$trigger.repetition = $trigger.repetition[0]
|
|
}
|
|
|
|
$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
|