diff options
author | Lamont Granquist <lamont@scriptkiddie.org> | 2015-11-23 19:59:02 -0800 |
---|---|---|
committer | Lamont Granquist <lamont@scriptkiddie.org> | 2015-12-02 13:41:33 -0800 |
commit | 602e09a72c6617ee01ca3febb519d3fd36bd2b8c (patch) | |
tree | 376938e5ee04b27b41f4e62ff511644455bfd82e /lib/chef/provider/package | |
parent | de1f684f415faa54599c6b3abbe211d64a319aa6 (diff) | |
download | chef-602e09a72c6617ee01ca3febb519d3fd36bd2b8c.tar.gz |
dpkg multipackage and bonus fixes
- multipackages dpkg_package
- fixes edge conditions in load-current-resource around purging/removing
packages that threw errors before
- fixes the ability to purge packages that have been removed
- adds a lot of functional tests for dpkg_package
Diffstat (limited to 'lib/chef/provider/package')
-rw-r--r-- | lib/chef/provider/package/dpkg.rb | 179 |
1 files changed, 140 insertions, 39 deletions
diff --git a/lib/chef/provider/package/dpkg.rb b/lib/chef/provider/package/dpkg.rb index 2de6226bb9..35b6f4beee 100644 --- a/lib/chef/provider/package/dpkg.rb +++ b/lib/chef/provider/package/dpkg.rb @@ -19,31 +19,40 @@ require 'chef/provider/package' require 'chef/mixin/command' require 'chef/resource/package' -require 'chef/mixin/get_source_from_package' class Chef class Provider class Package class Dpkg < Chef::Provider::Package + DPKG_REMOVED = /^Status: deinstall ok config-files/ DPKG_INSTALLED = /^Status: install ok installed/ DPKG_VERSION = /^Version: (.+)$/ provides :dpkg_package, os: "linux" - include Chef::Mixin::GetSourceFromPackage + use_multipackage_api + use_package_name_for_source + + # semantics of dpkg properties: + # + # new_resource.name is always an array for this resource + # new_resource.package_name is always an array for this resource + # new_resource.source is always an array and may be [ nil ] for this resource. properly use #sources or + # #name_sources to also get the automatic package-name-to-source-conversion. this will never be nil? + # def define_resource_requirements super requirements.assert(:install, :upgrade) do |a| - a.assertion { !new_resource.source.nil? } + a.assertion { !sources.compact.empty? } a.failure_message Chef::Exceptions::Package, "#{new_resource} the source property is required for action :install or :upgrade" end requirements.assert(:install, :upgrade) do |a| - a.assertion { source_file_exist? } - a.failure_message Chef::Exceptions::Package, "#{new_resource} source file does not exist: #{new_resource.source}" - a.whyrun "Assuming it would have been previously created." + a.assertion { source_files_exist? } + a.failure_message Chef::Exceptions::Package, "#{new_resource} source file(s) do not exist: #{missing_sources}" + a.whyrun "Assuming they would have been previously created." end end @@ -51,11 +60,11 @@ class Chef @current_resource = Chef::Resource::Package.new(new_resource.name) current_resource.package_name(new_resource.package_name) - if source_file_exist? + if source_files_exist? @candidate_version = get_candidate_version current_resource.package_name(get_package_name) # if the source file exists then our package_name is right - current_resource.version(get_current_version) + current_resource.version(get_current_version_from(current_package_name_array)) elsif !installing? # we can't do this if we're installing with no source, because our package_name # is probably not right. @@ -65,31 +74,26 @@ class Chef # # we don't error here on the dpkg command since we'll handle the exception or # the why-run message in define_resource_requirements. - current_resource.version(get_current_version) + current_resource.version(get_current_version_from(current_package_name_array)) end current_resource end def install_package(name, version) - Chef::Log.info("#{new_resource} installing #{new_resource.source}") - run_noninteractive( - "dpkg -i#{expand_options(new_resource.options)} #{new_resource.source}" - ) + sources = name.map { |n| name_sources[n] } + Chef::Log.info("#{new_resource} installing package(s): #{name.join(' ')}") + run_noninteractive("dpkg -i", new_resource.options, *sources) end def remove_package(name, version) - Chef::Log.info("#{new_resource} removing #{new_resource.package_name}") - run_noninteractive( - "dpkg -r#{expand_options(new_resource.options)} #{new_resource.package_name}" - ) + Chef::Log.info("#{new_resource} removing package(s): #{name.join(' ')}") + run_noninteractive("dpkg -r", new_resource.options, *name) end def purge_package(name, version) - Chef::Log.info("#{new_resource} purging #{new_resource.package_name}") - run_noninteractive( - "dpkg -P#{expand_options(new_resource.options)} #{new_resource.package_name}" - ) + Chef::Log.info("#{new_resource} purging packages(s): #{name.join(' ')}") + run_noninteractive("dpkg -P", new_resource.options, *name) end def upgrade_package(name, version) @@ -98,22 +102,29 @@ class Chef def preseed_package(preseed_file) Chef::Log.info("#{new_resource} pre-seeding package installation instructions") - run_noninteractive("debconf-set-selections #{preseed_file}") + run_noninteractive("debconf-set-selections", *preseed_file) end def reconfig_package(name, version) Chef::Log.info("#{new_resource} reconfiguring") - run_noninteractive("dpkg-reconfigure #{name}") + run_noninteractive("dpkg-reconfigure", *name) + end + + # Override the superclass check. Multiple sources are required here. + def check_resource_semantics! end private - def get_current_version - Chef::Log.debug("#{new_resource} checking install state") - status = shell_out_with_timeout("dpkg -s #{current_resource.package_name}") + def read_current_version_of_package(package_name) + Chef::Log.debug("#{new_resource} checking install state of #{package_name}") + status = shell_out_with_timeout("dpkg -s #{package_name}") package_installed = false status.stdout.each_line do |line| case line + when DPKG_REMOVED + # if we are 'purging' then we consider 'removed' to be 'installed' + package_installed = true if action == :purge when DPKG_INSTALLED package_installed = true when DPKG_VERSION @@ -126,34 +137,124 @@ class Chef return nil end + def get_current_version_from(array) + array.map do |name| + read_current_version_of_package(name) + end + end + # Runs command via shell_out_with_timeout with magic environment to disable - # interactive prompts. Command is run with default localization rather - # than forcing locale to "C", so command output may not be stable. - def run_noninteractive(command) - shell_out_with_timeout!(command, :env => { "DEBIAN_FRONTEND" => "noninteractive" }) + # interactive prompts. + def run_noninteractive(*command) + shell_out_with_timeout!(a_to_s(*command), :env => { "DEBIAN_FRONTEND" => "noninteractive" }) + end + + # Returns true if all sources exist. Returns false if any do not, or if no + # sources were specified. + # + # @return [Boolean] True if all sources exist + def source_files_exist? + sources.all? {|s| s && ::File.exist?(s) } end - def source_file_exist? - new_resource.source && ::File.exist?(new_resource.source) + # Helper to return all the nanes of the missing sources for error messages. + # + # @return [Array<String>] Array of missing sources + def missing_sources + sources.select {|s| s.nil? || !::File.exist?(s) } + end + + def current_package_name_array + [ current_resource.package_name ].flatten + end + + def source_array + if new_resource.source.nil? + package_name_array.map { nil } + else + [ new_resource.source ].flatten + end + end + + # Helper to construct Array of sources. If the new_resource.source is nil it + # will return an array filled will nil the same size as the package_name array + # For all the nil source values, if a file exists on the filesystem that + # matches the package name it will use that name as the source. + # + # @return [Array] Array of normalized sources with package_names converted to sources + def sources + @sources ||= + begin + 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 dpkg_package 'foo' breaking due to a random 'foo' file in cwd + if use_package_name_for_source? && source.nil? && package_name.match(/#{::File::SEPARATOR}/) && ::File.exist?(package_name) + Chef::Log.debug("No package source specified, but #{package_name} exists on filesystem, using #{package_name} as source.") + package_name + else + source + end + end + end + end + + # Helper to construct Hash of names-to-sources. + # + # @return [Hash] Mapping of package names to sources + def name_sources + @name_sources = + begin + Hash[*package_name_array.zip(sources).flatten] + end + end + + # Helper to construct Hash of names-to-package-information. + # + # @return [Hash] Mapping of package names to package information + def name_pkginfo + @name_pkginfo ||= + begin + pkginfos = sources.map do |src| + Chef::Log.debug("#{new_resource} checking #{src} dpkg status") + status = shell_out_with_timeout!("dpkg-deb -W #{src}") + status.stdout + end + Hash[*package_name_array.zip(pkginfos).flatten] + end + end + + def name_candidate_version + @name_candidate_version ||= + begin + Hash[name_pkginfo.map {|k, v| [k, v ? v.split("\t")[1].strip : nil] }] + end end - def pkginfo - @pkginfo ||= + def name_package_name + @name_package_name ||= begin - Chef::Log.debug("#{new_resource} checking dpkg status") - status = shell_out_with_timeout!("dpkg-deb -W #{new_resource.source}") - status.stdout.split("\t") + Hash[name_pkginfo.map {|k, v| [k, v ? v.split("\t")[0] : nil] }] end end + # Return candidate version array from pkg-deb -W against the source file(s). + # + # @return [Array] Array of candidate versions read from the source files def get_candidate_version - pkginfo[1].strip unless pkginfo.empty? + package_name_array.map { |name| name_candidate_version[name] } end + # Return package names from the candidate source file(s). + # + # @return [Array] Array of actual package names read from the source files def get_package_name - pkginfo[0] unless pkginfo.empty? + package_name_array.map { |name| name_package_name[name] } end + # Since upgrade just calls install, this is a helper to determine + # if our action means that we'll be calling install_package. + # + # @return [Boolean] true if we're doing :install or :upgrade def installing? [:install, :upgrade].include?(action) end |