class Chef::Provider::Package

Attributes

candidate_version[RW]

Hook that subclasses use to populate the candidate_version(s)

@return [Array, String] candidate_version(s) may be a string or array

Public Class Methods

new(new_resource, run_context) click to toggle source
Calls superclass method Chef::Provider::new
# File lib/chef/provider/package.rb, line 72
def initialize(new_resource, run_context)
  super
  @candidate_version = nil
end

Public Instance Methods

as_array(thing) click to toggle source

helper method used by subclasses

# File lib/chef/provider/package.rb, line 388
def as_array(thing)
  [ thing ].flatten
end
check_resource_semantics!() click to toggle source
# File lib/chef/provider/package.rb, line 81
def check_resource_semantics!
  # FIXME: this is not universally true and subclasses are needing to override this and no-ops it.  It should be turned into
  # another "subclass_directive" and the apt and yum providers should declare that they need this behavior.
  if new_resource.package_name.is_a?(Array) && !new_resource.source.nil?
    raise Chef::Exceptions::InvalidResourceSpecification, "You may not specify both multipackage and source"
  end
end
define_resource_requirements() click to toggle source
# File lib/chef/provider/package.rb, line 91
def define_resource_requirements
  # XXX: upgrade with a specific version doesn't make a whole lot of sense, but why don't we throw this anyway if it happens?
  # if not, shouldn't we raise to tell the user to use install instead of upgrade if they want to pin a version?
  requirements.assert(:install) do |a|
    a.assertion { candidates_exist_for_all_forced_changes? }
    a.failure_message(Chef::Exceptions::Package, "No version specified, and no candidate version available for #{forced_packages_missing_candidates.join(", ")}")
    a.whyrun("Assuming a repository that offers #{forced_packages_missing_candidates.join(", ")} would have been configured")
  end

  # XXX: Does it make sense to pass in a source with :upgrade? Probably
  # not, but as with the above comment, we don't yet enforce such a thing,
  # so we'll just leave things as-is for now.
  requirements.assert(:upgrade, :install) do |a|
    a.assertion { candidates_exist_for_all_uninstalled? || new_resource.source }
    a.failure_message(Chef::Exceptions::Package, "No candidate version available for #{packages_missing_candidates.join(", ")}")
    a.whyrun("Assuming a repository that offers #{packages_missing_candidates.join(", ")} would have been configured")
  end
end
expand_options(options) click to toggle source

used by subclasses. deprecated. use a_to_s instead.

# File lib/chef/provider/package.rb, line 306
def expand_options(options)
  # its deprecated but still work to do to deprecate it fully
  # Chef.deprecated(:package_misc, "expand_options is deprecated, use shell_out instead")
  if options
    " #{options.is_a?(Array) ? Shellwords.join(options) : options}"
  else
    ""
  end
end
have_any_matching_version?() click to toggle source
# File lib/chef/provider/package.rb, line 184
def have_any_matching_version?
  f = []
  new_version_array.each_with_index do |item, index|
    f << (item == current_version_array[index])
  end
  f.any?
end
install_package(name, version) click to toggle source
# File lib/chef/provider/package.rb, line 273
def install_package(name, version)
  raise Chef::Exceptions::UnsupportedAction, "#{self} does not support :install"
end
load_current_resource() click to toggle source
# File lib/chef/provider/package.rb, line 89
def load_current_resource; end
lock_package(name, version) click to toggle source
# File lib/chef/provider/package.rb, line 297
def lock_package(name, version)
  raise( Chef::Exceptions::UnsupportedAction, "#{self} does not support :lock" )
end
multipackage_api_adapter(name, version) { |[name].flatten, [version].flatten| ... } click to toggle source

@todo use composition rather than inheritance

# File lib/chef/provider/package.rb, line 265
def multipackage_api_adapter(name, version)
  if use_multipackage_api?
    yield [name].flatten, [version].flatten
  else
    yield name, version
  end
end
options() click to toggle source
# File lib/chef/provider/package.rb, line 77
def options
  new_resource.options
end
package_locked(name, version) click to toggle source

for multipackage just implement packages_all_locked? properly and omit implementing this API

# File lib/chef/provider/package.rb, line 256
def package_locked(name, version)
  raise Chef::Exceptions::UnsupportedAction, "#{self} has no way to detect if package is locked"
end
prepare_for_installation() click to toggle source

Subclasses will override this to a method and provide a preseed file path

# File lib/chef/provider/package.rb, line 261
def prepare_for_installation; end
preseed_package(file) click to toggle source
# File lib/chef/provider/package.rb, line 289
def preseed_package(file)
  raise Chef::Exceptions::UnsupportedAction, "#{self} does not support pre-seeding package install/upgrade instructions"
end
purge_package(name, version) click to toggle source
# File lib/chef/provider/package.rb, line 285
def purge_package(name, version)
  raise Chef::Exceptions::UnsupportedAction, "#{self} does not support :purge"
end
reconfig_package(name) click to toggle source
# File lib/chef/provider/package.rb, line 293
def reconfig_package(name)
  raise( Chef::Exceptions::UnsupportedAction, "#{self} does not support :reconfig" )
end
remove_package(name, version) click to toggle source
# File lib/chef/provider/package.rb, line 281
def remove_package(name, version)
  raise Chef::Exceptions::UnsupportedAction, "#{self} does not support :remove"
end
removing_package?() click to toggle source
# File lib/chef/provider/package.rb, line 192
def removing_package?
  if !current_version_array.any?
    # ! any? means it's all nil's, which means nothing is installed
    false
  elsif !new_version_array.any?
    true # remove any version of all packages
  elsif have_any_matching_version?
    true # remove the version we have
  else
    false # we don't have the version we want to remove
  end
end
target_version_already_installed?(current_version, target_version) click to toggle source

This method performs a strict equality check between two strings representing version numbers

This function will eventually be deprecated in favour of the below version_equals function.

# File lib/chef/provider/package.rb, line 340
def target_version_already_installed?(current_version, target_version)
  version_equals?(current_version, target_version)
end
unlock_package(name, version) click to toggle source
# File lib/chef/provider/package.rb, line 301
def unlock_package(name, version)
  raise( Chef::Exceptions::UnsupportedAction, "#{self} does not support :unlock" )
end
upgrade_package(name, version) click to toggle source
# File lib/chef/provider/package.rb, line 277
def upgrade_package(name, version)
  raise Chef::Exceptions::UnsupportedAction, "#{self} does not support :upgrade"
end
version_compare(v1, v2) click to toggle source

This function compares two version numbers and returns ‘spaceship operator’ style results, ie: if v1 < v2 then return -1 if v1 = v2 then return 0 if v1 > v2 then return 1 if v1 and v2 are not comparable then return nil

By default, this function will use Gem::Version comparison. Subclasses can reimplement this method for package-management system specific versions.

(In other words, pull requests to introduce domain specific mangling of versions into this method will be closed – that logic must go into the subclass – we understand that this is far from perfect but it is a better default than outright buggy things like v1.to_f <=> v2.to_f)

# File lib/chef/provider/package.rb, line 368
def version_compare(v1, v2)
  gem_v1 = Gem::Version.new(v1.gsub(/\A\s*(#{Gem::Version::VERSION_PATTERN}).*/, '\1'))
  gem_v2 = Gem::Version.new(v2.gsub(/\A\s*(#{Gem::Version::VERSION_PATTERN}).*/, '\1'))

  gem_v1 <=> gem_v2
end
version_equals?(v1, v2) click to toggle source

This method performs a strict equality check between two strings representing version numbers

# File lib/chef/provider/package.rb, line 349
def version_equals?(v1, v2)
  return false unless v1 && v2

  v1 == v2
end
version_requirement_satisfied?(current_version, new_version) click to toggle source

Check the current_version against the new_resource.version, possibly using fuzzy matching criteria.

Subclasses MAY override this to provide fuzzy matching on the resource (‘>=’ and ‘~>’ stuff)

‘version_satisfied_by?(version, constraint)` might be a better name to make this generic.

# File lib/chef/provider/package.rb, line 382
def version_requirement_satisfied?(current_version, new_version)
  target_version_already_installed?(current_version, new_version)
end

Private Instance Methods

allow_downgrade() click to toggle source
# File lib/chef/provider/package.rb, line 660
def allow_downgrade
  if new_resource.respond_to?(:allow_downgrade)
    new_resource.allow_downgrade
  else
    true
  end
end
candidate_version_array() click to toggle source

@return [Array] candidate_version(s) as an array

# File lib/chef/provider/package.rb, line 614
def candidate_version_array
  # NOTE: even with use_multipackage_api candidate_version may be a bare nil and need wrapping
  # ( looking at you, dpkg provider... )
  Chef::Decorator::LazyArray.new { [ candidate_version ].flatten }
end
candidates_exist_for_all_forced_changes?() click to toggle source

This looks for packages which have a new_version and a current_version, and they are different (a “forced change”) and for which there is no candidate. This is an edge condition that candidates_exist_for_all_uninstalled? does not catch since in this case it is not uninstalled but must be installed anyway and no version exists.

@return [Boolean] valid candidates exist for all uninstalled packages

# File lib/chef/provider/package.rb, line 562
def candidates_exist_for_all_forced_changes?
  forced_packages_missing_candidates.empty?
end
candidates_exist_for_all_uninstalled?() click to toggle source

Check the list of current_version_array and candidate_version_array. For any of the packages if both versions are missing (uninstalled and no candidate) this will be an unsolvable error.

@return [Boolean] valid candidates exist for all uninstalled packages

# File lib/chef/provider/package.rb, line 538
def candidates_exist_for_all_uninstalled?
  packages_missing_candidates.empty?
end
current_version_array() click to toggle source

@return [Array] current_version(s) as an array

# File lib/chef/provider/package.rb, line 621
def current_version_array
  @current_version_array ||= [ current_resource.version ].flatten
end
each_package() { |package_name, new_version, current_version, candidate_version, magic_version| ... } click to toggle source

Helper to iterate over all the indexed *_array’s in sync

@yield [package_name, new_version, current_version, candidate_version] Description of block

# File lib/chef/provider/package.rb, line 593
def each_package
  package_name_array.each_with_index do |package_name, i|
    candidate_version = candidate_version_array[i]
    current_version = current_version_array[i]
    magic_version = use_magic_version? ? magic_version_array[i] : current_version_array[i]
    new_version = new_version_array[i]
    yield package_name, new_version, current_version, candidate_version, magic_version
  end
end
forced_packages_missing_candidates() click to toggle source

Returns an array of all forced packages which are missing candidate versions

@return [Array] names of packages missing candidates

# File lib/chef/provider/package.rb, line 569
def forced_packages_missing_candidates
  @forced_packages_missing_candidates ||=
    begin
      missing = []
      each_package do |package_name, new_version, current_version, candidate_version, magic_version|
        next if new_version.nil? || current_version.nil?

        if use_magic_version?
          if !magic_version && candidate_version.nil?
            missing.push(package_name)
          end
        else
          if !version_requirement_satisfied?(current_version, new_version) && candidate_version.nil?
            missing.push(package_name)
          end
        end
      end
      missing
    end
end
install_description() click to toggle source
# File lib/chef/provider/package.rb, line 126
def install_description
  description = []
  target_version_array.each_with_index do |target_version, i|
    next if target_version.nil?

    package_name = package_name_array[i]
    description << "install version #{target_version} of package #{package_name}"
  end
  description
end
multipackage?() click to toggle source

@return [Boolean] if we’re doing a multipackage install or not

# File lib/chef/provider/package.rb, line 604
def multipackage?
  @multipackage_bool ||= new_resource.package_name.is_a?(Array)
end
new_version_array() click to toggle source

@return [Array] new_version(s) as an array

# File lib/chef/provider/package.rb, line 626
def new_version_array
  @new_version_array ||= [ new_resource.version ].flatten.map { |v| v.to_s.empty? ? nil : v }
end
package_name_array() click to toggle source

@return [Array] package_name(s) as an array

# File lib/chef/provider/package.rb, line 609
def package_name_array
  @package_name_array ||= [ new_resource.package_name ].flatten
end
package_names_for_targets() click to toggle source

Returns the package names which need to be modified. If the resource was called with an array of packages then this will return an array of packages to update (may have 0 or 1 entries). If the resource was called with a non-array package_name to manage then this will return a string rather than an Array. The output of this is meant to be fed into subclass interfaces to install/upgrade packages and not all of them are Array-aware.

@return [String, Array<String>] package_name(s) to actually update/install

# File lib/chef/provider/package.rb, line 401
def package_names_for_targets
  package_names_for_targets = []
  target_version_array.each_with_index do |target_version, i|
    if !target_version.nil?
      package_name = package_name_array[i]
      package_names_for_targets.push(package_name)
    else
      package_names_for_targets.push(nil) if allow_nils?
    end
  end
  multipackage? ? package_names_for_targets : package_names_for_targets[0]
end
packages_missing_candidates() click to toggle source

Returns array of all packages which are missing candidate versions.

@return [Array<String>] names of packages missing candidates

# File lib/chef/provider/package.rb, line 545
def packages_missing_candidates
  @packages_missing_candidates ||=
    begin
      missing = []
      each_package do |package_name, new_version, current_version, candidate_version, magic_version|
        missing.push(package_name) if magic_version.nil? && candidate_version.nil?
      end
      missing
    end
end
resolved_source_array() click to toggle source

Helper to handle use_package_name_for_source to convert names into local packages to install.

@return [Array] Array of sources with package_names converted to sources

# File lib/chef/provider/package.rb, line 646
def resolved_source_array
  @resolved_source_array ||=
    source_array.each_with_index.map do |source, i|
      package_name = package_name_array[i]
      # we require at least one '/' in the package_name to avoid [XXX_]package 'foo' breaking due to a random 'foo' file in cwd
      if use_package_name_for_source? && source.nil? && package_name.match(/#{::File::SEPARATOR}/) && ::TargetIO::File.exist?(package_name)
        logger.trace("No package source specified, but #{package_name} exists on filesystem, using #{package_name} as source.")
        package_name
      else
        source
      end
    end
end
source_array() click to toggle source

TIP: less error prone to simply always call resolved_source_array, even if you don’t think that you need to.

@return [Array] new_resource.source as an array

# File lib/chef/provider/package.rb, line 634
def source_array
  @source_array ||=
    if new_resource.source.nil?
      package_name_array.map { nil }
    else
      [ new_resource.source ].flatten
    end
end
target_version_array() click to toggle source

Return an array indexed the same as *_version_array which contains either the target version to install/upgrade to or else nil if the package is not being modified.

@return [Array<String,NilClass>] array of package versions which need to be upgraded (nil = not being upgraded)

# File lib/chef/provider/package.rb, line 437
def target_version_array
  @target_version_array ||=
    begin
      target_version_array = []
      each_package do |package_name, new_version, current_version, candidate_version, magic_version|
        case action
        when :upgrade
          if version_equals?(current_version, new_version)
            # This is a short-circuit (mostly for the rubygems provider) to avoid needing to
            # expensively query the candidate_version which must come later.  This only checks
            # exact matching, the check for fuzzy matching is later.
            logger.trace("#{new_resource} #{package_name} #{new_version} is already installed")
            target_version_array.push(nil)
          elsif current_version.nil?
            # This is a simple check to see if we have any currently installed version at all, this is
            # safe to do before the allow_downgrade check so we check this before.
            logger.trace("#{new_resource} has no existing installed version. Installing install #{candidate_version}")
            target_version_array.push(candidate_version)
          elsif !allow_downgrade && version_compare(current_version, candidate_version) == 1
            # This check for downgrading when allow_downgrade is false uses the current_version rather
            # than the magic_version since we never want to downgrade even if the constraints are not met
            # if the version is higher.  This check does use the candidate_version and unlazies this so
            # there will a perf hit on idempotent use when allow_downgrade is false which is unavoidable.
            logger.trace("#{new_resource} #{package_name} has installed version #{current_version}, which is newer than available version #{candidate_version}. Skipping...)")
            target_version_array.push(nil)
          elsif magic_version.nil?
            # This is the check for fuzzy matching of the installed_version, where if the installed version
            # does not match the desired version constraints (but is not an exact match) then we need to
            # install the candidate_version (this must come after the allow_downgrade check)
            logger.trace("#{new_resource} has an installed version that does not match the version constraint. Installing install #{candidate_version}")
            target_version_array.push(candidate_version)
          elsif candidate_version.nil?
            # This check necessarily unlazies the candidate_version and may be expensive (connecting to
            # rubygems.org or whatever).  It comes as late as possible.
            logger.trace("#{new_resource} #{package_name} has no candidate_version to upgrade to")
            target_version_array.push(nil)
          elsif version_equals?(current_version, candidate_version)
            # This check sees if the candidate_version is already installed or if we should upgrade/update the
            # package.  This is the normal idempotent behavior of :upgrade and is inherently expensive due to
            # unlazying the candidate_version.  To prevent the perf hit the version may be specified with a full
            # version constraint.  Then the cookbook can roll the version forward and use :upgrade to force version
            # pinning.
            logger.trace("#{new_resource} #{package_name} #{candidate_version} is already installed")
            target_version_array.push(nil)
          else
            logger.trace("#{new_resource} #{package_name} is out of date, will update to #{candidate_version}")
            target_version_array.push(candidate_version)
          end

        when :install
          if current_version && new_version && !allow_downgrade && version_compare(current_version, new_version) == 1
            # This is the idempotency guard for downgrades when downgrades are not allowed.  This should perhaps raise
            # an exception since we were told to install an exact package version but we are silently refusing to do so
            # because a higher version is already installed.  Maybe we need a flag for users to apply their preferred
            # declarative philosophy?  This has to come early and outside of the two code paths below.
            logger.warn("#{new_resource} #{package_name} has installed version #{current_version}, which is newer than available version #{new_version}. Skipping...)")
            target_version_array.push(nil)
          elsif new_version && !use_magic_version?
            # This is for "non magic version using" subclasses to do comparisons between the current_version and the
            # desired new_version.  XXX: If we converted this to current_version_requirement_satisfied? and made it specific
            # to the current version check and then eliminated the magic_version, we might be able to eliminate separate codepaths
            # here, and eliminate the semantic confusion around the magic_version?
            if version_requirement_satisfied?(current_version, new_version)
              logger.trace("#{new_resource} #{package_name} #{current_version} satisfies #{new_version} requirement")
              target_version_array.push(nil)
            else
              # XXX: some subclasses seem to depend on this behavior where the new_version can be different from the
              # candidate_version and we install the new_version, it seems like the candidate_version should be fixed to
              # be resolved correctly to the new_version for those providers.  although it may just be unit test bugs.
              # it would be more correct to use the candidate_version here, but then it needs to be the correctly resolved
              # candidate_version against the new_version constraint.
              logger.trace("#{new_resource} #{package_name} #{current_version} needs updating to #{new_version}")
              target_version_array.push(new_version)
            end
          elsif magic_version.nil?
            # This is for when we have a "magic version using" subclass and where the installed version does not match the
            # constraint specified in the new_version, so we need to upgrade to the candidate_version.  This is the only
            # codepath in the :install branch which references the candidate_version so it is slow, but it is the path where
            # we need to do work anyway.  XXX: should we check for candidate_version.nil? somewhere around here?
            logger.trace("#{new_resource} #{package_name} not installed, installing #{candidate_version}")
            target_version_array.push(candidate_version)
          else
            logger.trace("#{new_resource} #{package_name} #{current_version} already installed")
            target_version_array.push(nil)
          end

        else
          # in specs please test the public interface provider.run_action(:install) instead of provider.action_install
          raise "internal error - target_version_array in package provider does not understand this action"
        end
      end

      target_version_array
    end
end
upgrade_description() click to toggle source
# File lib/chef/provider/package.rb, line 154
def upgrade_description
  log_allow_downgrade = allow_downgrade ? "(allow_downgrade)" : ""
  description = []
  target_version_array.each_with_index do |target_version, i|
    next if target_version.nil?

    package_name = package_name_array[i]
    candidate_version = candidate_version_array[i]
    current_version = current_version_array[i] || "uninstalled"
    description << "upgrade#{log_allow_downgrade} package #{package_name} from #{current_version} to #{candidate_version}"
  end
  description
end
versions_for_targets() click to toggle source

Returns the package versions which need to be modified. If the resource was called with an array of packages then this will return an array of versions to update (may have 0 or 1 entries). If the resource was called with a non-array package_name to manage then this will return a string rather than an Array. The output of this is meant to be fed into subclass interfaces to install/upgrade packages and not all of them are Array-aware.

@return [String, Array<String>] package version(s) to actually update/install

# File lib/chef/provider/package.rb, line 421
def versions_for_targets
  versions_for_targets = []
  target_version_array.each_with_index do |target_version, i|
    if !target_version.nil?
      versions_for_targets.push(target_version)
    else
      versions_for_targets.push(nil) if allow_nils?
    end
  end
  multipackage? ? versions_for_targets : versions_for_targets[0]
end