summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorBryan McLellan <btm@opscode.com>2011-05-19 18:06:34 -0700
committerBryan McLellan <btm@opscode.com>2011-05-19 18:06:34 -0700
commitc039deea60518ae77864f23deb3e6050161c9864 (patch)
tree483d3d8870b6b1a0e886a2a1730fb1a741355180
parentf46c774ed54632d8fe8a62119ec73c9fc03f5940 (diff)
parent6d28564e2be8838baa2e585f43f48b57d8f61d0c (diff)
downloadchef-c039deea60518ae77864f23deb3e6050161c9864.tar.gz
Merge branch 'yum-improvements'
-rw-r--r--chef/lib/chef/monkey_patches/numeric.rb10
-rw-r--r--chef/lib/chef/monkey_patches/string.rb9
-rw-r--r--chef/lib/chef/provider/package/yum-dump.py265
-rw-r--r--chef/lib/chef/provider/package/yum.rb803
-rw-r--r--chef/lib/chef/resource/yum_package.rb20
-rw-r--r--chef/spec/unit/provider/package/yum_spec.rb1118
-rw-r--r--chef/spec/unit/resource/yum_package_spec.rb36
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