From 57ff84251ea4d7c317c8cc5e50c906f9dca8c0f8 Mon Sep 17 00:00:00 2001 From: David Vilar Benet <32133475+d-vb@users.noreply.github.com> Date: Thu, 11 Jan 2018 23:08:50 +0100 Subject: [PATCH] Implemented Trigger Repetition (#32938) * Implemented Trigger Repetition * Refactorings and integration tests for Trigger Repetition * Suggestions of first review * Changes of second review --- .../modules/windows/win_scheduled_task.ps1 | 158 +++++++++++---- .../modules/windows/win_scheduled_task.py | 22 +++ .../win_scheduled_task/tasks/failures.yml | 34 ++++ .../win_scheduled_task/tasks/triggers.yml | 187 +++++++++++++++++- 4 files changed, 360 insertions(+), 41 deletions(-) diff --git a/lib/ansible/modules/windows/win_scheduled_task.ps1 b/lib/ansible/modules/windows/win_scheduled_task.ps1 index fcf985260a..6e2ce4d99b 100644 --- a/lib/ansible/modules/windows/win_scheduled_task.ps1 +++ b/lib/ansible/modules/windows/win_scheduled_task.ps1 @@ -198,6 +198,15 @@ Function Compare-Properties($property_name, $parent_property, $map, $enum_map=$n 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 @@ -266,11 +275,21 @@ Function Compare-PropertyList { # 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") + + 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") + } } } @@ -285,7 +304,16 @@ Function Compare-PropertyList { 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") + + 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 { @@ -295,14 +323,31 @@ Function Compare-PropertyList { [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 - $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") + 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") + } } } } @@ -313,15 +358,27 @@ Function Compare-PropertyList { $diff_list = [System.Collections.ArrayList]@() foreach ($property_arg in $total_args) { - $new_value = $new_property.$property_arg $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 ($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 ($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") } } @@ -334,13 +391,18 @@ Function Compare-PropertyList { $new_object = $collection.Create($type) foreach ($property_arg in $total_args) { $new_value = $new_property.$property_arg - if ($new_value -ne $null) { + if ($new_value -is [Hashtable]) { $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)" + $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 } } } @@ -543,52 +605,51 @@ Function Compare-Triggers($task_definition) { $task_triggers.Clear() } - # TODO: solve repetition, takes in a COM object $map = @{ [TASK_TRIGGER_TYPE2]::TASK_TRIGGER_BOOT = @{ mandatory = @() - optional = @('delay', 'enabled', 'end_boundary', 'execution_time_limit', 'start_boundary') + 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') + 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') + 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') + 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') + 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') + 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') + 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') + 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') + 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') + 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') + 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 @@ -614,6 +675,17 @@ Function Test-TaskExists($task_folder, $name) { 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 ### ###################################### @@ -774,15 +846,27 @@ for ($i = 0; $i -lt $triggers.Count; $i++) { $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'" - } + 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)'" } } diff --git a/lib/ansible/modules/windows/win_scheduled_task.py b/lib/ansible/modules/windows/win_scheduled_task.py index 86535392a1..a46b59a301 100644 --- a/lib/ansible/modules/windows/win_scheduled_task.py +++ b/lib/ansible/modules/windows/win_scheduled_task.py @@ -193,6 +193,14 @@ options: - 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). + repetition: + description: + - Allows you to define the repetition action of the trigger that defines how often the task is run and how long the repetition pattern is repeated + after the task is started. + - It takes in the following keys, C(duration), C(interval), C(stop_at_duration_end) + - C(duration) is how long the pattern is repeated and is written in the ISO 8601 Duration format C(P[n]Y[n]M[n]DT[n]H[n]M[n]S). + - C(interval) is the amount of time between earch restart of the task and is written in the ISO 8601 Duration format C(P[n]Y[n]M[n]DT[n]H[n]M[n]S). + - C(stop_at_duration_end) is a boolean value that indicates if a running instance of the task is stopped at the end of the repetition pattern. version_added: '2.5' days_of_week: description: @@ -488,6 +496,20 @@ EXAMPLES = r''' win_scheduled_task: name: TaskToDisable enabled: no + +- name: create a task that will be repeated every minute for five minutes + win_scheduled_task: + name: RepeatedTask + description: open command prompt + actions: + - path: cmd.exe + arguments: /c hostname + triggers: + - type: registration + repetition: + - interval: PT1M + duration: PT5M + stop_at_duration_end: yes ''' RETURN = r''' diff --git a/test/integration/targets/win_scheduled_task/tasks/failures.yml b/test/integration/targets/win_scheduled_task/tasks/failures.yml index 2b9e3e2184..6ddcc8b906 100644 --- a/test/integration/targets/win_scheduled_task/tasks/failures.yml +++ b/test/integration/targets/win_scheduled_task/tasks/failures.yml @@ -121,3 +121,37 @@ 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" + +- name: fail trigger repetition with duration in incorrect format + win_scheduled_task: + name: '{{test_scheduled_task_name}}' + state: present + triggers: + - type: boot + repetition: + - duration: fake + register: fail_trigger_repetition_invalid_duration + failed_when: fail_trigger_repetition_invalid_duration.msg != "trigger option 'duration' must be in the XML duration format but was 'fake'" + +- name: fail trigger repetition with interval in incorrect format + win_scheduled_task: + name: '{{test_scheduled_task_name}}' + state: present + triggers: + - type: boot + repetition: + - interval: fake + register: fail_trigger_repetition_invalid_interval + failed_when: fail_trigger_repetition_invalid_interval.msg != "trigger option 'interval' must be in the XML duration format but was 'fake'" + +- name: fail trigger repetition option interval greater than duration + win_scheduled_task: + name: '{{test_scheduled_task_name}}' + state: present + triggers: + - type: boot + repetition: + - interval: PT5M + duration: PT1M + register: fail_trigger_repetition_interval_greater_than_duration + failed_when: fail_trigger_repetition_interval_greater_than_duration.msg != "trigger repetition option 'interval' value 'PT5M' must be less than or equal to 'duration' value 'PT1M'" diff --git a/test/integration/targets/win_scheduled_task/tasks/triggers.yml b/test/integration/targets/win_scheduled_task/tasks/triggers.yml index 5e8f0f067a..e80d72b2b7 100644 --- a/test/integration/targets/win_scheduled_task/tasks/triggers.yml +++ b/test/integration/targets/win_scheduled_task/tasks/triggers.yml @@ -288,6 +288,184 @@ that: - trigger_monthlydow_again is not changed +- name: create trigger repetition (check mode) + win_scheduled_task: + name: '{{test_scheduled_task_name}}' + state: present + actions: + - path: cmd.exe + triggers: + - type: registration + repetition: + - interval: PT1M + duration: PT5M + stop_at_duration_end: yes + register: create_trigger_repetition_check + check_mode: yes + +- name: get result of create trigger repetition (check mode) + win_scheduled_task_stat: + path: \ + name: '{{test_scheduled_task_name}}' + register: create_trigger_repetition_result_check + +- name: assert results of create trigger repetition (check mode) + assert: + that: + - create_trigger_repetition_check is changed + - create_trigger_repetition_result_check.task_exists == True + - create_trigger_repetition_result_check.triggers|count == 1 + - create_trigger_repetition_result_check.triggers[0].type == "TASK_TRIGGER_MONTHLYDOW" + - create_trigger_repetition_result_check.triggers[0].enabled == True + - create_trigger_repetition_result_check.triggers[0].start_boundary == "2000-01-01T00:00:01" + - create_trigger_repetition_result_check.triggers[0].end_boundary == None + - create_trigger_repetition_result_check.triggers[0].weeks_of_month == "1,2" + - create_trigger_repetition_result_check.triggers[0].days_of_week == "monday,wednesday" + - create_trigger_repetition_result_check.triggers[0].repetition.interval == None + - create_trigger_repetition_result_check.triggers[0].repetition.duration == None + - create_trigger_repetition_result_check.triggers[0].repetition.stop_at_duration_end == False + +- name: create trigger repetition + win_scheduled_task: + name: '{{test_scheduled_task_name}}' + state: present + actions: + - path: cmd.exe + triggers: + - type: registration + repetition: + - interval: PT1M + duration: PT5M + stop_at_duration_end: yes + register: create_trigger_repetition + +- name: get result of create trigger repetition + win_scheduled_task_stat: + path: \ + name: '{{test_scheduled_task_name}}' + register: create_trigger_repetition_result + +- name: assert results of create trigger repetition + assert: + that: + - create_trigger_repetition is changed + - create_trigger_repetition_result.task_exists == True + - create_trigger_repetition_result.triggers|count == 1 + - create_trigger_repetition_result.triggers[0].type == "TASK_TRIGGER_REGISTRATION" + - create_trigger_repetition_result.triggers[0].enabled == True + - create_trigger_repetition_result.triggers[0].start_boundary == None + - create_trigger_repetition_result.triggers[0].end_boundary == None + - create_trigger_repetition_result.triggers[0].repetition.interval == "PT1M" + - create_trigger_repetition_result.triggers[0].repetition.duration == "PT5M" + - create_trigger_repetition_result.triggers[0].repetition.stop_at_duration_end == True + +- name: create trigger repetition (idempotent) + win_scheduled_task: + name: '{{test_scheduled_task_name}}' + state: present + actions: + - path: cmd.exe + triggers: + - type: registration + repetition: + - interval: PT1M + duration: PT5M + stop_at_duration_end: yes + register: create_trigger_repetition_again + +- name: assert results of create trigger repetition (idempotent) + assert: + that: + - create_trigger_repetition_again is not changed + +- name: change trigger repetition (check mode) + win_scheduled_task: + name: '{{test_scheduled_task_name}}' + state: present + actions: + - path: cmd.exe + triggers: + - type: registration + repetition: + - interval: PT10M + duration: PT20M + stop_at_duration_end: no + register: change_trigger_repetition_check + check_mode: yes + +- name: get result of change trigger repetition (check mode) + win_scheduled_task_stat: + path: \ + name: '{{test_scheduled_task_name}}' + register: change_trigger_repetition_result_check + +- name: assert results of change trigger repetition (check mode) + assert: + that: + - change_trigger_repetition_check is changed + - change_trigger_repetition_result_check.task_exists == True + - change_trigger_repetition_result_check.triggers|count == 1 + - change_trigger_repetition_result_check.triggers[0].type == "TASK_TRIGGER_REGISTRATION" + - change_trigger_repetition_result_check.triggers[0].enabled == True + - change_trigger_repetition_result_check.triggers[0].start_boundary == None + - change_trigger_repetition_result_check.triggers[0].end_boundary == None + - change_trigger_repetition_result_check.triggers[0].repetition.interval == "PT1M" + - change_trigger_repetition_result_check.triggers[0].repetition.duration == "PT5M" + - change_trigger_repetition_result_check.triggers[0].repetition.stop_at_duration_end == True + +- name: change trigger repetition + win_scheduled_task: + name: '{{test_scheduled_task_name}}' + state: present + actions: + - path: cmd.exe + triggers: + - type: registration + repetition: + - interval: PT10M + duration: PT20M + stop_at_duration_end: no + register: change_trigger_repetition + +- name: get result of change trigger repetition + win_scheduled_task_stat: + path: \ + name: '{{test_scheduled_task_name}}' + register: change_trigger_repetition_result + +- name: assert results of change trigger repetition + assert: + that: + - change_trigger_repetition is changed + - change_trigger_repetition_result.task_exists == True + - change_trigger_repetition_result.triggers|count == 1 + - change_trigger_repetition_result.triggers[0].type == "TASK_TRIGGER_REGISTRATION" + - change_trigger_repetition_result.triggers[0].enabled == True + - change_trigger_repetition_result.triggers[0].start_boundary == None + - change_trigger_repetition_result.triggers[0].end_boundary == None + - change_trigger_repetition_result.triggers[0].repetition.interval == "PT10M" + - change_trigger_repetition_result.triggers[0].repetition.duration == "PT20M" + - change_trigger_repetition_result.triggers[0].repetition.stop_at_duration_end == False + +- name: change trigger repetition (idempotent) + win_scheduled_task: + name: '{{test_scheduled_task_name}}' + state: present + actions: + - path: cmd.exe + triggers: + - type: registration + repetition: + - interval: PT10M + duration: PT20M + stop_at_duration_end: no + register: change_trigger_repetition_again + +- name: assert results of change trigger repetition (idempotent) + assert: + that: + - change_trigger_repetition_again is not changed + - name: create task with multiple triggers (check mode) win_scheduled_task: name: '{{test_scheduled_task_name}}' @@ -321,12 +499,13 @@ - create_multiple_triggers_check is changed - create_multiple_triggers_result_check.task_exists == True - create_multiple_triggers_result_check.triggers|count == 1 - - create_multiple_triggers_result_check.triggers[0].type == "TASK_TRIGGER_MONTHLYDOW" + - create_multiple_triggers_result_check.triggers[0].type == "TASK_TRIGGER_REGISTRATION" - create_multiple_triggers_result_check.triggers[0].enabled == True - - create_multiple_triggers_result_check.triggers[0].start_boundary == "2000-01-01T00:00:01" + - create_multiple_triggers_result_check.triggers[0].start_boundary == None - create_multiple_triggers_result_check.triggers[0].end_boundary == None - - create_multiple_triggers_result_check.triggers[0].weeks_of_month == "1,2" - - create_multiple_triggers_result_check.triggers[0].days_of_week == "monday,wednesday" + - create_multiple_triggers_result_check.triggers[0].repetition.interval == "PT10M" + - create_multiple_triggers_result_check.triggers[0].repetition.duration == "PT20M" + - create_multiple_triggers_result_check.triggers[0].repetition.stop_at_duration_end == False - name: create task with multiple triggers win_scheduled_task: