summaryrefslogtreecommitdiff
path: root/lib/chef/provider/package/yum/yum_cache.rb
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 /lib/chef/provider/package/yum/yum_cache.rb
parentb28096923b23fb18dfa397b52fc3decf9c161cbb (diff)
parentfba56bf27a22ade011d7bdd2d566b392ed9f8b41 (diff)
downloadchef-27c762a6606091f29d0a05289bec39c292fb5861.tar.gz
Merge pull request #4901 from chef/lcg/yum-readability
break yum classes out into files
Diffstat (limited to 'lib/chef/provider/package/yum/yum_cache.rb')
-rw-r--r--lib/chef/provider/package/yum/yum_cache.rb376
1 files changed, 376 insertions, 0 deletions
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