summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLamont Granquist <lamont@scriptkiddie.org>2016-05-25 14:39:14 -0700
committerLamont Granquist <lamont@scriptkiddie.org>2016-05-25 14:39:14 -0700
commit27c762a6606091f29d0a05289bec39c292fb5861 (patch)
treeb37c8d9bef1738ea26caea7c7b7074e976a93969
parentb28096923b23fb18dfa397b52fc3decf9c161cbb (diff)
parentfba56bf27a22ade011d7bdd2d566b392ed9f8b41 (diff)
downloadchef-27c762a6606091f29d0a05289bec39c292fb5861.tar.gz
Merge pull request #4901 from chef/lcg/yum-readability
break yum classes out into files
-rw-r--r--lib/chef/provider/package/yum.rb963
-rw-r--r--lib/chef/provider/package/yum/rpm_utils.rb642
-rw-r--r--lib/chef/provider/package/yum/yum-dump.py (renamed from lib/chef/provider/package/yum-dump.py)0
-rw-r--r--lib/chef/provider/package/yum/yum_cache.rb376
-rw-r--r--spec/unit/provider/package/yum/yum_cache_spec.rb27
5 files changed, 1049 insertions, 959 deletions
diff --git a/lib/chef/provider/package/yum.rb b/lib/chef/provider/package/yum.rb
index 64f1b2043c..74d52946f7 100644
--- a/lib/chef/provider/package/yum.rb
+++ b/lib/chef/provider/package/yum.rb
@@ -18,10 +18,10 @@
require "chef/config"
require "chef/provider/package"
-require "chef/mixin/which"
-require "chef/resource/package"
-require "singleton"
+require "chef/resource/yum_package"
require "chef/mixin/get_source_from_package"
+require "chef/provider/package/yum/rpm_utils"
+require "chef/provider/package/yum/yum_cache"
class Chef
class Provider
@@ -31,961 +31,6 @@ class Chef
provides :package, platform_family: %w{rhel fedora}
provides :yum_package, os: "linux"
- 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 %r{^([\d]+):}.match(evr) # rubocop:disable Performance/RedundantMatch
- epoch = $1.to_i
- lead = $1.length + 1
- elsif evr[0].ord == ":".ord
- epoch = 0
- lead = 1
- end
-
- if %r{:?.*-(.*)$}.match(evr) # rubocop:disable Performance/RedundantMatch
- release = $1
- tail = evr.length - release.length - lead - 1
-
- if release.empty?
- release = nil
- end
- end
-
- version = evr[lead, tail]
- if version.empty?
- version = nil
- end
-
- [ epoch, version, release ]
- end
-
- # verify
- def isalnum(x)
- isalpha(x) || isdigit(x)
- end
-
- def isalpha(x)
- v = x.ord
- (v >= 65 && v <= 90) || (v >= 97 && v <= 122)
- end
-
- def isdigit(x)
- v = x.ord
- v >= 48 && 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 && y_pos <= y_pos_max
- # first we skip over anything non alphanumeric
- while (x_pos <= x_pos_max) && (isalnum(x[x_pos]) == false)
- x_pos += 1 # +1 over pos_max if end of string
- end
- while (y_pos <= y_pos_max) && (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) || (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) && 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) && 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) && 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) && 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) && (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
-
- def <=>(y)
- compare_versions(y)
- end
-
- def compare(y)
- compare_versions(y, false)
- end
-
- def partial_compare(y)
- compare_versions(y, true)
- end
-
- # RPM::Version rpm_version_to_s equivalent
- def to_s
- if @r.nil?
- @v
- else
- "#{@v}-#{@r}"
- end
- end
-
- def evr
- "#{@e}:#{@v}-#{@r}"
- end
-
- private
-
- # Rough RPM::Version rpm_version_cmp equivalent - except much slower :)
- #
- # partial lets epoch and version segment equality be good enough to return equal, eg:
- #
- # 2:1.2-1 == 2:1.2
- # 2:1.2-1 == 2:
- #
- def compare_versions(y, partial = false)
- x = self
-
- # compare epoch
- if (x.e.nil? == false && x.e > 0) && y.e.nil?
- return 1
- elsif x.e.nil? && (y.e.nil? == false && y.e > 0)
- return -1
- elsif x.e.nil? == false && y.e.nil? == false
- if x.e < y.e
- return -1
- elsif x.e > y.e
- return 1
- end
- end
-
- # compare version
- if partial && (x.v.nil? || y.v.nil?)
- return 0
- elsif x.v.nil? == false && y.v.nil?
- return 1
- elsif x.v.nil? && y.v.nil? == false
- return -1
- elsif x.v.nil? == false && y.v.nil? == false
- cmp = RPMUtils.rpmvercmp(x.v, y.v)
- return cmp if cmp != 0
- end
-
- # compare release
- if partial && (x.r.nil? || y.r.nil?)
- return 0
- elsif x.r.nil? == false && y.r.nil?
- return 1
- elsif x.r.nil? && y.r.nil? == false
- return -1
- elsif x.r.nil? == false && y.r.nil? == false
- cmp = RPMUtils.rpmvercmp(x.r, y.r)
- return cmp
- end
-
- return 0
- end
- end
-
- class RPMPackage
- include Comparable
-
- def initialize(*args)
- if args.size == 4
- @n = args[0]
- @version = RPMVersion.new(args[1])
- @a = args[2]
- @provides = args[3]
- elsif args.size == 6
- @n = args[0]
- e = args[1].to_i
- v = args[2]
- r = args[3]
- @version = RPMVersion.new(e, v, r)
- @a = args[4]
- @provides = args[5]
- else
- raise ArgumentError, "Expecting either 'name, epoch-version-release, arch, provides' " +
- "or 'name, epoch, version, release, arch, provides'"
- end
-
- # We always have one, ourselves!
- if @provides.empty?
- @provides = [ RPMProvide.new(@n, @version.evr, :==) ]
- end
- end
- attr_reader :n, :a, :version, :provides
- alias :name :n
- alias :arch :a
-
- def <=>(y)
- compare(y)
- end
-
- def compare(y)
- x = self
-
- # easy! :)
- return 0 if x.nevra == y.nevra
-
- # compare name
- if x.n.nil? == false && y.n.nil?
- return 1
- elsif x.n.nil? && y.n.nil? == false
- return -1
- elsif x.n.nil? == false && y.n.nil? == false
- if x.n < y.n
- return -1
- elsif x.n > y.n
- return 1
- end
- end
-
- # compare version
- if x.version > y.version
- return 1
- elsif x.version < y.version
- return -1
- end
-
- # compare arch
- if x.a.nil? == false && y.a.nil?
- return 1
- elsif x.a.nil? && y.a.nil? == false
- return -1
- elsif x.a.nil? == false && y.a.nil? == false
- if x.a < y.a
- return -1
- elsif x.a > y.a
- return 1
- end
- end
-
- return 0
- end
-
- def to_s
- nevra
- end
-
- def nevra
- "#{@n}-#{@version.evr}.#{@a}"
- end
- end
-
- # Simple implementation from rpm and ruby-rpm reference code
- class RPMDependency
- def initialize(*args)
- if args.size == 3
- @name = args[0]
- @version = RPMVersion.new(args[1])
- # Our requirement to other dependencies
- @flag = args[2] || :==
- elsif args.size == 5
- @name = args[0]
- e = args[1].to_i
- v = args[2]
- r = args[3]
- @version = RPMVersion.new(e, v, r)
- @flag = args[4] || :==
- else
- raise ArgumentError, "Expecting either 'name, epoch-version-release, flag' or " +
- "'name, epoch, version, release, flag'"
- end
- end
- attr_reader :name, :version, :flag
-
- # Parses 2 forms:
- #
- # "mtr >= 2:0.71-3.0"
- # "mta"
- def self.parse(string)
- if %r{^(\S+)\s+(>|>=|=|==|<=|<)\s+(\S+)$}.match(string) # rubocop:disable Performance/RedundantMatch
- name = $1
- if $2 == "="
- flag = :==
- else
- flag = :"#{$2}"
- end
- version = $3
-
- return self.new(name, version, flag)
- else
- name = string
- return self.new(name, nil, nil)
- end
- end
-
- # Test if another RPMDependency satisfies our requirements
- def satisfy?(y)
- unless y.kind_of?(RPMDependency)
- raise ArgumentError, "Expecting an RPMDependency object"
- end
-
- x = self
-
- # Easy!
- if x.name != y.name
- return false
- end
-
- # Partial compare
- #
- # eg: x.version 2.3 == y.version 2.3-1
- sense = x.version.partial_compare(y.version)
-
- # Thanks to rpmdsCompare() rpmds.c
- if (sense < 0) && ((x.flag == :> || x.flag == :>=) || (y.flag == :<= || y.flag == :<))
- return true
- elsif (sense > 0) && ((x.flag == :< || x.flag == :<=) || (y.flag == :>= || y.flag == :>))
- return true
- elsif sense == 0 && (
- ((x.flag == :== || x.flag == :<= || x.flag == :>=) && (y.flag == :== || y.flag == :<= || y.flag == :>=)) ||
- (x.flag == :< && y.flag == :<) ||
- (x.flag == :> && y.flag == :>)
- )
- return true
- end
-
- return false
- end
- end
-
- class RPMProvide < RPMDependency; end
- class RPMRequire < RPMDependency; end
-
- class RPMDbPackage < RPMPackage
- # <rpm parts>, installed, available
- def initialize(*args)
- @repoid = args.pop
- # state
- @available = args.pop
- @installed = args.pop
- super(*args)
- end
- attr_reader :repoid, :available, :installed
- end
-
- # Simple storage for RPMPackage objects - keeps them unique and sorted
- class RPMDb
- def initialize
- # package name => [ RPMPackage, RPMPackage ] of different versions
- @rpms = Hash.new
- # package nevra => RPMPackage for lookups
- @index = Hash.new
- # provide name (aka feature) => [RPMPackage, RPMPackage] each providing this feature
- @provides = Hash.new
- # RPMPackages listed as available
- @available = Set.new
- # RPMPackages listed as installed
- @installed = Set.new
- end
-
- def [](package_name)
- self.lookup(package_name)
- end
-
- # Lookup package_name and return a descending array of package objects
- def lookup(package_name)
- pkgs = @rpms[package_name]
- if pkgs
- return pkgs.sort.reverse
- else
- return nil
- end
- end
-
- def lookup_provides(provide_name)
- @provides[provide_name]
- end
-
- # Using the package name as a key, and nevra for an index, keep a unique 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
-
- # we may already have this one, like when the installed list is refreshed
- idx = @index[new_rpm.nevra]
- if idx
- # grab the existing package if it's not
- curr_rpm = idx
- else
- @rpms[new_rpm.n] << new_rpm
-
- new_rpm.provides.each do |provide|
- @provides[provide.name] ||= Array.new
- @provides[provide.name] << new_rpm
- end
-
- curr_rpm = new_rpm
- end
-
- # Track the nevra -> RPMPackage association to avoid having to compare versions
- # with @rpms[new_rpm.n] on the next round
- @index[new_rpm.nevra] = curr_rpm
-
- # these are overwritten for existing packages
- if new_rpm.available
- @available << curr_rpm
- end
- if new_rpm.installed
- @installed << curr_rpm
- end
- end
- end
-
- def <<(*args)
- self.push(args)
- end
-
- def clear
- @rpms.clear
- @index.clear
- @provides.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
-
- def whatprovides(rpmdep)
- unless rpmdep.kind_of?(RPMDependency)
- raise ArgumentError, "Expecting an RPMDependency object"
- end
-
- what = []
-
- packages = lookup_provides(rpmdep.name)
- if packages
- packages.each do |pkg|
- pkg.provides.each do |provide|
- if provide.satisfy?(rpmdep)
- what << pkg
- end
- end
- end
- end
-
- return what
- end
- end
-
- # Cache for our installed and available packages, pulled in from yum-dump.py
- class YumCache
- include Chef::Mixin::Which
- include Chef::Mixin::ShellOut
- include Singleton
-
- attr_accessor :yum_binary
-
- def initialize
- @rpmdb = RPMDb.new
-
- # Next time @rpmdb is accessed:
- # :all - Trigger a run of "yum-dump.py --options --installed-provides", updates
- # yum's cache and parses options from /etc/yum.conf. Pulls in Provides
- # dependency data for installed packages only - this data is slow to
- # gather.
- # :provides - Same as :all but pulls in Provides data for available packages as well.
- # Used as a last resort when we can't find a Provides match.
- # :installed - Trigger a run of "yum-dump.py --installed", only reads the local rpm
- # db. Used between client runs for a quick refresh.
- # :none - Do nothing, a call to one of the reload methods is required.
- @next_refresh = :all
-
- @allow_multi_install = []
-
- @extra_repo_control = nil
-
- # these are for subsequent runs if we are on an interval
- Chef::Client.when_run_starts do
- YumCache.instance.reload
- end
- end
-
- attr_reader :extra_repo_control
-
- # Cache management
- #
-
- def refresh
- case @next_refresh
- when :none
- return nil
- when :installed
- reset_installed
- # fast
- opts = " --installed"
- when :all
- reset
- # medium
- opts = " --options --installed-provides"
- when :provides
- reset
- # slow!
- opts = " --options --all-provides"
- else
- raise ArgumentError, "Unexpected value in next_refresh: #{@next_refresh}"
- end
-
- if @extra_repo_control
- opts << " #{@extra_repo_control}"
- end
-
- opts << " --yum-lock-timeout #{Chef::Config[:yum_lock_timeout]}"
-
- one_line = false
- error = nil
-
- helper = ::File.join(::File.dirname(__FILE__), "yum-dump.py")
- status = nil
-
- begin
- status = shell_out!("#{python_bin} #{helper}#{opts}", :timeout => Chef::Config[:yum_timeout])
- status.stdout.each_line do |line|
- one_line = true
-
- line.chomp!
- if line =~ %r{\[option (.*)\] (.*)}
- if $1 == "installonlypkgs"
- @allow_multi_install = $2.split
- else
- raise Chef::Exceptions::Package, "Strange, unknown option line '#{line}' from yum-dump.py"
- end
- next
- end
-
- if line =~ %r{^(\S+) ([0-9]+) (\S+) (\S+) (\S+) \[(.*)\] ([i,a,r]) (\S+)$}
- name = $1
- epoch = $2
- version = $3
- release = $4
- arch = $5
- provides = parse_provides($6)
- type = $7
- repoid = $8
- else
- Chef::Log.warn("Problem parsing line '#{line}' from yum-dump.py! " +
- "Please check your yum configuration.")
- next
- end
-
- case type
- when "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
- when "a"
- available = true
- installed = false
- when "r"
- available = true
- installed = true
- end
-
- pkg = RPMDbPackage.new(name, epoch, version, release, arch, provides, installed, available, repoid)
- @rpmdb << pkg
- end
-
- error = status.stderr
- rescue Mixlib::ShellOut::CommandTimeout => e
- Chef::Log.error("#{helper} exceeded timeout #{Chef::Config[:yum_timeout]}")
- raise(e)
- end
-
- 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 python_bin
- yum_executable = which(yum_binary)
- if yum_executable && shabang?(yum_executable)
- shabang_or_fallback(extract_interpreter(yum_executable))
- else
- Chef::Log.warn("Yum executable not found or doesn't start with #!. Using default python.")
- "/usr/bin/python"
- end
- rescue StandardError => e
- Chef::Log.warn("An error occurred attempting to determine correct python executable. Using default.")
- Chef::Log.debug(e)
- "/usr/bin/python"
- end
-
- def extract_interpreter(file)
- ::File.open(file, "r", &:readline)[2..-1].strip
- end
-
- # dnf based systems have a yum shim that has /bin/bash as the interpreter. Don't use this.
- def shabang_or_fallback(interpreter)
- if interpreter == "/bin/bash"
- Chef::Log.warn("Yum executable interpreter is /bin/bash. Falling back to default python.")
- "/usr/bin/python"
- else
- interpreter
- end
- end
-
- def shabang?(file)
- ::File.open(file, "r") do |f|
- f.read(2) == '#!'
- end
- rescue Errno::ENOENT
- false
- end
-
- def reload
- @next_refresh = :all
- end
-
- def reload_installed
- @next_refresh = :installed
- end
-
- def reload_provides
- @next_refresh = :provides
- end
-
- def reset
- @rpmdb.clear
- end
-
- def reset_installed
- @rpmdb.clear_installed
- end
-
- # Querying the cache
- #
-
- # Check for package by name or name+arch
- def package_available?(package_name)
- refresh
-
- if @rpmdb.lookup(package_name)
- return true
- else
- if package_name =~ %r{^(.*)\.(.*)$}
- pkg_name = $1
- pkg_arch = $2
-
- if matches = @rpmdb.lookup(pkg_name)
- matches.each do |m|
- return true if m.arch == pkg_arch
- end
- end
- end
- end
-
- return false
- end
-
- # Returns a array of packages satisfying an RPMDependency
- def packages_from_require(rpmdep)
- refresh
- @rpmdb.whatprovides(rpmdep)
- end
-
- # Check if a package-version.arch is available to install
- 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
-
- # Return the source repository for a package-version.arch
- def package_repository(package_name, desired_version, arch = nil)
- package(package_name, arch, true, false) do |pkg|
- return pkg.repoid if desired_version == pkg.version.to_s
- end
-
- return nil
- end
-
- # Return the latest available version for a package.arch
- def available_version(package_name, arch = nil)
- version(package_name, arch, true, false)
- end
- alias :candidate_version :available_version
-
- # Return the currently installed version for a package.arch
- def installed_version(package_name, arch = nil)
- version(package_name, arch, false, true)
- end
-
- # Return an array of packages allowed to be installed multiple times, such as the kernel
- def allow_multi_install
- refresh
- @allow_multi_install
- end
-
- def enable_extra_repo_control(arg)
- # Don't touch cache if it's the same repos as the last load
- unless @extra_repo_control == arg
- @extra_repo_control = arg
- reload
- end
- end
-
- def disable_extra_repo_control
- # Only force reload when set
- if @extra_repo_control
- @extra_repo_control = nil
- reload
- end
- end
-
- private
-
- def version(package_name, arch = nil, is_available = false, is_installed = false)
- package(package_name, arch, is_available, is_installed) do |pkg|
- if block_given?
- yield pkg.version.to_s
- else
- # first match is latest version
- return pkg.version.to_s
- end
- end
-
- if block_given?
- return self
- else
- return nil
- end
- end
-
- def package(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
- else
- # first match is latest version
- return pkg
- end
- end
- end
-
- if block_given?
- return self
- else
- return nil
- end
- end
-
- # Parse provides from yum-dump.py output
- def parse_provides(string)
- ret = []
- # ['atk = 1.12.2-1.fc6', 'libatk-1.0.so.0']
- string.split(", ").each do |seg|
- # 'atk = 1.12.2-1.fc6'
- if seg =~ %r{^'(.*)'$}
- ret << RPMProvide.parse($1)
- end
- end
-
- return ret
- end
-
- end # YumCache
-
include Chef::Mixin::GetSourceFromPackage
def initialize(new_resource, run_context)
@@ -1126,7 +171,7 @@ class Chef
end
end
- @current_resource = Chef::Resource::Package.new(@new_resource.name)
+ @current_resource = Chef::Resource::YumPackage.new(@new_resource.name)
@current_resource.package_name(@new_resource.package_name)
installed_version = []
diff --git a/lib/chef/provider/package/yum/rpm_utils.rb b/lib/chef/provider/package/yum/rpm_utils.rb
new file mode 100644
index 0000000000..a748c664a9
--- /dev/null
+++ b/lib/chef/provider/package/yum/rpm_utils.rb
@@ -0,0 +1,642 @@
+
+# Author:: Adam Jacob (<adam@chef.io>)
+# Copyright:: Copyright 2008-2016, Chef Software, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require "chef/provider/package"
+
+class Chef
+ class Provider
+ class Package
+ class Yum < Chef::Provider::Package
+ 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 %r{^([\d]+):}.match(evr) # rubocop:disable Performance/RedundantMatch
+ epoch = $1.to_i
+ lead = $1.length + 1
+ elsif evr[0].ord == ":".ord
+ epoch = 0
+ lead = 1
+ end
+
+ if %r{:?.*-(.*)$}.match(evr) # rubocop:disable Performance/RedundantMatch
+ release = $1
+ tail = evr.length - release.length - lead - 1
+
+ if release.empty?
+ release = nil
+ end
+ end
+
+ version = evr[lead, tail]
+ if version.empty?
+ version = nil
+ end
+
+ [ epoch, version, release ]
+ end
+
+ # verify
+ def isalnum(x)
+ isalpha(x) || isdigit(x)
+ end
+
+ def isalpha(x)
+ v = x.ord
+ (v >= 65 && v <= 90) || (v >= 97 && v <= 122)
+ end
+
+ def isdigit(x)
+ v = x.ord
+ v >= 48 && 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 && y_pos <= y_pos_max
+ # first we skip over anything non alphanumeric
+ while (x_pos <= x_pos_max) && (isalnum(x[x_pos]) == false)
+ x_pos += 1 # +1 over pos_max if end of string
+ end
+ while (y_pos <= y_pos_max) && (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) || (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) && 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) && 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) && 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) && 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) && (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
+
+ def <=>(y)
+ compare_versions(y)
+ end
+
+ def compare(y)
+ compare_versions(y, false)
+ end
+
+ def partial_compare(y)
+ compare_versions(y, true)
+ end
+
+ # RPM::Version rpm_version_to_s equivalent
+ def to_s
+ if @r.nil?
+ @v
+ else
+ "#{@v}-#{@r}"
+ end
+ end
+
+ def evr
+ "#{@e}:#{@v}-#{@r}"
+ end
+
+ private
+
+ # Rough RPM::Version rpm_version_cmp equivalent - except much slower :)
+ #
+ # partial lets epoch and version segment equality be good enough to return equal, eg:
+ #
+ # 2:1.2-1 == 2:1.2
+ # 2:1.2-1 == 2:
+ #
+ def compare_versions(y, partial = false)
+ x = self
+
+ # compare epoch
+ if (x.e.nil? == false && x.e > 0) && y.e.nil?
+ return 1
+ elsif x.e.nil? && (y.e.nil? == false && y.e > 0)
+ return -1
+ elsif x.e.nil? == false && y.e.nil? == false
+ if x.e < y.e
+ return -1
+ elsif x.e > y.e
+ return 1
+ end
+ end
+
+ # compare version
+ if partial && (x.v.nil? || y.v.nil?)
+ return 0
+ elsif x.v.nil? == false && y.v.nil?
+ return 1
+ elsif x.v.nil? && y.v.nil? == false
+ return -1
+ elsif x.v.nil? == false && y.v.nil? == false
+ cmp = RPMUtils.rpmvercmp(x.v, y.v)
+ return cmp if cmp != 0
+ end
+
+ # compare release
+ if partial && (x.r.nil? || y.r.nil?)
+ return 0
+ elsif x.r.nil? == false && y.r.nil?
+ return 1
+ elsif x.r.nil? && y.r.nil? == false
+ return -1
+ elsif x.r.nil? == false && y.r.nil? == false
+ cmp = RPMUtils.rpmvercmp(x.r, y.r)
+ return cmp
+ end
+
+ return 0
+ end
+ end
+
+ class RPMPackage
+ include Comparable
+
+ def initialize(*args)
+ if args.size == 4
+ @n = args[0]
+ @version = RPMVersion.new(args[1])
+ @a = args[2]
+ @provides = args[3]
+ elsif args.size == 6
+ @n = args[0]
+ e = args[1].to_i
+ v = args[2]
+ r = args[3]
+ @version = RPMVersion.new(e, v, r)
+ @a = args[4]
+ @provides = args[5]
+ else
+ raise ArgumentError, "Expecting either 'name, epoch-version-release, arch, provides' " +
+ "or 'name, epoch, version, release, arch, provides'"
+ end
+
+ # We always have one, ourselves!
+ if @provides.empty?
+ @provides = [ RPMProvide.new(@n, @version.evr, :==) ]
+ end
+ end
+ attr_reader :n, :a, :version, :provides
+ alias :name :n
+ alias :arch :a
+
+ def <=>(y)
+ compare(y)
+ end
+
+ def compare(y)
+ x = self
+
+ # easy! :)
+ return 0 if x.nevra == y.nevra
+
+ # compare name
+ if x.n.nil? == false && y.n.nil?
+ return 1
+ elsif x.n.nil? && y.n.nil? == false
+ return -1
+ elsif x.n.nil? == false && y.n.nil? == false
+ if x.n < y.n
+ return -1
+ elsif x.n > y.n
+ return 1
+ end
+ end
+
+ # compare version
+ if x.version > y.version
+ return 1
+ elsif x.version < y.version
+ return -1
+ end
+
+ # compare arch
+ if x.a.nil? == false && y.a.nil?
+ return 1
+ elsif x.a.nil? && y.a.nil? == false
+ return -1
+ elsif x.a.nil? == false && y.a.nil? == false
+ if x.a < y.a
+ return -1
+ elsif x.a > y.a
+ return 1
+ end
+ end
+
+ return 0
+ end
+
+ def to_s
+ nevra
+ end
+
+ def nevra
+ "#{@n}-#{@version.evr}.#{@a}"
+ end
+ end
+
+ # Simple implementation from rpm and ruby-rpm reference code
+ class RPMDependency
+ def initialize(*args)
+ if args.size == 3
+ @name = args[0]
+ @version = RPMVersion.new(args[1])
+ # Our requirement to other dependencies
+ @flag = args[2] || :==
+ elsif args.size == 5
+ @name = args[0]
+ e = args[1].to_i
+ v = args[2]
+ r = args[3]
+ @version = RPMVersion.new(e, v, r)
+ @flag = args[4] || :==
+ else
+ raise ArgumentError, "Expecting either 'name, epoch-version-release, flag' or " +
+ "'name, epoch, version, release, flag'"
+ end
+ end
+ attr_reader :name, :version, :flag
+
+ # Parses 2 forms:
+ #
+ # "mtr >= 2:0.71-3.0"
+ # "mta"
+ def self.parse(string)
+ if %r{^(\S+)\s+(>|>=|=|==|<=|<)\s+(\S+)$}.match(string) # rubocop:disable Performance/RedundantMatch
+ name = $1
+ if $2 == "="
+ flag = :==
+ else
+ flag = :"#{$2}"
+ end
+ version = $3
+
+ return self.new(name, version, flag)
+ else
+ name = string
+ return self.new(name, nil, nil)
+ end
+ end
+
+ # Test if another RPMDependency satisfies our requirements
+ def satisfy?(y)
+ unless y.kind_of?(RPMDependency)
+ raise ArgumentError, "Expecting an RPMDependency object"
+ end
+
+ x = self
+
+ # Easy!
+ if x.name != y.name
+ return false
+ end
+
+ # Partial compare
+ #
+ # eg: x.version 2.3 == y.version 2.3-1
+ sense = x.version.partial_compare(y.version)
+
+ # Thanks to rpmdsCompare() rpmds.c
+ if (sense < 0) && ((x.flag == :> || x.flag == :>=) || (y.flag == :<= || y.flag == :<))
+ return true
+ elsif (sense > 0) && ((x.flag == :< || x.flag == :<=) || (y.flag == :>= || y.flag == :>))
+ return true
+ elsif sense == 0 && (
+ ((x.flag == :== || x.flag == :<= || x.flag == :>=) && (y.flag == :== || y.flag == :<= || y.flag == :>=)) ||
+ (x.flag == :< && y.flag == :<) ||
+ (x.flag == :> && y.flag == :>)
+ )
+ return true
+ end
+
+ return false
+ end
+ end
+
+ class RPMProvide < RPMDependency; end
+ class RPMRequire < RPMDependency; end
+
+ class RPMDbPackage < RPMPackage
+ # <rpm parts>, installed, available
+ def initialize(*args)
+ @repoid = args.pop
+ # state
+ @available = args.pop
+ @installed = args.pop
+ super(*args)
+ end
+ attr_reader :repoid, :available, :installed
+ end
+
+ # Simple storage for RPMPackage objects - keeps them unique and sorted
+ class RPMDb
+ def initialize
+ # package name => [ RPMPackage, RPMPackage ] of different versions
+ @rpms = Hash.new
+ # package nevra => RPMPackage for lookups
+ @index = Hash.new
+ # provide name (aka feature) => [RPMPackage, RPMPackage] each providing this feature
+ @provides = Hash.new
+ # RPMPackages listed as available
+ @available = Set.new
+ # RPMPackages listed as installed
+ @installed = Set.new
+ end
+
+ def [](package_name)
+ self.lookup(package_name)
+ end
+
+ # Lookup package_name and return a descending array of package objects
+ def lookup(package_name)
+ pkgs = @rpms[package_name]
+ if pkgs
+ return pkgs.sort.reverse
+ else
+ return nil
+ end
+ end
+
+ def lookup_provides(provide_name)
+ @provides[provide_name]
+ end
+
+ # Using the package name as a key, and nevra for an index, keep a unique 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
+
+ # we may already have this one, like when the installed list is refreshed
+ idx = @index[new_rpm.nevra]
+ if idx
+ # grab the existing package if it's not
+ curr_rpm = idx
+ else
+ @rpms[new_rpm.n] << new_rpm
+
+ new_rpm.provides.each do |provide|
+ @provides[provide.name] ||= Array.new
+ @provides[provide.name] << new_rpm
+ end
+
+ curr_rpm = new_rpm
+ end
+
+ # Track the nevra -> RPMPackage association to avoid having to compare versions
+ # with @rpms[new_rpm.n] on the next round
+ @index[new_rpm.nevra] = curr_rpm
+
+ # these are overwritten for existing packages
+ if new_rpm.available
+ @available << curr_rpm
+ end
+ if new_rpm.installed
+ @installed << curr_rpm
+ end
+ end
+ end
+
+ def <<(*args)
+ self.push(args)
+ end
+
+ def clear
+ @rpms.clear
+ @index.clear
+ @provides.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
+
+ def whatprovides(rpmdep)
+ unless rpmdep.kind_of?(RPMDependency)
+ raise ArgumentError, "Expecting an RPMDependency object"
+ end
+
+ what = []
+
+ packages = lookup_provides(rpmdep.name)
+ if packages
+ packages.each do |pkg|
+ pkg.provides.each do |provide|
+ if provide.satisfy?(rpmdep)
+ what << pkg
+ end
+ end
+ end
+ end
+
+ return what
+ end
+ end
+
+ end
+ end
+ end
+end
diff --git a/lib/chef/provider/package/yum-dump.py b/lib/chef/provider/package/yum/yum-dump.py
index 6183460195..6183460195 100644
--- a/lib/chef/provider/package/yum-dump.py
+++ b/lib/chef/provider/package/yum/yum-dump.py
diff --git a/lib/chef/provider/package/yum/yum_cache.rb b/lib/chef/provider/package/yum/yum_cache.rb
new file mode 100644
index 0000000000..fb25a91c8c
--- /dev/null
+++ b/lib/chef/provider/package/yum/yum_cache.rb
@@ -0,0 +1,376 @@
+
+# Author:: Adam Jacob (<adam@chef.io>)
+# Copyright:: Copyright 2008-2016, Chef Software, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require "chef/config"
+require "chef/provider/package"
+require "chef/mixin/which"
+require "chef/mixin/shell_out"
+require "singleton"
+require "chef/provider/package/yum/rpm_utils"
+
+class Chef
+ class Provider
+ class Package
+ class Yum < Chef::Provider::Package
+ # Cache for our installed and available packages, pulled in from yum-dump.py
+ class YumCache
+ include Chef::Mixin::Which
+ include Chef::Mixin::ShellOut
+ include Singleton
+
+ attr_accessor :yum_binary
+
+ def initialize
+ @rpmdb = RPMDb.new
+
+ # Next time @rpmdb is accessed:
+ # :all - Trigger a run of "yum-dump.py --options --installed-provides", updates
+ # yum's cache and parses options from /etc/yum.conf. Pulls in Provides
+ # dependency data for installed packages only - this data is slow to
+ # gather.
+ # :provides - Same as :all but pulls in Provides data for available packages as well.
+ # Used as a last resort when we can't find a Provides match.
+ # :installed - Trigger a run of "yum-dump.py --installed", only reads the local rpm
+ # db. Used between client runs for a quick refresh.
+ # :none - Do nothing, a call to one of the reload methods is required.
+ @next_refresh = :all
+
+ @allow_multi_install = []
+
+ @extra_repo_control = nil
+
+ # these are for subsequent runs if we are on an interval
+ Chef::Client.when_run_starts do
+ YumCache.instance.reload
+ end
+ end
+
+ attr_reader :extra_repo_control
+
+ # Cache management
+ #
+
+ def yum_dump_path
+ ::File.join(::File.dirname(__FILE__), "yum-dump.py")
+ end
+
+ def refresh
+ case @next_refresh
+ when :none
+ return nil
+ when :installed
+ reset_installed
+ # fast
+ opts = " --installed"
+ when :all
+ reset
+ # medium
+ opts = " --options --installed-provides"
+ when :provides
+ reset
+ # slow!
+ opts = " --options --all-provides"
+ else
+ raise ArgumentError, "Unexpected value in next_refresh: #{@next_refresh}"
+ end
+
+ if @extra_repo_control
+ opts << " #{@extra_repo_control}"
+ end
+
+ opts << " --yum-lock-timeout #{Chef::Config[:yum_lock_timeout]}"
+
+ one_line = false
+ error = nil
+
+ status = nil
+
+ begin
+ status = shell_out!("#{python_bin} #{yum_dump_path}#{opts}", :timeout => Chef::Config[:yum_timeout])
+ status.stdout.each_line do |line|
+ one_line = true
+
+ line.chomp!
+ if line =~ %r{\[option (.*)\] (.*)}
+ if $1 == "installonlypkgs"
+ @allow_multi_install = $2.split
+ else
+ raise Chef::Exceptions::Package, "Strange, unknown option line '#{line}' from yum-dump.py"
+ end
+ next
+ end
+
+ if line =~ %r{^(\S+) ([0-9]+) (\S+) (\S+) (\S+) \[(.*)\] ([i,a,r]) (\S+)$}
+ name = $1
+ epoch = $2
+ version = $3
+ release = $4
+ arch = $5
+ provides = parse_provides($6)
+ type = $7
+ repoid = $8
+ else
+ Chef::Log.warn("Problem parsing line '#{line}' from yum-dump.py! " +
+ "Please check your yum configuration.")
+ next
+ end
+
+ case type
+ when "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
+ when "a"
+ available = true
+ installed = false
+ when "r"
+ available = true
+ installed = true
+ end
+
+ pkg = RPMDbPackage.new(name, epoch, version, release, arch, provides, installed, available, repoid)
+ @rpmdb << pkg
+ end
+
+ error = status.stderr
+ rescue Mixlib::ShellOut::CommandTimeout => e
+ Chef::Log.error("#{yum_dump_path} exceeded timeout #{Chef::Config[:yum_timeout]}")
+ raise(e)
+ end
+
+ 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 python_bin
+ yum_executable = which(yum_binary)
+ if yum_executable && shabang?(yum_executable)
+ shabang_or_fallback(extract_interpreter(yum_executable))
+ else
+ Chef::Log.warn("Yum executable not found or doesn't start with #!. Using default python.")
+ "/usr/bin/python"
+ end
+ rescue StandardError => e
+ Chef::Log.warn("An error occurred attempting to determine correct python executable. Using default.")
+ Chef::Log.debug(e)
+ "/usr/bin/python"
+ end
+
+ def extract_interpreter(file)
+ ::File.open(file, "r", &:readline)[2..-1].strip
+ end
+
+ # dnf based systems have a yum shim that has /bin/bash as the interpreter. Don't use this.
+ def shabang_or_fallback(interpreter)
+ if interpreter == "/bin/bash"
+ Chef::Log.warn("Yum executable interpreter is /bin/bash. Falling back to default python.")
+ "/usr/bin/python"
+ else
+ interpreter
+ end
+ end
+
+ def shabang?(file)
+ ::File.open(file, "r") do |f|
+ f.read(2) == '#!'
+ end
+ rescue Errno::ENOENT
+ false
+ end
+
+ def reload
+ @next_refresh = :all
+ end
+
+ def reload_installed
+ @next_refresh = :installed
+ end
+
+ def reload_provides
+ @next_refresh = :provides
+ end
+
+ def reset
+ @rpmdb.clear
+ end
+
+ def reset_installed
+ @rpmdb.clear_installed
+ end
+
+ # Querying the cache
+ #
+
+ # Check for package by name or name+arch
+ def package_available?(package_name)
+ refresh
+
+ if @rpmdb.lookup(package_name)
+ return true
+ else
+ if package_name =~ %r{^(.*)\.(.*)$}
+ pkg_name = $1
+ pkg_arch = $2
+
+ if matches = @rpmdb.lookup(pkg_name)
+ matches.each do |m|
+ return true if m.arch == pkg_arch
+ end
+ end
+ end
+ end
+
+ return false
+ end
+
+ # Returns a array of packages satisfying an RPMDependency
+ def packages_from_require(rpmdep)
+ refresh
+ @rpmdb.whatprovides(rpmdep)
+ end
+
+ # Check if a package-version.arch is available to install
+ 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
+
+ # Return the source repository for a package-version.arch
+ def package_repository(package_name, desired_version, arch = nil)
+ package(package_name, arch, true, false) do |pkg|
+ return pkg.repoid if desired_version == pkg.version.to_s
+ end
+
+ return nil
+ end
+
+ # Return the latest available version for a package.arch
+ def available_version(package_name, arch = nil)
+ version(package_name, arch, true, false)
+ end
+ alias :candidate_version :available_version
+
+ # Return the currently installed version for a package.arch
+ def installed_version(package_name, arch = nil)
+ version(package_name, arch, false, true)
+ end
+
+ # Return an array of packages allowed to be installed multiple times, such as the kernel
+ def allow_multi_install
+ refresh
+ @allow_multi_install
+ end
+
+ def enable_extra_repo_control(arg)
+ # Don't touch cache if it's the same repos as the last load
+ unless @extra_repo_control == arg
+ @extra_repo_control = arg
+ reload
+ end
+ end
+
+ def disable_extra_repo_control
+ # Only force reload when set
+ if @extra_repo_control
+ @extra_repo_control = nil
+ reload
+ end
+ end
+
+ private
+
+ def version(package_name, arch = nil, is_available = false, is_installed = false)
+ package(package_name, arch, is_available, is_installed) do |pkg|
+ if block_given?
+ yield pkg.version.to_s
+ else
+ # first match is latest version
+ return pkg.version.to_s
+ end
+ end
+
+ if block_given?
+ return self
+ else
+ return nil
+ end
+ end
+
+ def package(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
+ else
+ # first match is latest version
+ return pkg
+ end
+ end
+ end
+
+ if block_given?
+ return self
+ else
+ return nil
+ end
+ end
+
+ # Parse provides from yum-dump.py output
+ def parse_provides(string)
+ ret = []
+ # ['atk = 1.12.2-1.fc6', 'libatk-1.0.so.0']
+ string.split(", ").each do |seg|
+ # 'atk = 1.12.2-1.fc6'
+ if seg =~ %r{^'(.*)'$}
+ ret << RPMProvide.parse($1)
+ end
+ end
+
+ return ret
+ end
+
+ end # YumCache
+ end
+ end
+ end
+end
diff --git a/spec/unit/provider/package/yum/yum_cache_spec.rb b/spec/unit/provider/package/yum/yum_cache_spec.rb
new file mode 100644
index 0000000000..e9d615d734
--- /dev/null
+++ b/spec/unit/provider/package/yum/yum_cache_spec.rb
@@ -0,0 +1,27 @@
+#
+# Author:: Adam Jacob (<adam@chef.io>)
+# Copyright:: Copyright 2008-2016, Chef Software, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require "spec_helper"
+
+describe Chef::Provider::Package::Yum::YumCache do
+
+ it "can find yum-dump.py" do
+ expect(File.exist?(Chef::Provider::Package::Yum::YumCache.instance.yum_dump_path)).to be true
+ end
+
+end