diff --git a/lib/puppet/provider/package/yum.rb b/lib/puppet/provider/package/yum.rb index 1c9cbf06e..bc065b7fa 100644 --- a/lib/puppet/provider/package/yum.rb +++ b/lib/puppet/provider/package/yum.rb @@ -1,113 +1,138 @@ 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") - attr_accessor :latest_info - 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 - return unless packages.detect { |name, package| package.should(:ensure) == :latest } + end - # collect our 'latest' info - updates = {} - python(self::YUMHELPER).each_line do |l| - l.chomp! - next if l.empty? - if l[0,4] == "_pkg" - hash = nevra_to_hash(l[5..-1]) - [hash[:name], "#{hash[:name]}.#{hash[:arch]}"].each do |n| - updates[n] ||= [] - updates[n] << hash - end - end + # Retrieve the latest package version information for a given package name. + # + # @api private + # @param package [String] The name of the package to query + # @return [Hash] + def self.latest_package_version(package) + if @latest_versions.nil? + @latest_versions = fetch_latest_versions end - # Add our 'latest' info to the providers. - packages.each do |name, package| - if info = updates[package[:name]] - package.provider.latest_info = info[0] + @latest_versions[package].first + end + + # Search for all installed packages that have newer versions. + # + # @api private + # @return [Hash>>] + def self.fetch_latest_versions + latest_versions = Hash.new {|h, k| h[k] = []} + + python(self::YUMHELPER).each_line do |l| + if (match = l.match /^_pkg (.*)$/) + hash = nevra_to_hash(match[1]) + + 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 = latest_info + upd = self.class.latest_package_version(@resource[:name]) 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") + @latest_info + end + + # @deprecated + def latest_info=(latest) + Puppet.deprecation_warning("#{self.class}#{__method__} is deprecated and no longer used") + @latest_info = latest + end end diff --git a/spec/unit/provider/package/yum_spec.rb b/spec/unit/provider/package/yum_spec.rb index d509df3d6..4172a814b 100755 --- a/spec/unit/provider/package/yum_spec.rb +++ b/spec/unit/provider/package/yum_spec.rb @@ -1,215 +1,206 @@ #! /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 '#latest' do - describe 'when latest_info is nil' do + describe 'determining the latest version available for a package' do + describe 'and a newer version is not available' do before :each do - provider.stubs(:latest_info).returns(nil) + described_class.stubs(:latest_package_version).with('mypackage').returns nil end - it 'raises if ensure is absent and latest_info is nil' do + 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' - ) + expect { + provider.latest + }.to raise_error(Puppet::DevError, 'Tried to get latest on a missing package') end - it 'returns the ensure value if the package is not already installed' do + 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 'when latest_info is populated' do - before :each do - provider.stubs(:latest_info).returns({ + describe 'and a newer version is available' do + let(:latest_version) do + { :name => 'mypackage', :epoch => '1', :version => '2.3.4', :release => '5', :arch => 'i686', - :provider => :yum, - :ensure => '2.3.4-5' - }) + } end it 'includes the epoch in the version string' do + described_class.stubs(:latest_package_version).returns(latest_version) provider.latest.should == '1:2.3.4-5' end end end - describe 'prefetching' do - let(:nevra_format) { Puppet::Type::Package::ProviderRpm::NEVRA_FORMAT } + describe "lazy loading of latest package versions" do + after { described_class.clear } + + let(:mypackage_version) do + { + :name => 'mypackage', + :epoch => '1', + :version => '2.3.4', + :release => '5', + :arch => 'i686', + } + end + + let(:latest_versions) { {'mypackage' => [mypackage_version]} } - let(:packages) do - <<-RPM_OUTPUT - cracklib-dicts 0 2.8.9 3.3 x86_64 - basesystem 0 8.0 5.1.1.el5.centos noarch - chkconfig 0 1.3.30.2 2.el5 x86_64 - myresource 0 1.2.3.4 5.el4 noarch - mysummaryless 0 1.2.3.4 5.el4 noarch - RPM_OUTPUT + 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) end - let(:yumhelper_output) do + 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) + 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(:execute_options) do - {:failonfail => true, :combine => true, :custom_environment => {}} + 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 - let(:rpm_version) { "RPM version 4.8.0\n" } - - let(:package_type) { Puppet::Type.type(:package) } - let(:yum_provider) { provider_class } - def pretend_we_are_root_for_yum_provider - Process.stubs(:euid).returns(0) + 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 + 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 - def expect_yum_provider_to_provide_rpm - Puppet::Type::Package::ProviderYum.stubs(:rpm).with('--version').returns(rpm_version) - Puppet::Type::Package::ProviderYum.expects(:command).with(:rpm).returns("/bin/rpm") + 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 + 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 - def expect_execpipe_to_provide_package_info_for_an_rpm_query - Puppet::Util::Execution.expects(:execpipe).with("/bin/rpm -qa --nosignature --nodigest --qf '#{nevra_format}'").yields(packages) - end - - def expect_python_yumhelper_call_to_return_latest_info - Puppet::Type::Package::ProviderYum.expects(:python).with(regexp_matches(/yumhelper.py$/)).returns(yumhelper_output) - end - - def a_package_type_instance_with_yum_provider_and_ensure_latest(name) - type_instance = package_type.new(:name => name) - type_instance.provider = yum_provider.new - type_instance[:ensure] = :latest - return type_instance - end - - before do - pretend_we_are_root_for_yum_provider - expect_yum_provider_to_provide_rpm - expect_execpipe_to_provide_package_info_for_an_rpm_query - expect_python_yumhelper_call_to_return_latest_info - end - - it "injects latest provider info into passed resources when prefetching" do - myresource = a_package_type_instance_with_yum_provider_and_ensure_latest('myresource') - mysummaryless = a_package_type_instance_with_yum_provider_and_ensure_latest('mysummaryless') - - yum_provider.prefetch({ "myresource" => myresource, "mysummaryless" => mysummaryless }) + 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 + expect(entries.keys).to include 'nss-tools.i386' + expect(entries.keys).to include 'pixman.i386' - expect(@logs.map(&:message).grep(/^Failed to match rpm line/)).to be_empty - expect(myresource.provider.latest_info).to eq({ - :name=>"myresource", - :epoch=>"0", - :version=>"1.2.3.4", - :release=>"5.el4", - :arch=>"noarch", - :provider=>:yum, - :ensure=>"1.2.3.4-5.el4" - }) + expect(entries['nss-tools']).to have(2).items + expect(entries['pixman']).to have(2).items end end end