diff options
Diffstat (limited to 'lib/chef/provider')
111 files changed, 3249 insertions, 1583 deletions
diff --git a/lib/chef/provider/batch.rb b/lib/chef/provider/batch.rb index b6b386e5a8..63286f91f6 100644 --- a/lib/chef/provider/batch.rb +++ b/lib/chef/provider/batch.rb @@ -16,7 +16,7 @@ # limitations under the License. # -require 'chef/provider/windows_script' +require "chef/provider/windows_script" class Chef class Provider @@ -25,11 +25,19 @@ class Chef provides :batch, os: "windows" def initialize (new_resource, run_context) - super(new_resource, run_context, '.bat') + super(new_resource, run_context, ".bat") + end + + def command + basepath = is_forced_32bit ? wow64_directory : run_context.node.kernel.os_info.system_directory + + interpreter_path = Chef::Util::PathHelper.join(basepath, interpreter) + + "\"#{interpreter_path}\" #{flags} \"#{script_file.path}\"" end def flags - @new_resource.flags.nil? ? '/c' : new_resource.flags + ' /c' + @new_resource.flags.nil? ? "/c" : new_resource.flags + " /c" end end diff --git a/lib/chef/provider/cookbook_file.rb b/lib/chef/provider/cookbook_file.rb index b501a9b41d..a537ca5fd5 100644 --- a/lib/chef/provider/cookbook_file.rb +++ b/lib/chef/provider/cookbook_file.rb @@ -16,9 +16,9 @@ # limitations under the License. # -require 'chef/provider/file' -require 'chef/deprecation/provider/cookbook_file' -require 'chef/deprecation/warnings' +require "chef/provider/file" +require "chef/deprecation/provider/cookbook_file" +require "chef/deprecation/warnings" class Chef class Provider diff --git a/lib/chef/provider/cookbook_file/content.rb b/lib/chef/provider/cookbook_file/content.rb index 9f49ba885c..97290ff456 100644 --- a/lib/chef/provider/cookbook_file/content.rb +++ b/lib/chef/provider/cookbook_file/content.rb @@ -16,8 +16,8 @@ # limitations under the License. # -require 'chef/file_content_management/content_base' -require 'chef/file_content_management/tempfile' +require "chef/file_content_management/content_base" +require "chef/file_content_management/tempfile" class Chef class Provider diff --git a/lib/chef/provider/cron.rb b/lib/chef/provider/cron.rb index 6d86e336ec..361691aa0a 100644 --- a/lib/chef/provider/cron.rb +++ b/lib/chef/provider/cron.rb @@ -16,9 +16,9 @@ # limitations under the License. # -require 'chef/log' -require 'chef/mixin/command' -require 'chef/provider' +require "chef/log" +require "chef/mixin/command" +require "chef/provider" class Chef class Provider @@ -199,7 +199,7 @@ class Chef private def set_environment_var(attr_name, attr_value) - if %w(MAILTO PATH SHELL HOME).include?(attr_name) + if %w{MAILTO PATH SHELL HOME}.include?(attr_name) @current_resource.send(attr_name.downcase.to_sym, attr_value) else @current_resource.environment(@current_resource.environment.merge(attr_name => attr_value)) diff --git a/lib/chef/provider/cron/unix.rb b/lib/chef/provider/cron/unix.rb index 0750c0420b..0c4235df45 100644 --- a/lib/chef/provider/cron/unix.rb +++ b/lib/chef/provider/cron/unix.rb @@ -18,8 +18,9 @@ # limitations under the License. # -require 'chef/log' -require 'chef/provider' +require "chef/log" +require "chef/provider" +require "chef/provider/cron" class Chef class Provider @@ -27,12 +28,12 @@ class Chef class Unix < Chef::Provider::Cron include Chef::Mixin::ShellOut - provides :cron, os: 'solaris2' + provides :cron, os: "solaris2" private def read_crontab - crontab = shell_out('/usr/bin/crontab -l', :user => @new_resource.user) + crontab = shell_out("/usr/bin/crontab -l", :user => @new_resource.user) status = crontab.status.exitstatus Chef::Log.debug crontab.format_for_exception if status > 0 diff --git a/lib/chef/provider/deploy.rb b/lib/chef/provider/deploy.rb index 19e7c01ab1..fe6b288eda 100644 --- a/lib/chef/provider/deploy.rb +++ b/lib/chef/provider/deploy.rb @@ -201,7 +201,7 @@ class Chef converge_by("execute migration command #{@new_resource.migration_command}") do Chef::Log.info "#{@new_resource} migrating #{@new_resource.user} with environment #{env_info}" - run_command(run_options(:command => @new_resource.migration_command, :cwd=>release_path, :log_level => :info)) + shell_out!(@new_resource.migration_command,run_options(:cwd=>release_path, :log_level => :info)) end end end @@ -221,7 +221,7 @@ class Chef else converge_by("restart app using command #{@new_resource.restart_command}") do Chef::Log.info("#{@new_resource} restarting app") - run_command(run_options(:command => @new_resource.restart_command, :cwd => @new_resource.current_path)) + shell_out!(@new_resource.restart_command,run_options(:cwd=>@new_resource.current_path)) end end end @@ -276,7 +276,7 @@ class Chef def enforce_ownership converge_by("force ownership of #{@new_resource.deploy_to} to #{@new_resource.group}:#{@new_resource.user}") do - FileUtils.chown_R(@new_resource.user, @new_resource.group, @new_resource.deploy_to) + FileUtils.chown_R(@new_resource.user, @new_resource.group, @new_resource.deploy_to, :force => true) Chef::Log.info("#{@new_resource} set user to #{@new_resource.user}") if @new_resource.user Chef::Log.info("#{@new_resource} set group to #{@new_resource.group}") if @new_resource.group end @@ -365,7 +365,7 @@ class Chef end def release_slug - raise Chef::Exceptions::Override, "You must override release_slug in #{self.to_s}" + raise Chef::Exceptions::Override, "You must override release_slug in #{self}" end def install_gems @@ -373,11 +373,9 @@ class Chef end def gem_resource_collection_runner - gems_collection = Chef::ResourceCollection.new - gem_packages.each { |rbgem| gems_collection.insert(rbgem) } - gems_run_context = run_context.dup - gems_run_context.resource_collection = gems_collection - Chef::Runner.new(gems_run_context) + child_context = run_context.create_child + gem_packages.each { |rbgem| child_context.resource_collection.insert(rbgem) } + Chef::Runner.new(child_context) end def gem_packages diff --git a/lib/chef/provider/deploy/revision.rb b/lib/chef/provider/deploy/revision.rb index 62aa0e87f6..c3b61a564e 100644 --- a/lib/chef/provider/deploy/revision.rb +++ b/lib/chef/provider/deploy/revision.rb @@ -19,9 +19,9 @@ # limitations under the License. # -require 'chef/provider' -require 'chef/provider/deploy' -require 'chef/json_compat' +require "chef/provider" +require "chef/provider/deploy" +require "chef/json_compat" class Chef class Provider diff --git a/lib/chef/provider/directory.rb b/lib/chef/provider/directory.rb index 416393ac60..c9b8b3a844 100644 --- a/lib/chef/provider/directory.rb +++ b/lib/chef/provider/directory.rb @@ -16,12 +16,12 @@ # limitations under the License. # -require 'chef/config' -require 'chef/log' -require 'chef/resource/directory' -require 'chef/provider' -require 'chef/provider/file' -require 'fileutils' +require "chef/config" +require "chef/log" +require "chef/resource/directory" +require "chef/provider" +require "chef/provider/file" +require "fileutils" class Chef class Provider @@ -43,6 +43,9 @@ class Chef end def define_resource_requirements + # deep inside FAC we have to assert requirements, so call FACs hook to set that up + access_controls.define_resource_requirements + requirements.assert(:create) do |a| # Make sure the parent dir exists, or else fail. # for why run, print a message explaining the potential error. @@ -61,7 +64,13 @@ class Chef is_parent_writable = lambda do |base_dir| base_dir = ::File.dirname(base_dir) if ::File.exists?(base_dir) - Chef::FileAccessControl.writable?(base_dir) + if Chef::FileAccessControl.writable?(base_dir) + true + elsif Chef::Util::PathHelper.is_sip_path?(base_dir, node) + Chef::Util::PathHelper.writable_sip_path?(base_dir) + else + false + end else is_parent_writable.call(base_dir) end @@ -71,7 +80,13 @@ class Chef # in why run mode & parent directory does not exist no permissions check is required # If not in why run, permissions must be valid and we rely on prior assertion that dir exists if !whyrun_mode? || ::File.exists?(parent_directory) - Chef::FileAccessControl.writable?(parent_directory) + if Chef::FileAccessControl.writable?(parent_directory) + true + elsif Chef::Util::PathHelper.is_sip_path?(parent_directory, node) + Chef::Util::PathHelper.writable_sip_path?(@new_resource.path) + else + false + end else true end diff --git a/lib/chef/provider/dsc_resource.rb b/lib/chef/provider/dsc_resource.rb index 2812c154c6..b2946352fe 100644 --- a/lib/chef/provider/dsc_resource.rb +++ b/lib/chef/provider/dsc_resource.rb @@ -15,29 +15,28 @@ # See the License for the specific language governing permissions and # limitations under the License. # - -require 'chef/util/powershell/cmdlet' -require 'chef/util/dsc/local_configuration_manager' -require 'chef/mixin/powershell_type_coercions' -require 'chef/util/dsc/resource_store' +require "chef/util/powershell/cmdlet" +require "chef/util/dsc/local_configuration_manager" +require "chef/mixin/powershell_type_coercions" +require "chef/util/dsc/resource_store" class Chef class Provider class DscResource < Chef::Provider include Chef::Mixin::PowershellTypeCoercions - provides :dsc_resource, os: "windows" - def initialize(new_resource, run_context) super @new_resource = new_resource @module_name = new_resource.module_name + @reboot_resource = nil end def action_run if ! test_resource converge_by(generate_description) do result = set_resource + reboot_if_required end end end @@ -53,17 +52,16 @@ class Chef requirements.assert(:run) do |a| a.assertion { supports_dsc_invoke_resource? } err = ["You must have Powershell version >= 5.0.10018.0 to use dsc_resource."] - a.failure_message Chef::Exceptions::NoProviderAvailable, + a.failure_message Chef::Exceptions::ProviderNotFound, err a.whyrun err + ["Assuming a previous resource installs Powershell 5.0.10018.0 or higher."] a.block_action! end requirements.assert(:run) do |a| - a.assertion { - meta_configuration['RefreshMode'] == 'Disabled' - } - err = ["The LCM must have its RefreshMode set to Disabled. "] - a.failure_message Chef::Exceptions::NoProviderAvailable, err.join(' ') + a.assertion { supports_refresh_mode_enabled? || dsc_refresh_mode_disabled? } + err = ["The LCM must have its RefreshMode set to Disabled for" \ + " PowerShell versions before 5.0.10586.0."] + a.failure_message Chef::Exceptions::ProviderNotFound, err.join(" ") a.whyrun err + ["Assuming a previous resource sets the RefreshMode."] a.block_action! end @@ -74,7 +72,7 @@ class Chef def local_configuration_manager @local_configuration_manager ||= Chef::Util::DSC::LocalConfigurationManager.new( node, - nil + nil, ) end @@ -86,6 +84,14 @@ class Chef run_context && Chef::Platform.supports_dsc_invoke_resource?(node) end + def dsc_refresh_mode_disabled? + Chef::Platform.dsc_refresh_mode_disabled?(node) + end + + def supports_refresh_mode_enabled? + Chef::Platform.supports_refresh_mode_enabled?(node) + end + def generate_description @converge_description end @@ -97,17 +103,16 @@ class Chef def module_name @module_name ||= begin found = resource_store.find(dsc_resource_name) - r = case found.length when 0 raise Chef::Exceptions::ResourceNotFound, "Could not find #{dsc_resource_name}. Check to make "\ "sure that it shows up when running Get-DscResource" when 1 - if found[0]['Module'].nil? + if found[0]["Module"].nil? :none else - found[0]['Module']['Name'] + found[0]["Module"]["Name"] end else raise Chef::Exceptions::MultipleDscResourcesFound, found @@ -117,41 +122,73 @@ class Chef def test_resource result = invoke_resource(:test) + @converge_description = result.stream(:verbose) + # We really want this information from the verbose stream, - # however Invoke-DscResource is not correctly writing to that - # stream and instead just dumping to stdout - @converge_description = result.stdout - result.return_value[0]["InDesiredState"] + # however in some versions of WMF, Invoke-DscResource is not correctly + # writing to that stream and instead just dumping to stdout + if @converge_description.empty? + @converge_description = result.stdout + end + + return_dsc_resource_result(result, "InDesiredState") end def set_resource result = invoke_resource(:set) + if return_dsc_resource_result(result, "RebootRequired") + create_reboot_resource + end result.return_value end def invoke_resource(method, output_format=:object) properties = translate_type(@new_resource.properties) - switches = "-Method #{method.to_s} -Name #{@new_resource.resource}"\ + switches = "-Method #{method} -Name #{@new_resource.resource}"\ " -Property #{properties} -Verbose" - if module_name != :none switches += " -Module #{module_name}" end - cmdlet = Chef::Util::Powershell::Cmdlet.new( node, "Invoke-DscResource #{switches}", - output_format + output_format, ) - cmdlet.run! + cmdlet.run!({}, {:timeout => new_resource.timeout}) end - def meta_configuration - cmdlet = Chef::Util::Powershell::Cmdlet.new(node, "Get-DscLocalConfigurationManager", :object) - result = cmdlet.run! - result.return_value + def return_dsc_resource_result(result, property_name) + if result.return_value.is_a?(Array) + # WMF Feb 2015 Preview + result.return_value[0][property_name] + else + # WMF April 2015 Preview + result.return_value[property_name] + end end + def create_reboot_resource + @reboot_resource = Chef::Resource::Reboot.new( + "Reboot for #{@new_resource.name}", + run_context, + ).tap do |r| + r.reason("Reboot for #{@new_resource.resource}.") + end + end + + def reboot_if_required + reboot_action = @new_resource.reboot_action + unless @reboot_resource.nil? + case reboot_action + when :nothing + Chef::Log.debug("A reboot was requested by the DSC resource, but reboot_action is :nothing.") + Chef::Log.debug("This dsc_resource will not reboot the node.") + else + Chef::Log.debug("Requesting node reboot with #{reboot_action}.") + @reboot_resource.run_action(reboot_action) + end + end + end end end end diff --git a/lib/chef/provider/dsc_script.rb b/lib/chef/provider/dsc_script.rb index a75e68a475..3577c23e7b 100644 --- a/lib/chef/provider/dsc_script.rb +++ b/lib/chef/provider/dsc_script.rb @@ -16,10 +16,10 @@ # limitations under the License. # -require 'chef/util/powershell/cmdlet' -require 'chef/util/dsc/configuration_generator' -require 'chef/util/dsc/local_configuration_manager' -require 'chef/util/path_helper' +require "chef/util/powershell/cmdlet" +require "chef/util/dsc/configuration_generator" +require "chef/util/dsc/local_configuration_manager" +require "chef/util/path_helper" class Chef class Provider @@ -65,12 +65,12 @@ class Chef def define_resource_requirements requirements.assert(:run) do |a| err = [ - 'Could not find PowerShell DSC support on the system', + "Could not find PowerShell DSC support on the system", powershell_info_str, "Powershell 4.0 or higher was not detected on your system and is required to use the dsc_script resource.", ] a.assertion { supports_dsc? } - a.failure_message Chef::Exceptions::NoProviderAvailable, err.join(' ') + a.failure_message Chef::Exceptions::ProviderNotFound, err.join(" ") a.whyrun err + ["Assuming a previous resource installs Powershell 4.0 or higher."] a.block_action! end @@ -92,14 +92,14 @@ class Chef shellout_flags = { :cwd => @dsc_resource.cwd, :environment => @dsc_resource.environment, - :timeout => @dsc_resource.timeout + :timeout => @dsc_resource.timeout, } begin configuration_document = generate_configuration_document(config_directory, configuration_flags) @operations[operation].call(config_manager, configuration_document, shellout_flags) rescue Exception => e - Chef::Log.error("DSC operation failed: #{e.message.to_s}") + Chef::Log.error("DSC operation failed: #{e.message}") raise e ensure ::FileUtils.rm_rf(config_directory) @@ -119,7 +119,7 @@ class Chef shellout_flags = { :cwd => @dsc_resource.cwd, :environment => @dsc_resource.environment, - :timeout => @dsc_resource.timeout + :timeout => @dsc_resource.timeout, } generator = Chef::Util::DSC::ConfigurationGenerator.new(@run_context.node, config_directory) @@ -129,7 +129,7 @@ class Chef else # If code is also not provided, we mimic what the other script resources do (execute nothing) Chef::Log.warn("Neither code or command were provided for dsc_resource[#{@dsc_resource.name}].") unless @dsc_resource.code - generator.configuration_document_from_script_code(@dsc_resource.code || '', configuration_flags, @dsc_resource.imports, shellout_flags) + generator.configuration_document_from_script_code(@dsc_resource.code || "", configuration_flags, @dsc_resource.imports, shellout_flags) end end @@ -138,7 +138,7 @@ class Chef @dsc_resource.configuration_data_script elsif @dsc_resource.configuration_data configuration_data_path = "#{config_directory}/chef_dsc_config_data.psd1" - ::File.open(configuration_data_path, 'wt') do | script | + ::File.open(configuration_data_path, "wt") do | script | script.write(@dsc_resource.configuration_data) end configuration_data_path @@ -164,7 +164,7 @@ class Chef @dsc_resources_info.map do |resource| if resource.changes_state? # We ignore the last log message because it only contains the time it took, which looks weird - cleaned_messages = resource.change_log[0..-2].map { |c| c.sub(/^#{Regexp.escape(resource.name)}/, '').strip } + cleaned_messages = resource.change_log[0..-2].map { |c| c.sub(/^#{Regexp.escape(resource.name)}/, "").strip } "converge DSC resource #{resource.name} by #{cleaned_messages.find_all{ |c| c != ''}.join("\n")}" else # This is needed because a dsc script can have resources that are both converged and not @@ -177,7 +177,7 @@ class Chef if run_context && run_context.node[:languages] && run_context.node[:languages][:powershell] install_info = "Powershell #{run_context.node[:languages][:powershell][:version]} was found on the system." else - install_info = 'Powershell was not found.' + install_info = "Powershell was not found." end end end diff --git a/lib/chef/provider/env.rb b/lib/chef/provider/env.rb index cf75ff7d85..e8ac88e8c4 100644 --- a/lib/chef/provider/env.rb +++ b/lib/chef/provider/env.rb @@ -16,9 +16,9 @@ # limitations under the License. # -require 'chef/provider' -require 'chef/mixin/command' -require 'chef/resource/env' +require "chef/provider" +require "chef/mixin/command" +require "chef/resource/env" class Chef class Provider @@ -48,7 +48,7 @@ class Chef end def env_value(key_name) - raise Chef::Exceptions::Env, "#{self.to_s} provider does not implement env_value!" + raise Chef::Exceptions::Env, "#{self} provider does not implement env_value!" end def env_key_exists(key_name) @@ -108,15 +108,15 @@ class Chef not new_values.include?(item) end.join(@new_resource.delim) - if new_value.empty? - return false #nothing left here, delete the key - else - old_value = @new_resource.value(new_value) - create_env - Chef::Log.debug("#{@new_resource} deleted #{old_value} element") - @new_resource.updated_by_last_action(true) - return true #we removed the element and updated; do not delete the key - end + if new_value.empty? + return false #nothing left here, delete the key + else + old_value = @new_resource.value(new_value) + create_env + Chef::Log.debug("#{@new_resource} deleted #{old_value} element") + @new_resource.updated_by_last_action(true) + return true #we removed the element and updated; do not delete the key + end end end @@ -141,11 +141,11 @@ class Chef end def create_env - raise Chef::Exceptions::UnsupportedAction, "#{self.to_s} does not support :#{@new_resource.action}" + raise Chef::Exceptions::UnsupportedAction, "#{self} does not support :#{@new_resource.action}" end def delete_env - raise Chef::Exceptions::UnsupportedAction, "#{self.to_s} does not support :delete" + raise Chef::Exceptions::UnsupportedAction, "#{self} does not support :delete" end def modify_env @@ -164,6 +164,6 @@ class Chef def new_values @new_values ||= @new_resource.value.split(@new_resource.delim) end - end + end end end diff --git a/lib/chef/provider/env/windows.rb b/lib/chef/provider/env/windows.rb index 56cebdb888..ec05420471 100644 --- a/lib/chef/provider/env/windows.rb +++ b/lib/chef/provider/env/windows.rb @@ -16,7 +16,7 @@ # limitations under the License. # -require 'chef/mixin/windows_env_helper' +require "chef/mixin/windows_env_helper" class Chef class Provider @@ -36,7 +36,7 @@ class Chef obj.variablevalue = @new_resource.value obj.put_ value = @new_resource.value - value = expand_path(value) if @new_resource.key_name.upcase == 'PATH' + value = expand_path(value) if @new_resource.key_name.upcase == "PATH" ENV[@new_resource.key_name] = value broadcast_env_change end diff --git a/lib/chef/provider/erl_call.rb b/lib/chef/provider/erl_call.rb index f5855bcce6..dc3a8ea50e 100644 --- a/lib/chef/provider/erl_call.rb +++ b/lib/chef/provider/erl_call.rb @@ -16,9 +16,9 @@ # limitations under the License. # -require 'chef/log' -require 'chef/mixin/command' -require 'chef/provider' +require "chef/log" +require "chef/mixin/command" +require "chef/provider" class Chef class Provider @@ -89,7 +89,7 @@ class Chef end # fail if the first 4 characters aren't "{ok," - unless stdout_output[0..3].include?('{ok,') + unless stdout_output[0..3].include?("{ok,") raise Chef::Exceptions::ErlCall, stdout_output end diff --git a/lib/chef/provider/execute.rb b/lib/chef/provider/execute.rb index b44112c19e..b291786391 100644 --- a/lib/chef/provider/execute.rb +++ b/lib/chef/provider/execute.rb @@ -16,9 +16,9 @@ # limitations under the License. # -require 'chef/log' -require 'chef/provider' -require 'forwardable' +require "chef/log" +require "chef/provider" +require "forwardable" class Chef class Provider @@ -41,7 +41,7 @@ class Chef def define_resource_requirements # @todo: this should change to raise in some appropriate major version bump. if creates && creates_relative? && !cwd - Chef::Log.warn "Providing a relative path for the creates attribute without the cwd is deprecated and will be changed to fail (CHEF-3819)" + Chef::Log.warn "Providing a relative path for the creates attribute without the cwd is deprecated and will be changed to fail in the future (CHEF-3819)" end end @@ -58,7 +58,16 @@ class Chef end converge_by("execute #{description}") do - result = shell_out!(command, opts) + begin + shell_out!(command, opts) + rescue Mixlib::ShellOut::ShellCommandFailed + if sensitive? + raise Mixlib::ShellOut::ShellCommandFailed, + "Command execution failed. STDOUT/STDERR suppressed for sensitive resource" + else + raise + end + end Chef::Log.info("#{new_resource} ran successfully") end end @@ -69,6 +78,14 @@ class Chef !!new_resource.sensitive end + def live_stream? + Chef::Config[:stream_execute_output] || !!new_resource.live_stream + end + + def stream_to_stdout? + STDOUT.tty? && !Chef::Config[:daemon] + end + def opts opts = {} opts[:timeout] = timeout @@ -80,8 +97,12 @@ class Chef opts[:umask] = umask if umask opts[:log_level] = :info opts[:log_tag] = new_resource.to_s - if STDOUT.tty? && !Chef::Config[:daemon] && Chef::Log.info? && !sensitive? - opts[:live_stream] = STDOUT + if (Chef::Log.info? || live_stream?) && !sensitive? + if run_context.events.formatter? + opts[:live_stream] = Chef::EventDispatch::EventsOutputStream.new(run_context.events, :name => :execute) + elsif stream_to_stdout? + opts[:live_stream] = STDOUT + end end opts end diff --git a/lib/chef/provider/file.rb b/lib/chef/provider/file.rb index c070d29458..a605682718 100644 --- a/lib/chef/provider/file.rb +++ b/lib/chef/provider/file.rb @@ -1,7 +1,7 @@ # # Author:: Adam Jacob (<adam@opscode.com>) # Author:: Lamont Granquist (<lamont@opscode.com>) -# Copyright:: Copyright (c) 2008-2013 Opscode, Inc. +# Copyright:: Copyright (c) 2008-2015 Chef Software, Inc. # License:: Apache License, Version 2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,20 +17,22 @@ # limitations under the License. # -require 'chef/config' -require 'chef/log' -require 'chef/resource/file' -require 'chef/provider' -require 'etc' -require 'fileutils' -require 'chef/scan_access_control' -require 'chef/mixin/checksum' -require 'chef/mixin/file_class' -require 'chef/util/backup' -require 'chef/util/diff' -require 'chef/deprecation/provider/file' -require 'chef/deprecation/warnings' -require 'chef/file_content_management/deploy' +require "chef/config" +require "chef/log" +require "chef/resource/file" +require "chef/provider" +require "etc" +require "fileutils" +require "chef/scan_access_control" +require "chef/mixin/checksum" +require "chef/mixin/file_class" +require "chef/mixin/enforce_ownership_and_permissions" +require "chef/util/backup" +require "chef/util/diff" +require "chef/util/selinux" +require "chef/deprecation/provider/file" +require "chef/deprecation/warnings" +require "chef/file_content_management/deploy" # The Tao of File Providers: # - the content provider must always return a tempfile that we can delete/mv @@ -244,7 +246,7 @@ class Chef else [ Chef::Exceptions::FileTypeMismatch, "File #{path} exists, but is a #{file_type_string(@new_resource.path)}, set force_unlink to true to remove", - "Assuming #{file_type_string(@new_resource.path)} at #{@new_resource.path} would have been removed by a previous resource" + "Assuming #{file_type_string(@new_resource.path)} at #{@new_resource.path} would have been removed by a previous resource", ] end end @@ -265,7 +267,7 @@ class Chef [ Chef::Exceptions::FileTypeMismatch, "File #{path} exists, but is a symlink to #{real_path} which is a #{file_type_string(real_path)}. " + "Disable manage_symlink_source and set force_unlink to remove it.", - "Assuming symlink #{path} or source file #{real_path} would have been fixed by a previous resource" + "Assuming symlink #{path} or source file #{real_path} would have been fixed by a previous resource", ] end rescue Errno::ELOOP @@ -386,10 +388,11 @@ class Chef def update_file_contents do_backup unless needs_creating? - deployment_strategy.deploy(tempfile.path, ::File.realpath(@new_resource.path)) - Chef::Log.info("#{@new_resource} updated file contents #{@new_resource.path}") + deployment_strategy.deploy(tempfile.path, ::File.realpath(new_resource.path)) + Chef::Log.info("#{new_resource} updated file contents #{new_resource.path}") if managing_content? - @new_resource.checksum(checksum(@new_resource.path)) # for reporting + # save final checksum for reporting. + new_resource.final_checksum = checksum(new_resource.path) end end diff --git a/lib/chef/provider/file/content.rb b/lib/chef/provider/file/content.rb index f82bc49db4..96dda1bcbc 100644 --- a/lib/chef/provider/file/content.rb +++ b/lib/chef/provider/file/content.rb @@ -16,8 +16,8 @@ # limitations under the License. # -require 'chef/file_content_management/content_base' -require 'chef/file_content_management/tempfile' +require "chef/file_content_management/content_base" +require "chef/file_content_management/tempfile" class Chef class Provider diff --git a/lib/chef/provider/git.rb b/lib/chef/provider/git.rb index 8418f22933..a0f6d076df 100644 --- a/lib/chef/provider/git.rb +++ b/lib/chef/provider/git.rb @@ -16,10 +16,10 @@ # limitations under the License. # -require 'chef/exceptions' -require 'chef/log' -require 'chef/provider' -require 'fileutils' +require "chef/exceptions" +require "chef/log" +require "chef/provider" +require "fileutils" class Chef class Provider @@ -105,7 +105,7 @@ class Chef end def git_minor_version - @git_minor_version ||= Gem::Version.new(shell_out!('git --version', run_options).stdout.split.last) + @git_minor_version ||= Gem::Version.new(shell_out!("git --version", run_options).stdout.split.last) end def existing_git_clone? @@ -113,14 +113,14 @@ class Chef end def target_dir_non_existent_or_empty? - !::File.exist?(@new_resource.destination) || Dir.entries(@new_resource.destination).sort == ['.','..'] + !::File.exist?(@new_resource.destination) || Dir.entries(@new_resource.destination).sort == [".",".."] end def find_current_revision Chef::Log.debug("#{@new_resource} finding current git revision") if ::File.exist?(::File.join(cwd, ".git")) # 128 is returned when we're not in a git repo. this is fine - result = shell_out!('git rev-parse HEAD', :cwd => cwd, :returns => [0,128]).stdout.strip + result = shell_out!("git rev-parse HEAD", :cwd => cwd, :returns => [0,128]).stdout.strip end sha_hash?(result) ? result : nil end @@ -141,9 +141,9 @@ class Chef remote = @new_resource.remote args = [] - args << "-o #{remote}" unless remote == 'origin' + args << "-o #{remote}" unless remote == "origin" args << "--depth #{@new_resource.depth}" if @new_resource.depth - args << "--no-single-branch" if @new_resource.depth and git_minor_version >= Gem::Version.new('1.7.10') + args << "--no-single-branch" if @new_resource.depth and git_minor_version >= Gem::Version.new("1.7.10") Chef::Log.info "#{@new_resource} cloning repo #{@new_resource.repository} to #{@new_resource.destination}" @@ -250,18 +250,18 @@ class Chef # Using such a degenerate annotated tag would be very # confusing. We avoid the issue by disallowing the use of # annotated tags named 'HEAD'. - if rev_search_pattern != 'HEAD' - found = find_revision(refs, @new_resource.revision, '^{}') + if rev_search_pattern != "HEAD" + found = find_revision(refs, @new_resource.revision, "^{}") else - found = refs_search(refs, 'HEAD') + found = refs_search(refs, "HEAD") end found = find_revision(refs, @new_resource.revision) if found.empty? found.size == 1 ? found.first[0] : nil end def find_revision(refs, revision, suffix="") - found = refs_search(refs, rev_match_pattern('refs/tags/', revision) + suffix) - found = refs_search(refs, rev_match_pattern('refs/heads/', revision) + suffix) if found.empty? + found = refs_search(refs, rev_match_pattern("refs/tags/", revision) + suffix) + found = refs_search(refs, rev_match_pattern("refs/heads/", revision) + suffix) if found.empty? found = refs_search(refs, revision + suffix) if found.empty? found end @@ -275,15 +275,15 @@ class Chef end def rev_search_pattern - if ['', 'HEAD'].include? @new_resource.revision - 'HEAD' + if ["", "HEAD"].include? @new_resource.revision + "HEAD" else - @new_resource.revision + '*' + @new_resource.revision + "*" end end def git_ls_remote(rev_pattern) - command = git(%Q(ls-remote "#{@new_resource.repository}" "#{rev_pattern}")) + command = git(%Q{ls-remote "#{@new_resource.repository}" "#{rev_pattern}"}) shell_out!(command, run_options).stdout end @@ -300,15 +300,15 @@ class Chef # Certain versions of `git` misbehave if git configuration is # inaccessible in $HOME. We need to ensure $HOME matches the # user who is executing `git` not the user running Chef. - env['HOME'] = begin - require 'etc' + env["HOME"] = begin + require "etc" Etc.getpwnam(@new_resource.user).dir rescue ArgumentError # user not found raise Chef::Exceptions::User, "Could not determine HOME for specified user '#{@new_resource.user}' for resource '#{@new_resource.name}'" end end run_opts[:group] = @new_resource.group if @new_resource.group - env['GIT_SSH'] = @new_resource.ssh_wrapper if @new_resource.ssh_wrapper + env["GIT_SSH"] = @new_resource.ssh_wrapper if @new_resource.ssh_wrapper run_opts[:log_tag] = @new_resource.to_s run_opts[:timeout] = @new_resource.timeout if @new_resource.timeout env.merge!(@new_resource.environment) if @new_resource.environment diff --git a/lib/chef/provider/group.rb b/lib/chef/provider/group.rb index a802758dce..2aa9889b17 100644 --- a/lib/chef/provider/group.rb +++ b/lib/chef/provider/group.rb @@ -16,10 +16,10 @@ # limitations under the License. # -require 'chef/provider' -require 'chef/mixin/shell_out' -require 'chef/mixin/command' -require 'etc' +require "chef/provider" +require "chef/mixin/shell_out" +require "chef/mixin/command" +require "etc" class Chef class Provider @@ -125,7 +125,7 @@ class Chef def action_create case @group_exists when false - converge_by("create #{@new_resource.group_name}") do + converge_by("create group #{@new_resource.group_name}") do create_group Chef::Log.info("#{@new_resource} created") end diff --git a/lib/chef/provider/group/aix.rb b/lib/chef/provider/group/aix.rb index 6ac9d03357..1059208ed8 100644 --- a/lib/chef/provider/group/aix.rb +++ b/lib/chef/provider/group/aix.rb @@ -16,13 +16,14 @@ # limitations under the License. # -require 'chef/provider/group/groupadd' -require 'chef/mixin/shell_out' +require "chef/provider/group/groupadd" +require "chef/mixin/shell_out" class Chef class Provider class Group class Aix < Chef::Provider::Group::Groupadd + provides :group, platform: "aix" def required_binaries [ "/usr/bin/mkgroup", @@ -71,7 +72,7 @@ class Chef { :gid => "id" }.sort { |a,b| a[0] <=> b[0] }.each do |field, option| if @current_resource.send(field) != @new_resource.send(field) if @new_resource.send(field) - Chef::Log.debug("#{@new_resource} setting #{field.to_s} to #{@new_resource.send(field)}") + Chef::Log.debug("#{@new_resource} setting #{field} to #{@new_resource.send(field)}") opts << " '#{option}=#{@new_resource.send(field)}'" end end diff --git a/lib/chef/provider/group/dscl.rb b/lib/chef/provider/group/dscl.rb index d7e8f2e827..0b39571458 100644 --- a/lib/chef/provider/group/dscl.rb +++ b/lib/chef/provider/group/dscl.rb @@ -53,14 +53,14 @@ class Chef if group_info group_info.each_line do |line| - key, val = line.split(': ') + key, val = line.split(": ") val.strip! if val case key.downcase - when 'primarygroupid' + when "primarygroupid" @new_resource.gid(val) unless @new_resource.gid @current_resource.gid(val) - when 'groupmembership' - @current_resource.members(val.split(' ')) + when "groupmembership" + @current_resource.members(val.split(" ")) end end end diff --git a/lib/chef/provider/group/gpasswd.rb b/lib/chef/provider/group/gpasswd.rb index 521affac11..011a9d1e63 100644 --- a/lib/chef/provider/group/gpasswd.rb +++ b/lib/chef/provider/group/gpasswd.rb @@ -16,12 +16,13 @@ # limitations under the License. # -require 'chef/provider/group/groupadd' +require "chef/provider/group/groupadd" class Chef class Provider class Group class Gpasswd < Chef::Provider::Group::Groupadd + provides :group def load_current_resource super diff --git a/lib/chef/provider/group/groupadd.rb b/lib/chef/provider/group/groupadd.rb index cb480aab54..684df8455e 100644 --- a/lib/chef/provider/group/groupadd.rb +++ b/lib/chef/provider/group/groupadd.rb @@ -96,15 +96,15 @@ class Chef end def add_member(member) - raise Chef::Exceptions::Group, "you must override add_member in #{self.to_s}" + raise Chef::Exceptions::Group, "you must override add_member in #{self}" end def remove_member(member) - raise Chef::Exceptions::Group, "you must override remove_member in #{self.to_s}" + raise Chef::Exceptions::Group, "you must override remove_member in #{self}" end def set_members(members) - raise Chef::Exceptions::Group, "you must override set_members in #{self.to_s}" + raise Chef::Exceptions::Group, "you must override set_members in #{self}" end # Little bit of magic as per Adam's useradd provider to pull the assign the command line flags @@ -117,7 +117,7 @@ class Chef if @current_resource.send(field) != @new_resource.send(field) if @new_resource.send(field) opts << " #{option} '#{@new_resource.send(field)}'" - Chef::Log.debug("#{@new_resource} set #{field.to_s} to #{@new_resource.send(field)}") + Chef::Log.debug("#{@new_resource} set #{field} to #{@new_resource.send(field)}") end end end @@ -125,7 +125,7 @@ class Chef end def groupadd_options - opts = '' + opts = "" opts << " -r" if @new_resource.system opts << " -o" if @new_resource.non_unique opts diff --git a/lib/chef/provider/group/pw.rb b/lib/chef/provider/group/pw.rb index 7a66ab4d69..e4c129ba9f 100644 --- a/lib/chef/provider/group/pw.rb +++ b/lib/chef/provider/group/pw.rb @@ -20,6 +20,7 @@ class Chef class Provider class Group class Pw < Chef::Provider::Group + provides :group, platform: "freebsd" def load_current_resource super @@ -108,7 +109,7 @@ class Chef else # Append is not set so we're resetting the membership of # the group to the given members. - members_to_be_added = @new_resource.members + members_to_be_added = @new_resource.members.dup @current_resource.members.each do |member| # No need to re-add a member if it's present in the new # list of members diff --git a/lib/chef/provider/group/suse.rb b/lib/chef/provider/group/suse.rb index 7ac2831d02..42e525169f 100644 --- a/lib/chef/provider/group/suse.rb +++ b/lib/chef/provider/group/suse.rb @@ -16,12 +16,14 @@ # limitations under the License. # -require 'chef/provider/group/groupadd' +require "chef/provider/group/groupadd" class Chef class Provider class Group class Suse < Chef::Provider::Group::Groupadd + provides :group, platform: "opensuse", platform_version: "< 12.3" + provides :group, platform: "suse", platform_version: "< 12.0" def load_current_resource super diff --git a/lib/chef/provider/group/usermod.rb b/lib/chef/provider/group/usermod.rb index e50e13c443..f4f3ac55ae 100644 --- a/lib/chef/provider/group/usermod.rb +++ b/lib/chef/provider/group/usermod.rb @@ -16,14 +16,15 @@ # limitations under the License. # -require 'chef/provider/group/groupadd' +require "chef/provider/group/groupadd" class Chef class Provider class Group class Usermod < Chef::Provider::Group::Groupadd - provides :group, os: "openbsd" + provides :group, os: %w{openbsd solaris2 hpux} + provides :group, platform: "opensuse" def load_current_resource super @@ -40,13 +41,13 @@ class Chef requirements.assert(:modify, :manage) do |a| a.assertion { @new_resource.members.empty? || @new_resource.append } - a.failure_message Chef::Exceptions::Group, "setting group members directly is not supported by #{self.to_s}, must set append true in group" + a.failure_message Chef::Exceptions::Group, "setting group members directly is not supported by #{self}, must set append true in group" # No whyrun alternative - this action is simply not supported. end requirements.assert(:all_actions) do |a| a.assertion { @new_resource.excluded_members.empty? } - a.failure_message Chef::Exceptions::Group, "excluded_members is not supported by #{self.to_s}" + a.failure_message Chef::Exceptions::Group, "excluded_members is not supported by #{self}" # No whyrun alternative - this action is simply not supported. end end @@ -61,7 +62,7 @@ class Chef add_member(member) end else - raise Chef::Exceptions::UnsupportedAction, "Setting members directly is not supported by #{self.to_s}" + raise Chef::Exceptions::UnsupportedAction, "Setting members directly is not supported by #{self}" end end @@ -72,7 +73,7 @@ class Chef def remove_member(member) # This provider only supports adding members with # append. This function should never be called. - raise Chef::Exceptions::UnsupportedAction, "Removing members members is not supported by #{self.to_s}" + raise Chef::Exceptions::UnsupportedAction, "Removing members members is not supported by #{self}" end def append_flags diff --git a/lib/chef/provider/group/windows.rb b/lib/chef/provider/group/windows.rb index 54e49b0e06..a665757df4 100644 --- a/lib/chef/provider/group/windows.rb +++ b/lib/chef/provider/group/windows.rb @@ -16,9 +16,9 @@ # limitations under the License. # -require 'chef/provider/user' +require "chef/provider/user" if RUBY_PLATFORM =~ /mswin|mingw32|windows/ - require 'chef/util/windows/net_group' + require "chef/util/windows/net_group" end class Chef diff --git a/lib/chef/provider/http_request.rb b/lib/chef/provider/http_request.rb index 61aff434ed..62dc0b0a46 100644 --- a/lib/chef/provider/http_request.rb +++ b/lib/chef/provider/http_request.rb @@ -16,8 +16,8 @@ # limitations under the License. # -require 'tempfile' -require 'chef/http/simple' +require "tempfile" +require "chef/http/simple" class Chef class Provider @@ -42,7 +42,7 @@ class Chef # and false for a "304 Not Modified" response modified = @http.head( "#{@new_resource.url}", - @new_resource.headers + @new_resource.headers, ) Chef::Log.info("#{@new_resource} HEAD to #{@new_resource.url} successful") Chef::Log.debug("#{@new_resource} HEAD request response: #{modified}") @@ -59,7 +59,7 @@ class Chef message = check_message(@new_resource.message) body = @http.get( "#{@new_resource.url}", - @new_resource.headers + @new_resource.headers, ) Chef::Log.info("#{@new_resource} GET to #{@new_resource.url} successful") Chef::Log.debug("#{@new_resource} GET request response: #{body}") @@ -73,7 +73,7 @@ class Chef body = @http.put( "#{@new_resource.url}", message, - @new_resource.headers + @new_resource.headers, ) Chef::Log.info("#{@new_resource} PUT to #{@new_resource.url} successful") Chef::Log.debug("#{@new_resource} PUT request response: #{body}") @@ -87,7 +87,7 @@ class Chef body = @http.post( "#{@new_resource.url}", message, - @new_resource.headers + @new_resource.headers, ) Chef::Log.info("#{@new_resource} POST to #{@new_resource.url} message: #{message.inspect} successful") Chef::Log.debug("#{@new_resource} POST request response: #{body}") @@ -99,7 +99,7 @@ class Chef converge_by("#{@new_resource} DELETE to #{@new_resource.url}") do body = @http.delete( "#{@new_resource.url}", - @new_resource.headers + @new_resource.headers, ) @new_resource.updated_by_last_action(true) Chef::Log.info("#{@new_resource} DELETE to #{@new_resource.url} successful") diff --git a/lib/chef/provider/ifconfig.rb b/lib/chef/provider/ifconfig.rb index 06080c90c3..95f4b979f5 100644 --- a/lib/chef/provider/ifconfig.rb +++ b/lib/chef/provider/ifconfig.rb @@ -16,13 +16,13 @@ # limitations under the License. # -require 'chef/log' -require 'chef/mixin/command' -require 'chef/mixin/shell_out' -require 'chef/provider' -require 'chef/resource/file' -require 'chef/exceptions' -require 'erb' +require "chef/log" +require "chef/mixin/command" +require "chef/mixin/shell_out" +require "chef/provider" +require "chef/resource/file" +require "chef/exceptions" +require "erb" # Recipe example: # @@ -39,6 +39,8 @@ require 'erb' class Chef class Provider class Ifconfig < Chef::Provider + provides :ifconfig + include Chef::Mixin::ShellOut include Chef::Mixin::Command @@ -107,7 +109,7 @@ class Chef command = add_command converge_by ("run #{command} to add #{@new_resource}") do run_command( - :command => command + :command => command, ) Chef::Log.info("#{@new_resource} added") end @@ -125,7 +127,7 @@ class Chef command = enable_command converge_by ("run #{command} to enable #{@new_resource}") do run_command( - :command => command + :command => command, ) Chef::Log.info("#{@new_resource} enabled") end @@ -139,7 +141,7 @@ class Chef command = delete_command converge_by ("run #{command} to delete #{@new_resource}") do run_command( - :command => command + :command => command, ) Chef::Log.info("#{@new_resource} deleted") end @@ -156,7 +158,7 @@ class Chef command = disable_command converge_by ("run #{command} to disable #{@new_resource}") do run_command( - :command => command + :command => command, ) Chef::Log.info("#{@new_resource} disabled") end @@ -192,7 +194,7 @@ class Chef private def add_command - command = "ifconfig #{@new_resource.device} #{@new_resource.name}" + command = "ifconfig #{@new_resource.device} #{@new_resource.target}" command << " netmask #{@new_resource.mask}" if @new_resource.mask command << " metric #{@new_resource.metric}" if @new_resource.metric command << " mtu #{@new_resource.mtu}" if @new_resource.mtu @@ -200,7 +202,7 @@ class Chef end def enable_command - command = "ifconfig #{@new_resource.device} #{@new_resource.name}" + command = "ifconfig #{@new_resource.device} #{@new_resource.target}" command << " netmask #{@new_resource.mask}" if @new_resource.mask command << " metric #{@new_resource.metric}" if @new_resource.metric command << " mtu #{@new_resource.mtu}" if @new_resource.mtu @@ -216,7 +218,7 @@ class Chef end def loopback_device - 'lo' + "lo" end end end diff --git a/lib/chef/provider/ifconfig/aix.rb b/lib/chef/provider/ifconfig/aix.rb index 8fead44bc6..30e702fe10 100644 --- a/lib/chef/provider/ifconfig/aix.rb +++ b/lib/chef/provider/ifconfig/aix.rb @@ -16,12 +16,13 @@ # limitations under the License. # -require 'chef/provider/ifconfig' +require "chef/provider/ifconfig" class Chef class Provider class Ifconfig class Aix < Chef::Provider::Ifconfig + provides :ifconfig, platform: %w{aix} def load_current_resource @current_resource = Chef::Resource::Ifconfig.new(@new_resource.name) diff --git a/lib/chef/provider/ifconfig/debian.rb b/lib/chef/provider/ifconfig/debian.rb index 7589971143..3885e55998 100644 --- a/lib/chef/provider/ifconfig/debian.rb +++ b/lib/chef/provider/ifconfig/debian.rb @@ -16,13 +16,15 @@ # limitations under the License. # -require 'chef/provider/ifconfig' -require 'chef/util/file_edit' +require "chef/provider/ifconfig" +require "chef/util/file_edit" class Chef class Provider class Ifconfig class Debian < Chef::Provider::Ifconfig + provides :ifconfig, platform: %w{ubuntu}, platform_version: ">= 11.10" + provides :ifconfig, platform: %w{debian}, platform_version: ">= 7.0" INTERFACES_FILE = "/etc/network/interfaces" INTERFACES_DOT_D_DIR = "/etc/network/interfaces.d" diff --git a/lib/chef/provider/ifconfig/redhat.rb b/lib/chef/provider/ifconfig/redhat.rb index ef35b0e012..8c02c1be07 100644 --- a/lib/chef/provider/ifconfig/redhat.rb +++ b/lib/chef/provider/ifconfig/redhat.rb @@ -16,12 +16,13 @@ # limitations under the License. # -require 'chef/provider/ifconfig' +require "chef/provider/ifconfig" class Chef class Provider class Ifconfig class Redhat < Chef::Provider::Ifconfig + provides :ifconfig, platform_family: %w{fedora rhel} def initialize(new_resource, run_context) super(new_resource, run_context) diff --git a/lib/chef/provider/link.rb b/lib/chef/provider/link.rb index c811c13cdf..571610aeed 100644 --- a/lib/chef/provider/link.rb +++ b/lib/chef/provider/link.rb @@ -16,13 +16,13 @@ # limitations under the License. # -require 'chef/config' -require 'chef/log' -require 'chef/mixin/file_class' -require 'chef/resource/link' -require 'chef/provider' -require 'chef/scan_access_control' -require 'chef/util/path_helper' +require "chef/config" +require "chef/log" +require "chef/mixin/file_class" +require "chef/resource/link" +require "chef/provider" +require "chef/scan_access_control" +require "chef/util/path_helper" class Chef class Provider @@ -75,18 +75,18 @@ class Chef a.assertion do if @current_resource.to @current_resource.link_type == @new_resource.link_type and - (@current_resource.link_type == :symbolic or @current_resource.to != '') + (@current_resource.link_type == :symbolic or @current_resource.to != "") else true end end - a.failure_message Chef::Exceptions::Link, "Cannot delete #{@new_resource} at #{@new_resource.target_file}! Not a #{@new_resource.link_type.to_s} link." + a.failure_message Chef::Exceptions::Link, "Cannot delete #{@new_resource} at #{@new_resource.target_file}! Not a #{@new_resource.link_type} link." a.whyrun("Would assume the link at #{@new_resource.target_file} was previously created") end end def canonicalize(path) - Chef::Platform.windows? ? path.gsub('/', '\\') : path + Chef::Platform.windows? ? path.gsub("/", '\\') : path end def action_create diff --git a/lib/chef/provider/lwrp_base.rb b/lib/chef/provider/lwrp_base.rb index 492ddda6da..4298a4aa98 100644 --- a/lib/chef/provider/lwrp_base.rb +++ b/lib/chef/provider/lwrp_base.rb @@ -18,7 +18,9 @@ # limitations under the License. # -require 'chef/provider' +require "chef/provider" +require "chef/dsl/recipe" +require "chef/dsl/include_recipe" class Chef class Provider @@ -27,124 +29,71 @@ class Chef # Base class from which LWRP providers inherit. class LWRPBase < Provider - # Chef::Provider::LWRPBase::InlineResources - # Implementation of inline resource convergence for LWRP providers. See - # Provider::LWRPBase.use_inline_resources for a longer explanation. - # - # This code is restricted to a module so that it can be selectively - # applied to providers on an opt-in basis. - module InlineResources - - # Class methods for InlineResources. Overrides the `action` DSL method - # with one that enables inline resource convergence. - module ClassMethods - # Defines an action method on the provider, using - # recipe_eval_with_update_check to execute the given block. - def action(name, &block) - define_method("action_#{name}") do - recipe_eval_with_update_check(&block) - end - end - end - - # Executes the given block in a temporary run_context with its own - # resource collection. After the block is executed, any resources - # declared inside are converged, and if any are updated, the - # new_resource will be marked updated. - def recipe_eval_with_update_check(&block) - saved_run_context = @run_context - temp_run_context = @run_context.dup - @run_context = temp_run_context - @run_context.resource_collection = Chef::ResourceCollection.new - - return_value = instance_eval(&block) - Chef::Runner.new(@run_context).converge - return_value - ensure - @run_context = saved_run_context - if temp_run_context.resource_collection.any? {|r| r.updated? } - new_resource.updated_by_last_action(true) - end - end - - end - - extend Chef::Mixin::ConvertToClassName - extend Chef::Mixin::FromFile - include Chef::DSL::Recipe # These were previously provided by Chef::Mixin::RecipeDefinitionDSLCore. - # They are not included by its replacment, Chef::DSL::Recipe, but + # They are not included by its replacement, Chef::DSL::Recipe, but # they may be used in existing LWRPs. include Chef::DSL::PlatformIntrospection include Chef::DSL::DataQuery - def self.build_from_file(cookbook_name, filename, run_context) - provider_class = nil - provider_name = filename_to_qualified_string(cookbook_name, filename) + # Allow include_recipe from within LWRP provider code + include Chef::DSL::IncludeRecipe + + # no-op `load_current_resource`. Allows simple LWRP providers to work + # without defining this method explicitly (silences + # Chef::Exceptions::Override exception) + def load_current_resource + end + + # class methods + class <<self + include Chef::Mixin::ConvertToClassName + include Chef::Mixin::FromFile + + def build_from_file(cookbook_name, filename, run_context) + if LWRPBase.loaded_lwrps[filename] + Chef::Log.debug("LWRP provider #{filename} from cookbook #{cookbook_name} has already been loaded! Skipping the reload.") + return loaded_lwrps[filename] + end - class_name = convert_to_class_name(provider_name) + resource_name = filename_to_qualified_string(cookbook_name, filename) - if Chef::Provider.const_defined?(class_name, false) - Chef::Log.info("#{class_name} light-weight provider is already initialized -- Skipping loading #{filename}!") - Chef::Log.debug("Overriding already defined LWRPs is not supported anymore starting with Chef 12.") - provider_class = Chef::Provider.const_get(class_name) - else + # We load the class first to give it a chance to set its own name provider_class = Class.new(self) - Chef::Provider.const_set(class_name, provider_class) + provider_class.provides resource_name.to_sym provider_class.class_from_file(filename) - Chef::Log.debug("Loaded contents of #{filename} into a provider named #{provider_name} defined in Chef::Provider::#{class_name}") - end - provider_class - end + # Respect resource_name set inside the LWRP + provider_class.instance_eval do + define_singleton_method(:to_s) do + "LWRP provider #{resource_name} from cookbook #{cookbook_name}" + end + define_singleton_method(:inspect) { to_s } + end - # Enables inline evaluation of resources in provider actions. - # - # Without this option, any resources declared inside the LWRP are added - # to the resource collection after the current position at the time the - # action is executed. Because they are added to the primary resource - # collection for the chef run, they can notify other resources outside - # the LWRP, and potentially be notified by resources outside the LWRP - # (but this is complicated by the fact that they don't exist until the - # provider executes). In this mode, it is impossible to correctly set the - # updated_by_last_action flag on the parent LWRP resource, since it - # executes and returns before its component resources are run. - # - # With this option enabled, each action creates a temporary run_context - # with its own resource collection, evaluates the action's code in that - # context, and then converges the resources created. If any resources - # were updated, then this provider's new_resource will be marked updated. - # - # In this mode, resources created within the LWRP cannot interact with - # external resources via notifies, though notifications to other - # resources within the LWRP will work. Delayed notifications are executed - # at the conclusion of the provider's action, *not* at the end of the - # main chef run. - # - # This mode of evaluation is experimental, but is believed to be a better - # set of tradeoffs than the append-after mode, so it will likely become - # the default in a future major release of Chef. - # - def self.use_inline_resources - extend InlineResources::ClassMethods - include InlineResources - end + Chef::Log.debug("Loaded contents of #{filename} into provider #{resource_name} (#{provider_class})") - # DSL for defining a provider's actions. - def self.action(name, &block) - define_method("action_#{name}") do - instance_eval(&block) + LWRPBase.loaded_lwrps[filename] = true + + Chef::Provider.register_deprecated_lwrp_class(provider_class, convert_to_class_name(resource_name)) + + provider_class end - end - # no-op `load_current_resource`. Allows simple LWRP providers to work - # without defining this method explicitly (silences - # Chef::Exceptions::Override exception) - def load_current_resource - end + # DSL for defining a provider's actions. + def action(name, &block) + define_method("action_#{name}") do + instance_eval(&block) + end + end + + protected + def loaded_lwrps + @loaded_lwrps ||= {} + end + end end end end diff --git a/lib/chef/provider/mdadm.rb b/lib/chef/provider/mdadm.rb index 325f1b5977..b55e6abc1a 100644 --- a/lib/chef/provider/mdadm.rb +++ b/lib/chef/provider/mdadm.rb @@ -16,8 +16,8 @@ # limitations under the License. # -require 'chef/log' -require 'chef/provider' +require "chef/log" +require "chef/provider" class Chef class Provider diff --git a/lib/chef/provider/mount.rb b/lib/chef/provider/mount.rb index 1631d87033..ede36417d1 100644 --- a/lib/chef/provider/mount.rb +++ b/lib/chef/provider/mount.rb @@ -17,14 +17,13 @@ # limitations under the License. # -require 'chef/log' -require 'chef/mixin/shell_out' -require 'chef/provider' +require "chef/log" +require "chef/mixin/shell_out" +require "chef/provider" class Chef class Provider class Mount < Chef::Provider - include Chef::Mixin::ShellOut attr_accessor :unmount_retries @@ -43,13 +42,17 @@ class Chef end def action_mount - unless current_resource.mounted + if current_resource.mounted + if mount_options_unchanged? + Chef::Log.debug("#{new_resource} is already mounted") + else + action_remount + end + else converge_by("mount #{current_resource.device} to #{current_resource.mount_point}") do mount_fs Chef::Log.info("#{new_resource} mounted") end - else - Chef::Log.debug("#{new_resource} is already mounted") end end @@ -115,12 +118,12 @@ class Chef # should actually check if the filesystem is mounted (not just return current_resource) and return true/false def mounted? - raise Chef::Exceptions::UnsupportedAction, "#{self.to_s} does not implement #mounted?" + raise Chef::Exceptions::UnsupportedAction, "#{self} does not implement #mounted?" end # should check new_resource against current_resource to see if mount options need updating, returns true/false def mount_options_unchanged? - raise Chef::Exceptions::UnsupportedAction, "#{self.to_s} does not implement #mount_options_unchanged?" + raise Chef::Exceptions::UnsupportedAction, "#{self} does not implement #mount_options_unchanged?" end # @@ -131,28 +134,28 @@ class Chef # should implement mounting of the filesystem, raises if action does not succeed def mount_fs - raise Chef::Exceptions::UnsupportedAction, "#{self.to_s} does not support :mount" + raise Chef::Exceptions::UnsupportedAction, "#{self} does not support :mount" end # should implement unmounting of the filesystem, raises if action does not succeed def umount_fs - raise Chef::Exceptions::UnsupportedAction, "#{self.to_s} does not support :umount" + raise Chef::Exceptions::UnsupportedAction, "#{self} does not support :umount" end # should implement remounting of the filesystem (via a -o remount or some other atomic-ish action that isn't # simply a umount/mount style remount), raises if action does not succeed def remount_fs - raise Chef::Exceptions::UnsupportedAction, "#{self.to_s} does not support :remount" + raise Chef::Exceptions::UnsupportedAction, "#{self} does not support :remount" end # should implement enabling of the filesystem (e.g. in /etc/fstab), raises if action does not succeed def enable_fs - raise Chef::Exceptions::UnsupportedAction, "#{self.to_s} does not support :enable" + raise Chef::Exceptions::UnsupportedAction, "#{self} does not support :enable" end # should implement disabling of the filesystem (e.g. in /etc/fstab), raises if action does not succeed def disable_fs - raise Chef::Exceptions::UnsupportedAction, "#{self.to_s} does not support :disable" + raise Chef::Exceptions::UnsupportedAction, "#{self} does not support :disable" end private diff --git a/lib/chef/provider/mount/aix.rb b/lib/chef/provider/mount/aix.rb index 0d7e11a1b8..a6fccfadf4 100644 --- a/lib/chef/provider/mount/aix.rb +++ b/lib/chef/provider/mount/aix.rb @@ -16,12 +16,13 @@ # limitations under the License. # -require 'chef/provider/mount' +require "chef/provider/mount" class Chef class Provider class Mount class Aix < Chef::Provider::Mount::Mount + provides :mount, platform: %w{aix} # Override for aix specific handling def initialize(new_resource, run_context) @@ -31,7 +32,7 @@ class Chef @new_resource.options.clear end if @new_resource.fstype == "auto" - @new_resource.fstype = nil + @new_resource.send(:clear_fstype) end end @@ -98,13 +99,13 @@ class Chef end command << case @new_resource.device_type - when :device - " #{device_real}" - when :label - " -L #{@new_resource.device}" - when :uuid - " -U #{@new_resource.device}" - end + when :device + " #{device_real}" + when :label + " -L #{@new_resource.device}" + when :uuid + " -U #{@new_resource.device}" + end command << " #{@new_resource.mount_point}" shell_out!(command) Chef::Log.debug("#{@new_resource} is mounted at #{@new_resource.mount_point}") @@ -173,7 +174,7 @@ class Chef end end + end end - end end end diff --git a/lib/chef/provider/mount/mount.rb b/lib/chef/provider/mount/mount.rb index 0a6e269d2d..e0bddb454a 100644 --- a/lib/chef/provider/mount/mount.rb +++ b/lib/chef/provider/mount/mount.rb @@ -16,14 +16,16 @@ # limitations under the License. # -require 'chef/provider/mount' -require 'chef/log' +require "chef/provider/mount" +require "chef/log" class Chef class Provider class Mount class Mount < Chef::Provider::Mount + provides :mount + def initialize(new_resource, run_context) super @real_device = nil @@ -102,13 +104,13 @@ class Chef command = "mount -t #{@new_resource.fstype}" command << " -o #{@new_resource.options.join(',')}" unless @new_resource.options.nil? || @new_resource.options.empty? command << case @new_resource.device_type - when :device - " #{device_real}" - when :label - " -L #{@new_resource.device}" - when :uuid - " -U #{@new_resource.device}" - end + when :device + " #{device_real}" + when :label + " -L #{@new_resource.device}" + when :uuid + " -U #{@new_resource.device}" + end command << " #{@new_resource.mount_point}" shell_out!(command) Chef::Log.debug("#{@new_resource} is mounted at #{@new_resource.mount_point}") @@ -168,7 +170,7 @@ class Chef found = false ::File.readlines("/etc/fstab").reverse_each do |line| if !found && line =~ /^#{device_fstab_regex}\s+#{Regexp.escape(@new_resource.mount_point)}\s/ - found = true + found = true Chef::Log.debug("#{@new_resource} is removed from fstab") next else @@ -191,7 +193,7 @@ class Chef def device_should_exist? ( @new_resource.device != "none" ) && ( not network_device? ) && - ( not %w[ cgroup tmpfs fuse ].include? @new_resource.fstype ) + ( not %w{ cgroup tmpfs fuse }.include? @new_resource.fstype ) end private @@ -257,9 +259,9 @@ class Chef def mount_options_unchanged? @current_resource.fstype == @new_resource.fstype and - @current_resource.options == @new_resource.options and - @current_resource.dump == @new_resource.dump and - @current_resource.pass == @new_resource.pass + @current_resource.options == @new_resource.options and + @current_resource.dump == @new_resource.dump and + @current_resource.pass == @new_resource.pass end end diff --git a/lib/chef/provider/mount/solaris.rb b/lib/chef/provider/mount/solaris.rb index d8cec24138..76f1ac0137 100644 --- a/lib/chef/provider/mount/solaris.rb +++ b/lib/chef/provider/mount/solaris.rb @@ -18,18 +18,20 @@ # limitations under the License. # -require 'chef/provider/mount' -require 'chef/log' -require 'forwardable' +require "chef/provider/mount" +require "chef/log" +require "forwardable" class Chef class Provider class Mount # Mount Solaris File systems class Solaris < Chef::Provider::Mount + provides :mount, platform: %w{openindiana opensolaris nexentacore omnios solaris2 smartos} + extend Forwardable - VFSTAB = '/etc/vfstab'.freeze + VFSTAB = "/etc/vfstab".freeze def_delegator :@new_resource, :device, :device def_delegator :@new_resource, :device_type, :device_type @@ -56,7 +58,7 @@ class Chef a.whyrun("Assuming device #{device} would have been created") end - unless fsck_device == '-' + unless fsck_device == "-" requirements.assert(:mount, :remount) do |a| a.assertion { ::File.exist?(fsck_device) } a.failure_message(Chef::Exceptions::Mount, "Device #{fsck_device} does not exist") @@ -73,7 +75,7 @@ class Chef def mount_fs actual_options = options || [] - actual_options.delete('noauto') + actual_options.delete("noauto") command = "mount -F #{fstype}" command << " -o #{actual_options.join(',')}" unless actual_options.empty? command << " #{device} #{mount_point}" @@ -87,8 +89,8 @@ class Chef def remount_fs # FIXME: Should remount always do the remount or only if the options change? actual_options = options || [] - actual_options.delete('noauto') - mount_options = actual_options.empty? ? '' : ",#{actual_options.join(',')}" + actual_options.delete("noauto") + mount_options = actual_options.empty? ? "" : ",#{actual_options.join(',')}" shell_out!("mount -o remount#{mount_options} #{mount_point}") end @@ -115,7 +117,7 @@ class Chef end def etc_tempfile - yield Tempfile.open('vfstab', '/etc') + yield Tempfile.open("vfstab", "/etc") end def mount_options_unchanged? @@ -127,7 +129,7 @@ class Chef current_options == new_options && current_resource.dump == dump && current_resource.pass == pass && - current_resource.options.include?('noauto') == !mount_at_boot? + current_resource.options.include?("noauto") == !mount_at_boot? end def update_current_resource_state @@ -148,7 +150,7 @@ class Chef # /dev/dsk/c1t0d0s0 on / type ufs read/write/setuid/devices/intr/largefiles/logging/xattr/onerror=panic/dev=700040 on Tue May 1 11:33:55 2012 def mounted? mounted = false - shell_out!('mount -v').stdout.each_line do |line| + shell_out!("mount -v").stdout.each_line do |line| case line when /^#{device_regex}\s+on\s+#{Regexp.escape(mount_point)}\s+/ Chef::Log.debug("Special device #{device} is mounted as #{mount_point}") @@ -180,14 +182,14 @@ class Chef options = Regexp.last_match[4] # Store the 'mount at boot' column from vfstab as the 'noauto' option # in current_resource.options (linux style) - if Regexp.last_match[3] == 'no' + if Regexp.last_match[3] == "no" if options.nil? || options.empty? - options = 'noauto' + options = "noauto" else - options += ',noauto' + options += ",noauto" end end - pass = (Regexp.last_match[2] == '-') ? 0 : Regexp.last_match[2].to_i + pass = (Regexp.last_match[2] == "-") ? 0 : Regexp.last_match[2].to_i Chef::Log.debug("Found mount #{device} to #{mount_point} in #{VFSTAB}") next when /^[-\/\w]+\s+[-\/\w]+\s+#{Regexp.escape(mount_point)}\s+/ @@ -200,16 +202,16 @@ class Chef end def device_should_exist? - !%w(tmpfs nfs ctfs proc mntfs objfs sharefs fd smbfs vxfs).include?(fstype) + !%w{tmpfs nfs ctfs proc mntfs objfs sharefs fd smbfs vxfs}.include?(fstype) end def mount_at_boot? - options.nil? || !options.include?('noauto') + options.nil? || !options.include?("noauto") end def vfstab_write(contents) etc_tempfile do |f| - f.write(contents.join('')) + f.write(contents.join("")) f.close # move, preserving modes of destination file mover = Chef::FileContentManagement::Deploy.strategy(true) @@ -219,13 +221,13 @@ class Chef def vfstab_entry actual_options = unless options.nil? - tempops = options.dup - tempops.delete('noauto') - tempops - end - autostr = mount_at_boot? ? 'yes' : 'no' - passstr = pass == 0 ? '-' : pass - optstr = (actual_options.nil? || actual_options.empty?) ? '-' : actual_options.join(',') + tempops = options.dup + tempops.delete("noauto") + tempops + end + autostr = mount_at_boot? ? "yes" : "no" + passstr = pass == 0 ? "-" : pass + optstr = (actual_options.nil? || actual_options.empty?) ? "-" : actual_options.join(",") "\n#{device}\t#{fsck_device}\t#{mount_point}\t#{fstype}\t#{passstr}\t#{autostr}\t#{optstr}\n" end @@ -234,7 +236,7 @@ class Chef found = false ::File.readlines(VFSTAB).reverse_each do |line| if !found && line =~ /^#{device_regex}\s+\S+\s+#{Regexp.escape(mount_point)}/ - found = true + found = true Chef::Log.debug("#{new_resource} is removed from vfstab") next end @@ -252,7 +254,7 @@ class Chef def options_remove_noauto(temp_options) new_options = [] new_options += temp_options.nil? ? [] : temp_options - new_options.delete('noauto') + new_options.delete("noauto") new_options end diff --git a/lib/chef/provider/mount/windows.rb b/lib/chef/provider/mount/windows.rb index 87873474b3..0cf50d07bf 100644 --- a/lib/chef/provider/mount/windows.rb +++ b/lib/chef/provider/mount/windows.rb @@ -16,10 +16,10 @@ # limitations under the License. # -require 'chef/provider/mount' +require "chef/provider/mount" if RUBY_PLATFORM =~ /mswin|mingw32|windows/ - require 'chef/util/windows/net_use' - require 'chef/util/windows/volume' + require "chef/util/windows/net_use" + require "chef/util/windows/volume" end class Chef diff --git a/lib/chef/provider/ohai.rb b/lib/chef/provider/ohai.rb index a6b5ab5daa..6535805013 100644 --- a/lib/chef/provider/ohai.rb +++ b/lib/chef/provider/ohai.rb @@ -16,11 +16,12 @@ # limitations under the License. # -require 'ohai' +require "ohai" class Chef class Provider class Ohai < Chef::Provider + provides :ohai def whyrun_supported? true diff --git a/lib/chef/provider/osx_profile.rb b/lib/chef/provider/osx_profile.rb new file mode 100644 index 0000000000..071b4b0462 --- /dev/null +++ b/lib/chef/provider/osx_profile.rb @@ -0,0 +1,254 @@ +# +# Author:: Nate Walck (<nate.walck@gmail.com>) +# Copyright:: Copyright (c) 2015 Facebook, 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/log" +require "chef/provider" +require "chef/resource" +require "chef/resource/file" +require "uuidtools" + +class Chef + class Provider + class OsxProfile < Chef::Provider + include Chef::Mixin::Command + provides :osx_profile, os: "darwin" + provides :osx_config_profile, os: "darwin" + + def whyrun_supported? + true + end + + def load_current_resource + @current_resource = Chef::Resource::OsxProfile.new(@new_resource.name) + @current_resource.profile_name(@new_resource.profile_name) + + all_profiles = get_installed_profiles + @new_resource.profile( + @new_resource.profile || + @new_resource.profile_name + ) + + @new_profile_hash = get_profile_hash(@new_resource.profile) + @new_profile_hash["PayloadUUID"] = + config_uuid(@new_profile_hash) if @new_profile_hash + + if @new_profile_hash + @new_profile_identifier = @new_profile_hash["PayloadIdentifier"] + else + @new_profile_identifier = @new_resource.identifier || + @new_resource.profile_name + end + + current_profile = all_profiles["_computerlevel"].find { + |item| item["ProfileIdentifier"] == + @new_profile_identifier + } + @current_resource.profile(current_profile) + + end + + def define_resource_requirements + requirements.assert(:remove) do |a| + if @new_profile_identifier + a.assertion { + !@new_profile_identifier.nil? and + !@new_profile_identifier.end_with?(".mobileconfig") and + /^\w+(?:\.\w+)+$/.match(@new_profile_identifier) + } + a.failure_message RuntimeError, "when removing using the identifier attribute, it must match the profile identifier" + else + new_profile_name = @new_resource.profile_name + a.assertion { + !new_profile_name.end_with?(".mobileconfig") and + /^\w+(?:\.\w+)+$/.match(new_profile_name) + } + a.failure_message RuntimeError, "When removing by resource name, it must match the profile identifier " + end + end + + requirements.assert(:install) do |a| + if @new_profile_hash.is_a?(Hash) + a.assertion { + @new_profile_hash.include?("PayloadIdentifier") + } + a.failure_message RuntimeError, "The specified profile does not seem to be valid" + end + if @new_profile_hash.is_a?(String) + a.assertion { + @new_profile_hash.end_with?(".mobileconfig") + } + a.failure_message RuntimeError, "#{new_profile_hash}' is not a valid profile" + end + end + end + + def action_install + unless profile_installed? + converge_by("install profile #{@new_profile_identifier}") do + profile_path = write_profile_to_disk + install_profile(profile_path) + get_installed_profiles(true) + end + end + end + + def action_remove + # Clean up profile after removing it + if profile_installed? + converge_by("remove profile #{@new_profile_identifier}") do + remove_profile + get_installed_profiles(true) + end + end + end + + def load_profile_hash(new_profile) + # file must exist in cookbook + if new_profile.end_with?(".mobileconfig") + unless cookbook_file_available?(new_profile) + error_string = "#{self}: '#{new_profile}' not found in cookbook" + raise Chef::Exceptions::FileNotFound, error_string + end + cookbook_profile = cache_cookbook_profile(new_profile) + return read_plist(cookbook_profile) + else + return nil + end + end + + def cookbook_file_available?(cookbook_file) + run_context.has_cookbook_file_in_cookbook?( + @new_resource.cookbook_name, cookbook_file + ) + end + + def get_cache_dir + cache_dir = Chef::FileCache.create_cache_path( + "profiles/#{@new_resource.cookbook_name}" + ) + end + + def cache_cookbook_profile(cookbook_file) + Chef::FileCache.create_cache_path( + ::File.join( + "profiles", + @new_resource.cookbook_name, + ::File.dirname(cookbook_file), + ) + ) + remote_file = Chef::Resource::CookbookFile.new( + ::File.join( + get_cache_dir, + "#{cookbook_file}.remote", + ), + run_context, + ) + remote_file.cookbook_name = @new_resource.cookbook_name + remote_file.source(cookbook_file) + remote_file.backup(false) + remote_file.run_action(:create) + remote_file.path + end + + def get_profile_hash(new_profile) + if new_profile.is_a?(Hash) + return new_profile + elsif new_profile.is_a?(String) + return load_profile_hash(new_profile) + end + end + + def config_uuid(profile) + # Make a UUID of the profile contents and return as string + UUIDTools::UUID.sha1_create( + UUIDTools::UUID_DNS_NAMESPACE, + profile.to_s, + ).to_s + end + + def write_profile_to_disk + @new_resource.path(Chef::FileCache.create_cache_path("profiles")) + tempfile = Chef::FileContentManagement::Tempfile.new(@new_resource).tempfile + tempfile.write(@new_profile_hash.to_plist) + tempfile.close + tempfile.path + end + + def install_profile(profile_path) + cmd = "profiles -I -F '#{profile_path}'" + Chef::Log.debug("cmd: #{cmd}") + shellout_results = shell_out(cmd) + shellout_results.exitstatus + end + + def remove_profile + cmd = "profiles -R -p '#{@new_profile_identifier}'" + Chef::Log.debug("cmd: #{cmd}") + shellout_results = shell_out(cmd) + shellout_results.exitstatus + end + + def get_installed_profiles(update=nil) + if update + node.run_state[:config_profiles] = query_installed_profiles + else + node.run_state[:config_profiles] ||= query_installed_profiles + end + end + + def query_installed_profiles + # Dump all profile metadata to a tempfile + tempfile = generate_tempfile + write_installed_profiles(tempfile) + installed_profiles = read_plist(tempfile) + Chef::Log.debug("Saved profiles to run_state") + # Clean up the temp file as we do not need it anymore + ::File.unlink(tempfile) + installed_profiles + end + + def generate_tempfile + tempfile = ::Dir::Tmpname.create("allprofiles.plist") {} + end + + def write_installed_profiles(tempfile) + cmd = "profiles -P -o '#{tempfile}'" + shell_out!(cmd) + end + + def read_plist(xml_file) + Plist::parse_xml(xml_file) + end + + def profile_installed? + # Profile Identifier and UUID must match a currently installed profile + if @current_resource.profile.nil? or @current_resource.profile.empty? + false + else + if @new_resource.action.include?(:remove) + true + else + @current_resource.profile["ProfileUUID"] == + @new_profile_hash["PayloadUUID"] + end + end + end + + end + end +end diff --git a/lib/chef/provider/package.rb b/lib/chef/provider/package.rb index 2e8e29981b..ba256f6bea 100644 --- a/lib/chef/provider/package.rb +++ b/lib/chef/provider/package.rb @@ -1,6 +1,6 @@ # # Author:: Adam Jacob (<adam@opscode.com>) -# Copyright:: Copyright (c) 2008 Opscode, Inc. +# Copyright:: Copyright (c) 2008-2015 Chef Software, Inc. # License:: Apache License, Version 2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -16,17 +16,24 @@ # limitations under the License. # -require 'chef/mixin/shell_out' -require 'chef/mixin/command' -require 'chef/log' -require 'chef/file_cache' -require 'chef/platform' +require "chef/mixin/shell_out" +require "chef/mixin/command" +require "chef/mixin/subclass_directive" +require "chef/log" +require "chef/file_cache" +require "chef/platform" class Chef class Provider class Package < Chef::Provider include Chef::Mixin::Command include Chef::Mixin::ShellOut + extend Chef::Mixin::SubclassDirective + + # subclasses declare this if they want all their arguments as arrays of packages and names + subclass_directive :use_multipackage_api + # subclasses declare this if they want sources (filenames) pulled from their package names + subclass_directive :use_package_name_for_source # # Hook that subclasses use to populate the candidate_version(s) @@ -43,6 +50,14 @@ class Chef true end + def check_resource_semantics! + # FIXME: this is not universally true and subclasses are needing to override this and no-ops it. It should be turned into + # another "subclass_directive" and the apt and yum providers should declare that they need this behavior. + if new_resource.package_name.is_a?(Array) && new_resource.source != nil + raise Chef::Exceptions::InvalidResourceSpecification, "You may not specify both multipackage and source" + end + end + def load_current_resource end @@ -80,11 +95,10 @@ class Chef end end - # XXX: mutating the new resource is generally bad - @new_resource.version(versions_for_new_resource) - converge_by(install_description) do - install_package(package_names_for_targets, versions_for_targets) + multipackage_api_adapter(package_names_for_targets, versions_for_targets) do |name, version| + install_package(name, version) + end Chef::Log.info("#{@new_resource} installed #{package_names_for_targets} at #{versions_for_targets}") end end @@ -107,18 +121,17 @@ class Chef return end - # XXX: mutating the new resource is generally bad - @new_resource.version(versions_for_new_resource) - converge_by(upgrade_description) do - upgrade_package(package_names_for_targets, versions_for_targets) - log_allow_downgrade = allow_downgrade ? '(allow_downgrade)' : '' + multipackage_api_adapter(package_names_for_targets, versions_for_targets) do |name, version| + upgrade_package(name, version) + end + log_allow_downgrade = allow_downgrade ? "(allow_downgrade)" : "" Chef::Log.info("#{@new_resource} upgraded#{log_allow_downgrade} #{package_names_for_targets} to #{versions_for_targets}") end end def upgrade_description - log_allow_downgrade = allow_downgrade ? '(allow_downgrade)' : '' + log_allow_downgrade = allow_downgrade ? "(allow_downgrade)" : "" description = [] target_version_array.each_with_index do |target_version, i| next if target_version.nil? @@ -132,12 +145,13 @@ class Chef private :upgrade_description - # @todo: ability to remove an array of packages def action_remove if removing_package? description = @new_resource.version ? "version #{@new_resource.version} of " : "" - converge_by("remove #{description} package #{@current_resource.package_name}") do - remove_package(@current_resource.package_name, @new_resource.version) + converge_by("remove #{description}package #{@current_resource.package_name}") do + multipackage_api_adapter(@current_resource.package_name, @new_resource.version) do |name, version| + remove_package(name, version) + end Chef::Log.info("#{@new_resource} removed") end else @@ -166,18 +180,18 @@ class Chef end end - # @todo: ability to purge an array of packages def action_purge if removing_package? description = @new_resource.version ? "version #{@new_resource.version} of" : "" converge_by("purge #{description} package #{@current_resource.package_name}") do - purge_package(@current_resource.package_name, @new_resource.version) + multipackage_api_adapter(@current_resource.package_name, @new_resource.version) do |name, version| + purge_package(name, version) + end Chef::Log.info("#{@new_resource} purged") end end end - # @todo: ability to reconfigure an array of packages def action_reconfig if @current_resource.version == nil then Chef::Log.debug("#{@new_resource} is NOT installed - nothing to do") @@ -192,7 +206,10 @@ class Chef if preseed_file = get_preseed_file(@new_resource.package_name, @current_resource.version) converge_by("reconfigure package #{@new_resource.package_name}") do preseed_package(preseed_file) - reconfig_package(@new_resource.package_name, @current_resource.version) + multipackage_api_adapter(@new_resource.package_name, @current_resource.version) do |name, version| + reconfig_package(name, version) + + end Chef::Log.info("#{@new_resource} reconfigured") end else @@ -201,31 +218,40 @@ class Chef end # @todo use composition rather than inheritance + + def multipackage_api_adapter(name, version) + if use_multipackage_api? + yield [name].flatten, [version].flatten + else + yield name, version + end + end + def install_package(name, version) - raise Chef::Exceptions::UnsupportedAction, "#{self.to_s} does not support :install" + raise Chef::Exceptions::UnsupportedAction, "#{self} does not support :install" end def upgrade_package(name, version) - raise Chef::Exceptions::UnsupportedAction, "#{self.to_s} does not support :upgrade" + raise Chef::Exceptions::UnsupportedAction, "#{self} does not support :upgrade" end def remove_package(name, version) - raise Chef::Exceptions::UnsupportedAction, "#{self.to_s} does not support :remove" + raise Chef::Exceptions::UnsupportedAction, "#{self} does not support :remove" end def purge_package(name, version) - raise Chef::Exceptions::UnsupportedAction, "#{self.to_s} does not support :purge" + raise Chef::Exceptions::UnsupportedAction, "#{self} does not support :purge" end def preseed_package(file) - raise Chef::Exceptions::UnsupportedAction, "#{self.to_s} does not support pre-seeding package install/upgrade instructions" + raise Chef::Exceptions::UnsupportedAction, "#{self} does not support pre-seeding package install/upgrade instructions" end def reconfig_package(name, version) - raise( Chef::Exceptions::UnsupportedAction, "#{self.to_s} does not support :reconfig" ) + raise( Chef::Exceptions::UnsupportedAction, "#{self} does not support :reconfig" ) end - # this is heavily used by subclasses + # used by subclasses. deprecated. use #a_to_s instead. def expand_options(options) options ? " #{options}" : "" end @@ -316,18 +342,6 @@ class Chef multipackage? ? versions_for_targets : versions_for_targets[0] end - # We need to mutate @new_resource.version() for some reason and this is a helper so that we inject the right - # class (String or Array) into that attribute based on if we're handling an array of package names or not. - # - # @return [String, Array<String>] target_versions coerced into the correct type for back-compat - def versions_for_new_resource - if multipackage? - target_version_array - else - target_version_array[0] - end - end - # Return an array indexed the same as *_version_array which contains either the target version to install/upgrade to # or else nil if the package is not being modified. # @@ -464,10 +478,38 @@ class Chef # @return [Array] new_version(s) as an array def new_version_array - @new_version_array ||= - [ new_resource.version ].flatten.map do |v| - ( v.nil? || v.empty? ) ? nil : v + [ new_resource.version ].flatten.map { |v| v.to_s.empty? ? nil : v } + end + + # TIP: less error prone to simply always call resolved_source_array, even if you + # don't think that you need to. + # + # @return [Array] new_resource.source as an array + def source_array + if new_resource.source.nil? + package_name_array.map { nil } + else + [ new_resource.source ].flatten + end + end + + # Helper to handle use_package_name_for_source to convert names into local packages to install. + # + # @return [Array] Array of sources with package_names converted to sources + def resolved_source_array + @resolved_source_array ||= + begin + source_array.each_with_index.map do |source, i| + package_name = package_name_array[i] + # we require at least one '/' in the package_name to avoid [XXX_]package 'foo' breaking due to a random 'foo' file in cwd + if use_package_name_for_source? && source.nil? && package_name.match(/#{::File::SEPARATOR}/) && ::File.exist?(package_name) + Chef::Log.debug("No package source specified, but #{package_name} exists on filesystem, using #{package_name} as source.") + package_name + else + source + end end + end end # @todo: extract apt/dpkg specific preseeding to a helper class @@ -487,6 +529,37 @@ class Chef false end end + + def shell_out_with_timeout(*command_args) + shell_out(*add_timeout_option(command_args)) + end + + def shell_out_with_timeout!(*command_args) + shell_out!(*add_timeout_option(command_args)) + end + + def add_timeout_option(command_args) + args = command_args.dup + if args.last.is_a?(Hash) + options = args.pop.dup + options[:timeout] = new_resource.timeout if new_resource.timeout + options[:timeout] = 900 unless options.has_key?(:timeout) + args << options + else + args << { :timeout => new_resource.timeout ? new_resource.timeout : 900 } + end + args + end + + # Helper for sublcasses to convert an array of string args into a string. It + # will compact nil or empty strings in the array and will join the array elements + # with spaces, without introducing any double spaces for nil/empty elements. + # + # @param args [String] variable number of string arguments + # @return [String] nicely concatenated string or empty string + def a_to_s(*args) + args.reject {|i| i.nil? || i == "" }.join(" ") + end end end end diff --git a/lib/chef/provider/package/aix.rb b/lib/chef/provider/package/aix.rb index 107f914c05..29e2d1eb94 100644 --- a/lib/chef/provider/package/aix.rb +++ b/lib/chef/provider/package/aix.rb @@ -16,16 +16,17 @@ # limitations under the License. # # -require 'chef/provider/package' -require 'chef/mixin/command' -require 'chef/resource/package' -require 'chef/mixin/get_source_from_package' +require "chef/provider/package" +require "chef/mixin/command" +require "chef/resource/package" +require "chef/mixin/get_source_from_package" class Chef class Provider class Package class Aix < Chef::Provider::Package + provides :package, os: "aix" provides :bff_package, os: "aix" include Chef::Mixin::GetSourceFromPackage @@ -46,13 +47,12 @@ class Chef def load_current_resource @current_resource = Chef::Resource::Package.new(@new_resource.name) @current_resource.package_name(@new_resource.package_name) - @new_resource.version(nil) if @new_resource.source @package_source_found = ::File.exists?(@new_resource.source) if @package_source_found Chef::Log.debug("#{@new_resource} checking pkg status") - ret = shell_out("installp -L -d #{@new_resource.source}") + ret = shell_out_with_timeout("installp -L -d #{@new_resource.source}") ret.stdout.each_line do | line | case line when /#{@new_resource.package_name}:/ @@ -60,11 +60,12 @@ class Chef @new_resource.version(fields[2]) end end + raise Chef::Exceptions::Package, "package source #{@new_resource.source} does not provide package #{@new_resource.package_name}" unless @new_resource.version end end Chef::Log.debug("#{@new_resource} checking install state") - ret = shell_out("lslpp -lcq #{@current_resource.package_name}") + ret = shell_out_with_timeout("lslpp -lcq #{@current_resource.package_name}") ret.stdout.each_line do | line | case line when /#{@current_resource.package_name}/ @@ -83,7 +84,7 @@ class Chef def candidate_version return @candidate_version if @candidate_version - ret = shell_out("installp -L -d #{@new_resource.source}") + ret = shell_out_with_timeout("installp -L -d #{@new_resource.source}") ret.stdout.each_line do | line | case line when /\w:#{Regexp.escape(@new_resource.package_name)}:(.*)/ @@ -109,10 +110,10 @@ class Chef def install_package(name, version) Chef::Log.debug("#{@new_resource} package install options: #{@new_resource.options}") if @new_resource.options.nil? - shell_out!( "installp -aYF -d #{@new_resource.source} #{@new_resource.package_name}" ) + shell_out_with_timeout!( "installp -aYF -d #{@new_resource.source} #{@new_resource.package_name}" ) Chef::Log.debug("#{@new_resource} installed version #{@new_resource.version} from: #{@new_resource.source}") else - shell_out!( "installp -aYF #{expand_options(@new_resource.options)} -d #{@new_resource.source} #{@new_resource.package_name}" ) + shell_out_with_timeout!( "installp -aYF #{expand_options(@new_resource.options)} -d #{@new_resource.source} #{@new_resource.package_name}" ) Chef::Log.debug("#{@new_resource} installed version #{@new_resource.version} from: #{@new_resource.source}") end end @@ -121,10 +122,10 @@ class Chef def remove_package(name, version) if @new_resource.options.nil? - shell_out!( "installp -u #{name}" ) + shell_out_with_timeout!( "installp -u #{name}" ) Chef::Log.debug("#{@new_resource} removed version #{@new_resource.version}") else - shell_out!( "installp -u #{expand_options(@new_resource.options)} #{name}" ) + shell_out_with_timeout!( "installp -u #{expand_options(@new_resource.options)} #{name}" ) Chef::Log.debug("#{@new_resource} removed version #{@new_resource.version}") end end diff --git a/lib/chef/provider/package/apt.rb b/lib/chef/provider/package/apt.rb index e426b51992..03cbff4d1c 100644 --- a/lib/chef/provider/package/apt.rb +++ b/lib/chef/provider/package/apt.rb @@ -16,15 +16,16 @@ # limitations under the License. # -require 'chef/provider/package' -require 'chef/mixin/command' -require 'chef/resource/package' +require "chef/provider/package" +require "chef/mixin/command" +require "chef/resource/package" class Chef class Provider class Package class Apt < Chef::Provider::Package + provides :package, platform_family: "debian" provides :apt_package, os: "linux" # return [Hash] mapping of package name to Boolean value @@ -47,7 +48,7 @@ class Chef requirements.assert(:all_actions) do |a| a.assertion { !@new_resource.source } - a.failure_message(Chef::Exceptions::Package, 'apt package provider cannot handle source attribute. Use dpkg provider instead') + a.failure_message(Chef::Exceptions::Package, "apt package provider cannot handle source attribute. Use dpkg provider instead") end end @@ -62,11 +63,11 @@ class Chef installed_version = nil candidate_version = nil - shell_out!("apt-cache#{expand_options(default_release_options)} policy #{pkg}", {:timeout=>900}).stdout.each_line do |line| + shell_out_with_timeout!("apt-cache#{expand_options(default_release_options)} policy #{pkg}").stdout.each_line do |line| case line when /^\s{2}Installed: (.+)$/ installed_version = $1 - if installed_version == '(none)' + if installed_version == "(none)" Chef::Log.debug("#{@new_resource} current version is nil") installed_version = nil else @@ -75,10 +76,10 @@ class Chef end when /^\s{2}Candidate: (.+)$/ candidate_version = $1 - if candidate_version == '(none)' + if candidate_version == "(none)" # This may not be an appropriate assumption, but it shouldn't break anything that already worked -- btm is_virtual_package = true - showpkg = shell_out!("apt-cache showpkg #{pkg}", {:timeout => 900}).stdout + showpkg = shell_out_with_timeout!("apt-cache showpkg #{pkg}").stdout providers = Hash.new showpkg.rpartition(/Reverse Provides: ?#{$/}/)[2].each_line do |line| provider, version = line.split @@ -140,7 +141,7 @@ class Chef version_array = [ version ].flatten package_name = name_array.zip(version_array).map do |n, v| is_virtual_package[n] ? n : "#{n}=#{v}" - end.join(' ') + end.join(" ") run_noninteractive("apt-get -q -y#{expand_options(default_release_options)}#{expand_options(@new_resource.options)} install #{package_name}") end @@ -149,12 +150,12 @@ class Chef end def remove_package(name, version) - package_name = [ name ].flatten.join(' ') + package_name = [ name ].flatten.join(" ") run_noninteractive("apt-get -q -y#{expand_options(@new_resource.options)} remove #{package_name}") end def purge_package(name, version) - package_name = [ name ].flatten.join(' ') + package_name = [ name ].flatten.join(" ") run_noninteractive("apt-get -q -y#{expand_options(@new_resource.options)} purge #{package_name}") end @@ -164,7 +165,7 @@ class Chef end def reconfig_package(name, version) - package_name = [ name ].flatten.join(' ') + package_name = [ name ].flatten.join(" ") Chef::Log.info("#{@new_resource} reconfiguring") run_noninteractive("dpkg-reconfigure #{package_name}") end @@ -175,7 +176,7 @@ class Chef # interactive prompts. Command is run with default localization rather # than forcing locale to "C", so command output may not be stable. def run_noninteractive(command) - shell_out!(command, :env => { "DEBIAN_FRONTEND" => "noninteractive", "LC_ALL" => nil }, :timeout => @new_resource.timeout) + shell_out_with_timeout!(command, :env => { "DEBIAN_FRONTEND" => "noninteractive", "LC_ALL" => nil }) end end diff --git a/lib/chef/provider/package/chocolatey.rb b/lib/chef/provider/package/chocolatey.rb new file mode 100644 index 0000000000..87abe299b6 --- /dev/null +++ b/lib/chef/provider/package/chocolatey.rb @@ -0,0 +1,257 @@ +# +# Copyright:: Copyright (c) 2015 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/provider/package" +require "chef/resource/chocolatey_package" +require "chef/mixin/powershell_out" + +class Chef + class Provider + class Package + class Chocolatey < Chef::Provider::Package + include Chef::Mixin::PowershellOut + + provides :chocolatey_package, os: "windows" + + # Declare that our arguments should be arrays + use_multipackage_api + + # Responsible for building the current_resource. + # + # @return [Chef::Resource::ChocolateyPackage] the current_resource + def load_current_resource + @current_resource = Chef::Resource::ChocolateyPackage.new(new_resource.name) + current_resource.package_name(new_resource.package_name) + current_resource.version(build_current_versions) + current_resource + end + + def define_resource_requirements + super + + # Chocolatey source attribute points to an alternate feed + # and not a package specific alternate source like other providers + # so we want to assert candidates exist for the alternate source + requirements.assert(:upgrade, :install) do |a| + a.assertion { candidates_exist_for_all_uninstalled? } + a.failure_message(Chef::Exceptions::Package, "No candidate version available for #{packages_missing_candidates.join(", ")}") + a.whyrun("Assuming a repository that offers #{packages_missing_candidates.join(", ")} would have been configured") + end + end + + # Lazy initializer for candidate_version. A nil value means that there is no candidate + # version and the package is not installable (generally an error). + # + # @return [Array] list of candidate_versions indexed same as new_resource.package_name/version + def candidate_version + @candidate_version ||= build_candidate_versions + end + + # Install multiple packages via choco.exe + # + # @param names [Array<String>] array of package names to install + # @param versions [Array<String>] array of versions to install + def install_package(names, versions) + name_versions_to_install = desired_name_versions.select { |n, v| lowercase_names(names).include?(n) } + + name_nil_versions = name_versions_to_install.select { |n,v| v.nil? } + name_has_versions = name_versions_to_install.reject { |n,v| v.nil? } + + # choco does not support installing multiple packages with version pins + name_has_versions.each do |name, version| + choco_command("install -y -version", version, cmd_args, name) + end + + # but we can do all the ones without version pins at once + unless name_nil_versions.empty? + cmd_names = name_nil_versions.keys + choco_command("install -y", cmd_args, *cmd_names) + end + end + + # Upgrade multiple packages via choco.exe + # + # @param names [Array<String>] array of package names to install + # @param versions [Array<String>] array of versions to install + def upgrade_package(names, versions) + name_versions_to_install = desired_name_versions.select { |n, v| lowercase_names(names).include?(n) } + + name_nil_versions = name_versions_to_install.select { |n,v| v.nil? } + name_has_versions = name_versions_to_install.reject { |n,v| v.nil? } + + # choco does not support installing multiple packages with version pins + name_has_versions.each do |name, version| + choco_command("upgrade -y -version", version, cmd_args, name) + end + + # but we can do all the ones without version pins at once + unless name_nil_versions.empty? + cmd_names = name_nil_versions.keys + choco_command("upgrade -y", cmd_args, *cmd_names) + end + end + + # Remove multiple packages via choco.exe + # + # @param names [Array<String>] array of package names to install + # @param versions [Array<String>] array of versions to install + def remove_package(names, versions) + choco_command("uninstall -y", cmd_args, *names) + end + + # Support :uninstall as an action in order for users to easily convert + # from the `chocolatey` provider in the cookbook. It is, however, + # already deprecated. + def action_uninstall + Chef::Log.deprecation "The use of action :uninstall on the chocolatey_package provider is deprecated, please use :remove" + action_remove + end + + # Choco does not have dpkg's distinction between purge and remove + alias_method :purge_package, :remove_package + + # Override the superclass check. The semantics for our new_resource.source is not files to + # install from, but like the rubygem provider's sources which are more like repos. + def check_resource_semantics! + end + + private + + # Magic to find where chocolatey is installed in the system, and to + # return the full path of choco.exe + # + # @return [String] full path of choco.exe + def choco_exe + @choco_exe ||= + ::File.join( + powershell_out!( + "[System.Environment]::GetEnvironmentVariable('ChocolateyInstall', 'MACHINE')" + ).stdout.chomp, + "bin", + "choco.exe", + ) + end + + # Helper to dispatch a choco command through shell_out using the timeout + # set on the new resource, with nice command formatting. + # + # @param args [String] variable number of string arguments + # @return [Mixlib::ShellOut] object returned from shell_out! + def choco_command(*args) + shell_out_with_timeout!(args_to_string(choco_exe, *args)) + end + + # Use the available_packages Hash helper to create an array suitable for + # using in candidate_version + # + # @return [Array] list of candidate_version, same index as new_resource.package_name/version + def build_candidate_versions + new_resource.package_name.map do |package_name| + available_packages[package_name.downcase] + end + end + + # Use the installed_packages Hash helper to create an array suitable for + # using in current_resource.version + # + # @return [Array] list of candidate_version, same index as new_resource.package_name/version + def build_current_versions + new_resource.package_name.map do |package_name| + installed_packages[package_name.downcase] + end + end + + # Helper to construct Hash of names-to-versions, requested on the new_resource. + # If new_resource.version is nil, then all values will be nil. + # + # @return [Hash] Mapping of requested names to versions + def desired_name_versions + desired_versions = new_resource.version || new_resource.package_name.map { nil } + Hash[*lowercase_names(new_resource.package_name).zip(desired_versions).flatten] + end + + # Helper to construct optional args out of new_resource + # + # @return [String] options from new_resource or empty string + def cmd_args + cmd_args = [ new_resource.options ] + cmd_args.push( "-source #{new_resource.source}" ) if new_resource.source + args_to_string(*cmd_args) + end + + # Helper to nicely convert variable string args into a single command line. It + # will compact nulls or empty strings and join arguments with single spaces, without + # introducing any double-spaces for missing args. + # + # @param args [String] variable number of string arguments + # @return [String] nicely concatenated string or empty string + def args_to_string(*args) + args.reject {|i| i.nil? || i == "" }.join(" ") + end + + # Available packages in chocolatey as a Hash of names mapped to versions + # If pinning a package to a specific version, filter out all non matching versions + # (names are downcased for case-insensitive matching) + # + # @return [Hash] name-to-version mapping of available packages + def available_packages + @available_packages ||= + begin + cmd = [ "list -ar #{package_name_array.join ' '}" ] + cmd.push( "-source #{new_resource.source}" ) if new_resource.source + parse_list_output(*cmd).each_with_object({}) do |name_version, available| + name, version = name_version + if desired_name_versions[name].nil? || desired_name_versions[name] == version + available[name] = version + end + end + end + end + + # Installed packages in chocolatey as a Hash of names mapped to versions + # (names are downcased for case-insensitive matching) + # + # @return [Hash] name-to-version mapping of installed packages + def installed_packages + @installed_packages ||= Hash[*parse_list_output("list -l -r").flatten] + end + + # Helper to convert choco.exe list output to a Hash + # (names are downcased for case-insenstive matching) + # + # @param cmd [String] command to run + # @return [Array] list output converted to ruby Hash + def parse_list_output(*args) + list = [] + choco_command(*args).stdout.each_line do |line| + name, version = line.split("|") + list << [ name.downcase, version.chomp ] + end + list + end + + # Helper to downcase all names in an array + # + # @param names [Array] original mixed case names + # @return [Array] same names in lower case + def lowercase_names(names) + names.map { |name| name.downcase } + end + end + end + end +end diff --git a/lib/chef/provider/package/dpkg.rb b/lib/chef/provider/package/dpkg.rb index 11691a2479..bcf7cff4e4 100644 --- a/lib/chef/provider/package/dpkg.rb +++ b/lib/chef/provider/package/dpkg.rb @@ -16,131 +16,208 @@ # limitations under the License. # -require 'chef/provider/package' -require 'chef/mixin/command' -require 'chef/resource/package' -require 'chef/mixin/get_source_from_package' +require "chef/provider/package" +require "chef/resource/package" class Chef class Provider class Package class Dpkg < Chef::Provider::Package - # http://www.debian.org/doc/debian-policy/ch-controlfields.html#s-f-Version - DPKG_INFO = /([a-z\d\-\+\.]+)\t([\w\d.~:-]+)/ + DPKG_REMOVED = /^Status: deinstall ok config-files/ DPKG_INSTALLED = /^Status: install ok installed/ DPKG_VERSION = /^Version: (.+)$/ provides :dpkg_package, os: "linux" - include Chef::Mixin::GetSourceFromPackage + use_multipackage_api + use_package_name_for_source def define_resource_requirements super - requirements.assert(:install) do |a| - a.assertion{ not @new_resource.source.nil? } - a.failure_message Chef::Exceptions::Package, "Source for package #{@new_resource.name} required for action install" + + requirements.assert(:install, :upgrade) do |a| + a.assertion { !resolved_source_array.compact.empty? } + a.failure_message Chef::Exceptions::Package, "#{new_resource} the source property is required for action :install or :upgrade" end - # TODO this was originally written for any action in which .source is provided - # but would it make more sense to only look at source if the action is :install? - requirements.assert(:all_actions) do |a| - a.assertion { @source_exists } - a.failure_message Chef::Exceptions::Package, "Package #{@new_resource.name} not found: #{@new_resource.source}" - a.whyrun "Assuming it would have been previously downloaded." + requirements.assert(:install, :upgrade) do |a| + a.assertion { source_files_exist? } + a.failure_message Chef::Exceptions::Package, "#{new_resource} source file(s) do not exist: #{missing_sources}" + a.whyrun "Assuming they would have been previously created." end end def load_current_resource - @source_exists = true - @current_resource = Chef::Resource::Package.new(@new_resource.name) - @current_resource.package_name(@new_resource.package_name) - @new_resource.version(nil) - - if @new_resource.source - @source_exists = ::File.exists?(@new_resource.source) - if @source_exists - # Get information from the package if supplied - Chef::Log.debug("#{@new_resource} checking dpkg status") - - shell_out("dpkg-deb -W #{@new_resource.source}").stdout.each_line do |line| - if pkginfo = DPKG_INFO.match(line) - @current_resource.package_name(pkginfo[1]) - @new_resource.version(pkginfo[2]) - @candidate_version = pkginfo[2] - end - end - else - # Source provided but not valid means we can't safely do further processing - return - end - + @current_resource = Chef::Resource::Package.new(new_resource.name) + current_resource.package_name(new_resource.package_name) + + if source_files_exist? + @candidate_version = get_candidate_version + current_resource.package_name(get_package_name) + # if the source file exists then our package_name is right + current_resource.version(get_current_version_from(current_package_name_array)) + elsif !installing? + # we can't do this if we're installing with no source, because our package_name + # is probably not right. + # + # if we're removing or purging we don't use source, and our package_name must + # be right so we can do this. + # + # we don't error here on the dpkg command since we'll handle the exception or + # the why-run message in define_resource_requirements. + current_resource.version(get_current_version_from(current_package_name_array)) end - # Check to see if it is installed - package_installed = nil - Chef::Log.debug("#{@new_resource} checking install state") - status = shell_out("dpkg -s #{@current_resource.package_name}") + current_resource + end + + def install_package(name, version) + sources = name.map { |n| name_sources[n] } + Chef::Log.info("#{new_resource} installing package(s): #{name.join(' ')}") + run_noninteractive("dpkg -i", new_resource.options, *sources) + end + + def remove_package(name, version) + Chef::Log.info("#{new_resource} removing package(s): #{name.join(' ')}") + run_noninteractive("dpkg -r", new_resource.options, *name) + end + + def purge_package(name, version) + Chef::Log.info("#{new_resource} purging packages(s): #{name.join(' ')}") + run_noninteractive("dpkg -P", new_resource.options, *name) + end + + def upgrade_package(name, version) + install_package(name, version) + end + + def preseed_package(preseed_file) + Chef::Log.info("#{new_resource} pre-seeding package installation instructions") + run_noninteractive("debconf-set-selections", *preseed_file) + end + + def reconfig_package(name, version) + Chef::Log.info("#{new_resource} reconfiguring") + run_noninteractive("dpkg-reconfigure", *name) + end + + # Override the superclass check. Multiple sources are required here. + def check_resource_semantics! + end + + private + + def read_current_version_of_package(package_name) + Chef::Log.debug("#{new_resource} checking install state of #{package_name}") + status = shell_out_with_timeout!("dpkg -s #{package_name}", returns: [0, 1]) + package_installed = false status.stdout.each_line do |line| case line + when DPKG_REMOVED + # if we are 'purging' then we consider 'removed' to be 'installed' + package_installed = true if action == :purge when DPKG_INSTALLED package_installed = true when DPKG_VERSION if package_installed - Chef::Log.debug("#{@new_resource} current version is #{$1}") - @current_resource.version($1) + Chef::Log.debug("#{new_resource} current version is #{$1}") + return $1 end end end + return nil + end - unless status.exitstatus == 0 || status.exitstatus == 1 - raise Chef::Exceptions::Package, "dpkg failed - #{status.inspect}!" + def get_current_version_from(array) + array.map do |name| + read_current_version_of_package(name) end + end - @current_resource + # Runs command via shell_out_with_timeout with magic environment to disable + # interactive prompts. + def run_noninteractive(*command) + shell_out_with_timeout!(a_to_s(*command), :env => { "DEBIAN_FRONTEND" => "noninteractive" }) end - def install_package(name, version) - Chef::Log.info("#{@new_resource} installing #{@new_resource.source}") - run_noninteractive( - "dpkg -i#{expand_options(@new_resource.options)} #{@new_resource.source}" - ) + # Returns true if all sources exist. Returns false if any do not, or if no + # sources were specified. + # + # @return [Boolean] True if all sources exist + def source_files_exist? + resolved_source_array.all? {|s| s && ::File.exist?(s) } end - def remove_package(name, version) - Chef::Log.info("#{@new_resource} removing #{@new_resource.package_name}") - run_noninteractive( - "dpkg -r#{expand_options(@new_resource.options)} #{@new_resource.package_name}" - ) + # Helper to return all the nanes of the missing sources for error messages. + # + # @return [Array<String>] Array of missing sources + def missing_sources + resolved_source_array.select {|s| s.nil? || !::File.exist?(s) } end - def purge_package(name, version) - Chef::Log.info("#{@new_resource} purging #{@new_resource.package_name}") - run_noninteractive( - "dpkg -P#{expand_options(@new_resource.options)} #{@new_resource.package_name}" - ) + def current_package_name_array + [ current_resource.package_name ].flatten end - def upgrade_package(name, version) - install_package(name, version) + # Helper to construct Hash of names-to-sources. + # + # @return [Hash] Mapping of package names to sources + def name_sources + @name_sources = + begin + Hash[*package_name_array.zip(resolved_source_array).flatten] + end end - def preseed_package(preseed_file) - Chef::Log.info("#{@new_resource} pre-seeding package installation instructions") - run_noninteractive("debconf-set-selections #{preseed_file}") + # Helper to construct Hash of names-to-package-information. + # + # @return [Hash] Mapping of package names to package information + def name_pkginfo + @name_pkginfo ||= + begin + pkginfos = resolved_source_array.map do |src| + Chef::Log.debug("#{new_resource} checking #{src} dpkg status") + status = shell_out_with_timeout!("dpkg-deb -W #{src}") + status.stdout + end + Hash[*package_name_array.zip(pkginfos).flatten] + end end - def reconfig_package(name, version) - Chef::Log.info("#{@new_resource} reconfiguring") - run_noninteractive("dpkg-reconfigure #{name}") + def name_candidate_version + @name_candidate_version ||= + begin + Hash[name_pkginfo.map {|k, v| [k, v ? v.split("\t")[1].strip : nil] }] + end + end + + def name_package_name + @name_package_name ||= + begin + Hash[name_pkginfo.map {|k, v| [k, v ? v.split("\t")[0] : nil] }] + end + end + + # Return candidate version array from pkg-deb -W against the source file(s). + # + # @return [Array] Array of candidate versions read from the source files + def get_candidate_version + package_name_array.map { |name| name_candidate_version[name] } + end + + # Return package names from the candidate source file(s). + # + # @return [Array] Array of actual package names read from the source files + def get_package_name + package_name_array.map { |name| name_package_name[name] } end - # Runs command via shell_out with magic environment to disable - # interactive prompts. Command is run with default localization rather - # than forcing locale to "C", so command output may not be stable. + # Since upgrade just calls install, this is a helper to determine + # if our action means that we'll be calling install_package. # - # FIXME: This should be "LC_ALL" => "en_US.UTF-8" in order to stabilize the output and get UTF-8 - def run_noninteractive(command) - shell_out!(command, :env => { "DEBIAN_FRONTEND" => "noninteractive", "LC_ALL" => nil }) + # @return [Boolean] true if we're doing :install or :upgrade + def installing? + [:install, :upgrade].include?(action) end end diff --git a/lib/chef/provider/package/easy_install.rb b/lib/chef/provider/package/easy_install.rb index 90727b738d..91bb54999b 100644 --- a/lib/chef/provider/package/easy_install.rb +++ b/lib/chef/provider/package/easy_install.rb @@ -16,9 +16,9 @@ # limitations under the License. # -require 'chef/provider/package' -require 'chef/mixin/command' -require 'chef/resource/package' +require "chef/provider/package" +require "chef/mixin/command" +require "chef/resource/package" class Chef class Provider @@ -32,10 +32,10 @@ class Chef begin # first check to see if we can import it - output = shell_out!("#{python_binary_path} -c \"import #{name}\"", :returns=>[0,1]).stderr + output = shell_out_with_timeout!("#{python_binary_path} -c \"import #{name}\"", :returns=>[0,1]).stderr if output.include? "ImportError" # then check to see if its on the path - output = shell_out!("#{python_binary_path} -c \"import sys; print sys.path\"", :returns=>[0,1]).stdout + output = shell_out_with_timeout!("#{python_binary_path} -c \"import sys; print sys.path\"", :returns=>[0,1]).stdout if output.downcase.include? "#{name.downcase}" check = true end @@ -51,12 +51,12 @@ class Chef def easy_install_binary_path path = @new_resource.easy_install_binary - path ? path : 'easy_install' + path ? path : "easy_install" end def python_binary_path path = @new_resource.python_binary - path ? path : 'python' + path ? path : "python" end def module_name @@ -67,18 +67,17 @@ class Chef def load_current_resource @current_resource = Chef::Resource::Package.new(@new_resource.name) @current_resource.package_name(@new_resource.package_name) - @current_resource.version(nil) # get the currently installed version if installed package_version = nil if install_check(module_name) begin - output = shell_out!("#{python_binary_path} -c \"import #{module_name}; print #{module_name}.__version__\"").stdout + output = shell_out_with_timeout!("#{python_binary_path} -c \"import #{module_name}; print #{module_name}.__version__\"").stdout package_version = output.strip rescue - output = shell_out!("#{python_binary_path} -c \"import sys; print sys.path\"", :returns=>[0,1]).stdout + output = shell_out_with_timeout!("#{python_binary_path} -c \"import sys; print sys.path\"", :returns=>[0,1]).stdout - output_array = output.gsub(/[\[\]]/,'').split(/\s*,\s*/) + output_array = output.gsub(/[\[\]]/,"").split(/\s*,\s*/) package_path = "" output_array.each do |entry| @@ -107,7 +106,7 @@ class Chef return @candidate_version if @candidate_version # do a dry run to get the latest version - result = shell_out!("#{easy_install_binary_path} -n #{@new_resource.package_name}", :returns=>[0,1]) + result = shell_out_with_timeout!("#{easy_install_binary_path} -n #{@new_resource.package_name}", :returns=>[0,1]) @candidate_version = result.stdout[/(.*)Best match: (.*) (.*)$/, 3] @candidate_version end diff --git a/lib/chef/provider/package/freebsd/base.rb b/lib/chef/provider/package/freebsd/base.rb index 6a3b97a4fd..4957812e99 100644 --- a/lib/chef/provider/package/freebsd/base.rb +++ b/lib/chef/provider/package/freebsd/base.rb @@ -19,9 +19,9 @@ # limitations under the License. # -require 'chef/resource/package' -require 'chef/provider/package' -require 'chef/mixin/get_source_from_package' +require "chef/resource/package" +require "chef/provider/package" +require "chef/mixin/get_source_from_package" class Chef class Provider @@ -47,7 +47,7 @@ class Chef # Otherwise look up the path to the ports directory using 'whereis' else - whereis = shell_out!("whereis -s #{port}", :env => nil) + whereis = shell_out_with_timeout!("whereis -s #{port}", :env => nil) unless path = whereis.stdout[/^#{Regexp.escape(port)}:\s+(.+)$/, 1] raise Chef::Exceptions::Package, "Could not find port with the name #{port}" end @@ -57,7 +57,7 @@ class Chef def makefile_variable_value(variable, dir = nil) options = dir ? { :cwd => dir } : {} - make_v = shell_out!("make -V #{variable}", options.merge!(:env => nil, :returns => [0,1])) + make_v = shell_out_with_timeout!("make -V #{variable}", options.merge!(:env => nil, :returns => [0,1])) make_v.exitstatus.zero? ? make_v.stdout.strip.split($\).first : nil # $\ is the line separator, i.e. newline. end end diff --git a/lib/chef/provider/package/freebsd/pkg.rb b/lib/chef/provider/package/freebsd/pkg.rb index ebbfbb19b4..2d13a0d2c1 100644 --- a/lib/chef/provider/package/freebsd/pkg.rb +++ b/lib/chef/provider/package/freebsd/pkg.rb @@ -19,8 +19,8 @@ # limitations under the License. # -require 'chef/provider/package/freebsd/base' -require 'chef/util/path_helper' +require "chef/provider/package/freebsd/base" +require "chef/util/path_helper" class Chef class Provider @@ -34,24 +34,24 @@ class Chef case @new_resource.source when /^http/, /^ftp/ if @new_resource.source =~ /\/$/ - shell_out!("pkg_add -r #{package_name}", :env => { "PACKAGESITE" => @new_resource.source, 'LC_ALL' => nil }).status + shell_out_with_timeout!("pkg_add -r #{package_name}", :env => { "PACKAGESITE" => @new_resource.source, "LC_ALL" => nil }).status else - shell_out!("pkg_add -r #{package_name}", :env => { "PACKAGEROOT" => @new_resource.source, 'LC_ALL' => nil }).status + shell_out_with_timeout!("pkg_add -r #{package_name}", :env => { "PACKAGEROOT" => @new_resource.source, "LC_ALL" => nil }).status end Chef::Log.debug("#{@new_resource} installed from: #{@new_resource.source}") when /^\// - shell_out!("pkg_add #{file_candidate_version_path}", :env => { "PKG_PATH" => @new_resource.source , 'LC_ALL'=>nil}).status + shell_out_with_timeout!("pkg_add #{file_candidate_version_path}", :env => { "PKG_PATH" => @new_resource.source , "LC_ALL"=>nil}).status Chef::Log.debug("#{@new_resource} installed from: #{@new_resource.source}") else - shell_out!("pkg_add -r #{latest_link_name}", :env => nil).status + shell_out_with_timeout!("pkg_add -r #{latest_link_name}", :env => nil).status end end end def remove_package(name, version) - shell_out!("pkg_delete #{package_name}-#{version || @current_resource.version}", :env => nil).status + shell_out_with_timeout!("pkg_delete #{package_name}-#{version || @current_resource.version}", :env => nil).status end # The name of the package (without the version number) as understood by pkg_add and pkg_info. @@ -72,7 +72,7 @@ class Chef end def current_installed_version - pkg_info = shell_out!("pkg_info -E \"#{package_name}*\"", :env => nil, :returns => [0,1]) + pkg_info = shell_out_with_timeout!("pkg_info -E \"#{package_name}*\"", :env => nil, :returns => [0,1]) pkg_info.stdout[/^#{Regexp.escape(package_name)}-(.+)/, 1] end diff --git a/lib/chef/provider/package/freebsd/pkgng.rb b/lib/chef/provider/package/freebsd/pkgng.rb index bfe6dca617..2d23620a66 100644 --- a/lib/chef/provider/package/freebsd/pkgng.rb +++ b/lib/chef/provider/package/freebsd/pkgng.rb @@ -16,7 +16,7 @@ # limitations under the License. # -require 'chef/provider/package/freebsd/base' +require "chef/provider/package/freebsd/base" class Chef class Provider @@ -28,23 +28,23 @@ class Chef unless @current_resource.version case @new_resource.source when /^(http|ftp|\/)/ - shell_out!("pkg add#{expand_options(@new_resource.options)} #{@new_resource.source}", :env => { 'LC_ALL' => nil }).status + shell_out_with_timeout!("pkg add#{expand_options(@new_resource.options)} #{@new_resource.source}", :env => { "LC_ALL" => nil }).status Chef::Log.debug("#{@new_resource} installed from: #{@new_resource.source}") else - shell_out!("pkg install -y#{expand_options(@new_resource.options)} #{name}", :env => { 'LC_ALL' => nil }).status + shell_out_with_timeout!("pkg install -y#{expand_options(@new_resource.options)} #{name}", :env => { "LC_ALL" => nil }).status end end end def remove_package(name, version) - options = @new_resource.options && @new_resource.options.sub(repo_regex, '') + options = @new_resource.options && @new_resource.options.sub(repo_regex, "") options && !options.empty? || options = nil - shell_out!("pkg delete -y#{expand_options(options)} #{name}#{version ? '-' + version : ''}", :env => nil).status + shell_out_with_timeout!("pkg delete -y#{expand_options(options)} #{name}#{version ? '-' + version : ''}", :env => nil).status end def current_installed_version - pkg_info = shell_out!("pkg info \"#{@new_resource.package_name}\"", :env => nil, :returns => [0,70]) + pkg_info = shell_out_with_timeout!("pkg info \"#{@new_resource.package_name}\"", :env => nil, :returns => [0,70]) pkg_info.stdout[/^Version +: (.+)$/, 1] end @@ -63,7 +63,7 @@ class Chef options = $1 end - pkg_query = shell_out!("pkg rquery#{expand_options(options)} '%v' #{@new_resource.package_name}", :env => nil) + pkg_query = shell_out_with_timeout!("pkg rquery#{expand_options(options)} '%v' #{@new_resource.package_name}", :env => nil) pkg_query.exitstatus.zero? ? pkg_query.stdout.strip.split(/\n/).last : nil end diff --git a/lib/chef/provider/package/freebsd/port.rb b/lib/chef/provider/package/freebsd/port.rb index 8b191179f0..89220e38b4 100644 --- a/lib/chef/provider/package/freebsd/port.rb +++ b/lib/chef/provider/package/freebsd/port.rb @@ -16,7 +16,7 @@ # limitations under the License. # -require 'chef/provider/package/freebsd/base' +require "chef/provider/package/freebsd/base" class Chef class Provider @@ -26,18 +26,18 @@ class Chef include PortsHelper def install_package(name, version) - shell_out!("make -DBATCH install clean", :timeout => 1800, :env => nil, :cwd => port_dir).status + shell_out_with_timeout!("make -DBATCH install clean", :timeout => 1800, :env => nil, :cwd => port_dir).status end def remove_package(name, version) - shell_out!("make deinstall", :timeout => 300, :env => nil, :cwd => port_dir).status + shell_out_with_timeout!("make deinstall", :timeout => 300, :env => nil, :cwd => port_dir).status end def current_installed_version pkg_info = if @new_resource.supports_pkgng? - shell_out!("pkg info \"#{@new_resource.package_name}\"", :env => nil, :returns => [0,70]) + shell_out_with_timeout!("pkg info \"#{@new_resource.package_name}\"", :env => nil, :returns => [0,70]) else - shell_out!("pkg_info -E \"#{@new_resource.package_name}*\"", :env => nil, :returns => [0,1]) + shell_out_with_timeout!("pkg_info -E \"#{@new_resource.package_name}*\"", :env => nil, :returns => [0,1]) end pkg_info.stdout[/^#{Regexp.escape(@new_resource.package_name)}-(.+)/, 1] end diff --git a/lib/chef/provider/package/homebrew.rb b/lib/chef/provider/package/homebrew.rb index 603899646f..661236a56c 100644 --- a/lib/chef/provider/package/homebrew.rb +++ b/lib/chef/provider/package/homebrew.rb @@ -18,16 +18,16 @@ # limitations under the License. # -require 'etc' -require 'chef/mixin/homebrew_user' +require "etc" +require "chef/mixin/homebrew_user" class Chef class Provider class Package class Homebrew < Chef::Provider::Package + provides :package, os: "darwin", override: true provides :homebrew_package - provides :package, os: "darwin" include Chef::Mixin::HomebrewUser @@ -46,7 +46,7 @@ class Chef def install_package(name, version) unless current_resource.version == version - brew('install', new_resource.options, name) + brew("install", new_resource.options, name) end end @@ -56,19 +56,19 @@ class Chef if current_version.nil? or current_version.empty? install_package(name, version) elsif current_version != version - brew('upgrade', new_resource.options, name) + brew("upgrade", new_resource.options, name) end end def remove_package(name, version) if current_resource.version - brew('uninstall', new_resource.options, name) + brew("uninstall", new_resource.options, name) end end # Homebrew doesn't really have a notion of purging, do a "force remove" def purge_package(name, version) - new_resource.options((new_resource.options || '') << ' --force').strip + new_resource.options((new_resource.options || "") << " --force").strip remove_package(name, version) end @@ -85,7 +85,7 @@ class Chef # # https://github.com/Homebrew/homebrew/wiki/Querying-Brew def brew_info - @brew_info ||= Chef::JSONCompat.from_json(brew('info', '--json=v1', new_resource.package_name)).first + @brew_info ||= Chef::JSONCompat.from_json(brew("info", "--json=v1", new_resource.package_name)).first end # Some packages (formula) are "keg only" and aren't linked, @@ -95,14 +95,14 @@ class Chef # that brew thinks is linked as the current version. # def current_installed_version - if brew_info['keg_only'] - if brew_info['installed'].empty? + if brew_info["keg_only"] + if brew_info["installed"].empty? nil else - brew_info['installed'].last['version'] + brew_info["installed"].last["version"] end else - brew_info['linked_keg'] + brew_info["linked_keg"] end end @@ -116,7 +116,7 @@ class Chef # # https://github.com/Homebrew/homebrew/wiki/Acceptable-Formulae#stable-versions def candidate_version - brew_info['versions']['stable'] + brew_info["versions"]["stable"] end private @@ -126,7 +126,8 @@ class Chef homebrew_user = Etc.getpwuid(homebrew_uid) Chef::Log.debug "Executing '#{command}' as user '#{homebrew_user.name}'" - output = shell_out!(command, :timeout => 1800, :user => homebrew_uid, :environment => { 'HOME' => homebrew_user.dir, 'RUBYOPT' => nil }) + # FIXME: this 1800 second default timeout should be deprecated + output = shell_out_with_timeout!(command, :timeout => 1800, :user => homebrew_uid, :environment => { "HOME" => homebrew_user.dir, "RUBYOPT" => nil }) output.stdout.chomp end diff --git a/lib/chef/provider/package/ips.rb b/lib/chef/provider/package/ips.rb index 87022d770a..a69a2e24e0 100644 --- a/lib/chef/provider/package/ips.rb +++ b/lib/chef/provider/package/ips.rb @@ -17,16 +17,17 @@ # limitations under the License. # -require 'open3' -require 'chef/provider/package' -require 'chef/mixin/command' -require 'chef/resource/package' +require "open3" +require "chef/provider/package" +require "chef/mixin/command" +require "chef/resource/package" class Chef class Provider class Package class Ips < Chef::Provider::Package + provides :package, platform: %w{openindiana opensolaris omnios solaris2} provides :ips_package, os: "solaris2" attr_accessor :virtual @@ -42,14 +43,14 @@ class Chef end def get_current_version - shell_out("pkg info #{@new_resource.package_name}").stdout.each_line do |line| + shell_out_with_timeout("pkg info #{@new_resource.package_name}").stdout.each_line do |line| return $1.split[0] if line =~ /^\s+Version: (.*)/ end return nil end def get_candidate_version - shell_out!("pkg info -r #{new_resource.package_name}").stdout.each_line do |line| + shell_out_with_timeout!("pkg info -r #{new_resource.package_name}").stdout.each_line do |line| return $1.split[0] if line =~ /Version: (.*)/ end return nil @@ -69,11 +70,11 @@ class Chef normal_command = "pkg#{expand_options(@new_resource.options)} install -q #{package_name}" command = if @new_resource.respond_to?(:accept_license) and @new_resource.accept_license - normal_command.gsub('-q', '-q --accept') + normal_command.gsub("-q", "-q --accept") else normal_command end - shell_out(command) + shell_out_with_timeout(command) end def upgrade_package(name, version) @@ -82,7 +83,7 @@ class Chef def remove_package(name, version) package_name = "#{name}@#{version}" - shell_out!( "pkg#{expand_options(@new_resource.options)} uninstall -q #{package_name}" ) + shell_out_with_timeout!( "pkg#{expand_options(@new_resource.options)} uninstall -q #{package_name}" ) end end end diff --git a/lib/chef/provider/package/macports.rb b/lib/chef/provider/package/macports.rb index b252344c99..c7ea71ac8c 100644 --- a/lib/chef/provider/package/macports.rb +++ b/lib/chef/provider/package/macports.rb @@ -3,8 +3,8 @@ class Chef class Package class Macports < Chef::Provider::Package - provides :macports_package provides :package, os: "darwin" + provides :macports_package def load_current_resource @current_resource = Chef::Resource::Package.new(@new_resource.name) @@ -49,21 +49,21 @@ class Chef unless @current_resource.version == version command = "port#{expand_options(@new_resource.options)} install #{name}" command << " @#{version}" if version and !version.empty? - shell_out!(command) + shell_out_with_timeout!(command) end end def purge_package(name, version) command = "port#{expand_options(@new_resource.options)} uninstall #{name}" command << " @#{version}" if version and !version.empty? - shell_out!(command) + shell_out_with_timeout!(command) end def remove_package(name, version) command = "port#{expand_options(@new_resource.options)} deactivate #{name}" command << " @#{version}" if version and !version.empty? - shell_out!(command) + shell_out_with_timeout!(command) end def upgrade_package(name, version) @@ -76,14 +76,14 @@ class Chef # that hasn't been installed. install_package(name, version) elsif current_version != version - shell_out!( "port#{expand_options(@new_resource.options)} upgrade #{name} @#{version}" ) + shell_out_with_timeout!( "port#{expand_options(@new_resource.options)} upgrade #{name} @#{version}" ) end end private def get_response_from_command(command) output = nil - status = shell_out(command) + status = shell_out_with_timeout(command) begin output = status.stdout rescue Exception diff --git a/lib/chef/provider/package/openbsd.rb b/lib/chef/provider/package/openbsd.rb index 82048c3bd4..539af1c409 100644 --- a/lib/chef/provider/package/openbsd.rb +++ b/lib/chef/provider/package/openbsd.rb @@ -20,11 +20,10 @@ # limitations under the License. # -require 'chef/resource/package' -require 'chef/provider/package' -require 'chef/mixin/shell_out' -require 'chef/mixin/get_source_from_package' -require 'chef/exceptions' +require "chef/resource/package" +require "chef/provider/package" +require "chef/mixin/get_source_from_package" +require "chef/exceptions" class Chef class Provider @@ -32,6 +31,7 @@ class Chef class Openbsd < Chef::Provider::Package provides :package, os: "openbsd" + provides :openbsd_package include Chef::Mixin::ShellOut include Chef::Mixin::GetSourceFromPackage @@ -53,7 +53,7 @@ class Chef # Below are incomplete/missing features for this package provider requirements.assert(:all_actions) do |a| a.assertion { !new_resource.source } - a.failure_message(Chef::Exceptions::Package, 'The openbsd package provider does not support the source attribute') + a.failure_message(Chef::Exceptions::Package, "The openbsd package provider does not support the source attribute") end requirements.assert(:all_actions) do |a| a.assertion do @@ -63,7 +63,7 @@ class Chef true end end - a.failure_message(Chef::Exceptions::Package, 'The openbsd package provider does not support providing a version and flavor') + a.failure_message(Chef::Exceptions::Package, "The openbsd package provider does not support providing a version and flavor") end end @@ -72,18 +72,16 @@ class Chef if parts = name.match(/^(.+?)--(.+)/) # use double-dash for stems with flavors, see man page for pkg_add name = parts[1] end - shell_out!("pkg_add -r #{name}#{version_string}", :env => {"PKG_PATH" => pkg_path}).status + shell_out_with_timeout!("pkg_add -r #{name}#{version_string(version)}", :env => {"PKG_PATH" => pkg_path}).status Chef::Log.debug("#{new_resource.package_name} installed") end end def remove_package(name, version) - version_string = '' - version_string += "-#{version}" if version if parts = name.match(/^(.+?)--(.+)/) name = parts[1] end - shell_out!("pkg_delete #{name}#{version_string}", :env => nil).status + shell_out_with_timeout!("pkg_delete #{name}#{version_string(version)}", :env => nil).status end private @@ -94,7 +92,7 @@ class Chef else name = new_resource.package_name end - pkg_info = shell_out!("pkg_info -e \"#{name}->0\"", :env => nil, :returns => [0,1]) + pkg_info = shell_out_with_timeout!("pkg_info -e \"#{name}->0\"", :env => nil, :returns => [0,1]) result = pkg_info.stdout[/^inst:#{Regexp.escape(name)}-(.+?)\s/, 1] Chef::Log.debug("installed_version of '#{new_resource.package_name}' is '#{result}'") result @@ -103,7 +101,7 @@ class Chef def candidate_version @candidate_version ||= begin results = [] - shell_out!("pkg_info -I \"#{new_resource.package_name}#{version_string}\"", :env => nil, :returns => [0,1]).stdout.each_line do |line| + shell_out_with_timeout!("pkg_info -I \"#{new_resource.package_name}#{version_string(new_resource.version)}\"", :env => nil, :returns => [0,1]).stdout.each_line do |line| if parts = new_resource.package_name.match(/^(.+?)--(.+)/) results << line[/^#{Regexp.escape(parts[1])}-(.+?)\s/, 1] else @@ -111,7 +109,7 @@ class Chef end end results = results.reject(&:nil?) - Chef::Log.debug("candidate versions of '#{new_resource.package_name}' are '#{results}'") + Chef::Log.debug("Candidate versions of '#{new_resource.package_name}' are '#{results}'") case results.length when 0 [] @@ -123,13 +121,13 @@ class Chef end end - def version_string - ver = '' - ver += "-#{new_resource.version}" if new_resource.version + def version_string(version) + ver = "" + ver += "-#{version}" if version end def pkg_path - ENV['PKG_PATH'] || "http://ftp.OpenBSD.org/pub/#{node.kernel.name}/#{node.kernel.release}/packages/#{node.kernel.machine}/" + ENV["PKG_PATH"] || "http://ftp.OpenBSD.org/pub/#{node.kernel.name}/#{node.kernel.release}/packages/#{node.kernel.machine}/" end end diff --git a/lib/chef/provider/package/pacman.rb b/lib/chef/provider/package/pacman.rb index f16fc811f5..6bb2250da5 100644 --- a/lib/chef/provider/package/pacman.rb +++ b/lib/chef/provider/package/pacman.rb @@ -16,25 +16,24 @@ # limitations under the License. # -require 'chef/provider/package' -require 'chef/mixin/command' -require 'chef/resource/package' +require "chef/provider/package" +require "chef/mixin/command" +require "chef/resource/package" class Chef class Provider class Package class Pacman < Chef::Provider::Package + provides :package, platform: "arch" provides :pacman_package, os: "linux" def load_current_resource @current_resource = Chef::Resource::Package.new(@new_resource.name) @current_resource.package_name(@new_resource.package_name) - @current_resource.version(nil) - Chef::Log.debug("#{@new_resource} checking pacman for #{@new_resource.package_name}") - status = shell_out("pacman -Qi #{@new_resource.package_name}") + status = shell_out_with_timeout("pacman -Qi #{@new_resource.package_name}") status.stdout.each_line do |line| case line when /^Version(\s?)*: (.+)$/ @@ -60,9 +59,9 @@ class Chef repos = pacman.scan(/\[(.+)\]/).flatten end - package_repos = repos.map {|r| Regexp.escape(r) }.join('|') + package_repos = repos.map {|r| Regexp.escape(r) }.join("|") - status = shell_out("pacman -Sl") + status = shell_out_with_timeout("pacman -Sl") status.stdout.each_line do |line| case line when /^(#{package_repos}) #{Regexp.escape(@new_resource.package_name)} (.+)$/ @@ -85,7 +84,7 @@ class Chef end def install_package(name, version) - shell_out!( "pacman --sync --noconfirm --noprogressbar#{expand_options(@new_resource.options)} #{name}" ) + shell_out_with_timeout!( "pacman --sync --noconfirm --noprogressbar#{expand_options(@new_resource.options)} #{name}" ) end def upgrade_package(name, version) @@ -93,7 +92,7 @@ class Chef end def remove_package(name, version) - shell_out!( "pacman --remove --noconfirm --noprogressbar#{expand_options(@new_resource.options)} #{name}" ) + shell_out_with_timeout!( "pacman --remove --noconfirm --noprogressbar#{expand_options(@new_resource.options)} #{name}" ) end def purge_package(name, version) diff --git a/lib/chef/provider/package/paludis.rb b/lib/chef/provider/package/paludis.rb index 407e0d0110..c2b1069e1e 100644 --- a/lib/chef/provider/package/paludis.rb +++ b/lib/chef/provider/package/paludis.rb @@ -16,38 +16,36 @@ # limitations under the License. # -require 'chef/provider/package' -require 'chef/resource/package' +require "chef/provider/package" +require "chef/resource/package" class Chef class Provider class Package class Paludis < Chef::Provider::Package + provides :package, platform: "exherbo" provides :paludis_package, os: "linux" def load_current_resource @current_resource = Chef::Resource::Package.new(@new_resource.package_name) @current_resource.package_name(@new_resource.package_name) - @current_resource.version(nil) - Chef::Log.debug("Checking package status for #{@new_resource.package_name}") installed = false - re = Regexp.new('(.*)[[:blank:]](.*)[[:blank:]](.*)$') + re = Regexp.new("(.*)[[:blank:]](.*)[[:blank:]](.*)$") shell_out!("cave -L warning print-ids -M none -m \"#{@new_resource.package_name}\" -f \"%c/%p %v %r\n\"").stdout.each_line do |line| res = re.match(line) unless res.nil? case res[3] - when 'accounts', 'installed-accounts' + when "accounts", "installed-accounts" next - when 'installed' + when "installed" installed = true @current_resource.version(res[2]) else @candidate_version = res[2] - @current_resource.version(nil) end end end diff --git a/lib/chef/provider/package/portage.rb b/lib/chef/provider/package/portage.rb index bb047ad2fa..4a1559637a 100644 --- a/lib/chef/provider/package/portage.rb +++ b/lib/chef/provider/package/portage.rb @@ -16,23 +16,25 @@ # limitations under the License. # -require 'chef/provider/package' -require 'chef/mixin/command' -require 'chef/resource/package' -require 'chef/util/path_helper' +require "chef/provider/package" +require "chef/mixin/command" +require "chef/resource/package" +require "chef/util/path_helper" class Chef class Provider class Package class Portage < Chef::Provider::Package + + provides :package, platform: "gentoo" + provides :portage_package + PACKAGE_NAME_PATTERN = %r{(?:([^/]+)/)?([^/]+)} def load_current_resource @current_resource = Chef::Resource::Package.new(@new_resource.name) @current_resource.package_name(@new_resource.package_name) - @current_resource.version(nil) - category, pkg = %r{^#{PACKAGE_NAME_PATTERN}$}.match(@new_resource.package_name)[1,2] globsafe_category = category ? Chef::Util::PathHelper.escape_glob(category) : nil @@ -46,7 +48,7 @@ class Chef if versions.size > 1 atoms = versions.map {|v| v.first }.sort - categories = atoms.map {|v| v.split('/')[0] }.uniq + categories = atoms.map {|v| v.split("/")[0] }.uniq if !category && categories.size > 1 raise Chef::Exceptions::Package, "Multiple packages found for #{@new_resource.package_name}: #{atoms.join(" ")}. Specify a category." end @@ -64,7 +66,7 @@ class Chef txt.each_line do |line| if line =~ /\*\s+#{PACKAGE_NAME_PATTERN}/ - found_package_name = $&.gsub(/\*/, '').strip + found_package_name = $&.gsub(/\*/, "").strip if package =~ /\// #the category is specified if found_package_name == package availables[found_package_name] = nil diff --git a/lib/chef/provider/package/rpm.rb b/lib/chef/provider/package/rpm.rb index f10fe23c71..9ab10b062d 100644 --- a/lib/chef/provider/package/rpm.rb +++ b/lib/chef/provider/package/rpm.rb @@ -15,11 +15,10 @@ # See the License for the specific language governing permissions and # limitations under the License. # -require 'chef/provider/package' -require 'chef/mixin/command' -require 'chef/mixin/shell_out' -require 'chef/resource/package' -require 'chef/mixin/get_source_from_package' +require "chef/provider/package" +require "chef/mixin/command" +require "chef/resource/package" +require "chef/mixin/get_source_from_package" class Chef class Provider @@ -51,7 +50,6 @@ class Chef @current_resource = Chef::Resource::Package.new(@new_resource.name) @current_resource.package_name(@new_resource.package_name) - @new_resource.version(nil) if @new_resource.source unless uri_scheme?(@new_resource.source) || ::File.exists?(@new_resource.source) @@ -60,9 +58,9 @@ class Chef end Chef::Log.debug("#{@new_resource} checking rpm status") - shell_out!("rpm -qp --queryformat '%{NAME} %{VERSION}-%{RELEASE}\n' #{@new_resource.source}").stdout.each_line do |line| + shell_out_with_timeout!("rpm -qp --queryformat '%{NAME} %{VERSION}-%{RELEASE}\n' #{@new_resource.source}").stdout.each_line do |line| case line - when /^([\w\d+_.-]+)\s([\w\d_.-]+)$/ + when /^(\S+)\s(\S+)$/ @current_resource.package_name($1) @new_resource.version($2) @candidate_version = $2 @@ -76,10 +74,10 @@ class Chef end Chef::Log.debug("#{@new_resource} checking install state") - @rpm_status = shell_out("rpm -q --queryformat '%{NAME} %{VERSION}-%{RELEASE}\n' #{@current_resource.package_name}") + @rpm_status = shell_out_with_timeout("rpm -q --queryformat '%{NAME} %{VERSION}-%{RELEASE}\n' #{@current_resource.package_name}") @rpm_status.stdout.each_line do |line| case line - when /^([\w\d+_.-]+)\s([\w\d_.-]+)$/ + when /^(\S+)\s(\S+)$/ Chef::Log.debug("#{@new_resource} current version is #{$2}") @current_resource.version($2) end @@ -90,12 +88,12 @@ class Chef def install_package(name, version) unless @current_resource.version - shell_out!( "rpm #{@new_resource.options} -i #{@new_resource.source}" ) + shell_out_with_timeout!( "rpm #{@new_resource.options} -i #{@new_resource.source}" ) else if allow_downgrade - shell_out!( "rpm #{@new_resource.options} -U --oldpackage #{@new_resource.source}" ) + shell_out_with_timeout!( "rpm #{@new_resource.options} -U --oldpackage #{@new_resource.source}" ) else - shell_out!( "rpm #{@new_resource.options} -U #{@new_resource.source}" ) + shell_out_with_timeout!( "rpm #{@new_resource.options} -U #{@new_resource.source}" ) end end end @@ -104,9 +102,9 @@ class Chef def remove_package(name, version) if version - shell_out!( "rpm #{@new_resource.options} -e #{name}-#{version}" ) + shell_out_with_timeout!( "rpm #{@new_resource.options} -e #{name}-#{version}" ) else - shell_out!( "rpm #{@new_resource.options} -e #{name}" ) + shell_out_with_timeout!( "rpm #{@new_resource.options} -e #{name}" ) end end @@ -115,7 +113,7 @@ class Chef def uri_scheme?(str) scheme = URI.split(str).first return false unless scheme - %w(http https ftp file).include?(scheme.downcase) + %w{http https ftp file}.include?(scheme.downcase) rescue URI::InvalidURIError return false end diff --git a/lib/chef/provider/package/rubygems.rb b/lib/chef/provider/package/rubygems.rb index c53aa8934a..85796e8b9a 100644 --- a/lib/chef/provider/package/rubygems.rb +++ b/lib/chef/provider/package/rubygems.rb @@ -1,7 +1,7 @@ # # Author:: Adam Jacob (<adam@opscode.com>) # Author:: Daniel DeLeo (<dan@opscode.com>) -# Copyright:: Copyright (c) 2008, 2010 Opscode, Inc. +# Copyright:: Copyright (c) 2008, 2010-2016 Chef Software, Inc. # License:: Apache License, Version 2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,32 +17,25 @@ # limitations under the License. # -require 'uri' -require 'chef/provider/package' -require 'chef/mixin/command' -require 'chef/resource/package' -require 'chef/mixin/get_source_from_package' +require "uri" +require "chef/provider/package" +require "chef/mixin/command" +require "chef/resource/package" +require "chef/mixin/get_source_from_package" # Class methods on Gem are defined in rubygems -require 'rubygems' +require "rubygems" # Ruby 1.9's gem_prelude can interact poorly with loading the full rubygems # explicitly like this. Make sure rubygems/specification is always last in this # list -require 'rubygems/version' -require 'rubygems/dependency' -require 'rubygems/spec_fetcher' -require 'rubygems/platform' - -# Compatibility note: Rubygems 2.0 removes rubygems/format in favor of -# rubygems/package. -begin - require 'rubygems/format' -rescue LoadError - require 'rubygems/package' -end -require 'rubygems/dependency_installer' -require 'rubygems/uninstaller' -require 'rubygems/specification' +require "rubygems/version" +require "rubygems/dependency" +require "rubygems/spec_fetcher" +require "rubygems/platform" +require "rubygems/package" +require "rubygems/dependency_installer" +require "rubygems/uninstaller" +require "rubygems/specification" class Chef class Provider @@ -93,7 +86,7 @@ class Chef # === Returns # [Gem::Specification] an array of Gem::Specification objects def installed_versions(gem_dep) - if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.8.0') + if Gem::Version.new(Gem::VERSION) >= Gem::Version.new("1.8.0") gem_specification.find_all_by_name(gem_dep.name, gem_dep.requirement) else gem_source_index.search(gem_dep) @@ -144,7 +137,7 @@ class Chef spec.version else # This is probably going to end badly... - logger.warn { "#{@new_resource} gem package #{source} does not satisfy the requirements #{gem_dependency.to_s}" } + logger.warn { "#{@new_resource} gem package #{source} does not satisfy the requirements #{gem_dependency}" } nil end end @@ -168,17 +161,17 @@ class Chef def find_newest_remote_version(gem_dependency, *sources) available_gems = dependency_installer.find_gems_with_sources(gem_dependency) spec, source = if available_gems.respond_to?(:last) - # DependencyInstaller sorts the results such that the last one is - # always the one it considers best. - spec_with_source = available_gems.last - spec_with_source && spec_with_source - else - # Rubygems 2.0 returns a Gem::Available set, which is a - # collection of AvailableSet::Tuple structs - available_gems.pick_best! - best_gem = available_gems.set.first - best_gem && [best_gem.spec, best_gem.source] - end + # DependencyInstaller sorts the results such that the last one is + # always the one it considers best. + spec_with_source = available_gems.last + spec_with_source && spec_with_source + else + # Rubygems 2.0 returns a Gem::Available set, which is a + # collection of AvailableSet::Tuple structs + available_gems.pick_best! + best_gem = available_gems.set.first + best_gem && [best_gem.spec, best_gem.source] + end version = spec && spec.version if version @@ -295,7 +288,7 @@ class Chef end def gem_source_index - @source_index ||= Gem::SourceIndex.from_gems_in(*gem_paths.map { |p| p + '/specifications' }) + @source_index ||= Gem::SourceIndex.from_gems_in(*gem_paths.map { |p| p + "/specifications" }) end def gem_specification @@ -327,7 +320,7 @@ class Chef else gem_environment = shell_out!("#{@gem_binary_location} env").stdout if jruby = gem_environment[JRUBY_PLATFORM] - self.class.platform_cache[@gem_binary_location] = ['ruby', Gem::Platform.new(jruby)] + self.class.platform_cache[@gem_binary_location] = ["ruby", Gem::Platform.new(jruby)] else self.class.platform_cache[@gem_binary_location] = Gem.platforms end @@ -401,11 +394,11 @@ class Chef end def is_omnibus? - if RbConfig::CONFIG['bindir'] =~ %r!/opt/(opscode|chef)/embedded/bin! + if RbConfig::CONFIG["bindir"] =~ %r{/(opscode|chef|chefdk)/embedded/bin} Chef::Log.debug("#{@new_resource} detected omnibus installation in #{RbConfig::CONFIG['bindir']}") # Omnibus installs to a static path because of linking on unix, find it. true - elsif RbConfig::CONFIG['bindir'].sub(/^[\w]:/, '') == "/opscode/chef/embedded/bin" + elsif RbConfig::CONFIG["bindir"].sub(/^[\w]:/, "") == "/opscode/chef/embedded/bin" Chef::Log.debug("#{@new_resource} detected omnibus installation in #{RbConfig::CONFIG['bindir']}") # windows, with the drive letter removed true @@ -417,7 +410,7 @@ class Chef def find_gem_by_path Chef::Log.debug("#{@new_resource} searching for 'gem' binary in path: #{ENV['PATH']}") separator = ::File::ALT_SEPARATOR ? ::File::ALT_SEPARATOR : ::File::SEPARATOR - path_to_first_gem = ENV['PATH'].split(::File::PATH_SEPARATOR).select { |path| ::File.exists?(path + separator + "gem") }.first + path_to_first_gem = ENV["PATH"].split(::File::PATH_SEPARATOR).select { |path| ::File.exists?(path + separator + "gem") }.first raise Chef::Exceptions::FileNotFound, "Unable to find 'gem' binary in path: #{ENV['PATH']}" if path_to_first_gem.nil? path_to_first_gem + separator + "gem" end @@ -445,14 +438,14 @@ class Chef gemspec = matching_installed_versions.last logger.debug { "#{@new_resource} found installed gem #{gemspec.name} version #{gemspec.version} matching #{gem_dependency}"} gemspec - # If no version matching the requirements exists, the latest installed - # version is the current version. + # If no version matching the requirements exists, the latest installed + # version is the current version. elsif !all_installed_versions.empty? gemspec = all_installed_versions.last logger.debug { "#{@new_resource} newest installed version of gem #{gemspec.name} is #{gemspec.version}" } gemspec else - logger.debug { "#{@new_resource} no installed version found for #{gem_dependency.to_s}"} + logger.debug { "#{@new_resource} no installed version found for #{gem_dependency}"} nil end end @@ -463,8 +456,8 @@ class Chef def all_installed_versions @all_installed_versions ||= begin - @gem_env.installed_versions(Gem::Dependency.new(gem_dependency.name, '>= 0')) - end + @gem_env.installed_versions(Gem::Dependency.new(gem_dependency.name, ">= 0")) + end end def gem_sources @@ -489,14 +482,14 @@ class Chef def candidate_version @candidate_version ||= begin - if target_version_already_installed?(@current_resource.version, @new_resource.version) - nil - elsif source_is_remote? - @gem_env.candidate_version_from_remote(gem_dependency, *gem_sources).to_s - else - @gem_env.candidate_version_from_file(gem_dependency, @new_resource.source).to_s - end - end + if target_version_already_installed?(@current_resource.version, @new_resource.version) + nil + elsif source_is_remote? + @gem_env.candidate_version_from_remote(gem_dependency, *gem_sources).to_s + else + @gem_env.candidate_version_from_file(gem_dependency, @new_resource.source).to_s + end + end end def target_version_already_installed?(current_version, new_version) @@ -532,22 +525,22 @@ class Chef end def gem_binary_path - @new_resource.gem_binary || 'gem' + @new_resource.gem_binary || "gem" end def install_via_gem_command(name, version) if @new_resource.source =~ /\.gem$/i name = @new_resource.source elsif @new_resource.clear_sources - src = ' --clear-sources' - src << (@new_resource.source && " --source=#{@new_resource.source}" || '') + src = " --clear-sources" + src << (@new_resource.source && " --source=#{@new_resource.source}" || "") else src = @new_resource.source && " --source=#{@new_resource.source} --source=https://rubygems.org" end if !version.nil? && version.length > 0 - shell_out!("#{gem_binary_path} install #{name} -q --no-rdoc --no-ri -v \"#{version}\"#{src}#{opts}", :env=>nil) + shell_out_with_timeout!("#{gem_binary_path} install #{name} -q --no-rdoc --no-ri -v \"#{version}\"#{src}#{opts}", :env=>nil) else - shell_out!("#{gem_binary_path} install \"#{name}\" -q --no-rdoc --no-ri #{src}#{opts}", :env=>nil) + shell_out_with_timeout!("#{gem_binary_path} install \"#{name}\" -q --no-rdoc --no-ri #{src}#{opts}", :env=>nil) end end @@ -571,9 +564,9 @@ class Chef def uninstall_via_gem_command(name, version) if version - shell_out!("#{gem_binary_path} uninstall #{name} -q -x -I -v \"#{version}\"#{opts}", :env=>nil) + shell_out_with_timeout!("#{gem_binary_path} uninstall #{name} -q -x -I -v \"#{version}\"#{opts}", :env=>nil) else - shell_out!("#{gem_binary_path} uninstall #{name} -q -x -I -a#{opts}", :env=>nil) + shell_out_with_timeout!("#{gem_binary_path} uninstall #{name} -q -x -I -a#{opts}", :env=>nil) end end diff --git a/lib/chef/provider/package/smartos.rb b/lib/chef/provider/package/smartos.rb index 7cef91953a..98264ecd1e 100644 --- a/lib/chef/provider/package/smartos.rb +++ b/lib/chef/provider/package/smartos.rb @@ -19,9 +19,9 @@ # limitations under the License. # -require 'chef/provider/package' -require 'chef/resource/package' -require 'chef/mixin/get_source_from_package' +require "chef/provider/package" +require "chef/resource/package" +require "chef/mixin/get_source_from_package" class Chef class Provider @@ -29,13 +29,13 @@ class Chef class SmartOS < Chef::Provider::Package attr_accessor :is_virtual_package + provides :package, platform: "smartos" provides :smartos_package, os: "solaris2", platform_family: "smartos" def load_current_resource Chef::Log.debug("#{@new_resource} loading current resource") @current_resource = Chef::Resource::Package.new(@new_resource.name) @current_resource.package_name(@new_resource.package_name) - @current_resource.version(nil) check_package_state(@new_resource.package_name) @current_resource # modified by check_package_state end @@ -43,15 +43,13 @@ class Chef def check_package_state(name) Chef::Log.debug("#{@new_resource} checking package #{name}") version = nil - info = shell_out!("/opt/local/sbin/pkg_info -E \"#{name}*\"", :env => nil, :returns => [0,1]) + info = shell_out_with_timeout!("/opt/local/sbin/pkg_info", "-E", "#{name}*", :env => nil, :returns => [0,1]) if info.stdout version = info.stdout[/^#{@new_resource.package_name}-(.+)/, 1] end - if !version - @current_resource.version(nil) - else + if version @current_resource.version(version) end end @@ -60,11 +58,11 @@ class Chef return @candidate_version if @candidate_version name = nil version = nil - pkg = shell_out!("/opt/local/bin/pkgin se #{new_resource.package_name}", :env => nil, :returns => [0,1]) + pkg = shell_out_with_timeout!("/opt/local/bin/pkgin", "se", new_resource.package_name, :env => nil, :returns => [0,1]) pkg.stdout.each_line do |line| case line when /^#{new_resource.package_name}/ - name, version = line.split[0].split(/-([^-]+)$/) + name, version = line.split(/[; ]/)[0].split(/-([^-]+)$/) end end @candidate_version = version @@ -74,7 +72,7 @@ class Chef def install_package(name, version) Chef::Log.debug("#{@new_resource} installing package #{name} version #{version}") package = "#{name}-#{version}" - out = shell_out!("/opt/local/bin/pkgin -y install #{package}", :env => nil) + out = shell_out_with_timeout!("/opt/local/bin/pkgin", "-y", "install", package, :env => nil) end def upgrade_package(name, version) @@ -85,7 +83,7 @@ class Chef def remove_package(name, version) Chef::Log.debug("#{@new_resource} removing package #{name} version #{version}") package = "#{name}" - out = shell_out!("/opt/local/bin/pkgin -y remove #{package}", :env => nil) + out = shell_out_with_timeout!("/opt/local/bin/pkgin", "-y", "remove", package, :env => nil) end end diff --git a/lib/chef/provider/package/solaris.rb b/lib/chef/provider/package/solaris.rb index a2cfd93ef6..8442e57dbb 100644 --- a/lib/chef/provider/package/solaris.rb +++ b/lib/chef/provider/package/solaris.rb @@ -15,10 +15,10 @@ # See the License for the specific language governing permissions and # limitations under the License. # -require 'chef/provider/package' -require 'chef/mixin/command' -require 'chef/resource/package' -require 'chef/mixin/get_source_from_package' +require "chef/provider/package" +require "chef/mixin/command" +require "chef/resource/package" +require "chef/mixin/get_source_from_package" class Chef class Provider @@ -27,6 +27,8 @@ class Chef include Chef::Mixin::GetSourceFromPackage + provides :package, platform: "nexentacore" + provides :package, platform: "solaris2", platform_version: "< 5.11" provides :solaris_package, os: "solaris2" # def initialize(*args) @@ -49,13 +51,12 @@ class Chef def load_current_resource @current_resource = Chef::Resource::Package.new(@new_resource.name) @current_resource.package_name(@new_resource.package_name) - @new_resource.version(nil) if @new_resource.source @package_source_found = ::File.exists?(@new_resource.source) if @package_source_found Chef::Log.debug("#{@new_resource} checking pkg status") - shell_out("pkginfo -l -d #{@new_resource.source} #{@new_resource.package_name}").stdout.each_line do |line| + shell_out_with_timeout("pkginfo -l -d #{@new_resource.source} #{@new_resource.package_name}").stdout.each_line do |line| case line when /VERSION:\s+(.+)/ @new_resource.version($1) @@ -65,7 +66,7 @@ class Chef end Chef::Log.debug("#{@new_resource} checking install state") - status = shell_out("pkginfo -l #{@current_resource.package_name}") + status = shell_out_with_timeout("pkginfo -l #{@current_resource.package_name}") status.stdout.each_line do |line| case line when /VERSION:\s+(.+)/ @@ -78,16 +79,12 @@ class Chef raise Chef::Exceptions::Package, "pkginfo failed - #{status.inspect}!" end - unless @current_resource.version.nil? - @current_resource.version(nil) - end - @current_resource end def candidate_version return @candidate_version if @candidate_version - status = shell_out("pkginfo -l -d #{@new_resource.source} #{new_resource.package_name}") + status = shell_out_with_timeout("pkginfo -l -d #{@new_resource.source} #{new_resource.package_name}") status.stdout.each_line do |line| case line when /VERSION:\s+(.+)/ @@ -110,7 +107,7 @@ class Chef else command = "pkgadd -n -d #{@new_resource.source} all" end - shell_out!(command) + shell_out_with_timeout!(command) Chef::Log.debug("#{@new_resource} installed version #{@new_resource.version} from: #{@new_resource.source}") else if ::File.directory?(@new_resource.source) # CHEF-4469 @@ -118,17 +115,19 @@ class Chef else command = "pkgadd -n#{expand_options(@new_resource.options)} -d #{@new_resource.source} all" end - shell_out!(command) + shell_out_with_timeout!(command) Chef::Log.debug("#{@new_resource} installed version #{@new_resource.version} from: #{@new_resource.source}") end end + alias_method :upgrade_package, :install_package + def remove_package(name, version) if @new_resource.options.nil? - shell_out!( "pkgrm -n #{name}" ) + shell_out_with_timeout!( "pkgrm -n #{name}" ) Chef::Log.debug("#{@new_resource} removed version #{@new_resource.version}") else - shell_out!( "pkgrm -n#{expand_options(@new_resource.options)} #{name}" ) + shell_out_with_timeout!( "pkgrm -n#{expand_options(@new_resource.options)} #{name}" ) Chef::Log.debug("#{@new_resource} removed version #{@new_resource.version}") end end diff --git a/lib/chef/provider/package/windows.rb b/lib/chef/provider/package/windows.rb index 143d82f111..87fadc27cf 100644 --- a/lib/chef/provider/package/windows.rb +++ b/lib/chef/provider/package/windows.rb @@ -16,70 +16,244 @@ # limitations under the License. # -require 'chef/resource/windows_package' -require 'chef/provider/package' -require 'chef/util/path_helper' +require "chef/mixin/uris" +require "chef/resource/windows_package" +require "chef/provider/package" +require "chef/util/path_helper" +require "chef/mixin/checksum" class Chef class Provider class Package class Windows < Chef::Provider::Package + include Chef::Mixin::Uris + include Chef::Mixin::Checksum provides :package, os: "windows" provides :windows_package, os: "windows" - # Depending on the installer, we may need to examine installer_type or - # source attributes, or search for text strings in the installer file - # binary to determine the installer type for the user. Since the file - # must be on disk to do so, we have to make this choice in the provider. - require 'chef/provider/package/windows/msi.rb' + require "chef/provider/package/windows/registry_uninstall_entry.rb" + + def define_resource_requirements + requirements.assert(:install) do |a| + a.assertion { new_resource.source unless package_provider == :msi } + a.failure_message Chef::Exceptions::NoWindowsPackageSource, "Source for package #{new_resource.name} must be specified in the resource's source property for package to be installed because the package_name property is used to test for the package installation state for this package type." + end + end # load_current_resource is run in Chef::Provider#run_action when not in whyrun_mode? def load_current_resource - @new_resource.source(Chef::Util::PathHelper.validate_path(@new_resource.source)) + @current_resource = Chef::Resource::WindowsPackage.new(new_resource.name) + if downloadable_file_missing? + Chef::Log.debug("We do not know the version of #{new_resource.source} because the file is not downloaded") + current_resource.version(:unknown.to_s) + else + current_resource.version(package_provider.installed_version) + new_resource.version(package_provider.package_version) if package_provider.package_version + end - @current_resource = Chef::Resource::WindowsPackage.new(@new_resource.name) - @current_resource.version(package_provider.installed_version) - @new_resource.version(package_provider.package_version) - @current_resource + current_resource end def package_provider @package_provider ||= begin case installer_type when :msi - Chef::Provider::Package::Windows::MSI.new(@new_resource) + Chef::Log.debug("#{new_resource} is MSI") + require "chef/provider/package/windows/msi" + Chef::Provider::Package::Windows::MSI.new(resource_for_provider, uninstall_registry_entries) else - raise "Unable to find a Chef::Provider::Package::Windows provider for installer_type '#{installer_type}'" + Chef::Log.debug("#{new_resource} is EXE with type '#{installer_type}'") + require "chef/provider/package/windows/exe" + Chef::Provider::Package::Windows::Exe.new(resource_for_provider, installer_type, uninstall_registry_entries) end end end def installer_type + # Depending on the installer, we may need to examine installer_type or + # source attributes, or search for text strings in the installer file + # binary to determine the installer type for the user. Since the file + # must be on disk to do so, we have to make this choice in the provider. @installer_type ||= begin - if @new_resource.installer_type - @new_resource.installer_type + if new_resource.installer_type + new_resource.installer_type + elsif source_location.nil? + inferred_registry_type else - file_extension = ::File.basename(@new_resource.source).split(".").last.downcase + basename = ::File.basename(source_location) + file_extension = basename.split(".").last.downcase if file_extension == "msi" :msi else - raise ArgumentError, "Installer type for Windows Package '#{@new_resource.name}' not specified and cannot be determined from file extension '#{file_extension}'" + # search the binary file for installer type + ::Kernel.open(::File.expand_path(source_location), "rb") do |io| + filesize = io.size + bufsize = 4096 # read 4K buffers + overlap = 16 # bytes to overlap between buffer reads + + until io.eof + contents = io.read(bufsize) + + case contents + when /inno/i # Inno Setup + return :inno + when /wise/i # Wise InstallMaster + return :wise + when /nullsoft/i # Nullsoft Scriptable Install System + return :nsis + end + + if (io.tell() < filesize) + io.seek(io.tell() - overlap) + end + end + end + + # if file is named 'setup.exe' assume installshield + if basename == "setup.exe" + :installshield + else + raise Chef::Exceptions::CannotDetermineWindowsInstallerType, "Installer type for Windows Package '#{new_resource.name}' not specified and cannot be determined from file extension '#{file_extension}'" + end end end end end + def action_install + if uri_scheme?(new_resource.source) + download_source_file + load_current_resource + else + validate_content! + end + + super + end + # Chef::Provider::Package action_install + action_remove call install_package + remove_package # Pass those calls to the correct sub-provider def install_package(name, version) - package_provider.install_package(name, version) + package_provider.install_package end def remove_package(name, version) - package_provider.remove_package(name, version) + package_provider.remove_package + end + + # @return [Array] new_version(s) as an array + def new_version_array + # Because the one in the parent caches things + [new_resource.version] + end + + # @return [String] candidate_version + def candidate_version + @candidate_version ||= (new_resource.version || "latest") + end + + # @return [Array] current_version(s) as an array + # this package provider does not support package arrays + # However, There may be multiple versions for a single + # package so the first element may be a nested array + def current_version_array + [ current_resource.version ] + end + + # @param current_version<String> one or more versions currently installed + # @param new_version<String> version of the new resource + # + # @return [Boolean] true if new_version is equal to or included in current_version + def target_version_already_installed?(current_version, new_version) + Chef::Log.debug("Checking if #{new_resource} version '#{new_version}' is already installed. #{current_version} is currently installed") + if current_version.is_a?(Array) + current_version.include?(new_version) + else + new_version == current_version + end + end + + def have_any_matching_version? + target_version_already_installed?(current_resource.version, new_resource.version) + end + + private + + def uninstall_registry_entries + @uninstall_registry_entries ||= Chef::Provider::Package::Windows::RegistryUninstallEntry.find_entries(new_resource.package_name) end + + def inferred_registry_type + uninstall_registry_entries.each do |entry| + return :inno if entry.key.end_with?("_is1") + return :msi if entry.uninstall_string.downcase.start_with?("msiexec.exe ") + return :nsis if entry.uninstall_string.downcase.end_with?("uninst.exe\"") + end + nil + end + + def downloadable_file_missing? + !new_resource.source.nil? && uri_scheme?(new_resource.source) && !::File.exists?(source_location) + end + + def resource_for_provider + @resource_for_provider = Chef::Resource::WindowsPackage.new(new_resource.name).tap do |r| + r.source(Chef::Util::PathHelper.validate_path(source_location)) unless source_location.nil? + r.version(new_resource.version) + r.timeout(new_resource.timeout) + r.returns(new_resource.returns) + r.options(new_resource.options) + end + end + + def download_source_file + source_resource.run_action(:create) + Chef::Log.debug("#{new_resource} fetched source file to #{source_resource.path}") + end + + def source_resource + @source_resource ||= Chef::Resource::RemoteFile.new(default_download_cache_path, run_context).tap do |r| + r.source(new_resource.source) + r.checksum(new_resource.checksum) + r.backup(false) + + if new_resource.remote_file_attributes + new_resource.remote_file_attributes.each do |(k,v)| + r.send(k.to_sym, v) + end + end + end + end + + def default_download_cache_path + uri = ::URI.parse(new_resource.source) + filename = ::File.basename(::URI.unescape(uri.path)) + file_cache_dir = Chef::FileCache.create_cache_path("package/") + Chef::Util::PathHelper.cleanpath("#{file_cache_dir}/#{filename}") + end + + def source_location + if new_resource.source.nil? + nil + elsif uri_scheme?(new_resource.source) + source_resource.path + else + new_source = Chef::Util::PathHelper.cleanpath(new_resource.source) + ::File.exist?(new_source) ? new_source : nil + end + end + + def validate_content! + if new_resource.checksum + source_checksum = checksum(source_location) + if new_resource.checksum != source_checksum + raise Chef::Exceptions::ChecksumMismatch.new(short_cksum(new_resource.checksum), short_cksum(source_checksum)) + end + end + end + end end end diff --git a/lib/chef/provider/package/windows/exe.rb b/lib/chef/provider/package/windows/exe.rb new file mode 100644 index 0000000000..e510755281 --- /dev/null +++ b/lib/chef/provider/package/windows/exe.rb @@ -0,0 +1,117 @@ +# +# Author:: Seth Chisamore (<schisamo@chef.io>) +# Author:: Matt Wrock <matt@mattwrock.com> +# Copyright:: Copyright (c) 2011, 2015 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" + +class Chef + class Provider + class Package + class Windows + class Exe + include Chef::Mixin::ShellOut + + def initialize(resource, installer_type, uninstall_entries) + @new_resource = resource + @installer_type = installer_type + @uninstall_entries = uninstall_entries + end + + attr_reader :new_resource + attr_reader :installer_type + attr_reader :uninstall_entries + + # From Chef::Provider::Package + def expand_options(options) + options ? " #{options}" : "" + end + + # Returns a version if the package is installed or nil if it is not. + def installed_version + Chef::Log.debug("#{new_resource} checking package version") + current_installed_version + end + + def package_version + new_resource.version + end + + def install_package + Chef::Log.debug("#{new_resource} installing #{new_resource.installer_type} package '#{new_resource.source}'") + shell_out!( + [ + "start", + "\"\"", + "/wait", + "\"#{new_resource.source}\"", + unattended_flags, + expand_options(new_resource.options), + "& exit %%%%ERRORLEVEL%%%%", + ].join(" "), timeout: new_resource.timeout, returns: new_resource.returns + ) + end + + def remove_package + uninstall_version = new_resource.version || current_installed_version + uninstall_entries.select { |entry| [uninstall_version].flatten.include?(entry.display_version) } + .map { |version| version.uninstall_string }.uniq.each do |uninstall_string| + Chef::Log.debug("Registry provided uninstall string for #{new_resource} is '#{uninstall_string}'") + shell_out!(uninstall_command(uninstall_string), { returns: new_resource.returns }) + end + end + + private + + def uninstall_command(uninstall_string) + uninstall_string.delete!('"') + uninstall_string = [ + %q{/d"}, + ::File.dirname(uninstall_string), + %q{" }, + ::File.basename(uninstall_string), + expand_options(new_resource.options), + " ", + unattended_flags, + ].join + %Q{start "" /wait #{uninstall_string} & exit %%%%ERRORLEVEL%%%%} + end + + def current_installed_version + @current_installed_version ||= uninstall_entries.count == 0 ? nil : begin + uninstall_entries.map { |entry| entry.display_version }.uniq + end + end + + # http://unattended.sourceforge.net/installers.php + def unattended_flags + case installer_type + when :installshield + "/s /sms" + when :nsis + "/S /NCRC" + when :inno + "/VERYSILENT /SUPPRESSMSGBOXES /NORESTART" + when :wise + "/s" + end + end + end + end + end + end +end diff --git a/lib/chef/provider/package/windows/msi.rb b/lib/chef/provider/package/windows/msi.rb index 938452945e..106dc496b6 100644 --- a/lib/chef/provider/package/windows/msi.rb +++ b/lib/chef/provider/package/windows/msi.rb @@ -18,21 +18,25 @@ # TODO: Allow @new_resource.source to be a Product Code as a GUID for uninstall / network install -require 'chef/win32/api/installer' if RUBY_PLATFORM =~ /mswin|mingw32|windows/ -require 'chef/mixin/shell_out' +require "chef/win32/api/installer" if (RUBY_PLATFORM =~ /mswin|mingw32|windows/) && Chef::Platform.supports_msi? +require "chef/mixin/shell_out" class Chef class Provider class Package class Windows class MSI - include Chef::ReservedNames::Win32::API::Installer if RUBY_PLATFORM =~ /mswin|mingw32|windows/ + include Chef::ReservedNames::Win32::API::Installer if (RUBY_PLATFORM =~ /mswin|mingw32|windows/) && Chef::Platform.supports_msi? include Chef::Mixin::ShellOut - def initialize(resource) + def initialize(resource, uninstall_entries) @new_resource = resource + @uninstall_entries = uninstall_entries end + attr_reader :new_resource + attr_reader :uninstall_entries + # From Chef::Provider::Package def expand_options(options) options ? " #{options}" : "" @@ -40,27 +44,47 @@ class Chef # Returns a version if the package is installed or nil if it is not. def installed_version - Chef::Log.debug("#{@new_resource} getting product code for package at #{@new_resource.source}") - product_code = get_product_property(@new_resource.source, "ProductCode") - Chef::Log.debug("#{@new_resource} checking package status and version for #{product_code}") - get_installed_version(product_code) + if !new_resource.source.nil? && ::File.exist?(new_resource.source) + Chef::Log.debug("#{new_resource} getting product code for package at #{new_resource.source}") + product_code = get_product_property(new_resource.source, "ProductCode") + Chef::Log.debug("#{new_resource} checking package status and version for #{product_code}") + get_installed_version(product_code) + else + uninstall_entries.count == 0 ? nil : begin + uninstall_entries.map { |entry| entry.display_version }.uniq + end + end end def package_version - Chef::Log.debug("#{@new_resource} getting product version for package at #{@new_resource.source}") - get_product_property(@new_resource.source, "ProductVersion") + return new_resource.version if new_resource.version + if !new_resource.source.nil? && ::File.exist?(new_resource.source) + Chef::Log.debug("#{new_resource} getting product version for package at #{new_resource.source}") + get_product_property(new_resource.source, "ProductVersion") + end end - def install_package(name, version) + def install_package # We could use MsiConfigureProduct here, but we'll start off with msiexec - Chef::Log.debug("#{@new_resource} installing MSI package '#{@new_resource.source}'") - shell_out!("msiexec /qn /i \"#{@new_resource.source}\" #{expand_options(@new_resource.options)}", {:timeout => @new_resource.timeout, :returns => @new_resource.returns}) + Chef::Log.debug("#{new_resource} installing MSI package '#{new_resource.source}'") + shell_out!("msiexec /qn /i \"#{new_resource.source}\" #{expand_options(new_resource.options)}", {:timeout => new_resource.timeout, :returns => new_resource.returns}) end - - def remove_package(name, version) + + def remove_package # We could use MsiConfigureProduct here, but we'll start off with msiexec - Chef::Log.debug("#{@new_resource} removing MSI package '#{@new_resource.source}'") - shell_out!("msiexec /qn /x \"#{@new_resource.source}\" #{expand_options(@new_resource.options)}", {:timeout => @new_resource.timeout, :returns => @new_resource.returns}) + if !new_resource.source.nil? && ::File.exist?(new_resource.source) + Chef::Log.debug("#{new_resource} removing MSI package '#{new_resource.source}'") + shell_out!("msiexec /qn /x \"#{new_resource.source}\" #{expand_options(new_resource.options)}", {:timeout => new_resource.timeout, :returns => new_resource.returns}) + else + uninstall_version = new_resource.version || installed_version + uninstall_entries.select { |entry| [uninstall_version].flatten.include?(entry.display_version) } + .map { |version| version.uninstall_string }.uniq.each do |uninstall_string| + Chef::Log.debug("#{new_resource} removing MSI package version using '#{uninstall_string}'") + uninstall_string += expand_options(new_resource.options) + uninstall_string += " /Q" unless uninstall_string =~ / \/Q\b/ + shell_out!(uninstall_string, {:timeout => new_resource.timeout, :returns => new_resource.returns}) + end + end end end end diff --git a/lib/chef/provider/package/windows/registry_uninstall_entry.rb b/lib/chef/provider/package/windows/registry_uninstall_entry.rb new file mode 100644 index 0000000000..145f799e05 --- /dev/null +++ b/lib/chef/provider/package/windows/registry_uninstall_entry.rb @@ -0,0 +1,89 @@ +# +# Author:: Seth Chisamore (<schisamo@chef.io>) +# Author:: Matt Wrock <matt@mattwrock.com> +# Copyright:: Copyright (c) 2011, 2015 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 "win32/registry" if (RUBY_PLATFORM =~ /mswin|mingw32|windows/) + +class Chef + class Provider + class Package + class Windows + class RegistryUninstallEntry + + def self.find_entries(package_name) + Chef::Log.debug("Finding uninstall entries for #{package_name}") + entries = [] + [ + [::Win32::Registry::HKEY_LOCAL_MACHINE, (::Win32::Registry::Constants::KEY_READ | 0x0100)], + [::Win32::Registry::HKEY_LOCAL_MACHINE, (::Win32::Registry::Constants::KEY_READ | 0x0200)], + [::Win32::Registry::HKEY_CURRENT_USER], + ].each do |hkey| + desired = hkey.length > 1 ? hkey[1] : ::Win32::Registry::Constants::KEY_READ + begin + ::Win32::Registry.open(hkey[0], UNINSTALL_SUBKEY, desired) do |reg| + reg.each_key do |key, _wtime| + begin + entry = reg.open(key, desired) + display_name = read_registry_property(entry, "DisplayName") + if display_name == package_name + entries.push(RegistryUninstallEntry.new(hkey, key, entry)) + end + rescue ::Win32::Registry::Error => ex + Chef::Log.debug("Registry error opening key '#{key}' on node #{desired}: #{ex}") + end + end + end + rescue ::Win32::Registry::Error => ex + Chef::Log.debug("Registry error opening hive '#{hkey[0]}' :: #{desired}: #{ex}") + end + end + entries + end + + def self.read_registry_property(data, property) + data[property] + rescue ::Win32::Registry::Error => ex + Chef::Log.debug("Failure to read property '#{property}'") + nil + end + + def initialize(hive, key, registry_data) + Chef::Log.debug("Creating uninstall entry for #{hive}::#{key}") + @hive = hive + @key = key + @data = registry_data + @display_name = RegistryUninstallEntry.read_registry_property(registry_data, "DisplayName") + @display_version = RegistryUninstallEntry.read_registry_property(registry_data, "DisplayVersion") + @uninstall_string = RegistryUninstallEntry.read_registry_property(registry_data, "UninstallString") + end + + attr_reader :hive + attr_reader :key + attr_reader :display_name + attr_reader :display_version + attr_reader :uninstall_string + attr_reader :data + + private + + UNINSTALL_SUBKEY = 'Software\Microsoft\Windows\CurrentVersion\Uninstall'.freeze + end + end + end + end +end diff --git a/lib/chef/provider/package/yum.rb b/lib/chef/provider/package/yum.rb index 49c6f6beb5..c3fd3f69ec 100644 --- a/lib/chef/provider/package/yum.rb +++ b/lib/chef/provider/package/yum.rb @@ -1,6 +1,6 @@ -# + # Author:: Adam Jacob (<adam@opscode.com>) -# Copyright:: Copyright (c) 2008 Opscode, Inc. +# Copyright:: Copyright (c) 2008-2015 Chef Software, Inc. # License:: Apache License, Version 2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -16,18 +16,19 @@ # limitations under the License. # -require 'chef/config' -require 'chef/provider/package' -require 'chef/mixin/shell_out' -require 'chef/resource/package' -require 'singleton' -require 'chef/mixin/get_source_from_package' +require "chef/config" +require "chef/provider/package" +require "chef/mixin/which" +require "chef/resource/package" +require "singleton" +require "chef/mixin/get_source_from_package" class Chef class Provider class Package class Yum < Chef::Provider::Package + provides :package, platform_family: %w{rhel fedora} provides :yum_package, os: "linux" class RPMUtils @@ -646,10 +647,12 @@ class Chef # Cache for our installed and available packages, pulled in from yum-dump.py class YumCache - include Chef::Mixin::Command + include Chef::Mixin::Which include Chef::Mixin::ShellOut include Singleton + attr_accessor :yum_binary + def initialize @rpmdb = RPMDb.new @@ -709,11 +712,11 @@ class Chef one_line = false error = nil - helper = ::File.join(::File.dirname(__FILE__), 'yum-dump.py') + helper = ::File.join(::File.dirname(__FILE__), "yum-dump.py") status = nil begin - status = shell_out!("/usr/bin/python #{helper}#{opts}", :timeout => Chef::Config[:yum_timeout]) + status = shell_out!("#{python_bin} #{helper}#{opts}", :timeout => Chef::Config[:yum_timeout]) status.stdout.each_line do |line| one_line = true @@ -779,6 +782,42 @@ class Chef @next_refresh = :none end + def python_bin + yum_executable = which(yum_binary) + if yum_executable && shabang?(yum_executable) + shabang_or_fallback(extract_interpreter(yum_executable)) + else + Chef::Log.warn("Yum executable not found or doesn't start with #!. Using default python.") + "/usr/bin/python" + end + rescue StandardError => e + Chef::Log.warn("An error occurred attempting to determine correct python executable. Using default.") + Chef::Log.debug(e) + "/usr/bin/python" + end + + def extract_interpreter(file) + ::File.open(file, "r", &:readline)[2..-1].strip + end + + # dnf based systems have a yum shim that has /bin/bash as the interpreter. Don't use this. + def shabang_or_fallback(interpreter) + if interpreter == "/bin/bash" + Chef::Log.warn("Yum executable interpreter is /bin/bash. Falling back to default python.") + "/usr/bin/python" + else + interpreter + end + end + + def shabang?(file) + ::File.open(file, "r") do |f| + f.read(2) == '#!' + end + rescue Errno::ENOENT + false + end + def reload @next_refresh = :all end @@ -953,11 +992,31 @@ class Chef super @yum = YumCache.instance + @yum.yum_binary = yum_binary + end + + def yum_binary + @yum_binary ||= + begin + yum_binary = new_resource.yum_binary if new_resource.is_a?(Chef::Resource::YumPackage) + yum_binary ||= ::File.exist?("/usr/bin/yum-deprecated") ? "yum-deprecated" : "yum" + end end # Extra attributes # + def arch_for_name(n) + if @new_resource.respond_to?("arch") + @new_resource.arch + elsif @arch + idx = package_name_array.index(n) + as_array(@arch)[idx] + else + nil + end + end + def arch if @new_resource.respond_to?("arch") @new_resource.arch @@ -966,6 +1025,12 @@ class Chef end end + def set_arch(arch) + if @new_resource.respond_to?("arch") + @new_resource.arch(arch) + end + end + def flush_cache if @new_resource.respond_to?("flush_cache") @new_resource.flush_cache @@ -977,12 +1042,14 @@ class Chef # Helpers # - def yum_arch + def yum_arch(arch) arch ? ".#{arch}" : nil end def yum_command(command) - status = shell_out(command, {:timeout => Chef::Config[:yum_timeout]}) + command = "#{yum_binary} #{command}" + Chef::Log.debug("#{@new_resource}: yum command: \"#{command}\"") + status = shell_out_with_timeout(command, {:timeout => Chef::Config[:yum_timeout]}) # This is fun: rpm can encounter errors in the %post/%postun scripts which aren't # considered fatal - meaning the rpm is still successfully installed. These issue @@ -999,7 +1066,7 @@ class Chef if l =~ %r{^error: %(post|postun)\(.*\) scriptlet failed, exit status \d+$} Chef::Log.warn("#{@new_resource} caught non-fatal scriptlet issue: \"#{l}\". Can't trust yum exit status " + "so running install again to verify.") - status = shell_out(command, {:timeout => Chef::Config[:yum_timeout]}) + status = shell_out_with_timeout(command, {:timeout => Chef::Config[:yum_timeout]}) break end end @@ -1059,23 +1126,20 @@ class Chef end end - # Don't overwrite an existing arch - unless arch - parse_arch - end @current_resource = Chef::Resource::Package.new(@new_resource.name) @current_resource.package_name(@new_resource.package_name) installed_version = [] @candidate_version = [] + @arch = [] if @new_resource.source unless ::File.exists?(@new_resource.source) raise Chef::Exceptions::Package, "Package #{@new_resource.name} not found: #{@new_resource.source}" end Chef::Log.debug("#{@new_resource} checking rpm status") - shell_out!("rpm -qp --queryformat '%{NAME} %{VERSION}-%{RELEASE}\n' #{@new_resource.source}", :timeout => Chef::Config[:yum_timeout]).stdout.each_line do |line| + shell_out_with_timeout!("rpm -qp --queryformat '%{NAME} %{VERSION}-%{RELEASE}\n' #{@new_resource.source}", :timeout => Chef::Config[:yum_timeout]).stdout.each_line do |line| case line when /([\w\d_.-]+)\s([\w\d_.-]+)/ @current_resource.package_name($1) @@ -1085,24 +1149,43 @@ class Chef @candidate_version << @new_resource.version installed_version << @yum.installed_version(@current_resource.package_name, arch) else - if @new_resource.version - new_resource = "#{@new_resource.package_name}-#{@new_resource.version}#{yum_arch}" - else - new_resource = "#{@new_resource.package_name}#{yum_arch}" - end - Chef::Log.debug("#{@new_resource} checking yum info for #{new_resource}") + package_name_array.each_with_index do |pkg, idx| + # Don't overwrite an existing arch + if arch + name, parch = pkg, arch + else + name, parch = parse_arch(pkg) + # if we parsed an arch from the name, update the name + # to be just the package name. + if parch + if @new_resource.package_name.is_a?(Array) + @new_resource.package_name[idx] = name + else + @new_resource.package_name(name) + # only set the arch if it's a single package + set_arch(parch) + end + end + end - package_name_array.each do |pkg| - installed_version << @yum.installed_version(pkg, arch) - @candidate_version << @yum.candidate_version(pkg, arch) + if @new_resource.version + new_resource = + "#{@new_resource.package_name}-#{@new_resource.version}#{yum_arch(parch)}" + else + new_resource = "#{@new_resource.package_name}#{yum_arch(parch)}" + end + Chef::Log.debug("#{@new_resource} checking yum info for #{new_resource}") + installed_version << @yum.installed_version(name, parch) + @candidate_version << @yum.candidate_version(name, parch) + @arch << parch end - end if installed_version.size == 1 @current_resource.version(installed_version[0]) @candidate_version = @candidate_version[0] + @arch = @arch[0] else @current_resource.version(installed_version) end @@ -1117,7 +1200,7 @@ class Chef # Work around yum not exiting with an error if a package doesn't exist # for CHEF-2062 all_avail = as_array(name).zip(as_array(version)).any? do |n, v| - @yum.version_available?(n, v, arch) + @yum.version_available?(n, v, arch_for_name(n)) end method = log_method = nil methods = [] @@ -1159,20 +1242,20 @@ class Chef repos = [] pkg_string_bits = [] - index = 0 as_array(name).zip(as_array(version)).each do |n, v| - s = '' - unless v == current_version_array[index] - s = "#{n}-#{v}#{yum_arch}" - repo = @yum.package_repository(n, v, arch) + idx = package_name_array.index(n) + a = arch_for_name(n) + s = "" + unless v == current_version_array[idx] + s = "#{n}-#{v}#{yum_arch(a)}" + repo = @yum.package_repository(n, v, a) repos << "#{s} from #{repo} repository" pkg_string_bits << s end - index += 1 end - pkg_string = pkg_string_bits.join(' ') + pkg_string = pkg_string_bits.join(" ") Chef::Log.info("#{@new_resource} #{log_method} #{repos.join(' ')}") - yum_command("yum -d0 -e0 -y#{expand_options(@new_resource.options)} #{method} #{pkg_string}") + yum_command("-d0 -e0 -y#{expand_options(@new_resource.options)} #{method} #{pkg_string}") else raise Chef::Exceptions::Package, "Version #{version} of #{name} not found. Did you specify both version " + "and release? (version-release, e.g. 1.84-10.fc6)" @@ -1181,7 +1264,7 @@ class Chef def install_package(name, version) if @new_resource.source - yum_command("yum -d0 -e0 -y#{expand_options(@new_resource.options)} localinstall #{@new_resource.source}") + yum_command("-d0 -e0 -y#{expand_options(@new_resource.options)} localinstall #{@new_resource.source}") else install_remote_package(name, version) end @@ -1219,13 +1302,17 @@ class Chef def remove_package(name, version) if version - remove_str = as_array(name).zip(as_array(version)).map do |x| - "#{x.join('-')}#{yum_arch}" - end.join(' ') + remove_str = as_array(name).zip(as_array(version)).map do |n, v| + a = arch_for_name(n) + "#{[n, v].join('-')}#{yum_arch(a)}" + end.join(" ") else - remove_str = as_array(name).map { |n| "#{n}#{yum_arch}" }.join(' ') + remove_str = as_array(name).map do |n| + a = arch_for_name(n) + "#{n}#{yum_arch(a)}" + end.join(" ") end - yum_command("yum -d0 -e0 -y#{expand_options(@new_resource.options)} remove #{remove_str}") + yum_command("-d0 -e0 -y#{expand_options(@new_resource.options)} remove #{remove_str}") if flush_cache[:after] @yum.reload @@ -1240,22 +1327,26 @@ class Chef private - def parse_arch + def parse_arch(package_name) # Allow for foo.x86_64 style package_name like yum uses in it's output # - if @new_resource.package_name =~ %r{^(.*)\.(.*)$} + if package_name =~ %r{^(.*)\.(.*)$} new_package_name = $1 new_arch = $2 # foo.i386 and foo.beta1 are both valid package names or expressions of an arch. # Ensure we don't have an existing package matching package_name, then ensure we at # least have a match for the new_package+new_arch before we overwrite. If neither # then fall through to standard package handling. - if (@yum.installed_version(@new_resource.package_name).nil? and @yum.candidate_version(@new_resource.package_name).nil?) and - (@yum.installed_version(new_package_name, new_arch) or @yum.candidate_version(new_package_name, new_arch)) - @new_resource.package_name(new_package_name) - @new_resource.arch(new_arch) + old_installed = @yum.installed_version(package_name) + old_candidate = @yum.candidate_version(package_name) + new_installed = @yum.installed_version(new_package_name, new_arch) + new_candidate = @yum.candidate_version(new_package_name, new_arch) + if (old_installed.nil? and old_candidate.nil?) and (new_installed or new_candidate) + Chef::Log.debug("Parsed out arch #{new_arch}, new package name is #{new_package_name}") + return new_package_name, new_arch end end + return package_name, nil end # If we don't have the package we could have been passed a 'whatprovides' feature @@ -1300,7 +1391,7 @@ class Chef new_package_name = packages.first.name new_package_version = packages.first.version.to_s debug_msg = "#{name}: Unable to match package '#{name}' but matched #{packages.size} " - debug_msg << packages.size == 1 ? "package" : "packages" + debug_msg << (packages.size == 1 ? "package" : "packages") debug_msg << ", selected '#{new_package_name}' version '#{new_package_version}'" Chef::Log.debug(debug_msg) diff --git a/lib/chef/provider/package/zypper.rb b/lib/chef/provider/package/zypper.rb index 2cd321660b..9b0aaf322a 100644 --- a/lib/chef/provider/package/zypper.rb +++ b/lib/chef/provider/package/zypper.rb @@ -19,56 +19,58 @@ # limitations under the License. # -require 'chef/provider/package' -require 'chef/mixin/command' -require 'chef/resource/package' -require 'singleton' +require "chef/provider/package" +require "chef/mixin/command" +require "chef/resource/package" +require "singleton" class Chef class Provider class Package class Zypper < Chef::Provider::Package + provides :package, platform_family: "suse" + provides :zypper_package, os: "linux" + def load_current_resource - @current_resource = Chef::Resource::Package.new(@new_resource.name) - @current_resource.package_name(@new_resource.package_name) + @current_resource = Chef::Resource::ZypperPackage.new(new_resource.name) + current_resource.package_name(new_resource.package_name) is_installed=false is_out_of_date=false - version='' - oud_version='' - Chef::Log.debug("#{@new_resource} checking zypper") - status = shell_out("zypper --non-interactive info #{@new_resource.package_name}") + version="" + oud_version="" + Chef::Log.debug("#{new_resource} checking zypper") + status = shell_out_with_timeout("zypper --non-interactive info #{new_resource.package_name}") status.stdout.each_line do |line| case line when /^Version: (.+)$/ version = $1 - Chef::Log.debug("#{@new_resource} version #{$1}") + Chef::Log.debug("#{new_resource} version #{$1}") when /^Installed: Yes$/ is_installed=true - Chef::Log.debug("#{@new_resource} is installed") + Chef::Log.debug("#{new_resource} is installed") when /^Installed: No$/ is_installed=false - Chef::Log.debug("#{@new_resource} is not installed") + Chef::Log.debug("#{new_resource} is not installed") when /^Status: out-of-date \(version (.+) installed\)$/ is_out_of_date=true oud_version=$1 - Chef::Log.debug("#{@new_resource} out of date version #{$1}") + Chef::Log.debug("#{new_resource} out of date version #{$1}") end end if is_installed==false @candidate_version=version - @current_resource.version(nil) end if is_installed==true if is_out_of_date==true - @current_resource.version(oud_version) + current_resource.version(oud_version) @candidate_version=version else - @current_resource.version(version) + current_resource.version(version) @candidate_version=version end end @@ -77,7 +79,7 @@ class Chef raise Chef::Exceptions::Package, "zypper failed - #{status.inspect}!" end - @current_resource + current_resource end def zypper_version() @@ -104,9 +106,9 @@ class Chef def zypper_package(command, pkgname, version) version = "=#{version}" unless version.nil? || version.empty? if zypper_version < 1.0 - shell_out!("zypper#{gpg_checks} #{command} -y #{pkgname}") + shell_out_with_timeout!("zypper#{gpg_checks} #{command} -y #{pkgname}") else - shell_out!("zypper --non-interactive#{gpg_checks} "+ + shell_out_with_timeout!("zypper --non-interactive#{gpg_checks} "+ "#{command} #{pkgname}#{version}") end end diff --git a/lib/chef/provider/powershell_script.rb b/lib/chef/provider/powershell_script.rb index f9dcd6d80c..d3b586e75d 100644 --- a/lib/chef/provider/powershell_script.rb +++ b/lib/chef/provider/powershell_script.rb @@ -16,7 +16,8 @@ # limitations under the License. # -require 'chef/provider/windows_script' +require "chef/platform/query_helpers" +require "chef/provider/windows_script" class Chef class Provider @@ -24,71 +25,191 @@ class Chef provides :powershell_script, os: "windows" + def initialize (new_resource, run_context) + super(new_resource, run_context, ".ps1") + add_exit_status_wrapper + end + + def action_run + validate_script_syntax! + super + end + + def command + basepath = is_forced_32bit ? wow64_directory : run_context.node.kernel.os_info.system_directory + + # Powershell.exe is always in "v1.0" folder (for backwards compatibility) + interpreter_path = Chef::Util::PathHelper.join(basepath, "WindowsPowerShell", "v1.0", interpreter) + + # Must use -File rather than -Command to launch the script + # file created by the base class that contains the script + # code -- otherwise, powershell.exe does not propagate the + # error status of a failed Windows process that ran at the + # end of the script, it gets changed to '1'. + # + # Nano only supports -Command + cmd = "\"#{interpreter_path}\" #{flags}" + if Chef::Platform.windows_nano_server? + cmd << " -Command \". '#{script_file.path}'\"" + else + cmd << " -File \"#{script_file.path}\"" + end + cmd + end + + def flags + interpreter_flags = [*default_interpreter_flags].join(" ") + + if ! (@new_resource.flags.nil?) + interpreter_flags = [@new_resource.flags, interpreter_flags].join(" ") + end + + interpreter_flags + end + protected - EXIT_STATUS_EXCEPTION_HANDLER = "\ntrap [Exception] {write-error -exception ($_.Exception.Message);exit 1}".freeze - EXIT_STATUS_NORMALIZATION_SCRIPT = "\nif ($? -ne $true) { if ( $LASTEXITCODE ) {exit $LASTEXITCODE} else { exit 1 }}".freeze - EXIT_STATUS_RESET_SCRIPT = "\n$global:LASTEXITCODE=$null".freeze - # Process exit codes are strange with PowerShell. Unless you - # explicitly call exit in Powershell, the powershell.exe - # interpreter returns only 0 for success or 1 for failure. Since - # we'd like to get specific exit codes from executable tools run - # with Powershell, we do some work using the automatic variables - # $? and $LASTEXITCODE to return the process exit code of the - # last process run in the script if it is the last command - # executed, otherwise 0 or 1 based on whether $? is set to true - # (success, where we return 0) or false (where we return 1). - def normalize_script_exit_status( code ) - target_code = ( EXIT_STATUS_EXCEPTION_HANDLER + - EXIT_STATUS_RESET_SCRIPT + - "\n" + - code.to_s + - EXIT_STATUS_NORMALIZATION_SCRIPT ) - convert_boolean_return = @new_resource.convert_boolean_return - self.code = <<EOH -new-variable -name interpolatedexitcode -visibility private -value $#{convert_boolean_return} -new-variable -name chefscriptresult -visibility private -$chefscriptresult = { -#{target_code} -}.invokereturnasis() -if ($interpolatedexitcode -and $chefscriptresult.gettype().name -eq 'boolean') { exit [int32](!$chefscriptresult) } else { exit 0 } -EOH - Chef::Log.debug("powershell_script provider called with script code:\n\n#{code}\n") + # Process exit codes are strange with PowerShell and require + # special handling to cover common use cases. + def add_exit_status_wrapper + self.code = wrapper_script + Chef::Log.debug("powershell_script provider called with script code:\n\n#{@new_resource.code}\n") Chef::Log.debug("powershell_script provider will execute transformed code:\n\n#{self.code}\n") end - public + def validate_script_syntax! + interpreter_arguments = default_interpreter_flags.join(" ") + Tempfile.open(["chef_powershell_script-user-code", ".ps1"]) do | user_script_file | + # Wrap the user's code in a PowerShell script block so that + # it isn't executed. However, syntactically invalid script + # in that block will still trigger a syntax error which is + # exactly what we want here -- verify the syntax without + # actually running the script. + user_code_wrapped_in_powershell_script_block = <<-EOH +{ + #{@new_resource.code} +} +EOH + user_script_file.puts user_code_wrapped_in_powershell_script_block - def initialize (new_resource, run_context) - super(new_resource, run_context, '.ps1') - normalize_script_exit_status(new_resource.code) + # A .close or explicit .flush required to ensure the file is + # written to the file system at this point, which is required since + # the intent is to execute the code just written to it. + user_script_file.close + validation_command = "\"#{interpreter}\" #{interpreter_arguments} -Command \". '#{user_script_file.path}'\"" + + # Note that other script providers like bash allow syntax errors + # to be suppressed by setting 'returns' to a value that the + # interpreter would return as a status code in the syntax + # error case. We explicitly don't do this here -- syntax + # errors will not be suppressed, since doing so could make + # it harder for users to detect / debug invalid scripts. + + # Therefore, the only return value for a syntactically valid + # script is 0. If an exception is raised by shellout, this + # means a non-zero return and thus a syntactically invalid script. + + with_os_architecture(node, architecture: new_resource.architecture) do + shell_out!(validation_command, {returns: [0]}) + end + end end - def flags - default_flags = [ + def default_interpreter_flags + return [] if Chef::Platform.windows_nano_server? + + # Execution policy 'Bypass' is preferable since it doesn't require + # user input confirmation for files such as PowerShell modules + # downloaded from the Internet. However, 'Bypass' is not supported + # prior to PowerShell 3.0, so the fallback is 'Unrestricted' + execution_policy = Chef::Platform.supports_powershell_execution_bypass?(run_context.node) ? "Bypass" : "Unrestricted" + + [ "-NoLogo", "-NonInteractive", "-NoProfile", - "-ExecutionPolicy Unrestricted", + "-ExecutionPolicy #{execution_policy}", # Powershell will hang if STDIN is redirected # http://connect.microsoft.com/PowerShell/feedback/details/572313/powershell-exe-can-hang-if-stdin-is-redirected "-InputFormat None", - # Must use -File rather than -Command to launch the script - # file created by the base class that contains the script - # code -- otherwise, powershell.exe does not propagate the - # error status of a failed Windows process that ran at the - # end of the script, it gets changed to '1'. - "-File" ] + end - interpreter_flags = default_flags.join(' ') + # A wrapper script is used to launch user-supplied script while + # still obtaining useful process exit codes. Unless you + # explicitly call exit in Powershell, the powershell.exe + # interpreter returns only 0 for success or 1 for failure. Since + # we'd like to get specific exit codes from executable tools run + # with Powershell, we do some work using the automatic variables + # $? and $LASTEXITCODE to return the process exit code of the + # last process run in the script if it is the last command + # executed, otherwise 0 or 1 based on whether $? is set to true + # (success, where we return 0) or false (where we return 1). + def wrapper_script +<<-EOH +# Chef Client wrapper for powershell_script resources - if ! (@new_resource.flags.nil?) - interpreter_flags = [@new_resource.flags, interpreter_flags].join(' ') - end +# LASTEXITCODE can be uninitialized -- make it explictly 0 +# to avoid incorrect detection of failure (non-zero) codes +$global:LASTEXITCODE = 0 - interpreter_flags +# Catch any exceptions -- without this, exceptions will result +# In a zero return code instead of the desired non-zero code +# that indicates a failure +trap [Exception] {write-error ($_.Exception.Message);exit 1} + +# Variable state that should not be accessible to the user code +new-variable -name interpolatedexitcode -visibility private -value $#{@new_resource.convert_boolean_return} +new-variable -name chefscriptresult -visibility private + +# Initialize a variable we use to capture $? inside a block +$global:lastcmdlet = $null + +# Execute the user's code in a script block -- +$chefscriptresult = +{ + #{@new_resource.code} + + # This assignment doesn't affect the block's return value + $global:lastcmdlet = $? +}.invokereturnasis() + +# Assume failure status of 1 -- success cases +# will have to override this +$exitstatus = 1 + +# If convert_boolean_return is enabled, the block's return value +# gets precedence in determining our exit status +if ($interpolatedexitcode -and $chefscriptresult -ne $null -and $chefscriptresult.gettype().name -eq 'boolean') +{ + $exitstatus = [int32](!$chefscriptresult) +} +elseif ($lastcmdlet) +{ + # Otherwise, a successful cmdlet execution defines the status + $exitstatus = 0 +} +elseif ( $LASTEXITCODE -ne $null -and $LASTEXITCODE -ne 0 ) +{ + # If the cmdlet status is failed, allow the Win32 status + # in $LASTEXITCODE to define exit status. This handles the case + # where no cmdlets, only Win32 processes have run since $? + # will be set to $false whenever a Win32 process returns a non-zero + # status. + $exitstatus = $LASTEXITCODE +} + +# Print STDOUT for the script execution +Write-Output $chefscriptresult + +# If this script is launched with -File, the process exit +# status of PowerShell.exe will be $exitstatus. If it was +# launched with -Command, it will be 0 if $exitstatus was 0, +# 1 (i.e. failed) otherwise. +exit $exitstatus +EOH end + end end end diff --git a/lib/chef/provider/reboot.rb b/lib/chef/provider/reboot.rb index 8dde4653ec..3f64955d21 100644 --- a/lib/chef/provider/reboot.rb +++ b/lib/chef/provider/reboot.rb @@ -16,12 +16,13 @@ # limitations under the License. # -require 'chef/log' -require 'chef/provider' +require "chef/log" +require "chef/provider" class Chef class Provider class Reboot < Chef::Provider + provides :reboot def whyrun_supported? true @@ -39,7 +40,7 @@ class Chef :delay_mins => @new_resource.delay_mins, :reason => @new_resource.reason, :timestamp => Time.now, - :requested_by => @new_resource.name + :requested_by => @new_resource.name, ) end diff --git a/lib/chef/provider/registry_key.rb b/lib/chef/provider/registry_key.rb index 94f4e2655b..c6a06e0974 100644 --- a/lib/chef/provider/registry_key.rb +++ b/lib/chef/provider/registry_key.rb @@ -17,20 +17,22 @@ # limitations under the License. # -require 'chef/config' -require 'chef/log' -require 'chef/resource/file' -require 'chef/mixin/checksum' -require 'chef/provider' -require 'etc' -require 'fileutils' -require 'chef/scan_access_control' -require 'chef/win32/registry' +require "chef/config" +require "chef/log" +require "chef/resource/file" +require "chef/mixin/checksum" +require "chef/provider" +require "etc" +require "fileutils" +require "chef/scan_access_control" +require "chef/win32/registry" class Chef class Provider class RegistryKey < Chef::Provider + provides :registry_key + include Chef::Mixin::Checksum def whyrun_supported? @@ -62,7 +64,7 @@ class Chef def values_to_hash(values) if values - @name_hash = Hash[values.map { |val| [val[:name], val] }] + @name_hash = Hash[values.map { |val| [val[:name].downcase, val] }] else @name_hash = {} end @@ -98,8 +100,8 @@ class Chef end end @new_resource.unscrubbed_values.each do |value| - if @name_hash.has_key?(value[:name]) - current_value = @name_hash[value[:name]] + if @name_hash.has_key?(value[:name].downcase) + current_value = @name_hash[value[:name].downcase] unless current_value[:type] == value[:type] && current_value[:data] == value[:data] converge_by("set value #{value}") do registry.set_value(@new_resource.key, value) @@ -120,7 +122,7 @@ class Chef end end @new_resource.unscrubbed_values.each do |value| - unless @name_hash.has_key?(value[:name]) + unless @name_hash.has_key?(value[:name].downcase) converge_by("create value #{value}") do registry.set_value(@new_resource.key, value) end @@ -131,7 +133,7 @@ class Chef def action_delete if registry.key_exists?(@new_resource.key) @new_resource.unscrubbed_values.each do |value| - if @name_hash.has_key?(value[:name]) + if @name_hash.has_key?(value[:name].downcase) converge_by("delete value #{value}") do registry.delete_value(@new_resource.key, value) end diff --git a/lib/chef/provider/remote_directory.rb b/lib/chef/provider/remote_directory.rb index eaccce46cf..02270201cb 100644 --- a/lib/chef/provider/remote_directory.rb +++ b/lib/chef/provider/remote_directory.rb @@ -1,6 +1,6 @@ # # Author:: Adam Jacob (<adam@opscode.com>) -# Copyright:: Copyright (c) 2008 Opscode, Inc. +# Copyright:: Copyright (c) 2008-2015 Chef Software, Inc. # License:: Apache License, Version 2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -16,178 +16,266 @@ # limitations under the License. # -require 'chef/provider/file' -require 'chef/provider/directory' -require 'chef/resource/directory' -require 'chef/resource/remote_file' -require 'chef/mixin/file_class' -require 'chef/platform' -require 'uri' -require 'tempfile' -require 'net/https' -require 'set' -require 'chef/util/path_helper' +require "chef/provider/directory" +require "chef/resource/file" +require "chef/resource/directory" +require "chef/resource/cookbook_file" +require "chef/mixin/file_class" +require "chef/platform/query_helpers" +require "chef/util/path_helper" +require "chef/deprecation/warnings" +require "chef/deprecation/provider/remote_directory" + +require "forwardable" class Chef class Provider class RemoteDirectory < Chef::Provider::Directory + extend Forwardable + include Chef::Mixin::FileClass provides :remote_directory - include Chef::Mixin::FileClass + def_delegators :@new_resource, :purge, :path, :source, :cookbook, :cookbook_name + def_delegators :@new_resource, :files_rights, :files_mode, :files_group, :files_owner, :files_backup + def_delegators :@new_resource, :rights, :mode, :group, :owner + + # The overwrite property on the resource. Delegates to new_resource but can be mutated. + # + # @return [Boolean] if we are overwriting + # + def overwrite? + @overwrite = new_resource.overwrite if @overwrite.nil? + !!@overwrite + end + + attr_accessor :managed_files + + # Hash containing keys of the paths for all the files that we sync, plus all their + # parent directories. + # + # @return [Set] Ruby Set of the files that we manage + # + def managed_files + @managed_files ||= Set.new + end + # Handle action :create. + # def action_create super - # Mark all files as needing to be purged - files_to_purge = Set.new(ls(@new_resource.path)) # Make sure each path is clean # Transfer files files_to_transfer.each do |cookbook_file_relative_path| create_cookbook_file(cookbook_file_relative_path) - # parent directories and file being transferred are removed from the purge list - Pathname.new(Chef::Util::PathHelper.cleanpath(::File.join(@new_resource.path, cookbook_file_relative_path))).descend do |d| - files_to_purge.delete(d.to_s) - end + # parent directories and file being transferred need to not be removed in the purge + add_managed_file(cookbook_file_relative_path) end - purge_unmanaged_files(files_to_purge) + purge_unmanaged_files end + # Handle action :create_if_missing. + # def action_create_if_missing # if this action is called, ignore the existing overwrite flag - @new_resource.overwrite(false) + @overwrite = false action_create end - protected + private - # List all excluding . and .. - def ls(path) - files = Dir.glob(::File.join(Chef::Util::PathHelper.escape_glob(path), '**', '*'), - ::File::FNM_DOTMATCH) - - # Remove current directory and previous directory - files.reject! do |name| - basename = Pathname.new(name).basename().to_s - ['.', '..'].include?(basename) + # Add a file and its parent directories to the managed_files Hash. + # + # @param [String] cookbook_file_relative_path relative path to the file + # @api private + # + def add_managed_file(cookbook_file_relative_path) + if purge + Pathname.new(Chef::Util::PathHelper.cleanpath(::File.join(path, cookbook_file_relative_path))).descend do |d| + managed_files.add(d.to_s) + end end - - # Clean all the paths... this is required because of the join - files.map {|f| Chef::Util::PathHelper.cleanpath(f)} end - def purge_unmanaged_files(unmanaged_files) - if @new_resource.purge - unmanaged_files.sort.reverse.each do |f| - # file_class comes from Chef::Mixin::FileClass - if ::File.directory?(f) && !Chef::Platform.windows? && !file_class.symlink?(f.dup) - # Linux treats directory symlinks as files - # Remove a directory as a directory when not on windows if it is not a symlink - purge_directory(f) - elsif ::File.directory?(f) && Chef::Platform.windows? - # Windows treats directory symlinks as directories so we delete them here - purge_directory(f) - else - converge_by("delete unmanaged file #{f}") do - ::File.delete(f) - Chef::Log.debug("#{@new_resource} deleted file #{f}") + # Remove all files not in the managed_files Set. + # + # @api private + # + def purge_unmanaged_files + if purge + Dir.glob(::File.join(Chef::Util::PathHelper.escape_glob(path), "**", "*"), ::File::FNM_DOTMATCH).sort!.reverse!.each do |file| + # skip '.' and '..' + next if [".",".."].include?(Pathname.new(file).basename().to_s) + + # Clean the path. This is required because of the ::File.join + file = Chef::Util::PathHelper.cleanpath(file) + + # Skip files that we've sync'd and their parent dirs + next if managed_files.include?(file) + + if ::File.directory?(file) + if !Chef::Platform.windows? && file_class.symlink?(file.dup) + # Unix treats dir symlinks as files + purge_file(file) + else + # Unix dirs are dirs, Windows dirs and dir symlinks are dirs + purge_directory(file) end + else + purge_file(file) end end end end + # Use a Chef directory sub-resource to remove a directory. + # + # @param [String] dir The path of the directory to remove + # @api private + # def purge_directory(dir) - converge_by("delete unmanaged directory #{dir}") do - Dir::rmdir(dir) - Chef::Log.debug("#{@new_resource} removed directory #{dir}") - end + res = Chef::Resource::Directory.new(dir, run_context) + res.run_action(:delete) + new_resource.updated_by_last_action(true) if res.updated? end + # Use a Chef file sub-resource to remove a file. + # + # @param [String] file The path of the file to remove + # @api private + # + def purge_file(file) + res = Chef::Resource::File.new(file, run_context) + res.run_action(:delete) + new_resource.updated_by_last_action(true) if res.updated? + end + + # Get the files to tranfer. This returns files in lexicographical sort order. + # + # FIXME: it should do breadth-first, see CHEF-5080 (please use a performant sort) + # + # @return Array<String> The list of files to transfer + # @api private + # def files_to_transfer cookbook = run_context.cookbook_collection[resource_cookbook] - files = cookbook.relative_filenames_in_preferred_directory(node, :files, @new_resource.source) - files.sort.reverse + files = cookbook.relative_filenames_in_preferred_directory(node, :files, source) + files.sort_by! { |x| x.count(::File::SEPARATOR) } end - def directory_root_in_cookbook_cache - @directory_root_in_cookbook_cache ||= begin - cookbook = run_context.cookbook_collection[resource_cookbook] - cookbook.preferred_filename_on_disk_location(node, :files, @new_resource.source, @new_resource.path) - end + # Either the explicit cookbook that the user sets on the resource, or the implicit + # cookbook_name that the resource was declared in. + # + # @return [String] Cookbook to get file from. + # @api private + # + def resource_cookbook + cookbook || cookbook_name end - # Determine the cookbook to get the file from. If new resource sets an - # explicit cookbook, use it, otherwise fall back to the implicit cookbook - # i.e., the cookbook the resource was declared in. - def resource_cookbook - @new_resource.cookbook || @new_resource.cookbook_name + # If we are overwriting, then cookbook_file sub-resources should all be action :create, + # otherwise they should be :create_if_missing + # + # @return [Symbol] Action to take on cookbook_file sub-resources + # @api private + # + def action_for_cookbook_file + overwrite? ? :create : :create_if_missing end + # This creates and uses a cookbook_file resource to sync a single file from the cookbook. + # + # @param [String] cookbook_file_relative_path The relative path to the cookbook file + # @api private + # def create_cookbook_file(cookbook_file_relative_path) - full_path = ::File.join(@new_resource.path, cookbook_file_relative_path) + full_path = ::File.join(path, cookbook_file_relative_path) ensure_directory_exists(::File.dirname(full_path)) - file_to_fetch = cookbook_file_resource(full_path, cookbook_file_relative_path) - if @new_resource.overwrite - file_to_fetch.run_action(:create) - else - file_to_fetch.run_action(:create_if_missing) - end - @new_resource.updated_by_last_action(true) if file_to_fetch.updated? + res = cookbook_file_resource(full_path, cookbook_file_relative_path) + res.run_action(action_for_cookbook_file) + new_resource.updated_by_last_action(true) if res.updated? end + # This creates the cookbook_file resource for use by create_cookbook_file. + # + # @param [String] target_path Path on the system to create + # @param [String] relative_source_path Relative path in the cookbook to the base source + # @return [Chef::Resource::CookbookFile] The built cookbook_file resource + # @api private + # def cookbook_file_resource(target_path, relative_source_path) - cookbook_file = Chef::Resource::CookbookFile.new(target_path, run_context) - cookbook_file.cookbook_name = @new_resource.cookbook || @new_resource.cookbook_name - cookbook_file.source(::File.join(@new_resource.source, relative_source_path)) - if Chef::Platform.windows? && @new_resource.files_rights - @new_resource.files_rights.each_pair do |permission, *args| - cookbook_file.rights(permission, *args) + res = Chef::Resource::CookbookFile.new(target_path, run_context) + res.cookbook_name = resource_cookbook + res.source(::File.join(source, relative_source_path)) + if Chef::Platform.windows? && files_rights + files_rights.each_pair do |permission, *args| + res.rights(permission, *args) end end - cookbook_file.mode(@new_resource.files_mode) if @new_resource.files_mode - cookbook_file.group(@new_resource.files_group) if @new_resource.files_group - cookbook_file.owner(@new_resource.files_owner) if @new_resource.files_owner - cookbook_file.backup(@new_resource.files_backup) if @new_resource.files_backup + res.mode(files_mode) if files_mode + res.group(files_group) if files_group + res.owner(files_owner) if files_owner + res.backup(files_backup) if files_backup - cookbook_file + res end - def ensure_directory_exists(path) - unless ::File.directory?(path) - directory_to_create = resource_for_directory(path) - directory_to_create.run_action(:create) - @new_resource.updated_by_last_action(true) if directory_to_create.updated? + # This creates and uses a directory resource to create a directory if it is needed. + # + # @param [String] dir The path to the directory to create. + # @api private + # + def ensure_directory_exists(dir) + # doing the check here and skipping the resource should be more performant + unless ::File.directory?(dir) + res = directory_resource(dir) + res.run_action(:create) + new_resource.updated_by_last_action(true) if res.updated? end end - def resource_for_directory(path) - dir = Chef::Resource::Directory.new(path, run_context) - dir.cookbook_name = @new_resource.cookbook || @new_resource.cookbook_name - if Chef::Platform.windows? && @new_resource.rights + # This creates the directory resource for ensure_directory_exists. + # + # @param [String] dir Directory path on the system + # @return [Chef::Resource::Directory] The built directory resource + # @api private + # + def directory_resource(dir) + res = Chef::Resource::Directory.new(dir, run_context) + res.cookbook_name = resource_cookbook + if Chef::Platform.windows? && rights # rights are only meant to be applied to the toppest-level directory; # Windows will handle inheritance. - if path == @new_resource.path - @new_resource.rights.each do |rights| #rights is a hash - permissions = rights.delete(:permissions) #delete will return the value or nil if not found - principals = rights.delete(:principals) - dir.rights(permissions, principals, rights) + if dir == path + rights.each do |r| + r = r.dup # do not update the new_resource + permissions = r.delete(:permissions) + principals = r.delete(:principals) + res.rights(permissions, principals, r) end end end - dir.mode(@new_resource.mode) if @new_resource.mode - dir.group(@new_resource.group) - dir.owner(@new_resource.owner) - dir.recursive(true) - dir - end + res.mode(mode) if mode + res.group(group) if group + res.owner(owner) if owner + res.recursive(true) - def whyrun_supported? - true + res end + # + # Add back deprecated methods and aliases that are internally unused and should be removed in Chef-13 + # + extend Chef::Deprecation::Warnings + include Chef::Deprecation::Provider::RemoteDirectory + add_deprecation_warnings_for(Chef::Deprecation::Provider::RemoteDirectory.instance_methods) + + alias_method :resource_for_directory, :directory_resource + add_deprecation_warnings_for([:resource_for_directory]) + end end end diff --git a/lib/chef/provider/remote_file.rb b/lib/chef/provider/remote_file.rb index da2573dacb..6548300c5d 100644 --- a/lib/chef/provider/remote_file.rb +++ b/lib/chef/provider/remote_file.rb @@ -17,13 +17,14 @@ # limitations under the License. # -require 'chef/provider/file' -require 'chef/deprecation/provider/remote_file' -require 'chef/deprecation/warnings' +require "chef/provider/file" +require "chef/deprecation/provider/remote_file" +require "chef/deprecation/warnings" class Chef class Provider class RemoteFile < Chef::Provider::File + provides :remote_file extend Chef::Deprecation::Warnings include Chef::Deprecation::Provider::RemoteFile diff --git a/lib/chef/provider/remote_file/cache_control_data.rb b/lib/chef/provider/remote_file/cache_control_data.rb index f9b729362c..85b723ece4 100644 --- a/lib/chef/provider/remote_file/cache_control_data.rb +++ b/lib/chef/provider/remote_file/cache_control_data.rb @@ -19,11 +19,11 @@ # limitations under the License. # -require 'stringio' -require 'chef/file_cache' -require 'chef/json_compat' -require 'chef/digester' -require 'chef/exceptions' +require "stringio" +require "chef/file_cache" +require "chef/json_compat" +require "chef/digester" +require "chef/exceptions" class Chef class Provider @@ -145,18 +145,51 @@ class Chef end def load_json_data - Chef::FileCache.load("remote_file/#{sanitized_cache_file_basename}") + path = sanitized_cache_file_path(sanitized_cache_file_basename) + if Chef::FileCache.has_key?(path) + Chef::FileCache.load(path) + else + old_path = sanitized_cache_file_path(sanitized_cache_file_basename_md5) + if Chef::FileCache.has_key?(old_path) + # We found an old cache control data file. We started using sha256 instead of md5 + # to name these. Upgrade the file to the new name. + Chef::Log.debug("Found old cache control data file at #{old_path}. Moving to #{path}.") + Chef::FileCache.load(old_path).tap do |data| + Chef::FileCache.store(path, data) + Chef::FileCache.delete(old_path) + end + else + raise Chef::Exceptions::FileNotFound + end + end end - def sanitized_cache_file_basename + def sanitized_cache_file_path(basename) + "remote_file/#{basename}" + end + + def scrubbed_uri # Scrub and truncate in accordance with the goals of keeping the name # human-readable but within the bounds of local file system # path length limits - scrubbed_uri = uri.gsub(/\W/, '_')[0..63] + uri.gsub(/\W/, "_")[0..63] + end + + def sanitized_cache_file_basename + uri_sha2 = Chef::Digester.instance.generate_checksum(StringIO.new(uri)) + cache_file_basename(uri_sha2[0,32]) + end + + + def sanitized_cache_file_basename_md5 + # Old way of creating the file basename uri_md5 = Chef::Digester.instance.generate_md5_checksum(StringIO.new(uri)) - "#{scrubbed_uri}-#{uri_md5}.json" + cache_file_basename(uri_md5) end + def cache_file_basename(checksum) + "#{scrubbed_uri}-#{checksum}.json" + end end end end diff --git a/lib/chef/provider/remote_file/content.rb b/lib/chef/provider/remote_file/content.rb index ef55dd77cd..02c0cff457 100644 --- a/lib/chef/provider/remote_file/content.rb +++ b/lib/chef/provider/remote_file/content.rb @@ -1,7 +1,7 @@ # # Author:: Jesse Campbell (<hikeit@gmail.com>) # Author:: Lamont Granquist (<lamont@opscode.com>) -# Copyright:: Copyright (c) 2013 Opscode, Inc. +# Copyright:: Copyright (c) 2013-2016 Chef Software, Inc. # License:: Apache License, Version 2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,9 +17,10 @@ # limitations under the License. # -require 'uri' -require 'tempfile' -require 'chef/file_content_management/content_base' +require "uri" +require "tempfile" +require "chef/file_content_management/content_base" +require "chef/mixin/uris" class Chef class Provider @@ -28,6 +29,8 @@ class Chef private + include Chef::Mixin::Uris + def file_for_provider Chef::Log.debug("#{@new_resource} checking for changes") @@ -45,10 +48,14 @@ class Chef sources = sources.dup source = sources.shift begin - uri = URI.parse(source) + uri = if Chef::Provider::RemoteFile::Fetcher.network_share?(source) + source + else + as_uri(source) + end raw_file = grab_file_from_uri(uri) rescue SocketError, Errno::ECONNREFUSED, Errno::ENOENT, Errno::EACCES, Timeout::Error, Net::HTTPServerException, Net::HTTPFatalError, Net::FTPError => e - Chef::Log.warn("#{@new_resource} cannot be downloaded from #{source}: #{e.to_s}") + Chef::Log.warn("#{@new_resource} cannot be downloaded from #{source}: #{e}") if source = sources.shift Chef::Log.info("#{@new_resource} trying to download from another mirror") retry diff --git a/lib/chef/provider/remote_file/fetcher.rb b/lib/chef/provider/remote_file/fetcher.rb index 249b29186f..53bfe9935c 100644 --- a/lib/chef/provider/remote_file/fetcher.rb +++ b/lib/chef/provider/remote_file/fetcher.rb @@ -23,15 +23,29 @@ class Chef class Fetcher def self.for_resource(uri, new_resource, current_resource) - case uri.scheme - when "http", "https" - Chef::Provider::RemoteFile::HTTP.new(uri, new_resource, current_resource) - when "ftp" - Chef::Provider::RemoteFile::FTP.new(uri, new_resource, current_resource) - when "file" - Chef::Provider::RemoteFile::LocalFile.new(uri, new_resource, current_resource) + if network_share?(uri) + Chef::Provider::RemoteFile::NetworkFile.new(uri, new_resource, current_resource) else - raise ArgumentError, "Invalid uri, Only http(s), ftp, and file are currently supported" + case uri.scheme + when "http", "https" + Chef::Provider::RemoteFile::HTTP.new(uri, new_resource, current_resource) + when "ftp" + Chef::Provider::RemoteFile::FTP.new(uri, new_resource, current_resource) + when "file" + Chef::Provider::RemoteFile::LocalFile.new(uri, new_resource, current_resource) + else + raise ArgumentError, "Invalid uri, Only http(s), ftp, and file are currently supported" + end + end + end + + # Windows network share: \\computername\share\file + def self.network_share?(source) + case source + when String + !!(%r{\A\\\\[A-Za-z0-9+\-\.]+} =~ source) + else + false end end diff --git a/lib/chef/provider/remote_file/ftp.rb b/lib/chef/provider/remote_file/ftp.rb index 3f78286aa3..a81126f27f 100644 --- a/lib/chef/provider/remote_file/ftp.rb +++ b/lib/chef/provider/remote_file/ftp.rb @@ -16,11 +16,11 @@ # limitations under the License. # -require 'uri' -require 'tempfile' -require 'net/ftp' -require 'chef/provider/remote_file' -require 'chef/file_content_management/tempfile' +require "uri" +require "tempfile" +require "net/ftp" +require "chef/provider/remote_file" +require "chef/file_content_management/tempfile" class Chef class Provider @@ -59,7 +59,7 @@ class Chef if uri.userinfo URI.unescape(uri.user) else - 'anonymous' + "anonymous" end end @@ -94,11 +94,11 @@ class Chef private def with_proxy_env - saved_socks_env = ENV['SOCKS_SERVER'] - ENV['SOCKS_SERVER'] = proxy_uri(@uri).to_s + saved_socks_env = ENV["SOCKS_SERVER"] + ENV["SOCKS_SERVER"] = proxy_uri(@uri).to_s yield ensure - ENV['SOCKS_SERVER'] = saved_socks_env + ENV["SOCKS_SERVER"] = saved_socks_env end def with_connection @@ -162,7 +162,7 @@ class Chef end def parse_path - path = uri.path.sub(%r{\A/}, '%2F') # re-encode the beginning slash because uri library decodes it. + path = uri.path.sub(%r{\A/}, "%2F") # re-encode the beginning slash because uri library decodes it. directories = path.split(%r{/}, -1) directories.each {|d| d.gsub!(/%([0-9A-Fa-f][0-9A-Fa-f])/) { [$1].pack("H2") } diff --git a/lib/chef/provider/remote_file/http.rb b/lib/chef/provider/remote_file/http.rb index f17ab5a56d..218d7ef223 100644 --- a/lib/chef/provider/remote_file/http.rb +++ b/lib/chef/provider/remote_file/http.rb @@ -17,10 +17,10 @@ # limitations under the License. # -require 'chef/http/simple' -require 'chef/digester' -require 'chef/provider/remote_file' -require 'chef/provider/remote_file/cache_control_data' +require "chef/http/simple" +require "chef/digester" +require "chef/provider/remote_file" +require "chef/provider/remote_file/cache_control_data" class Chef class Provider @@ -87,11 +87,11 @@ class Chef end def last_modified_time_from(response) - response['last_modified'] || response['date'] + response["last_modified"] || response["date"] end def etag_from(response) - response['etag'] + response["etag"] end def http_client_opts @@ -105,7 +105,7 @@ class Chef # case you'd end up with a tar archive (no gzip) named, e.g., foo.tgz, # which is not what you wanted. if uri.to_s =~ /gz$/ - Chef::Log.debug("turning gzip compression off due to filename ending in gz") + Chef::Log.debug("Turning gzip compression off due to filename ending in gz") opts[:disable_gzip] = true end opts diff --git a/lib/chef/provider/remote_file/local_file.rb b/lib/chef/provider/remote_file/local_file.rb index e78311f2c3..2e99886a00 100644 --- a/lib/chef/provider/remote_file/local_file.rb +++ b/lib/chef/provider/remote_file/local_file.rb @@ -16,9 +16,9 @@ # limitations under the License. # -require 'uri' -require 'tempfile' -require 'chef/provider/remote_file' +require "uri" +require "tempfile" +require "chef/provider/remote_file" class Chef class Provider @@ -32,15 +32,21 @@ class Chef @new_resource = new_resource @uri = uri end - + # CHEF-4472: Remove the leading slash from windows paths that we receive from a file:// URI - def fix_windows_path(path) - path.gsub(/^\/([a-zA-Z]:)/,'\1') + def fix_windows_path(path) + path.gsub(/^\/([a-zA-Z]:)/,'\1') + end + + def source_path + @source_path ||= begin + path = URI.unescape(uri.path) + Chef::Platform.windows? ? fix_windows_path(path) : path + end end # Fetches the file at uri, returning a Tempfile-like File handle def fetch - source_path = Chef::Platform.windows? ? fix_windows_path(uri.path) : uri.path tempfile = Chef::FileContentManagement::Tempfile.new(new_resource).tempfile Chef::Log.debug("#{new_resource} staging #{source_path} to #{tempfile.path}") FileUtils.cp(source_path, tempfile.path) diff --git a/lib/chef/provider/remote_file/network_file.rb b/lib/chef/provider/remote_file/network_file.rb new file mode 100644 index 0000000000..7c066cb052 --- /dev/null +++ b/lib/chef/provider/remote_file/network_file.rb @@ -0,0 +1,48 @@ +# +# Author:: Jesse Campbell (<hikeit@gmail.com>) +# Copyright:: Copyright (c) 2013 Jesse Campbell +# 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 "uri" +require "tempfile" +require "chef/provider/remote_file" + +class Chef + class Provider + class RemoteFile + class NetworkFile + + attr_reader :new_resource + + def initialize(source, new_resource, current_resource) + @new_resource = new_resource + @source = source + end + + # Fetches the file on a network share, returning a Tempfile-like File handle + # windows only + def fetch + tempfile = Chef::FileContentManagement::Tempfile.new(new_resource).tempfile + Chef::Log.debug("#{new_resource} staging #{@source} to #{tempfile.path}") + FileUtils.cp(@source, tempfile.path) + tempfile.close if tempfile + tempfile + end + + end + end + end +end diff --git a/lib/chef/provider/route.rb b/lib/chef/provider/route.rb index 72a5029a94..4abfd806b9 100644 --- a/lib/chef/provider/route.rb +++ b/lib/chef/provider/route.rb @@ -16,10 +16,10 @@ # limitations under the License. # -require 'chef/log' -require 'chef/mixin/command' -require 'chef/provider' -require 'ipaddr' +require "chef/log" +require "chef/mixin/command" +require "chef/provider" +require "ipaddr" class Chef::Provider::Route < Chef::Provider include Chef::Mixin::Command @@ -28,52 +28,52 @@ class Chef::Provider::Route < Chef::Provider attr_accessor :is_running - MASK = {'0.0.0.0' => '0', - '128.0.0.0' => '1', - '192.0.0.0' => '2', - '224.0.0.0' => '3', - '240.0.0.0' => '4', - '248.0.0.0' => '5', - '252.0.0.0' => '6', - '254.0.0.0' => '7', - '255.0.0.0' => '8', - '255.128.0.0' => '9', - '255.192.0.0' => '10', - '255.224.0.0' => '11', - '255.240.0.0' => '12', - '255.248.0.0' => '13', - '255.252.0.0' => '14', - '255.254.0.0' => '15', - '255.255.0.0' => '16', - '255.255.128.0' => '17', - '255.255.192.0' => '18', - '255.255.224.0' => '19', - '255.255.240.0' => '20', - '255.255.248.0' => '21', - '255.255.252.0' => '22', - '255.255.254.0' => '23', - '255.255.255.0' => '24', - '255.255.255.128' => '25', - '255.255.255.192' => '26', - '255.255.255.224' => '27', - '255.255.255.240' => '28', - '255.255.255.248' => '29', - '255.255.255.252' => '30', - '255.255.255.254' => '31', - '255.255.255.255' => '32' } + MASK = {"0.0.0.0" => "0", + "128.0.0.0" => "1", + "192.0.0.0" => "2", + "224.0.0.0" => "3", + "240.0.0.0" => "4", + "248.0.0.0" => "5", + "252.0.0.0" => "6", + "254.0.0.0" => "7", + "255.0.0.0" => "8", + "255.128.0.0" => "9", + "255.192.0.0" => "10", + "255.224.0.0" => "11", + "255.240.0.0" => "12", + "255.248.0.0" => "13", + "255.252.0.0" => "14", + "255.254.0.0" => "15", + "255.255.0.0" => "16", + "255.255.128.0" => "17", + "255.255.192.0" => "18", + "255.255.224.0" => "19", + "255.255.240.0" => "20", + "255.255.248.0" => "21", + "255.255.252.0" => "22", + "255.255.254.0" => "23", + "255.255.255.0" => "24", + "255.255.255.128" => "25", + "255.255.255.192" => "26", + "255.255.255.224" => "27", + "255.255.255.240" => "28", + "255.255.255.248" => "29", + "255.255.255.252" => "30", + "255.255.255.254" => "31", + "255.255.255.255" => "32" } def hex2ip(hex_data) # Cleanup hex data - hex_ip = hex_data.to_s.downcase.gsub(/[^0-9a-f]/, '') + hex_ip = hex_data.to_s.downcase.gsub(/[^0-9a-f]/, "") # Check hex data format (IP is a 32bit integer, so should be 8 chars long) return nil if hex_ip.length != hex_data.length || hex_ip.length != 8 # Extract octets from hex data - octets = hex_ip.scan(/../).reverse.collect { |octet| [octet].pack('H2').unpack("C").first } + octets = hex_ip.scan(/../).reverse.collect { |octet| [octet].pack("H2").unpack("C").first } # Validate IP - ip = octets.join('.') + ip = octets.join(".") begin IPAddr.new(ip, Socket::AF_INET).to_s rescue ArgumentError @@ -197,7 +197,7 @@ class Chef::Provider::Route < Chef::Provider end def generate_command(action) - common_route_items = '' + common_route_items = "" common_route_items << "/#{MASK[@new_resource.netmask.to_s]}" if @new_resource.netmask common_route_items << " via #{@new_resource.gateway} " if @new_resource.gateway @@ -215,7 +215,7 @@ class Chef::Provider::Route < Chef::Provider end def config_file_contents(action, options={}) - content = '' + content = "" case action when :add content << "#{options[:target]}" diff --git a/lib/chef/provider/script.rb b/lib/chef/provider/script.rb index e8b5235b7a..d95962a15b 100644 --- a/lib/chef/provider/script.rb +++ b/lib/chef/provider/script.rb @@ -16,9 +16,9 @@ # limitations under the License. # -require 'tempfile' -require 'chef/provider/execute' -require 'forwardable' +require "tempfile" +require "chef/provider/execute" +require "forwardable" class Chef class Provider @@ -27,6 +27,7 @@ class Chef provides :bash provides :csh + provides :ksh provides :perl provides :python provides :ruby diff --git a/lib/chef/provider/service.rb b/lib/chef/provider/service.rb index 75da2ddb31..6e1d0b4064 100644 --- a/lib/chef/provider/service.rb +++ b/lib/chef/provider/service.rb @@ -1,6 +1,6 @@ # # Author:: AJ Christensen (<aj@hjksolutions.com>) -# Copyright:: Copyright (c) 2008 Opscode, Inc. +# Copyright:: Copyright (c) 2008-2015 Chef Software, Inc. # License:: Apache License, Version 2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -16,8 +16,8 @@ # limitations under the License. # -require 'chef/mixin/command' -require 'chef/provider' +require "chef/mixin/command" +require "chef/provider" class Chef class Provider @@ -25,6 +25,10 @@ class Chef include Chef::Mixin::Command + def supports + @supports ||= new_resource.supports.dup + end + def initialize(new_resource, run_context) super @enabled = nil @@ -34,24 +38,33 @@ class Chef true end - def load_new_resource_state - # If the user didn't specify a change in enabled state, - # it will be the same as the old resource - if ( @new_resource.enabled.nil? ) - @new_resource.enabled(@current_resource.enabled) - end - if ( @new_resource.running.nil? ) - @new_resource.running(@current_resource.running) - end - end + def load_current_resource + supports[:status] = false if supports[:status].nil? + supports[:reload] = false if supports[:reload].nil? + supports[:restart] = false if supports[:restart].nil? + end + + # the new_resource#enabled and #running variables are not user input, but when we + # do (e.g.) action_enable we want to set new_resource.enabled so that the comparison + # between desired and current state produces the correct change in reporting. + # XXX?: the #nil? check below will likely fail if this is a cloned resource or if + # we just run multiple actions. + def load_new_resource_state + if ( @new_resource.enabled.nil? ) + @new_resource.enabled(@current_resource.enabled) + end + if ( @new_resource.running.nil? ) + @new_resource.running(@current_resource.running) + end + end def shared_resource_requirements end def define_resource_requirements requirements.assert(:reload) do |a| - a.assertion { @new_resource.supports[:reload] || @new_resource.reload_command } - a.failure_message Chef::Exceptions::UnsupportedAction, "#{self.to_s} does not support :reload" + a.assertion { supports[:reload] || @new_resource.reload_command } + a.failure_message Chef::Exceptions::UnsupportedAction, "#{self} does not support :reload" # if a service is not declared to support reload, that won't # typically change during the course of a run - so no whyrun # alternative here. @@ -130,27 +143,27 @@ class Chef end def enable_service - raise Chef::Exceptions::UnsupportedAction, "#{self.to_s} does not support :enable" + raise Chef::Exceptions::UnsupportedAction, "#{self} does not support :enable" end def disable_service - raise Chef::Exceptions::UnsupportedAction, "#{self.to_s} does not support :disable" + raise Chef::Exceptions::UnsupportedAction, "#{self} does not support :disable" end def start_service - raise Chef::Exceptions::UnsupportedAction, "#{self.to_s} does not support :start" + raise Chef::Exceptions::UnsupportedAction, "#{self} does not support :start" end def stop_service - raise Chef::Exceptions::UnsupportedAction, "#{self.to_s} does not support :stop" + raise Chef::Exceptions::UnsupportedAction, "#{self} does not support :stop" end def restart_service - raise Chef::Exceptions::UnsupportedAction, "#{self.to_s} does not support :restart" + raise Chef::Exceptions::UnsupportedAction, "#{self} does not support :restart" end def reload_service - raise Chef::Exceptions::UnsupportedAction, "#{self.to_s} does not support :reload" + raise Chef::Exceptions::UnsupportedAction, "#{self} does not support :reload" end protected @@ -168,6 +181,32 @@ class Chef @new_resource.respond_to?(method_name) && !!@new_resource.send(method_name) end + + module ServicePriorityInit + + # + # Platform-specific versions + # + + # + # Linux + # + + require "chef/chef_class" + require "chef/provider/service/systemd" + require "chef/provider/service/insserv" + require "chef/provider/service/redhat" + require "chef/provider/service/arch" + require "chef/provider/service/gentoo" + require "chef/provider/service/upstart" + require "chef/provider/service/debian" + require "chef/provider/service/invokercd" + + Chef.set_provider_priority_array :service, [ Systemd, Arch ], platform_family: "arch" + Chef.set_provider_priority_array :service, [ Systemd, Gentoo ], platform_family: "gentoo" + Chef.set_provider_priority_array :service, [ Systemd, Upstart, Insserv, Debian, Invokercd ], platform_family: "debian" + Chef.set_provider_priority_array :service, [ Systemd, Insserv, Redhat ], platform_family: %w{rhel fedora suse} + end end end end diff --git a/lib/chef/provider/service/aix.rb b/lib/chef/provider/service/aix.rb index 0aef62c62e..1968f8f3de 100644 --- a/lib/chef/provider/service/aix.rb +++ b/lib/chef/provider/service/aix.rb @@ -16,7 +16,7 @@ # limitations under the License. # -require 'chef/provider/service' +require "chef/provider/service" class Chef class Provider @@ -91,15 +91,18 @@ class Chef protected def determine_current_status! - Chef::Log.debug "#{@new_resource} using lssrc to check the status " + Chef::Log.debug "#{@new_resource} using lssrc to check the status" begin - services = shell_out!("lssrc -a | grep -w #{@new_resource.service_name}").stdout.split("\n") - is_resource_group?(services) - - if services.length == 1 && services[0].split(' ').last == "active" - @current_resource.running true - else + if is_resource_group? + # Groups as a whole have no notion of whether they're running @current_resource.running false + else + service = shell_out!("lssrc -s #{@new_resource.service_name}").stdout + if service.split(" ").last == "active" + @current_resource.running true + else + @current_resource.running false + end end Chef::Log.debug "#{@new_resource} running: #{@current_resource.running}" # ShellOut sometimes throws different types of Exceptions than ShellCommandFailed. @@ -112,11 +115,9 @@ class Chef end end - def is_resource_group? (services) - if services.length > 1 - Chef::Log.debug("#{@new_resource.service_name} is a group") - @is_resource_group = true - elsif services[0].split(' ')[1] == @new_resource.service_name + def is_resource_group? + so = shell_out("lssrc -g #{@new_resource.service_name}") + if so.exitstatus == 0 Chef::Log.debug("#{@new_resource.service_name} is a group") @is_resource_group = true end diff --git a/lib/chef/provider/service/aixinit.rb b/lib/chef/provider/service/aixinit.rb index 19beac79f0..66d85984fa 100644 --- a/lib/chef/provider/service/aixinit.rb +++ b/lib/chef/provider/service/aixinit.rb @@ -16,7 +16,7 @@ # limitations under the License. # -require 'chef/provider/service/init' +require "chef/provider/service/init" class Chef class Provider @@ -60,14 +60,14 @@ class Chef Dir.glob(["/etc/rc.d/rc2.d/[SK][0-9][0-9]#{@new_resource.service_name}", "/etc/rc.d/rc2.d/[SK]#{@new_resource.service_name}"]).each { |f| ::File.delete(f)} if @new_resource.priority.is_a? Integer - create_symlink(2, 'S', @new_resource.priority) + create_symlink(2, "S", @new_resource.priority) elsif @new_resource.priority.is_a? Hash @new_resource.priority.each do |level,o| - create_symlink(level,(o[0] == :start ? 'S' : 'K'),o[1]) + create_symlink(level,(o[0] == :start ? "S" : "K"),o[1]) end else - create_symlink(2, 'S', '') + create_symlink(2, "S", "") end end @@ -75,13 +75,13 @@ class Chef Dir.glob(["/etc/rc.d/rc2.d/[SK][0-9][0-9]#{@new_resource.service_name}", "/etc/rc.d/rc2.d/[SK]#{@new_resource.service_name}"]).each { |f| ::File.delete(f) } if @new_resource.priority.is_a? Integer - create_symlink(2, 'K',100 - @new_resource.priority) + create_symlink(2, "K",100 - @new_resource.priority) elsif @new_resource.priority.is_a? Hash @new_resource.priority.each do |level,o| - create_symlink(level, 'K', 100 - o[1]) if o[0] == :stop + create_symlink(level, "K", 100 - o[1]) if o[0] == :stop end else - create_symlink(2, 'K', '') + create_symlink(2, "K", "") end end @@ -98,7 +98,7 @@ class Chef files.each do |file| if (RC_D_SCRIPT_NAME =~ file) - priority[2] = [($1 == "S" ? :start : :stop), ($2.empty? ? '' : $2.to_i)] + priority[2] = [($1 == "S" ? :start : :stop), ($2.empty? ? "" : $2.to_i)] if $1 == "S" is_enabled = true end diff --git a/lib/chef/provider/service/arch.rb b/lib/chef/provider/service/arch.rb index e7fbcc820c..3e7b9fcaca 100644 --- a/lib/chef/provider/service/arch.rb +++ b/lib/chef/provider/service/arch.rb @@ -16,7 +16,7 @@ # limitations under the License. # -require 'chef/provider/service/init' +require "chef/provider/service/init" class Chef::Provider::Service::Arch < Chef::Provider::Service::Init @@ -50,7 +50,7 @@ class Chef::Provider::Service::Arch < Chef::Provider::Service::Init def daemons entries = [] if ::File.read("/etc/rc.conf").match(/DAEMONS=\((.*)\)/m) - entries += $1.gsub(/\\?[\r\n]/, ' ').gsub(/# *[^ ]+/,' ').split(' ') if $1.length > 0 + entries += $1.gsub(/\\?[\r\n]/, " ").gsub(/# *[^ ]+/," ").split(" ") if $1.length > 0 end yield(entries) if block_given? diff --git a/lib/chef/provider/service/debian.rb b/lib/chef/provider/service/debian.rb index 01505924cb..559ad48418 100644 --- a/lib/chef/provider/service/debian.rb +++ b/lib/chef/provider/service/debian.rb @@ -16,21 +16,19 @@ # limitations under the License. # -require 'chef/provider/service/init' +require "chef/provider/service/init" class Chef class Provider class Service class Debian < Chef::Provider::Service::Init + provides :service, platform_family: "debian" do |node| + Chef::Platform::ServiceHelpers.service_resource_providers.include?(:debian) + end + UPDATE_RC_D_ENABLED_MATCHES = /\/rc[\dS].d\/S|not installed/i UPDATE_RC_D_PRIORITIES = /\/rc([\dS]).d\/([SK])(\d\d)/i - provides :service, platform_family: "debian" - - def self.provides?(node, resource) - super && Chef::Platform::ServiceHelpers.service_resource_providers.include?(:debian) - end - def self.supports?(resource, action) Chef::Platform::ServiceHelpers.config_for_service(resource.service_name).include?(:initd) end @@ -111,7 +109,7 @@ class Chef priority.each { |runlevel, arguments| Chef::Log.debug("#{new_resource} runlevel #{runlevel}, action #{arguments[0]}, priority #{arguments[1]}") # if we are in a update-rc.d default startup runlevel && we start in this runlevel - if %w[ 1 2 3 4 5 S ].include?(runlevel) && arguments[0] == :start + if %w{ 1 2 3 4 5 S }.include?(runlevel) && arguments[0] == :start enabled = true end } diff --git a/lib/chef/provider/service/freebsd.rb b/lib/chef/provider/service/freebsd.rb index 9204e3ef92..4aa9eb09f6 100644 --- a/lib/chef/provider/service/freebsd.rb +++ b/lib/chef/provider/service/freebsd.rb @@ -16,9 +16,9 @@ # limitations under the License. # -require 'chef/resource/service' -require 'chef/provider/service/init' -require 'chef/mixin/command' +require "chef/resource/service" +require "chef/provider/service/init" +require "chef/mixin/command" class Chef class Provider @@ -99,7 +99,7 @@ class Chef def restart_service if new_resource.restart_command super - elsif new_resource.supports[:restart] + elsif supports[:restart] shell_out_with_systems_locale!("#{init_command} fastrestart") else stop_service @@ -119,11 +119,11 @@ class Chef private def read_rc_conf - ::File.open("/etc/rc.conf", 'r') { |file| file.readlines } + ::File.open("/etc/rc.conf", "r") { |file| file.readlines } end def write_rc_conf(lines) - ::File.open("/etc/rc.conf", 'w') do |file| + ::File.open("/etc/rc.conf", "w") do |file| lines.each { |line| file.puts(line) } end end @@ -147,7 +147,7 @@ class Chef # some scripts support multiple instances through symlinks such as openvpn. # We should get the service name from rcvar. Chef::Log.debug("name=\"service\" not found at #{init_command}. falling back to rcvar") - sn = shell_out!("#{init_command} rcvar").stdout[/(\w+_enable)=/, 1] + shell_out!("#{init_command} rcvar").stdout[/(\w+_enable)=/, 1] else # for why-run mode when the rcd_script is not there yet new_resource.service_name diff --git a/lib/chef/provider/service/gentoo.rb b/lib/chef/provider/service/gentoo.rb index 3dab920f06..28fd43a709 100644 --- a/lib/chef/provider/service/gentoo.rb +++ b/lib/chef/provider/service/gentoo.rb @@ -1,7 +1,7 @@ # # Author:: Lee Jensen (<ljensen@engineyard.com>) # Author:: AJ Christensen (<aj@opscode.com>) -# Copyright:: Copyright (c) 2008 Opscode, Inc. +# Copyright:: Copyright (c) 2008-2015 Chef Software, Inc. # License:: Apache License, Version 2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,18 +17,18 @@ # limitations under the License. # -require 'chef/provider/service/init' -require 'chef/mixin/command' -require 'chef/util/path_helper' +require "chef/provider/service/init" +require "chef/mixin/command" +require "chef/util/path_helper" class Chef::Provider::Service::Gentoo < Chef::Provider::Service::Init provides :service, platform_family: "gentoo" def load_current_resource + supports[:status] = true if supports[:status].nil? + supports[:restart] = true if supports[:restart].nil? - @new_resource.supports[:status] = true - @new_resource.supports[:restart] = true @found_script = false super diff --git a/lib/chef/provider/service/init.rb b/lib/chef/provider/service/init.rb index 0a219a69e1..90c0ec2b34 100644 --- a/lib/chef/provider/service/init.rb +++ b/lib/chef/provider/service/init.rb @@ -1,6 +1,6 @@ # # Author:: AJ Christensen (<aj@hjksolutions.com>) -# Copyright:: Copyright (c) 2008 Opscode, Inc. +# Copyright:: Copyright (c) 2008-2015 Chef Software, Inc. # License:: Apache License, Version 2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -16,8 +16,9 @@ # limitations under the License. # -require 'chef/provider/service/simple' -require 'chef/mixin/command' +require "chef/provider/service/simple" +require "chef/mixin/command" +require "chef/platform/service_helpers" class Chef class Provider @@ -71,7 +72,7 @@ class Chef def restart_service if @new_resource.restart_command super - elsif @new_resource.supports[:restart] + elsif supports[:restart] shell_out_with_systems_locale!("#{default_init_command} restart") else stop_service @@ -83,7 +84,7 @@ class Chef def reload_service if @new_resource.reload_command super - elsif @new_resource.supports[:reload] + elsif supports[:reload] shell_out_with_systems_locale!("#{default_init_command} reload") end end diff --git a/lib/chef/provider/service/insserv.rb b/lib/chef/provider/service/insserv.rb index 31965a4bc6..1c4d294053 100644 --- a/lib/chef/provider/service/insserv.rb +++ b/lib/chef/provider/service/insserv.rb @@ -16,18 +16,16 @@ # limitations under the License. # -require 'chef/provider/service/init' -require 'chef/util/path_helper' +require "chef/provider/service/init" +require "chef/util/path_helper" class Chef class Provider class Service class Insserv < Chef::Provider::Service::Init - provides :service, os: "linux" - - def self.provides?(node, resource) - super && Chef::Platform::ServiceHelpers.service_resource_providers.include?(:insserv) + provides :service, platform_family: %w{debian rhel fedora suse} do |node| + Chef::Platform::ServiceHelpers.service_resource_providers.include?(:insserv) end def self.supports?(resource, action) diff --git a/lib/chef/provider/service/invokercd.rb b/lib/chef/provider/service/invokercd.rb index 5ff24e0dbb..a48e2ac815 100644 --- a/lib/chef/provider/service/invokercd.rb +++ b/lib/chef/provider/service/invokercd.rb @@ -16,17 +16,15 @@ # limitations under the License. # -require 'chef/provider/service/init' +require "chef/provider/service/init" class Chef class Provider class Service class Invokercd < Chef::Provider::Service::Init - provides :service, platform_family: "debian" - - def self.provides?(node, resource) - super && Chef::Platform::ServiceHelpers.service_resource_providers.include?(:invokercd) + provides :service, platform_family: "debian", override: true do |node| + Chef::Platform::ServiceHelpers.service_resource_providers.include?(:invokercd) end def self.supports?(resource, action) diff --git a/lib/chef/provider/service/macosx.rb b/lib/chef/provider/service/macosx.rb index df5be54fda..b939f78b03 100644 --- a/lib/chef/provider/service/macosx.rb +++ b/lib/chef/provider/service/macosx.rb @@ -16,16 +16,19 @@ # limitations under the License. # -require 'rexml/document' -require 'chef/resource/service' -require 'chef/provider/service/simple' -require 'chef/util/path_helper' +require "etc" +require "rexml/document" +require "chef/resource/service" +require "chef/resource/macosx_service" +require "chef/provider/service/simple" +require "chef/util/path_helper" class Chef class Provider class Service class Macosx < Chef::Provider::Service::Simple + provides :macosx_service, os: "darwin" provides :service, os: "darwin" def self.gather_plist_dirs @@ -33,27 +36,45 @@ class Chef /Library/LaunchDaemons /System/Library/LaunchAgents /System/Library/LaunchDaemons } - Chef::Util::PathHelper.home('Library', 'LaunchAgents') { |p| locations << p } + Chef::Util::PathHelper.home("Library", "LaunchAgents") { |p| locations << p } locations end PLIST_DIRS = gather_plist_dirs + def this_version_or_newer?(this_version) + Gem::Version.new(node["platform_version"]) >= Gem::Version.new(this_version) + end + def load_current_resource - @current_resource = Chef::Resource::Service.new(@new_resource.name) + @current_resource = Chef::Resource::MacosxService.new(@new_resource.name) @current_resource.service_name(@new_resource.service_name) @plist_size = 0 - @plist = find_service_plist + @plist = @new_resource.plist ? @new_resource.plist : find_service_plist @service_label = find_service_label + # LauchAgents should be loaded as the console user. + @console_user = @plist ? @plist.include?("LaunchAgents") : false + @session_type = @new_resource.session_type + + if @console_user + @console_user = Etc.getlogin + Chef::Log.debug("#{new_resource} console_user: '#{@console_user}'") + cmd = "su " + param = this_version_or_newer?("10.10") ? "" : "-l " + @base_user_cmd = cmd + param + "#{@console_user} -c" + # Default LauchAgent session should be Aqua + @session_type = "Aqua" if @session_type.nil? + end + + Chef::Log.debug("#{new_resource} Plist: '#{@plist}' service_label: '#{@service_label}'") set_service_status @current_resource end def define_resource_requirements - #super requirements.assert(:reload) do |a| - a.failure_message Chef::Exceptions::UnsupportedAction, "#{self.to_s} does not support :reload" + a.failure_message Chef::Exceptions::UnsupportedAction, "#{self} does not support :reload" end requirements.assert(:all_actions) do |a| @@ -61,6 +82,12 @@ class Chef a.failure_message Chef::Exceptions::Service, "Several plist files match service name. Please use full service name." end + requirements.assert(:all_actions) do |a| + a.assertion {::File.exists?(@plist.to_s) } + a.failure_message Chef::Exceptions::Service, + "Could not find plist for #{@new_resource}" + end + requirements.assert(:enable, :disable) do |a| a.assertion { !@service_label.to_s.empty? } a.failure_message Chef::Exceptions::Service, @@ -69,7 +96,7 @@ class Chef requirements.assert(:all_actions) do |a| a.assertion { @plist_size > 0 } - # No failrue here in original code - so we also will not + # No failure here in original code - so we also will not # fail. Instead warn that the service is potentially missing a.whyrun "Assuming that the service would have been previously installed and is currently disabled." do @current_resource.enabled(false) @@ -85,7 +112,7 @@ class Chef if @new_resource.start_command super else - shell_out_with_systems_locale!("launchctl load -w '#{@plist}'", :user => @owner_uid, :group => @owner_gid) + load_service end end end @@ -97,7 +124,7 @@ class Chef if @new_resource.stop_command super else - shell_out_with_systems_locale!("launchctl unload '#{@plist}'", :user => @owner_uid, :group => @owner_gid) + unload_service end end end @@ -106,9 +133,9 @@ class Chef if @new_resource.restart_command super else - stop_service + unload_service sleep 1 - start_service + load_service end end @@ -121,10 +148,7 @@ class Chef if @current_resource.enabled Chef::Log.debug("#{@new_resource} already enabled, not enabling") else - shell_out!( - "launchctl load -w '#{@plist}'", - :user => @owner_uid, :group => @owner_gid - ) + load_service end end @@ -132,38 +156,49 @@ class Chef unless @current_resource.enabled Chef::Log.debug("#{@new_resource} not enabled, not disabling") else - shell_out!( - "launchctl unload -w '#{@plist}'", - :user => @owner_uid, :group => @owner_gid - ) + unload_service + end + end + + def load_service + session = @session_type ? "-S #{@session_type} " : "" + cmd = "launchctl load -w " + session + @plist + shell_out_as_user(cmd) + end + + def unload_service + cmd = "launchctl unload -w " + @plist + shell_out_as_user(cmd) + end + + def shell_out_as_user(cmd) + if @console_user + shell_out_with_systems_locale("#{@base_user_cmd} '#{cmd}'") + else + shell_out_with_systems_locale(cmd) + end end def set_service_status return if @plist == nil or @service_label.to_s.empty? - cmd = shell_out( - "launchctl list #{@service_label}", - :user => @owner_uid, :group => @owner_gid - ) + cmd = "launchctl list #{@service_label}" + res = shell_out_as_user(cmd) - if cmd.exitstatus == 0 + if res.exitstatus == 0 @current_resource.enabled(true) else @current_resource.enabled(false) end if @current_resource.enabled - @owner_uid = ::File.stat(@plist).uid - @owner_gid = ::File.stat(@plist).gid - - shell_out!( - "launchctl list", :user => @owner_uid, :group => @owner_gid - ).stdout.each_line do |line| - case line - when /(\d+|-)\s+(?:\d+|-)\s+(.*\.?)#{@service_label}/ + res.stdout.each_line do |line| + case line.downcase + when /\s+\"pid\"\s+=\s+(\d+).*/ pid = $1 @current_resource.running(!pid.to_i.zero?) + Chef::Log.debug("Current PID for #{@service_label} is #{pid}") end end else @@ -171,13 +206,16 @@ class Chef end end - private + private def find_service_label # CHEF-5223 "you can't glob for a file that hasn't been converged # onto the node yet." return nil if @plist.nil? + # Plist must exist by this point + raise Chef::Exceptions::FileNotFound, "Cannot find #{@plist}!" unless ::File.exists?(@plist) + # Most services have the same internal label as the name of the # plist file. However, there is no rule saying that *has* to be # the case, and some core services (notably, ssh) do not follow @@ -185,7 +223,9 @@ class Chef # plist files can come in XML or Binary formats. this command # will make sure we get XML every time. - plist_xml = shell_out!("plutil -convert xml1 -o - #{@plist}").stdout + plist_xml = shell_out_with_systems_locale!( + "plutil -convert xml1 -o - #{@plist}" + ).stdout plist_doc = REXML::Document.new(plist_xml) plist_doc.elements[ diff --git a/lib/chef/provider/service/openbsd.rb b/lib/chef/provider/service/openbsd.rb index d509ee10ff..10d7b21953 100644 --- a/lib/chef/provider/service/openbsd.rb +++ b/lib/chef/provider/service/openbsd.rb @@ -16,35 +16,36 @@ # limitations under the License. # -require 'chef/mixin/command' -require 'chef/mixin/shell_out' -require 'chef/provider/service/init' -require 'chef/resource/service' +require "chef/mixin/command" +require "chef/mixin/shell_out" +require "chef/provider/service/init" +require "chef/resource/service" class Chef class Provider class Service class Openbsd < Chef::Provider::Service::Init - provides :service, os: [ "openbsd" ] + provides :service, os: "openbsd" include Chef::Mixin::ShellOut attr_reader :init_command, :rc_conf, :rc_conf_local, :enabled_state_found - RC_CONF_PATH = '/etc/rc.conf' - RC_CONF_LOCAL_PATH = '/etc/rc.conf.local' + RC_CONF_PATH = "/etc/rc.conf" + RC_CONF_LOCAL_PATH = "/etc/rc.conf.local" def initialize(new_resource, run_context) super - @rc_conf = ::File.read(RC_CONF_PATH) rescue '' - @rc_conf_local = ::File.read(RC_CONF_LOCAL_PATH) rescue '' + @rc_conf = ::File.read(RC_CONF_PATH) rescue "" + @rc_conf_local = ::File.read(RC_CONF_LOCAL_PATH) rescue "" @init_command = ::File.exist?(rcd_script_path) ? rcd_script_path : nil - new_resource.supports[:status] = true new_resource.status_command("#{default_init_command} check") end def load_current_resource + supports[:status] = true if supports[:status].nil? + @current_resource = Chef::Resource::Service.new(new_resource.name) current_resource.service_name(new_resource.service_name) @@ -81,7 +82,7 @@ class Chef if !is_enabled? if is_builtin? if is_enabled_by_default? - update_rcl rc_conf_local.sub(/^#{Regexp.escape(builtin_service_enable_variable_name)}=.*/, '') + update_rcl rc_conf_local.sub(/^#{Regexp.escape(builtin_service_enable_variable_name)}=.*/, "") else # add line with blank string, which means enable update_rcl rc_conf_local + "\n" + "#{builtin_service_enable_variable_name}=\"\"\n" @@ -89,7 +90,7 @@ class Chef else # add to pkg_scripts, most recent addition goes last old_services_list = rc_conf_local.match(/^pkg_scripts="(.*)"/) - old_services_list = old_services_list ? old_services_list[1].split(' ') : [] + old_services_list = old_services_list ? old_services_list[1].split(" ") : [] new_services_list = old_services_list + [new_resource.service_name] if rc_conf_local.match(/^pkg_scripts="(.*)"/) new_rcl = rc_conf_local.sub(/^pkg_scripts="(.*)"/, "pkg_scripts=\"#{new_services_list.join(' ')}\"") @@ -109,12 +110,12 @@ class Chef update_rcl rc_conf_local + "\n" + "#{builtin_service_enable_variable_name}=\"NO\"\n" else # remove line to disable - update_rcl rc_conf_local.sub(/^#{Regexp.escape(builtin_service_enable_variable_name)}=.*/, '') + update_rcl rc_conf_local.sub(/^#{Regexp.escape(builtin_service_enable_variable_name)}=.*/, "") end else # remove from pkg_scripts old_list = rc_conf_local.match(/^pkg_scripts="(.*)"/) - old_list = old_list ? old_list[1].split(' ') : [] + old_list = old_list ? old_list[1].split(" ") : [] new_list = old_list - [new_resource.service_name] update_rcl rc_conf_local.sub(/^pkg_scripts="(.*)"/, pkg_scripts="#{new_list.join(' ')}") end diff --git a/lib/chef/provider/service/redhat.rb b/lib/chef/provider/service/redhat.rb index 850953125e..9cc4258b70 100644 --- a/lib/chef/provider/service/redhat.rb +++ b/lib/chef/provider/service/redhat.rb @@ -1,6 +1,6 @@ # # Author:: AJ Christensen (<aj@hjksolutions.com>) -# Copyright:: Copyright (c) 2008 Opscode, Inc. +# Copyright:: Copyright (c) 2008-2015 Chef Software, Inc. # License:: Apache License, Version 2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -16,31 +16,39 @@ # limitations under the License. # -require 'chef/provider/service/init' +require "chef/provider/service/init" class Chef class Provider class Service class Redhat < Chef::Provider::Service::Init - CHKCONFIG_ON = /\d:on/ - CHKCONFIG_MISSING = /No such/ - - provides :service, platform_family: [ "rhel", "fedora", "suse" ] + # @api private + attr_accessor :service_missing + # @api private + attr_accessor :current_run_levels - def self.provides?(node, resource) - super && Chef::Platform::ServiceHelpers.service_resource_providers.include?(:redhat) + provides :service, platform_family: %w{rhel fedora suse} do |node| + Chef::Platform::ServiceHelpers.service_resource_providers.include?(:redhat) end + CHKCONFIG_ON = /\d:on/ + CHKCONFIG_MISSING = /No such/ + def self.supports?(resource, action) Chef::Platform::ServiceHelpers.config_for_service(resource.service_name).include?(:initd) end def initialize(new_resource, run_context) super - @init_command = "/sbin/service #{@new_resource.service_name}" - @new_resource.supports[:status] = true + @init_command = "/sbin/service #{new_resource.service_name}" @service_missing = false + @current_run_levels = [] + end + + # @api private + def run_levels + new_resource.run_levels end def define_resource_requirements @@ -49,34 +57,62 @@ class Chef requirements.assert(:all_actions) do |a| chkconfig_file = "/sbin/chkconfig" a.assertion { ::File.exists? chkconfig_file } - a.failure_message Chef::Exceptions::Service, "#{chkconfig_file} does not exist!" + a.failure_message Chef::Exceptions::Service, "#{chkconfig_file} dbleoes not exist!" end requirements.assert(:start, :enable, :reload, :restart) do |a| - a.assertion { !@service_missing } - a.failure_message Chef::Exceptions::Service, "#{@new_resource}: unable to locate the init.d script!" + a.assertion do + custom_command_for_action?(action) || !@service_missing + end + a.failure_message Chef::Exceptions::Service, "#{new_resource}: No custom command for #{action} specified and unable to locate the init.d script!" a.whyrun "Assuming service would be disabled. The init script is not presently installed." end end def load_current_resource + supports[:status] = true if supports[:status].nil? + super if ::File.exists?("/sbin/chkconfig") - chkconfig = shell_out!("/sbin/chkconfig --list #{@current_resource.service_name}", :returns => [0,1]) - @current_resource.enabled(!!(chkconfig.stdout =~ CHKCONFIG_ON)) + chkconfig = shell_out!("/sbin/chkconfig --list #{current_resource.service_name}", :returns => [0,1]) + unless run_levels.nil? or run_levels.empty? + all_levels_match = true + chkconfig.stdout.split(/\s+/)[1..-1].each do |level| + index = level.split(":").first + status = level.split(":").last + if level =~ CHKCONFIG_ON + @current_run_levels << index.to_i + all_levels_match = false unless run_levels.include?(index.to_i) + else + all_levels_match = false if run_levels.include?(index.to_i) + end + end + current_resource.enabled(all_levels_match) + else + current_resource.enabled(!!(chkconfig.stdout =~ CHKCONFIG_ON)) + end @service_missing = !!(chkconfig.stderr =~ CHKCONFIG_MISSING) end - @current_resource + current_resource + end + + # @api private + def levels + (run_levels.nil? or run_levels.empty?) ? "" : "--level #{run_levels.join('')} " end def enable_service() - shell_out! "/sbin/chkconfig #{@new_resource.service_name} on" + unless run_levels.nil? or run_levels.empty? + disable_levels = current_run_levels - run_levels + shell_out! "/sbin/chkconfig --level #{disable_levels.join('')} #{new_resource.service_name} off" unless disable_levels.empty? + end + shell_out! "/sbin/chkconfig #{levels}#{new_resource.service_name} on" end def disable_service() - shell_out! "/sbin/chkconfig #{@new_resource.service_name} off" + shell_out! "/sbin/chkconfig #{levels}#{new_resource.service_name} off" end end end diff --git a/lib/chef/provider/service/simple.rb b/lib/chef/provider/service/simple.rb index ee403ee163..a096c9dfc0 100644 --- a/lib/chef/provider/service/simple.rb +++ b/lib/chef/provider/service/simple.rb @@ -16,9 +16,9 @@ # limitations under the License. # -require 'chef/provider/service' -require 'chef/resource/service' -require 'chef/mixin/command' +require "chef/provider/service" +require "chef/resource/service" +require "chef/mixin/command" class Chef class Provider @@ -58,25 +58,25 @@ class Chef shared_resource_requirements requirements.assert(:start) do |a| a.assertion { @new_resource.start_command } - a.failure_message Chef::Exceptions::Service, "#{self.to_s} requires that start_command be set" + a.failure_message Chef::Exceptions::Service, "#{self} requires that start_command be set" end requirements.assert(:stop) do |a| a.assertion { @new_resource.stop_command } - a.failure_message Chef::Exceptions::Service, "#{self.to_s} requires that stop_command be set" + a.failure_message Chef::Exceptions::Service, "#{self} requires that stop_command be set" end requirements.assert(:restart) do |a| a.assertion { @new_resource.restart_command || ( @new_resource.start_command && @new_resource.stop_command ) } - a.failure_message Chef::Exceptions::Service, "#{self.to_s} requires a restart_command or both start_command and stop_command be set in order to perform a restart" + a.failure_message Chef::Exceptions::Service, "#{self} requires a restart_command or both start_command and stop_command be set in order to perform a restart" end requirements.assert(:reload) do |a| a.assertion { @new_resource.reload_command } - a.failure_message Chef::Exceptions::UnsupportedAction, "#{self.to_s} requires a reload_command be set in order to perform a reload" + a.failure_message Chef::Exceptions::UnsupportedAction, "#{self} requires a reload_command be set in order to perform a reload" end requirements.assert(:all_actions) do |a| - a.assertion { @new_resource.status_command or @new_resource.supports[:status] or + a.assertion { @new_resource.status_command or supports[:status] or (!ps_cmd.nil? and !ps_cmd.empty?) } a.failure_message Chef::Exceptions::Service, "#{@new_resource} could not determine how to inspect the process table, please set this node's 'command.ps' attribute" end @@ -127,7 +127,7 @@ class Chef nil end - elsif @new_resource.supports[:status] + elsif supports[:status] Chef::Log.debug("#{@new_resource} supports status, running") begin if shell_out("#{default_init_command} status").exitstatus == 0 diff --git a/lib/chef/provider/service/solaris.rb b/lib/chef/provider/service/solaris.rb index eaea6bb1ab..c43a25258c 100644 --- a/lib/chef/provider/service/solaris.rb +++ b/lib/chef/provider/service/solaris.rb @@ -16,9 +16,9 @@ # limitations under the License. # -require 'chef/provider/service' -require 'chef/resource/service' -require 'chef/mixin/command' +require "chef/provider/service" +require "chef/resource/service" +require "chef/mixin/command" class Chef class Provider @@ -30,35 +30,39 @@ class Chef def initialize(new_resource, run_context=nil) super - @init_command = "/usr/sbin/svcadm" - @status_command = "/bin/svcs -l" + @init_command = "/usr/sbin/svcadm" + @status_command = "/bin/svcs" @maintenace = false end def load_current_resource @current_resource = Chef::Resource::Service.new(@new_resource.name) @current_resource.service_name(@new_resource.service_name) - unless ::File.exists? "/bin/svcs" - raise Chef::Exceptions::Service, "/bin/svcs does not exist!" + + [@init_command, @status_command].each do |cmd| + unless ::File.executable? cmd then + raise Chef::Exceptions::Service, "#{cmd} not executable!" + end end @status = service_status.enabled + @current_resource end def enable_service - shell_out!("#{default_init_command} clear #{@new_resource.service_name}") if @maintenance - shell_out!("#{default_init_command} enable -s #{@new_resource.service_name}") + shell_out!(default_init_command, "clear", @new_resource.service_name) if @maintenance + shell_out!(default_init_command, "enable", "-s", @new_resource.service_name) end def disable_service - shell_out!("#{default_init_command} disable -s #{@new_resource.service_name}") + shell_out!(default_init_command, "disable", "-s", @new_resource.service_name) end alias_method :stop_service, :disable_service alias_method :start_service, :enable_service def reload_service - shell_out_with_systems_locale!("#{default_init_command} refresh #{@new_resource.service_name}") + shell_out!(default_init_command, "refresh", @new_resource.service_name) end def restart_service @@ -68,16 +72,38 @@ class Chef end def service_status - status = shell_out!("#{@status_command} #{@current_resource.service_name}", :returns => [0, 1]) - status.stdout.each_line do |line| - case line - when /state\s+online/ - @current_resource.enabled(true) - @current_resource.running(true) - when /state\s+maintenance/ - @maintenance = true - end + cmd = shell_out!(@status_command, "-l", @current_resource.service_name, :returns => [0, 1]) + # Example output + # $ svcs -l rsyslog + # fmri svc:/application/rsyslog:default + # name rsyslog logging utility + # enabled true + # state online + # next_state none + # state_time April 2, 2015 04:25:19 PM EDT + # logfile /var/svc/log/application-rsyslog:default.log + # restarter svc:/system/svc/restarter:default + # contract_id 1115271 + # dependency require_all/error svc:/milestone/multi-user:default (online) + # $ + + # load output into hash + status = {} + cmd.stdout.each_line do |line| + key, value = line.strip.split(/\s+/, 2) + status[key] = value + end + + # check service state + @maintenance = false + case status["state"] + when "online" + @current_resource.enabled(true) + @current_resource.running(true) + when "maintenance" + @maintenance = true end + unless @current_resource.enabled @current_resource.enabled(false) @current_resource.running(false) diff --git a/lib/chef/provider/service/systemd.rb b/lib/chef/provider/service/systemd.rb index 9085ffde2e..bf4b324dce 100644 --- a/lib/chef/provider/service/systemd.rb +++ b/lib/chef/provider/service/systemd.rb @@ -16,22 +16,20 @@ # limitations under the License. # -require 'chef/resource/service' -require 'chef/provider/service/simple' -require 'chef/mixin/which' +require "chef/resource/service" +require "chef/provider/service/simple" +require "chef/mixin/which" class Chef::Provider::Service::Systemd < Chef::Provider::Service::Simple include Chef::Mixin::Which - provides :service, os: "linux" + provides :service, os: "linux" do |node| + Chef::Platform::ServiceHelpers.service_resource_providers.include?(:systemd) + end attr_accessor :status_check_success - def self.provides?(node, resource) - super && Chef::Platform::ServiceHelpers.service_resource_providers.include?(:systemd) - end - def self.supports?(resource, action) Chef::Platform::ServiceHelpers.config_for_service(resource.service_name).include?(:systemd) end diff --git a/lib/chef/provider/service/upstart.rb b/lib/chef/provider/service/upstart.rb index 8d4aa41035..c0f7e020f6 100644 --- a/lib/chef/provider/service/upstart.rb +++ b/lib/chef/provider/service/upstart.rb @@ -16,23 +16,22 @@ # limitations under the License. # -require 'chef/resource/service' -require 'chef/provider/service/simple' -require 'chef/mixin/command' -require 'chef/util/file_edit' +require "chef/resource/service" +require "chef/provider/service/simple" +require "chef/mixin/command" +require "chef/util/file_edit" class Chef class Provider class Service class Upstart < Chef::Provider::Service::Simple - UPSTART_STATE_FORMAT = /\w+ \(?(\w+)\)?[\/ ](\w+)/ - provides :service, os: "linux" - - def self.provides?(node, resource) - super && Chef::Platform::ServiceHelpers.service_resource_providers.include?(:upstart) + provides :service, platform_family: "debian", override: true do |node| + Chef::Platform::ServiceHelpers.service_resource_providers.include?(:upstart) end + UPSTART_STATE_FORMAT = /\S+ \(?(start|stop)?\)? ?[\/ ](\w+)/ + def self.supports?(resource, action) Chef::Platform::ServiceHelpers.config_for_service(resource.service_name).include?(:upstart) end @@ -107,7 +106,7 @@ class Chef Chef::Log.debug("#{@new_resource} you have specified a status command, running..") begin - if shell_out!(@new_resource.status_command) == 0 + if shell_out!(@new_resource.status_command).exitstatus == 0 @current_resource.running true end rescue @@ -131,7 +130,7 @@ class Chef # Get enabled/disabled state by reading job configuration file if ::File.exists?("#{@upstart_job_dir}/#{@new_resource.service_name}#{@upstart_conf_suffix}") Chef::Log.debug("#{@new_resource} found #{@upstart_job_dir}/#{@new_resource.service_name}#{@upstart_conf_suffix}") - ::File.open("#{@upstart_job_dir}/#{@new_resource.service_name}#{@upstart_conf_suffix}",'r') do |file| + ::File.open("#{@upstart_job_dir}/#{@new_resource.service_name}#{@upstart_conf_suffix}","r") do |file| while line = file.gets case line when /^start on/ @@ -225,10 +224,10 @@ class Chef command = "/sbin/status #{@job}" status = popen4(command) do |pid, stdin, stdout, stderr| stdout.each_line do |line| - # rsyslog stop/waiting # service goal/state # OR - # rsyslog (stop) waiting + # service (instance) goal/state + # OR # service (goal) state line =~ UPSTART_STATE_FORMAT data = Regexp.last_match diff --git a/lib/chef/provider/service/windows.rb b/lib/chef/provider/service/windows.rb index ba53f0a3c3..0ae3c5b09f 100644 --- a/lib/chef/provider/service/windows.rb +++ b/lib/chef/provider/service/windows.rb @@ -18,14 +18,13 @@ # limitations under the License. # -require 'chef/provider/service/simple' +require "chef/provider/service/simple" if RUBY_PLATFORM =~ /mswin|mingw32|windows/ - require 'chef/win32/error' - require 'win32/service' + require "chef/win32/error" + require "win32/service" end class Chef::Provider::Service::Windows < Chef::Provider::Service - provides :service, os: "windows" provides :windows_service, os: "windows" @@ -33,21 +32,23 @@ class Chef::Provider::Service::Windows < Chef::Provider::Service include Chef::ReservedNames::Win32::API::Error rescue LoadError #Win32::Service.get_start_type - AUTO_START = 'auto start' - MANUAL = 'demand start' - DISABLED = 'disabled' + AUTO_START = "auto start" + MANUAL = "demand start" + DISABLED = "disabled" #Win32::Service.get_current_state - RUNNING = 'running' - STOPPED = 'stopped' - CONTINUE_PENDING = 'continue pending' - PAUSE_PENDING = 'pause pending' - PAUSED = 'paused' - START_PENDING = 'start pending' - STOP_PENDING = 'stop pending' + RUNNING = "running" + STOPPED = "stopped" + CONTINUE_PENDING = "continue pending" + PAUSE_PENDING = "pause pending" + PAUSED = "paused" + START_PENDING = "start pending" + STOP_PENDING = "stop pending" TIMEOUT = 60 + SERVICE_RIGHT = "SeServiceLogonRight" + def whyrun_supported? false end @@ -79,10 +80,10 @@ class Chef::Provider::Service::Windows < Chef::Provider::Service Win32::Service.configure(new_config) Chef::Log.info "#{@new_resource} configured with #{new_config.inspect}" - # it would be nice to check if the user already has the logon privilege, but that turns out to be - # nontrivial. if new_config.has_key?(:service_start_name) - grant_service_logon(new_config[:service_start_name]) + unless Chef::ReservedNames::Win32::Security.get_account_right(canonicalize_username(new_config[:service_start_name])).include?(SERVICE_RIGHT) + grant_service_logon(new_config[:service_start_name]) + end end state = current_state @@ -237,74 +238,25 @@ class Chef::Provider::Service::Windows < Chef::Provider::Service end private - def make_policy_text(username) - text = <<-EOS -[Unicode] -Unicode=yes -[Privilege Rights] -SeServiceLogonRight = \\\\#{canonicalize_username(username)},*S-1-5-80-0 -[Version] -signature="$CHICAGO$" -Revision=1 -EOS - end - - def grant_logfile_name(username) - Chef::Util::PathHelper.canonical_path("#{Dir.tmpdir}/logon_grant-#{clean_username_for_path(username)}-#{$$}.log", prefix=false) - end - - def grant_policyfile_name(username) - Chef::Util::PathHelper.canonical_path("#{Dir.tmpdir}/service_logon_policy-#{clean_username_for_path(username)}-#{$$}.inf", prefix=false) - end - - def grant_dbfile_name(username) - "#{ENV['TEMP']}\\secedit.sdb" - end - def grant_service_logon(username) - logfile = grant_logfile_name(username) - policy_file = ::File.new(grant_policyfile_name(username), 'w') - policy_text = make_policy_text(username) - dbfile = grant_dbfile_name(username) # this is just an audit file. - begin - Chef::Log.debug "Policy file text:\n#{policy_text}" - policy_file.puts(policy_text) - policy_file.close # need to flush the buffer. - - # it would be nice to do this with APIs instead, but the LSA_* APIs are - # particularly onerous and life is short. - cmd = %Q{secedit.exe /configure /db "#{dbfile}" /cfg "#{policy_file.path}" /areas USER_RIGHTS SECURITYPOLICY SERVICES /log "#{logfile}"} - Chef::Log.debug "Granting logon-as-service privilege with: #{cmd}" - runner = shell_out(cmd) - - if runner.exitstatus != 0 - Chef::Log.fatal "Logon-as-service grant failed with output: #{runner.stdout}" - raise Chef::Exceptions::Service, <<-EOS -Logon-as-service grant failed with policy file #{policy_file.path}. -You can look at #{logfile} for details, or do `secedit /analyze #{dbfile}`. -The failed command was `#{cmd}`. -EOS - end - - Chef::Log.info "Grant logon-as-service to user '#{username}' successful." - - ::File.delete(dbfile) rescue nil - ::File.delete(policy_file) - ::File.delete(logfile) rescue nil # logfile is not always present at end. + Chef::ReservedNames::Win32::Security.add_account_right(canonicalize_username(username), SERVICE_RIGHT) + rescue Chef::Exceptions::Win32APIError => err + Chef::Log.fatal "Logon-as-service grant failed with output: #{err}" + raise Chef::Exceptions::Service, "Logon-as-service grant failed for #{username}: #{err}" end + + Chef::Log.info "Grant logon-as-service to user '#{username}' successful." true end # remove characters that make for broken or wonky filenames. def clean_username_for_path(username) - username.gsub(/[\/\\. ]+/, '_') + username.gsub(/[\/\\. ]+/, "_") end - # the security policy file only seems to accept \\username, so fix .\username or .\\username. - # TODO: this probably has to be fixed to handle various valid Windows names correctly. def canonicalize_username(username) - username.sub(/^\.?\\+/, '') + username.sub(/^\.?\\+/, "") end def current_state @@ -353,7 +305,7 @@ EOS Chef::Log.debug "#{@new_resource.name} setting start_type to #{type}" Win32::Service.configure( :service_name => @new_resource.service_name, - :start_type => allowed_types[type] + :start_type => allowed_types[type], ) @new_resource.updated_by_last_action(true) end diff --git a/lib/chef/provider/subversion.rb b/lib/chef/provider/subversion.rb index 5f36483c32..2cc0da65a0 100644 --- a/lib/chef/provider/subversion.rb +++ b/lib/chef/provider/subversion.rb @@ -18,10 +18,10 @@ #TODO subversion and git should both extend from a base SCM provider. -require 'chef/log' -require 'chef/provider' -require 'chef/mixin/command' -require 'fileutils' +require "chef/log" +require "chef/provider" +require "chef/mixin/command" +require "fileutils" class Chef class Provider @@ -130,8 +130,8 @@ class Chef @new_resource.revision else command = scm(:info, @new_resource.repository, @new_resource.svn_info_args, authentication, "-r#{@new_resource.revision}") - status, svn_info, error_message = output_of_command(command, run_options) - handle_command_failures(status, "STDOUT: #{svn_info}\nSTDERR: #{error_message}") + svn_info = shell_out!(command, run_options(:cwd => cwd, :returns => [0,1])).stdout + extract_revision_info(svn_info) end end @@ -142,11 +142,8 @@ class Chef def find_current_revision return nil unless ::File.exist?(::File.join(@new_resource.destination, ".svn")) command = scm(:info) - status, svn_info, error_message = output_of_command(command, run_options(:cwd => cwd)) + svn_info = shell_out!(command, run_options(:cwd => cwd, :returns => [0,1])).stdout - unless [0,1].include?(status.exitstatus) - handle_command_failures(status, "STDOUT: #{svn_info}\nSTDERR: #{error_message}") - end extract_revision_info(svn_info) end @@ -179,7 +176,8 @@ class Chef end attrs end - rev = (repo_attrs['Last Changed Rev'] || repo_attrs['Revision']) + rev = (repo_attrs["Last Changed Rev"] || repo_attrs["Revision"]) + rev.strip! if rev raise "Could not parse `svn info` data: #{svn_info}" if repo_attrs.empty? Chef::Log.debug "#{@new_resource} resolved revision #{@new_resource.revision} to #{rev}" rev @@ -197,12 +195,20 @@ class Chef end def scm(*args) - ['svn', *args].compact.join(" ") + binary = svn_binary + binary = "\"#{binary}\"" if binary =~ /\s/ + [binary, *args].compact.join(" ") end def target_dir_non_existent_or_empty? - !::File.exist?(@new_resource.destination) || Dir.entries(@new_resource.destination).sort == ['.','..'] + !::File.exist?(@new_resource.destination) || Dir.entries(@new_resource.destination).sort == [".",".."] end + + def svn_binary + @new_resource.svn_binary || + (Chef::Platform.windows? ? "svn.exe" : "svn") + end + def assert_target_directory_valid! target_parent_directory = ::File.dirname(@new_resource.destination) unless ::File.directory?(target_parent_directory) diff --git a/lib/chef/provider/template.rb b/lib/chef/provider/template.rb index 1e759074b9..7cbfca025f 100644 --- a/lib/chef/provider/template.rb +++ b/lib/chef/provider/template.rb @@ -17,10 +17,10 @@ # limitations under the License. # -require 'chef/provider/template_finder' -require 'chef/provider/file' -require 'chef/deprecation/provider/template' -require 'chef/deprecation/warnings' +require "chef/provider/template_finder" +require "chef/provider/file" +require "chef/deprecation/provider/template" +require "chef/deprecation/warnings" class Chef class Provider diff --git a/lib/chef/provider/template/content.rb b/lib/chef/provider/template/content.rb index 7fc680ec85..891861de8b 100644 --- a/lib/chef/provider/template/content.rb +++ b/lib/chef/provider/template/content.rb @@ -16,8 +16,8 @@ # limitations under the License. # -require 'chef/mixin/template' -require 'chef/file_content_management/content_base' +require "chef/mixin/template" +require "chef/file_content_management/content_base" class Chef class Provider @@ -29,20 +29,30 @@ class Chef def template_location @template_file_cache_location ||= begin - template_finder.find(@new_resource.source, :local => @new_resource.local, :cookbook => @new_resource.cookbook) + template_finder.find(new_resource.source, :local => new_resource.local, :cookbook => new_resource.cookbook) end end private def file_for_provider - context = TemplateContext.new(@new_resource.variables) - context[:node] = @run_context.node + context = TemplateContext.new(new_resource.variables) + context[:node] = run_context.node context[:template_finder] = template_finder - context._extend_modules(@new_resource.helper_modules) + + # helper variables + context[:cookbook_name] = new_resource.cookbook_name unless context.keys.include?(:coookbook_name) + context[:recipe_name] = new_resource.recipe_name unless context.keys.include?(:recipe_name) + context[:recipe_line_string] = new_resource.source_line unless context.keys.include?(:recipe_line_string) + context[:recipe_path] = new_resource.source_line_file unless context.keys.include?(:recipe_path) + context[:recipe_line] = new_resource.source_line_number unless context.keys.include?(:recipe_line) + context[:template_name] = new_resource.source unless context.keys.include?(:template_name) + context[:template_path] = template_location unless context.keys.include?(:template_path) + + context._extend_modules(new_resource.helper_modules) output = context.render_template(template_location) - tempfile = Tempfile.open("chef-rendered-template") + tempfile = Chef::FileContentManagement::Tempfile.new(new_resource).tempfile tempfile.binmode tempfile.write(output) tempfile.close @@ -51,7 +61,7 @@ class Chef def template_finder @template_finder ||= begin - TemplateFinder.new(run_context, @new_resource.cookbook_name, @run_context.node) + TemplateFinder.new(run_context, new_resource.cookbook_name, run_context.node) end end end diff --git a/lib/chef/provider/user.rb b/lib/chef/provider/user.rb index f6ac72448e..b819401731 100644 --- a/lib/chef/provider/user.rb +++ b/lib/chef/provider/user.rb @@ -16,14 +16,13 @@ # limitations under the License. # -require 'chef/provider' -require 'chef/mixin/command' -require 'etc' +require "chef/provider" +require "chef/mixin/command" +require "etc" class Chef class Provider class User < Chef::Provider - include Chef::Mixin::Command attr_accessor :user_exists, :locked @@ -72,9 +71,9 @@ class Chef end @current_resource.comment(user_info.gecos) - if @new_resource.password && @current_resource.password == 'x' + if @new_resource.password && @current_resource.password == "x" begin - require 'shadow' + require "shadow" rescue LoadError @shadow_lib_ok = false else @@ -90,7 +89,7 @@ class Chef end def define_resource_requirements - requirements.assert(:all_actions) do |a| + requirements.assert(:create, :modify, :manage, :lock, :unlock) do |a| a.assertion { @group_name_resolved } a.failure_message Chef::Exceptions::User, "Couldn't lookup integer GID for group name #{@new_resource.gid}" a.whyrun "group name #{@new_resource.gid} does not exist. This will cause group assignment to fail. Assuming this group will have been created previously." @@ -208,7 +207,6 @@ class Chef def unlock_user raise NotImplementedError end - end end end diff --git a/lib/chef/provider/user/aix.rb b/lib/chef/provider/user/aix.rb index af08ab4364..83bd900f79 100644 --- a/lib/chef/provider/user/aix.rb +++ b/lib/chef/provider/user/aix.rb @@ -18,9 +18,10 @@ class Chef class Provider class User class Aix < Chef::Provider::User::Useradd + provides :user, platform: %w{aix} UNIVERSAL_OPTIONS = [[:comment, "-c"], [:gid, "-g"], [:shell, "-s"], [:uid, "-u"]] - + def create_user super add_password @@ -88,7 +89,7 @@ class Chef end end end - + end end end diff --git a/lib/chef/provider/user/dscl.rb b/lib/chef/provider/user/dscl.rb index 0c0c85e18b..563d56dce7 100644 --- a/lib/chef/provider/user/dscl.rb +++ b/lib/chef/provider/user/dscl.rb @@ -16,11 +16,11 @@ # limitations under the License. # -require 'mixlib/shellout' -require 'chef/provider/user' -require 'openssl' -require 'plist' -require 'chef/util/path_helper' +require "mixlib/shellout" +require "chef/provider/user" +require "openssl" +require "plist" +require "chef/util/path_helper" class Chef class Provider @@ -44,6 +44,10 @@ class Chef # This provider only supports Mac OSX versions 10.7 and above class Dscl < Chef::Provider::User + attr_accessor :user_info + attr_accessor :authentication_authority + attr_accessor :password_shadow_conversion_algorithm + provides :user, os: "darwin" def define_resource_requirements @@ -56,19 +60,19 @@ class Chef requirements.assert(:all_actions) do |a| a.assertion { ::File.exists?("/usr/bin/dscl") } - a.failure_message(Chef::Exceptions::User, "Cannot find binary '/usr/bin/dscl' on the system for #{@new_resource}!") + a.failure_message(Chef::Exceptions::User, "Cannot find binary '/usr/bin/dscl' on the system for #{new_resource}!") end requirements.assert(:all_actions) do |a| a.assertion { ::File.exists?("/usr/bin/plutil") } - a.failure_message(Chef::Exceptions::User, "Cannot find binary '/usr/bin/plutil' on the system for #{@new_resource}!") + a.failure_message(Chef::Exceptions::User, "Cannot find binary '/usr/bin/plutil' on the system for #{new_resource}!") end requirements.assert(:create, :modify, :manage) do |a| a.assertion do - if @new_resource.password && mac_osx_version_greater_than_10_7? + if new_resource.password && mac_osx_version_greater_than_10_7? # SALTED-SHA512 password shadow hashes are not supported on 10.8 and above. - !salted_sha512?(@new_resource.password) + !salted_sha512?(new_resource.password) else true end @@ -80,10 +84,10 @@ in 'password', with the associated 'salt' and 'iterations'.") requirements.assert(:create, :modify, :manage) do |a| a.assertion do - if @new_resource.password && mac_osx_version_greater_than_10_7? && salted_sha512_pbkdf2?(@new_resource.password) + if new_resource.password && mac_osx_version_greater_than_10_7? && salted_sha512_pbkdf2?(new_resource.password) # salt and iterations should be specified when # SALTED-SHA512-PBKDF2 password shadow hash is given - !@new_resource.salt.nil? && !@new_resource.iterations.nil? + !new_resource.salt.nil? && !new_resource.iterations.nil? else true end @@ -94,9 +98,9 @@ in 'password', with the associated 'salt' and 'iterations'.") requirements.assert(:create, :modify, :manage) do |a| a.assertion do - if @new_resource.password && !mac_osx_version_greater_than_10_7? + if new_resource.password && !mac_osx_version_greater_than_10_7? # On 10.7 SALTED-SHA512-PBKDF2 is not supported - !salted_sha512_pbkdf2?(@new_resource.password) + !salted_sha512_pbkdf2?(new_resource.password) else true end @@ -109,21 +113,21 @@ user password using shadow hash.") end def load_current_resource - @current_resource = Chef::Resource::User.new(@new_resource.username) - @current_resource.username(@new_resource.username) + @current_resource = Chef::Resource::User.new(new_resource.username) + current_resource.username(new_resource.username) @user_info = read_user_info - if @user_info - @current_resource.uid(dscl_get(@user_info, :uid)) - @current_resource.gid(dscl_get(@user_info, :gid)) - @current_resource.home(dscl_get(@user_info, :home)) - @current_resource.shell(dscl_get(@user_info, :shell)) - @current_resource.comment(dscl_get(@user_info, :comment)) - @authentication_authority = dscl_get(@user_info, :auth_authority) - - if @new_resource.password && dscl_get(@user_info, :password) == "********" + if user_info + current_resource.uid(dscl_get(user_info, :uid)) + current_resource.gid(dscl_get(user_info, :gid)) + current_resource.home(dscl_get(user_info, :home)) + current_resource.shell(dscl_get(user_info, :shell)) + current_resource.comment(dscl_get(user_info, :comment)) + @authentication_authority = dscl_get(user_info, :auth_authority) + + if new_resource.password && dscl_get(user_info, :password) == "********" # A password is set. Let's get the password information from shadow file - shadow_hash_binary = dscl_get(@user_info, :shadow_hash) + shadow_hash_binary = dscl_get(user_info, :shadow_hash) # Calling shell_out directly since we want to give an input stream shadow_hash_xml = convert_binary_plist_to_xml(shadow_hash_binary.string) @@ -132,26 +136,26 @@ user password using shadow hash.") if shadow_hash["SALTED-SHA512"] # Convert the shadow value from Base64 encoding to hex before consuming them @password_shadow_conversion_algorithm = "SALTED-SHA512" - @current_resource.password(shadow_hash["SALTED-SHA512"].string.unpack('H*').first) + current_resource.password(shadow_hash["SALTED-SHA512"].string.unpack("H*").first) elsif shadow_hash["SALTED-SHA512-PBKDF2"] @password_shadow_conversion_algorithm = "SALTED-SHA512-PBKDF2" # Convert the entropy from Base64 encoding to hex before consuming them - @current_resource.password(shadow_hash["SALTED-SHA512-PBKDF2"]["entropy"].string.unpack('H*').first) - @current_resource.iterations(shadow_hash["SALTED-SHA512-PBKDF2"]["iterations"]) + current_resource.password(shadow_hash["SALTED-SHA512-PBKDF2"]["entropy"].string.unpack("H*").first) + current_resource.iterations(shadow_hash["SALTED-SHA512-PBKDF2"]["iterations"]) # Convert the salt from Base64 encoding to hex before consuming them - @current_resource.salt(shadow_hash["SALTED-SHA512-PBKDF2"]["salt"].string.unpack('H*').first) + current_resource.salt(shadow_hash["SALTED-SHA512-PBKDF2"]["salt"].string.unpack("H*").first) else raise(Chef::Exceptions::User,"Unknown shadow_hash format: #{shadow_hash.keys.join(' ')}") end end - convert_group_name if @new_resource.gid + convert_group_name if new_resource.gid else @user_exists = false - Chef::Log.debug("#{@new_resource} user does not exist") + Chef::Log.debug("#{new_resource} user does not exist") end - @current_resource + current_resource end # @@ -190,15 +194,16 @@ user password using shadow hash.") # Create a user using dscl # def dscl_create_user - run_dscl("create /Users/#{@new_resource.username}") + run_dscl("create /Users/#{new_resource.username}") end # # Saves the specified Chef user `comment` into RealName attribute - # of Mac user. + # of Mac user. If `comment` is not specified, it takes `username` value. # def dscl_create_comment - run_dscl("create /Users/#{@new_resource.username} RealName '#{@new_resource.comment}'") + comment = new_resource.comment || new_resource.username + run_dscl("create /Users/#{new_resource.username} RealName '#{comment}'") end # @@ -207,13 +212,14 @@ user password using shadow hash.") # from 200 if `system` is set, 500 otherwise. # def dscl_set_uid - @new_resource.uid(get_free_uid) if (@new_resource.uid.nil? || @new_resource.uid == '') + # XXX: mutates the new resource + new_resource.uid(get_free_uid) if (new_resource.uid.nil? || new_resource.uid == "") - if uid_used?(@new_resource.uid) - raise(Chef::Exceptions::RequestedUIDUnavailable, "uid #{@new_resource.uid} is already in use") + if uid_used?(new_resource.uid) + raise(Chef::Exceptions::RequestedUIDUnavailable, "uid #{new_resource.uid} is already in use") end - run_dscl("create /Users/#{@new_resource.username} UniqueID #{@new_resource.uid}") + run_dscl("create /Users/#{new_resource.username} UniqueID #{new_resource.uid}") end # @@ -222,7 +228,7 @@ user password using shadow hash.") # def get_free_uid(search_limit=1000) uid = nil - base_uid = @new_resource.system ? 200 : 500 + base_uid = new_resource.system ? 200 : 500 next_uid_guess = base_uid users_uids = run_dscl("list /Users uid") while(next_uid_guess < search_limit + base_uid) @@ -248,7 +254,7 @@ user password using shadow hash.") tmap end if uid_map[uid.to_s] - unless uid_map[uid.to_s] == @new_resource.username.to_s + unless uid_map[uid.to_s] == new_resource.username.to_s return true end end @@ -257,18 +263,23 @@ user password using shadow hash.") # # Sets the group id for the user using dscl. Fails if a group doesn't - # exist on the system with given group id. + # exist on the system with given group id. If `gid` is not specified, it + # sets a default Mac user group "staff", with id 20. # def dscl_set_gid - unless @new_resource.gid && @new_resource.gid.to_s.match(/^\d+$/) + if new_resource.gid.nil? + # XXX: mutates the new resource + new_resource.gid(20) + elsif !new_resource.gid.to_s.match(/^\d+$/) begin - possible_gid = run_dscl("read /Groups/#{@new_resource.gid} PrimaryGroupID").split(" ").last + possible_gid = run_dscl("read /Groups/#{new_resource.gid} PrimaryGroupID").split(" ").last rescue Chef::Exceptions::DsclCommandFailed => e - raise Chef::Exceptions::GroupIDNotFound.new("Group not found for #{@new_resource.gid} when creating user #{@new_resource.username}") + raise Chef::Exceptions::GroupIDNotFound.new("Group not found for #{new_resource.gid} when creating user #{new_resource.username}") end - @new_resource.gid(possible_gid) if possible_gid && possible_gid.match(/^\d+$/) + # XXX: mutates the new resource + new_resource.gid(possible_gid) if possible_gid && possible_gid.match(/^\d+$/) end - run_dscl("create /Users/#{@new_resource.username} PrimaryGroupID '#{@new_resource.gid}'") + run_dscl("create /Users/#{new_resource.username} PrimaryGroupID '#{new_resource.gid}'") end # @@ -276,15 +287,15 @@ user password using shadow hash.") # directory is managed (moved / created) for the user. # def dscl_set_home - if @new_resource.home.nil? || @new_resource.home.empty? - run_dscl("delete /Users/#{@new_resource.username} NFSHomeDirectory") + if new_resource.home.nil? || new_resource.home.empty? + run_dscl("delete /Users/#{new_resource.username} NFSHomeDirectory") return end - if @new_resource.supports[:manage_home] + if new_resource.supports[:manage_home] validate_home_dir_specification! - if (@current_resource.home == @new_resource.home) && !new_home_exists? + if (current_resource.home == new_resource.home) && !new_home_exists? ditto_home elsif !current_home_exists? && !new_home_exists? ditto_home @@ -292,49 +303,49 @@ user password using shadow hash.") move_home end end - run_dscl("create /Users/#{@new_resource.username} NFSHomeDirectory '#{@new_resource.home}'") + run_dscl("create /Users/#{new_resource.username} NFSHomeDirectory '#{new_resource.home}'") end def validate_home_dir_specification! - unless @new_resource.home =~ /^\// - raise(Chef::Exceptions::InvalidHomeDirectory,"invalid path spec for User: '#{@new_resource.username}', home directory: '#{@new_resource.home}'") + unless new_resource.home =~ /^\// + raise(Chef::Exceptions::InvalidHomeDirectory,"invalid path spec for User: '#{new_resource.username}', home directory: '#{new_resource.home}'") end end def current_home_exists? - ::File.exist?("#{@current_resource.home}") + ::File.exist?("#{current_resource.home}") end def new_home_exists? - ::File.exist?("#{@new_resource.home}") + ::File.exist?("#{new_resource.home}") end def ditto_home skel = "/System/Library/User Template/English.lproj" raise(Chef::Exceptions::User,"can't find skel at: #{skel}") unless ::File.exists?(skel) - shell_out! "ditto '#{skel}' '#{@new_resource.home}'" - ::FileUtils.chown_R(@new_resource.username,@new_resource.gid.to_s,@new_resource.home) + shell_out! "ditto '#{skel}' '#{new_resource.home}'" + ::FileUtils.chown_R(new_resource.username,new_resource.gid.to_s,new_resource.home) end def move_home - Chef::Log.debug("#{@new_resource} moving #{self} home from #{@current_resource.home} to #{@new_resource.home}") + Chef::Log.debug("#{new_resource} moving #{self} home from #{current_resource.home} to #{new_resource.home}") - src = @current_resource.home - FileUtils.mkdir_p(@new_resource.home) + src = current_resource.home + FileUtils.mkdir_p(new_resource.home) files = ::Dir.glob("#{Chef::Util::PathHelper.escape_glob(src)}/*", ::File::FNM_DOTMATCH) - ["#{src}/.","#{src}/.."] - ::FileUtils.mv(files,@new_resource.home, :force => true) + ::FileUtils.mv(files,new_resource.home, :force => true) ::FileUtils.rmdir(src) - ::FileUtils.chown_R(@new_resource.username,@new_resource.gid.to_s,@new_resource.home) + ::FileUtils.chown_R(new_resource.username,new_resource.gid.to_s,new_resource.home) end # # Sets the shell for the user using dscl. # def dscl_set_shell - if @new_resource.shell || ::File.exists?("#{@new_resource.shell}") - run_dscl("create /Users/#{@new_resource.username} UserShell '#{@new_resource.shell}'") + if new_resource.shell || ::File.exists?("#{new_resource.shell}") + run_dscl("create /Users/#{new_resource.username} UserShell '#{new_resource.shell}'") else - run_dscl("create /Users/#{@new_resource.username} UserShell '/usr/bin/false'") + run_dscl("create /Users/#{new_resource.username} UserShell '/usr/bin/false'") end end @@ -345,17 +356,17 @@ user password using shadow hash.") # def set_password # Return if there is no password to set - return if @new_resource.password.nil? + return if new_resource.password.nil? shadow_info = prepare_password_shadow_info # Shadow info is saved as binary plist. Convert the info to binary plist. shadow_info_binary = StringIO.new command = Mixlib::ShellOut.new("plutil -convert binary1 -o - -", - :input => shadow_info.to_plist, :live_stream => shadow_info_binary) + :input => shadow_info.to_plist, :live_stream => shadow_info_binary) command.run_command - if @user_info.nil? + if user_info.nil? # User is just created. read_user_info() will read the fresh information # for the user with a cache flush. However with experimentation we've seen # that dscl cache is not immediately updated after the creation of the user @@ -365,8 +376,8 @@ user password using shadow hash.") end # Replace the shadow info in user's plist - dscl_set(@user_info, :shadow_hash, shadow_info_binary) - save_user_info(@user_info) + dscl_set(user_info, :shadow_hash, shadow_info_binary) + save_user_info(user_info) end # @@ -379,33 +390,33 @@ user password using shadow hash.") iterations = nil if mac_osx_version_10_7? - hash_value = if salted_sha512?(@new_resource.password) - @new_resource.password - else - # Create a random 4 byte salt - salt = OpenSSL::Random.random_bytes(4) - encoded_password = OpenSSL::Digest::SHA512.hexdigest(salt + @new_resource.password) - hash_value = salt.unpack('H*').first + encoded_password - end + hash_value = if salted_sha512?(new_resource.password) + new_resource.password + else + # Create a random 4 byte salt + salt = OpenSSL::Random.random_bytes(4) + encoded_password = OpenSSL::Digest::SHA512.hexdigest(salt + new_resource.password) + hash_value = salt.unpack("H*").first + encoded_password + end shadow_info["SALTED-SHA512"] = StringIO.new shadow_info["SALTED-SHA512"].string = convert_to_binary(hash_value) shadow_info else - if salted_sha512_pbkdf2?(@new_resource.password) - entropy = convert_to_binary(@new_resource.password) - salt = convert_to_binary(@new_resource.salt) - iterations = @new_resource.iterations + if salted_sha512_pbkdf2?(new_resource.password) + entropy = convert_to_binary(new_resource.password) + salt = convert_to_binary(new_resource.salt) + iterations = new_resource.iterations else salt = OpenSSL::Random.random_bytes(32) - iterations = @new_resource.iterations # Use the default if not specified by the user + iterations = new_resource.iterations # Use the default if not specified by the user entropy = OpenSSL::PKCS5::pbkdf2_hmac( - @new_resource.password, + new_resource.password, salt, iterations, 128, - OpenSSL::Digest::SHA512.new + OpenSSL::Digest::SHA512.new, ) end @@ -427,43 +438,43 @@ user password using shadow hash.") # and deleting home directory if needed. # def remove_user - if @new_resource.supports[:manage_home] + if new_resource.supports[:manage_home] # Remove home directory - FileUtils.rm_rf(@current_resource.home) + FileUtils.rm_rf(current_resource.home) end # Remove the user from its groups run_dscl("list /Groups").each_line do |group| if member_of_group?(group.chomp) - run_dscl("delete /Groups/#{group.chomp} GroupMembership '#{@new_resource.username}'") + run_dscl("delete /Groups/#{group.chomp} GroupMembership '#{new_resource.username}'") end end # Remove user account - run_dscl("delete /Users/#{@new_resource.username}") + run_dscl("delete /Users/#{new_resource.username}") end # # Locks the user. # def lock_user - run_dscl("append /Users/#{@new_resource.username} AuthenticationAuthority ';DisabledUser;'") + run_dscl("append /Users/#{new_resource.username} AuthenticationAuthority ';DisabledUser;'") end # # Unlocks the user # def unlock_user - auth_string = @authentication_authority.gsub(/AuthenticationAuthority: /,"").gsub(/;DisabledUser;/,"").strip - run_dscl("create /Users/#{@new_resource.username} AuthenticationAuthority '#{auth_string}'") + auth_string = authentication_authority.gsub(/AuthenticationAuthority: /,"").gsub(/;DisabledUser;/,"").strip + run_dscl("create /Users/#{new_resource.username} AuthenticationAuthority '#{auth_string}'") end # # Returns true if the user is locked, false otherwise. # def locked? - if @authentication_authority - !!(@authentication_authority =~ /DisabledUser/ ) + if authentication_authority + !!(authentication_authority =~ /DisabledUser/ ) else false end @@ -485,11 +496,11 @@ user password using shadow hash.") # given attribute. # def diverged?(parameter) - parameter_updated?(parameter) && (not @new_resource.send(parameter).nil?) + parameter_updated?(parameter) && (not new_resource.send(parameter).nil?) end def parameter_updated?(parameter) - not (@new_resource.send(parameter) == @current_resource.send(parameter)) + not (new_resource.send(parameter) == current_resource.send(parameter)) end # @@ -500,11 +511,11 @@ user password using shadow hash.") # type of the password specified. # def diverged_password? - return false if @new_resource.password.nil? + return false if new_resource.password.nil? # Dscl provider supports both plain text passwords and shadow hashes. if mac_osx_version_10_7? - if salted_sha512?(@new_resource.password) + if salted_sha512?(new_resource.password) diverged?(:password) else !salted_sha512_password_match? @@ -514,14 +525,14 @@ user password using shadow hash.") # will be updated when the user logs in. So it's possible that we will have # SALTED-SHA512 password in the current_resource. In that case we will force # password to be updated. - return true if salted_sha512?(@current_resource.password) + return true if salted_sha512?(current_resource.password) # Some system users don't have salts; this can happen if the system is # upgraded and the user hasn't logged in yet. In this case, we will force # the password to be updated. - return true if @current_resource.salt.nil? + return true if current_resource.salt.nil? - if salted_sha512_pbkdf2?(@new_resource.password) + if salted_sha512_pbkdf2?(new_resource.password) diverged?(:password) || diverged?(:salt) || diverged?(:iterations) else !salted_sha512_pbkdf2_password_match? @@ -543,7 +554,7 @@ user password using shadow hash.") # GroupMembership: root admin etc members = membership_info.split(" ") members.shift # Get rid of GroupMembership: string - members.include?(@new_resource.username) + members.include?(new_resource.username) end # @@ -559,7 +570,7 @@ user password using shadow hash.") :comment => "realname", :password => "passwd", :auth_authority => "authentication_authority", - :shadow_hash => "ShadowHashData" + :shadow_hash => "ShadowHashData", }.freeze # Directory where the user plist files are stored for versions 10.7 and above @@ -577,7 +588,7 @@ user password using shadow hash.") shell_out("dscacheutil '-flushcache'") begin - user_plist_file = "#{USER_PLIST_DIRECTORY}/#{@new_resource.username}.plist" + user_plist_file = "#{USER_PLIST_DIRECTORY}/#{new_resource.username}.plist" user_plist_info = run_plutil("convert xml1 -o - #{user_plist_file}") user_info = Plist::parse_xml(user_plist_info) rescue Chef::Exceptions::PlistUtilCommandFailed @@ -591,7 +602,7 @@ user password using shadow hash.") # in DSCL_PROPERTY_MAP to the disk. # def save_user_info(user_info) - user_plist_file = "#{USER_PLIST_DIRECTORY}/#{@new_resource.username}.plist" + user_plist_file = "#{USER_PLIST_DIRECTORY}/#{new_resource.username}.plist" Plist::Emit.save_plist(user_info, user_plist_file) run_plutil("convert binary1 #{user_plist_file}") end @@ -653,7 +664,7 @@ user password using shadow hash.") result = shell_out("plutil -#{args.join(' ')}") raise(Chef::Exceptions::PlistUtilCommandFailed,"plutil error: #{result.inspect}") unless result.exitstatus == 0 if result.stdout.encoding == Encoding::ASCII_8BIT - result.stdout.encode("utf-8", "binary", :undef => :replace, :invalid => :replace, :replace => '?') + result.stdout.encode("utf-8", "binary", :undef => :replace, :invalid => :replace, :replace => "?") else result.stdout end @@ -664,7 +675,7 @@ user password using shadow hash.") end def convert_to_binary(string) - string.unpack('a2'*(string.size/2)).collect { |i| i.hex.chr }.join + string.unpack("a2"*(string.size/2)).collect { |i| i.hex.chr }.join end def salted_sha512?(string) @@ -673,9 +684,9 @@ user password using shadow hash.") def salted_sha512_password_match? # Salt is included in the first 4 bytes of shadow data - salt = @current_resource.password.slice(0,8) - shadow = OpenSSL::Digest::SHA512.hexdigest(convert_to_binary(salt) + @new_resource.password) - @current_resource.password == salt + shadow + salt = current_resource.password.slice(0,8) + shadow = OpenSSL::Digest::SHA512.hexdigest(convert_to_binary(salt) + new_resource.password) + current_resource.password == salt + shadow end def salted_sha512_pbkdf2?(string) @@ -683,15 +694,15 @@ user password using shadow hash.") end def salted_sha512_pbkdf2_password_match? - salt = convert_to_binary(@current_resource.salt) + salt = convert_to_binary(current_resource.salt) OpenSSL::PKCS5::pbkdf2_hmac( - @new_resource.password, + new_resource.password, salt, - @current_resource.iterations, + current_resource.iterations, 128, - OpenSSL::Digest::SHA512.new - ).unpack('H*').first == @current_resource.password + OpenSSL::Digest::SHA512.new, + ).unpack("H*").first == current_resource.password end end diff --git a/lib/chef/provider/user/pw.rb b/lib/chef/provider/user/pw.rb index fe71e93561..60df7d55f8 100644 --- a/lib/chef/provider/user/pw.rb +++ b/lib/chef/provider/user/pw.rb @@ -16,12 +16,13 @@ # limitations under the License. # -require 'chef/provider/user' +require "chef/provider/user" class Chef class Provider class User class Pw < Chef::Provider::User + provides :user, platform: %w{freebsd} def load_current_resource super @@ -70,11 +71,11 @@ class Chef opts = " #{@new_resource.username}" field_list = { - 'comment' => "-c", - 'home' => "-d", - 'gid' => "-g", - 'uid' => "-u", - 'shell' => "-s" + "comment" => "-c", + "home" => "-d", + "gid" => "-g", + "uid" => "-u", + "shell" => "-s", } field_list.sort{ |a,b| a[0] <=> b[0] }.each do |field, option| field_symbol = field.to_sym diff --git a/lib/chef/provider/user/solaris.rb b/lib/chef/provider/user/solaris.rb index d480acaced..18f44523ff 100644 --- a/lib/chef/provider/user/solaris.rb +++ b/lib/chef/provider/user/solaris.rb @@ -1,7 +1,9 @@ # # Author:: Stephen Nelson-Smith (<sns@opscode.com>) # Author:: Jon Ramsey (<jonathon.ramsey@gmail.com>) +# Author:: Dave Eddy (<dave@daveeddy.com>) # Copyright:: Copyright (c) 2012 Opscode, Inc. +# Copyright:: Copyright 2015, Dave Eddy # License:: Apache License, Version 2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -16,12 +18,13 @@ # See the License for the specific language governing permissions and # limitations under the License. -require 'chef/provider/user/useradd' +require "chef/provider/user/useradd" class Chef class Provider class User class Solaris < Chef::Provider::User::Useradd + provides :user, platform: %w{omnios solaris2} UNIVERSAL_OPTIONS = [[:comment, "-c"], [:gid, "-g"], [:shell, "-s"], [:uid, "-u"]] attr_writer :password_file @@ -41,6 +44,32 @@ class Chef super end + def check_lock + shadow_line = shell_out!("getent", "shadow", new_resource.username).stdout.strip rescue nil + + # if the command fails we return nil, this can happen if the user + # in question doesn't exist + return nil if shadow_line.nil? + + # convert "dave:NP:16507::::::\n" to "NP" + fields = shadow_line.split(":") + + # '*LK*...' and 'LK' are both considered locked, + # so look for LK at the beginning of the shadow entry + # optionally surrounded by '*' + @locked = !!fields[1].match(/^\*?LK\*?/) + + @locked + end + + def lock_user + shell_out!("passwd", "-l", new_resource.username) + end + + def unlock_user + shell_out!("passwd", "-u", new_resource.username) + end + private def manage_password @@ -65,9 +94,10 @@ class Chef buffer.close # FIXME: mostly duplicates code with file provider deploying a file - mode = ::File.stat(@password_file).mode & 07777 - uid = ::File.stat(@password_file).uid - gid = ::File.stat(@password_file).gid + s = ::File.stat(@password_file) + mode = s.mode & 07777 + uid = s.uid + gid = s.gid FileUtils.chown uid, gid, buffer.path FileUtils.chmod mode, buffer.path diff --git a/lib/chef/provider/user/useradd.rb b/lib/chef/provider/user/useradd.rb index cc770c0be2..da6606bc14 100644 --- a/lib/chef/provider/user/useradd.rb +++ b/lib/chef/provider/user/useradd.rb @@ -16,13 +16,14 @@ # limitations under the License. # -require 'pathname' -require 'chef/provider/user' +require "pathname" +require "chef/provider/user" class Chef class Provider class User class Useradd < Chef::Provider::User + provides :user UNIVERSAL_OPTIONS = [[:comment, "-c"], [:gid, "-g"], [:password, "-p"], [:shell, "-s"], [:uid, "-u"]] @@ -62,7 +63,7 @@ class Chef raise Chef::Exceptions::User, "Cannot determine if #{@new_resource} is locked!" if passwd_s.stdout.empty? - status_line = passwd_s.stdout.split(' ') + status_line = passwd_s.stdout.split(" ") case status_line[1] when /^P/ @locked = false @@ -74,11 +75,11 @@ class Chef unless passwd_s.exitstatus == 0 raise_lock_error = false - if ['redhat', 'centos'].include?(node[:platform]) - passwd_version_check = shell_out!('rpm -q passwd') + if ["redhat", "centos"].include?(node[:platform]) + passwd_version_check = shell_out!("rpm -q passwd") passwd_version = passwd_version_check.stdout.chomp - unless passwd_version == 'passwd-0.73-1' + unless passwd_version == "passwd-0.73-1" raise_lock_error = true end else diff --git a/lib/chef/provider/user/windows.rb b/lib/chef/provider/user/windows.rb index e282a11d45..0851c2272a 100644 --- a/lib/chef/provider/user/windows.rb +++ b/lib/chef/provider/user/windows.rb @@ -16,10 +16,10 @@ # limitations under the License. # -require 'chef/provider/user' -require 'chef/exceptions' +require "chef/provider/user" +require "chef/exceptions" if RUBY_PLATFORM =~ /mswin|mingw32|windows/ - require 'chef/util/windows/net_user' + require "chef/util/windows/net_user" end class Chef @@ -35,6 +35,10 @@ class Chef end def load_current_resource + if @new_resource.gid + Chef::Log.warn("The 'gid' attribute is not implemented by the Windows platform. Please use the 'group' resource to assign a user to a group.") + end + @current_resource = Chef::Resource::User.new(@new_resource.name) @current_resource.username(@new_resource.username) user_info = nil @@ -42,7 +46,6 @@ class Chef user_info = @net_user.get_info @current_resource.uid(user_info[:user_id]) - @current_resource.gid(user_info[:primary_group_id]) @current_resource.comment(user_info[:full_name]) @current_resource.home(user_info[:home_dir]) @current_resource.shell(user_info[:script_path]) @@ -65,7 +68,7 @@ class Chef Chef::Log.debug("#{@new_resource} password has changed") return true end - [ :uid, :gid, :comment, :home, :shell ].any? do |user_attrib| + [ :uid, :comment, :home, :shell ].any? do |user_attrib| !@new_resource.send(user_attrib).nil? && @new_resource.send(user_attrib) != @current_resource.send(user_attrib) end end @@ -98,12 +101,11 @@ class Chef opts = {:name => @new_resource.username} field_list = { - 'comment' => 'full_name', - 'home' => 'home_dir', - 'gid' => 'primary_group_id', - 'uid' => 'user_id', - 'shell' => 'script_path', - 'password' => 'password' + "comment" => "full_name", + "home" => "home_dir", + "uid" => "user_id", + "shell" => "script_path", + "password" => "password", } field_list.sort{ |a,b| a[0] <=> b[0] }.each do |field, option| diff --git a/lib/chef/provider/windows_script.rb b/lib/chef/provider/windows_script.rb index e600bb2837..342158fa06 100644 --- a/lib/chef/provider/windows_script.rb +++ b/lib/chef/provider/windows_script.rb @@ -16,18 +16,20 @@ # limitations under the License. # -require 'chef/provider/script' -require 'chef/mixin/windows_architecture_helper' +require "chef/provider/script" +require "chef/mixin/windows_architecture_helper" class Chef class Provider class WindowsScript < Chef::Provider::Script + attr_reader :is_forced_32bit + protected include Chef::Mixin::WindowsArchitectureHelper - def initialize( new_resource, run_context, script_extension='') + def initialize( new_resource, run_context, script_extension="") super( new_resource, run_context ) @script_extension = script_extension @@ -36,11 +38,7 @@ class Chef @is_wow64 = wow64_architecture_override_required?(run_context.node, target_architecture) - # if the user wants to run the script 32 bit && we are on a 64bit windows system && we are running a 64bit ruby ==> fail - if ( target_architecture == :i386 ) && node_windows_architecture(run_context.node) == :x86_64 && !is_i386_process_on_x86_64_windows? - raise Chef::Exceptions::Win32ArchitectureIncorrect, - "Support for the i386 architecture from a 64-bit Ruby runtime is not yet implemented" - end + @is_forced_32bit = forced_32bit_override_required?(run_context.node, target_architecture) end public |