diff options
author | Lamont Granquist <lamont@scriptkiddie.org> | 2016-05-25 14:39:14 -0700 |
---|---|---|
committer | Lamont Granquist <lamont@scriptkiddie.org> | 2016-05-25 14:39:14 -0700 |
commit | 27c762a6606091f29d0a05289bec39c292fb5861 (patch) | |
tree | b37c8d9bef1738ea26caea7c7b7074e976a93969 /lib/chef/provider/package/yum/yum_cache.rb | |
parent | b28096923b23fb18dfa397b52fc3decf9c161cbb (diff) | |
parent | fba56bf27a22ade011d7bdd2d566b392ed9f8b41 (diff) | |
download | chef-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.rb | 376 |
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 |