summaryrefslogtreecommitdiff
path: root/lib/chef/provider/windows_task.rb
diff options
context:
space:
mode:
Diffstat (limited to 'lib/chef/provider/windows_task.rb')
-rw-r--r--lib/chef/provider/windows_task.rb587
1 files changed, 587 insertions, 0 deletions
diff --git a/lib/chef/provider/windows_task.rb b/lib/chef/provider/windows_task.rb
new file mode 100644
index 0000000000..9a6fd39582
--- /dev/null
+++ b/lib/chef/provider/windows_task.rb
@@ -0,0 +1,587 @@
+#
+# Author:: Nimisha Sharad (<nimisha.sharad@msystechnologies.com>)
+# Copyright:: Copyright 2008-2018, 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 "chef/mixin/shell_out"
+require "rexml/document"
+require "iso8601"
+require "chef/mixin/powershell_out"
+require "chef/provider"
+require "win32/taskscheduler" if Chef::Platform.windows?
+
+class Chef
+ class Provider
+ class WindowsTask < Chef::Provider
+ include Chef::Mixin::ShellOut
+ include Chef::Mixin::PowershellOut
+
+ if Chef::Platform.windows?
+ include Win32
+
+ provides :windows_task
+
+ MONTHS = {
+ JAN: TaskScheduler::JANUARY,
+ FEB: TaskScheduler::FEBRUARY,
+ MAR: TaskScheduler::MARCH,
+ APR: TaskScheduler::APRIL,
+ MAY: TaskScheduler::MAY,
+ JUN: TaskScheduler::JUNE,
+ JUL: TaskScheduler::JULY,
+ AUG: TaskScheduler::AUGUST,
+ SEP: TaskScheduler::SEPTEMBER,
+ OCT: TaskScheduler::OCTOBER,
+ NOV: TaskScheduler::NOVEMBER,
+ DEC: TaskScheduler::DECEMBER
+ }
+
+ DAYS_OF_WEEK = { MON: TaskScheduler::MONDAY,
+ TUE: TaskScheduler::TUESDAY,
+ WED: TaskScheduler::WEDNESDAY,
+ THU: TaskScheduler::THURSDAY,
+ FRI: TaskScheduler::FRIDAY,
+ SAT: TaskScheduler::SATURDAY,
+ SUN: TaskScheduler::SUNDAY }
+
+ WEEKS_OF_MONTH = {
+ FIRST: TaskScheduler::FIRST_WEEK,
+ SECOND: TaskScheduler::SECOND_WEEK,
+ THIRD: TaskScheduler::THIRD_WEEK,
+ FOURTH: TaskScheduler::FOURTH_WEEK
+ }
+
+ DAYS_OF_MONTH = {
+ 1 => TaskScheduler::TASK_FIRST,
+ 2 => TaskScheduler::TASK_SECOND,
+ 3 => TaskScheduler::TASK_THIRD,
+ 4 => TaskScheduler::TASK_FOURTH,
+ 5 => TaskScheduler::TASK_FIFTH,
+ 6 => TaskScheduler::TASK_SIXTH,
+ 7 => TaskScheduler::TASK_SEVENTH,
+ 8 => TaskScheduler::TASK_EIGHTH,
+ 9 => TaskScheduler::TASK_NINETH,
+ 10 => TaskScheduler::TASK_TENTH,
+ 11 => TaskScheduler::TASK_ELEVENTH,
+ 12 => TaskScheduler::TASK_TWELFTH,
+ 13 => TaskScheduler::TASK_THIRTEENTH,
+ 14 => TaskScheduler::TASK_FOURTEENTH,
+ 15 => TaskScheduler::TASK_FIFTEENTH,
+ 16 => TaskScheduler::TASK_SIXTEENTH,
+ 17 => TaskScheduler::TASK_SEVENTEENTH,
+ 18 => TaskScheduler::TASK_EIGHTEENTH,
+ 19 => TaskScheduler::TASK_NINETEENTH,
+ 20 => TaskScheduler::TASK_TWENTIETH,
+ 21 => TaskScheduler::TASK_TWENTY_FIRST,
+ 22 => TaskScheduler::TASK_TWENTY_SECOND,
+ 23 => TaskScheduler::TASK_TWENTY_THIRD,
+ 24 => TaskScheduler::TASK_TWENTY_FOURTH,
+ 25 => TaskScheduler::TASK_TWENTY_FIFTH,
+ 26 => TaskScheduler::TASK_TWENTY_SIXTH,
+ 27 => TaskScheduler::TASK_TWENTY_SEVENTH,
+ 28 => TaskScheduler::TASK_TWENTY_EIGHTH,
+ 29 => TaskScheduler::TASK_TWENTY_NINTH,
+ 30 => TaskScheduler::TASK_THIRTYETH,
+ 31 => TaskScheduler::TASK_THIRTY_FIRST
+ }
+
+ def load_current_resource
+ @current_resource = Chef::Resource::WindowsTask.new(new_resource.name)
+ task = TaskScheduler.new
+ if task.exists?(new_resource.task_name)
+ @current_resource.exists = true
+ task.get_task(new_resource.task_name)
+ @current_resource.task = task
+ pathed_task_name = new_resource.task_name.start_with?('\\') ? new_resource.task_name : "\\#{new_resource.task_name}"
+ @current_resource.task_name(pathed_task_name)
+ else
+ @current_resource.exists = false
+ end
+ @current_resource
+ end
+
+ def action_create
+ if current_resource.exists
+ logger.trace "#{new_resource} task exist."
+ unless (task_needs_update?(current_resource.task)) || (new_resource.force)
+ logger.info "#{new_resource} task does not need updating and force is not specified - nothing to do"
+ return
+ end
+
+ # if start_day and start_time is not set by user current date and time will be set while updating any property
+ set_start_day_and_time unless new_resource.frequency == :none
+ update_task(current_resource.task)
+ else
+ basic_validation
+ set_start_day_and_time
+ converge_by("#{new_resource} task created") do
+ task = TaskScheduler.new
+ if new_resource.frequency == :none
+ task.new_work_item(new_resource.task_name, {})
+ task.activate(new_resource.task_name)
+ else
+ task.new_work_item(new_resource.task_name, trigger)
+ end
+ task.application_name = new_resource.command
+ task.working_directory = new_resource.cwd if new_resource.cwd
+ task.configure_settings(config_settings)
+ task.configure_principals(principal_settings)
+ task.set_account_information(new_resource.user, new_resource.password)
+ task.creator = new_resource.user
+ task.activate(new_resource.task_name)
+ end
+ end
+ end
+
+ def action_run
+ if current_resource.exists
+ logger.trace "#{new_resource} task exists"
+ if current_resource.task.status == "running"
+ logger.info "#{new_resource} task is currently running, skipping run"
+ else
+ converge_by("run scheduled task #{new_resource}") do
+ current_resource.task.run
+ end
+ end
+ else
+ logger.warn "#{new_resource} task does not exist - nothing to do"
+ end
+ end
+
+ def action_delete
+ if current_resource.exists
+ logger.trace "#{new_resource} task exists"
+ converge_by("delete scheduled task #{new_resource}") do
+ ts = TaskScheduler.new
+ ts.delete(current_resource.task_name)
+ end
+ else
+ logger.warn "#{new_resource} task does not exist - nothing to do"
+ end
+ end
+
+ def action_end
+ if current_resource.exists
+ logger.trace "#{new_resource} task exists"
+ if current_resource.task.status != "running"
+ logger.trace "#{new_resource} is not running - nothing to do"
+ else
+ converge_by("#{new_resource} task ended") do
+ current_resource.task.stop
+ end
+ end
+ else
+ logger.warn "#{new_resource} task does not exist - nothing to do"
+ end
+ end
+
+ def action_enable
+ if current_resource.exists
+ logger.trace "#{new_resource} task exists"
+ if current_resource.task.status == "not scheduled"
+ converge_by("#{new_resource} task enabled") do
+ #TODO wind32-taskscheduler currently not having any method to handle this so using schtasks.exe here
+ run_schtasks "CHANGE", "ENABLE" => ""
+ end
+ else
+ logger.trace "#{new_resource} already enabled - nothing to do"
+ end
+ else
+ logger.fatal "#{new_resource} task does not exist - nothing to do"
+ raise Errno::ENOENT, "#{new_resource}: task does not exist, cannot enable"
+ end
+ end
+
+ def action_disable
+ if current_resource.exists
+ logger.info "#{new_resource} task exists"
+ if %w{ready running}.include?(current_resource.task.status)
+ converge_by("#{new_resource} task disabled") do
+ #TODO: in win32-taskscheduler there is no method whcih disbales the task so currently calling disable with schtasks.exe
+ run_schtasks "CHANGE", "DISABLE" => ""
+ end
+ else
+ logger.warn "#{new_resource} already disabled - nothing to do"
+ end
+ else
+ logger.warn "#{new_resource} task does not exist - nothing to do"
+ end
+ end
+
+ alias_method :action_change, :action_create
+
+ private
+
+ def set_start_day_and_time
+ new_resource.start_day = Time.now.strftime("%m/%d/%Y") unless new_resource.start_day
+ new_resource.start_time = Time.now.strftime("%H:%M") unless new_resource.start_time
+ end
+
+ def update_task(task)
+ converge_by("#{new_resource} task updated") do
+ task.set_account_information(new_resource.user, new_resource.password)
+ task.application_name = new_resource.command if new_resource.command
+ task.working_directory = new_resource.cwd if new_resource.cwd
+ task.trigger = trigger unless new_resource.frequency == :none
+ task.configure_settings(config_settings)
+ task.creator = new_resource.user
+ task.configure_principals(principal_settings)
+ end
+ end
+
+ def trigger
+ start_month, start_day, start_year = new_resource.start_day.to_s.split("/")
+ start_hour, start_minute = new_resource.start_time.to_s.split(":")
+ #TODO currently end_month, end_year and end_year needs to be set to 0. If not set win32-taskscheduler throwing nil into integer error.
+ trigger_hash = {
+ start_year: start_year.to_i,
+ start_month: start_month.to_i,
+ start_day: start_day.to_i,
+ start_hour: start_hour.to_i,
+ start_minute: start_minute.to_i,
+ end_month: 0,
+ end_day: 0,
+ end_year: 0,
+ trigger_type: trigger_type,
+ type: type,
+ random_minutes_interval: new_resource.random_delay
+ }
+
+ if new_resource.frequency == :minute
+ trigger_hash[:minutes_interval] = new_resource.frequency_modifier
+ end
+
+ if new_resource.frequency == :hourly
+ minutes = convert_hours_in_minutes(new_resource.frequency_modifier.to_i)
+ trigger_hash[:minutes_interval] = minutes
+ end
+
+ if new_resource.minutes_interval
+ trigger_hash[:minutes_interval] = new_resource.minutes_interval
+ end
+
+ if new_resource.minutes_duration
+ trigger_hash[:minutes_duration] = new_resource.minutes_duration
+ end
+
+ if trigger_type == TaskScheduler::MONTHLYDOW && frequency_modifier_contains_last_week?(new_resource.frequency_modifier)
+ trigger_hash[:run_on_last_week_of_month] = true
+ else
+ trigger_hash[:run_on_last_week_of_month] = false
+ end
+
+ if trigger_type == TaskScheduler::MONTHLYDATE && day_includes_last_or_lastday?(new_resource.day)
+ trigger_hash[:run_on_last_day_of_month] = true
+ else
+ trigger_hash[:run_on_last_day_of_month] = false
+ end
+ trigger_hash
+ end
+
+ def frequency_modifier_contains_last_week?(frequency_modifier)
+ frequency_modifier = frequency_modifier.to_s.split(",")
+ frequency_modifier.map! { |value| value.strip.upcase }
+ frequency_modifier.include?("LAST")
+ end
+
+ def day_includes_last_or_lastday?(day)
+ day = day.to_s.split(",")
+ day.map! { |value| value.strip.upcase }
+ day.include?("LAST") || day.include?("LASTDAY")
+ end
+
+ def convert_hours_in_minutes(hours)
+ hours.to_i * 60 if hours
+ end
+
+ #TODO : Try to optimize this method
+ # known issue : Since start_day and time is not mandatory while updating weekly frequency for which start_day is not mentioned by user idempotency
+ # is not gettting maintained as new_resource.start_day is nil and we fetch the day of week from start_day to set and its currently coming as nil and don't match with current_task
+ def task_needs_update?(task)
+ flag = false
+ if new_resource.frequency == :none
+ flag = (task.account_information != new_resource.user ||
+ task.application_name != new_resource.command ||
+ task.principals[:run_level] != run_level)
+ else
+ current_task_trigger = task.trigger(0)
+ new_task_trigger = trigger
+ flag = (ISO8601::Duration.new(task.idle_settings[:idle_duration])) != (ISO8601::Duration.new(new_resource.idle_time * 60)) if new_resource.frequency == :on_idle
+ flag = (ISO8601::Duration.new(task.execution_time_limit)) != (ISO8601::Duration.new(new_resource.execution_time_limit * 60)) unless new_resource.execution_time_limit.nil?
+
+ # if trigger not found updating the task to add the trigger
+ if current_task_trigger.nil?
+ flag = true
+ else
+ flag = true if start_day_updated?(current_task_trigger, new_task_trigger) == true ||
+ start_time_updated?(current_task_trigger, new_task_trigger) == true ||
+ current_task_trigger[:trigger_type] != new_task_trigger[:trigger_type] ||
+ current_task_trigger[:type] != new_task_trigger[:type] ||
+ current_task_trigger[:random_minutes_interval].to_i != new_task_trigger[:random_minutes_interval].to_i ||
+ current_task_trigger[:minutes_interval].to_i != new_task_trigger[:minutes_interval].to_i ||
+ task.account_information != new_resource.user ||
+ task.application_name != new_resource.command ||
+ task.working_directory != new_resource.cwd.to_s ||
+ task.principals[:logon_type] != logon_type ||
+ task.principals[:run_level] != run_level
+
+ if trigger_type == TaskScheduler::MONTHLYDATE
+ flag = true if current_task_trigger[:run_on_last_day_of_month] != new_task_trigger[:run_on_last_day_of_month]
+ end
+
+ if trigger_type == TaskScheduler::MONTHLYDOW
+ flag = true if current_task_trigger[:run_on_last_week_of_month] != new_task_trigger[:run_on_last_week_of_month]
+ end
+ end
+ end
+ flag
+ end
+
+ def start_day_updated?(current_task_trigger, new_task_trigger)
+ ( new_resource.start_day && (current_task_trigger[:start_year].to_i != new_task_trigger[:start_year] ||
+ current_task_trigger[:start_month].to_i != new_task_trigger[:start_month] ||
+ current_task_trigger[:start_day].to_i != new_task_trigger[:start_day]) )
+ end
+
+ def start_time_updated?(current_task_trigger, new_task_trigger)
+ ( new_resource.start_time && ( current_task_trigger[:start_hour].to_i != new_task_trigger[:start_hour] ||
+ current_task_trigger[:start_minute].to_i != new_task_trigger[:start_minute] ) )
+ end
+
+ def trigger_type
+ case new_resource.frequency
+ when :once, :minute, :hourly
+ TaskScheduler::ONCE
+ when :daily
+ TaskScheduler::DAILY
+ when :weekly
+ TaskScheduler::WEEKLY
+ when :monthly
+ # If frequency modifier is set with frequency :monthly we are setting taskscheduler as monthlydow
+ # Ref https://msdn.microsoft.com/en-us/library/windows/desktop/aa382061(v=vs.85).aspx
+ new_resource.frequency_modifier.to_i.between?(1, 12) ? TaskScheduler::MONTHLYDATE : TaskScheduler::MONTHLYDOW
+ when :on_idle
+ TaskScheduler::ON_IDLE
+ when :onstart
+ TaskScheduler::AT_SYSTEMSTART
+ when :on_logon
+ TaskScheduler::AT_LOGON
+ else
+ raise ArgumentError, "Please set frequency"
+ end
+ end
+
+ def type
+ case trigger_type
+ when TaskScheduler::ONCE
+ { once: nil }
+ when TaskScheduler::DAILY
+ { days_interval: new_resource.frequency_modifier.to_i }
+ when TaskScheduler::WEEKLY
+ { weeks_interval: new_resource.frequency_modifier.to_i, days_of_week: days_of_week.to_i }
+ when TaskScheduler::MONTHLYDATE
+ { months: months_of_year.to_i, days: days_of_month.to_i }
+ when TaskScheduler::MONTHLYDOW
+ { months: months_of_year.to_i, days_of_week: days_of_week.to_i, weeks_of_month: weeks_of_month.to_i }
+ when TaskScheduler::ON_IDLE
+ # TODO: handle option for this trigger
+ when TaskScheduler::AT_LOGON
+ # TODO: handle option for this trigger
+ when TaskScheduler::AT_SYSTEMSTART
+ # TODO: handle option for this trigger
+ end
+ end
+
+ # Deleting last from the array of weeks of month since last week is handled in :run_on_last_week_of_month parameter.
+ def weeks_of_month
+ weeks_of_month = []
+ if new_resource.frequency_modifier
+ weeks = new_resource.frequency_modifier.split(",")
+ weeks.map! { |week| week.to_s.strip.upcase }
+ weeks.delete("LAST") if weeks.include?("LAST")
+ weeks_of_month = get_binary_values_from_constants(weeks, WEEKS_OF_MONTH)
+ end
+ weeks_of_month
+ end
+
+ # Deleting the "LAST" and "LASTDAY" from days since last day is handled in :run_on_last_day_of_month parameter.
+ def days_of_month
+ days_of_month = []
+ if new_resource.day
+ days = new_resource.day.split(",")
+ days.map! { |day| day.to_s.strip.upcase }
+ days.delete("LAST") if days.include?("LAST")
+ days.delete("LASTDAY") if days.include?("LASTDAY")
+ if days - (1..31).to_a
+ days.each do |day|
+ days_of_month << DAYS_OF_MONTH[day.to_i]
+ end
+ days_of_month = days_of_month.size > 1 ? days_of_month.inject(:|) : days_of_month[0]
+ end
+ else
+ days_of_month = DAYS_OF_MONTH[1]
+ end
+ days_of_month
+ end
+
+ def days_of_week
+ if new_resource.day
+ #this line of code is just to support backward compatibility of wild card *
+ new_resource.day = "mon, tue, wed, thu, fri, sat, sun" if new_resource.day == "*" && new_resource.frequency == :weekly
+ days = new_resource.day.split(",")
+ days.map! { |day| day.to_s.strip.upcase }
+ weeks_days = get_binary_values_from_constants(days, DAYS_OF_WEEK)
+ else
+ # following condition will make the frequency :weekly idempotent if start_day is not provided by user setting day as the current_resource day
+ if (current_resource) && (current_resource.task) && (current_resource.task.trigger(0)[:type][:days_of_week]) && (new_resource.start_day.nil?)
+ weeks_days = current_resource.task.trigger(0)[:type][:days_of_week]
+ else
+ day = get_day(new_resource.start_day).to_sym if new_resource.start_day
+ DAYS_OF_WEEK[day]
+ end
+ end
+ end
+
+ def months_of_year
+ months_of_year = []
+ if new_resource.frequency_modifier.to_i.between?(1, 12) && !(new_resource.months)
+ new_resource.months = set_months(new_resource.frequency_modifier.to_i)
+ end
+
+ if new_resource.months
+ #this line of code is just to support backward compatibility of wild card *
+ new_resource.months = "jan, feb, mar, apr, may, jun, jul, aug, sep, oct, nov, dec" if new_resource.months == "*" && new_resource.frequency == :monthly
+ months = new_resource.months.split(",")
+ months.map! { |month| month.to_s.strip.upcase }
+ months_of_year = get_binary_values_from_constants(months, MONTHS)
+ else
+ MONTHS.each do |key, value|
+ months_of_year << MONTHS[key]
+ end
+ months_of_year = months_of_year.inject(:|)
+ end
+ months_of_year
+ end
+
+ # This values are set for frequency_modifier set as 1-12
+ # This is to give backward compatibility validated this values with earlier code and running schtask.exe
+ # Used this as reference https://docs.microsoft.com/en-us/windows-server/administration/windows-commands/schtasks#d-dayday--
+ def set_months(frequency_modifier)
+ case frequency_modifier
+ when 1
+ "jan, feb, mar, apr, may, jun, jul, aug, sep, oct, nov, dec"
+ when 2
+ "feb, apr, jun, aug, oct, dec"
+ when 3
+ "mar, jun, sep, dec"
+ when 4
+ "apr, aug, dec"
+ when 5
+ "may, oct"
+ when 6
+ "jun, dec"
+ when 7
+ "jul"
+ when 8
+ "aug"
+ when 9
+ "sep"
+ when 10
+ "oct"
+ when 11
+ "nov"
+ when 12
+ "dec"
+ end
+ end
+
+ def get_binary_values_from_constants(array_values, constant)
+ data = []
+ array_values.each do |value|
+ value = value.to_sym
+ data << constant[value]
+ end
+ data.size > 1 ? data.inject(:|) : data[0]
+ end
+
+ def run_level
+ case new_resource.run_level
+ when :highest
+ TaskScheduler::TASK_RUNLEVEL_HIGHEST
+ when :limited
+ TaskScheduler::TASK_RUNLEVEL_LUA
+ end
+ end
+
+ #TODO: while creating the configuration settings win32-taskscheduler it accepts execution time limit values in ISO8601 formata
+ def config_settings
+ settings = {
+ execution_time_limit: new_resource.execution_time_limit,
+ enabled: true
+ }
+ settings[:idle_duration] = new_resource.idle_time if new_resource.idle_time
+ settings[:run_only_if_idle] = true if new_resource.idle_time
+ settings
+ end
+
+ def principal_settings
+ settings = {}
+ settings [:run_level] = run_level
+ settings[:logon_type] = logon_type
+ settings
+ end
+
+ def logon_type
+ # Ref: https://msdn.microsoft.com/en-us/library/windows/desktop/aa383566(v=vs.85).aspx
+ # if nothing is passed as logon_type the TASK_LOGON_SERVICE_ACCOUNT is getting set as default so using that for comparision.
+ new_resource.password.nil? ? TaskScheduler::TASK_LOGON_SERVICE_ACCOUNT : TaskScheduler::TASK_LOGON_PASSWORD
+ end
+
+ # This method checks if task and command attributes exist since those two are mandatory attributes to create a schedules task.
+ def basic_validation
+ validate = []
+ validate << "Command" if new_resource.command.nil? || new_resource.command.empty?
+ validate << "Task Name" if new_resource.task_name.nil? || new_resource.task_name.empty?
+ return true if validate.empty?
+ raise Chef::Exceptions::ValidationFailed.new "Value for '#{validate.join(', ')}' option cannot be empty"
+ end
+
+ # rubocop:disable Style/StringLiteralsInInterpolation
+ def run_schtasks(task_action, options = {})
+ cmd = "schtasks /#{task_action} /TN \"#{new_resource.task_name}\" "
+ options.each_key do |option|
+ unless option == "TR"
+ cmd += "/#{option} "
+ cmd += "\"#{options[option].to_s.gsub('"', "\\\"")}\" " unless options[option] == ""
+ end
+ end
+ # Appending Task Run [TR] option at the end since appending causing sometimes to append other options in option["TR"] value
+ if options["TR"]
+ cmd += "/TR \"#{options["TR"]} \" " unless task_action == "DELETE"
+ end
+ logger.trace("running: ")
+ logger.trace(" #{cmd}")
+ shell_out!(cmd, returns: [0])
+ end
+ # rubocop:enable Style/StringLiteralsInInterpolation
+
+ def get_day(date)
+ Date.strptime(date, "%m/%d/%Y").strftime("%a").upcase
+ end
+ end
+ end
+ end
+end