diff --git a/lib/puppet/provider/package/yum.rb b/lib/puppet/provider/package/yum.rb index bc065b7fa..9eb6214ec 100644 --- a/lib/puppet/provider/package/yum.rb +++ b/lib/puppet/provider/package/yum.rb @@ -1,138 +1,189 @@ require 'puppet/util/package' Puppet::Type.type(:package).provide :yum, :parent => :rpm, :source => :rpm do desc "Support via `yum`. Using this provider's `uninstallable` feature will not remove dependent packages. To remove dependent packages with this provider use the `purgeable` feature, but note this feature is destructive and should be used with the utmost care." has_feature :install_options, :versionable commands :yum => "yum", :rpm => "rpm", :python => "python" self::YUMHELPER = File::join(File::dirname(__FILE__), "yumhelper.py") if command('rpm') confine :true => begin rpm('--version') rescue Puppet::ExecutionFailure false else true end end defaultfor :operatingsystem => [:fedora, :centos, :redhat] def self.prefetch(packages) raise Puppet::Error, "The yum provider can only be used as root" if Process.euid != 0 super end - # Retrieve the latest package version information for a given package name. + # Retrieve the latest package version information for a given package name + # and combination of repos to enable and disable. + # + # @note If multiple package versions are defined (such as in the case where a + # package is built for multiple architectures), the first package found + # will be used. # # @api private # @param package [String] The name of the package to query + # @param enablerepo [Array] A list of repositories to enable for this query + # @param disablerepo [Array] A list of repositories to disable for this query # @return [Hash] - def self.latest_package_version(package) - if @latest_versions.nil? - @latest_versions = fetch_latest_versions + def self.latest_package_version(package, enablerepo, disablerepo) + + key = [enablerepo, disablerepo] + + @latest_versions ||= {} + if @latest_versions[key].nil? + @latest_versions[key] = fetch_latest_versions(enablerepo, disablerepo) end - @latest_versions[package].first + if @latest_versions[key][package] + @latest_versions[key][package].first + end end - # Search for all installed packages that have newer versions. + # Search for all installed packages that have newer versions, given a + # combination of repositories to enable and disable. # # @api private - # @return [Hash>>] - def self.fetch_latest_versions + # @param enablerepo [Array] A list of repositories to enable for this query + # @param disablerepo [Array] A list of repositories to disable for this query + # @return [Hash>>] All packages that were + # found with a list of found versions for each package. + def self.fetch_latest_versions(enablerepo, disablerepo) latest_versions = Hash.new {|h, k| h[k] = []} - python(self::YUMHELPER).each_line do |l| + args = [self::YUMHELPER] + args.concat(enablerepo.map { |repo| ['-e', repo] }.flatten) + args.concat(disablerepo.map { |repo| ['-d', repo] }.flatten) + + python(args).each_line do |l| if (match = l.match /^_pkg (.*)$/) hash = nevra_to_hash(match[1]) + # Create entries for both the package name without a version and a + # version since yum considers those as mostly interchangeable. short_name = hash[:name] long_name = "#{hash[:name]}.#{hash[:arch]}" latest_versions[short_name] << hash latest_versions[long_name] << hash end end latest_versions end def self.clear @latest_versions = nil end def install should = @resource.should(:ensure) self.debug "Ensuring => #{should}" wanted = @resource[:name] operation = :install case should when true, false, Symbol # pass should = nil else # Add the package version wanted += "-#{should}" is = self.query if is && Puppet::Util::Package.versioncmp(should, is[:ensure]) < 0 self.debug "Downgrading package #{@resource[:name]} from version #{is[:ensure]} to #{should}" operation = :downgrade end end args = ["-d", "0", "-e", "0", "-y", install_options, operation, wanted].compact yum *args is = self.query raise Puppet::Error, "Could not find package #{self.name}" unless is # FIXME: Should we raise an exception even if should == :latest # and yum updated us to a version other than @param_hash[:ensure] ? raise Puppet::Error, "Failed to update to version #{should}, got version #{is[:ensure]} instead" if should && should != is[:ensure] end # What's the latest package version available? def latest - upd = self.class.latest_package_version(@resource[:name]) + upd = self.class.latest_package_version(@resource[:name], enablerepo, disablerepo) unless upd.nil? # FIXME: there could be more than one update for a package # because of multiarch return "#{upd[:epoch]}:#{upd[:version]}-#{upd[:release]}" else # Yum didn't find updates, pretend the current # version is the latest raise Puppet::DevError, "Tried to get latest on a missing package" if properties[:ensure] == :absent return properties[:ensure] end end def update # Install in yum can be used for update, too self.install end def purge yum "-y", :erase, @resource[:name] end # @deprecated def latest_info - Puppet.deprecation_warning("#{self.class}#{__method__} is deprecated and no longer used") + Puppet.deprecation_warning("#{self.class}##{__method__} is deprecated and is no longer used.") @latest_info end # @deprecated def latest_info=(latest) - Puppet.deprecation_warning("#{self.class}#{__method__} is deprecated and no longer used") + Puppet.deprecation_warning("#{self.class}##{__method__} is deprecated and is no longer used.") @latest_info = latest end + + private + + def enablerepo + scan_options(resource[:install_options], '--enablerepo') + end + + def disablerepo + scan_options(resource[:install_options], '--disablerepo') + end + + # Scan a structure that looks like the package type 'install_options' + # structure for all hashes that have a specific key. + # + # @api private + # @param options [Array, nil] The options structure. If the + # options are nil an empty array will be returned. + # @param key [String] The key to look for in all contained hashes + # @return [Array] All hash values with the given key. + def scan_options(options, key) + return [] if options.nil? + options.inject([]) do |repos, opt| + if opt.is_a? Hash and opt[key] + repos << opt[key] + end + repos + end + end end diff --git a/spec/unit/provider/package/yum_spec.rb b/spec/unit/provider/package/yum_spec.rb index 4172a814b..03825a8fd 100755 --- a/spec/unit/provider/package/yum_spec.rb +++ b/spec/unit/provider/package/yum_spec.rb @@ -1,206 +1,290 @@ #! /usr/bin/env ruby require 'spec_helper' provider_class = Puppet::Type.type(:package).provider(:yum) describe provider_class do let(:name) { 'mypackage' } let(:resource) do Puppet::Type.type(:package).new( :name => name, :ensure => :installed, :provider => 'yum' ) end let(:provider) do provider = provider_class.new provider.resource = resource provider end before do provider.stubs(:yum).returns 'yum' provider.stubs(:rpm).returns 'rpm' provider.stubs(:get).with(:version).returns '1' provider.stubs(:get).with(:release).returns '1' provider.stubs(:get).with(:arch).returns 'i386' end # provider should repond to the following methods [:install, :latest, :update, :purge, :install_options].each do |method| it "should have a(n) #{method}" do provider.should respond_to(method) end end describe 'when installing' do before(:each) do Puppet::Util.stubs(:which).with("rpm").returns("/bin/rpm") provider.stubs(:which).with("rpm").returns("/bin/rpm") Puppet::Util::Execution.expects(:execute).with(["/bin/rpm", "--version"], {:combine => true, :custom_environment => {}, :failonfail => true}).returns("4.10.1\n").at_most_once end it 'should call yum install for :installed' do resource.stubs(:should).with(:ensure).returns :installed provider.expects(:yum).with('-d', '0', '-e', '0', '-y', :install, 'mypackage') provider.install end it 'should use :install to update' do provider.expects(:install) provider.update end it 'should be able to set version' do resource[:ensure] = '1.2' provider.expects(:yum).with('-d', '0', '-e', '0', '-y', :install, 'mypackage-1.2') provider.stubs(:query).returns :ensure => '1.2' provider.install end it 'should be able to downgrade' do resource[:ensure] = '1.0' provider.expects(:yum).with('-d', '0', '-e', '0', '-y', :downgrade, 'mypackage-1.0') provider.stubs(:query).returns(:ensure => '1.2').then.returns(:ensure => '1.0') provider.install end it 'should accept install options' do resource[:ensure] = :installed resource[:install_options] = ['-t', {'-x' => 'expackage'}] provider.expects(:yum).with('-d', '0', '-e', '0', '-y', ['-t', '-x=expackage'], :install, 'mypackage') provider.install end end describe 'when uninstalling' do it 'should use erase to purge' do provider.expects(:yum).with('-y', :erase, 'mypackage') provider.purge end end it 'should be versionable' do provider.should be_versionable end describe 'determining the latest version available for a package' do + + it "passes the value of enablerepo install_options when querying" do + resource[:install_options] = [ + {'--enablerepo' => 'contrib'}, + {'--enablerepo' => 'centosplus'}, + ] + provider.stubs(:properties).returns({:ensure => '3.4.5'}) + + described_class.expects(:latest_package_version).with('mypackage', ['contrib', 'centosplus'], []) + provider.latest + end + + it "passes the value of disablerepo install_options when querying" do + resource[:install_options] = [ + {'--disablerepo' => 'updates'}, + {'--disablerepo' => 'centosplus'}, + ] + provider.stubs(:properties).returns({:ensure => '3.4.5'}) + + described_class.expects(:latest_package_version).with('mypackage', [], ['updates', 'centosplus']) + provider.latest + end + describe 'and a newer version is not available' do before :each do - described_class.stubs(:latest_package_version).with('mypackage').returns nil + described_class.stubs(:latest_package_version).with('mypackage', [], []).returns nil end it 'raises an error the package is not installed' do provider.stubs(:properties).returns({:ensure => :absent}) expect { provider.latest }.to raise_error(Puppet::DevError, 'Tried to get latest on a missing package') end it 'returns version of the currently installed package' do provider.stubs(:properties).returns({:ensure => '3.4.5'}) provider.latest.should == '3.4.5' end end describe 'and a newer version is available' do let(:latest_version) do { :name => 'mypackage', :epoch => '1', :version => '2.3.4', :release => '5', :arch => 'i686', } end it 'includes the epoch in the version string' do - described_class.stubs(:latest_package_version).returns(latest_version) + described_class.stubs(:latest_package_version).with('mypackage', [], []).returns(latest_version) provider.latest.should == '1:2.3.4-5' end end end describe "lazy loading of latest package versions" do + before { described_class.clear } after { described_class.clear } let(:mypackage_version) do { :name => 'mypackage', :epoch => '1', :version => '2.3.4', :release => '5', :arch => 'i686', } end + let(:mypackage_newerversion) do + { + :name => 'mypackage', + :epoch => '1', + :version => '4.5.6', + :release => '7', + :arch => 'i686', + } + end + let(:latest_versions) { {'mypackage' => [mypackage_version]} } + let(:enabled_versions) { {'mypackage' => [mypackage_newerversion]} } + + it "returns the version hash if the package was found" do + described_class.expects(:fetch_latest_versions).with([], []).once.returns(latest_versions) + version = described_class.latest_package_version('mypackage', [], []) + expect(version).to eq(mypackage_version) + end - it "updates the list of latest packages if the list is unpopulated" do - described_class.clear - described_class.expects(:fetch_latest_versions).once.returns(latest_versions) - expect(described_class.latest_package_version('mypackage')).to eq(mypackage_version) + it "is nil if the package was not found in the query" do + described_class.expects(:fetch_latest_versions).with([], []).once.returns(latest_versions) + version = described_class.latest_package_version('nopackage', [], []) + expect(version).to be_nil end it "caches the package list and reuses that for subsequent queries" do - described_class.clear - described_class.expects(:fetch_latest_versions).once.returns(latest_versions) - expect(described_class.latest_package_version('mypackage')).to eq(mypackage_version) - expect(described_class.latest_package_version('mypackage')).to eq(mypackage_version) + described_class.expects(:fetch_latest_versions).with([], []).once.returns(latest_versions) + + 2.times { + version = described_class.latest_package_version('mypackage', [], []) + expect(version).to eq mypackage_version + } + end + + it "caches separate lists for each combination of 'enablerepo' and 'disablerepo'" do + described_class.expects(:fetch_latest_versions).with([], []).once.returns(latest_versions) + described_class.expects(:fetch_latest_versions).with(['enabled'], ['disabled']).once.returns(enabled_versions) + + 2.times { + version = described_class.latest_package_version('mypackage', [], []) + expect(version).to eq mypackage_version + } + + 2.times { + version = described_class.latest_package_version('mypackage', ['enabled'], ['disabled']) + expect(version).to eq(mypackage_newerversion) + } end end describe "querying for the latest version of all packages" do let(:yumhelper_single_arch) do <<-YUMHELPER_OUTPUT * base: centos.tcpdiag.net * extras: centos.mirrors.hoobly.com * updates: mirrors.arsc.edu _pkg nss-tools 0 3.14.3 4.el6_4 x86_64 _pkg pixman 0 0.26.2 5.el6_4 x86_64 _pkg myresource 0 1.2.3.4 5.el4 noarch _pkg mysummaryless 0 1.2.3.4 5.el4 noarch YUMHELPER_OUTPUT end let(:yumhelper_multi_arch) do yumhelper_single_arch + <<-YUMHELPER_OUTPUT _pkg nss-tools 0 3.14.3 4.el6_4 i386 _pkg pixman 0 0.26.2 5.el6_4 i386 YUMHELPER_OUTPUT end it "creates an entry for each line that's prefixed with '_pkg'" do - described_class.expects(:python).with(described_class::YUMHELPER).returns(yumhelper_single_arch) - entries = described_class.fetch_latest_versions + described_class.expects(:python).with([described_class::YUMHELPER]).returns(yumhelper_single_arch) + entries = described_class.fetch_latest_versions([], []) expect(entries.keys).to include 'nss-tools' expect(entries.keys).to include 'pixman' expect(entries.keys).to include 'myresource' expect(entries.keys).to include 'mysummaryless' end it "creates an entry for each package name and architecture" do - described_class.expects(:python).with(described_class::YUMHELPER).returns(yumhelper_single_arch) - entries = described_class.fetch_latest_versions + described_class.expects(:python).with([described_class::YUMHELPER]).returns(yumhelper_single_arch) + entries = described_class.fetch_latest_versions([], []) expect(entries.keys).to include 'nss-tools.x86_64' expect(entries.keys).to include 'pixman.x86_64' expect(entries.keys).to include 'myresource.noarch' expect(entries.keys).to include 'mysummaryless.noarch' end it "stores multiple entries if a package is build for multiple architectures" do - described_class.expects(:python).with(described_class::YUMHELPER).returns(yumhelper_multi_arch) - entries = described_class.fetch_latest_versions + described_class.expects(:python).with([described_class::YUMHELPER]).returns(yumhelper_multi_arch) + entries = described_class.fetch_latest_versions([], []) + expect(entries.keys).to include 'nss-tools.x86_64' + expect(entries.keys).to include 'pixman.x86_64' expect(entries.keys).to include 'nss-tools.i386' expect(entries.keys).to include 'pixman.i386' expect(entries['nss-tools']).to have(2).items expect(entries['pixman']).to have(2).items end + + it "passes the repos to enable to the helper" do + described_class.expects(:python).with do |script, *args| + expect(script).to eq described_class::YUMHELPER + expect(args).to eq %w[-e updates -e centosplus] + end.returns('') + described_class.fetch_latest_versions(['updates', 'centosplus'], []) + end + + it "passes the repos to disable to the helper" do + described_class.expects(:python).with do |script, *args| + expect(script).to eq described_class::YUMHELPER + expect(args).to eq %w[-d updates -d centosplus] + end.returns('') + described_class.fetch_latest_versions([], ['updates', 'centosplus']) + end + + it 'passes a combination of repos to the helper' do + described_class.expects(:python).with do |script, *args| + expect(script).to eq described_class::YUMHELPER + expect(args).to eq %w[-e os -e contrib -d updates -d centosplus] + end.returns('') + described_class.fetch_latest_versions(['os', 'contrib'], ['updates', 'centosplus']) + end end end