diff options
author | Lamont Granquist <lamont@scriptkiddie.org> | 2015-02-04 11:41:05 -0800 |
---|---|---|
committer | Lamont Granquist <lamont@scriptkiddie.org> | 2015-02-04 11:41:05 -0800 |
commit | 6eae70225008c8b06601a8343a3f57cb10f3c065 (patch) | |
tree | 8580a82903860094def6c54c782022352739385f | |
parent | 0c05b112458ad62005e871dba22a7e44a845bf85 (diff) | |
parent | 4805025146896c537f3110aeceb1fba852897f8c (diff) | |
download | chef-6eae70225008c8b06601a8343a3f57cb10f3c065.tar.gz |
Merge pull request #2692 from jaymzh/multipackage
Multipackge support
-rw-r--r-- | CHANGELOG.md | 2 | ||||
-rw-r--r-- | DOC_CHANGES.md | 5 | ||||
-rw-r--r-- | RELEASE_NOTES.md | 5 | ||||
-rw-r--r-- | lib/chef/mixin/get_source_from_package.rb | 1 | ||||
-rw-r--r-- | lib/chef/provider/package.rb | 333 | ||||
-rw-r--r-- | lib/chef/provider/package/apt.rb | 133 | ||||
-rw-r--r-- | lib/chef/provider/package/rubygems.rb | 11 | ||||
-rw-r--r-- | lib/chef/provider/package/yum.rb | 131 | ||||
-rw-r--r-- | lib/chef/resource/package.rb | 4 | ||||
-rw-r--r-- | spec/unit/provider/package/aix_spec.rb | 2 | ||||
-rw-r--r-- | spec/unit/provider/package/apt_spec.rb | 8 | ||||
-rw-r--r-- | spec/unit/provider/package/dpkg_spec.rb | 2 | ||||
-rw-r--r-- | spec/unit/provider/package/ips_spec.rb | 3 | ||||
-rw-r--r-- | spec/unit/provider/package/rubygems_spec.rb | 70 | ||||
-rw-r--r-- | spec/unit/provider/package/solaris_spec.rb | 2 | ||||
-rw-r--r-- | spec/unit/provider/package/yum_spec.rb | 139 | ||||
-rw-r--r-- | spec/unit/provider/package_spec.rb | 276 | ||||
-rw-r--r-- | spec/unit/provider/package_spec.rbe | 0 |
18 files changed, 938 insertions, 189 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index c434650fb3..10829a08d7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -50,6 +50,8 @@ Fix knife cookbook upload messages * [**David Crowder**] (https://github.com/david-crowder) refactor to use shell_out in rpm provider +* [**Phil Dibowitz**](https://github.com/jaymzh): + Multi-package support ### Chef Contributions * ruby 1.9.3 support is dropped diff --git a/DOC_CHANGES.md b/DOC_CHANGES.md index dbe79478f5..4e11a6f957 100644 --- a/DOC_CHANGES.md +++ b/DOC_CHANGES.md @@ -39,3 +39,8 @@ This probably only needs to be a bullet point added to http://docs.getchef.com/n ## Drop SSL Warnings Now that the default for SSL checking is on, no more warning is emitted when SSL checking is off. + +## Multi-package Support +The `package` provider has been extended to support multiple packages. This +support is new and and not all subproviders yet support it. Full support for +`apt` and `yum` has been implemented. diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 329f55555b..3c94bf21a9 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -68,6 +68,11 @@ Previously, when a URI scheme contained all uppercase letters, Chef would reject Now that the default for SSL checking is on, no more warning is emitted when SSL checking is off. +## Multi-package Support +The `package` provider has been extended to support multiple packages. This +support is new and and not all subproviders yet support it. Full support for +`apt` and `yum` has been implemented. + # Chef Client Release Notes 12.0.0: # Internal API Changes in this Release diff --git a/lib/chef/mixin/get_source_from_package.rb b/lib/chef/mixin/get_source_from_package.rb index 6d5cb56a27..2ed251854a 100644 --- a/lib/chef/mixin/get_source_from_package.rb +++ b/lib/chef/mixin/get_source_from_package.rb @@ -29,6 +29,7 @@ class Chef module GetSourceFromPackage def initialize(new_resource, run_context) super + return if new_resource.package_name.is_a?(Array) # if we're passed something that looks like a filesystem path, with no source, use it # - require at least one '/' in the path to avoid gem_package "foo" breaking if a file named 'foo' exists in the cwd if new_resource.source.nil? && new_resource.package_name.match(/#{::File::SEPARATOR}/) && ::File.exists?(new_resource.package_name) diff --git a/lib/chef/provider/package.rb b/lib/chef/provider/package.rb index a4a056dfec..9edf8d5f52 100644 --- a/lib/chef/provider/package.rb +++ b/lib/chef/provider/package.rb @@ -25,9 +25,15 @@ class Chef class Provider class Package < Chef::Provider + # @todo: validate no subclasses need this and nuke it include Chef::Mixin::Command + # + # Hook that subclasses use to populate the candidate_version(s) + # + # @return [Array, String] candidate_version(s) may be a string or array attr_accessor :candidate_version + def initialize(new_resource, run_context) super @candidate_version = nil @@ -41,63 +47,87 @@ class Chef 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 { ((@new_resource.version != nil) && !(target_version_already_installed?)) \ - || !(@current_resource.version.nil? && candidate_version.nil?) } - a.failure_message(Chef::Exceptions::Package, "No version specified, and no candidate version available for #{@new_resource.package_name}") - a.whyrun("Assuming a repository that offers #{@new_resource.package_name} would have been configured") + 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") end - requirements.assert(:upgrade) do |a| - # Can't upgrade what we don't have - a.assertion { !(@current_resource.version.nil? && candidate_version.nil?) } - a.failure_message(Chef::Exceptions::Package, "No candidate version available for #{@new_resource.package_name}") - a.whyrun("Assuming a repository that offers #{@new_resource.package_name} would have been configured") + 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 def action_install - # If we specified a version, and it's not the current version, move to the specified version - if !@new_resource.version.nil? && !(target_version_already_installed?) - install_version = @new_resource.version - # If it's not installed at all, install it - elsif @current_resource.version.nil? - install_version = candidate_version - else + unless target_version_array.any? Chef::Log.debug("#{@new_resource} is already installed - nothing to do") return end - # We need to make sure we handle the preseed file + # @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 preseed_file = get_preseed_file(@new_resource.package_name, install_version) - converge_by("preseed package #{@new_resource.package_name}") do + 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) end end end - description = install_version ? "version #{install_version} of" : "" - converge_by("install #{description} package #{@new_resource.package_name}") do - @new_resource.version(install_version) - install_package(@new_resource.package_name, install_version) + + # 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) + Chef::Log.info("#{@new_resource} installed #{package_names_for_targets} at #{versions_for_targets}") end end + def install_description + description = [] + target_version_array.each_with_index do |target_version, i| + next if target_version.nil? + package_name = package_name_array[i] + description << "install version #{target_version} of package #{package_name}" + end + description + end + + private :install_description + def action_upgrade - if candidate_version.nil? - Chef::Log.debug("#{@new_resource} no candidate version - nothing to do") - elsif @current_resource.version == candidate_version - Chef::Log.debug("#{@new_resource} is at the latest version - nothing to do") - else - @new_resource.version(candidate_version) - orig_version = @current_resource.version || "uninstalled" - converge_by("upgrade package #{@new_resource.package_name} from #{orig_version} to #{candidate_version}") do - upgrade_package(@new_resource.package_name, candidate_version) - Chef::Log.info("#{@new_resource} upgraded from #{orig_version} to #{candidate_version}") - end + if !target_version_array.any? + Chef::Log.debug("#{@new_resource} no versions to upgrade - nothing to do") + 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) + Chef::Log.info("#{@new_resource} upgraded #{package_names_for_targets} to #{versions_for_targets}") end end + def upgrade_description + description = [] + target_version_array.each_with_index do |target_version, i| + next if target_version.nil? + package_name = package_name_array[i] + candidate_version = candidate_version_array[i] + current_version = current_version_array[i] || "uninstalled" + description << "upgrade package #{package_name} from #{current_version} to #{candidate_version}" + end + description + end + + 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 " : "" @@ -110,18 +140,28 @@ class Chef end end + def have_any_matching_version? + f = [] + new_version_array.each_with_index do |item, index| + f << (item == current_version_array[index]) + end + f.any? + end + def removing_package? - if @current_resource.version.nil? - false # nothing to remove - elsif @new_resource.version.nil? - true # remove any version of a package - elsif @new_resource.version == @current_resource.version + if !current_version_array.any? + # ! any? means it's all nil's, which means nothing is installed + false + elsif !new_version_array.any? + true # remove any version of all packages + elsif have_any_matching_version? true # remove the version we have else false # we don't have the version we want to remove 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" : "" @@ -132,6 +172,7 @@ class Chef 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") @@ -154,6 +195,7 @@ class Chef end end + # @todo use composition rather than inheritance def install_package(name, version) raise Chef::Exceptions::UnsupportedAction, "#{self.to_s} does not support :install" end @@ -178,6 +220,17 @@ class Chef raise( Chef::Exceptions::UnsupportedAction, "#{self.to_s} does not support :reconfig" ) end + # this is heavily used by subclasses + def expand_options(options) + options ? " #{options}" : "" + 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 + 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) @@ -190,6 +243,7 @@ class Chef end end + # @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}") @@ -216,22 +270,209 @@ class Chef remote_file end - def expand_options(options) - options ? " #{options}" : "" + # helper method used by subclasses + # + def as_array(thing) + [ thing ].flatten end - def target_version_already_installed? - @new_resource.version == @current_resource.version + private + + # Returns the package names which need to be modified. If the resource was called with an array of packages + # then this will return an array of packages to update (may have 0 or 1 entries). If the resource was called + # with a non-array package_name to manage then this will return a string rather than an Array. The output + # of this is meant to be fed into subclass interfaces to install/upgrade packages and not all of them are + # Array-aware. + # + # @return [String, Array<String>] package_name(s) to actually update/install + 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) + end + multipackage? ? package_names_for_targets : package_names_for_targets[0] end - private + # Returns the package versions which need to be modified. If the resource was called with an array of packages + # then this will return an array of versions to update (may have 0 or 1 entries). If the resource was called + # with a non-array package_name to manage then this will return a string rather than an Array. The output + # of this is meant to be fed into subclass interfaces to install/upgrade packages and not all of them are + # Array-aware. + # + # @return [String, Array<String>] package version(s) to actually update/install + 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) + end + 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. + # + # @return [Array<String,NilClass>] array of package versions which need to be upgraded (nil = not being upgraded) + def target_version_array + @target_version_array ||= + begin + target_version_array = [] + + 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") + target_version_array.push(nil) + elsif current_version == candidate_version + Chef::Log.debug("#{new_resource} #{package_name} the #{candidate_version} is already installed") + target_version_array.push(nil) + else + Chef::Log.debug("#{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") + target_version_array.push(nil) + else + Chef::Log.debug("#{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}") + target_version_array.push(candidate_version) + else + Chef::Log.debug("#{new_resource} #{package_name} #{current_version} already installed") + target_version_array.push(nil) + end + + else + # in specs please test the public interface provider.run_action(:install) instead of provider.action_install + raise "internal error - target_version_array in package provider does not understand this action" + end + end + + target_version_array + end + end + + # Check the list of current_version_array and candidate_version_array. For any of the + # packages if both versions are missing (uninstalled and no candidate) this will be an + # unsolvable error. + # + # @return [Boolean] valid candidates exist for all uninstalled packages + def candidates_exist_for_all_uninstalled? + packages_missing_candidates.empty? + end + + # Returns array of all packages which are missing candidate versions. + # + # @return [Array<String>] names of packages missing candidates + def packages_missing_candidates + @packages_missing_candidates ||= + begin + missing = [] + each_package do |package_name, new_version, current_version, candidate_version| + missing.push(package_name) if candidate_version.nil? && current_version.nil? + end + missing + end + end + + # This looks for packages which have a new_version and a current_version, and they are + # different (a "forced change") and for which there is no candidate. This is an edge + # condition that candidates_exist_for_all_uninstalled? does not catch since in this case + # it is not uninstalled but must be installed anyway and no version exists. + # + # @return [Boolean] valid candidates exist for all uninstalled packages + def candidates_exist_for_all_forced_changes? + forced_packages_missing_candidates.empty? + end + + # Returns an array of all forced packages which are missing candidate versions + # + # @return [Array] names of packages missing candidates + def forced_packages_missing_candidates + @forced_packages_missing_candidates ||= + begin + 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) + missing.push(package_name) + end + end + missing + end + end + + # Helper to iterate over all the indexed *_array's in sync + # + # @yield [package_name, new_version, current_version, candidate_version] Description of block + def each_package + package_name_array.each_with_index do |package_name, i| + candidate_version = candidate_version_array[i] + current_version = current_version_array[i] + new_version = new_version_array[i] + yield package_name, new_version, current_version, candidate_version + end + end + + # @return [Boolean] if we're doing a multipackage install or not + def multipackage? + 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 + end + + # @return [Array] candidate_version(s) as an array + def candidate_version_array + [ candidate_version ].flatten + end + + # @return [Array] current_version(s) as an array + def current_version_array + [ current_resource.version ].flatten + end + + # @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 + end + end + # @todo: extract apt/dpkg specific preseeding to a helper class def template_available?(path) - run_context.has_template_in_cookbook?(@new_resource.cookbook_name, path) + run_context.has_template_in_cookbook?(new_resource.cookbook_name, path) end + # @todo: extract apt/dpkg specific preseeding to a helper class def cookbook_file_available?(path) - run_context.has_cookbook_file_in_cookbook?(@new_resource.cookbook_name, path) + run_context.has_cookbook_file_in_cookbook?(new_resource.cookbook_name, path) end end diff --git a/lib/chef/provider/package/apt.rb b/lib/chef/provider/package/apt.rb index fd132c817c..c960806e8f 100644 --- a/lib/chef/provider/package/apt.rb +++ b/lib/chef/provider/package/apt.rb @@ -51,54 +51,85 @@ class Chef end def check_package_state(package) - Chef::Log.debug("#{@new_resource} checking package status for #{package}") - installed = false - - shell_out!("apt-cache#{expand_options(default_release_options)} policy #{package}", :timeout => @new_resource.timeout).stdout.each_line do |line| - case line - when /^\s{2}Installed: (.+)$/ - installed_version = $1 - if installed_version == '(none)' - Chef::Log.debug("#{@new_resource} current version is nil") - @current_resource.version(nil) - else - Chef::Log.debug("#{@new_resource} current version is #{installed_version}") - @current_resource.version(installed_version) - installed = true - end - when /^\s{2}Candidate: (.+)$/ - candidate_version = $1 - 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 #{package}", :timeout => @new_resource.timeout).stdout - providers = Hash.new - # Returns all lines after 'Reverse Provides:' - showpkg.rpartition(/Reverse Provides:\s*#{$/}/)[2].each_line do |line| - provider, version = line.split - providers[provider] = version + final_installed_version = [] + final_candidate_version = [] + final_installed = [] + final_virtual = [] + + [package].flatten.each do |pkg| + installed = virtual = false + installed_version = candidate_version = nil + shell_out!("apt-cache#{expand_options(default_release_options)} policy #{pkg}", {:timeout=>900}).stdout.each_line do |line| + case line + when /^\s{2}Installed: (.+)$/ + installed_version = $1 + if installed_version == '(none)' + Chef::Log.debug("#{@new_resource} current version is nil") + installed_version = nil + else + Chef::Log.debug("#{@new_resource} current version is #{installed_version}") + installed = true + end + when /^\s{2}Candidate: (.+)$/ + candidate_version = $1 + if candidate_version == '(none)' + # This may not be an appropriate assumption, but it shouldn't break anything that already worked -- btm + virtual = true + showpkg = shell_out!("apt-cache showpkg #{package}", {:timeout => 900}).stdout + providers = Hash.new + showpkg.rpartition(/Reverse Provides: ?#{$/}/)[2].each_line do |line| + provider, version = line.split + providers[provider] = version + end + # Check if the package providing this virtual package is installed + num_providers = providers.length + raise Chef::Exceptions::Package, "#{@new_resource.package_name} has no candidate in the apt-cache" if num_providers == 0 + # apt will only install a virtual package if there is a single providing package + raise Chef::Exceptions::Package, "#{@new_resource.package_name} is a virtual package provided by #{num_providers} packages, you must explicitly select one to install" if num_providers > 1 + # Check if the package providing this virtual package is installed + Chef::Log.info("#{@new_resource} is a virtual package, actually acting on package[#{providers.keys.first}]") + installed = check_package_state(providers.keys.first) + else + Chef::Log.debug("#{@new_resource} candidate version is #{$1}") end - # Check if the package providing this virtual package is installed - num_providers = providers.length - raise Chef::Exceptions::Package, "#{@new_resource.package_name} has no candidate in the apt-cache" if num_providers == 0 - # apt will only install a virtual package if there is a single providing package - raise Chef::Exceptions::Package, "#{@new_resource.package_name} is a virtual package provided by #{num_providers} packages, you must explicitly select one to install" if num_providers > 1 - # Check if the package providing this virtual package is installed - Chef::Log.info("#{@new_resource} is a virtual package, actually acting on package[#{providers.keys.first}]") - installed = check_package_state(providers.keys.first) - else - Chef::Log.debug("#{@new_resource} candidate version is #{$1}") - @candidate_version = $1 end end + if package.is_a?(Array) + final_installed_version << installed_version + final_candidate_version << candidate_version + final_installed << installed + final_virtual << virtual + else + final_installed_version = installed_version + final_candidate_version = candidate_version + final_installed = installed + final_virtual = virtual + end end - - return installed + @candidate_version = final_candidate_version + @current_resource.version(final_installed_version) + @is_virtual_package = final_virtual + + return final_installed.is_a?(Array) ? final_installed.any? : final_installed end def install_package(name, version) - package_name = "#{name}=#{version}" - package_name = name if @is_virtual_package + if name.is_a?(Array) + index = 0 + package_name = name.zip(version).map do |x, y| + namestr = nil + if @is_virtual_package[index] + namestr = x + else + namestr = "#{x}=#{y}" + end + index += 1 + namestr + end.join(' ') + else + package_name = "#{name}=#{version}" + package_name = name if @is_virtual_package + end run_noninteractive("apt-get -q -y#{expand_options(default_release_options)}#{expand_options(@new_resource.options)} install #{package_name}") end @@ -107,12 +138,21 @@ class Chef end def remove_package(name, version) - package_name = "#{name}" + if name.is_a?(Array) + package_name = name.join(' ') + else + package_name = name + end run_noninteractive("apt-get -q -y#{expand_options(@new_resource.options)} remove #{package_name}") end def purge_package(name, version) - run_noninteractive("apt-get -q -y#{expand_options(@new_resource.options)} purge #{@new_resource.package_name}") + if name.is_a?(Array) + package_name = name.join(' ') + else + package_name = "#{name}" + end + run_noninteractive("apt-get -q -y#{expand_options(@new_resource.options)} purge #{package_name}") end def preseed_package(preseed_file) @@ -121,8 +161,13 @@ class Chef end def reconfig_package(name, version) + if name.is_a?(Array) + package_name = name.join(' ') + else + package_name = "#{name}" + end Chef::Log.info("#{@new_resource} reconfiguring") - run_noninteractive("dpkg-reconfigure #{name}") + run_noninteractive("dpkg-reconfigure #{package_name}") end private diff --git a/lib/chef/provider/package/rubygems.rb b/lib/chef/provider/package/rubygems.rb index 1f33cc5a9b..ff1e346cd1 100644 --- a/lib/chef/provider/package/rubygems.rb +++ b/lib/chef/provider/package/rubygems.rb @@ -484,7 +484,7 @@ class Chef def candidate_version @candidate_version ||= begin - if target_version_already_installed? + 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 @@ -494,12 +494,11 @@ class Chef end end - def target_version_already_installed? - return false unless @current_resource && @current_resource.version - return false if @current_resource.version.nil? - return false if @new_resource.version.nil? + def target_version_already_installed?(current_version, new_version) + return false unless current_version + return false if new_version.nil? - Gem::Requirement.new(@new_resource.version).satisfied_by?(Gem::Version.new(@current_resource.version)) + Gem::Requirement.new(new_version).satisfied_by?(Gem::Version.new(current_version)) end ## diff --git a/lib/chef/provider/package/yum.rb b/lib/chef/provider/package/yum.rb index 505f5fd6a3..c077dfb4c2 100644 --- a/lib/chef/provider/package/yum.rb +++ b/lib/chef/provider/package/yum.rb @@ -1054,9 +1054,18 @@ class Chef # 3) or a dependency, eg: "foo >= 1.1" # Check if we have name or name+arch which has a priority over a dependency - unless @yum.package_available?(@new_resource.package_name) - # If they aren't in the installed packages they could be a dependency - parse_dependency + package_name_array.each do |n| + unless @yum.package_available?(n) + # If they aren't in the installed packages they could be a dependency + dep = parse_dependency(n) + if dep + if @new_resource.package_name.is_a?(Array) + @new_resource.package_name(package_name_array - [n] + [dep]) + else + @new_resource.package_name(dep) + end + end + end end # Don't overwrite an existing arch @@ -1090,10 +1099,18 @@ class Chef Chef::Log.debug("#{@new_resource} checking yum info for #{new_resource}") - installed_version = @yum.installed_version(@new_resource.package_name, arch) - @current_resource.version(installed_version) - - @candidate_version = @yum.candidate_version(@new_resource.package_name, arch) + installed_version = [] + @candidate_version = [] + package_name_array.each do |pkg| + installed_version << @yum.installed_version(pkg, arch) + @candidate_version << @yum.candidate_version(pkg, arch) + end + if installed_version.size == 1 + @current_resource.version(installed_version[0]) + @candidate_version = @candidate_version[0] + else + @current_resource.version(installed_version) + end Chef::Log.debug("#{@new_resource} installed version: #{installed_version || "(none)"} candidate version: " + "#{@candidate_version || "(none)"}") @@ -1101,43 +1118,77 @@ class Chef @current_resource end - def install_package(name, version) - if @new_resource.source - yum_command("yum -d0 -e0 -y#{expand_options(@new_resource.options)} localinstall #{@new_resource.source}") - else - # Work around yum not exiting with an error if a package doesn't exist for CHEF-2062 - if @yum.version_available?(name, version, arch) + def install_remote_package(name, version) + # 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) + end + method = log_method = nil + methods = [] + if all_avail + # More Yum fun: + # + # yum install of an old name+version will exit(1) + # yum install of an old name+version+arch will exit(0) for some reason + # + # Some packages can be installed multiple times like the kernel + as_array(name).zip(as_array(version)).each do |n, v| method = "install" log_method = "installing" - - # More Yum fun: - # - # yum install of an old name+version will exit(1) - # yum install of an old name+version+arch will exit(0) for some reason - # - # Some packages can be installed multiple times like the kernel - unless @yum.allow_multi_install.include?(name) - if RPMVersion.parse(@current_resource.version) > RPMVersion.parse(version) - # Unless they want this... + idx = package_name_array.index(n) + unless @yum.allow_multi_install.include?(n) + if RPMVersion.parse(current_version_array[idx]) > RPMVersion.parse(v) + # We allow downgrading only in the evenit of single-package + # rules where the user explicitly allowed it if allow_downgrade method = "downgrade" log_method = "downgrading" else # we bail like yum when the package is older raise Chef::Exceptions::Package, "Installed package #{name}-#{@current_resource.version} is newer " + - "than candidate package #{name}-#{version}" + "than candidate package #{n}-#{v}" end end end + # methods don't count for packages we won't be touching + next if RPMVersion.parse(current_version_array[idx]) == RPMVersion.parse(v) + methods << method + end - repo = @yum.package_repository(name, version, arch) - Chef::Log.info("#{@new_resource} #{log_method} #{name}-#{version}#{yum_arch} from #{repo} repository") + # We could split this up into two commands if we wanted to, but + # for now, just don't support this. + if methods.uniq.length > 1 + raise Chef::Exceptions::Package, "Multipackage rule #{name} has a mix of upgrade and downgrade packages. Cannot proceed." + end - yum_command("yum -d0 -e0 -y#{expand_options(@new_resource.options)} #{method} #{name}-#{version}#{yum_arch}") - 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)" + 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) + repos << "#{s} from #{repo} repository" + pkg_string_bits << s + end + index += 1 end + 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}") + 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)" + end + end + + def install_package(name, version) + if @new_resource.source + yum_command("yum -d0 -e0 -y#{expand_options(@new_resource.options)} localinstall #{@new_resource.source}") + else + install_remote_package(name, version) end if flush_cache[:after] @@ -1156,10 +1207,11 @@ class Chef # Hacky - better overall solution? Custom compare in Package provider? def action_upgrade # Could be uninstalled or have no candidate - if @current_resource.version.nil? || candidate_version.nil? + if @current_resource.version.nil? || !candidate_version_array.any? super - # Ensure the candidate is newer - elsif RPMVersion.parse(candidate_version) > RPMVersion.parse(@current_resource.version) + elsif candidate_version_array.zip(current_version_array).any? do |c, i| + RPMVersion.parse(c) > RPMVersion.parse(i) + end super else Chef::Log.debug("#{@new_resource} is at the latest version - nothing to do") @@ -1172,10 +1224,13 @@ class Chef def remove_package(name, version) if version - yum_command("yum -d0 -e0 -y#{expand_options(@new_resource.options)} remove #{name}-#{version}#{yum_arch}") + remove_str = as_array(name).zip(as_array(version)).map do |x| + "#{x.join('-')}#{yum_arch}" + end.join(' ') else - yum_command("yum -d0 -e0 -y#{expand_options(@new_resource.options)} remove #{name}#{yum_arch}") + remove_str = as_array(name).map { |n| "#{n}#{yum_arch}" }.join(' ') end + yum_command("yum -d0 -e0 -y#{expand_options(@new_resource.options)} remove #{remove_str}") if flush_cache[:after] @yum.reload @@ -1218,9 +1273,9 @@ class Chef # matching them up with an actual package so the standard resource handling can apply. # # There is currently no support for filename matching. - def parse_dependency + def parse_dependency(name) # Transform the package_name into a requirement - yum_require = RPMRequire.parse(@new_resource.package_name) + yum_require = RPMRequire.parse(name) # and gather all the packages that have a Provides feature satisfying the requirement. # It could be multiple be we can only manage one packages = @yum.packages_from_require(yum_require) @@ -1254,7 +1309,7 @@ class Chef "specific version.") end - @new_resource.package_name(new_package_name) + new_package_name end end diff --git a/lib/chef/resource/package.rb b/lib/chef/resource/package.rb index 772439b06c..f4f49b543b 100644 --- a/lib/chef/resource/package.rb +++ b/lib/chef/resource/package.rb @@ -46,7 +46,7 @@ class Chef set_or_return( :package_name, arg, - :kind_of => [ String ] + :kind_of => [ String, Array ] ) end @@ -54,7 +54,7 @@ class Chef set_or_return( :version, arg, - :kind_of => [ String ] + :kind_of => [ String, Array ] ) end diff --git a/spec/unit/provider/package/aix_spec.rb b/spec/unit/provider/package/aix_spec.rb index 6908b1288d..a39ab096c7 100644 --- a/spec/unit/provider/package/aix_spec.rb +++ b/spec/unit/provider/package/aix_spec.rb @@ -54,8 +54,8 @@ describe Chef::Provider::Package::Aix do it "should raise an exception if a source is supplied but not found" do allow(@provider).to receive(:popen4).and_return(@status) allow(::File).to receive(:exists?).and_return(false) - @provider.define_resource_requirements @provider.load_current_resource + @provider.define_resource_requirements expect { @provider.process_resource_requirements }.to raise_error(Chef::Exceptions::Package) end diff --git a/spec/unit/provider/package/apt_spec.rb b/spec/unit/provider/package/apt_spec.rb index e53fdc3f27..acf0707bbf 100644 --- a/spec/unit/provider/package/apt_spec.rb +++ b/spec/unit/provider/package/apt_spec.rb @@ -198,6 +198,11 @@ mpg123 1.12.1-0ubuntu1 it "raises an exception if a source is specified (CHEF-5113)" do @new_resource.source "pluto" + expect(@provider).to receive(:shell_out!).with( + "apt-cache policy #{@new_resource.package_name}", + :timeout => @timeout + ).and_return(@shell_out) + @provider.load_current_resource @provider.define_resource_requirements expect(@provider).to receive(:shell_out!).with("apt-cache policy irssi", {:timeout=>900}).and_return(@shell_out) expect { @provider.run_action(:install) }.to raise_error(Chef::Exceptions::Package) @@ -307,8 +312,7 @@ mpg123 1.12.1-0ubuntu1 end it "should get the full path to the preseed response file" do - expect(@provider).to receive(:get_preseed_file).with("irssi", "0.8.12-7").and_return("/tmp/irssi-0.8.12-7.seed") - file = @provider.get_preseed_file("irssi", "0.8.12-7") + file = "/tmp/irssi-0.8.12-7.seed" expect(@provider).to receive(:shell_out!).with( "debconf-set-selections /tmp/irssi-0.8.12-7.seed", diff --git a/spec/unit/provider/package/dpkg_spec.rb b/spec/unit/provider/package/dpkg_spec.rb index fdd9e50c8e..154809f88c 100644 --- a/spec/unit/provider/package/dpkg_spec.rb +++ b/spec/unit/provider/package/dpkg_spec.rb @@ -88,8 +88,8 @@ describe Chef::Provider::Package::Dpkg do it "should raise an exception if the source is not set but we are installing" do @new_resource = Chef::Resource::Package.new("wget") @provider.new_resource = @new_resource - @provider.define_resource_requirements @provider.load_current_resource + @provider.define_resource_requirements expect { @provider.run_action(:install)}.to raise_error(Chef::Exceptions::Package) end diff --git a/spec/unit/provider/package/ips_spec.rb b/spec/unit/provider/package/ips_spec.rb index 4e0afc46e9..342ac4c040 100644 --- a/spec/unit/provider/package/ips_spec.rb +++ b/spec/unit/provider/package/ips_spec.rb @@ -190,9 +190,8 @@ REMOTE expect(@provider).to receive(:shell_out).with("pkg info #{@new_resource.package_name}").and_return(local) expect(@provider).to receive(:shell_out!).with("pkg info -r #{@new_resource.package_name}").and_return(remote) - @provider.load_current_resource expect(@provider).to receive(:install_package).exactly(0).times - @provider.action_install + @provider.run_action(:install) end context "when accept_license is true" do diff --git a/spec/unit/provider/package/rubygems_spec.rb b/spec/unit/provider/package/rubygems_spec.rb index b4960b2af3..b17c216ddd 100644 --- a/spec/unit/provider/package/rubygems_spec.rb +++ b/spec/unit/provider/package/rubygems_spec.rb @@ -371,6 +371,8 @@ describe Chef::Provider::Package::Rubygems do # We choose detect omnibus via RbConfig::CONFIG['bindir'] in Chef::Provider::Package::Rubygems.new allow(RbConfig::CONFIG).to receive(:[]).with('bindir').and_return("/usr/bin/ruby") + # Rubygems uses this interally + allow(RbConfig::CONFIG).to receive(:[]).with('arch').and_call_original @provider = Chef::Provider::Package::Rubygems.new(@new_resource, @run_context) end @@ -379,7 +381,7 @@ describe Chef::Provider::Package::Rubygems do it "target_version_already_installed? should return false so that we can search for candidates" do @provider.load_current_resource - expect(@provider.target_version_already_installed?).to be_falsey + expect(@provider.target_version_already_installed?(@provider.current_resource.version, @new_resource.version)).to be_falsey end end @@ -469,6 +471,8 @@ describe Chef::Provider::Package::Rubygems do it "determines the candidate version by querying the remote gem servers" do @new_resource.source('http://mygems.example.com') + @provider.load_current_resource + @provider.current_resource.version('0.0.1') version = Gem::Version.new(@spec_version) expect(@provider.gem_env).to receive(:candidate_version_from_remote). with(Gem::Dependency.new('rspec-core', @spec_version), "http://mygems.example.com"). @@ -478,8 +482,9 @@ describe Chef::Provider::Package::Rubygems do it "parses the gem's specification if the requested source is a file" do @new_resource.package_name('chef-integration-test') - @new_resource.version('>= 0') @new_resource.source(CHEF_SPEC_DATA + '/gems/chef-integration-test-0.1.0.gem') + @new_resource.version('>= 0') + @provider.load_current_resource expect(@provider.candidate_version).to eq('0.1.0') end @@ -496,20 +501,23 @@ describe Chef::Provider::Package::Rubygems do describe "in the current gem environment" do it "installs the gem via the gems api when no explicit options are used" do expect(@provider.gem_env).to receive(:install).with(@gem_dep, :sources => nil) - expect(@provider.action_install).to be_truthy + @provider.run_action(:install) + expect(@new_resource).to be_updated_by_last_action end it "installs the gem via the gems api when a remote source is provided" do @new_resource.source('http://gems.example.org') sources = ['http://gems.example.org'] expect(@provider.gem_env).to receive(:install).with(@gem_dep, :sources => sources) - expect(@provider.action_install).to be_truthy + @provider.run_action(:install) + expect(@new_resource).to be_updated_by_last_action end it "installs the gem from file via the gems api when no explicit options are used" do @new_resource.source(CHEF_SPEC_DATA + '/gems/chef-integration-test-0.1.0.gem') expect(@provider.gem_env).to receive(:install).with(CHEF_SPEC_DATA + '/gems/chef-integration-test-0.1.0.gem') - expect(@provider.action_install).to be_truthy + @provider.run_action(:install) + expect(@new_resource).to be_updated_by_last_action end it "installs the gem from file via the gems api when the package is a path and the source is nil" do @@ -518,7 +526,8 @@ describe Chef::Provider::Package::Rubygems do @provider.current_resource = @current_resource expect(@new_resource.source).to eq(CHEF_SPEC_DATA + '/gems/chef-integration-test-0.1.0.gem') expect(@provider.gem_env).to receive(:install).with(CHEF_SPEC_DATA + '/gems/chef-integration-test-0.1.0.gem') - expect(@provider.action_install).to be_truthy + @provider.run_action(:install) + expect(@new_resource).to be_updated_by_last_action end # this catches 'gem_package "foo"' when "./foo" is a file in the cwd, and instead of installing './foo' it fetches the remote gem @@ -526,28 +535,35 @@ describe Chef::Provider::Package::Rubygems do allow(::File).to receive(:exists?).and_return(true) @new_resource.package_name('rspec-core') expect(@provider.gem_env).to receive(:install).with(@gem_dep, :sources => nil) - expect(@provider.action_install).to be_truthy + @provider.run_action(:install) + expect(@new_resource).to be_updated_by_last_action end it "installs the gem by shelling out when options are provided as a String" do @new_resource.options('-i /alt/install/location') expected ="gem install rspec-core -q --no-rdoc --no-ri -v \"#{@spec_version}\" -i /alt/install/location" expect(@provider).to receive(:shell_out!).with(expected, :env => nil) - expect(@provider.action_install).to be_truthy + @provider.run_action(:install) + expect(@new_resource).to be_updated_by_last_action end - it "installs the gem by shelling out when options are provided but no version is given" do - @new_resource.options('-i /alt/install/location') - @new_resource.version('') - expected ="gem install \"rspec-core\" -q --no-rdoc --no-ri -i /alt/install/location" - expect(@provider).to receive(:shell_out!).with(expected, :env => nil) - expect(@provider.action_install).to be_truthy + context "when no version is given" do + let(:target_version) { nil } + + it "installs the gem by shelling out when options are provided but no version is given" do + @new_resource.options('-i /alt/install/location') + expected ="gem install rspec-core -q --no-rdoc --no-ri -v \"#{@provider.candidate_version}\" -i /alt/install/location" + expect(@provider).to receive(:shell_out!).with(expected, :env => nil) + @provider.run_action(:install) + expect(@new_resource).to be_updated_by_last_action + end end it "installs the gem via the gems api when options are given as a Hash" do @new_resource.options(:install_dir => '/alt/install/location') expect(@provider.gem_env).to receive(:install).with(@gem_dep, :sources => nil, :install_dir => '/alt/install/location') - expect(@provider.action_install).to be_truthy + @provider.run_action(:install) + expect(@new_resource).to be_updated_by_last_action end describe "at a specific version" do @@ -557,24 +573,25 @@ describe Chef::Provider::Package::Rubygems do it "installs the gem via the gems api" do expect(@provider.gem_env).to receive(:install).with(@gem_dep, :sources => nil) - expect(@provider.action_install).to be_truthy + @provider.run_action(:install) + expect(@new_resource).to be_updated_by_last_action end end describe "at version specified with comparison operator" do it "skips install if current version satisifies requested version" do - allow(@current_resource).to receive(:version).and_return("2.3.3") - allow(@new_resource).to receive(:version).and_return(">=2.3.0") + @current_resource.version("2.3.3") + @new_resource.version(">=2.3.0") expect(@provider.gem_env).not_to receive(:install) - @provider.action_install + @provider.run_action(:install) end it "allows user to specify gem version with fuzzy operator" do - allow(@current_resource).to receive(:version).and_return("2.3.3") - allow(@new_resource).to receive(:version).and_return("~>2.3.0") + @current_resource.version("2.3.3") + @new_resource.version("~>2.3.0") expect(@provider.gem_env).not_to receive(:install) - @provider.action_install + @provider.run_action(:install) end end end @@ -583,7 +600,8 @@ describe Chef::Provider::Package::Rubygems do it "installs the gem by shelling out to gem install" do @new_resource.gem_binary('/usr/weird/bin/gem') expect(@provider).to receive(:shell_out!).with("/usr/weird/bin/gem install rspec-core -q --no-rdoc --no-ri -v \"#{@spec_version}\"", :env=>nil) - expect(@provider.action_install).to be_truthy + @provider.run_action(:install) + expect(@new_resource).to be_updated_by_last_action end it "installs the gem from file by shelling out to gem install" do @@ -591,7 +609,8 @@ describe Chef::Provider::Package::Rubygems do @new_resource.source(CHEF_SPEC_DATA + '/gems/chef-integration-test-0.1.0.gem') @new_resource.version('>= 0') expect(@provider).to receive(:shell_out!).with("/usr/weird/bin/gem install #{CHEF_SPEC_DATA}/gems/chef-integration-test-0.1.0.gem -q --no-rdoc --no-ri -v \">= 0\"", :env=>nil) - expect(@provider.action_install).to be_truthy + @provider.run_action(:install) + expect(@new_resource).to be_updated_by_last_action end it "installs the gem from file by shelling out to gem install when the package is a path and the source is nil" do @@ -602,7 +621,8 @@ describe Chef::Provider::Package::Rubygems do @new_resource.version('>= 0') expect(@new_resource.source).to eq(CHEF_SPEC_DATA + '/gems/chef-integration-test-0.1.0.gem') expect(@provider).to receive(:shell_out!).with("/usr/weird/bin/gem install #{CHEF_SPEC_DATA}/gems/chef-integration-test-0.1.0.gem -q --no-rdoc --no-ri -v \">= 0\"", :env=>nil) - expect(@provider.action_install).to be_truthy + @provider.run_action(:install) + expect(@new_resource).to be_updated_by_last_action end end diff --git a/spec/unit/provider/package/solaris_spec.rb b/spec/unit/provider/package/solaris_spec.rb index 8438202576..332fa9db1a 100644 --- a/spec/unit/provider/package/solaris_spec.rb +++ b/spec/unit/provider/package/solaris_spec.rb @@ -64,8 +64,8 @@ PKGINFO it "should raise an exception if a source is supplied but not found" do allow(@provider).to receive(:popen4).and_return(@status) allow(::File).to receive(:exists?).and_return(false) - @provider.define_resource_requirements @provider.load_current_resource + @provider.define_resource_requirements expect { @provider.process_resource_requirements }.to raise_error(Chef::Exceptions::Package) end diff --git a/spec/unit/provider/package/yum_spec.rb b/spec/unit/provider/package/yum_spec.rb index 0d2a44f3ae..8b4c71ee0a 100644 --- a/spec/unit/provider/package/yum_spec.rb +++ b/spec/unit/provider/package/yum_spec.rb @@ -337,9 +337,9 @@ describe Chef::Provider::Package::Yum do @provider.load_current_resource allow(Chef::Provider::Package::Yum::RPMUtils).to receive(:rpmvercmp).and_return(-1) expect(@provider).to receive(:yum_command).with( - "yum -d0 -e0 -y install emacs-1.0" + "yum -d0 -e0 -y install cups-1.2.4-11.19.el5" ) - @provider.install_package("emacs", "1.0") + @provider.install_package("cups", "1.2.4-11.19.el5") end it "should run yum localinstall if given a path to an rpm" do @@ -366,14 +366,14 @@ describe Chef::Provider::Package::Yum do allow(@new_resource).to receive(:arch).and_return("i386") allow(Chef::Provider::Package::Yum::RPMUtils).to receive(:rpmvercmp).and_return(-1) expect(@provider).to receive(:yum_command).with( - "yum -d0 -e0 -y install emacs-21.4-20.el5.i386" + "yum -d0 -e0 -y install cups-1.2.4-11.19.el5.i386" ) - @provider.install_package("emacs", "21.4-20.el5") + @provider.install_package("cups", "1.2.4-11.19.el5") end it "installs the package with the options given in the resource" do @provider.load_current_resource - @provider.candidate_version = '11' + allow(@provider).to receive(:candidate_version).and_return('11') allow(@new_resource).to receive(:options).and_return("--disablerepo epmd") allow(Chef::Provider::Package::Yum::RPMUtils).to receive(:rpmvercmp).and_return(-1) expect(@provider).to receive(:yum_command).with( @@ -467,10 +467,10 @@ describe Chef::Provider::Package::Yum do @provider.load_current_resource allow(Chef::Provider::Package::Yum::RPMUtils).to receive(:rpmvercmp).and_return(-1) expect(@provider).to receive(:yum_command).with( - "yum -d0 -e0 -y install emacs-1.0" + "yum -d0 -e0 -y install cups-1.2.4-11.15.el5" ) expect(@yum_cache).to receive(:reload).once - @provider.install_package("emacs", "1.0") + @provider.install_package("cups", "1.2.4-11.15.el5") end it "should run yum install then not flush the cache if :after is false" do @@ -478,17 +478,17 @@ describe Chef::Provider::Package::Yum do @provider.load_current_resource allow(Chef::Provider::Package::Yum::RPMUtils).to receive(:rpmvercmp).and_return(-1) expect(@provider).to receive(:yum_command).with( - "yum -d0 -e0 -y install emacs-1.0" + "yum -d0 -e0 -y install cups-1.2.4-11.15.el5" ) expect(@yum_cache).not_to receive(:reload) - @provider.install_package("emacs", "1.0") + @provider.install_package("cups", "1.2.4-11.15.el5") end end describe "when upgrading a package" do it "should run yum install if the package is installed and a version is given" do @provider.load_current_resource - @provider.candidate_version = '11' + allow(@provider).to receive(:candidate_version).and_return('11') allow(Chef::Provider::Package::Yum::RPMUtils).to receive(:rpmvercmp).and_return(-1) expect(@provider).to receive(:yum_command).with( "yum -d0 -e0 -y install cups-11" @@ -499,7 +499,7 @@ describe Chef::Provider::Package::Yum do it "should run yum install if the package is not installed" do @provider.load_current_resource @current_resource = Chef::Resource::Package.new('cups') - @provider.candidate_version = '11' + allow(@provider).to receive(:candidate_version).and_return('11') allow(Chef::Provider::Package::Yum::RPMUtils).to receive(:rpmvercmp).and_return(-1) expect(@provider).to receive(:yum_command).with( "yum -d0 -e0 -y install cups-11" @@ -528,42 +528,41 @@ describe Chef::Provider::Package::Yum do # Test our little workaround, some crossover into Chef::Provider::Package territory it "should call action_upgrade in the parent if the current resource version is nil" do allow(@yum_cache).to receive(:installed_version).and_return(nil) - @provider.load_current_resource @current_resource = Chef::Resource::Package.new('cups') - @provider.candidate_version = '11' + allow(@provider).to receive(:candidate_version).and_return('11') expect(@provider).to receive(:upgrade_package).with( "cups", "11" ) - @provider.action_upgrade + @provider.run_action(:upgrade) end it "should call action_upgrade in the parent if the candidate version is nil" do @provider.load_current_resource @current_resource = Chef::Resource::Package.new('cups') - @provider.candidate_version = nil + allow(@provider).to receive(:candidate_version).and_return(nil) expect(@provider).not_to receive(:upgrade_package) - @provider.action_upgrade + @provider.run_action(:upgrade) end it "should call action_upgrade in the parent if the candidate is newer" do @provider.load_current_resource @current_resource = Chef::Resource::Package.new('cups') - @provider.candidate_version = '11' + allow(@provider).to receive(:candidate_version).and_return('11') expect(@provider).to receive(:upgrade_package).with( "cups", "11" ) - @provider.action_upgrade + @provider.run_action(:upgrade) end it "should not call action_upgrade in the parent if the candidate is older" do allow(@yum_cache).to receive(:installed_version).and_return("12") @provider.load_current_resource @current_resource = Chef::Resource::Package.new('cups') - @provider.candidate_version = '11' + allow(@provider).to receive(:candidate_version).and_return('11') expect(@provider).not_to receive(:upgrade_package) - @provider.action_upgrade + @provider.run_action(:upgrade) end end @@ -1861,3 +1860,103 @@ EOF end end + +describe "Chef::Provider::Package::Yum - Multi" do + before(:each) do + @node = Chef::Node.new + @events = Chef::EventDispatch::Dispatcher.new + @run_context = Chef::RunContext.new(@node, {}, @events) + @new_resource = Chef::Resource::Package.new(['cups', 'vim']) + @status = double("Status", :exitstatus => 0) + @yum_cache = double( + 'Chef::Provider::Yum::YumCache', + :reload_installed => true, + :reset => true, + :installed_version => 'XXXX', + :candidate_version => 'YYYY', + :package_available? => true, + :version_available? => true, + :allow_multi_install => [ 'kernel' ], + :package_repository => 'base', + :disable_extra_repo_control => true + ) + allow(Chef::Provider::Package::Yum::YumCache).to receive(:instance).and_return(@yum_cache) + @provider = Chef::Provider::Package::Yum.new(@new_resource, @run_context) + @pid = double("PID") + end + + describe "when loading the current system state" do + it "should create a current resource with the name of the new_resource" do + @provider.load_current_resource + expect(@provider.current_resource.name).to eq(['cups', 'vim']) + end + + it "should set the current resources package name to the new resources package name" do + @provider.load_current_resource + expect(@provider.current_resource.package_name).to eq(['cups', 'vim']) + end + + it "should set the installed version to nil on the current resource if no installed package" do + allow(@yum_cache).to receive(:installed_version).and_return(nil) + @provider.load_current_resource + expect(@provider.current_resource.version).to eq([nil, nil]) + end + + it "should set the installed version if yum has one" do + allow(@yum_cache).to receive(:installed_version).with('cups', nil).and_return('1.2.4-11.18.el5') + allow(@yum_cache).to receive(:installed_version).with('vim', nil).and_return('1.0') + allow(@yum_cache).to receive(:candidate_version).with('cups', nil).and_return('1.2.4-11.18.el5_2.3') + allow(@yum_cache).to receive(:candidate_version).with('vim', nil).and_return('1.5') + @provider.load_current_resource + expect(@provider.current_resource.version).to eq(['1.2.4-11.18.el5', '1.0']) + end + + it "should set the candidate version if yum info has one" do + allow(@yum_cache).to receive(:installed_version).with('cups', nil).and_return('1.2.4-11.18.el5') + allow(@yum_cache).to receive(:installed_version).with('vim', nil).and_return('1.0') + allow(@yum_cache).to receive(:candidate_version).with('cups', nil).and_return('1.2.4-11.18.el5_2.3') + allow(@yum_cache).to receive(:candidate_version).with('vim', nil).and_return('1.5') + @provider.load_current_resource + expect(@provider.candidate_version).to eql(['1.2.4-11.18.el5_2.3', '1.5']) + end + + it "should return the current resouce" do + expect(@provider.load_current_resource).to eql(@provider.current_resource) + end + end + + describe "when installing a package" do + it "should run yum install with the package name and version" do + @provider.load_current_resource + allow(Chef::Provider::Package::Yum::RPMUtils).to receive(:rpmvercmp).and_return(-1) + allow(@yum_cache).to receive(:installed_version).with('cups', nil).and_return('1.2.4-11.18.el5') + allow(@yum_cache).to receive(:installed_version).with('vim', nil).and_return('0.9') + expect(@provider).to receive(:yum_command).with( + "yum -d0 -e0 -y install cups-1.2.4-11.19.el5 vim-1.0" + ) + @provider.install_package(["cups", "vim"], ["1.2.4-11.19.el5", '1.0']) + end + + it "should run yum install with the package name, version and arch" do + @provider.load_current_resource + allow(@new_resource).to receive(:arch).and_return("i386") + allow(Chef::Provider::Package::Yum::RPMUtils).to receive(:rpmvercmp).and_return(-1) + expect(@provider).to receive(:yum_command).with( + "yum -d0 -e0 -y install cups-1.2.4-11.19.el5.i386 vim-1.0.i386" + ) + @provider.install_package(["cups", "vim"], ["1.2.4-11.19.el5", "1.0"]) + end + + it "installs the package with the options given in the resource" do + @provider.load_current_resource + allow(Chef::Provider::Package::Yum::RPMUtils).to receive(:rpmvercmp).and_return(-1) + allow(@yum_cache).to receive(:installed_version).with('cups', nil).and_return('1.2.4-11.18.el5') + allow(@yum_cache).to receive(:installed_version).with('vim', nil).and_return('0.9') + expect(@provider).to receive(:yum_command).with( + "yum -d0 -e0 -y --disablerepo epmd install cups-1.2.4-11.19.el5 vim-1.0" + ) + allow(@new_resource).to receive(:options).and_return("--disablerepo epmd") + @provider.install_package(["cups", "vim"], ["1.2.4-11.19.el5", '1.0']) + end + end +end diff --git a/spec/unit/provider/package_spec.rb b/spec/unit/provider/package_spec.rb index 2aeaf717e6..6a0cb695b0 100644 --- a/spec/unit/provider/package_spec.rb +++ b/spec/unit/provider/package_spec.rb @@ -152,7 +152,7 @@ describe Chef::Provider::Package do it "should print the word 'uninstalled' if there was no original version" do allow(@current_resource).to receive(:version).and_return(nil) - expect(Chef::Log).to receive(:info).with("package[emacs] upgraded from uninstalled to 1.0") + expect(Chef::Log).to receive(:info).with("package[emacs] upgraded emacs to 1.0") @provider.run_action(:upgrade) expect(@new_resource).to be_updated_by_last_action end @@ -425,3 +425,277 @@ describe Chef::Provider::Package do end end + +describe "Chef::Provider::Package - Multi" do + before do + @node = Chef::Node.new + @events = Chef::EventDispatch::Dispatcher.new + @run_context = Chef::RunContext.new(@node, {}, @events) + @new_resource = Chef::Resource::Package.new(['emacs', 'vi']) + @current_resource = Chef::Resource::Package.new(['emacs', 'vi']) + @provider = Chef::Provider::Package.new(@new_resource, @run_context) + @provider.current_resource = @current_resource + @provider.candidate_version = ['1.0', '6.2'] + end + + describe "when installing multiple packages" do + before(:each) do + @provider.current_resource = @current_resource + allow(@provider).to receive(:install_package).and_return(true) + end + + it "installs the candidate versions when none are installed" do + expect(@provider).to receive(:install_package).with( + ["emacs", "vi"], + ["1.0", "6.2"] + ).and_return(true) + @provider.run_action(:install) + expect(@new_resource).to be_updated + end + + it "installs the candidate versions when some are installed" do + expect(@provider).to receive(:install_package).with( + [ 'vi' ], + [ '6.2' ] + ).and_return(true) + @current_resource.version(['1.0', nil]) + @provider.run_action(:install) + expect(@new_resource).to be_updated + end + + it "installs the specified version when some are out of date" do + @current_resource.version(['1.0', '6.2']) + @new_resource.version(['1.0', '6.1']) + @provider.run_action(:install) + expect(@new_resource).to be_updated + end + + it "does not install any version if all are installed at the right version" do + @current_resource.version(['1.0', '6.2']) + @new_resource.version(['1.0', '6.2']) + @provider.run_action(:install) + expect(@new_resource).not_to be_updated_by_last_action + end + + it "does not install any version if all are installed, and no version was specified" do + @current_resource.version(['1.0', '6.2']) + @provider.run_action(:install) + expect(@new_resource).not_to be_updated_by_last_action + end + + it "raises an exception if both are not installed and no caondidates are available" do + @current_resource.version([nil, nil]) + @provider.candidate_version = [nil, nil] + expect { @provider.run_action(:install) }.to raise_error(Chef::Exceptions::Package) + end + + it "raises an exception if one is not installed and no candidates are available" do + @current_resource.version(['1.0', nil]) + @provider.candidate_version = ['1.0', nil] + expect { @provider.run_action(:install) }.to raise_error(Chef::Exceptions::Package) + end + + it "does not raise an exception if the packages are installed or have a candidate" do + @current_resource.version(['1.0', nil]) + @provider.candidate_version = [nil, '6.2'] + expect { @provider.run_action(:install) }.not_to raise_error + end + + it "raises an exception if an explicit version is asked for, an old version is installed, but no candidate" do + @new_resource.version ['1.0', '6.2'] + @current_resource.version(['1.0', '6.1']) + @provider.candidate_version = ['1.0', nil] + expect { @provider.run_action(:install) }.to raise_error(Chef::Exceptions::Package) + end + + it "does not raise an exception if an explicit version is asked for, and is installed, but no candidate" do + @new_resource.version ['1.0', '6.2'] + @current_resource.version(['1.0', '6.2']) + @provider.candidate_version = ['1.0', nil] + expect { @provider.run_action(:install) }.not_to raise_error + end + + it "raise an exception if an explicit version is asked for, and is not installed, and no candidate" do + @new_resource.version ['1.0', '6.2'] + @current_resource.version(['1.0', nil]) + @provider.candidate_version = ['1.0', nil] + expect { @provider.run_action(:install) }.to raise_error(Chef::Exceptions::Package) + end + + it "does not raise an exception if an explicit version is asked for, and is not installed, and there is a candidate" do + @new_resource.version ['1.0', '6.2'] + @current_resource.version(['1.0', nil]) + @provider.candidate_version = ['1.0', '6.2'] + expect { @provider.run_action(:install) }.not_to raise_error + end + end + + describe "when upgrading multiple packages" do + before(:each) do + @provider.current_resource = @current_resource + allow(@provider).to receive(:upgrade_package).and_return(true) + end + + it "should upgrade the package if the current versions are not the candidate version" do + @current_resource.version ['0.9', '6.1'] + expect(@provider).to receive(:upgrade_package).with( + @new_resource.name, + @provider.candidate_version + ).and_return(true) + @provider.run_action(:upgrade) + expect(@new_resource).to be_updated_by_last_action + end + + it "should upgrade the package if some of current versions are not the candidate versions" do + @current_resource.version ['1.0', '6.1'] + expect(@provider).to receive(:upgrade_package).with( + ["vi"], + ["6.2"] + ).and_return(true) + @provider.run_action(:upgrade) + expect(@new_resource).to be_updated_by_last_action + end + + it "should not install the package if the current versions are the candidate version" do + @current_resource.version ['1.0', '6.2'] + expect(@provider).not_to receive(:upgrade_package) + @provider.run_action(:upgrade) + expect(@new_resource).not_to be_updated_by_last_action + end + + it "should raise an exception if both are not installed and no caondidates are available" do + @current_resource.version([nil, nil]) + @provider.candidate_version = [nil, nil] + expect { @provider.run_action(:upgrade) }.to raise_error(Chef::Exceptions::Package) + end + + it "should raise an exception if one is not installed and no candidates are available" do + @current_resource.version(['1.0', nil]) + @provider.candidate_version = ['1.0', nil] + expect { @provider.run_action(:upgrade) }.to raise_error(Chef::Exceptions::Package) + end + + it "should not raise an exception if the packages are installed or have a candidate" do + @current_resource.version(['1.0', nil]) + @provider.candidate_version = [nil, '6.2'] + expect { @provider.run_action(:upgrade) }.not_to raise_error + end + + it "should not raise an exception if the packages are installed or have a candidate" do + @current_resource.version(['1.0', nil]) + @provider.candidate_version = [nil, '6.2'] + expect { @provider.run_action(:upgrade) }.not_to raise_error + end + end + + describe "When removing multiple packages " do + before(:each) do + allow(@provider).to receive(:remove_package).and_return(true) + @current_resource.version ['1.0', '6.2'] + end + + it "should remove the packages if all are installed" do + expect(@provider).to be_removing_package + expect(@provider).to receive(:remove_package).with(['emacs', 'vi'], nil) + @provider.run_action(:remove) + expect(@new_resource).to be_updated + expect(@new_resource).to be_updated_by_last_action + end + + it "should remove the packages if some are installed" do + @current_resource.version ['1.0', nil] + expect(@provider).to be_removing_package + expect(@provider).to receive(:remove_package).with(['emacs', 'vi'], nil) + @provider.run_action(:remove) + expect(@new_resource).to be_updated + expect(@new_resource).to be_updated_by_last_action + end + + it "should remove the packages at a specific version if they are installed at that version" do + @new_resource.version ['1.0', '6.2'] + expect(@provider).to be_removing_package + expect(@provider).to receive(:remove_package).with(['emacs', 'vi'], ['1.0', '6.2']) + @provider.run_action(:remove) + expect(@new_resource).to be_updated_by_last_action + end + + it "should remove the packages at a specific version any are is installed at that version" do + @new_resource.version ['0.5', '6.2'] + expect(@provider).to be_removing_package + expect(@provider).to receive(:remove_package).with(['emacs', 'vi'], ['0.5', '6.2']) + @provider.run_action(:remove) + expect(@new_resource).to be_updated_by_last_action + end + + it "should not remove the packages at a specific version if they are not installed at that version" do + @new_resource.version ['0.5', '6.0'] + expect(@provider).not_to be_removing_package + expect(@provider).not_to receive(:remove_package) + @provider.run_action(:remove) + expect(@new_resource).not_to be_updated_by_last_action + end + + it "should not remove the packages if they are not installed" do + expect(@provider).not_to receive(:remove_package) + allow(@current_resource).to receive(:version).and_return(nil) + @provider.run_action(:remove) + expect(@new_resource).not_to be_updated_by_last_action + end + + end + + describe "When purging multiple packages " do + before(:each) do + allow(@provider).to receive(:purge_package).and_return(true) + @current_resource.version ['1.0', '6.2'] + end + + it "should purge the packages if all are installed" do + expect(@provider).to be_removing_package + expect(@provider).to receive(:purge_package).with(['emacs', 'vi'], nil) + @provider.run_action(:purge) + expect(@new_resource).to be_updated + expect(@new_resource).to be_updated_by_last_action + end + + it "should purge the packages if some are installed" do + @current_resource.version ['1.0', nil] + expect(@provider).to be_removing_package + expect(@provider).to receive(:purge_package).with(['emacs', 'vi'], nil) + @provider.run_action(:purge) + expect(@new_resource).to be_updated + expect(@new_resource).to be_updated_by_last_action + end + + it "should purge the packages at a specific version if they are installed at that version" do + @new_resource.version ['1.0', '6.2'] + expect(@provider).to be_removing_package + expect(@provider).to receive(:purge_package).with(['emacs', 'vi'], ['1.0', '6.2']) + @provider.run_action(:purge) + expect(@new_resource).to be_updated_by_last_action + end + + it "should purge the packages at a specific version any are is installed at that version" do + @new_resource.version ['0.5', '6.2'] + expect(@provider).to be_removing_package + expect(@provider).to receive(:purge_package).with(['emacs', 'vi'], ['0.5', '6.2']) + @provider.run_action(:purge) + expect(@new_resource).to be_updated_by_last_action + end + + it "should not purge the packages at a specific version if they are not installed at that version" do + @new_resource.version ['0.5', '6.0'] + expect(@provider).not_to be_removing_package + expect(@provider).not_to receive(:purge_package) + @provider.run_action(:purge) + expect(@new_resource).not_to be_updated_by_last_action + end + + it "should not purge the packages if they are not installed" do + expect(@provider).not_to receive(:purge_package) + allow(@current_resource).to receive(:version).and_return(nil) + @provider.run_action(:purge) + expect(@new_resource).not_to be_updated_by_last_action + end + end +end diff --git a/spec/unit/provider/package_spec.rbe b/spec/unit/provider/package_spec.rbe new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/spec/unit/provider/package_spec.rbe |