diff options
author | Lamont Granquist <lamont@scriptkiddie.org> | 2020-10-26 22:36:19 -0700 |
---|---|---|
committer | Lamont Granquist <lamont@scriptkiddie.org> | 2020-10-26 23:07:32 -0700 |
commit | 45874fecc5010353a5aadf4a0893737ad9282f7b (patch) | |
tree | acd5098251e4d845f5b7d3d1a3d79931f0c580b1 /lib | |
parent | accb875c6d01205fdbb4e2bcb190057763bf7669 (diff) | |
download | chef-45874fecc5010353a5aadf4a0893737ad9282f7b.tar.gz |
Final batch of unified_mode providers
Also converts some of them to custom resources
Signed-off-by: Lamont Granquist <lamont@scriptkiddie.org>
Diffstat (limited to 'lib')
-rw-r--r-- | lib/chef/provider/windows_env.rb | 210 | ||||
-rw-r--r-- | lib/chef/provider/windows_path.rb | 61 | ||||
-rw-r--r-- | lib/chef/provider/windows_task.rb | 631 | ||||
-rw-r--r-- | lib/chef/providers.rb | 3 | ||||
-rw-r--r-- | lib/chef/resource/windows_env.rb | 173 | ||||
-rw-r--r-- | lib/chef/resource/windows_feature.rb | 2 | ||||
-rw-r--r-- | lib/chef/resource/windows_package.rb | 1 | ||||
-rw-r--r-- | lib/chef/resource/windows_path.rb | 38 | ||||
-rw-r--r-- | lib/chef/resource/windows_service.rb | 1 | ||||
-rw-r--r-- | lib/chef/resource/windows_task.rb | 661 | ||||
-rw-r--r-- | lib/chef/resource/windows_workgroup.rb | 2 |
11 files changed, 852 insertions, 931 deletions
diff --git a/lib/chef/provider/windows_env.rb b/lib/chef/provider/windows_env.rb deleted file mode 100644 index f60dc79c5b..0000000000 --- a/lib/chef/provider/windows_env.rb +++ /dev/null @@ -1,210 +0,0 @@ -# -# Author:: Doug MacEachern (<dougm@vmware.com>) -# Copyright:: Copyright 2010-2016, VMware, 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 "../provider" -require_relative "../resource/windows_env" -require_relative "../mixin/windows_env_helper" - -class Chef - class Provider - class WindowsEnv < Chef::Provider - include Chef::Mixin::WindowsEnvHelper - attr_accessor :key_exists - - provides :env - provides :windows_env - - def whyrun_supported? - false - end - - def initialize(new_resource, run_context) - super - @key_exists = true - end - - def load_current_resource - @current_resource = Chef::Resource::WindowsEnv.new(new_resource.name) - current_resource.key_name(new_resource.key_name) - - if env_key_exists(new_resource.key_name) - current_resource.value(env_value(new_resource.key_name)) - else - @key_exists = false - logger.trace("#{new_resource} key does not exist") - end - - current_resource - end - - def env_key_exists(key_name) - env_value(key_name) ? true : false - end - - # Check to see if value needs any changes - # - # ==== Returns - # <true>:: If a change is required - # <false>:: If a change is not required - def requires_modify_or_create? - if new_resource.delim - # e.g. check for existing value within PATH - new_values.inject(0) do |index, val| - next_index = current_values.find_index val - return true if next_index.nil? || next_index < index - - next_index - end - false - else - new_resource.value != current_resource.value - end - end - - alias_method :compare_value, :requires_modify_or_create? - - action :create do - if @key_exists - if requires_modify_or_create? - modify_env - logger.info("#{new_resource} altered") - new_resource.updated_by_last_action(true) - end - else - create_env - logger.info("#{new_resource} created") - new_resource.updated_by_last_action(true) - end - end - - # e.g. delete a PATH element - # - # ==== Returns - # <true>:: If we handled the element case and caller should not delete the key - # <false>:: Caller should delete the key, either no :delim was specific or value was empty - # after we removed the element. - def delete_element - return false unless new_resource.delim # no delim: delete the key - - needs_delete = new_values.any? { |v| current_values.include?(v) } - if !needs_delete - logger.trace("#{new_resource} element '#{new_resource.value}' does not exist") - true # do not delete the key - else - new_value = - current_values.select do |item| - not new_values.include?(item) - end.join(new_resource.delim) - - if new_value.empty? - false # nothing left here, delete the key - else - old_value = new_resource.value(new_value) - create_env - logger.trace("#{new_resource} deleted #{old_value} element") - new_resource.updated_by_last_action(true) - true # we removed the element and updated; do not delete the key - end - end - end - - action :delete do - if ( ENV[new_resource.key_name] || @key_exists ) && !delete_element - delete_env - logger.info("#{new_resource} deleted") - new_resource.updated_by_last_action(true) - end - end - - action :modify do - if @key_exists - if requires_modify_or_create? - modify_env - logger.info("#{new_resource} modified") - new_resource.updated_by_last_action(true) - end - else - raise Chef::Exceptions::WindowsEnv, "Cannot modify #{new_resource} - key does not exist!" - end - end - - def create_env - obj = env_obj(@new_resource.key_name) - unless obj - obj = WIN32OLE.connect("winmgmts://").get("Win32_Environment").spawninstance_ - obj.name = @new_resource.key_name - obj.username = new_resource.user - end - obj.variablevalue = @new_resource.value - obj.put_ - value = @new_resource.value - value = expand_path(value) if @new_resource.key_name.casecmp("PATH") == 0 - ENV[@new_resource.key_name] = value - broadcast_env_change - end - - def delete_env - obj = env_obj(@new_resource.key_name) - if obj - obj.delete_ - broadcast_env_change - end - if ENV[@new_resource.key_name] - ENV.delete(@new_resource.key_name) - end - end - - def modify_env - if new_resource.delim - new_resource.value((new_values + current_values).uniq.join(new_resource.delim)) - end - create_env - end - - # Returns the current values to split by delimiter - def current_values - @current_values ||= current_resource.value.split(new_resource.delim) - end - - # Returns the new values to split by delimiter - def new_values - @new_values ||= new_resource.value.split(new_resource.delim) - end - - def env_value(key_name) - obj = env_obj(key_name) - obj.variablevalue if obj - end - - def env_obj(key_name) - return @env_obj if @env_obj - - wmi = WmiLite::Wmi.new - # Note that by design this query is case insensitive with regard to key_name - environment_variables = wmi.query("select * from Win32_Environment where name = '#{key_name}'") - if environment_variables && environment_variables.length > 0 - environment_variables.each do |env| - @env_obj = env.wmi_ole_object - return @env_obj if @env_obj.username.split('\\').last.casecmp(new_resource.user) == 0 - end - end - @env_obj = nil - end - end - end -end diff --git a/lib/chef/provider/windows_path.rb b/lib/chef/provider/windows_path.rb deleted file mode 100644 index 36d202ad49..0000000000 --- a/lib/chef/provider/windows_path.rb +++ /dev/null @@ -1,61 +0,0 @@ -# -# 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 "../mixin/windows_env_helper" if ChefUtils.windows? -require_relative "../mixin/wide_string" -require_relative "../exceptions" - -class Chef - class Provider - class WindowsPath < Chef::Provider - include Chef::Mixin::WindowsEnvHelper if ChefUtils.windows? - - provides :windows_path - - def load_current_resource - @current_resource = Chef::Resource::WindowsPath.new(new_resource.name) - @current_resource.path(new_resource.path) - @current_resource - end - - action :add do - # The windows Env provider does not correctly expand variables in - # the PATH environment variable. Ruby expects these to be expanded. - # - path = expand_path(new_resource.path) - declare_resource(:env, "path") do - action :modify - delim ::File::PATH_SEPARATOR - value path.tr("/", '\\') - end - end - - action :remove do - # The windows Env provider does not correctly expand variables in - # the PATH environment variable. Ruby expects these to be expanded. - # - path = expand_path(new_resource.path) - declare_resource(:env, "path") do - action :delete - delim ::File::PATH_SEPARATOR - value path.tr("/", '\\') - end - end - end - end -end diff --git a/lib/chef/provider/windows_task.rb b/lib/chef/provider/windows_task.rb deleted file mode 100644 index 963c52ef02..0000000000 --- a/lib/chef/provider/windows_task.rb +++ /dev/null @@ -1,631 +0,0 @@ -# -# 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. -# - -autoload :ISO8601, "iso8601" if ChefUtils.windows? -require_relative "../provider" -require_relative "../util/path_helper" -require "win32/taskscheduler" if ChefUtils.windows? - -class Chef - class Provider - class WindowsTask < Chef::Provider - if ChefUtils.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, - }.freeze - - DAYS_OF_WEEK = { MON: TaskScheduler::MONDAY, - TUE: TaskScheduler::TUESDAY, - WED: TaskScheduler::WEDNESDAY, - THU: TaskScheduler::THURSDAY, - FRI: TaskScheduler::FRIDAY, - SAT: TaskScheduler::SATURDAY, - SUN: TaskScheduler::SUNDAY }.freeze - - WEEKS_OF_MONTH = { - FIRST: TaskScheduler::FIRST_WEEK, - SECOND: TaskScheduler::SECOND_WEEK, - THIRD: TaskScheduler::THIRD_WEEK, - FOURTH: TaskScheduler::FOURTH_WEEK, - }.freeze - - 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, - # cspell:disable-next-line - 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, - # cspell:disable-next-line - 30 => TaskScheduler::TASK_THIRTYETH, - 31 => TaskScheduler::TASK_THIRTY_FIRST, - }.freeze - - PRIORITY = { "critical" => 0, "highest" => 1, "above_normal_2" => 2 , "above_normal_3" => 3, "normal_4" => 4, - "normal_5" => 5, "normal_6" => 6, "below_normal_7" => 7, "below_normal_8" => 8, "lowest" => 9, "idle" => 10 }.freeze - - def load_current_resource - @current_resource = Chef::Resource::WindowsTask.new(new_resource.name) - task = TaskScheduler.new(new_resource.task_name, nil, "\\", false) - @current_resource.exists = task.exists?(new_resource.task_name) - if @current_resource.exists - 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) - end - @current_resource - end - - action :create do - set_command_and_arguments if new_resource.command - - 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, {}, { user: new_resource.user, password: new_resource.password, interactive: new_resource.interactive_enabled }) - task.activate(new_resource.task_name) - else - task.new_work_item(new_resource.task_name, trigger, { user: new_resource.user, password: new_resource.password, interactive: new_resource.interactive_enabled }) - end - task.application_name = new_resource.command - task.parameters = new_resource.command_arguments if new_resource.command_arguments - 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, new_resource.interactive_enabled) - task.creator = new_resource.user - task.description = new_resource.description unless new_resource.description.nil? - task.activate(new_resource.task_name) - end - end - end - - action :run do - 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 - - action :delete do - 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 - - action :end do - 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 - - action :enable do - 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 - - action :disable do - 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 which disables 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 - - # separated command arguments from :command property - def set_command_and_arguments - cmd, *args = Chef::Util::PathHelper.split_args(new_resource.command) - new_resource.command = cmd - new_resource.command_arguments = args.join(" ") - end - - 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, new_resource.interactive_enabled) - task.application_name = new_resource.command if new_resource.command - task.parameters = new_resource.command_arguments if new_resource.command_arguments - 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.description = new_resource.description unless new_resource.description.nil? - 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 getting 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.author != new_resource.user || - task.application_name != new_resource.command || - description_needs_update?(task) || - task.parameters != new_resource.command_arguments.to_s || - task.principals[:run_level] != run_level || - task.settings[:disallow_start_if_on_batteries] != new_resource.disallow_start_if_on_batteries || - task.settings[:stop_if_going_on_batteries] != new_resource.stop_if_going_on_batteries || - task.settings[:start_when_available] != new_resource.start_when_available) - 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.author.to_s.casecmp(new_resource.user.to_s) != 0 || - task.application_name != new_resource.command || - description_needs_update?(task) || - task.parameters != new_resource.command_arguments.to_s || - task.working_directory != new_resource.cwd.to_s || - task.principals[:logon_type] != logon_type || - task.principals[:run_level] != run_level || - PRIORITY[task.priority] != new_resource.priority || - task.settings[:disallow_start_if_on_batteries] != new_resource.disallow_start_if_on_batteries || - task.settings[:stop_if_going_on_batteries] != new_resource.stop_if_going_on_batteries || - task.settings[:start_when_available] != new_resource.start_when_available - 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.to_s.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.to_s.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 format - 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[:priority] = new_resource.priority - settings[:disallow_start_if_on_batteries] = new_resource.disallow_start_if_on_batteries - settings[:stop_if_going_on_batteries] = new_resource.stop_if_going_on_batteries - settings[:start_when_available] = new_resource.start_when_available - settings - end - - def principal_settings - settings = {} - settings[:run_level] = run_level - settings[:logon_type] = logon_type - settings - end - - def description_needs_update?(task) - task.description != new_resource.description unless new_resource.description.nil? - 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 comparison. - user_id = new_resource.user.to_s - password = new_resource.password.to_s - if Chef::ReservedNames::Win32::Security::SID.service_account_user?(user_id) - TaskScheduler::TASK_LOGON_SERVICE_ACCOUNT - elsif Chef::ReservedNames::Win32::Security::SID.group_user?(user_id) - TaskScheduler::TASK_LOGON_GROUP - elsif !user_id.empty? && !password.empty? - if new_resource.interactive_enabled - TaskScheduler::TASK_LOGON_INTERACTIVE_TOKEN - else - TaskScheduler::TASK_LOGON_PASSWORD - end - else - TaskScheduler::TASK_LOGON_INTERACTIVE_TOKEN - end - end - - # This method checks if task and command properties exist since those two are mandatory properties 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 diff --git a/lib/chef/providers.rb b/lib/chef/providers.rb index 6b099fc2b7..7652d60896 100644 --- a/lib/chef/providers.rb +++ b/lib/chef/providers.rb @@ -47,11 +47,8 @@ require_relative "provider/systemd_unit" require_relative "provider/template" require_relative "provider/user" require_relative "provider/whyrun_safe_ruby_block" -require_relative "provider/windows_env" require_relative "provider/yum_repository" -require_relative "provider/windows_task" require_relative "provider/zypper_repository" -require_relative "provider/windows_path" require_relative "provider/package/apt" require_relative "provider/package/chocolatey" diff --git a/lib/chef/resource/windows_env.rb b/lib/chef/resource/windows_env.rb index 35070d6623..ab65465ed6 100644 --- a/lib/chef/resource/windows_env.rb +++ b/lib/chef/resource/windows_env.rb @@ -18,10 +18,13 @@ # require_relative "../resource" +require_relative "../mixin/windows_env_helper" class Chef class Resource class WindowsEnv < Chef::Resource + unified_mode true + provides :windows_env provides :env # backwards compat with the pre-Chef 14 resource name @@ -52,6 +55,176 @@ class Chef desired_state: false property :user, String, default: "<System>" + + action_class do + include Chef::Mixin::WindowsEnvHelper + + def whyrun_supported? + false + end + + def load_current_resource + @current_resource = Chef::Resource::WindowsEnv.new(new_resource.name) + current_resource.key_name(new_resource.key_name) + + if key_exists? + current_resource.value(env_value(new_resource.key_name)) + else + logger.trace("#{new_resource} key does not exist") + end + + current_resource + end + + def key_exists? + @key_exists ||= !!env_value(new_resource.key_name) + end + + def requires_modify_or_create? + if new_resource.delim + # e.g. check for existing value within PATH + new_values.inject(0) do |index, val| + next_index = current_values.find_index val + return true if next_index.nil? || next_index < index + + next_index + end + false + else + new_resource.value != current_resource.value + end + end + + alias_method :compare_value, :requires_modify_or_create? + + # e.g. delete a PATH element + # + # ==== Returns + # <true>:: If we handled the element case and caller should not delete the key + # <false>:: Caller should delete the key, either no :delim was specific or value was empty + # after we removed the element. + def delete_element + return false unless new_resource.delim # no delim: delete the key + + needs_delete = new_values.any? { |v| current_values.include?(v) } + if !needs_delete + logger.trace("#{new_resource} element '#{new_resource.value}' does not exist") + true # do not delete the key + else + new_value = + current_values.select do |item| + not new_values.include?(item) + end.join(new_resource.delim) + + if new_value.empty? + false # nothing left here, delete the key + else + old_value = new_resource.value(new_value) + create_env + logger.trace("#{new_resource} deleted #{old_value} element") + new_resource.updated_by_last_action(true) + true # we removed the element and updated; do not delete the key + end + end + end + + def create_env + obj = env_obj(@new_resource.key_name) + unless obj + obj = WIN32OLE.connect("winmgmts://").get("Win32_Environment").spawninstance_ + obj.name = @new_resource.key_name + obj.username = new_resource.user + end + obj.variablevalue = @new_resource.value + obj.put_ + value = @new_resource.value + value = expand_path(value) if @new_resource.key_name.casecmp("PATH") == 0 + ENV[@new_resource.key_name] = value + broadcast_env_change + end + + def delete_env + obj = env_obj(@new_resource.key_name) + if obj + obj.delete_ + broadcast_env_change + end + if ENV[@new_resource.key_name] + ENV.delete(@new_resource.key_name) + end + end + + def modify_env + if new_resource.delim + new_resource.value((new_values + current_values).uniq.join(new_resource.delim)) + end + create_env + end + + # Returns the current values to split by delimiter + def current_values + @current_values ||= current_resource.value.split(new_resource.delim) + end + + # Returns the new values to split by delimiter + def new_values + @new_values ||= new_resource.value.split(new_resource.delim) + end + + def env_value(key_name) + obj = env_obj(key_name) + obj.variablevalue if obj + end + + def env_obj(key_name) + return @env_obj if @env_obj + + wmi = WmiLite::Wmi.new + # Note that by design this query is case insensitive with regard to key_name + environment_variables = wmi.query("select * from Win32_Environment where name = '#{key_name}'") + if environment_variables && environment_variables.length > 0 + environment_variables.each do |env| + @env_obj = env.wmi_ole_object + return @env_obj if @env_obj.username.split('\\').last.casecmp(new_resource.user) == 0 + end + end + @env_obj = nil + end + end + + action :create do + if key_exists? + if requires_modify_or_create? + modify_env + logger.info("#{new_resource} altered") + new_resource.updated_by_last_action(true) + end + else + create_env + logger.info("#{new_resource} created") + new_resource.updated_by_last_action(true) + end + end + + action :delete do + if ( ENV[new_resource.key_name] || key_exists? ) && !delete_element + delete_env + logger.info("#{new_resource} deleted") + new_resource.updated_by_last_action(true) + end + end + + action :modify do + if key_exists? + if requires_modify_or_create? + modify_env + logger.info("#{new_resource} modified") + new_resource.updated_by_last_action(true) + end + else + raise Chef::Exceptions::WindowsEnv, "Cannot modify #{new_resource} - key does not exist!" + end + end end end end diff --git a/lib/chef/resource/windows_feature.rb b/lib/chef/resource/windows_feature.rb index aa90f67a37..760a7fe3f1 100644 --- a/lib/chef/resource/windows_feature.rb +++ b/lib/chef/resource/windows_feature.rb @@ -21,6 +21,8 @@ require_relative "../resource" class Chef class Resource class WindowsFeature < Chef::Resource + unified_mode true + provides(:windows_feature) { true } description "Use the **windows_feature** resource to add, remove or entirely delete Windows features and roles. This resource calls the 'windows_feature_dism' or 'windows_feature_powershell' resources depending on the specified installation method, and defaults to DISM, which is available on both Workstation and Server editions of Windows." diff --git a/lib/chef/resource/windows_package.rb b/lib/chef/resource/windows_package.rb index 2e10dde43d..0072d70656 100644 --- a/lib/chef/resource/windows_package.rb +++ b/lib/chef/resource/windows_package.rb @@ -26,6 +26,7 @@ class Chef class Resource class WindowsPackage < Chef::Resource::Package include Chef::Mixin::Uris + unified_mode true provides(:windows_package) { true } provides :package, os: "windows" diff --git a/lib/chef/resource/windows_path.rb b/lib/chef/resource/windows_path.rb index 850be66ba0..427360023c 100644 --- a/lib/chef/resource/windows_path.rb +++ b/lib/chef/resource/windows_path.rb @@ -16,11 +16,15 @@ # limitations under the License. # +require_relative "../mixin/windows_env_helper" if ChefUtils.windows? +require_relative "../mixin/wide_string" require_relative "../resource" class Chef class Resource class WindowsPath < Chef::Resource + unified_mode true + provides(:windows_path) { true } description "Use the **windows_path** resource to manage the path environment variable on Microsoft Windows." @@ -49,6 +53,40 @@ class Chef property :path, String, description: "An optional property to set the path value if it differs from the resource block's name.", name_property: true + + action_class do + include Chef::Mixin::WindowsEnvHelper if ChefUtils.windows? + end + + def load_current_resource + @current_resource = Chef::Resource::WindowsPath.new(new_resource.name) + @current_resource.path(new_resource.path) + @current_resource + end + + action :add do + # The windows Env provider does not correctly expand variables in + # the PATH environment variable. Ruby expects these to be expanded. + # + path = expand_path(new_resource.path) + env "path" do + action :modify + delim ::File::PATH_SEPARATOR + value path.tr("/", '\\') + end + end + + action :remove do + # The windows Env provider does not correctly expand variables in + # the PATH environment variable. Ruby expects these to be expanded. + # + path = expand_path(new_resource.path) + env "path" do + action :delete + delim ::File::PATH_SEPARATOR + value path.tr("/", '\\') + end + end end end end diff --git a/lib/chef/resource/windows_service.rb b/lib/chef/resource/windows_service.rb index defdf9fe29..0fa007497b 100644 --- a/lib/chef/resource/windows_service.rb +++ b/lib/chef/resource/windows_service.rb @@ -23,6 +23,7 @@ class Chef class Resource class WindowsService < Chef::Resource::Service include Chef::Win32ServiceConstants + unified_mode true ALLOWED_START_TYPES = { automatic: SERVICE_AUTO_START, diff --git a/lib/chef/resource/windows_task.rb b/lib/chef/resource/windows_task.rb index 4b6b9ba9cf..8a22ed321b 100644 --- a/lib/chef/resource/windows_task.rb +++ b/lib/chef/resource/windows_task.rb @@ -16,12 +16,18 @@ # limitations under the License. # +require "chef-utils" unless defined?(ChefUtils::CANARY) require_relative "../resource" -require_relative "../win32/security" if Chef::Platform.windows? +require_relative "../win32/security" if ChefUtils.windows_ruby? +autoload :ISO8601, "iso8601" if ChefUtils.windows_ruby? +require_relative "../util/path_helper" +require "win32/taskscheduler" if ChefUtils.windows_ruby? class Chef class Resource class WindowsTask < Chef::Resource + unified_mode true + provides(:windows_task) { true } description "Use the **windows_task** resource to create, delete or run a Windows scheduled task." @@ -144,8 +150,8 @@ class Chef 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 + 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." @@ -155,15 +161,15 @@ class Chef 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: lazy { Chef::ReservedNames::Win32::Security::SID.LocalSystem.account_simple_name if ChefUtils.windows_ruby? }, 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 + description: "Run with `:limited` or `:highest` privileges.", + default: :limited property :force, [TrueClass, FalseClass], description: "When used with create, will update the task.", @@ -177,16 +183,16 @@ class Chef 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." + 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.", @@ -383,16 +389,16 @@ class Chef 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 + 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}." @@ -464,6 +470,609 @@ class Chef def sec_to_min(seconds) seconds.to_i / 60 end + + action_class do + if ChefUtils.windows_ruby? + include ::Win32 + + MONTHS = { + JAN: ::Win32::TaskScheduler::JANUARY, + FEB: ::Win32::TaskScheduler::FEBRUARY, + MAR: ::Win32::TaskScheduler::MARCH, + APR: ::Win32::TaskScheduler::APRIL, + MAY: ::Win32::TaskScheduler::MAY, + JUN: ::Win32::TaskScheduler::JUNE, + JUL: ::Win32::TaskScheduler::JULY, + AUG: ::Win32::TaskScheduler::AUGUST, + SEP: ::Win32::TaskScheduler::SEPTEMBER, + OCT: ::Win32::TaskScheduler::OCTOBER, + NOV: ::Win32::TaskScheduler::NOVEMBER, + DEC: ::Win32::TaskScheduler::DECEMBER, + }.freeze + + DAYS_OF_WEEK = { MON: ::Win32::TaskScheduler::MONDAY, + TUE: ::Win32::TaskScheduler::TUESDAY, + WED: ::Win32::TaskScheduler::WEDNESDAY, + THU: ::Win32::TaskScheduler::THURSDAY, + FRI: ::Win32::TaskScheduler::FRIDAY, + SAT: ::Win32::TaskScheduler::SATURDAY, + SUN: ::Win32::TaskScheduler::SUNDAY }.freeze + + WEEKS_OF_MONTH = { + FIRST: ::Win32::TaskScheduler::FIRST_WEEK, + SECOND: ::Win32::TaskScheduler::SECOND_WEEK, + THIRD: ::Win32::TaskScheduler::THIRD_WEEK, + FOURTH: ::Win32::TaskScheduler::FOURTH_WEEK, + }.freeze + + DAYS_OF_MONTH = { + 1 => ::Win32::TaskScheduler::TASK_FIRST, + 2 => ::Win32::TaskScheduler::TASK_SECOND, + 3 => ::Win32::TaskScheduler::TASK_THIRD, + 4 => ::Win32::TaskScheduler::TASK_FOURTH, + 5 => ::Win32::TaskScheduler::TASK_FIFTH, + 6 => ::Win32::TaskScheduler::TASK_SIXTH, + 7 => ::Win32::TaskScheduler::TASK_SEVENTH, + 8 => ::Win32::TaskScheduler::TASK_EIGHTH, + # cspell:disable-next-line + 9 => ::Win32::TaskScheduler::TASK_NINETH, + 10 => ::Win32::TaskScheduler::TASK_TENTH, + 11 => ::Win32::TaskScheduler::TASK_ELEVENTH, + 12 => ::Win32::TaskScheduler::TASK_TWELFTH, + 13 => ::Win32::TaskScheduler::TASK_THIRTEENTH, + 14 => ::Win32::TaskScheduler::TASK_FOURTEENTH, + 15 => ::Win32::TaskScheduler::TASK_FIFTEENTH, + 16 => ::Win32::TaskScheduler::TASK_SIXTEENTH, + 17 => ::Win32::TaskScheduler::TASK_SEVENTEENTH, + 18 => ::Win32::TaskScheduler::TASK_EIGHTEENTH, + 19 => ::Win32::TaskScheduler::TASK_NINETEENTH, + 20 => ::Win32::TaskScheduler::TASK_TWENTIETH, + 21 => ::Win32::TaskScheduler::TASK_TWENTY_FIRST, + 22 => ::Win32::TaskScheduler::TASK_TWENTY_SECOND, + 23 => ::Win32::TaskScheduler::TASK_TWENTY_THIRD, + 24 => ::Win32::TaskScheduler::TASK_TWENTY_FOURTH, + 25 => ::Win32::TaskScheduler::TASK_TWENTY_FIFTH, + 26 => ::Win32::TaskScheduler::TASK_TWENTY_SIXTH, + 27 => ::Win32::TaskScheduler::TASK_TWENTY_SEVENTH, + 28 => ::Win32::TaskScheduler::TASK_TWENTY_EIGHTH, + 29 => ::Win32::TaskScheduler::TASK_TWENTY_NINTH, + # cspell:disable-next-line + 30 => ::Win32::TaskScheduler::TASK_THIRTYETH, + 31 => ::Win32::TaskScheduler::TASK_THIRTY_FIRST, + }.freeze + + PRIORITY = { "critical" => 0, "highest" => 1, "above_normal_2" => 2 , "above_normal_3" => 3, "normal_4" => 4, + "normal_5" => 5, "normal_6" => 6, "below_normal_7" => 7, "below_normal_8" => 8, "lowest" => 9, "idle" => 10 }.freeze + end + + def load_current_resource + @current_resource = Chef::Resource::WindowsTask.new(new_resource.name) + task = ::Win32::TaskScheduler.new(new_resource.task_name, nil, "\\", false) + @current_resource.exists = task.exists?(new_resource.task_name) + if @current_resource.exists + 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) + end + @current_resource + end + + # separated command arguments from :command property + def set_command_and_arguments + cmd, *args = Chef::Util::PathHelper.split_args(new_resource.command) + new_resource.command = cmd + new_resource.command_arguments = args.join(" ") + end + + 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, new_resource.interactive_enabled) + task.application_name = new_resource.command if new_resource.command + task.parameters = new_resource.command_arguments if new_resource.command_arguments + 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.description = new_resource.description unless new_resource.description.nil? + 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 == ::Win32::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 == ::Win32::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 getting 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.author != new_resource.user || + task.application_name != new_resource.command || + description_needs_update?(task) || + task.parameters != new_resource.command_arguments.to_s || + task.principals[:run_level] != run_level || + task.settings[:disallow_start_if_on_batteries] != new_resource.disallow_start_if_on_batteries || + task.settings[:stop_if_going_on_batteries] != new_resource.stop_if_going_on_batteries || + task.settings[:start_when_available] != new_resource.start_when_available) + 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.author.to_s.casecmp(new_resource.user.to_s) != 0 || + task.application_name != new_resource.command || + description_needs_update?(task) || + task.parameters != new_resource.command_arguments.to_s || + task.working_directory != new_resource.cwd.to_s || + task.principals[:logon_type] != logon_type || + task.principals[:run_level] != run_level || + PRIORITY[task.priority] != new_resource.priority || + task.settings[:disallow_start_if_on_batteries] != new_resource.disallow_start_if_on_batteries || + task.settings[:stop_if_going_on_batteries] != new_resource.stop_if_going_on_batteries || + task.settings[:start_when_available] != new_resource.start_when_available + if trigger_type == ::Win32::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 == ::Win32::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 + ::Win32::TaskScheduler::ONCE + when :daily + ::Win32::TaskScheduler::DAILY + when :weekly + ::Win32::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) ? ::Win32::TaskScheduler::MONTHLYDATE : ::Win32::TaskScheduler::MONTHLYDOW + when :on_idle + ::Win32::TaskScheduler::ON_IDLE + when :onstart + ::Win32::TaskScheduler::AT_SYSTEMSTART + when :on_logon + ::Win32::TaskScheduler::AT_LOGON + else + raise ArgumentError, "Please set frequency" + end + end + + def type + case trigger_type + when ::Win32::TaskScheduler::ONCE + { once: nil } + when ::Win32::TaskScheduler::DAILY + { days_interval: new_resource.frequency_modifier.to_i } + when ::Win32::TaskScheduler::WEEKLY + { weeks_interval: new_resource.frequency_modifier.to_i, days_of_week: days_of_week.to_i } + when ::Win32::TaskScheduler::MONTHLYDATE + { months: months_of_year.to_i, days: days_of_month.to_i } + when ::Win32::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 ::Win32::TaskScheduler::ON_IDLE + # TODO: handle option for this trigger + when ::Win32::TaskScheduler::AT_LOGON + # TODO: handle option for this trigger + when ::Win32::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.to_s.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.to_s.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 + ::Win32::TaskScheduler::TASK_RUNLEVEL_HIGHEST + when :limited + ::Win32::TaskScheduler::TASK_RUNLEVEL_LUA + end + end + + # TODO: while creating the configuration settings win32-taskscheduler it accepts execution time limit values in ISO8601 format + 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[:priority] = new_resource.priority + settings[:disallow_start_if_on_batteries] = new_resource.disallow_start_if_on_batteries + settings[:stop_if_going_on_batteries] = new_resource.stop_if_going_on_batteries + settings[:start_when_available] = new_resource.start_when_available + settings + end + + def principal_settings + settings = {} + settings[:run_level] = run_level + settings[:logon_type] = logon_type + settings + end + + def description_needs_update?(task) + task.description != new_resource.description unless new_resource.description.nil? + 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 comparison. + user_id = new_resource.user.to_s + password = new_resource.password.to_s + if Chef::ReservedNames::Win32::Security::SID.service_account_user?(user_id) + ::Win32::TaskScheduler::TASK_LOGON_SERVICE_ACCOUNT + elsif Chef::ReservedNames::Win32::Security::SID.group_user?(user_id) + ::Win32::TaskScheduler::TASK_LOGON_GROUP + elsif !user_id.empty? && !password.empty? + if new_resource.interactive_enabled + ::Win32::TaskScheduler::TASK_LOGON_INTERACTIVE_TOKEN + else + ::Win32::TaskScheduler::TASK_LOGON_PASSWORD + end + else + ::Win32::TaskScheduler::TASK_LOGON_INTERACTIVE_TOKEN + end + end + + # This method checks if task and command properties exist since those two are mandatory properties 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 + + action :create do + set_command_and_arguments if new_resource.command + + 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 = ::Win32::TaskScheduler.new + if new_resource.frequency == :none + task.new_work_item(new_resource.task_name, {}, { user: new_resource.user, password: new_resource.password, interactive: new_resource.interactive_enabled }) + task.activate(new_resource.task_name) + else + task.new_work_item(new_resource.task_name, trigger, { user: new_resource.user, password: new_resource.password, interactive: new_resource.interactive_enabled }) + end + task.application_name = new_resource.command + task.parameters = new_resource.command_arguments if new_resource.command_arguments + 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, new_resource.interactive_enabled) + task.creator = new_resource.user + task.description = new_resource.description unless new_resource.description.nil? + task.activate(new_resource.task_name) + end + end + end + + action :run do + 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 + + action :delete do + if current_resource.exists + logger.trace "#{new_resource} task exists" + converge_by("delete scheduled task #{new_resource}") do + ts = ::Win32::TaskScheduler.new + ts.delete(current_resource.task_name) + end + else + logger.warn "#{new_resource} task does not exist - nothing to do" + end + end + + action :end do + 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 + + action :enable do + 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 + + action :disable do + 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 which disables 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 + + action_class do + alias_method :action_change, :action_create + end end end end diff --git a/lib/chef/resource/windows_workgroup.rb b/lib/chef/resource/windows_workgroup.rb index aa2af646dc..e506b51257 100644 --- a/lib/chef/resource/windows_workgroup.rb +++ b/lib/chef/resource/windows_workgroup.rb @@ -21,6 +21,8 @@ require "chef-utils/dist" unless defined?(ChefUtils::Dist) class Chef class Resource class WindowsWorkgroup < Chef::Resource + unified_mode true + provides :windows_workgroup description "Use the **windows_workgroup** resource to join or change the workgroup of a Windows host." |