File Manager
Viewing File: windows_task.rb
#
# Author:: Nimisha Sharad (<nimisha.sharad@msystechnologies.com>)
# Copyright:: Copyright (c) Chef Software Inc.
# License:: Apache License, Version 2.0
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
require_relative "../resource"
require_relative "../win32/security" if Chef::Platform.windows?
class Chef
class Resource
class WindowsTask < Chef::Resource
provides(:windows_task) { true }
description "Use the **windows_task** resource to create, delete or run a Windows scheduled task."
introduced "13.0"
examples <<~DOC
**Create a scheduled task to run every 15 minutes as the Administrator user**:
```ruby
windows_task 'chef-client' do
user 'Administrator'
password 'password'
command 'chef-client'
run_level :highest
frequency :minute
frequency_modifier 15
end
```
**Create a scheduled task to run every 2 days**:
``` ruby
windows_task 'chef-client' do
command 'chef-client'
run_level :highest
frequency :daily
frequency_modifier 2
end
```
**Create a scheduled task to run on specific days of the week**:
```ruby
windows_task 'chef-client' do
command 'chef-client'
run_level :highest
frequency :weekly
day 'Mon, Thu'
end
```
**Create a scheduled task to run only once**:
```ruby
windows_task 'chef-client' do
command 'chef-client'
run_level :highest
frequency :once
start_time "16:10"
end
```
**Create a scheduled task to run on current day every 3 weeks and delay upto 1 min**:
```ruby
windows_task 'chef-client' do
command 'chef-client'
run_level :highest
frequency :weekly
frequency_modifier 3
random_delay '60'
end
```
**Create a scheduled task to run weekly starting on Dec 28th 2018**:
```ruby
windows_task 'chef-client 8' do
command 'chef-client'
run_level :highest
frequency :weekly
start_day '12/28/2018'
end
```
**Create a scheduled task to run every Monday, Friday every 2 weeks**:
```ruby
windows_task 'chef-client' do
command 'chef-client'
run_level :highest
frequency :weekly
frequency_modifier 2
day 'Mon, Fri'
end
```
**Create a scheduled task to run when computer is idle with idle duration 20 min**:
```ruby
windows_task 'chef-client' do
command 'chef-client'
run_level :highest
frequency :on_idle
idle_time 20
end
```
**Delete a task named "old task"**:
```ruby
windows_task 'old task' do
action :delete
end
```
**Enable a task named "chef-client"**:
```ruby
windows_task 'chef-client' do
action :enable
end
```
**Disable a task named "ProgramDataUpdater" with TaskPath "\\Microsoft\\Windows\\Application Experience\\ProgramDataUpdater"**
```ruby
windows_task '\\Microsoft\\Windows\\Application Experience\\ProgramDataUpdater' do
action :disable
end
```
DOC
allowed_actions :create, :delete, :run, :end, :enable, :disable, :change
default_action :create
property :task_name, String, regex: [%r{\A[^/\:\*\?\<\>\|]+\z}],
description: "An optional property to set the task name if it differs from the resource block's name. Example: `Task Name` or `/Task Name`",
name_property: true
property :command, String,
description: "The command to be executed by the windows scheduled task."
property :cwd, String,
description: "The directory the task will be run from."
property :user, String,
description: "The user to run the task as.",
default: lazy { Chef::ReservedNames::Win32::Security::SID.LocalSystem.account_simple_name if Chef::Platform.windows? },
default_description: "The localized SYSTEM user for the node."
property :password, String,
description: "The user's password. The user property must be set if using this property."
property :run_level, Symbol, equal_to: %i{highest limited},
description: "Run with `:limited` or `:highest` privileges.",
default: :limited
property :force, [TrueClass, FalseClass],
description: "When used with create, will update the task.",
default: false
property :interactive_enabled, [TrueClass, FalseClass],
description: "Allow task to run interactively or non-interactively. Requires user and password to also be set.",
default: false
property :frequency_modifier, [Integer, String],
default: 1
property :frequency, Symbol, equal_to: %i{minute
hourly
daily
weekly
monthly
once
on_logon
onstart
on_idle
none},
description: "The frequency with which to run the task."
property :start_day, String,
description: "Specifies the first date on which the task runs in **MM/DD/YYYY** format.",
default_description: "The current date."
property :start_time, String,
description: "Specifies the start time to run the task, in **HH:mm** format."
property :day, [String, Integer],
description: "The day(s) on which the task runs."
property :months, String,
description: "The Months of the year on which the task runs, such as: `JAN, FEB` or `*`. Multiple months should be comma delimited. e.g. `Jan, Feb, Mar, Dec`."
property :idle_time, Integer,
description: "For `:on_idle` frequency, the time (in minutes) without user activity that must pass to trigger the task, from `1` - `999`."
property :random_delay, [String, Integer],
description: "Delays the task up to a given time (in seconds)."
property :execution_time_limit, [String, Integer],
description: "The maximum time the task will run. This field accepts either seconds or an ISO8601 duration value.",
default: "PT72H",
default_description: "PT72H (72 hours in ISO8601 duration format)"
property :minutes_duration, [String, Integer],
description: ""
property :minutes_interval, [String, Integer],
description: ""
property :priority, Integer,
description: "Use to set Priority Levels range from 0 to 10.",
default: 7, callbacks: { "should be in range of 0 to 10" => proc { |v| v >= 0 && v <= 10 } }
property :disallow_start_if_on_batteries, [TrueClass, FalseClass],
introduced: "14.4", default: false,
description: "Disallow start of the task if the system is running on battery power."
property :stop_if_going_on_batteries, [TrueClass, FalseClass],
introduced: "14.4", default: false,
description: "Scheduled task option when system is switching on battery."
property :description, String,
introduced: "14.7",
description: "The task description."
property :start_when_available, [TrueClass, FalseClass],
introduced: "14.15", default: false,
description: "To start the task at any time after its scheduled time has passed."
attr_accessor :exists, :task, :command_arguments
VALID_WEEK_DAYS = %w{ mon tue wed thu fri sat sun * }.freeze
VALID_DAYS_OF_MONTH = ("1".."31").to_a << "last" << "lastday"
VALID_MONTHS = %w{JAN FEB MAR APR MAY JUN JUL AUG SEP OCT NOV DEC *}.freeze
VALID_WEEKS = %w{FIRST SECOND THIRD FOURTH LAST LASTDAY}.freeze
def after_created
if random_delay
validate_random_delay(random_delay, frequency)
random_delay(sec_to_min(random_delay))
end
if execution_time_limit
execution_time_limit(259200) if execution_time_limit == "PT72H"
raise ArgumentError, "Invalid value passed for `execution_time_limit`. Please pass seconds as an Integer (e.g. 60) or a String with numeric values only (e.g. '60')." unless numeric_value_in_string?(execution_time_limit)
execution_time_limit(sec_to_min(execution_time_limit))
end
validate_frequency(frequency) if action.include?(:create) || action.include?(:change)
validate_start_time(start_time, frequency)
validate_start_day(start_day, frequency) if start_day
validate_user_and_password(user, password)
validate_create_frequency_modifier(frequency, frequency_modifier) if frequency_modifier
validate_create_day(day, frequency, frequency_modifier) if day
validate_create_months(months, frequency) if months
validate_frequency_monthly(frequency_modifier, months, day) if frequency == :monthly
validate_idle_time(idle_time, frequency)
idempotency_warning_for_frequency_weekly(day, start_day) if frequency == :weekly
end
private
## Resource is not idempotent when day, start_day is not provided with frequency :weekly
## we set start_day when not given by user as current date based on which we set the day property for current current date day is monday ..
## we set the monday as the day so at next run when new_resource.day is nil and current_resource day is monday due to which update gets called
def idempotency_warning_for_frequency_weekly(day, start_day)
if start_day.nil? && day.nil?
logger.warn "To maintain idempotency for frequency :weekly provide start_day, start_time and day."
end
end
# Validate the passed value is numeric values only if it is a string
def numeric_value_in_string?(val)
return true if Integer(val)
rescue ArgumentError
false
end
def validate_frequency(frequency)
if frequency.nil? || !(%i{minute hourly daily weekly monthly once on_logon onstart on_idle none}.include?(frequency))
raise ArgumentError, "Frequency needs to be provided. Valid frequencies are :minute, :hourly, :daily, :weekly, :monthly, :once, :on_logon, :onstart, :on_idle, :none."
end
end
def validate_frequency_monthly(frequency_modifier, months, day)
# validates the frequency :monthly and raises error if frequency_modifier is first, second, third etc and day is not provided
if (frequency_modifier != 1) && (frequency_modifier_includes_days_of_weeks?(frequency_modifier)) && !(day)
raise ArgumentError, "Please select day on which you want to run the task e.g. 'Mon, Tue'. Multiple values must be separated by comma."
end
# frequency_modifier 2-12 is used to set every (n) months, so using :months property with frequency_modifier is not valid since they both used to set months.
# Not checking value 1 here for frequency_modifier since we are setting that as default value it won't break anything since preference is given to months property
if (frequency_modifier.to_i.between?(2, 12)) && !(months.nil?)
raise ArgumentError, "For frequency :monthly either use property months or frequency_modifier to set months."
end
end
# returns true if frequency_modifier has values First, second, third, fourth, last, lastday
def frequency_modifier_includes_days_of_weeks?(frequency_modifier)
frequency_modifier = frequency_modifier.to_s.split(",")
frequency_modifier.map! { |value| value.strip.upcase }
(frequency_modifier - VALID_WEEKS).empty?
end
def validate_random_delay(random_delay, frequency)
if %i{on_logon onstart on_idle none}.include? frequency
raise ArgumentError, "`random_delay` property is supported only for frequency :once, :minute, :hourly, :daily, :weekly and :monthly"
end
raise ArgumentError, "Invalid value passed for `random_delay`. Please pass seconds as an Integer (e.g. 60) or a String with numeric values only (e.g. '60')." unless numeric_value_in_string?(random_delay)
end
# @todo when we drop ruby 2.3 support this should be converted to .match?() instead of =~f
def validate_start_day(start_day, frequency)
if start_day && frequency == :none
raise ArgumentError, "`start_day` property is not supported with frequency: #{frequency}"
end
# make sure the start_day is in MM/DD/YYYY format: http://rubular.com/r/cgjHemtWl5
if start_day
raise ArgumentError, "`start_day` property must be in the MM/DD/YYYY format." unless %r{^(0[1-9]|1[012])[- /.](0[1-9]|[12][0-9]|3[01])[- /.](19|20)\d\d$}.match?(start_day)
end
end
# @todo when we drop ruby 2.3 support this should be converted to .match?() instead of =~
def validate_start_time(start_time, frequency)
if start_time
raise ArgumentError, "`start_time` property is not supported with `frequency :none`" if frequency == :none
raise ArgumentError, "`start_time` property must be in the HH:mm format (e.g. 6:20pm -> 18:20)." unless /^[0-2][0-9]:[0-5][0-9]$/.match?(start_time)
else
raise ArgumentError, "`start_time` needs to be provided with `frequency :once`" if frequency == :once
end
end
# System users will not require a password
# Other users will require a password if the task is non-interactive.
#
# @param [String] user
# @param [String] password
#
def validate_user_and_password(user, password)
if non_system_user?(user)
if password.nil? && !interactive_enabled
raise ArgumentError, "Please provide a password or check if this task needs to be interactive! Valid passwordless users are: '#{Chef::ReservedNames::Win32::Security::SID::SYSTEM_USER.join("', '")}'"
end
else
unless password.nil?
raise ArgumentError, "Password is not required for system users."
end
end
end
# Password is not required for system user and required for non-system user.
def password_required?(user)
@password_required ||= (!user.nil? && !Chef::ReservedNames::Win32::Security::SID.system_user?(user))
end
alias non_system_user? password_required?
def validate_create_frequency_modifier(frequency, frequency_modifier)
if (%i{on_logon onstart on_idle none}.include?(frequency)) && ( frequency_modifier != 1)
raise ArgumentError, "frequency_modifier property not supported with frequency :#{frequency}"
end
if frequency == :monthly
unless (1..12).cover?(frequency_modifier.to_i) || frequency_modifier_includes_days_of_weeks?(frequency_modifier)
raise ArgumentError, "frequency_modifier value #{frequency_modifier} is invalid. Valid values for :monthly frequency are 1 - 12, 'FIRST', 'SECOND', 'THIRD', 'FOURTH', 'LAST'."
end
else
unless frequency.nil? || frequency_modifier.nil?
frequency_modifier = frequency_modifier.to_i
min = 1
max = case frequency
when :minute
1439
when :hourly
23
when :daily
365
when :weekly
52
else
min
end
unless frequency_modifier.between?(min, max)
raise ArgumentError, "frequency_modifier value #{frequency_modifier} is invalid. Valid values for :#{frequency} frequency are #{min} - #{max}."
end
end
end
end
def validate_create_day(day, frequency, frequency_modifier)
raise ArgumentError, "day property is only valid for tasks that run monthly or weekly" unless %i{weekly monthly}.include?(frequency)
# This has been verified with schtask.exe https://docs.microsoft.com/en-us/windows-server/administration/windows-commands/schtasks#d-dayday--
# verified with earlier code if day "*" is given with frequency it raised exception Invalid value for /D option
raise ArgumentError, "day wild card (*) is only valid with frequency :weekly" if frequency == :monthly && day == "*"
if day.is_a?(String) && day.to_i.to_s != day
days = day.split(",")
if days_includes_days_of_months?(days)
# Following error will be raise if day is set as 1-31 and frequency is selected as :weekly since those values are valid with only frequency :monthly
raise ArgumentError, "day values 1-31 or last is only valid with frequency :monthly" if frequency == :weekly
else
days.map! { |day| day.to_s.strip.downcase }
unless (days - VALID_WEEK_DAYS).empty?
raise ArgumentError, "day property invalid. Only valid values are: #{VALID_WEEK_DAYS.map(&:upcase).join(", ")}. Multiple values must be separated by a comma."
end
end
end
end
def validate_create_months(months, frequency)
raise ArgumentError, "months property is only valid for tasks that run monthly" if frequency != :monthly
if months.is_a?(String)
months = months.split(",")
months.map! { |month| month.strip.upcase }
unless (months - VALID_MONTHS).empty?
raise ArgumentError, "months property invalid. Only valid values are: #{VALID_MONTHS.join(", ")}. Multiple values must be separated by a comma."
end
end
end
# This method returns true if day has values from 1-31 which is a days of moths and used with frequency :monthly
def days_includes_days_of_months?(days)
days.map! { |day| day.to_s.strip.downcase }
(days - VALID_DAYS_OF_MONTH).empty?
end
def validate_idle_time(idle_time, frequency)
if !idle_time.nil? && frequency != :on_idle
raise ArgumentError, "idle_time property is only valid for tasks that run on_idle"
end
if idle_time.nil? && frequency == :on_idle
raise ArgumentError, "idle_time value should be set for :on_idle frequency."
end
unless idle_time.nil? || idle_time > 0 && idle_time <= 999
raise ArgumentError, "idle_time value #{idle_time} is invalid. Valid values for :on_idle frequency are 1 - 999."
end
end
# Converts the number of seconds to an ISO8601 duration format and returns it.
# Ref : https://github.com/arnau/ISO8601/blob/master/lib/iso8601/duration.rb#L18-L23
# e.g.
# ISO8601::Duration.new(65707200).to_s
# returns 'PT65707200S'
def sec_to_dur(seconds)
ISO8601::Duration.new(seconds.to_i).to_s
end
def sec_to_min(seconds)
seconds.to_i / 60
end
end
end
end