diff options
author | Thom May <thom@may.lt> | 2017-04-06 14:15:38 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2017-04-06 14:15:38 +0100 |
commit | 4e9305e8474a1faa72b7d89a2836a907909609af (patch) | |
tree | 717c9213fbb1861b90fdecba739b8fef42b84eac | |
parent | 1e6273029283af6efbedb8fb5ecc8840f90051dd (diff) | |
parent | d656b364bd9cd0cbe06eb9eee372997e6110e1ae (diff) | |
download | chef-4e9305e8474a1faa72b7d89a2836a907909609af.tar.gz |
Merge pull request #5886 from MsysTechnologiesllc/nim/windows_task
Adding windows_task resource
-rw-r--r-- | Gemfile.lock | 3 | ||||
-rw-r--r-- | chef.gemspec | 1 | ||||
-rw-r--r-- | lib/chef/provider/windows_task.rb | 418 | ||||
-rw-r--r-- | lib/chef/providers.rb | 1 | ||||
-rw-r--r-- | lib/chef/resource/windows_task.rb | 237 | ||||
-rw-r--r-- | lib/chef/resources.rb | 1 | ||||
-rw-r--r-- | spec/unit/provider/windows_task_spec.rb | 392 | ||||
-rw-r--r-- | spec/unit/resource/windows_task_spec.rb | 213 |
8 files changed, 1266 insertions, 0 deletions
diff --git a/Gemfile.lock b/Gemfile.lock index 13a6e8fdcd..2f1ebf16ef 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -123,6 +123,7 @@ PATH ffi-yajl (~> 2.2) highline (~> 1.6, >= 1.6.9) iniparse (~> 1.4) + iso8601 (~> 0.9.1) mixlib-archive (~> 0.4) mixlib-authentication (~> 1.4) mixlib-cli (~> 1.7) @@ -153,6 +154,7 @@ PATH ffi-yajl (~> 2.2) highline (~> 1.6, >= 1.6.9) iniparse (~> 1.4) + iso8601 (~> 0.9.1) mixlib-archive (~> 0.4) mixlib-authentication (~> 1.4) mixlib-cli (~> 1.7) @@ -331,6 +333,7 @@ GEM toml (~> 0.1) train (>= 0.22.0, < 1.0) ipaddress (0.8.3) + iso8601 (0.9.1) jmespath (1.3.1) json (2.0.3) kitchen-docker (2.6.0) diff --git a/chef.gemspec b/chef.gemspec index eab62b8eaa..93bfd26ea6 100644 --- a/chef.gemspec +++ b/chef.gemspec @@ -37,6 +37,7 @@ Gem::Specification.new do |s| s.add_dependency "plist", "~> 3.2" s.add_dependency "iniparse", "~> 1.4" s.add_dependency "addressable" + s.add_dependency "iso8601", "~> 0.9.1" # Audit mode requires these, so they are non-developmental dependencies now %w{rspec-core rspec-expectations rspec-mocks}.each { |gem| s.add_dependency gem, "~> 3.5" } diff --git a/lib/chef/provider/windows_task.rb b/lib/chef/provider/windows_task.rb new file mode 100644 index 0000000000..a96d4b2b7e --- /dev/null +++ b/lib/chef/provider/windows_task.rb @@ -0,0 +1,418 @@ +# +# Author:: Nimisha Sharad (<nimisha.sharad@msystechnologies.com>) +# Copyright:: Copyright 2008-2016, 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" + +class Chef + class Provider + class WindowsTask < Chef::Provider + include Chef::Mixin::ShellOut + include Chef::Mixin::PowershellOut + + provides :windows_task, os: "windows" + + def load_current_resource + @current_resource = Chef::Resource::WindowsTask.new(new_resource.name) + pathed_task_name = new_resource.task_name.start_with?('\\') ? new_resource.task_name : "\\#{new_resource.task_name}" + + @current_resource.task_name(pathed_task_name) + task_hash = load_task_hash(pathed_task_name) + + set_current_resource(task_hash) if task_hash.respond_to?(:[]) && task_hash[:TaskName] == pathed_task_name + @current_resource + end + + def set_current_resource(task_hash) + @current_resource.exists = true + @current_resource.command(task_hash[:TaskToRun]) + @current_resource.cwd(task_hash[:StartIn]) unless task_hash[:StartIn] == "N/A" + @current_resource.user(task_hash[:RunAsUser]) + set_current_run_level task_hash[:run_level] + set_current_frequency task_hash + @current_resource.day(task_hash[:day]) if task_hash[:day] + @current_resource.months(task_hash[:months]) if task_hash[:months] + set_current_idle_time(task_hash[:idle_time]) if task_hash[:idle_time] + @current_resource.random_delay(task_hash[:random_delay]) if task_hash[:random_delay] + @current_resource.execution_time_limit(task_hash[:execution_time_limit]) if task_hash[:execution_time_limit] + + @current_resource.status = :running if task_hash[:Status] == "Running" + @current_resource.enabled = true if task_hash[:ScheduledTaskState] == "Enabled" + end + + def action_create + if @current_resource.exists && !(task_need_update? || @new_resource.force) + Chef::Log.info "#{@new_resource} task already exists - nothing to do" + else + options = {} + options["F"] = "" if @new_resource.force || task_need_update? + options["SC"] = schedule + options["MO"] = @new_resource.frequency_modifier if frequency_modifier_allowed + options["I"] = @new_resource.idle_time unless @new_resource.idle_time.nil? + options["SD"] = @new_resource.start_day unless @new_resource.start_day.nil? + options["ST"] = @new_resource.start_time unless @new_resource.start_time.nil? + options["TR"] = @new_resource.command + options["RU"] = @new_resource.user + options["RP"] = @new_resource.password if use_password? + options["RL"] = "HIGHEST" if @new_resource.run_level == :highest + options["IT"] = "" if @new_resource.interactive_enabled + options["D"] = @new_resource.day if @new_resource.day + options["M"] = @new_resource.months unless @new_resource.months.nil? + + run_schtasks "CREATE", options + xml_options = [] + xml_options << "cwd" if new_resource.cwd + xml_options << "random_delay" if new_resource.random_delay + xml_options << "execution_time_limit" if new_resource.execution_time_limit + update_task_xml(xml_options) unless xml_options.empty? + + new_resource.updated_by_last_action true + Chef::Log.info "#{@new_resource} task created" + end + end + + def action_run + if @current_resource.exists + if @current_resource.status == :running + Chef::Log.info "#{@new_resource} task is currently running, skipping run" + else + run_schtasks "RUN" + new_resource.updated_by_last_action true + Chef::Log.info "#{@new_resource} task ran" + end + else + Chef::Log.warn "#{@new_resource} task doesn't exists - nothing to do" + end + end + + def action_delete + if @current_resource.exists + # always need to force deletion + run_schtasks "DELETE", "F" => "" + new_resource.updated_by_last_action true + Chef::Log.info "#{@new_resource} task deleted" + else + Chef::Log.warn "#{@new_resource} task doesn't exists - nothing to do" + end + end + + def action_end + if @current_resource.exists + if @current_resource.status != :running + Chef::Log.debug "#{@new_resource} is not running - nothing to do" + else + run_schtasks "END" + @new_resource.updated_by_last_action true + Chef::Log.info "#{@new_resource} task ended" + end + else + Chef::Log.warn "#{@new_resource} task doesn't exist - nothing to do" + end + end + + def action_enable + if @current_resource.exists + if @current_resource.enabled + Chef::Log.debug "#{@new_resource} already enabled - nothing to do" + else + run_schtasks "CHANGE", "ENABLE" => "" + @new_resource.updated_by_last_action true + Chef::Log.info "#{@new_resource} task enabled" + end + else + Chef::Log.fatal "#{@new_resource} task doesn't exist - nothing to do" + raise Errno::ENOENT, "#{@new_resource}: task does not exist, cannot enable" + end + end + + def action_disable + if @current_resource.exists + if @current_resource.enabled + run_schtasks "CHANGE", "DISABLE" => "" + @new_resource.updated_by_last_action true + Chef::Log.info "#{@new_resource} task disabled" + else + Chef::Log.warn "#{@new_resource} already disabled - nothing to do" + end + else + Chef::Log.warn "#{@new_resource} task doesn't exist - nothing to do" + end + end + + private + + # rubocop:disable Style/StringLiteralsInInterpolation + def run_schtasks(task_action, options = {}) + cmd = "schtasks /#{task_action} /TN \"#{@new_resource.task_name}\" " + options.keys.each do |option| + cmd += "/#{option} " + cmd += "\"#{options[option].to_s.gsub('"', "\\\"")}\" " unless options[option] == "" + end + Chef::Log.debug("running: ") + Chef::Log.debug(" #{cmd}") + shell_out!(cmd, returns: [0]) + end + # rubocop:enable Style/StringLiteralsInInterpolation + + def task_need_update? + return true if @current_resource.command != @new_resource.command.tr("'", '"') || + @current_resource.user != @new_resource.user || + @current_resource.run_level != @new_resource.run_level || + @current_resource.cwd != @new_resource.cwd || + @current_resource.frequency_modifier != @new_resource.frequency_modifier || + @current_resource.frequency != @new_resource.frequency || + @current_resource.idle_time != @new_resource.idle_time || + @current_resource.random_delay != @new_resource.random_delay || + @current_resource.execution_time_limit != @new_resource.execution_time_limit || + !@new_resource.start_day.nil? || !@new_resource.start_time.nil? + + begin + return true if @new_resource.day.to_s.casecmp(@current_resource.day.to_s) != 0 || + @new_resource.months.to_s.casecmp(@current_resource.months.to_s) != 0 + rescue + Chef::Log.debug "caught a raise in task_needs_update?" + end + + false + end + + def update_task_xml(options = []) + # random_delay xml element is different for different frequencies + random_delay_xml_element = { + :minute => "Triggers/TimeTrigger/RandomDelay", + :hourly => "Triggers/TimeTrigger/RandomDelay", + :once => "Triggers/TimeTrigger/RandomDelay", + :daily => "Triggers/CalendarTrigger/RandomDelay", + :weekly => "Triggers/CalendarTrigger/RandomDelay", + :monthly => "Triggers/CalendarTrigger/RandomDelay", + } + + xml_element_mapping = { + "cwd" => "Actions/Exec/WorkingDirectory", + "random_delay" => random_delay_xml_element[@new_resource.frequency], + "execution_time_limit" => "Settings/ExecutionTimeLimit", + } + + Chef::Log.debug "looking for existing tasks" + + task_script = <<-EOH + [Console]::OutputEncoding = [Text.UTF8Encoding]::UTF8 + schtasks /Query /TN \"#{@new_resource.task_name}\" /XML + EOH + xml_cmd = powershell_out(task_script) + + return if xml_cmd.exitstatus != 0 + + doc = REXML::Document.new(xml_cmd.stdout) + + options.each do |option| + Chef::Log.debug 'Removing former #{option} if any' + doc.root.elements.delete(xml_element_mapping[option]) + option_value = @new_resource.send("#{option}") + + if option_value + Chef::Log.debug "Setting #option as #option_value" + split_xml_path = xml_element_mapping[option].split("/") # eg. if xml_element_mapping[option] = "Actions/Exec/WorkingDirectory" + element_name = split_xml_path.last # element_name = "WorkingDirectory" + cwd_element = REXML::Element.new(element_name) + cwd_element.add_text(option_value) + element_root = (split_xml_path - [element_name]).join("/") # element_root = 'Actions/Exec' + exec_element = doc.root.elements[element_root] + exec_element.add_element(cwd_element) + end + end + + temp_task_file = ::File.join(ENV["TEMP"], "windows_task.xml") + begin + ::File.open(temp_task_file, "w:UTF-16LE") do |f| + doc.write(f) + end + + options = {} + options["RU"] = @new_resource.user if @new_resource.user + options["RP"] = @new_resource.password if @new_resource.password + options["IT"] = "" if @new_resource.interactive_enabled + options["XML"] = temp_task_file + + run_schtasks("DELETE", "F" => "") + run_schtasks("CREATE", options) + ensure + ::File.delete(temp_task_file) + end + end + + def load_task_hash(task_name) + Chef::Log.debug "Looking for existing tasks" + + task_script = <<-EOH + [Console]::OutputEncoding = [Text.UTF8Encoding]::UTF8 + schtasks /Query /FO LIST /V /TN \"#{task_name}\" + EOH + + output = powershell_out(task_script).stdout.force_encoding("UTF-8") + if output.empty? + task = false + else + task = {} + + output.split("\n").map! do |line| + line.split(": ").map!(&:strip) + end.each do |field| + if field.is_a?(Array) && field[0].respond_to?(:to_sym) + key = (field - [field.last]).join(": ") + task[key.gsub(/\s+/, "").to_sym] = field.last + end + end + end + + task_xml = load_task_xml task_name + task.merge!(task_xml) if task && task_xml + + task + end + + def load_task_xml(task_name) + task_script = <<-EOH + [Console]::OutputEncoding = [Text.UTF8Encoding]::UTF8 + schtasks /Query /TN \"#{task_name}\" /XML + EOH + xml_cmd = powershell_out(task_script) + + return if xml_cmd.exitstatus != 0 + + doc = REXML::Document.new(xml_cmd.stdout) + root = doc.root + + task = {} + task[:run_level] = root.elements["Principals/Principal/RunLevel"].text if root.elements["Principals/Principal/RunLevel"] + + # for frequency = :minutes, :hourly + task[:repetition_interval] = root.elements["Triggers/TimeTrigger/Repetition/Interval"].text if root.elements["Triggers/TimeTrigger/Repetition/Interval"] + + # for frequency = :daily + task[:schedule_by_day] = root.elements["Triggers/CalendarTrigger/ScheduleByDay/DaysInterval"].text if root.elements["Triggers/CalendarTrigger/ScheduleByDay/DaysInterval"] + + # for frequency = :weekly + task[:schedule_by_week] = root.elements["Triggers/CalendarTrigger/ScheduleByWeek/WeeksInterval"].text if root.elements["Triggers/CalendarTrigger/ScheduleByWeek/WeeksInterval"] + if root.elements["Triggers/CalendarTrigger/ScheduleByWeek/DaysOfWeek"] + task[:day] = [] + root.elements["Triggers/CalendarTrigger/ScheduleByWeek/DaysOfWeek"].elements.each do |e| + task[:day] << e.to_s[0..3].delete("<").delete("/>") + end + task[:day] = task[:day].join(", ") + end + + # for frequency = :monthly + task[:schedule_by_month] = root.elements["Triggers/CalendarTrigger/ScheduleByMonth/DaysOfMonth/Day"].text if root.elements["Triggers/CalendarTrigger/ScheduleByMonth/DaysOfMonth/Day"] + if root.elements["Triggers/CalendarTrigger/ScheduleByMonth/Months"] + task[:months] = [] + root.elements["Triggers/CalendarTrigger/ScheduleByMonth/Months"].elements.each do |e| + task[:months] << e.to_s[0..3].delete("<").delete("/>") + end + task[:months] = task[:months].join(", ") + end + + task[:on_logon] = true if root.elements["Triggers/LogonTrigger"] + task[:onstart] = true if root.elements["Triggers/BootTrigger"] + task[:on_idle] = true if root.elements["Triggers/IdleTrigger"] + + task[:idle_time] = root.elements["Settings/IdleSettings/Duration"].text if root.elements["Settings/IdleSettings/Duration"] && task[:on_idle] + + task[:once] = true if !(task[:repetition_interval] || task[:schedule_by_day] || task[:schedule_by_week] || task[:schedule_by_month] || task[:on_logon] || task[:onstart] || task[:on_idle]) + task[:execution_time_limit] = root.elements["Settings/ExecutionTimeLimit"].text if root.elements["Settings/ExecutionTimeLimit"] #by default PT72H + task[:random_delay] = root.elements["Triggers/TimeTrigger/RandomDelay"].text if root.elements["Triggers/TimeTrigger/RandomDelay"] + task[:random_delay] = root.elements["Triggers/CalendarTrigger/RandomDelay"].text if root.elements["Triggers/CalendarTrigger/RandomDelay"] + task + end + + SYSTEM_USERS = ['NT AUTHORITY\SYSTEM', "SYSTEM", 'NT AUTHORITY\LOCALSERVICE', 'NT AUTHORITY\NETWORKSERVICE', 'BUILTIN\USERS', "USERS"].freeze + + def use_password? + @use_password ||= !SYSTEM_USERS.include?(@new_resource.user.upcase) + end + + def schedule + case @new_resource.frequency + when :on_logon + "ONLOGON" + when :on_idle + "ONIDLE" + else + @new_resource.frequency + end + end + + def frequency_modifier_allowed + case @new_resource.frequency + when :minute, :hourly, :daily, :weekly + true + when :monthly + @new_resource.months.nil? || %w{ FIRST SECOND THIRD FOURTH LAST LASTDAY }.include?(@new_resource.frequency_modifier) + else + false + end + end + + def set_current_run_level(run_level) + case run_level + when "HighestAvailable" + @current_resource.run_level(:highest) + when "LeastPrivilege" + @current_resource.run_level(:limited) + end + end + + def set_current_frequency(task_hash) + if task_hash[:repetition_interval] + duration = ISO8601::Duration.new(task_hash[:repetition_interval]) + if task_hash[:repetition_interval].include?("M") + @current_resource.frequency(:minute) + @current_resource.frequency_modifier(duration.minutes.atom.to_i) + elsif task_hash[:repetition_interval].include?("H") + @current_resource.frequency(:hourly) + @current_resource.frequency_modifier(duration.hours.atom.to_i) + end + end + + if task_hash[:schedule_by_day] + @current_resource.frequency(:daily) + @current_resource.frequency_modifier(task_hash[:schedule_by_day].to_i) + end + + if task_hash[:schedule_by_week] + @current_resource.frequency(:weekly) + @current_resource.frequency_modifier(task_hash[:schedule_by_week].to_i) + end + + @current_resource.frequency(:monthly) if task_hash[:schedule_by_month] + @current_resource.frequency(:on_logon) if task_hash[:on_logon] + @current_resource.frequency(:onstart) if task_hash[:onstart] + @current_resource.frequency(:on_idle) if task_hash[:on_idle] + @current_resource.frequency(:once) if task_hash[:once] + end + + def set_current_idle_time(idle_time) + duration = ISO8601::Duration.new(idle_time) + @current_resource.idle_time(duration.minutes.atom.to_i) + end + + end + end +end diff --git a/lib/chef/providers.rb b/lib/chef/providers.rb index 0f19f56a8f..41de44a1d6 100644 --- a/lib/chef/providers.rb +++ b/lib/chef/providers.rb @@ -59,6 +59,7 @@ require "chef/provider/template" require "chef/provider/user" require "chef/provider/whyrun_safe_ruby_block" require "chef/provider/yum_repository" +require "chef/provider/windows_task" require "chef/provider/env/windows" diff --git a/lib/chef/resource/windows_task.rb b/lib/chef/resource/windows_task.rb new file mode 100644 index 0000000000..25e76f4220 --- /dev/null +++ b/lib/chef/resource/windows_task.rb @@ -0,0 +1,237 @@ +# +# Author:: Nimisha Sharad (<nimisha.sharad@msystechnologies.com>) +# Copyright:: Copyright 2008-2016, 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/resource" + +class Chef + class Resource + class WindowsTask < Chef::Resource + + provides :windows_task, os: "windows" + + allowed_actions :create, :delete, :run, :end, :change, :enable, :disable + default_action :create + + def initialize(name, run_context = nil) + super + @resource_name = :windows_task + @task_name = name + @action = :create + end + + property :task_name, String, regex: [/\A[^\/\:\*\?\<\>\|]+\z/] + property :command, String + property :cwd, String + property :user, String, default: "SYSTEM" + property :password, String + property :run_level, equal_to: [:highest, :limited], default: :limited + property :force, [TrueClass, FalseClass], default: false + property :interactive_enabled, [TrueClass, FalseClass], default: false + property :frequency_modifier, [Integer, String], default: 1 + property :frequency, equal_to: [:minute, + :hourly, + :daily, + :weekly, + :monthly, + :once, + :on_logon, + :onstart, + :on_idle], default: :hourly + property :start_day, String + property :start_time, String + property :day, [String, Integer] + property :months, String + property :idle_time, Integer + property :random_delay, String + property :execution_time_limit, String + + attr_accessor :exists, :status, :enabled + + def after_created + if random_delay + validate_random_delay(random_delay, frequency) + duration = sec_to_dur(random_delay) + random_delay(duration) + end + + if execution_time_limit + raise ArgumentError, "Invalid value passed for `execution_time_limit`. Please pass seconds as a String e.g. '60'." if execution_time_limit.to_i == 0 + duration = sec_to_dur(execution_time_limit) + execution_time_limit(duration) + else + # schtask sets execution_time_limit as PT72H by default + # We are setting the default value here so that we can do idempotency check later + # Note: We can't use `default` in the property + # because it will raise error for Invalid values passed as "PT72H" is not in seconds + execution_time_limit("PT72H") + end + + validate_start_time(start_time) if frequency == :once + validate_start_day(start_day, frequency) if start_day + validate_user_and_password(user, password) + validate_interactive_setting(interactive_enabled, password) + validate_create_frequency_modifier(frequency, frequency_modifier) + validate_create_day(day, frequency) if day + validate_create_months(months, frequency) if months + validate_idle_time(idle_time, frequency) if idle_time + end + + private + + def validate_random_delay(random_delay, frequency) + if [:once, :on_logon, :onstart, :on_idle].include? frequency + raise ArgumentError, "`random_delay` property is supported only for frequency :minute, :hourly, :daily, :weekly and :monthly" + end + + raise ArgumentError, "Invalid value passed for `random_delay`. Please pass seconds as a String e.g. '60'." if random_delay.to_i == 0 + end + + def validate_start_day(start_day, frequency) + if [:once, :on_logon, :onstart, :on_idle].include? frequency + raise ArgumentError, "`start_day` property is not supported with frequency: #{frequency}" + end + end + + def validate_start_time(start_time) + raise ArgumentError, "`start_time` needs to be provided with `frequency :once`" unless start_time + end + + SYSTEM_USERS = ['NT AUTHORITY\SYSTEM', "SYSTEM", 'NT AUTHORITY\LOCALSERVICE', 'NT AUTHORITY\NETWORKSERVICE', 'BUILTIN\USERS', "USERS"].freeze + + def validate_user_and_password(user, password) + if user && use_password?(user) + if password.nil? + raise ArgumentError, "Can't specify a non-system user without a password!" + end + end + end + + def use_password?(user) + @use_password ||= !SYSTEM_USERS.include?(user.upcase) + end + + def validate_interactive_setting(interactive_enabled, password) + if interactive_enabled && password.nil? + raise ArgumentError, "Please provide the password when attempting to set interactive/non-interactive." + end + end + + def validate_create_frequency_modifier(frequency, frequency_modifier) + # Currently is handled in create action 'frequency_modifier_allowed' line. Does not allow for frequency_modifier for once,onstart,onlogon,onidle + # Note that 'OnEvent' is not a supported frequency. + unless frequency.nil? || frequency_modifier.nil? + case frequency + when :minute + unless frequency_modifier.to_i > 0 && frequency_modifier.to_i <= 1439 + raise ArgumentError, "frequency_modifier value #{frequency_modifier} is invalid. Valid values for :minute frequency are 1 - 1439." + end + when :hourly + unless frequency_modifier.to_i > 0 && frequency_modifier.to_i <= 23 + raise ArgumentError, "frequency_modifier value #{frequency_modifier} is invalid. Valid values for :hourly frequency are 1 - 23." + end + when :daily + unless frequency_modifier.to_i > 0 && frequency_modifier.to_i <= 365 + raise ArgumentError, "frequency_modifier value #{frequency_modifier} is invalid. Valid values for :daily frequency are 1 - 365." + end + when :weekly + unless frequency_modifier.to_i > 0 && frequency_modifier.to_i <= 52 + raise ArgumentError, "frequency_modifier value #{frequency_modifier} is invalid. Valid values for :weekly frequency are 1 - 52." + end + when :monthly + unless ("1".."12").to_a.push("FIRST", "SECOND", "THIRD", "FOURTH", "LAST", "LASTDAY").include?(frequency_modifier.to_s.upcase) + raise ArgumentError, "frequency_modifier value #{frequency_modifier} is invalid. Valid values for :monthly frequency are 1 - 12, 'FIRST', 'SECOND', 'THIRD', 'FOURTH', 'LAST', 'LASTDAY'." + end + end + end + end + + def validate_create_day(day, frequency) + unless [:weekly].include?(frequency) + raise "day attribute is only valid for tasks that run weekly" + end + if day.is_a?(String) && day.to_i.to_s != day + days = day.split(",") + days.each do |d| + unless ["mon", "tue", "wed", "thu", "fri", "sat", "sun", "*"].include?(d.strip.downcase) + raise "day attribute invalid. Only valid values are: MON, TUE, WED, THU, FRI, SAT, SUN and *. Multiple values must be separated by a comma." + end + end + end + end + + def validate_create_months(months, frequency) + unless [:monthly].include?(frequency) + raise "months attribute is only valid for tasks that run monthly" + end + if months.is_a? String + months.split(",").each do |month| + unless ["JAN", "FEB", "MAR", "APR", "MAY", "JUN", "JUL", "AUG", "SEP", "OCT", "NOV", "DEC", "*"].include?(month.strip.upcase) + raise "months attribute invalid. Only valid values are: JAN, FEB, MAR, APR, MAY, JUN, JUL, AUG, SEP, OCT, NOV, DEC and *. Multiple values must be separated by a comma." + end + end + end + end + + def validate_idle_time(idle_time, frequency) + unless [:on_idle].include?(frequency) + raise "idle_time attribute is only valid for tasks that run on_idle" + end + + unless idle_time.to_i > 0 && idle_time.to_i <= 999 + raise "idle_time value #{idle_time} is invalid. Valid values for :on_idle frequency are 1 - 999." + end + end + + # Convert the number of seconds to an ISO8601 duration format + # @see http://tools.ietf.org/html/rfc2445#section-4.3.6 + # @param [Integer] seconds The amount of seconds for this duration + def sec_to_dur(seconds) + seconds = seconds.to_i + return if seconds == 0 + iso_str = "P" + if seconds > 604_800 # more than a week + weeks = seconds / 604_800 + seconds -= (604_800 * weeks) + iso_str << "#{weeks}W" + end + if seconds > 86_400 # more than a day + days = seconds / 86_400 + seconds -= (86_400 * days) + iso_str << "#{days}D" + end + if seconds > 0 + iso_str << "T" + if seconds > 3600 # more than an hour + hours = seconds / 3600 + seconds -= (3600 * hours) + iso_str << "#{hours}H" + end + if seconds > 60 # more than a minute + minutes = seconds / 60 + seconds -= (60 * minutes) + iso_str << "#{minutes}M" + end + iso_str << "#{seconds}S" + end + + iso_str + end + + end + end +end diff --git a/lib/chef/resources.rb b/lib/chef/resources.rb index a254fa601f..9f87cb2454 100644 --- a/lib/chef/resources.rb +++ b/lib/chef/resources.rb @@ -98,3 +98,4 @@ require "chef/resource/zypper_package" require "chef/resource/cab_package" require "chef/resource/powershell_package" require "chef/resource/msu_package" +require "chef/resource/windows_task" diff --git a/spec/unit/provider/windows_task_spec.rb b/spec/unit/provider/windows_task_spec.rb new file mode 100644 index 0000000000..80038aa6db --- /dev/null +++ b/spec/unit/provider/windows_task_spec.rb @@ -0,0 +1,392 @@ +# +# Author:: Nimisha Sharad (<nimisha.sharad@msystechnologies.com>) +# Copyright:: Copyright 2008-2016, 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 "spec_helper" + +describe Chef::Provider::WindowsTask do + let(:new_resource) { Chef::Resource::WindowsTask.new("sample_task") } + + let(:provider) do + node = Chef::Node.new + events = Chef::EventDispatch::Dispatcher.new + run_context = Chef::RunContext.new(node, {}, events) + Chef::Provider::WindowsTask.new(new_resource, run_context) + end + + let(:task_hash) do + { + :"" => "", + :Folder => "\\", + :HostName => "NIMISHA-PC", + :TaskName => "\\sample_task", + :NextRunTime => "3/30/2017 2:42:00 PM", + :Status => "Ready", + :LogonMode => "Interactive/Background", + :LastRunTime => "3/30/2017 2:27:00 PM", + :LastResult => "1", + :Author => "Administrator", + :TaskToRun => "chef-client", + :StartIn => "N/A", + :Comment => "N/A", + :ScheduledTaskState => "Enabled", + :IdleTime => "Disabled", + :PowerManagement => "Stop On Battery Mode, No Start On Batteries", + :RunAsUser => "SYSTEM", + :DeleteTaskIfNotRescheduled => "Enabled", + :StopTaskIfRunsXHoursandXMins => "72:00:00", + :Schedule => "Scheduling data is not available in this format.", + :ScheduleType => "One Time Only, Minute", + :StartTime => "1:12:00 PM", + :StartDate => "3/30/2017", + :EndDate => "N/A", + :Days => "N/A", + :Months => "N/A", + :"Repeat:Every" => "0 Hour(s), 15 Minute(s)", + :"Repeat:Until:Time" => "None", + :"Repeat:Until:Duration" => "Disabled", + :"Repeat:StopIfStillRunning" => "Disabled", + :run_level => "HighestAvailable", + :repetition_interval => "PT15M", + :execution_time_limit => "PT72H", + } + end + + let(:task_xml) do + "<?xml version=\"1.0\" encoding=\"UTF-16\"?>\r\r\n<Task version=\"1.2\" xmlns=\"http://schemas.microsoft.com/windows/2004/02/mit/task\">\r\r\n <RegistrationInfo>\r\r\n <Date>2017-03-31T15:34:44</Date>\r\r\n <Author>Administrator</Author>\r\r\n </RegistrationInfo>\r\r\n<Triggers>\r\r\n <TimeTrigger>\r\r\n <Repetition>\r\r\n <Interval>PT15M</Interval>\r\r\n <StopAtDurationEnd>false</StopAtDurationEnd>\r\r\n </Repetition>\r\r\n <StartBoundary>2017-03-31T15:34:00</StartBoundary>\r\r\n <Enabled>true</Enabled>\r\r\n </TimeTrigger>\r\r\n </Triggers>\r\r\n <Principals>\r\r\n <Principal id=\"Author\">\r\r\n <RunLevel>HighestAvailable</RunLevel>\r\r\n <UserId>S-1-5-18</UserId>\r\r\n </Principal>\r\r\n </Principals>\r\r\n <Settings>\r\r\n <MultipleInstancesPolicy>IgnoreNew</MultipleInstancesPolicy>\r\r\n <DisallowStartIfOnBatteries>true</DisallowStartIfOnBatteries>\r\r\n <StopIfGoingOnBatteries>true</StopIfGoingOnBatteries>\r\r\n <AllowHardTerminate>true</AllowHardTerminate>\r\r\n <StartWhenAvailable>false</StartWhenAvailable>\r\r\n <RunOnlyIfNetworkAvailable>false</RunOnlyIfNetworkAvailable>\r\r\n <IdleSettings>\r\r\n <Duration>PT10M</Duration>\r\r\n<WaitTimeout>PT1H</WaitTimeout>\r\r\n <StopOnIdleEnd>true</StopOnIdleEnd>\r\r\n <RestartOnIdle>false</RestartOnIdle>\r\r\n </IdleSettings>\r\r\n <AllowStartOnDemand>true</AllowStartOnDemand>\r\r\n <Enabled>true</Enabled>\r\r\n <Hidden>false</Hidden>\r\r\n<RunOnlyIfIdle>false</RunOnlyIfIdle>\r\r\n <WakeToRun>false</WakeToRun>\r\r\n <ExecutionTimeLimit>PT72H</ExecutionTimeLimit>\r\r\n<Priority>7</Priority>\r\r\n </Settings>\r\r\n <Actions Context=\"Author\">\r\r\n <Exec>\r\r\n <Command>chef-client</Command>\r\r\n </Exec>\r\r\n </Actions>\r\r\n</Task>" + end + + describe "#load_current_resource" do + it "returns a current_resource" do + allow(provider).to receive(:load_task_hash) + expect(provider.load_current_resource).to be_kind_of(Chef::Resource::WindowsTask) + end + + context "if the given task name already exists" do + before do + allow(provider).to receive(:load_task_hash).and_return({ :TaskName => "\\sample_task" }) + end + + it "calls set_current_resource" do + expect(provider).to receive(:set_current_resource) + provider.load_current_resource + end + end + + it "sets the attributes of current_resource" do + allow(provider).to receive(:load_task_hash).and_return(task_hash) + current_resource = provider.load_current_resource + expect(current_resource.exists).to be(true) + expect(current_resource.command).to eq("chef-client") + expect(current_resource.user).to eq("SYSTEM") + expect(current_resource.run_level).to eq(:highest) + expect(current_resource.frequency).to eq(:minute) + expect(current_resource.frequency_modifier).to eq(15) + expect(current_resource.execution_time_limit).to eq("PT72H") + expect(current_resource.enabled).to be(true) + end + end + + describe "#action_create" do + it "doesn't create the same task if it's already existing" do + allow(provider).to receive(:load_task_hash).and_return(task_hash) + provider.load_current_resource + allow(provider).to receive(:task_need_update?).and_return(false) + provider.run_action(:create) + expect(new_resource).not_to be_updated_by_last_action + end + + context "when task is not existing" do + before do + allow(provider).to receive(:load_task_hash) + provider.load_current_resource + end + + it "creates the task if it's not already existing" do + allow(provider).to receive(:task_need_update?).and_return(true) + expect(provider).to receive(:run_schtasks).with("CREATE", { "F" => "", "SC" => :hourly, "MO" => 1, "TR" => nil, "RU" => "SYSTEM" }) + provider.run_action(:create) + expect(new_resource).to be_updated_by_last_action + end + + it "updates the task XML if random_delay is provided" do + new_resource.random_delay "20" + allow(provider).to receive(:task_need_update?).and_return(true) + expect(provider).to receive(:run_schtasks).with("CREATE", { "F" => "", "SC" => :hourly, "MO" => 1, "TR" => nil, "RU" => "SYSTEM" }) + expect(provider).to receive(:update_task_xml) + provider.run_action(:create) + expect(new_resource).to be_updated_by_last_action + end + + it "updates the task XML if execution_time_limit is provided" do + new_resource.execution_time_limit "20" + allow(provider).to receive(:task_need_update?).and_return(true) + expect(provider).to receive(:run_schtasks).with("CREATE", { "F" => "", "SC" => :hourly, "MO" => 1, "TR" => nil, "RU" => "SYSTEM" }) + expect(provider).to receive(:update_task_xml) + provider.run_action(:create) + expect(new_resource).to be_updated_by_last_action + end + end + end + + describe "#action_run" do + it "does nothing if the task doesn't exist" do + allow(provider).to receive(:load_task_hash) + provider.load_current_resource + provider.run_action(:run) + expect(new_resource).not_to be_updated_by_last_action + end + + context "when the task exists" do + it "does nothing if the task is already running" do + task_hash[:Status] = "Running" + allow(provider).to receive(:load_task_hash).and_return(task_hash) + provider.load_current_resource + provider.run_action(:run) + expect(new_resource).not_to be_updated_by_last_action + end + + it "runs the task" do + allow(provider).to receive(:load_task_hash).and_return(task_hash) + provider.load_current_resource + expect(provider).to receive(:run_schtasks).with("RUN") + provider.run_action(:run) + expect(new_resource).to be_updated_by_last_action + end + end + end + + describe "#action_delete" do + it "deletes the task if it exists" do + allow(provider).to receive(:load_task_hash).and_return(task_hash) + provider.load_current_resource + expect(provider).to receive(:run_schtasks).with("DELETE", { "F" => "" }) + provider.run_action(:delete) + expect(new_resource).to be_updated_by_last_action + end + + it "does nothing if the task doesn't exist" do + allow(provider).to receive(:load_task_hash) + provider.load_current_resource + provider.run_action(:delete) + expect(new_resource).not_to be_updated_by_last_action + end + end + + describe "#action_end" do + it "does nothing if the task doesn't exist" do + allow(provider).to receive(:load_task_hash) + provider.load_current_resource + provider.run_action(:end) + expect(new_resource).not_to be_updated_by_last_action + end + + context "when the task exists" do + it "does nothing if the task is not running" do + allow(provider).to receive(:load_task_hash).and_return(task_hash) + provider.load_current_resource + provider.run_action(:end) + expect(new_resource).not_to be_updated_by_last_action + end + + it "ends the task if it's running" do + task_hash[:Status] = "Running" + allow(provider).to receive(:load_task_hash).and_return(task_hash) + provider.load_current_resource + expect(provider).to receive(:run_schtasks).with("END") + provider.run_action(:end) + expect(new_resource).to be_updated_by_last_action + end + end + end + + describe "#action_enable" do + it "raises error if the task doesn't exist" do + allow(provider).to receive(:load_task_hash) + provider.load_current_resource + expect { provider.run_action(:enable) }.to raise_error(Errno::ENOENT) + end + + context "when the task exists" do + it "does nothing if the task is already enabled" do + allow(provider).to receive(:load_task_hash).and_return(task_hash) + provider.load_current_resource + provider.run_action(:enable) + expect(new_resource).not_to be_updated_by_last_action + end + + it "enables the task if it exists" do + task_hash[:ScheduledTaskState] = "Disabled" + allow(provider).to receive(:load_task_hash).and_return(task_hash) + provider.load_current_resource + expect(provider).to receive(:run_schtasks).with("CHANGE", { "ENABLE" => "" }) + provider.run_action(:enable) + expect(new_resource).to be_updated_by_last_action + end + end + end + + describe "#action_disable" do + it "does nothing if the task doesn't exist" do + allow(provider).to receive(:load_task_hash) + provider.load_current_resource + provider.run_action(:disable) + expect(new_resource).not_to be_updated_by_last_action + end + + context "when the task exists" do + it "disables the task if it's enabled" do + allow(provider).to receive(:load_task_hash).and_return(task_hash) + provider.load_current_resource + expect(provider).to receive(:run_schtasks).with("CHANGE", { "DISABLE" => "" }) + provider.run_action(:disable) + expect(new_resource).to be_updated_by_last_action + end + + it "does nothing if the task is already disabled" do + task_hash[:ScheduledTaskState] = "Disabled" + allow(provider).to receive(:load_task_hash).and_return(task_hash) + provider.load_current_resource + provider.run_action(:disable) + expect(new_resource).not_to be_updated_by_last_action + end + end + end + + describe "#run_schtasks" do + before do + @task_action = "CREATE" + @options = { "F" => "", "SC" => :minute, "MO" => 15, "TR" => "chef-client", "RU" => "SYSTEM", "RL" => "HIGHEST" } + @cmd = "schtasks /CREATE /TN \"sample_task\" /F /SC \"minute\" /MO \"15\" /TR \"chef-client\" /RU \"SYSTEM\" /RL \"HIGHEST\" " + end + + it "forms the command properly from the given options" do + expect(provider).to receive(:shell_out!).with(@cmd, { :returns => [0] }) + provider.send(:run_schtasks, @task_action, @options) + end + end + + describe "#task_need_update?" do + context "when task doesn't exist" do + before do + allow(provider).to receive(:load_task_hash) + provider.load_current_resource + end + + it "returns true" do + new_resource.command "chef-client" + expect(provider.send(:task_need_update?)).to be(true) + end + end + + context "when the task exists" do + before do + allow(provider).to receive(:load_task_hash).and_return(task_hash) + provider.load_current_resource + + new_resource.command "chef-client" + new_resource.run_level :highest + new_resource.frequency :minute + new_resource.frequency_modifier 15 + new_resource.user "SYSTEM" + new_resource.execution_time_limit "PT72H" + end + + context "when no attributes are modified" do + it "returns false" do + expect(provider.send(:task_need_update?)).to be(false) + end + end + + context "when frequency_modifier is updated" do + it "returns true" do + new_resource.frequency_modifier 25 + expect(provider.send(:task_need_update?)).to be(true) + end + end + + context "when months are updated" do + it "returns true" do + new_resource.months "JAN" + expect(provider.send(:task_need_update?)).to be(true) + end + end + end + end + + describe "#update_task_xml" do + before do + new_resource.command "chef-client" + new_resource.run_level :highest + new_resource.frequency :minute + new_resource.frequency_modifier 15 + new_resource.user "SYSTEM" + new_resource.random_delay "20" + end + + it "does nothing if the task doesn't exist" do + task_xml = double("xml", :exitstatus => 1) + allow(provider).to receive(:powershell_out).and_return(task_xml) + output = provider.send(:update_task_xml, ["random_delay"]) + expect(output).to be(nil) + end + + it "updates the task XML if random_delay is passed" do + shell_out_obj = double("xml", :exitstatus => 0, :stdout => task_xml) + allow(provider).to receive(:powershell_out).and_return(shell_out_obj) + expect(::File).to receive(:join) + expect(::File).to receive(:open) + expect(::File).to receive(:delete) + expect(provider).to receive(:run_schtasks).twice + output = provider.send(:update_task_xml, ["random_delay"]) + end + end + + describe "#load_task_hash" do + it "returns false if the task doesn't exist" do + allow(provider).to receive_message_chain(:powershell_out, :stdout, :force_encoding).and_return("") + allow(provider).to receive(:load_task_xml) + expect(provider.send(:load_task_hash, "chef-client")).to be(false) + end + + it "returns task hash if the task exists" do + powershell_output = "\r\nFolder: \\\r\nHostName: NIMISHA-PC\r\nTaskName: \\chef-client\r\n" + task_h = { :"" => "", :Folder => "\\", :HostName => "NIMISHA-PC", :TaskName => "\\chef-client" } + allow(provider).to receive_message_chain(:powershell_out, :stdout, :force_encoding).and_return(powershell_output) + allow(provider).to receive(:load_task_xml).with("chef-client") + expect(provider.send(:load_task_hash, "chef-client")).to eq(task_h) + end + end + + describe "#frequency_modifier_allowed" do + it "returns true for frequency :hourly" do + new_resource.frequency :hourly + expect(provider.send(:frequency_modifier_allowed)).to be(true) + end + + it "returns true for frequency :monthly if frequency_modifier is THIRD" do + new_resource.frequency :monthly + new_resource.frequency_modifier "THIRD" + expect(provider.send(:frequency_modifier_allowed)).to be(true) + end + + it "returns false for frequency :once" do + new_resource.frequency :once + expect(provider.send(:frequency_modifier_allowed)).to be(false) + end + end +end diff --git a/spec/unit/resource/windows_task_spec.rb b/spec/unit/resource/windows_task_spec.rb new file mode 100644 index 0000000000..fa2d458bbb --- /dev/null +++ b/spec/unit/resource/windows_task_spec.rb @@ -0,0 +1,213 @@ +# +# Author:: Nimisha Sharad (<nimisha.sharad@msystechnologies.com>) +# Copyright:: Copyright 2008-2017, 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 "spec_helper" + +describe Chef::Resource::WindowsTask do + let(:resource) { Chef::Resource::WindowsTask.new("sample_task") } + + it "creates a new Chef::Resource::WindowsTask" do + expect(resource).to be_a_kind_of(Chef::Resource) + expect(resource).to be_a_instance_of(Chef::Resource::WindowsTask) + end + + it "sets resource name as :windows_task" do + expect(resource.resource_name).to eql(:windows_task) + end + + it "sets the task_name as it's name" do + expect(resource.task_name).to eql("sample_task") + end + + it "sets the default action as :create" do + expect(resource.action).to eql(:create) + end + + it "sets the default user as System" do + expect(resource.user).to eql("SYSTEM") + end + + it "sets the default run_level as :limited" do + expect(resource.run_level).to eql(:limited) + end + + it "sets the default force as false" do + expect(resource.force).to eql(false) + end + + it "sets the default interactive_enabled as false" do + expect(resource.interactive_enabled).to eql(false) + end + + it "sets the default frequency_modifier as 1" do + expect(resource.frequency_modifier).to eql(1) + end + + it "sets the default frequency as :hourly" do + expect(resource.frequency).to eql(:hourly) + end + + context "when random_delay is passed" do + it "raises error if frequency is `:once`" do + resource.frequency :once + resource.random_delay "20" + expect { resource.after_created }.to raise_error(Chef::Exceptions::ArgumentError, "`random_delay` property is supported only for frequency :minute, :hourly, :daily, :weekly and :monthly") + end + + it "raises error for invalid random_delay" do + resource.frequency :monthly + resource.random_delay "xyz" + expect { resource.after_created }.to raise_error(Chef::Exceptions::ArgumentError, "Invalid value passed for `random_delay`. Please pass seconds as a String e.g. '60'.") + end + + it "converts seconds into iso8601 format" do + resource.frequency :monthly + resource.random_delay "60" + resource.after_created + expect(resource.random_delay).to eq("PT60S") + end + end + + context "when execution_time_limit is passed" do + it "sets the deafult value as PT72H" do + resource.after_created + expect(resource.execution_time_limit).to eq("PT72H") + end + + it "raises error for invalid execution_time_limit" do + resource.execution_time_limit "abc" + expect { resource.after_created }.to raise_error(Chef::Exceptions::ArgumentError, "Invalid value passed for `execution_time_limit`. Please pass seconds as a String e.g. '60'.") + end + + it "converts seconds into iso8601 format" do + resource.execution_time_limit "60" + resource.after_created + expect(resource.execution_time_limit).to eq("PT60S") + end + end + + context "#validate_start_time" do + it "raises error if start_time is nil" do + expect { resource.send(:validate_start_time, nil) }.to raise_error(Chef::Exceptions::ArgumentError, "`start_time` needs to be provided with `frequency :once`") + end + end + + context "#validate_start_day" do + it "raise error if start_day is passed with frequency :on_logon" do + resource.frequency :on_logon + expect { resource.send(:validate_start_day, "Wed", :on_logon) }.to raise_error(Chef::Exceptions::ArgumentError, "`start_day` property is not supported with frequency: on_logon") + end + end + + context "#validate_user_and_password" do + context "when password is not passed" do + it "raises error with non-system users" do + allow(resource).to receive(:use_password?).and_return(true) + expect { resource.send(:validate_user_and_password, "Testuser", nil) }.to raise_error("Can't specify a non-system user without a password!") + end + end + end + + context "#validate_interactive_setting" do + it "raises error when interactive_enabled is passed without password" do + expect { resource.send(:validate_interactive_setting, true, nil) }.to raise_error("Please provide the password when attempting to set interactive/non-interactive.") + end + end + + context "#validate_create_frequency_modifier" do + context "when frequency is :minute" do + it "raises error if frequency_modifier > 1439" do + expect { resource.send(:validate_create_frequency_modifier, :minute, 1500) }.to raise_error("frequency_modifier value 1500 is invalid. Valid values for :minute frequency are 1 - 1439.") + end + end + + context "when frequency is :hourly" do + it "raises error if frequency_modifier > 23" do + expect { resource.send(:validate_create_frequency_modifier, :hourly, 24) }.to raise_error("frequency_modifier value 24 is invalid. Valid values for :hourly frequency are 1 - 23.") + end + end + + context "when frequency is :daily" do + it "raises error if frequency_modifier > 365" do + expect { resource.send(:validate_create_frequency_modifier, :daily, 366) }.to raise_error("frequency_modifier value 366 is invalid. Valid values for :daily frequency are 1 - 365.") + end + end + + context "when frequency is :weekly" do + it "raises error if frequency_modifier > 52" do + expect { resource.send(:validate_create_frequency_modifier, :weekly, 53) }.to raise_error("frequency_modifier value 53 is invalid. Valid values for :weekly frequency are 1 - 52.") + end + end + + context "when frequency is :monthly" do + it "raises error if frequency_modifier > 12" do + expect { resource.send(:validate_create_frequency_modifier, :monthly, 14) }.to raise_error("frequency_modifier value 14 is invalid. Valid values for :monthly frequency are 1 - 12, 'FIRST', 'SECOND', 'THIRD', 'FOURTH', 'LAST', 'LASTDAY'.") + end + + it "raises error if frequency_modifier is invalid" do + expect { resource.send(:validate_create_frequency_modifier, :monthly, "abc") }.to raise_error("frequency_modifier value abc is invalid. Valid values for :monthly frequency are 1 - 12, 'FIRST', 'SECOND', 'THIRD', 'FOURTH', 'LAST', 'LASTDAY'.") + end + end + end + + context "#validate_create_day" do + it "raises error if frequency is not :weekly" do + expect { resource.send(:validate_create_day, "Mon", :monthly) }.to raise_error("day attribute is only valid for tasks that run weekly") + end + + it "accepts a valid single day" do + expect { resource.send(:validate_create_day, "Mon", :weekly) }.not_to raise_error + end + + it "accepts a comma separated list of valid days" do + expect { resource.send(:validate_create_day, "Mon, tue, THU", :weekly) }.not_to raise_error + end + + it "raises error for invalid day value" do + expect { resource.send(:validate_create_day, "xyz", :weekly) }.to raise_error("day attribute invalid. Only valid values are: MON, TUE, WED, THU, FRI, SAT, SUN and *. Multiple values must be separated by a comma.") + end + end + + context "#validate_create_months" do + it "raises error if frequency is not :monthly" do + expect { resource.send(:validate_create_months, "Jan", :once) }.to raise_error("months attribute is only valid for tasks that run monthly") + end + + it "accepts a valid single month" do + expect { resource.send(:validate_create_months, "Feb", :monthly) }.not_to raise_error + end + + it "accepts a comma separated list of valid months" do + expect { resource.send(:validate_create_months, "Jan, mar, AUG", :monthly) }.not_to raise_error + end + + it "raises error for invalid month value" do + expect { resource.send(:validate_create_months, "xyz", :monthly) }.to raise_error("months attribute invalid. Only valid values are: JAN, FEB, MAR, APR, MAY, JUN, JUL, AUG, SEP, OCT, NOV, DEC and *. Multiple values must be separated by a comma.") + end + end + + context "#validate_idle_time" do + it "raises error if frequency is not :on_idle" do + expect { resource.send(:validate_idle_time, 5, :hourly) }.to raise_error("idle_time attribute is only valid for tasks that run on_idle") + end + + it "raises error if idle_time > 999" do + expect { resource.send(:validate_idle_time, 1000, :on_idle) }.to raise_error("idle_time value 1000 is invalid. Valid values for :on_idle frequency are 1 - 999.") + end + end +end |