diff --git a/lib/puppet/provider/package/yum.rb b/lib/puppet/provider/package/yum.rb index 8c4317d45..8c778cd8b 100644 --- a/lib/puppet/provider/package/yum.rb +++ b/lib/puppet/provider/package/yum.rb @@ -1,252 +1,284 @@ 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. This provider supports the `install_options` attribute, which allows command-line flags to be passed to yum. These options should be specified as a string (e.g. '--flag'), a hash (e.g. {'--flag' => 'value'}), or an array where each element is either a string or a hash." has_feature :install_options, :versionable, :virtual_packages 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 :osfamily => :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 # 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, enablerepo, disablerepo) key = [enablerepo, disablerepo] @latest_versions ||= {} if @latest_versions[key].nil? - @latest_versions[key] = fetch_latest_versions(enablerepo, disablerepo) + @latest_versions[key] = check_updates(enablerepo, disablerepo) end if @latest_versions[key][package] @latest_versions[key][package].first end end # Search for all installed packages that have newer versions, given a # combination of repositories to enable and disable. # # @api private # @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] = []} - - args = [self::YUMHELPER] + def self.check_updates(enablerepo, disablerepo) + args = [command(:yum), 'check-update'] args.concat(enablerepo.map { |repo| ['-e', repo] }.flatten) args.concat(disablerepo.map { |repo| ['-d', repo] }.flatten) - python(args).scan(/^_pkg (.*)$/) do |match| - hash = nevra_to_hash(match[0]) + output = Puppet::Util::Execution.execute(args, :failonfail => false, :combine => false) + + updates = {} + if output.exitstatus == 100 + updates = parse_updates(output) + elsif output.exitstatus == 0 + self.debug "yum check-update exited with 0; no package updates available." + else + self.warn "Could not check for updates, 'yum check-update' exited with #{output.exitstatus}" + end + updates + end + + def self.parse_updates(str) + # Strip off all content before the first blank line + body = str.partition(/^\s*\n/m).last + updates = Hash.new { |h, k| h[k] = [] } + body.lines.each do |line| + hash = update_to_hash(line) # 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 + updates[short_name] << hash + updates[long_name] << hash end - latest_versions + + updates + end + + def self.update_to_hash(line) + pkgname, pkgversion, *_ = line.split(/\s+/) + name, arch = pkgname.split('.') + + match = pkgversion.match(/^(?:(\d+):)?(\S+)-(\S+)$/) + epoch = match[1] || '0' + version = match[2] + release = match[3] + + { + :name => name, + :epoch => epoch, + :version => version, + :release => release, + :arch => arch, + } end def self.clear @latest_versions = nil end def install wanted = @resource[:name] # If not allowing virtual packages, do a query to ensure a real package exists unless @resource.allow_virtual? yum *['-d', '0', '-e', '0', '-y', install_options, :list, wanted].compact end should = @resource.should(:ensure) self.debug "Ensuring => #{should}" operation = :install case should when true, false, Symbol # pass should = nil else # Add the package version wanted += "-#{should}" is = self.query if is && yum_compareEVR(yum_parse_evr(should), yum_parse_evr(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 # If a version was specified, query again to see if it is a matching version if should 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] ? vercmp_result = yum_compareEVR(yum_parse_evr(should), yum_parse_evr(is[:ensure])) raise Puppet::Error, "Failed to update to version #{should}, got version #{is[:ensure]} instead" if vercmp_result != 0 end end # What's the latest package version available? def latest 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 # parse a yum "version" specification # this re-implements yum's # rpmUtils.miscutils.stringToVersion() in ruby def yum_parse_evr(s) ei = s.index(':') if ei e = s[0,ei] s = s[ei+1,s.length] else e = nil end e = String(Bignum(e)) rescue '0' ri = s.index('-') if ri v = s[0,ri] r = s[ri+1,s.length] else v = s r = nil end return { :epoch => e, :version => v, :release => r } end # how yum compares two package versions: # rpmUtils.miscutils.compareEVR(), which massages data types and then calls # rpm.labelCompare(), found in rpm.git/python/header-py.c, which # sets epoch to 0 if null, then compares epoch, then ver, then rel # using compare_values() and returns the first non-0 result, else 0. # This function combines the logic of compareEVR() and labelCompare(). # # "version_should" can be v, v-r, or e:v-r. # "version_is" will always be at least v-r, can be e:v-r def yum_compareEVR(should_hash, is_hash) # pass on to rpm labelCompare rc = compare_values(should_hash[:epoch], is_hash[:epoch]) return rc unless rc == 0 rc = compare_values(should_hash[:version], is_hash[:version]) return rc unless rc == 0 # here is our special case, PUP-1244. # if should_hash[:release] is nil (not specified by the user), # and comparisons up to here are equal, return equal. We need to # evaluate to whatever level of detail the user specified, so we # don't end up upgrading or *downgrading* when not intended. # # This should NOT be triggered if we're trying to ensure latest. return 0 if should_hash[:release].nil? rc = compare_values(should_hash[:release], is_hash[:release]) return rc end # this method is a native implementation of the # compare_values function in rpm's python bindings, # found in python/header-py.c, as used by yum. def compare_values(s1, s2) if s1.nil? && s2.nil? return 0 elsif ( not s1.nil? ) && s2.nil? return 1 elsif s1.nil? && (not s2.nil?) return -1 end return rpmvercmp(s1, s2) 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/lib/puppet/provider/package/yumhelper.py b/lib/puppet/provider/package/yumhelper.py deleted file mode 100644 index a1f3f68e3..000000000 --- a/lib/puppet/provider/package/yumhelper.py +++ /dev/null @@ -1,159 +0,0 @@ -# Python helper script to query for the packages that have -# pending updates. Called by the yum package provider -# -# (C) 2007 Red Hat Inc. -# David Lutterkort - -import sys -import string -import re - -# this maintains compatibility with really old platforms with python 1.x -from os import popen, WEXITSTATUS - -# Try to use the yum libraries by default, but shell out to the yum executable -# if they are not present (i.e. yum <= 2.0). This is only required for RHEL3 -# and earlier that do not support later versions of Yum. Once RHEL3 is EOL, -# shell_out() and related code can be removed. -try: - import yum -except ImportError: - useyumlib = 0 -else: - useyumlib = 1 - -OVERRIDE_OPTS = { - 'debuglevel': 0, - 'errorlevel': 0, - 'logfile': '/dev/null' -} - -enable = [] -disable = [] -args = list(sys.argv) - -while len(args) > 0: - a = args.pop(0) - - if a == "-d": - if len(args) < 1: - raise ValueError, a - next = args.pop(0) - disable.extend( next.split(",") ) - if a == "-e": - if len(args) < 1: - raise ValueError, a - next = args.pop(0) - enable.extend( next.split(",") ) - -def pkg_lists(my): - my.doConfigSetup() - - for k in OVERRIDE_OPTS.keys(): - if hasattr(my.conf, k): - setattr(my.conf, k, OVERRIDE_OPTS[k]) - else: - my.conf.setConfigOption(k, OVERRIDE_OPTS[k]) - - my.doTsSetup() - my.doRpmDBSetup() - - # Yum 2.2/2.3 python libraries require a couple of extra function calls to setup package sacks. - # They also don't have a __version__ attribute - try: - yumver = yum.__version__ - except AttributeError: - my.doRepoSetup() - my.doSackSetup() - - return my.doPackageLists('updates') - -def shell_out(): - try: - repostring = "" - if disable: - repostring += " '--disablerepo=%s'" % ",".join(disable) - if enable: - repostring += " '--disablerepo=%s'" % ",".join(enable) - p = popen("/usr/bin/env yum%s check-update 2>&1" % repostring) - output = p.readlines() - rc = p.close() - - if rc is not None: - # None represents exit code of 0, otherwise the exit code is in the - # format returned by wait(). Exit code of 100 from yum represents - # updates available. - if WEXITSTATUS(rc) != 100: - return WEXITSTATUS(rc) - else: - # Exit code is None (0), no updates waiting so don't both parsing output - return 0 - - # Yum prints a line of hyphens (old versions) or a blank line between - # headers and package data, so skip everything before them - skipheaders = 0 - for line in output: - if not skipheaders: - if re.compile("^((-){80}|)$").search(line): - skipheaders = 1 - continue - - # Skip any blank lines - if re.compile("^[ \t]*$").search(line): - continue - - # Format is: - # Yum 1.x: name arch (epoch:)?version - # Yum 2.0: name arch (epoch:)?version repo - # epoch is optional if 0 - - p = string.split(line) - pname = p[0] - parch = p[1] - pevr = p[2] - - # Separate out epoch:version-release - evr_re = re.compile("^(\d:)?(\S+)-(\S+)$") - evr = evr_re.match(pevr) - - pepoch = "" - if evr.group(1) is None: - pepoch = "0" - else: - pepoch = evr.group(1).replace(":", "") - pversion = evr.group(2) - prelease = evr.group(3) - - print "_pkg", pname, pepoch, pversion, prelease, parch - - return 0 - except: - print sys.exc_info()[0] - return 1 - -if useyumlib: - try: - try: - my = yum.YumBase() - - for repo in disable: - my.repos.disableRepo(repo) - - for repo in enable: - my.repos.enableRepo(repo) - - ypl = pkg_lists(my) - for pkg in ypl.updates: - print "_pkg %s %s %s %s %s" % (pkg.name, pkg.epoch, pkg.version, pkg.release, pkg.arch) - finally: - my.closeRpmDB() - except IOError, e: - print "_err IOError %d %s" % (e.errno, e) - sys.exit(1) - except AttributeError, e: - # catch yumlib errors in buggy 2.x versions of yum - print "_err AttributeError %s" % e - sys.exit(1) -else: - rc = shell_out() - sys.exit(rc) diff --git a/spec/unit/provider/package/yum_spec.rb b/spec/unit/provider/package/yum_spec.rb index 9b6b69d71..63e653ded 100755 --- a/spec/unit/provider/package/yum_spec.rb +++ b/spec/unit/provider/package/yum_spec.rb @@ -1,446 +1,479 @@ #! /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 describe 'provider features' do it { should be_versionable } it { should be_install_options } it { should be_virtual_packages } 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 'package evr parsing' do it 'should parse full simple evr' do v = provider.yum_parse_evr('0:1.2.3-4.el5') v[:epoch].should == '0' v[:version].should == '1.2.3' v[:release].should == '4.el5' end it 'should parse version only' do v = provider.yum_parse_evr('1.2.3') v[:epoch].should == '0' v[:version].should == '1.2.3' v[:release].should == nil end it 'should parse version-release' do v = provider.yum_parse_evr('1.2.3-4.5.el6') v[:epoch].should == '0' v[:version].should == '1.2.3' v[:release].should == '4.5.el6' end it 'should parse release with git hash' do v = provider.yum_parse_evr('1.2.3-4.1234aefd') v[:epoch].should == '0' v[:version].should == '1.2.3' v[:release].should == '4.1234aefd' end it 'should parse single integer versions' do v = provider.yum_parse_evr('12345') v[:epoch].should == '0' v[:version].should == '12345' v[:release].should == nil end it 'should parse text in the epoch to 0' do v = provider.yum_parse_evr('foo0:1.2.3-4') v[:epoch].should == '0' v[:version].should == '1.2.3' v[:release].should == '4' end it 'should parse revisions with text' do v = provider.yum_parse_evr('1.2.3-SNAPSHOT20140107') v[:epoch].should == '0' v[:version].should == '1.2.3' v[:release].should == 'SNAPSHOT20140107' end # test cases for PUP-682 it 'should parse revisions with text and numbers' do v = provider.yum_parse_evr('2.2-SNAPSHOT20121119105647') v[:epoch].should == '0' v[:version].should == '2.2' v[:release].should == 'SNAPSHOT20121119105647' end end describe 'yum evr comparison' do # currently passing tests it 'should evaluate identical version-release as equal' do v = provider.yum_compareEVR({:epoch => '0', :version => '1.2.3', :release => '1.el5'}, {:epoch => '0', :version => '1.2.3', :release => '1.el5'}) v.should == 0 end it 'should evaluate identical version as equal' do v = provider.yum_compareEVR({:epoch => '0', :version => '1.2.3', :release => nil}, {:epoch => '0', :version => '1.2.3', :release => nil}) v.should == 0 end it 'should evaluate identical version but older release as less' do v = provider.yum_compareEVR({:epoch => '0', :version => '1.2.3', :release => '1.el5'}, {:epoch => '0', :version => '1.2.3', :release => '2.el5'}) v.should == -1 end it 'should evaluate identical version but newer release as greater' do v = provider.yum_compareEVR({:epoch => '0', :version => '1.2.3', :release => '3.el5'}, {:epoch => '0', :version => '1.2.3', :release => '2.el5'}) v.should == 1 end it 'should evaluate a newer epoch as greater' do v = provider.yum_compareEVR({:epoch => '1', :version => '1.2.3', :release => '4.5'}, {:epoch => '0', :version => '1.2.3', :release => '4.5'}) v.should == 1 end # these tests describe PUP-1244 logic yet to be implemented it 'should evaluate any version as equal to the same version followed by release' do v = provider.yum_compareEVR({:epoch => '0', :version => '1.2.3', :release => nil}, {:epoch => '0', :version => '1.2.3', :release => '2.el5'}) v.should == 0 end # test cases for PUP-682 it 'should evaluate same-length numeric revisions numerically' do provider.yum_compareEVR({:epoch => '0', :version => '2.2', :release => '405'}, {:epoch => '0', :version => '2.2', :release => '406'}).should == -1 end end describe 'yum version segment comparison' do it 'should treat two nil values as equal' do v = provider.compare_values(nil, nil) v.should == 0 end it 'should treat a nil value as less than a non-nil value' do v = provider.compare_values(nil, '0') v.should == -1 end it 'should treat a non-nil value as greater than a nil value' do v = provider.compare_values('0', nil) v.should == 1 end it 'should pass two non-nil values on to rpmvercmp' do provider.stubs(:rpmvercmp) { 0 } provider.expects(:rpmvercmp).with('s1', 's2') provider.compare_values('s1', 's2') 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, name) provider.install end it 'should use :install to update' do provider.expects(:install) provider.update end it 'should be able to set version' do version = '1.2' resource[:ensure] = version provider.expects(:yum).with('-d', '0', '-e', '0', '-y', :install, "#{name}-#{version}") provider.stubs(:query).returns :ensure => version provider.install end it 'should handle partial versions specified' do version = '1.3.4' resource[:ensure] = version provider.stubs(:query).returns :ensure => '1.3.4-1.el6' provider.install end it 'should be able to downgrade' do current_version = '1.2' version = '1.0' resource[:ensure] = '1.0' provider.expects(:yum).with('-d', '0', '-e', '0', '-y', :downgrade, "#{name}-#{version}") provider.stubs(:query).returns(:ensure => current_version).then.returns(:ensure => version) 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, name) provider.install end it 'allow virtual packages' do resource[:ensure] = :installed resource[:allow_virtual] = true provider.expects(:yum).with('-d', '0', '-e', '0', '-y', :list, name).never provider.expects(:yum).with('-d', '0', '-e', '0', '-y', :install, name) provider.install end end describe 'when uninstalling' do it 'should use erase to purge' do provider.expects(:yum).with('-y', :erase, name) 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(name, ['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(name, [], ['updates', 'centosplus']) provider.latest end describe 'and a newer version is not available' do before :each do described_class.stubs(:latest_package_version).with(name, [], []).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 => name, :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).with(name, [], []).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 => name, :epoch => '1', :version => '2.3.4', :release => '5', :arch => 'i686', } end let(:mypackage_newerversion) do { :name => name, :epoch => '1', :version => '4.5.6', :release => '7', :arch => 'i686', } end let(:latest_versions) { {name => [mypackage_version]} } let(:enabled_versions) { {name => [mypackage_newerversion]} } it "returns the version hash if the package was found" do - described_class.expects(:fetch_latest_versions).with([], []).once.returns(latest_versions) + described_class.expects(:check_updates).with([], []).once.returns(latest_versions) version = described_class.latest_package_version(name, [], []) expect(version).to eq(mypackage_version) end it "is nil if the package was not found in the query" do - described_class.expects(:fetch_latest_versions).with([], []).once.returns(latest_versions) + described_class.expects(:check_updates).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.expects(:fetch_latest_versions).with([], []).once.returns(latest_versions) + described_class.expects(:check_updates).with([], []).once.returns(latest_versions) 2.times { version = described_class.latest_package_version(name, [], []) 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) + described_class.expects(:check_updates).with([], []).once.returns(latest_versions) + described_class.expects(:check_updates).with(['enabled'], ['disabled']).once.returns(enabled_versions) 2.times { version = described_class.latest_package_version(name, [], []) expect(version).to eq mypackage_version } 2.times { version = described_class.latest_package_version(name, ['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([], []) - 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([], []) - 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([], []) - 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']) + describe "executing yum check-update" do + before do + described_class.stubs(:command).with(:yum).returns '/usr/bin/yum' + end + + it "passes repos to enable to 'yum check-update'" do + Puppet::Util::Execution.expects(:execute).with do |args, *rest| + expect(args).to eq %w[/usr/bin/yum check-update -e updates -e centosplus] + end.returns(stub(:exitstatus => 0)) + described_class.check_updates(%w[updates centosplus], []) + end + + it "passes repos to disable to 'yum check-update'" do + Puppet::Util::Execution.expects(:execute).with do |args, *rest| + expect(args).to eq %w[/usr/bin/yum check-update -d updates -d centosplus] + end.returns(stub(:exitstatus => 0)) + described_class.check_updates([],%w[updates centosplus]) + end + + it "passes a combination of repos to enable and disable to 'yum check-update'" do + Puppet::Util::Execution.expects(:execute).with do |args, *rest| + expect(args).to eq %w[/usr/bin/yum check-update -e os -e contrib -d updates -d centosplus] + end.returns(stub(:exitstatus => 0)) + described_class.check_updates(%w[os contrib], %w[updates centosplus]) + end + + it "returns an empty hash if 'yum check-update' returned 0" do + Puppet::Util::Execution.expects(:execute).returns(stub :exitstatus => 0) + expect(described_class.check_updates([], [])).to be_empty + end + + it "returns a populated hash if 'yum check-update returned 100'" do + output = stub(:exitstatus => 100) + Puppet::Util::Execution.expects(:execute).returns(output) + described_class.expects(:parse_updates).with(output).returns({:has => :updates}) + expect(described_class.check_updates([], [])).to eq({:has => :updates}) + end + + it "returns an empty hash if 'yum check-update' returned an exit code that was not 0 or 100" do + Puppet::Util::Execution.expects(:execute).returns(stub(:exitstatus => 1)) + described_class.expects(:warn) + expect(described_class.check_updates([], [])).to eq({}) + end + end + + describe "parsing the output of check-update" do + let(:check_update) do + # Trailing whitespace is intentional + <<-EOD +Loaded plugins: fastestmirror +Determining fastest mirrors + * base: centos.sonn.com + * epel: ftp.osuosl.org + * extras: mirror.web-ster.com + * updates: centos.sonn.com + +curl.i686 7.32.0-10.fc20 updates +curl.x86_64 7.32.0-10.fc20 updates +gawk.i686 4.1.0-3.fc20 updates +dhclient.i686 12:4.1.1-38.P1.fc20 updates +selinux-policy.noarch 3.12.1-163.fc20 updates-testing + EOD + end + + it 'creates an entry for each package keyed on the package name' do + output = described_class.parse_updates(check_update) + expect(output['curl']).to eq([{:name => 'curl', :epoch => '0', :version => '7.32.0', :release => '10.fc20', :arch => 'i686'}, {:name => 'curl', :epoch => '0', :version => '7.32.0', :release => '10.fc20', :arch => 'x86_64'}]) + expect(output['gawk']).to eq([{:name => 'gawk', :epoch => '0', :version => '4.1.0', :release => '3.fc20', :arch => 'i686'}]) + expect(output['dhclient']).to eq([{:name => 'dhclient', :epoch => '12', :version => '4.1.1', :release => '38.P1.fc20', :arch => 'i686'}]) + expect(output['selinux-policy']).to eq([{:name => 'selinux-policy', :epoch => '0', :version => '3.12.1', :release => '163.fc20', :arch => 'noarch'}]) + end + + it 'creates an entry for each package keyed on the package name and package architecture' do + output = described_class.parse_updates(check_update) + expect(output['curl.i686']).to eq([{:name => 'curl', :epoch => '0', :version => '7.32.0', :release => '10.fc20', :arch => 'i686'}]) + expect(output['curl.x86_64']).to eq([{:name => 'curl', :epoch => '0', :version => '7.32.0', :release => '10.fc20', :arch => 'x86_64'}]) + expect(output['gawk.i686']).to eq([{:name => 'gawk', :epoch => '0', :version => '4.1.0', :release => '3.fc20', :arch => 'i686'}]) + expect(output['dhclient.i686']).to eq([{:name => 'dhclient', :epoch => '12', :version => '4.1.1', :release => '38.P1.fc20', :arch => 'i686'}]) + expect(output['selinux-policy.noarch']).to eq([{:name => 'selinux-policy', :epoch => '0', :version => '3.12.1', :release => '163.fc20', :arch => 'noarch'}]) + end + end + + describe "parsing a line from yum check-update" do + it "splits up the package name and architecture fields" do + checkupdate = "curl.i686 7.32.0-10.fc20 updates" + + parsed = described_class.update_to_hash(checkupdate) + expect(parsed[:name]).to eq 'curl' + expect(parsed[:arch]).to eq 'i686' + end + + it "splits up the epoch, version, and release fields" do + checkupdate = "dhclient.i686 12:4.1.1-38.P1.el6.centos base" + parsed = described_class.update_to_hash(checkupdate) + expect(parsed[:epoch]).to eq '12' + expect(parsed[:version]).to eq '4.1.1' + expect(parsed[:release]).to eq '38.P1.el6.centos' + end + + it "sets the epoch to 0 when an epoch is not specified" do + checkupdate = "curl.i686 7.32.0-10.fc20 updates" + + parsed = described_class.update_to_hash(checkupdate) + expect(parsed[:epoch]).to eq '0' + expect(parsed[:version]).to eq '7.32.0' + expect(parsed[:release]).to eq '10.fc20' end end end