diff options
Diffstat (limited to 'lib/chef/provider/package.rb')
-rw-r--r-- | lib/chef/provider/package.rb | 328 |
1 files changed, 229 insertions, 99 deletions
diff --git a/lib/chef/provider/package.rb b/lib/chef/provider/package.rb index ca9b526920..133f87dad9 100644 --- a/lib/chef/provider/package.rb +++ b/lib/chef/provider/package.rb @@ -1,6 +1,6 @@ # # Author:: Adam Jacob (<adam@chef.io>) -# Copyright:: Copyright 2008-2016, Chef Software, Inc. +# Copyright:: Copyright 2008-2018, Chef Software Inc. # License:: Apache License, Version 2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,16 +17,16 @@ # require "chef/mixin/shell_out" -require "chef/mixin/command" require "chef/mixin/subclass_directive" require "chef/log" require "chef/file_cache" require "chef/platform" +require "chef/decorator/lazy_array" +require "shellwords" class Chef class Provider class Package < Chef::Provider - include Chef::Mixin::Command include Chef::Mixin::ShellOut extend Chef::Mixin::SubclassDirective @@ -34,6 +34,9 @@ class Chef 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 + # keeps package_names_for_targets and versions_for_targets indexed the same as package_name at + # the cost of having the subclass needing to deal with nils + subclass_directive :allow_nils # # Hook that subclasses use to populate the candidate_version(s) @@ -46,28 +49,27 @@ class Chef @candidate_version = nil end - def whyrun_supported? - true + def options + new_resource.options 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 + 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 + def load_current_resource; end def define_resource_requirements # XXX: upgrade with a specific version doesn't make a whole lot of sense, but why don't we throw this anyway if it happens? # if not, shouldn't we raise to tell the user to use install instead of upgrade if they want to pin a version? requirements.assert(:install) do |a| a.assertion { candidates_exist_for_all_forced_changes? } - a.failure_message(Chef::Exceptions::Package, "No version specified, and no candidate version available for #{forced_packages_missing_candidates.join(", ")}") - a.whyrun("Assuming a repository that offers #{forced_packages_missing_candidates.join(", ")} would have been configured") + a.failure_message(Chef::Exceptions::Package, "No version specified, and no candidate version available for #{forced_packages_missing_candidates.join(', ')}") + a.whyrun("Assuming a repository that offers #{forced_packages_missing_candidates.join(', ')} would have been configured") end # XXX: Does it make sense to pass in a source with :upgrade? Probably @@ -75,19 +77,19 @@ class Chef # so we'll just leave things as-is for now. requirements.assert(:upgrade, :install) do |a| a.assertion { candidates_exist_for_all_uninstalled? || new_resource.source } - 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") + 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 - def action_install + action :install do unless target_version_array.any? - Chef::Log.debug("#{@new_resource} is already installed - nothing to do") + logger.trace("#{new_resource} is already installed - nothing to do") return end # @todo: move the preseed code out of the base class (and complete the fix for Array of preseeds? ugh...) - if @new_resource.response_file + if new_resource.response_file if preseed_file = get_preseed_file(package_names_for_targets, versions_for_targets) converge_by("preseed package #{package_names_for_targets}") do preseed_package(preseed_file) @@ -99,7 +101,7 @@ class Chef 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}") + logger.info("#{new_resource} installed #{package_names_for_targets} at #{versions_for_targets}") end end @@ -115,9 +117,9 @@ class Chef private :install_description - def action_upgrade - if !target_version_array.any? - Chef::Log.debug("#{@new_resource} no versions to upgrade - nothing to do") + action :upgrade do + unless target_version_array.any? + logger.trace("#{new_resource} no versions to upgrade - nothing to do") return end @@ -126,7 +128,7 @@ class Chef 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}") + logger.info("#{new_resource} upgraded#{log_allow_downgrade} #{package_names_for_targets} to #{versions_for_targets}") end end @@ -145,17 +147,17 @@ class Chef private :upgrade_description - def action_remove + action :remove do if removing_package? - description = @new_resource.version ? "version #{@new_resource.version} of " : "" - converge_by("remove #{description}package #{@current_resource.package_name}") do - multipackage_api_adapter(@current_resource.package_name, @new_resource.version) do |name, version| + description = new_resource.version ? "version #{new_resource.version} of " : "" + 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") + logger.info("#{new_resource} removed") end else - Chef::Log.debug("#{@new_resource} package does not exist - nothing to do") + logger.trace("#{new_resource} package does not exist - nothing to do") end end @@ -180,43 +182,86 @@ class Chef end end - def action_purge + action :purge do if removing_package? - description = @new_resource.version ? "version #{@new_resource.version} of" : "" - converge_by("purge #{description} package #{@current_resource.package_name}") do - multipackage_api_adapter(@current_resource.package_name, @new_resource.version) do |name, version| + description = new_resource.version ? "version #{new_resource.version} of" : "" + converge_by("purge #{description} package #{current_resource.package_name}") do + multipackage_api_adapter(current_resource.package_name, new_resource.version) do |name, version| purge_package(name, version) end - Chef::Log.info("#{@new_resource} purged") + logger.info("#{new_resource} purged") end end end - def action_reconfig - if @current_resource.version == nil then - Chef::Log.debug("#{@new_resource} is NOT installed - nothing to do") + action :reconfig do + if current_resource.version.nil? + logger.trace("#{new_resource} is NOT installed - nothing to do") return end - unless @new_resource.response_file then - Chef::Log.debug("#{@new_resource} no response_file provided - nothing to do") + unless new_resource.response_file + logger.trace("#{new_resource} no response_file provided - nothing to do") return end - if preseed_file = get_preseed_file(@new_resource.package_name, @current_resource.version) - converge_by("reconfigure package #{@new_resource.package_name}") do + 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) - multipackage_api_adapter(@new_resource.package_name, @current_resource.version) do |name, 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") + logger.info("#{new_resource} reconfigured") + end + else + logger.trace("#{new_resource} preseeding has not changed - nothing to do") + end + end + + def action_lock + packages_locked = if respond_to?(:packages_all_locked?, true) + packages_all_locked?(Array(new_resource.package_name), Array(new_resource.version)) + else + package_locked(new_resource.package_name, new_resource.version) + end + unless packages_locked + description = new_resource.version ? "version #{new_resource.version} of " : "" + converge_by("lock #{description}package #{current_resource.package_name}") do + multipackage_api_adapter(current_resource.package_name, new_resource.version) do |name, version| + lock_package(name, version) + logger.info("#{new_resource} locked") + end end else - Chef::Log.debug("#{@new_resource} preseeding has not changed - nothing to do") + logger.trace("#{new_resource} is already locked") end end + def action_unlock + packages_unlocked = if respond_to?(:packages_all_unlocked?, true) + packages_all_unlocked?(Array(new_resource.package_name), Array(new_resource.version)) + else + !package_locked(new_resource.package_name, new_resource.version) + end + unless packages_unlocked + description = new_resource.version ? "version #{new_resource.version} of " : "" + converge_by("unlock #{description}package #{current_resource.package_name}") do + multipackage_api_adapter(current_resource.package_name, new_resource.version) do |name, version| + unlock_package(name, version) + logger.info("#{new_resource} unlocked") + end + end + else + logger.trace("#{new_resource} is already unlocked") + end + end + + # for multipackage just implement packages_all_[un]locked? properly and omit implementing this API + def package_locked(name, version) + raise Chef::Exceptions::UnsupportedAction, "#{self} has no way to detect if package is locked" + end + # @todo use composition rather than inheritance def multipackage_api_adapter(name, version) @@ -251,21 +296,94 @@ class Chef raise( Chef::Exceptions::UnsupportedAction, "#{self} does not support :reconfig" ) end + def lock_package(name, version) + raise( Chef::Exceptions::UnsupportedAction, "#{self} does not support :lock" ) + end + + def unlock_package(name, version) + raise( Chef::Exceptions::UnsupportedAction, "#{self} does not support :unlock" ) + end + # used by subclasses. deprecated. use #a_to_s instead. def expand_options(options) - options ? " #{options}" : "" + # its deprecated but still work to do to deprecate it fully + #Chef.deprecated(:package_misc, "expand_options is deprecated, use shell_out_compact or shell_out_compact_timeout instead") + if options + " #{options.is_a?(Array) ? Shellwords.join(options) : options}" + else + "" + end end - # this is public and overridden by subclasses (rubygems package implements '>=' and '~>' operators) - def target_version_already_installed?(current_version, new_version) - new_version == current_version + # Check the current_version against either the candidate_version or the new_version + # + # For some reason the windows provider subclasses this (to implement passing Arrays to + # versions for some reason other than multipackage stuff, which is mildly terrifying). + # + # This MUST have 'equality' semantics -- the exact thing matches the exact thing. + # + # The name is not just bad, but i find it completely misleading, consider: + # + # target_version_already_installed?(current_version, new_version) + # target_version_already_installed?(current_version, candidate_version) + # + # Which of those is the 'target_version'? I'd say the new_version and I'm confused when + # i see it called with the candidate_version. + # + # `version_equals?(v1, v2)` would be a better name. + # + # Note that most likely we need a spaceship operator on versions that subclasses can implement + # and we should have `version_compare(v1, v2)` that returns `v1 <=> v2`. + + # This method performs a strict equality check between two strings representing version numbers + # + # This function will eventually be deprecated in favour of the below version_equals function. + + def target_version_already_installed?(current_version, target_version) + version_equals?(current_version, target_version) + end + + # Note that most likely we need a spaceship operator on versions that subclasses can implement + # and we should have `version_compare(v1, v2)` that returns `v1 <=> v2`. + + # This method performs a strict equality check between two strings representing version numbers + # + def version_equals?(v1, v2) + return false unless v1 && v2 + v1 == v2 + end + + # This function compares two version numbers and returns 'spaceship operator' style results, ie: + # if v1 < v2 then return -1 + # if v1 = v2 then return 0 + # if v1 > v2 then return 1 + # if v1 and v2 are not comparable then return nil + # + # By default, this function will use Gem::Version comparison. Subclasses can reimplement this method + # for package-management system specific versions. + def version_compare(v1, v2) + gem_v1 = Gem::Version.new(v1) + gem_v2 = Gem::Version.new(v2) + + gem_v1 <=> gem_v2 + end + + # Check the current_version against the new_resource.version, possibly using fuzzy + # matching criteria. + # + # Subclasses MAY override this to provide fuzzy matching on the resource ('>=' and '~>' stuff) + # + # `version_satisfied_by?(version, constraint)` might be a better name to make this generic. + # + def version_requirement_satisfied?(current_version, new_version) + target_version_already_installed?(current_version, new_version) end # @todo: extract apt/dpkg specific preseeding to a helper class def get_preseed_file(name, version) resource = preseed_resource(name, version) resource.run_action(:create) - Chef::Log.debug("#{@new_resource} fetched preseed file to #{resource.path}") + logger.trace("#{new_resource} fetched preseed file to #{resource.path}") if resource.updated_by_last_action? resource.path @@ -277,26 +395,26 @@ class Chef # @todo: extract apt/dpkg specific preseeding to a helper class def preseed_resource(name, version) # A directory in our cache to store this cookbook's preseed files in - file_cache_dir = Chef::FileCache.create_cache_path("preseed/#{@new_resource.cookbook_name}") + file_cache_dir = Chef::FileCache.create_cache_path("preseed/#{new_resource.cookbook_name}") # The full path where the preseed file will be stored cache_seed_to = "#{file_cache_dir}/#{name}-#{version}.seed" - Chef::Log.debug("#{@new_resource} fetching preseed file to #{cache_seed_to}") + logger.trace("#{new_resource} fetching preseed file to #{cache_seed_to}") - if template_available?(@new_resource.response_file) - Chef::Log.debug("#{@new_resource} fetching preseed file via Template") + if template_available?(new_resource.response_file) + logger.trace("#{new_resource} fetching preseed file via Template") remote_file = Chef::Resource::Template.new(cache_seed_to, run_context) - remote_file.variables(@new_resource.response_file_variables) - elsif cookbook_file_available?(@new_resource.response_file) - Chef::Log.debug("#{@new_resource} fetching preseed file via cookbook_file") + remote_file.variables(new_resource.response_file_variables) + elsif cookbook_file_available?(new_resource.response_file) + logger.trace("#{new_resource} fetching preseed file via cookbook_file") remote_file = Chef::Resource::CookbookFile.new(cache_seed_to, run_context) else - message = "No template or cookbook file found for response file #{@new_resource.response_file}" + message = "No template or cookbook file found for response file #{new_resource.response_file}" raise Chef::Exceptions::FileNotFound, message end - remote_file.cookbook_name = @new_resource.cookbook_name - remote_file.source(@new_resource.response_file) + remote_file.cookbook_name = new_resource.cookbook_name + remote_file.source(new_resource.response_file) remote_file.backup(false) remote_file end @@ -319,9 +437,12 @@ class Chef def package_names_for_targets package_names_for_targets = [] target_version_array.each_with_index do |target_version, i| - next if target_version.nil? - package_name = package_name_array[i] - package_names_for_targets.push(package_name) + if !target_version.nil? + package_name = package_name_array[i] + package_names_for_targets.push(package_name) + else + package_names_for_targets.push(nil) if allow_nils? + end end multipackage? ? package_names_for_targets : package_names_for_targets[0] end @@ -336,8 +457,11 @@ class Chef def versions_for_targets versions_for_targets = [] target_version_array.each_with_index do |target_version, i| - next if target_version.nil? - versions_for_targets.push(target_version) + if !target_version.nil? + versions_for_targets.push(target_version) + else + versions_for_targets.push(nil) if allow_nils? + end end multipackage? ? versions_for_targets : versions_for_targets[0] end @@ -354,33 +478,42 @@ class Chef each_package do |package_name, new_version, current_version, candidate_version| case action when :upgrade - - if !candidate_version - Chef::Log.debug("#{new_resource} #{package_name} has no candidate_version to upgrade to") + if version_equals?(current_version, new_version) + # this is an odd use case + logger.trace("#{new_resource} #{package_name} #{new_version} is already installed -- you are equality pinning with an :upgrade action, this may be deprecated in the future") + target_version_array.push(nil) + elsif version_equals?(current_version, candidate_version) + logger.trace("#{new_resource} #{package_name} #{candidate_version} is already installed") target_version_array.push(nil) - elsif current_version == candidate_version - Chef::Log.debug("#{new_resource} #{package_name} the #{candidate_version} is already installed") + elsif candidate_version.nil? + logger.trace("#{new_resource} #{package_name} has no candidate_version to upgrade to") + target_version_array.push(nil) + elsif current_version.nil? + logger.trace("#{new_resource} has no existing installed version. Installing install #{candidate_version}") + target_version_array.push(candidate_version) + elsif version_compare(current_version, candidate_version) == 1 && !allow_downgrade + logger.trace("#{new_resource} #{package_name} has installed version #{current_version}, which is newer than available version #{candidate_version}. Skipping...)") target_version_array.push(nil) else - Chef::Log.debug("#{new_resource} #{package_name} is out of date, will upgrade to #{candidate_version}") + logger.trace("#{new_resource} #{package_name} is out of date, will upgrade to #{candidate_version}") target_version_array.push(candidate_version) end when :install if new_version - if target_version_already_installed?(current_version, new_version) - Chef::Log.debug("#{new_resource} #{package_name} #{current_version} satisifies #{new_version} requirement") + if version_requirement_satisfied?(current_version, new_version) + logger.trace("#{new_resource} #{package_name} #{current_version} satisifies #{new_version} requirement") target_version_array.push(nil) else - Chef::Log.debug("#{new_resource} #{package_name} #{current_version} needs updating to #{new_version}") + logger.trace("#{new_resource} #{package_name} #{current_version} needs updating to #{new_version}") target_version_array.push(new_version) end elsif current_version.nil? - Chef::Log.debug("#{new_resource} #{package_name} not installed, installing #{candidate_version}") + logger.trace("#{new_resource} #{package_name} not installed, installing #{candidate_version}") target_version_array.push(candidate_version) else - Chef::Log.debug("#{new_resource} #{package_name} #{current_version} already installed") + logger.trace("#{new_resource} #{package_name} #{current_version} already installed") target_version_array.push(nil) end @@ -411,7 +544,7 @@ class Chef begin missing = [] each_package do |package_name, new_version, current_version, candidate_version| - missing.push(package_name) if candidate_version.nil? && current_version.nil? + missing.push(package_name) if current_version.nil? && candidate_version.nil? end missing end @@ -436,7 +569,7 @@ class Chef missing = [] each_package do |package_name, new_version, current_version, candidate_version| next if new_version.nil? || current_version.nil? - if candidate_version.nil? && !target_version_already_installed?(current_version, new_version) + if !version_requirement_satisfied?(current_version, new_version) && candidate_version.nil? missing.push(package_name) end end @@ -458,27 +591,29 @@ class Chef # @return [Boolean] if we're doing a multipackage install or not def multipackage? - new_resource.package_name.is_a?(Array) + @multipackage_bool ||= new_resource.package_name.is_a?(Array) end # @return [Array] package_name(s) as an array def package_name_array - [ new_resource.package_name ].flatten + @package_name_array ||= [ new_resource.package_name ].flatten end # @return [Array] candidate_version(s) as an array def candidate_version_array - [ candidate_version ].flatten + # NOTE: even with use_multipackage_api candidate_version may be a bare nil and need wrapping + # ( looking at you, dpkg provider... ) + Chef::Decorator::LazyArray.new { [ candidate_version ].flatten } end # @return [Array] current_version(s) as an array def current_version_array - [ current_resource.version ].flatten + @current_version_array ||= [ current_resource.version ].flatten end # @return [Array] new_version(s) as an array def new_version_array - [ new_resource.version ].flatten.map { |v| v.to_s.empty? ? nil : v } + @new_version_array ||= [ 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 @@ -486,11 +621,14 @@ class Chef # # @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 + @source_array ||= + begin + if new_resource.source.nil? + package_name_array.map { nil } + else + [ new_resource.source ].flatten + end + end end # Helper to handle use_package_name_for_source to convert names into local packages to install. @@ -503,7 +641,7 @@ class Chef 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.") + logger.trace("No package source specified, but #{package_name} exists on filesystem, using #{package_name} as source.") package_name else source @@ -523,8 +661,8 @@ class Chef end def allow_downgrade - if @new_resource.respond_to?("allow_downgrade") - @new_resource.allow_downgrade + if new_resource.respond_to?("allow_downgrade") + new_resource.allow_downgrade else false end @@ -539,27 +677,19 @@ class Chef end def add_timeout_option(command_args) + # this is deprecated but its not quite done yet + #Chef.deprecated(:package_misc, "shell_out_with_timeout and add_timeout_option are deprecated methods, use shell_out_compact_timeout instead") 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) + options[:timeout] = 900 unless options.key?(:timeout) args << options else - args << { :timeout => new_resource.timeout ? new_resource.timeout : 900 } + 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 |