diff options
author | Bryan McLellan <btm@opscode.com> | 2011-05-19 18:06:34 -0700 |
---|---|---|
committer | Bryan McLellan <btm@opscode.com> | 2011-05-19 18:06:34 -0700 |
commit | c039deea60518ae77864f23deb3e6050161c9864 (patch) | |
tree | 483d3d8870b6b1a0e886a2a1730fb1a741355180 | |
parent | f46c774ed54632d8fe8a62119ec73c9fc03f5940 (diff) | |
parent | 6d28564e2be8838baa2e585f43f48b57d8f61d0c (diff) | |
download | chef-c039deea60518ae77864f23deb3e6050161c9864.tar.gz |
Merge branch 'yum-improvements'
-rw-r--r-- | chef/lib/chef/monkey_patches/numeric.rb | 10 | ||||
-rw-r--r-- | chef/lib/chef/monkey_patches/string.rb | 9 | ||||
-rw-r--r-- | chef/lib/chef/provider/package/yum-dump.py | 265 | ||||
-rw-r--r-- | chef/lib/chef/provider/package/yum.rb | 803 | ||||
-rw-r--r-- | chef/lib/chef/resource/yum_package.rb | 20 | ||||
-rw-r--r-- | chef/spec/unit/provider/package/yum_spec.rb | 1118 | ||||
-rw-r--r-- | chef/spec/unit/resource/yum_package_spec.rb | 36 |
7 files changed, 2047 insertions, 214 deletions
diff --git a/chef/lib/chef/monkey_patches/numeric.rb b/chef/lib/chef/monkey_patches/numeric.rb index fef1f27693..1f5ff14209 100644 --- a/chef/lib/chef/monkey_patches/numeric.rb +++ b/chef/lib/chef/monkey_patches/numeric.rb @@ -4,4 +4,12 @@ unless 0.respond_to?(:fdiv) to_f / other end end -end
\ No newline at end of file +end + +# String elements referenced with [] <= 1.8.6 return a Fixnum. Cheat to allow +# for the simpler "test"[2].ord construct +class Numeric + def ord + return self + end +end diff --git a/chef/lib/chef/monkey_patches/string.rb b/chef/lib/chef/monkey_patches/string.rb index ac23f09647..1ea4be3dd0 100644 --- a/chef/lib/chef/monkey_patches/string.rb +++ b/chef/lib/chef/monkey_patches/string.rb @@ -26,3 +26,12 @@ class String alias :bytesize :size end end + +# <= 1.8.6 needs some ord! +class String + unless method_defined?(:ord) + def ord + self.unpack('c').first + end + end +end diff --git a/chef/lib/chef/provider/package/yum-dump.py b/chef/lib/chef/provider/package/yum-dump.py index 28348d58bb..4cb7dd139c 100644 --- a/chef/lib/chef/provider/package/yum-dump.py +++ b/chef/lib/chef/provider/package/yum-dump.py @@ -1,6 +1,6 @@ # # Author:: Matthew Kent (<mkent@magoazul.com>) -# Copyright:: Copyright (c) 2009 Matthew Kent +# Copyright:: Copyright (c) 2009, 2011 Matthew Kent # License:: Apache License, Version 2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -19,110 +19,217 @@ # yum-dump.py # Inspired by yumhelper.py by David Lutterkort # -# Produce a list of installed and available packages using yum and dump the -# result to stdout. +# Produce a list of installed, available and re-installable packages using yum +# and dump the results to stdout. # -# This invokes yum just as the command line would which makes it subject to -# all the caching related configuration paramaters in yum.conf. +# yum-dump invokes yum similarly to the command line interface which makes it +# subject to most of the configuration paramaters in yum.conf. yum-dump will +# also load yum plugins in the same manor as yum - these can affect the output. # # Can be run as non root, but that won't update the cache. +# +# Intended to support yum 2.x and 3.x import os import sys import time import yum +import re +import errno from yum import Errors +from optparse import OptionParser -PIDFILE='/var/run/yum.pid' +YUM_PID_FILE='/var/run/yum.pid' # Seconds to wait for exclusive access to yum -lock_timeout = 10 +LOCK_TIMEOUT = 10 -failure = False +if re.search(r"^3\.", yum.__version__): + YUM_VER = 3 +elif re.search(r"^2\.", yum.__version__): + YUM_VER = 2 +else: + print >> sys.stderr, "yum-dump Error: Can't match supported yum version" \ + " (%s)" % yum.__version__ + sys.exit(1) -# Can't do try: except: finally: in python 2.4 it seems, hence this fun. -try: - try: - y = yum.YumBase() +def setup(yb, options): + # Only want our output + # + if YUM_VER == 3: try: - # Only want our output - y.doConfigSetup(errorlevel=0,debuglevel=0) - except: - # but of course, yum on even moderately old - # redhat/centosen doesn't know how to do logging properly - # so we duck punch our way to victory - def __log(a,b): pass - y.doConfigSetup() - y.log = __log - y.errorlog = __log - - # Yum assumes it can update the cache directory. Disable this for non root - # users. - y.conf.cache = os.geteuid() != 0 - - # Override any setting in yum.conf - we only care about the newest - y.conf.showdupesfromrepos = False - - # Spin up to lock_timeout. - countdown = lock_timeout + yb.preconf.errorlevel=0 + yb.preconf.debuglevel=0 + + # initialize the config + yb.conf + except yum.Errors.ConfigError, e: + # supresses an ignored exception at exit + yb.preconf = None + print >> sys.stderr, "yum-dump Config Error: %s" % e + return 1 + except ValueError, e: + yb.preconf = None + print >> sys.stderr, "yum-dump Options Error: %s" % e + return 1 + elif YUM_VER == 2: + yb.doConfigSetup() + + def __log(a,b): pass + + yb.log = __log + yb.errorlog = __log + + # Give Chef every possible package version, it can decide what to do with them + if YUM_VER == 3: + yb.conf.showdupesfromrepos = True + elif YUM_VER == 2: + yb.conf.setConfigOption('showdupesfromrepos', True) + + # Optionally run only on cached repositories, but non root must use the cache + if os.geteuid() != 0: + if YUM_VER == 3: + yb.conf.cache = True + elif YUM_VER == 2: + yb.conf.setConfigOption('cache', True) + else: + if YUM_VER == 3: + yb.conf.cache = options.cache + elif YUM_VER == 2: + yb.conf.setConfigOption('cache', options.cache) + + return 0 + +def dump_packages(yb, list): + packages = {} + + if YUM_VER == 2: + yb.doTsSetup() + yb.doRepoSetup() + yb.doSackSetup() + + db = yb.doPackageLists(list) + + for pkg in db.installed: + pkg.type = 'i' + # __str__ contains epoch, name etc + packages[str(pkg)] = pkg + + for pkg in db.available: + pkg.type = 'a' + packages[str(pkg)] = pkg + + if YUM_VER == 2: + # ugh - can't get the availability state of our installed rpms, lets assume + # they are available to install + for pkg in db.installed: + pkg.type = 'r' + packages[str(pkg)] = pkg + else: + # These are both installed and available + for pkg in db.reinstall_available: + pkg.type = 'r' + packages[str(pkg)] = pkg + + unique_packages = packages.values() + + unique_packages.sort(lambda x, y: cmp(x.name, y.name)) + + for pkg in unique_packages: + print '%s %s %s %s %s %s' % ( pkg.name, + pkg.epoch, + pkg.version, + pkg.release, + pkg.arch, + pkg.type ) + return 0 + +def yum_dump(options): + lock_obtained = False + + yb = yum.YumBase() + + status = setup(yb, options) + if status != 0: + return status + + if options.output_options: + print "[option installonlypkgs] %s" % " ".join(yb.conf.installonlypkgs) + + # Non root can't handle locking on rhel/centos 4 + if os.geteuid() != 0: + return dump_packages(yb, options.package_list) + + # Wrap the collection and output of packages in yum's global lock to prevent + # any inconsistencies. + try: + # Spin up to LOCK_TIMEOUT + countdown = LOCK_TIMEOUT while True: try: - y.doLock(PIDFILE) + yb.doLock(YUM_PID_FILE) + lock_obtained = True except Errors.LockError, e: time.sleep(1) countdown -= 1 if countdown == 0: - print >> sys.stderr, "Error! Couldn't obtain an exclusive yum lock in %d seconds. Giving up." % lock_timeout - failure = True - sys.exit(1) + print >> sys.stderr, "yum-dump Locking Error! Couldn't obtain an " \ + "exclusive yum lock in %d seconds. Giving up." % LOCK_TIMEOUT + return 200 else: break - - y.doTsSetup() - y.doRpmDBSetup() - + + return dump_packages(yb, options.package_list) + + # Ensure we clear the lock and cleanup any resources + finally: try: - db = y.doPackageLists('all') - except AttributeError: - # some people claim that testing for yum.__version__ should be - # enough to see if this is required, but I say they're liars. - # the yum on 4.8 at least understands yum.__version__ but still - # needs to get its repos and sacks set up manually. - # Thus, we just try it, fail, and then try again. WCPGW? - y.doRepoSetup() - y.doSackSetup() - db = y.doPackageLists('all') - - y.closeRpmDB() + yb.closeRpmDB() + if lock_obtained == True: + yb.doUnlock(YUM_PID_FILE) + except Errors.LockError, e: + print >> sys.stderr, "yum-dump Unlock Error: %s" % e + return 200 + +def main(): + usage = "Usage: %prog [options]\n" + \ + "Output a list of installed, available and re-installable packages via yum" + parser = OptionParser(usage=usage) + parser.add_option("-o", "--options", + action="store_true", dest="output_options", default=False, + help="output select yum options useful to Chef") + parser.add_option("-C", "--cache", + action="store_true", dest="cache", default=False, + help="run entirely from cache, don't update cache") + parser.add_option("-i", "--installed", + action="store_const", const="installed", dest="package_list", default="all", + help="output only installed packages") + parser.add_option("-a", "--available", + action="store_const", const="available", dest="package_list", default="all", + help="output only available and re-installable packages") - except Errors.YumBaseError, e: - print >> sys.stderr, "Error! %s" % e - failure = True - sys.exit(1) + (options, args) = parser.parse_args() -# Ensure we clear the lock. -finally: try: - y.doUnlock(PIDFILE) - # Keep Unlock from raising a second exception as it does with a yum.conf - # config error. - except Errors.YumBaseError: - if failure == False: - print >> sys.stderr, "Error! %s" % e + return yum_dump(options) + + except yum.Errors.RepoError, e: + print >> sys.stderr, "yum-dump Repository Error: %s" % e + return 1 + + except yum.Errors.YumBaseError, e: + print >> sys.stderr, "yum-dump General Error: %s" % e + return 1 + +try: + status = main() +# Suppress a nasty broken pipe error when output is piped to utilities like +# 'head' +except IOError, e: + if e.errno == errno.EPIPE: sys.exit(1) - -for pkg in db.installed: - print '%s,installed,%s,%s,%s,%s' % ( pkg.name, - pkg.epoch, - pkg.version, - pkg.release, - pkg.arch ) -for pkg in db.available: - print '%s,available,%s,%s,%s,%s' % ( pkg.name, - pkg.epoch, - pkg.version, - pkg.release, - pkg.arch ) - -sys.exit(0) + else: + raise + +sys.exit(status) diff --git a/chef/lib/chef/provider/package/yum.rb b/chef/lib/chef/provider/package/yum.rb index f57243321f..2ac745edc9 100644 --- a/chef/lib/chef/provider/package/yum.rb +++ b/chef/lib/chef/provider/package/yum.rb @@ -26,135 +26,636 @@ class Chef class Package class Yum < Chef::Provider::Package - class YumCache - include Chef::Mixin::Command - include Singleton + class RPMUtils + class << self + + # RPM::Version version_parse equivalent + def version_parse(evr) + return if evr.nil? + + epoch = nil + # assume this is a version + version = evr + release = nil + + lead = 0 + tail = evr.size + + if evr =~ %r{^([\d]+):} + epoch = $1.to_i + lead = $1.length + 1 + elsif evr[0].ord == ":".ord + epoch = 0 + lead = 1 + end - def initialize - load_data + if evr =~ %r{:?.*-(.*)$} + release = $1 + tail = evr.length - release.length - lead - 1 + + if release.empty? + release = nil + end + end + + version = evr[lead,tail] + + [ epoch, version, release ] + end + + # verify + def isalnum(x) + isalpha(x) or isdigit(x) + end + + def isalpha(x) + v = x.ord + (v >= 65 and v <= 90) or (v >= 97 and v <= 122) + end + + def isdigit(x) + v = x.ord + v >= 48 and v <= 57 + end + + # based on the reference spec in lib/rpmvercmp.c in rpm 4.9.0 + def rpmvercmp(x, y) + # easy! :) + return 0 if x == y + + if x.nil? + x = "" + end + + if y.nil? + y = "" + end + + # not so easy :( + # + # takes 2 strings like + # + # x = "1.20.b18.el5" + # y = "1.20.b17.el5" + # + # breaks into purely alpha and numeric segments and compares them using + # some rules + # + # * 10 > 1 + # * 1 > a + # * z > a + # * Z > A + # * z > Z + # * leading zeros are ignored + # * separators (periods, commas) are ignored + # * "1.20.b18.el5.extrastuff" > "1.20.b18.el5" + + x_pos = 0 # overall string element reference position + x_pos_max = x.length - 1 # number of elements in string, starting from 0 + x_seg_pos = 0 # segment string element reference position + x_comp = nil # segment to compare + + y_pos = 0 + y_seg_pos = 0 + y_pos_max = y.length - 1 + y_comp = nil + + while (x_pos <= x_pos_max and y_pos <= y_pos_max) + # first we skip over anything non alphanumeric + while (x_pos <= x_pos_max) and (isalnum(x[x_pos]) == false) + x_pos += 1 # +1 over pos_max if end of string + end + while (y_pos <= y_pos_max) and (isalnum(y[y_pos]) == false) + y_pos += 1 + end + + # if we hit the end of either we are done matching segments + if (x_pos == x_pos_max + 1) or (y_pos == y_pos_max + 1) + break + end + + # we are now at the start of a alpha or numeric segment + x_seg_pos = x_pos + y_seg_pos = y_pos + + # grab segment so we can compare them + if isdigit(x[x_seg_pos].ord) + x_seg_is_num = true + + # already know it's a digit + x_seg_pos += 1 + + # gather up our digits + while (x_seg_pos <= x_pos_max) and isdigit(x[x_seg_pos]) + x_seg_pos += 1 + end + # copy the segment but not the unmatched character that x_seg_pos will + # refer to + x_comp = x[x_pos,x_seg_pos - x_pos] + + while (y_seg_pos <= y_pos_max) and isdigit(y[y_seg_pos]) + y_seg_pos += 1 + end + y_comp = y[y_pos,y_seg_pos - y_pos] + else + # we are comparing strings + x_seg_is_num = false + + while (x_seg_pos <= x_pos_max) and isalpha(x[x_seg_pos]) + x_seg_pos += 1 + end + x_comp = x[x_pos,x_seg_pos - x_pos] + + while (y_seg_pos <= y_pos_max) and isalpha(y[y_seg_pos]) + y_seg_pos += 1 + end + y_comp = y[y_pos,y_seg_pos - y_pos] + end + + # if y_seg_pos didn't advance in the above loop it means the segments are + # different types + if y_pos == y_seg_pos + # numbers always win over letters + return x_seg_is_num ? 1 : -1 + end + + # move the ball forward before we mess with the segments + x_pos += x_comp.length # +1 over pos_max if end of string + y_pos += y_comp.length + + # we are comparing numbers - simply convert them + if x_seg_is_num + x_comp = x_comp.to_i + y_comp = y_comp.to_i + end + + # compares ints or strings + # don't return if equal - try the next segment + if x_comp > y_comp + return 1 + elsif x_comp < y_comp + return -1 + end + + # if we've reached here than the segments are the same - try again + end + + # we must have reached the end of one or both of the strings and they + # matched up until this point + + # segments matched completely but the segment separators were different - + # rpm reference code treats these as equal. + if (x_pos == x_pos_max + 1) and (y_pos == y_pos_max + 1) + return 0 + end + + # the most unprocessed characters left wins + if (x_pos_max - x_pos) > (y_pos_max - y_pos) + return 1 + else + return -1 + end + end + + end # self + end # RPMUtils + + class RPMVersion + include Comparable + + def initialize(*args) + if args.size == 1 + @e, @v, @r = RPMUtils.version_parse(args[0]) + elsif args.size == 3 + @e = args[0].to_i + @v = args[1] + @r = args[2] + else + raise ArgumentError, "Expecting either 'epoch-version-release' or 'epoch, " + + "version, release'" + end end + attr_reader :e, :v, :r + alias :epoch :e + alias :version :v + alias :release :r + + def self.parse(*args) + self.new(*args) + end + + # rough RPM::Version rpm_version_cmp equivalent - except much slower :) + def <=>(y) + x = self + + # compare epoch + if (x.e.nil? == false and x.e > 0) and y.e.nil? + return 1 + elsif x.e.nil? and (y.e.nil? == false and y.e > 0) + return -1 + elsif x.e.nil? == false and y.e.nil? == false + if x.e < y.e + return -1 + elsif x.e > y.e + return 1 + end + end - def stale? - interval = Chef::Config[:interval].to_f + # compare version + if x.v.nil? == false and y.v.nil? + return 1 + elsif x.v.nil? and y.v.nil? == false + return -1 + elsif x.v.nil? == false and y.v.nil? == false + cmp = RPMUtils.rpmvercmp(x.v, y.v) + return cmp if cmp != 0 + end - # run once mode - if interval == 0 - return false - elsif (Time.now - @updated_at) > interval - return true + # compare release + if x.r.nil? == false and y.r.nil? + return 1 + elsif x.r.nil? and y.r.nil? == false + return -1 + elsif x.r.nil? == false and y.r.nil? == false + cmp = RPMUtils.rpmvercmp(x.r, y.r) + return cmp end - false + return 0 end - - def refresh - if @data.empty? - reload - elsif stale? - reload + + # RPM::Version rpm_version_to_s equivalent + def to_s + if @r.nil? + @v + else + "#{@v}-#{@r}" end end - def load_data - @data = Hash.new - error = String.new + def evr + "#{@e}:#{@v}-#{@r}" + end + end - helper = ::File.join(::File.dirname(__FILE__), 'yum-dump.py') - status = popen4("python #{helper}", :waitlast => true) do |pid, stdin, stdout, stderr| - stdout.each do |line| - line.chomp! - name, type, epoch, version, release, arch = line.split(',') - type_sym = type.to_sym - if !@data.has_key?(name) - @data[name] = Hash.new - end - if !@data[name].has_key?(type_sym) - @data[name][type_sym] = Hash.new - end - @data[name][type_sym][arch] = { :epoch => epoch, :version => version, - :release => release } + class RPMPackage + include Comparable + + def initialize(*args) + if args.size == 3 + @n = args[0] + @version = RPMVersion.new(args[1]) + @a = args[2] + elsif args.size == 5 + @n = args[0] + e = args[1].to_i + v = args[2] + r = args[3] + @version = RPMVersion.new(e,v,r) + @a = args[4] + else + raise ArgumentError, "Expecting either 'name, epoch-version-release, arch' or " + + "'name, epoch, version, release, arch'" + end + end + attr_reader :n, :a, :version, :provides + alias :name :n + alias :arch :a + + # rough RPM::Version rpm_version_cmp equivalent - except much slower :) + def <=>(y) + x = self + + # compare name + if x.n.nil? == false and y.n.nil? + return 1 + elsif x.n.nil? and y.n.nil? == false + return -1 + elsif x.n.nil? == false and y.n.nil? == false + if x.n < y.n + return -1 + elsif x.n > y.n + return 1 end - - error = stderr.readlines end - unless status.exitstatus == 0 - raise Chef::Exceptions::Package, "yum failed - #{status.inspect} - returns: #{error}" + # compare version + if x.version > y.version + return 1 + elsif x.version < y.version + return -1 end - @updated_at = Time.now + # compare arch + if x.a.nil? == false and y.a.nil? + return 1 + elsif x.a.nil? and y.a.nil? == false + return -1 + elsif x.a.nil? == false and y.a.nil? == false + if x.a < y.a + return -1 + elsif x.a > y.a + return 1 + end + end + + return 0 end - alias :reload :load_data - def version(package_name, type, arch) - if (x = @data[package_name]) - if (y = x[type]) - if arch - if (z = y[arch]) - return "#{z[:version]}-#{z[:release]}" - end - else - # no arch specified - take the first match - z = y.to_a[0][1] - return "#{z[:version]}-#{z[:release]}" - end + def to_s + nevra + end + + def nevra + "#{@n}-#{@version.evr}.#{@a}" + end + + end + + class RPMDbPackage < RPMPackage + # <rpm parts>, installed, available + def initialize(*args) + # state + @available = args.pop + @installed = args.pop + super(*args) + end + attr_reader :available, :installed + end + + # Simple storage for RPMPackage objects - keeps them unique and sorted + class RPMDb + def initialize + @rpms = Hash.new + @available = Set.new + @installed = Set.new + end + + def [](package_name) + self.lookup(package_name) + end + + def lookup(package_name) + @rpms[package_name] + end + + # Using the package name as a key keep a unique, descending list of packages. + # The available/installed state can be overwritten for existing packages. + def push(*args) + args.flatten.each do |new_rpm| + unless new_rpm.kind_of?(RPMDbPackage) + raise ArgumentError, "Expecting an RPMDbPackage object" + end + + @rpms[new_rpm.n] ||= Array.new + + # new_rpm may be a different object but it will be compared using RPMPackages <=> + idx = @rpms[new_rpm.n].index(new_rpm) + if idx + # grab the existing package if it's not + curr_rpm = @rpms[new_rpm.n][idx] + else + @rpms[new_rpm.n] << new_rpm + @rpms[new_rpm.n].sort! + @rpms[new_rpm.n].reverse! + + curr_rpm = new_rpm + end + + # these are overwritten for existing packages + if new_rpm.available + @available << curr_rpm + end + if new_rpm.installed + @installed << curr_rpm end end + end - nil + def <<(*args) + self.push(args) end - def version_available?(package_name, desired_version, arch) - if (package_data = @data[package_name]) - if (available_versions = package_data[:available]) - if arch - # arch gets passed like ".x86_64" - matching_versions = [ available_versions[arch.sub(/^./, '')]] - else - matching_versions = available_versions.values - end + def clear + @rpms.clear + clear_available + clear_installed + end + + def clear_available + @available.clear + end + + def clear_installed + @installed.clear + end + + def size + @rpms.size + end + alias :length :size + + def available_size + @available.size + end + + def installed_size + @installed.size + end + + def available?(package) + @available.include?(package) + end + + def installed?(package) + @installed.include?(package) + end + end + + # Cache for our installed and available packages, pulled in from yum-dump.py + class YumCache + include Chef::Mixin::Command + include Singleton + + def initialize + @rpmdb = RPMDb.new + + # Next time installed/available is accessed: + # :all - Trigger a run of "yum-dump.py --options", updates yum's cache and + # parses options from /etc/yum.conf + # :installed - Trigger a run of "yum-dump.py --installed", only reads the local rpm db + # :none - Do nothing, a call to reload or reload_installed is required + @next_refresh = :all + + @allow_multi_install = [] + + # these are for subsequent runs if we are on an interval + Chef::Client.when_run_starts do + YumCache.instance.reload + end + end + + # Cache management + # + + def refresh + return if @next_refresh == :none + + if @next_refresh == :installed + reset_installed + opts=" --installed" + elsif @next_refresh == :all + reset + opts=" --options" + end - if matching_versions.nil? - if arch.empty? - arch_msg = "" + one_line = false + error = nil + + helper = ::File.join(::File.dirname(__FILE__), 'yum-dump.py') + + status = popen4("/usr/bin/python #{helper}#{opts}", :waitlast => true) do |pid, stdin, stdout, stderr| + stdout.each do |line| + one_line = true + + line.chomp! + + if line =~ %r{\[option (.*)\] (.*)} + if $1 == "installonlypkgs" + @allow_multi_install = $2.split else - arch_msg = "with arch #{arch.sub(/^./, '')} " + raise Chef::Exceptions::Package, "Strange, unknown option line '#{line}' from yum-dump.py" end + next + end - raise ArgumentError, "#{package_name}: Found no available versions #{arch_msg}to match" + parts = line.split + unless parts.size == 6 + Chef::Log.warn("Problem parsing line '#{line}' from yum-dump.py! " + + "Please check your yum configuration.") + next end - # Expect [ { :version => "ver", :release => "rel" }, { :version => "ver", :release => "rel" }, { :version => "ver", :release => "rel" } ] ??? - matching_versions.each do |ver| - Chef::Log.debug("#{@new_resource} trying to match #{desired_version} to version #{ver[:version]} and release #{ver[:release]}") - if (desired_version == "#{ver[:version]}-#{ver[:release]}") - return true - end + type = parts.pop + if type == "i" + # if yum-dump was called with --installed this may not be true, but it's okay + # since we don't touch the @available Set in reload_installed + available = false + installed = true + elsif type == "a" + available = true + installed = false + elsif type == "r" + available = true + installed = true + else + Chef::Log.warn("Can't parse type from output of yum-dump.py! Skipping line") end + + pkg = RPMDbPackage.new(*(parts + [installed, available])) + @rpmdb << pkg end + + error = stderr.readlines end - nil + if status.exitstatus != 0 + raise Chef::Exceptions::Package, "Yum failed - #{status.inspect} - returns: #{error}" + else + unless one_line + Chef::Log.warn("Odd, no output from yum-dump.py. Please check " + + "your yum configuration.") + end + end + + # A reload method must be called before the cache is altered + @next_refresh = :none end - def installed_version(package_name, arch) - version(package_name, :installed, arch) + def reload + @next_refresh = :all end - def candidate_version(package_name, arch) - version(package_name, :available, arch) + def reload_installed + @next_refresh = :installed end - def flush - @data.clear + def reset + @rpmdb.clear + end + + def reset_installed + @rpmdb.clear_installed end - end + + # Querying the cache + # + + def version_available?(package_name, desired_version, arch=nil) + version(package_name, arch, true, false) do |v| + return true if desired_version == v + end + + return false + end + + def available_version(package_name, arch=nil) + version(package_name, arch, true, false) + end + alias :candidate_version :available_version + + def installed_version(package_name, arch=nil) + version(package_name, arch, false, true) + end + + def allow_multi_install + refresh + @allow_multi_install + end + + private + + def version(package_name, arch=nil, is_available=false, is_installed=false) + refresh + packages = @rpmdb[package_name] + if packages + packages.each do |pkg| + if is_available + next unless @rpmdb.available?(pkg) + end + if is_installed + next unless @rpmdb.installed?(pkg) + end + if arch + next unless pkg.arch == arch + end + + if block_given? + yield pkg.version.to_s + else + # first match is latest version + return pkg.version.to_s + end + end + end + + if block_given? + return self + else + return nil + end + end + end # YumCache def initialize(new_resource, run_context) super + @yum = YumCache.instance end + # Extra attributes + # + def arch if @new_resource.respond_to?("arch") @new_resource.arch @@ -163,11 +664,56 @@ class Chef end end + def flush_cache + if @new_resource.respond_to?("flush_cache") + @new_resource.flush_cache + else + { :before => false, :after => false } + end + end + + def allow_downgrade + if @new_resource.respond_to?("allow_downgrade") + @new_resource.allow_downgrade + else + false + end + end + + # Helpers + # + def yum_arch arch ? ".#{arch}" : nil end + # Standard Provider methods for Parent + # + def load_current_resource + if flush_cache[:before] + @yum.reload + end + + # Allow for foo.x86_64 style package_name like yum uses in it's output + # + # Don't overwrite an existing arch + unless arch + if @new_resource.package_name =~ %r{^(.*)\.(.*)$} + new_package_name = $1 + new_arch = $2 + # foo.i386 and foo.beta1 are both valid package names or expressions of an arch. + # Ensure we don't have an existing package matching package_name, then ensure we at + # least have a match for the new_package+new_arch before we overwrite. If neither + # then fall through to standard package handling. + if (@yum.installed_version(@new_resource.package_name).nil? and @yum.candidate_version(@new_resource.package_name).nil?) and + (@yum.installed_version(new_package_name, new_arch) or @yum.candidate_version(new_package_name, new_arch)) + @new_resource.package_name(new_package_name) + @new_resource.arch(new_arch) + end + end + end + @current_resource = Chef::Resource::Package.new(@new_resource.name) @current_resource.package_name(@new_resource.package_name) @@ -188,20 +734,24 @@ class Chef end end - Chef::Log.debug("#{@new_resource} checking yum info for #{@new_resource.package_name}#{yum_arch}") + if @new_resource.version + new_resource = "#{@new_resource.package_name}-#{@new_resource.version}#{yum_arch}" + else + new_resource = "#{@new_resource.package_name}#{yum_arch}" + end - @yum.refresh + 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) - @current_resource.version(installed_version) - if candidate_version - @candidate_version = candidate_version - else - @candidate_version = installed_version + if @candidate_version.nil? + raise Chef::Exceptions::Package, "Yum installed and available lists don't have a version of package #{@new_resource.package_name}" end - Chef::Log.debug("#{@new_resource} installed version: #{installed_version} candidate version: #{candidate_version}") + + Chef::Log.debug("#{@new_resource} installed version: #{installed_version || "(none)"} candidate version: #{@candidate_version}") @current_resource end @@ -209,46 +759,81 @@ class Chef def install_package(name, version) if @new_resource.source run_command_with_systems_locale( - :command => "yum -d0 -e0 -y #{@new_resource.options} localinstall #{@new_resource.source}" + :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, yum_arch) + if @yum.version_available?(name, version, arch) + method = "install" + + # 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... + if allow_downgrade + method = "downgrade" + 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}" + end + end + end + run_command_with_systems_locale( - :command => "yum -d0 -e0 -y #{@new_resource.options} install #{name}-#{version}#{yum_arch}" + :command => "yum -d0 -e0 -y#{expand_options(@new_resource.options)} #{method} #{name}-#{version}#{yum_arch}" ) else - raise ArgumentError, "#{@new_resource.name}: Version #{version} of #{name} not found. Did you specify both version and release? (version-release, e.g. 1.84-10.fc6)" + 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 - @yum.flush + if flush_cache[:after] + @yum.reload + else + @yum.reload_installed + end end - def upgrade_package(name, version) - # If we're not given a version, running update is the correct - # option. If we are, then running install_package is right. - unless version - run_command_with_systems_locale( - :command => "yum -d0 -e0 -y #{@new_resource.options} update #{name}#{yum_arch}" - ) - @yum.flush + # Keep upgrades from trying to install an older candidate version. Can happen when a new + # version is installed then removed from a repository, now the older available version + # shows up as a viable install candidate. + # + # Can be done in upgrade_package but an upgraded from->to log message slips out + # + # Hacky - better overall solution? Custom compare in Package provider? + def action_upgrade + # Ensure the candidate is newer + if RPMVersion.parse(candidate_version) > RPMVersion.parse(@current_resource.version) + super + # Candidate is older else - install_package(name, version) + Chef::Log.debug("#{@new_resource} is at the latest version - nothing to do") end end + def upgrade_package(name, version) + install_package(name, version) + end + def remove_package(name, version) if version run_command_with_systems_locale( - :command => "yum -d0 -e0 -y #{@new_resource.options} remove #{name}-#{version}#{yum_arch}" + :command => "yum -d0 -e0 -y#{expand_options(@new_resource.options)} remove #{name}-#{version}#{yum_arch}" ) else run_command_with_systems_locale( - :command => "yum -d0 -e0 -y #{@new_resource.options} remove #{name}#{yum_arch}" + :command => "yum -d0 -e0 -y#{expand_options(@new_resource.options)} remove #{name}#{yum_arch}" ) end - - @yum.flush + if flush_cache[:after] + @yum.reload + else + @yum.reload_installed + end end def purge_package(name, version) diff --git a/chef/lib/chef/resource/yum_package.rb b/chef/lib/chef/resource/yum_package.rb index 4a415fb48f..bcb1f65667 100644 --- a/chef/lib/chef/resource/yum_package.rb +++ b/chef/lib/chef/resource/yum_package.rb @@ -27,6 +27,8 @@ class Chef super @resource_name = :yum_package @provider = Chef::Provider::Package::Yum + @flush_cache = { :before => false, :after => false } + @allow_downgrade = false end # Install a specific arch @@ -38,6 +40,24 @@ class Chef ) end + def flush_cache(args={}) + if args.is_a? Array + args.each { |arg| @flush_cache[arg] = true } + elsif args.any? + @flush_cache = args + else + @flush_cache + end + end + + def allow_downgrade(arg=nil) + set_or_return( + :allow_downgrade, + arg, + :kind_of => [ TrueClass, FalseClass ] + ) + end + end end end diff --git a/chef/spec/unit/provider/package/yum_spec.rb b/chef/spec/unit/provider/package/yum_spec.rb index e2810523d5..3e7c048980 100644 --- a/chef/spec/unit/provider/package/yum_spec.rb +++ b/chef/spec/unit/provider/package/yum_spec.rb @@ -26,11 +26,12 @@ describe Chef::Provider::Package::Yum do @status = mock("Status", :exitstatus => 0) @yum_cache = mock( 'Chef::Provider::Yum::YumCache', - :refresh => true, - :flush => true, + :reload_installed => true, + :reset => true, :installed_version => "1.2.4-11.18.el5", :candidate_version => "1.2.4-11.18.el5_2.3", - :version_available? => true + :version_available? => true, + :allow_multi_install => [ "kernel" ] ) Chef::Provider::Package::Yum::YumCache.stub!(:instance).and_return(@yum_cache) @provider = Chef::Provider::Package::Yum.new(@new_resource, @run_context) @@ -39,7 +40,6 @@ describe Chef::Provider::Package::Yum do 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 @provider.current_resource.name.should == "cups" @@ -69,12 +69,160 @@ describe Chef::Provider::Package::Yum do it "should return the current resouce" do @provider.load_current_resource.should eql(@provider.current_resource) end + + it "should raise an error if a candidate version can't be found" do + @yum_cache = mock( + 'Chef::Provider::Yum::YumCache', + :reload_installed => true, + :reset => true, + :installed_version => "1.2.4-11.18.el5", + :candidate_version => nil, + :version_available? => true + ) + Chef::Provider::Package::Yum::YumCache.stub!(:instance).and_return(@yum_cache) + @provider = Chef::Provider::Package::Yum.new(@new_resource, @run_context) + lambda { @provider.load_current_resource }.should raise_error(Chef::Exceptions::Package, %r{don't have a version of package}) + end + + describe "when arch in package_name" do + it "should set the arch if no existing package_name is found and new_package_name+new_arch is available" do + @new_resource = Chef::Resource::YumPackage.new('testing.noarch') + @yum_cache = mock( + 'Chef::Provider::Yum::YumCache' + ) + @yum_cache.stub!(:installed_version) do |package_name, arch| + # nothing installed for package_name/new_package_name + nil + end + @yum_cache.stub!(:candidate_version) do |package_name, arch| + if package_name == "testing.noarch" || package_name == "testing.more.noarch" + nil + # candidate for new_package_name + elsif package_name == "testing" || package_name == "testing.more" + "1.1" + end + end + Chef::Provider::Package::Yum::YumCache.stub!(:instance).and_return(@yum_cache) + @provider = Chef::Provider::Package::Yum.new(@new_resource, @run_context) + @provider.load_current_resource + @provider.new_resource.package_name.should == "testing" + @provider.new_resource.arch.should == "noarch" + @provider.arch.should == "noarch" + + @new_resource = Chef::Resource::YumPackage.new('testing.more.noarch') + @provider = Chef::Provider::Package::Yum.new(@new_resource, @run_context) + @provider.load_current_resource + @provider.new_resource.package_name.should == "testing.more" + @provider.new_resource.arch.should == "noarch" + @provider.arch.should == "noarch" + end + + it "should not set the arch when an existing package_name is found" do + @new_resource = Chef::Resource::YumPackage.new('testing.beta3') + @yum_cache = mock( + 'Chef::Provider::Yum::YumCache' + ) + @yum_cache.stub!(:installed_version) do |package_name, arch| + # installed for package_name + if package_name == "testing.beta3" || package_name == "testing.beta3.more" + "1.1" + elsif package_name == "testing" || package_name = "testing.beta3" + nil + end + end + @yum_cache.stub!(:candidate_version) do |package_name, arch| + # no candidate for package_name/new_package_name + nil + end + Chef::Provider::Package::Yum::YumCache.stub!(:instance).and_return(@yum_cache) + @provider = Chef::Provider::Package::Yum.new(@new_resource, @run_context) + # annoying side effect of the fun stub'ing above + lambda { @provider.load_current_resource }.should raise_error(Chef::Exceptions::Package, %r{don't have a version of package}) + @provider.new_resource.package_name.should == "testing.beta3" + @provider.new_resource.arch.should == nil + @provider.arch.should == nil + + @new_resource = Chef::Resource::YumPackage.new('testing.beta3.more') + @provider = Chef::Provider::Package::Yum.new(@new_resource, @run_context) + lambda { @provider.load_current_resource }.should raise_error(Chef::Exceptions::Package, %r{don't have a version of package}) + @provider.new_resource.package_name.should == "testing.beta3.more" + @provider.new_resource.arch.should == nil + @provider.arch.should == nil + end + + it "should not set the arch when no existing package_name or new_package_name+new_arch is found" do + @new_resource = Chef::Resource::YumPackage.new('testing.beta3') + @yum_cache = mock( + 'Chef::Provider::Yum::YumCache' + ) + @yum_cache.stub!(:installed_version) do |package_name, arch| + # nothing installed for package_name/new_package_name + nil + end + @yum_cache.stub!(:candidate_version) do |package_name, arch| + # no candidate for package_name/new_package_name + nil + end + Chef::Provider::Package::Yum::YumCache.stub!(:instance).and_return(@yum_cache) + @provider = Chef::Provider::Package::Yum.new(@new_resource, @run_context) + lambda { @provider.load_current_resource }.should raise_error(Chef::Exceptions::Package, %r{don't have a version of package}) + @provider.new_resource.package_name.should == "testing.beta3" + @provider.new_resource.arch.should == nil + @provider.arch.should == nil + + @new_resource = Chef::Resource::YumPackage.new('testing.beta3.more') + @provider = Chef::Provider::Package::Yum.new(@new_resource, @run_context) + lambda { @provider.load_current_resource }.should raise_error(Chef::Exceptions::Package, %r{don't have a version of package}) + @provider.new_resource.package_name.should == "testing.beta3.more" + @provider.new_resource.arch.should == nil + @provider.arch.should == nil + end + + it "should ensure it doesn't clobber an existing arch if passed" do + @new_resource = Chef::Resource::YumPackage.new('testing.i386') + @new_resource.arch("x86_64") + @yum_cache = mock( + 'Chef::Provider::Yum::YumCache' + ) + @yum_cache.stub!(:installed_version) do |package_name, arch| + # nothing installed for package_name/new_package_name + nil + end + @yum_cache.stub!(:candidate_version) do |package_name, arch| + if package_name == "testing.noarch" + nil + # candidate for new_package_name + elsif package_name == "testing" + "1.1" + end + end.and_return("something") + Chef::Provider::Package::Yum::YumCache.stub!(:instance).and_return(@yum_cache) + @provider = Chef::Provider::Package::Yum.new(@new_resource, @run_context) + @provider.load_current_resource + @provider.new_resource.package_name.should == "testing.i386" + @provider.new_resource.arch.should == "x86_64" + end + end + + it "should flush the cache if :before is true" do + @new_resource.stub!(:flush_cache).and_return({:after => false, :before => true}) + @yum_cache.should_receive(:reload).once + @provider.load_current_resource + end + + it "should flush the cache if :before is false" do + @new_resource.stub!(:flush_cache).and_return({:after => false, :before => false}) + @yum_cache.should_not_receive(:reload) + @provider.load_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 + Chef::Provider::Package::Yum::RPMUtils.stub!(:rpmvercmp).and_return(-1) @provider.should_receive(:run_command_with_systems_locale).with({ - :command => "yum -d0 -e0 -y install emacs-1.0" + :command => "yum -d0 -e0 -y install emacs-1.0" }) @provider.install_package("emacs", "1.0") end @@ -82,81 +230,168 @@ describe Chef::Provider::Package::Yum do it "should run yum localinstall if given a path to an rpm" do @new_resource.stub!(:source).and_return("/tmp/emacs-21.4-20.el5.i386.rpm") @provider.should_receive(:run_command_with_systems_locale).with({ - :command => "yum -d0 -e0 -y localinstall /tmp/emacs-21.4-20.el5.i386.rpm" + :command => "yum -d0 -e0 -y localinstall /tmp/emacs-21.4-20.el5.i386.rpm" }) @provider.install_package("emacs", "21.4-20.el5") end it "should run yum install with the package name, version and arch" do + @provider.load_current_resource @new_resource.stub!(:arch).and_return("i386") + Chef::Provider::Package::Yum::RPMUtils.stub!(:rpmvercmp).and_return(-1) @provider.should_receive(:run_command_with_systems_locale).with({ - :command => "yum -d0 -e0 -y install emacs-21.4-20.el5.i386" + :command => "yum -d0 -e0 -y install emacs-21.4-20.el5.i386" }) @provider.install_package("emacs", "21.4-20.el5") end it "installs the package with the options given in the resource" do + @provider.load_current_resource @provider.candidate_version = '11' @new_resource.stub!(:options).and_return("--disablerepo epmd") + Chef::Provider::Package::Yum::RPMUtils.stub!(:rpmvercmp).and_return(-1) @provider.should_receive(:run_command_with_systems_locale).with({ :command => "yum -d0 -e0 -y --disablerepo epmd install cups-11" }) @provider.install_package(@new_resource.name, @provider.candidate_version) end - it "should fail if the package is not available" do + it "should raise an exception if the package is not available" do @yum_cache = mock( 'Chef::Provider::Yum::YumCache', - :refresh => true, - :flush => true, + :reload_from_cache => true, + :reset => true, :installed_version => "1.2.4-11.18.el5", :candidate_version => "1.2.4-11.18.el5_2.3", :version_available? => nil ) Chef::Provider::Package::Yum::YumCache.stub!(:instance).and_return(@yum_cache) @provider = Chef::Provider::Package::Yum.new(@new_resource, @run_context) - lambda { @provider.install_package("lolcats", "0.99") }.should raise_error(ArgumentError) + lambda { @provider.install_package("lolcats", "0.99") }.should raise_error(Chef::Exceptions::Package, %r{Version .* not found}) end - end - describe "when upgrading a package" do - it "should run yum update if the package is installed and no version is given" do + it "should raise an exception if candidate version is older than the installed version and allow_downgrade is false" do + @new_resource.stub!(:allow_downgrade).and_return(false) + @yum_cache = mock( + 'Chef::Provider::Yum::YumCache', + :reload_installed => true, + :reset => true, + :installed_version => "1.2.4-11.18.el5", + :candidate_version => "1.2.4-11.15.el5", + :version_available? => true, + :allow_multi_install => [ "kernel" ] + ) + Chef::Provider::Package::Yum::YumCache.stub!(:instance).and_return(@yum_cache) + @provider = Chef::Provider::Package::Yum.new(@new_resource, @run_context) + @provider.load_current_resource + lambda { @provider.install_package("cups", "1.2.4-11.15.el5") }.should raise_error(Chef::Exceptions::Package, %r{is newer than candidate package}) + end + + it "should not raise an exception if candidate version is older than the installed version and the package is list in yum's installonlypkg option" do + @yum_cache = mock( + 'Chef::Provider::Yum::YumCache', + :reload_installed => true, + :reset => true, + :installed_version => "1.2.4-11.18.el5", + :candidate_version => "1.2.4-11.15.el5", + :version_available? => true, + :allow_multi_install => [ "cups" ] + ) + Chef::Provider::Package::Yum::YumCache.stub!(:instance).and_return(@yum_cache) + @provider = Chef::Provider::Package::Yum.new(@new_resource, @run_context) + @provider.load_current_resource @provider.should_receive(:run_command_with_systems_locale).with({ - :command => "yum -d0 -e0 -y update cups" + :command => "yum -d0 -e0 -y install cups-1.2.4-11.15.el5" }) - @provider.upgrade_package(@new_resource.name, nil) + @provider.install_package("cups", "1.2.4-11.15.el5") end - it "should run yum update with arch if the package is installed and no version is given" do - @new_resource.stub!(:arch).and_return("i386") + it "should run yum downgrade if candidate version is older than the installed version and allow_downgrade is true" do + @new_resource.stub!(:allow_downgrade).and_return(true) + @yum_cache = mock( + 'Chef::Provider::Yum::YumCache', + :reload_installed => true, + :reset => true, + :installed_version => "1.2.4-11.18.el5", + :candidate_version => "1.2.4-11.15.el5", + :version_available? => true, + :allow_multi_install => [] + ) + Chef::Provider::Package::Yum::YumCache.stub!(:instance).and_return(@yum_cache) + @provider = Chef::Provider::Package::Yum.new(@new_resource, @run_context) + @provider.load_current_resource + @provider.should_receive(:run_command_with_systems_locale).with({ + :command => "yum -d0 -e0 -y downgrade cups-1.2.4-11.15.el5" + }) + @provider.install_package("cups", "1.2.4-11.15.el5") + end + + it "should run yum install then flush the cache if :after is true" do + @new_resource.stub!(:flush_cache).and_return({:after => true, :before => false}) + @provider.load_current_resource + Chef::Provider::Package::Yum::RPMUtils.stub!(:rpmvercmp).and_return(-1) @provider.should_receive(:run_command_with_systems_locale).with({ - :command => "yum -d0 -e0 -y update cups.i386" + :command => "yum -d0 -e0 -y install emacs-1.0" }) - @provider.upgrade_package(@new_resource.name, nil) + @yum_cache.should_receive(:reload).once + @provider.install_package("emacs", "1.0") + end + + it "should run yum install then not flush the cache if :after is false" do + @new_resource.stub!(:flush_cache).and_return({:after => false, :before => false}) + @provider.load_current_resource + Chef::Provider::Package::Yum::RPMUtils.stub!(:rpmvercmp).and_return(-1) + @provider.should_receive(:run_command_with_systems_locale).with({ + :command => "yum -d0 -e0 -y install emacs-1.0" + }) + @yum_cache.should_not_receive(:reload) + @provider.install_package("emacs", "1.0") 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' + Chef::Provider::Package::Yum::RPMUtils.stub!(:rpmvercmp).and_return(-1) @provider.should_receive(:run_command_with_systems_locale).with({ - :command => "yum -d0 -e0 -y install cups-11" + :command => "yum -d0 -e0 -y install cups-11" }) @provider.upgrade_package(@new_resource.name, @provider.candidate_version) end 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' + Chef::Provider::Package::Yum::RPMUtils.stub!(:rpmvercmp).and_return(-1) @provider.should_receive(:run_command_with_systems_locale).with({ - :command => "yum -d0 -e0 -y install cups-11" + :command => "yum -d0 -e0 -y install cups-11" }) @provider.upgrade_package(@new_resource.name, @provider.candidate_version) end + + it "should raise an exception if candidate version is older than the installed version" do + @yum_cache = mock( + 'Chef::Provider::Yum::YumCache', + :reload_installed => true, + :reset => true, + :installed_version => "1.2.4-11.18.el5", + :candidate_version => "1.2.4-11.15.el5", + :version_available? => true, + :allow_multi_install => [ "kernel" ] + ) + Chef::Provider::Package::Yum::YumCache.stub!(:instance).and_return(@yum_cache) + @provider = Chef::Provider::Package::Yum.new(@new_resource, @run_context) + @provider.load_current_resource + lambda { @provider.upgrade_package("cups", "1.2.4-11.15.el5") }.should raise_error(Chef::Exceptions::Package, %r{is newer than candidate package}) + end end describe "when removing a package" do it "should run yum remove with the package name" do @provider.should_receive(:run_command_with_systems_locale).with({ - :command => "yum -d0 -e0 -y remove emacs-1.0" + :command => "yum -d0 -e0 -y remove emacs-1.0" }) @provider.remove_package("emacs", "1.0") end @@ -164,7 +399,7 @@ describe Chef::Provider::Package::Yum do it "should run yum remove with the package name and arch" do @new_resource.stub!(:arch).and_return("x86_64") @provider.should_receive(:run_command_with_systems_locale).with({ - :command => "yum -d0 -e0 -y remove emacs-1.0.x86_64" + :command => "yum -d0 -e0 -y remove emacs-1.0.x86_64" }) @provider.remove_package("emacs", "1.0") end @@ -173,10 +408,843 @@ describe Chef::Provider::Package::Yum do describe "when purging a package" do it "should run yum remove with the package name" do @provider.should_receive(:run_command_with_systems_locale).with({ - :command => "yum -d0 -e0 -y remove emacs-1.0" + :command => "yum -d0 -e0 -y remove emacs-1.0" }) @provider.purge_package("emacs", "1.0") end end end + +describe Chef::Provider::Package::Yum::RPMUtils do + describe "version_parse" do + before do + @rpmutils = Chef::Provider::Package::Yum::RPMUtils + end + + it "parses known good epoch strings" do + [ + [ "0:3.3", [ 0, "3.3", nil ] ], + [ "9:1.7.3", [ 9, "1.7.3", nil ] ], + [ "15:20020927", [ 15, "20020927", nil ] ] + ].each do |x, y| + @rpmutils.version_parse(x).should == y + end + end + + it "parses strange epoch strings" do + [ + [ ":3.3", [ 0, "3.3", nil ] ], + [ "-1:1.7.3", [ nil, "", "1:1.7.3" ] ], + [ "-:20020927", [ nil, "", ":20020927" ] ] + ].each do |x, y| + @rpmutils.version_parse(x).should == y + end + end + + it "parses known good version strings" do + [ + [ "3.3", [ nil, "3.3", nil ] ], + [ "1.7.3", [ nil, "1.7.3", nil ] ], + [ "20020927", [ nil, "20020927", nil ] ] + ].each do |x, y| + @rpmutils.version_parse(x).should == y + end + end + + it "parses strange version strings" do + [ + [ "3..3", [ nil, "3..3", nil ] ], + [ "0001.7.3", [ nil, "0001.7.3", nil ] ], + [ "20020927,3", [ nil, "20020927,3", nil ] ] + ].each do |x, y| + @rpmutils.version_parse(x).should == y + end + end + + it "parses known good version release strings" do + [ + [ "3.3-0.pre3.1.60.el5_5.1", [ nil, "3.3", "0.pre3.1.60.el5_5.1" ] ], + [ "1.7.3-1jpp.2.el5", [ nil, "1.7.3", "1jpp.2.el5" ] ], + [ "20020927-46.el5", [ nil, "20020927", "46.el5" ] ] + ].each do |x, y| + @rpmutils.version_parse(x).should == y + end + end + + it "parses strange version release strings" do + [ + [ "3.3-", [ nil, "3.3", nil ] ], + [ "-1jpp.2.el5", [ nil, "", "1jpp.2.el5" ] ], + [ "-0020020927-46.el5", [ nil, "-0020020927", "46.el5" ] ] + ].each do |x, y| + @rpmutils.version_parse(x).should == y + end + end + end + + describe "rpmvercmp" do + before do + @rpmutils = Chef::Provider::Package::Yum::RPMUtils + end + + it "standard comparison examples" do + [ + # numeric + [ "0.0.2", "0.0.1", 1 ], + [ "0.2.0", "0.1.0", 1 ], + [ "2.0.0", "1.0.0", 1 ], + [ "0.0.1", "0.0.1", 0 ], + [ "0.0.1", "0.0.2", -1 ], + [ "0.1.0", "0.2.0", -1 ], + [ "1.0.0", "2.0.0", -1 ], + # alpha + [ "bb", "aa", 1 ], + [ "ab", "aa", 1 ], + [ "aa", "aa", 0 ], + [ "aa", "bb", -1 ], + [ "aa", "ab", -1 ], + [ "BB", "AA", 1 ], + [ "AA", "AA", 0 ], + [ "AA", "BB", -1 ], + [ "aa", "AA", 1 ], + [ "AA", "aa", -1 ], + # alphanumeric + [ "0.0.1b", "0.0.1a", 1 ], + [ "0.1b.0", "0.1a.0", 1 ], + [ "1b.0.0", "1a.0.0", 1 ], + [ "0.0.1a", "0.0.1a", 0 ], + [ "0.0.1a", "0.0.1b", -1 ], + [ "0.1a.0", "0.1b.0", -1 ], + [ "1a.0.0", "1b.0.0", -1 ], + # alphanumeric against alphanumeric + [ "0.0.1", "0.0.a", 1 ], + [ "0.1.0", "0.a.0", 1 ], + [ "1.0.0", "a.0.0", 1 ], + [ "0.0.a", "0.0.a", 0 ], + [ "0.0.a", "0.0.1", -1 ], + [ "0.a.0", "0.1.0", -1 ], + [ "a.0.0", "1.0.0", -1 ], + # alphanumeric against numeric + [ "0.0.2", "0.0.1a", 1 ], + [ "0.0.2a", "0.0.1", 1 ], + [ "0.0.1", "0.0.2a", -1 ], + [ "0.0.1a", "0.0.2", -1 ], + # length + [ "0.0.1aa", "0.0.1a", 1 ], + [ "0.0.1aa", "0.0.1aa", 0 ], + [ "0.0.1a", "0.0.1aa", -1 ], + ].each do |x, y, result| + @rpmutils.rpmvercmp(x,y).should == result + end + end + + it "strange comparison examples" do + [ + [ "2,0,0", "1.0.0", 1 ], + [ "0.0.1", "0,0.1", 0 ], + [ "1.0.0", "2,0,0", -1 ], + [ "002.0.0", "001.0.0", 1 ], + [ "001..0.1", "001..0.0", 1 ], + [ "-001..1", "-001..0", 1 ], + [ "1.0.1", nil, 1 ], + [ nil, nil, 0 ], + [ nil, "1.0.1", -1 ], + [ "1.0.1", "", 1 ], + [ "", "", 0 ], + [ "", "1.0.1", -1 ] + ].each do |x, y, result| + @rpmutils.rpmvercmp(x,y).should == result + end + end + + it "tests isalnum good input" do + [ 'a', 'z', 'A', 'Z', '0', '9' ].each do |t| + @rpmutils.isalnum(t).should == true + end + end + + it "tests isalnum bad input" do + [ '-', '.', '!', '^', ':', '_' ].each do |t| + @rpmutils.isalnum(t).should == false + end + end + + it "tests isalpha good input" do + [ 'a', 'z', 'A', 'Z', ].each do |t| + @rpmutils.isalpha(t).should == true + end + end + + it "tests isalpha bad input" do + [ '0', '9', '-', '.', '!', '^', ':', '_' ].each do |t| + @rpmutils.isalpha(t).should == false + end + end + + it "tests isdigit good input" do + [ '0', '9', ].each do |t| + @rpmutils.isdigit(t).should == true + end + end + + it "tests isdigit bad input" do + [ 'A', 'z', '-', '.', '!', '^', ':', '_' ].each do |t| + @rpmutils.isdigit(t).should == false + end + end + end + +end + +describe Chef::Provider::Package::Yum::RPMVersion do + describe "new - with parsing" do + before do + @rpmv = Chef::Provider::Package::Yum::RPMVersion.new("1:1.6.5-9.36.el5") + end + + it "should expose evr (name-version-release) available" do + @rpmv.e.should == 1 + @rpmv.v.should == "1.6.5" + @rpmv.r.should == "9.36.el5" + + @rpmv.evr.should == "1:1.6.5-9.36.el5" + end + + it "should output a version-release string" do + @rpmv.to_s.should == "1.6.5-9.36.el5" + end + end + + describe "new - no parsing" do + before do + @rpmv = Chef::Provider::Package::Yum::RPMVersion.new("1", "1.6.5", "9.36.el5") + end + + it "should expose evr (name-version-release) available" do + @rpmv.e.should == 1 + @rpmv.v.should == "1.6.5" + @rpmv.r.should == "9.36.el5" + + @rpmv.evr.should == "1:1.6.5-9.36.el5" + end + + it "should output a version-release string" do + @rpmv.to_s.should == "1.6.5-9.36.el5" + end + end + + it "should raise an error unless passed 1 or 3 args" do + lambda { + Chef::Provider::Package::Yum::RPMVersion.new() + }.should raise_error(ArgumentError) + lambda { + Chef::Provider::Package::Yum::RPMVersion.new("1:1.6.5-9.36.el5") + }.should_not raise_error + lambda { + Chef::Provider::Package::Yum::RPMVersion.new("1:1.6.5-9.36.el5", "extra") + }.should raise_error(ArgumentError) + lambda { + Chef::Provider::Package::Yum::RPMVersion.new("1", "1.6.5", "9.36.el5") + }.should_not raise_error + lambda { + Chef::Provider::Package::Yum::RPMVersion.new("1", "1.6.5", "9.36.el5", "extra") + }.should raise_error(ArgumentError) + end + + # thanks version_class_spec.rb! + describe "<=>" do + it "should sort based on complete epoch-version-release data" do + [ + # smaller, larger + [ "0:1.6.5-9.36.el5", + "1:1.6.5-9.36.el5" ], + [ "0:2.3-15.el5", + "0:3.3-15.el5" ], + [ "0:alpha9.8-27.2", + "0:beta9.8-27.2" ], + [ "0:0.09-14jpp.3", + "0:0.09-15jpp.3" ], + [ "0:0.9.0-0.6.20110211.el5", + "0:0.9.0-0.6.20120211.el5" ], + [ "0:1.9.1-4.el5", + "0:1.9.1-5.el5" ], + [ "0:1.4.10-7.20090624svn.el5", + "0:1.4.10-7.20090625svn.el5" ], + [ "0:2.3.4-2.el5", + "0:2.3.4-2.el6" ] + ].each do |smaller, larger| + sm = Chef::Provider::Package::Yum::RPMVersion.new(smaller) + lg = Chef::Provider::Package::Yum::RPMVersion.new(larger) + sm.should be < lg + lg.should be > sm + sm.should_not == lg + end + end + + it "should sort based on partial epoch-version-release data" do + [ + # smaller, larger + [ ":1.6.5-9.36.el5", + "1:1.6.5-9.36.el5" ], + [ "2.3-15.el5", + "3.3-15.el5" ], + [ "alpha9.8", + "beta9.8" ], + [ "14jpp", + "15jpp" ], + [ "0.9.0-0.6", + "0.9.0-0.7" ], + [ "0:1.9", + "3:1.9" ], + [ "2.3-2.el5", + "2.3-2.el6" ] + ].each do |smaller, larger| + sm = Chef::Provider::Package::Yum::RPMVersion.new(smaller) + lg = Chef::Provider::Package::Yum::RPMVersion.new(larger) + sm.should be < lg + lg.should be > sm + sm.should_not == lg + end + end + + it "should verify equality of complete epoch-version-release data" do + [ + [ "0:1.6.5-9.36.el5", + "0:1.6.5-9.36.el5" ], + [ "0:2.3-15.el5", + "0:2.3-15.el5" ], + [ "0:alpha9.8-27.2", + "0:alpha9.8-27.2" ] + ].each do |smaller, larger| + sm = Chef::Provider::Package::Yum::RPMVersion.new(smaller) + lg = Chef::Provider::Package::Yum::RPMVersion.new(larger) + sm.should be == lg + end + end + + it "should verify equality of partial epoch-version-release data" do + [ + [ ":1.6.5-9.36.el5", + "0:1.6.5-9.36.el5" ], + [ "2.3-15.el5", + "2.3-15.el5" ], + [ "alpha9.8-3", + "alpha9.8-3" ] + ].each do |smaller, larger| + sm = Chef::Provider::Package::Yum::RPMVersion.new(smaller) + lg = Chef::Provider::Package::Yum::RPMVersion.new(larger) + sm.should be == lg + end + end + end + +end + +describe Chef::Provider::Package::Yum::RPMPackage do + describe "new - with parsing" do + before do + @rpm = Chef::Provider::Package::Yum::RPMPackage.new("testing", "1:1.6.5-9.36.el5", "x86_64") + end + + it "should expose nevra (name-epoch-version-release-arch) available" do + @rpm.name.should == "testing" + @rpm.version.e.should == 1 + @rpm.version.v.should == "1.6.5" + @rpm.version.r.should == "9.36.el5" + @rpm.arch.should == "x86_64" + + @rpm.nevra.should == "testing-1:1.6.5-9.36.el5.x86_64" + @rpm.to_s.should == @rpm.nevra + end + end + + describe "new - no parsing" do + before do + @rpm = Chef::Provider::Package::Yum::RPMPackage.new("testing", "1", "1.6.5", "9.36.el5", "x86_64") + end + + it "should expose nevra (name-epoch-version-release-arch) available" do + @rpm.name.should == "testing" + @rpm.version.e.should == 1 + @rpm.version.v.should == "1.6.5" + @rpm.version.r.should == "9.36.el5" + @rpm.arch.should == "x86_64" + + @rpm.nevra.should == "testing-1:1.6.5-9.36.el5.x86_64" + @rpm.to_s.should == @rpm.nevra + end + end + + it "should raise an error unless passed 3 or 5 args" do + lambda { + Chef::Provider::Package::Yum::RPMPackage.new() + }.should raise_error(ArgumentError) + lambda { + Chef::Provider::Package::Yum::RPMPackage.new("testing") + }.should raise_error(ArgumentError) + lambda { + Chef::Provider::Package::Yum::RPMPackage.new("testing", "1:1.6.5-9.36.el5") + }.should raise_error(ArgumentError) + lambda { + Chef::Provider::Package::Yum::RPMPackage.new("testing", "1:1.6.5-9.36.el5", "x86_64") + }.should_not raise_error + lambda { + Chef::Provider::Package::Yum::RPMPackage.new("testing", "1:1.6.5-9.36.el5", "x86_64", true) + }.should raise_error(ArgumentError) + lambda { + Chef::Provider::Package::Yum::RPMPackage.new("testing", "1:1.6.5-9.36.el5", "x86_64", true, true) + }.should_not raise_error + lambda { + Chef::Provider::Package::Yum::RPMPackage.new("testing", "1:1.6.5-9.36.el5", "x86_64", true, true, "extra") + }.should raise_error(ArgumentError) + end + + describe "<=>" do + it "should sort alphabetically based on package name" do + [ + [ "a-test", + "b-test" ], + [ "B-test", + "a-test" ], + [ "A-test", + "B-test" ], + [ "Aa-test", + "aA-test" ], + [ "1test", + "2test" ], + ].each do |smaller, larger| + sm = Chef::Provider::Package::Yum::RPMPackage.new(smaller, "0:0.0.1-1", "x86_64") + lg = Chef::Provider::Package::Yum::RPMPackage.new(larger, "0:0.0.1-1", "x86_64") + sm.should be < lg + lg.should be > sm + sm.should_not == lg + end + end + + it "should sort alphabetically based on package arch" do + [ + [ "i386", + "x86_64" ], + [ "i386", + "noarch" ], + [ "noarch", + "x86_64" ], + ].each do |smaller, larger| + sm = Chef::Provider::Package::Yum::RPMPackage.new("test-package", "0:0.0.1-1", smaller) + lg = Chef::Provider::Package::Yum::RPMPackage.new("test-package", "0:0.0.1-1", larger) + sm.should be < lg + lg.should be > sm + sm.should_not == lg + end + end + end + +end + +describe Chef::Provider::Package::Yum::RPMDbPackage do + before(:each) do + # name, version, arch, installed, available + @rpm_x = Chef::Provider::Package::Yum::RPMDbPackage.new("test-package-b", "0:1.6.5-9.36.el5", "noarch", false, true) + @rpm_y = Chef::Provider::Package::Yum::RPMDbPackage.new("test-package-b", "0:1.6.5-9.36.el5", "noarch", true, true) + @rpm_z = Chef::Provider::Package::Yum::RPMDbPackage.new("test-package-b", "0:1.6.5-9.36.el5", "noarch", true, false) + end + + describe "initialize" do + it "should return a Chef::Provider::Package::Yum::RPMDbPackage object" do + @rpm_x.should be_kind_of(Chef::Provider::Package::Yum::RPMDbPackage) + end + end + + describe "available" do + it "should return true" do + @rpm_x.available.should be == true + @rpm_y.available.should be == true + @rpm_z.available.should be == false + end + end + + describe "installed" do + it "should return true" do + @rpm_x.installed.should be == false + @rpm_y.installed.should be == true + @rpm_z.installed.should be == true + end + end + +end + +# thanks resource_collection_spec.rb! +describe Chef::Provider::Package::Yum::RPMDb do + before(:each) do + @rpmdb = Chef::Provider::Package::Yum::RPMDb.new + # name, version, arch, installed, available + @rpm_v = Chef::Provider::Package::Yum::RPMDbPackage.new("test-package-a", "0:1.6.5-9.36.el5", "i386", true, false) + @rpm_w = Chef::Provider::Package::Yum::RPMDbPackage.new("test-package-b", "0:1.6.5-9.36.el5", "i386", true, true) + @rpm_x = Chef::Provider::Package::Yum::RPMDbPackage.new("test-package-b", "0:1.6.5-9.36.el5", "x86_64", false, true) + @rpm_y = Chef::Provider::Package::Yum::RPMDbPackage.new("test-package-b", "1:1.6.5-9.36.el5", "x86_64", true, true) + @rpm_z = Chef::Provider::Package::Yum::RPMDbPackage.new("test-package-c", "0:1.6.5-9.36.el5", "noarch", true, true) + @rpm_z_mirror = Chef::Provider::Package::Yum::RPMDbPackage.new("test-package-c", "0:1.6.5-9.36.el5", "noarch", true, true) + end + + describe "initialize" do + it "should return a Chef::Provider::Package::Yum::RPMDb object" do + @rpmdb.should be_kind_of(Chef::Provider::Package::Yum::RPMDb) + end + end + + describe "push" do + it "should accept an RPMDbPackage object through pushing" do + lambda { @rpmdb.push(@rpm_w) }.should_not raise_error + end + + it "should accept multiple RPMDbPackage object through pushing" do + lambda { @rpmdb.push(@rpm_w, @rpm_x, @rpm_y, @rpm_z) }.should_not raise_error + end + + it "should only accept an RPMDbPackage object" do + lambda { @rpmdb.push("string") }.should raise_error + end + + it "should add the package to the package db" do + @rpmdb.push(@rpm_w) + @rpmdb["test-package-b"].should_not be == nil + end + + it "should add conditionally add the package to the available list" do + @rpmdb.available_size.should be == 0 + @rpmdb.push(@rpm_v, @rpm_w) + @rpmdb.available_size.should be == 1 + end + + it "should add conditionally add the package to the installed list" do + @rpmdb.installed_size.should be == 0 + @rpmdb.push(@rpm_w, @rpm_x) + @rpmdb.installed_size.should be == 1 + end + + it "should have a total of 2 packages in the RPMDb" do + @rpmdb.size.should be == 0 + @rpmdb.push(@rpm_w, @rpm_x, @rpm_y, @rpm_z) + @rpmdb.size.should be == 2 + end + + it "should keep the Array unique when a duplicate is pushed" do + @rpmdb.push(@rpm_z, @rpm_z_mirror) + @rpmdb["test-package-c"].size.should be == 1 + end + end + + describe "<<" do + it "should accept an RPMPackage object through the << operator" do + lambda { @rpmdb << @rpm_w }.should_not raise_error + end + end + + describe "lookup" do + it "should return an Array of RPMPackage objects by index" do + @rpmdb << @rpm_w + @rpmdb.lookup("test-package-b").should be_kind_of(Array) + end + end + + describe "[]" do + it "should return an Array of RPMPackage objects though the [index] operator" do + @rpmdb << @rpm_w + @rpmdb["test-package-b"].should be_kind_of(Array) + end + + it "should return an Array of 3 RPMPackage objects" do + @rpmdb.push(@rpm_w, @rpm_x, @rpm_y, @rpm_z) + @rpmdb["test-package-b"].size.should be == 3 + end + + it "should return an Array of RPMPackage objects sorted from newest to oldest" do + @rpmdb.push(@rpm_w, @rpm_x, @rpm_y, @rpm_z) + @rpmdb["test-package-b"][0].should be == @rpm_y + @rpmdb["test-package-b"][1].should be == @rpm_x + @rpmdb["test-package-b"][2].should be == @rpm_w + end + end + + describe "clear" do + it "should clear the RPMDb" do + @rpmdb.should_receive(:clear_available).once + @rpmdb.should_receive(:clear_installed).once + @rpmdb.push(@rpm_w, @rpm_x, @rpm_y, @rpm_z) + @rpmdb.size.should_not be == 0 + @rpmdb.clear + @rpmdb.size.should be == 0 + end + end + + describe "clear_available" do + it "should clear the available list" do + @rpmdb.push(@rpm_w, @rpm_x, @rpm_y, @rpm_z) + @rpmdb.available_size.should_not be == 0 + @rpmdb.clear_available + @rpmdb.available_size.should be == 0 + end + end + + describe "available?" do + it "should return true if a package is available" do + @rpmdb.available?(@rpm_w).should be == false + @rpmdb.push(@rpm_v, @rpm_w) + @rpmdb.available?(@rpm_v).should be == false + @rpmdb.available?(@rpm_w).should be == true + end + end + + describe "clear_installed" do + it "should clear the installed list" do + @rpmdb.push(@rpm_w, @rpm_x, @rpm_y, @rpm_z) + @rpmdb.installed_size.should_not be == 0 + @rpmdb.clear_installed + @rpmdb.installed_size.should be == 0 + end + end + + describe "installed?" do + it "should return true if a package is installed" do + @rpmdb.installed?(@rpm_w).should be == false + @rpmdb.push(@rpm_w, @rpm_x) + @rpmdb.installed?(@rpm_w).should be == true + @rpmdb.installed?(@rpm_x).should be == false + end + end + +end + +describe Chef::Provider::Package::Yum::YumCache do + # allow for the reset of a Singleton + # thanks to Ian White (http://blog.ardes.com/2006/12/11/testing-singletons-with-ruby) + class << Chef::Provider::Package::Yum::YumCache + def reset_instance + Singleton.send :__init__, self + self + end + end + + before(:each) do + yum_dump_good_output = <<EOF +[option installonlypkgs] kernel kernel-bigmem kernel-enterprise +erlang-mochiweb 0 1.4.1 1.el5 x86_64 i +zip 0 2.31 2.el5 x86_64 r +zisofs-tools 0 1.0.6 3.2.2 x86_64 a +zlib 0 1.2.3 3 x86_64 r +zlib 0 1.2.3 3 i386 r +zlib-devel 0 1.2.3 3 i386 a +zlib-devel 0 1.2.3 3 x86_64 r +znc 0 0.098 1.el5 x86_64 a +znc-devel 0 0.098 1.el5 i386 a +znc-devel 0 0.098 1.el5 x86_64 a +znc-extra 0 0.098 1.el5 x86_64 a +znc-modtcl 0 0.098 1.el5 x86_64 a +EOF + + yum_dump_bad_output_separators = <<EOF +zip 0 2.31 2.el5 x86_64 r +zlib 0 1.2.3 3 x86_64 i bad +zlib-devel 0 1.2.3 3 i386 a +bad zlib-devel 0 1.2.3 3 x86_64 i +znc-modtcl 0 0.098 1.el5 x86_64 a bad +EOF + + yum_dump_bad_output_type = <<EOF +zip 0 2.31 2.el5 x86_64 r +zlib 0 1.2.3 3 x86_64 c +zlib-devel 0 1.2.3 3 i386 a +zlib-devel 0 1.2.3 3 x86_64 bad +znc-modtcl 0 0.098 1.el5 x86_64 a +EOF + + yum_dump_error = <<EOF +yum-dump Config Error: File contains no section headers. +file: file://///etc/yum.repos.d/CentOS-Base.repo, line: 12 +'qeqwewe\n' +EOF + + @status = mock("Status", :exitstatus => 0) + @status_bad = mock("Status", :exitstatus => 1) + @stdin = mock("STDIN", :nil_object => true) + @stdout = mock("STDOUT", :nil_object => true) + @stdout_good = yum_dump_good_output.split("\n") + @stdout_bad_type = yum_dump_bad_output_type.split("\n") + @stdout_bad_separators = yum_dump_bad_output_separators.split("\n") + @stderr = mock("STDERR", :nil_object => true) + @stderr.stub!(:readlines).and_return(yum_dump_error.split("\n")) + @pid = mock("PID", :nil_object => true) + + # new singleton each time + Chef::Provider::Package::Yum::YumCache.reset_instance + @yc = Chef::Provider::Package::Yum::YumCache.instance + # load valid data + @yc.stub!(:popen4).and_yield(@pid, @stdin, @stdout_good, @stderr).and_return(@status) + end + + describe "initialize" do + it "should return a Chef::Provider::Package::Yum::YumCache object" do + @yc.should be_kind_of(Chef::Provider::Package::Yum::YumCache) + end + + it "should register reload for start of Chef::Client runs" do + Chef::Provider::Package::Yum::YumCache.reset_instance + Chef::Client.should_receive(:when_run_starts) do |&b| + b.should_not be_nil + end + @yc = Chef::Provider::Package::Yum::YumCache.instance + end + end + + describe "refresh" do + it "should implicitly call yum-dump.py only once by default after being instantiated" do + @yc.should_receive(:popen4).once + @yc.installed_version("zlib") + @yc.reset + @yc.installed_version("zlib") + end + + it "should run yum-dump.py using the system python when next_refresh is for :all" do + @yc.reload + @yc.should_receive(:popen4).with(%r{^/usr/bin/python .*/yum-dump.py --options$}, :waitlast=>true) + @yc.refresh + end + + it "should run yum-dump.py with the installed flag when next_refresh is for :installed" do + @yc.reload_installed + @yc.should_receive(:popen4).with(%r{^/usr/bin/python .*/yum-dump.py --installed$}, :waitlast=>true) + @yc.refresh + end + + it "should warn about invalid data with too many separators" do + @yc.stub!(:popen4).and_yield(@pid, @stdin, @stdout_bad_separators, @stderr).and_return(@status) + Chef::Log.should_receive(:warn).exactly(3).times.with(%r{Problem parsing}) + @yc.refresh + end + + it "should warn about invalid data with an incorrect type" do + @yc.stub!(:popen4).and_yield(@pid, @stdin, @stdout_bad_type, @stderr).and_return(@status) + Chef::Log.should_receive(:warn).exactly(2).times.with(%r{Skipping line}) + @yc.refresh + end + + it "should warn about no output from yum-dump.py" do + @yc.stub!(:popen4).and_yield(@pid, @stdin, [], @stderr).and_return(@status) + Chef::Log.should_receive(:warn).exactly(1).times.with(%r{no output from yum-dump.py}) + @yc.refresh + end + + it "should raise exception yum-dump.py exits with a non zero status" do + @yc.stub!(:popen4).and_yield(@pid, @stdin, [], @stderr).and_return(@status_bad) + lambda { @yc.refresh}.should raise_error(Chef::Exceptions::Package, %r{CentOS-Base.repo, line: 12}) + end + + it "should parse type 'i' into an installed state for a package" do + @yc.available_version("erlang-mochiweb").should be == nil + @yc.installed_version("erlang-mochiweb").should_not be == nil + end + + it "should parse type 'a' into an available state for a package" do + @yc.available_version("znc").should_not be == nil + @yc.installed_version("znc").should be == nil + end + + it "should parse type 'r' into an installed and available states for a package" do + @yc.available_version("zip").should_not be == nil + @yc.installed_version("zip").should_not be == nil + end + + it "should parse installonlypkgs from yum-dump.py options output" do + @yc.allow_multi_install.should be == %w{kernel kernel-bigmem kernel-enterprise} + end + end + + describe "installed_version" do + it "should take one or two arguments" do + lambda { @yc.installed_version("zip") }.should_not raise_error(ArgumentError) + lambda { @yc.installed_version("zip", "i386") }.should_not raise_error(ArgumentError) + lambda { @yc.installed_version("zip", "i386", "extra") }.should raise_error(ArgumentError) + end + + it "should return version-release for matching package regardless of arch" do + @yc.installed_version("zip", "x86_64").should be == "2.31-2.el5" + @yc.installed_version("zip", nil).should be == "2.31-2.el5" + end + + it "should return version-release for matching package and arch" do + @yc.installed_version("zip", "x86_64").should be == "2.31-2.el5" + @yc.installed_version("zisofs-tools", "i386").should be == nil + end + + it "should return nil for an unmatched package" do + @yc.installed_version(nil, nil).should be == nil + @yc.installed_version("test1", nil).should be == nil + @yc.installed_version("test2", "x86_64").should be == nil + end + end + + describe "available_version" do + it "should take one or two arguments" do + lambda { @yc.available_version("zisofs-tools") }.should_not raise_error(ArgumentError) + lambda { @yc.available_version("zisofs-tools", "i386") }.should_not raise_error(ArgumentError) + lambda { @yc.available_version("zisofs-tools", "i386", "extra") }.should raise_error(ArgumentError) + end + + it "should return version-release for matching package regardless of arch" do + @yc.available_version("zip", "x86_64").should be == "2.31-2.el5" + @yc.available_version("zip", nil).should be == "2.31-2.el5" + end + + it "should return version-release for matching package and arch" do + @yc.available_version("zip", "x86_64").should be == "2.31-2.el5" + @yc.available_version("zisofs-tools", "i386").should be == nil + end + + it "should return nil for an unmatched package" do + @yc.available_version(nil, nil).should be == nil + @yc.available_version("test1", nil).should be == nil + @yc.available_version("test2", "x86_64").should be == nil + end + end + + describe "version_available" do + it "should take two or three arguments" do + lambda { @yc.version_available?("zisofs-tools") }.should raise_error(ArgumentError) + lambda { @yc.version_available?("zisofs-tools", "1.0.6-3.2.2") }.should_not raise_error(ArgumentError) + lambda { @yc.version_available?("zisofs-tools", "1.0.6-3.2.2", "x86_64") }.should_not raise_error(ArgumentError) + end + + it "should return true if our package-version-arch is available" do + @yc.version_available?("zisofs-tools", "1.0.6-3.2.2", "x86_64").should be == true + end + + it "should return true if our package-version, no arch, is available" do + @yc.version_available?("zisofs-tools", "1.0.6-3.2.2", nil).should be == true + @yc.version_available?("zisofs-tools", "1.0.6-3.2.2").should be == true + end + + it "should return false if our package-version-arch isn't available" do + @yc.version_available?("zisofs-tools", "1.0.6-3.2.2", "pretend").should be == false + @yc.version_available?("zisofs-tools", "pretend", "x86_64").should be == false + @yc.version_available?("pretend", "1.0.6-3.2.2", "x86_64").should be == false + end + + it "should return false if our package-version, no arch, isn't available" do + @yc.version_available?("zisofs-tools", "pretend", nil).should be == false + @yc.version_available?("zisofs-tools", "pretend").should be == false + @yc.version_available?("pretend", "1.0.6-3.2.2").should be == false + end + end + + describe "reset" do + it "should empty the installed and available packages RPMDb" do + @yc.available_version("zip", "x86_64").should be == "2.31-2.el5" + @yc.installed_version("zip", "x86_64").should be == "2.31-2.el5" + @yc.reset + @yc.available_version("zip", "x86_64").should be == nil + @yc.installed_version("zip", "x86_64").should be == nil + end + end + +end diff --git a/chef/spec/unit/resource/yum_package_spec.rb b/chef/spec/unit/resource/yum_package_spec.rb index a414b6830f..ec01eb1fb3 100644 --- a/chef/spec/unit/resource/yum_package_spec.rb +++ b/chef/spec/unit/resource/yum_package_spec.rb @@ -47,3 +47,39 @@ describe Chef::Resource::YumPackage, "arch" do @resource.arch.should eql("i386") end end + +describe Chef::Resource::YumPackage, "flush_cache" do + before(:each) do + @resource = Chef::Resource::YumPackage.new("foo") + end + + it "should default the flush timing to false" do + flush_hash = { :before => false, :after => false } + @resource.flush_cache.should == flush_hash + end + + it "should allow you to set the flush timing with an array" do + flush_array = [ :before, :after ] + flush_hash = { :before => true, :after => true } + @resource.flush_cache(flush_array) + @resource.flush_cache.should == flush_hash + end + + it "should allow you to set the flush timing with a hash" do + flush_hash = { :before => true, :after => true } + @resource.flush_cache(flush_hash) + @resource.flush_cache.should == flush_hash + end +end + +describe Chef::Resource::YumPackage, "allow_downgrade" do + before(:each) do + @resource = Chef::Resource::YumPackage.new("foo") + end + + it "should allow you to specify whether allow_downgrade is true or false" do + lambda { @resource.allow_downgrade true }.should_not raise_error(ArgumentError) + lambda { @resource.allow_downgrade false }.should_not raise_error(ArgumentError) + lambda { @resource.allow_downgrade "monkey" }.should raise_error(ArgumentError) + end +end |