diff options
author | Nick McSpadden <nmcspadden@gmail.com> | 2018-05-14 23:33:45 -0700 |
---|---|---|
committer | Nick McSpadden <nmcspadden@gmail.com> | 2018-05-14 23:33:45 -0700 |
commit | c935ce85f591e53f2f200e55cb252185e1801387 (patch) | |
tree | dfeee6151c50d805138a3d10761a531c2ffbf984 /lib/chef/provider | |
parent | c48fd22a51deb74a3d4bc3abca308043689b494a (diff) | |
parent | c0609e449135fae43d436136a4f0fd3889a9b8f1 (diff) | |
download | chef-c935ce85f591e53f2f200e55cb252185e1801387.tar.gz |
Merge branch 'master' into mac_uid
Diffstat (limited to 'lib/chef/provider')
155 files changed, 9035 insertions, 5761 deletions
diff --git a/lib/chef/provider/apt_preference.rb b/lib/chef/provider/apt_preference.rb new file mode 100644 index 0000000000..416a1c0d1d --- /dev/null +++ b/lib/chef/provider/apt_preference.rb @@ -0,0 +1,94 @@ +# +# Author:: Tim Smith (<tsmith@chef.io>) +# Copyright:: 2016-2017, 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" +require "chef/dsl/declare_resource" +require "chef/provider/noop" +require "chef/log" + +class Chef + class Provider + class AptPreference < Chef::Provider + provides :apt_preference, platform_family: "debian" + + APT_PREFERENCE_DIR = "/etc/apt/preferences.d".freeze + + def load_current_resource + end + + action :add do + preference = build_pref( + new_resource.glob || new_resource.package_name, + new_resource.pin, + new_resource.pin_priority + ) + + declare_resource(:directory, APT_PREFERENCE_DIR) do + mode "0755" + action :create + end + + sanitized_prefname = safe_name(new_resource.package_name) + + # cleanup any existing pref files w/o the sanitized name (created by old apt cookbook) + if (sanitized_prefname != new_resource.package_name) && ::File.exist?("#{APT_PREFERENCE_DIR}/#{new_resource.package_name}.pref") + logger.warn "Replacing legacy #{new_resource.package_name}.pref with #{sanitized_prefname}.pref in #{APT_PREFERENCE_DIR}" + declare_resource(:file, "#{APT_PREFERENCE_DIR}/#{new_resource.package_name}.pref") do + action :delete + end + end + + # cleanup any existing pref files without the .pref extension (created by old apt cookbook) + if ::File.exist?("#{APT_PREFERENCE_DIR}/#{new_resource.package_name}") + logger.warn "Replacing legacy #{new_resource.package_name} with #{sanitized_prefname}.pref in #{APT_PREFERENCE_DIR}" + declare_resource(:file, "#{APT_PREFERENCE_DIR}/#{new_resource.package_name}") do + action :delete + end + end + + declare_resource(:file, "#{APT_PREFERENCE_DIR}/#{sanitized_prefname}.pref") do + mode "0644" + content preference + action :create + end + end + + action :remove do + sanitized_prefname = safe_name(new_resource.package_name) + + if ::File.exist?("#{APT_PREFERENCE_DIR}/#{sanitized_prefname}.pref") + logger.info "Un-pinning #{sanitized_prefname} from #{APT_PREFERENCE_DIR}" + declare_resource(:file, "#{APT_PREFERENCE_DIR}/#{sanitized_prefname}.pref") do + action :delete + end + end + end + + # Build preferences.d file contents + def build_pref(package_name, pin, pin_priority) + "Package: #{package_name}\nPin: #{pin}\nPin-Priority: #{pin_priority}\n" + end + + def safe_name(name) + name.tr(".", "_").gsub("*", "wildcard") + end + end + end +end + +Chef::Provider::Noop.provides :apt_preference diff --git a/lib/chef/provider/apt_repository.rb b/lib/chef/provider/apt_repository.rb index 1e7db80620..973c10e94a 100644 --- a/lib/chef/provider/apt_repository.rb +++ b/lib/chef/provider/apt_repository.rb @@ -1,6 +1,6 @@ # # Author:: Thom May (<thom@chef.io>) -# Copyright:: Copyright (c) 2016 Chef Software, Inc. +# Copyright:: Copyright (c) 2016-2017, Chef Software Inc. # License:: Apache License, Version 2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -25,27 +25,25 @@ require "chef/provider/noop" class Chef class Provider class AptRepository < Chef::Provider - use_inline_resources - include Chef::Mixin::ShellOut - provides :apt_repository do - uses_apt? - end + provides :apt_repository, platform_family: "debian" - def whyrun_supported? - true - end + LIST_APT_KEY_FINGERPRINTS = "apt-key adv --list-public-keys --with-fingerprint --with-colons".freeze def load_current_resource end action :add do - unless new_resource.key.nil? - if is_key_id?(new_resource.key) && !has_cookbook_file?(new_resource.key) - install_key_from_keyserver - else - install_key_from_uri + if new_resource.key.nil? + logger.debug "No 'key' property specified skipping key import" + else + new_resource.key.each do |k| + if is_key_id?(k) && !has_cookbook_file?(k) + install_key_from_keyserver(k) + else + install_key_from_uri(k) + end end end @@ -59,22 +57,18 @@ class Chef action :nothing end - components = if is_ppa_url?(new_resource.uri) && new_resource.components.empty? - "main" - else - new_resource.components - end + cleanup_legacy_file! repo = build_repo( new_resource.uri, new_resource.distribution, - components, + repo_components, new_resource.trusted, new_resource.arch, new_resource.deb_src ) - declare_resource(:file, "/etc/apt/sources.list.d/#{new_resource.name}.list") do + declare_resource(:file, "/etc/apt/sources.list.d/#{new_resource.repo_name}.list") do owner "root" group "root" mode "0644" @@ -87,9 +81,10 @@ class Chef end action :remove do - if ::File.exist?("/etc/apt/sources.list.d/#{new_resource.name}.list") - converge_by "Removing #{new_resource.name} repository from /etc/apt/sources.list.d/" do - declare_resource(:file, "/etc/apt/sources.list.d/#{new_resource.name}.list") do + cleanup_legacy_file! + if ::File.exist?("/etc/apt/sources.list.d/#{new_resource.repo_name}.list") + converge_by "Removing #{new_resource.repo_name} repository from /etc/apt/sources.list.d/" do + declare_resource(:file, "/etc/apt/sources.list.d/#{new_resource.repo_name}.list") do sensitive new_resource.sensitive action :delete notifies :update, "apt_update[#{new_resource.name}]", :immediately if new_resource.cache_rebuild @@ -99,94 +94,133 @@ class Chef ignore_failure true action :nothing end - end + else + logger.trace("/etc/apt/sources.list.d/#{new_resource.repo_name}.list does not exist. Nothing to do") end end - def self.uses_apt? - ENV["PATH"] ||= "" - paths = %w{ /bin /usr/bin /sbin /usr/sbin } + ENV["PATH"].split(::File::PATH_SEPARATOR) - paths.any? { |path| ::File.executable?(::File.join(path, "apt-get")) } - end - + # is the provided ID a key ID from a keyserver. Looks at length and HEX only values + # @param [String] id the key value passed by the user that *may* be an ID def is_key_id?(id) id = id[2..-1] if id.start_with?("0x") id =~ /^\h+$/ && [8, 16, 40].include?(id.length) end + # run the specified command and extract the fingerprints from the output + # accepts a command so it can be used to extract both the current key's fingerprints + # and the fingerprint of the new key + # @param [String] cmd the command to run + # + # @return [Array] an array of fingerprints def extract_fingerprints_from_cmd(cmd) so = shell_out(cmd) - so.run_command so.stdout.split(/\n/).map do |t| - if z = t.match(/^ +Key fingerprint = ([0-9A-F ]+)/) + if z = t.match(/^fpr:+([0-9A-F]+):/) z[1].split.join end end.compact end - def key_is_valid?(cmd, key) + # validate the key against the apt keystore to see if that version is expired + # @param [String] key + # + # @return [Boolean] is the key valid or not + def key_is_valid?(key) valid = true - so = shell_out(cmd) - so.run_command + so = shell_out("apt-key list") so.stdout.split(/\n/).map do |t| if t =~ %r{^\/#{key}.*\[expired: .*\]$} - Chef::Log.debug "Found expired key: #{t}" + logger.debug "Found expired key: #{t}" valid = false break end end - Chef::Log.debug "key #{key} #{valid ? "is valid" : "is not valid"}" + logger.debug "key #{key} #{valid ? "is valid" : "is not valid"}" valid end + # return the specified cookbook name or the cookbook containing the + # resource. + # + # @return [String] name of the cookbook def cookbook_name new_resource.cookbook || new_resource.cookbook_name end + # determine if a cookbook file is available in the run + # @param [String] fn the path to the cookbook file + # + # @return [Boolean] cookbook file exists or doesn't def has_cookbook_file?(fn) run_context.has_cookbook_file_in_cookbook?(cookbook_name, fn) end + # determine if there are any new keys by comparing the fingerprints of installed + # keys to those of the passed file + # @param [String] file the keyfile of the new repository + # + # @return [Boolean] true: no new keys in the file. false: there are new keys def no_new_keys?(file) - installed_keys = extract_fingerprints_from_cmd("apt-key finger") - proposed_keys = extract_fingerprints_from_cmd("gpg --with-fingerprint #{file}") + # Now we are using the option --with-colons that works across old os versions + # as well as the latest (16.10). This for both `apt-key` and `gpg` commands + installed_keys = extract_fingerprints_from_cmd(LIST_APT_KEY_FINGERPRINTS) + proposed_keys = extract_fingerprints_from_cmd("gpg --with-fingerprint --with-colons #{file}") (installed_keys & proposed_keys).sort == proposed_keys.sort end - def install_key_from_uri - key_name = new_resource.key.split(%r{\/}).last + # Given the provided key URI determine what kind of chef resource we need + # to fetch the key + # @param [String] uri the uri of the gpg key (local path or http URL) + # + # @raise [Chef::Exceptions::FileNotFound] Key isn't remote or found in the current run + # + # @return [Symbol] :remote_file or :cookbook_file + def key_type(uri) + if uri.start_with?("http") + :remote_file + elsif has_cookbook_file?(uri) + :cookbook_file + else + raise Chef::Exceptions::FileNotFound, "Cannot locate key file: #{uri}" + end + end + + # Fetch the key using either cookbook_file or remote_file, validate it, + # and install it with apt-key add + # @param [String] key the key to install + # + # @raise [RuntimeError] Invalid key which can't verify the apt repository + # + # @return [void] + def install_key_from_uri(key) + key_name = key.gsub(/[^0-9A-Za-z\-]/, "_") cached_keyfile = ::File.join(Chef::Config[:file_cache_path], key_name) - type = if new_resource.key.start_with?("http") - :remote_file - elsif has_cookbook_file?(new_resource.key) - :cookbook_file - else - raise Chef::Exceptions::FileNotFound, "Cannot locate key file" - end - declare_resource(type, cached_keyfile) do - source new_resource.key + declare_resource(key_type(key), cached_keyfile) do + source key mode "0644" sensitive new_resource.sensitive action :create + verify "gpg %{path}" end - raise "The key #{cached_keyfile} is invalid and cannot be used to verify an apt repository." unless key_is_valid?("gpg #{cached_keyfile}", "") - declare_resource(:execute, "apt-key add #{cached_keyfile}") do sensitive new_resource.sensitive action :run - not_if do - no_new_keys?(cached_keyfile) - end + not_if { no_new_keys?(cached_keyfile) } notifies :run, "execute[apt-cache gencaches]", :immediately end end - def install_key_from_keyserver(key = new_resource.key, keyserver = new_resource.keyserver) + # build the apt-key command to install the keyserver + # @param [String] key the key to install + # @param [String] keyserver the key server to use + # + # @return [String] the full apt-key command to run + def keyserver_install_cmd(key, keyserver) cmd = "apt-key adv --recv" cmd << " --keyserver-options http-proxy=#{new_resource.key_proxy}" if new_resource.key_proxy cmd << " --keyserver " @@ -197,22 +231,37 @@ class Chef end cmd << " #{key}" + cmd + end + # @param [String] key + # @param [String] keyserver + # + # @raise [RuntimeError] Invalid key which can't verify the apt repository + # + # @return [void] + def install_key_from_keyserver(key, keyserver = new_resource.keyserver) declare_resource(:execute, "install-key #{key}") do - command cmd + command keyserver_install_cmd(key, keyserver) sensitive new_resource.sensitive not_if do - present = extract_fingerprints_from_cmd("apt-key finger").any? do |fp| + present = extract_fingerprints_from_cmd(LIST_APT_KEY_FINGERPRINTS).any? do |fp| fp.end_with? key.upcase end - present && key_is_valid?("apt-key list", key.upcase) + present && key_is_valid?(key.upcase) end notifies :run, "execute[apt-cache gencaches]", :immediately end - raise "The key #{key} is invalid and cannot be used to verify an apt repository." unless key_is_valid?("apt-key list", key.upcase) + raise "The key #{key} is invalid and cannot be used to verify an apt repository." unless key_is_valid?(key.upcase) end + # @param [String] owner + # @param [String] repo + # + # @raise [RuntimeError] Could not access the Launchpad PPA API + # + # @return [void] def install_ppa_key(owner, repo) url = "https://launchpad.net/api/1.0/~#{owner}/+archive/#{repo}" key_id = Chef::HTTP::Simple.new(url).get("signing_key_fingerprint").delete('"') @@ -221,12 +270,33 @@ class Chef raise "Could not access Launchpad ppa API: #{e.message}" end + # determine if the repository URL is a PPA + # @param [String] url the url of the repository + # + # @return [Boolean] is the repo URL a PPA def is_ppa_url?(url) url.start_with?("ppa:") end + # determine the repository's components: + # - "components" property if defined + # - "main" if "components" not defined and the repo is a PPA URL + # - otherwise nothing + # + # @return [String] the repository component + def repo_components + if is_ppa_url?(new_resource.uri) && new_resource.components.empty? + "main" + else + new_resource.components + end + end + + # given a PPA return a PPA URL in http://ppa.launchpad.net format + # @param [String] ppa the ppa URL + # + # @return [String] full PPA URL def make_ppa_url(ppa) - return unless is_ppa_url?(ppa) owner, repo = ppa[4..-1].split("/") repo ||= "ppa" @@ -234,6 +304,14 @@ class Chef "http://ppa.launchpad.net/#{owner}/#{repo}/ubuntu" end + # build complete repo text that will be written to the config + # @param [String] uri + # @param [Array] components + # @param [Boolean] trusted + # @param [String] arch + # @param [Boolean] add_src + # + # @return [String] complete repo config text def build_repo(uri, distribution, components, trusted, arch, add_src = false) uri = make_ppa_url(uri) if is_ppa_url?(uri) @@ -250,8 +328,25 @@ class Chef repo << "deb-src #{info}\n" if add_src repo end + + # clean up a potentially legacy file from before we fixed the usage of + # new_resource.name vs. new_resource.repo_name. We might have the + # name.list file hanging around and need to clean it up. + # + # @return [void] + def cleanup_legacy_file! + legacy_path = "/etc/apt/sources.list.d/#{new_resource.name}.list" + if new_resource.name != new_resource.repo_name && ::File.exist?(legacy_path) + converge_by "Cleaning up legacy #{legacy_path} repo file" do + declare_resource(:file, legacy_path) do + action :delete + # Not triggering an update since it isn't super likely to be needed. + end + end + end + end end end end -Chef::Provider::Noop.provides :apt_resource +Chef::Provider::Noop.provides :apt_repository diff --git a/lib/chef/provider/apt_update.rb b/lib/chef/provider/apt_update.rb index d2dd5cfb14..9d794abcf0 100644 --- a/lib/chef/provider/apt_update.rb +++ b/lib/chef/provider/apt_update.rb @@ -1,6 +1,6 @@ # # Author:: Thom May (<thom@chef.io>) -# Copyright:: Copyright (c) 2016 Chef Software, Inc. +# Copyright:: Copyright (c) 2016-2017, Chef Software Inc. # License:: Apache License, Version 2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -16,23 +16,18 @@ # limitations under the License. # -require "chef/resource" +require "chef/provider" +require "chef/provider/noop" require "chef/dsl/declare_resource" class Chef class Provider class AptUpdate < Chef::Provider - use_inline_resources - - provides :apt_update, os: "linux" + provides :apt_update, platform_family: "debian" APT_CONF_DIR = "/etc/apt/apt.conf.d" STAMP_DIR = "/var/lib/apt/periodic" - def whyrun_supported? - true - end - def load_current_resource end @@ -68,7 +63,7 @@ class Chef end declare_resource(:file, "#{APT_CONF_DIR}/15update-stamp") do - content "APT::Update::Post-Invoke-Success {\"touch #{STAMP_DIR}/update-success-stamp 2>/dev/null || true\";};" + content "APT::Update::Post-Invoke-Success {\"touch #{STAMP_DIR}/update-success-stamp 2>/dev/null || true\";};\n" action :create_if_missing end @@ -78,3 +73,5 @@ class Chef end end end + +Chef::Provider::Noop.provides :apt_update diff --git a/lib/chef/provider/batch.rb b/lib/chef/provider/batch.rb index bb294afd3f..dae1513a51 100644 --- a/lib/chef/provider/batch.rb +++ b/lib/chef/provider/batch.rb @@ -22,14 +22,14 @@ class Chef class Provider class Batch < Chef::Provider::WindowsScript - provides :batch, os: "windows" + provides :batch def initialize(new_resource, run_context) super(new_resource, run_context, ".bat") end def command - basepath = is_forced_32bit ? wow64_directory : run_context.node.kernel.os_info.system_directory + basepath = is_forced_32bit ? wow64_directory : run_context.node["kernel"]["os_info"]["system_directory"] interpreter_path = Chef::Util::PathHelper.join(basepath, interpreter) @@ -37,7 +37,7 @@ class Chef end def flags - @new_resource.flags.nil? ? "/c" : new_resource.flags + " /c" + new_resource.flags.nil? ? "/c" : new_resource.flags + " /c" end end diff --git a/lib/chef/provider/breakpoint.rb b/lib/chef/provider/breakpoint.rb deleted file mode 100644 index a71c9e317d..0000000000 --- a/lib/chef/provider/breakpoint.rb +++ /dev/null @@ -1,38 +0,0 @@ -# -# Author:: Daniel DeLeo (<dan@kallistec.com>) -# 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. -# - -class Chef - class Provider - class Breakpoint < Chef::Provider - - provides :breakpoint - - def load_current_resource - end - - def action_break - if defined?(Shell) && Shell.running? - run_context.resource_collection.iterator.pause - @new_resource.updated_by_last_action(true) - run_context.resource_collection.iterator - end - end - - end - end -end diff --git a/lib/chef/provider/cookbook_file.rb b/lib/chef/provider/cookbook_file.rb index 3ca0bbd47a..92383fd5fa 100644 --- a/lib/chef/provider/cookbook_file.rb +++ b/lib/chef/provider/cookbook_file.rb @@ -17,8 +17,6 @@ # require "chef/provider/file" -require "chef/deprecation/provider/cookbook_file" -require "chef/deprecation/warnings" class Chef class Provider @@ -26,25 +24,21 @@ class Chef provides :cookbook_file - extend Chef::Deprecation::Warnings - include Chef::Deprecation::Provider::CookbookFile - add_deprecation_warnings_for(Chef::Deprecation::Provider::CookbookFile.instance_methods) - def initialize(new_resource, run_context) @content_class = Chef::Provider::CookbookFile::Content super end def load_current_resource - @current_resource = Chef::Resource::CookbookFile.new(@new_resource.name) + @current_resource = Chef::Resource::CookbookFile.new(new_resource.name) super end private def managing_content? - return true if @new_resource.checksum - return true if !@new_resource.source.nil? && @action != :create_if_missing + return true if new_resource.checksum + return true if !new_resource.source.nil? && @action != :create_if_missing false end diff --git a/lib/chef/provider/cookbook_file/content.rb b/lib/chef/provider/cookbook_file/content.rb index 1d24dee3e7..82e2cf4390 100644 --- a/lib/chef/provider/cookbook_file/content.rb +++ b/lib/chef/provider/cookbook_file/content.rb @@ -34,7 +34,7 @@ class Chef else tempfile = Chef::FileContentManagement::Tempfile.new(@new_resource).tempfile tempfile.close - Chef::Log.debug("#{@new_resource} staging #{file_cache_location} to #{tempfile.path}") + logger.trace("#{@new_resource} staging #{file_cache_location} to #{tempfile.path}") FileUtils.cp(file_cache_location, tempfile.path) tempfile end diff --git a/lib/chef/provider/cron.rb b/lib/chef/provider/cron.rb index 36b67ab6a5..70edd89636 100644 --- a/lib/chef/provider/cron.rb +++ b/lib/chef/provider/cron.rb @@ -17,13 +17,11 @@ # require "chef/log" -require "chef/mixin/command" require "chef/provider" class Chef class Provider class Cron < Chef::Provider - include Chef::Mixin::Command provides :cron, os: ["!aix", "!solaris2"] @@ -42,21 +40,17 @@ class Chef end attr_accessor :cron_exists, :cron_empty - def whyrun_supported? - true - end - def load_current_resource crontab_lines = [] - @current_resource = Chef::Resource::Cron.new(@new_resource.name) - @current_resource.user(@new_resource.user) + @current_resource = Chef::Resource::Cron.new(new_resource.name) + current_resource.user(new_resource.user) @cron_exists = false if crontab = read_crontab cron_found = false crontab.each_line do |line| case line.chomp - when "# Chef Name: #{@new_resource.name}" - Chef::Log.debug("Found cron '#{@new_resource.name}'") + when "# Chef Name: #{new_resource.name}" + logger.trace("Found cron '#{new_resource.name}'") cron_found = true @cron_exists = true next @@ -65,18 +59,18 @@ class Chef next when SPECIAL_PATTERN if cron_found - @current_resource.time($2.to_sym) - @current_resource.command($3) + current_resource.time($2.to_sym) + current_resource.command($3) cron_found = false end when CRON_PATTERN if cron_found - @current_resource.minute($1) - @current_resource.hour($2) - @current_resource.day($3) - @current_resource.month($4) - @current_resource.weekday($5) - @current_resource.command($6) + current_resource.minute($1) + current_resource.hour($2) + current_resource.day($3) + current_resource.month($4) + current_resource.weekday($5) + current_resource.command($6) cron_found = false end next @@ -85,36 +79,36 @@ class Chef next end end - Chef::Log.debug("Cron '#{@new_resource.name}' not found") unless @cron_exists + logger.trace("Cron '#{new_resource.name}' not found") unless @cron_exists else - Chef::Log.debug("Cron empty for '#{@new_resource.user}'") + logger.trace("Cron empty for '#{new_resource.user}'") @cron_empty = true end - @current_resource + current_resource end def cron_different? CRON_ATTRIBUTES.any? do |cron_var| - @new_resource.send(cron_var) != @current_resource.send(cron_var) + new_resource.send(cron_var) != current_resource.send(cron_var) end end def action_create - crontab = String.new - newcron = String.new + crontab = "" + newcron = "" cron_found = false newcron = get_crontab_entry if @cron_exists unless cron_different? - Chef::Log.debug("Skipping existing cron entry '#{@new_resource.name}'") + logger.trace("Skipping existing cron entry '#{new_resource.name}'") return end read_crontab.each_line do |line| case line.chomp - when "# Chef Name: #{@new_resource.name}" + when "# Chef Name: #{new_resource.name}" cron_found = true next when ENV_PATTERN @@ -144,29 +138,29 @@ class Chef # Handle edge case where the Chef comment is the last line in the current crontab crontab << newcron if cron_found - converge_by("update crontab entry for #{@new_resource}") do + converge_by("update crontab entry for #{new_resource}") do write_crontab crontab - Chef::Log.info("#{@new_resource} updated crontab entry") + logger.info("#{new_resource} updated crontab entry") end else crontab = read_crontab unless @cron_empty crontab << newcron - converge_by("add crontab entry for #{@new_resource}") do + converge_by("add crontab entry for #{new_resource}") do write_crontab crontab - Chef::Log.info("#{@new_resource} added crontab entry") + logger.info("#{new_resource} added crontab entry") end end end def action_delete if @cron_exists - crontab = String.new + crontab = "" cron_found = false read_crontab.each_line do |line| case line.chomp - when "# Chef Name: #{@new_resource.name}" + when "# Chef Name: #{new_resource.name}" cron_found = true next when ENV_PATTERN @@ -187,11 +181,10 @@ class Chef end crontab << line end - description = cron_found ? "remove #{@new_resource.name} from crontab" : - "save unmodified crontab" + description = cron_found ? "remove #{new_resource.name} from crontab" : "save unmodified crontab" converge_by(description) do write_crontab crontab - Chef::Log.info("#{@new_resource} deleted crontab entry") + logger.info("#{new_resource} deleted crontab entry") end end end @@ -200,60 +193,48 @@ class Chef def set_environment_var(attr_name, attr_value) if %w{MAILTO PATH SHELL HOME}.include?(attr_name) - @current_resource.send(attr_name.downcase.to_sym, attr_value) + current_resource.send(attr_name.downcase.to_sym, attr_value.gsub(/^"|"$/, "")) else - @current_resource.environment(@current_resource.environment.merge(attr_name => attr_value)) + current_resource.environment(current_resource.environment.merge(attr_name => attr_value)) end end def read_crontab - crontab = nil - status = popen4("crontab -l -u #{@new_resource.user}") do |pid, stdin, stdout, stderr| - crontab = stdout.read - end - if status.exitstatus > 1 - raise Chef::Exceptions::Cron, "Error determining state of #{@new_resource.name}, exit: #{status.exitstatus}" - end - crontab + so = shell_out!("crontab -l -u #{new_resource.user}", returns: [0, 1]) + return nil if so.exitstatus == 1 + so.stdout + rescue => e + raise Chef::Exceptions::Cron, "Error determining state of #{new_resource.name}, error: #{e}" end def write_crontab(crontab) write_exception = false - status = popen4("crontab -u #{@new_resource.user} -", :waitlast => true) do |pid, stdin, stdout, stderr| - begin - stdin.write crontab - rescue Errno::EPIPE => e - # popen4 could yield while child has already died. - write_exception = true - Chef::Log.debug("#{e.message}") - end - end - if status.exitstatus > 0 || write_exception - raise Chef::Exceptions::Cron, "Error updating state of #{@new_resource.name}, exit: #{status.exitstatus}" - end + so = shell_out!("crontab -u #{new_resource.user} -", input: crontab) + rescue => e + raise Chef::Exceptions::Cron, "Error updating state of #{new_resource.name}, error: #{e}" end def get_crontab_entry newcron = "" newcron << "# Chef Name: #{new_resource.name}\n" [ :mailto, :path, :shell, :home ].each do |v| - newcron << "#{v.to_s.upcase}=#{@new_resource.send(v)}\n" if @new_resource.send(v) + newcron << "#{v.to_s.upcase}=\"#{new_resource.send(v)}\"\n" if new_resource.send(v) end - @new_resource.environment.each do |name, value| + new_resource.environment.each do |name, value| newcron << "#{name}=#{value}\n" end - if @new_resource.time - newcron << "@#{@new_resource.time} #{@new_resource.command}\n" + if new_resource.time + newcron << "@#{new_resource.time} #{new_resource.command}\n" else - newcron << "#{@new_resource.minute} #{@new_resource.hour} #{@new_resource.day} #{@new_resource.month} #{@new_resource.weekday} #{@new_resource.command}\n" + newcron << "#{new_resource.minute} #{new_resource.hour} #{new_resource.day} #{new_resource.month} #{new_resource.weekday} #{new_resource.command}\n" end newcron end def weekday_in_crontab - weekday_in_crontab = WEEKDAY_SYMBOLS.index(@new_resource.weekday) + weekday_in_crontab = WEEKDAY_SYMBOLS.index(new_resource.weekday) if weekday_in_crontab.nil? - @new_resource.weekday + new_resource.weekday else weekday_in_crontab.to_s end diff --git a/lib/chef/provider/cron/unix.rb b/lib/chef/provider/cron/unix.rb index 108e73c9d3..15195dbb88 100644 --- a/lib/chef/provider/cron/unix.rb +++ b/lib/chef/provider/cron/unix.rb @@ -36,7 +36,7 @@ class Chef crontab = shell_out("/usr/bin/crontab -l", :user => @new_resource.user) status = crontab.status.exitstatus - Chef::Log.debug crontab.format_for_exception if status > 0 + logger.trace crontab.format_for_exception if status > 0 if status > 1 raise Chef::Exceptions::Cron, "Error determining state of #{@new_resource.name}, exit: #{status}" @@ -62,12 +62,12 @@ class Chef exit_status = 1 end rescue Chef::Exceptions::Exec => e - Chef::Log.debug(e.message) + logger.trace(e.message) exit_status = 1 error_message = e.message rescue ArgumentError => e # usually raised on invalid user. - Chef::Log.debug(e.message) + logger.trace(e.message) exit_status = 1 error_message = e.message end diff --git a/lib/chef/provider/deploy.rb b/lib/chef/provider/deploy.rb deleted file mode 100644 index 4c758033ac..0000000000 --- a/lib/chef/provider/deploy.rb +++ /dev/null @@ -1,476 +0,0 @@ -# -# Author:: Daniel DeLeo (<dan@kallistec.com>) -# 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/mixin/command" -require "chef/mixin/from_file" -require "chef/provider/git" -require "chef/provider/subversion" -require "chef/dsl/recipe" -require "chef/util/path_helper" - -class Chef - class Provider - class Deploy < Chef::Provider - - include Chef::DSL::Recipe - include Chef::Mixin::FromFile - include Chef::Mixin::Command - - attr_reader :scm_provider, :release_path, :shared_path, :previous_release_path - - def initialize(new_resource, run_context) - super(new_resource, run_context) - - # will resolve to either git or svn based on resource attributes, - # and will create a resource corresponding to that provider - @scm_provider = new_resource.scm_provider.new(new_resource, run_context) - - # @configuration is not used by Deploy, it is only for backwards compat with - # chef-deploy or capistrano hooks that might use it to get environment information - @configuration = @new_resource.to_hash - @configuration[:environment] = @configuration[:environment] && @configuration[:environment]["RAILS_ENV"] - end - - def whyrun_supported? - true - end - - def load_current_resource - @scm_provider.load_current_resource - @release_path = @new_resource.deploy_to + "/releases/#{release_slug}" - @shared_path = @new_resource.shared_path - end - - def sudo(command, &block) - execute(command, &block) - end - - def run(command, &block) - exec = execute(command, &block) - exec.user(@new_resource.user) if @new_resource.user - exec.group(@new_resource.group) if @new_resource.group - exec.cwd(release_path) unless exec.cwd - exec.environment(@new_resource.environment) unless exec.environment - converge_by("execute #{command}") do - exec - end - end - - def define_resource_requirements - requirements.assert(:rollback) do |a| - a.assertion { all_releases[-2] } - a.failure_message(RuntimeError, "There is no release to rollback to!") - #There is no reason to assume 2 deployments in a single chef run, hence fails in whyrun. - end - - [ @new_resource.before_migrate, @new_resource.before_symlink, - @new_resource.before_restart, @new_resource.after_restart ].each do |script| - requirements.assert(:deploy, :force_deploy) do |a| - callback_file = "#{release_path}/#{script}" - a.assertion do - if script && script.class == String - ::File.exist?(callback_file) - else - true - end - end - a.failure_message(RuntimeError, "Can't find your callback file #{callback_file}") - a.whyrun("Would assume callback file #{callback_file} included in release") - end - end - end - - def action_deploy - save_release_state - if deployed?(release_path ) - if current_release?(release_path ) - Chef::Log.debug("#{@new_resource} is the latest version") - else - rollback_to release_path - end - else - - with_rollback_on_error do - deploy - end - end - end - - def action_force_deploy - if deployed?(release_path) - converge_by("delete deployed app at #{release_path} prior to force-deploy") do - Chef::Log.info("Already deployed app at #{release_path}, forcing.") - FileUtils.rm_rf(release_path) - Chef::Log.info("#{@new_resource} forcing deploy of already deployed app at #{release_path}") - end - end - - # Alternatives: - # * Move release_path directory before deploy and move it back when error occurs - # * Rollback to previous commit - # * Do nothing - because deploy is force, it will be retried in short time - # Because last is simplest, keep it - deploy - end - - def action_rollback - rollback_to all_releases[-2] - end - - def rollback_to(target_release_path) - @release_path = target_release_path - - rp_index = all_releases.index(release_path) - releases_to_nuke = all_releases[(rp_index + 1)..-1] - - rollback - - releases_to_nuke.each do |i| - converge_by("roll back by removing release #{i}") do - Chef::Log.info "#{@new_resource} removing release: #{i}" - FileUtils.rm_rf i - end - release_deleted(i) - end - end - - def deploy - verify_directories_exist - update_cached_repo # no converge-by - scm provider will dothis - enforce_ownership - copy_cached_repo - install_gems - enforce_ownership - callback(:before_migrate, @new_resource.before_migrate) - migrate - callback(:before_symlink, @new_resource.before_symlink) - symlink - callback(:before_restart, @new_resource.before_restart) - restart - callback(:after_restart, @new_resource.after_restart) - cleanup! - Chef::Log.info "#{@new_resource} deployed to #{@new_resource.deploy_to}" - end - - def rollback - Chef::Log.info "#{@new_resource} rolling back to previous release #{release_path}" - symlink - Chef::Log.info "#{@new_resource} restarting with previous release" - restart - end - - def callback(what, callback_code = nil) - @collection = Chef::ResourceCollection.new - case callback_code - when Proc - Chef::Log.info "#{@new_resource} running callback #{what}" - recipe_eval(&callback_code) - when String - run_callback_from_file("#{release_path}/#{callback_code}") - when nil - run_callback_from_file("#{release_path}/deploy/#{what}.rb") - end - end - - def migrate - run_symlinks_before_migrate - - if @new_resource.migrate - enforce_ownership - - environment = @new_resource.environment - env_info = environment && environment.map do |key_and_val| - "#{key_and_val.first}='#{key_and_val.last}'" - end.join(" ") - - converge_by("execute migration command #{@new_resource.migration_command}") do - Chef::Log.info "#{@new_resource} migrating #{@new_resource.user} with environment #{env_info}" - shell_out!(@new_resource.migration_command, run_options(:cwd => release_path, :log_level => :info)) - end - end - end - - def symlink - purge_tempfiles_from_current_release - link_tempfiles_to_current_release - link_current_release_to_production - Chef::Log.info "#{@new_resource} updated symlinks" - end - - def restart - if restart_cmd = @new_resource.restart_command - if restart_cmd.kind_of?(Proc) - Chef::Log.info("#{@new_resource} restarting app with embedded recipe") - recipe_eval(&restart_cmd) - else - converge_by("restart app using command #{@new_resource.restart_command}") do - Chef::Log.info("#{@new_resource} restarting app") - shell_out!(@new_resource.restart_command, run_options(:cwd => @new_resource.current_path)) - end - end - end - end - - def cleanup! - converge_by("update release history data") do - release_created(release_path) - end - - chop = -1 - @new_resource.keep_releases - all_releases[0..chop].each do |old_release| - converge_by("remove old release #{old_release}") do - Chef::Log.info "#{@new_resource} removing old release #{old_release}" - FileUtils.rm_rf(old_release) - end - release_deleted(old_release) - end - end - - def all_releases - Dir.glob(Chef::Util::PathHelper.escape_glob_dir(@new_resource.deploy_to) + "/releases/*").sort - end - - def update_cached_repo - if @new_resource.svn_force_export - # TODO assertion, non-recoverable - @scm_provider must be svn if force_export? - svn_force_export - else - run_scm_sync - end - end - - def run_scm_sync - @scm_provider.run_action(:sync) - end - - def svn_force_export - Chef::Log.info "#{@new_resource} exporting source repository" - @scm_provider.run_action(:force_export) - end - - def copy_cached_repo - target_dir_path = @new_resource.deploy_to + "/releases" - converge_by("deploy from repo to #{target_dir_path} ") do - FileUtils.rm_rf(release_path) if ::File.exist?(release_path) - FileUtils.mkdir_p(target_dir_path) - FileUtils.cp_r(::File.join(@new_resource.destination, "."), release_path, :preserve => true) - Chef::Log.info "#{@new_resource} copied the cached checkout to #{release_path}" - end - end - - def enforce_ownership - converge_by("force ownership of #{@new_resource.deploy_to} to #{@new_resource.group}:#{@new_resource.user}") do - FileUtils.chown_R(@new_resource.user, @new_resource.group, @new_resource.deploy_to, :force => true) - Chef::Log.info("#{@new_resource} set user to #{@new_resource.user}") if @new_resource.user - Chef::Log.info("#{@new_resource} set group to #{@new_resource.group}") if @new_resource.group - end - end - - def verify_directories_exist - create_dir_unless_exists(@new_resource.deploy_to) - create_dir_unless_exists(@new_resource.shared_path) - end - - def link_current_release_to_production - converge_by(["remove existing link at #{@new_resource.current_path}", - "link release #{release_path} into production at #{@new_resource.current_path}"]) do - FileUtils.rm_f(@new_resource.current_path) - begin - FileUtils.ln_sf(release_path, @new_resource.current_path) - rescue => e - raise Chef::Exceptions::FileNotFound.new("Cannot symlink current release to production: #{e.message}") - end - Chef::Log.info "#{@new_resource} linked release #{release_path} into production at #{@new_resource.current_path}" - end - enforce_ownership - end - - def run_symlinks_before_migrate - links_info = @new_resource.symlink_before_migrate.map { |src, dst| "#{src} => #{dst}" }.join(", ") - converge_by("make pre-migration symlinks: #{links_info}") do - @new_resource.symlink_before_migrate.each do |src, dest| - begin - FileUtils.ln_sf(@new_resource.shared_path + "/#{src}", release_path + "/#{dest}") - rescue => e - raise Chef::Exceptions::FileNotFound.new("Cannot symlink #{@new_resource.shared_path}/#{src} to #{release_path}/#{dest} before migrate: #{e.message}") - end - end - Chef::Log.info "#{@new_resource} made pre-migration symlinks" - end - end - - def link_tempfiles_to_current_release - dirs_info = @new_resource.create_dirs_before_symlink.join(",") - @new_resource.create_dirs_before_symlink.each do |dir| - create_dir_unless_exists(release_path + "/#{dir}") - end - Chef::Log.info("#{@new_resource} created directories before symlinking: #{dirs_info}") - - links_info = @new_resource.symlinks.map { |src, dst| "#{src} => #{dst}" }.join(", ") - converge_by("link shared paths into current release: #{links_info}") do - @new_resource.symlinks.each do |src, dest| - begin - FileUtils.ln_sf(::File.join(@new_resource.shared_path, src), ::File.join(release_path, dest)) - rescue => e - raise Chef::Exceptions::FileNotFound.new("Cannot symlink shared data #{::File.join(@new_resource.shared_path, src)} to #{::File.join(release_path, dest)}: #{e.message}") - end - end - Chef::Log.info("#{@new_resource} linked shared paths into current release: #{links_info}") - end - run_symlinks_before_migrate - enforce_ownership - end - - def create_dirs_before_symlink - end - - def purge_tempfiles_from_current_release - log_info = @new_resource.purge_before_symlink.join(", ") - converge_by("purge directories in checkout #{log_info}") do - @new_resource.purge_before_symlink.each { |dir| FileUtils.rm_rf(release_path + "/#{dir}") } - Chef::Log.info("#{@new_resource} purged directories in checkout #{log_info}") - end - end - - protected - - # Internal callback, called after copy_cached_repo. - # Override if you need to keep state externally. - # Note that YOU are responsible for implementing whyrun-friendly behavior - # in any actions you take in this callback. - def release_created(release_path) - end - - # Note that YOU are responsible for using appropriate whyrun nomenclature - # Override if you need to keep state externally. - # Note that YOU are responsible for implementing whyrun-friendly behavior - # in any actions you take in this callback. - def release_deleted(release_path) - end - - def release_slug - raise Chef::Exceptions::Override, "You must override release_slug in #{self}" - end - - def install_gems - gem_resource_collection_runner.converge - end - - def gem_resource_collection_runner - child_context = run_context.create_child - gem_packages.each { |rbgem| child_context.resource_collection.insert(rbgem) } - Chef::Runner.new(child_context) - end - - def gem_packages - return [] unless ::File.exist?("#{release_path}/gems.yml") - gems = YAML.load(IO.read("#{release_path}/gems.yml")) - - gems.map do |g| - r = Chef::Resource::GemPackage.new(g[:name], run_context) - r.version g[:version] - r.action :install - r.source "http://gems.github.com" - r - end - end - - def run_options(run_opts = {}) - run_opts[:user] = @new_resource.user if @new_resource.user - run_opts[:group] = @new_resource.group if @new_resource.group - run_opts[:environment] = @new_resource.environment if @new_resource.environment - run_opts[:log_tag] = @new_resource.to_s - run_opts[:log_level] ||= :debug - if run_opts[:log_level] == :info - if STDOUT.tty? && !Chef::Config[:daemon] && Chef::Log.info? - run_opts[:live_stream] = STDOUT - end - end - run_opts - end - - def run_callback_from_file(callback_file) - Chef::Log.info "#{@new_resource} queueing checkdeploy hook #{callback_file}" - recipe_eval do - Dir.chdir(release_path) do - from_file(callback_file) if ::File.exist?(callback_file) - end - end - end - - def create_dir_unless_exists(dir) - if ::File.directory?(dir) - Chef::Log.debug "#{@new_resource} not creating #{dir} because it already exists" - return false - end - converge_by("create new directory #{dir}") do - begin - FileUtils.mkdir_p(dir) - Chef::Log.debug "#{@new_resource} created directory #{dir}" - if @new_resource.user - FileUtils.chown(@new_resource.user, nil, dir) - Chef::Log.debug("#{@new_resource} set user to #{@new_resource.user} for #{dir}") - end - if @new_resource.group - FileUtils.chown(nil, @new_resource.group, dir) - Chef::Log.debug("#{@new_resource} set group to #{@new_resource.group} for #{dir}") - end - rescue => e - raise Chef::Exceptions::FileNotFound.new("Cannot create directory #{dir}: #{e.message}") - end - end - end - - def with_rollback_on_error - yield - rescue ::Exception => e - if @new_resource.rollback_on_error - Chef::Log.warn "Error on deploying #{release_path}: #{e.message}" - failed_release = release_path - - if previous_release_path - @release_path = previous_release_path - rollback - end - converge_by("remove failed deploy #{failed_release}") do - Chef::Log.info "Removing failed deploy #{failed_release}" - FileUtils.rm_rf failed_release - end - release_deleted(failed_release) - end - - raise - end - - def save_release_state - if ::File.exists?(@new_resource.current_path) - release = ::File.readlink(@new_resource.current_path) - @previous_release_path = release if ::File.exists?(release) - end - end - - def deployed?(release) - all_releases.include?(release) - end - - def current_release?(release) - @previous_release_path == release - end - end - end -end diff --git a/lib/chef/provider/deploy/revision.rb b/lib/chef/provider/deploy/revision.rb deleted file mode 100644 index f61e439486..0000000000 --- a/lib/chef/provider/deploy/revision.rb +++ /dev/null @@ -1,109 +0,0 @@ -# -# Author:: Daniel DeLeo (<dan@kallistec.com>) -# Author:: Tim Hinderliter (<tim@chef.io>) -# Author:: Seth Falcon (<seth@chef.io>) -# Copyright:: Copyright 2009-2016, Daniel DeLeo -# Copyright:: Copyright 2010-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" -require "chef/provider/deploy" -require "chef/json_compat" - -class Chef - class Provider - class Deploy - class Revision < Chef::Provider::Deploy - provides :deploy_revision - provides :deploy_branch - - def all_releases - sorted_releases - end - - def action_deploy - validate_release_history! - super - end - - def cleanup! - super - - known_releases = sorted_releases - - Dir["#{Chef::Util::PathHelper.escape_glob_dir(new_resource.deploy_to)}/releases/*"].each do |release_dir| - unless known_releases.include?(release_dir) - converge_by("Remove unknown release in #{release_dir}") do - FileUtils.rm_rf(release_dir) - end - end - end - end - - protected - - def release_created(release) - sorted_releases { |r| r.delete(release); r << release } - end - - def release_deleted(release) - sorted_releases { |r| r.delete(release) } - end - - def release_slug - scm_provider.revision_slug - end - - private - - def sorted_releases - cache = load_cache - if block_given? - yield cache - save_cache(cache) - end - cache - end - - def validate_release_history! - sorted_releases do |release_list| - release_list.each do |path| - release_list.delete(path) unless ::File.exist?(path) - end - end - end - - def sorted_releases_from_filesystem - Dir.glob(Chef::Util::PathHelper.escape_glob_dir(new_resource.deploy_to) + "/releases/*").sort_by { |d| ::File.ctime(d) } - end - - def load_cache - begin - Chef::JSONCompat.parse(Chef::FileCache.load("revision-deploys/#{new_resource.name}")) - rescue Chef::Exceptions::FileNotFound - sorted_releases_from_filesystem - end - end - - def save_cache(cache) - Chef::FileCache.store("revision-deploys/#{new_resource.name}", Chef::JSONCompat.to_json(cache)) - cache - end - - end - end - end -end diff --git a/lib/chef/provider/deploy/timestamped.rb b/lib/chef/provider/deploy/timestamped.rb deleted file mode 100644 index 5486b092d3..0000000000 --- a/lib/chef/provider/deploy/timestamped.rb +++ /dev/null @@ -1,34 +0,0 @@ -# -# Author:: Daniel DeLeo (<dan@kallistec.com>) -# Copyright:: Copyright 2009-2016, Daniel DeLeo -# 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. -# - -class Chef - class Provider - class Deploy - class Timestamped < Chef::Provider::Deploy - provides :timestamped_deploy - provides :deploy - - protected - - def release_slug - Time.now.utc.strftime("%Y%m%d%H%M%S") - end - end - end - end -end diff --git a/lib/chef/provider/directory.rb b/lib/chef/provider/directory.rb index 7cc05259b6..3e816d5a06 100644 --- a/lib/chef/provider/directory.rb +++ b/lib/chef/provider/directory.rb @@ -1,6 +1,6 @@ # # Author:: Adam Jacob (<adam@chef.io>) -# Copyright:: Copyright 2008-2016, Chef Software Inc. +# Copyright:: Copyright 2008-2017, Chef Software Inc. # License:: Apache License, Version 2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -29,17 +29,13 @@ class Chef provides :directory - def whyrun_supported? - true - end - def load_current_resource - @current_resource = Chef::Resource::Directory.new(@new_resource.name) - @current_resource.path(@new_resource.path) - if ::File.exists?(@current_resource.path) && @action != :create_if_missing - load_resource_attributes_from_file(@current_resource) + @current_resource = Chef::Resource::Directory.new(new_resource.name) + current_resource.path(new_resource.path) + if ::File.exists?(current_resource.path) && @action != :create_if_missing + load_resource_attributes_from_file(current_resource) end - @current_resource + current_resource end def define_resource_requirements @@ -49,9 +45,9 @@ class Chef requirements.assert(:create) do |a| # Make sure the parent dir exists, or else fail. # for why run, print a message explaining the potential error. - parent_directory = ::File.dirname(@new_resource.path) + parent_directory = ::File.dirname(new_resource.path) a.assertion do - if @new_resource.recursive + if new_resource.recursive does_parent_exist = lambda do |base_dir| base_dir = ::File.dirname(base_dir) if ::File.exist?(base_dir) @@ -60,20 +56,20 @@ class Chef does_parent_exist.call(base_dir) end end - does_parent_exist.call(@new_resource.path) + does_parent_exist.call(new_resource.path) else ::File.directory?(parent_directory) end end - a.failure_message(Chef::Exceptions::EnclosingDirectoryDoesNotExist, "Parent directory #{parent_directory} does not exist, cannot create #{@new_resource.path}") + a.failure_message(Chef::Exceptions::EnclosingDirectoryDoesNotExist, "Parent directory #{parent_directory} does not exist, cannot create #{new_resource.path}") a.whyrun("Assuming directory #{parent_directory} would have been created") end requirements.assert(:create) do |a| - parent_directory = ::File.dirname(@new_resource.path) + parent_directory = ::File.dirname(new_resource.path) a.assertion do - if @new_resource.recursive - # find the lowest-level directory in @new_resource.path that already exists + if new_resource.recursive + # find the lowest-level directory in new_resource.path that already exists # make sure we have write permissions to that directory is_parent_writable = lambda do |base_dir| base_dir = ::File.dirname(base_dir) @@ -89,7 +85,7 @@ class Chef is_parent_writable.call(base_dir) end end - is_parent_writable.call(@new_resource.path) + is_parent_writable.call(new_resource.path) else # in why run mode & parent directory does not exist no permissions check is required # If not in why run, permissions must be valid and we rely on prior assertion that dir exists @@ -97,7 +93,7 @@ class Chef if Chef::FileAccessControl.writable?(parent_directory) true elsif Chef::Util::PathHelper.is_sip_path?(parent_directory, node) - Chef::Util::PathHelper.writable_sip_path?(@new_resource.path) + Chef::Util::PathHelper.writable_sip_path?(new_resource.path) else false end @@ -107,18 +103,18 @@ class Chef end end a.failure_message(Chef::Exceptions::InsufficientPermissions, - "Cannot create #{@new_resource} at #{@new_resource.path} due to insufficient permissions") + "Cannot create #{new_resource} at #{new_resource.path} due to insufficient permissions") end requirements.assert(:delete) do |a| a.assertion do - if ::File.exists?(@new_resource.path) - ::File.directory?(@new_resource.path) && Chef::FileAccessControl.writable?(@new_resource.path) + if ::File.exists?(new_resource.path) + ::File.directory?(new_resource.path) && Chef::FileAccessControl.writable?(new_resource.path) else true end end - a.failure_message(RuntimeError, "Cannot delete #{@new_resource} at #{@new_resource.path}!") + a.failure_message(RuntimeError, "Cannot delete #{new_resource} at #{new_resource.path}!") # No why-run handling here: # * if we don't have permissions, this is unlikely to be changed earlier in the run # * if the target is a file (not a dir), there's no reasonable path by which this would have been changed @@ -126,30 +122,32 @@ class Chef end def action_create - unless ::File.exists?(@new_resource.path) - converge_by("create new directory #{@new_resource.path}") do - if @new_resource.recursive == true - ::FileUtils.mkdir_p(@new_resource.path) + unless ::File.exists?(new_resource.path) + converge_by("create new directory #{new_resource.path}") do + if new_resource.recursive == true + ::FileUtils.mkdir_p(new_resource.path) else - ::Dir.mkdir(@new_resource.path) + ::Dir.mkdir(new_resource.path) end - Chef::Log.info("#{@new_resource} created directory #{@new_resource.path}") + logger.info("#{new_resource} created directory #{new_resource.path}") end end do_acl_changes do_selinux(true) - load_resource_attributes_from_file(@new_resource) + load_resource_attributes_from_file(new_resource) unless Chef::Config[:why_run] end def action_delete - if ::File.exists?(@new_resource.path) - converge_by("delete existing directory #{@new_resource.path}") do - if @new_resource.recursive == true - FileUtils.rm_rf(@new_resource.path) - Chef::Log.info("#{@new_resource} deleted #{@new_resource.path} recursively") + if ::File.exists?(new_resource.path) + converge_by("delete existing directory #{new_resource.path}") do + if new_resource.recursive == true + # we don't use rm_rf here because it masks all errors, including + # IO errors or permission errors that would prvent the deletion + FileUtils.rm_r(new_resource.path) + logger.info("#{new_resource} deleted #{new_resource.path} recursively") else - ::Dir.delete(@new_resource.path) - Chef::Log.info("#{@new_resource} deleted #{@new_resource.path}") + ::Dir.delete(new_resource.path) + logger.info("#{new_resource} deleted #{new_resource.path}") end end end diff --git a/lib/chef/provider/dsc_resource.rb b/lib/chef/provider/dsc_resource.rb index 0f25065925..9c147cb634 100644 --- a/lib/chef/provider/dsc_resource.rb +++ b/lib/chef/provider/dsc_resource.rb @@ -1,7 +1,7 @@ # # Author:: Adam Edwards (<adamed@chef.io>) # -# Copyright:: Copyright 2014-2016, Chef Software, Inc. +# Copyright:: Copyright 2014-2017, Chef Software Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -24,11 +24,12 @@ class Chef class Provider class DscResource < Chef::Provider include Chef::Mixin::PowershellTypeCoercions - provides :dsc_resource, os: "windows" + provides :dsc_resource def initialize(new_resource, run_context) super @new_resource = new_resource @module_name = new_resource.module_name + @module_version = new_resource.module_version @reboot_resource = nil end @@ -44,10 +45,6 @@ class Chef def load_current_resource end - def whyrun_supported? - true - end - def define_resource_requirements requirements.assert(:run) do |a| a.assertion { supports_dsc_invoke_resource? } @@ -65,6 +62,13 @@ class Chef a.whyrun err + ["Assuming a previous resource sets the RefreshMode."] a.block_action! end + requirements.assert(:run) do |a| + a.assertion { module_usage_valid? } + err = ["module_name must be supplied along with module_version."] + a.failure_message Chef::Exceptions::DSCModuleNameMissing, + err + a.block_action! + end end protected @@ -92,6 +96,10 @@ class Chef Chef::Platform.supports_refresh_mode_enabled?(node) end + def module_usage_valid? + !(!@module_name && @module_version) + end + def generate_description @converge_description end @@ -148,10 +156,14 @@ class Chef end end + def module_info_object + @module_version.nil? ? module_name : "@{ModuleName='#{module_name}';ModuleVersion='#{@module_version}'}" + end + def invoke_resource(method, output_format = :object) - properties = translate_type(@new_resource.properties) - switches = "-Method #{method} -Name #{@new_resource.resource}"\ - " -Property #{properties} -Module #{module_name} -Verbose" + properties = translate_type(new_resource.properties) + switches = "-Method #{method} -Name #{new_resource.resource}"\ + " -Property #{properties} -Module #{module_info_object} -Verbose" cmdlet = Chef::Util::Powershell::Cmdlet.new( node, "Invoke-DscResource #{switches}", @@ -172,22 +184,22 @@ class Chef def create_reboot_resource @reboot_resource = Chef::Resource::Reboot.new( - "Reboot for #{@new_resource.name}", + "Reboot for #{new_resource.name}", run_context ).tap do |r| - r.reason("Reboot for #{@new_resource.resource}.") + r.reason("Reboot for #{new_resource.resource}.") end end def reboot_if_required - reboot_action = @new_resource.reboot_action + reboot_action = new_resource.reboot_action unless @reboot_resource.nil? case reboot_action when :nothing - Chef::Log.debug("A reboot was requested by the DSC resource, but reboot_action is :nothing.") - Chef::Log.debug("This dsc_resource will not reboot the node.") + logger.trace("A reboot was requested by the DSC resource, but reboot_action is :nothing.") + logger.trace("This dsc_resource will not reboot the node.") else - Chef::Log.debug("Requesting node reboot with #{reboot_action}.") + logger.trace("Requesting node reboot with #{reboot_action}.") @reboot_resource.run_action(reboot_action) end end diff --git a/lib/chef/provider/dsc_script.rb b/lib/chef/provider/dsc_script.rb index 79769d9773..7a101fa68b 100644 --- a/lib/chef/provider/dsc_script.rb +++ b/lib/chef/provider/dsc_script.rb @@ -1,7 +1,7 @@ # # Author:: Adam Edwards (<adamed@chef.io>) # -# Copyright:: Copyright 2014-2016, Chef Software, Inc. +# Copyright:: Copyright 2014-2017, Chef Software Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -25,26 +25,26 @@ class Chef class Provider class DscScript < Chef::Provider - provides :dsc_script, os: "windows" + provides :dsc_script def initialize(dsc_resource, run_context) super(dsc_resource, run_context) @dsc_resource = dsc_resource @resource_converged = false @operations = { - :set => Proc.new { |config_manager, document, shellout_flags| + :set => Proc.new do |config_manager, document, shellout_flags| config_manager.set_configuration(document, shellout_flags) - }, - :test => Proc.new { |config_manager, document, shellout_flags| + end, + :test => Proc.new do |config_manager, document, shellout_flags| config_manager.test_configuration(document, shellout_flags) - } } + end } end def action_run if ! @resource_converged converge_by(generate_description) do run_configuration(:set) - Chef::Log.info("DSC resource configuration completed successfully") + logger.info("DSC resource configuration completed successfully") end end end @@ -58,10 +58,6 @@ class Chef end end - def whyrun_supported? - true - end - def define_resource_requirements requirements.assert(:run) do |a| err = [ @@ -99,7 +95,7 @@ class Chef configuration_document = generate_configuration_document(config_directory, configuration_flags) @operations[operation].call(config_manager, configuration_document, shellout_flags) rescue Exception => e - Chef::Log.error("DSC operation failed: #{e.message}") + logger.error("DSC operation failed: #{e.message}") raise e ensure ::FileUtils.rm_rf(config_directory) @@ -128,7 +124,7 @@ class Chef generator.configuration_document_from_script_path(@dsc_resource.command, configuration_name, configuration_flags, shellout_flags) else # If code is also not provided, we mimic what the other script resources do (execute nothing) - Chef::Log.warn("Neither code or command were provided for dsc_resource[#{@dsc_resource.name}].") unless @dsc_resource.code + logger.warn("Neither code or command were provided for dsc_resource[#{@dsc_resource.name}].") unless @dsc_resource.code generator.configuration_document_from_script_code(@dsc_resource.code || "", configuration_flags, @dsc_resource.imports, shellout_flags) end end @@ -165,7 +161,11 @@ class Chef if resource.changes_state? # We ignore the last log message because it only contains the time it took, which looks weird cleaned_messages = resource.change_log[0..-2].map { |c| c.sub(/^#{Regexp.escape(resource.name)}/, "").strip } - "converge DSC resource #{resource.name} by #{cleaned_messages.find_all { |c| c != '' }.join("\n")}" + unless cleaned_messages.empty? + "converge DSC resource #{resource.name} by #{cleaned_messages.find_all { |c| c != '' }.join("\n")}" + else + "converge DSC resource #{resource.name}" + end else # This is needed because a dsc script can have resources that are both converged and not "converge DSC resource #{resource.name} by doing nothing because it is already converged" diff --git a/lib/chef/provider/env.rb b/lib/chef/provider/env.rb deleted file mode 100644 index 5b252dd344..0000000000 --- a/lib/chef/provider/env.rb +++ /dev/null @@ -1,169 +0,0 @@ -# -# Author:: Doug MacEachern (<dougm@vmware.com>) -# Copyright:: Copyright 2010-2016, VMware, 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" -require "chef/mixin/command" -require "chef/resource/env" - -class Chef - class Provider - class Env < Chef::Provider - include Chef::Mixin::Command - attr_accessor :key_exists - - provides :env, os: "!windows" - - def initialize(new_resource, run_context) - super - @key_exists = true - end - - def load_current_resource - @current_resource = Chef::Resource::Env.new(@new_resource.name) - @current_resource.key_name(@new_resource.key_name) - - if env_key_exists(@new_resource.key_name) - @current_resource.value(env_value(@new_resource.key_name)) - else - @key_exists = false - Chef::Log.debug("#{@new_resource} key does not exist") - end - - @current_resource - end - - def env_value(key_name) - raise Chef::Exceptions::Env, "#{self} provider does not implement env_value!" - end - - def env_key_exists(key_name) - env_value(key_name) ? true : false - end - - # Check to see if value needs any changes - # - # ==== Returns - # <true>:: If a change is required - # <false>:: If a change is not required - def requires_modify_or_create? - if @new_resource.delim - #e.g. check for existing value within PATH - new_values.inject(0) do |index, val| - next_index = current_values.find_index val - return true if next_index.nil? || next_index < index - next_index - end - false - else - @new_resource.value != @current_resource.value - end - end - - alias_method :compare_value, :requires_modify_or_create? - - def action_create - if @key_exists - if requires_modify_or_create? - modify_env - Chef::Log.info("#{@new_resource} altered") - @new_resource.updated_by_last_action(true) - end - else - create_env - Chef::Log.info("#{@new_resource} created") - @new_resource.updated_by_last_action(true) - end - end - - #e.g. delete a PATH element - # - # ==== Returns - # <true>:: If we handled the element case and caller should not delete the key - # <false>:: Caller should delete the key, either no :delim was specific or value was empty - # after we removed the element. - def delete_element - return false unless @new_resource.delim #no delim: delete the key - needs_delete = new_values.any? { |v| current_values.include?(v) } - if !needs_delete - Chef::Log.debug("#{@new_resource} element '#{@new_resource.value}' does not exist") - return true #do not delete the key - else - new_value = - current_values.select do |item| - not new_values.include?(item) - end.join(@new_resource.delim) - - if new_value.empty? - return false #nothing left here, delete the key - else - old_value = @new_resource.value(new_value) - create_env - Chef::Log.debug("#{@new_resource} deleted #{old_value} element") - @new_resource.updated_by_last_action(true) - return true #we removed the element and updated; do not delete the key - end - end - end - - def action_delete - if @key_exists && !delete_element - delete_env - Chef::Log.info("#{@new_resource} deleted") - @new_resource.updated_by_last_action(true) - end - end - - def action_modify - if @key_exists - if requires_modify_or_create? - modify_env - Chef::Log.info("#{@new_resource} modified") - @new_resource.updated_by_last_action(true) - end - else - raise Chef::Exceptions::Env, "Cannot modify #{@new_resource} - key does not exist!" - end - end - - def create_env - raise Chef::Exceptions::UnsupportedAction, "#{self} does not support :#{@new_resource.action}" - end - - def delete_env - raise Chef::Exceptions::UnsupportedAction, "#{self} does not support :delete" - end - - def modify_env - if @new_resource.delim - @new_resource.value((new_values + current_values).uniq.join(@new_resource.delim)) - end - create_env - end - - # Returns the current values to split by delimiter - def current_values - @current_values ||= @current_resource.value.split(@new_resource.delim) - end - - # Returns the new values to split by delimiter - def new_values - @new_values ||= @new_resource.value.split(@new_resource.delim) - end - end - end -end diff --git a/lib/chef/provider/env/windows.rb b/lib/chef/provider/env/windows.rb deleted file mode 100644 index a68c8276e0..0000000000 --- a/lib/chef/provider/env/windows.rb +++ /dev/null @@ -1,72 +0,0 @@ -# -# Author:: Doug MacEachern (<dougm@vmware.com>) -# Copyright:: Copyright 2010-2016, VMware, 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/mixin/windows_env_helper" - -class Chef - class Provider - class Env - class Windows < Chef::Provider::Env - include Chef::Mixin::WindowsEnvHelper - - provides :env, os: "windows" - - def create_env - obj = env_obj(@new_resource.key_name) - unless obj - obj = WIN32OLE.connect("winmgmts://").get("Win32_Environment").spawninstance_ - obj.name = @new_resource.key_name - obj.username = "<System>" - end - obj.variablevalue = @new_resource.value - obj.put_ - value = @new_resource.value - value = expand_path(value) if @new_resource.key_name.casecmp("PATH").zero? - ENV[@new_resource.key_name] = value - broadcast_env_change - end - - def delete_env - obj = env_obj(@new_resource.key_name) - if obj - obj.delete_ - broadcast_env_change - end - if ENV[@new_resource.key_name] - ENV.delete(@new_resource.key_name) - end - end - - def env_value(key_name) - obj = env_obj(key_name) - return obj ? obj.variablevalue : ENV[key_name] - end - - def env_obj(key_name) - wmi = WmiLite::Wmi.new - # Note that by design this query is case insensitive with regard to key_name - environment_variables = wmi.query("select * from Win32_Environment where name = '#{key_name}'") - if environment_variables && environment_variables.length > 0 - environment_variables[0].wmi_ole_object - end - end - - end - end - end -end diff --git a/lib/chef/provider/erl_call.rb b/lib/chef/provider/erl_call.rb deleted file mode 100644 index 7167f3b8a5..0000000000 --- a/lib/chef/provider/erl_call.rb +++ /dev/null @@ -1,108 +0,0 @@ -# -# Author:: Joe Williams (<joe@joetify.com>) -# Copyright:: Copyright 2009-2016, Joe Williams -# 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/log" -require "chef/mixin/command" -require "chef/provider" - -class Chef - class Provider - class ErlCall < Chef::Provider - include Chef::Mixin::Command - - provides :erl_call - - def initialize(node, new_resource) - super(node, new_resource) - end - - def whyrun_supported? - true - end - - def load_current_resource - true - end - - def action_run - case @new_resource.name_type - when "sname" - node = "-sname #{@new_resource.node_name}" - when "name" - node = "-name #{@new_resource.node_name}" - end - - if @new_resource.cookie - cookie = "-c #{@new_resource.cookie}" - else - cookie = "" - end - - if @new_resource.distributed - distributed = "-s" - else - distributed = "" - end - - command = "erl_call -e #{distributed} #{node} #{cookie}" - - converge_by("run erlang block") do - begin - pid, stdin, stdout, stderr = popen4(command, :waitlast => true) - - Chef::Log.debug("#{@new_resource} running") - Chef::Log.debug("#{@new_resource} command: #{command}") - Chef::Log.debug("#{@new_resource} code: #{@new_resource.code}") - - @new_resource.code.each_line { |line| stdin.puts(line.chomp) } - - stdin.close - - Chef::Log.debug("#{@new_resource} output: ") - - stdout_output = "" - stdout.each_line { |line| stdout_output << line } - stdout.close - - stderr_output = "" - stderr.each_line { |line| stderr_output << line } - stderr.close - - # fail if stderr contains anything - if stderr_output.length > 0 - raise Chef::Exceptions::ErlCall, stderr_output - end - - # fail if the first 4 characters aren't "{ok," - unless stdout_output[0..3].include?("{ok,") - raise Chef::Exceptions::ErlCall, stdout_output - end - - @new_resource.updated_by_last_action(true) - - Chef::Log.debug("#{@new_resource} #{stdout_output}") - Chef::Log.info("#{@new_resouce} ran successfully") - ensure - Process.wait(pid) if pid - end - end - end - - end - end -end diff --git a/lib/chef/provider/execute.rb b/lib/chef/provider/execute.rb index 45f0ad5488..6872e2d67d 100644 --- a/lib/chef/provider/execute.rb +++ b/lib/chef/provider/execute.rb @@ -1,6 +1,6 @@ # # Author:: Adam Jacob (<adam@chef.io>) -# Copyright:: Copyright 2008-2016, Chef Software Inc. +# Copyright:: Copyright 2008-2017, Chef Software Inc. # License:: Apache License, Version 2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -27,21 +27,17 @@ class Chef provides :execute - def_delegators :@new_resource, :command, :returns, :environment, :user, :group, :cwd, :umask, :creates + def_delegators :new_resource, :command, :returns, :environment, :user, :domain, :password, :group, :cwd, :umask, :creates, :elevated def load_current_resource current_resource = Chef::Resource::Execute.new(new_resource.name) current_resource end - def whyrun_supported? - true - end - def define_resource_requirements - # @todo: this should change to raise in some appropriate major version bump. if creates && creates_relative? && !cwd - Chef::Log.warn "Providing a relative path for the creates attribute without the cwd is deprecated and will be changed to fail in the future (CHEF-3819)" + # FIXME? move this onto the resource? + raise Chef::Exceptions::Execute, "Please either specify a full path for the creates attribute, or specify a cwd property to the #{new_resource} resource" end end @@ -53,22 +49,26 @@ class Chef def action_run if creates && sentinel_file.exist? - Chef::Log.debug("#{new_resource} sentinel file #{sentinel_file} exists - nothing to do") + logger.debug("#{new_resource} sentinel file #{sentinel_file} exists - nothing to do") return false end converge_by("execute #{description}") do begin - shell_out!(command, opts) + shell_out_with_systems_locale!(command, opts) rescue Mixlib::ShellOut::ShellCommandFailed if sensitive? - raise Mixlib::ShellOut::ShellCommandFailed, - "Command execution failed. STDOUT/STDERR suppressed for sensitive resource" + ex = Mixlib::ShellOut::ShellCommandFailed.new("Command execution failed. STDOUT/STDERR suppressed for sensitive resource") + # Forcibly hide the exception cause chain here so we don't log the unredacted version + def ex.cause + nil + end + raise ex else raise end end - Chef::Log.info("#{new_resource} ran successfully") + logger.info("#{new_resource} ran successfully") end end @@ -92,18 +92,21 @@ class Chef opts[:returns] = returns if returns opts[:environment] = environment if environment opts[:user] = user if user + opts[:domain] = domain if domain + opts[:password] = password if password opts[:group] = group if group opts[:cwd] = cwd if cwd opts[:umask] = umask if umask opts[:log_level] = :info opts[:log_tag] = new_resource.to_s - if (Chef::Log.info? || live_stream?) && !sensitive? + if (logger.info? || live_stream?) && !sensitive? if run_context.events.formatter? opts[:live_stream] = Chef::EventDispatch::EventsOutputStream.new(run_context.events, :name => :execute) elsif stream_to_stdout? opts[:live_stream] = STDOUT end end + opts[:elevated] = elevated if elevated opts end @@ -120,6 +123,7 @@ class Chef ( cwd && creates_relative? ) ? ::File.join(cwd, creates) : creates )) end + end end end diff --git a/lib/chef/provider/file.rb b/lib/chef/provider/file.rb index 7f85085eeb..05522f1eb8 100644 --- a/lib/chef/provider/file.rb +++ b/lib/chef/provider/file.rb @@ -1,7 +1,7 @@ # # Author:: Adam Jacob (<adam@chef.io>) # Author:: Lamont Granquist (<lamont@chef.io>) -# Copyright:: Copyright 2008-2016, Chef Software, Inc. +# Copyright:: Copyright 2008-2017, Chef Software Inc. # License:: Apache License, Version 2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -30,8 +30,6 @@ require "chef/mixin/enforce_ownership_and_permissions" require "chef/util/backup" require "chef/util/diff" require "chef/util/selinux" -require "chef/deprecation/provider/file" -require "chef/deprecation/warnings" require "chef/file_content_management/deploy" # The Tao of File Providers: @@ -52,10 +50,6 @@ class Chef include Chef::Util::Selinux include Chef::Mixin::FileClass - extend Chef::Deprecation::Warnings - include Chef::Deprecation::Provider::File - add_deprecation_warnings_for(Chef::Deprecation::Provider::File.instance_methods) - provides :file attr_reader :deployment_strategy @@ -72,10 +66,6 @@ class Chef super end - def whyrun_supported? - true - end - def load_current_resource # true if there is a symlink and we need to manage what it points at @managing_symlink = file_class.symlink?(new_resource.path) && ( new_resource.manage_symlink_source || new_resource.manage_symlink_source.nil? ) @@ -95,14 +85,14 @@ class Chef # true if we are going to be creating a new file @needs_creating = !::File.exist?(new_resource.path) || needs_unlinking? - # Let children resources override constructing the @current_resource + # Let children resources override constructing the current_resource @current_resource ||= Chef::Resource::File.new(new_resource.name) current_resource.path(new_resource.path) if !needs_creating? # we are updating an existing file if managing_content? - Chef::Log.debug("#{new_resource} checksumming file at #{new_resource.path}.") + logger.trace("#{new_resource} checksumming file at #{new_resource.path}.") current_resource.checksum(checksum(current_resource.path)) else # if the file does not exist or is not a file, then the checksum is invalid/pointless @@ -120,17 +110,17 @@ class Chef # Make sure the parent directory exists, otherwise fail. For why-run assume it would have been created. requirements.assert(:create, :create_if_missing, :touch) do |a| - parent_directory = ::File.dirname(@new_resource.path) + parent_directory = ::File.dirname(new_resource.path) a.assertion { ::File.directory?(parent_directory) } a.failure_message(Chef::Exceptions::EnclosingDirectoryDoesNotExist, "Parent directory #{parent_directory} does not exist.") a.whyrun("Assuming directory #{parent_directory} would have been created") end # Make sure the file is deletable if it exists, otherwise fail. - if ::File.exist?(@new_resource.path) + if ::File.exist?(new_resource.path) requirements.assert(:delete) do |a| - a.assertion { ::File.writable?(@new_resource.path) } - a.failure_message(Chef::Exceptions::InsufficientPermissions, "File #{@new_resource.path} exists but is not writable so it cannot be deleted") + a.assertion { ::File.writable?(new_resource.path) } + a.failure_message(Chef::Exceptions::InsufficientPermissions, "File #{new_resource.path} exists but is not writable so it cannot be deleted") end end @@ -154,33 +144,33 @@ class Chef do_contents_changes do_acl_changes do_selinux - load_resource_attributes_from_file(@new_resource) + load_resource_attributes_from_file(new_resource) unless Chef::Config[:why_run] end def action_create_if_missing - unless ::File.exist?(@new_resource.path) + unless ::File.exist?(new_resource.path) action_create else - Chef::Log.debug("#{@new_resource} exists at #{@new_resource.path} taking no action.") + logger.trace("#{new_resource} exists at #{new_resource.path} taking no action.") end end def action_delete - if ::File.exists?(@new_resource.path) - converge_by("delete file #{@new_resource.path}") do - do_backup unless file_class.symlink?(@new_resource.path) - ::File.delete(@new_resource.path) - Chef::Log.info("#{@new_resource} deleted file at #{@new_resource.path}") + if ::File.exists?(new_resource.path) + converge_by("delete file #{new_resource.path}") do + do_backup unless file_class.symlink?(new_resource.path) + ::File.delete(new_resource.path) + logger.info("#{new_resource} deleted file at #{new_resource.path}") end end end def action_touch action_create - converge_by("update utime on file #{@new_resource.path}") do + converge_by("update utime on file #{new_resource.path}") do time = Time.now - ::File.utime(time, time, @new_resource.path) - Chef::Log.info("#{@new_resource} updated atime and mtime to #{time}") + ::File.utime(time, time, new_resource.path) + logger.info("#{new_resource} updated atime and mtime to #{time}") end end @@ -197,8 +187,8 @@ class Chef # content (for things like doing checksums in load_current_resource). Expected to # be overridden in subclasses. def managing_content? - return true if @new_resource.checksum - return true if !@new_resource.content.nil? && @action != :create_if_missing + return true if new_resource.checksum + return true if !new_resource.content.nil? && @action != :create_if_missing false end @@ -228,25 +218,25 @@ class Chef # assertions, which then decide whether or not to raise or issue a # warning for whyrun mode. def inspect_existing_fs_entry - path = @new_resource.path + path = new_resource.path if !l_exist?(path) [nil, nil, nil] elsif real_file?(path) [nil, nil, nil] - elsif file_class.symlink?(path) && @new_resource.manage_symlink_source + elsif file_class.symlink?(path) && new_resource.manage_symlink_source verify_symlink_sanity(path) - elsif file_class.symlink?(@new_resource.path) && @new_resource.manage_symlink_source.nil? - Chef::Log.warn("File #{path} managed by #{@new_resource} is really a symlink. Managing the source file instead.") - Chef::Log.warn("Disable this warning by setting `manage_symlink_source true` on the resource") - Chef::Log.warn("In a future Chef release, 'manage_symlink_source' will not be enabled by default") + elsif file_class.symlink?(new_resource.path) && new_resource.manage_symlink_source.nil? + logger.warn("File #{path} managed by #{new_resource} is really a symlink. Managing the source file instead.") + logger.warn("Disable this warning by setting `manage_symlink_source true` on the resource") + logger.warn("In a future Chef release, 'manage_symlink_source' will not be enabled by default") verify_symlink_sanity(path) - elsif @new_resource.force_unlink + elsif new_resource.force_unlink [nil, nil, nil] else [ Chef::Exceptions::FileTypeMismatch, - "File #{path} exists, but is a #{file_type_string(@new_resource.path)}, set force_unlink to true to remove", - "Assuming #{file_type_string(@new_resource.path)} at #{@new_resource.path} would have been removed by a previous resource", + "File #{path} exists, but is a #{file_type_string(new_resource.path)}, set force_unlink to true to remove", + "Assuming #{file_type_string(new_resource.path)} at #{new_resource.path} would have been removed by a previous resource", ] end end @@ -282,8 +272,8 @@ class Chef def content @content ||= begin - load_current_resource if @current_resource.nil? - @content_class.new(@new_resource, @current_resource, @run_context) + load_current_resource if current_resource.nil? + @content_class.new(new_resource, current_resource, @run_context, logger) end end @@ -312,11 +302,9 @@ class Chef # like real_file? that follows (sane) symlinks def symlink_to_real_file?(path) - begin - real_file?(::File.realpath(path)) - rescue Errno::ELOOP, Errno::ENOENT - false - end + real_file?(::File.realpath(path)) + rescue Errno::ELOOP, Errno::ENOENT + false end # Similar to File.exist?, but also returns true in the case that the @@ -344,26 +332,26 @@ class Chef end def do_validate_content - if new_resource.checksum && tempfile && ( new_resource.checksum != tempfile_checksum ) + if new_resource.checksum && tempfile && ( new_resource.checksum.downcase != tempfile_checksum ) raise Chef::Exceptions::ChecksumMismatch.new(short_cksum(new_resource.checksum), short_cksum(tempfile_checksum)) end if tempfile new_resource.verify.each do |v| if ! v.verify(tempfile.path) - raise Chef::Exceptions::ValidationFailed.new "Proposed content for #{new_resource.path} failed verification #{v}" + raise Chef::Exceptions::ValidationFailed.new "Proposed content for #{new_resource.path} failed verification #{new_resource.sensitive ? '[sensitive]' : v}" end end end end def do_unlink - if @new_resource.force_unlink + if new_resource.force_unlink if needs_unlinking? # unlink things that aren't normal files - description = "unlink #{file_type_string(@new_resource.path)} at #{@new_resource.path}" + description = "unlink #{file_type_string(new_resource.path)} at #{new_resource.path}" converge_by(description) do - unlink(@new_resource.path) + unlink(new_resource.path) end end end @@ -371,15 +359,15 @@ class Chef def do_create_file if needs_creating? - converge_by("create new file #{@new_resource.path}") do - deployment_strategy.create(@new_resource.path) - Chef::Log.info("#{@new_resource} created file #{@new_resource.path}") + converge_by("create new file #{new_resource.path}") do + deployment_strategy.create(new_resource.path) + logger.info("#{new_resource} created file #{new_resource.path}") end end end def do_backup(file = nil) - Chef::Util::Backup.new(@new_resource, file).backup! + Chef::Util::Backup.new(new_resource, file).backup! end def diff @@ -389,7 +377,7 @@ class Chef def update_file_contents do_backup unless needs_creating? deployment_strategy.deploy(tempfile.path, ::File.realpath(new_resource.path)) - Chef::Log.info("#{new_resource} updated file contents #{new_resource.path}") + logger.info("#{new_resource} updated file contents #{new_resource.path}") if managing_content? # save final checksum for reporting. new_resource.final_checksum = checksum(new_resource.path) @@ -405,17 +393,17 @@ class Chef end # the file? on the next line suppresses the case in why-run when we have a not-file here that would have otherwise been removed - if ::File.file?(@new_resource.path) && contents_changed? - description = [ "update content in file #{@new_resource.path} from \ -#{short_cksum(@current_resource.checksum)} to #{short_cksum(tempfile_checksum)}" ] + if ::File.file?(new_resource.path) && contents_changed? + description = [ "update content in file #{new_resource.path} from \ +#{short_cksum(current_resource.checksum)} to #{short_cksum(tempfile_checksum)}" ] # Hide the diff output if the resource is marked as a sensitive resource - if @new_resource.sensitive - @new_resource.diff("suppressed sensitive resource") + if new_resource.sensitive + new_resource.diff("suppressed sensitive resource") description << "suppressed sensitive resource" else - diff.diff(@current_resource.path, tempfile.path) - @new_resource.diff( diff.for_reporting ) unless needs_creating? + diff.diff(current_resource.path, tempfile.path) + new_resource.diff( diff.for_reporting ) unless needs_creating? description << diff.for_output end @@ -437,10 +425,10 @@ class Chef if resource_updated? && Chef::Config[:enable_selinux_file_permission_fixup] if selinux_enabled? converge_by("restore selinux security context") do - restore_security_context(::File.realpath(@new_resource.path), recursive) + restore_security_context(::File.realpath(new_resource.path), recursive) end else - Chef::Log.debug "selinux utilities can not be found. Skipping selinux permission fixup." + logger.trace "selinux utilities can not be found. Skipping selinux permission fixup." end end end @@ -454,19 +442,14 @@ class Chef end def contents_changed? - Chef::Log.debug "calculating checksum of #{tempfile.path} to compare with #{@current_resource.checksum}" - tempfile_checksum != @current_resource.checksum + logger.trace "calculating checksum of #{tempfile.path} to compare with #{current_resource.checksum}" + tempfile_checksum != current_resource.checksum end def tempfile @tempfile ||= content.tempfile end - def short_cksum(checksum) - return "none" if checksum.nil? - checksum.slice(0, 6) - end - def load_resource_attributes_from_file(resource) if Chef::Platform.windows? # This is a work around for CHEF-3554. @@ -475,7 +458,7 @@ class Chef # reporting won't work for Windows. return end - acl_scanner = ScanAccessControl.new(@new_resource, resource) + acl_scanner = ScanAccessControl.new(new_resource, resource) acl_scanner.set_all! end diff --git a/lib/chef/provider/git.rb b/lib/chef/provider/git.rb index 82f5ca2ba5..239dfa09e6 100644 --- a/lib/chef/provider/git.rb +++ b/lib/chef/provider/git.rb @@ -1,6 +1,6 @@ # # Author:: Daniel DeLeo (<dan@kallistec.com>) -# Copyright:: Copyright 2008-2016, Chef Software Inc. +# Copyright:: Copyright 2008-2017, Chef Software Inc. # License:: Apache License, Version 2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -25,78 +25,79 @@ class Chef class Provider class Git < Chef::Provider + extend Forwardable provides :git - def whyrun_supported? - true - end + GIT_VERSION_PATTERN = Regexp.compile('git version (\d+\.\d+.\d+)') + + def_delegator :new_resource, :destination, :cwd def load_current_resource @resolved_reference = nil - @current_resource = Chef::Resource::Git.new(@new_resource.name) + @current_resource = Chef::Resource::Git.new(new_resource.name) if current_revision = find_current_revision - @current_resource.revision current_revision + current_resource.revision current_revision end end def define_resource_requirements # Parent directory of the target must exist. requirements.assert(:checkout, :sync) do |a| - dirname = ::File.dirname(@new_resource.destination) + dirname = ::File.dirname(cwd) a.assertion { ::File.directory?(dirname) } a.whyrun("Directory #{dirname} does not exist, this run will fail unless it has been previously created. Assuming it would have been created.") a.failure_message(Chef::Exceptions::MissingParentDirectory, - "Cannot clone #{@new_resource} to #{@new_resource.destination}, the enclosing directory #{dirname} does not exist") + "Cannot clone #{new_resource} to #{cwd}, the enclosing directory #{dirname} does not exist") end requirements.assert(:all_actions) do |a| - a.assertion { !(@new_resource.revision =~ /^origin\//) } + a.assertion { !(new_resource.revision =~ /^origin\//) } a.failure_message Chef::Exceptions::InvalidRemoteGitReference, "Deploying remote branches is not supported. " + "Specify the remote branch as a local branch for " + "the git repository you're deploying from " + - "(ie: '#{@new_resource.revision.gsub('origin/', '')}' rather than '#{@new_resource.revision}')." + "(ie: '#{new_resource.revision.gsub('origin/', '')}' rather than '#{new_resource.revision}')." end requirements.assert(:all_actions) do |a| # this can't be recovered from in why-run mode, because nothing that # we do in the course of a run is likely to create a valid target_revision # if we can't resolve it up front. - a.assertion { target_revision != nil } + a.assertion { !target_revision.nil? } a.failure_message Chef::Exceptions::UnresolvableGitReference, - "Unable to parse SHA reference for '#{@new_resource.revision}' in repository '#{@new_resource.repository}'. " + + "Unable to parse SHA reference for '#{new_resource.revision}' in repository '#{new_resource.repository}'. " + "Verify your (case-sensitive) repository URL and revision.\n" + - "`git ls-remote '#{@new_resource.repository}' '#{rev_search_pattern}'` output: #{@resolved_reference}" + "`git ls-remote '#{new_resource.repository}' '#{rev_search_pattern}'` output: #{@resolved_reference}" end end def action_checkout if target_dir_non_existent_or_empty? clone - if @new_resource.enable_checkout + if new_resource.enable_checkout checkout end enable_submodules add_remotes else - Chef::Log.debug "#{@new_resource} checkout destination #{@new_resource.destination} already exists or is a non-empty directory" + logger.trace "#{new_resource} checkout destination #{cwd} already exists or is a non-empty directory" end end def action_export action_checkout - converge_by("complete the export by removing #{@new_resource.destination}.git after checkout") do - FileUtils.rm_rf(::File.join(@new_resource.destination, ".git")) + converge_by("complete the export by removing #{cwd}.git after checkout") do + FileUtils.rm_rf(::File.join(cwd, ".git")) end end def action_sync if existing_git_clone? - Chef::Log.debug "#{@new_resource} current revision: #{@current_resource.revision} target revision: #{target_revision}" + logger.trace "#{new_resource} current revision: #{current_resource.revision} target revision: #{target_revision}" unless current_revision_matches_target_revision? fetch_updates enable_submodules - Chef::Log.info "#{@new_resource} updated to revision #{target_revision}" + logger.info "#{new_resource} updated to revision #{target_revision}" end add_remotes else @@ -104,32 +105,45 @@ class Chef end end - def git_minor_version - @git_minor_version ||= Gem::Version.new(shell_out!("git --version", run_options).stdout.split.last) + def git_has_single_branch_option? + @git_has_single_branch_option ||= !git_gem_version.nil? && git_gem_version >= Gem::Version.new("1.7.10") + end + + def git_gem_version + return @git_gem_version if defined?(@git_gem_version) + output = git("--version").stdout + match = GIT_VERSION_PATTERN.match(output) + if match + @git_gem_version = Gem::Version.new(match[1]) + else + logger.warn "Unable to parse git version from '#{output}'" + @git_gem_version = nil + end + @git_gem_version end def existing_git_clone? - ::File.exist?(::File.join(@new_resource.destination, ".git")) + ::File.exist?(::File.join(cwd, ".git")) end def target_dir_non_existent_or_empty? - !::File.exist?(@new_resource.destination) || Dir.entries(@new_resource.destination).sort == [".", ".."] + !::File.exist?(cwd) || Dir.entries(cwd).sort == [".", ".."] end def find_current_revision - Chef::Log.debug("#{@new_resource} finding current git revision") + logger.trace("#{new_resource} finding current git revision") if ::File.exist?(::File.join(cwd, ".git")) # 128 is returned when we're not in a git repo. this is fine - result = shell_out!("git rev-parse HEAD", :cwd => cwd, :returns => [0, 128]).stdout.strip + result = git("rev-parse", "HEAD", cwd: cwd, returns: [0, 128]).stdout.strip end sha_hash?(result) ? result : nil end def add_remotes - if @new_resource.additional_remotes.length > 0 - @new_resource.additional_remotes.each_pair do |remote_name, remote_url| + if new_resource.additional_remotes.length > 0 + new_resource.additional_remotes.each_pair do |remote_name, remote_url| converge_by("add remote #{remote_name} from #{remote_url}") do - Chef::Log.info "#{@new_resource} adding git remote #{remote_name} = #{remote_url}" + logger.info "#{new_resource} adding git remote #{remote_name} = #{remote_url}" setup_remote_tracking_branches(remote_name, remote_url) end end @@ -137,61 +151,60 @@ class Chef end def clone - converge_by("clone from #{@new_resource.repository} into #{@new_resource.destination}") do - remote = @new_resource.remote - - args = [] - args << "-o #{remote}" unless remote == "origin" - args << "--depth #{@new_resource.depth}" if @new_resource.depth - args << "--no-single-branch" if @new_resource.depth && git_minor_version >= Gem::Version.new("1.7.10") - - Chef::Log.info "#{@new_resource} cloning repo #{@new_resource.repository} to #{@new_resource.destination}" - - clone_cmd = "git clone #{args.join(' ')} \"#{@new_resource.repository}\" \"#{@new_resource.destination}\"" - shell_out!(clone_cmd, run_options) + converge_by("clone from #{new_resource.repository} into #{cwd}") do + remote = new_resource.remote + + clone_cmd = ["clone"] + clone_cmd << "-o #{remote}" unless remote == "origin" + clone_cmd << "--depth #{new_resource.depth}" if new_resource.depth + clone_cmd << "--no-single-branch" if new_resource.depth && git_has_single_branch_option? + clone_cmd << "\"#{new_resource.repository}\"" + clone_cmd << "\"#{cwd}\"" + + logger.info "#{new_resource} cloning repo #{new_resource.repository} to #{cwd}" + git clone_cmd end end def checkout sha_ref = target_revision - converge_by("checkout ref #{sha_ref} branch #{@new_resource.revision}") do + converge_by("checkout ref #{sha_ref} branch #{new_resource.revision}") do # checkout into a local branch rather than a detached HEAD - shell_out!("git branch -f #{@new_resource.checkout_branch} #{sha_ref}", run_options(:cwd => @new_resource.destination)) - shell_out!("git checkout #{@new_resource.checkout_branch}", run_options(:cwd => @new_resource.destination)) - Chef::Log.info "#{@new_resource} checked out branch: #{@new_resource.revision} onto: #{@new_resource.checkout_branch} reference: #{sha_ref}" + git("branch", "-f", new_resource.checkout_branch, sha_ref, cwd: cwd) + git("checkout", new_resource.checkout_branch, cwd: cwd) + logger.info "#{new_resource} checked out branch: #{new_resource.revision} onto: #{new_resource.checkout_branch} reference: #{sha_ref}" end end def enable_submodules - if @new_resource.enable_submodules - converge_by("enable git submodules for #{@new_resource}") do - Chef::Log.info "#{@new_resource} synchronizing git submodules" - command = "git submodule sync" - shell_out!(command, run_options(:cwd => @new_resource.destination)) - Chef::Log.info "#{@new_resource} enabling git submodules" + if new_resource.enable_submodules + converge_by("enable git submodules for #{new_resource}") do + logger.info "#{new_resource} synchronizing git submodules" + git("submodule", "sync", cwd: cwd) + logger.info "#{new_resource} enabling git submodules" # the --recursive flag means we require git 1.6.5+ now, see CHEF-1827 - command = "git submodule update --init --recursive" - shell_out!(command, run_options(:cwd => @new_resource.destination)) + git("submodule", "update", "--init", "--recursive", cwd: cwd) end end end def fetch_updates - setup_remote_tracking_branches(@new_resource.remote, @new_resource.repository) - converge_by("fetch updates for #{@new_resource.remote}") do + setup_remote_tracking_branches(new_resource.remote, new_resource.repository) + converge_by("fetch updates for #{new_resource.remote}") do # since we're in a local branch already, just reset to specified revision rather than merge - fetch_command = "git fetch #{@new_resource.remote} && git fetch #{@new_resource.remote} --tags && git reset --hard #{target_revision}" - Chef::Log.debug "Fetching updates from #{new_resource.remote} and resetting to revision #{target_revision}" - shell_out!(fetch_command, run_options(:cwd => @new_resource.destination)) + logger.trace "Fetching updates from #{new_resource.remote} and resetting to revision #{target_revision}" + git("fetch", "--prune", new_resource.remote, cwd: cwd) + git("fetch", new_resource.remote, "--tags", cwd: cwd) + git("reset", "--hard", target_revision, cwd: cwd) end end def setup_remote_tracking_branches(remote_name, remote_url) converge_by("set up remote tracking branches for #{remote_url} at #{remote_name}") do - Chef::Log.debug "#{@new_resource} configuring remote tracking branches for repository #{remote_url} " + "at remote #{remote_name}" - check_remote_command = "git config --get remote.#{remote_name}.url" - remote_status = shell_out!(check_remote_command, run_options(:cwd => @new_resource.destination, :returns => [0, 1, 2])) + logger.trace "#{new_resource} configuring remote tracking branches for repository #{remote_url} " + "at remote #{remote_name}" + check_remote_command = ["config", "--get", "remote.#{remote_name}.url"] + remote_status = git(check_remote_command, cwd: cwd, returns: [0, 1, 2]) case remote_status.exitstatus when 0, 2 # * Status 0 means that we already have a remote with this name, so we should update the url @@ -200,12 +213,10 @@ class Chef # which we can fix by replacing them all with our target url (hence the --replace-all option) if multiple_remotes?(remote_status) || !remote_matches?(remote_url, remote_status) - update_remote_url_command = "git config --replace-all remote.#{remote_name}.url #{remote_url}" - shell_out!(update_remote_url_command, run_options(:cwd => @new_resource.destination)) + git("config", "--replace-all", "remote.#{remote_name}.url", remote_url, cwd: cwd) end when 1 - add_remote_command = "git remote add #{remote_name} #{remote_url}" - shell_out!(add_remote_command, run_options(:cwd => @new_resource.destination)) + git("remote", "add", remote_name, remote_url, cwd: cwd) end end end @@ -219,13 +230,13 @@ class Chef end def current_revision_matches_target_revision? - (!@current_resource.revision.nil?) && (target_revision.strip.to_i(16) == @current_resource.revision.strip.to_i(16)) + (!current_resource.revision.nil?) && (target_revision.strip.to_i(16) == current_resource.revision.strip.to_i(16)) end def target_revision @target_revision ||= begin - if sha_hash?(@new_resource.revision) - @target_revision = @new_resource.revision + if sha_hash?(new_resource.revision) + @target_revision = new_resource.revision else @target_revision = remote_resolve_reference end @@ -235,7 +246,7 @@ class Chef alias :revision_slug :target_revision def remote_resolve_reference - Chef::Log.debug("#{@new_resource} resolving remote reference") + logger.trace("#{new_resource} resolving remote reference") # The sha pointed to by an annotated tag is identified by the # '^{}' suffix appended to the tag. In order to resolve # annotated tags, we have to search for "revision*" and @@ -250,11 +261,11 @@ class Chef # confusing. We avoid the issue by disallowing the use of # annotated tags named 'HEAD'. if rev_search_pattern != "HEAD" - found = find_revision(refs, @new_resource.revision, "^{}") + found = find_revision(refs, new_resource.revision, "^{}") else found = refs_search(refs, "HEAD") end - found = find_revision(refs, @new_resource.revision) if found.empty? + found = find_revision(refs, new_resource.revision) if found.empty? found.size == 1 ? found.first[0] : nil end @@ -274,53 +285,57 @@ class Chef end def rev_search_pattern - if ["", "HEAD"].include? @new_resource.revision + if ["", "HEAD"].include? new_resource.revision "HEAD" else - @new_resource.revision + "*" + new_resource.revision + "*" end end def git_ls_remote(rev_pattern) - command = git(%Q{ls-remote "#{@new_resource.repository}" "#{rev_pattern}"}) - shell_out!(command, run_options).stdout + git("ls-remote", "\"#{new_resource.repository}\"", "\"#{rev_pattern}\"").stdout end def refs_search(refs, pattern) refs.find_all { |m| m[1] == pattern } end + alias git_minor_version git_gem_version + private def run_options(run_opts = {}) env = {} - if @new_resource.user - run_opts[:user] = @new_resource.user + if new_resource.user + run_opts[:user] = new_resource.user # Certain versions of `git` misbehave if git configuration is # inaccessible in $HOME. We need to ensure $HOME matches the # user who is executing `git` not the user running Chef. env["HOME"] = begin require "etc" - Etc.getpwnam(@new_resource.user).dir + case new_resource.user + when Integer + Etc.getpwuid(new_resource.user).dir + else + Etc.getpwnam(new_resource.user.to_s).dir + end rescue ArgumentError # user not found - raise Chef::Exceptions::User, "Could not determine HOME for specified user '#{@new_resource.user}' for resource '#{@new_resource.name}'" + raise Chef::Exceptions::User, "Could not determine HOME for specified user '#{new_resource.user}' for resource '#{new_resource.name}'" end end - run_opts[:group] = @new_resource.group if @new_resource.group - env["GIT_SSH"] = @new_resource.ssh_wrapper if @new_resource.ssh_wrapper - run_opts[:log_tag] = @new_resource.to_s - run_opts[:timeout] = @new_resource.timeout if @new_resource.timeout - env.merge!(@new_resource.environment) if @new_resource.environment + run_opts[:group] = new_resource.group if new_resource.group + env["GIT_SSH"] = new_resource.ssh_wrapper if new_resource.ssh_wrapper + run_opts[:log_tag] = new_resource.to_s + run_opts[:timeout] = new_resource.timeout if new_resource.timeout + env.merge!(new_resource.environment) if new_resource.environment run_opts[:environment] = env unless env.empty? run_opts end - def cwd - @new_resource.destination - end - - def git(*args) - ["git", *args].compact.join(" ") + def git(*args, **run_opts) + git_command = ["git", args].compact.join(" ") + logger.trace "running #{git_command}" + shell_out!(git_command, run_options(run_opts)) end def sha_hash?(string) diff --git a/lib/chef/provider/group.rb b/lib/chef/provider/group.rb index 8936bd2031..51e4f280fe 100644 --- a/lib/chef/provider/group.rb +++ b/lib/chef/provider/group.rb @@ -1,6 +1,6 @@ # # Author:: AJ Christensen (<aj@chef.io>) -# Copyright:: Copyright 2008-2016, Chef Software Inc. +# Copyright:: Copyright 2008-2017, Chef Software Inc. # License:: Apache License, Version 2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -18,59 +18,53 @@ require "chef/provider" require "chef/mixin/shell_out" -require "chef/mixin/command" require "etc" class Chef class Provider class Group < Chef::Provider include Chef::Mixin::ShellOut - include Chef::Mixin::Command attr_accessor :group_exists attr_accessor :change_desc - def whyrun_supported? - true - end - def initialize(new_resource, run_context) super @group_exists = true end def load_current_resource - @current_resource = Chef::Resource::Group.new(@new_resource.name) - @current_resource.group_name(@new_resource.group_name) + @current_resource = Chef::Resource::Group.new(new_resource.name) + current_resource.group_name(new_resource.group_name) group_info = nil begin - group_info = Etc.getgrnam(@new_resource.group_name) - rescue ArgumentError => e + group_info = Etc.getgrnam(new_resource.group_name) + rescue ArgumentError @group_exists = false - Chef::Log.debug("#{@new_resource} group does not exist") + logger.trace("#{new_resource} group does not exist") end if group_info - @new_resource.gid(group_info.gid) unless @new_resource.gid - @current_resource.gid(group_info.gid) - @current_resource.members(group_info.mem) + new_resource.gid(group_info.gid) unless new_resource.gid + current_resource.gid(group_info.gid) + current_resource.members(group_info.mem) end - @current_resource + current_resource end def define_resource_requirements requirements.assert(:modify) do |a| a.assertion { @group_exists } - a.failure_message(Chef::Exceptions::Group, "Cannot modify #{@new_resource} - group does not exist!") - a.whyrun("Group #{@new_resource} does not exist. Unless it would have been created earlier in this run, this attempt to modify it would fail.") + a.failure_message(Chef::Exceptions::Group, "Cannot modify #{new_resource} - group does not exist!") + a.whyrun("Group #{new_resource} does not exist. Unless it would have been created earlier in this run, this attempt to modify it would fail.") end requirements.assert(:all_actions) do |a| # Make sure that the resource doesn't contain any common # user names in the members and exclude_members properties. - if !@new_resource.members.nil? && !@new_resource.excluded_members.nil? - common_members = @new_resource.members & @new_resource.excluded_members + if !new_resource.members.nil? && !new_resource.excluded_members.nil? + common_members = new_resource.members & new_resource.excluded_members a.assertion { common_members.empty? } a.failure_message(Chef::Exceptions::ConflictingMembersInGroup, "Attempting to both add and remove users from a group: '#{common_members.join(', ')}'") # No why-run alternative @@ -86,41 +80,39 @@ class Chef # <false>:: If a change is not required def compare_group @change_desc = [ ] - if @new_resource.gid.to_s != @current_resource.gid.to_s - @change_desc << "change gid #{@current_resource.gid} to #{@new_resource.gid}" + if new_resource.gid.to_s != current_resource.gid.to_s + @change_desc << "change gid #{current_resource.gid} to #{new_resource.gid}" end - if @new_resource.append + if new_resource.append missing_members = [] - @new_resource.members.each do |member| + new_resource.members.each do |member| next if has_current_group_member?(member) validate_member!(member) missing_members << member end - if missing_members.length > 0 - @change_desc << "add missing member(s): #{missing_members.join(", ")}" + unless missing_members.empty? + @change_desc << "add missing member(s): #{missing_members.join(', ')}" end members_to_be_removed = [] - @new_resource.excluded_members.each do |member| + new_resource.excluded_members.each do |member| if has_current_group_member?(member) members_to_be_removed << member end end - if members_to_be_removed.length > 0 - @change_desc << "remove existing member(s): #{members_to_be_removed.join(", ")}" - end - else - if @new_resource.members != @current_resource.members - @change_desc << "replace group members with new list of members" + unless members_to_be_removed.empty? + @change_desc << "remove existing member(s): #{members_to_be_removed.join(', ')}" end + elsif new_resource.members != current_resource.members + @change_desc << "replace group members with new list of members" end !@change_desc.empty? end def has_current_group_member?(member) - @current_resource.members.include?(member) + current_resource.members.include?(member) end def validate_member!(member) @@ -132,44 +124,41 @@ class Chef def action_create case @group_exists when false - converge_by("create group #{@new_resource.group_name}") do + converge_by("create group #{new_resource.group_name}") do create_group - Chef::Log.info("#{@new_resource} created") + logger.info("#{new_resource} created") end else if compare_group - converge_by(["alter group #{@new_resource.group_name}"] + change_desc) do + converge_by(["alter group #{new_resource.group_name}"] + change_desc) do manage_group - Chef::Log.info("#{@new_resource} altered") + logger.info("#{new_resource} altered") end end end end def action_remove - if @group_exists - converge_by("remove group #{@new_resource.group_name}") do - remove_group - Chef::Log.info("#{@new_resource} removed") - end + return unless @group_exists + converge_by("remove group #{new_resource.group_name}") do + remove_group + logger.info("#{new_resource} removed") end end def action_manage - if @group_exists && compare_group - converge_by(["manage group #{@new_resource.group_name}"] + change_desc) do - manage_group - Chef::Log.info("#{@new_resource} managed") - end + return unless @group_exists && compare_group + converge_by(["manage group #{new_resource.group_name}"] + change_desc) do + manage_group + logger.info("#{new_resource} managed") end end def action_modify - if compare_group - converge_by(["modify group #{@new_resource.group_name}"] + change_desc) do - manage_group - Chef::Log.info("#{@new_resource} modified") - end + return unless compare_group + converge_by(["modify group #{new_resource.group_name}"] + change_desc) do + manage_group + logger.info("#{new_resource} modified") end end diff --git a/lib/chef/provider/group/aix.rb b/lib/chef/provider/group/aix.rb index 4a02d5ef8c..5c500e2753 100644 --- a/lib/chef/provider/group/aix.rb +++ b/lib/chef/provider/group/aix.rb @@ -17,7 +17,6 @@ # require "chef/provider/group/groupadd" -require "chef/mixin/shell_out" class Chef class Provider @@ -33,48 +32,42 @@ class Chef end def create_group - command = "mkgroup" - command << set_options << " #{@new_resource.group_name}" - run_command(:command => command) + shell_out_compact!("mkgroup", set_options, new_resource.group_name) modify_group_members end def manage_group - command = "chgroup" options = set_options - #Usage: chgroup [-R load_module] "attr=value" ... group if options.size > 0 - command << options << " #{@new_resource.group_name}" - run_command(:command => command) + shell_out_compact!("chgroup", options, new_resource.group_name) end modify_group_members end def remove_group - run_command(:command => "rmgroup #{@new_resource.group_name}") + shell_out_compact!("rmgroup", new_resource.group_name) end def add_member(member) - shell_out!("chgrpmem -m + #{member} #{@new_resource.group_name}") + shell_out_compact!("chgrpmem", "-m", "+", member, new_resource.group_name) end def set_members(members) return if members.empty? - shell_out!("chgrpmem -m = #{members.join(',')} #{@new_resource.group_name}") + shell_out_compact!("chgrpmem", "-m", "=", members.join(","), new_resource.group_name) end def remove_member(member) - shell_out!("chgrpmem -m - #{member} #{@new_resource.group_name}") + shell_out_compact!("chgrpmem", "-m", "-", member, new_resource.group_name) end def set_options - opts = "" - { :gid => "id" }.sort { |a, b| a[0] <=> b[0] }.each do |field, option| - if @current_resource.send(field) != @new_resource.send(field) - if @new_resource.send(field) - Chef::Log.debug("#{@new_resource} setting #{field} to #{@new_resource.send(field)}") - opts << " '#{option}=#{@new_resource.send(field)}'" - end + opts = [] + { gid: "id" }.sort_by { |a| a[0] }.each do |field, option| + next unless current_resource.send(field) != new_resource.send(field) + if new_resource.send(field) + logger.trace("#{new_resource} setting #{field} to #{new_resource.send(field)}") + opts << "#{option}=#{new_resource.send(field)}" end end opts diff --git a/lib/chef/provider/group/dscl.rb b/lib/chef/provider/group/dscl.rb index 00b4ce2b93..81c7d66aa8 100644 --- a/lib/chef/provider/group/dscl.rb +++ b/lib/chef/provider/group/dscl.rb @@ -24,12 +24,15 @@ class Chef provides :group, os: "darwin" def dscl(*args) - host = "." - stdout_result = ""; stderr_result = ""; cmd = "dscl #{host} -#{args.join(' ')}" - status = shell_out(cmd) + argdup = args.dup + cmd = argdup.shift + shellcmd = [ "dscl", ".", "-#{cmd}", argdup ] + status = shell_out_compact(shellcmd) + stdout_result = "" + stderr_result = "" status.stdout.each_line { |line| stdout_result << line } status.stderr.each_line { |line| stderr_result << line } - return [cmd, status, stdout_result, stderr_result] + [shellcmd.flatten.compact.join(" "), status, stdout_result, stderr_result] end def safe_dscl(*args) @@ -37,18 +40,18 @@ class Chef return "" if ( args.first =~ /^delete/ ) && ( result[1].exitstatus != 0 ) raise(Chef::Exceptions::Group, "dscl error: #{result.inspect}") unless result[1].exitstatus == 0 raise(Chef::Exceptions::Group, "dscl error: #{result.inspect}") if result[2] =~ /No such key: / - return result[2] + result[2] end def load_current_resource - @current_resource = Chef::Resource::Group.new(@new_resource.name) - @current_resource.group_name(@new_resource.group_name) + @current_resource = Chef::Resource::Group.new(new_resource.name) + current_resource.group_name(new_resource.group_name) group_info = nil begin - group_info = safe_dscl("read /Groups/#{@new_resource.group_name}") + group_info = safe_dscl("read", "/Groups/#{new_resource.group_name}") rescue Chef::Exceptions::Group @group_exists = false - Chef::Log.debug("#{@new_resource} group does not exist") + logger.trace("#{new_resource} group does not exist") end if group_info @@ -57,21 +60,21 @@ class Chef val.strip! if val case key.downcase when "primarygroupid" - @new_resource.gid(val) unless @new_resource.gid - @current_resource.gid(val) + new_resource.gid(val) unless new_resource.gid + current_resource.gid(val) when "groupmembership" - @current_resource.members(val.split(" ")) + current_resource.members(val.split(" ")) end end end - @current_resource + current_resource end # get a free GID greater than 200 def get_free_gid(search_limit = 1000) gid = nil; next_gid_guess = 200 - groups_gids = safe_dscl("list /Groups gid") + groups_gids = safe_dscl("list", "/Groups", "gid") while next_gid_guess < search_limit + 200 if groups_gids =~ Regexp.new("#{Regexp.escape(next_gid_guess.to_s)}\n") next_gid_guess += 1 @@ -80,51 +83,55 @@ class Chef break end end - return gid || raise("gid not found. Exhausted. Searched #{search_limit} times") + gid || raise("gid not found. Exhausted. Searched #{search_limit} times") end def gid_used?(gid) return false unless gid - groups_gids = safe_dscl("list /Groups gid") - !! ( groups_gids =~ Regexp.new("#{Regexp.escape(gid.to_s)}\n") ) + search_gids = safe_dscl("search", "/Groups", "PrimaryGroupID", gid.to_s) + + # dscl -search should not return anything if the gid doesn't exist, + # but on the off-chance that it does, check whether the given gid is + # in the output. + !!(search_gids =~ /\b#{gid}\b/) end def set_gid - @new_resource.gid(get_free_gid) if [nil, ""].include? @new_resource.gid - raise(Chef::Exceptions::Group, "gid is already in use") if gid_used?(@new_resource.gid) - safe_dscl("create /Groups/#{@new_resource.group_name} PrimaryGroupID #{@new_resource.gid}") + new_resource.gid(get_free_gid) if [nil, ""].include? new_resource.gid + raise(Chef::Exceptions::Group, "gid is already in use") if gid_used?(new_resource.gid) + safe_dscl("create", "/Groups/#{new_resource.group_name}", "PrimaryGroupID", new_resource.gid) end def set_members # First reset the memberships if the append is not set - unless @new_resource.append - Chef::Log.debug("#{@new_resource} removing group members #{@current_resource.members.join(' ')}") unless @current_resource.members.empty? - safe_dscl("create /Groups/#{@new_resource.group_name} GroupMembers ''") # clear guid list - safe_dscl("create /Groups/#{@new_resource.group_name} GroupMembership ''") # clear user list - @current_resource.members([ ]) + unless new_resource.append + logger.trace("#{new_resource} removing group members #{current_resource.members.join(' ')}") unless current_resource.members.empty? + safe_dscl("create", "/Groups/#{new_resource.group_name}", "GroupMembers", "") # clear guid list + safe_dscl("create", "/Groups/#{new_resource.group_name}", "GroupMembership", "") # clear user list + current_resource.members([ ]) end # Add any members that need to be added - if @new_resource.members && !@new_resource.members.empty? + if new_resource.members && !new_resource.members.empty? members_to_be_added = [ ] - @new_resource.members.each do |member| - members_to_be_added << member if !@current_resource.members.include?(member) + new_resource.members.each do |member| + members_to_be_added << member unless current_resource.members.include?(member) end unless members_to_be_added.empty? - Chef::Log.debug("#{@new_resource} setting group members #{members_to_be_added.join(', ')}") - safe_dscl("append /Groups/#{@new_resource.group_name} GroupMembership #{members_to_be_added.join(' ')}") + logger.trace("#{new_resource} setting group members #{members_to_be_added.join(', ')}") + safe_dscl("append", "/Groups/#{new_resource.group_name}", "GroupMembership", *members_to_be_added) end end # Remove any members that need to be removed - if @new_resource.excluded_members && !@new_resource.excluded_members.empty? + if new_resource.excluded_members && !new_resource.excluded_members.empty? members_to_be_removed = [ ] - @new_resource.excluded_members.each do |member| - members_to_be_removed << member if @current_resource.members.include?(member) + new_resource.excluded_members.each do |member| + members_to_be_removed << member if current_resource.members.include?(member) end unless members_to_be_removed.empty? - Chef::Log.debug("#{@new_resource} removing group members #{members_to_be_removed.join(', ')}") - safe_dscl("delete /Groups/#{@new_resource.group_name} GroupMembership #{members_to_be_removed.join(' ')}") + logger.trace("#{new_resource} removing group members #{members_to_be_removed.join(', ')}") + safe_dscl("delete", "/Groups/#{new_resource.group_name}", "GroupMembership", *members_to_be_removed) end end end @@ -132,8 +139,8 @@ class Chef def define_resource_requirements super requirements.assert(:all_actions) do |a| - a.assertion { ::File.exists?("/usr/bin/dscl") } - a.failure_message Chef::Exceptions::Group, "Could not find binary /usr/bin/dscl for #{@new_resource.name}" + a.assertion { ::File.exist?("/usr/bin/dscl") } + a.failure_message Chef::Exceptions::Group, "Could not find binary /usr/bin/dscl for #{new_resource.name}" # No whyrun alternative: this component should be available in the base install of any given system that uses it end end @@ -145,24 +152,24 @@ class Chef end def manage_group - if @new_resource.group_name && (@current_resource.group_name != @new_resource.group_name) + if new_resource.group_name && (current_resource.group_name != new_resource.group_name) dscl_create_group end - if @new_resource.gid && (@current_resource.gid != @new_resource.gid) + if new_resource.gid && (current_resource.gid != new_resource.gid) set_gid end - if @new_resource.members || @new_resource.excluded_members + if new_resource.members || new_resource.excluded_members set_members end end def dscl_create_group - safe_dscl("create /Groups/#{@new_resource.group_name}") - safe_dscl("create /Groups/#{@new_resource.group_name} Password '*'") + safe_dscl("create", "/Groups/#{new_resource.group_name}") + safe_dscl("create", "/Groups/#{new_resource.group_name}", "Password", "*") end def remove_group - safe_dscl("delete /Groups/#{@new_resource.group_name}") + safe_dscl("delete", "/Groups/#{new_resource.group_name}") end end end diff --git a/lib/chef/provider/group/gpasswd.rb b/lib/chef/provider/group/gpasswd.rb index dcf526b211..d8aff10d5b 100644 --- a/lib/chef/provider/group/gpasswd.rb +++ b/lib/chef/provider/group/gpasswd.rb @@ -31,26 +31,26 @@ class Chef def define_resource_requirements super requirements.assert(:all_actions) do |a| - a.assertion { ::File.exists?("/usr/bin/gpasswd") } - a.failure_message Chef::Exceptions::Group, "Could not find binary /usr/bin/gpasswd for #{@new_resource}" + a.assertion { ::File.exist?("/usr/bin/gpasswd") } + a.failure_message Chef::Exceptions::Group, "Could not find binary /usr/bin/gpasswd for #{new_resource}" # No whyrun alternative: this component should be available in the base install of any given system that uses it end end def set_members(members) - unless members.empty? - shell_out!("gpasswd -M #{members.join(',')} #{@new_resource.group_name}") + if members.empty? + shell_out_compact!("gpasswd", "-M", "", new_resource.group_name) else - shell_out!("gpasswd -M \"\" #{@new_resource.group_name}") + shell_out_compact!("gpasswd", "-M", members.join(","), new_resource.group_name) end end def add_member(member) - shell_out!("gpasswd -a #{member} #{@new_resource.group_name}") + shell_out_compact!("gpasswd", "-a", member, new_resource.group_name) end def remove_member(member) - shell_out!("gpasswd -d #{member} #{@new_resource.group_name}") + shell_out_compact!("gpasswd", "-d", member, new_resource.group_name) end end end diff --git a/lib/chef/provider/group/groupadd.rb b/lib/chef/provider/group/groupadd.rb index bc6b5d0208..7d7fac146c 100644 --- a/lib/chef/provider/group/groupadd.rb +++ b/lib/chef/provider/group/groupadd.rb @@ -35,8 +35,8 @@ class Chef super required_binaries.each do |required_binary| requirements.assert(:all_actions) do |a| - a.assertion { ::File.exists?(required_binary) } - a.failure_message Chef::Exceptions::Group, "Could not find binary #{required_binary} for #{@new_resource}" + a.assertion { ::File.exist?(required_binary) } + a.failure_message Chef::Exceptions::Group, "Could not find binary #{required_binary} for #{new_resource}" # No whyrun alternative: this component should be available in the base install of any given system that uses it end end @@ -44,54 +44,49 @@ class Chef # Create the group def create_group - command = "groupadd" - command << set_options - command << groupadd_options - run_command(:command => command) + shell_out_compact!("groupadd", set_options, groupadd_options) modify_group_members end # Manage the group when it already exists def manage_group - command = "groupmod" - command << set_options - run_command(:command => command) + shell_out_compact!("groupmod", set_options) modify_group_members end # Remove the group def remove_group - run_command(:command => "groupdel #{@new_resource.group_name}") + shell_out_compact!("groupdel", new_resource.group_name) end def modify_group_members - if @new_resource.append - if @new_resource.members && !@new_resource.members.empty? + if new_resource.append + if new_resource.members && !new_resource.members.empty? members_to_be_added = [ ] - @new_resource.members.each do |member| - members_to_be_added << member if !@current_resource.members.include?(member) + new_resource.members.each do |member| + members_to_be_added << member unless current_resource.members.include?(member) end members_to_be_added.each do |member| - Chef::Log.debug("#{@new_resource} appending member #{member} to group #{@new_resource.group_name}") + logger.trace("#{new_resource} appending member #{member} to group #{new_resource.group_name}") add_member(member) end end - if @new_resource.excluded_members && !@new_resource.excluded_members.empty? + if new_resource.excluded_members && !new_resource.excluded_members.empty? members_to_be_removed = [ ] - @new_resource.excluded_members.each do |member| - members_to_be_removed << member if @current_resource.members.include?(member) + new_resource.excluded_members.each do |member| + members_to_be_removed << member if current_resource.members.include?(member) end members_to_be_removed.each do |member| - Chef::Log.debug("#{@new_resource} removing member #{member} from group #{@new_resource.group_name}") + logger.trace("#{new_resource} removing member #{member} from group #{new_resource.group_name}") remove_member(member) end end else - members_description = @new_resource.members.empty? ? "none" : @new_resource.members.join(", ") - Chef::Log.debug("#{@new_resource} setting group members to: #{members_description}") - set_members(@new_resource.members) + members_description = new_resource.members.empty? ? "none" : new_resource.members.join(", ") + logger.trace("#{new_resource} setting group members to: #{members_description}") + set_members(new_resource.members) end end @@ -112,22 +107,23 @@ class Chef # ==== Returns # <string>:: A string containing the option and then the quoted value def set_options - opts = "" - { :gid => "-g" }.sort { |a, b| a[0] <=> b[0] }.each do |field, option| - if @current_resource.send(field) != @new_resource.send(field) - if @new_resource.send(field) - opts << " #{option} '#{@new_resource.send(field)}'" - Chef::Log.debug("#{@new_resource} set #{field} to #{@new_resource.send(field)}") - end - end + opts = [] + { gid: "-g" }.sort_by { |a| a[0] }.each do |field, option| + next unless current_resource.send(field) != new_resource.send(field) + next unless new_resource.send(field) + opts << option + opts << new_resource.send(field) + logger.trace("#{new_resource} set #{field} to #{new_resource.send(field)}") end - opts << " #{@new_resource.group_name}" + opts << new_resource.group_name + opts end def groupadd_options - opts = "" - opts << " -r" if @new_resource.system - opts << " -o" if @new_resource.non_unique + opts = [] + # Solaris doesn't support system groups. + opts << "-r" if new_resource.system && !node.platform?("solaris2") + opts << "-o" if new_resource.non_unique opts end diff --git a/lib/chef/provider/group/groupmod.rb b/lib/chef/provider/group/groupmod.rb index 295d3b3425..13f83db4c4 100644 --- a/lib/chef/provider/group/groupmod.rb +++ b/lib/chef/provider/group/groupmod.rb @@ -26,28 +26,26 @@ class Chef def load_current_resource super %w{group user}.each do |binary| - raise Chef::Exceptions::Group, "Could not find binary /usr/sbin/#{binary} for #{@new_resource}" unless ::File.exists?("/usr/sbin/#{binary}") + raise Chef::Exceptions::Group, "Could not find binary /usr/sbin/#{binary} for #{new_resource}" unless ::File.exist?("/usr/sbin/#{binary}") end end # Create the group def create_group - command = "group add" - command << set_options - shell_out!(command) + shell_out_compact!("group", "add", set_options) - add_group_members(@new_resource.members) + add_group_members(new_resource.members) end # Manage the group when it already exists def manage_group - if @new_resource.append + if new_resource.append members_to_be_added = [ ] - if @new_resource.excluded_members && !@new_resource.excluded_members.empty? + if new_resource.excluded_members && !new_resource.excluded_members.empty? # First find out if any member needs to be removed members_to_be_removed = [ ] - @new_resource.excluded_members.each do |member| - members_to_be_removed << member if @current_resource.members.include?(member) + new_resource.excluded_members.each do |member| + members_to_be_removed << member if current_resource.members.include?(member) end unless members_to_be_removed.empty? @@ -56,39 +54,39 @@ class Chef # Capture the members we need to add in # members_to_be_added to be added later on. - @current_resource.members.each do |member| + current_resource.members.each do |member| members_to_be_added << member unless members_to_be_removed.include?(member) end end end - if @new_resource.members && !@new_resource.members.empty? - @new_resource.members.each do |member| - members_to_be_added << member if !@current_resource.members.include?(member) + if new_resource.members && !new_resource.members.empty? + new_resource.members.each do |member| + members_to_be_added << member unless current_resource.members.include?(member) end end - Chef::Log.debug("#{@new_resource} not changing group members, the group has no members to add") if members_to_be_added.empty? + logger.trace("#{new_resource} not changing group members, the group has no members to add") if members_to_be_added.empty? add_group_members(members_to_be_added) else # We are resetting the members of a group so use the same trick reset_group_membership - Chef::Log.debug("#{@new_resource} setting group members to: none") if @new_resource.members.empty? - add_group_members(@new_resource.members) + logger.trace("#{new_resource} setting group members to: none") if new_resource.members.empty? + add_group_members(new_resource.members) end end # Remove the group def remove_group - shell_out!("group del #{@new_resource.group_name}") + shell_out_compact!("group", "del", new_resource.group_name) end # Adds a list of usernames to the group using `user mod` def add_group_members(members) - Chef::Log.debug("#{@new_resource} adding members #{members.join(', ')}") if !members.empty? + logger.trace("#{new_resource} adding members #{members.join(', ')}") unless members.empty? members.each do |user| - shell_out!("user mod -G #{@new_resource.group_name} #{user}") + shell_out_compact!("user", "mod", "-G", new_resource.group_name, user) end end @@ -96,15 +94,11 @@ class Chef # "<name>_bak", create a new group with the same GID and # "<name>", then set correct members on that group def reset_group_membership - rename = "group mod -n #{@new_resource.group_name}_bak #{@new_resource.group_name}" - shell_out!(rename) + shell_out_compact!("group", "mod", "-n", "#{new_resource.group_name}_bak", new_resource.group_name) - create = "group add" - create << set_options(:overwrite_gid => true) - shell_out!(create) + shell_out_compact!("group", "add", set_options(overwrite_gid: true)) - remove = "group del #{@new_resource.group_name}_bak" - shell_out!(remove) + shell_out_compact!("group", "del", "#{new_resource.group_name}_bak") end # Little bit of magic as per Adam's useradd provider to pull and assign the command line flags @@ -112,14 +106,15 @@ class Chef # ==== Returns # <string>:: A string containing the option and then the quoted value def set_options(overwrite_gid = false) - opts = "" - if overwrite_gid || @new_resource.gid && (@current_resource.gid != @new_resource.gid) - opts << " -g '#{@new_resource.gid}'" + opts = [] + if overwrite_gid || new_resource.gid && (current_resource.gid != new_resource.gid) + opts << "-g" + opts << new_resource.gid end if overwrite_gid - opts << " -o" + opts << "-o" end - opts << " #{@new_resource.group_name}" + opts << new_resource.group_name opts end end diff --git a/lib/chef/provider/group/pw.rb b/lib/chef/provider/group/pw.rb index 4fd78b6b31..b0393a147e 100644 --- a/lib/chef/provider/group/pw.rb +++ b/lib/chef/provider/group/pw.rb @@ -30,46 +30,42 @@ class Chef super requirements.assert(:all_actions) do |a| - a.assertion { ::File.exists?("/usr/sbin/pw") } - a.failure_message Chef::Exceptions::Group, "Could not find binary /usr/sbin/pw for #{@new_resource}" + a.assertion { ::File.exist?("/usr/sbin/pw") } + a.failure_message Chef::Exceptions::Group, "Could not find binary /usr/sbin/pw for #{new_resource}" # No whyrun alternative: this component should be available in the base install of any given system that uses it end end # Create the group def create_group - command = "pw groupadd" - command << set_options - - unless @new_resource.members.empty? + command = [ "pw", "groupadd", set_options ] + unless new_resource.members.empty? # pw group[add|mod] -M is used to set the full membership list on a # new or existing group. Because pw groupadd does not support the -m # and -d options used by manage_group, we treat group creation as a # special case and use -M. - Chef::Log.debug("#{@new_resource} setting group members: #{@new_resource.members.join(',')}") - command += " -M #{@new_resource.members.join(',')}" + logger.trace("#{new_resource} setting group members: #{new_resource.members.join(',')}") + command += [ "-M", new_resource.members.join(",") ] end - run_command(:command => command) + shell_out_compact!(command) end # Manage the group when it already exists def manage_group - command = "pw groupmod" - command << set_options member_options = set_members_options if member_options.empty? - run_command(:command => command) + shell_out_compact!("pw", "groupmod", set_options) else member_options.each do |option| - run_command(:command => command + option) + shell_out_compact!("pw", "groupmod", set_options, option) end end end # Remove the group def remove_group - run_command(:command => "pw groupdel #{@new_resource.group_name}") + shell_out_compact!("pw", "groupdel", new_resource.group_name) end # Little bit of magic as per Adam's useradd provider to pull and assign the command line flags @@ -77,10 +73,11 @@ class Chef # ==== Returns # <string>:: A string containing the option and then the quoted value def set_options - opts = " #{@new_resource.group_name}" - if @new_resource.gid && (@current_resource.gid != @new_resource.gid) - Chef::Log.debug("#{@new_resource}: current gid (#{@current_resource.gid}) doesnt match target gid (#{@new_resource.gid}), changing it") - opts << " -g '#{@new_resource.gid}'" + opts = [ new_resource.group_name ] + if new_resource.gid && (current_resource.gid != new_resource.gid) + logger.trace("#{new_resource}: current gid (#{current_resource.gid}) doesnt match target gid (#{new_resource.gid}), changing it") + opts << "-g" + opts << new_resource.gid end opts end @@ -91,26 +88,26 @@ class Chef members_to_be_added = [ ] members_to_be_removed = [ ] - if @new_resource.append + if new_resource.append # Append is set so we will only add members given in the # members list and remove members given in the # excluded_members list. - if @new_resource.members && !@new_resource.members.empty? - @new_resource.members.each do |member| - members_to_be_added << member if !@current_resource.members.include?(member) + if new_resource.members && !new_resource.members.empty? + new_resource.members.each do |member| + members_to_be_added << member unless current_resource.members.include?(member) end end - if @new_resource.excluded_members && !@new_resource.excluded_members.empty? - @new_resource.excluded_members.each do |member| - members_to_be_removed << member if @current_resource.members.include?(member) + if new_resource.excluded_members && !new_resource.excluded_members.empty? + new_resource.excluded_members.each do |member| + members_to_be_removed << member if current_resource.members.include?(member) end end else # Append is not set so we're resetting the membership of # the group to the given members. - members_to_be_added = @new_resource.members.dup - @current_resource.members.each do |member| + members_to_be_added = new_resource.members.dup + current_resource.members.each do |member| # No need to re-add a member if it's present in the new # list of members if members_to_be_added.include? member @@ -122,13 +119,13 @@ class Chef end unless members_to_be_added.empty? - Chef::Log.debug("#{@new_resource} adding group members: #{members_to_be_added.join(',')}") - opts << " -m #{members_to_be_added.join(',')}" + logger.trace("#{new_resource} adding group members: #{members_to_be_added.join(',')}") + opts << [ "-m", members_to_be_added.join(",") ] end unless members_to_be_removed.empty? - Chef::Log.debug("#{@new_resource} removing group members: #{members_to_be_removed.join(',')}") - opts << " -d #{members_to_be_removed.join(',')}" + logger.trace("#{new_resource} removing group members: #{members_to_be_removed.join(',')}") + opts << [ "-d", members_to_be_removed.join(",") ] end opts diff --git a/lib/chef/provider/group/suse.rb b/lib/chef/provider/group/suse.rb index a79038e25e..0790d2c2d9 100644 --- a/lib/chef/provider/group/suse.rb +++ b/lib/chef/provider/group/suse.rb @@ -17,6 +17,7 @@ # require "chef/provider/group/groupadd" +require "etc" class Chef class Provider @@ -32,30 +33,48 @@ class Chef def define_resource_requirements super requirements.assert(:all_actions) do |a| - a.assertion { ::File.exists?("/usr/sbin/groupmod") } - a.failure_message Chef::Exceptions::Group, "Could not find binary /usr/sbin/groupmod for #{@new_resource.name}" + a.assertion { ::File.exist?("/usr/sbin/groupmod") } + a.failure_message Chef::Exceptions::Group, "Could not find binary /usr/sbin/groupmod for #{new_resource.name}" # No whyrun alternative: this component should be available in the base install of any given system that uses it end + + requirements.assert(:create, :manage, :modify) do |a| + a.assertion do + begin + to_add(new_resource.members).all? { |member| Etc.getpwnam(member) } + rescue + false + end + end + a.failure_message Chef::Exceptions::Group, "Could not add users #{to_add(new_resource.members).join(', ')} to #{new_resource.group_name}: one of these users does not exist" + a.whyrun "Could not find one of these users: #{to_add(new_resource.members).join(', ')}. Assuming it will be created by a prior step" + end end def set_members(members) - to_delete = @current_resource.members - members - to_delete.each do |member| + to_remove(members).each do |member| remove_member(member) end - to_add = members - @current_resource.members - to_add.each do |member| + to_add(members).each do |member| add_member(member) end end + def to_add(members) + members - current_resource.members + end + def add_member(member) - shell_out!("groupmod -A #{member} #{@new_resource.group_name}") + shell_out_compact!("groupmod", "-A", member, new_resource.group_name) + end + + def to_remove(members) + current_resource.members - members end def remove_member(member) - shell_out!("groupmod -R #{member} #{@new_resource.group_name}") + shell_out_compact!("groupmod", "-R", member, new_resource.group_name) end end diff --git a/lib/chef/provider/group/usermod.rb b/lib/chef/provider/group/usermod.rb index e4b19181aa..3874f7b4de 100644 --- a/lib/chef/provider/group/usermod.rb +++ b/lib/chef/provider/group/usermod.rb @@ -34,19 +34,19 @@ class Chef super requirements.assert(:all_actions) do |a| - a.assertion { ::File.exists?("/usr/sbin/usermod") } - a.failure_message Chef::Exceptions::Group, "Could not find binary /usr/sbin/usermod for #{@new_resource}" + a.assertion { ::File.exist?("/usr/sbin/usermod") } + a.failure_message Chef::Exceptions::Group, "Could not find binary /usr/sbin/usermod for #{new_resource}" # No whyrun alternative: this component should be available in the base install of any given system that uses it end requirements.assert(:modify, :manage) do |a| - a.assertion { @new_resource.members.empty? || @new_resource.append } + a.assertion { new_resource.members.empty? || new_resource.append } a.failure_message Chef::Exceptions::Group, "setting group members directly is not supported by #{self}, must set append true in group" # No whyrun alternative - this action is simply not supported. end requirements.assert(:all_actions) do |a| - a.assertion { @new_resource.excluded_members.empty? } + a.assertion { new_resource.excluded_members.empty? } a.failure_message Chef::Exceptions::Group, "excluded_members is not supported by #{self}" # No whyrun alternative - this action is simply not supported. end @@ -57,17 +57,16 @@ class Chef # This provider only supports adding members with # append. Only if the action is create we will go # ahead and add members. - if @new_resource.action == :create - members.each do |member| - add_member(member) - end - else + unless new_resource.action.include?(:create) raise Chef::Exceptions::UnsupportedAction, "Setting members directly is not supported by #{self}" end + members.each do |member| + add_member(member) + end end def add_member(member) - shell_out!("usermod #{append_flags} #{@new_resource.group_name} #{member}") + shell_out_compact!("usermod", append_flags, new_resource.group_name, member) end def remove_member(member) @@ -81,7 +80,7 @@ class Chef when "openbsd", "netbsd", "aix", "solaris2", "smartos", "omnios" "-G" when "solaris", "suse", "opensuse" - "-a -G" + [ "-a", "-G" ] end end diff --git a/lib/chef/provider/group/windows.rb b/lib/chef/provider/group/windows.rb index 5873e42a6b..e0cb3b804b 100644 --- a/lib/chef/provider/group/windows.rb +++ b/lib/chef/provider/group/windows.rb @@ -30,26 +30,26 @@ class Chef def initialize(new_resource, run_context) super - @net_group = Chef::Util::Windows::NetGroup.new(@new_resource.group_name) + @net_group = Chef::Util::Windows::NetGroup.new(new_resource.group_name) end def load_current_resource - @current_resource = Chef::Resource::Group.new(@new_resource.name) - @current_resource.group_name(@new_resource.group_name) + @current_resource = Chef::Resource::Group.new(new_resource.name) + current_resource.group_name(new_resource.group_name) members = nil begin members = @net_group.local_get_members - rescue => e + rescue @group_exists = false - Chef::Log.debug("#{@new_resource} group does not exist") + logger.trace("#{new_resource} group does not exist") end if members - @current_resource.members(members) + current_resource.members(members) end - @current_resource + current_resource end def create_group @@ -58,10 +58,10 @@ class Chef end def manage_group - if @new_resource.append + if new_resource.append members_to_be_added = [ ] - @new_resource.members.each do |member| - members_to_be_added << member if ! has_current_group_member?(member) && validate_member!(member) + new_resource.members.each do |member| + members_to_be_added << member if !has_current_group_member?(member) && validate_member!(member) end # local_add_members will raise ERROR_MEMBER_IN_ALIAS if a @@ -69,19 +69,19 @@ class Chef @net_group.local_add_members(members_to_be_added) unless members_to_be_added.empty? members_to_be_removed = [ ] - @new_resource.excluded_members.each do |member| - member_sid = lookup_account_name(member) + new_resource.excluded_members.each do |member| + lookup_account_name(member) members_to_be_removed << member if has_current_group_member?(member) end @net_group.local_delete_members(members_to_be_removed) unless members_to_be_removed.empty? else - @net_group.local_set_members(@new_resource.members) + @net_group.local_set_members(new_resource.members) end end def has_current_group_member?(member) member_sid = lookup_account_name(member) - @current_resource.members.include?(member_sid) + current_resource.members.include?(member_sid) end def remove_group @@ -97,12 +97,10 @@ class Chef end def lookup_account_name(account_name) - begin - Chef::ReservedNames::Win32::Security.lookup_account_name(locally_qualified_name(account_name))[1].to_s - rescue Chef::Exceptions::Win32APIError - Chef::Log.warn("SID for '#{locally_qualified_name(account_name)}' could not be found") - "" - end + Chef::ReservedNames::Win32::Security.lookup_account_name(locally_qualified_name(account_name))[1].to_s + rescue Chef::Exceptions::Win32APIError + logger.warn("SID for '#{locally_qualified_name(account_name)}' could not be found") + "" end end diff --git a/lib/chef/provider/http_request.rb b/lib/chef/provider/http_request.rb index e1ee01d9b4..885d473a7a 100644 --- a/lib/chef/provider/http_request.rb +++ b/lib/chef/provider/http_request.rb @@ -1,6 +1,6 @@ # # Author:: Adam Jacob (<adam@chef.io>) -# Copyright:: Copyright 2008-2016, Chef Software Inc. +# Copyright:: Copyright 2008-2017, Chef Software Inc. # License:: Apache License, Version 2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -27,83 +27,93 @@ class Chef attr_accessor :http - def whyrun_supported? - true - end - def load_current_resource - @http = Chef::HTTP::Simple.new(@new_resource.url) + @http = Chef::HTTP::Simple.new(new_resource.url) end - # Send a HEAD request to @new_resource.url + # Send a HEAD request to new_resource.url def action_head - message = check_message(@new_resource.message) + message = check_message(new_resource.message) # CHEF-4762: we expect a nil return value from Chef::HTTP for a "200 Success" response # and false for a "304 Not Modified" response modified = @http.head( - "#{@new_resource.url}", - @new_resource.headers + "#{new_resource.url}", + new_resource.headers ) - Chef::Log.info("#{@new_resource} HEAD to #{@new_resource.url} successful") - Chef::Log.debug("#{@new_resource} HEAD request response: #{modified}") + logger.info("#{new_resource} HEAD to #{new_resource.url} successful") + logger.trace("#{new_resource} HEAD request response: #{modified}") # :head is usually used to trigger notifications, which converge_by now does if modified != false - converge_by("#{@new_resource} HEAD to #{@new_resource.url} returned modified, trigger notifications") {} + converge_by("#{new_resource} HEAD to #{new_resource.url} returned modified, trigger notifications") {} end end - # Send a GET request to @new_resource.url + # Send a GET request to new_resource.url def action_get - converge_by("#{@new_resource} GET to #{@new_resource.url}") do + converge_by("#{new_resource} GET to #{new_resource.url}") do - message = check_message(@new_resource.message) + message = check_message(new_resource.message) body = @http.get( - "#{@new_resource.url}", - @new_resource.headers + "#{new_resource.url}", + new_resource.headers + ) + logger.info("#{new_resource} GET to #{new_resource.url} successful") + logger.trace("#{new_resource} GET request response: #{body}") + end + end + + # Send a PATCH request to new_resource.url, with the message as the payload + def action_patch + converge_by("#{new_resource} PATCH to #{new_resource.url}") do + message = check_message(new_resource.message) + body = @http.patch( + "#{new_resource.url}", + message, + new_resource.headers ) - Chef::Log.info("#{@new_resource} GET to #{@new_resource.url} successful") - Chef::Log.debug("#{@new_resource} GET request response: #{body}") + logger.info("#{new_resource} PATCH to #{new_resource.url} successful") + logger.trace("#{new_resource} PATCH request response: #{body}") end end - # Send a PUT request to @new_resource.url, with the message as the payload + # Send a PUT request to new_resource.url, with the message as the payload def action_put - converge_by("#{@new_resource} PUT to #{@new_resource.url}") do - message = check_message(@new_resource.message) + converge_by("#{new_resource} PUT to #{new_resource.url}") do + message = check_message(new_resource.message) body = @http.put( - "#{@new_resource.url}", + "#{new_resource.url}", message, - @new_resource.headers + new_resource.headers ) - Chef::Log.info("#{@new_resource} PUT to #{@new_resource.url} successful") - Chef::Log.debug("#{@new_resource} PUT request response: #{body}") + logger.info("#{new_resource} PUT to #{new_resource.url} successful") + logger.trace("#{new_resource} PUT request response: #{body}") end end - # Send a POST request to @new_resource.url, with the message as the payload + # Send a POST request to new_resource.url, with the message as the payload def action_post - converge_by("#{@new_resource} POST to #{@new_resource.url}") do - message = check_message(@new_resource.message) + converge_by("#{new_resource} POST to #{new_resource.url}") do + message = check_message(new_resource.message) body = @http.post( - "#{@new_resource.url}", + "#{new_resource.url}", message, - @new_resource.headers + new_resource.headers ) - Chef::Log.info("#{@new_resource} POST to #{@new_resource.url} message: #{message.inspect} successful") - Chef::Log.debug("#{@new_resource} POST request response: #{body}") + logger.info("#{new_resource} POST to #{new_resource.url} message: #{message.inspect} successful") + logger.trace("#{new_resource} POST request response: #{body}") end end - # Send a DELETE request to @new_resource.url + # Send a DELETE request to new_resource.url def action_delete - converge_by("#{@new_resource} DELETE to #{@new_resource.url}") do + converge_by("#{new_resource} DELETE to #{new_resource.url}") do body = @http.delete( - "#{@new_resource.url}", - @new_resource.headers + "#{new_resource.url}", + new_resource.headers ) - @new_resource.updated_by_last_action(true) - Chef::Log.info("#{@new_resource} DELETE to #{@new_resource.url} successful") - Chef::Log.debug("#{@new_resource} DELETE request response: #{body}") + new_resource.updated_by_last_action(true) + logger.info("#{new_resource} DELETE to #{new_resource.url} successful") + logger.trace("#{new_resource} DELETE request response: #{body}") end end diff --git a/lib/chef/provider/ifconfig.rb b/lib/chef/provider/ifconfig.rb index 4cfb257bb9..243c8ee9c3 100644 --- a/lib/chef/provider/ifconfig.rb +++ b/lib/chef/provider/ifconfig.rb @@ -17,32 +17,24 @@ # require "chef/log" -require "chef/mixin/command" require "chef/mixin/shell_out" require "chef/provider" require "chef/resource/file" require "chef/exceptions" require "erb" -# Recipe example: -# -# int = {Hash with your network settings...} -# -# ifconfig int['ip'] do -# ignore_failure true -# device int['dev'] -# mask int['mask'] -# gateway int['gateway'] -# mtu int['mtu'] -# end - class Chef class Provider + # use the ifconfig resource to manage interfaces on *nix systems + # + # @example set a static ip on eth1 + # ifconfig '33.33.33.80' do + # device 'eth1' + # end class Ifconfig < Chef::Provider provides :ifconfig include Chef::Mixin::ShellOut - include Chef::Mixin::Command attr_accessor :config_template attr_accessor :config_path @@ -53,43 +45,106 @@ class Chef @config_path = nil end - def whyrun_supported? - true - end - def load_current_resource - @current_resource = Chef::Resource::Ifconfig.new(@new_resource.name) + @current_resource = Chef::Resource::Ifconfig.new(new_resource.name) @ifconfig_success = true @interfaces = {} - @status = shell_out("ifconfig") - @status.stdout.each_line do |line| - if !line[0..9].strip.empty? - @int_name = line[0..9].strip - @interfaces[@int_name] = { "hwaddr" => (line =~ /(HWaddr)/ ? ($') : "nil").strip.chomp } - else - @interfaces[@int_name]["inet_addr"] = (line =~ /inet addr:(\S+)/ ? ($1) : "nil") if line =~ /inet addr:/ - @interfaces[@int_name]["bcast"] = (line =~ /Bcast:(\S+)/ ? ($1) : "nil") if line =~ /Bcast:/ - @interfaces[@int_name]["mask"] = (line =~ /Mask:(\S+)/ ? ($1) : "nil") if line =~ /Mask:/ - @interfaces[@int_name]["mtu"] = (line =~ /MTU:(\S+)/ ? ($1) : "nil") if line =~ /MTU:/ - @interfaces[@int_name]["metric"] = (line =~ /Metric:(\S+)/ ? ($1) : "nil") if line =~ /Metric:/ + @ifconfig_version = nil + + @net_tools_version = shell_out("ifconfig --version") + @net_tools_version.stderr.each_line do |line| + if line =~ /^net-tools (\d+.\d+)/ + @ifconfig_version = line.match(/^net-tools (\d+.\d+)/)[1] + end + end + + if @ifconfig_version.nil? + raise "net-tools not found - this is required for ifconfig" + elsif @ifconfig_version.to_f < 2.0 + # Example output for 1.60 is as follows: (sanitized but format intact) + # eth0 Link encap:Ethernet HWaddr 00:00:00:00:00:00 + # inet addr:192.168.1.1 Bcast:192.168.0.1 Mask:255.255.248.0 + # inet6 addr: 0000::00:0000:0000:0000/64 Scope:Link + # UP BROADCAST RUNNING MULTICAST MTU:1500 Metric:1 + # RX packets:65158911 errors:0 dropped:0 overruns:0 frame:0 + # TX packets:41723949 errors:0 dropped:0 overruns:0 carrier:0 + # collisions:0 txqueuelen:1000 + # RX bytes:42664658792 (39.7 GiB) TX bytes:52722603938 (49.1 GiB) + # Interrupt:30 + @status = shell_out("ifconfig") + @status.stdout.each_line do |line| + if !line[0..9].strip.empty? + @int_name = line[0..9].strip + @interfaces[@int_name] = { "hwaddr" => (line =~ /(HWaddr)/ ? ($') : "nil").strip.chomp } + else + @interfaces[@int_name]["inet_addr"] = (line =~ /inet addr:(\S+)/ ? Regexp.last_match(1) : "nil") if line =~ /inet addr:/ + @interfaces[@int_name]["bcast"] = (line =~ /Bcast:(\S+)/ ? Regexp.last_match(1) : "nil") if line =~ /Bcast:/ + @interfaces[@int_name]["mask"] = (line =~ /Mask:(\S+)/ ? Regexp.last_match(1) : "nil") if line =~ /Mask:/ + @interfaces[@int_name]["mtu"] = (line =~ /MTU:(\S+)/ ? Regexp.last_match(1) : "nil") if line =~ /MTU:/ + @interfaces[@int_name]["metric"] = (line =~ /Metric:(\S+)/ ? Regexp.last_match(1) : "nil") if line =~ /Metric:/ + end + + next unless @interfaces.key?(new_resource.device) + @interface = @interfaces.fetch(new_resource.device) + + current_resource.target(new_resource.target) + current_resource.device(new_resource.device) + current_resource.inet_addr(@interface["inet_addr"]) + current_resource.hwaddr(@interface["hwaddr"]) + current_resource.bcast(@interface["bcast"]) + current_resource.mask(@interface["mask"]) + current_resource.mtu(@interface["mtu"]) + current_resource.metric(@interface["metric"]) end + elsif @ifconfig_version.to_f >= 2.0 + # Example output for 2.10-alpha is as follows: (sanitized but format intact) + # eth0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 1500 + # inet 192.168.1.1 netmask 255.255.240.0 broadcast 192.168.0.1 + # inet6 0000::0000:000:0000:0000 prefixlen 64 scopeid 0x20<link> + # ether 00:00:00:00:00:00 txqueuelen 1000 (Ethernet) + # RX packets 2383836 bytes 1642630840 (1.5 GiB) + # RX errors 0 dropped 0 overruns 0 frame 0 + # TX packets 1244218 bytes 977339327 (932.0 MiB) + # TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0 + @status = shell_out("ifconfig") + @status.stdout.each_line do |line| + addr_regex = /^(\w+):?(\d*):?\ .+$/ + if line =~ addr_regex + if line.match(addr_regex).nil? + @int_name = "nil" + elsif line.match(addr_regex)[2] == "" + @int_name = line.match(addr_regex)[1] + @interfaces[@int_name] = Hash.new + @interfaces[@int_name]["mtu"] = (line =~ /mtu (\S+)/ ? Regexp.last_match(1) : "nil") if line =~ /mtu/ && @interfaces[@int_name]["mtu"].nil? + else + @int_name = "#{line.match(addr_regex)[1]}:#{line.match(addr_regex)[2]}" + @interfaces[@int_name] = Hash.new + @interfaces[@int_name]["mtu"] = (line =~ /mtu (\S+)/ ? Regexp.last_match(1) : "nil") if line =~ /mtu/ && @interfaces[@int_name]["mtu"].nil? + end + else + @interfaces[@int_name]["inet_addr"] = (line =~ /inet (\S+)/ ? Regexp.last_match(1) : "nil") if line =~ /inet/ && @interfaces[@int_name]["inet_addr"].nil? + @interfaces[@int_name]["bcast"] = (line =~ /broadcast (\S+)/ ? Regexp.last_match(1) : "nil") if line =~ /broadcast/ && @interfaces[@int_name]["bcast"].nil? + @interfaces[@int_name]["mask"] = (line =~ /netmask (\S+)/ ? Regexp.last_match(1) : "nil") if line =~ /netmask/ && @interfaces[@int_name]["mask"].nil? + @interfaces[@int_name]["hwaddr"] = (line =~ /ether (\S+)/ ? Regexp.last_match(1) : "nil") if line =~ /ether/ && @interfaces[@int_name]["hwaddr"].nil? + @interfaces[@int_name]["metric"] = (line =~ /Metric:(\S+)/ ? Regexp.last_match(1) : "nil") if line =~ /Metric:/ && @interfaces[@int_name]["metric"].nil? + end + + next unless @interfaces.key?(new_resource.device) + @interface = @interfaces.fetch(new_resource.device) - if @interfaces.has_key?(@new_resource.device) - @interface = @interfaces.fetch(@new_resource.device) - - @current_resource.target(@new_resource.target) - @current_resource.device(@new_resource.device) - @current_resource.inet_addr(@interface["inet_addr"]) - @current_resource.hwaddr(@interface["hwaddr"]) - @current_resource.bcast(@interface["bcast"]) - @current_resource.mask(@interface["mask"]) - @current_resource.mtu(@interface["mtu"]) - @current_resource.metric(@interface["metric"]) + current_resource.target(new_resource.target) + current_resource.device(new_resource.device) + current_resource.inet_addr(@interface["inet_addr"]) + current_resource.hwaddr(@interface["hwaddr"]) + current_resource.bcast(@interface["bcast"]) + current_resource.mask(@interface["mask"]) + current_resource.mtu(@interface["mtu"]) + current_resource.metric(@interface["metric"]) end end - @current_resource + current_resource end def define_resource_requirements @@ -104,14 +159,12 @@ class Chef def action_add # check to see if load_current_resource found interface in ifconfig - unless @current_resource.inet_addr - unless @new_resource.device == loopback_device + unless current_resource.inet_addr + unless new_resource.device == loopback_device command = add_command - converge_by ("run #{command} to add #{@new_resource}") do - run_command( - :command => command - ) - Chef::Log.info("#{@new_resource} added") + converge_by("run #{command.join(' ')} to add #{new_resource}") do + shell_out_compact!(command) + logger.info("#{new_resource} added") end end end @@ -122,31 +175,25 @@ class Chef def action_enable # check to see if load_current_resource found ifconfig # enables, but does not manage config files - unless @current_resource.inet_addr - unless @new_resource.device == loopback_device - command = enable_command - converge_by ("run #{command} to enable #{@new_resource}") do - run_command( - :command => command - ) - Chef::Log.info("#{@new_resource} enabled") - end - end + return if current_resource.inet_addr + return if new_resource.device == loopback_device + command = enable_command + converge_by("run #{command.join(' ')} to enable #{new_resource}") do + shell_out_compact!(command) + logger.info("#{new_resource} enabled") end end def action_delete # check to see if load_current_resource found the interface - if @current_resource.device + if current_resource.device command = delete_command - converge_by ("run #{command} to delete #{@new_resource}") do - run_command( - :command => command - ) - Chef::Log.info("#{@new_resource} deleted") + converge_by("run #{command.join(' ')} to delete #{new_resource}") do + shell_out_compact!(command) + logger.info("#{new_resource} deleted") end else - Chef::Log.debug("#{@new_resource} does not exist - nothing to do") + logger.trace("#{new_resource} does not exist - nothing to do") end delete_config end @@ -154,21 +201,19 @@ class Chef def action_disable # check to see if load_current_resource found the interface # disables, but leaves config files in place. - if @current_resource.device + if current_resource.device command = disable_command - converge_by ("run #{command} to disable #{@new_resource}") do - run_command( - :command => command - ) - Chef::Log.info("#{@new_resource} disabled") + converge_by("run #{command.join(' ')} to disable #{new_resource}") do + shell_out_compact!(command) + logger.info("#{new_resource} disabled") end else - Chef::Log.debug("#{@new_resource} does not exist - nothing to do") + logger.trace("#{new_resource} does not exist - nothing to do") end end def can_generate_config? - ! @config_template.nil? && ! @config_path.nil? + !@config_template.nil? && !@config_path.nil? end def resource_for_config(path) @@ -182,40 +227,40 @@ class Chef config = resource_for_config(@config_path) config.content(template.result(b)) config.run_action(:create) - @new_resource.updated_by_last_action(true) if config.updated? + new_resource.updated_by_last_action(true) if config.updated? end def delete_config return unless can_generate_config? config = resource_for_config(@config_path) config.run_action(:delete) - @new_resource.updated_by_last_action(true) if config.updated? + new_resource.updated_by_last_action(true) if config.updated? end private def add_command - command = "ifconfig #{@new_resource.device} #{@new_resource.target}" - command << " netmask #{@new_resource.mask}" if @new_resource.mask - command << " metric #{@new_resource.metric}" if @new_resource.metric - command << " mtu #{@new_resource.mtu}" if @new_resource.mtu + command = [ "ifconfig", new_resource.device, new_resource.target ] + command += [ "netmask", new_resource.mask ] if new_resource.mask + command += [ "metric", new_resource.metric ] if new_resource.metric + command += [ "mtu", new_resource.mtu ] if new_resource.mtu command end def enable_command - command = "ifconfig #{@new_resource.device} #{@new_resource.target}" - command << " netmask #{@new_resource.mask}" if @new_resource.mask - command << " metric #{@new_resource.metric}" if @new_resource.metric - command << " mtu #{@new_resource.mtu}" if @new_resource.mtu + command = [ "ifconfig", new_resource.device, new_resource.target ] + command += [ "netmask", new_resource.mask ] if new_resource.mask + command += [ "metric", new_resource.metric ] if new_resource.metric + command += [ "mtu", new_resource.mtu ] if new_resource.mtu command end def disable_command - "ifconfig #{@new_resource.device} down" + [ "ifconfig", new_resource.device, "down" ] end def delete_command - "ifconfig #{@new_resource.device} down" + [ "ifconfig", new_resource.device, "down" ] end def loopback_device diff --git a/lib/chef/provider/ifconfig/aix.rb b/lib/chef/provider/ifconfig/aix.rb index 81164db304..b68c5d5364 100644 --- a/lib/chef/provider/ifconfig/aix.rb +++ b/lib/chef/provider/ifconfig/aix.rb @@ -22,64 +22,59 @@ class Chef class Provider class Ifconfig class Aix < Chef::Provider::Ifconfig - provides :ifconfig, platform: %w{aix} + provides :ifconfig, platform: "aix" def load_current_resource - @current_resource = Chef::Resource::Ifconfig.new(@new_resource.name) + @current_resource = Chef::Resource::Ifconfig.new(new_resource.name) @interface_exists = false found_interface = false interface = {} - @status = shell_out("ifconfig -a") + @status = shell_out_compact("ifconfig", "-a") @status.stdout.each_line do |line| if !found_interface if line =~ /^(\S+):\sflags=(\S+)/ - # We have interface name, if this is the interface for @current_resource, load info else skip till next interface is found. - if $1 == @new_resource.device + # We have interface name, if this is the interface for current_resource, load info else skip till next interface is found. + if Regexp.last_match(1) == new_resource.device # Found interface found_interface = true @interface_exists = true - @current_resource.target(@new_resource.target) - @current_resource.device($1) - interface[:flags] = $2 - @current_resource.metric($1) if line =~ /metric\s(\S+)/ - end - end - else - # parse interface related information, stop when next interface is found. - if line =~ /^(\S+):\sflags=(\S+)/ - # we are done parsing interface info and hit another one, so stop. - found_interface = false - break - else - if found_interface - # read up interface info - @current_resource.inet_addr($1) if line =~ /inet\s(\S+)\s/ - @current_resource.bcast($1) if line =~ /broadcast\s(\S+)/ - @current_resource.mask(hex_to_dec_netmask($1)) if line =~ /netmask\s(\S+)\s/ + current_resource.target(new_resource.target) + current_resource.device(Regexp.last_match(1)) + interface[:flags] = Regexp.last_match(2) + current_resource.metric(Regexp.last_match(1)) if line =~ /metric\s(\S+)/ end end + elsif line =~ /^(\S+):\sflags=(\S+)/ + # we are done parsing interface info and hit another one, so stop. + found_interface = false + break + elsif found_interface + # read up interface info + current_resource.inet_addr(Regexp.last_match(1)) if line =~ /inet\s(\S+)\s/ + current_resource.bcast(Regexp.last_match(1)) if line =~ /broadcast\s(\S+)/ + current_resource.mask(hex_to_dec_netmask(Regexp.last_match(1))) if line =~ /netmask\s(\S+)\s/ end end - @current_resource + current_resource end private def add_command # ifconfig changes are temporary, chdev persist across reboots. - raise Chef::Exceptions::Ifconfig, "interface metric attribute cannot be set for :add action" if @new_resource.metric - command = "chdev -l #{@new_resource.device} -a netaddr=#{@new_resource.name}" - command << " -a netmask=#{@new_resource.mask}" if @new_resource.mask - command << " -a mtu=#{@new_resource.mtu}" if @new_resource.mtu + raise Chef::Exceptions::Ifconfig, "interface metric attribute cannot be set for :add action" if new_resource.metric + command = [ "chdev", "-l", new_resource.device, "-a", "netaddr=#{new_resource.name}" ] + command += [ "-a", "netmask=#{new_resource.mask}" ] if new_resource.mask + command += [ "-a", "mtu=#{new_resource.mtu}" ] if new_resource.mtu command end def delete_command # ifconfig changes are temporary, chdev persist across reboots. - "chdev -l #{@new_resource.device} -a state=down" + [ "chdev", "-l", new_resource.device, "-a", "state=down" ] end def loopback_device diff --git a/lib/chef/provider/ifconfig/debian.rb b/lib/chef/provider/ifconfig/debian.rb index 872b0db152..aee3ca02dc 100644 --- a/lib/chef/provider/ifconfig/debian.rb +++ b/lib/chef/provider/ifconfig/debian.rb @@ -26,32 +26,32 @@ class Chef provides :ifconfig, platform: %w{ubuntu}, platform_version: ">= 11.10" provides :ifconfig, platform: %w{debian}, platform_version: ">= 7.0" - INTERFACES_FILE = "/etc/network/interfaces" - INTERFACES_DOT_D_DIR = "/etc/network/interfaces.d" + INTERFACES_FILE = "/etc/network/interfaces".freeze + INTERFACES_DOT_D_DIR = "/etc/network/interfaces.d".freeze def initialize(new_resource, run_context) super(new_resource, run_context) @config_template = %{ -<% if @new_resource.device %> -<% if @new_resource.onboot == "yes" %>auto <%= @new_resource.device %><% end %> -<% case @new_resource.bootproto +<% if new_resource.device %> +<% if new_resource.onboot == "yes" %>auto <%= new_resource.device %><% end %> +<% case new_resource.bootproto when "dhcp" %> -iface <%= @new_resource.device %> inet dhcp +iface <%= new_resource.device %> <%= new_resource.family %> dhcp <% when "bootp" %> -iface <%= @new_resource.device %> inet bootp +iface <%= new_resource.device %> <%= new_resource.family %> bootp <% else %> -iface <%= @new_resource.device %> inet static - <% if @new_resource.target %>address <%= @new_resource.target %><% end %> - <% if @new_resource.mask %>netmask <%= @new_resource.mask %><% end %> - <% if @new_resource.network %>network <%= @new_resource.network %><% end %> - <% if @new_resource.bcast %>broadcast <%= @new_resource.bcast %><% end %> - <% if @new_resource.metric %>metric <%= @new_resource.metric %><% end %> - <% if @new_resource.hwaddr %>hwaddress <%= @new_resource.hwaddr %><% end %> - <% if @new_resource.mtu %>mtu <%= @new_resource.mtu %><% end %> +iface <%= new_resource.device %> <%= new_resource.family %> static + <% if new_resource.target %>address <%= new_resource.target %><% end %> + <% if new_resource.mask %>netmask <%= new_resource.mask %><% end %> + <% if new_resource.network %>network <%= new_resource.network %><% end %> + <% if new_resource.bcast %>broadcast <%= new_resource.bcast %><% end %> + <% if new_resource.metric %>metric <%= new_resource.metric %><% end %> + <% if new_resource.hwaddr %>hwaddress <%= new_resource.hwaddr %><% end %> + <% if new_resource.mtu %>mtu <%= new_resource.mtu %><% end %> <% end %> <% end %> } - @config_path = "#{INTERFACES_DOT_D_DIR}/ifcfg-#{@new_resource.device}" + @config_path = "#{INTERFACES_DOT_D_DIR}/ifcfg-#{new_resource.device}" end def generate_config @@ -62,6 +62,8 @@ iface <%= @new_resource.device %> inet static protected def enforce_interfaces_dot_d_sanity + # on ubuntu 18.04 there's no interfaces file and it uses interfaces.d by default + return if ::File.directory?(INTERFACES_DOT_D_DIR) && !::File.exist?(INTERFACES_FILE) # create /etc/network/interfaces.d via dir resource (to get reporting, etc) dir = Chef::Resource::Directory.new(INTERFACES_DOT_D_DIR, run_context) dir.run_action(:create) @@ -69,12 +71,13 @@ iface <%= @new_resource.device %> inet static # roll our own file_edit resource, this will not get reported until we have a file_edit resource interfaces_dot_d_for_regexp = INTERFACES_DOT_D_DIR.gsub(/\./, '\.') # escape dots for the regexp regexp = %r{^\s*source\s+#{interfaces_dot_d_for_regexp}/\*\s*$} - unless ::File.exists?(INTERFACES_FILE) && regexp.match(IO.read(INTERFACES_FILE)) - converge_by("modifying #{INTERFACES_FILE} to source #{INTERFACES_DOT_D_DIR}") do - conf = Chef::Util::FileEdit.new(INTERFACES_FILE) - conf.insert_line_if_no_match(regexp, "source #{INTERFACES_DOT_D_DIR}/*") - conf.write_file - end + + return if ::File.exist?(INTERFACES_FILE) && regexp.match(IO.read(INTERFACES_FILE)) + + converge_by("modifying #{INTERFACES_FILE} to source #{INTERFACES_DOT_D_DIR}") do + conf = Chef::Util::FileEdit.new(INTERFACES_FILE) + conf.insert_line_if_no_match(regexp, "source #{INTERFACES_DOT_D_DIR}/*") + conf.write_file end end diff --git a/lib/chef/provider/ifconfig/redhat.rb b/lib/chef/provider/ifconfig/redhat.rb index 0c28e6407a..bf3d979e86 100644 --- a/lib/chef/provider/ifconfig/redhat.rb +++ b/lib/chef/provider/ifconfig/redhat.rb @@ -22,24 +22,28 @@ class Chef class Provider class Ifconfig class Redhat < Chef::Provider::Ifconfig - provides :ifconfig, platform_family: %w{fedora rhel} + provides :ifconfig, platform_family: %w{fedora rhel amazon} def initialize(new_resource, run_context) super(new_resource, run_context) @config_template = %{ -<% if @new_resource.device %>DEVICE=<%= @new_resource.device %><% end %> -<% if @new_resource.onboot == "yes" %>ONBOOT=<%= @new_resource.onboot %><% end %> -<% if @new_resource.bootproto %>BOOTPROTO=<%= @new_resource.bootproto %><% end %> -<% if @new_resource.target %>IPADDR=<%= @new_resource.target %><% end %> -<% if @new_resource.mask %>NETMASK=<%= @new_resource.mask %><% end %> -<% if @new_resource.network %>NETWORK=<%= @new_resource.network %><% end %> -<% if @new_resource.bcast %>BROADCAST=<%= @new_resource.bcast %><% end %> -<% if @new_resource.onparent %>ONPARENT=<%= @new_resource.onparent %><% end %> -<% if @new_resource.hwaddr %>HWADDR=<%= @new_resource.hwaddr %><% end %> -<% if @new_resource.metric %>METRIC=<%= @new_resource.metric %><% end %> -<% if @new_resource.mtu %>MTU=<%= @new_resource.mtu %><% end %> +<% if new_resource.device %>DEVICE=<%= new_resource.device %><% end %> +<% if new_resource.onboot == "yes" %>ONBOOT=<%= new_resource.onboot %><% end %> +<% if new_resource.bootproto %>BOOTPROTO=<%= new_resource.bootproto %><% end %> +<% if new_resource.target %>IPADDR=<%= new_resource.target %><% end %> +<% if new_resource.mask %>NETMASK=<%= new_resource.mask %><% end %> +<% if new_resource.network %>NETWORK=<%= new_resource.network %><% end %> +<% if new_resource.bcast %>BROADCAST=<%= new_resource.bcast %><% end %> +<% if new_resource.onparent %>ONPARENT=<%= new_resource.onparent %><% end %> +<% if new_resource.hwaddr %>HWADDR=<%= new_resource.hwaddr %><% end %> +<% if new_resource.metric %>METRIC=<%= new_resource.metric %><% end %> +<% if new_resource.mtu %>MTU=<%= new_resource.mtu %><% end %> +<% if new_resource.ethtool_opts %>ETHTOOL_OPTS="<%= new_resource.ethtool_opts %>"<% end %> +<% if new_resource.bonding_opts %>BONDING_OPTS="<%= new_resource.bonding_opts %>"<% end %> +<% if new_resource.master %>MASTER=<%= new_resource.master %><% end %> +<% if new_resource.slave %>SLAVE=<%= new_resource.slave %><% end %> } - @config_path = "/etc/sysconfig/network-scripts/ifcfg-#{@new_resource.device}" + @config_path = "/etc/sysconfig/network-scripts/ifcfg-#{new_resource.device}" end end diff --git a/lib/chef/provider/launchd.rb b/lib/chef/provider/launchd.rb index c58d4bfa34..0bac995247 100644 --- a/lib/chef/provider/launchd.rb +++ b/lib/chef/provider/launchd.rb @@ -17,7 +17,6 @@ # require "chef/provider" -require "chef/resource/launchd" require "chef/resource/file" require "chef/resource/cookbook_file" require "chef/resource/macosx_service" @@ -30,7 +29,7 @@ class Chef extend Forwardable provides :launchd, os: "darwin" - def_delegators :@new_resource, *[ + def_delegators :new_resource, *[ :backup, :cookbook, :group, @@ -85,7 +84,12 @@ class Chef manage_service(:disable) end + def action_restart + manage_service(:restart) + end + def manage_plist(action) + return unless manage_agent?(action) if source res = cookbook_file_resource else @@ -97,11 +101,30 @@ class Chef end def manage_service(action) + return unless manage_agent?(action) res = service_resource res.run_action(action) new_resource.updated_by_last_action(true) if res.updated? end + def manage_agent?(action) + # Gets UID of console_user and converts to string. + console_user = Etc.getpwuid(::File.stat("/dev/console").uid).name + root = console_user == "root" + agent = type == "agent" + invalid_action = [:delete, :disable, :enable, :restart].include?(action) + lltstype = "" + if new_resource.limit_load_to_session_type + lltstype = new_resource.limit_load_to_session_type + end + invalid_type = lltstype != "LoginWindow" + if root && agent && invalid_action && invalid_type + logger.trace("#{label}: Aqua LaunchAgents shouldn't be loaded as root") + return false + end + true + end + def service_resource res = Chef::Resource::MacosxService.new(label, run_context) res.name(label) if label @@ -115,7 +138,7 @@ class Chef res = Chef::Resource::File.new(@path, run_context) res.name(@path) if @path res.backup(backup) if backup - res.content(content) if content + res.content(content) if content? res.group(group) if group res.mode(mode) if mode res.owner(owner) if owner @@ -150,7 +173,7 @@ class Chef end def content - plist_hash = new_resource.hash || gen_hash + plist_hash = new_resource.plist_hash || gen_hash Plist::Emit.dump(plist_hash) unless plist_hash.nil? end diff --git a/lib/chef/provider/link.rb b/lib/chef/provider/link.rb index 5fce97e5b3..783d52d09a 100644 --- a/lib/chef/provider/link.rb +++ b/lib/chef/provider/link.rb @@ -1,6 +1,6 @@ # # Author:: Adam Jacob (<adam@chef.io>) -# Copyright:: Copyright 2008-2016, Chef Software Inc. +# Copyright:: Copyright 2008-2017, Chef Software Inc. # License:: Apache License, Version 2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -42,46 +42,42 @@ class Chef private :negative_complement - def whyrun_supported? - true - end - def load_current_resource - @current_resource = Chef::Resource::Link.new(@new_resource.name) - @current_resource.target_file(@new_resource.target_file) - if file_class.symlink?(@current_resource.target_file) - @current_resource.link_type(:symbolic) - @current_resource.to( - canonicalize(file_class.readlink(@current_resource.target_file)) + @current_resource = Chef::Resource::Link.new(new_resource.name) + current_resource.target_file(new_resource.target_file) + if file_class.symlink?(current_resource.target_file) + current_resource.link_type(:symbolic) + current_resource.to( + canonicalize(file_class.readlink(current_resource.target_file)) ) else - @current_resource.link_type(:hard) - if ::File.exists?(@current_resource.target_file) - if ::File.exists?(@new_resource.to) && - file_class.stat(@current_resource.target_file).ino == - file_class.stat(@new_resource.to).ino - @current_resource.to(canonicalize(@new_resource.to)) + current_resource.link_type(:hard) + if ::File.exists?(current_resource.target_file) + if ::File.exists?(new_resource.to) && + file_class.stat(current_resource.target_file).ino == + file_class.stat(new_resource.to).ino + current_resource.to(canonicalize(new_resource.to)) else - @current_resource.to("") + current_resource.to("") end end end - ScanAccessControl.new(@new_resource, @current_resource).set_all! - @current_resource + ScanAccessControl.new(new_resource, current_resource).set_all! + current_resource end def define_resource_requirements requirements.assert(:delete) do |a| a.assertion do - if @current_resource.to - @current_resource.link_type == @new_resource.link_type && - (@current_resource.link_type == :symbolic || @current_resource.to != "") + if current_resource.to + current_resource.link_type == new_resource.link_type && + (current_resource.link_type == :symbolic || current_resource.to != "") else true end end - a.failure_message Chef::Exceptions::Link, "Cannot delete #{@new_resource} at #{@new_resource.target_file}! Not a #{@new_resource.link_type} link." - a.whyrun("Would assume the link at #{@new_resource.target_file} was previously created") + a.failure_message Chef::Exceptions::Link, "Cannot delete #{new_resource} at #{new_resource.target_file}! Not a #{new_resource.link_type} link." + a.whyrun("Would assume the link at #{new_resource.target_file} was previously created") end end @@ -95,42 +91,48 @@ class Chef # to - the location to link to # target_file - the name of the link - if @current_resource.to != canonicalize(@new_resource.to) || - @current_resource.link_type != @new_resource.link_type + if current_resource.to != canonicalize(new_resource.to) || + current_resource.link_type != new_resource.link_type # Handle the case where the symlink already exists and is pointing at a valid to_file - if @current_resource.to + if current_resource.to # On Windows, to fix a symlink already pointing at a directory we must first # ::Dir.unlink the symlink (not the directory), while if we have a symlink # pointing at file we must use ::File.unlink on the symlink. # However if the new symlink will point to a file and the current symlink is pointing at a # directory we want to throw an exception and calling ::File.unlink on the directory symlink # will throw the correct ones. - if Chef::Platform.windows? && ::File.directory?(@new_resource.to) && - ::File.directory?(@current_resource.target_file) - converge_by("unlink existing windows symlink to dir at #{@new_resource.target_file}") do - ::Dir.unlink(@new_resource.target_file) + if Chef::Platform.windows? && ::File.directory?(new_resource.to) && + ::File.directory?(current_resource.target_file) + converge_by("unlink existing windows symlink to dir at #{new_resource.target_file}") do + ::Dir.unlink(new_resource.target_file) end else - converge_by("unlink existing symlink to file at #{@new_resource.target_file}") do - ::File.unlink(@new_resource.target_file) + converge_by("unlink existing symlink to file at #{new_resource.target_file}") do + ::File.unlink(new_resource.target_file) end end end - if @new_resource.link_type == :symbolic - converge_by("create symlink at #{@new_resource.target_file} to #{@new_resource.to}") do - file_class.symlink(canonicalize(@new_resource.to), @new_resource.target_file) - Chef::Log.debug("#{@new_resource} created #{@new_resource.link_type} link from #{@new_resource.target_file} -> #{@new_resource.to}") - Chef::Log.info("#{@new_resource} created") + if new_resource.link_type == :symbolic + converge_by("create symlink at #{new_resource.target_file} to #{new_resource.to}") do + file_class.symlink(canonicalize(new_resource.to), new_resource.target_file) + logger.trace("#{new_resource} created #{new_resource.link_type} link from #{new_resource.target_file} -> #{new_resource.to}") + logger.info("#{new_resource} created") + # file_class.symlink will create the link with default access controls. + # This means that the access controls of the file could be different + # than those captured during the initial evaluation of current_resource. + # We need to re-evaluate the current_resource to ensure that the desired + # access controls are applied. + ScanAccessControl.new(new_resource, current_resource).set_all! end - elsif @new_resource.link_type == :hard - converge_by("create hard link at #{@new_resource.target_file} to #{@new_resource.to}") do - file_class.link(@new_resource.to, @new_resource.target_file) - Chef::Log.debug("#{@new_resource} created #{@new_resource.link_type} link from #{@new_resource.target_file} -> #{@new_resource.to}") - Chef::Log.info("#{@new_resource} created") + elsif new_resource.link_type == :hard + converge_by("create hard link at #{new_resource.target_file} to #{new_resource.to}") do + file_class.link(new_resource.to, new_resource.target_file) + logger.trace("#{new_resource} created #{new_resource.link_type} link from #{new_resource.target_file} -> #{new_resource.to}") + logger.info("#{new_resource} created") end end end - if @new_resource.link_type == :symbolic + if new_resource.link_type == :symbolic if access_controls.requires_changes? converge_by(access_controls.describe_changes) do access_controls.set_all @@ -140,10 +142,17 @@ class Chef end def action_delete - if @current_resource.to # Exists - converge_by("delete link at #{@new_resource.target_file}") do - ::File.delete(@new_resource.target_file) - Chef::Log.info("#{@new_resource} deleted") + if current_resource.to # Exists + if Chef::Platform.windows? && ::File.directory?(current_resource.target_file) + converge_by("delete link to dir at #{new_resource.target_file}") do + ::Dir.delete(new_resource.target_file) + logger.info("#{new_resource} deleted") + end + else + converge_by("delete link to file at #{new_resource.target_file}") do + ::File.delete(new_resource.target_file) + logger.info("#{new_resource} deleted") + end end end end @@ -152,7 +161,7 @@ class Chef # access control (e.g., use lchmod instead of chmod) if the resource is a # symlink. def manage_symlink_access? - @new_resource.link_type == :symbolic + new_resource.link_type == :symbolic end end end diff --git a/lib/chef/provider/log.rb b/lib/chef/provider/log.rb index eef4077c07..8d15883ff6 100644 --- a/lib/chef/provider/log.rb +++ b/lib/chef/provider/log.rb @@ -1,6 +1,6 @@ # # Author:: Cary Penniman (<cary@rightscale.com>) -# Copyright:: Copyright 2008-2016, Chef Software Inc. +# Copyright:: Copyright 2008-2017, Chef Software Inc. # License:: Apache License, Version 2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,41 +17,27 @@ # class Chef - class Provider - class Log - - # Chef log provider, allows logging to chef's logs from recipes + # Chef log provider, allows logging to chef's logs class ChefLog < Chef::Provider - provides :log - def whyrun_supported? - true - end - # No concept of a 'current' resource for logs, this is a no-op # - # === Return - # true:: Always return true + # @return [true] Always returns true def load_current_resource true end # Write the log to Chef's log # - # === Return - # true:: Always return true + # @return [true] Always returns true def action_write - Chef::Log.send(@new_resource.level, @new_resource.message) - @new_resource.updated_by_last_action(true) + logger.send(new_resource.level, new_resource.message) + new_resource.updated_by_last_action(true) if Chef::Config[:count_log_resource_updates] end - end - end - end - end diff --git a/lib/chef/provider/lwrp_base.rb b/lib/chef/provider/lwrp_base.rb index f5ba30ba15..70f2af6539 100644 --- a/lib/chef/provider/lwrp_base.rb +++ b/lib/chef/provider/lwrp_base.rb @@ -34,7 +34,6 @@ class Chef # These were previously provided by Chef::Mixin::RecipeDefinitionDSLCore. # They are not included by its replacement, Chef::DSL::Recipe, but # they may be used in existing LWRPs. - include Chef::DSL::PlatformIntrospection include Chef::DSL::DataQuery # Allow include_recipe from within LWRP provider code @@ -53,7 +52,7 @@ class Chef def build_from_file(cookbook_name, filename, run_context) if LWRPBase.loaded_lwrps[filename] - Chef::Log.debug("LWRP provider #{filename} from cookbook #{cookbook_name} has already been loaded! Skipping the reload.") + Chef::Log.trace("LWRP provider #{filename} from cookbook #{cookbook_name} has already been loaded! Skipping the reload.") return loaded_lwrps[filename] end @@ -72,22 +71,13 @@ class Chef define_singleton_method(:inspect) { to_s } end - Chef::Log.debug("Loaded contents of #{filename} into provider #{resource_name} (#{provider_class})") + Chef::Log.trace("Loaded contents of #{filename} into provider #{resource_name} (#{provider_class})") LWRPBase.loaded_lwrps[filename] = true - Chef::Provider.register_deprecated_lwrp_class(provider_class, convert_to_class_name(resource_name)) - provider_class end - # DSL for defining a provider's actions. - def action(name, &block) - define_method("action_#{name}") do - instance_eval(&block) - end - end - protected def loaded_lwrps diff --git a/lib/chef/provider/mdadm.rb b/lib/chef/provider/mdadm.rb index f8225ff63a..c5f15a851a 100644 --- a/lib/chef/provider/mdadm.rb +++ b/lib/chef/provider/mdadm.rb @@ -25,66 +25,58 @@ class Chef provides :mdadm - def popen4 - raise Exception, "deprecated" - end - - def whyrun_supported? - true - end - def load_current_resource - @current_resource = Chef::Resource::Mdadm.new(@new_resource.name) - @current_resource.raid_device(@new_resource.raid_device) - Chef::Log.debug("#{@new_resource} checking for software raid device #{@current_resource.raid_device}") + @current_resource = Chef::Resource::Mdadm.new(new_resource.name) + current_resource.raid_device(new_resource.raid_device) + logger.trace("#{new_resource} checking for software raid device #{current_resource.raid_device}") device_not_found = 4 - mdadm = shell_out!("mdadm --detail --test #{@new_resource.raid_device}", :returns => [0, device_not_found]) + mdadm = shell_out!("mdadm --detail --test #{new_resource.raid_device}", :returns => [0, device_not_found]) exists = (mdadm.status == 0) - @current_resource.exists(exists) + current_resource.exists(exists) end def action_create - unless @current_resource.exists + unless current_resource.exists converge_by("create RAID device #{new_resource.raid_device}") do - command = "yes | mdadm --create #{@new_resource.raid_device} --level #{@new_resource.level}" - command << " --chunk=#{@new_resource.chunk}" unless @new_resource.level == 1 - command << " --metadata=#{@new_resource.metadata}" - command << " --bitmap=#{@new_resource.bitmap}" if @new_resource.bitmap - command << " --layout=#{@new_resource.layout}" if @new_resource.layout - command << " --raid-devices #{@new_resource.devices.length} #{@new_resource.devices.join(" ")}" - Chef::Log.debug("#{@new_resource} mdadm command: #{command}") + command = "yes | mdadm --create #{new_resource.raid_device} --level #{new_resource.level}" + command << " --chunk=#{new_resource.chunk}" unless new_resource.level == 1 + command << " --metadata=#{new_resource.metadata}" + command << " --bitmap=#{new_resource.bitmap}" if new_resource.bitmap + command << " --layout=#{new_resource.layout}" if new_resource.layout + command << " --raid-devices #{new_resource.devices.length} #{new_resource.devices.join(" ")}" + logger.trace("#{new_resource} mdadm command: #{command}") shell_out!(command) - Chef::Log.info("#{@new_resource} created raid device (#{@new_resource.raid_device})") + logger.info("#{new_resource} created raid device (#{new_resource.raid_device})") end else - Chef::Log.debug("#{@new_resource} raid device already exists, skipping create (#{@new_resource.raid_device})") + logger.trace("#{new_resource} raid device already exists, skipping create (#{new_resource.raid_device})") end end def action_assemble - unless @current_resource.exists + unless current_resource.exists converge_by("assemble RAID device #{new_resource.raid_device}") do - command = "yes | mdadm --assemble #{@new_resource.raid_device} #{@new_resource.devices.join(" ")}" - Chef::Log.debug("#{@new_resource} mdadm command: #{command}") + command = "yes | mdadm --assemble #{new_resource.raid_device} #{new_resource.devices.join(" ")}" + logger.trace("#{new_resource} mdadm command: #{command}") shell_out!(command) - Chef::Log.info("#{@new_resource} assembled raid device (#{@new_resource.raid_device})") + logger.info("#{new_resource} assembled raid device (#{new_resource.raid_device})") end else - Chef::Log.debug("#{@new_resource} raid device already exists, skipping assemble (#{@new_resource.raid_device})") + logger.trace("#{new_resource} raid device already exists, skipping assemble (#{new_resource.raid_device})") end end def action_stop - if @current_resource.exists + if current_resource.exists converge_by("stop RAID device #{new_resource.raid_device}") do - command = "yes | mdadm --stop #{@new_resource.raid_device}" - Chef::Log.debug("#{@new_resource} mdadm command: #{command}") + command = "yes | mdadm --stop #{new_resource.raid_device}" + logger.trace("#{new_resource} mdadm command: #{command}") shell_out!(command) - Chef::Log.info("#{@new_resource} stopped raid device (#{@new_resource.raid_device})") + logger.info("#{new_resource} stopped raid device (#{new_resource.raid_device})") end else - Chef::Log.debug("#{@new_resource} raid device doesn't exist (#{@new_resource.raid_device}) - not stopping") + logger.trace("#{new_resource} raid device doesn't exist (#{new_resource.raid_device}) - not stopping") end end diff --git a/lib/chef/provider/mount.rb b/lib/chef/provider/mount.rb index 9e9ee29bde..994d67939b 100644 --- a/lib/chef/provider/mount.rb +++ b/lib/chef/provider/mount.rb @@ -1,7 +1,7 @@ # # Author:: Joshua Timberman (<joshua@chef.io>) # Author:: Lamont Granquist (<lamont@chef.io>) -# Copyright:: Copyright 2009-2016, Chef Software, Inc. +# Copyright:: Copyright 2009-2017, Chef Software Inc. # License:: Apache License, Version 2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -28,10 +28,6 @@ class Chef attr_accessor :unmount_retries - def whyrun_supported? - true - end - def load_current_resource true end @@ -45,10 +41,10 @@ class Chef unless current_resource.mounted converge_by("mount #{current_resource.device} to #{current_resource.mount_point}") do mount_fs - Chef::Log.info("#{new_resource} mounted") + logger.info("#{new_resource} mounted") end else - Chef::Log.debug("#{new_resource} is already mounted") + logger.trace("#{new_resource} is already mounted") end end @@ -56,10 +52,10 @@ class Chef if current_resource.mounted converge_by("unmount #{current_resource.device}") do umount_fs - Chef::Log.info("#{new_resource} unmounted") + logger.info("#{new_resource} unmounted") end else - Chef::Log.debug("#{new_resource} is already unmounted") + logger.trace("#{new_resource} is already unmounted") end end @@ -68,32 +64,32 @@ class Chef if new_resource.supports[:remount] converge_by("remount #{current_resource.device}") do remount_fs - Chef::Log.info("#{new_resource} remounted") + logger.info("#{new_resource} remounted") end else converge_by("unmount #{current_resource.device}") do umount_fs - Chef::Log.info("#{new_resource} unmounted") + logger.info("#{new_resource} unmounted") end wait_until_unmounted(unmount_retries) converge_by("mount #{current_resource.device}") do mount_fs - Chef::Log.info("#{new_resource} mounted") + logger.info("#{new_resource} mounted") end end else - Chef::Log.debug("#{new_resource} not mounted, nothing to remount") + logger.trace("#{new_resource} not mounted, nothing to remount") end end def action_enable - unless current_resource.enabled && mount_options_unchanged? + unless current_resource.enabled && mount_options_unchanged? && device_unchanged? converge_by("enable #{current_resource.device}") do enable_fs - Chef::Log.info("#{new_resource} enabled") + logger.info("#{new_resource} enabled") end else - Chef::Log.debug("#{new_resource} already enabled") + logger.trace("#{new_resource} already enabled") end end @@ -101,13 +97,15 @@ class Chef if current_resource.enabled converge_by("disable #{current_resource.device}") do disable_fs - Chef::Log.info("#{new_resource} disabled") + logger.info("#{new_resource} disabled") end else - Chef::Log.debug("#{new_resource} already disabled") + logger.trace("#{new_resource} already disabled") end end + alias :action_unmount :action_umount + # # Abstract Methods to be implemented by subclasses # @@ -122,6 +120,14 @@ class Chef raise Chef::Exceptions::UnsupportedAction, "#{self} does not implement #mount_options_unchanged?" end + # It's entirely plausible that a site might prefer UUIDs or labels, so + # we need to be able to update fstab to conform with their wishes + # without necessarily needing to remount the device. + # See #6851 for more. + def device_unchanged? + @current_resource.device == @new_resource.device + end + # # NOTE: for the following methods, this superclass will already have checked if the filesystem is # enabled and/or mounted and they will be called in converge_by blocks, so most defensive checking diff --git a/lib/chef/provider/mount/aix.rb b/lib/chef/provider/mount/aix.rb index 12f0d67e6b..c1ed499957 100644 --- a/lib/chef/provider/mount/aix.rb +++ b/lib/chef/provider/mount/aix.rb @@ -1,6 +1,5 @@ # -# Author:: -# Copyright:: Copyright 2009-2016, Chef Software Inc. +# Copyright:: Copyright 2009-2018, Chef Software Inc. # License:: Apache License, Version 2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -22,14 +21,14 @@ class Chef class Provider class Mount class Aix < Chef::Provider::Mount::Mount - provides :mount, platform: %w{aix} + provides :mount, platform: "aix" # Override for aix specific handling def initialize(new_resource, run_context) super # options and fstype are set to "defaults" and "auto" respectively in the Mount Resource class. These options are not valid for AIX, override them. if @new_resource.options[0] == "defaults" - @new_resource.options.clear + @new_resource.options([]) end if @new_resource.fstype == "auto" @new_resource.send(:clear_fstype) @@ -40,30 +39,60 @@ class Chef # Check to see if there is an entry in /etc/filesystems. Last entry for a volume wins. Using command "lsfs" to fetch entries. enabled = false + regex_arr = device_fstab_regex.split(":") + if regex_arr.size == 2 + nodename = regex_arr[0] + devicename = regex_arr[1] + else + devicename = regex_arr[0] + end # lsfs o/p = #MountPoint:Device:Vfs:Nodename:Type:Size:Options:AutoMount:Acct # search only for current mount point shell_out("lsfs -c #{@new_resource.mount_point}").stdout.each_line do |line| case line when /^#\s/ next - when /^#{Regexp.escape(@new_resource.mount_point)}:#{device_fstab_regex}:(\S+):(\[\S+\])?:(\S+)?:(\S+):(\S+):(\S+):(\S+)/ + when /^#{Regexp.escape(@new_resource.mount_point)}:#{devicename}:(\S+):#{nodename}:(\S+)?:(\S+):(\S+):(\S+):(\S+)/ # mount point entry with ipv6 address for nodename (ipv6 address use ':') enabled = true @current_resource.fstype($1) - @current_resource.options($5) - Chef::Log.debug("Found mount #{device_fstab} to #{@new_resource.mount_point} in /etc/filesystems") + @current_resource.options($4) + logger.trace("Found mount point #{@new_resource.mount_point} :: device_type #{@current_resource.device_type} in /etc/filesystems") next - when /^#{Regexp.escape(@new_resource.mount_point)}:#{device_fstab_regex}::(\S+):(\S+)?:(\S+)?:(\S+):(\S+):(\S+):(\S+)/ + when /^#{Regexp.escape(@new_resource.mount_point)}:#{nodename}:(\S+)::(\S+)?:(\S+):(\S+):(\S+):(\S+)/ # mount point entry with hostname or ipv4 address enabled = true @current_resource.fstype($1) + @current_resource.options($4) + @current_resource.device("") + logger.trace("Found mount point #{@new_resource.mount_point} :: device_type #{@current_resource.device_type} in /etc/filesystems") + next + when /^#{Regexp.escape(@new_resource.mount_point)}:(\S+)?:(\S+):#{devicename}:(\S+)?:(\S+):(\S+):(\S+):(\S+)/ + # mount point entry with hostname or ipv4 address + enabled = true + @current_resource.fstype($2) @current_resource.options($5) - Chef::Log.debug("Found mount #{device_fstab} to #{@new_resource.mount_point} in /etc/filesystems") + @current_resource.device(devicename + "/") + logger.trace("Found mount point #{@new_resource.mount_point} :: device_type #{@current_resource.device_type} in /etc/filesystems") next - when /^#{Regexp.escape(@new_resource.mount_point)}/ - enabled = false - Chef::Log.debug("Found conflicting mount point #{@new_resource.mount_point} in /etc/filesystems") + when /^#{Regexp.escape(@new_resource.mount_point)}:(.*?):(.*?):(.*?):(.*?):(.*?):(.*?):(.*?):(.*?)/ + if $3.split("=")[0] == "LABEL" || $1.split("=")[0] == "LABEL" + @current_resource.device_type("label") + elsif $3.split("=")[0] == "UUID" || $1.split("=")[0] == "UUID" + @current_resource.device_type("uuid") + else + @current_resource.device_type("device") + end + + if @current_resource.device_type != @new_resource.device_type + enabled = true + logger.trace("Found mount point #{@new_resource.mount_point} :: device_type #{@current_resource.device_type} in /etc/filesystems") + else + enabled = false + logger.trace("Found conflicting mount point #{@new_resource.mount_point} in /etc/filesystems") + end end + end @current_resource.enabled(enabled) end @@ -80,10 +109,10 @@ class Chef case line when /#{search_device}\s+#{Regexp.escape(@new_resource.mount_point)}/ mounted = true - Chef::Log.debug("Special device #{device_logstring} mounted as #{@new_resource.mount_point}") + logger.trace("Special device #{device_logstring} mounted as #{@new_resource.mount_point}") when /^[\/\w]+\s+#{Regexp.escape(@new_resource.mount_point)}\s+/ mounted = false - Chef::Log.debug("Found conflicting mount point #{@new_resource.mount_point} in /etc/fstab") + logger.trace("Found conflicting mount point #{@new_resource.mount_point} in /etc/fstab") end end @current_resource.mounted(mounted) @@ -108,23 +137,23 @@ class Chef end command << " #{@new_resource.mount_point}" shell_out!(command) - Chef::Log.debug("#{@new_resource} is mounted at #{@new_resource.mount_point}") + logger.trace("#{@new_resource} is mounted at #{@new_resource.mount_point}") else - Chef::Log.debug("#{@new_resource} is already mounted at #{@new_resource.mount_point}") + logger.trace("#{@new_resource} is already mounted at #{@new_resource.mount_point}") end end def remount_command if !(@new_resource.options.nil? || @new_resource.options.empty?) - return "mount -o remount,#{@new_resource.options.join(',')} #{@new_resource.device} #{@new_resource.mount_point}" + "mount -o remount,#{@new_resource.options.join(',')} #{@new_resource.device} #{@new_resource.mount_point}" else - return "mount -o remount #{@new_resource.device} #{@new_resource.mount_point}" + "mount -o remount #{@new_resource.device} #{@new_resource.mount_point}" end end def enable_fs if @current_resource.enabled && mount_options_unchanged? - Chef::Log.debug("#{@new_resource} is already enabled - nothing to do") + logger.trace("#{@new_resource} is already enabled - nothing to do") return nil end @@ -145,10 +174,21 @@ class Chef fstab.puts("\tvfs\t\t= #{@new_resource.fstype}") fstab.puts("\tmount\t\t= false") fstab.puts "\toptions\t\t= #{@new_resource.options.join(',')}" unless @new_resource.options.nil? || @new_resource.options.empty? - Chef::Log.debug("#{@new_resource} is enabled at #{@new_resource.mount_point}") + logger.trace("#{@new_resource} is enabled at #{@new_resource.mount_point}") end end + def mount_options_unchanged? + current_resource_options = @current_resource.options.delete_if { |x| x == "rw" } + + @current_resource.device == @new_resource.device && + @current_resource.fsck_device == @new_resource.fsck_device && + @current_resource.fstype == @new_resource.fstype && + current_resource_options == @new_resource.options && + @current_resource.dump == @new_resource.dump && + @current_resource.pass == @new_resource.pass + end + def disable_fs contents = [] if @current_resource.enabled @@ -170,7 +210,7 @@ class Chef contents.each { |line| fstab.puts line } end else - Chef::Log.debug("#{@new_resource} is not enabled - nothing to do") + logger.trace("#{@new_resource} is not enabled - nothing to do") end end diff --git a/lib/chef/provider/mount/mount.rb b/lib/chef/provider/mount/mount.rb index 07da6ac361..d0da30a30a 100644 --- a/lib/chef/provider/mount/mount.rb +++ b/lib/chef/provider/mount/mount.rb @@ -47,7 +47,7 @@ class Chef elsif @new_resource.mount_point != "none" && !::File.exists?(@new_resource.mount_point) raise Chef::Exceptions::Mount, "Mount point #{@new_resource.mount_point} does not exist" end - return true + true end def enabled? @@ -57,17 +57,18 @@ class Chef case line when /^[#\s]/ next - when /^#{device_fstab_regex}\s+#{Regexp.escape(@new_resource.mount_point)}\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)/ + when /^(#{device_fstab_regex})\s+#{Regexp.escape(@new_resource.mount_point)}\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)/ enabled = true - @current_resource.fstype($1) - @current_resource.options($2) - @current_resource.dump($3.to_i) - @current_resource.pass($4.to_i) - Chef::Log.debug("Found mount #{device_fstab} to #{@new_resource.mount_point} in /etc/fstab") + @current_resource.device($1) + @current_resource.fstype($2) + @current_resource.options($3) + @current_resource.dump($4.to_i) + @current_resource.pass($5.to_i) + logger.trace("Found mount #{device_fstab} to #{@new_resource.mount_point} in /etc/fstab") next when /^[\/\w]+\s+#{Regexp.escape(@new_resource.mount_point)}\s+/ enabled = false - Chef::Log.debug("Found conflicting mount point #{@new_resource.mount_point} in /etc/fstab") + logger.trace("Found conflicting mount point #{@new_resource.mount_point} in /etc/fstab") end end @current_resource.enabled(enabled) @@ -89,10 +90,10 @@ class Chef case line when /^#{device_mount_regex}\s+on\s+#{Regexp.escape(real_mount_point)}\s/ mounted = true - Chef::Log.debug("Special device #{device_logstring} mounted as #{real_mount_point}") + logger.trace("Special device #{device_logstring} mounted as #{real_mount_point}") when /^([\/\w])+\son\s#{Regexp.escape(real_mount_point)}\s+/ mounted = false - Chef::Log.debug("Special device #{$~[1]} mounted as #{real_mount_point}") + logger.trace("Special device #{$~[1]} mounted as #{real_mount_point}") end end @current_resource.mounted(mounted) @@ -113,42 +114,47 @@ class Chef end command << " #{@new_resource.mount_point}" shell_out!(command) - Chef::Log.debug("#{@new_resource} is mounted at #{@new_resource.mount_point}") + logger.trace("#{@new_resource} is mounted at #{@new_resource.mount_point}") else - Chef::Log.debug("#{@new_resource} is already mounted at #{@new_resource.mount_point}") + logger.trace("#{@new_resource} is already mounted at #{@new_resource.mount_point}") end end def umount_fs if @current_resource.mounted shell_out!("umount #{@new_resource.mount_point}") - Chef::Log.debug("#{@new_resource} is no longer mounted at #{@new_resource.mount_point}") + logger.trace("#{@new_resource} is no longer mounted at #{@new_resource.mount_point}") else - Chef::Log.debug("#{@new_resource} is not mounted at #{@new_resource.mount_point}") + logger.trace("#{@new_resource} is not mounted at #{@new_resource.mount_point}") end end def remount_command - return "mount -o remount,#{@new_resource.options.join(',')} #{@new_resource.mount_point}" + "mount -o remount,#{@new_resource.options.join(',')} #{@new_resource.mount_point}" end def remount_fs if @current_resource.mounted && @new_resource.supports[:remount] shell_out!(remount_command) @new_resource.updated_by_last_action(true) - Chef::Log.debug("#{@new_resource} is remounted at #{@new_resource.mount_point}") + logger.trace("#{@new_resource} is remounted at #{@new_resource.mount_point}") elsif @current_resource.mounted umount_fs sleep 1 mount_fs else - Chef::Log.debug("#{@new_resource} is not mounted at #{@new_resource.mount_point} - nothing to do") + logger.trace("#{@new_resource} is not mounted at #{@new_resource.mount_point} - nothing to do") end end + # Return appropriate default mount options according to the given os. + def default_mount_options + node[:os] == "linux" ? "defaults" : "rw" + end + def enable_fs - if @current_resource.enabled && mount_options_unchanged? - Chef::Log.debug("#{@new_resource} is already enabled - nothing to do") + if @current_resource.enabled && mount_options_unchanged? && device_unchanged? + logger.trace("#{@new_resource} is already enabled - nothing to do") return nil end @@ -158,8 +164,8 @@ class Chef disable_fs end ::File.open("/etc/fstab", "a") do |fstab| - fstab.puts("#{device_fstab} #{@new_resource.mount_point} #{@new_resource.fstype} #{@new_resource.options.nil? ? "defaults" : @new_resource.options.join(",")} #{@new_resource.dump} #{@new_resource.pass}") - Chef::Log.debug("#{@new_resource} is enabled at #{@new_resource.mount_point}") + fstab.puts("#{device_fstab} #{@new_resource.mount_point} #{@new_resource.fstype} #{@new_resource.options.nil? ? default_mount_options : @new_resource.options.join(",")} #{@new_resource.dump} #{@new_resource.pass}") + logger.trace("#{@new_resource} is enabled at #{@new_resource.mount_point}") end end @@ -171,7 +177,7 @@ class Chef ::File.readlines("/etc/fstab").reverse_each do |line| if !found && line =~ /^#{device_fstab_regex}\s+#{Regexp.escape(@new_resource.mount_point)}\s/ found = true - Chef::Log.debug("#{@new_resource} is removed from fstab") + logger.trace("#{@new_resource} is removed from fstab") next else contents << line @@ -182,7 +188,7 @@ class Chef contents.reverse_each { |line| fstab.puts line } end else - Chef::Log.debug("#{@new_resource} is not enabled - nothing to do") + logger.trace("#{@new_resource} is not enabled - nothing to do") end end @@ -193,7 +199,7 @@ class Chef def device_should_exist? ( @new_resource.device != "none" ) && ( not network_device? ) && - ( not %w{ cgroup tmpfs fuse vboxsf }.include? @new_resource.fstype ) + ( not %w{ cgroup tmpfs fuse vboxsf zfs }.include? @new_resource.fstype ) end private @@ -210,7 +216,7 @@ class Chef end def device_real - if @real_device == nil + if @real_device.nil? if @new_resource.device_type == :device @real_device = @new_resource.device else @@ -253,7 +259,7 @@ class Chef if @new_resource.device_type == :device device_mount_regex else - device_fstab + Regexp.union(device_fstab, device_mount_regex) end end diff --git a/lib/chef/provider/mount/solaris.rb b/lib/chef/provider/mount/solaris.rb index a5a7a327cb..095c07432a 100644 --- a/lib/chef/provider/mount/solaris.rb +++ b/lib/chef/provider/mount/solaris.rb @@ -74,8 +74,8 @@ class Chef end def mount_fs - actual_options = options || [] - actual_options.delete("noauto") + actual_options = native_options(options) + actual_options.delete("-") command = "mount -F #{fstype}" command << " -o #{actual_options.join(',')}" unless actual_options.empty? command << " #{device} #{mount_point}" @@ -88,8 +88,8 @@ class Chef def remount_fs # FIXME: Should remount always do the remount or only if the options change? - actual_options = options || [] - actual_options.delete("noauto") + actual_options = native_options(options) + actual_options.delete("-") mount_options = actual_options.empty? ? "" : ",#{actual_options.join(',')}" shell_out!("mount -o remount#{mount_options} #{mount_point}") end @@ -112,7 +112,7 @@ class Chef else # this is likely some kind of internal error, since we should only call disable_fs when there # the filesystem we want to disable is enabled. - Chef::Log.warn("#{new_resource} did not find the mountpoint to disable in the vfstab") + logger.warn("#{new_resource} did not find the mountpoint to disable in the vfstab") end end @@ -121,8 +121,8 @@ class Chef end def mount_options_unchanged? - new_options = options_remove_noauto(options) - current_options = options_remove_noauto(current_resource.nil? ? nil : current_resource.options) + new_options = native_options(options) + current_options = native_options(current_resource.nil? ? nil : current_resource.options) current_resource.fsck_device == fsck_device && current_resource.fstype == fstype && @@ -153,10 +153,10 @@ class Chef shell_out!("mount -v").stdout.each_line do |line| case line when /^#{device_regex}\s+on\s+#{Regexp.escape(mount_point)}\s+/ - Chef::Log.debug("Special device #{device} is mounted as #{mount_point}") + logger.trace("Special device #{device} is mounted as #{mount_point}") mounted = true when /^([\/\w]+)\son\s#{Regexp.escape(mount_point)}\s+/ - Chef::Log.debug("Special device #{Regexp.last_match[1]} is mounted as #{mount_point}") + logger.trace("Special device #{Regexp.last_match[1]} is mounted as #{mount_point}") mounted = false end end @@ -168,7 +168,8 @@ class Chef def read_vfstab_status # Check to see if there is an entry in /etc/vfstab. Last entry for a volume wins. enabled = false - fstype = options = pass = nil + pass = false + fstype = options = nil ::File.foreach(VFSTAB) do |line| case line when /^[#\s]/ @@ -190,12 +191,12 @@ class Chef end end pass = (Regexp.last_match[2] == "-") ? 0 : Regexp.last_match[2].to_i - Chef::Log.debug("Found mount #{device} to #{mount_point} in #{VFSTAB}") + logger.trace("Found mount #{device} to #{mount_point} in #{VFSTAB}") next when /^[-\/\w]+\s+[-\/\w]+\s+#{Regexp.escape(mount_point)}\s+/ # if we find a mountpoint on top of our mountpoint, then we are not enabled enabled = false - Chef::Log.debug("Found conflicting mount point #{mount_point} in #{VFSTAB}") + logger.trace("Found conflicting mount point #{mount_point} in #{VFSTAB}") end end [enabled, fstype, options, pass] @@ -220,11 +221,7 @@ class Chef end def vfstab_entry - actual_options = unless options.nil? - tempops = options.dup - tempops.delete("noauto") - tempops - end + actual_options = native_options(options) autostr = mount_at_boot? ? "yes" : "no" passstr = pass == 0 ? "-" : pass optstr = (actual_options.nil? || actual_options.empty?) ? "-" : actual_options.join(",") @@ -237,7 +234,7 @@ class Chef ::File.readlines(VFSTAB).reverse_each do |line| if !found && line =~ /^#{device_regex}\s+\S+\s+#{Regexp.escape(mount_point)}/ found = true - Chef::Log.debug("#{new_resource} is removed from vfstab") + logger.trace("#{new_resource} is removed from vfstab") next end contents << line @@ -251,11 +248,15 @@ class Chef contents << vfstab_entry end - def options_remove_noauto(temp_options) - new_options = [] - new_options += temp_options.nil? ? [] : temp_options - new_options.delete("noauto") - new_options + def native_options(temp_options) + if temp_options == %w{defaults} + ["-"] + else + new_options = [] + new_options += temp_options.nil? ? [] : temp_options.dup + new_options.delete("noauto") + new_options + end end def device_regex diff --git a/lib/chef/provider/mount/windows.rb b/lib/chef/provider/mount/windows.rb index 0fb5aa7645..1bd932729d 100644 --- a/lib/chef/provider/mount/windows.rb +++ b/lib/chef/provider/mount/windows.rb @@ -47,15 +47,15 @@ class Chef @current_resource = Chef::Resource::Mount.new(@new_resource.name) @current_resource.mount_point(@new_resource.mount_point) - Chef::Log.debug("Checking for mount point #{@current_resource.mount_point}") + logger.trace("Checking for mount point #{@current_resource.mount_point}") begin @current_resource.device(@mount.device) - Chef::Log.debug("#{@current_resource.device} mounted on #{@new_resource.mount_point}") + logger.trace("#{@current_resource.device} mounted on #{@new_resource.mount_point}") @current_resource.mounted(true) rescue ArgumentError => e @current_resource.mounted(false) - Chef::Log.debug("#{@new_resource.mount_point} is not mounted: #{e.message}") + logger.trace("#{@new_resource.mount_point} is not mounted: #{e.message}") end end @@ -65,18 +65,18 @@ class Chef :username => @new_resource.username, :domainname => @new_resource.domain, :password => @new_resource.password) - Chef::Log.debug("#{@new_resource} is mounted at #{@new_resource.mount_point}") + logger.trace("#{@new_resource} is mounted at #{@new_resource.mount_point}") else - Chef::Log.debug("#{@new_resource} is already mounted at #{@new_resource.mount_point}") + logger.trace("#{@new_resource} is already mounted at #{@new_resource.mount_point}") end end def umount_fs if @current_resource.mounted @mount.delete - Chef::Log.debug("#{@new_resource} is no longer mounted at #{@new_resource.mount_point}") + logger.trace("#{@new_resource} is no longer mounted at #{@new_resource.mount_point}") else - Chef::Log.debug("#{@new_resource} is not mounted at #{@new_resource.mount_point}") + logger.trace("#{@new_resource} is not mounted at #{@new_resource.mount_point}") end end diff --git a/lib/chef/provider/noop.rb b/lib/chef/provider/noop.rb index 207bf7dedb..077da3f0b4 100644 --- a/lib/chef/provider/noop.rb +++ b/lib/chef/provider/noop.rb @@ -1,6 +1,6 @@ # # Author:: Thom May (<thom@chef.io>) -# Copyright:: Copyright (c) 2016 Chef Software, Inc. +# Copyright:: Copyright (c) 2016-2017, Chef Software Inc. # License:: Apache License, Version 2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -27,7 +27,7 @@ class Chef def method_missing(method_sym, *arguments, &block) if method_sym.to_s =~ /^action_/ - Chef::Log.debug("NoOp-ing for #{method_sym}") + logger.trace("NoOp-ing for #{method_sym}") else super end diff --git a/lib/chef/provider/ohai.rb b/lib/chef/provider/ohai.rb index 6b5a605ed5..0b65a1d28a 100644 --- a/lib/chef/provider/ohai.rb +++ b/lib/chef/provider/ohai.rb @@ -23,25 +23,21 @@ class Chef class Ohai < Chef::Provider provides :ohai - def whyrun_supported? - true - end - def load_current_resource true end - def action_reload + action :reload do converge_by("re-run ohai and merge results into node attributes") do ohai = ::Ohai::System.new - # If @new_resource.plugin is nil, ohai will reload all the plugins + # If new_resource.plugin is nil, ohai will reload all the plugins # Otherwise it will only reload the specified plugin # Note that any changes to plugins, or new plugins placed on # the path are picked up by ohai. - ohai.all_plugins @new_resource.plugin + ohai.all_plugins new_resource.plugin node.automatic_attrs.merge! ohai.data - Chef::Log.info("#{@new_resource} reloaded") + logger.info("#{new_resource} reloaded") end end end diff --git a/lib/chef/provider/osx_profile.rb b/lib/chef/provider/osx_profile.rb index 6ac67e0560..e753f84d86 100644 --- a/lib/chef/provider/osx_profile.rb +++ b/lib/chef/provider/osx_profile.rb @@ -25,75 +25,71 @@ require "uuidtools" class Chef class Provider class OsxProfile < Chef::Provider - include Chef::Mixin::Command - provides :osx_profile, os: "darwin" - provides :osx_config_profile, os: "darwin" - - def whyrun_supported? - true - end + provides :osx_profile + provides :osx_config_profile def load_current_resource - @current_resource = Chef::Resource::OsxProfile.new(@new_resource.name) - @current_resource.profile_name(@new_resource.profile_name) + @current_resource = Chef::Resource::OsxProfile.new(new_resource.name) + current_resource.profile_name(new_resource.profile_name) all_profiles = get_installed_profiles - @new_resource.profile( - @new_resource.profile || - @new_resource.profile_name + new_resource.profile( + new_resource.profile || + new_resource.profile_name ) - @new_profile_hash = get_profile_hash(@new_resource.profile) - @new_profile_hash["PayloadUUID"] = - config_uuid(@new_profile_hash) if @new_profile_hash + @new_profile_hash = get_profile_hash(new_resource.profile) + if @new_profile_hash + @new_profile_hash["PayloadUUID"] = + config_uuid(@new_profile_hash) + end if @new_profile_hash @new_profile_identifier = @new_profile_hash["PayloadIdentifier"] else - @new_profile_identifier = @new_resource.identifier || - @new_resource.profile_name + @new_profile_identifier = new_resource.identifier || + new_resource.profile_name end - if all_profiles.empty? - current_profile = nil - else + current_profile = nil + if all_profiles && all_profiles.key?("_computerlevel") current_profile = all_profiles["_computerlevel"].find do |item| item["ProfileIdentifier"] == @new_profile_identifier end end - @current_resource.profile(current_profile) + current_resource.profile(current_profile) end def define_resource_requirements requirements.assert(:remove) do |a| if @new_profile_identifier - a.assertion { + a.assertion do !@new_profile_identifier.nil? && !@new_profile_identifier.end_with?(".mobileconfig") && - /^\w+(?:\.\w+)+$/.match(@new_profile_identifier) - } + /^\w+(?:(\.| )\w+)+$/.match(@new_profile_identifier) + end a.failure_message RuntimeError, "when removing using the identifier attribute, it must match the profile identifier" else - new_profile_name = @new_resource.profile_name - a.assertion { + new_profile_name = new_resource.profile_name + a.assertion do !new_profile_name.end_with?(".mobileconfig") && - /^\w+(?:\.\w+)+$/.match(new_profile_name) - } + /^\w+(?:(\.| )\w+)+$/.match(new_profile_name) + end a.failure_message RuntimeError, "When removing by resource name, it must match the profile identifier " end end requirements.assert(:install) do |a| if @new_profile_hash.is_a?(Hash) - a.assertion { + a.assertion do @new_profile_hash.include?("PayloadIdentifier") - } + end a.failure_message RuntimeError, "The specified profile does not seem to be valid" end if @new_profile_hash.is_a?(String) - a.assertion { + a.assertion do @new_profile_hash.end_with?(".mobileconfig") - } + end a.failure_message RuntimeError, "#{new_profile_hash}' is not a valid profile" end end @@ -127,21 +123,21 @@ class Chef raise Chef::Exceptions::FileNotFound, error_string end cookbook_profile = cache_cookbook_profile(new_profile) - return read_plist(cookbook_profile) + read_plist(cookbook_profile) else - return nil + nil end end def cookbook_file_available?(cookbook_file) run_context.has_cookbook_file_in_cookbook?( - @new_resource.cookbook_name, cookbook_file + new_resource.cookbook_name, cookbook_file ) end def get_cache_dir cache_dir = Chef::FileCache.create_cache_path( - "profiles/#{@new_resource.cookbook_name}" + "profiles/#{new_resource.cookbook_name}" ) end @@ -149,7 +145,7 @@ class Chef Chef::FileCache.create_cache_path( ::File.join( "profiles", - @new_resource.cookbook_name, + new_resource.cookbook_name, ::File.dirname(cookbook_file) ) ) @@ -160,7 +156,7 @@ class Chef ), run_context ) - remote_file.cookbook_name = @new_resource.cookbook_name + remote_file.cookbook_name = new_resource.cookbook_name remote_file.source(cookbook_file) remote_file.backup(false) remote_file.run_action(:create) @@ -169,9 +165,9 @@ class Chef def get_profile_hash(new_profile) if new_profile.is_a?(Hash) - return new_profile + new_profile elsif new_profile.is_a?(String) - return load_profile_hash(new_profile) + load_profile_hash(new_profile) end end @@ -184,8 +180,8 @@ class Chef end def write_profile_to_disk - @new_resource.path(Chef::FileCache.create_cache_path("profiles")) - tempfile = Chef::FileContentManagement::Tempfile.new(@new_resource).tempfile + new_resource.path(Chef::FileCache.create_cache_path("profiles")) + tempfile = Chef::FileContentManagement::Tempfile.new(new_resource).tempfile tempfile.write(@new_profile_hash.to_plist) tempfile.close tempfile.path @@ -193,14 +189,14 @@ class Chef def install_profile(profile_path) cmd = "profiles -I -F '#{profile_path}'" - Chef::Log.debug("cmd: #{cmd}") + logger.trace("cmd: #{cmd}") shellout_results = shell_out(cmd) shellout_results.exitstatus end def remove_profile cmd = "profiles -R -p '#{@new_profile_identifier}'" - Chef::Log.debug("cmd: #{cmd}") + logger.trace("cmd: #{cmd}") shellout_results = shell_out(cmd) shellout_results.exitstatus end @@ -218,7 +214,7 @@ class Chef tempfile = generate_tempfile write_installed_profiles(tempfile) installed_profiles = read_plist(tempfile) - Chef::Log.debug("Saved profiles to run_state") + logger.trace("Saved profiles to run_state") # Clean up the temp file as we do not need it anymore ::File.unlink(tempfile) installed_profiles @@ -239,13 +235,13 @@ class Chef def profile_installed? # Profile Identifier and UUID must match a currently installed profile - if @current_resource.profile.nil? || @current_resource.profile.empty? + if current_resource.profile.nil? || current_resource.profile.empty? false else - if @new_resource.action.include?(:remove) + if new_resource.action.include?(:remove) true else - @current_resource.profile["ProfileUUID"] == + current_resource.profile["ProfileUUID"] == @new_profile_hash["PayloadUUID"] end end diff --git a/lib/chef/provider/package.rb b/lib/chef/provider/package.rb index ca9b526920..133f87dad9 100644 --- a/lib/chef/provider/package.rb +++ b/lib/chef/provider/package.rb @@ -1,6 +1,6 @@ # # Author:: Adam Jacob (<adam@chef.io>) -# Copyright:: Copyright 2008-2016, Chef Software, Inc. +# Copyright:: Copyright 2008-2018, Chef Software Inc. # License:: Apache License, Version 2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,16 +17,16 @@ # require "chef/mixin/shell_out" -require "chef/mixin/command" require "chef/mixin/subclass_directive" require "chef/log" require "chef/file_cache" require "chef/platform" +require "chef/decorator/lazy_array" +require "shellwords" class Chef class Provider class Package < Chef::Provider - include Chef::Mixin::Command include Chef::Mixin::ShellOut extend Chef::Mixin::SubclassDirective @@ -34,6 +34,9 @@ class Chef subclass_directive :use_multipackage_api # subclasses declare this if they want sources (filenames) pulled from their package names subclass_directive :use_package_name_for_source + # keeps package_names_for_targets and versions_for_targets indexed the same as package_name at + # the cost of having the subclass needing to deal with nils + subclass_directive :allow_nils # # Hook that subclasses use to populate the candidate_version(s) @@ -46,28 +49,27 @@ class Chef @candidate_version = nil end - def whyrun_supported? - true + def options + new_resource.options end 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 + 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 - def load_current_resource - end + def load_current_resource; end 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") + 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 @@ -75,19 +77,19 @@ class Chef # 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") + 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 - def action_install + action :install do unless target_version_array.any? - Chef::Log.debug("#{@new_resource} is already installed - nothing to do") + logger.trace("#{new_resource} is already installed - nothing to do") return end # @todo: move the preseed code out of the base class (and complete the fix for Array of preseeds? ugh...) - if @new_resource.response_file + if new_resource.response_file if preseed_file = get_preseed_file(package_names_for_targets, versions_for_targets) converge_by("preseed package #{package_names_for_targets}") do preseed_package(preseed_file) @@ -99,7 +101,7 @@ class Chef multipackage_api_adapter(package_names_for_targets, versions_for_targets) do |name, version| install_package(name, version) end - Chef::Log.info("#{@new_resource} installed #{package_names_for_targets} at #{versions_for_targets}") + logger.info("#{new_resource} installed #{package_names_for_targets} at #{versions_for_targets}") end end @@ -115,9 +117,9 @@ class Chef private :install_description - def action_upgrade - if !target_version_array.any? - Chef::Log.debug("#{@new_resource} no versions to upgrade - nothing to do") + action :upgrade do + unless target_version_array.any? + logger.trace("#{new_resource} no versions to upgrade - nothing to do") return end @@ -126,7 +128,7 @@ class Chef upgrade_package(name, version) end log_allow_downgrade = allow_downgrade ? "(allow_downgrade)" : "" - Chef::Log.info("#{@new_resource} upgraded#{log_allow_downgrade} #{package_names_for_targets} to #{versions_for_targets}") + logger.info("#{new_resource} upgraded#{log_allow_downgrade} #{package_names_for_targets} to #{versions_for_targets}") end end @@ -145,17 +147,17 @@ class Chef private :upgrade_description - def action_remove + action :remove do if removing_package? - description = @new_resource.version ? "version #{@new_resource.version} of " : "" - converge_by("remove #{description}package #{@current_resource.package_name}") do - multipackage_api_adapter(@current_resource.package_name, @new_resource.version) do |name, version| + description = new_resource.version ? "version #{new_resource.version} of " : "" + converge_by("remove #{description}package #{current_resource.package_name}") do + multipackage_api_adapter(current_resource.package_name, new_resource.version) do |name, version| remove_package(name, version) end - Chef::Log.info("#{@new_resource} removed") + logger.info("#{new_resource} removed") end else - Chef::Log.debug("#{@new_resource} package does not exist - nothing to do") + logger.trace("#{new_resource} package does not exist - nothing to do") end end @@ -180,43 +182,86 @@ class Chef end end - def action_purge + action :purge do if removing_package? - description = @new_resource.version ? "version #{@new_resource.version} of" : "" - converge_by("purge #{description} package #{@current_resource.package_name}") do - multipackage_api_adapter(@current_resource.package_name, @new_resource.version) do |name, version| + description = new_resource.version ? "version #{new_resource.version} of" : "" + converge_by("purge #{description} package #{current_resource.package_name}") do + multipackage_api_adapter(current_resource.package_name, new_resource.version) do |name, version| purge_package(name, version) end - Chef::Log.info("#{@new_resource} purged") + logger.info("#{new_resource} purged") end end end - def action_reconfig - if @current_resource.version == nil then - Chef::Log.debug("#{@new_resource} is NOT installed - nothing to do") + action :reconfig do + if current_resource.version.nil? + logger.trace("#{new_resource} is NOT installed - nothing to do") return end - unless @new_resource.response_file then - Chef::Log.debug("#{@new_resource} no response_file provided - nothing to do") + unless new_resource.response_file + logger.trace("#{new_resource} no response_file provided - nothing to do") return end - if preseed_file = get_preseed_file(@new_resource.package_name, @current_resource.version) - converge_by("reconfigure package #{@new_resource.package_name}") do + if preseed_file = get_preseed_file(new_resource.package_name, current_resource.version) + converge_by("reconfigure package #{new_resource.package_name}") do preseed_package(preseed_file) - multipackage_api_adapter(@new_resource.package_name, @current_resource.version) do |name, version| + multipackage_api_adapter(new_resource.package_name, current_resource.version) do |name, version| reconfig_package(name, version) end - Chef::Log.info("#{@new_resource} reconfigured") + logger.info("#{new_resource} reconfigured") + end + else + logger.trace("#{new_resource} preseeding has not changed - nothing to do") + end + end + + def action_lock + packages_locked = if respond_to?(:packages_all_locked?, true) + packages_all_locked?(Array(new_resource.package_name), Array(new_resource.version)) + else + package_locked(new_resource.package_name, new_resource.version) + end + unless packages_locked + description = new_resource.version ? "version #{new_resource.version} of " : "" + converge_by("lock #{description}package #{current_resource.package_name}") do + multipackage_api_adapter(current_resource.package_name, new_resource.version) do |name, version| + lock_package(name, version) + logger.info("#{new_resource} locked") + end end else - Chef::Log.debug("#{@new_resource} preseeding has not changed - nothing to do") + logger.trace("#{new_resource} is already locked") end end + def action_unlock + packages_unlocked = if respond_to?(:packages_all_unlocked?, true) + packages_all_unlocked?(Array(new_resource.package_name), Array(new_resource.version)) + else + !package_locked(new_resource.package_name, new_resource.version) + end + unless packages_unlocked + description = new_resource.version ? "version #{new_resource.version} of " : "" + converge_by("unlock #{description}package #{current_resource.package_name}") do + multipackage_api_adapter(current_resource.package_name, new_resource.version) do |name, version| + unlock_package(name, version) + logger.info("#{new_resource} unlocked") + end + end + else + logger.trace("#{new_resource} is already unlocked") + end + end + + # for multipackage just implement packages_all_[un]locked? properly and omit implementing this API + def package_locked(name, version) + raise Chef::Exceptions::UnsupportedAction, "#{self} has no way to detect if package is locked" + end + # @todo use composition rather than inheritance def multipackage_api_adapter(name, version) @@ -251,21 +296,94 @@ class Chef raise( Chef::Exceptions::UnsupportedAction, "#{self} does not support :reconfig" ) end + def lock_package(name, version) + raise( Chef::Exceptions::UnsupportedAction, "#{self} does not support :lock" ) + end + + def unlock_package(name, version) + raise( Chef::Exceptions::UnsupportedAction, "#{self} does not support :unlock" ) + end + # used by subclasses. deprecated. use #a_to_s instead. def expand_options(options) - options ? " #{options}" : "" + # its deprecated but still work to do to deprecate it fully + #Chef.deprecated(:package_misc, "expand_options is deprecated, use shell_out_compact or shell_out_compact_timeout instead") + if options + " #{options.is_a?(Array) ? Shellwords.join(options) : options}" + else + "" + end end - # this is public and overridden by subclasses (rubygems package implements '>=' and '~>' operators) - def target_version_already_installed?(current_version, new_version) - new_version == current_version + # Check the current_version against either the candidate_version or the new_version + # + # For some reason the windows provider subclasses this (to implement passing Arrays to + # versions for some reason other than multipackage stuff, which is mildly terrifying). + # + # This MUST have 'equality' semantics -- the exact thing matches the exact thing. + # + # The name is not just bad, but i find it completely misleading, consider: + # + # target_version_already_installed?(current_version, new_version) + # target_version_already_installed?(current_version, candidate_version) + # + # Which of those is the 'target_version'? I'd say the new_version and I'm confused when + # i see it called with the candidate_version. + # + # `version_equals?(v1, v2)` would be a better name. + # + # Note that most likely we need a spaceship operator on versions that subclasses can implement + # and we should have `version_compare(v1, v2)` that returns `v1 <=> v2`. + + # 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. + + def target_version_already_installed?(current_version, target_version) + version_equals?(current_version, target_version) + end + + # Note that most likely we need a spaceship operator on versions that subclasses can implement + # and we should have `version_compare(v1, v2)` that returns `v1 <=> v2`. + + # This method performs a strict equality check between two strings representing version numbers + # + def version_equals?(v1, v2) + return false unless v1 && v2 + v1 == v2 + end + + # 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. + def version_compare(v1, v2) + gem_v1 = Gem::Version.new(v1) + gem_v2 = Gem::Version.new(v2) + + gem_v1 <=> gem_v2 + end + + # 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. + # + def version_requirement_satisfied?(current_version, new_version) + target_version_already_installed?(current_version, new_version) end # @todo: extract apt/dpkg specific preseeding to a helper class def get_preseed_file(name, version) resource = preseed_resource(name, version) resource.run_action(:create) - Chef::Log.debug("#{@new_resource} fetched preseed file to #{resource.path}") + logger.trace("#{new_resource} fetched preseed file to #{resource.path}") if resource.updated_by_last_action? resource.path @@ -277,26 +395,26 @@ class Chef # @todo: extract apt/dpkg specific preseeding to a helper class def preseed_resource(name, version) # A directory in our cache to store this cookbook's preseed files in - file_cache_dir = Chef::FileCache.create_cache_path("preseed/#{@new_resource.cookbook_name}") + file_cache_dir = Chef::FileCache.create_cache_path("preseed/#{new_resource.cookbook_name}") # The full path where the preseed file will be stored cache_seed_to = "#{file_cache_dir}/#{name}-#{version}.seed" - Chef::Log.debug("#{@new_resource} fetching preseed file to #{cache_seed_to}") + logger.trace("#{new_resource} fetching preseed file to #{cache_seed_to}") - if template_available?(@new_resource.response_file) - Chef::Log.debug("#{@new_resource} fetching preseed file via Template") + if template_available?(new_resource.response_file) + logger.trace("#{new_resource} fetching preseed file via Template") remote_file = Chef::Resource::Template.new(cache_seed_to, run_context) - remote_file.variables(@new_resource.response_file_variables) - elsif cookbook_file_available?(@new_resource.response_file) - Chef::Log.debug("#{@new_resource} fetching preseed file via cookbook_file") + remote_file.variables(new_resource.response_file_variables) + elsif cookbook_file_available?(new_resource.response_file) + logger.trace("#{new_resource} fetching preseed file via cookbook_file") remote_file = Chef::Resource::CookbookFile.new(cache_seed_to, run_context) else - message = "No template or cookbook file found for response file #{@new_resource.response_file}" + message = "No template or cookbook file found for response file #{new_resource.response_file}" raise Chef::Exceptions::FileNotFound, message end - remote_file.cookbook_name = @new_resource.cookbook_name - remote_file.source(@new_resource.response_file) + remote_file.cookbook_name = new_resource.cookbook_name + remote_file.source(new_resource.response_file) remote_file.backup(false) remote_file end @@ -319,9 +437,12 @@ class Chef def package_names_for_targets package_names_for_targets = [] target_version_array.each_with_index do |target_version, i| - next if target_version.nil? - package_name = package_name_array[i] - package_names_for_targets.push(package_name) + 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 @@ -336,8 +457,11 @@ class Chef def versions_for_targets versions_for_targets = [] target_version_array.each_with_index do |target_version, i| - next if target_version.nil? - versions_for_targets.push(target_version) + 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 @@ -354,33 +478,42 @@ class Chef each_package do |package_name, new_version, current_version, candidate_version| case action when :upgrade - - if !candidate_version - Chef::Log.debug("#{new_resource} #{package_name} has no candidate_version to upgrade to") + if version_equals?(current_version, new_version) + # this is an odd use case + logger.trace("#{new_resource} #{package_name} #{new_version} is already installed -- you are equality pinning with an :upgrade action, this may be deprecated in the future") + target_version_array.push(nil) + elsif version_equals?(current_version, candidate_version) + logger.trace("#{new_resource} #{package_name} #{candidate_version} is already installed") target_version_array.push(nil) - elsif current_version == candidate_version - Chef::Log.debug("#{new_resource} #{package_name} the #{candidate_version} is already installed") + elsif candidate_version.nil? + logger.trace("#{new_resource} #{package_name} has no candidate_version to upgrade to") + target_version_array.push(nil) + elsif current_version.nil? + logger.trace("#{new_resource} has no existing installed version. Installing install #{candidate_version}") + target_version_array.push(candidate_version) + elsif version_compare(current_version, candidate_version) == 1 && !allow_downgrade + 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) else - Chef::Log.debug("#{new_resource} #{package_name} is out of date, will upgrade to #{candidate_version}") + logger.trace("#{new_resource} #{package_name} is out of date, will upgrade to #{candidate_version}") target_version_array.push(candidate_version) end when :install if new_version - if target_version_already_installed?(current_version, new_version) - Chef::Log.debug("#{new_resource} #{package_name} #{current_version} satisifies #{new_version} requirement") + if version_requirement_satisfied?(current_version, new_version) + logger.trace("#{new_resource} #{package_name} #{current_version} satisifies #{new_version} requirement") target_version_array.push(nil) else - Chef::Log.debug("#{new_resource} #{package_name} #{current_version} needs updating to #{new_version}") + logger.trace("#{new_resource} #{package_name} #{current_version} needs updating to #{new_version}") target_version_array.push(new_version) end elsif current_version.nil? - Chef::Log.debug("#{new_resource} #{package_name} not installed, installing #{candidate_version}") + logger.trace("#{new_resource} #{package_name} not installed, installing #{candidate_version}") target_version_array.push(candidate_version) else - Chef::Log.debug("#{new_resource} #{package_name} #{current_version} already installed") + logger.trace("#{new_resource} #{package_name} #{current_version} already installed") target_version_array.push(nil) end @@ -411,7 +544,7 @@ class Chef begin missing = [] each_package do |package_name, new_version, current_version, candidate_version| - missing.push(package_name) if candidate_version.nil? && current_version.nil? + missing.push(package_name) if current_version.nil? && candidate_version.nil? end missing end @@ -436,7 +569,7 @@ class Chef missing = [] each_package do |package_name, new_version, current_version, candidate_version| next if new_version.nil? || current_version.nil? - if candidate_version.nil? && !target_version_already_installed?(current_version, new_version) + if !version_requirement_satisfied?(current_version, new_version) && candidate_version.nil? missing.push(package_name) end end @@ -458,27 +591,29 @@ class Chef # @return [Boolean] if we're doing a multipackage install or not def multipackage? - new_resource.package_name.is_a?(Array) + @multipackage_bool ||= new_resource.package_name.is_a?(Array) end # @return [Array] package_name(s) as an array def package_name_array - [ new_resource.package_name ].flatten + @package_name_array ||= [ new_resource.package_name ].flatten end # @return [Array] candidate_version(s) as an array def candidate_version_array - [ candidate_version ].flatten + # 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 # @return [Array] current_version(s) as an array def current_version_array - [ current_resource.version ].flatten + @current_version_array ||= [ current_resource.version ].flatten end # @return [Array] new_version(s) as an array def new_version_array - [ new_resource.version ].flatten.map { |v| v.to_s.empty? ? nil : v } + @new_version_array ||= [ new_resource.version ].flatten.map { |v| v.to_s.empty? ? nil : v } end # TIP: less error prone to simply always call resolved_source_array, even if you @@ -486,11 +621,14 @@ class Chef # # @return [Array] new_resource.source as an array def source_array - if new_resource.source.nil? - package_name_array.map { nil } - else - [ new_resource.source ].flatten - end + @source_array ||= + begin + if new_resource.source.nil? + package_name_array.map { nil } + else + [ new_resource.source ].flatten + end + end end # Helper to handle use_package_name_for_source to convert names into local packages to install. @@ -503,7 +641,7 @@ class Chef 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}/) && ::File.exist?(package_name) - Chef::Log.debug("No package source specified, but #{package_name} exists on filesystem, using #{package_name} as source.") + logger.trace("No package source specified, but #{package_name} exists on filesystem, using #{package_name} as source.") package_name else source @@ -523,8 +661,8 @@ class Chef end def allow_downgrade - if @new_resource.respond_to?("allow_downgrade") - @new_resource.allow_downgrade + if new_resource.respond_to?("allow_downgrade") + new_resource.allow_downgrade else false end @@ -539,27 +677,19 @@ class Chef end def add_timeout_option(command_args) + # this is deprecated but its not quite done yet + #Chef.deprecated(:package_misc, "shell_out_with_timeout and add_timeout_option are deprecated methods, use shell_out_compact_timeout instead") args = command_args.dup if args.last.is_a?(Hash) options = args.pop.dup options[:timeout] = new_resource.timeout if new_resource.timeout - options[:timeout] = 900 unless options.has_key?(:timeout) + options[:timeout] = 900 unless options.key?(:timeout) args << options else - args << { :timeout => new_resource.timeout ? new_resource.timeout : 900 } + args << { timeout: new_resource.timeout ? new_resource.timeout : 900 } end args end - - # Helper for sublcasses to convert an array of string args into a string. It - # will compact nil or empty strings in the array and will join the array elements - # with spaces, without introducing any double spaces for nil/empty elements. - # - # @param args [String] variable number of string arguments - # @return [String] nicely concatenated string or empty string - def a_to_s(*args) - args.reject { |i| i.nil? || i == "" }.join(" ") - end end end end diff --git a/lib/chef/provider/package/aix.rb b/lib/chef/provider/package/aix.rb deleted file mode 100644 index a1709c4af7..0000000000 --- a/lib/chef/provider/package/aix.rb +++ /dev/null @@ -1,136 +0,0 @@ -# -# Author:: Deepali Jagtap -# Copyright:: Copyright 2013-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" -require "chef/mixin/command" -require "chef/resource/package" -require "chef/mixin/get_source_from_package" - -class Chef - class Provider - class Package - class Aix < Chef::Provider::Package - - provides :package, os: "aix" - provides :bff_package, os: "aix" - - include Chef::Mixin::GetSourceFromPackage - - def define_resource_requirements - super - requirements.assert(:install) do |a| - a.assertion { @new_resource.source } - a.failure_message Chef::Exceptions::Package, "Source for package #{@new_resource.name} required for action install" - end - requirements.assert(:all_actions) do |a| - a.assertion { !@new_resource.source || @package_source_found } - a.failure_message Chef::Exceptions::Package, "Package #{@new_resource.name} not found: #{@new_resource.source}" - a.whyrun "would assume #{@new_resource.source} would be have previously been made available" - end - end - - def load_current_resource - @current_resource = Chef::Resource::Package.new(@new_resource.name) - @current_resource.package_name(@new_resource.package_name) - - if @new_resource.source - @package_source_found = ::File.exists?(@new_resource.source) - if @package_source_found - Chef::Log.debug("#{@new_resource} checking pkg status") - ret = shell_out_with_timeout("installp -L -d #{@new_resource.source}") - ret.stdout.each_line do |line| - case line - when /#{@new_resource.package_name}:/ - fields = line.split(":") - @new_resource.version(fields[2]) - end - end - raise Chef::Exceptions::Package, "package source #{@new_resource.source} does not provide package #{@new_resource.package_name}" unless @new_resource.version - end - end - - Chef::Log.debug("#{@new_resource} checking install state") - ret = shell_out_with_timeout("lslpp -lcq #{@current_resource.package_name}") - ret.stdout.each_line do |line| - case line - when /#{@current_resource.package_name}/ - fields = line.split(":") - Chef::Log.debug("#{@new_resource} version #{fields[2]} is already installed") - @current_resource.version(fields[2]) - end - end - - unless ret.exitstatus == 0 || ret.exitstatus == 1 - raise Chef::Exceptions::Package, "lslpp failed - #{ret.format_for_exception}!" - end - - @current_resource - end - - def candidate_version - return @candidate_version if @candidate_version - ret = shell_out_with_timeout("installp -L -d #{@new_resource.source}") - ret.stdout.each_line do |line| - case line - when /\w:#{Regexp.escape(@new_resource.package_name)}:(.*)/ - fields = line.split(":") - @candidate_version = fields[2] - @new_resource.version(fields[2]) - Chef::Log.debug("#{@new_resource} setting install candidate version to #{@candidate_version}") - end - end - unless ret.exitstatus == 0 - raise Chef::Exceptions::Package, "installp -L -d #{@new_resource.source} - #{ret.format_for_exception}!" - end - @candidate_version - end - - # - # The install/update action needs to be tested with various kinds of packages - # on AIX viz. packages with or without licensing file dependencies, packages - # with dependencies on other packages which will help to test additional - # options of installp. - # So far, the code has been tested only with standalone packages. - # - def install_package(name, version) - Chef::Log.debug("#{@new_resource} package install options: #{@new_resource.options}") - if @new_resource.options.nil? - shell_out_with_timeout!( "installp -aYF -d #{@new_resource.source} #{@new_resource.package_name}" ) - Chef::Log.debug("#{@new_resource} installed version #{@new_resource.version} from: #{@new_resource.source}") - else - shell_out_with_timeout!( "installp -aYF #{expand_options(@new_resource.options)} -d #{@new_resource.source} #{@new_resource.package_name}" ) - Chef::Log.debug("#{@new_resource} installed version #{@new_resource.version} from: #{@new_resource.source}") - end - end - - alias_method :upgrade_package, :install_package - - def remove_package(name, version) - if @new_resource.options.nil? - shell_out_with_timeout!( "installp -u #{name}" ) - Chef::Log.debug("#{@new_resource} removed version #{@new_resource.version}") - else - shell_out_with_timeout!( "installp -u #{expand_options(@new_resource.options)} #{name}" ) - Chef::Log.debug("#{@new_resource} removed version #{@new_resource.version}") - end - end - - end - end - end -end diff --git a/lib/chef/provider/package/apt.rb b/lib/chef/provider/package/apt.rb index ac730202b8..798abf4680 100644 --- a/lib/chef/provider/package/apt.rb +++ b/lib/chef/provider/package/apt.rb @@ -1,6 +1,6 @@ # # Author:: Adam Jacob (<adam@chef.io>) -# Copyright:: Copyright 2008-2016, Chef Software Inc. +# Copyright:: Copyright 2008-2017, Chef Software Inc. # License:: Apache License, Version 2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,132 +17,82 @@ # require "chef/provider/package" -require "chef/mixin/command" -require "chef/resource/package" +require "chef/resource/apt_package" class Chef class Provider class Package class Apt < Chef::Provider::Package + use_multipackage_api provides :package, platform_family: "debian" - provides :apt_package, os: "linux" - - # return [Hash] mapping of package name to Boolean value - attr_accessor :is_virtual_package + provides :apt_package def initialize(new_resource, run_context) super - @is_virtual_package = {} end def load_current_resource - @current_resource = Chef::Resource::Package.new(@new_resource.name) - @current_resource.package_name(@new_resource.package_name) - check_all_packages_state(@new_resource.package_name) - @current_resource + @current_resource = Chef::Resource::AptPackage.new(new_resource.name) + current_resource.package_name(new_resource.package_name) + current_resource.version(get_current_versions) + current_resource end def define_resource_requirements super requirements.assert(:all_actions) do |a| - a.assertion { !@new_resource.source } + a.assertion { !new_resource.source } a.failure_message(Chef::Exceptions::Package, "apt package provider cannot handle source attribute. Use dpkg provider instead") end end - def default_release_options - # Use apt::Default-Release option only if provider supports it - "-o APT::Default-Release=#{@new_resource.default_release}" if @new_resource.respond_to?(:default_release) && @new_resource.default_release + def package_data + @package_data ||= Hash.new do |hash, key| + hash[key] = package_data_for(key) + end end - def check_package_state(pkg) - is_virtual_package = false - installed = false - installed_version = nil - candidate_version = nil + def get_current_versions + package_name_array.map do |package_name| + package_data[package_name][:current_version] + end + end - shell_out_with_timeout!("apt-cache#{expand_options(default_release_options)} policy #{pkg}").stdout.each_line do |line| - case line - when /^\s{2}Installed: (.+)$/ - installed_version = $1 - if installed_version == "(none)" - Chef::Log.debug("#{@new_resource} current version is nil") - installed_version = nil - else - Chef::Log.debug("#{@new_resource} current version is #{installed_version}") - installed = true - end - when /^\s{2}Candidate: (.+)$/ - candidate_version = $1 - if candidate_version == "(none)" - # This may not be an appropriate assumption, but it shouldn't break anything that already worked -- btm - is_virtual_package = true - showpkg = shell_out_with_timeout!("apt-cache showpkg #{pkg}").stdout - providers = Hash.new - showpkg.rpartition(/Reverse Provides: ?#{$/}/)[2].each_line do |line| - provider, version = line.split - providers[provider] = version - end - # Check if the package providing this virtual package is installed - num_providers = providers.length - raise Chef::Exceptions::Package, "#{@new_resource.package_name} has no candidate in the apt-cache" if num_providers == 0 - # apt will only install a virtual package if there is a single providing package - raise Chef::Exceptions::Package, "#{@new_resource.package_name} is a virtual package provided by #{num_providers} packages, you must explicitly select one to install" if num_providers > 1 - # Check if the package providing this virtual package is installed - Chef::Log.info("#{@new_resource} is a virtual package, actually acting on package[#{providers.keys.first}]") - ret = check_package_state(providers.keys.first) - installed = ret[:installed] - installed_version = ret[:installed_version] - else - Chef::Log.debug("#{@new_resource} candidate version is #{$1}") - end - end + def get_candidate_versions + package_name_array.map do |package_name| + package_data[package_name][:candidate_version] end + end - return { - installed_version: installed_version, - installed: installed, - candidate_version: candidate_version, - is_virtual_package: is_virtual_package, - } + def candidate_version + @candidate_version ||= get_candidate_versions end - def check_all_packages_state(package) - installed_version = {} - candidate_version = {} - installed = {} + def packages_all_locked?(names, versions) + names.all? { |n| locked_packages.include? n } + end - [package].flatten.each do |pkg| - ret = check_package_state(pkg) - is_virtual_package[pkg] = ret[:is_virtual_package] - installed[pkg] = ret[:installed] - installed_version[pkg] = ret[:installed_version] - candidate_version[pkg] = ret[:candidate_version] - end + def packages_all_unlocked?(names, versions) + names.all? { |n| !locked_packages.include? n } + end - if package.is_a?(Array) - @candidate_version = [] - final_installed_version = [] - [package].flatten.each do |pkg| - @candidate_version << candidate_version[pkg] - final_installed_version << installed_version[pkg] + def locked_packages + @locked_packages ||= + begin + locked = shell_out_compact_timeout!("apt-mark", "showhold") + locked.stdout.each_line.map do |line| + line.strip + end end - @current_resource.version(final_installed_version) - else - @candidate_version = candidate_version[package] - @current_resource.version(installed_version[package]) - end end def install_package(name, version) - name_array = [ name ].flatten - version_array = [ version ].flatten - package_name = name_array.zip(version_array).map do |n, v| - is_virtual_package[n] ? n : "#{n}=#{v}" - end.join(" ") - run_noninteractive("apt-get -q -y#{expand_options(default_release_options)}#{expand_options(@new_resource.options)} install #{package_name}") + package_name = name.zip(version).map do |n, v| + package_data[n][:virtual] ? n : "#{n}=#{v}" + end + run_noninteractive("apt-get", "-q", "-y", config_file_options, default_release_options, options, "install", package_name) end def upgrade_package(name, version) @@ -150,33 +100,135 @@ class Chef end def remove_package(name, version) - package_name = [ name ].flatten.join(" ") - run_noninteractive("apt-get -q -y#{expand_options(@new_resource.options)} remove #{package_name}") + package_name = name.map do |n| + package_data[n][:virtual] ? resolve_virtual_package_name(n) : n + end + run_noninteractive("apt-get", "-q", "-y", options, "remove", package_name) end def purge_package(name, version) - package_name = [ name ].flatten.join(" ") - run_noninteractive("apt-get -q -y#{expand_options(@new_resource.options)} purge #{package_name}") + package_name = name.map do |n| + package_data[n][:virtual] ? resolve_virtual_package_name(n) : n + end + run_noninteractive("apt-get", "-q", "-y", options, "purge", package_name) end def preseed_package(preseed_file) - Chef::Log.info("#{@new_resource} pre-seeding package installation instructions") - run_noninteractive("debconf-set-selections #{preseed_file}") + logger.info("#{new_resource} pre-seeding package installation instructions") + run_noninteractive("debconf-set-selections", preseed_file) end def reconfig_package(name, version) - package_name = [ name ].flatten.join(" ") - Chef::Log.info("#{@new_resource} reconfiguring") - run_noninteractive("dpkg-reconfigure #{package_name}") + logger.info("#{new_resource} reconfiguring") + run_noninteractive("dpkg-reconfigure", name) + end + + def lock_package(name, version) + run_noninteractive("apt-mark", options, "hold", name) + end + + def unlock_package(name, version) + run_noninteractive("apt-mark", options, "unhold", name) end private + # compare 2 versions to each other to see which is newer. + # this differs from the standard package method because we + # need to be able to parse debian version strings which contain + # tildes which Gem cannot properly parse + # + # @return [Integer] 1 if v1 > v2. 0 if they're equal. -1 if v1 < v2 + def version_compare(v1, v2) + if !shell_out_compact_timeout("dpkg", "--compare-versions", v1.to_s, "gt", v2.to_s).error? + 1 + elsif !shell_out_compact_timeout("dpkg", "--compare-versions", v1.to_s, "eq", v2.to_s).error? + 0 + else + -1 + end + end + # Runs command via shell_out 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", "LC_ALL" => nil }) + def run_noninteractive(*args) + shell_out_compact_timeout!(*args, env: { "DEBIAN_FRONTEND" => "noninteractive" }) + end + + def default_release_options + # Use apt::Default-Release option only if provider supports it + if new_resource.respond_to?(:default_release) && new_resource.default_release + [ "-o", "APT::Default-Release=#{new_resource.default_release}" ] + end + end + + def config_file_options + # If the user has specified config file options previously, respect those. + return if Array(options).any? { |opt| opt =~ /--force-conf/ } + + # It doesn't make sense to install packages in a scenario that can + # result in a prompt. Have users decide up-front whether they want to + # forcibly overwrite the config file, otherwise preserve it. + if new_resource.overwrite_config_files + [ "-o", "Dpkg::Options::=--force-confnew" ] + else + [ "-o", "Dpkg::Options::=--force-confdef", "-o", "Dpkg::Options::=--force-confold" ] + end + end + + def resolve_package_versions(pkg) + current_version = nil + candidate_version = nil + run_noninteractive("apt-cache", default_release_options, "policy", pkg).stdout.each_line do |line| + case line + when /^\s{2}Installed: (.+)$/ + current_version = ( $1 != "(none)" ) ? $1 : nil + logger.trace("#{new_resource} installed version for #{pkg} is #{$1}") + when /^\s{2}Candidate: (.+)$/ + candidate_version = ( $1 != "(none)" ) ? $1 : nil + logger.trace("#{new_resource} candidate version for #{pkg} is #{$1}") + end + end + [ current_version, candidate_version ] + end + + def resolve_virtual_package_name(pkg) + showpkg = run_noninteractive("apt-cache", "showpkg", pkg).stdout + partitions = showpkg.rpartition(/Reverse Provides: ?#{$/}/) + return nil if partitions[0] == "" && partitions[1] == "" # not found in output + set = partitions[2].lines.each_with_object(Set.new) do |line, acc| + # there may be multiple reverse provides for a single package + acc.add(line.split[0]) + end + if set.size > 1 + raise Chef::Exceptions::Package, "#{new_resource.package_name} is a virtual package provided by multiple packages, you must explicitly select one" + end + set.to_a.first + end + + def package_data_for(pkg) + virtual = false + current_version = nil + candidate_version = nil + + current_version, candidate_version = resolve_package_versions(pkg) + + if candidate_version.nil? + newpkg = resolve_virtual_package_name(pkg) + + if newpkg + virtual = true + logger.info("#{new_resource} is a virtual package, actually acting on package[#{newpkg}]") + current_version, candidate_version = resolve_package_versions(newpkg) + end + end + + { + current_version: current_version, + candidate_version: candidate_version, + virtual: virtual, + } end end diff --git a/lib/chef/provider/package/bff.rb b/lib/chef/provider/package/bff.rb new file mode 100644 index 0000000000..44fadd92df --- /dev/null +++ b/lib/chef/provider/package/bff.rb @@ -0,0 +1,142 @@ +# +# Author:: Deepali Jagtap +# Copyright:: Copyright 2013-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" +require "chef/resource/package" +require "chef/mixin/get_source_from_package" + +class Chef + class Provider + class Package + class Bff < Chef::Provider::Package + + provides :package, os: "aix" + provides :bff_package + + include Chef::Mixin::GetSourceFromPackage + + def define_resource_requirements + super + requirements.assert(:install) do |a| + a.assertion { new_resource.source } + a.failure_message Chef::Exceptions::Package, "Source for package #{new_resource.name} required for action install" + end + requirements.assert(:all_actions) do |a| + a.assertion { !new_resource.source || package_source_found? } + a.failure_message Chef::Exceptions::Package, "Package #{new_resource.name} not found: #{new_resource.source}" + a.whyrun "would assume #{new_resource.source} would be have previously been made available" + end + end + + def load_current_resource + @current_resource = Chef::Resource::Package.new(new_resource.name) + current_resource.package_name(new_resource.package_name) + + if package_source_found? + logger.trace("#{new_resource} checking pkg status") + ret = shell_out_compact_timeout("installp", "-L", "-d", new_resource.source) + ret.stdout.each_line do |line| + case line + when /:#{new_resource.package_name}:/ + fields = line.split(":") + new_resource.version(fields[2]) + when /^#{new_resource.package_name}:/ + logger.warn("You are installing a bff package by product name. For idempotent installs, please install individual filesets") + fields = line.split(":") + new_resource.version(fields[2]) + end + end + raise Chef::Exceptions::Package, "package source #{new_resource.source} does not provide package #{new_resource.package_name}" unless new_resource.version + end + + logger.trace("#{new_resource} checking install state") + ret = shell_out_compact_timeout("lslpp", "-lcq", current_resource.package_name) + ret.stdout.each_line do |line| + case line + when /#{current_resource.package_name}/ + fields = line.split(":") + logger.trace("#{new_resource} version #{fields[2]} is already installed") + current_resource.version(fields[2]) + end + end + + unless ret.exitstatus == 0 || ret.exitstatus == 1 + raise Chef::Exceptions::Package, "lslpp failed - #{ret.format_for_exception}!" + end + + current_resource + end + + def candidate_version + return @candidate_version if @candidate_version + if package_source_found? + ret = shell_out_compact_timeout("installp", "-L", "-d", new_resource.source) + ret.stdout.each_line do |line| + case line + when /\w:#{Regexp.escape(new_resource.package_name)}:(.*)/ + fields = line.split(":") + @candidate_version = fields[2] + new_resource.version(fields[2]) + logger.trace("#{new_resource} setting install candidate version to #{@candidate_version}") + end + end + unless ret.exitstatus == 0 + raise Chef::Exceptions::Package, "installp -L -d #{new_resource.source} - #{ret.format_for_exception}!" + end + end + @candidate_version + end + + # + # The install/update action needs to be tested with various kinds of packages + # on AIX viz. packages with or without licensing file dependencies, packages + # with dependencies on other packages which will help to test additional + # options of installp. + # So far, the code has been tested only with standalone packages. + # + def install_package(name, version) + logger.trace("#{new_resource} package install options: #{options}") + if options.nil? + shell_out_compact_timeout!("installp", "-aYF", "-d", new_resource.source, new_resource.package_name) + logger.trace("#{new_resource} installed version #{new_resource.version} from: #{new_resource.source}") + else + shell_out_compact_timeout!("installp", "-aYF", options, "-d", new_resource.source, new_resource.package_name) + logger.trace("#{new_resource} installed version #{new_resource.version} from: #{new_resource.source}") + end + end + + alias upgrade_package install_package + + def remove_package(name, version) + if options.nil? + shell_out_compact_timeout!("installp", "-u", name) + logger.trace("#{new_resource} removed version #{new_resource.version}") + else + shell_out_compact_timeout!("installp", "-u", options, name) + logger.trace("#{new_resource} removed version #{new_resource.version}") + end + end + + def package_source_found? + @package_source_found ||= new_resource.source && ::File.exist?(new_resource.source) + end + + end + end + end +end diff --git a/lib/chef/provider/package/cab.rb b/lib/chef/provider/package/cab.rb new file mode 100644 index 0000000000..79292293d2 --- /dev/null +++ b/lib/chef/provider/package/cab.rb @@ -0,0 +1,183 @@ +# +# Author:: Vasundhara Jagdale (<vasundhara.jagdale@msystechnologies.com>) +# Copyright:: Copyright 2015-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" +require "chef/resource/cab_package" +require "chef/mixin/shell_out" +require "chef/mixin/uris" +require "chef/mixin/checksum" + +class Chef + class Provider + class Package + class Cab < Chef::Provider::Package + include Chef::Mixin::ShellOut + include Chef::Mixin::Uris + include Chef::Mixin::Checksum + + provides :cab_package, os: "windows" + + def load_current_resource + @current_resource = Chef::Resource::CabPackage.new(new_resource.name) + current_resource.source(cab_file_source) + new_resource.version(package_version) + current_resource.version(installed_version) + current_resource + end + + def cab_file_source + @cab_file_source ||= uri_scheme?(new_resource.source) ? download_source_file : new_resource.source + end + + def download_source_file + source_resource.run_action(:create) + logger.trace("#{new_resource} fetched source file to #{source_resource.path}") + source_resource.path + end + + def source_resource + @source_resource ||= declare_resource(:remote_file, new_resource.name) do + path default_download_cache_path + source new_resource.source + backup false + end + end + + def default_download_cache_path + uri = ::URI.parse(new_resource.source) + filename = ::File.basename(::URI.unescape(uri.path)) + file_cache_dir = Chef::FileCache.create_cache_path("package/") + Chef::Util::PathHelper.cleanpath("#{file_cache_dir}/#{filename}") + end + + def install_package(name, version) + dism_command("/Add-Package /PackagePath:\"#{cab_file_source}\"") + end + + def remove_package(name, version) + dism_command("/Remove-Package /PackagePath:\"#{cab_file_source}\"") + end + + def dism_command(command) + shellout = Mixlib::ShellOut.new("dism.exe /Online /English #{command} /NoRestart", timeout: new_resource.timeout) + with_os_architecture(nil) do + shellout.run_command + end + end + + def installed_version + # e.g. Package_for_KB2975719~31bf3856ad364e35~amd64~~6.3.1.8 + package = new_cab_identity + # Search for just the package name to catch a different version being installed + logger.trace("#{new_resource} searching for installed package #{package['name']}") + existing_package_identities = installed_packages.map do |p| + split_package_identity(p["package_identity"]) + end + found_packages = existing_package_identities.select do |existing_package_ident| + existing_package_ident["name"] == package["name"] + end + if found_packages.empty? + nil + elsif found_packages.length == 1 + found_packages.first["version"] + else + # Presuming this won't happen, otherwise we need to handle it + raise Chef::Exceptions::Package, "Found multiple packages installed matching name #{package['name']}, found: #{found_packages.length} matches" + end + end + + def cab_identity_from_cab_file + stdout = dism_command("/Get-PackageInfo /PackagePath:\"#{cab_file_source}\"").stdout + package_info = parse_dism_get_package_info(stdout) + split_package_identity(package_info["package_information"]["package_identity"]) + end + + def new_cab_identity + logger.trace("#{new_resource} getting product version for package at #{cab_file_source}") + @new_cab_identity ||= cab_identity_from_cab_file + end + + def package_version + new_cab_identity["version"].chomp + end + + # returns a hash of package state information given the output of dism /get-packages + # expected keys: package_identity + def parse_dism_get_packages(text) + packages = [] + text.each_line do |line| + key, value = line.split(":") if line.start_with?("Package Identity") + next if key.nil? || value.nil? + package = {} + package[key.downcase.strip.tr(" ", "_")] = value.strip.chomp + packages << package + end + packages + end + + # returns a hash of package information given the output of dism /get-packageinfo + def parse_dism_get_package_info(text) + package_data = {} + errors = [] + in_section = false + section_headers = [ "Package information", "Custom Properties", "Features" ] + text.each_line do |line| + if line =~ /Error: (.*)/ + errors << $1.strip + elsif section_headers.any? { |header| line =~ /^(#{header})/ } + in_section = $1.downcase.tr(" ", "_") + elsif line =~ /(.*) ?: (.*)/ + v = $2 # has to be first or the gsub below replaces this variable + k = $1.downcase.strip.tr(" ", "_") + if in_section + package_data[in_section] = {} unless package_data[in_section] + package_data[in_section][k] = v + else + package_data[k] = v + end + end + end + unless errors.empty? + if errors.include?("0x80070003") || errors.include?("0x80070002") + raise Chef::Exceptions::Package, "DISM: The system cannot find the path or file specified." + elsif errors.include?("740") + raise Chef::Exceptions::Package, "DISM: Error 740: Elevated permissions are required to run DISM." + else + raise Chef::Exceptions::Package, "Unknown errors encountered parsing DISM output: #{errors}" + end + end + package_data + end + + def split_package_identity(identity) + data = {} + data["name"], data["publisher"], data["arch"], data["resource_id"], data["version"] = identity.split("~") + data + end + + def installed_packages + @packages ||= begin + output = dism_command("/Get-Packages").stdout + packages = parse_dism_get_packages(output) + packages + end + end + end + end + end +end diff --git a/lib/chef/provider/package/chocolatey.rb b/lib/chef/provider/package/chocolatey.rb index 44fb1de235..a6abdd5b46 100644 --- a/lib/chef/provider/package/chocolatey.rb +++ b/lib/chef/provider/package/chocolatey.rb @@ -25,13 +25,13 @@ class Chef class Chocolatey < Chef::Provider::Package include Chef::Mixin::PowershellOut - provides :chocolatey_package, os: "windows" + provides :chocolatey_package # Declare that our arguments should be arrays use_multipackage_api - PATHFINDING_POWERSHELL_COMMAND = "[System.Environment]::GetEnvironmentVariable('ChocolateyInstall', 'MACHINE')" - CHOCO_MISSING_MSG = <<-EOS + PATHFINDING_POWERSHELL_COMMAND = "[System.Environment]::GetEnvironmentVariable('ChocolateyInstall', 'MACHINE')".freeze + CHOCO_MISSING_MSG = <<-EOS.freeze Could not locate your Chocolatey install. To install chocolatey, we recommend the 'chocolatey' cookbook (https://github.com/chocolatey/chocolatey-cookbook). If Chocolatey is installed, ensure that the 'ChocolateyInstall' environment @@ -59,8 +59,8 @@ EOS # so we want to assert candidates exist for the alternate source requirements.assert(:upgrade, :install) do |a| a.assertion { candidates_exist_for_all_uninstalled? } - 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") + 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 @@ -84,7 +84,7 @@ EOS # choco does not support installing multiple packages with version pins name_has_versions.each do |name, version| - choco_command("install -y -version", version, cmd_args, name) + choco_command("install -y --version", version, cmd_args, name) end # but we can do all the ones without version pins at once @@ -106,7 +106,7 @@ EOS # choco does not support installing multiple packages with version pins name_has_versions.each do |name, version| - choco_command("upgrade -y -version", version, cmd_args, name) + choco_command("upgrade -y --version", version, cmd_args, name) end # but we can do all the ones without version pins at once @@ -124,24 +124,26 @@ EOS choco_command("uninstall -y", cmd_args(include_source: false), *names) end - # Support :uninstall as an action in order for users to easily convert - # from the `chocolatey` provider in the cookbook. It is, however, - # already deprecated. - def action_uninstall - Chef::Log.deprecation "The use of action :uninstall on the chocolatey_package provider is deprecated, please use :remove" - action_remove - end - # Choco does not have dpkg's distinction between purge and remove - alias_method :purge_package, :remove_package + alias purge_package remove_package # Override the superclass check. The semantics for our new_resource.source is not files to # install from, but like the rubygem provider's sources which are more like repos. - def check_resource_semantics! - end + def check_resource_semantics!; end private + def version_compare(v1, v2) + if v1 == "latest" || v2 == "latest" + return 0 + end + + gem_v1 = Gem::Version.new(v1) + gem_v2 = Gem::Version.new(v2) + + gem_v1 <=> gem_v2 + end + # Magic to find where chocolatey is installed in the system, and to # return the full path of choco.exe # @@ -160,7 +162,7 @@ EOS def choco_install_path @choco_install_path ||= powershell_out!( PATHFINDING_POWERSHELL_COMMAND - ).stdout.chomp + ).stdout.chomp end # Helper to dispatch a choco command through shell_out using the timeout @@ -169,7 +171,7 @@ EOS # @param args [String] variable number of string arguments # @return [Mixlib::ShellOut] object returned from shell_out! def choco_command(*args) - shell_out_with_timeout!(args_to_string(choco_exe, *args)) + shell_out_with_timeout!(args_to_string(choco_exe, *args), returns: new_resource.returns) end # Use the available_packages Hash helper to create an array suitable for @@ -227,17 +229,21 @@ EOS # # @return [Hash] name-to-version mapping of available packages def available_packages - @available_packages ||= - begin - cmd = [ "list -ar #{package_name_array.join ' '}" ] - cmd.push( "-source #{new_resource.source}" ) if new_resource.source - parse_list_output(*cmd).each_with_object({}) do |name_version, available| - name, version = name_version - if desired_name_versions[name].nil? || desired_name_versions[name] == version - available[name] = version + return @available_packages if @available_packages + @available_packages = {} + package_name_array.each do |pkg| + available_versions = + begin + cmd = [ "list -r #{pkg}" ] + cmd.push( "-source #{new_resource.source}" ) if new_resource.source + raw = parse_list_output(*cmd) + raw.keys.each_with_object({}) do |name, available| + available[name] = desired_name_versions[name] || raw[name] end end - end + @available_packages.merge! available_versions + end + @available_packages end # Installed packages in chocolatey as a Hash of names mapped to versions @@ -246,20 +252,22 @@ EOS # @return [Hash] name-to-version mapping of installed packages def installed_packages @installed_packages ||= Hash[*parse_list_output("list -l -r").flatten] + @installed_packages end # Helper to convert choco.exe list output to a Hash # (names are downcased for case-insenstive matching) # # @param cmd [String] command to run - # @return [Array] list output converted to ruby Hash + # @return [Hash] list output converted to ruby Hash def parse_list_output(*args) - list = [] + hash = {} choco_command(*args).stdout.each_line do |line| + next if line.start_with?("Chocolatey v") name, version = line.split("|") - list << [ name.downcase, version.chomp ] + hash[name.downcase] = version&.chomp end - list + hash end # Helper to downcase all names in an array @@ -267,7 +275,7 @@ EOS # @param names [Array] original mixed case names # @return [Array] same names in lower case def lowercase_names(names) - names.map { |name| name.downcase } + names.map(&:downcase) end end end diff --git a/lib/chef/provider/package/dnf.rb b/lib/chef/provider/package/dnf.rb new file mode 100644 index 0000000000..90a5596727 --- /dev/null +++ b/lib/chef/provider/package/dnf.rb @@ -0,0 +1,196 @@ +# +# Copyright:: Copyright 2016-2017, 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" +require "chef/resource/dnf_package" +require "chef/mixin/which" +require "chef/mixin/shell_out" +require "chef/mixin/get_source_from_package" +require "chef/provider/package/dnf/python_helper" +require "chef/provider/package/dnf/version" + +class Chef + class Provider + class Package + class Dnf < Chef::Provider::Package + extend Chef::Mixin::Which + extend Chef::Mixin::ShellOut + include Chef::Mixin::GetSourceFromPackage + + allow_nils + use_multipackage_api + use_package_name_for_source + + # all rhel variants >= 8 will use DNF + provides :package, platform_family: "rhel", platform_version: ">= 8" + + # fedora >= 22 uses DNF + provides :package, platform: "fedora", platform_version: ">= 22" + + # amazon will eventually use DNF + provides :package, platform: "amazon" do + which("dnf") + end + + provides :dnf_package + + # + # Most of the magic in this class happens in the python helper script. The ruby side of this + # provider knows only enough to translate Chef-style new_resource name+package+version into + # a request to the python side. The python side is then responsible for knowing everything + # about RPMs and what is installed and what is available. The ruby side of this class should + # remain a lightweight translation layer to translate Chef requests into RPC requests to + # python. This class knows nothing about how to compare RPM versions, and does not maintain + # any cached state of installed/available versions and should be kept that way. + # + def python_helper + @python_helper ||= PythonHelper.instance + end + + def load_current_resource + flushcache if new_resource.flush_cache[:before] + + @current_resource = Chef::Resource::DnfPackage.new(new_resource.name) + current_resource.package_name(new_resource.package_name) + current_resource.version(get_current_versions) + + current_resource + end + + def define_resource_requirements + requirements.assert(:install, :upgrade, :remove, :purge) do |a| + a.assertion { !new_resource.source || ::File.exist?(new_resource.source) } + a.failure_message Chef::Exceptions::Package, "Package #{new_resource.package_name} not found: #{new_resource.source}" + a.whyrun "assuming #{new_resource.source} would have previously been created" + end + + super + end + + def candidate_version + package_name_array.each_with_index.map do |pkg, i| + available_version(i).version_with_arch + end + end + + def get_current_versions + package_name_array.each_with_index.map do |pkg, i| + installed_version(i).version_with_arch + end + end + + def install_package(names, versions) + if new_resource.source + dnf(options, "-y install", new_resource.source) + else + resolved_names = names.each_with_index.map { |name, i| available_version(i).to_s unless name.nil? } + dnf(options, "-y install", resolved_names) + end + flushcache + end + + # dnf upgrade does not work on uninstalled packaged, while install will upgrade + alias upgrade_package install_package + + def remove_package(names, versions) + resolved_names = names.each_with_index.map { |name, i| installed_version(i).to_s unless name.nil? } + dnf(options, "-y remove", resolved_names) + flushcache + end + + alias purge_package remove_package + + action :flush_cache do + flushcache + end + + private + + def resolve_source_to_version_obj + shell_out_with_timeout!("rpm -qp --queryformat '%{NAME} %{EPOCH} %{VERSION} %{RELEASE} %{ARCH}\n' #{new_resource.source}").stdout.each_line do |line| + # this is another case of committing the sin of doing some lightweight mangling of RPM versions in ruby -- but the output of the rpm command + # does not match what the dnf library accepts. + case line + when /^(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)$/ + return Version.new($1, "#{$2 == '(none)' ? '0' : $2}:#{$3}-#{$4}", $5) + end + end + end + + def version_compare(v1, v2) + python_helper.compare_versions(v1, v2) + end + + # @returns Array<Version> + def available_version(index) + @available_version ||= [] + + @available_version[index] ||= if new_resource.source + resolve_source_to_version_obj + else + python_helper.query(:whatavailable, package_name_array[index], safe_version_array[index], safe_arch_array[index]) + end + + @available_version[index] + end + + # @return [Array<Version>] + def installed_version(index) + @installed_version ||= [] + @installed_version[index] ||= if new_resource.source + python_helper.query(:whatinstalled, available_version(index).name, safe_version_array[index], safe_arch_array[index]) + else + python_helper.query(:whatinstalled, package_name_array[index], safe_version_array[index], safe_arch_array[index]) + end + @installed_version[index] + end + + # cache flushing is accomplished by simply restarting the python helper. this produces a roughly + # 15% hit to the runtime of installing/removing/upgrading packages. correctly using multipackage + # array installs (and the multipackage cookbook) can produce 600% improvements in runtime. + def flushcache + python_helper.restart + end + + def dnf(*args) + shell_out_with_timeout!(a_to_s("dnf", *args)) + end + + def safe_version_array + if new_resource.version.is_a?(Array) + new_resource.version + elsif new_resource.version.nil? + package_name_array.map { nil } + else + [ new_resource.version ] + end + end + + def safe_arch_array + if new_resource.arch.is_a?(Array) + new_resource.arch + elsif new_resource.arch.nil? + package_name_array.map { nil } + else + [ new_resource.arch ] + end + end + + end + end + end +end diff --git a/lib/chef/provider/package/dnf/dnf_helper.py b/lib/chef/provider/package/dnf/dnf_helper.py new file mode 100644 index 0000000000..501d6fceee --- /dev/null +++ b/lib/chef/provider/package/dnf/dnf_helper.py @@ -0,0 +1,100 @@ +#!/usr/bin/env python3 +# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 + +import sys +import dnf +import hawkey +import signal +import os +import json + +base = None + +def get_sack(): + global base + if base is None: + base = dnf.Base() + base.read_all_repos() + base.fill_sack() + return base.sack + +# FIXME: leaks memory and does not work +def flushcache(): + try: + os.remove('/var/cache/dnf/@System.solv') + except OSError: + pass + get_sack().load_system_repo(build_cache=True) + +def versioncompare(versions): + sack = get_sack() + if (versions[0] is None) or (versions[1] is None): + sys.stdout.write('0\n') + else: + evr_comparison = sack.evr_cmp(versions[0], versions[1]) + sys.stdout.write('{}\n'.format(evr_comparison)) + +def query(command): + sack = get_sack() + + subj = dnf.subject.Subject(command['provides']) + q = subj.get_best_query(sack, with_provides=True) + + if command['action'] == "whatinstalled": + q = q.installed() + + if command['action'] == "whatavailable": + q = q.available() + + if 'epoch' in command: + q = q.filterm(epoch=int(command['epoch'])) + if 'version' in command: + q = q.filterm(version__glob=command['version']) + if 'release' in command: + q = q.filterm(release__glob=command['release']) + + if 'arch' in command: + q = q.filterm(arch__glob=command['arch']) + + # only apply the default arch query filter if it returns something + archq = q.filter(arch=[ 'noarch', hawkey.detect_arch() ]) + if len(archq.run()) > 0: + q = archq + + pkgs = q.latest(1).run() + + if not pkgs: + sys.stdout.write('{} nil nil\n'.format(command['provides'].split().pop(0))) + else: + # make sure we picked the package with the highest version + pkgs.sort + pkg = pkgs.pop() + sys.stdout.write('{} {}:{}-{} {}\n'.format(pkg.name, pkg.epoch, pkg.version, pkg.release, pkg.arch)) + +# the design of this helper is that it should try to be 'brittle' and fail hard and exit in order +# to keep process tables clean. additional error handling should probably be added to the retry loop +# on the ruby side. +def exit_handler(signal, frame): + sys.exit(0) + +signal.signal(signal.SIGINT, exit_handler) +signal.signal(signal.SIGHUP, exit_handler) +signal.signal(signal.SIGPIPE, exit_handler) + +while 1: + # kill self if we get orphaned (tragic) + ppid = os.getppid() + if ppid == 1: + sys.exit(0) + line = sys.stdin.readline() + command = json.loads(line) + if command['action'] == "whatinstalled": + query(command) + elif command['action'] == "whatavailable": + query(command) + elif command['action'] == "flushcache": + flushcache() + elif command['action'] == "versioncompare": + versioncompare(command['versions']) + else: + raise RuntimeError("bad command") diff --git a/lib/chef/provider/package/dnf/python_helper.rb b/lib/chef/provider/package/dnf/python_helper.rb new file mode 100644 index 0000000000..5524740fc4 --- /dev/null +++ b/lib/chef/provider/package/dnf/python_helper.rb @@ -0,0 +1,172 @@ +# +# Copyright:: Copyright 2016-2017, 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/mixin/which" +require "chef/mixin/shell_out" +require "chef/provider/package/dnf/version" +require "timeout" + +class Chef + class Provider + class Package + class Dnf < Chef::Provider::Package + class PythonHelper + include Singleton + include Chef::Mixin::Which + include Chef::Mixin::ShellOut + + attr_accessor :stdin + attr_accessor :stdout + attr_accessor :stderr + attr_accessor :wait_thr + + DNF_HELPER = ::File.expand_path(::File.join(::File.dirname(__FILE__), "dnf_helper.py")).freeze + + def dnf_command + @dnf_command ||= which("python", "python3", "python2", "python2.7") do |f| + shell_out("#{f} -c 'import dnf'").exitstatus == 0 + end + " #{DNF_HELPER}" + end + + def start + ENV["PYTHONUNBUFFERED"] = "1" + @stdin, @stdout, @stderr, @wait_thr = Open3.popen3(dnf_command) + end + + def reap + unless wait_thr.nil? + Process.kill("KILL", wait_thr.pid) rescue nil + stdin.close unless stdin.nil? + stdout.close unless stdout.nil? + stderr.close unless stderr.nil? + wait_thr.value # this calls waitpit() + end + end + + def check + start if stdin.nil? + end + + def compare_versions(version1, version2) + with_helper do + json = build_version_query("versioncompare", [version1, version2]) + Chef::Log.trace "sending '#{json}' to python helper" + stdin.syswrite json + "\n" + stdout.sysread(4096).chomp.to_i + end + end + + # @returns Array<Version> + def query(action, provides, version = nil, arch = nil) + with_helper do + json = build_query(action, provides, version, arch) + Chef::Log.trace "sending '#{json}' to python helper" + stdin.syswrite json + "\n" + output = stdout.sysread(4096).chomp + Chef::Log.trace "got '#{output}' from python helper" + version = parse_response(output) + Chef::Log.trace "parsed #{version} from python helper" + version + end + end + + def restart + reap + start + end + + private + + # i couldn't figure out how to decompose an evr on the python side, it seems reasonably + # painless to do it in ruby (generally massaging nevras in the ruby side is HIGHLY + # discouraged -- this is an "every rule has an exception" exception -- any additional + # functionality should probably trigger moving this regexp logic into python) + def add_version(hash, version) + epoch = nil + if version =~ /(\S+):(\S+)/ + epoch = $1 + version = $2 + end + if version =~ /(\S+)-(\S+)/ + version = $1 + release = $2 + end + hash["epoch"] = epoch unless epoch.nil? + hash["release"] = release unless release.nil? + hash["version"] = version + end + + def build_query(action, provides, version, arch) + hash = { "action" => action } + hash["provides"] = provides + add_version(hash, version) unless version.nil? + hash["arch" ] = arch unless arch.nil? + FFI_Yajl::Encoder.encode(hash) + end + + def build_version_query(action, versions) + hash = { "action" => action } + hash["versions"] = versions + FFI_Yajl::Encoder.encode(hash) + end + + def parse_response(output) + array = output.split.map { |x| x == "nil" ? nil : x } + array.each_slice(3).map { |x| Version.new(*x) }.first + end + + def drain_stderr + output = "" + until IO.select([stderr], nil, nil, 0).nil? + output += stderr.sysread(4096).chomp + end + output + rescue + # we must rescue EOFError, and we don't much care about errors on stderr anyway + output + end + + def with_helper + max_retries ||= 5 + ret = nil + Timeout.timeout(600) do + check + ret = yield + end + output = drain_stderr + unless output.empty? + Chef::Log.trace "discarding output on stderr from python helper: #{output}" + end + ret + rescue EOFError, Errno::EPIPE, Timeout::Error, Errno::ESRCH => e + output = drain_stderr + if ( max_retries -= 1 ) > 0 + unless output.empty? + Chef::Log.trace "discarding output on stderr from python helper: #{output}" + end + restart + retry + else + raise e if output.empty? + raise "dnf-helper.py had stderr output:\n\n#{output}" + end + end + end + end + end + end +end diff --git a/lib/chef/provider/package/dnf/version.rb b/lib/chef/provider/package/dnf/version.rb new file mode 100644 index 0000000000..3cff5b0437 --- /dev/null +++ b/lib/chef/provider/package/dnf/version.rb @@ -0,0 +1,56 @@ +# +# Copyright:: Copyright 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. +# + +class Chef + class Provider + class Package + class Dnf < Chef::Provider::Package + + # helper class to assist in passing around name/version/arch triples + class Version + attr_accessor :name + attr_accessor :version + attr_accessor :arch + + def initialize(name, version, arch) + @name = name + @version = version + @arch = arch + end + + def to_s + "#{name}-#{version}.#{arch}" + end + + def version_with_arch + "#{version}.#{arch}" unless version.nil? + end + + def matches_name_and_arch?(other) + other.version == version && other.arch == arch + end + + def ==(other) + name == other.name && version == other.version && arch == other.arch + end + + alias eql? == + end + end + end + end +end diff --git a/lib/chef/provider/package/dpkg.rb b/lib/chef/provider/package/dpkg.rb index a5a80e14d6..aa53f6145f 100644 --- a/lib/chef/provider/package/dpkg.rb +++ b/lib/chef/provider/package/dpkg.rb @@ -23,11 +23,11 @@ class Chef class Provider class Package class Dpkg < Chef::Provider::Package - DPKG_REMOVED = /^Status: deinstall ok config-files/ + DPKG_REMOVED = /^Status: deinstall ok config-files/ DPKG_INSTALLED = /^Status: install ok installed/ - DPKG_VERSION = /^Version: (.+)$/ + DPKG_VERSION = /^Version: (.+)$/ - provides :dpkg_package, os: "linux" + provides :dpkg_package use_multipackage_api use_package_name_for_source @@ -73,18 +73,18 @@ class Chef def install_package(name, version) 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) + logger.info("#{new_resource} installing package(s): #{name.join(' ')}") + run_noninteractive("dpkg", "-i", *options, *sources) end def remove_package(name, version) - Chef::Log.info("#{new_resource} removing package(s): #{name.join(' ')}") - run_noninteractive("dpkg -r", new_resource.options, *name) + logger.info("#{new_resource} removing package(s): #{name.join(' ')}") + run_noninteractive("dpkg", "-r", *options, *name) end def purge_package(name, version) - Chef::Log.info("#{new_resource} purging packages(s): #{name.join(' ')}") - run_noninteractive("dpkg -P", new_resource.options, *name) + logger.info("#{new_resource} purging packages(s): #{name.join(' ')}") + run_noninteractive("dpkg", "-P", *options, *name) end def upgrade_package(name, version) @@ -92,24 +92,39 @@ class Chef end def preseed_package(preseed_file) - Chef::Log.info("#{new_resource} pre-seeding package installation instructions") + logger.info("#{new_resource} pre-seeding package installation instructions") run_noninteractive("debconf-set-selections", *preseed_file) end def reconfig_package(name, version) - Chef::Log.info("#{new_resource} reconfiguring") + logger.info("#{new_resource} reconfiguring") run_noninteractive("dpkg-reconfigure", *name) end # Override the superclass check. Multiple sources are required here. - def check_resource_semantics! - end + def check_resource_semantics!; end private + # compare 2 versions to each other to see which is newer. + # this differs from the standard package method because we + # need to be able to parse debian version strings which contain + # tildes which Gem cannot properly parse + # + # @return [Integer] 1 if v1 > v2. 0 if they're equal. -1 if v1 < v2 + def version_compare(v1, v2) + if !shell_out_compact_timeout("dpkg", "--compare-versions", v1.to_s, "gt", v2.to_s).error? + 1 + elsif !shell_out_compact_timeout("dpkg", "--compare-versions", v1.to_s, "eq", v2.to_s).error? + 0 + else + -1 + end + end + 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}", returns: [0, 1]) + logger.trace("#{new_resource} checking install state of #{package_name}") + status = shell_out_compact_timeout!("dpkg", "-s", package_name, returns: [0, 1]) package_installed = false status.stdout.each_line do |line| case line @@ -120,12 +135,12 @@ class Chef package_installed = true when DPKG_VERSION if package_installed - Chef::Log.debug("#{new_resource} current version is #{$1}") + logger.trace("#{new_resource} current version is #{$1}") return $1 end end end - return nil + nil end def get_current_version_from(array) @@ -137,7 +152,7 @@ class Chef # Runs command via shell_out_with_timeout with magic environment to disable # interactive prompts. def run_noninteractive(*command) - shell_out_with_timeout!(a_to_s(*command), :env => { "DEBIAN_FRONTEND" => "noninteractive" }) + shell_out_compact_timeout!(*command, env: { "DEBIAN_FRONTEND" => "noninteractive" }) end # Returns true if all sources exist. Returns false if any do not, or if no @@ -176,8 +191,8 @@ class Chef @name_pkginfo ||= begin pkginfos = resolved_source_array.map do |src| - Chef::Log.debug("#{new_resource} checking #{src} dpkg status") - status = shell_out_with_timeout!("dpkg-deb -W #{src}") + logger.trace("#{new_resource} checking #{src} dpkg status") + status = shell_out_compact_timeout!("dpkg-deb", "-W", src) status.stdout end Hash[*package_name_array.zip(pkginfos).flatten] diff --git a/lib/chef/provider/package/easy_install.rb b/lib/chef/provider/package/easy_install.rb deleted file mode 100644 index 989f2ab9d2..0000000000 --- a/lib/chef/provider/package/easy_install.rb +++ /dev/null @@ -1,135 +0,0 @@ -# -# Author:: Joe Williams (<joe@joetify.com>) -# Copyright:: Copyright 2009-2016, Joe Williams -# 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" -require "chef/mixin/command" -require "chef/resource/package" - -class Chef - class Provider - class Package - class EasyInstall < Chef::Provider::Package - - provides :easy_install_package - - def install_check(name) - check = false - - begin - # first check to see if we can import it - output = shell_out_with_timeout!("#{python_binary_path} -c \"import #{name}\"", :returns => [0, 1]).stderr - if output.include? "ImportError" - # then check to see if its on the path - output = shell_out_with_timeout!("#{python_binary_path} -c \"import sys; print sys.path\"", :returns => [0, 1]).stdout - if output.downcase.include? "#{name.downcase}" - check = true - end - else - check = true - end - rescue - # it's probably not installed - end - - check - end - - def easy_install_binary_path - path = @new_resource.easy_install_binary - path ? path : "easy_install" - end - - def python_binary_path - path = @new_resource.python_binary - path ? path : "python" - end - - def module_name - m = @new_resource.module_name - m ? m : @new_resource.name - end - - def load_current_resource - @current_resource = Chef::Resource::Package.new(@new_resource.name) - @current_resource.package_name(@new_resource.package_name) - - # get the currently installed version if installed - package_version = nil - if install_check(module_name) - begin - output = shell_out_with_timeout!("#{python_binary_path} -c \"import #{module_name}; print #{module_name}.__version__\"").stdout - package_version = output.strip - rescue - output = shell_out_with_timeout!("#{python_binary_path} -c \"import sys; print sys.path\"", :returns => [0, 1]).stdout - - output_array = output.gsub(/[\[\]]/, "").split(/\s*,\s*/) - package_path = "" - - output_array.each do |entry| - if entry.downcase.include?(@new_resource.package_name) - package_path = entry - end - end - - package_path[/\S\S(.*)\/(.*)-(.*)-py(.*).egg\S/] - package_version = $3 - end - end - - if package_version == @new_resource.version - Chef::Log.debug("#{@new_resource} at version #{@new_resource.version}") - @current_resource.version(@new_resource.version) - else - Chef::Log.debug("#{@new_resource} at version #{package_version}") - @current_resource.version(package_version) - end - - @current_resource - end - - def candidate_version - return @candidate_version if @candidate_version - - # do a dry run to get the latest version - result = shell_out_with_timeout!("#{easy_install_binary_path} -n #{@new_resource.package_name}", :returns => [0, 1]) - @candidate_version = result.stdout[/(.*)Best match: (.*) (.*)$/, 3] - @candidate_version - end - - def install_package(name, version) - Chef.log_deprecation("The easy_install package provider is deprecated and will be removed in Chef 13.") - run_command(:command => "#{easy_install_binary_path}#{expand_options(@new_resource.options)} \"#{name}==#{version}\"") - end - - def upgrade_package(name, version) - install_package(name, version) - end - - def remove_package(name, version) - Chef.log_deprecation("The easy_install package provider is deprecated and will be removed in Chef 13.") - run_command(:command => "#{easy_install_binary_path }#{expand_options(@new_resource.options)} -m #{name}") - end - - def purge_package(name, version) - remove_package(name, version) - end - - end - end - end -end diff --git a/lib/chef/provider/package/freebsd/base.rb b/lib/chef/provider/package/freebsd/base.rb index 7104a71f70..fc62fa7cc0 100644 --- a/lib/chef/provider/package/freebsd/base.rb +++ b/lib/chef/provider/package/freebsd/base.rb @@ -47,7 +47,7 @@ class Chef # Otherwise look up the path to the ports directory using 'whereis' else - whereis = shell_out_with_timeout!("whereis -s #{port}", :env => nil) + whereis = shell_out_compact_timeout!("whereis", "-s", port, env: nil) unless path = whereis.stdout[/^#{Regexp.escape(port)}:\s+(.+)$/, 1] raise Chef::Exceptions::Package, "Could not find port with the name #{port}" end @@ -56,9 +56,9 @@ class Chef end def makefile_variable_value(variable, dir = nil) - options = dir ? { :cwd => dir } : {} - make_v = shell_out_with_timeout!("make -V #{variable}", options.merge!(:env => nil, :returns => [0, 1])) - make_v.exitstatus.zero? ? make_v.stdout.strip.split($\).first : nil # $\ is the line separator, i.e. newline. + options = dir ? { cwd: dir } : {} + make_v = shell_out_compact_timeout!("make", "-V", variable, options.merge!(env: nil, returns: [0, 1])) + make_v.exitstatus == 0 ? make_v.stdout.strip.split($OUTPUT_RECORD_SEPARATOR).first : nil # $\ is the line separator, i.e. newline. end end @@ -67,19 +67,19 @@ class Chef def initialize(*args) super - @current_resource = Chef::Resource::Package.new(@new_resource.name) + @current_resource = Chef::Resource::Package.new(new_resource.name) end def load_current_resource - @current_resource.package_name(@new_resource.package_name) + current_resource.package_name(new_resource.package_name) - @current_resource.version(current_installed_version) - Chef::Log.debug("#{@new_resource} current version is #{@current_resource.version}") if @current_resource.version + current_resource.version(current_installed_version) + logger.trace("#{new_resource} current version is #{current_resource.version}") if current_resource.version @candidate_version = candidate_version - Chef::Log.debug("#{@new_resource} candidate version is #{@candidate_version}") if @candidate_version + logger.trace("#{new_resource} candidate version is #{@candidate_version}") if @candidate_version - @current_resource + current_resource end end diff --git a/lib/chef/provider/package/freebsd/pkg.rb b/lib/chef/provider/package/freebsd/pkg.rb index 78d9449454..04e6e5c427 100644 --- a/lib/chef/provider/package/freebsd/pkg.rb +++ b/lib/chef/provider/package/freebsd/pkg.rb @@ -30,28 +30,28 @@ class Chef include PortsHelper def install_package(name, version) - unless @current_resource.version - case @new_resource.source + unless current_resource.version + case new_resource.source when /^http/, /^ftp/ - if @new_resource.source =~ /\/$/ - shell_out_with_timeout!("pkg_add -r #{package_name}", :env => { "PACKAGESITE" => @new_resource.source, "LC_ALL" => nil }).status + if new_resource.source =~ /\/$/ + shell_out_compact_timeout!("pkg_add", "-r", package_name, env: { "PACKAGESITE" => new_resource.source, "LC_ALL" => nil }).status else - shell_out_with_timeout!("pkg_add -r #{package_name}", :env => { "PACKAGEROOT" => @new_resource.source, "LC_ALL" => nil }).status + shell_out_compact_timeout!("pkg_add", "-r", package_name, env: { "PACKAGEROOT" => new_resource.source, "LC_ALL" => nil }).status end - Chef::Log.debug("#{@new_resource} installed from: #{@new_resource.source}") + logger.trace("#{new_resource} installed from: #{new_resource.source}") when /^\// - shell_out_with_timeout!("pkg_add #{file_candidate_version_path}", :env => { "PKG_PATH" => @new_resource.source , "LC_ALL" => nil }).status - Chef::Log.debug("#{@new_resource} installed from: #{@new_resource.source}") + shell_out_compact_timeout!("pkg_add", file_candidate_version_path, env: { "PKG_PATH" => new_resource.source, "LC_ALL" => nil }).status + logger.trace("#{new_resource} installed from: #{new_resource.source}") else - shell_out_with_timeout!("pkg_add -r #{latest_link_name}", :env => nil).status + shell_out_compact_timeout!("pkg_add", "-r", latest_link_name, env: nil).status end end end def remove_package(name, version) - shell_out_with_timeout!("pkg_delete #{package_name}-#{version || @current_resource.version}", :env => nil).status + shell_out_compact_timeout!("pkg_delete", "#{package_name}-#{version || current_resource.version}", env: nil).status end # The name of the package (without the version number) as understood by pkg_add and pkg_info. @@ -63,7 +63,7 @@ class Chef raise Chef::Exceptions::Package, "Unexpected form for PKGNAME variable in #{port_path}/Makefile" end else - @new_resource.package_name + new_resource.package_name end end @@ -72,12 +72,12 @@ class Chef end def current_installed_version - pkg_info = shell_out_with_timeout!("pkg_info -E \"#{package_name}*\"", :env => nil, :returns => [0, 1]) + pkg_info = shell_out_compact_timeout!("pkg_info", "-E", "#{package_name}*", env: nil, returns: [0, 1]) pkg_info.stdout[/^#{Regexp.escape(package_name)}-(.+)/, 1] end def candidate_version - case @new_resource.source + case new_resource.source when /^http/, /^ftp/ repo_candidate_version when /^\// @@ -88,7 +88,7 @@ class Chef end def file_candidate_version_path - Dir[Chef::Util::PathHelper.escape_glob_dir("#{@new_resource.source}/#{@current_resource.package_name}") + "*"][-1].to_s + Dir[Chef::Util::PathHelper.escape_glob_dir("#{new_resource.source}/#{current_resource.package_name}") + "*"][-1].to_s end def file_candidate_version @@ -104,7 +104,7 @@ class Chef end def port_path - port_dir @new_resource.package_name + port_dir new_resource.package_name end end diff --git a/lib/chef/provider/package/freebsd/pkgng.rb b/lib/chef/provider/package/freebsd/pkgng.rb index de7bea6387..c9c0947f9b 100644 --- a/lib/chef/provider/package/freebsd/pkgng.rb +++ b/lib/chef/provider/package/freebsd/pkgng.rb @@ -25,46 +25,44 @@ class Chef class Pkgng < Base def install_package(name, version) - unless @current_resource.version - case @new_resource.source + unless current_resource.version + case new_resource.source when /^(http|ftp|\/)/ - shell_out_with_timeout!("pkg add#{expand_options(@new_resource.options)} #{@new_resource.source}", :env => { "LC_ALL" => nil }).status - Chef::Log.debug("#{@new_resource} installed from: #{@new_resource.source}") - + shell_out_compact_timeout!("pkg", "add", options, new_resource.source, env: { "LC_ALL" => nil }).status + logger.trace("#{new_resource} installed from: #{new_resource.source}") else - shell_out_with_timeout!("pkg install -y#{expand_options(@new_resource.options)} #{name}", :env => { "LC_ALL" => nil }).status + shell_out_compact_timeout!("pkg", "install", "-y", options, name, env: { "LC_ALL" => nil }).status end end end def remove_package(name, version) - options = @new_resource.options && @new_resource.options.sub(repo_regex, "") - options && !options.empty? || options = nil - shell_out_with_timeout!("pkg delete -y#{expand_options(options)} #{name}#{version ? '-' + version : ''}", :env => nil).status + options_dup = options && options.map { |str| str.sub(repo_regex, "") }.reject!(&:empty?) + shell_out_compact_timeout!("pkg", "delete", "-y", options_dup, "#{name}#{version ? '-' + version : ''}", env: nil).status end def current_installed_version - pkg_info = shell_out_with_timeout!("pkg info \"#{@new_resource.package_name}\"", :env => nil, :returns => [0, 70]) + pkg_info = shell_out_compact_timeout!("pkg", "info", new_resource.package_name, env: nil, returns: [0, 70]) pkg_info.stdout[/^Version +: (.+)$/, 1] end def candidate_version - @new_resource.source ? file_candidate_version : repo_candidate_version + new_resource.source ? file_candidate_version : repo_candidate_version end private def file_candidate_version - @new_resource.source[/#{Regexp.escape(@new_resource.package_name)}-(.+)\.txz/, 1] + new_resource.source[/#{Regexp.escape(new_resource.package_name)}-(.+)\.txz/, 1] end def repo_candidate_version - if @new_resource.options && @new_resource.options.match(repo_regex) - options = $1 + if options && options.join(" ").match(repo_regex) + options = $1.split(" ") end - pkg_query = shell_out_with_timeout!("pkg rquery#{expand_options(options)} '%v' #{@new_resource.package_name}", :env => nil) - pkg_query.exitstatus.zero? ? pkg_query.stdout.strip.split(/\n/).last : nil + pkg_query = shell_out_compact_timeout!("pkg", "rquery", options, "%v", new_resource.package_name, env: nil) + pkg_query.exitstatus == 0 ? pkg_query.stdout.strip.split(/\n/).last : nil end def repo_regex diff --git a/lib/chef/provider/package/freebsd/port.rb b/lib/chef/provider/package/freebsd/port.rb index 3eb3c5ab01..e87be4d304 100644 --- a/lib/chef/provider/package/freebsd/port.rb +++ b/lib/chef/provider/package/freebsd/port.rb @@ -26,20 +26,20 @@ class Chef include PortsHelper def install_package(name, version) - shell_out_with_timeout!("make -DBATCH install clean", :timeout => 1800, :env => nil, :cwd => port_dir).status + shell_out_compact_timeout!("make", "-DBATCH", "install", "clean", timeout: 1800, env: nil, cwd: port_dir).status end def remove_package(name, version) - shell_out_with_timeout!("make deinstall", :timeout => 300, :env => nil, :cwd => port_dir).status + shell_out_compact_timeout!("make", "deinstall", timeout: 300, env: nil, cwd: port_dir).status end def current_installed_version - pkg_info = if @new_resource.supports_pkgng? - shell_out_with_timeout!("pkg info \"#{@new_resource.package_name}\"", :env => nil, :returns => [0, 70]) + pkg_info = if new_resource.supports_pkgng? + shell_out_compact_timeout!("pkg", "info", new_resource.package_name, env: nil, returns: [0, 70]) else - shell_out_with_timeout!("pkg_info -E \"#{@new_resource.package_name}*\"", :env => nil, :returns => [0, 1]) + shell_out_compact_timeout!("pkg_info", "-E", "#{new_resource.package_name}*", env: nil, returns: [0, 1]) end - pkg_info.stdout[/^#{Regexp.escape(@new_resource.package_name)}-(.+)/, 1] + pkg_info.stdout[/^#{Regexp.escape(new_resource.package_name)}-(.+)/, 1] end def candidate_version @@ -51,7 +51,7 @@ class Chef end def port_dir - super(@new_resource.package_name) + super(new_resource.package_name) end end end diff --git a/lib/chef/provider/package/homebrew.rb b/lib/chef/provider/package/homebrew.rb index a105f6d7d0..643faf23c6 100644 --- a/lib/chef/provider/package/homebrew.rb +++ b/lib/chef/provider/package/homebrew.rb @@ -32,21 +32,21 @@ class Chef include Chef::Mixin::HomebrewUser def load_current_resource - self.current_resource = Chef::Resource::Package.new(new_resource.name) + self.current_resource = Chef::Resource::HomebrewPackage.new(new_resource.name) current_resource.package_name(new_resource.package_name) current_resource.version(current_installed_version) - Chef::Log.debug("#{new_resource} current version is #{current_resource.version}") if current_resource.version + logger.trace("#{new_resource} current version is #{current_resource.version}") if current_resource.version @candidate_version = candidate_version - Chef::Log.debug("#{new_resource} candidate version is #{@candidate_version}") if @candidate_version + logger.trace("#{new_resource} candidate version is #{@candidate_version}") if @candidate_version current_resource end def install_package(name, version) unless current_resource.version == version - brew("install", new_resource.options, name) + brew("install", options, name) end end @@ -56,24 +56,25 @@ class Chef if current_version.nil? || current_version.empty? install_package(name, version) elsif current_version != version - brew("upgrade", new_resource.options, name) + brew("upgrade", options, name) end end def remove_package(name, version) if current_resource.version - brew("uninstall", new_resource.options, name) + brew("uninstall", options, name) end end # Homebrew doesn't really have a notion of purging, do a "force remove" def purge_package(name, version) - new_resource.options((new_resource.options || "") << " --force").strip - remove_package(name, version) + if current_resource.version + brew("uninstall", "--force", options, name) + end end def brew(*args) - get_response_from_command("brew #{args.join(' ')}") + get_response_from_command("brew", *args) end # We implement a querying method that returns the JSON-as-Hash @@ -121,13 +122,13 @@ class Chef private - def get_response_from_command(command) + def get_response_from_command(*command) homebrew_uid = find_homebrew_uid(new_resource.respond_to?(:homebrew_user) && new_resource.homebrew_user) homebrew_user = Etc.getpwuid(homebrew_uid) - Chef::Log.debug "Executing '#{command}' as user '#{homebrew_user.name}'" + logger.trace "Executing '#{command.join(' ')}' as user '#{homebrew_user.name}'" # FIXME: this 1800 second default timeout should be deprecated - output = shell_out_with_timeout!(command, :timeout => 1800, :user => homebrew_uid, :environment => { "HOME" => homebrew_user.dir, "RUBYOPT" => nil, "TMPDIR" => nil }) + output = shell_out_compact_timeout!(*command, timeout: 1800, user: homebrew_uid, environment: { "HOME" => homebrew_user.dir, "RUBYOPT" => nil, "TMPDIR" => nil }) output.stdout.chomp end diff --git a/lib/chef/provider/package/ips.rb b/lib/chef/provider/package/ips.rb index 85053d47f2..cabc7fc68b 100644 --- a/lib/chef/provider/package/ips.rb +++ b/lib/chef/provider/package/ips.rb @@ -1,7 +1,7 @@ # # Author:: Jason J. W. Williams (<williamsjj@digitar.com>) # Author:: Stephen Nelson-Smith (<sns@chef.io>) -# Copyright:: Copyright 2011-2016, Chef Software Inc. +# Copyright:: Copyright 2011-2017, Chef Software Inc. # License:: Apache License, Version 2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -19,7 +19,6 @@ require "open3" require "chef/provider/package" -require "chef/mixin/command" require "chef/resource/package" class Chef @@ -28,7 +27,7 @@ class Chef class Ips < Chef::Provider::Package provides :package, platform: %w{openindiana opensolaris omnios solaris2} - provides :ips_package, os: "solaris2" + provides :ips_package attr_accessor :virtual @@ -36,45 +35,40 @@ class Chef super requirements.assert(:all_actions) do |a| - a.assertion { ! @candidate_version.nil? } - a.failure_message Chef::Exceptions::Package, "Package #{@new_resource.package_name} not found" - a.whyrun "Assuming package #{@new_resource.package_name} would have been made available." + a.assertion { !@candidate_version.nil? } + a.failure_message Chef::Exceptions::Package, "Package #{new_resource.package_name} not found" + a.whyrun "Assuming package #{new_resource.package_name} would have been made available." end end def get_current_version - shell_out_with_timeout("pkg info #{@new_resource.package_name}").stdout.each_line do |line| + shell_out_compact_timeout("pkg", "info", new_resource.package_name).stdout.each_line do |line| return $1.split[0] if line =~ /^\s+Version: (.*)/ end - return nil + nil end def get_candidate_version - shell_out_with_timeout!("pkg info -r #{new_resource.package_name}").stdout.each_line do |line| + shell_out_compact_timeout!("pkg", "info", "-r", new_resource.package_name).stdout.each_line do |line| return $1.split[0] if line =~ /Version: (.*)/ end - return nil + nil end def load_current_resource - @current_resource = Chef::Resource::Package.new(@new_resource.name) - @current_resource.package_name(@new_resource.package_name) - Chef::Log.debug("Checking package status for #{@new_resource.name}") - @current_resource.version(get_current_version) + @current_resource = Chef::Resource::IpsPackage.new(new_resource.name) + current_resource.package_name(new_resource.package_name) + logger.trace("Checking package status for #{new_resource.name}") + current_resource.version(get_current_version) @candidate_version = get_candidate_version - @current_resource + current_resource end def install_package(name, version) - package_name = "#{name}@#{version}" - normal_command = "pkg#{expand_options(@new_resource.options)} install -q #{package_name}" - command = - if @new_resource.respond_to?(:accept_license) && @new_resource.accept_license - normal_command.gsub("-q", "-q --accept") - else - normal_command - end - shell_out_with_timeout(command) + command = [ "pkg", options, "install", "-q" ] + command << "--accept" if new_resource.accept_license + command << "#{name}@#{version}" + shell_out_compact_timeout!(command) end def upgrade_package(name, version) @@ -83,7 +77,7 @@ class Chef def remove_package(name, version) package_name = "#{name}@#{version}" - shell_out_with_timeout!( "pkg#{expand_options(@new_resource.options)} uninstall -q #{package_name}" ) + shell_out_compact_timeout!( "pkg", options, "uninstall", "-q", package_name ) end end end diff --git a/lib/chef/provider/package/macports.rb b/lib/chef/provider/package/macports.rb index 7bbc68aba8..ddaf19a76f 100644 --- a/lib/chef/provider/package/macports.rb +++ b/lib/chef/provider/package/macports.rb @@ -7,25 +7,25 @@ class Chef provides :macports_package def load_current_resource - @current_resource = Chef::Resource::Package.new(@new_resource.name) - @current_resource.package_name(@new_resource.package_name) + @current_resource = Chef::Resource::Package.new(new_resource.name) + current_resource.package_name(new_resource.package_name) - @current_resource.version(current_installed_version) - Chef::Log.debug("#{@new_resource} current version is #{@current_resource.version}") if @current_resource.version + current_resource.version(current_installed_version) + logger.trace("#{new_resource} current version is #{current_resource.version}") if current_resource.version @candidate_version = macports_candidate_version - if !@new_resource.version && !@candidate_version - raise Chef::Exceptions::Package, "Could not get a candidate version for this package -- #{@new_resource.name} does not seem to be a valid package!" + if !new_resource.version && !@candidate_version + raise Chef::Exceptions::Package, "Could not get a candidate version for this package -- #{new_resource.name} does not seem to be a valid package!" end - Chef::Log.debug("#{@new_resource} candidate version is #{@candidate_version}") if @candidate_version + logger.trace("#{new_resource} candidate version is #{@candidate_version}") if @candidate_version - @current_resource + current_resource end def current_installed_version - command = "port installed #{@new_resource.package_name}" + command = [ "port", "installed", new_resource.package_name ] output = get_response_from_command(command) response = nil @@ -37,7 +37,7 @@ class Chef end def macports_candidate_version - command = "port info --version #{@new_resource.package_name}" + command = [ "port", "info", "--version", new_resource.package_name ] output = get_response_from_command(command) match = output.match(/^version: (.+)$/) @@ -46,37 +46,37 @@ class Chef end def install_package(name, version) - unless @current_resource.version == version - command = "port#{expand_options(@new_resource.options)} install #{name}" - command << " @#{version}" if version && !version.empty? - shell_out_with_timeout!(command) + unless current_resource.version == version + command = [ "port", options, "install", name ] + command << "@#{version}" if version && !version.empty? + shell_out_compact_timeout!(command) end end def purge_package(name, version) - command = "port#{expand_options(@new_resource.options)} uninstall #{name}" - command << " @#{version}" if version && !version.empty? - shell_out_with_timeout!(command) + command = [ "port", options, "uninstall", name ] + command << "@#{version}" if version && !version.empty? + shell_out_compact_timeout!(command) end def remove_package(name, version) - command = "port#{expand_options(@new_resource.options)} deactivate #{name}" - command << " @#{version}" if version && !version.empty? + command = [ "port", options, "deactivate", name ] + command << "@#{version}" if version && !version.empty? - shell_out_with_timeout!(command) + shell_out_compact_timeout!(command) end def upgrade_package(name, version) # Saving this to a variable -- weird rSpec behavior # happens otherwise... - current_version = @current_resource.version + current_version = current_resource.version if current_version.nil? || current_version.empty? # Macports doesn't like when you upgrade a package # that hasn't been installed. install_package(name, version) elsif current_version != version - shell_out_with_timeout!( "port#{expand_options(@new_resource.options)} upgrade #{name} @#{version}" ) + shell_out_compact_timeout!( "port", options, "upgrade", name, "@#{version}" ) end end @@ -84,14 +84,14 @@ class Chef def get_response_from_command(command) output = nil - status = shell_out_with_timeout(command) + status = shell_out_compact_timeout(command) begin output = status.stdout rescue Exception raise Chef::Exceptions::Package, "Could not read from STDOUT on command: #{command}" end unless status.exitstatus == 0 || status.exitstatus == 1 - raise Chef::Exceptions::Package, "#{command} failed - #{status.insect}!" + raise Chef::Exceptions::Package, "#{command} failed - #{status.inspect}!" end output end diff --git a/lib/chef/provider/package/msu.rb b/lib/chef/provider/package/msu.rb new file mode 100644 index 0000000000..c4e53a0fdf --- /dev/null +++ b/lib/chef/provider/package/msu.rb @@ -0,0 +1,161 @@ +# +# Author:: Nimisha Sharad (<nimisha.sharad@msystechnologies.com>) +# Copyright:: Copyright 2015-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. +# + +# msu_package leverages cab_package +# The contents of msu file are extracted, which contains one or more cab files. +# The extracted cab files are installed using Chef::Resource::Package::CabPackage +# Reference: https://support.microsoft.com/en-in/kb/934307 +require "chef/provider/package" +require "chef/resource/msu_package" +require "chef/mixin/shell_out" +require "chef/provider/package/cab" +require "chef/util/path_helper" +require "chef/mixin/uris" +require "chef/mixin/checksum" + +class Chef + class Provider + class Package + class Msu < Chef::Provider::Package + include Chef::Mixin::ShellOut + include Chef::Mixin::Uris + include Chef::Mixin::Checksum + + provides :msu_package + + def load_current_resource + @current_resource = Chef::Resource::MsuPackage.new(new_resource.name) + + # download file if source is a url + msu_file = uri_scheme?(new_resource.source) ? download_source_file : new_resource.source + + # temp directory where the contents of msu file get extracted + @temp_directory = Dir.mktmpdir("chef") + extract_msu_contents(msu_file, @temp_directory) + @cab_files = read_cab_files_from_xml(@temp_directory) + + if @cab_files.empty? + raise Chef::Exceptions::Package, "Corrupt MSU package: MSU package XML does not contain any cab file" + else + current_resource.version(get_current_versions) + end + current_resource + end + + def get_current_versions + @cab_files.map do |cabfile| + cab_pkg = get_cab_package(cabfile) + cab_pkg.installed_version + end + end + + def get_candidate_versions + @cab_files.map do |cabfile| + cab_pkg = get_cab_package(cabfile) + cab_pkg.package_version + end + end + + def candidate_version + @candidate_version ||= get_candidate_versions + end + + def get_cab_package(cab_file) + cab_resource = new_resource + cab_resource.source = cab_file + Chef::Provider::Package::Cab.new(cab_resource, nil) + end + + def download_source_file + source_resource.run_action(:create) + logger.trace("#{new_resource} fetched source file to #{source_resource.path}") + source_resource.path + end + + def source_resource + @source_resource ||= declare_resource(:remote_file, new_resource.name) do + path default_download_cache_path + source new_resource.source + checksum new_resource.checksum + backup false + end + end + + def default_download_cache_path + uri = ::URI.parse(new_resource.source) + filename = ::File.basename(::URI.unescape(uri.path)) + file_cache_dir = Chef::FileCache.create_cache_path("package/") + Chef::Util::PathHelper.cleanpath("#{file_cache_dir}/#{filename}") + end + + def install_package(name, version) + # use cab_package resource to install the extracted cab packages + @cab_files.each do |cab_file| + declare_resource(:cab_package, new_resource.name) do + source cab_file + action :install + end + end + end + + def remove_package(name, version) + # use cab_package provider to remove the extracted cab packages + @cab_files.each do |cab_file| + declare_resource(:cab_package, new_resource.name) do + source cab_file + action :remove + end + end + end + + def extract_msu_contents(msu_file, destination) + with_os_architecture(nil) do + shell_out_with_timeout!("#{ENV['SYSTEMROOT']}\\system32\\expand.exe -f:* #{msu_file} #{destination}") + end + end + + # msu package can contain multiple cab files + # Reading cab files from xml to ensure the order of installation in case of multiple cab files + def read_cab_files_from_xml(msu_dir) + # get the file with .xml extension + xml_files = Dir.glob("#{msu_dir}/*.xml") + cab_files = [] + + if xml_files.empty? + raise Chef::Exceptions::Package, "Corrupt MSU package: MSU package doesn't contain any xml file" + else + # msu package contains only single xml file. So using xml_files.first is sufficient + doc = ::File.open(xml_files.first.to_s) { |f| REXML::Document.new f } + locations = doc.elements.each("unattend/servicing/package/source") { |element| element.attributes["location"] } + locations.each do |loc| + cab_files << msu_dir + "/" + loc.attribute("location").value.split("\\")[1] + end + + cab_files + end + cab_files + end + + def cleanup_after_converge + # delete the temp directory where the contents of msu file are extracted + FileUtils.rm_rf(@temp_directory) if Dir.exist?(@temp_directory) + end + end + end + end +end diff --git a/lib/chef/provider/package/openbsd.rb b/lib/chef/provider/package/openbsd.rb index 2120b9aa48..f528c48f08 100644 --- a/lib/chef/provider/package/openbsd.rb +++ b/lib/chef/provider/package/openbsd.rb @@ -42,9 +42,9 @@ class Chef end def load_current_resource - @current_resource.package_name(new_resource.package_name) - @current_resource.version(installed_version) - @current_resource + current_resource.package_name(new_resource.package_name) + current_resource.version(installed_version) + current_resource end def define_resource_requirements @@ -68,12 +68,12 @@ class Chef end def install_package(name, version) - unless @current_resource.version + unless current_resource.version if parts = name.match(/^(.+?)--(.+)/) # use double-dash for stems with flavors, see man page for pkg_add name = parts[1] end - shell_out_with_timeout!("pkg_add -r #{name}#{version_string(version)}", :env => { "PKG_PATH" => pkg_path }).status - Chef::Log.debug("#{new_resource.package_name} installed") + shell_out_compact_timeout!("pkg_add", "-r", package_string(name, version), env: { "PKG_PATH" => pkg_path }).status + logger.trace("#{new_resource.package_name} installed") end end @@ -81,35 +81,35 @@ class Chef if parts = name.match(/^(.+?)--(.+)/) name = parts[1] end - shell_out_with_timeout!("pkg_delete #{name}#{version_string(version)}", :env => nil).status + shell_out_compact_timeout!("pkg_delete", package_string(name, version), env: nil).status end private def installed_version - if parts = new_resource.package_name.match(/^(.+?)--(.+)/) - name = parts[1] - else - name = new_resource.package_name - end - pkg_info = shell_out_with_timeout!("pkg_info -e \"#{name}->0\"", :env => nil, :returns => [0, 1]) + name = if parts = new_resource.package_name.match(/^(.+?)--(.+)/) + parts[1] + else + new_resource.package_name + end + pkg_info = shell_out_compact_timeout!("pkg_info", "-e", "#{name}->0", env: nil, returns: [0, 1]) result = pkg_info.stdout[/^inst:#{Regexp.escape(name)}-(.+?)\s/, 1] - Chef::Log.debug("installed_version of '#{new_resource.package_name}' is '#{result}'") + logger.trace("installed_version of '#{new_resource.package_name}' is '#{result}'") result end def candidate_version @candidate_version ||= begin results = [] - shell_out_with_timeout!("pkg_info -I \"#{new_resource.package_name}#{version_string(new_resource.version)}\"", :env => nil, :returns => [0, 1]).stdout.each_line do |line| - if parts = new_resource.package_name.match(/^(.+?)--(.+)/) - results << line[/^#{Regexp.escape(parts[1])}-(.+?)\s/, 1] - else - results << line[/^#{Regexp.escape(new_resource.package_name)}-(.+?)\s/, 1] - end + shell_out_compact_timeout!("pkg_info", "-I", package_string(new_resource.package_name, new_resource.version), env: nil, returns: [0, 1]).stdout.each_line do |line| + results << if parts = new_resource.package_name.match(/^(.+?)--(.+)/) + line[/^#{Regexp.escape(parts[1])}-(.+?)\s/, 1] + else + line[/^#{Regexp.escape(new_resource.package_name)}-(.+?)\s/, 1] + end end results = results.reject(&:nil?) - Chef::Log.debug("Candidate versions of '#{new_resource.package_name}' are '#{results}'") + logger.trace("Candidate versions of '#{new_resource.package_name}' are '#{results}'") case results.length when 0 [] @@ -121,13 +121,16 @@ class Chef end end - def version_string(version) - ver = "" - ver += "-#{version}" if version + def package_string(name, version) + if version + "#{name}-#{version}" + else + name + end end def pkg_path - ENV["PKG_PATH"] || "http://ftp.OpenBSD.org/pub/#{node.kernel.name}/#{node.kernel.release}/packages/#{node.kernel.machine}/" + ENV["PKG_PATH"] || "http://ftp.OpenBSD.org/pub/#{node['kernel']['name']}/#{node['kernel']['release']}/packages/#{node['kernel']['machine']}/" end end diff --git a/lib/chef/provider/package/pacman.rb b/lib/chef/provider/package/pacman.rb index bd8028d881..f6dde66219 100644 --- a/lib/chef/provider/package/pacman.rb +++ b/lib/chef/provider/package/pacman.rb @@ -17,7 +17,6 @@ # require "chef/provider/package" -require "chef/mixin/command" require "chef/resource/package" class Chef @@ -26,19 +25,19 @@ class Chef class Pacman < Chef::Provider::Package provides :package, platform: "arch" - provides :pacman_package, os: "linux" + provides :pacman_package def load_current_resource - @current_resource = Chef::Resource::Package.new(@new_resource.name) - @current_resource.package_name(@new_resource.package_name) + @current_resource = Chef::Resource::Package.new(new_resource.name) + current_resource.package_name(new_resource.package_name) - Chef::Log.debug("#{@new_resource} checking pacman for #{@new_resource.package_name}") - status = shell_out_with_timeout("pacman -Qi #{@new_resource.package_name}") + logger.trace("#{new_resource} checking pacman for #{new_resource.package_name}") + status = shell_out_compact_timeout("pacman", "-Qi", new_resource.package_name) status.stdout.each_line do |line| case line when /^Version(\s?)*: (.+)$/ - Chef::Log.debug("#{@new_resource} current version is #{$2}") - @current_resource.version($2) + logger.trace("#{new_resource} current version is #{$2}") + current_resource.version($2) end end @@ -46,7 +45,7 @@ class Chef raise Chef::Exceptions::Package, "pacman failed - #{status.inspect}!" end - @current_resource + current_resource end def candidate_version @@ -54,20 +53,20 @@ class Chef repos = %w{extra core community} - if ::File.exists?("/etc/pacman.conf") + if ::File.exist?("/etc/pacman.conf") pacman = ::File.read("/etc/pacman.conf") repos = pacman.scan(/\[(.+)\]/).flatten end package_repos = repos.map { |r| Regexp.escape(r) }.join("|") - status = shell_out_with_timeout("pacman -Sl") + status = shell_out_compact_timeout("pacman", "-Sl") status.stdout.each_line do |line| case line - when /^(#{package_repos}) #{Regexp.escape(@new_resource.package_name)} (.+)$/ - # $2 contains a string like "4.4.0-1" or "3.10-4 [installed]" - # simply split by space and use first token - @candidate_version = $2.split(" ").first + when /^(#{package_repos}) #{Regexp.escape(new_resource.package_name)} (.+)$/ + # $2 contains a string like "4.4.0-1" or "3.10-4 [installed]" + # simply split by space and use first token + @candidate_version = $2.split(" ").first end end @@ -76,14 +75,14 @@ class Chef end unless @candidate_version - raise Chef::Exceptions::Package, "pacman does not have a version of package #{@new_resource.package_name}" + raise Chef::Exceptions::Package, "pacman does not have a version of package #{new_resource.package_name}" end @candidate_version end def install_package(name, version) - shell_out_with_timeout!( "pacman --sync --noconfirm --noprogressbar#{expand_options(@new_resource.options)} #{name}" ) + shell_out_compact_timeout!( "pacman", "--sync", "--noconfirm", "--noprogressbar", options, name) end def upgrade_package(name, version) @@ -91,7 +90,7 @@ class Chef end def remove_package(name, version) - shell_out_with_timeout!( "pacman --remove --noconfirm --noprogressbar#{expand_options(@new_resource.options)} #{name}" ) + shell_out_compact_timeout!( "pacman", "--remove", "--noconfirm", "--noprogressbar", options, name ) end def purge_package(name, version) diff --git a/lib/chef/provider/package/paludis.rb b/lib/chef/provider/package/paludis.rb index 557e7ebc22..f6274d7553 100644 --- a/lib/chef/provider/package/paludis.rb +++ b/lib/chef/provider/package/paludis.rb @@ -25,41 +25,40 @@ class Chef class Paludis < Chef::Provider::Package provides :package, platform: "exherbo" - provides :paludis_package, os: "linux" + provides :paludis_package def load_current_resource - @current_resource = Chef::Resource::Package.new(@new_resource.package_name) - @current_resource.package_name(@new_resource.package_name) + @current_resource = Chef::Resource::Package.new(new_resource.package_name) + current_resource.package_name(new_resource.package_name) - Chef::Log.debug("Checking package status for #{@new_resource.package_name}") + logger.trace("Checking package status for #{new_resource.package_name}") installed = false re = Regexp.new("(.*)[[:blank:]](.*)[[:blank:]](.*)$") - shell_out!("cave -L warning print-ids -M none -m \"#{@new_resource.package_name}\" -f \"%c/%p %v %r\n\"").stdout.each_line do |line| + shell_out_compact!("cave", "-L", "warning", "print-ids", "-M", "none", "-m", new_resource.package_name, "-f", "%c/%p %v %r\n").stdout.each_line do |line| res = re.match(line) - unless res.nil? - case res[3] - when "accounts", "installed-accounts" - next - when "installed" - installed = true - @current_resource.version(res[2]) - else - @candidate_version = res[2] - end + next if res.nil? + case res[3] + when "accounts", "installed-accounts" + next + when "installed" + installed = true + current_resource.version(res[2]) + else + @candidate_version = res[2] end end - @current_resource + current_resource end def install_package(name, version) - if version - pkg = "=#{name}-#{version}" - else - pkg = "#{@new_resource.package_name}" - end - shell_out!("cave -L warning resolve -x#{expand_options(@new_resource.options)} \"#{pkg}\"", :timeout => @new_resource.timeout) + pkg = if version + "=#{name}-#{version}" + else + new_resource.package_name.to_s + end + shell_out_compact_timeout!("cave", "-L", "warning", "resolve", "-x", options, pkg) end def upgrade_package(name, version) @@ -67,13 +66,13 @@ class Chef end def remove_package(name, version) - if version - pkg = "=#{@new_resource.package_name}-#{version}" - else - pkg = "#{@new_resource.package_name}" - end + pkg = if version + "=#{new_resource.package_name}-#{version}" + else + new_resource.package_name.to_s + end - shell_out!("cave -L warning uninstall -x#{expand_options(@new_resource.options)} \"#{pkg}\"") + shell_out_compact!("cave", "-L", "warning", "uninstall", "-x", options, pkg) end def purge_package(name, version) diff --git a/lib/chef/provider/package/portage.rb b/lib/chef/provider/package/portage.rb index 7c52e43bff..fecbba9dc9 100644 --- a/lib/chef/provider/package/portage.rb +++ b/lib/chef/provider/package/portage.rb @@ -17,8 +17,7 @@ # require "chef/provider/package" -require "chef/mixin/command" -require "chef/resource/package" +require "chef/resource/portage_package" require "chef/util/path_helper" class Chef @@ -32,74 +31,68 @@ class Chef PACKAGE_NAME_PATTERN = %r{(?:([^/]+)/)?([^/]+)} def load_current_resource - @current_resource = Chef::Resource::Package.new(@new_resource.name) - @current_resource.package_name(@new_resource.package_name) + @current_resource = Chef::Resource::Package.new(new_resource.name) + current_resource.package_name(new_resource.package_name) - category, pkg = %r{^#{PACKAGE_NAME_PATTERN}$}.match(@new_resource.package_name)[1, 2] + category, pkg = /^#{PACKAGE_NAME_PATTERN}$/.match(new_resource.package_name)[1, 2] globsafe_category = category ? Chef::Util::PathHelper.escape_glob_dir(category) : nil globsafe_pkg = Chef::Util::PathHelper.escape_glob_dir(pkg) - possibilities = Dir["/var/db/pkg/#{globsafe_category || "*"}/#{globsafe_pkg}-*"].map { |d| d.sub(%r{/var/db/pkg/}, "") } + possibilities = Dir["/var/db/pkg/#{globsafe_category || '*'}/#{globsafe_pkg}-*"].map { |d| d.sub(%r{/var/db/pkg/}, "") } versions = possibilities.map do |entry| - if entry =~ %r{[^/]+/#{Regexp.escape(pkg)}\-(\d[\.\d]*((_(alpha|beta|pre|rc|p)\d*)*)?(-r\d+)?)} + if entry =~ %r{[^/]+/#{Regexp.escape(pkg)}\-(\d[\.\d]*[a-z]?((_(alpha|beta|pre|rc|p)\d*)*)?(-r\d+)?)} [$&, $1] end end.compact if versions.size > 1 - atoms = versions.map { |v| v.first }.sort + atoms = versions.map(&:first).sort categories = atoms.map { |v| v.split("/")[0] }.uniq if !category && categories.size > 1 - raise Chef::Exceptions::Package, "Multiple packages found for #{@new_resource.package_name}: #{atoms.join(" ")}. Specify a category." + raise Chef::Exceptions::Package, "Multiple packages found for #{new_resource.package_name}: #{atoms.join(' ')}. Specify a category." end elsif versions.size == 1 - @current_resource.version(versions.first.last) - Chef::Log.debug("#{@new_resource} current version #{$1}") + current_resource.version(versions.first.last) + logger.trace("#{new_resource} current version #{$1}") end - @current_resource + current_resource end - def parse_emerge(package, txt) - availables = {} - found_package_name = nil + def raise_error_for_query(msg) + raise Chef::Exceptions::Package, "Query for '#{new_resource.package_name}' #{msg}" + end - txt.each_line do |line| - if line =~ /\*\s+#{PACKAGE_NAME_PATTERN}/ - found_package_name = $&.delete("*").strip - if package =~ /\// #the category is specified - if found_package_name == package - availables[found_package_name] = nil - end - else #the category is not specified - if found_package_name.split("/").last == package - availables[found_package_name] = nil - end + def candidate_version + return @candidate_version if @candidate_version + + pkginfo = shell_out_compact("portageq", "best_visible", "/", new_resource.package_name) + + if pkginfo.exitstatus != 0 + pkginfo.stderr.each_line do |line| + if line =~ /[Uu]nqualified atom .*match.* multiple/ + raise_error_for_query("matched multiple packages (please specify a category):\n#{pkginfo.inspect}") end end - if line =~ /Latest version available: (.*)/ && availables.has_key?(found_package_name) - availables[found_package_name] = $1.strip + if pkginfo.stdout.strip.empty? + raise_error_for_query("did not find a matching package:\n#{pkginfo.inspect}") end - end - if availables.size > 1 - # shouldn't happen if a category is specified so just use `package` - raise Chef::Exceptions::Package, "Multiple emerge results found for #{package}: #{availables.keys.join(" ")}. Specify a category." + raise_error_for_query("resulted in an unknown error:\n#{pkginfo.inspect}") end - availables.values.first - end - - def candidate_version - return @candidate_version if @candidate_version - - status = shell_out("emerge --color n --nospinner --search #{@new_resource.package_name.split('/').last}") - available, installed = parse_emerge(@new_resource.package_name, status.stdout) - @candidate_version = available + if pkginfo.stdout.lines.count > 1 + raise_error_for_query("produced unexpected output (multiple lines):\n#{pkginfo.inspect}") + end - unless status.exitstatus == 0 - raise Chef::Exceptions::Package, "emerge --search failed - #{status.inspect}!" + pkginfo.stdout.chomp! + if pkginfo.stdout =~ /-r\d+$/ + # Latest/Best version of the package is a revision (-rX). + @candidate_version = pkginfo.stdout.split(/(?<=-)/).last(2).join + else + # Latest/Best version of the package is NOT a revision (-rX). + @candidate_version = pkginfo.stdout.split("-").last end @candidate_version @@ -113,7 +106,7 @@ class Chef pkg = "~#{name}-#{$1}" end - shell_out!( "emerge -g --color n --nospinner --quiet#{expand_options(@new_resource.options)} #{pkg}" ) + shell_out_compact!( "emerge", "-g", "--color", "n", "--nospinner", "--quiet", options, pkg ) end def upgrade_package(name, version) @@ -121,13 +114,13 @@ class Chef end def remove_package(name, version) - if version - pkg = "=#{@new_resource.package_name}-#{version}" - else - pkg = "#{@new_resource.package_name}" - end + pkg = if version + "=#{new_resource.package_name}-#{version}" + else + new_resource.package_name.to_s + end - shell_out!( "emerge --unmerge --color n --nospinner --quiet#{expand_options(@new_resource.options)} #{pkg}" ) + shell_out_compact!( "emerge", "--unmerge", "--color", "n", "--nospinner", "--quiet", options, pkg ) end def purge_package(name, version) diff --git a/lib/chef/provider/package/powershell.rb b/lib/chef/provider/package/powershell.rb new file mode 100644 index 0000000000..44b6e69a00 --- /dev/null +++ b/lib/chef/provider/package/powershell.rb @@ -0,0 +1,129 @@ +# Author:: Dheeraj Dubey(dheeraj.dubey@msystechnologies.com) +# Copyright:: Copyright 2015-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" +require "chef/resource/powershell_package" +require "chef/mixin/powershell_out" + +class Chef + class Provider + class Package + class Powershell < Chef::Provider::Package + include Chef::Mixin::PowershellOut + + provides :powershell_package + + def load_current_resource + @current_resource = Chef::Resource::PowershellPackage.new(new_resource.name) + current_resource.package_name(new_resource.package_name) + current_resource.version(build_current_versions) + current_resource + end + + def define_resource_requirements + super + if powershell_out("$PSVersionTable.PSVersion.Major").stdout.strip.to_i < 5 + raise "Minimum installed Powershell Version required is 5" + end + requirements.assert(:install) do |a| + a.assertion { candidates_exist_for_all_uninstalled? } + 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 + + def candidate_version + @candidate_version ||= build_candidate_versions + end + + # Installs the package specified with the version passed else latest version will be installed + def install_package(names, versions) + names.each_with_index do |name, index| + powershell_out(build_powershell_package_command("Install-Package '#{name}'", versions[index]), timeout: new_resource.timeout) + end + end + + # Removes the package for the version passed and if no version is passed, then all installed versions of the package are removed + def remove_package(names, versions) + names.each_with_index do |name, index| + if versions && !versions[index].nil? + powershell_out(build_powershell_package_command("Uninstall-Package '#{name}'", versions[index]), timeout: new_resource.timeout) + else + version = "0" + until version.empty? + version = powershell_out(build_powershell_package_command("Uninstall-Package '#{name}'"), timeout: new_resource.timeout).stdout.strip + unless version.empty? + logger.info("Removed package '#{name}' with version #{version}") + end + end + end + end + end + + # Returns array of available available online + def build_candidate_versions + versions = [] + new_resource.package_name.each_with_index do |name, index| + version = if new_resource.version && !new_resource.version[index].nil? + powershell_out(build_powershell_package_command("Find-Package '#{name}'", new_resource.version[index]), timeout: new_resource.timeout).stdout.strip + else + powershell_out(build_powershell_package_command("Find-Package '#{name}'"), timeout: new_resource.timeout).stdout.strip + end + if version.empty? + version = nil + end + versions.push(version) + end + versions + end + + # Returns version array of installed version on the system + def build_current_versions + version_list = [] + new_resource.package_name.each_with_index do |name, index| + version = if new_resource.version && !new_resource.version[index].nil? + powershell_out(build_powershell_package_command("Get-Package '#{name}'", new_resource.version[index]), timeout: new_resource.timeout).stdout.strip + else + powershell_out(build_powershell_package_command("Get-Package '#{name}'"), timeout: new_resource.timeout).stdout.strip + end + if version.empty? + version = nil + end + version_list.push(version) + end + version_list + end + + def build_powershell_package_command(command, version = nil) + command = [command] unless command.is_a?(Array) + command.unshift("(") + %w{-Force -ForceBootstrap}.each do |arg| + command.push(arg) + end + command.push("-RequiredVersion #{version}") if version + command.push("-Source #{new_resource.source}") if new_resource.source && command[1] =~ Regexp.union(/Install-Package/, /Find-Package/) + command.push(").Version") + command.join(" ") + end + + def check_resource_semantics! + # This validation method from Chef::Provider::Package does not apply here, so no-op it. + end + end + end + end +end diff --git a/lib/chef/provider/package/rpm.rb b/lib/chef/provider/package/rpm.rb index 777cc6d209..9d4f2f3c23 100644 --- a/lib/chef/provider/package/rpm.rb +++ b/lib/chef/provider/package/rpm.rb @@ -16,16 +16,15 @@ # limitations under the License. # require "chef/provider/package" -require "chef/mixin/command" require "chef/resource/package" require "chef/mixin/get_source_from_package" +require "chef/provider/package/yum/rpm_utils" class Chef class Provider class Package class Rpm < Chef::Provider::Package - - provides :rpm_package, os: %w{linux aix} + provides :rpm_package include Chef::Mixin::GetSourceFromPackage @@ -34,13 +33,13 @@ class Chef requirements.assert(:all_actions) do |a| a.assertion { @package_source_exists } - a.failure_message Chef::Exceptions::Package, "Package #{@new_resource.name} not found: #{@new_resource.source}" - a.whyrun "Assuming package #{@new_resource.name} would have been made available." + a.failure_message Chef::Exceptions::Package, "Package #{new_resource.name} not found: #{new_resource.source}" + a.whyrun "Assuming package #{new_resource.name} would have been made available." end requirements.assert(:all_actions) do |a| a.assertion { !@rpm_status.nil? && (@rpm_status.exitstatus == 0 || @rpm_status.exitstatus == 1) } a.failure_message Chef::Exceptions::Package, "Unable to determine current version due to RPM failure. Detail: #{@rpm_status.inspect}" - a.whyrun "Assuming current version would have been determined for package#{@new_resource.name}." + a.whyrun "Assuming current version would have been determined for package#{new_resource.name}." end end @@ -48,74 +47,78 @@ class Chef @package_source_provided = true @package_source_exists = true - @current_resource = Chef::Resource::Package.new(@new_resource.name) - @current_resource.package_name(@new_resource.package_name) + @current_resource = Chef::Resource::Package.new(new_resource.name) + current_resource.package_name(new_resource.package_name) - if @new_resource.source - unless uri_scheme?(@new_resource.source) || ::File.exists?(@new_resource.source) + if new_resource.source + unless uri_scheme?(new_resource.source) || ::File.exist?(new_resource.source) @package_source_exists = false return end - Chef::Log.debug("#{@new_resource} checking rpm status") - shell_out_with_timeout!("rpm -qp --queryformat '%{NAME} %{VERSION}-%{RELEASE}\n' #{@new_resource.source}").stdout.each_line do |line| + logger.trace("#{new_resource} checking rpm status") + shell_out_compact_timeout!("rpm", "-qp", "--queryformat", "%{NAME} %{VERSION}-%{RELEASE}\n", new_resource.source).stdout.each_line do |line| case line when /^(\S+)\s(\S+)$/ - @current_resource.package_name($1) - @new_resource.version($2) + current_resource.package_name($1) + new_resource.version($2) @candidate_version = $2 end end else - if Array(@new_resource.action).include?(:install) + if Array(new_resource.action).include?(:install) @package_source_exists = false return end end - Chef::Log.debug("#{@new_resource} checking install state") - @rpm_status = shell_out_with_timeout("rpm -q --queryformat '%{NAME} %{VERSION}-%{RELEASE}\n' #{@current_resource.package_name}") + logger.trace("#{new_resource} checking install state") + @rpm_status = shell_out_compact_timeout("rpm", "-q", "--queryformat", "%{NAME} %{VERSION}-%{RELEASE}\n", current_resource.package_name) @rpm_status.stdout.each_line do |line| case line when /^(\S+)\s(\S+)$/ - Chef::Log.debug("#{@new_resource} current version is #{$2}") - @current_resource.version($2) + logger.trace("#{new_resource} current version is #{$2}") + current_resource.version($2) end end - @current_resource + current_resource end def install_package(name, version) - unless @current_resource.version - shell_out_with_timeout!( "rpm #{@new_resource.options} -i #{@new_resource.source}" ) - else + if current_resource.version if allow_downgrade - shell_out_with_timeout!( "rpm #{@new_resource.options} -U --oldpackage #{@new_resource.source}" ) + shell_out_compact_timeout!("rpm", options, "-U", "--oldpackage", new_resource.source) else - shell_out_with_timeout!( "rpm #{@new_resource.options} -U #{@new_resource.source}" ) + shell_out_compact_timeout!("rpm", options, "-U", new_resource.source) end + else + shell_out_compact_timeout!("rpm", options, "-i", new_resource.source) end end - alias_method :upgrade_package, :install_package + alias upgrade_package install_package def remove_package(name, version) if version - shell_out_with_timeout!( "rpm #{@new_resource.options} -e #{name}-#{version}" ) + shell_out_compact_timeout!("rpm", options, "-e", "#{name}-#{version}") else - shell_out_with_timeout!( "rpm #{@new_resource.options} -e #{name}" ) + shell_out_compact_timeout!("rpm", options, "-e", name) end end private + def version_compare(v1, v2) + Chef::Provider::Package::Yum::RPMVersion.parse(v1) <=> Chef::Provider::Package::Yum::RPMVersion.parse(v2) + end + def uri_scheme?(str) scheme = URI.split(str).first return false unless scheme %w{http https ftp file}.include?(scheme.downcase) rescue URI::InvalidURIError - return false + false end end end diff --git a/lib/chef/provider/package/rubygems.rb b/lib/chef/provider/package/rubygems.rb index a15ee1bdea..6b04af547b 100644 --- a/lib/chef/provider/package/rubygems.rb +++ b/lib/chef/provider/package/rubygems.rb @@ -1,7 +1,7 @@ # # Author:: Adam Jacob (<adam@chef.io>) # Author:: Daniel DeLeo (<dan@chef.io>) -# Copyright:: Copyright 2008-2016, 2010-2016 Chef Software, Inc. +# Copyright:: Copyright 2008-2016, 2010-2017, Chef Software Inc. # License:: Apache License, Version 2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -19,9 +19,9 @@ require "uri" require "chef/provider/package" -require "chef/mixin/command" require "chef/resource/package" require "chef/mixin/get_source_from_package" +require "chef/mixin/which" # Class methods on Gem are defined in rubygems require "rubygems" @@ -47,7 +47,7 @@ class Chef # alternate value and overwrite it with the defaults. Gem.configuration - DEFAULT_UNINSTALLER_OPTS = { :ignore => true, :executables => true } + DEFAULT_UNINSTALLER_OPTS = { ignore: true, executables: true }.freeze ## # The paths where rubygems should search for installed gems. @@ -86,7 +86,22 @@ class Chef # === Returns # [Gem::Specification] an array of Gem::Specification objects def installed_versions(gem_dep) - if Gem::Version.new(Gem::VERSION) >= Gem::Version.new("1.8.0") + rubygems_version = Gem::Version.new(Gem::VERSION) + if rubygems_version >= Gem::Version.new("2.7") + # In newer Rubygems, bundler is now a "default gem" which means + # even with AlternateGemEnvironment when you try to get the + # installed versions, you get the one from Chef's Ruby's default + # gems. This workaround ignores default gems entirely so we see + # only the installed gems. + stubs = gem_specification.send(:installed_stubs, gem_specification.dirs, "#{gem_dep.name}-*.gemspec") + # Filter down to only to only stubs we actually want. The name + # filter is needed in case of things like `foo-*.gemspec` also + # matching a gem named `foo-bar`. + stubs.select! { |stub| stub.name == gem_dep.name && gem_dep.requirement.satisfied_by?(stub.version) } + # This isn't sorting before returning beacuse the only code that + # uses this method calls `max_by` so it doesn't need to be sorted. + stubs + elsif rubygems_version >= Gem::Version.new("1.8.0") gem_specification.find_all_by_name(gem_dep.name, gem_dep.requirement) else gem_source_index.search(gem_dep) @@ -133,11 +148,11 @@ class Chef def candidate_version_from_file(gem_dependency, source) spec = spec_from_file(source) if spec.satisfies_requirement?(gem_dependency) - logger.debug { "#{@new_resource} found candidate gem version #{spec.version} from local gem package #{source}" } + logger.trace { "found candidate gem version #{spec.version} from local gem package #{source}" } spec.version else # This is probably going to end badly... - logger.warn { "#{@new_resource} gem package #{source} does not satisfy the requirements #{gem_dependency}" } + logger.warn { "gem package #{source} does not satisfy the requirements #{gem_dependency}" } nil end end @@ -159,27 +174,30 @@ class Chef # Find the newest gem version available from Gem.sources that satisfies # the constraints of +gem_dependency+ def find_newest_remote_version(gem_dependency, *sources) - available_gems = dependency_installer.find_gems_with_sources(gem_dependency) - spec, source = if available_gems.respond_to?(:last) - # DependencyInstaller sorts the results such that the last one is - # always the one it considers best. - spec_with_source = available_gems.last - spec_with_source && spec_with_source - else - # Rubygems 2.0 returns a Gem::Available set, which is a - # collection of AvailableSet::Tuple structs - available_gems.pick_best! - best_gem = available_gems.set.first - best_gem && [best_gem.spec, best_gem.source] - end + spec, source = + if Chef::Config[:rubygems_cache_enabled] + # This code caches every gem on rubygems.org and uses lots of RAM + available_gems = dependency_installer.find_gems_with_sources(gem_dependency) + available_gems.pick_best! + best_gem = available_gems.set.first + best_gem && [best_gem.spec, best_gem.source] + else + # Use the API that 'gem install' calls which does not pull down the rubygems universe + begin + rs = dependency_installer.resolve_dependencies gem_dependency.name, gem_dependency.requirement + rs.specs.select { |s| s.name == gem_dependency.name }.first + rescue Gem::UnsatisfiableDependencyError + nil + end + end version = spec && spec.version if version - logger.debug { "#{@new_resource} found gem #{spec.name} version #{version} for platform #{spec.platform} from #{source}" } + logger.trace { "found gem #{spec.name} version #{version} for platform #{spec.platform} from #{source}" } version else source_list = sources.compact.empty? ? "[#{Gem.sources.to_a.join(', ')}]" : "[#{sources.join(', ')}]" - logger.warn { "#{@new_resource} failed to find gem #{gem_dependency} from #{source_list}" } + logger.warn { "failed to find gem #{gem_dependency} from #{source_list}" } nil end end @@ -214,7 +232,7 @@ class Chef # Set rubygems' user interaction to ConsoleUI or SilentUI depending # on our current debug level def with_correct_verbosity - Gem::DefaultUserInteraction.ui = Chef::Log.debug? ? Gem::ConsoleUI.new : Gem::SilentUI.new + Gem::DefaultUserInteraction.ui = logger.trace? ? Gem::ConsoleUI.new : Gem::SilentUI.new yield end @@ -229,7 +247,7 @@ class Chef private def logger - Chef::Log.logger + Chef::Log.with_child({ subsytem: "gem_installer_environment" }) end end @@ -282,7 +300,7 @@ class Chef # shellout! is a fork/exec which won't work on windows shell_style_paths = shell_out!("#{@gem_binary_location} env gempath").stdout # on windows, the path separator is (usually? always?) semicolon - paths = shell_style_paths.split(::File::PATH_SEPARATOR).map { |path| path.strip } + paths = shell_style_paths.split(::File::PATH_SEPARATOR).map(&:strip) self.class.gempath_cache[@gem_binary_location] = paths end end @@ -319,11 +337,11 @@ class Chef self.class.platform_cache[@gem_binary_location] else gem_environment = shell_out!("#{@gem_binary_location} env").stdout - if jruby = gem_environment[JRUBY_PLATFORM] - self.class.platform_cache[@gem_binary_location] = ["ruby", Gem::Platform.new(jruby)] - else - self.class.platform_cache[@gem_binary_location] = Gem.platforms - end + self.class.platform_cache[@gem_binary_location] = if jruby = gem_environment[JRUBY_PLATFORM] + ["ruby", Gem::Platform.new(jruby)] + else + Gem.platforms + end end end @@ -349,30 +367,27 @@ class Chef attr_reader :gem_env attr_reader :cleanup_gem_env - def logger - Chef::Log.logger - end - provides :chef_gem provides :gem_package include Chef::Mixin::GetSourceFromPackage + include Chef::Mixin::Which def initialize(new_resource, run_context = nil) super @cleanup_gem_env = true if new_resource.gem_binary - if new_resource.options && new_resource.options.kind_of?(Hash) + if new_resource.options && new_resource.options.is_a?(Hash) msg = "options cannot be given as a hash when using an explicit gem_binary\n" msg << "in #{new_resource} from #{new_resource.source_line}" raise ArgumentError, msg end @gem_env = AlternateGemEnvironment.new(new_resource.gem_binary) - Chef::Log.debug("#{@new_resource} using gem '#{new_resource.gem_binary}'") - elsif is_omnibus? && (!@new_resource.instance_of? Chef::Resource::ChefGem) + logger.trace("#{new_resource} using gem '#{new_resource.gem_binary}'") + elsif is_omnibus? && (!new_resource.instance_of? Chef::Resource::ChefGem) # Opscode Omnibus - The ruby that ships inside omnibus is only used for Chef # Default to installing somewhere more functional - if new_resource.options && new_resource.options.kind_of?(Hash) + if new_resource.options && new_resource.options.is_a?(Hash) msg = [ "Gem options must be passed to gem_package as a string instead of a hash when", "using this installation of Chef because it runs with its own packaged Ruby. A hash", @@ -383,23 +398,23 @@ class Chef raise ArgumentError, msg end gem_location = find_gem_by_path - @new_resource.gem_binary gem_location + new_resource.gem_binary gem_location @gem_env = AlternateGemEnvironment.new(gem_location) - Chef::Log.debug("#{@new_resource} using gem '#{gem_location}'") + logger.trace("#{new_resource} using gem '#{gem_location}'") else @gem_env = CurrentGemEnvironment.new @cleanup_gem_env = false - Chef::Log.debug("#{@new_resource} using gem from running ruby environment") + logger.trace("#{new_resource} using gem from running ruby environment") end end def is_omnibus? if RbConfig::CONFIG["bindir"] =~ %r{/(opscode|chef|chefdk)/embedded/bin} - Chef::Log.debug("#{@new_resource} detected omnibus installation in #{RbConfig::CONFIG['bindir']}") + logger.trace("#{new_resource} detected omnibus installation in #{RbConfig::CONFIG['bindir']}") # Omnibus installs to a static path because of linking on unix, find it. true elsif RbConfig::CONFIG["bindir"].sub(/^[\w]:/, "") == "/opscode/chef/embedded/bin" - Chef::Log.debug("#{@new_resource} detected omnibus installation in #{RbConfig::CONFIG['bindir']}") + logger.trace("#{new_resource} detected omnibus installation in #{RbConfig::CONFIG['bindir']}") # windows, with the drive letter removed true else @@ -408,44 +423,40 @@ class Chef end def find_gem_by_path - Chef::Log.debug("#{@new_resource} searching for 'gem' binary in path: #{ENV['PATH']}") - separator = ::File::ALT_SEPARATOR ? ::File::ALT_SEPARATOR : ::File::SEPARATOR - path_to_first_gem = ENV["PATH"].split(::File::PATH_SEPARATOR).find { |path| ::File.exists?(path + separator + "gem") } - raise Chef::Exceptions::FileNotFound, "Unable to find 'gem' binary in path: #{ENV['PATH']}" if path_to_first_gem.nil? - path_to_first_gem + separator + "gem" + which("gem", extra_path: RbConfig::CONFIG["bindir"]) end def gem_dependency - Gem::Dependency.new(@new_resource.package_name, @new_resource.version) + Gem::Dependency.new(new_resource.package_name, new_resource.version) end def source_is_remote? - return true if @new_resource.source.nil? - scheme = URI.parse(@new_resource.source).scheme + return true if new_resource.source.nil? + return true if new_resource.source.is_a?(Array) + scheme = URI.parse(new_resource.source).scheme # URI.parse gets confused by MS Windows paths with forward slashes. scheme = nil if scheme =~ /^[a-z]$/ %w{http https}.include?(scheme) rescue URI::InvalidURIError - Chef::Log.debug("#{@new_resource} failed to parse source '#{@new_resource.source}' as a URI, assuming a local path") + logger.trace("#{new_resource} failed to parse source '#{new_resource.source}' as a URI, assuming a local path") false end def current_version - #raise 'todo' # If one or more matching versions are installed, the newest of them # is the current version if !matching_installed_versions.empty? - gemspec = matching_installed_versions.last - logger.debug { "#{@new_resource} found installed gem #{gemspec.name} version #{gemspec.version} matching #{gem_dependency}" } + gemspec = matching_installed_versions.max_by(&:version) + logger.trace { "#{new_resource} found installed gem #{gemspec.name} version #{gemspec.version} matching #{gem_dependency}" } gemspec # If no version matching the requirements exists, the latest installed # version is the current version. elsif !all_installed_versions.empty? - gemspec = all_installed_versions.last - logger.debug { "#{@new_resource} newest installed version of gem #{gemspec.name} is #{gemspec.version}" } + gemspec = all_installed_versions.max_by(&:version) + logger.trace { "#{new_resource} newest installed version of gem #{gemspec.name} is #{gemspec.version}" } gemspec else - logger.debug { "#{@new_resource} no installed version found for #{gem_dependency}" } + logger.trace { "#{new_resource} no installed version found for #{gem_dependency}" } nil end end @@ -461,41 +472,39 @@ class Chef end def gem_sources - @new_resource.source ? Array(@new_resource.source) : nil + srcs = [ new_resource.source ] + srcs << Chef::Config[:rubygems_url] if new_resource.include_default_source + srcs.flatten.compact end def load_current_resource - @current_resource = Chef::Resource::Package::GemPackage.new(@new_resource.name) - @current_resource.package_name(@new_resource.package_name) + @current_resource = Chef::Resource::Package::GemPackage.new(new_resource.name) + current_resource.package_name(new_resource.package_name) if current_spec = current_version - @current_resource.version(current_spec.version.to_s) + current_resource.version(current_spec.version.to_s) end - @current_resource + current_resource end def cleanup_after_converge if @cleanup_gem_env - logger.debug { "#{@new_resource} resetting gem environment to default" } + logger.trace { "#{new_resource} resetting gem environment to default" } Gem.clear_paths end end def candidate_version @candidate_version ||= begin - if target_version_already_installed?(@current_resource.version, @new_resource.version) - nil - elsif source_is_remote? - @gem_env.candidate_version_from_remote(gem_dependency, *gem_sources).to_s - else - @gem_env.candidate_version_from_file(gem_dependency, @new_resource.source).to_s - end - end + if source_is_remote? + @gem_env.candidate_version_from_remote(gem_dependency, *gem_sources).to_s + else + @gem_env.candidate_version_from_file(gem_dependency, new_resource.source).to_s + end + end end - def target_version_already_installed?(current_version, new_version) - return false unless current_version - return false if new_version.nil? - + def version_requirement_satisfied?(current_version, new_version) + return false unless current_version && new_version Gem::Requirement.new(new_version).satisfied_by?(Gem::Version.new(current_version)) end @@ -506,20 +515,18 @@ class Chef # 2. shell out to `gem install` when a String of options is given # 3. use gems API with options if a hash of options is given def install_package(name, version) - if source_is_remote? && @new_resource.gem_binary.nil? - if @new_resource.options.nil? - @gem_env.install(gem_dependency, :sources => gem_sources) - elsif @new_resource.options.kind_of?(Hash) - options = @new_resource.options + if source_is_remote? && new_resource.gem_binary.nil? + if new_resource.options.nil? + @gem_env.install(gem_dependency, sources: gem_sources) + elsif new_resource.options.is_a?(Hash) + options = new_resource.options options[:sources] = gem_sources @gem_env.install(gem_dependency, options) else install_via_gem_command(name, version) end - elsif @new_resource.gem_binary.nil? - # domain is used by Gem::DependencyInstaller rather than by Chef code - # domain can be :local, :remote or :both - @gem_env.install(@new_resource.source, domain: :local) + elsif new_resource.gem_binary.nil? + @gem_env.install(new_resource.source) else install_via_gem_command(name, version) end @@ -527,23 +534,22 @@ class Chef end def gem_binary_path - @new_resource.gem_binary || "gem" + new_resource.gem_binary || "gem" end def install_via_gem_command(name, version) - if @new_resource.source =~ /\.gem$/i - name = @new_resource.source - src = " --local" unless source_is_remote? - elsif @new_resource.clear_sources - src = " --clear-sources" - src << (@new_resource.source && " --source=#{@new_resource.source}" || "") + src = [] + if new_resource.source.is_a?(String) && new_resource.source =~ /\.gem$/i + name = new_resource.source else - src = @new_resource.source && " --source=#{@new_resource.source} --source=#{Chef::Config[:rubygems_url]}" + src << "--clear-sources" if new_resource.clear_sources + src += gem_sources.map { |s| "--source=#{s}" } end - if !version.nil? && version.length > 0 - shell_out_with_timeout!("#{gem_binary_path} install #{name} -q --no-rdoc --no-ri -v \"#{version}\"#{src}#{opts}", :env => nil) + src_str = src.empty? ? "" : " #{src.join(" ")}" + if !version.nil? && !version.empty? + shell_out_with_timeout!("#{gem_binary_path} install #{name} -q --no-rdoc --no-ri -v \"#{version}\"#{src_str}#{opts}", env: nil) else - shell_out_with_timeout!("#{gem_binary_path} install \"#{name}\" -q --no-rdoc --no-ri #{src}#{opts}", :env => nil) + shell_out_with_timeout!("#{gem_binary_path} install \"#{name}\" -q --no-rdoc --no-ri #{src_str}#{opts}", env: nil) end end @@ -552,11 +558,11 @@ class Chef end def remove_package(name, version) - if @new_resource.gem_binary.nil? - if @new_resource.options.nil? + if new_resource.gem_binary.nil? + if new_resource.options.nil? @gem_env.uninstall(name, version) - elsif @new_resource.options.kind_of?(Hash) - @gem_env.uninstall(name, version, @new_resource.options) + elsif new_resource.options.is_a?(Hash) + @gem_env.uninstall(name, version, new_resource.options) else uninstall_via_gem_command(name, version) end @@ -567,9 +573,9 @@ class Chef def uninstall_via_gem_command(name, version) if version - shell_out_with_timeout!("#{gem_binary_path} uninstall #{name} -q -x -I -v \"#{version}\"#{opts}", :env => nil) + shell_out_with_timeout!("#{gem_binary_path} uninstall #{name} -q -x -I -v \"#{version}\"#{opts}", env: nil) else - shell_out_with_timeout!("#{gem_binary_path} uninstall #{name} -q -x -I -a#{opts}", :env => nil) + shell_out_with_timeout!("#{gem_binary_path} uninstall #{name} -q -x -I -a#{opts}", env: nil) end end @@ -580,7 +586,7 @@ class Chef private def opts - expand_options(@new_resource.options) + expand_options(new_resource.options) end end diff --git a/lib/chef/provider/package/smartos.rb b/lib/chef/provider/package/smartos.rb index 3f09bef212..5c637814a6 100644 --- a/lib/chef/provider/package/smartos.rb +++ b/lib/chef/provider/package/smartos.rb @@ -3,7 +3,7 @@ # Bryan McLellan (btm@loftninjas.org) # Matthew Landauer (matthew@openaustralia.org) # Ben Rockwood (benr@joyent.com) -# Copyright:: Copyright 2009-2016, Bryan McLellan, Matthew Landauer +# Copyright:: Copyright 2009-2018, Bryan McLellan, Matthew Landauer # License:: Apache License, Version 2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -30,27 +30,27 @@ class Chef attr_accessor :is_virtual_package provides :package, platform: "smartos" - provides :smartos_package, os: "solaris2", platform_family: "smartos" + provides :smartos_package def load_current_resource - Chef::Log.debug("#{@new_resource} loading current resource") - @current_resource = Chef::Resource::Package.new(@new_resource.name) - @current_resource.package_name(@new_resource.package_name) - check_package_state(@new_resource.package_name) - @current_resource # modified by check_package_state + logger.trace("#{new_resource} loading current resource") + @current_resource = Chef::Resource::Package.new(new_resource.name) + current_resource.package_name(new_resource.package_name) + check_package_state(new_resource.package_name) + current_resource # modified by check_package_state end def check_package_state(name) - Chef::Log.debug("#{@new_resource} checking package #{name}") + logger.trace("#{new_resource} checking package #{name}") version = nil - info = shell_out_with_timeout!("/opt/local/sbin/pkg_info", "-E", "#{name}*", :env => nil, :returns => [0, 1]) + info = shell_out_compact_timeout!("/opt/local/sbin/pkg_info", "-E", "#{name}*", env: nil, returns: [0, 1]) if info.stdout - version = info.stdout[/^#{@new_resource.package_name}-(.+)/, 1] + version = info.stdout[/^#{new_resource.package_name}-(.+)/, 1] end if version - @current_resource.version(version) + current_resource.version(version) end end @@ -58,7 +58,7 @@ class Chef return @candidate_version if @candidate_version name = nil version = nil - pkg = shell_out_with_timeout!("/opt/local/bin/pkgin", "se", new_resource.package_name, :env => nil, :returns => [0, 1]) + pkg = shell_out_compact_timeout!("/opt/local/bin/pkgin", "se", new_resource.package_name, env: nil, returns: [0, 1]) pkg.stdout.each_line do |line| case line when /^#{new_resource.package_name}/ @@ -70,20 +70,20 @@ class Chef end def install_package(name, version) - Chef::Log.debug("#{@new_resource} installing package #{name} version #{version}") + logger.trace("#{new_resource} installing package #{name} version #{version}") package = "#{name}-#{version}" - out = shell_out_with_timeout!("/opt/local/bin/pkgin", "-y", "install", package, :env => nil) + out = shell_out_compact_timeout!("/opt/local/bin/pkgin", "-y", "install", package, env: nil) end def upgrade_package(name, version) - Chef::Log.debug("#{@new_resource} upgrading package #{name} version #{version}") + logger.trace("#{new_resource} upgrading package #{name} version #{version}") install_package(name, version) end def remove_package(name, version) - Chef::Log.debug("#{@new_resource} removing package #{name} version #{version}") - package = "#{name}" - out = shell_out_with_timeout!("/opt/local/bin/pkgin", "-y", "remove", package, :env => nil) + logger.trace("#{new_resource} removing package #{name} version #{version}") + package = name.to_s + out = shell_out_compact_timeout!("/opt/local/bin/pkgin", "-y", "remove", package, env: nil) end end diff --git a/lib/chef/provider/package/solaris.rb b/lib/chef/provider/package/solaris.rb index 1c393e6a20..9c75c76929 100644 --- a/lib/chef/provider/package/solaris.rb +++ b/lib/chef/provider/package/solaris.rb @@ -16,7 +16,6 @@ # limitations under the License. # require "chef/provider/package" -require "chef/mixin/command" require "chef/resource/package" require "chef/mixin/get_source_from_package" @@ -29,49 +28,49 @@ class Chef provides :package, platform: "nexentacore" provides :package, platform: "solaris2", platform_version: "< 5.11" - provides :solaris_package, os: "solaris2" + provides :solaris_package # def initialize(*args) # super - # @current_resource = Chef::Resource::Package.new(@new_resource.name) + # @current_resource = Chef::Resource::Package.new(new_resource.name) # end def define_resource_requirements super requirements.assert(:install) do |a| - a.assertion { @new_resource.source } - a.failure_message Chef::Exceptions::Package, "Source for package #{@new_resource.name} required for action install" + a.assertion { new_resource.source } + a.failure_message Chef::Exceptions::Package, "Source for package #{new_resource.name} required for action install" end requirements.assert(:all_actions) do |a| - a.assertion { !@new_resource.source || @package_source_found } - a.failure_message Chef::Exceptions::Package, "Package #{@new_resource.name} not found: #{@new_resource.source}" - a.whyrun "would assume #{@new_resource.source} would be have previously been made available" + a.assertion { !new_resource.source || @package_source_found } + a.failure_message Chef::Exceptions::Package, "Package #{new_resource.name} not found: #{new_resource.source}" + a.whyrun "would assume #{new_resource.source} would be have previously been made available" end end def load_current_resource - @current_resource = Chef::Resource::Package.new(@new_resource.name) - @current_resource.package_name(@new_resource.package_name) + @current_resource = Chef::Resource::Package.new(new_resource.name) + current_resource.package_name(new_resource.package_name) - if @new_resource.source - @package_source_found = ::File.exists?(@new_resource.source) + if new_resource.source + @package_source_found = ::File.exist?(new_resource.source) if @package_source_found - Chef::Log.debug("#{@new_resource} checking pkg status") - shell_out_with_timeout("pkginfo -l -d #{@new_resource.source} #{@new_resource.package_name}").stdout.each_line do |line| + logger.trace("#{new_resource} checking pkg status") + shell_out_compact_timeout("pkginfo", "-l", "-d", new_resource.source, new_resource.package_name).stdout.each_line do |line| case line when /VERSION:\s+(.+)/ - @new_resource.version($1) + new_resource.version($1) end end end end - Chef::Log.debug("#{@new_resource} checking install state") - status = shell_out_with_timeout("pkginfo -l #{@current_resource.package_name}") + logger.trace("#{new_resource} checking install state") + status = shell_out_compact_timeout("pkginfo", "-l", current_resource.package_name) status.stdout.each_line do |line| case line when /VERSION:\s+(.+)/ - Chef::Log.debug("#{@new_resource} version #{$1} is already installed") - @current_resource.version($1) + logger.trace("#{new_resource} version #{$1} is already installed") + current_resource.version($1) end end @@ -79,56 +78,56 @@ class Chef raise Chef::Exceptions::Package, "pkginfo failed - #{status.inspect}!" end - @current_resource + current_resource end def candidate_version return @candidate_version if @candidate_version - status = shell_out_with_timeout("pkginfo -l -d #{@new_resource.source} #{new_resource.package_name}") + status = shell_out_compact_timeout("pkginfo", "-l", "-d", new_resource.source, new_resource.package_name) status.stdout.each_line do |line| case line when /VERSION:\s+(.+)/ @candidate_version = $1 - @new_resource.version($1) - Chef::Log.debug("#{@new_resource} setting install candidate version to #{@candidate_version}") + new_resource.version($1) + logger.trace("#{new_resource} setting install candidate version to #{@candidate_version}") end end unless status.exitstatus == 0 - raise Chef::Exceptions::Package, "pkginfo -l -d #{@new_resource.source} - #{status.inspect}!" + raise Chef::Exceptions::Package, "pkginfo -l -d #{new_resource.source} - #{status.inspect}!" end @candidate_version end def install_package(name, version) - Chef::Log.debug("#{@new_resource} package install options: #{@new_resource.options}") - if @new_resource.options.nil? - if ::File.directory?(@new_resource.source) # CHEF-4469 - command = "pkgadd -n -d #{@new_resource.source} #{@new_resource.package_name}" - else - command = "pkgadd -n -d #{@new_resource.source} all" - end - shell_out_with_timeout!(command) - Chef::Log.debug("#{@new_resource} installed version #{@new_resource.version} from: #{@new_resource.source}") + logger.trace("#{new_resource} package install options: #{options}") + if options.nil? + command = if ::File.directory?(new_resource.source) # CHEF-4469 + [ "pkgadd", "-n", "-d", new_resource.source, new_resource.package_name ] + else + [ "pkgadd", "-n", "-d", new_resource.source, "all" ] + end + shell_out_compact_timeout!(command) + logger.trace("#{new_resource} installed version #{new_resource.version} from: #{new_resource.source}") else - if ::File.directory?(@new_resource.source) # CHEF-4469 - command = "pkgadd -n#{expand_options(@new_resource.options)} -d #{@new_resource.source} #{@new_resource.package_name}" - else - command = "pkgadd -n#{expand_options(@new_resource.options)} -d #{@new_resource.source} all" - end - shell_out_with_timeout!(command) - Chef::Log.debug("#{@new_resource} installed version #{@new_resource.version} from: #{@new_resource.source}") + command = if ::File.directory?(new_resource.source) # CHEF-4469 + [ "pkgadd", "-n", options, "-d", new_resource.source, new_resource.package_name ] + else + [ "pkgadd", "-n", options, "-d", new_resource.source, "all" ] + end + shell_out_compact_timeout!(*command) + logger.trace("#{new_resource} installed version #{new_resource.version} from: #{new_resource.source}") end end - alias_method :upgrade_package, :install_package + alias upgrade_package install_package def remove_package(name, version) - if @new_resource.options.nil? - shell_out_with_timeout!( "pkgrm -n #{name}" ) - Chef::Log.debug("#{@new_resource} removed version #{@new_resource.version}") + if options.nil? + shell_out_compact_timeout!( "pkgrm", "-n", name ) + logger.trace("#{new_resource} removed version #{new_resource.version}") else - shell_out_with_timeout!( "pkgrm -n#{expand_options(@new_resource.options)} #{name}" ) - Chef::Log.debug("#{@new_resource} removed version #{@new_resource.version}") + shell_out_compact_timeout!( "pkgrm", "-n", options, name ) + logger.trace("#{new_resource} removed version #{new_resource.version}") end end diff --git a/lib/chef/provider/package/windows.rb b/lib/chef/provider/package/windows.rb index a5f3ff7191..0fea32dcf3 100644 --- a/lib/chef/provider/package/windows.rb +++ b/lib/chef/provider/package/windows.rb @@ -30,7 +30,7 @@ class Chef include Chef::Mixin::Checksum provides :package, os: "windows" - provides :windows_package, os: "windows" + provides :windows_package require "chef/provider/package/windows/registry_uninstall_entry.rb" @@ -45,7 +45,7 @@ class Chef def load_current_resource @current_resource = Chef::Resource::WindowsPackage.new(new_resource.name) if downloadable_file_missing? - Chef::Log.debug("We do not know the version of #{new_resource.source} because the file is not downloaded") + logger.trace("We do not know the version of #{new_resource.source} because the file is not downloaded") current_resource.version(:unknown.to_s) else current_resource.version(package_provider.installed_version) @@ -59,11 +59,11 @@ class Chef @package_provider ||= begin case installer_type when :msi - Chef::Log.debug("#{new_resource} is MSI") + logger.trace("#{new_resource} is MSI") require "chef/provider/package/windows/msi" Chef::Provider::Package::Windows::MSI.new(resource_for_provider, uninstall_registry_entries) else - Chef::Log.debug("#{new_resource} is EXE with type '#{installer_type}'") + logger.trace("#{new_resource} is EXE with type '#{installer_type}'") require "chef/provider/package/windows/exe" Chef::Provider::Package::Windows::Exe.new(resource_for_provider, installer_type, uninstall_registry_entries) end @@ -104,8 +104,8 @@ class Chef return :nsis end - if io.tell() < filesize - io.seek(io.tell() - overlap) + if io.tell < filesize + io.seek(io.tell - overlap) end end @@ -165,7 +165,11 @@ class Chef # # @return [Boolean] true if new_version is equal to or included in current_version def target_version_already_installed?(current_version, new_version) - Chef::Log.debug("Checking if #{new_resource} version '#{new_version}' is already installed. #{current_version} is currently installed") + version_equals?(current_version, new_version) + end + + def version_equals?(current_version, new_version) + logger.trace("Checking if #{new_resource} version '#{new_version}' is already installed. #{current_version} is currently installed") if current_version.is_a?(Array) current_version.include?(new_version) else @@ -179,6 +183,17 @@ class Chef private + def version_compare(v1, v2) + if v1 == "latest" || v2 == "latest" + return 0 + end + + gem_v1 = Gem::Version.new(v1) + gem_v2 = Gem::Version.new(v2) + + gem_v1 <=> gem_v2 + end + def uninstall_registry_entries @uninstall_registry_entries ||= Chef::Provider::Package::Windows::RegistryUninstallEntry.find_entries(new_resource.package_name) end @@ -195,12 +210,13 @@ class Chef end def downloadable_file_missing? - !new_resource.source.nil? && uri_scheme?(new_resource.source) && !::File.exists?(source_location) + !new_resource.source.nil? && uri_scheme?(new_resource.source) && !::File.exist?(source_location) end def resource_for_provider @resource_for_provider = Chef::Resource::WindowsPackage.new(new_resource.name).tap do |r| r.source(Chef::Util::PathHelper.validate_path(source_location)) unless source_location.nil? + r.cookbook_name = new_resource.cookbook_name r.version(new_resource.version) r.timeout(new_resource.timeout) r.returns(new_resource.returns) @@ -210,12 +226,13 @@ class Chef def download_source_file source_resource.run_action(:create) - Chef::Log.debug("#{new_resource} fetched source file to #{source_resource.path}") + logger.trace("#{new_resource} fetched source file to #{source_resource.path}") end def source_resource @source_resource ||= Chef::Resource::RemoteFile.new(default_download_cache_path, run_context).tap do |r| r.source(new_resource.source) + r.cookbook_name = new_resource.cookbook_name r.checksum(new_resource.checksum) r.backup(false) @@ -248,7 +265,7 @@ class Chef def validate_content! if new_resource.checksum source_checksum = checksum(source_location) - if new_resource.checksum != source_checksum + if new_resource.checksum.downcase != source_checksum raise Chef::Exceptions::ChecksumMismatch.new(short_cksum(new_resource.checksum), short_cksum(source_checksum)) end end @@ -260,7 +277,7 @@ class Chef if source_location.nil? inferred_registry_type == :msi else - ::File.extname(source_location).casecmp(".msi").zero? + ::File.extname(source_location).casecmp(".msi") == 0 end end end diff --git a/lib/chef/provider/package/windows/exe.rb b/lib/chef/provider/package/windows/exe.rb index 70c9879845..6499d0cfeb 100644 --- a/lib/chef/provider/package/windows/exe.rb +++ b/lib/chef/provider/package/windows/exe.rb @@ -28,11 +28,13 @@ class Chef def initialize(resource, installer_type, uninstall_entries) @new_resource = resource + @logger = new_resource.logger @installer_type = installer_type @uninstall_entries = uninstall_entries end attr_reader :new_resource + attr_reader :logger attr_reader :installer_type attr_reader :uninstall_entries @@ -43,7 +45,7 @@ class Chef # Returns a version if the package is installed or nil if it is not. def installed_version - Chef::Log.debug("#{new_resource} checking package version") + logger.trace("#{new_resource} checking package version") current_installed_version end @@ -52,7 +54,7 @@ class Chef end def install_package - Chef::Log.debug("#{new_resource} installing #{new_resource.installer_type} package '#{new_resource.source}'") + logger.trace("#{new_resource} installing #{new_resource.installer_type} package '#{new_resource.source}'") shell_out!( [ "start", @@ -69,32 +71,30 @@ class Chef def remove_package uninstall_version = new_resource.version || current_installed_version uninstall_entries.select { |entry| [uninstall_version].flatten.include?(entry.display_version) } - .map { |version| version.uninstall_string }.uniq.each do |uninstall_string| - Chef::Log.debug("Registry provided uninstall string for #{new_resource} is '#{uninstall_string}'") - shell_out!(uninstall_command(uninstall_string), { returns: new_resource.returns }) - end + .map(&:uninstall_string).uniq.each do |uninstall_string| + logger.trace("Registry provided uninstall string for #{new_resource} is '#{uninstall_string}'") + shell_out!(uninstall_command(uninstall_string), timeout: new_resource.timeout, returns: new_resource.returns) + end end private def uninstall_command(uninstall_string) - uninstall_string.delete!('"') + uninstall_string = "\"#{uninstall_string}\"" if ::File.exist?(uninstall_string) uninstall_string = [ - %q{/d"}, - ::File.dirname(uninstall_string), - %q{" }, - ::File.basename(uninstall_string), + uninstall_string, expand_options(new_resource.options), " ", unattended_flags, ].join - %Q{start "" /wait #{uninstall_string} & exit %%%%ERRORLEVEL%%%%} + %{start "" /wait #{uninstall_string} & exit %%%%ERRORLEVEL%%%%} end def current_installed_version - @current_installed_version ||= uninstall_entries.count == 0 ? nil : begin - uninstall_entries.map { |entry| entry.display_version }.uniq - end + @current_installed_version ||= + if uninstall_entries.count != 0 + uninstall_entries.map(&:display_version).uniq + end end # http://unattended.sourceforge.net/installers.php diff --git a/lib/chef/provider/package/windows/msi.rb b/lib/chef/provider/package/windows/msi.rb index ac771688e7..51afcab2a2 100644 --- a/lib/chef/provider/package/windows/msi.rb +++ b/lib/chef/provider/package/windows/msi.rb @@ -1,6 +1,6 @@ # # Author:: Bryan McLellan <btm@loftninjas.org> -# Copyright:: Copyright 2014-2016, Chef Software, Inc. +# Copyright:: Copyright 2014-2017, Chef Software Inc. # License:: Apache License, Version 2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -16,7 +16,7 @@ # limitations under the License. # -# TODO: Allow @new_resource.source to be a Product Code as a GUID for uninstall / network install +# TODO: Allow new_resource.source to be a Product Code as a GUID for uninstall / network install require "chef/win32/api/installer" if (RUBY_PLATFORM =~ /mswin|mingw32|windows/) && Chef::Platform.supports_msi? require "chef/mixin/shell_out" @@ -31,10 +31,12 @@ class Chef def initialize(resource, uninstall_entries) @new_resource = resource + @logger = new_resource.logger @uninstall_entries = uninstall_entries end attr_reader :new_resource + attr_reader :logger attr_reader :uninstall_entries # From Chef::Provider::Package @@ -45,13 +47,13 @@ class Chef # Returns a version if the package is installed or nil if it is not. def installed_version if !new_resource.source.nil? && ::File.exist?(new_resource.source) - Chef::Log.debug("#{new_resource} getting product code for package at #{new_resource.source}") + logger.trace("#{new_resource} getting product code for package at #{new_resource.source}") product_code = get_product_property(new_resource.source, "ProductCode") - Chef::Log.debug("#{new_resource} checking package status and version for #{product_code}") + logger.trace("#{new_resource} checking package status and version for #{product_code}") get_installed_version(product_code) else - uninstall_entries.count == 0 ? nil : begin - uninstall_entries.map { |entry| entry.display_version }.uniq + if uninstall_entries.count != 0 + uninstall_entries.map(&:display_version).uniq end end end @@ -59,30 +61,31 @@ class Chef def package_version return new_resource.version if new_resource.version if !new_resource.source.nil? && ::File.exist?(new_resource.source) - Chef::Log.debug("#{new_resource} getting product version for package at #{new_resource.source}") + logger.trace("#{new_resource} getting product version for package at #{new_resource.source}") get_product_property(new_resource.source, "ProductVersion") end end def install_package # We could use MsiConfigureProduct here, but we'll start off with msiexec - Chef::Log.debug("#{new_resource} installing MSI package '#{new_resource.source}'") - shell_out!("msiexec /qn /i \"#{new_resource.source}\" #{expand_options(new_resource.options)}", { :timeout => new_resource.timeout, :returns => new_resource.returns }) + logger.trace("#{new_resource} installing MSI package '#{new_resource.source}'") + shell_out!("msiexec /qn /i \"#{new_resource.source}\" #{expand_options(new_resource.options)}", timeout: new_resource.timeout, returns: new_resource.returns) end def remove_package # We could use MsiConfigureProduct here, but we'll start off with msiexec if !new_resource.source.nil? && ::File.exist?(new_resource.source) - Chef::Log.debug("#{new_resource} removing MSI package '#{new_resource.source}'") - shell_out!("msiexec /qn /x \"#{new_resource.source}\" #{expand_options(new_resource.options)}", { :timeout => new_resource.timeout, :returns => new_resource.returns }) + logger.trace("#{new_resource} removing MSI package '#{new_resource.source}'") + shell_out!("msiexec /qn /x \"#{new_resource.source}\" #{expand_options(new_resource.options)}", timeout: new_resource.timeout, returns: new_resource.returns) else uninstall_version = new_resource.version || installed_version uninstall_entries.select { |entry| [uninstall_version].flatten.include?(entry.display_version) } - .map { |version| version.uninstall_string }.uniq.each do |uninstall_string| - Chef::Log.debug("#{new_resource} removing MSI package version using '#{uninstall_string}'") + .map(&:uninstall_string).uniq.each do |uninstall_string| + uninstall_string = "msiexec /x #{uninstall_string.match(/{.*}/)}" uninstall_string += expand_options(new_resource.options) - uninstall_string += " /Q" unless uninstall_string =~ / \/Q\b/ - shell_out!(uninstall_string, { :timeout => new_resource.timeout, :returns => new_resource.returns }) + uninstall_string += " /q" unless uninstall_string.downcase =~ / \/q/ + logger.trace("#{new_resource} removing MSI package version using '#{uninstall_string}'") + shell_out!(uninstall_string, timeout: new_resource.timeout, returns: new_resource.returns) end end end diff --git a/lib/chef/provider/package/windows/registry_uninstall_entry.rb b/lib/chef/provider/package/windows/registry_uninstall_entry.rb index 3fa00b6081..d57f700799 100644 --- a/lib/chef/provider/package/windows/registry_uninstall_entry.rb +++ b/lib/chef/provider/package/windows/registry_uninstall_entry.rb @@ -26,7 +26,7 @@ class Chef class RegistryUninstallEntry def self.find_entries(package_name) - Chef::Log.debug("Finding uninstall entries for #{package_name}") + logger.trace("Finding uninstall entries for #{package_name}") entries = [] [ [::Win32::Registry::HKEY_LOCAL_MACHINE, (::Win32::Registry::Constants::KEY_READ | 0x0100)], @@ -40,36 +40,47 @@ class Chef begin entry = reg.open(key, desired) display_name = read_registry_property(entry, "DisplayName") - if display_name == package_name - entries.push(RegistryUninstallEntry.new(hkey, key, entry)) + if display_name.to_s.rstrip == package_name + quiet_uninstall_string = RegistryUninstallEntry.read_registry_property(entry, "QuietUninstallString") + entries.push(quiet_uninstall_string_key?(quiet_uninstall_string, hkey, key, entry)) end rescue ::Win32::Registry::Error => ex - Chef::Log.debug("Registry error opening key '#{key}' on node #{desired}: #{ex}") + logger.trace("Registry error opening key '#{key}' on node #{desired}: #{ex}") end end end rescue ::Win32::Registry::Error => ex - Chef::Log.debug("Registry error opening hive '#{hkey[0]}' :: #{desired}: #{ex}") + logger.trace("Registry error opening hive '#{hkey[0]}' :: #{desired}: #{ex}") end end entries end + def self.quiet_uninstall_string_key?(quiet_uninstall_string, hkey, key, entry) + return RegistryUninstallEntry.new(hkey, key, entry) if quiet_uninstall_string.nil? + RegistryUninstallEntry.new(hkey, key, entry, "QuietUninstallString") + end + def self.read_registry_property(data, property) data[property] - rescue ::Win32::Registry::Error => ex - Chef::Log.debug("Failure to read property '#{property}'") + rescue ::Win32::Registry::Error + logger.trace("Failure to read property '#{property}'") nil end - def initialize(hive, key, registry_data) - Chef::Log.debug("Creating uninstall entry for #{hive}::#{key}") + def self.logger + Chef::Log + end + + def initialize(hive, key, registry_data, uninstall_key = "UninstallString") + @logger = Chef::Log.with_child({ subsystem: "registry_uninstall_entry" }) + logger.trace("Creating uninstall entry for #{hive}::#{key}") @hive = hive @key = key @data = registry_data @display_name = RegistryUninstallEntry.read_registry_property(registry_data, "DisplayName") @display_version = RegistryUninstallEntry.read_registry_property(registry_data, "DisplayVersion") - @uninstall_string = RegistryUninstallEntry.read_registry_property(registry_data, "UninstallString") + @uninstall_string = RegistryUninstallEntry.read_registry_property(registry_data, uninstall_key) end attr_reader :hive @@ -78,8 +89,7 @@ class Chef attr_reader :display_version attr_reader :uninstall_string attr_reader :data - - private + attr_reader :logger UNINSTALL_SUBKEY = 'Software\Microsoft\Windows\CurrentVersion\Uninstall'.freeze end diff --git a/lib/chef/provider/package/yum-dump.py b/lib/chef/provider/package/yum-dump.py deleted file mode 100644 index 6183460195..0000000000 --- a/lib/chef/provider/package/yum-dump.py +++ /dev/null @@ -1,307 +0,0 @@ -# -# Author:: Matthew Kent (<mkent@magoazul.com>) -# Copyright:: Copyright 2009-2016, Matthew Kent -# 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. -# - -# yum-dump.py -# Inspired by yumhelper.py by David Lutterkort -# -# Produce a list of installed, available and re-installable packages using yum -# and dump the results to stdout. -# -# yum-dump invokes yum similarly to the command line interface which makes it -# subject to most of the configuration parameters in yum.conf. yum-dump will -# also load yum plugins in the same manor as yum - these can affect the output. -# -# Can be run as non root, but that won't update the cache. -# -# Intended to support yum 2.x and 3.x - -import os -import sys -import time -import yum -import re -import errno - -from yum import Errors -from optparse import OptionParser -from distutils import version - -YUM_PID_FILE='/var/run/yum.pid' - -YUM_VER = version.StrictVersion(yum.__version__) -YUM_MAJOR = YUM_VER.version[0] - -if YUM_MAJOR > 3 or YUM_MAJOR < 2: - print >> sys.stderr, "yum-dump Error: Can't match supported yum version" \ - " (%s)" % yum.__version__ - sys.exit(1) - -# Required for Provides output -if YUM_MAJOR == 2: - import rpm - import rpmUtils.miscutils - -def setup(yb, options): - # Only want our output - # - if YUM_MAJOR == 3: - try: - if YUM_VER >= version.StrictVersion("3.2.22"): - yb.preconf.errorlevel=0 - yb.preconf.debuglevel=0 - - # initialize the config - yb.conf - else: - yb.doConfigSetup(errorlevel=0, debuglevel=0) - except yum.Errors.ConfigError, e: - # suppresses an ignored exception at exit - yb.preconf = None - print >> sys.stderr, "yum-dump Config Error: %s" % e - return 1 - except ValueError, e: - yb.preconf = None - print >> sys.stderr, "yum-dump Options Error: %s" % e - return 1 - elif YUM_MAJOR == 2: - yb.doConfigSetup() - - def __log(a,b): pass - - yb.log = __log - yb.errorlog = __log - - # Give Chef every possible package version, it can decide what to do with them - if YUM_MAJOR == 3: - yb.conf.showdupesfromrepos = True - elif YUM_MAJOR == 2: - yb.conf.setConfigOption('showdupesfromrepos', True) - - # Optionally run only on cached repositories, but non root must use the cache - if os.geteuid() != 0: - if YUM_MAJOR == 3: - yb.conf.cache = True - elif YUM_MAJOR == 2: - yb.conf.setConfigOption('cache', True) - else: - if YUM_MAJOR == 3: - yb.conf.cache = options.cache - elif YUM_MAJOR == 2: - yb.conf.setConfigOption('cache', options.cache) - - # Handle repo toggle via id or glob exactly like yum - for opt, repos in options.repo_control: - for repo in repos: - if opt == '--enablerepo': - yb.repos.enableRepo(repo) - elif opt == '--disablerepo': - yb.repos.disableRepo(repo) - - return 0 - -def dump_packages(yb, list, output_provides): - packages = {} - - if YUM_MAJOR == 2: - yb.doTsSetup() - yb.doRepoSetup() - yb.doSackSetup() - - db = yb.doPackageLists(list) - - for pkg in db.installed: - pkg.type = 'i' - packages[str(pkg)] = pkg - - if YUM_VER >= version.StrictVersion("3.2.21"): - for pkg in db.available: - pkg.type = 'a' - packages[str(pkg)] = pkg - - # These are both installed and available - for pkg in db.reinstall_available: - pkg.type = 'r' - packages[str(pkg)] = pkg - else: - # Old style method - no reinstall list - for pkg in yb.pkgSack.returnPackages(): - - if str(pkg) in packages: - if packages[str(pkg)].type == "i": - packages[str(pkg)].type = 'r' - continue - - pkg.type = 'a' - packages[str(pkg)] = pkg - - unique_packages = packages.values() - - unique_packages.sort(lambda x, y: cmp(x.name, y.name)) - - for pkg in unique_packages: - if output_provides == "all" or \ - (output_provides == "installed" and (pkg.type == "i" or pkg.type == "r")): - - # yum 2 doesn't have provides_print, implement it ourselves using methods - # based on requires gathering in packages.py - if YUM_MAJOR == 2: - provlist = [] - - # Installed and available are gathered in different ways - if pkg.type == 'i' or pkg.type == 'r': - names = pkg.hdr[rpm.RPMTAG_PROVIDENAME] - flags = pkg.hdr[rpm.RPMTAG_PROVIDEFLAGS] - ver = pkg.hdr[rpm.RPMTAG_PROVIDEVERSION] - if names is not None: - tmplst = zip(names, flags, ver) - - for (n, f, v) in tmplst: - prov = rpmUtils.miscutils.formatRequire(n, v, f) - provlist.append(prov) - # This is slow :( - elif pkg.type == 'a': - for prcoTuple in pkg.returnPrco('provides'): - prcostr = pkg.prcoPrintable(prcoTuple) - provlist.append(prcostr) - - provides = provlist - else: - provides = pkg.provides_print - else: - provides = "[]" - - print '%s %s %s %s %s %s %s %s' % ( - pkg.name, - pkg.epoch, - pkg.version, - pkg.release, - pkg.arch, - provides, - pkg.type, - pkg.repoid ) - - return 0 - -def yum_dump(options): - lock_obtained = False - - yb = yum.YumBase() - - status = setup(yb, options) - if status != 0: - return status - - if options.output_options: - print "[option installonlypkgs] %s" % " ".join(yb.conf.installonlypkgs) - - # Non root can't handle locking on rhel/centos 4 - if os.geteuid() != 0: - return dump_packages(yb, options.package_list, options.output_provides) - - # Wrap the collection and output of packages in yum's global lock to prevent - # any inconsistencies. - try: - # Spin up to --yum-lock-timeout option - countdown = options.yum_lock_timeout - while True: - try: - yb.doLock(YUM_PID_FILE) - lock_obtained = True - except Errors.LockError, e: - time.sleep(1) - countdown -= 1 - if countdown == 0: - print >> sys.stderr, "yum-dump Locking Error! Couldn't obtain an " \ - "exclusive yum lock in %d seconds. Giving up." % options.yum_lock_timeout - return 200 - else: - break - - return dump_packages(yb, options.package_list, options.output_provides) - - # Ensure we clear the lock and cleanup any resources - finally: - try: - yb.closeRpmDB() - if lock_obtained == True: - yb.doUnlock(YUM_PID_FILE) - except Errors.LockError, e: - print >> sys.stderr, "yum-dump Unlock Error: %s" % e - return 200 - -# Preserve order of enable/disable repo args like yum does -def gather_repo_opts(option, opt, value, parser): - if getattr(parser.values, option.dest, None) is None: - setattr(parser.values, option.dest, []) - getattr(parser.values, option.dest).append((opt, value.split(','))) - -def main(): - usage = "Usage: %prog [options]\n" + \ - "Output a list of installed, available and re-installable packages via yum" - parser = OptionParser(usage=usage) - parser.add_option("-C", "--cache", - action="store_true", dest="cache", default=False, - help="run entirely from cache, don't update cache") - parser.add_option("-o", "--options", - action="store_true", dest="output_options", default=False, - help="output select yum options useful to Chef") - parser.add_option("-p", "--installed-provides", - action="store_const", const="installed", dest="output_provides", default="none", - help="output Provides for installed packages, big/wide output") - parser.add_option("-P", "--all-provides", - action="store_const", const="all", dest="output_provides", default="none", - help="output Provides for all package, slow, big/wide output") - parser.add_option("-i", "--installed", - action="store_const", const="installed", dest="package_list", default="all", - help="output only installed packages") - parser.add_option("-a", "--available", - action="store_const", const="available", dest="package_list", default="all", - help="output only available and re-installable packages") - parser.add_option("--enablerepo", - action="callback", callback=gather_repo_opts, type="string", dest="repo_control", default=[], - help="enable disabled repositories by id or glob") - parser.add_option("--disablerepo", - action="callback", callback=gather_repo_opts, type="string", dest="repo_control", default=[], - help="disable repositories by id or glob") - parser.add_option("--yum-lock-timeout", - action="store", type="int", dest="yum_lock_timeout", default=30, - help="Time in seconds to wait for yum process lock") - - (options, args) = parser.parse_args() - - try: - return yum_dump(options) - - except yum.Errors.RepoError, e: - print >> sys.stderr, "yum-dump Repository Error: %s" % e - return 1 - - except yum.Errors.YumBaseError, e: - print >> sys.stderr, "yum-dump General Error: %s" % e - return 1 - -try: - status = main() -# Suppress a nasty broken pipe error when output is piped to utilities like 'head' -except IOError, e: - if e.errno == errno.EPIPE: - sys.exit(1) - else: - raise - -sys.exit(status) diff --git a/lib/chef/provider/package/yum.rb b/lib/chef/provider/package/yum.rb index dfd32fde55..805a74d013 100644 --- a/lib/chef/provider/package/yum.rb +++ b/lib/chef/provider/package/yum.rb @@ -1,6 +1,5 @@ - -# Author:: Adam Jacob (<adam@chef.io>) -# Copyright:: Copyright 2008-2016, Chef Software, Inc. +# +# Copyright:: Copyright 2016-2018, Chef Software Inc. # License:: Apache License, Version 2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -16,1402 +15,271 @@ # limitations under the License. # -require "chef/config" require "chef/provider/package" +require "chef/resource/yum_package" require "chef/mixin/which" -require "chef/resource/package" -require "singleton" +require "chef/mixin/shell_out" require "chef/mixin/get_source_from_package" +require "chef/provider/package/yum/python_helper" +require "chef/provider/package/yum/version" +# the stubs in the YumCache class are still an external API +require "chef/provider/package/yum/yum_cache" class Chef class Provider class Package class Yum < Chef::Provider::Package + extend Chef::Mixin::Which + extend Chef::Mixin::ShellOut + include Chef::Mixin::GetSourceFromPackage - 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 evr =~ %r{^([\d]+):} - epoch = $1.to_i - lead = $1.length + 1 - elsif evr[0].ord == ":".ord - epoch = 0 - lead = 1 - end - - if evr =~ %r{:?.*-(.*)$} - 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 + allow_nils + use_multipackage_api + use_package_name_for_source - 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 + provides :package, platform_family: %w{fedora amazon rhel} - def self.parse(*args) - self.new(*args) - end + provides :yum_package - def <=>(y) - compare_versions(y) - end + # + # Most of the magic in this class happens in the python helper script. The ruby side of this + # provider knows only enough to translate Chef-style new_resource name+package+version into + # a request to the python side. The python side is then responsible for knowing everything + # about RPMs and what is installed and what is available. The ruby side of this class should + # remain a lightweight translation layer to translate Chef requests into RPC requests to + # python. This class knows nothing about how to compare RPM versions, and does not maintain + # any cached state of installed/available versions and should be kept that way. + # + def python_helper + @python_helper ||= PythonHelper.instance + end - def compare(y) - compare_versions(y, false) - end + def load_current_resource + flushcache if new_resource.flush_cache[:before] - def partial_compare(y) - compare_versions(y, true) - end + @current_resource = Chef::Resource::YumPackage.new(new_resource.name) + current_resource.package_name(new_resource.package_name) + current_resource.version(get_current_versions) - # RPM::Version rpm_version_to_s equivalent - def to_s - if @r.nil? - @v - else - "#{@v}-#{@r}" - end - end + current_resource + end - def evr - "#{@e}:#{@v}-#{@r}" + def define_resource_requirements + requirements.assert(:install, :upgrade, :remove, :purge) do |a| + a.assertion { !new_resource.source || ::File.exist?(new_resource.source) } + a.failure_message Chef::Exceptions::Package, "Package #{new_resource.package_name} not found: #{new_resource.source}" + a.whyrun "assuming #{new_resource.source} would have previously been created" 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 + super + end - return 0 + def candidate_version + package_name_array.each_with_index.map do |pkg, i| + available_version(i).version_with_arch 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 + def get_current_versions + package_name_array.each_with_index.map do |pkg, i| + installed_version(i).version_with_arch end - attr_reader :n, :a, :version, :provides - alias :name :n - alias :arch :a + end - def <=>(y) - compare(y) - end + def install_package(names, versions) + method = nil + methods = [] + names.each_with_index do |n, i| + next if n.nil? - def compare(y) - x = self + av = available_version(i) - # easy! :) - return 0 if x.nevra == y.nevra + name = av.name # resolve the name via the available/candidate version - # 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 + iv = python_helper.package_query(:whatinstalled, name) - # compare version - if x.version > y.version - return 1 - elsif x.version < y.version - return -1 - end + method = "install" - # 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 + # If this is a package like the kernel that can be installed multiple times, we'll skip over this logic + if new_resource.allow_downgrade && version_gt?(iv.version_with_arch, av.version_with_arch) && !python_helper.install_only_packages(name) + # We allow downgrading only in the evenit of single-package + # rules where the user explicitly allowed it + method = "downgrade" end - return 0 + methods << method end - def to_s - nevra + # We could split this up into two commands if we wanted to, but + # for now, just don't support this. + if methods.uniq.length > 1 + raise Chef::Exceptions::Package, "Multipackage rule has a mix of upgrade and downgrade packages. Cannot proceed." end - def nevra - "#{@n}-#{@version.evr}.#{@a}" + if new_resource.source + yum(options, "-y #{method}", new_resource.source) + else + resolved_names = names.each_with_index.map { |name, i| available_version(i).to_s unless name.nil? } + yum(options, "-y #{method}", resolved_names) end + flushcache 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 string =~ %r{^(\S+)\s+(>|>=|=|==|<=|<)\s+(\S+)$} - 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) + # yum upgrade does not work on uninstalled packaged, while install will upgrade + alias upgrade_package install_package - # 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 + def remove_package(names, versions) + resolved_names = names.each_with_index.map { |name, i| installed_version(i).to_s unless name.nil? } + yum(options, "-y remove", resolved_names) + flushcache end - class RPMProvide < RPMDependency; end - class RPMRequire < RPMDependency; end + alias purge_package remove_package - 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 + action :flush_cache do + flushcache 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 + # NB: the yum_package provider manages individual single packages, please do not submit issues or PRs to try to add wildcard + # support to lock / unlock. The best solution is to write an execute resource which does a not_if `yum versionlock | grep '^pattern`` kind of approach + def lock_package(names, versions) + yum("-d0 -e0 -y", options, "versionlock add", resolved_package_lock_names(names)) + 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 + # NB: the yum_package provider manages individual single packages, please do not submit issues or PRs to try to add wildcard + # support to lock / unlock. The best solution is to write an execute resource which does a only_if `yum versionlock | grep '^pattern`` kind of approach + def unlock_package(names, versions) + # yum versionlock delete on rhel6 needs the glob nonsense in the following command + yum("-d0 -e0 -y", options, "versionlock delete", resolved_package_lock_names(names).map { |n| "'*:#{n}-*'" }) + end - @rpms[new_rpm.n] ||= Array.new + private - # 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 + # this will resolve things like `/usr/bin/perl` or virtual packages like `mysql` -- it will not work (well? at all?) with globs that match multiple packages + def resolved_package_lock_names(names) + names.each_with_index.map do |name, i| + if !name.nil? + if installed_version(i).version.nil? + available_version(i).name 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 + installed_version(i).name 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 - + def locked_packages + @locked_packages ||= 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 + locked = shell_out_with_timeout!("yum versionlock list") + locked.stdout.each_line.map do |line| + line.sub(/-[^-]*-[^-]*$/, "").split(":").last.strip 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) - super - - @yum = YumCache.instance - @yum.yum_binary = yum_binary end - def yum_binary - @yum_binary ||= - begin - yum_binary = new_resource.yum_binary if new_resource.is_a?(Chef::Resource::YumPackage) - yum_binary ||= ::File.exist?("/usr/bin/yum-deprecated") ? "yum-deprecated" : "yum" - end + def packages_all_locked?(names, versions) + resolved_package_lock_names(names).all? { |n| locked_packages.include? n } end - # Extra attributes - # - - def arch_for_name(n) - if @new_resource.respond_to?("arch") - @new_resource.arch - elsif @arch - idx = package_name_array.index(n) - as_array(@arch)[idx] - else - nil - end + def packages_all_unlocked?(names, versions) + !resolved_package_lock_names(names).any? { |n| locked_packages.include? n } end - def arch - if @new_resource.respond_to?("arch") - @new_resource.arch - else - nil - end + def version_gt?(v1, v2) + return false if v1.nil? || v2.nil? + python_helper.compare_versions(v1, v2) == 1 end - def set_arch(arch) - if @new_resource.respond_to?("arch") - @new_resource.arch(arch) - end + def version_equals?(v1, v2) + return false if v1.nil? || v2.nil? + python_helper.compare_versions(v1, v2) == 0 end - def flush_cache - if @new_resource.respond_to?("flush_cache") - @new_resource.flush_cache - else - { :before => false, :after => false } - end + def version_compare(v1, v2) + return false if v1.nil? || v2.nil? + python_helper.compare_versions(v1, v2) end - # Helpers - # - - def yum_arch(arch) - arch ? ".#{arch}" : nil + # Generate the yum syntax for the package + def yum_syntax(name, version, arch) + s = name + s += "-#{version}" if version + s += ".#{arch}" if arch + s end - def yum_command(command) - command = "#{yum_binary} #{command}" - Chef::Log.debug("#{@new_resource}: yum command: \"#{command}\"") - status = shell_out_with_timeout(command, { :timeout => Chef::Config[:yum_timeout] }) - - # This is fun: rpm can encounter errors in the %post/%postun scripts which aren't - # considered fatal - meaning the rpm is still successfully installed. These issue - # cause yum to emit a non fatal warning but still exit(1). As there's currently no - # way to suppress this behavior and an exit(1) will break a Chef run we make an - # effort to trap these and re-run the same install command - it will either fail a - # second time or succeed. - # - # A cleaner solution would have to be done in python and better hook into - # yum/rpm to handle exceptions as we see fit. - if status.exitstatus == 1 - status.stdout.each_line do |l| - # rpm-4.4.2.3 lib/psm.c line 2182 - if l =~ %r{^error: %(post|postun)\(.*\) scriptlet failed, exit status \d+$} - Chef::Log.warn("#{@new_resource} caught non-fatal scriptlet issue: \"#{l}\". Can't trust yum exit status " + - "so running install again to verify.") - status = shell_out_with_timeout(command, { :timeout => Chef::Config[:yum_timeout] }) - break - end + def resolve_source_to_version_obj + shell_out_with_timeout!("rpm -qp --queryformat '%{NAME} %{EPOCH} %{VERSION} %{RELEASE} %{ARCH}\n' #{new_resource.source}").stdout.each_line do |line| + # this is another case of committing the sin of doing some lightweight mangling of RPM versions in ruby -- but the output of the rpm command + # does not match what the yum library accepts. + case line + when /^(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)$/ + return Version.new($1, "#{$2 == '(none)' ? '0' : $2}:#{$3}-#{$4}", $5) end end - - if status.exitstatus > 0 - command_output = "STDOUT: #{status.stdout}\nSTDERR: #{status.stderr}" - raise Chef::Exceptions::Exec, "#{command} returned #{status.exitstatus}:\n#{command_output}" - end end - # Standard Provider methods for Parent - # - - def load_current_resource - if flush_cache[:before] - @yum.reload - end - - if @new_resource.options - repo_control = [] - @new_resource.options.split.each do |opt| - if opt =~ %r{--(enable|disable)repo=.+} - repo_control << opt - end - end - - if repo_control.size > 0 - @yum.enable_extra_repo_control(repo_control.join(" ")) - else - @yum.disable_extra_repo_control - end - else - @yum.disable_extra_repo_control - end - - # At this point package_name could be: - # - # 1) a package name, eg: "foo" - # 2) a package name.arch, eg: "foo.i386" - # 3) or a dependency, eg: "foo >= 1.1" - - # Check if we have name or name+arch which has a priority over a dependency - package_name_array.each_with_index do |n, index| - unless @yum.package_available?(n) - # If they aren't in the installed packages they could be a dependency - dep = parse_dependency(n, new_version_array[index]) - if dep - if @new_resource.package_name.is_a?(Array) - @new_resource.package_name(package_name_array - [n] + [dep.first]) - @new_resource.version(new_version_array - [new_version_array[index]] + [dep.last]) if dep.last - else - @new_resource.package_name(dep.first) - @new_resource.version(dep.last) if dep.last - end - end - end - end - - @current_resource = Chef::Resource::Package.new(@new_resource.name) - @current_resource.package_name(@new_resource.package_name) - - installed_version = [] - @candidate_version = [] - @arch = [] - if @new_resource.source - unless ::File.exists?(@new_resource.source) - raise Chef::Exceptions::Package, "Package #{@new_resource.name} not found: #{@new_resource.source}" - end - - Chef::Log.debug("#{@new_resource} checking rpm status") - shell_out_with_timeout!("rpm -qp --queryformat '%{NAME} %{VERSION}-%{RELEASE}\n' #{@new_resource.source}", :timeout => Chef::Config[:yum_timeout]).stdout.each_line do |line| - case line - when /([\w\d_.-]+)\s([\w\d_.-]+)/ - @current_resource.package_name($1) - @new_resource.version($2) - end - end - @candidate_version << @new_resource.version - installed_version << @yum.installed_version(@current_resource.package_name, arch) - else - - package_name_array.each_with_index do |pkg, idx| - # Don't overwrite an existing arch - if arch - name, parch = pkg, arch - else - name, parch = parse_arch(pkg) - # if we parsed an arch from the name, update the name - # to be just the package name. - if parch - if @new_resource.package_name.is_a?(Array) - @new_resource.package_name[idx] = name - else - @new_resource.package_name(name) - # only set the arch if it's a single package - set_arch(parch) - end - end - end + # @returns Array<Version> + def available_version(index) + @available_version ||= [] - if @new_resource.version - new_resource = - "#{@new_resource.package_name}-#{@new_resource.version}#{yum_arch(parch)}" - else - new_resource = "#{@new_resource.package_name}#{yum_arch(parch)}" - end - Chef::Log.debug("#{@new_resource} checking yum info for #{new_resource}") - installed_version << @yum.installed_version(name, parch) - @candidate_version << @yum.candidate_version(name, parch) - @arch << parch - end - end + @available_version[index] ||= if new_resource.source + resolve_source_to_version_obj + else + python_helper.package_query(:whatavailable, package_name_array[index], version: safe_version_array[index], arch: safe_arch_array[index], options: options) + end - if installed_version.size == 1 - @current_resource.version(installed_version[0]) - @candidate_version = @candidate_version[0] - @arch = @arch[0] - else - @current_resource.version(installed_version) - end - - Chef::Log.debug("#{@new_resource} installed version: #{installed_version || "(none)"} candidate version: " + - "#{@candidate_version || "(none)"}") - - @current_resource + @available_version[index] end - def install_remote_package(name, version) - # Work around yum not exiting with an error if a package doesn't exist - # for CHEF-2062 - all_avail = as_array(name).zip(as_array(version)).any? do |n, v| - @yum.version_available?(n, v, arch_for_name(n)) - end - method = log_method = nil - methods = [] - if all_avail - # More Yum fun: - # - # yum install of an old name+version will exit(1) - # yum install of an old name+version+arch will exit(0) for some reason - # - # Some packages can be installed multiple times like the kernel - as_array(name).zip(as_array(version)).each do |n, v| - method = "install" - log_method = "installing" - idx = package_name_array.index(n) - unless @yum.allow_multi_install.include?(n) - if RPMVersion.parse(current_version_array[idx]) > RPMVersion.parse(v) - # We allow downgrading only in the evenit of single-package - # rules where the user explicitly allowed it - if allow_downgrade - method = "downgrade" - log_method = "downgrading" - else - # we bail like yum when the package is older - raise Chef::Exceptions::Package, "Installed package #{n}-#{current_version_array[idx]} is newer " + - "than candidate package #{n}-#{v}" - end - end - end - # methods don't count for packages we won't be touching - next if RPMVersion.parse(current_version_array[idx]) == RPMVersion.parse(v) - methods << method - end - - # We could split this up into two commands if we wanted to, but - # for now, just don't support this. - if methods.uniq.length > 1 - raise Chef::Exceptions::Package, "Multipackage rule #{name} has a mix of upgrade and downgrade packages. Cannot proceed." - end - - repos = [] - pkg_string_bits = [] - as_array(name).zip(as_array(version)).each do |n, v| - idx = package_name_array.index(n) - a = arch_for_name(n) - s = "" - unless v == current_version_array[idx] - s = "#{n}-#{v}#{yum_arch(a)}" - repo = @yum.package_repository(n, v, a) - repos << "#{s} from #{repo} repository" - pkg_string_bits << s - end - end - pkg_string = pkg_string_bits.join(" ") - Chef::Log.info("#{@new_resource} #{log_method} #{repos.join(' ')}") - yum_command("-d0 -e0 -y#{expand_options(@new_resource.options)} #{method} #{pkg_string}") - else - raise Chef::Exceptions::Package, "Version #{version} of #{name} not found. Did you specify both version " + - "and release? (version-release, e.g. 1.84-10.fc6)" - end + # @returns Array<Version> + def installed_version(index) + @installed_version ||= [] + @installed_version[index] ||= if new_resource.source + python_helper.package_query(:whatinstalled, available_version(index).name, version: safe_version_array[index], arch: safe_arch_array[index]) + else + python_helper.package_query(:whatinstalled, package_name_array[index], version: safe_version_array[index], arch: safe_arch_array[index]) + end + @installed_version[index] end - def install_package(name, version) - if @new_resource.source - yum_command("-d0 -e0 -y#{expand_options(@new_resource.options)} localinstall #{@new_resource.source}") - else - install_remote_package(name, version) - end - - if flush_cache[:after] - @yum.reload - else - @yum.reload_installed - end + # cache flushing is accomplished by simply restarting the python helper. this produces a roughly + # 15% hit to the runtime of installing/removing/upgrading packages. correctly using multipackage + # array installs (and the multipackage cookbook) can produce 600% improvements in runtime. + def flushcache + python_helper.restart end - # Keep upgrades from trying to install an older candidate version. Can happen when a new - # version is installed then removed from a repository, now the older available version - # shows up as a viable install candidate. - # - # Can be done in upgrade_package but an upgraded from->to log message slips out - # - # Hacky - better overall solution? Custom compare in Package provider? - def action_upgrade - # Could be uninstalled or have no candidate - if @current_resource.version.nil? || !candidate_version_array.any? - super - elsif candidate_version_array.zip(current_version_array).any? do |c, i| - RPMVersion.parse(c) > RPMVersion.parse(i) - end - super - else - Chef::Log.debug("#{@new_resource} is at the latest version - nothing to do") - end + def yum_binary + @yum_binary ||= + begin + yum_binary = new_resource.yum_binary if new_resource.is_a?(Chef::Resource::YumPackage) + yum_binary ||= ::File.exist?("/usr/bin/yum-deprecated") ? "yum-deprecated" : "yum" + end end - def upgrade_package(name, version) - install_package(name, version) + def yum(*args) + shell_out_with_timeout!(a_to_s(yum_binary, *args)) end - def remove_package(name, version) - if version - remove_str = as_array(name).zip(as_array(version)).map do |n, v| - a = arch_for_name(n) - "#{[n, v].join('-')}#{yum_arch(a)}" - end.join(" ") + def safe_version_array + if new_resource.version.is_a?(Array) + new_resource.version + elsif new_resource.version.nil? + package_name_array.map { nil } else - remove_str = as_array(name).map do |n| - a = arch_for_name(n) - "#{n}#{yum_arch(a)}" - end.join(" ") + [ new_resource.version ] end - yum_command("-d0 -e0 -y#{expand_options(@new_resource.options)} remove #{remove_str}") - - if flush_cache[:after] - @yum.reload - else - @yum.reload_installed - end - end - - def purge_package(name, version) - remove_package(name, version) end - private - - def parse_arch(package_name) - # Allow for foo.x86_64 style package_name like yum uses in it's output - # - if package_name =~ %r{^(.*)\.(.*)$} - new_package_name = $1 - new_arch = $2 - # foo.i386 and foo.beta1 are both valid package names or expressions of an arch. - # Ensure we don't have an existing package matching package_name, then ensure we at - # least have a match for the new_package+new_arch before we overwrite. If neither - # then fall through to standard package handling. - old_installed = @yum.installed_version(package_name) - old_candidate = @yum.candidate_version(package_name) - new_installed = @yum.installed_version(new_package_name, new_arch) - new_candidate = @yum.candidate_version(new_package_name, new_arch) - if (old_installed.nil? && old_candidate.nil?) && (new_installed || new_candidate) - Chef::Log.debug("Parsed out arch #{new_arch}, new package name is #{new_package_name}") - return new_package_name, new_arch - end - end - return package_name, nil - end - - # If we don't have the package we could have been passed a 'whatprovides' feature - # - # eg: yum install "perl(Config)" - # yum install "mtr = 2:0.71-3.1" - # yum install "mtr > 2:0.71" - # - # We support resolving these out of the Provides data imported from yum-dump.py and - # matching them up with an actual package so the standard resource handling can apply. - # - # There is currently no support for filename matching. - def parse_dependency(name, version) - # Transform the package_name into a requirement - - # If we are passed a version or a version constraint we have to assume it's a requirement first. If it can't be - # parsed only yum_require.name will be set and @new_resource.version will be left intact - if version - require_string = "#{name} #{version}" + def safe_arch_array + if new_resource.arch.is_a?(Array) + new_resource.arch + elsif new_resource.arch.nil? + package_name_array.map { nil } else - # Transform the package_name into a requirement, might contain a version, could just be - # a match for virtual provides - require_string = name - end - yum_require = RPMRequire.parse(require_string) - # and gather all the packages that have a Provides feature satisfying the requirement. - # It could be multiple be we can only manage one - packages = @yum.packages_from_require(yum_require) - - if packages.empty? - # Don't bother if we are just ensuring a package is removed - we don't need Provides data - actions = Array(@new_resource.action) - unless actions.size == 1 && (actions[0] == :remove || actions[0] == :purge) - Chef::Log.debug("#{@new_resource} couldn't match #{@new_resource.package_name} in " + - "installed Provides, loading available Provides - this may take a moment") - @yum.reload_provides - packages = @yum.packages_from_require(yum_require) - end - end - - unless packages.empty? - new_package_name = packages.first.name - new_package_version = packages.first.version.to_s - debug_msg = "#{name}: Unable to match package '#{name}' but matched #{packages.size} " - debug_msg << (packages.size == 1 ? "package" : "packages") - debug_msg << ", selected '#{new_package_name}' version '#{new_package_version}'" - Chef::Log.debug(debug_msg) - - # Ensure it's not the same package under a different architecture - unique_names = [] - packages.each do |pkg| - unique_names << "#{pkg.name}-#{pkg.version.evr}" - end - unique_names.uniq! - - if unique_names.size > 1 - Chef::Log.warn("#{@new_resource} matched multiple Provides for #{@new_resource.package_name} " + - "but we can only use the first match: #{new_package_name}. Please use a more " + - "specific version.") - end - - if yum_require.version.to_s.nil? - new_package_version = nil - end - - [new_package_name, new_package_version] + [ new_resource.arch ] end end diff --git a/lib/chef/provider/package/yum/python_helper.rb b/lib/chef/provider/package/yum/python_helper.rb new file mode 100644 index 0000000000..0a09e12c3d --- /dev/null +++ b/lib/chef/provider/package/yum/python_helper.rb @@ -0,0 +1,221 @@ +# +# Copyright:: Copyright 2016-2018, 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/mixin/which" +require "chef/mixin/shell_out" +require "chef/provider/package/yum/version" +require "singleton" +require "timeout" + +class Chef + class Provider + class Package + class Yum < Chef::Provider::Package + class PythonHelper + include Singleton + include Chef::Mixin::Which + include Chef::Mixin::ShellOut + + attr_accessor :stdin + attr_accessor :stdout + attr_accessor :stderr + attr_accessor :inpipe + attr_accessor :outpipe + attr_accessor :wait_thr + + YUM_HELPER = ::File.expand_path(::File.join(::File.dirname(__FILE__), "yum_helper.py")).freeze + + def yum_command + @yum_command ||= which("python", "python2", "python2.7") do |f| + shell_out("#{f} -c 'import yum'").exitstatus == 0 + end + " #{YUM_HELPER}" + end + + def start + ENV["PYTHONUNBUFFERED"] = "1" + @inpipe, inpipe_write = IO.pipe + outpipe_read, @outpipe = IO.pipe + @stdin, @stdout, @stderr, @wait_thr = Open3.popen3("#{yum_command} #{outpipe_read.fileno} #{inpipe_write.fileno}", outpipe_read.fileno => outpipe_read, inpipe_write.fileno => inpipe_write, close_others: false) + outpipe_read.close + inpipe_write.close + end + + def reap + unless wait_thr.nil? + Process.kill("INT", wait_thr.pid) rescue nil + begin + Timeout.timeout(3) do + wait_thr.value # this calls waitpid() + end + rescue Timeout::Error + Process.kill("KILL", wait_thr.pid) rescue nil + end + stdin.close unless stdin.nil? + stdout.close unless stdout.nil? + stderr.close unless stderr.nil? + inpipe.close unless inpipe.nil? + outpipe.close unless outpipe.nil? + end + end + + def check + start if stdin.nil? + end + + def compare_versions(version1, version2) + query("versioncompare", { "versions" => [version1, version2] }).to_i + end + + def install_only_packages(name) + query_output = query("installonlypkgs", { "package" => name }) + if query_output == "False" + return false + elsif query_output == "True" + return true + end + end + + def options_params(options) + options.each_with_object({}) do |opt, h| + if opt =~ /--enablerepo=(.+)/ + $1.split(",").each do |repo| + h["repos"] ||= [] + h["repos"].push( { "enable" => repo } ) + end + end + if opt =~ /--disablerepo=(.+)/ + $1.split(",").each do |repo| + h["repos"] ||= [] + h["repos"].push( { "disable" => repo } ) + end + end + end + end + + # @returns Array<Version> + # NB: "options" here is the yum_package options hash and is deliberately not **opts + def package_query(action, provides, version: nil, arch: nil, options: {}) + parameters = { "provides" => provides, "version" => version, "arch" => arch } + repo_opts = options_params(options || {}) + parameters.merge!(repo_opts) + query_output = query(action, parameters) + version = parse_response(query_output.lines.last) + Chef::Log.trace "parsed #{version} from python helper" + # XXX: for now we restart after every query with an enablerepo/disablerepo to clean the helpers internal state + restart unless repo_opts.empty? + version + end + + def restart + reap + start + end + + private + + # i couldn't figure out how to decompose an evr on the python side, it seems reasonably + # painless to do it in ruby (generally massaging nevras in the ruby side is HIGHLY + # discouraged -- this is an "every rule has an exception" exception -- any additional + # functionality should probably trigger moving this regexp logic into python) + def add_version(hash, version) + epoch = nil + if version =~ /(\S+):(\S+)/ + epoch = $1 + version = $2 + end + if version =~ /(\S+)-(\S+)/ + version = $1 + release = $2 + end + hash["epoch"] = epoch unless epoch.nil? + hash["release"] = release unless release.nil? + hash["version"] = version + end + + def query(action, parameters) + with_helper do + json = build_query(action, parameters) + Chef::Log.trace "sending '#{json}' to python helper" + outpipe.syswrite json + "\n" + output = inpipe.sysread(4096).chomp + Chef::Log.trace "got '#{output}' from python helper" + return output + end + end + + def build_query(action, parameters) + hash = { "action" => action } + parameters.each do |param_name, param_value| + hash[param_name] = param_value unless param_value.nil? + end + + # Special handling for certain action / param combos + if [:whatinstalled, :whatavailable].include?(action) + add_version(hash, parameters["version"]) unless parameters["version"].nil? + end + + FFI_Yajl::Encoder.encode(hash) + end + + def parse_response(output) + array = output.split.map { |x| x == "nil" ? nil : x } + array.each_slice(3).map { |x| Version.new(*x) }.first + end + + def drain_fds + output = "" + fds, = IO.select([stderr, stdout, inpipe], nil, nil, 0) + unless fds.nil? + fds.each do |fd| + output += fd.sysread(4096) rescue "" + end + end + output + rescue => e + output + end + + def with_helper + max_retries ||= 5 + ret = nil + Timeout.timeout(600) do + check + ret = yield + end + output = drain_fds + unless output.empty? + Chef::Log.trace "discarding output on stderr/stdout from python helper: #{output}" + end + ret + rescue EOFError, Errno::EPIPE, Timeout::Error, Errno::ESRCH => e + output = drain_fds + if ( max_retries -= 1 ) > 0 + unless output.empty? + Chef::Log.trace "discarding output on stderr/stdout from python helper: #{output}" + end + restart + retry + else + raise e if output.empty? + raise "yum-helper.py had stderr/stdout output:\n\n#{output}" + end + end + end + end + end + end +end 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..eefc0b95b2 --- /dev/null +++ b/lib/chef/provider/package/yum/rpm_utils.rb @@ -0,0 +1,651 @@ + +# Author:: Adam Jacob (<adam@chef.io>) +# Copyright:: Copyright 2008-2018, 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" + +# +# BUGGY AND DEPRECATED: This ruby code is known to not match the python implementation for version comparisons. +# The APIs here should probably be converted to talk to the PythonHelper or just abandonded completely. +# +# e.g. this should just use Chef::Provider::Package::Yum::PythonHelper.instance.compare_versions(x,y) +# +# The python_helper could be extended to support additional APIs in here to remove the ruby code entirely. +# + +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 /^([\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 /:?.*-(.*)$/.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) + new(*args) + end + + def <=>(other) + compare_versions(other) + end + + def compare(other) + compare_versions(other, false) + end + + def partial_compare(other) + compare_versions(other, 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 + + 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 <=>(other) + compare(other) + 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 + + 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 /^(\S+)\s+(>|>=|=|==|<=|<)\s+(\S+)$/.match(string) # rubocop:disable Performance/RedundantMatch + name = $1 + flag = if $2 == "=" + :== + else + :"#{$2}" + end + version = $3 + + new(name, version, flag) + else + name = string + new(name, nil, nil) + end + end + + # Test if another RPMDependency satisfies our requirements + def satisfy?(y) + unless y.is_a?(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 + + 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 = {} + # package nevra => RPMPackage for lookups + @index = {} + # provide name (aka feature) => [RPMPackage, RPMPackage] each providing this feature + @provides = {} + # RPMPackages listed as available + @available = Set.new + # RPMPackages listed as installed + @installed = Set.new + end + + def [](package_name) + 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.is_a?(RPMDbPackage) + raise ArgumentError, "Expecting an RPMDbPackage object" + end + + @rpms[new_rpm.n] ||= [] + + # 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] ||= [] + @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) + 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.is_a?(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 + + what + end + end + + end + end + end +end diff --git a/lib/chef/provider/package/yum/simplejson/LICENSE.txt b/lib/chef/provider/package/yum/simplejson/LICENSE.txt new file mode 100644 index 0000000000..e05f49c3fd --- /dev/null +++ b/lib/chef/provider/package/yum/simplejson/LICENSE.txt @@ -0,0 +1,79 @@ +simplejson is dual-licensed software. It is available under the terms +of the MIT license, or the Academic Free License version 2.1. The full +text of each license agreement is included below. This code is also +licensed to the Python Software Foundation (PSF) under a Contributor +Agreement. + +MIT License +=========== + +Copyright (c) 2006 Bob Ippolito + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +Academic Free License v. 2.1 +============================ + +Copyright (c) 2006 Bob Ippolito. All rights reserved. + +This Academic Free License (the "License") applies to any original work of authorship (the "Original Work") whose owner (the "Licensor") has placed the following notice immediately following the copyright notice for the Original Work: + +Licensed under the Academic Free License version 2.1 + +1) Grant of Copyright License. Licensor hereby grants You a world-wide, royalty-free, non-exclusive, perpetual, sublicenseable license to do the following: + +a) to reproduce the Original Work in copies; + +b) to prepare derivative works ("Derivative Works") based upon the Original Work; + +c) to distribute copies of the Original Work and Derivative Works to the public; + +d) to perform the Original Work publicly; and + +e) to display the Original Work publicly. + +2) Grant of Patent License. Licensor hereby grants You a world-wide, royalty-free, non-exclusive, perpetual, sublicenseable license, under patent claims owned or controlled by the Licensor that are embodied in the Original Work as furnished by the Licensor, to make, use, sell and offer for sale the Original Work and Derivative Works. + +3) Grant of Source Code License. The term "Source Code" means the preferred form of the Original Work for making modifications to it and all available documentation describing how to modify the Original Work. Licensor hereby agrees to provide a machine-readable copy of the Source Code of the Original Work along with each copy of the Original Work that Licensor distributes. Licensor reserves the right to satisfy this obligation by placing a machine-readable copy of the Source Code in an information repository reasonably calculated to permit inexpensive and convenient access by You for as long as Licensor continues to distribute the Original Work, and by publishing the address of that information repository in a notice immediately following the copyright notice that applies to the Original Work. + +4) Exclusions From License Grant. Neither the names of Licensor, nor the names of any contributors to the Original Work, nor any of their trademarks or service marks, may be used to endorse or promote products derived from this Original Work without express prior written permission of the Licensor. Nothing in this License shall be deemed to grant any rights to trademarks, copyrights, patents, trade secrets or any other intellectual property of Licensor except as expressly stated herein. No patent license is granted to make, use, sell or offer to sell embodiments of any patent claims other than the licensed claims defined in Section 2. No right is granted to the trademarks of Licensor even if such marks are included in the Original Work. Nothing in this License shall be interpreted to prohibit Licensor from licensing under different terms from this License any Original Work that Licensor otherwise would have a right to license. + +5) This section intentionally omitted. + +6) Attribution Rights. You must retain, in the Source Code of any Derivative Works that You create, all copyright, patent or trademark notices from the Source Code of the Original Work, as well as any notices of licensing and any descriptive text identified therein as an "Attribution Notice." You must cause the Source Code for any Derivative Works that You create to carry a prominent Attribution Notice reasonably calculated to inform recipients that You have modified the Original Work. + +7) Warranty of Provenance and Disclaimer of Warranty. Licensor warrants that the copyright in and to the Original Work and the patent rights granted herein by Licensor are owned by the Licensor or are sublicensed to You under the terms of this License with the permission of the contributor(s) of those copyrights and patent rights. Except as expressly stated in the immediately proceeding sentence, the Original Work is provided under this License on an "AS IS" BASIS and WITHOUT WARRANTY, either express or implied, including, without limitation, the warranties of NON-INFRINGEMENT, MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY OF THE ORIGINAL WORK IS WITH YOU. This DISCLAIMER OF WARRANTY constitutes an essential part of this License. No license to Original Work is granted hereunder except under this disclaimer. + +8) Limitation of Liability. Under no circumstances and under no legal theory, whether in tort (including negligence), contract, or otherwise, shall the Licensor be liable to any person for any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or the use of the Original Work including, without limitation, damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses. This limitation of liability shall not apply to liability for death or personal injury resulting from Licensor's negligence to the extent applicable law prohibits such limitation. Some jurisdictions do not allow the exclusion or limitation of incidental or consequential damages, so this exclusion and limitation may not apply to You. + +9) Acceptance and Termination. If You distribute copies of the Original Work or a Derivative Work, You must make a reasonable effort under the circumstances to obtain the express assent of recipients to the terms of this License. Nothing else but this License (or another written agreement between Licensor and You) grants You permission to create Derivative Works based upon the Original Work or to exercise any of the rights granted in Section 1 herein, and any attempt to do so except under the terms of this License (or another written agreement between Licensor and You) is expressly prohibited by U.S. copyright law, the equivalent laws of other countries, and by international treaty. Therefore, by exercising any of the rights granted to You in Section 1 herein, You indicate Your acceptance of this License and all of its terms and conditions. + +10) Termination for Patent Action. This License shall terminate automatically and You may no longer exercise any of the rights granted to You by this License as of the date You commence an action, including a cross-claim or counterclaim, against Licensor or any licensee alleging that the Original Work infringes a patent. This termination provision shall not apply for an action alleging patent infringement by combinations of the Original Work with other software or hardware. + +11) Jurisdiction, Venue and Governing Law. Any action or suit relating to this License may be brought only in the courts of a jurisdiction wherein the Licensor resides or in which Licensor conducts its primary business, and under the laws of that jurisdiction excluding its conflict-of-law provisions. The application of the United Nations Convention on Contracts for the International Sale of Goods is expressly excluded. Any use of the Original Work outside the scope of this License or after its termination shall be subject to the requirements and penalties of the U.S. Copyright Act, 17 U.S.C. § 101 et seq., the equivalent laws of other countries, and international treaty. This section shall survive the termination of this License. + +12) Attorneys Fees. In any action to enforce the terms of this License or seeking damages relating thereto, the prevailing party shall be entitled to recover its costs and expenses, including, without limitation, reasonable attorneys' fees and costs incurred in connection with such action, including any appeal of such action. This section shall survive the termination of this License. + +13) Miscellaneous. This License represents the complete agreement concerning the subject matter hereof. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable. + +14) Definition of "You" in This License. "You" throughout this License, whether in upper or lower case, means an individual or a legal entity exercising rights under, and complying with all of the terms of, this License. For legal entities, "You" includes any entity that controls, is controlled by, or is under common control with you. For purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + +15) Right to Use. You may use the Original Work in all ways not otherwise restricted or conditioned by this License or by law, and Licensor promises not to interfere with or be responsible for such uses by You. + +This license is Copyright (C) 2003-2004 Lawrence E. Rosen. All rights reserved. Permission is hereby granted to copy and distribute this license without modification. This license may not be modified without the express written permission of its copyright owner. diff --git a/lib/chef/provider/package/yum/simplejson/__init__.py b/lib/chef/provider/package/yum/simplejson/__init__.py new file mode 100644 index 0000000000..d5b4d39913 --- /dev/null +++ b/lib/chef/provider/package/yum/simplejson/__init__.py @@ -0,0 +1,318 @@ +r"""JSON (JavaScript Object Notation) <http://json.org> is a subset of +JavaScript syntax (ECMA-262 3rd edition) used as a lightweight data +interchange format. + +:mod:`simplejson` exposes an API familiar to users of the standard library +:mod:`marshal` and :mod:`pickle` modules. It is the externally maintained +version of the :mod:`json` library contained in Python 2.6, but maintains +compatibility with Python 2.4 and Python 2.5 and (currently) has +significant performance advantages, even without using the optional C +extension for speedups. + +Encoding basic Python object hierarchies:: + + >>> import simplejson as json + >>> json.dumps(['foo', {'bar': ('baz', None, 1.0, 2)}]) + '["foo", {"bar": ["baz", null, 1.0, 2]}]' + >>> print json.dumps("\"foo\bar") + "\"foo\bar" + >>> print json.dumps(u'\u1234') + "\u1234" + >>> print json.dumps('\\') + "\\" + >>> print json.dumps({"c": 0, "b": 0, "a": 0}, sort_keys=True) + {"a": 0, "b": 0, "c": 0} + >>> from StringIO import StringIO + >>> io = StringIO() + >>> json.dump(['streaming API'], io) + >>> io.getvalue() + '["streaming API"]' + +Compact encoding:: + + >>> import simplejson as json + >>> json.dumps([1,2,3,{'4': 5, '6': 7}], separators=(',',':')) + '[1,2,3,{"4":5,"6":7}]' + +Pretty printing:: + + >>> import simplejson as json + >>> s = json.dumps({'4': 5, '6': 7}, sort_keys=True, indent=4) + >>> print '\n'.join([l.rstrip() for l in s.splitlines()]) + { + "4": 5, + "6": 7 + } + +Decoding JSON:: + + >>> import simplejson as json + >>> obj = [u'foo', {u'bar': [u'baz', None, 1.0, 2]}] + >>> json.loads('["foo", {"bar":["baz", null, 1.0, 2]}]') == obj + True + >>> json.loads('"\\"foo\\bar"') == u'"foo\x08ar' + True + >>> from StringIO import StringIO + >>> io = StringIO('["streaming API"]') + >>> json.load(io)[0] == 'streaming API' + True + +Specializing JSON object decoding:: + + >>> import simplejson as json + >>> def as_complex(dct): + ... if '__complex__' in dct: + ... return complex(dct['real'], dct['imag']) + ... return dct + ... + >>> json.loads('{"__complex__": true, "real": 1, "imag": 2}', + ... object_hook=as_complex) + (1+2j) + >>> import decimal + >>> json.loads('1.1', parse_float=decimal.Decimal) == decimal.Decimal('1.1') + True + +Specializing JSON object encoding:: + + >>> import simplejson as json + >>> def encode_complex(obj): + ... if isinstance(obj, complex): + ... return [obj.real, obj.imag] + ... raise TypeError(repr(o) + " is not JSON serializable") + ... + >>> json.dumps(2 + 1j, default=encode_complex) + '[2.0, 1.0]' + >>> json.JSONEncoder(default=encode_complex).encode(2 + 1j) + '[2.0, 1.0]' + >>> ''.join(json.JSONEncoder(default=encode_complex).iterencode(2 + 1j)) + '[2.0, 1.0]' + + +Using simplejson.tool from the shell to validate and pretty-print:: + + $ echo '{"json":"obj"}' | python -m simplejson.tool + { + "json": "obj" + } + $ echo '{ 1.2:3.4}' | python -m simplejson.tool + Expecting property name: line 1 column 2 (char 2) +""" +__version__ = '2.0.9' +__all__ = [ + 'dump', 'dumps', 'load', 'loads', + 'JSONDecoder', 'JSONEncoder', +] + +__author__ = 'Bob Ippolito <bob@redivi.com>' + +from decoder import JSONDecoder +from encoder import JSONEncoder + +_default_encoder = JSONEncoder( + skipkeys=False, + ensure_ascii=True, + check_circular=True, + allow_nan=True, + indent=None, + separators=None, + encoding='utf-8', + default=None, +) + +def dump(obj, fp, skipkeys=False, ensure_ascii=True, check_circular=True, + allow_nan=True, cls=None, indent=None, separators=None, + encoding='utf-8', default=None, **kw): + """Serialize ``obj`` as a JSON formatted stream to ``fp`` (a + ``.write()``-supporting file-like object). + + If ``skipkeys`` is true then ``dict`` keys that are not basic types + (``str``, ``unicode``, ``int``, ``long``, ``float``, ``bool``, ``None``) + will be skipped instead of raising a ``TypeError``. + + If ``ensure_ascii`` is false, then the some chunks written to ``fp`` + may be ``unicode`` instances, subject to normal Python ``str`` to + ``unicode`` coercion rules. Unless ``fp.write()`` explicitly + understands ``unicode`` (as in ``codecs.getwriter()``) this is likely + to cause an error. + + If ``check_circular`` is false, then the circular reference check + for container types will be skipped and a circular reference will + result in an ``OverflowError`` (or worse). + + If ``allow_nan`` is false, then it will be a ``ValueError`` to + serialize out of range ``float`` values (``nan``, ``inf``, ``-inf``) + in strict compliance of the JSON specification, instead of using the + JavaScript equivalents (``NaN``, ``Infinity``, ``-Infinity``). + + If ``indent`` is a non-negative integer, then JSON array elements and object + members will be pretty-printed with that indent level. An indent level + of 0 will only insert newlines. ``None`` is the most compact representation. + + If ``separators`` is an ``(item_separator, dict_separator)`` tuple + then it will be used instead of the default ``(', ', ': ')`` separators. + ``(',', ':')`` is the most compact JSON representation. + + ``encoding`` is the character encoding for str instances, default is UTF-8. + + ``default(obj)`` is a function that should return a serializable version + of obj or raise TypeError. The default simply raises TypeError. + + To use a custom ``JSONEncoder`` subclass (e.g. one that overrides the + ``.default()`` method to serialize additional types), specify it with + the ``cls`` kwarg. + + """ + # cached encoder + if (not skipkeys and ensure_ascii and + check_circular and allow_nan and + cls is None and indent is None and separators is None and + encoding == 'utf-8' and default is None and not kw): + iterable = _default_encoder.iterencode(obj) + else: + if cls is None: + cls = JSONEncoder + iterable = cls(skipkeys=skipkeys, ensure_ascii=ensure_ascii, + check_circular=check_circular, allow_nan=allow_nan, indent=indent, + separators=separators, encoding=encoding, + default=default, **kw).iterencode(obj) + # could accelerate with writelines in some versions of Python, at + # a debuggability cost + for chunk in iterable: + fp.write(chunk) + + +def dumps(obj, skipkeys=False, ensure_ascii=True, check_circular=True, + allow_nan=True, cls=None, indent=None, separators=None, + encoding='utf-8', default=None, **kw): + """Serialize ``obj`` to a JSON formatted ``str``. + + If ``skipkeys`` is false then ``dict`` keys that are not basic types + (``str``, ``unicode``, ``int``, ``long``, ``float``, ``bool``, ``None``) + will be skipped instead of raising a ``TypeError``. + + If ``ensure_ascii`` is false, then the return value will be a + ``unicode`` instance subject to normal Python ``str`` to ``unicode`` + coercion rules instead of being escaped to an ASCII ``str``. + + If ``check_circular`` is false, then the circular reference check + for container types will be skipped and a circular reference will + result in an ``OverflowError`` (or worse). + + If ``allow_nan`` is false, then it will be a ``ValueError`` to + serialize out of range ``float`` values (``nan``, ``inf``, ``-inf``) in + strict compliance of the JSON specification, instead of using the + JavaScript equivalents (``NaN``, ``Infinity``, ``-Infinity``). + + If ``indent`` is a non-negative integer, then JSON array elements and + object members will be pretty-printed with that indent level. An indent + level of 0 will only insert newlines. ``None`` is the most compact + representation. + + If ``separators`` is an ``(item_separator, dict_separator)`` tuple + then it will be used instead of the default ``(', ', ': ')`` separators. + ``(',', ':')`` is the most compact JSON representation. + + ``encoding`` is the character encoding for str instances, default is UTF-8. + + ``default(obj)`` is a function that should return a serializable version + of obj or raise TypeError. The default simply raises TypeError. + + To use a custom ``JSONEncoder`` subclass (e.g. one that overrides the + ``.default()`` method to serialize additional types), specify it with + the ``cls`` kwarg. + + """ + # cached encoder + if (not skipkeys and ensure_ascii and + check_circular and allow_nan and + cls is None and indent is None and separators is None and + encoding == 'utf-8' and default is None and not kw): + return _default_encoder.encode(obj) + if cls is None: + cls = JSONEncoder + return cls( + skipkeys=skipkeys, ensure_ascii=ensure_ascii, + check_circular=check_circular, allow_nan=allow_nan, indent=indent, + separators=separators, encoding=encoding, default=default, + **kw).encode(obj) + + +_default_decoder = JSONDecoder(encoding=None, object_hook=None) + + +def load(fp, encoding=None, cls=None, object_hook=None, parse_float=None, + parse_int=None, parse_constant=None, **kw): + """Deserialize ``fp`` (a ``.read()``-supporting file-like object containing + a JSON document) to a Python object. + + If the contents of ``fp`` is encoded with an ASCII based encoding other + than utf-8 (e.g. latin-1), then an appropriate ``encoding`` name must + be specified. Encodings that are not ASCII based (such as UCS-2) are + not allowed, and should be wrapped with + ``codecs.getreader(fp)(encoding)``, or simply decoded to a ``unicode`` + object and passed to ``loads()`` + + ``object_hook`` is an optional function that will be called with the + result of any object literal decode (a ``dict``). The return value of + ``object_hook`` will be used instead of the ``dict``. This feature + can be used to implement custom decoders (e.g. JSON-RPC class hinting). + + To use a custom ``JSONDecoder`` subclass, specify it with the ``cls`` + kwarg. + + """ + return loads(fp.read(), + encoding=encoding, cls=cls, object_hook=object_hook, + parse_float=parse_float, parse_int=parse_int, + parse_constant=parse_constant, **kw) + + +def loads(s, encoding=None, cls=None, object_hook=None, parse_float=None, + parse_int=None, parse_constant=None, **kw): + """Deserialize ``s`` (a ``str`` or ``unicode`` instance containing a JSON + document) to a Python object. + + If ``s`` is a ``str`` instance and is encoded with an ASCII based encoding + other than utf-8 (e.g. latin-1) then an appropriate ``encoding`` name + must be specified. Encodings that are not ASCII based (such as UCS-2) + are not allowed and should be decoded to ``unicode`` first. + + ``object_hook`` is an optional function that will be called with the + result of any object literal decode (a ``dict``). The return value of + ``object_hook`` will be used instead of the ``dict``. This feature + can be used to implement custom decoders (e.g. JSON-RPC class hinting). + + ``parse_float``, if specified, will be called with the string + of every JSON float to be decoded. By default this is equivalent to + float(num_str). This can be used to use another datatype or parser + for JSON floats (e.g. decimal.Decimal). + + ``parse_int``, if specified, will be called with the string + of every JSON int to be decoded. By default this is equivalent to + int(num_str). This can be used to use another datatype or parser + for JSON integers (e.g. float). + + ``parse_constant``, if specified, will be called with one of the + following strings: -Infinity, Infinity, NaN, null, true, false. + This can be used to raise an exception if invalid JSON numbers + are encountered. + + To use a custom ``JSONDecoder`` subclass, specify it with the ``cls`` + kwarg. + + """ + if (cls is None and encoding is None and object_hook is None and + parse_int is None and parse_float is None and + parse_constant is None and not kw): + return _default_decoder.decode(s) + if cls is None: + cls = JSONDecoder + if object_hook is not None: + kw['object_hook'] = object_hook + if parse_float is not None: + kw['parse_float'] = parse_float + if parse_int is not None: + kw['parse_int'] = parse_int + if parse_constant is not None: + kw['parse_constant'] = parse_constant + return cls(encoding=encoding, **kw).decode(s) diff --git a/lib/chef/provider/package/yum/simplejson/__init__.pyc b/lib/chef/provider/package/yum/simplejson/__init__.pyc Binary files differnew file mode 100644 index 0000000000..10679d3b04 --- /dev/null +++ b/lib/chef/provider/package/yum/simplejson/__init__.pyc diff --git a/lib/chef/provider/package/yum/simplejson/decoder.py b/lib/chef/provider/package/yum/simplejson/decoder.py new file mode 100644 index 0000000000..d921ce0b97 --- /dev/null +++ b/lib/chef/provider/package/yum/simplejson/decoder.py @@ -0,0 +1,354 @@ +"""Implementation of JSONDecoder +""" +import re +import sys +import struct + +from simplejson.scanner import make_scanner +try: + from simplejson._speedups import scanstring as c_scanstring +except ImportError: + c_scanstring = None + +__all__ = ['JSONDecoder'] + +FLAGS = re.VERBOSE | re.MULTILINE | re.DOTALL + +def _floatconstants(): + _BYTES = '7FF80000000000007FF0000000000000'.decode('hex') + if sys.byteorder != 'big': + _BYTES = _BYTES[:8][::-1] + _BYTES[8:][::-1] + nan, inf = struct.unpack('dd', _BYTES) + return nan, inf, -inf + +NaN, PosInf, NegInf = _floatconstants() + + +def linecol(doc, pos): + lineno = doc.count('\n', 0, pos) + 1 + if lineno == 1: + colno = pos + else: + colno = pos - doc.rindex('\n', 0, pos) + return lineno, colno + + +def errmsg(msg, doc, pos, end=None): + # Note that this function is called from _speedups + lineno, colno = linecol(doc, pos) + if end is None: + #fmt = '{0}: line {1} column {2} (char {3})' + #return fmt.format(msg, lineno, colno, pos) + fmt = '%s: line %d column %d (char %d)' + return fmt % (msg, lineno, colno, pos) + endlineno, endcolno = linecol(doc, end) + #fmt = '{0}: line {1} column {2} - line {3} column {4} (char {5} - {6})' + #return fmt.format(msg, lineno, colno, endlineno, endcolno, pos, end) + fmt = '%s: line %d column %d - line %d column %d (char %d - %d)' + return fmt % (msg, lineno, colno, endlineno, endcolno, pos, end) + + +_CONSTANTS = { + '-Infinity': NegInf, + 'Infinity': PosInf, + 'NaN': NaN, +} + +STRINGCHUNK = re.compile(r'(.*?)(["\\\x00-\x1f])', FLAGS) +BACKSLASH = { + '"': u'"', '\\': u'\\', '/': u'/', + 'b': u'\b', 'f': u'\f', 'n': u'\n', 'r': u'\r', 't': u'\t', +} + +DEFAULT_ENCODING = "utf-8" + +def py_scanstring(s, end, encoding=None, strict=True, _b=BACKSLASH, _m=STRINGCHUNK.match): + """Scan the string s for a JSON string. End is the index of the + character in s after the quote that started the JSON string. + Unescapes all valid JSON string escape sequences and raises ValueError + on attempt to decode an invalid string. If strict is False then literal + control characters are allowed in the string. + + Returns a tuple of the decoded string and the index of the character in s + after the end quote.""" + if encoding is None: + encoding = DEFAULT_ENCODING + chunks = [] + _append = chunks.append + begin = end - 1 + while 1: + chunk = _m(s, end) + if chunk is None: + raise ValueError( + errmsg("Unterminated string starting at", s, begin)) + end = chunk.end() + content, terminator = chunk.groups() + # Content is contains zero or more unescaped string characters + if content: + if not isinstance(content, unicode): + content = unicode(content, encoding) + _append(content) + # Terminator is the end of string, a literal control character, + # or a backslash denoting that an escape sequence follows + if terminator == '"': + break + elif terminator != '\\': + if strict: + msg = "Invalid control character %r at" % (terminator,) + #msg = "Invalid control character {0!r} at".format(terminator) + raise ValueError(errmsg(msg, s, end)) + else: + _append(terminator) + continue + try: + esc = s[end] + except IndexError: + raise ValueError( + errmsg("Unterminated string starting at", s, begin)) + # If not a unicode escape sequence, must be in the lookup table + if esc != 'u': + try: + char = _b[esc] + except KeyError: + msg = "Invalid \\escape: " + repr(esc) + raise ValueError(errmsg(msg, s, end)) + end += 1 + else: + # Unicode escape sequence + esc = s[end + 1:end + 5] + next_end = end + 5 + if len(esc) != 4: + msg = "Invalid \\uXXXX escape" + raise ValueError(errmsg(msg, s, end)) + uni = int(esc, 16) + # Check for surrogate pair on UCS-4 systems + if 0xd800 <= uni <= 0xdbff and sys.maxunicode > 65535: + msg = "Invalid \\uXXXX\\uXXXX surrogate pair" + if not s[end + 5:end + 7] == '\\u': + raise ValueError(errmsg(msg, s, end)) + esc2 = s[end + 7:end + 11] + if len(esc2) != 4: + raise ValueError(errmsg(msg, s, end)) + uni2 = int(esc2, 16) + uni = 0x10000 + (((uni - 0xd800) << 10) | (uni2 - 0xdc00)) + next_end += 6 + char = unichr(uni) + end = next_end + # Append the unescaped character + _append(char) + return u''.join(chunks), end + + +# Use speedup if available +scanstring = c_scanstring or py_scanstring + +WHITESPACE = re.compile(r'[ \t\n\r]*', FLAGS) +WHITESPACE_STR = ' \t\n\r' + +def JSONObject((s, end), encoding, strict, scan_once, object_hook, _w=WHITESPACE.match, _ws=WHITESPACE_STR): + pairs = {} + # Use a slice to prevent IndexError from being raised, the following + # check will raise a more specific ValueError if the string is empty + nextchar = s[end:end + 1] + # Normally we expect nextchar == '"' + if nextchar != '"': + if nextchar in _ws: + end = _w(s, end).end() + nextchar = s[end:end + 1] + # Trivial empty object + if nextchar == '}': + return pairs, end + 1 + elif nextchar != '"': + raise ValueError(errmsg("Expecting property name", s, end)) + end += 1 + while True: + key, end = scanstring(s, end, encoding, strict) + + # To skip some function call overhead we optimize the fast paths where + # the JSON key separator is ": " or just ":". + if s[end:end + 1] != ':': + end = _w(s, end).end() + if s[end:end + 1] != ':': + raise ValueError(errmsg("Expecting : delimiter", s, end)) + + end += 1 + + try: + if s[end] in _ws: + end += 1 + if s[end] in _ws: + end = _w(s, end + 1).end() + except IndexError: + pass + + try: + value, end = scan_once(s, end) + except StopIteration: + raise ValueError(errmsg("Expecting object", s, end)) + pairs[key] = value + + try: + nextchar = s[end] + if nextchar in _ws: + end = _w(s, end + 1).end() + nextchar = s[end] + except IndexError: + nextchar = '' + end += 1 + + if nextchar == '}': + break + elif nextchar != ',': + raise ValueError(errmsg("Expecting , delimiter", s, end - 1)) + + try: + nextchar = s[end] + if nextchar in _ws: + end += 1 + nextchar = s[end] + if nextchar in _ws: + end = _w(s, end + 1).end() + nextchar = s[end] + except IndexError: + nextchar = '' + + end += 1 + if nextchar != '"': + raise ValueError(errmsg("Expecting property name", s, end - 1)) + + if object_hook is not None: + pairs = object_hook(pairs) + return pairs, end + +def JSONArray((s, end), scan_once, _w=WHITESPACE.match, _ws=WHITESPACE_STR): + values = [] + nextchar = s[end:end + 1] + if nextchar in _ws: + end = _w(s, end + 1).end() + nextchar = s[end:end + 1] + # Look-ahead for trivial empty array + if nextchar == ']': + return values, end + 1 + _append = values.append + while True: + try: + value, end = scan_once(s, end) + except StopIteration: + raise ValueError(errmsg("Expecting object", s, end)) + _append(value) + nextchar = s[end:end + 1] + if nextchar in _ws: + end = _w(s, end + 1).end() + nextchar = s[end:end + 1] + end += 1 + if nextchar == ']': + break + elif nextchar != ',': + raise ValueError(errmsg("Expecting , delimiter", s, end)) + + try: + if s[end] in _ws: + end += 1 + if s[end] in _ws: + end = _w(s, end + 1).end() + except IndexError: + pass + + return values, end + +class JSONDecoder(object): + """Simple JSON <http://json.org> decoder + + Performs the following translations in decoding by default: + + +---------------+-------------------+ + | JSON | Python | + +===============+===================+ + | object | dict | + +---------------+-------------------+ + | array | list | + +---------------+-------------------+ + | string | unicode | + +---------------+-------------------+ + | number (int) | int, long | + +---------------+-------------------+ + | number (real) | float | + +---------------+-------------------+ + | true | True | + +---------------+-------------------+ + | false | False | + +---------------+-------------------+ + | null | None | + +---------------+-------------------+ + + It also understands ``NaN``, ``Infinity``, and ``-Infinity`` as + their corresponding ``float`` values, which is outside the JSON spec. + + """ + + def __init__(self, encoding=None, object_hook=None, parse_float=None, + parse_int=None, parse_constant=None, strict=True): + """``encoding`` determines the encoding used to interpret any ``str`` + objects decoded by this instance (utf-8 by default). It has no + effect when decoding ``unicode`` objects. + + Note that currently only encodings that are a superset of ASCII work, + strings of other encodings should be passed in as ``unicode``. + + ``object_hook``, if specified, will be called with the result + of every JSON object decoded and its return value will be used in + place of the given ``dict``. This can be used to provide custom + deserializations (e.g. to support JSON-RPC class hinting). + + ``parse_float``, if specified, will be called with the string + of every JSON float to be decoded. By default this is equivalent to + float(num_str). This can be used to use another datatype or parser + for JSON floats (e.g. decimal.Decimal). + + ``parse_int``, if specified, will be called with the string + of every JSON int to be decoded. By default this is equivalent to + int(num_str). This can be used to use another datatype or parser + for JSON integers (e.g. float). + + ``parse_constant``, if specified, will be called with one of the + following strings: -Infinity, Infinity, NaN. + This can be used to raise an exception if invalid JSON numbers + are encountered. + + """ + self.encoding = encoding + self.object_hook = object_hook + self.parse_float = parse_float or float + self.parse_int = parse_int or int + self.parse_constant = parse_constant or _CONSTANTS.__getitem__ + self.strict = strict + self.parse_object = JSONObject + self.parse_array = JSONArray + self.parse_string = scanstring + self.scan_once = make_scanner(self) + + def decode(self, s, _w=WHITESPACE.match): + """Return the Python representation of ``s`` (a ``str`` or ``unicode`` + instance containing a JSON document) + + """ + obj, end = self.raw_decode(s, idx=_w(s, 0).end()) + end = _w(s, end).end() + if end != len(s): + raise ValueError(errmsg("Extra data", s, end, len(s))) + return obj + + def raw_decode(self, s, idx=0): + """Decode a JSON document from ``s`` (a ``str`` or ``unicode`` beginning + with a JSON document) and return a 2-tuple of the Python + representation and the index in ``s`` where the document ended. + + This can be used to decode a JSON document from a string that may + have extraneous data at the end. + + """ + try: + obj, end = self.scan_once(s, idx) + except StopIteration: + raise ValueError("No JSON object could be decoded") + return obj, end diff --git a/lib/chef/provider/package/yum/simplejson/decoder.pyc b/lib/chef/provider/package/yum/simplejson/decoder.pyc Binary files differnew file mode 100644 index 0000000000..d402901870 --- /dev/null +++ b/lib/chef/provider/package/yum/simplejson/decoder.pyc diff --git a/lib/chef/provider/package/yum/simplejson/encoder.py b/lib/chef/provider/package/yum/simplejson/encoder.py new file mode 100644 index 0000000000..cf58290366 --- /dev/null +++ b/lib/chef/provider/package/yum/simplejson/encoder.py @@ -0,0 +1,440 @@ +"""Implementation of JSONEncoder +""" +import re + +try: + from simplejson._speedups import encode_basestring_ascii as c_encode_basestring_ascii +except ImportError: + c_encode_basestring_ascii = None +try: + from simplejson._speedups import make_encoder as c_make_encoder +except ImportError: + c_make_encoder = None + +ESCAPE = re.compile(r'[\x00-\x1f\\"\b\f\n\r\t]') +ESCAPE_ASCII = re.compile(r'([\\"]|[^\ -~])') +HAS_UTF8 = re.compile(r'[\x80-\xff]') +ESCAPE_DCT = { + '\\': '\\\\', + '"': '\\"', + '\b': '\\b', + '\f': '\\f', + '\n': '\\n', + '\r': '\\r', + '\t': '\\t', +} +for i in range(0x20): + #ESCAPE_DCT.setdefault(chr(i), '\\u{0:04x}'.format(i)) + ESCAPE_DCT.setdefault(chr(i), '\\u%04x' % (i,)) + +# Assume this produces an infinity on all machines (probably not guaranteed) +INFINITY = float('1e66666') +FLOAT_REPR = repr + +def encode_basestring(s): + """Return a JSON representation of a Python string + + """ + def replace(match): + return ESCAPE_DCT[match.group(0)] + return '"' + ESCAPE.sub(replace, s) + '"' + + +def py_encode_basestring_ascii(s): + """Return an ASCII-only JSON representation of a Python string + + """ + if isinstance(s, str) and HAS_UTF8.search(s) is not None: + s = s.decode('utf-8') + def replace(match): + s = match.group(0) + try: + return ESCAPE_DCT[s] + except KeyError: + n = ord(s) + if n < 0x10000: + #return '\\u{0:04x}'.format(n) + return '\\u%04x' % (n,) + else: + # surrogate pair + n -= 0x10000 + s1 = 0xd800 | ((n >> 10) & 0x3ff) + s2 = 0xdc00 | (n & 0x3ff) + #return '\\u{0:04x}\\u{1:04x}'.format(s1, s2) + return '\\u%04x\\u%04x' % (s1, s2) + return '"' + str(ESCAPE_ASCII.sub(replace, s)) + '"' + + +encode_basestring_ascii = c_encode_basestring_ascii or py_encode_basestring_ascii + +class JSONEncoder(object): + """Extensible JSON <http://json.org> encoder for Python data structures. + + Supports the following objects and types by default: + + +-------------------+---------------+ + | Python | JSON | + +===================+===============+ + | dict | object | + +-------------------+---------------+ + | list, tuple | array | + +-------------------+---------------+ + | str, unicode | string | + +-------------------+---------------+ + | int, long, float | number | + +-------------------+---------------+ + | True | true | + +-------------------+---------------+ + | False | false | + +-------------------+---------------+ + | None | null | + +-------------------+---------------+ + + To extend this to recognize other objects, subclass and implement a + ``.default()`` method with another method that returns a serializable + object for ``o`` if possible, otherwise it should call the superclass + implementation (to raise ``TypeError``). + + """ + item_separator = ', ' + key_separator = ': ' + def __init__(self, skipkeys=False, ensure_ascii=True, + check_circular=True, allow_nan=True, sort_keys=False, + indent=None, separators=None, encoding='utf-8', default=None): + """Constructor for JSONEncoder, with sensible defaults. + + If skipkeys is false, then it is a TypeError to attempt + encoding of keys that are not str, int, long, float or None. If + skipkeys is True, such items are simply skipped. + + If ensure_ascii is true, the output is guaranteed to be str + objects with all incoming unicode characters escaped. If + ensure_ascii is false, the output will be unicode object. + + If check_circular is true, then lists, dicts, and custom encoded + objects will be checked for circular references during encoding to + prevent an infinite recursion (which would cause an OverflowError). + Otherwise, no such check takes place. + + If allow_nan is true, then NaN, Infinity, and -Infinity will be + encoded as such. This behavior is not JSON specification compliant, + but is consistent with most JavaScript based encoders and decoders. + Otherwise, it will be a ValueError to encode such floats. + + If sort_keys is true, then the output of dictionaries will be + sorted by key; this is useful for regression tests to ensure + that JSON serializations can be compared on a day-to-day basis. + + If indent is a non-negative integer, then JSON array + elements and object members will be pretty-printed with that + indent level. An indent level of 0 will only insert newlines. + None is the most compact representation. + + If specified, separators should be a (item_separator, key_separator) + tuple. The default is (', ', ': '). To get the most compact JSON + representation you should specify (',', ':') to eliminate whitespace. + + If specified, default is a function that gets called for objects + that can't otherwise be serialized. It should return a JSON encodable + version of the object or raise a ``TypeError``. + + If encoding is not None, then all input strings will be + transformed into unicode using that encoding prior to JSON-encoding. + The default is UTF-8. + + """ + + self.skipkeys = skipkeys + self.ensure_ascii = ensure_ascii + self.check_circular = check_circular + self.allow_nan = allow_nan + self.sort_keys = sort_keys + self.indent = indent + if separators is not None: + self.item_separator, self.key_separator = separators + if default is not None: + self.default = default + self.encoding = encoding + + def default(self, o): + """Implement this method in a subclass such that it returns + a serializable object for ``o``, or calls the base implementation + (to raise a ``TypeError``). + + For example, to support arbitrary iterators, you could + implement default like this:: + + def default(self, o): + try: + iterable = iter(o) + except TypeError: + pass + else: + return list(iterable) + return JSONEncoder.default(self, o) + + """ + raise TypeError(repr(o) + " is not JSON serializable") + + def encode(self, o): + """Return a JSON string representation of a Python data structure. + + >>> JSONEncoder().encode({"foo": ["bar", "baz"]}) + '{"foo": ["bar", "baz"]}' + + """ + # This is for extremely simple cases and benchmarks. + if isinstance(o, basestring): + if isinstance(o, str): + _encoding = self.encoding + if (_encoding is not None + and not (_encoding == 'utf-8')): + o = o.decode(_encoding) + if self.ensure_ascii: + return encode_basestring_ascii(o) + else: + return encode_basestring(o) + # This doesn't pass the iterator directly to ''.join() because the + # exceptions aren't as detailed. The list call should be roughly + # equivalent to the PySequence_Fast that ''.join() would do. + chunks = self.iterencode(o, _one_shot=True) + if not isinstance(chunks, (list, tuple)): + chunks = list(chunks) + return ''.join(chunks) + + def iterencode(self, o, _one_shot=False): + """Encode the given object and yield each string + representation as available. + + For example:: + + for chunk in JSONEncoder().iterencode(bigobject): + mysocket.write(chunk) + + """ + if self.check_circular: + markers = {} + else: + markers = None + if self.ensure_ascii: + _encoder = encode_basestring_ascii + else: + _encoder = encode_basestring + if self.encoding != 'utf-8': + def _encoder(o, _orig_encoder=_encoder, _encoding=self.encoding): + if isinstance(o, str): + o = o.decode(_encoding) + return _orig_encoder(o) + + def floatstr(o, allow_nan=self.allow_nan, _repr=FLOAT_REPR, _inf=INFINITY, _neginf=-INFINITY): + # Check for specials. Note that this type of test is processor- and/or + # platform-specific, so do tests which don't depend on the internals. + + if o != o: + text = 'NaN' + elif o == _inf: + text = 'Infinity' + elif o == _neginf: + text = '-Infinity' + else: + return _repr(o) + + if not allow_nan: + raise ValueError( + "Out of range float values are not JSON compliant: " + + repr(o)) + + return text + + + if _one_shot and c_make_encoder is not None and not self.indent and not self.sort_keys: + _iterencode = c_make_encoder( + markers, self.default, _encoder, self.indent, + self.key_separator, self.item_separator, self.sort_keys, + self.skipkeys, self.allow_nan) + else: + _iterencode = _make_iterencode( + markers, self.default, _encoder, self.indent, floatstr, + self.key_separator, self.item_separator, self.sort_keys, + self.skipkeys, _one_shot) + return _iterencode(o, 0) + +def _make_iterencode(markers, _default, _encoder, _indent, _floatstr, _key_separator, _item_separator, _sort_keys, _skipkeys, _one_shot, + ## HACK: hand-optimized bytecode; turn globals into locals + False=False, + True=True, + ValueError=ValueError, + basestring=basestring, + dict=dict, + float=float, + id=id, + int=int, + isinstance=isinstance, + list=list, + long=long, + str=str, + tuple=tuple, + ): + + def _iterencode_list(lst, _current_indent_level): + if not lst: + yield '[]' + return + if markers is not None: + markerid = id(lst) + if markerid in markers: + raise ValueError("Circular reference detected") + markers[markerid] = lst + buf = '[' + if _indent is not None: + _current_indent_level += 1 + newline_indent = '\n' + (' ' * (_indent * _current_indent_level)) + separator = _item_separator + newline_indent + buf += newline_indent + else: + newline_indent = None + separator = _item_separator + first = True + for value in lst: + if first: + first = False + else: + buf = separator + if isinstance(value, basestring): + yield buf + _encoder(value) + elif value is None: + yield buf + 'null' + elif value is True: + yield buf + 'true' + elif value is False: + yield buf + 'false' + elif isinstance(value, (int, long)): + yield buf + str(value) + elif isinstance(value, float): + yield buf + _floatstr(value) + else: + yield buf + if isinstance(value, (list, tuple)): + chunks = _iterencode_list(value, _current_indent_level) + elif isinstance(value, dict): + chunks = _iterencode_dict(value, _current_indent_level) + else: + chunks = _iterencode(value, _current_indent_level) + for chunk in chunks: + yield chunk + if newline_indent is not None: + _current_indent_level -= 1 + yield '\n' + (' ' * (_indent * _current_indent_level)) + yield ']' + if markers is not None: + del markers[markerid] + + def _iterencode_dict(dct, _current_indent_level): + if not dct: + yield '{}' + return + if markers is not None: + markerid = id(dct) + if markerid in markers: + raise ValueError("Circular reference detected") + markers[markerid] = dct + yield '{' + if _indent is not None: + _current_indent_level += 1 + newline_indent = '\n' + (' ' * (_indent * _current_indent_level)) + item_separator = _item_separator + newline_indent + yield newline_indent + else: + newline_indent = None + item_separator = _item_separator + first = True + if _sort_keys: + items = dct.items() + items.sort(key=lambda kv: kv[0]) + else: + items = dct.iteritems() + for key, value in items: + if isinstance(key, basestring): + pass + # JavaScript is weakly typed for these, so it makes sense to + # also allow them. Many encoders seem to do something like this. + elif isinstance(key, float): + key = _floatstr(key) + elif key is True: + key = 'true' + elif key is False: + key = 'false' + elif key is None: + key = 'null' + elif isinstance(key, (int, long)): + key = str(key) + elif _skipkeys: + continue + else: + raise TypeError("key " + repr(key) + " is not a string") + if first: + first = False + else: + yield item_separator + yield _encoder(key) + yield _key_separator + if isinstance(value, basestring): + yield _encoder(value) + elif value is None: + yield 'null' + elif value is True: + yield 'true' + elif value is False: + yield 'false' + elif isinstance(value, (int, long)): + yield str(value) + elif isinstance(value, float): + yield _floatstr(value) + else: + if isinstance(value, (list, tuple)): + chunks = _iterencode_list(value, _current_indent_level) + elif isinstance(value, dict): + chunks = _iterencode_dict(value, _current_indent_level) + else: + chunks = _iterencode(value, _current_indent_level) + for chunk in chunks: + yield chunk + if newline_indent is not None: + _current_indent_level -= 1 + yield '\n' + (' ' * (_indent * _current_indent_level)) + yield '}' + if markers is not None: + del markers[markerid] + + def _iterencode(o, _current_indent_level): + if isinstance(o, basestring): + yield _encoder(o) + elif o is None: + yield 'null' + elif o is True: + yield 'true' + elif o is False: + yield 'false' + elif isinstance(o, (int, long)): + yield str(o) + elif isinstance(o, float): + yield _floatstr(o) + elif isinstance(o, (list, tuple)): + for chunk in _iterencode_list(o, _current_indent_level): + yield chunk + elif isinstance(o, dict): + for chunk in _iterencode_dict(o, _current_indent_level): + yield chunk + else: + if markers is not None: + markerid = id(o) + if markerid in markers: + raise ValueError("Circular reference detected") + markers[markerid] = o + o = _default(o) + for chunk in _iterencode(o, _current_indent_level): + yield chunk + if markers is not None: + del markers[markerid] + + return _iterencode diff --git a/lib/chef/provider/package/yum/simplejson/encoder.pyc b/lib/chef/provider/package/yum/simplejson/encoder.pyc Binary files differnew file mode 100644 index 0000000000..207bce5cfb --- /dev/null +++ b/lib/chef/provider/package/yum/simplejson/encoder.pyc diff --git a/lib/chef/provider/package/yum/simplejson/scanner.py b/lib/chef/provider/package/yum/simplejson/scanner.py new file mode 100644 index 0000000000..adbc6ec979 --- /dev/null +++ b/lib/chef/provider/package/yum/simplejson/scanner.py @@ -0,0 +1,65 @@ +"""JSON token scanner +""" +import re +try: + from simplejson._speedups import make_scanner as c_make_scanner +except ImportError: + c_make_scanner = None + +__all__ = ['make_scanner'] + +NUMBER_RE = re.compile( + r'(-?(?:0|[1-9]\d*))(\.\d+)?([eE][-+]?\d+)?', + (re.VERBOSE | re.MULTILINE | re.DOTALL)) + +def py_make_scanner(context): + parse_object = context.parse_object + parse_array = context.parse_array + parse_string = context.parse_string + match_number = NUMBER_RE.match + encoding = context.encoding + strict = context.strict + parse_float = context.parse_float + parse_int = context.parse_int + parse_constant = context.parse_constant + object_hook = context.object_hook + + def _scan_once(string, idx): + try: + nextchar = string[idx] + except IndexError: + raise StopIteration + + if nextchar == '"': + return parse_string(string, idx + 1, encoding, strict) + elif nextchar == '{': + return parse_object((string, idx + 1), encoding, strict, _scan_once, object_hook) + elif nextchar == '[': + return parse_array((string, idx + 1), _scan_once) + elif nextchar == 'n' and string[idx:idx + 4] == 'null': + return None, idx + 4 + elif nextchar == 't' and string[idx:idx + 4] == 'true': + return True, idx + 4 + elif nextchar == 'f' and string[idx:idx + 5] == 'false': + return False, idx + 5 + + m = match_number(string, idx) + if m is not None: + integer, frac, exp = m.groups() + if frac or exp: + res = parse_float(integer + (frac or '') + (exp or '')) + else: + res = parse_int(integer) + return res, m.end() + elif nextchar == 'N' and string[idx:idx + 3] == 'NaN': + return parse_constant('NaN'), idx + 3 + elif nextchar == 'I' and string[idx:idx + 8] == 'Infinity': + return parse_constant('Infinity'), idx + 8 + elif nextchar == '-' and string[idx:idx + 9] == '-Infinity': + return parse_constant('-Infinity'), idx + 9 + else: + raise StopIteration + + return _scan_once + +make_scanner = c_make_scanner or py_make_scanner diff --git a/lib/chef/provider/package/yum/simplejson/scanner.pyc b/lib/chef/provider/package/yum/simplejson/scanner.pyc Binary files differnew file mode 100644 index 0000000000..12df070e44 --- /dev/null +++ b/lib/chef/provider/package/yum/simplejson/scanner.pyc diff --git a/lib/chef/provider/package/yum/simplejson/tool.py b/lib/chef/provider/package/yum/simplejson/tool.py new file mode 100644 index 0000000000..90443317b2 --- /dev/null +++ b/lib/chef/provider/package/yum/simplejson/tool.py @@ -0,0 +1,37 @@ +r"""Command-line tool to validate and pretty-print JSON + +Usage:: + + $ echo '{"json":"obj"}' | python -m simplejson.tool + { + "json": "obj" + } + $ echo '{ 1.2:3.4}' | python -m simplejson.tool + Expecting property name: line 1 column 2 (char 2) + +""" +import sys +import simplejson + +def main(): + if len(sys.argv) == 1: + infile = sys.stdin + outfile = sys.stdout + elif len(sys.argv) == 2: + infile = open(sys.argv[1], 'rb') + outfile = sys.stdout + elif len(sys.argv) == 3: + infile = open(sys.argv[1], 'rb') + outfile = open(sys.argv[2], 'wb') + else: + raise SystemExit(sys.argv[0] + " [infile [outfile]]") + try: + obj = simplejson.load(infile) + except ValueError, e: + raise SystemExit(e) + simplejson.dump(obj, outfile, sort_keys=True, indent=4) + outfile.write('\n') + + +if __name__ == '__main__': + main() diff --git a/lib/chef/provider/package/yum/version.rb b/lib/chef/provider/package/yum/version.rb new file mode 100644 index 0000000000..b19f52fe09 --- /dev/null +++ b/lib/chef/provider/package/yum/version.rb @@ -0,0 +1,56 @@ +# +# Copyright:: Copyright 2016-2018, 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. +# + +class Chef + class Provider + class Package + class Yum < Chef::Provider::Package + + # helper class to assist in passing around name/version/arch triples + class Version + attr_accessor :name + attr_accessor :version + attr_accessor :arch + + def initialize(name, version, arch) + @name = name + @version = version + @arch = arch + end + + def to_s + "#{name}-#{version}.#{arch}" unless version.nil? + end + + def version_with_arch + "#{version}.#{arch}" unless version.nil? + end + + def matches_name_and_arch?(other) + other.version == version && other.arch == arch + end + + def ==(other) + name == other.name && version == other.version && arch == other.arch + end + + alias eql? == + end + end + end + end +end 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..fa0930109f --- /dev/null +++ b/lib/chef/provider/package/yum/yum_cache.rb @@ -0,0 +1,93 @@ + +# Author:: Adam Jacob (<adam@chef.io>) +# Copyright:: Copyright 2008-2018, 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/yum/python_helper" +require "chef/provider/package" +require "singleton" + +# +# These are largely historical APIs, the YumCache object no longer exists and this is a +# fascade over the python helper class. It should be considered deprecated-lite and +# no new APIs should be added and should be added to the python_helper instead. +# + +class Chef + class Provider + class Package + class Yum < Chef::Provider::Package + class YumCache + include Singleton + + def refresh + python_helper.restart + end + + def reload + python_helper.restart + end + + def reload_installed + python_helper.restart + end + + def reload_provides + python_helper.restart + end + + def reset + python_helper.restart + end + + def reset_installed + python_helper.restart + end + + def available_version(name, arch = nil) + p = python_helper.package_query(:whatavailable, name, arch: arch) + "#{p.version}.#{p.arch}" unless p.version.nil? + end + + def installed_version(name, arch = nil) + p = python_helper.package_query(:whatinstalled, name, arch: arch) + "#{p.version}.#{p.arch}" unless p.version.nil? + end + + def package_available?(name, arch = nil) + p = python_helper.package_query(:whatavailable, name, arch: arch) + !p.version.nil? + end + + # NOTE that it is the responsibility of the python_helper to get these APIs correct and + # we do not do any validation here that the e.g. version or arch matches the requested value + # (because the bigger issue there is a buggy+broken python_helper -- so don't try to fix those + # kinds of bugs here) + def version_available?(name, version, arch = nil) + p = python_helper.package_query(:whatavailable, name, version: version, arch: arch) + !p.version.nil? + end + + # @api private + def python_helper + @python_helper ||= PythonHelper.instance + end + + end # YumCache + end + end + end +end diff --git a/lib/chef/provider/package/yum/yum_helper.py b/lib/chef/provider/package/yum/yum_helper.py new file mode 100644 index 0000000000..d2c04c72db --- /dev/null +++ b/lib/chef/provider/package/yum/yum_helper.py @@ -0,0 +1,210 @@ +#!/usr/bin/env python3 +# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 + +# +# NOTE: this actually needs to run under python2.4 and centos 5.x through python3 and centos 7.x +# please manually test changes on centos5 boxes or you will almost certainly break things. +# + +import sys +import yum +import signal +import os +sys.path.append(os.path.join(os.path.dirname(__file__), '..', 'simplejson')) +try: import json +except ImportError: import simplejson as json +import re +from rpmUtils.miscutils import stringToVersion,compareEVR +from rpmUtils.arch import getBaseArch, getArchList + + +try: from yum.misc import string_to_prco_tuple +except ImportError: + # RHEL5 compat + def string_to_prco_tuple(prcoString): + prco_split = prcoString.split() + n, f, v = prco_split + (prco_e, prco_v, prco_r) = stringToVersion(v) + return (n, f, (prco_e, prco_v, prco_r)) + +# hack to work around https://github.com/chef/chef/issues/7126 +# see https://bugzilla.redhat.com/show_bug.cgi?id=1396248 +if not hasattr(yum.packages.FakeRepository, 'compare_providers_priority'): + yum.packages.FakeRepository.compare_providers_priority = 99 + +base = None + +def get_base(): + global base + if base is None: + base = yum.YumBase() + setup_exit_handler() + return base + +def versioncompare(versions): + arch_list = getArchList() + candidate_arch1 = versions[0].split(".")[-1] + candidate_arch2 = versions[1].split(".")[-1] + + # The first version number passed to this method is always a valid nevra (the current version) + # If the second version number looks like it does not contain a valid arch + # then we'll chop the arch component (assuming it *is* a valid one) from the first version string + # so we're only comparing the evr portions. + if (candidate_arch2 not in arch_list) and (candidate_arch1 in arch_list): + final_version1 = versions[0].replace("." + candidate_arch1,"") + else: + final_version1 = versions[0] + + final_version2 = versions[1] + + (e1, v1, r1) = stringToVersion(final_version1) + (e2, v2, r2) = stringToVersion(final_version2) + + evr_comparison = compareEVR((e1, v1, r1), (e2, v2, r2)) + outpipe.write("%(e)s\n" % { 'e': evr_comparison }) + outpipe.flush() + +def install_only_packages(name): + base = get_base() + if name in base.conf.installonlypkgs: + outpipe.write('True') + else: + outpipe.write('False') + outpipe.flush() + +# python2.4 / centos5 compat +try: + any +except NameError: + def any(s): + for v in s: + if v: + return True + return False + +def query(command): + base = get_base() + + enabled_repos = base.repos.listEnabled() + + # Handle any repocontrols passed in with our options + + if 'repos' in command: + for repo in command['repos']: + if 'enable' in repo: + base.repos.enableRepo(repo['enable']) + if 'disable' in repo: + base.repos.disableRepo(repo['disable']) + + args = { 'name': command['provides'] } + do_nevra = False + if 'epoch' in command: + args['epoch'] = command['epoch'] + do_nevra = True + if 'version' in command: + args['ver'] = command['version'] + do_nevra = True + if 'release' in command: + args['rel'] = command['release'] + do_nevra = True + if 'arch' in command: + desired_arch = command['arch'] + args['arch'] = command['arch'] + do_nevra = True + else: + desired_arch = getBaseArch() + + obj = None + if command['action'] == "whatinstalled": + obj = base.rpmdb + else: + obj = base.pkgSack + + # if we are given "name == 1.2.3" then we must use the getProvides() API. + # - this means that we ignore arch and version properties when given prco tuples as a package_name + # - in order to fix this, something would have to happen where getProvides was called first and + # then the result was searchNevra'd. please be extremely careful if attempting to fix that + # since searchNevra does not support prco tuples. + if any(elem in command['provides'] for elem in r"<=>"): + # handles flags (<, >, =, etc) and versions, but no wildcareds + pkgs = obj.getProvides(*string_to_prco_tuple(command['provides'])) + elif do_nevra: + # now if we're given version or arch properties explicitly, then we do a SearchNevra. + # - this means that wildcard version in the package_name with an arch property will not work correctly + # - again don't try to fix this just by pushing bugs around in the code, you would need to call + # returnPackages and searchProvides and then apply the Nevra filters to those results. + pkgs = obj.searchNevra(**args) + if (command['action'] == "whatinstalled") and (not pkgs): + pkgs = obj.searchNevra(name=args['name'], arch=desired_arch) + else: + pats = [command['provides']] + pkgs = obj.returnPackages(patterns=pats) + + if not pkgs: + # handles wildcards + pkgs = obj.searchProvides(command['provides']) + + if not pkgs: + outpipe.write(command['provides'].split().pop(0)+' nil nil\n') + outpipe.flush() + else: + # make sure we picked the package with the highest version + pkgs = base.bestPackagesFromList(pkgs,single_name=True) + pkg = pkgs.pop(0) + outpipe.write("%(n)s %(e)s:%(v)s-%(r)s %(a)s\n" % { 'n': pkg.name, 'e': pkg.epoch, 'v': pkg.version, 'r': pkg.release, 'a': pkg.arch }) + outpipe.flush() + + # Reset any repos we were passed in enablerepo/disablerepo to the original state in enabled_repos + if 'repos' in command: + for repo in command['repos']: + if 'enable' in repo: + if base.repos.getRepo(repo['enable']) not in enabled_repos: + base.repos.disableRepo(repo['enable']) + if 'disable' in repo: + if base.repos.getRepo(repo['disable']) in enabled_repos: + base.repos.enableRepo(repo['disable']) + +# the design of this helper is that it should try to be 'brittle' and fail hard and exit in order +# to keep process tables clean. additional error handling should probably be added to the retry loop +# on the ruby side. +def exit_handler(signal, frame): + base.closeRpmDB() + sys.exit(0) + +def setup_exit_handler(): + signal.signal(signal.SIGINT, exit_handler) + signal.signal(signal.SIGHUP, exit_handler) + signal.signal(signal.SIGPIPE, exit_handler) + signal.signal(signal.SIGQUIT, exit_handler) + +if len(sys.argv) < 3: + inpipe = sys.stdin + outpipe = sys.stdout +else: + inpipe = os.fdopen(int(sys.argv[1]), "r") + outpipe = os.fdopen(int(sys.argv[2]), "w") + +while 1: + # kill self if we get orphaned (tragic) + ppid = os.getppid() + if ppid == 1: + sys.exit(0) + setup_exit_handler() + line = inpipe.readline() + + try: + command = json.loads(line) + except ValueError, e: + base.closeRpmDB() + sys.exit(0) + + if command['action'] == "whatinstalled": + query(command) + elif command['action'] == "whatavailable": + query(command) + elif command['action'] == "versioncompare": + versioncompare(command['versions']) + elif command['action'] == "installonlypkgs": + install_only_packages(command['package']) + else: + raise RuntimeError("bad command") diff --git a/lib/chef/provider/package/zypper.rb b/lib/chef/provider/package/zypper.rb index 5ee1dbea8e..c2638fbfc1 100644 --- a/lib/chef/provider/package/zypper.rb +++ b/lib/chef/provider/package/zypper.rb @@ -2,7 +2,7 @@ # # Authors:: Adam Jacob (<adam@chef.io>) # Ionuț Arțăriși (<iartarisi@suse.cz>) -# Copyright:: Copyright 2008-2016, Chef Software, Inc. +# Copyright:: Copyright 2008-2017, Chef Software Inc. # Copyright 2013-2016, SUSE Linux GmbH # License:: Apache License, Version 2.0 # @@ -29,24 +29,24 @@ class Chef use_multipackage_api provides :package, platform_family: "suse" - provides :zypper_package, os: "linux" + provides :zypper_package def get_versions(package_name) candidate_version = current_version = nil is_installed = false - Chef::Log.debug("#{new_resource} checking zypper") - status = shell_out_with_timeout!("zypper --non-interactive info #{package_name}") + logger.trace("#{new_resource} checking zypper") + status = shell_out_compact_timeout!("zypper", "--non-interactive", "info", package_name) status.stdout.each_line do |line| case line - when /^Version: (.+)$/ - candidate_version = $1 - Chef::Log.debug("#{new_resource} version #{$1}") - when /^Installed: Yes$/ + when /^Version *: (.+) *$/ + candidate_version = $1.strip + logger.trace("#{new_resource} version #{candidate_version}") + when /^Installed *: Yes.*$/ # http://rubular.com/r/9StcAMjOn6 is_installed = true - Chef::Log.debug("#{new_resource} is installed") - when /^Status: out-of-date \(version (.+) installed\)$/ - current_version = $1 - Chef::Log.debug("#{new_resource} out of date version #{$1}") + logger.trace("#{new_resource} is installed") + when /^Status *: out-of-date \(version (.+) installed\) *$/ + current_version = $1.strip + logger.trace("#{new_resource} out of date version #{current_version}") end end current_version = candidate_version if is_installed @@ -75,6 +75,24 @@ class Chef end end + def packages_all_locked?(names, versions) + names.all? { |n| locked_packages.include? n } + end + + def packages_all_unlocked?(names, versions) + names.all? { |n| !locked_packages.include? n } + end + + def locked_packages + @locked_packages ||= + begin + locked = shell_out_compact_timeout!("zypper", "locks") + locked.stdout.each_line.map do |line| + line.split("|").shift(2).last.strip + end + end + end + def load_current_resource @current_resource = Chef::Resource::ZypperPackage.new(new_resource.name) current_resource.package_name(new_resource.package_name) @@ -91,7 +109,7 @@ class Chef end def install_package(name, version) - zypper_package("install --auto-agree-with-licenses", name, version) + zypper_package("install", *options, "--auto-agree-with-licenses", allow_downgrade, name, version) end def upgrade_package(name, version) @@ -100,11 +118,19 @@ class Chef end def remove_package(name, version) - zypper_package("remove", name, version) + zypper_package("remove", *options, name, version) end def purge_package(name, version) - zypper_package("remove --clean-deps", name, version) + zypper_package("remove", *options, "--clean-deps", name, version) + end + + def lock_package(name, version) + zypper_package("addlock", *options, name, version) + end + + def unlock_package(name, version) + zypper_package("removelock", *options, name, version) end private @@ -115,27 +141,21 @@ class Chef end end - def zypper_package(command, names, versions) + def zypper_package(command, *options, names, versions) zipped_names = zip(names, versions) if zypper_version < 1.0 - shell_out_with_timeout!(a_to_s("zypper", gpg_checks, command, "-y", names)) + shell_out_compact_timeout!("zypper", gpg_checks, command, *options, "-y", names) else - shell_out_with_timeout!(a_to_s("zypper --non-interactive", gpg_checks, command, zipped_names)) + shell_out_compact_timeout!("zypper", "--non-interactive", gpg_checks, command, *options, zipped_names) end end - def gpg_checks() - case Chef::Config[:zypper_check_gpg] - when true - "" - when false - "--no-gpg-checks" - when nil - Chef::Log.warn("Chef::Config[:zypper_check_gpg] was not set. " + - "All packages will be installed without gpg signature checks. " + - "This is a security hazard.") - "--no-gpg-checks" - end + def gpg_checks + "--no-gpg-checks" unless new_resource.gpg_check + end + + def allow_downgrade + "--oldpackage" if new_resource.allow_downgrade end end end diff --git a/lib/chef/provider/powershell_script.rb b/lib/chef/provider/powershell_script.rb index 6365f6a171..5af73b8b69 100644 --- a/lib/chef/provider/powershell_script.rb +++ b/lib/chef/provider/powershell_script.rb @@ -23,7 +23,7 @@ class Chef class Provider class PowershellScript < Chef::Provider::WindowsScript - provides :powershell_script, os: "windows" + provides :powershell_script def initialize(new_resource, run_context) super(new_resource, run_context, ".ps1") @@ -36,7 +36,7 @@ class Chef end def command - basepath = is_forced_32bit ? wow64_directory : run_context.node.kernel.os_info.system_directory + basepath = is_forced_32bit ? wow64_directory : run_context.node["kernel"]["os_info"]["system_directory"] # Powershell.exe is always in "v1.0" folder (for backwards compatibility) interpreter_path = Chef::Util::PathHelper.join(basepath, "WindowsPowerShell", "v1.0", interpreter) @@ -60,8 +60,8 @@ class Chef def flags interpreter_flags = [*default_interpreter_flags].join(" ") - if ! (@new_resource.flags.nil?) - interpreter_flags = [@new_resource.flags, interpreter_flags].join(" ") + if ! (new_resource.flags.nil?) + interpreter_flags = [new_resource.flags, interpreter_flags].join(" ") end interpreter_flags @@ -73,8 +73,8 @@ class Chef # special handling to cover common use cases. def add_exit_status_wrapper self.code = wrapper_script - Chef::Log.debug("powershell_script provider called with script code:\n\n#{@new_resource.code}\n") - Chef::Log.debug("powershell_script provider will execute transformed code:\n\n#{self.code}\n") + logger.trace("powershell_script provider called with script code:\n\n#{new_resource.code}\n") + logger.trace("powershell_script provider will execute transformed code:\n\n#{code}\n") end def validate_script_syntax! @@ -87,7 +87,7 @@ class Chef # actually running the script. user_code_wrapped_in_powershell_script_block = <<-EOH { - #{@new_resource.code} + #{new_resource.code} } EOH user_script_file.puts user_code_wrapped_in_powershell_script_block @@ -149,6 +149,14 @@ EOH <<-EOH # Chef Client wrapper for powershell_script resources +# In rare cases, such as when PowerShell is executed +# as an alternate user, the new-variable cmdlet is not +# available, so import it just in case +if ( get-module -ListAvailable Microsoft.PowerShell.Utility ) +{ + Import-Module Microsoft.PowerShell.Utility +} + # LASTEXITCODE can be uninitialized -- make it explictly 0 # to avoid incorrect detection of failure (non-zero) codes $global:LASTEXITCODE = 0 @@ -159,7 +167,7 @@ $global:LASTEXITCODE = 0 trap [Exception] {write-error ($_.Exception.Message);exit 1} # Variable state that should not be accessible to the user code -new-variable -name interpolatedexitcode -visibility private -value $#{@new_resource.convert_boolean_return} +new-variable -name interpolatedexitcode -visibility private -value $#{new_resource.convert_boolean_return} new-variable -name chefscriptresult -visibility private # Initialize a variable we use to capture $? inside a block @@ -168,7 +176,7 @@ $global:lastcmdlet = $null # Execute the user's code in a script block -- $chefscriptresult = { - #{@new_resource.code} + #{new_resource.code} # This assignment doesn't affect the block's return value $global:lastcmdlet = $? diff --git a/lib/chef/provider/reboot.rb b/lib/chef/provider/reboot.rb index 34eee9236d..f054af0567 100644 --- a/lib/chef/provider/reboot.rb +++ b/lib/chef/provider/reboot.rb @@ -21,39 +21,47 @@ require "chef/provider" class Chef class Provider + # Use the reboot resource to reboot a node, a necessary step with some + # installations on certain platforms. This resource is supported for use on + # the Microsoft Windows, macOS, and Linux platforms. + # + # In using this resource via notifications, it's important to *only* use + # immediate notifications. Delayed notifications produce unintuitive and + # probably undesired results. + # + # @since 12.0.0 class Reboot < Chef::Provider provides :reboot - def whyrun_supported? - true - end - + # @return [void] def load_current_resource - @current_resource ||= Chef::Resource::Reboot.new(@new_resource.name) - @current_resource.reason(@new_resource.reason) - @current_resource.delay_mins(@new_resource.delay_mins) - @current_resource + @current_resource ||= Chef::Resource::Reboot.new(new_resource.name) + current_resource.reason(new_resource.reason) + current_resource.delay_mins(new_resource.delay_mins) + current_resource end + # add a reboot to the node run_context + # @return [void] def request_reboot node.run_context.request_reboot( - :delay_mins => @new_resource.delay_mins, - :reason => @new_resource.reason, + :delay_mins => new_resource.delay_mins, + :reason => new_resource.reason, :timestamp => Time.now, - :requested_by => @new_resource.name + :requested_by => new_resource.name ) end def action_request_reboot converge_by("request a system reboot to occur if the run succeeds") do - Chef::Log.warn "Reboot requested:'#{@new_resource.name}'" + logger.warn "Reboot requested:'#{new_resource.name}'" request_reboot end end def action_reboot_now converge_by("rebooting the system immediately") do - Chef::Log.warn "Rebooting system immediately, requested by '#{@new_resource.name}'" + logger.warn "Rebooting system immediately, requested by '#{new_resource.name}'" request_reboot throw :end_client_run_early end @@ -61,7 +69,7 @@ class Chef def action_cancel converge_by("cancel any existing end-of-run reboot request") do - Chef::Log.warn "Reboot canceled: '#{@new_resource.name}'" + logger.warn "Reboot canceled: '#{new_resource.name}'" node.run_context.cancel_reboot end end diff --git a/lib/chef/provider/registry_key.rb b/lib/chef/provider/registry_key.rb index e516433ac8..a4a0465e11 100644 --- a/lib/chef/provider/registry_key.rb +++ b/lib/chef/provider/registry_key.rb @@ -2,7 +2,7 @@ # Author:: Prajakta Purohit (<prajakta@chef.io>) # Author:: Lamont Granquist (<lamont@chef.io>) # -# Copyright:: Copyright 2011-2016, Chef Software Inc. +# Copyright:: Copyright 2011-2017, Chef Software Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -35,10 +35,6 @@ class Chef include Chef::Mixin::Checksum - def whyrun_supported? - true - end - def running_on_windows! unless Chef::Platform.windows? raise Chef::Exceptions::Win32NotWindows, "Attempt to manipulate the windows registry on a non-windows node" @@ -47,19 +43,19 @@ class Chef def load_current_resource running_on_windows! - @current_resource ||= Chef::Resource::RegistryKey.new(@new_resource.key, run_context) - @current_resource.key(@new_resource.key) - @current_resource.architecture(@new_resource.architecture) - @current_resource.recursive(@new_resource.recursive) - if registry.key_exists?(@new_resource.key) - @current_resource.values(registry.get_values(@new_resource.key)) - end - values_to_hash(@current_resource.unscrubbed_values) - @current_resource + @current_resource ||= Chef::Resource::RegistryKey.new(new_resource.key, run_context) + current_resource.key(new_resource.key) + current_resource.architecture(new_resource.architecture) + current_resource.recursive(new_resource.recursive) + if registry.key_exists?(new_resource.key) + current_resource.values(registry.get_values(new_resource.key)) + end + values_to_hash(current_resource.unscrubbed_values) + current_resource end def registry - @registry ||= Chef::Win32::Registry.new(@run_context, @new_resource.architecture) + @registry ||= Chef::Win32::Registry.new(@run_context, new_resource.architecture) end def values_to_hash(values) @@ -70,72 +66,111 @@ class Chef end end + def key_missing?(values, name) + values.each do |v| + return true unless v.has_key?(name) + end + false + end + def define_resource_requirements requirements.assert(:create, :create_if_missing, :delete, :delete_key) do |a| - a.assertion { registry.hive_exists?(@new_resource.key) } - a.failure_message(Chef::Exceptions::Win32RegHiveMissing, "Hive #{@new_resource.key.split("\\").shift} does not exist") + a.assertion { registry.hive_exists?(new_resource.key) } + a.failure_message(Chef::Exceptions::Win32RegHiveMissing, "Hive #{new_resource.key.split("\\").shift} does not exist") end + requirements.assert(:create) do |a| - a.assertion { registry.key_exists?(@new_resource.key) } - a.whyrun("Key #{@new_resource.key} does not exist. Unless it would have been created before, attempt to modify its values would fail.") + a.assertion { registry.key_exists?(new_resource.key) } + a.whyrun("Key #{new_resource.key} does not exist. Unless it would have been created before, attempt to modify its values would fail.") end + requirements.assert(:create, :create_if_missing) do |a| - #If keys missing in the path and recursive == false - a.assertion { !registry.keys_missing?(@current_resource.key) || @new_resource.recursive } + # If keys missing in the path and recursive == false + a.assertion { !registry.keys_missing?(current_resource.key) || new_resource.recursive } a.failure_message(Chef::Exceptions::Win32RegNoRecursive, "Intermediate keys missing but recursive is set to false") - a.whyrun("Intermediate keys in #{@new_resource.key} do not exist. Unless they would have been created earlier, attempt to modify them would fail.") + a.whyrun("Intermediate keys in #{new_resource.key} do not exist. Unless they would have been created earlier, attempt to modify them would fail.") end + requirements.assert(:delete_key) do |a| - #If key to be deleted has subkeys but recurssive == false - a.assertion { !registry.key_exists?(@new_resource.key) || !registry.has_subkeys?(@new_resource.key) || @new_resource.recursive } - a.failure_message(Chef::Exceptions::Win32RegNoRecursive, "#{@new_resource.key} has subkeys but recursive is set to false.") - a.whyrun("#{@current_resource.key} has subkeys, but recursive is set to false. attempt to delete would fails unless subkeys were deleted prior to this action.") + # If key to be deleted has subkeys but recurssive == false + a.assertion { !registry.key_exists?(new_resource.key) || !registry.has_subkeys?(new_resource.key) || new_resource.recursive } + a.failure_message(Chef::Exceptions::Win32RegNoRecursive, "#{new_resource.key} has subkeys but recursive is set to false.") + a.whyrun("#{current_resource.key} has subkeys, but recursive is set to false. attempt to delete would fails unless subkeys were deleted prior to this action.") + end + + requirements.assert(:create, :create_if_missing) do |a| + # If type key missing in the RegistryKey values hash + a.assertion { !key_missing?(new_resource.values, :type) } + a.failure_message(Chef::Exceptions::RegKeyValuesTypeMissing, "Missing type key in RegistryKey values hash") + a.whyrun("Type key does not exist. Attempt would fail unless the complete values hash containing all the keys does not exist for registry_key resource's create action.") + end + + requirements.assert(:create, :create_if_missing) do |a| + # If data key missing in the RegistryKey values hash + a.assertion { !key_missing?(new_resource.values, :data) } + a.failure_message(Chef::Exceptions::RegKeyValuesDataMissing, "Missing data key in RegistryKey values hash") + a.whyrun("Data key does not exist. Attempt would fail unless the complete values hash containing all the keys does not exist for registry_key resource's create action.") end end def action_create - unless registry.key_exists?(@current_resource.key) - converge_by("create key #{@new_resource.key}") do - registry.create_key(@new_resource.key, @new_resource.recursive) + unless registry.key_exists?(current_resource.key) + converge_by("create key #{new_resource.key}") do + registry.create_key(new_resource.key, new_resource.recursive) end end - @new_resource.unscrubbed_values.each do |value| + new_resource.unscrubbed_values.each do |value| if @name_hash.has_key?(value[:name].downcase) current_value = @name_hash[value[:name].downcase] + if [:dword, :dword_big_endian, :qword].include? value[:type] + value[:data] = value[:data].to_i + end unless current_value[:type] == value[:type] && current_value[:data] == value[:data] - converge_by("set value #{value}") do - registry.set_value(@new_resource.key, value) + converge_by_value = value + converge_by_value[:data] = "*sensitive value suppressed*" if new_resource.sensitive + + converge_by("set value #{converge_by_value}") do + registry.set_value(new_resource.key, value) end end else - converge_by("set value #{value}") do - registry.set_value(@new_resource.key, value) + converge_by_value = value + converge_by_value[:data] = "*sensitive value suppressed*" if new_resource.sensitive + + converge_by("set value #{converge_by_value}") do + registry.set_value(new_resource.key, value) end end end end def action_create_if_missing - unless registry.key_exists?(@new_resource.key) - converge_by("create key #{@new_resource.key}") do - registry.create_key(@new_resource.key, @new_resource.recursive) + unless registry.key_exists?(new_resource.key) + converge_by("create key #{new_resource.key}") do + registry.create_key(new_resource.key, new_resource.recursive) end end - @new_resource.unscrubbed_values.each do |value| + new_resource.unscrubbed_values.each do |value| unless @name_hash.has_key?(value[:name].downcase) - converge_by("create value #{value}") do - registry.set_value(@new_resource.key, value) + converge_by_value = value + converge_by_value[:data] = "*sensitive value suppressed*" if new_resource.sensitive + + converge_by("create value #{converge_by_value}") do + registry.set_value(new_resource.key, value) end end end end def action_delete - if registry.key_exists?(@new_resource.key) - @new_resource.unscrubbed_values.each do |value| + if registry.key_exists?(new_resource.key) + new_resource.unscrubbed_values.each do |value| if @name_hash.has_key?(value[:name].downcase) - converge_by("delete value #{value}") do - registry.delete_value(@new_resource.key, value) + converge_by_value = value + converge_by_value[:data] = "*sensitive value suppressed*" if new_resource.sensitive + + converge_by("delete value #{converge_by_value}") do + registry.delete_value(new_resource.key, value) end end end @@ -143,9 +178,9 @@ class Chef end def action_delete_key - if registry.key_exists?(@new_resource.key) - converge_by("delete key #{@new_resource.key}") do - registry.delete_key(@new_resource.key, @new_resource.recursive) + if registry.key_exists?(new_resource.key) + converge_by("delete key #{new_resource.key}") do + registry.delete_key(new_resource.key, new_resource.recursive) end end end diff --git a/lib/chef/provider/remote_directory.rb b/lib/chef/provider/remote_directory.rb index e3bc579107..94de68c557 100644 --- a/lib/chef/provider/remote_directory.rb +++ b/lib/chef/provider/remote_directory.rb @@ -1,6 +1,6 @@ # # Author:: Adam Jacob (<adam@chef.io>) -# Copyright:: Copyright 2008-2016, Chef Software, Inc. +# Copyright:: Copyright 2008-2018, Chef Software Inc. # License:: Apache License, Version 2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -23,8 +23,6 @@ require "chef/resource/cookbook_file" require "chef/mixin/file_class" require "chef/platform/query_helpers" require "chef/util/path_helper" -require "chef/deprecation/warnings" -require "chef/deprecation/provider/remote_directory" require "forwardable" @@ -36,9 +34,9 @@ class Chef provides :remote_directory - def_delegators :@new_resource, :purge, :path, :source, :cookbook, :cookbook_name - def_delegators :@new_resource, :files_rights, :files_mode, :files_group, :files_owner, :files_backup - def_delegators :@new_resource, :rights, :mode, :group, :owner + def_delegators :new_resource, :purge, :path, :source, :cookbook, :cookbook_name + def_delegators :new_resource, :files_rights, :files_mode, :files_group, :files_owner, :files_backup + def_delegators :new_resource, :rights, :mode, :group, :owner # The overwrite property on the resource. Delegates to new_resource but can be mutated. # @@ -49,8 +47,6 @@ class Chef !!@overwrite end - attr_accessor :managed_files - # Hash containing keys of the paths for all the files that we sync, plus all their # parent directories. # @@ -155,7 +151,7 @@ class Chef # # FIXME: it should do breadth-first, see CHEF-5080 (please use a performant sort) # - # @return Array<String> The list of files to transfer + # @return [Array<String>] The list of files to transfer # @api private # def files_to_transfer @@ -209,6 +205,8 @@ class Chef def cookbook_file_resource(target_path, relative_source_path) res = Chef::Resource::CookbookFile.new(target_path, run_context) res.cookbook_name = resource_cookbook + # Set the sensitivity level + res.sensitive(new_resource.sensitive) res.source(::File.join(source, relative_source_path)) if Chef::Platform.windows? && files_rights files_rights.each_pair do |permission, *args| @@ -266,16 +264,6 @@ class Chef res end - # - # Add back deprecated methods and aliases that are internally unused and should be removed in Chef-13 - # - extend Chef::Deprecation::Warnings - include Chef::Deprecation::Provider::RemoteDirectory - add_deprecation_warnings_for(Chef::Deprecation::Provider::RemoteDirectory.instance_methods) - - alias_method :resource_for_directory, :directory_resource - add_deprecation_warnings_for([:resource_for_directory]) - end end end diff --git a/lib/chef/provider/remote_file.rb b/lib/chef/provider/remote_file.rb index 9207e62ac6..d2de3d0b5f 100644 --- a/lib/chef/provider/remote_file.rb +++ b/lib/chef/provider/remote_file.rb @@ -18,33 +18,46 @@ # require "chef/provider/file" -require "chef/deprecation/provider/remote_file" -require "chef/deprecation/warnings" class Chef class Provider class RemoteFile < Chef::Provider::File provides :remote_file - extend Chef::Deprecation::Warnings - include Chef::Deprecation::Provider::RemoteFile - add_deprecation_warnings_for(Chef::Deprecation::Provider::RemoteFile.instance_methods) - def initialize(new_resource, run_context) @content_class = Chef::Provider::RemoteFile::Content super end + def define_resource_requirements + [ new_resource.remote_user, new_resource.remote_domain, + new_resource.remote_password ].each do |prop| + requirements.assert(:all_actions) do |a| + a.assertion do + if prop + node[:platform_family] == "windows" + else + true + end + end + a.failure_message Chef::Exceptions::UnsupportedPlatform, "'remote_user', 'remote_domain' and 'remote_password' properties are supported only for Windows platform" + a.whyrun("Assuming that the platform is Windows while passing 'remote_user', 'remote_domain' and 'remote_password' properties") + end + end + + super + end + def load_current_resource - @current_resource = Chef::Resource::RemoteFile.new(@new_resource.name) + @current_resource = Chef::Resource::RemoteFile.new(new_resource.name) super end private def managing_content? - return true if @new_resource.checksum - return true if !@new_resource.source.nil? && @action != :create_if_missing + return true if new_resource.checksum + return true if !new_resource.source.nil? && @action != :create_if_missing false end diff --git a/lib/chef/provider/remote_file/cache_control_data.rb b/lib/chef/provider/remote_file/cache_control_data.rb index 8d7de5c370..32c1542d57 100644 --- a/lib/chef/provider/remote_file/cache_control_data.rb +++ b/lib/chef/provider/remote_file/cache_control_data.rb @@ -153,7 +153,7 @@ class Chef if Chef::FileCache.has_key?(old_path) # We found an old cache control data file. We started using sha256 instead of md5 # to name these. Upgrade the file to the new name. - Chef::Log.debug("Found old cache control data file at #{old_path}. Moving to #{path}.") + Chef::Log.trace("Found old cache control data file at #{old_path}. Moving to #{path}.") Chef::FileCache.load(old_path).tap do |data| Chef::FileCache.store(path, data) Chef::FileCache.delete(old_path) diff --git a/lib/chef/provider/remote_file/content.rb b/lib/chef/provider/remote_file/content.rb index e44096428b..4cf2c32287 100644 --- a/lib/chef/provider/remote_file/content.rb +++ b/lib/chef/provider/remote_file/content.rb @@ -32,10 +32,10 @@ class Chef include Chef::Mixin::Uris def file_for_provider - Chef::Log.debug("#{@new_resource} checking for changes") + logger.trace("#{@new_resource} checking for changes") if current_resource_matches_target_checksum? - Chef::Log.debug("#{@new_resource} checksum matches target checksum (#{@new_resource.checksum}) - not updating") + logger.trace("#{@new_resource} checksum matches target checksum (#{@new_resource.checksum}) - not updating") else sources = @new_resource.source raw_file = try_multiple_sources(sources) @@ -54,10 +54,10 @@ class Chef as_uri(source) end raw_file = grab_file_from_uri(uri) - rescue SocketError, Errno::ECONNREFUSED, Errno::ENOENT, Errno::EACCES, Timeout::Error, Net::HTTPServerException, Net::HTTPFatalError, Net::FTPError => e - Chef::Log.warn("#{@new_resource} cannot be downloaded from #{source}: #{e}") + rescue SocketError, Errno::ECONNREFUSED, Errno::ENOENT, Errno::EACCES, Timeout::Error, Net::HTTPServerException, Net::HTTPFatalError, Net::FTPError, Errno::ETIMEDOUT => e + logger.warn("#{@new_resource} cannot be downloaded from #{source}: #{e}") if source = sources.shift - Chef::Log.info("#{@new_resource} trying to download from another mirror") + logger.info("#{@new_resource} trying to download from another mirror") retry else raise e diff --git a/lib/chef/provider/remote_file/fetcher.rb b/lib/chef/provider/remote_file/fetcher.rb index 563d135d6a..3011dd80a0 100644 --- a/lib/chef/provider/remote_file/fetcher.rb +++ b/lib/chef/provider/remote_file/fetcher.rb @@ -24,6 +24,9 @@ class Chef def self.for_resource(uri, new_resource, current_resource) if network_share?(uri) + if !Chef::Platform.windows? + raise Exceptions::UnsupportedPlatform, "Fetching the file on a network share is supported only on the Windows platform. Please change your source: #{uri}" + end Chef::Provider::RemoteFile::NetworkFile.new(uri, new_resource, current_resource) else case uri.scheme diff --git a/lib/chef/provider/remote_file/ftp.rb b/lib/chef/provider/remote_file/ftp.rb index 5935e83301..b382c20c31 100644 --- a/lib/chef/provider/remote_file/ftp.rb +++ b/lib/chef/provider/remote_file/ftp.rb @@ -153,9 +153,9 @@ class Chef def parse_path path = uri.path.sub(%r{\A/}, "%2F") # re-encode the beginning slash because uri library decodes it. directories = path.split(%r{/}, -1) - directories.each {|d| + directories.each do |d| d.gsub!(/%([0-9A-Fa-f][0-9A-Fa-f])/) { [$1].pack("H2") } - } + end unless filename = directories.pop raise ArgumentError, "no filename: #{path.inspect}" end diff --git a/lib/chef/provider/remote_file/http.rb b/lib/chef/provider/remote_file/http.rb index ad044f9e3c..2122142608 100644 --- a/lib/chef/provider/remote_file/http.rb +++ b/lib/chef/provider/remote_file/http.rb @@ -31,12 +31,14 @@ class Chef attr_reader :uri attr_reader :new_resource attr_reader :current_resource + attr_reader :logger # Parse the uri into instance variables - def initialize(uri, new_resource, current_resource) + def initialize(uri, new_resource, current_resource, logger = Chef::Log.with_child) @uri = uri @new_resource = new_resource @current_resource = current_resource + @logger = logger end def events @@ -55,22 +57,28 @@ class Chef if (etag = cache_control_data.etag) && want_etag_cache_control? cache_control_headers["if-none-match"] = etag end - Chef::Log.debug("Cache control headers: #{cache_control_headers.inspect}") + logger.trace("Cache control headers: #{cache_control_headers.inspect}") cache_control_headers end def fetch http = Chef::HTTP::Simple.new(uri, http_client_opts) + orig_tempfile = Chef::FileContentManagement::Tempfile.new(@new_resource).tempfile if want_progress? - tempfile = http.streaming_request_with_progress(uri, headers) do |size, total| + tempfile = http.streaming_request_with_progress(uri, headers, orig_tempfile) do |size, total| events.resource_update_progress(new_resource, size, total, progress_interval) end else - tempfile = http.streaming_request(uri, headers) + tempfile = http.streaming_request(uri, headers, orig_tempfile) end if tempfile update_cache_control_data(tempfile, http.last_response) tempfile.close + else + # cache_control shows the file is unchanged, so we got back nil from the streaming_request above, and it is + # now our responsibility to unlink the tempfile we created + orig_tempfile.close + orig_tempfile.unlink end tempfile end @@ -123,7 +131,7 @@ class Chef # case you'd end up with a tar archive (no gzip) named, e.g., foo.tgz, # which is not what you wanted. if uri.to_s =~ /gz$/ - Chef::Log.debug("Turning gzip compression off due to filename ending in gz") + logger.trace("Turning gzip compression off due to filename ending in gz") opts[:disable_gzip] = true end opts diff --git a/lib/chef/provider/remote_file/local_file.rb b/lib/chef/provider/remote_file/local_file.rb index 613db02337..0719e5dbf7 100644 --- a/lib/chef/provider/remote_file/local_file.rb +++ b/lib/chef/provider/remote_file/local_file.rb @@ -48,7 +48,7 @@ class Chef # Fetches the file at uri, returning a Tempfile-like File handle def fetch tempfile = Chef::FileContentManagement::Tempfile.new(new_resource).tempfile - Chef::Log.debug("#{new_resource} staging #{source_path} to #{tempfile.path}") + Chef::Log.trace("#{new_resource} staging #{source_path} to #{tempfile.path}") FileUtils.cp(source_path, tempfile.path) tempfile.close if tempfile tempfile diff --git a/lib/chef/provider/remote_file/network_file.rb b/lib/chef/provider/remote_file/network_file.rb index 44046132a9..ffd2d0bbce 100644 --- a/lib/chef/provider/remote_file/network_file.rb +++ b/lib/chef/provider/remote_file/network_file.rb @@ -19,14 +19,18 @@ require "uri" require "tempfile" require "chef/provider/remote_file" +require "chef/mixin/user_context" class Chef class Provider class RemoteFile class NetworkFile + include Chef::Mixin::UserContext attr_reader :new_resource + TRANSFER_CHUNK_SIZE = 1048576 + def initialize(source, new_resource, current_resource) @new_resource = new_resource @source = source @@ -35,13 +39,22 @@ class Chef # Fetches the file on a network share, returning a Tempfile-like File handle # windows only def fetch - tempfile = Chef::FileContentManagement::Tempfile.new(new_resource).tempfile - Chef::Log.debug("#{new_resource} staging #{@source} to #{tempfile.path}") - FileUtils.cp(@source, tempfile.path) - tempfile.close if tempfile + begin + tempfile = Chef::FileContentManagement::Tempfile.new(new_resource).tempfile + Chef::Log.trace("#{new_resource} staging #{@source} to #{tempfile.path}") + + with_user_context(new_resource.remote_user, new_resource.remote_password, new_resource.remote_domain, new_resource.authentication) do + ::File.open(@source, "rb") do |remote_file| + while data = remote_file.read(TRANSFER_CHUNK_SIZE) + tempfile.write(data) + end + end + end + ensure + tempfile.close if tempfile + end tempfile end - end end end diff --git a/lib/chef/provider/remote_file/sftp.rb b/lib/chef/provider/remote_file/sftp.rb index 530977e3c8..21c5c4ca04 100644 --- a/lib/chef/provider/remote_file/sftp.rb +++ b/lib/chef/provider/remote_file/sftp.rb @@ -68,9 +68,9 @@ class Chef def validate_path! path = uri.path.sub(%r{\A/}, "%2F") # re-encode the beginning slash because uri library decodes it. directories = path.split(%r{/}, -1) - directories.each {|d| + directories.each do |d| d.gsub!(/%([0-9A-Fa-f][0-9A-Fa-f])/) { [$1].pack("H2") } - } + end unless filename = directories.pop raise ArgumentError, "no filename: #{path.inspect}" end diff --git a/lib/chef/provider/route.rb b/lib/chef/provider/route.rb index 64c89aac6d..b23d0307cc 100644 --- a/lib/chef/provider/route.rb +++ b/lib/chef/provider/route.rb @@ -17,213 +17,232 @@ # require "chef/log" -require "chef/mixin/command" require "chef/provider" require "ipaddr" -class Chef::Provider::Route < Chef::Provider - include Chef::Mixin::Command - - provides :route - - attr_accessor :is_running - - MASK = { "0.0.0.0" => "0", - "128.0.0.0" => "1", - "192.0.0.0" => "2", - "224.0.0.0" => "3", - "240.0.0.0" => "4", - "248.0.0.0" => "5", - "252.0.0.0" => "6", - "254.0.0.0" => "7", - "255.0.0.0" => "8", - "255.128.0.0" => "9", - "255.192.0.0" => "10", - "255.224.0.0" => "11", - "255.240.0.0" => "12", - "255.248.0.0" => "13", - "255.252.0.0" => "14", - "255.254.0.0" => "15", - "255.255.0.0" => "16", - "255.255.128.0" => "17", - "255.255.192.0" => "18", - "255.255.224.0" => "19", - "255.255.240.0" => "20", - "255.255.248.0" => "21", - "255.255.252.0" => "22", - "255.255.254.0" => "23", - "255.255.255.0" => "24", - "255.255.255.128" => "25", - "255.255.255.192" => "26", - "255.255.255.224" => "27", - "255.255.255.240" => "28", - "255.255.255.248" => "29", - "255.255.255.252" => "30", - "255.255.255.254" => "31", - "255.255.255.255" => "32" } - - def hex2ip(hex_data) - # Cleanup hex data - hex_ip = hex_data.to_s.downcase.gsub(/[^0-9a-f]/, "") - - # Check hex data format (IP is a 32bit integer, so should be 8 chars long) - return nil if hex_ip.length != hex_data.length || hex_ip.length != 8 - - # Extract octets from hex data - octets = hex_ip.scan(/../).reverse.collect { |octet| [octet].pack("H2").unpack("C").first } - - # Validate IP - ip = octets.join(".") - begin - IPAddr.new(ip, Socket::AF_INET).to_s - rescue ArgumentError - Chef::Log.debug("Invalid IP address data: hex=#{hex_ip}, ip=#{ip}") - return nil - end - end - - def whyrun_supported? - true - end - - def load_current_resource - self.is_running = false +class Chef + class Provider + class Route < Chef::Provider + + provides :route + + attr_accessor :is_running + + MASK = { "0.0.0.0" => "0", + "128.0.0.0" => "1", + "192.0.0.0" => "2", + "224.0.0.0" => "3", + "240.0.0.0" => "4", + "248.0.0.0" => "5", + "252.0.0.0" => "6", + "254.0.0.0" => "7", + "255.0.0.0" => "8", + "255.128.0.0" => "9", + "255.192.0.0" => "10", + "255.224.0.0" => "11", + "255.240.0.0" => "12", + "255.248.0.0" => "13", + "255.252.0.0" => "14", + "255.254.0.0" => "15", + "255.255.0.0" => "16", + "255.255.128.0" => "17", + "255.255.192.0" => "18", + "255.255.224.0" => "19", + "255.255.240.0" => "20", + "255.255.248.0" => "21", + "255.255.252.0" => "22", + "255.255.254.0" => "23", + "255.255.255.0" => "24", + "255.255.255.128" => "25", + "255.255.255.192" => "26", + "255.255.255.224" => "27", + "255.255.255.240" => "28", + "255.255.255.248" => "29", + "255.255.255.252" => "30", + "255.255.255.254" => "31", + "255.255.255.255" => "32" }.freeze + + def hex2ip(hex_data) + # Cleanup hex data + hex_ip = hex_data.to_s.downcase.gsub(/[^0-9a-f]/, "") + + # Check hex data format (IP is a 32bit integer, so should be 8 chars long) + return nil if hex_ip.length != hex_data.length || hex_ip.length != 8 + + # Extract octets from hex data + octets = hex_ip.scan(/../).reverse.collect { |octet| [octet].pack("H2").unpack("C").first } + + # Validate IP + ip = octets.join(".") + begin + IPAddr.new(ip, Socket::AF_INET).to_s + rescue ArgumentError + logger.trace("Invalid IP address data: hex=#{hex_ip}, ip=#{ip}") + return nil + end + end - # cidr or quad dot mask - if @new_resource.netmask - new_ip = IPAddr.new("#{@new_resource.target}/#{@new_resource.netmask}") - else - new_ip = IPAddr.new(@new_resource.target) - end + def load_current_resource + self.is_running = false + + # cidr or quad dot mask + new_ip = if new_resource.target == "default" + IPAddr.new(new_resource.gateway) + elsif new_resource.netmask + IPAddr.new("#{new_resource.target}/#{new_resource.netmask}") + else + IPAddr.new(new_resource.target) + end + + # For linux, we use /proc/net/route file to read proc table info + return if node[:os] != "linux" + + route_file = ::File.open("/proc/net/route", "r") + + # Read all routes + while (line = route_file.gets) + # Get all the fields for a route + _, destination, gateway, _, _, _, _, mask = line.split + + # Convert hex-encoded values to quad-dotted notation (e.g. 0064A8C0 => 192.168.100.0) + destination = hex2ip(destination) + gateway = hex2ip(gateway) + mask = hex2ip(mask) + + # Skip formatting lines (header, etc) + next unless destination && gateway && mask + logger.trace("#{new_resource} system has route: dest=#{destination} mask=#{mask} gw=#{gateway}") + + # check if what were trying to configure is already there + # use an ipaddr object with ip/mask this way we can have + # a new resource be in cidr format (i don't feel like + # expanding bitmask by hand. + # + running_ip = IPAddr.new("#{destination}/#{mask}") + logger.trace("#{new_resource} new ip: #{new_ip.inspect} running ip: #{running_ip.inspect}") + self.is_running = true if running_ip == new_ip && gateway == new_resource.gateway + end - # For linux, we use /proc/net/route file to read proc table info - if node[:os] == "linux" - route_file = ::File.open("/proc/net/route", "r") - - # Read all routes - while (line = route_file.gets) - # Get all the fields for a route - iface, destination, gateway, flags, refcnt, use, metric, mask, mtu, window, irtt = line.split - - # Convert hex-encoded values to quad-dotted notation (e.g. 0064A8C0 => 192.168.100.0) - destination = hex2ip(destination) - gateway = hex2ip(gateway) - mask = hex2ip(mask) - - # Skip formatting lines (header, etc) - next unless destination && gateway && mask - Chef::Log.debug("#{@new_resource} system has route: dest=#{destination} mask=#{mask} gw=#{gateway}") - - # check if what were trying to configure is already there - # use an ipaddr object with ip/mask this way we can have - # a new resource be in cidr format (i don't feel like - # expanding bitmask by hand. - # - running_ip = IPAddr.new("#{destination}/#{mask}") - Chef::Log.debug("#{@new_resource} new ip: #{new_ip.inspect} running ip: #{running_ip.inspect}") - self.is_running = true if running_ip == new_ip && gateway == @new_resource.gateway + route_file.close end - route_file.close - end - end + def action_add + # check to see if load_current_resource found the route + if is_running + logger.trace("#{new_resource} route already active - nothing to do") + else + command = generate_command(:add) + converge_by("run #{command.join(' ')} to add route") do + shell_out_compact!(command) + logger.info("#{new_resource} added") + end + end - def action_add - # check to see if load_current_resource found the route - if is_running - Chef::Log.debug("#{@new_resource} route already active - nothing to do") - else - command = generate_command(:add) - converge_by ("run #{ command } to add route") do - run_command( :command => command ) - Chef::Log.info("#{@new_resource} added") + # for now we always write the file (ugly but its what it is) + generate_config end - end - #for now we always write the file (ugly but its what it is) - generate_config - end + def action_delete + if is_running + command = generate_command(:delete) + converge_by("run #{command.join(' ')} to delete route ") do + shell_out_compact!(command) + logger.info("#{new_resource} removed") + end + else + logger.trace("#{new_resource} route does not exist - nothing to do") + end - def action_delete - if is_running - command = generate_command(:delete) - converge_by ("run #{ command } to delete route ") do - run_command( :command => command ) - Chef::Log.info("#{@new_resource} removed") + # for now we always write the file (ugly but its what it is) + generate_config end - else - Chef::Log.debug("#{@new_resource} route does not exist - nothing to do") - end - - #for now we always write the file (ugly but its what it is) - generate_config - end - def generate_config - conf = Hash.new - case node[:platform] - when "centos", "redhat", "fedora" - # walk the collection - run_context.resource_collection.each do |resource| - if resource.is_a? Chef::Resource::Route - # default to eth0 - if resource.device - dev = resource.device - else - dev = "eth0" + def generate_config + conf = {} + case node[:platform] + when "centos", "redhat", "fedora" + # walk the collection + run_context.resource_collection.each do |resource| + next unless resource.is_a? Chef::Resource::Route + # default to eth0 + dev = if resource.device + resource.device + else + "eth0" + end + + conf[dev] = "" if conf[dev].nil? + case @action + when :add + conf[dev] << config_file_contents(:add, comment: resource.comment, device: resource.device, target: resource.target, metric: resource.metric, netmask: resource.netmask, gateway: resource.gateway) if resource.action == [:add] + when :delete + # need to do this for the case when the last route on an int + # is removed + conf[dev] << config_file_contents(:delete) + end end - - conf[dev] = String.new if conf[dev].nil? - case @action - when :add - conf[dev] << config_file_contents(:add, :target => resource.target, :netmask => resource.netmask, :gateway => resource.gateway) if resource.action == [:add] - when :delete - # need to do this for the case when the last route on an int - # is removed - conf[dev] << config_file_contents(:delete) + conf.each_key do |k| + if new_resource.target == "default" + network_file_name = "/etc/sysconfig/network" + converge_by("write route default route to #{network_file_name}") do + logger.trace("#{new_resource} writing default route #{new_resource.gateway} to #{network_file_name}") + if ::File.exist?(network_file_name) + network_file = ::Chef::Util::FileEdit.new(network_file_name) + network_file.search_file_replace_line /^GATEWAY=/, "GATEWAY=#{new_resource.gateway}" + network_file.insert_line_if_no_match /^GATEWAY=/, "GATEWAY=#{new_resource.gateway}" + network_file.write_file + else + network_file = ::File.new(network_file_name, "w") + network_file.puts("GATEWAY=#{new_resource.gateway}") + network_file.close + end + end + else + network_file_name = "/etc/sysconfig/network-scripts/route-#{k}" + converge_by("write route route.#{k}\n#{conf[k]} to #{network_file_name}") do + network_file = ::File.new(network_file_name, "w") + network_file.puts(conf[k]) + logger.trace("#{new_resource} writing route.#{k}\n#{conf[k]}") + network_file.close + end + end end end end - conf.each do |k, v| - network_file_name = "/etc/sysconfig/network-scripts/route-#{k}" - converge_by ("write route route.#{k}\n#{conf[k]} to #{ network_file_name }") do - network_file = ::File.new(network_file_name, "w") - network_file.puts(conf[k]) - Chef::Log.debug("#{@new_resource} writing route.#{k}\n#{conf[k]}") - network_file.close + + def generate_command(action) + target = new_resource.target + target = "#{target}/#{MASK[new_resource.netmask.to_s]}" if new_resource.netmask + + case action + when :add + command = [ "ip", "route", "replace", target ] + command += [ "via", new_resource.gateway ] if new_resource.gateway + command += [ "dev", new_resource.device ] if new_resource.device + command += [ "metric", new_resource.metric ] if new_resource.metric + when :delete + command = [ "ip", "route", "delete", target ] + command += [ "via", new_resource.gateway ] if new_resource.gateway end - end - end - end - def generate_command(action) - common_route_items = "" - common_route_items << "/#{MASK[@new_resource.netmask.to_s]}" if @new_resource.netmask - common_route_items << " via #{@new_resource.gateway} " if @new_resource.gateway - - case action - when :add - command = "ip route replace #{@new_resource.target}" - command << common_route_items - command << " dev #{@new_resource.device} " if @new_resource.device - when :delete - command = "ip route delete #{@new_resource.target}" - command << common_route_items - end + command + end - return command - end + def config_file_contents(action, options = {}) + content = "" + case action + when :add + content << "# #{options[:comment]}\n" if options[:comment] + content << (options[:target]).to_s + content << "/#{MASK[options[:netmask].to_s]}" if options[:netmask] + content << " via #{options[:gateway]}" if options[:gateway] + content << " dev #{options[:device]}" if options[:device] + content << " metric #{options[:metric]}" if options[:metric] + content << "\n" + end - def config_file_contents(action, options = {}) - content = "" - case action - when :add - content << "#{options[:target]}" - content << "/#{options[:netmask]}" if options[:netmask] - content << " via #{options[:gateway]}" if options[:gateway] - content << "\n" + content + end end - - return content end end diff --git a/lib/chef/provider/ruby_block.rb b/lib/chef/provider/ruby_block.rb index 0817b14044..01f041ca3b 100644 --- a/lib/chef/provider/ruby_block.rb +++ b/lib/chef/provider/ruby_block.rb @@ -1,7 +1,7 @@ # # Author:: Adam Jacob (<adam@chef.io>) # Author:: AJ Christensen (<aj@chef.io>) -# Copyright:: Copyright 2009-2016, Opscode +# Copyright:: Copyright 2009-2017, Chef Software Inc. # License:: Apache License, Version 2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -22,18 +22,14 @@ class Chef class RubyBlock < Chef::Provider provides :ruby_block - def whyrun_supported? - true - end - def load_current_resource true end def action_run - converge_by("execute the ruby block #{@new_resource.name}") do - @new_resource.block.call - Chef::Log.info("#{@new_resource} called") + converge_by("execute the ruby block #{new_resource.name}") do + new_resource.block.call + logger.info("#{new_resource} called") end end diff --git a/lib/chef/provider/script.rb b/lib/chef/provider/script.rb index 6ca4e9f6f3..c5966370f7 100644 --- a/lib/chef/provider/script.rb +++ b/lib/chef/provider/script.rb @@ -18,6 +18,7 @@ require "tempfile" require "chef/provider/execute" +require "chef/win32/security" if Chef::Platform.windows? require "forwardable" class Chef @@ -33,7 +34,7 @@ class Chef provides :ruby provides :script - def_delegators :@new_resource, :interpreter, :flags + def_delegators :new_resource, :interpreter, :flags attr_accessor :code @@ -50,7 +51,7 @@ class Chef super # @todo Chef-13: change this to an exception if code.nil? - Chef::Log.warn "#{@new_resource}: No code attribute was given, resource does nothing, this behavior is deprecated and will be removed in Chef-13" + logger.warn "#{new_resource}: No code attribute was given, resource does nothing, this behavior is deprecated and will be removed in Chef-13" end end @@ -66,10 +67,45 @@ class Chef end def set_owner_and_group - # FileUtils itself implements a no-op if +user+ or +group+ are nil - # You can prove this by running FileUtils.chown(nil,nil,'/tmp/file') - # as an unprivileged user. - FileUtils.chown(new_resource.user, new_resource.group, script_file.path) + if Chef::Platform.windows? + # And on Windows also this is a no-op if there is no user specified. + grant_alternate_user_read_access + else + # FileUtils itself implements a no-op if +user+ or +group+ are nil + # You can prove this by running FileUtils.chown(nil,nil,'/tmp/file') + # as an unprivileged user. + FileUtils.chown(new_resource.user, new_resource.group, script_file.path) + end + end + + def grant_alternate_user_read_access + # Do nothing if an alternate user isn't specified -- the file + # will already have the correct permissions for the user as part + # of the default ACL behavior on Windows. + return if new_resource.user.nil? + + # Duplicate the script file's existing DACL + # so we can add an ACE later + securable_object = Chef::ReservedNames::Win32::Security::SecurableObject.new(script_file.path) + aces = securable_object.security_descriptor.dacl.reduce([]) { |result, current| result.push(current) } + + username = new_resource.user + + if new_resource.domain + username = new_resource.domain + '\\' + new_resource.user + end + + # Create an ACE that allows the alternate user read access to the script + # file so it can be read and executed. + user_sid = Chef::ReservedNames::Win32::Security::SID.from_account(username) + read_ace = Chef::ReservedNames::Win32::Security::ACE.access_allowed(user_sid, Chef::ReservedNames::Win32::API::Security::GENERIC_READ | Chef::ReservedNames::Win32::API::Security::GENERIC_EXECUTE, 0) + aces.push(read_ace) + acl = Chef::ReservedNames::Win32::Security::ACL.create(aces) + + # This actually applies the modified DACL to the file + # Use parentheses to bypass RuboCop / ChefStyle warning + # about useless setter + (securable_object.dacl = acl) end def script_file diff --git a/lib/chef/provider/service.rb b/lib/chef/provider/service.rb index e693bd2eed..c116d321f1 100644 --- a/lib/chef/provider/service.rb +++ b/lib/chef/provider/service.rb @@ -1,7 +1,7 @@ # # Author:: AJ Christensen (<aj@hjksolutions.com>) # Author:: Davide Cavalca (<dcavalca@fb.com>) -# Copyright:: Copyright 2008-2016, Chef Software, Inc. +# Copyright:: Copyright 2008-2017, Chef Software Inc. # License:: Apache License, Version 2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,15 +17,12 @@ # limitations under the License. # -require "chef/mixin/command" require "chef/provider" class Chef class Provider class Service < Chef::Provider - include Chef::Mixin::Command - def supports @supports ||= new_resource.supports.dup end @@ -35,10 +32,6 @@ class Chef @enabled = nil end - def whyrun_supported? - true - end - def load_current_resource supports[:status] = false if supports[:status].nil? supports[:reload] = false if supports[:reload].nil? @@ -51,21 +44,21 @@ class Chef # XXX?: the #nil? check below will likely fail if this is a cloned resource or if # we just run multiple actions. def load_new_resource_state - if @new_resource.enabled.nil? - @new_resource.enabled(@current_resource.enabled) + if new_resource.enabled.nil? + new_resource.enabled(current_resource.enabled) end - if @new_resource.running.nil? - @new_resource.running(@current_resource.running) + if new_resource.running.nil? + new_resource.running(current_resource.running) end - if @new_resource.masked.nil? - @new_resource.masked(@current_resource.masked) + if new_resource.masked.nil? + new_resource.masked(current_resource.masked) end end # subclasses should override this if they do implement user services def user_services_requirements requirements.assert(:all_actions) do |a| - a.assertion { @new_resource.user.nil? } + a.assertion { new_resource.user.nil? } a.failure_message Chef::Exceptions::UnsupportedAction, "#{self} does not support user services" end end @@ -76,7 +69,7 @@ class Chef def define_resource_requirements requirements.assert(:reload) do |a| - a.assertion { supports[:reload] || @new_resource.reload_command } + a.assertion { supports[:reload] || new_resource.reload_command } a.failure_message Chef::Exceptions::UnsupportedAction, "#{self} does not support :reload" # if a service is not declared to support reload, that won't # typically change during the course of a run - so no whyrun @@ -85,97 +78,97 @@ class Chef end def action_enable - if @current_resource.enabled - Chef::Log.debug("#{@new_resource} already enabled - nothing to do") + if current_resource.enabled + logger.trace("#{new_resource} already enabled - nothing to do") else - converge_by("enable service #{@new_resource}") do + converge_by("enable service #{new_resource}") do enable_service - Chef::Log.info("#{@new_resource} enabled") + logger.info("#{new_resource} enabled") end end load_new_resource_state - @new_resource.enabled(true) + new_resource.enabled(true) end def action_disable - if @current_resource.enabled - converge_by("disable service #{@new_resource}") do + if current_resource.enabled + converge_by("disable service #{new_resource}") do disable_service - Chef::Log.info("#{@new_resource} disabled") + logger.info("#{new_resource} disabled") end else - Chef::Log.debug("#{@new_resource} already disabled - nothing to do") + logger.trace("#{new_resource} already disabled - nothing to do") end load_new_resource_state - @new_resource.enabled(false) + new_resource.enabled(false) end def action_mask - if @current_resource.masked - Chef::Log.debug("#{@new_resource} already masked - nothing to do") + if current_resource.masked + logger.trace("#{new_resource} already masked - nothing to do") else - converge_by("mask service #{@new_resource}") do + converge_by("mask service #{new_resource}") do mask_service - Chef::Log.info("#{@new_resource} masked") + logger.info("#{new_resource} masked") end end load_new_resource_state - @new_resource.masked(true) + new_resource.masked(true) end def action_unmask - if @current_resource.masked - converge_by("unmask service #{@new_resource}") do + if current_resource.masked + converge_by("unmask service #{new_resource}") do unmask_service - Chef::Log.info("#{@new_resource} unmasked") + logger.info("#{new_resource} unmasked") end else - Chef::Log.debug("#{@new_resource} already unmasked - nothing to do") + logger.trace("#{new_resource} already unmasked - nothing to do") end load_new_resource_state - @new_resource.masked(false) + new_resource.masked(false) end def action_start - unless @current_resource.running - converge_by("start service #{@new_resource}") do + unless current_resource.running + converge_by("start service #{new_resource}") do start_service - Chef::Log.info("#{@new_resource} started") + logger.info("#{new_resource} started") end else - Chef::Log.debug("#{@new_resource} already running - nothing to do") + logger.trace("#{new_resource} already running - nothing to do") end load_new_resource_state - @new_resource.running(true) + new_resource.running(true) end def action_stop - if @current_resource.running - converge_by("stop service #{@new_resource}") do + if current_resource.running + converge_by("stop service #{new_resource}") do stop_service - Chef::Log.info("#{@new_resource} stopped") + logger.info("#{new_resource} stopped") end else - Chef::Log.debug("#{@new_resource} already stopped - nothing to do") + logger.trace("#{new_resource} already stopped - nothing to do") end load_new_resource_state - @new_resource.running(false) + new_resource.running(false) end def action_restart - converge_by("restart service #{@new_resource}") do + converge_by("restart service #{new_resource}") do restart_service - Chef::Log.info("#{@new_resource} restarted") + logger.info("#{new_resource} restarted") end load_new_resource_state - @new_resource.running(true) + new_resource.running(true) end def action_reload - if @current_resource.running - converge_by("reload service #{@new_resource}") do + if current_resource.running + converge_by("reload service #{new_resource}") do reload_service - Chef::Log.info("#{@new_resource} reloaded") + logger.info("#{new_resource} reloaded") end end load_new_resource_state @@ -216,17 +209,17 @@ class Chef protected def default_init_command - if @new_resource.init_command - @new_resource.init_command - elsif self.instance_variable_defined?(:@init_command) + if new_resource.init_command + new_resource.init_command + elsif instance_variable_defined?(:@init_command) @init_command end end def custom_command_for_action?(action) method_name = "#{action}_command".to_sym - @new_resource.respond_to?(method_name) && - !!@new_resource.send(method_name) + new_resource.respond_to?(method_name) && + !!new_resource.send(method_name) end module ServicePriorityInit @@ -252,7 +245,7 @@ class Chef Chef.set_provider_priority_array :service, [ Systemd, Arch ], platform_family: "arch" Chef.set_provider_priority_array :service, [ Systemd, Gentoo ], platform_family: "gentoo" Chef.set_provider_priority_array :service, [ Systemd, Upstart, Insserv, Debian, Invokercd ], platform_family: "debian" - Chef.set_provider_priority_array :service, [ Systemd, Insserv, Redhat ], platform_family: %w{rhel fedora suse} + Chef.set_provider_priority_array :service, [ Systemd, Insserv, Redhat ], platform_family: %w{rhel fedora suse amazon} end end end diff --git a/lib/chef/provider/service/aix.rb b/lib/chef/provider/service/aix.rb index 201f9ff5f9..10ea06152b 100644 --- a/lib/chef/provider/service/aix.rb +++ b/lib/chef/provider/service/aix.rb @@ -1,6 +1,6 @@ # # Author:: kaustubh (<kaustubh@clogeny.com>) -# Copyright:: Copyright 2014-2016, Chef Software, Inc. +# Copyright:: Copyright 2014-2017, Chef Software Inc. # License:: Apache License, Version 2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -43,10 +43,6 @@ class Chef @current_resource end - def whyrun_supported? - true - end - def start_service if @is_resource_group shell_out!("startsrc -g #{@new_resource.service_name}") @@ -92,7 +88,7 @@ class Chef protected def determine_current_status! - Chef::Log.debug "#{@new_resource} using lssrc to check the status" + logger.trace "#{@new_resource} using lssrc to check the status" begin if is_resource_group? # Groups as a whole have no notion of whether they're running @@ -105,7 +101,7 @@ class Chef @current_resource.running false end end - Chef::Log.debug "#{@new_resource} running: #{@current_resource.running}" + logger.trace "#{@new_resource} running: #{@current_resource.running}" # ShellOut sometimes throws different types of Exceptions than ShellCommandFailed. # Temporarily catching different types of exceptions here until we get Shellout fixed. # TODO: Remove the line before one we get the ShellOut fix. @@ -119,7 +115,7 @@ class Chef def is_resource_group? so = shell_out("lssrc -g #{@new_resource.service_name}") if so.exitstatus == 0 - Chef::Log.debug("#{@new_resource.service_name} is a group") + logger.trace("#{@new_resource.service_name} is a group") @is_resource_group = true end end diff --git a/lib/chef/provider/service/aixinit.rb b/lib/chef/provider/service/aixinit.rb index 73c5e07715..dd8514cf20 100644 --- a/lib/chef/provider/service/aixinit.rb +++ b/lib/chef/provider/service/aixinit.rb @@ -45,11 +45,11 @@ class Chef priority_ok = @current_resource.priority == @new_resource.priority end if @current_resource.enabled && priority_ok - Chef::Log.debug("#{@new_resource} already enabled - nothing to do") + logger.trace("#{@new_resource} already enabled - nothing to do") else converge_by("enable service #{@new_resource}") do enable_service - Chef::Log.info("#{@new_resource} enabled") + logger.info("#{@new_resource} enabled") end end load_new_resource_state diff --git a/lib/chef/provider/service/arch.rb b/lib/chef/provider/service/arch.rb index 2fd32e37aa..e34227036a 100644 --- a/lib/chef/provider/service/arch.rb +++ b/lib/chef/provider/service/arch.rb @@ -66,7 +66,7 @@ class Chef::Provider::Service::Arch < Chef::Provider::Service::Init end end - def enable_service() + def enable_service new_daemons = [] entries = daemons @@ -92,7 +92,7 @@ class Chef::Provider::Service::Arch < Chef::Provider::Service::Init end end - def disable_service() + def disable_service new_daemons = [] entries = daemons diff --git a/lib/chef/provider/service/debian.rb b/lib/chef/provider/service/debian.rb index 67b71953f7..09af9f224f 100644 --- a/lib/chef/provider/service/debian.rb +++ b/lib/chef/provider/service/debian.rb @@ -1,6 +1,6 @@ # # Author:: AJ Christensen (<aj@hjksolutions.com>) -# Copyright:: Copyright 2008-2016, Chef Software Inc. +# Copyright:: Copyright 2008-2017, Chef Software Inc. # License:: Apache License, Version 2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -35,8 +35,6 @@ class Chef def load_current_resource super - @priority_success = true - @rcd_status = nil current_resource.priority(get_priority) current_resource.enabled(service_currently_enabled?(current_resource.priority)) current_resource @@ -54,8 +52,8 @@ class Chef end requirements.assert(:all_actions) do |a| - a.assertion { @priority_success } - a.failure_message Chef::Exceptions::Service, "/usr/sbin/update-rc.d -n -f #{current_resource.service_name} failed - #{@rcd_status.inspect}" + a.assertion { @got_priority == true } + a.failure_message Chef::Exceptions::Service, "Unable to determine priority for service" # This can happen if the service is not yet installed,so we'll fake it. a.whyrun ["Unable to determine priority of service, assuming service would have been correctly installed earlier in the run.", "Assigning temporary priorities to continue.", @@ -72,23 +70,38 @@ class Chef end end + # returns a list of levels that the service should be stopped or started on + def parse_init_file(path) + return [] unless ::File.exist?(path) + in_info = false + ::File.readlines(path).each_with_object([]) do |line, acc| + if line =~ /^### BEGIN INIT INFO/ + in_info = true + elsif line =~ /^### END INIT INFO/ + in_info = false + elsif in_info + if line =~ /Default-(Start|Stop):\s+(\d.*)/ + acc << $2.split(" ") + end + end + end.flatten + end + def get_priority priority = {} + rc_files = [] - @rcd_status = popen4("/usr/sbin/update-rc.d -n -f #{current_resource.service_name} remove") do |pid, stdin, stdout, stderr| - - [stdout, stderr].each do |iop| - iop.each_line do |line| - if UPDATE_RC_D_PRIORITIES =~ line - # priority[runlevel] = [ S|K, priority ] - # S = Start, K = Kill - # debian runlevels: 0 Halt, 1 Singleuser, 2 Multiuser, 3-5 == 2, 6 Reboot - priority[$1] = [($2 == "S" ? :start : :stop), $3] - end - if line =~ UPDATE_RC_D_ENABLED_MATCHES - enabled = true - end - end + levels = parse_init_file(@init_command) + levels.each do |level| + rc_files.push Dir.glob("/etc/rc#{level}.d/[SK][0-9][0-9]#{current_resource.service_name}") + end + + rc_files.flatten.each do |line| + if UPDATE_RC_D_PRIORITIES =~ line + # priority[runlevel] = [ S|K, priority ] + # S = Start, K = Kill + # debian runlevels: 0 Halt, 1 Singleuser, 2 Multiuser, 3-5 == 2, 6 Reboot + priority[$1] = [($2 == "S" ? :start : :stop), $3] end end @@ -98,21 +111,19 @@ class Chef priority = priority[2].last end - unless @rcd_status.exitstatus == 0 - @priority_success = false - end + @got_priority = true priority end def service_currently_enabled?(priority) enabled = false - priority.each { |runlevel, arguments| - Chef::Log.debug("#{new_resource} runlevel #{runlevel}, action #{arguments[0]}, priority #{arguments[1]}") + priority.each do |runlevel, arguments| + logger.trace("#{new_resource} runlevel #{runlevel}, action #{arguments[0]}, priority #{arguments[1]}") # if we are in a update-rc.d default startup runlevel && we start in this runlevel if %w{ 1 2 3 4 5 S }.include?(runlevel) && arguments[0] == :start enabled = true end - } + end enabled end @@ -125,11 +136,11 @@ class Chef priority_ok = @current_resource.priority == new_resource.priority end if current_resource.enabled && priority_ok - Chef::Log.debug("#{new_resource} already enabled - nothing to do") + logger.trace("#{new_resource} already enabled - nothing to do") else converge_by("enable service #{new_resource}") do enable_service - Chef::Log.info("#{new_resource} enabled") + logger.info("#{new_resource} enabled") end end load_new_resource_state diff --git a/lib/chef/provider/service/freebsd.rb b/lib/chef/provider/service/freebsd.rb index 76d8c1d17b..68de39fd0f 100644 --- a/lib/chef/provider/service/freebsd.rb +++ b/lib/chef/provider/service/freebsd.rb @@ -18,7 +18,6 @@ require "chef/resource/service" require "chef/provider/service/init" -require "chef/mixin/command" class Chef class Provider @@ -48,7 +47,7 @@ class Chef return current_resource unless init_command - Chef::Log.debug("#{current_resource} found at #{init_command}") + logger.trace("#{current_resource} found at #{init_command}") @status_load_success = true determine_current_status! # see Chef::Provider::Service::Simple @@ -74,7 +73,7 @@ class Chef end requirements.assert(:start, :enable, :reload, :restart) do |a| - a.assertion { service_enable_variable_name != nil } + a.assertion { !service_enable_variable_name.nil? } a.failure_message Chef::Exceptions::Service, "Could not find the service name in #{init_command} and rcvar" # No recovery in whyrun mode - the init file is present but not correct. end @@ -146,7 +145,7 @@ class Chef end # some scripts support multiple instances through symlinks such as openvpn. # We should get the service name from rcvar. - Chef::Log.debug("name=\"service\" not found at #{init_command}. falling back to rcvar") + logger.trace("name=\"service\" not found at #{init_command}. falling back to rcvar") shell_out!("#{init_command} rcvar").stdout[/(\w+_enable)=/, 1] else # for why-run mode when the rcd_script is not there yet @@ -172,7 +171,7 @@ class Chef end if current_resource.enabled.nil? - Chef::Log.debug("#{new_resource.name} enable/disable state unknown") + logger.trace("#{new_resource.name} enable/disable state unknown") current_resource.enabled false end end diff --git a/lib/chef/provider/service/gentoo.rb b/lib/chef/provider/service/gentoo.rb index 8fb6d1f9af..69b3d20a3f 100644 --- a/lib/chef/provider/service/gentoo.rb +++ b/lib/chef/provider/service/gentoo.rb @@ -18,7 +18,6 @@ # require "chef/provider/service/init" -require "chef/mixin/command" require "chef/util/path_helper" class Chef::Provider::Service::Gentoo < Chef::Provider::Service::Init @@ -37,11 +36,11 @@ class Chef::Provider::Service::Gentoo < Chef::Provider::Service::Init @found_script = true exists = ::File.exists? file readable = ::File.readable? file - Chef::Log.debug "#{@new_resource} exists: #{exists}, readable: #{readable}" + logger.trace "#{@new_resource} exists: #{exists}, readable: #{readable}" exists && readable end ) - Chef::Log.debug "#{@new_resource} enabled: #{@current_resource.enabled}" + logger.trace "#{@new_resource} enabled: #{@current_resource.enabled}" @current_resource end @@ -61,11 +60,11 @@ class Chef::Provider::Service::Gentoo < Chef::Provider::Service::Init end end - def enable_service() + def enable_service shell_out!("/sbin/rc-update add #{@new_resource.service_name} default") end - def disable_service() + def disable_service shell_out!("/sbin/rc-update del #{@new_resource.service_name} default") end end diff --git a/lib/chef/provider/service/init.rb b/lib/chef/provider/service/init.rb index dff627d016..c6c582f8b8 100644 --- a/lib/chef/provider/service/init.rb +++ b/lib/chef/provider/service/init.rb @@ -17,7 +17,6 @@ # require "chef/provider/service/simple" -require "chef/mixin/command" require "chef/platform/service_helpers" class Chef diff --git a/lib/chef/provider/service/insserv.rb b/lib/chef/provider/service/insserv.rb index 76b2ee7477..a8e841f8b3 100644 --- a/lib/chef/provider/service/insserv.rb +++ b/lib/chef/provider/service/insserv.rb @@ -1,6 +1,6 @@ # # Author:: Bryan McLellan <btm@loftninjas.org> -# Copyright:: Copyright 2011-2016, Chef Software Inc. +# Copyright:: Copyright 2011-2017, Chef Software Inc. # License:: Apache License, Version 2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -24,7 +24,7 @@ class Chef class Service class Insserv < Chef::Provider::Service::Init - provides :service, platform_family: %w{debian rhel fedora suse} do |node| + provides :service, platform_family: %w{debian rhel fedora suse amazon} do |node| Chef::Platform::ServiceHelpers.service_resource_providers.include?(:insserv) end @@ -45,12 +45,12 @@ class Chef current_resource end - def enable_service() + def enable_service shell_out!("/sbin/insserv -r -f #{new_resource.service_name}") shell_out!("/sbin/insserv -d -f #{new_resource.service_name}") end - def disable_service() + def disable_service shell_out!("/sbin/insserv -r -f #{new_resource.service_name}") end end diff --git a/lib/chef/provider/service/macosx.rb b/lib/chef/provider/service/macosx.rb index 648cd9748b..40021f9ba6 100644 --- a/lib/chef/provider/service/macosx.rb +++ b/lib/chef/provider/service/macosx.rb @@ -28,7 +28,7 @@ class Chef class Service class Macosx < Chef::Provider::Service::Simple - provides :macosx_service, os: "darwin" + provides :macosx_service provides :service, os: "darwin" def self.gather_plist_dirs @@ -52,21 +52,22 @@ class Chef @plist_size = 0 @plist = @new_resource.plist ? @new_resource.plist : find_service_plist @service_label = find_service_label - # LauchAgents should be loaded as the console user. + # LaunchAgents should be loaded as the console user. @console_user = @plist ? @plist.include?("LaunchAgents") : false @session_type = @new_resource.session_type if @console_user - @console_user = Etc.getlogin - Chef::Log.debug("#{new_resource} console_user: '#{@console_user}'") + @console_user = Etc.getpwuid(::File.stat("/dev/console").uid).name + logger.trace("#{new_resource} console_user: '#{@console_user}'") cmd = "su " param = this_version_or_newer?("10.10") ? "" : "-l " + param = "-l " if this_version_or_newer?("10.12") @base_user_cmd = cmd + param + "#{@console_user} -c" - # Default LauchAgent session should be Aqua + # Default LaunchAgent session should be Aqua @session_type = "Aqua" if @session_type.nil? end - Chef::Log.debug("#{new_resource} Plist: '#{@plist}' service_label: '#{@service_label}'") + logger.trace("#{new_resource} Plist: '#{@plist}' service_label: '#{@service_label}'") set_service_status @current_resource @@ -107,7 +108,7 @@ class Chef def start_service if @current_resource.running - Chef::Log.debug("#{@new_resource} already running, not starting") + logger.trace("#{@new_resource} already running, not starting") else if @new_resource.start_command super @@ -119,7 +120,7 @@ class Chef def stop_service unless @current_resource.running - Chef::Log.debug("#{@new_resource} not running, not stopping") + logger.trace("#{@new_resource} not running, not stopping") else if @new_resource.stop_command super @@ -146,7 +147,7 @@ class Chef # supervisor that will restart daemons that are crashing, etc. def enable_service if @current_resource.enabled - Chef::Log.debug("#{@new_resource} already enabled, not enabling") + logger.trace("#{@new_resource} already enabled, not enabling") else load_service end @@ -154,7 +155,7 @@ class Chef def disable_service unless @current_resource.enabled - Chef::Log.debug("#{@new_resource} not enabled, not disabling") + logger.trace("#{@new_resource} not enabled, not disabling") else unload_service end @@ -181,7 +182,7 @@ class Chef end def set_service_status - return if @plist == nil || @service_label.to_s.empty? + return if @plist.nil? || @service_label.to_s.empty? cmd = "launchctl list #{@service_label}" res = shell_out_as_user(cmd) @@ -197,8 +198,8 @@ class Chef case line.downcase when /\s+\"pid\"\s+=\s+(\d+).*/ pid = $1 - @current_resource.running(!pid.to_i.zero?) - Chef::Log.debug("Current PID for #{@service_label} is #{pid}") + @current_resource.running(pid.to_i != 0) + logger.trace("Current PID for #{@service_label} is #{pid}") end end else diff --git a/lib/chef/provider/service/openbsd.rb b/lib/chef/provider/service/openbsd.rb index c60bbf170c..552173fbee 100644 --- a/lib/chef/provider/service/openbsd.rb +++ b/lib/chef/provider/service/openbsd.rb @@ -16,7 +16,6 @@ # limitations under the License. # -require "chef/mixin/command" require "chef/mixin/shell_out" require "chef/provider/service/init" require "chef/resource/service" @@ -49,7 +48,7 @@ class Chef @current_resource = Chef::Resource::Service.new(new_resource.name) current_resource.service_name(new_resource.service_name) - Chef::Log.debug("#{current_resource} found at #{init_command}") + logger.trace("#{current_resource} found at #{init_command}") determine_current_status! determine_enabled_status! @@ -72,7 +71,7 @@ class Chef end requirements.assert(:start, :enable, :reload, :restart) do |a| - a.assertion { init_command && builtin_service_enable_variable_name != nil } + a.assertion { init_command && !builtin_service_enable_variable_name.nil? } a.failure_message Chef::Exceptions::Service, "Could not find the service name in #{init_command} and rcvar" # No recovery in whyrun mode - the init file is present but not correct. end diff --git a/lib/chef/provider/service/redhat.rb b/lib/chef/provider/service/redhat.rb index 200a2d3400..1da3d7c01a 100644 --- a/lib/chef/provider/service/redhat.rb +++ b/lib/chef/provider/service/redhat.rb @@ -1,6 +1,6 @@ # # Author:: AJ Christensen (<aj@hjksolutions.com>) -# Copyright:: Copyright 2008-2016, Chef Software, Inc. +# Copyright:: Copyright 2008-2017, Chef Software Inc. # License:: Apache License, Version 2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -28,7 +28,7 @@ class Chef # @api private attr_accessor :current_run_levels - provides :service, platform_family: %w{rhel fedora suse} do |node| + provides :service, platform_family: %w{rhel fedora suse amazon} do |node| Chef::Platform::ServiceHelpers.service_resource_providers.include?(:redhat) end @@ -109,7 +109,7 @@ class Chef (run_levels.nil? || run_levels.empty?) ? "" : "--level #{run_levels.join('')} " end - def enable_service() + def enable_service unless run_levels.nil? || run_levels.empty? disable_levels = current_run_levels - run_levels shell_out! "/sbin/chkconfig --level #{disable_levels.join('')} #{new_resource.service_name} off" unless disable_levels.empty? @@ -117,7 +117,7 @@ class Chef shell_out! "/sbin/chkconfig #{levels}#{new_resource.service_name} on" end - def disable_service() + def disable_service shell_out! "/sbin/chkconfig #{levels}#{new_resource.service_name} off" end end diff --git a/lib/chef/provider/service/simple.rb b/lib/chef/provider/service/simple.rb index fe4768b2e8..7b5f75c4b3 100644 --- a/lib/chef/provider/service/simple.rb +++ b/lib/chef/provider/service/simple.rb @@ -18,7 +18,6 @@ require "chef/provider/service" require "chef/resource/service" -require "chef/mixin/command" class Chef class Provider @@ -41,10 +40,6 @@ class Chef @current_resource end - def whyrun_supported? - true - end - def shared_resource_requirements super requirements.assert(:all_actions) do |a| @@ -76,8 +71,9 @@ class Chef end requirements.assert(:all_actions) do |a| - a.assertion { @new_resource.status_command || supports[:status] || - (!ps_cmd.nil? && !ps_cmd.empty?) } + a.assertion do + @new_resource.status_command || supports[:status] || + (!ps_cmd.nil? && !ps_cmd.empty?) end a.failure_message Chef::Exceptions::Service, "#{@new_resource} could not determine how to inspect the process table, please set this node's 'command.ps' attribute" end requirements.assert(:all_actions) do |a| @@ -108,16 +104,16 @@ class Chef shell_out_with_systems_locale!(@new_resource.reload_command) end - protected + protected def determine_current_status! if @new_resource.status_command - Chef::Log.debug("#{@new_resource} you have specified a status command, running..") + logger.trace("#{@new_resource} you have specified a status command, running..") begin if shell_out(@new_resource.status_command).exitstatus == 0 @current_resource.running true - Chef::Log.debug("#{@new_resource} is running") + logger.trace("#{@new_resource} is running") end rescue Mixlib::ShellOut::ShellCommandFailed, SystemCallError # ShellOut sometimes throws different types of Exceptions than ShellCommandFailed. @@ -129,11 +125,11 @@ class Chef end elsif supports[:status] - Chef::Log.debug("#{@new_resource} supports status, running") + logger.trace("#{@new_resource} supports status, running") begin if shell_out("#{default_init_command} status").exitstatus == 0 @current_resource.running true - Chef::Log.debug("#{@new_resource} is running") + logger.trace("#{@new_resource} is running") end # ShellOut sometimes throws different types of Exceptions than ShellCommandFailed. # Temporarily catching different types of exceptions here until we get Shellout fixed. @@ -144,9 +140,9 @@ class Chef nil end else - Chef::Log.debug "#{@new_resource} falling back to process table inspection" + logger.trace "#{@new_resource} falling back to process table inspection" r = Regexp.new(@new_resource.pattern) - Chef::Log.debug "#{@new_resource} attempting to match '#{@new_resource.pattern}' (#{r.inspect}) against process list" + logger.trace "#{@new_resource} attempting to match '#{@new_resource.pattern}' (#{r.inspect}) against process list" begin shell_out!(ps_cmd).stdout.each_line do |line| if r.match(line) @@ -156,7 +152,7 @@ class Chef end @current_resource.running false unless @current_resource.running - Chef::Log.debug "#{@new_resource} running: #{@current_resource.running}" + logger.trace "#{@new_resource} running: #{@current_resource.running}" # ShellOut sometimes throws different types of Exceptions than ShellCommandFailed. # Temporarily catching different types of exceptions here until we get Shellout fixed. # TODO: Remove the line before one we get the ShellOut fix. diff --git a/lib/chef/provider/service/solaris.rb b/lib/chef/provider/service/solaris.rb index 1e5398eba8..f2b1ec4262 100644 --- a/lib/chef/provider/service/solaris.rb +++ b/lib/chef/provider/service/solaris.rb @@ -18,7 +18,6 @@ require "chef/provider/service" require "chef/resource/service" -require "chef/mixin/command" class Chef class Provider @@ -32,7 +31,7 @@ class Chef super @init_command = "/usr/sbin/svcadm" @status_command = "/bin/svcs" - @maintenace = false + @maintenance = false end def load_current_resource @@ -40,7 +39,7 @@ class Chef @current_resource.service_name(@new_resource.service_name) [@init_command, @status_command].each do |cmd| - unless ::File.executable? cmd then + unless ::File.executable? cmd raise Chef::Exceptions::Service, "#{cmd} not executable!" end end @@ -55,12 +54,16 @@ class Chef end def enable_service + # Running service status to update maintenance status to invoke svcadm clear + service_status shell_out!(default_init_command, "clear", @new_resource.service_name) if @maintenance - shell_out!(default_init_command, "enable", "-s", @new_resource.service_name) + enable_flags = [ "-s", @new_resource.options ].flatten.compact + shell_out!(default_init_command, "enable", *enable_flags, @new_resource.service_name) end def disable_service - shell_out!(default_init_command, "disable", "-s", @new_resource.service_name) + disable_flags = [ "-s", @new_resource.options ].flatten.compact + shell_out!(default_init_command, "disable", *disable_flags, @new_resource.service_name) end alias_method :stop_service, :disable_service @@ -73,7 +76,7 @@ class Chef def restart_service ## svcadm restart doesn't supports sync(-s) option disable_service - return enable_service + enable_service end def service_status @@ -92,6 +95,9 @@ class Chef # dependency require_all/error svc:/milestone/multi-user:default (online) # $ + # Set the default value for maintenance + @maintenance = false + # load output into hash status = {} cmd.stdout.each_line do |line| @@ -100,7 +106,6 @@ class Chef end # check service state - @maintenance = false case status["state"] when "online" @current_resource.enabled(true) diff --git a/lib/chef/provider/service/systemd.rb b/lib/chef/provider/service/systemd.rb index 1597d46a3d..1bcc2f3a00 100644 --- a/lib/chef/provider/service/systemd.rb +++ b/lib/chef/provider/service/systemd.rb @@ -20,6 +20,7 @@ require "chef/resource/service" require "chef/provider/service/simple" require "chef/mixin/which" +require "shellwords" class Chef::Provider::Service::Systemd < Chef::Provider::Service::Simple @@ -41,7 +42,7 @@ class Chef::Provider::Service::Systemd < Chef::Provider::Service::Simple @status_check_success = true if new_resource.status_command - Chef::Log.debug("#{new_resource} you have specified a status command, running..") + logger.trace("#{new_resource} you have specified a status command, running..") unless shell_out(new_resource.status_command).error? current_resource.running(true) @@ -76,12 +77,12 @@ class Chef::Provider::Service::Systemd < Chef::Provider::Service::Simple def get_systemctl_options_args if new_resource.user - uid = node["etc"]["passwd"][new_resource.user]["uid"] + uid = Etc.getpwuid(new_resource.user).uid options = { - "environment" => { + :environment => { "DBUS_SESSION_BUS_ADDRESS" => "unix:path=/run/user/#{uid}/bus", }, - "user" => new_resource.user, + :user => new_resource.user, } args = "--user" else @@ -89,31 +90,31 @@ class Chef::Provider::Service::Systemd < Chef::Provider::Service::Simple args = "--system" end - return options, args + [options, args] end def start_service if current_resource.running - Chef::Log.debug("#{new_resource} already running, not starting") + logger.trace("#{new_resource} already running, not starting") else if new_resource.start_command super else options, args = get_systemctl_options_args - shell_out_with_systems_locale!("#{systemctl_path} #{args} start #{new_resource.service_name}", options) + shell_out_with_systems_locale!("#{systemctl_path} #{args} start #{Shellwords.escape(new_resource.service_name)}", options) end end end def stop_service unless current_resource.running - Chef::Log.debug("#{new_resource} not running, not stopping") + logger.trace("#{new_resource} not running, not stopping") else if new_resource.stop_command super else options, args = get_systemctl_options_args - shell_out_with_systems_locale!("#{systemctl_path} #{args} stop #{new_resource.service_name}", options) + shell_out_with_systems_locale!("#{systemctl_path} #{args} stop #{Shellwords.escape(new_resource.service_name)}", options) end end end @@ -123,7 +124,7 @@ class Chef::Provider::Service::Systemd < Chef::Provider::Service::Simple super else options, args = get_systemctl_options_args - shell_out_with_systems_locale!("#{systemctl_path} #{args} restart #{new_resource.service_name}", options) + shell_out_with_systems_locale!("#{systemctl_path} #{args} restart #{Shellwords.escape(new_resource.service_name)}", options) end end @@ -133,7 +134,7 @@ class Chef::Provider::Service::Systemd < Chef::Provider::Service::Simple else if current_resource.running options, args = get_systemctl_options_args - shell_out_with_systems_locale!("#{systemctl_path} #{args} reload #{new_resource.service_name}", options) + shell_out_with_systems_locale!("#{systemctl_path} #{args} reload #{Shellwords.escape(new_resource.service_name)}", options) else start_service end @@ -142,37 +143,37 @@ class Chef::Provider::Service::Systemd < Chef::Provider::Service::Simple def enable_service options, args = get_systemctl_options_args - shell_out!("#{systemctl_path} #{args} enable #{new_resource.service_name}", options) + shell_out!("#{systemctl_path} #{args} enable #{Shellwords.escape(new_resource.service_name)}", options) end def disable_service options, args = get_systemctl_options_args - shell_out!("#{systemctl_path} #{args} disable #{new_resource.service_name}", options) + shell_out!("#{systemctl_path} #{args} disable #{Shellwords.escape(new_resource.service_name)}", options) end def mask_service options, args = get_systemctl_options_args - shell_out!("#{systemctl_path} #{args} mask #{new_resource.service_name}", options) + shell_out!("#{systemctl_path} #{args} mask #{Shellwords.escape(new_resource.service_name)}", options) end def unmask_service options, args = get_systemctl_options_args - shell_out!("#{systemctl_path} #{args} unmask #{new_resource.service_name}", options) + shell_out!("#{systemctl_path} #{args} unmask #{Shellwords.escape(new_resource.service_name)}", options) end def is_active? options, args = get_systemctl_options_args - shell_out("#{systemctl_path} #{args} is-active #{new_resource.service_name} --quiet", options).exitstatus == 0 + shell_out("#{systemctl_path} #{args} is-active #{Shellwords.escape(new_resource.service_name)} --quiet", options).exitstatus == 0 end def is_enabled? options, args = get_systemctl_options_args - shell_out("#{systemctl_path} #{args} is-enabled #{new_resource.service_name} --quiet", options).exitstatus == 0 + shell_out("#{systemctl_path} #{args} is-enabled #{Shellwords.escape(new_resource.service_name)} --quiet", options).exitstatus == 0 end def is_masked? options, args = get_systemctl_options_args - s = shell_out("#{systemctl_path} #{args} is-enabled #{new_resource.service_name}", options) + s = shell_out("#{systemctl_path} #{args} is-enabled #{Shellwords.escape(new_resource.service_name)}", options) s.exitstatus != 0 && s.stdout.include?("masked") end diff --git a/lib/chef/provider/service/upstart.rb b/lib/chef/provider/service/upstart.rb index 3ac5ff51da..810d9eabb7 100644 --- a/lib/chef/provider/service/upstart.rb +++ b/lib/chef/provider/service/upstart.rb @@ -18,7 +18,6 @@ require "chef/resource/service" require "chef/provider/service/simple" -require "chef/mixin/command" require "chef/util/file_edit" class Chef @@ -26,12 +25,16 @@ class Chef class Service class Upstart < Chef::Provider::Service::Simple + # to maintain a local state of service across restart's internal calls + attr_accessor :upstart_service_running + provides :service, platform_family: "debian", override: true do |node| Chef::Platform::ServiceHelpers.service_resource_providers.include?(:upstart) end UPSTART_STATE_FORMAT = /\S+ \(?(start|stop)?\)? ?[\/ ](\w+)/ + # Returns true if the configs for the service name has upstart variable def self.supports?(resource, action) Chef::Platform::ServiceHelpers.config_for_service(resource.service_name).include?(:upstart) end @@ -80,8 +83,11 @@ class Chef shared_resource_requirements requirements.assert(:all_actions) do |a| if !@command_success - whyrun_msg = @new_resource.status_command ? "Provided status command #{@new_resource.status_command} failed." : - "Could not determine upstart state for service" + whyrun_msg = if @new_resource.status_command + "Provided status command #{@new_resource.status_command} failed." + else + "Could not determine upstart state for service" + end end a.assertion { @command_success } # no failure here, just document the assumptions made. @@ -103,42 +109,42 @@ class Chef # We do not support searching for a service via ps when using upstart since status is a native # upstart function. We will however support status_command in case someone wants to do something special. if @new_resource.status_command - Chef::Log.debug("#{@new_resource} you have specified a status command, running..") + logger.trace("#{@new_resource} you have specified a status command, running..") begin if shell_out!(@new_resource.status_command).exitstatus == 0 - @current_resource.running true + @upstart_service_running = true end rescue @command_success = false - @current_resource.running false + @upstart_service_running = false nil end else begin - if upstart_state == "running" - @current_resource.running true + if upstart_goal_state == "start" + @upstart_service_running = true else - @current_resource.running false + @upstart_service_running = false end rescue Chef::Exceptions::Exec @command_success = false - @current_resource.running false + @upstart_service_running = false nil end end # Get enabled/disabled state by reading job configuration file if ::File.exists?("#{@upstart_job_dir}/#{@new_resource.service_name}#{@upstart_conf_suffix}") - Chef::Log.debug("#{@new_resource} found #{@upstart_job_dir}/#{@new_resource.service_name}#{@upstart_conf_suffix}") + logger.trace("#{@new_resource} found #{@upstart_job_dir}/#{@new_resource.service_name}#{@upstart_conf_suffix}") ::File.open("#{@upstart_job_dir}/#{@new_resource.service_name}#{@upstart_conf_suffix}", "r") do |file| while line = file.gets case line when /^start on/ - Chef::Log.debug("#{@new_resource} enabled: #{line.chomp}") + logger.trace("#{@new_resource} enabled: #{line.chomp}") @current_resource.enabled true break when /^#start on/ - Chef::Log.debug("#{@new_resource} disabled: #{line.chomp}") + logger.trace("#{@new_resource} disabled: #{line.chomp}") @current_resource.enabled false break end @@ -146,18 +152,19 @@ class Chef end else @config_file_found = false - Chef::Log.debug("#{@new_resource} did not find #{@upstart_job_dir}/#{@new_resource.service_name}#{@upstart_conf_suffix}") + logger.trace("#{@new_resource} did not find #{@upstart_job_dir}/#{@new_resource.service_name}#{@upstart_conf_suffix}") @current_resource.enabled false end + @current_resource.running @upstart_service_running @current_resource end def start_service # Calling start on a service that is already started will return 1 # Our 'goal' when we call start is to ensure the service is started - if @current_resource.running - Chef::Log.debug("#{@new_resource} already running, not starting") + if @upstart_service_running + logger.trace("#{@new_resource} already running, not starting") else if @new_resource.start_command super @@ -165,13 +172,15 @@ class Chef shell_out_with_systems_locale!("/sbin/start #{@job}") end end + + @upstart_service_running = true end def stop_service # Calling stop on a service that is already stopped will return 1 # Our 'goal' when we call stop is to ensure the service is stopped - unless @current_resource.running - Chef::Log.debug("#{@new_resource} not running, not stopping") + unless @upstart_service_running + logger.trace("#{@new_resource} not running, not stopping") else if @new_resource.stop_command super @@ -179,6 +188,8 @@ class Chef shell_out_with_systems_locale!("/sbin/stop #{@job}") end end + + @upstart_service_running = false end def restart_service @@ -186,13 +197,19 @@ class Chef super # Upstart always provides restart functionality so we don't need to mimic it with stop/sleep/start. # Older versions of upstart would fail on restart if the service was currently stopped, check for that. LP:430883 + # But for safe working of latest upstart job config being loaded, 'restart' can't be used as per link + # http://upstart.ubuntu.com/cookbook/#restart (it doesn't uses latest jon config from disk but retains old) else - if @current_resource.running - shell_out_with_systems_locale!("/sbin/restart #{@job}") + if @upstart_service_running + stop_service + sleep 1 + start_service else start_service end end + + @upstart_service_running = true end def reload_service @@ -202,37 +219,38 @@ class Chef # upstart >= 0.6.3-4 supports reload (HUP) shell_out_with_systems_locale!("/sbin/reload #{@job}") end + + @upstart_service_running = true end # https://bugs.launchpad.net/upstart/+bug/94065 def enable_service - Chef::Log.debug("#{@new_resource} upstart lacks inherent support for enabling services, editing job config file") + logger.trace("#{@new_resource} upstart lacks inherent support for enabling services, editing job config file") conf = Chef::Util::FileEdit.new("#{@upstart_job_dir}/#{@new_resource.service_name}#{@upstart_conf_suffix}") conf.search_file_replace(/^#start on/, "start on") conf.write_file end def disable_service - Chef::Log.debug("#{@new_resource} upstart lacks inherent support for disabling services, editing job config file") + logger.trace("#{@new_resource} upstart lacks inherent support for disabling services, editing job config file") conf = Chef::Util::FileEdit.new("#{@upstart_job_dir}/#{@new_resource.service_name}#{@upstart_conf_suffix}") conf.search_file_replace(/^start on/, "#start on") conf.write_file end - def upstart_state + def upstart_goal_state command = "/sbin/status #{@job}" - status = popen4(command) do |pid, stdin, stdout, stderr| - stdout.each_line do |line| - # service goal/state - # OR - # service (instance) goal/state - # OR - # service (goal) state - line =~ UPSTART_STATE_FORMAT - data = Regexp.last_match - return data[2] - end + so = shell_out(command) + so.stdout.each_line do |line| + # service goal/state + # OR + # service (instance) goal/state + # OR + # service (goal) state + line =~ UPSTART_STATE_FORMAT + data = Regexp.last_match + return data[1] end end diff --git a/lib/chef/provider/service/windows.rb b/lib/chef/provider/service/windows.rb index 9bfd9238cd..417ec03ef4 100644 --- a/lib/chef/provider/service/windows.rb +++ b/lib/chef/provider/service/windows.rb @@ -2,7 +2,7 @@ # Author:: Nuo Yan <nuo@chef.io> # Author:: Bryan McLellan <btm@loftninjas.org> # Author:: Seth Chisamore <schisamo@chef.io> -# Copyright:: Copyright 2010-2016, Chef Software Inc. +# Copyright:: Copyright 2010-2017, Chef Software Inc. # License:: Apache License, Version 2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -19,6 +19,7 @@ # require "chef/provider/service/simple" +require "chef/win32_service_constants" if RUBY_PLATFORM =~ /mswin|mingw32|windows/ require "chef/win32/error" require "win32/service" @@ -26,10 +27,11 @@ end class Chef::Provider::Service::Windows < Chef::Provider::Service provides :service, os: "windows" - provides :windows_service, os: "windows" + provides :windows_service include Chef::Mixin::ShellOut include Chef::ReservedNames::Win32::API::Error rescue LoadError + include Chef::Win32ServiceConstants #Win32::Service.get_start_type AUTO_START = "auto start" @@ -49,23 +51,34 @@ class Chef::Provider::Service::Windows < Chef::Provider::Service SERVICE_RIGHT = "SeServiceLogonRight" - def whyrun_supported? - false - end - def load_current_resource - @current_resource = Chef::Resource::WindowsService.new(@new_resource.name) - @current_resource.service_name(@new_resource.service_name) - @current_resource.running(current_state == RUNNING) - Chef::Log.debug "#{@new_resource} running: #{@current_resource.running}" - case current_start_type - when AUTO_START - @current_resource.enabled(true) - when DISABLED - @current_resource.enabled(false) + @current_resource = Chef::Resource::WindowsService.new(new_resource.name) + current_resource.service_name(new_resource.service_name) + + if Win32::Service.exists?(current_resource.service_name) + current_resource.running(current_state == RUNNING) + logger.trace "#{new_resource} running: #{current_resource.running}" + case current_startup_type + when :automatic + current_resource.enabled(true) + when :disabled + current_resource.enabled(false) + end + logger.trace "#{new_resource} enabled: #{current_resource.enabled}" + + config_info = Win32::Service.config_info(current_resource.service_name) + current_resource.service_type(get_service_type(config_info.service_type)) if config_info.service_type + current_resource.startup_type(start_type_to_sym(config_info.start_type)) if config_info.start_type + current_resource.error_control(get_error_control(config_info.error_control)) if config_info.error_control + current_resource.binary_path_name(config_info.binary_path_name) if config_info.binary_path_name + current_resource.load_order_group(config_info.load_order_group) if config_info.load_order_group + current_resource.dependencies(config_info.dependencies) if config_info.dependencies + current_resource.run_as_user(config_info.service_start_name) if config_info.service_start_name + current_resource.display_name(config_info.display_name) if config_info.display_name + current_resource.delayed_start(current_delayed_start) if current_delayed_start end - Chef::Log.debug "#{@new_resource} enabled: #{@current_resource.enabled}" - @current_resource + + current_resource end def start_service @@ -78,9 +91,10 @@ class Chef::Provider::Service::Windows < Chef::Provider::Service }.reject { |k, v| v.nil? || v.length == 0 } Win32::Service.configure(new_config) - Chef::Log.info "#{@new_resource} configured with #{new_config.inspect}" + logger.info "#{@new_resource} configured with #{new_config.inspect}" - if new_config.has_key?(:service_start_name) + # LocalSystem is the default runas user, which is a special service account that should ultimately have the rights of BUILTIN\Administrators, but we wouldn't see that from get_account_right + if new_config.has_key?(:service_start_name) && new_config[:service_start_name].casecmp("localsystem") != 0 unless Chef::ReservedNames::Win32::Security.get_account_right(canonicalize_username(new_config[:service_start_name])).include?(SERVICE_RIGHT) grant_service_logon(new_config[:service_start_name]) end @@ -88,13 +102,13 @@ class Chef::Provider::Service::Windows < Chef::Provider::Service state = current_state if state == RUNNING - Chef::Log.debug "#{@new_resource} already started - nothing to do" + logger.trace "#{@new_resource} already started - nothing to do" elsif state == START_PENDING - Chef::Log.debug "#{@new_resource} already sent start signal - waiting for start" + logger.trace "#{@new_resource} already sent start signal - waiting for start" wait_for_state(RUNNING) elsif state == STOPPED if @new_resource.start_command - Chef::Log.debug "#{@new_resource} starting service using the given start_command" + logger.trace "#{@new_resource} starting service using the given start_command" shell_out!(@new_resource.start_command) else spawn_command_thread do @@ -102,7 +116,7 @@ class Chef::Provider::Service::Windows < Chef::Provider::Service Win32::Service.start(@new_resource.service_name) rescue SystemCallError => ex if ex.errno == ERROR_SERVICE_LOGON_FAILED - Chef::Log.error ex.message + logger.error ex.message raise Chef::Exceptions::Service, "Service #{@new_resource} did not start due to a logon failure (error #{ERROR_SERVICE_LOGON_FAILED}): possibly the specified user '#{@new_resource.run_as_user}' does not have the 'log on as a service' privilege, or the password is incorrect." else @@ -117,7 +131,7 @@ class Chef::Provider::Service::Windows < Chef::Provider::Service raise Chef::Exceptions::Service, "Service #{@new_resource} can't be started from state [#{state}]" end else - Chef::Log.debug "#{@new_resource} does not exist - nothing to do" + logger.trace "#{@new_resource} does not exist - nothing to do" end end @@ -126,7 +140,7 @@ class Chef::Provider::Service::Windows < Chef::Provider::Service state = current_state if state == RUNNING if @new_resource.stop_command - Chef::Log.debug "#{@new_resource} stopping service using the given stop_command" + logger.trace "#{@new_resource} stopping service using the given stop_command" shell_out!(@new_resource.stop_command) else spawn_command_thread do @@ -136,22 +150,22 @@ class Chef::Provider::Service::Windows < Chef::Provider::Service end @new_resource.updated_by_last_action(true) elsif state == STOPPED - Chef::Log.debug "#{@new_resource} already stopped - nothing to do" + logger.trace "#{@new_resource} already stopped - nothing to do" elsif state == STOP_PENDING - Chef::Log.debug "#{@new_resource} already sent stop signal - waiting for stop" + logger.trace "#{@new_resource} already sent stop signal - waiting for stop" wait_for_state(STOPPED) else raise Chef::Exceptions::Service, "Service #{@new_resource} can't be stopped from state [#{state}]" end else - Chef::Log.debug "#{@new_resource} does not exist - nothing to do" + logger.trace "#{@new_resource} does not exist - nothing to do" end end def restart_service if Win32::Service.exists?(@new_resource.service_name) if @new_resource.restart_command - Chef::Log.debug "#{@new_resource} restarting service using the given restart_command" + logger.trace "#{@new_resource} restarting service using the given restart_command" shell_out!(@new_resource.restart_command) else stop_service @@ -159,7 +173,7 @@ class Chef::Provider::Service::Windows < Chef::Provider::Service end @new_resource.updated_by_last_action(true) else - Chef::Log.debug "#{@new_resource} does not exist - nothing to do" + logger.trace "#{@new_resource} does not exist - nothing to do" end end @@ -167,7 +181,7 @@ class Chef::Provider::Service::Windows < Chef::Provider::Service if Win32::Service.exists?(@new_resource.service_name) set_startup_type(:automatic) else - Chef::Log.debug "#{@new_resource} does not exist - nothing to do" + logger.trace "#{@new_resource} does not exist - nothing to do" end end @@ -175,62 +189,88 @@ class Chef::Provider::Service::Windows < Chef::Provider::Service if Win32::Service.exists?(@new_resource.service_name) set_startup_type(:disabled) else - Chef::Log.debug "#{@new_resource} does not exist - nothing to do" + logger.trace "#{@new_resource} does not exist - nothing to do" + end + end + + action :create do + if Win32::Service.exists?(new_resource.service_name) + logger.trace "#{new_resource} already exists - nothing to do" + return + end + + converge_by("create service #{new_resource.service_name}") do + Win32::Service.new(windows_service_config) end + + converge_delayed_start + end + + action :delete do + unless Win32::Service.exists?(new_resource.service_name) + logger.trace "#{new_resource} does not exist - nothing to do" + return + end + + converge_by("delete service #{new_resource.service_name}") do + Win32::Service.delete(new_resource.service_name) + end + end + + action :configure do + unless Win32::Service.exists?(new_resource.service_name) + logger.warn "#{new_resource} does not exist. Maybe you need to prepend action :create" + return + end + + # Until #6300 is solved this is required + if new_resource.run_as_user == new_resource.class.properties[:run_as_user].default + new_resource.run_as_user = new_resource.class.properties[:run_as_user].default + end + + converge_if_changed :service_type, :startup_type, :error_control, + :binary_path_name, :load_order_group, :dependencies, + :run_as_user, :display_name, :description do + Win32::Service.configure(windows_service_config(:configure)) + end + + converge_delayed_start end def action_enable - if current_start_type != AUTO_START + if current_startup_type != :automatic converge_by("enable service #{@new_resource}") do enable_service - Chef::Log.info("#{@new_resource} enabled") + logger.info("#{@new_resource} enabled") end else - Chef::Log.debug("#{@new_resource} already enabled - nothing to do") + logger.trace("#{@new_resource} already enabled - nothing to do") end load_new_resource_state @new_resource.enabled(true) end def action_disable - if current_start_type != DISABLED + if current_startup_type != :disabled converge_by("disable service #{@new_resource}") do disable_service - Chef::Log.info("#{@new_resource} disabled") + logger.info("#{@new_resource} disabled") end else - Chef::Log.debug("#{@new_resource} already disabled - nothing to do") + logger.trace("#{@new_resource} already disabled - nothing to do") end load_new_resource_state @new_resource.enabled(false) end def action_configure_startup - case @new_resource.startup_type - when :automatic - if current_start_type != AUTO_START - converge_by("set service #{@new_resource} startup type to automatic") do - set_startup_type(:automatic) - end - else - Chef::Log.debug("#{@new_resource} startup_type already automatic - nothing to do") - end - when :manual - if current_start_type != MANUAL - converge_by("set service #{@new_resource} startup type to manual") do - set_startup_type(:manual) - end - else - Chef::Log.debug("#{@new_resource} startup_type already manual - nothing to do") - end - when :disabled - if current_start_type != DISABLED - converge_by("set service #{@new_resource} startup type to disabled") do - set_startup_type(:disabled) - end - else - Chef::Log.debug("#{@new_resource} startup_type already disabled - nothing to do") + startup_type = @new_resource.startup_type + if current_startup_type != startup_type + converge_by("set service #{@new_resource} startup type to #{startup_type}") do + set_startup_type(startup_type) end + else + logger.trace("#{@new_resource} startup_type already #{startup_type} - nothing to do") end # Avoid changing enabled from true/false for now @@ -239,15 +279,23 @@ class Chef::Provider::Service::Windows < Chef::Provider::Service private + def current_delayed_start + if service = Win32::Service.services.find { |x| x.service_name == new_resource.service_name } + service.delayed_start == 0 ? false : true + else + nil + end + end + def grant_service_logon(username) begin Chef::ReservedNames::Win32::Security.add_account_right(canonicalize_username(username), SERVICE_RIGHT) rescue Chef::Exceptions::Win32APIError => err - Chef::Log.fatal "Logon-as-service grant failed with output: #{err}" + logger.fatal "Logon-as-service grant failed with output: #{err}" raise Chef::Exceptions::Service, "Logon-as-service grant failed for #{username}: #{err}" end - Chef::Log.info "Grant logon-as-service to user '#{username}' successful." + logger.info "Grant logon-as-service to user '#{username}' successful." true end @@ -264,8 +312,9 @@ class Chef::Provider::Service::Windows < Chef::Provider::Service Win32::Service.status(@new_resource.service_name).current_state end - def current_start_type - Win32::Service.config_info(@new_resource.service_name).start_type + def current_startup_type + start_type = Win32::Service.config_info(@new_resource.service_name).start_type + start_type_to_sym(start_type) end # Helper method that waits for a status to change its state since state @@ -293,21 +342,143 @@ class Chef::Provider::Service::Windows < Chef::Provider::Service end end - # Takes Win32::Service start_types - def set_startup_type(type) - # Set-Service Startup Type => Win32::Service Constant - allowed_types = { :automatic => Win32::Service::AUTO_START, - :manual => Win32::Service::DEMAND_START, - :disabled => Win32::Service::DISABLED } - unless allowed_types.keys.include?(type) + # @param type [Symbol] + # @return [Integer] + # @raise [Chef::Exceptions::ConfigurationError] if the startup type is + # not supported. + # @see Chef::Resource::WindowsService::ALLOWED_START_TYPES + def startup_type_to_int(type) + Chef::Resource::WindowsService::ALLOWED_START_TYPES.fetch(type) do raise Chef::Exceptions::ConfigurationError, "#{@new_resource.name}: Startup type '#{type}' is not supported" end + end - Chef::Log.debug "#{@new_resource.name} setting start_type to #{type}" + # Takes Win32::Service start_types + def set_startup_type(type) + startup_type = startup_type_to_int(type) + + logger.trace "#{@new_resource.name} setting start_type to #{type}" Win32::Service.configure( :service_name => @new_resource.service_name, - :start_type => allowed_types[type] + :start_type => startup_type ) @new_resource.updated_by_last_action(true) end + + def windows_service_config(action = :create) + config = {} + + config[:service_name] = new_resource.service_name + config[:display_name] = new_resource.display_name if new_resource.display_name + config[:service_type] = new_resource.service_type if new_resource.service_type + config[:start_type] = startup_type_to_int(new_resource.startup_type) if new_resource.startup_type + config[:error_control] = new_resource.error_control if new_resource.error_control + config[:binary_path_name] = new_resource.binary_path_name if new_resource.binary_path_name + config[:load_order_group] = new_resource.load_order_group if new_resource.load_order_group + config[:dependencies] = new_resource.dependencies if new_resource.dependencies + config[:service_start_name] = new_resource.run_as_user unless new_resource.run_as_user.empty? + config[:password] = new_resource.run_as_password unless new_resource.run_as_user.empty? || new_resource.run_as_password.empty? + config[:description] = new_resource.description if new_resource.description + + case action + when :create + config[:desired_access] = new_resource.desired_access if new_resource.desired_access + end + + config + end + + def converge_delayed_start + config = {} + config[:service_name] = new_resource.service_name + config[:delayed_start] = new_resource.delayed_start ? 1 : 0 + + # Until #6300 is solved this is required + if new_resource.delayed_start == new_resource.class.properties[:delayed_start].default + new_resource.delayed_start = new_resource.class.properties[:delayed_start].default + end + + converge_if_changed :delayed_start do + Win32::Service.configure(config) + end + end + + # @return [Symbol] + def start_type_to_sym(start_type) + case start_type + when "auto start" + :automatic + when "boot start" + raise("Unsupported start type, #{start_type}. Submit bug request to fix.") + when "demand start" + :manual + when "disabled" + :disabled + when "system start" + raise("Unsupported start type, #{start_type}. Submit bug request to fix.") + else + raise("Unsupported start type, #{start_type}. Submit bug request to fix.") + end + end + + def get_service_type(service_type) + case service_type + when "file system driver" + SERVICE_FILE_SYSTEM_DRIVER + when "kernel driver" + SERVICE_KERNEL_DRIVER + when "own process" + SERVICE_WIN32_OWN_PROCESS + when "share process" + SERVICE_WIN32_SHARE_PROCESS + when "recognizer driver" + SERVICE_RECOGNIZER_DRIVER + when "driver" + SERVICE_DRIVER + when "win32" + SERVICE_WIN32 + when "all" + SERVICE_TYPE_ALL + when "own process, interactive" + SERVICE_INTERACTIVE_PROCESS | SERVICE_WIN32_OWN_PROCESS + when "share process, interactive" + SERVICE_INTERACTIVE_PROCESS | SERVICE_WIN32_SHARE_PROCESS + else + raise("Unsupported service type, #{service_type}. Submit bug request to fix.") + end + end + + # @return [Integer] + def get_start_type(start_type) + case start_type + when "auto start" + SERVICE_AUTO_START + when "boot start" + SERVICE_BOOT_START + when "demand start" + SERVICE_DEMAND_START + when "disabled" + SERVICE_DISABLED + when "system start" + SERVICE_SYSTEM_START + else + raise("Unsupported start type, #{start_type}. Submit bug request to fix.") + end + end + + def get_error_control(error_control) + case error_control + when "critical" + SERVICE_ERROR_CRITICAL + when "ignore" + SERVICE_ERROR_IGNORE + when "normal" + SERVICE_ERROR_NORMAL + when "severe" + SERVICE_ERROR_SEVERE + else + nil + end + end + end diff --git a/lib/chef/provider/subversion.rb b/lib/chef/provider/subversion.rb index ea32283bc9..abcc260a78 100644 --- a/lib/chef/provider/subversion.rb +++ b/lib/chef/provider/subversion.rb @@ -1,6 +1,6 @@ # # Author:: Daniel DeLeo (<dan@kallistec.com>) -# Copyright:: Copyright 2008-2016, Chef Software Inc. +# Copyright:: Copyright 2008-2017, Chef Software Inc. # License:: Apache License, Version 2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -20,7 +20,6 @@ require "chef/log" require "chef/provider" -require "chef/mixin/command" require "chef-config/mixin/fuzzy_hostname_matcher" require "fileutils" @@ -32,19 +31,14 @@ class Chef SVN_INFO_PATTERN = /^([\w\s]+): (.+)$/ - include Chef::Mixin::Command include ChefConfig::Mixin::FuzzyHostnameMatcher - def whyrun_supported? - true - end - def load_current_resource - @current_resource = Chef::Resource::Subversion.new(@new_resource.name) + @current_resource = Chef::Resource::Subversion.new(new_resource.name) - unless [:export, :force_export].include?(Array(@new_resource.action).first) + unless [:export, :force_export].include?(Array(new_resource.action).first) if current_revision = find_current_revision - @current_resource.revision current_revision + current_resource.revision current_revision end end end @@ -53,21 +47,21 @@ class Chef requirements.assert(:all_actions) do |a| # Make sure the parent dir exists, or else fail. # for why run, print a message explaining the potential error. - parent_directory = ::File.dirname(@new_resource.destination) + parent_directory = ::File.dirname(new_resource.destination) a.assertion { ::File.directory?(parent_directory) } a.failure_message(Chef::Exceptions::MissingParentDirectory, - "Cannot clone #{@new_resource} to #{@new_resource.destination}, the enclosing directory #{parent_directory} does not exist") + "Cannot clone #{new_resource} to #{new_resource.destination}, the enclosing directory #{parent_directory} does not exist") a.whyrun("Directory #{parent_directory} does not exist, assuming it would have been created") end end def action_checkout if target_dir_non_existent_or_empty? - converge_by("perform checkout of #{@new_resource.repository} into #{@new_resource.destination}") do + converge_by("perform checkout of #{new_resource.repository} into #{new_resource.destination}") do shell_out!(checkout_command, run_options) end else - Chef::Log.debug "#{@new_resource} checkout destination #{@new_resource.destination} already exists or is a non-empty directory - nothing to do" + logger.trace "#{new_resource} checkout destination #{new_resource.destination} already exists or is a non-empty directory - nothing to do" end end @@ -75,25 +69,25 @@ class Chef if target_dir_non_existent_or_empty? action_force_export else - Chef::Log.debug "#{@new_resource} export destination #{@new_resource.destination} already exists or is a non-empty directory - nothing to do" + logger.trace "#{new_resource} export destination #{new_resource.destination} already exists or is a non-empty directory - nothing to do" end end def action_force_export - converge_by("export #{@new_resource.repository} into #{@new_resource.destination}") do + converge_by("export #{new_resource.repository} into #{new_resource.destination}") do shell_out!(export_command, run_options) end end def action_sync assert_target_directory_valid! - if ::File.exist?(::File.join(@new_resource.destination, ".svn")) + if ::File.exist?(::File.join(new_resource.destination, ".svn")) current_rev = find_current_revision - Chef::Log.debug "#{@new_resource} current revision: #{current_rev} target revision: #{revision_int}" + logger.trace "#{new_resource} current revision: #{current_rev} target revision: #{revision_int}" unless current_revision_matches_target_revision? - converge_by("sync #{@new_resource.destination} from #{@new_resource.repository}") do + converge_by("sync #{new_resource.destination} from #{new_resource.repository}") do shell_out!(sync_command, run_options) - Chef::Log.info "#{@new_resource} updated to revision: #{revision_int}" + logger.info "#{new_resource} updated to revision: #{revision_int}" end end else @@ -102,24 +96,24 @@ class Chef end def sync_command - c = scm :update, @new_resource.svn_arguments, verbose, authentication, proxy, "-r#{revision_int}", @new_resource.destination - Chef::Log.debug "#{@new_resource} updated working copy #{@new_resource.destination} to revision #{@new_resource.revision}" + c = scm :update, new_resource.svn_arguments, verbose, authentication, proxy, "-r#{revision_int}", new_resource.destination + logger.trace "#{new_resource} updated working copy #{new_resource.destination} to revision #{new_resource.revision}" c end def checkout_command - c = scm :checkout, @new_resource.svn_arguments, verbose, authentication, proxy, - "-r#{revision_int}", @new_resource.repository, @new_resource.destination - Chef::Log.info "#{@new_resource} checked out #{@new_resource.repository} at revision #{@new_resource.revision} to #{@new_resource.destination}" + c = scm :checkout, new_resource.svn_arguments, verbose, authentication, proxy, + "-r#{revision_int}", new_resource.repository, new_resource.destination + logger.info "#{new_resource} checked out #{new_resource.repository} at revision #{new_resource.revision} to #{new_resource.destination}" c end def export_command args = ["--force"] - args << @new_resource.svn_arguments << verbose << authentication << proxy << - "-r#{revision_int}" << @new_resource.repository << @new_resource.destination + args << new_resource.svn_arguments << verbose << authentication << proxy << + "-r#{revision_int}" << new_resource.repository << new_resource.destination c = scm :export, *args - Chef::Log.info "#{@new_resource} exported #{@new_resource.repository} at revision #{@new_resource.revision} to #{@new_resource.destination}" + logger.info "#{new_resource} exported #{new_resource.repository} at revision #{new_resource.revision} to #{new_resource.destination}" c end @@ -128,10 +122,10 @@ class Chef # If the specified revision is an integer, trust it. def revision_int @revision_int ||= begin - if @new_resource.revision =~ /^\d+$/ - @new_resource.revision + if new_resource.revision =~ /^\d+$/ + new_resource.revision else - command = scm(:info, @new_resource.repository, @new_resource.svn_info_args, authentication, "-r#{@new_resource.revision}") + command = scm(:info, new_resource.repository, new_resource.svn_info_args, authentication, "-r#{new_resource.revision}") svn_info = shell_out!(command, run_options(:cwd => cwd, :returns => [0, 1])).stdout extract_revision_info(svn_info) @@ -142,7 +136,7 @@ class Chef alias :revision_slug :revision_int def find_current_revision - return nil unless ::File.exist?(::File.join(@new_resource.destination, ".svn")) + return nil unless ::File.exist?(::File.join(new_resource.destination, ".svn")) command = scm(:info) svn_info = shell_out!(command, run_options(:cwd => cwd, :returns => [0, 1])).stdout @@ -150,20 +144,20 @@ class Chef end def current_revision_matches_target_revision? - (!@current_resource.revision.nil?) && (revision_int.strip.to_i == @current_resource.revision.strip.to_i) + (!current_resource.revision.nil?) && (revision_int.strip.to_i == current_resource.revision.strip.to_i) end def run_options(run_opts = {}) - run_opts[:user] = @new_resource.user if @new_resource.user - run_opts[:group] = @new_resource.group if @new_resource.group - run_opts[:timeout] = @new_resource.timeout if @new_resource.timeout + run_opts[:user] = new_resource.user if new_resource.user + run_opts[:group] = new_resource.group if new_resource.group + run_opts[:timeout] = new_resource.timeout if new_resource.timeout run_opts end private def cwd - @new_resource.destination + new_resource.destination end def verbose @@ -181,7 +175,7 @@ class Chef rev = (repo_attrs["Last Changed Rev"] || repo_attrs["Revision"]) rev.strip! if rev raise "Could not parse `svn info` data: #{svn_info}" if repo_attrs.empty? - Chef::Log.debug "#{@new_resource} resolved revision #{@new_resource.revision} to #{rev}" + logger.trace "#{new_resource} resolved revision #{new_resource.revision} to #{rev}" rev end @@ -190,14 +184,14 @@ class Chef # switch, since Capistrano will check for that prompt in the output # and will respond appropriately. def authentication - return "" unless @new_resource.svn_username - result = "--username #{@new_resource.svn_username} " - result << "--password #{@new_resource.svn_password} " + return "" unless new_resource.svn_username + result = "--username #{new_resource.svn_username} " + result << "--password #{new_resource.svn_password} " result end def proxy - repo_uri = URI.parse(@new_resource.repository) + repo_uri = URI.parse(new_resource.repository) proxy_uri = Chef::Config.proxy_uri(repo_uri.scheme, repo_uri.host, repo_uri.port) return "" if proxy_uri.nil? @@ -213,18 +207,18 @@ class Chef end def target_dir_non_existent_or_empty? - !::File.exist?(@new_resource.destination) || Dir.entries(@new_resource.destination).sort == [".", ".."] + !::File.exist?(new_resource.destination) || Dir.entries(new_resource.destination).sort == [".", ".."] end def svn_binary - @new_resource.svn_binary || + new_resource.svn_binary || (Chef::Platform.windows? ? "svn.exe" : "svn") end def assert_target_directory_valid! - target_parent_directory = ::File.dirname(@new_resource.destination) + target_parent_directory = ::File.dirname(new_resource.destination) unless ::File.directory?(target_parent_directory) - msg = "Cannot clone #{@new_resource} to #{@new_resource.destination}, the enclosing directory #{target_parent_directory} does not exist" + msg = "Cannot clone #{new_resource} to #{new_resource.destination}, the enclosing directory #{target_parent_directory} does not exist" raise Chef::Exceptions::MissingParentDirectory, msg end end diff --git a/lib/chef/provider/support/yum_repo.erb b/lib/chef/provider/support/yum_repo.erb new file mode 100644 index 0000000000..f60d8688da --- /dev/null +++ b/lib/chef/provider/support/yum_repo.erb @@ -0,0 +1,138 @@ +# This file was generated by Chef +# Do NOT modify this file by hand. + +[<%= @config.repositoryid %>] +name=<%= @config.description %> +<% if @config.baseurl %> +baseurl=<%= case @config.baseurl + when Array + @config.baseurl.join("\n") + else + @config.baseurl + end %> +<% end -%> +<% if @config.cost %> +cost=<%= @config.cost %> +<% end %> +<% if @config.enabled %> +enabled=1 +<% else %> +enabled=0 +<% end %> +<% if @config.enablegroups %> +enablegroups=1 +<% end %> +<% if @config.exclude %> +exclude=<%= @config.exclude %> +<% end %> +<% if @config.failovermethod %> +failovermethod=<%= @config.failovermethod %> +<% end %> +<% if @config.fastestmirror_enabled %> +fastestmirror_enabled=1 +<% else %> +fastestmirror_enabled=0 +<% end %> +<% if @config.gpgcheck %> +gpgcheck=1 +<% else %> +gpgcheck=0 +<% end %> +<% if @config.gpgkey %> +gpgkey=<%= case @config.gpgkey + when Array + @config.gpgkey.join("\n ") + else + @config.gpgkey + end %> +<% end -%> +<% if @config.http_caching %> +http_caching=<%= @config.http_caching %> +<% end %> +<% if @config.include_config %> +include=<%= @config.include_config %> +<% end %> +<% if @config.includepkgs %> +includepkgs=<%= @config.includepkgs %> +<% end %> +<% if @config.keepalive %> +keepalive=1 +<% end %> +<% if @config.metadata_expire %> +metadata_expire=<%= @config.metadata_expire %> +<% end %> +<% if @config.metalink %> +metalink=<%= @config.metalink %> +<% end %> +<% if @config.mirrorlist %> +mirrorlist=<%= @config.mirrorlist %> +<% end %> +<% if @config.mirror_expire %> +mirror_expire=<%= @config.mirror_expire %> +<% end %> +<% if @config.mirrorlist_expire %> +mirrorlist_expire=<%= @config.mirrorlist_expire %> +<% end %> +<% if @config.priority %> +priority=<%= @config.priority %> +<% end %> +<% if @config.proxy %> +proxy=<%= @config.proxy %> +<% end %> +<% if @config.proxy_username %> +proxy_username=<%= @config.proxy_username %> +<% end %> +<% if @config.proxy_password %> +proxy_password=<%= @config.proxy_password %> +<% end %> +<% if @config.username %> +username=<%= @config.username %> +<% end %> +<% if @config.password %> +password=<%= @config.password %> +<% end %> +<% if @config.repo_gpgcheck %> +repo_gpgcheck=1 +<% end %> +<% if @config.max_retries %> +retries=<%= @config.max_retries %> +<% end %> +<% if @config.report_instanceid %> +report_instanceid=<%= @config.report_instanceid %> +<% end %> +<% if @config.skip_if_unavailable %> +skip_if_unavailable=1 +<% end %> +<% if @config.sslcacert %> +sslcacert=<%= @config.sslcacert %> +<% end %> +<% if @config.sslclientcert %> +sslclientcert=<%= @config.sslclientcert %> +<% end %> +<% if @config.sslclientkey %> +sslclientkey=<%= @config.sslclientkey %> +<% end %> +<% unless @config.sslverify.nil? %> +sslverify=<%= ( @config.sslverify ) ? 'true' : 'false' %> +<% end %> +<% if @config.throttle %> +throttle=<%= @config.throttle %> +<% end %> +<% if @config.timeout %> +timeout=<%= @config.timeout %> +<% end %> +<% if @config.options -%> +<% @config.options.each do |key, value| -%> +<%= key %>=<%= + case value + when Array + value.join("\n ") + when TrueClass + '1' + when FalseClass + '0' + else + value + end %> +<% end -%> +<% end -%> diff --git a/lib/chef/provider/support/zypper_repo.erb b/lib/chef/provider/support/zypper_repo.erb new file mode 100644 index 0000000000..6d508fa77f --- /dev/null +++ b/lib/chef/provider/support/zypper_repo.erb @@ -0,0 +1,17 @@ +# This file was generated by Chef +# Do NOT modify this file by hand. + +[<%= @config.repo_name %>] +<% %w{ type enabled autorefresh gpgcheck gpgkey baseurl mirrorlist path priority keeppackages mode refresh_cache }.each do |prop| -%> +<% next if @config.send(prop.to_sym).nil? -%> +<%= prop %>=<%= + case @config.send(prop.to_sym) + when TrueClass + '1' + when FalseClass + '0' + else + @config.send(prop.to_sym) + end %> +<% end -%> +name=<%= @config.description || @config.repo_name %> diff --git a/lib/chef/provider/systemd_unit.rb b/lib/chef/provider/systemd_unit.rb index db71a6c234..d8c83d2b4b 100644 --- a/lib/chef/provider/systemd_unit.rb +++ b/lib/chef/provider/systemd_unit.rb @@ -1,6 +1,6 @@ # # Author:: Nathan Williams (<nath.e.will@gmail.com>) -# Copyright:: Copyright 2016, Nathan Williams +# Copyright:: Copyright 2016-2018, Nathan Williams # License:: Apache License, Version 2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -20,6 +20,9 @@ require "chef/provider" require "chef/mixin/which" require "chef/mixin/shell_out" require "chef/resource/file" +require "chef/resource/file/verification/systemd_unit" +require "iniparse" +require "shellwords" class Chef class Provider @@ -27,11 +30,12 @@ class Chef include Chef::Mixin::Which include Chef::Mixin::ShellOut - provides :systemd_unit, os: "linux" + provides :systemd_unit def load_current_resource @current_resource = Chef::Resource::SystemdUnit.new(new_resource.name) + current_resource.unit_name(new_resource.unit_name) current_resource.content(::File.read(unit_path)) if ::File.exist?(unit_path) current_resource.user(new_resource.user) current_resource.enabled(enabled?) @@ -43,9 +47,18 @@ class Chef current_resource end + def define_resource_requirements + super + + requirements.assert(:create) do |a| + a.assertion { IniParse.parse(new_resource.to_ini) } + a.failure_message "Unit content is not valid INI text" + end + end + def action_create if current_resource.content != new_resource.to_ini - converge_by("creating unit: #{new_resource.name}") do + converge_by("creating unit: #{new_resource.unit_name}") do manage_unit_file(:create) daemon_reload if new_resource.triggers_reload end @@ -54,108 +67,144 @@ class Chef def action_delete if ::File.exist?(unit_path) - converge_by("deleting unit: #{new_resource.name}") do + converge_by("deleting unit: #{new_resource.unit_name}") do manage_unit_file(:delete) daemon_reload if new_resource.triggers_reload end end end + def action_preset + converge_by("restoring enable/disable preset configuration for unit: #{new_resource.unit_name}") do + systemctl_execute!(:preset, new_resource.unit_name) + end + end + + def action_revert + converge_by("reverting to vendor version of unit: #{new_resource.unit_name}") do + systemctl_execute!(:revert, new_resource.unit_name) + end + end + def action_enable if current_resource.static - Chef::Log.debug("#{new_resource.name} is a static unit, enabling is a NOP.") + logger.trace("#{new_resource.unit_name} is a static unit, enabling is a NOP.") end unless current_resource.enabled || current_resource.static - converge_by("enabling unit: #{new_resource.name}") do - systemctl_execute!(:enable, new_resource.name) + converge_by("enabling unit: #{new_resource.unit_name}") do + systemctl_execute!(:enable, new_resource.unit_name) end end end def action_disable if current_resource.static - Chef::Log.debug("#{new_resource.name} is a static unit, disabling is a NOP.") + logger.trace("#{new_resource.unit_name} is a static unit, disabling is a NOP.") end if current_resource.enabled && !current_resource.static - converge_by("disabling unit: #{new_resource.name}") do - systemctl_execute!(:disable, new_resource.name) + converge_by("disabling unit: #{new_resource.unit_name}") do + systemctl_execute!(:disable, new_resource.unit_name) end end end + def action_reenable + converge_by("reenabling unit: #{new_resource.unit_name}") do + systemctl_execute!(:reenable, new_resource.unit_name) + end + end + def action_mask unless current_resource.masked - converge_by("masking unit: #{new_resource.name}") do - systemctl_execute!(:mask, new_resource.name) + converge_by("masking unit: #{new_resource.unit_name}") do + systemctl_execute!(:mask, new_resource.unit_name) end end end def action_unmask if current_resource.masked - converge_by("unmasking unit: #{new_resource.name}") do - systemctl_execute!(:unmask, new_resource.name) + converge_by("unmasking unit: #{new_resource.unit_name}") do + systemctl_execute!(:unmask, new_resource.unit_name) end end end def action_start unless current_resource.active - converge_by("starting unit: #{new_resource.name}") do - systemctl_execute!(:start, new_resource.name) + converge_by("starting unit: #{new_resource.unit_name}") do + systemctl_execute!(:start, new_resource.unit_name) end end end def action_stop if current_resource.active - converge_by("stopping unit: #{new_resource.name}") do - systemctl_execute!(:stop, new_resource.name) + converge_by("stopping unit: #{new_resource.unit_name}") do + systemctl_execute!(:stop, new_resource.unit_name) end end end def action_restart - converge_by("restarting unit: #{new_resource.name}") do - systemctl_execute!(:restart, new_resource.name) + converge_by("restarting unit: #{new_resource.unit_name}") do + systemctl_execute!(:restart, new_resource.unit_name) end end def action_reload if current_resource.active - converge_by("reloading unit: #{new_resource.name}") do - systemctl_execute!(:reload, new_resource.name) + converge_by("reloading unit: #{new_resource.unit_name}") do + systemctl_execute!(:reload, new_resource.unit_name) end else - Chef::Log.debug("#{new_resource.name} is not active, skipping reload.") + logger.trace("#{new_resource.unit_name} is not active, skipping reload.") + end + end + + def action_try_restart + converge_by("try-restarting unit: #{new_resource.unit_name}") do + systemctl_execute!("try-restart", new_resource.unit_name) + end + end + + def action_reload_or_restart + converge_by("reload-or-restarting unit: #{new_resource.unit_name}") do + systemctl_execute!("reload-or-restart", new_resource.unit_name) + end + end + + def action_reload_or_try_restart + converge_by("reload-or-try-restarting unit: #{new_resource.unit_name}") do + systemctl_execute!("reload-or-try-restart", new_resource.unit_name) end end def active? - systemctl_execute("is-active", new_resource.name).exitstatus == 0 + systemctl_execute("is-active", new_resource.unit_name).exitstatus == 0 end def enabled? - systemctl_execute("is-enabled", new_resource.name).exitstatus == 0 + systemctl_execute("is-enabled", new_resource.unit_name).exitstatus == 0 end def masked? - systemctl_execute(:status, new_resource.name).stdout.include?("masked") + systemctl_execute(:status, new_resource.unit_name).stdout.include?("masked") end def static? - systemctl_execute("is-enabled", new_resource.name).stdout.include?("static") + systemctl_execute("is-enabled", new_resource.unit_name).stdout.include?("static") end private def unit_path if new_resource.user - "/etc/systemd/user/#{new_resource.name}" + "/etc/systemd/user/#{new_resource.unit_name}" else - "/etc/systemd/system/#{new_resource.name}" + "/etc/systemd/system/#{new_resource.unit_name}" end end @@ -165,6 +214,7 @@ class Chef f.group "root" f.mode "0644" f.content new_resource.to_ini + f.verify :systemd_unit if new_resource.verify end.run_action(action) end @@ -173,11 +223,11 @@ class Chef end def systemctl_execute!(action, unit) - shell_out_with_systems_locale!("#{systemctl_cmd} #{action} #{unit}", systemctl_opts) + shell_out_with_systems_locale!("#{systemctl_cmd} #{action} #{Shellwords.escape(unit)}", systemctl_opts) end def systemctl_execute(action, unit) - shell_out("#{systemctl_cmd} #{action} #{unit}", systemctl_opts) + shell_out("#{systemctl_cmd} #{action} #{Shellwords.escape(unit)}", systemctl_opts) end def systemctl_cmd @@ -195,10 +245,11 @@ class Chef def systemctl_opts @systemctl_opts ||= if new_resource.user + uid = Etc.getpwuid(new_resource.user).uid { - "user" => new_resource.user, - "environment" => { - "DBUS_SESSION_BUS_ADDRESS" => "unix:path=/run/user/#{node['etc']['passwd'][new_resource.user]['uid']}/bus", + :user => new_resource.user, + :environment => { + "DBUS_SESSION_BUS_ADDRESS" => "unix:path=/run/user/#{uid}/bus", }, } else diff --git a/lib/chef/provider/template.rb b/lib/chef/provider/template.rb index 3c46a6eb7d..05cdbdbf62 100644 --- a/lib/chef/provider/template.rb +++ b/lib/chef/provider/template.rb @@ -19,25 +19,19 @@ require "chef/provider/template_finder" require "chef/provider/file" -require "chef/deprecation/provider/template" -require "chef/deprecation/warnings" class Chef class Provider class Template < Chef::Provider::File provides :template - extend Chef::Deprecation::Warnings - include Chef::Deprecation::Provider::Template - add_deprecation_warnings_for(Chef::Deprecation::Provider::Template.instance_methods) - def initialize(new_resource, run_context) @content_class = Chef::Provider::Template::Content super end def load_current_resource - @current_resource = Chef::Resource::Template.new(@new_resource.name) + @current_resource = Chef::Resource::Template.new(new_resource.name) super end @@ -55,8 +49,8 @@ class Chef private def managing_content? - return true if @new_resource.checksum - return true if !@new_resource.source.nil? && @action != :create_if_missing + return true if new_resource.checksum + return true if !new_resource.source.nil? && @action != :create_if_missing false end diff --git a/lib/chef/provider/template/content.rb b/lib/chef/provider/template/content.rb index acf200621d..b40794564a 100644 --- a/lib/chef/provider/template/content.rb +++ b/lib/chef/provider/template/content.rb @@ -36,7 +36,30 @@ class Chef private def file_for_provider - context = TemplateContext.new(new_resource.variables) + # Deal with any DelayedEvaluator values in the template variables. + visitor = lambda do |obj| + case obj + when Hash + # If this is an Attribute object, we need to change class otherwise + # we get the immutable behavior. This could probably be fixed by + # using Hash#transform_values once we only support Ruby 2.4. + obj_class = obj.is_a?(Chef::Node::ImmutableMash) ? Mash : obj.class + # Avoid mutating hashes in the resource in case we're changing anything. + obj.each_with_object(obj_class.new) do |(key, value), memo| + memo[key] = visitor.call(value) + end + when Array + # Avoid mutating arrays in the resource in case we're changing anything. + obj.map { |value| visitor.call(value) } + when DelayedEvaluator + new_resource.instance_eval(&obj) + else + obj + end + end + variables = visitor.call(new_resource.variables) + + context = TemplateContext.new(variables) context[:node] = run_context.node context[:template_finder] = template_finder diff --git a/lib/chef/provider/template_finder.rb b/lib/chef/provider/template_finder.rb index 67342a86ea..1e8b925071 100644 --- a/lib/chef/provider/template_finder.rb +++ b/lib/chef/provider/template_finder.rb @@ -40,7 +40,7 @@ class Chef cookbook.preferred_filename_on_disk_location(@node, :templates, template_name) end - protected + protected def template_source_name(name, options) if options[:source] diff --git a/lib/chef/provider/user.rb b/lib/chef/provider/user.rb index 85bd674d8d..18cf2d4d99 100644 --- a/lib/chef/provider/user.rb +++ b/lib/chef/provider/user.rb @@ -1,6 +1,6 @@ # # Author:: Adam Jacob (<adam@chef.io>) -# Copyright:: Copyright 2008-2016, Chef Software Inc. +# Copyright:: Copyright 2008-2017, Chef Software Inc. # License:: Apache License, Version 2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,13 +17,11 @@ # require "chef/provider" -require "chef/mixin/command" require "etc" class Chef class Provider class User < Chef::Provider - include Chef::Mixin::Command attr_accessor :user_exists, :locked @@ -36,74 +34,70 @@ class Chef end def convert_group_name - if @new_resource.gid.is_a? String - @new_resource.gid(Etc.getgrnam(@new_resource.gid).gid) + if new_resource.gid.is_a? String + new_resource.gid(Etc.getgrnam(new_resource.gid).gid) end - rescue ArgumentError => e + rescue ArgumentError @group_name_resolved = false end - def whyrun_supported? - true - end - def load_current_resource - @current_resource = Chef::Resource::User.new(@new_resource.name) - @current_resource.username(@new_resource.username) + @current_resource = Chef::Resource::User.new(new_resource.name) + current_resource.username(new_resource.username) begin - user_info = Etc.getpwnam(@new_resource.username) - rescue ArgumentError => e + user_info = Etc.getpwnam(new_resource.username) + rescue ArgumentError @user_exists = false - Chef::Log.debug("#{@new_resource} user does not exist") + logger.trace("#{new_resource} user does not exist") user_info = nil end if user_info - @current_resource.uid(user_info.uid) - @current_resource.gid(user_info.gid) - @current_resource.home(user_info.dir) - @current_resource.shell(user_info.shell) - @current_resource.password(user_info.passwd) - - if @new_resource.comment - user_info.gecos.force_encoding(@new_resource.comment.encoding) + current_resource.uid(user_info.uid) + current_resource.gid(user_info.gid) + current_resource.home(user_info.dir) + current_resource.shell(user_info.shell) + current_resource.password(user_info.passwd) + + if new_resource.comment + user_info.gecos.force_encoding(new_resource.comment.encoding) end - @current_resource.comment(user_info.gecos) + current_resource.comment(user_info.gecos) - if @new_resource.password && @current_resource.password == "x" + if new_resource.password && current_resource.password == "x" begin require "shadow" rescue LoadError @shadow_lib_ok = false else - shadow_info = Shadow::Passwd.getspnam(@new_resource.username) - @current_resource.password(shadow_info.sp_pwdp) + shadow_info = Shadow::Passwd.getspnam(new_resource.username) + current_resource.password(shadow_info.sp_pwdp) end end - convert_group_name if @new_resource.gid + convert_group_name if new_resource.gid end - @current_resource + current_resource end def define_resource_requirements requirements.assert(:create, :modify, :manage, :lock, :unlock) do |a| a.assertion { @group_name_resolved } - a.failure_message Chef::Exceptions::User, "Couldn't lookup integer GID for group name #{@new_resource.gid}" - a.whyrun "group name #{@new_resource.gid} does not exist. This will cause group assignment to fail. Assuming this group will have been created previously." + a.failure_message Chef::Exceptions::User, "Couldn't lookup integer GID for group name #{new_resource.gid}" + a.whyrun "group name #{new_resource.gid} does not exist. This will cause group assignment to fail. Assuming this group will have been created previously." end requirements.assert(:all_actions) do |a| a.assertion { @shadow_lib_ok } a.failure_message Chef::Exceptions::MissingLibrary, "You must have ruby-shadow installed for password support!" - a.whyrun "ruby-shadow is not installed. Attempts to set user password will cause failure. Assuming that this gem will have been previously installed." + + a.whyrun "ruby-shadow is not installed. Attempts to set user password will cause failure. Assuming that this gem will have been previously installed." \ "Note that user update converge may report false-positive on the basis of mismatched password. " end requirements.assert(:modify, :lock, :unlock) do |a| a.assertion { @user_exists } - a.failure_message(Chef::Exceptions::User, "Cannot modify user #{@new_resource.username} - does not exist!") - a.whyrun("Assuming user #{@new_resource.username} would have been created") + a.failure_message(Chef::Exceptions::User, "Cannot modify user #{new_resource.username} - does not exist!") + a.whyrun("Assuming user #{new_resource.username} would have been created") end end @@ -113,99 +107,98 @@ class Chef # <true>:: If a change is required # <false>:: If the users are identical def compare_user - changed = [ :comment, :home, :shell, :password ].select do |user_attrib| - !@new_resource.send(user_attrib).nil? && @new_resource.send(user_attrib) != @current_resource.send(user_attrib) - end + return true if !new_resource.home.nil? && Pathname.new(new_resource.home).cleanpath != Pathname.new(current_resource.home).cleanpath - changed += [ :uid, :gid ].select do |user_attrib| - !@new_resource.send(user_attrib).nil? && @new_resource.send(user_attrib).to_s != @current_resource.send(user_attrib).to_s + [ :comment, :shell, :password, :uid, :gid ].each do |user_attrib| + return true if !new_resource.send(user_attrib).nil? && new_resource.send(user_attrib).to_s != current_resource.send(user_attrib).to_s end - changed.any? + false end def action_create if !@user_exists - converge_by("create user #{@new_resource.username}") do + converge_by("create user #{new_resource.username}") do create_user - Chef::Log.info("#{@new_resource} created") + logger.info("#{new_resource} created") end elsif compare_user - converge_by("alter user #{@new_resource.username}") do + converge_by("alter user #{new_resource.username}") do manage_user - Chef::Log.info("#{@new_resource} altered") + logger.info("#{new_resource} altered") end end end def action_remove - if @user_exists - converge_by("remove user #{@new_resource.username}") do - remove_user - Chef::Log.info("#{@new_resource} removed") - end + return unless @user_exists + converge_by("remove user #{new_resource.username}") do + remove_user + logger.info("#{new_resource} removed") end end - def remove_user - raise NotImplementedError - end - def action_manage - if @user_exists && compare_user - converge_by("manage user #{@new_resource.username}") do - manage_user - Chef::Log.info("#{@new_resource} managed") - end + return unless @user_exists && compare_user + converge_by("manage user #{new_resource.username}") do + manage_user + logger.info("#{new_resource} managed") end end - def manage_user - raise NotImplementedError - end - def action_modify - if compare_user - converge_by("modify user #{@new_resource.username}") do - manage_user - Chef::Log.info("#{@new_resource} modified") - end + return unless compare_user + converge_by("modify user #{new_resource.username}") do + manage_user + logger.info("#{new_resource} modified") end end def action_lock - if check_lock() == false - converge_by("lock the user #{@new_resource.username}") do + if check_lock == false + converge_by("lock the user #{new_resource.username}") do lock_user - Chef::Log.info("#{@new_resource} locked") + logger.info("#{new_resource} locked") end else - Chef::Log.debug("#{@new_resource} already locked - nothing to do") + logger.trace("#{new_resource} already locked - nothing to do") end end - def check_lock + def action_unlock + if check_lock == true + converge_by("unlock user #{new_resource.username}") do + unlock_user + logger.info("#{new_resource} unlocked") + end + else + logger.trace("#{new_resource} already unlocked - nothing to do") + end + end + + def create_user raise NotImplementedError end - def lock_user + def remove_user raise NotImplementedError end - def action_unlock - if check_lock() == true - converge_by("unlock user #{@new_resource.username}") do - unlock_user - Chef::Log.info("#{@new_resource} unlocked") - end - else - Chef::Log.debug("#{@new_resource} already unlocked - nothing to do") - end + def manage_user + raise NotImplementedError + end + + def lock_user + raise NotImplementedError end def unlock_user raise NotImplementedError end + + def check_lock + raise NotImplementedError + end end end end diff --git a/lib/chef/provider/user/aix.rb b/lib/chef/provider/user/aix.rb index 3f168b8da3..64a088dd5c 100644 --- a/lib/chef/provider/user/aix.rb +++ b/lib/chef/provider/user/aix.rb @@ -14,13 +14,16 @@ # See the License for the specific language governing permissions and # limitations under the License. +require "chef/provider/user/useradd" + class Chef class Provider class User class Aix < Chef::Provider::User::Useradd - provides :user, platform: %w{aix} + provides :user, os: "aix" + provides :aix_user - UNIVERSAL_OPTIONS = [[:comment, "-c"], [:gid, "-g"], [:shell, "-s"], [:uid, "-u"]] + UNIVERSAL_OPTIONS = [[:comment, "-c"], [:gid, "-g"], [:shell, "-s"], [:uid, "-u"]].freeze def create_user super @@ -41,53 +44,52 @@ class Chef end def check_lock - lock_info = shell_out!("lsuser -a account_locked #{new_resource.username}") + lock_info = shell_out_compact!("lsuser", "-a", "account_locked", new_resource.username) if whyrun_mode? && passwd_s.stdout.empty? && lock_info.stderr.match(/does not exist/) # if we're in whyrun mode and the user is not yet created we assume it would be return false end - raise Chef::Exceptions::User, "Cannot determine if #{@new_resource} is locked!" if lock_info.stdout.empty? + raise Chef::Exceptions::User, "Cannot determine if #{new_resource} is locked!" if lock_info.stdout.empty? status = /\S+\s+account_locked=(\S+)/.match(lock_info.stdout) - if status && status[1] == "true" - @locked = true - else - @locked = false - end + @locked = + if status && status[1] == "true" + true + else + false + end @locked end def lock_user - shell_out!("chuser account_locked=true #{new_resource.username}") + shell_out_compact!("chuser", "account_locked=true", new_resource.username) end def unlock_user - shell_out!("chuser account_locked=false #{new_resource.username}") + shell_out_compact!("chuser", "account_locked=false", new_resource.username) end - private + private def add_password - if @current_resource.password != @new_resource.password && @new_resource.password - Chef::Log.debug("#{@new_resource.username} setting password to #{@new_resource.password}") - command = "echo '#{@new_resource.username}:#{@new_resource.password}' | chpasswd -e" - shell_out!(command) - end + return unless current_resource.password != new_resource.password && new_resource.password + logger.trace("#{new_resource.username} setting password to #{new_resource.password}") + command = "echo '#{new_resource.username}:#{new_resource.password}' | chpasswd -e" + shell_out!(command) end # Aix specific handling to update users home directory. def manage_home + return unless updating_home? && new_resource.manage_home # -m option does not work on aix, so move dir. - if updating_home? && managing_home_dir? - universal_options.delete("-m") - if ::File.directory?(@current_resource.home) - Chef::Log.debug("Changing users home directory from #{@current_resource.home} to #{new_resource.home}") - shell_out!("mv #{@current_resource.home} #{new_resource.home}") - else - Chef::Log.debug("Creating users home directory #{new_resource.home}") - shell_out!("mkdir -p #{new_resource.home}") - end + universal_options.delete("-m") + if ::File.directory?(current_resource.home) + logger.trace("Changing users home directory from #{current_resource.home} to #{new_resource.home}") + FileUtils.mv current_resource.home, new_resource.home + else + logger.trace("Creating users home directory #{new_resource.home}") + FileUtils.mkdir_p new_resource.home end end diff --git a/lib/chef/provider/user/dscl.rb b/lib/chef/provider/user/dscl.rb index 541160bf93..a1ff30ef1c 100644 --- a/lib/chef/provider/user/dscl.rb +++ b/lib/chef/provider/user/dscl.rb @@ -1,6 +1,6 @@ # # Author:: Dreamcat4 (<dreamcat4@gmail.com>) -# Copyright:: Copyright 2009-2016, Chef Software Inc. +# Copyright:: Copyright 2009-2017, Chef Software Inc. # License:: Apache License, Version 2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -48,8 +48,14 @@ class Chef attr_accessor :authentication_authority attr_accessor :password_shadow_conversion_algorithm + provides :dscl_user provides :user, os: "darwin" + # Just-in-case a recipe calls the user dscl provider without specifying + # a gid property. Avoids chown issues in move_home when the manage_home + # property is in use. #5393 + STAFF_GROUP_ID = 20 + def define_resource_requirements super @@ -59,12 +65,12 @@ class Chef end requirements.assert(:all_actions) do |a| - a.assertion { ::File.exists?("/usr/bin/dscl") } + a.assertion { ::File.exist?("/usr/bin/dscl") } a.failure_message(Chef::Exceptions::User, "Cannot find binary '/usr/bin/dscl' on the system for #{new_resource}!") end requirements.assert(:all_actions) do |a| - a.assertion { ::File.exists?("/usr/bin/plutil") } + a.assertion { ::File.exist?("/usr/bin/plutil") } a.failure_message(Chef::Exceptions::User, "Cannot find binary '/usr/bin/plutil' on the system for #{new_resource}!") end @@ -151,7 +157,7 @@ user password using shadow hash.") convert_group_name if new_resource.gid else @user_exists = false - Chef::Log.debug("#{new_resource} user does not exist") + logger.trace("#{new_resource} user does not exist") end current_resource @@ -193,7 +199,7 @@ user password using shadow hash.") # Create a user using dscl # def dscl_create_user - run_dscl("create /Users/#{new_resource.username}") + run_dscl("create", "/Users/#{new_resource.username}") end # @@ -202,7 +208,7 @@ user password using shadow hash.") # def dscl_create_comment comment = new_resource.comment || new_resource.username - run_dscl("create /Users/#{new_resource.username} RealName '#{comment}'") + run_dscl("create", "/Users/#{new_resource.username}", "RealName", comment) end # @@ -218,7 +224,7 @@ user password using shadow hash.") raise(Chef::Exceptions::RequestedUIDUnavailable, "uid #{new_resource.uid} is already in use") end - run_dscl("create /Users/#{new_resource.username} UniqueID #{new_resource.uid}") + run_dscl("create", "/Users/#{new_resource.username}", "UniqueID", new_resource.uid) end # @@ -229,7 +235,7 @@ user password using shadow hash.") uid = nil base_uid = new_resource.system ? 200 : 501 next_uid_guess = base_uid - users_uids = run_dscl("list /Users uid") + users_uids = run_dscl("list", "/Users", "uid") while next_uid_guess < search_limit + base_uid if users_uids =~ Regexp.new("#{Regexp.escape(next_uid_guess.to_s)}\n") next_uid_guess += 1 @@ -238,7 +244,7 @@ user password using shadow hash.") break end end - return uid || raise("uid not found. Exhausted. Searched #{search_limit} times") + uid || raise("uid not found. Exhausted. Searched #{search_limit} times") end # @@ -246,39 +252,39 @@ user password using shadow hash.") # def uid_used?(uid) return false unless uid - users_uids = run_dscl("list /Users uid").split("\n") - uid_map = users_uids.inject({}) do |tmap, tuid| + users_uids = run_dscl("list", "/Users", "uid").split("\n") + uid_map = users_uids.each_with_object({}) do |tuid, tmap| x = tuid.split tmap[x[1]] = x[0] tmap end if uid_map[uid.to_s] - unless uid_map[uid.to_s] == new_resource.username.to_s + unless uid_map[uid.to_s] == new_resource.username return true end end - return false + false end # # Sets the group id for the user using dscl. Fails if a group doesn't # exist on the system with given group id. If `gid` is not specified, it - # sets a default Mac user group "staff", with id 20. + # sets a default Mac user group "staff", with id 20 using the CONSTANT # def dscl_set_gid if new_resource.gid.nil? # XXX: mutates the new resource - new_resource.gid(20) + new_resource.gid(STAFF_GROUP_ID) elsif !new_resource.gid.to_s.match(/^\d+$/) begin - possible_gid = run_dscl("read /Groups/#{new_resource.gid} PrimaryGroupID").split(" ").last - rescue Chef::Exceptions::DsclCommandFailed => e - raise Chef::Exceptions::GroupIDNotFound.new("Group not found for #{new_resource.gid} when creating user #{new_resource.username}") + possible_gid = run_dscl("read", "/Groups/#{new_resource.gid}", "PrimaryGroupID").split(" ").last + rescue Chef::Exceptions::DsclCommandFailed + raise Chef::Exceptions::GroupIDNotFound, "Group not found for #{new_resource.gid} when creating user #{new_resource.username}" end # XXX: mutates the new resource new_resource.gid(possible_gid) if possible_gid && possible_gid.match(/^\d+$/) end - run_dscl("create /Users/#{new_resource.username} PrimaryGroupID '#{new_resource.gid}'") + run_dscl("create", "/Users/#{new_resource.username}", "PrimaryGroupID", new_resource.gid) end # @@ -287,13 +293,11 @@ user password using shadow hash.") # def dscl_set_home if new_resource.home.nil? || new_resource.home.empty? - run_dscl("delete /Users/#{new_resource.username} NFSHomeDirectory") + run_dscl("delete", "/Users/#{new_resource.username}", "NFSHomeDirectory") return end - run_dscl("create /Users/#{new_resource.username} NFSHomeDirectory '#{new_resource.home}'") - - if new_resource.supports[:manage_home] + if new_resource.manage_home validate_home_dir_specification! if (current_resource.home == new_resource.home) && !new_home_exists? @@ -304,6 +308,7 @@ user password using shadow hash.") move_home end end + run_dscl("create", "/Users/#{new_resource.username}", "NFSHomeDirectory", new_resource.home) end def validate_home_dir_specification! @@ -313,24 +318,24 @@ user password using shadow hash.") end def current_home_exists? - ::File.exist?("#{current_resource.home}") + !!current_resource.home && ::File.exist?(current_resource.home) end def new_home_exists? - ::File.exist?("#{new_resource.home}") + ::File.exist?(new_resource.home) end def ditto_home - shell_out! "/usr/sbin/createhomedir -c -u #{new_resource.username}" + shell_out_compact!("/usr/sbin/createhomedir -c -u #{new_resource.username}") end def move_home - Chef::Log.debug("#{new_resource} moving #{self} home from #{current_resource.home} to #{new_resource.home}") - + logger.trace("#{new_resource} moving #{self} home from #{current_resource.home} to #{new_resource.home}") + new_resource.gid(STAFF_GROUP_ID) if new_resource.gid.nil? src = current_resource.home FileUtils.mkdir_p(new_resource.home) files = ::Dir.glob("#{Chef::Util::PathHelper.escape_glob_dir(src)}/*", ::File::FNM_DOTMATCH) - ["#{src}/.", "#{src}/.."] - ::FileUtils.mv(files, new_resource.home, :force => true) + ::FileUtils.mv(files, new_resource.home, force: true) ::FileUtils.rmdir(src) ::FileUtils.chown_R(new_resource.username, new_resource.gid.to_s, new_resource.home) end @@ -339,10 +344,10 @@ user password using shadow hash.") # Sets the shell for the user using dscl. # def dscl_set_shell - if new_resource.shell || ::File.exists?("#{new_resource.shell}") - run_dscl("create /Users/#{new_resource.username} UserShell '#{new_resource.shell}'") + if new_resource.shell + run_dscl("create", "/Users/#{new_resource.username}", "UserShell", new_resource.shell) else - run_dscl("create /Users/#{new_resource.username} UserShell '/usr/bin/false'") + run_dscl("create", "/Users/#{new_resource.username}", "UserShell", "/usr/bin/false") end end @@ -359,9 +364,8 @@ user password using shadow hash.") # Shadow info is saved as binary plist. Convert the info to binary plist. shadow_info_binary = StringIO.new - command = Mixlib::ShellOut.new("plutil -convert binary1 -o - -", - :input => shadow_info.to_plist, :live_stream => shadow_info_binary) - command.run_command + shell_out_compact("plutil", "-convert", "binary1", "-o", "-", "-", + input: shadow_info.to_plist, live_stream: shadow_info_binary) if user_info.nil? # User is just created. read_user_info() will read the fresh information @@ -393,7 +397,7 @@ user password using shadow hash.") # Create a random 4 byte salt salt = OpenSSL::Random.random_bytes(4) encoded_password = OpenSSL::Digest::SHA512.hexdigest(salt + new_resource.password) - hash_value = salt.unpack("H*").first + encoded_password + salt.unpack("H*").first + encoded_password end shadow_info["SALTED-SHA512"] = StringIO.new @@ -435,27 +439,27 @@ user password using shadow hash.") # and deleting home directory if needed. # def remove_user - if new_resource.supports[:manage_home] + if new_resource.manage_home # Remove home directory FileUtils.rm_rf(current_resource.home) end # Remove the user from its groups - run_dscl("list /Groups").each_line do |group| + run_dscl("list", "/Groups").each_line do |group| if member_of_group?(group.chomp) - run_dscl("delete /Groups/#{group.chomp} GroupMembership '#{new_resource.username}'") + run_dscl("delete", "/Groups/#{group.chomp}", "GroupMembership", new_resource.username) end end # Remove user account - run_dscl("delete /Users/#{new_resource.username}") + run_dscl("delete", "/Users/#{new_resource.username}") end # # Locks the user. # def lock_user - run_dscl("append /Users/#{new_resource.username} AuthenticationAuthority ';DisabledUser;'") + run_dscl("append", "/Users/#{new_resource.username}", "AuthenticationAuthority", ";DisabledUser;") end # @@ -463,7 +467,7 @@ user password using shadow hash.") # def unlock_user auth_string = authentication_authority.gsub(/AuthenticationAuthority: /, "").gsub(/;DisabledUser;/, "").strip - run_dscl("create /Users/#{new_resource.username} AuthenticationAuthority '#{auth_string}'") + run_dscl("create", "/Users/#{new_resource.username}", "AuthenticationAuthority", auth_string) end # @@ -481,7 +485,7 @@ user password using shadow hash.") # This is the interface base User provider requires to provide idempotency. # def check_lock - return @locked = locked? + @locked = locked? end # @@ -493,11 +497,11 @@ user password using shadow hash.") # given attribute. # def diverged?(parameter) - parameter_updated?(parameter) && (not new_resource.send(parameter).nil?) + parameter_updated?(parameter) && !new_resource.send(parameter).nil? end def parameter_updated?(parameter) - not (new_resource.send(parameter) == current_resource.send(parameter)) + !(new_resource.send(parameter) == current_resource.send(parameter)) end # @@ -543,7 +547,7 @@ user password using shadow hash.") def member_of_group?(group_name) membership_info = "" begin - membership_info = run_dscl("read /Groups/#{group_name}") + membership_info = run_dscl("read", "/Groups/#{group_name}") rescue Chef::Exceptions::DsclCommandFailed # Raised if the group doesn't contain any members end @@ -560,14 +564,14 @@ user password using shadow hash.") # A simple map of Chef's terms to DSCL's terms. DSCL_PROPERTY_MAP = { - :uid => "uid", - :gid => "gid", - :home => "home", - :shell => "shell", - :comment => "realname", - :password => "passwd", - :auth_authority => "authentication_authority", - :shadow_hash => "ShadowHashData", + uid: "uid", + gid: "gid", + home: "home", + shell: "shell", + comment: "realname", + password: "passwd", + auth_authority: "authentication_authority", + shadow_hash: "ShadowHashData", }.freeze # Directory where the user plist files are stored for versions 10.7 and above @@ -582,11 +586,11 @@ user password using shadow hash.") # We flush the cache here in order to make sure that we read fresh information # for the user. - shell_out("dscacheutil '-flushcache'") + shell_out_compact("dscacheutil", "-flushcache") # FIXME: this is MacOS version dependent begin user_plist_file = "#{USER_PLIST_DIRECTORY}/#{new_resource.username}.plist" - user_plist_info = run_plutil("convert xml1 -o - #{user_plist_file}") + user_plist_info = run_plutil("convert", "xml1", "-o", "-", user_plist_file) user_info = Plist.parse_xml(user_plist_info) rescue Chef::Exceptions::PlistUtilCommandFailed end @@ -601,7 +605,7 @@ user password using shadow hash.") def save_user_info(user_info) user_plist_file = "#{USER_PLIST_DIRECTORY}/#{new_resource.username}.plist" Plist::Emit.save_plist(user_info, user_plist_file) - run_plutil("convert binary1 #{user_plist_file}") + run_plutil("convert", "binary1", user_plist_file) end # @@ -650,7 +654,9 @@ user password using shadow hash.") end def run_dscl(*args) - result = shell_out("dscl . -#{args.join(' ')}") + argdup = args.dup + cmd = argdup.shift + result = shell_out_compact("dscl", ".", "-#{cmd}", argdup) return "" if ( args.first =~ /^delete/ ) && ( result.exitstatus != 0 ) raise(Chef::Exceptions::DsclCommandFailed, "dscl error: #{result.inspect}") unless result.exitstatus == 0 raise(Chef::Exceptions::DsclCommandFailed, "dscl error: #{result.inspect}") if result.stdout =~ /No such key: / @@ -658,17 +664,19 @@ user password using shadow hash.") end def run_plutil(*args) - result = shell_out("plutil -#{args.join(' ')}") + argdup = args.dup + cmd = argdup.shift + result = shell_out_compact("plutil", "-#{cmd}", argdup) raise(Chef::Exceptions::PlistUtilCommandFailed, "plutil error: #{result.inspect}") unless result.exitstatus == 0 if result.stdout.encoding == Encoding::ASCII_8BIT - result.stdout.encode("utf-8", "binary", :undef => :replace, :invalid => :replace, :replace => "?") + result.stdout.encode("utf-8", "binary", undef: :replace, invalid: :replace, replace: "?") else result.stdout end end def convert_binary_plist_to_xml(binary_plist_string) - Mixlib::ShellOut.new("plutil -convert xml1 -o - -", :input => binary_plist_string).run_command.stdout + shell_out_compact("plutil", "-convert", "xml1", "-o", "-", "-", input: binary_plist_string).stdout end def convert_to_binary(string) diff --git a/lib/chef/provider/user/linux.rb b/lib/chef/provider/user/linux.rb new file mode 100644 index 0000000000..2db6c218bd --- /dev/null +++ b/lib/chef/provider/user/linux.rb @@ -0,0 +1,126 @@ +# +# Copyright:: Copyright 2016-2017, 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/user" + +class Chef + class Provider + class User + class Linux < Chef::Provider::User + provides :linux_user + provides :user, os: "linux" + + def create_user + shell_out_compact!("useradd", universal_options, useradd_options, new_resource.username) + end + + def manage_user + shell_out_compact!("usermod", universal_options, usermod_options, new_resource.username) + end + + def remove_user + shell_out_compact!("userdel", userdel_options, new_resource.username) + end + + def lock_user + shell_out_compact!("usermod", "-L", new_resource.username) + end + + def unlock_user + shell_out_compact!("usermod", "-U", new_resource.username) + end + + # common to usermod and useradd + def universal_options + opts = [] + opts << "-c" << new_resource.comment if should_set?(:comment) + opts << "-g" << new_resource.gid if should_set?(:gid) + opts << "-p" << new_resource.password if should_set?(:password) + opts << "-s" << new_resource.shell if should_set?(:shell) + opts << "-u" << new_resource.uid if should_set?(:uid) + opts << "-d" << new_resource.home if updating_home? + opts << "-o" if new_resource.non_unique + opts + end + + def usermod_options + opts = [] + opts += [ "-u", new_resource.uid ] if new_resource.non_unique + if updating_home? + if new_resource.manage_home + opts << "-m" + end + end + opts + end + + def useradd_options + opts = [] + opts << "-r" if new_resource.system + opts << if new_resource.manage_home + "-m" + else + "-M" + end + opts + end + + def userdel_options + opts = [] + opts << "-r" if new_resource.manage_home + opts << "-f" if new_resource.force + opts + end + + def should_set?(sym) + current_resource.send(sym).to_s != new_resource.send(sym).to_s && new_resource.send(sym) + end + + def updating_home? + return false unless new_resource.home + return true unless current_resource.home + new_resource.home && Pathname.new(current_resource.home).cleanpath != Pathname.new(new_resource.home).cleanpath + end + + def check_lock + # there's an old bug in rhel (https://bugzilla.redhat.com/show_bug.cgi?id=578534) + # which means that both 0 and 1 can be success. + passwd_s = shell_out_compact("passwd", "-S", new_resource.username, returns: [ 0, 1 ]) + + # checking "does not exist" has to come before exit code handling since centos and ubuntu differ in exit codes + if passwd_s.stderr =~ /does not exist/ + return false if whyrun_mode? + raise Chef::Exceptions::User, "User #{new_resource.username} does not exist when checking lock status for #{new_resource}" + end + + # now raise if we didn't get a 0 or 1 (see above) + passwd_s.error! + + # now the actual output parsing + @locked = nil + status_line = passwd_s.stdout.split(" ") + @locked = false if status_line[1] =~ /^[PN]/ + @locked = true if status_line[1] =~ /^L/ + + raise Chef::Exceptions::User, "Cannot determine if user #{new_resource.username} is locked for #{new_resource}" if @locked.nil? + + # FIXME: should probably go on the current_resource + @locked + end + end + end + end +end diff --git a/lib/chef/provider/user/pw.rb b/lib/chef/provider/user/pw.rb index 949a21790b..695dbfd539 100644 --- a/lib/chef/provider/user/pw.rb +++ b/lib/chef/provider/user/pw.rb @@ -1,6 +1,6 @@ # # Author:: Stephen Haynes (<sh@nomitor.com>) -# Copyright:: Copyright 2009-2016, Chef Software Inc. +# Copyright:: Copyright 2009-2017, Chef Software Inc. # License:: Apache License, Version 2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -22,53 +22,50 @@ class Chef class Provider class User class Pw < Chef::Provider::User - provides :user, platform: %w{freebsd} + provides :pw_user + provides :user, os: "freebsd" def load_current_resource super - raise Chef::Exceptions::User, "Could not find binary /usr/sbin/pw for #{@new_resource}" unless ::File.exists?("/usr/sbin/pw") + raise Chef::Exceptions::User, "Could not find binary /usr/sbin/pw for #{new_resource}" unless ::File.exist?("/usr/sbin/pw") end def create_user - command = "pw useradd" - command << set_options - run_command(:command => command) + shell_out_compact!("pw", "useradd", set_options) modify_password end def manage_user - command = "pw usermod" - command << set_options - run_command(:command => command) + shell_out_compact!("pw", "usermod", set_options) modify_password end def remove_user - command = "pw userdel #{@new_resource.username}" - command << " -r" if @new_resource.supports[:manage_home] - run_command(:command => command) + command = [ "pw", "userdel", new_resource.username ] + command << "-r" if new_resource.manage_home + shell_out_compact!(command) end def check_lock - case @current_resource.password - when /^\*LOCKED\*/ - @locked = true - else - @locked = false - end + @locked = case current_resource.password + when /^\*LOCKED\*/ + true + else + false + end @locked end def lock_user - run_command(:command => "pw lock #{@new_resource.username}") + shell_out_compact!("pw", "lock", new_resource.username) end def unlock_user - run_command(:command => "pw unlock #{@new_resource.username}") + shell_out_compact!("pw", "unlock", new_resource.username) end def set_options - opts = " #{@new_resource.username}" + opts = [ new_resource.username ] field_list = { "comment" => "-c", @@ -77,35 +74,29 @@ class Chef "uid" => "-u", "shell" => "-s", } - field_list.sort { |a, b| a[0] <=> b[0] }.each do |field, option| + field_list.sort_by { |a| a[0] }.each do |field, option| field_symbol = field.to_sym - if @current_resource.send(field_symbol) != @new_resource.send(field_symbol) - if @new_resource.send(field_symbol) - Chef::Log.debug("#{@new_resource} setting #{field} to #{@new_resource.send(field_symbol)}") - opts << " #{option} '#{@new_resource.send(field_symbol)}'" - end + next unless current_resource.send(field_symbol) != new_resource.send(field_symbol) + if new_resource.send(field_symbol) + logger.trace("#{new_resource} setting #{field} to #{new_resource.send(field_symbol)}") + opts << option + opts << new_resource.send(field_symbol) end end - if @new_resource.supports[:manage_home] - Chef::Log.debug("#{@new_resource} is managing the users home directory") - opts << " -m" + if new_resource.manage_home + logger.trace("#{new_resource} is managing the users home directory") + opts << "-m" end opts end def modify_password - if (not @new_resource.password.nil?) && (@current_resource.password != @new_resource.password) - Chef::Log.debug("#{new_resource} updating password") - command = "pw usermod #{@new_resource.username} -H 0" - status = popen4(command, :waitlast => true) do |pid, stdin, stdout, stderr| - stdin.puts "#{@new_resource.password}" - end - - unless status.exitstatus == 0 - raise Chef::Exceptions::User, "pw failed - #{status.inspect}!" - end + if !new_resource.password.nil? && (current_resource.password != new_resource.password) + logger.trace("#{new_resource} updating password") + command = "pw usermod #{new_resource.username} -H 0" + shell_out!(command, input: new_resource.password.to_s) else - Chef::Log.debug("#{new_resource} no change needed to password") + logger.trace("#{new_resource} no change needed to password") end end end diff --git a/lib/chef/provider/user/solaris.rb b/lib/chef/provider/user/solaris.rb index 1f0cbb6054..59074d5ba8 100644 --- a/lib/chef/provider/user/solaris.rb +++ b/lib/chef/provider/user/solaris.rb @@ -2,7 +2,7 @@ # Author:: Stephen Nelson-Smith (<sns@chef.io>) # Author:: Jon Ramsey (<jonathon.ramsey@gmail.com>) # Author:: Dave Eddy (<dave@daveeddy.com>) -# Copyright:: Copyright 2012-2016, Chef Software Inc. +# Copyright:: Copyright 2012-2017, Chef Software Inc. # Copyright:: Copyright 2015-2016, Dave Eddy # License:: Apache License, Version 2.0 # @@ -24,8 +24,9 @@ class Chef class Provider class User class Solaris < Chef::Provider::User::Useradd - provides :user, platform: %w{omnios solaris2} - UNIVERSAL_OPTIONS = [[:comment, "-c"], [:gid, "-g"], [:shell, "-s"], [:uid, "-u"]] + provides :solaris_user + provides :user, os: %w{omnios solaris2} + UNIVERSAL_OPTIONS = [[:comment, "-c"], [:gid, "-g"], [:shell, "-s"], [:uid, "-u"]].freeze attr_writer :password_file @@ -45,38 +46,45 @@ class Chef end def check_lock - shadow_line = shell_out!("getent", "shadow", new_resource.username).stdout.strip rescue nil + user = IO.read(@password_file).match(/^#{Regexp.escape(new_resource.username)}:([^:]*):/) - # if the command fails we return nil, this can happen if the user - # in question doesn't exist - return nil if shadow_line.nil? + # If we're in whyrun mode, and the user is not created, we assume it will be + return false if whyrun_mode? && user.nil? - # convert "dave:NP:16507::::::\n" to "NP" - fields = shadow_line.split(":") + raise Chef::Exceptions::User, "Cannot determine if #{new_resource} is locked!" if user.nil? - # '*LK*...' and 'LK' are both considered locked, - # so look for LK at the beginning of the shadow entry - # optionally surrounded by '*' - @locked = !!fields[1].match(/^\*?LK\*?/) - - @locked + @locked = user[1].start_with?("*LK*") end def lock_user - shell_out!("passwd", "-l", new_resource.username) + shell_out_compact!("passwd", "-l", new_resource.username) end def unlock_user - shell_out!("passwd", "-u", new_resource.username) + shell_out_compact!("passwd", "-u", new_resource.username) end - private + private + + # Override the version from {#Useradd} because Solaris doesn't support + # system users and therefore has no `-r` option. This also inverts the + # logic for manage_home as Solaris defaults to no-manage-home and only + # offers `-m`. + # + # @since 12.15 + # @api private + # @see Useradd#useradd_options + # @return [Array<String>] + def useradd_options + opts = [] + opts << "-m" if new_resource.manage_home + opts + end def manage_password - if @current_resource.password != @new_resource.password && @new_resource.password - Chef::Log.debug("#{@new_resource} setting password to #{@new_resource.password}") - write_shadow_file - end + return unless current_resource.password != new_resource.password && new_resource.password + logger.trace("#{new_resource} setting password to #{new_resource.password}") + write_shadow_file end def write_shadow_file @@ -84,7 +92,7 @@ class Chef ::File.open(@password_file) do |shadow_file| shadow_file.each do |entry| user = entry.split(":").first - if user == @new_resource.username + if user == new_resource.username buffer.write(updated_password(entry)) else buffer.write(entry) @@ -95,7 +103,7 @@ class Chef # FIXME: mostly duplicates code with file provider deploying a file s = ::File.stat(@password_file) - mode = s.mode & 07777 + mode = s.mode & 0o7777 uid = s.uid gid = s.gid @@ -107,7 +115,7 @@ class Chef def updated_password(entry) fields = entry.split(":") - fields[1] = @new_resource.password + fields[1] = new_resource.password fields[2] = days_since_epoch fields.join(":") end diff --git a/lib/chef/provider/user/useradd.rb b/lib/chef/provider/user/useradd.rb index 3fef8d3642..47c0ece101 100644 --- a/lib/chef/provider/user/useradd.rb +++ b/lib/chef/provider/user/useradd.rb @@ -1,6 +1,6 @@ # # Author:: Adam Jacob (<adam@chef.io>) -# Copyright:: Copyright 2008-2016, Chef Software Inc. +# Copyright:: Copyright 2008-2017, Chef Software Inc. # License:: Apache License, Version 2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -23,45 +23,45 @@ class Chef class Provider class User class Useradd < Chef::Provider::User - provides :user + # the linux version of this has been forked off, this is the base class now of solaris and AIX and should be abandoned + # and those provider should be rewritten like the linux version. - UNIVERSAL_OPTIONS = [[:comment, "-c"], [:gid, "-g"], [:password, "-p"], [:shell, "-s"], [:uid, "-u"]] + UNIVERSAL_OPTIONS = [[:comment, "-c"], [:gid, "-g"], [:password, "-p"], [:shell, "-s"], [:uid, "-u"]].freeze def create_user command = compile_command("useradd") do |useradd| useradd.concat(universal_options) useradd.concat(useradd_options) end - shell_out!(*command) + shell_out_compact!(command) end def manage_user - unless universal_options.empty? - command = compile_command("usermod") do |u| - u.concat(universal_options) - end - shell_out!(*command) + return if universal_options.empty? + command = compile_command("usermod") do |u| + u.concat(universal_options) end + shell_out_compact!(command) end def remove_user command = [ "userdel" ] - command << "-r" if managing_home_dir? + command << "-r" if new_resource.manage_home command << "-f" if new_resource.force command << new_resource.username - shell_out!(*command) + shell_out_compact!(command) end def check_lock # we can get an exit code of 1 even when it's successful on # rhel/centos (redhat bug 578534). See additional error checks below. - passwd_s = shell_out!("passwd", "-S", new_resource.username, :returns => [0, 1]) + passwd_s = shell_out_compact!("passwd", "-S", new_resource.username, returns: [0, 1]) if whyrun_mode? && passwd_s.stdout.empty? && passwd_s.stderr.match(/does not exist/) # if we're in whyrun mode and the user is not yet created we assume it would be return false end - raise Chef::Exceptions::User, "Cannot determine if #{@new_resource} is locked!" if passwd_s.stdout.empty? + raise Chef::Exceptions::User, "Cannot determine if #{new_resource} is locked!" if passwd_s.stdout.empty? status_line = passwd_s.stdout.split(" ") case status_line[1] @@ -76,7 +76,7 @@ class Chef unless passwd_s.exitstatus == 0 raise_lock_error = false if %w{redhat centos}.include?(node[:platform]) - passwd_version_check = shell_out!("rpm -q passwd") + passwd_version_check = shell_out_compact!("rpm", "-q", "passwd") passwd_version = passwd_version_check.stdout.chomp unless passwd_version == "passwd-0.73-1" @@ -93,11 +93,11 @@ class Chef end def lock_user - shell_out!("usermod", "-L", new_resource.username) + shell_out_compact!("usermod", "-L", new_resource.username) end def unlock_user - shell_out!("usermod", "-U", new_resource.username) + shell_out_compact!("usermod", "-U", new_resource.username) end def compile_command(base_command) @@ -116,31 +116,30 @@ class Chef update_options(field, option, opts) end if updating_home? - if managing_home_dir? - Chef::Log.debug("#{new_resource} managing the users home directory") - opts << "-d" << new_resource.home << "-m" + opts << "-d" << new_resource.home + if new_resource.manage_home + logger.trace("#{new_resource} managing the users home directory") + opts << "-m" else - Chef::Log.debug("#{new_resource} setting home to #{new_resource.home}") - opts << "-d" << new_resource.home + logger.trace("#{new_resource} setting home to #{new_resource.home}") end end - opts << "-o" if new_resource.non_unique || new_resource.supports[:non_unique] + opts << "-o" if new_resource.non_unique opts end end def update_options(field, option, opts) - if @current_resource.send(field).to_s != new_resource.send(field).to_s - if new_resource.send(field) - Chef::Log.debug("#{new_resource} setting #{field} to #{new_resource.send(field)}") - opts << option << new_resource.send(field).to_s - end - end + return unless current_resource.send(field).to_s != new_resource.send(field).to_s + return unless new_resource.send(field) + logger.trace("#{new_resource} setting #{field} to #{new_resource.send(field)}") + opts << option << new_resource.send(field).to_s end def useradd_options opts = [] opts << "-r" if new_resource.system + opts << "-M" unless new_resource.manage_home opts end @@ -149,12 +148,8 @@ class Chef # Pathname#cleanpath does a better job than ::File::expand_path (on both unix and windows) # ::File.expand_path("///tmp") == ::File.expand_path("/tmp") => false # ::File.expand_path("\\tmp") => "C:/tmp" - return true if @current_resource.home.nil? && new_resource.home - new_resource.home && Pathname.new(@current_resource.home).cleanpath != Pathname.new(new_resource.home).cleanpath - end - - def managing_home_dir? - new_resource.manage_home || new_resource.supports[:manage_home] + return true if current_resource.home.nil? && new_resource.home + new_resource.home && Pathname.new(current_resource.home).cleanpath != Pathname.new(new_resource.home).cleanpath end end diff --git a/lib/chef/provider/user/windows.rb b/lib/chef/provider/user/windows.rb index 9545b1fd59..994f1a6774 100644 --- a/lib/chef/provider/user/windows.rb +++ b/lib/chef/provider/user/windows.rb @@ -26,36 +26,35 @@ class Chef class Provider class User class Windows < Chef::Provider::User - + provides :windows_user provides :user, os: "windows" def initialize(new_resource, run_context) super - @net_user = Chef::Util::Windows::NetUser.new(@new_resource.username) + @net_user = Chef::Util::Windows::NetUser.new(new_resource.username) end def load_current_resource - if @new_resource.gid - Chef::Log.warn("The 'gid' attribute is not implemented by the Windows platform. Please use the 'group' resource to assign a user to a group.") + if new_resource.gid + logger.warn("The 'gid' (or 'group') property is not implemented on the Windows platform. Please use the `members` property of the 'group' resource to assign a user to a group.") end - @current_resource = Chef::Resource::User.new(@new_resource.name) - @current_resource.username(@new_resource.username) - user_info = nil + @current_resource = Chef::Resource::User.new(new_resource.name) + current_resource.username(new_resource.username) begin user_info = @net_user.get_info - @current_resource.uid(user_info[:user_id]) - @current_resource.comment(user_info[:full_name]) - @current_resource.home(user_info[:home_dir]) - @current_resource.shell(user_info[:script_path]) + current_resource.uid(user_info[:user_id]) + current_resource.comment(user_info[:full_name]) + current_resource.home(user_info[:home_dir]) + current_resource.shell(user_info[:script_path]) rescue Chef::Exceptions::UserIDNotFound => e # e.message should be "The user name could not be found" but checking for that could cause a localization bug @user_exists = false - Chef::Log.debug("#{@new_resource} does not exist (#{e.message})") + logger.trace("#{new_resource} does not exist (#{e.message})") end - @current_resource + current_resource end # Check to see if the user needs any changes @@ -64,12 +63,12 @@ class Chef # <true>:: If a change is required # <false>:: If the users are identical def compare_user - unless @net_user.validate_credentials(@new_resource.password) - Chef::Log.debug("#{@new_resource} password has changed") + unless @net_user.validate_credentials(new_resource.password) + logger.trace("#{new_resource} password has changed") return true end [ :uid, :comment, :home, :shell ].any? do |user_attrib| - !@new_resource.send(user_attrib).nil? && @new_resource.send(user_attrib) != @current_resource.send(user_attrib) + !new_resource.send(user_attrib).nil? && new_resource.send(user_attrib) != current_resource.send(user_attrib) end end @@ -98,7 +97,7 @@ class Chef end def set_options - opts = { :name => @new_resource.username } + opts = { name: new_resource.username } field_list = { "comment" => "full_name", @@ -108,16 +107,14 @@ class Chef "password" => "password", } - field_list.sort { |a, b| a[0] <=> b[0] }.each do |field, option| + field_list.sort_by { |a| a[0] }.each do |field, option| field_symbol = field.to_sym - if @current_resource.send(field_symbol) != @new_resource.send(field_symbol) - if @new_resource.send(field_symbol) - unless field_symbol == :password - Chef::Log.debug("#{@new_resource} setting #{field} to #{@new_resource.send(field_symbol)}") - end - opts[option.to_sym] = @new_resource.send(field_symbol) - end + next unless current_resource.send(field_symbol) != new_resource.send(field_symbol) + next unless new_resource.send(field_symbol) + unless field_symbol == :password + logger.trace("#{new_resource} setting #{field} to #{new_resource.send(field_symbol)}") end + opts[option.to_sym] = new_resource.send(field_symbol) end opts end diff --git a/lib/chef/provider/whyrun_safe_ruby_block.rb b/lib/chef/provider/whyrun_safe_ruby_block.rb index 3ea48017b7..ee4a659b00 100644 --- a/lib/chef/provider/whyrun_safe_ruby_block.rb +++ b/lib/chef/provider/whyrun_safe_ruby_block.rb @@ -22,10 +22,10 @@ class Chef provides :whyrun_safe_ruby_block def action_run - @new_resource.block.call - @new_resource.updated_by_last_action(true) - @run_context.events.resource_update_applied(@new_resource, :create, "execute the whyrun_safe_ruby_block #{@new_resource.name}") - Chef::Log.info("#{@new_resource} called") + new_resource.block.call + new_resource.updated_by_last_action(true) + @run_context.events.resource_update_applied(new_resource, :create, "execute the whyrun_safe_ruby_block #{new_resource.name}") + logger.info("#{new_resource} called") end end end diff --git a/lib/chef/provider/windows_env.rb b/lib/chef/provider/windows_env.rb new file mode 100644 index 0000000000..4e7fa34216 --- /dev/null +++ b/lib/chef/provider/windows_env.rb @@ -0,0 +1,207 @@ +# +# Author:: Doug MacEachern (<dougm@vmware.com>) +# Copyright:: Copyright 2010-2016, VMware, 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" +require "chef/resource/windows_env" +require "chef/mixin/windows_env_helper" + +class Chef + class Provider + class WindowsEnv < Chef::Provider + include Chef::Mixin::WindowsEnvHelper + attr_accessor :key_exists + + provides :env + provides :windows_env + + def whyrun_supported? + false + end + + def initialize(new_resource, run_context) + super + @key_exists = true + end + + def load_current_resource + @current_resource = Chef::Resource::WindowsEnv.new(new_resource.name) + current_resource.key_name(new_resource.key_name) + + if env_key_exists(new_resource.key_name) + current_resource.value(env_value(new_resource.key_name)) + else + @key_exists = false + logger.trace("#{new_resource} key does not exist") + end + + current_resource + end + + def env_key_exists(key_name) + env_value(key_name) ? true : false + end + + # Check to see if value needs any changes + # + # ==== Returns + # <true>:: If a change is required + # <false>:: If a change is not required + def requires_modify_or_create? + if new_resource.delim + #e.g. check for existing value within PATH + new_values.inject(0) do |index, val| + next_index = current_values.find_index val + return true if next_index.nil? || next_index < index + next_index + end + false + else + new_resource.value != current_resource.value + end + end + + alias_method :compare_value, :requires_modify_or_create? + + def action_create + if @key_exists + if requires_modify_or_create? + modify_env + logger.info("#{new_resource} altered") + new_resource.updated_by_last_action(true) + end + else + create_env + logger.info("#{new_resource} created") + new_resource.updated_by_last_action(true) + end + end + + #e.g. delete a PATH element + # + # ==== Returns + # <true>:: If we handled the element case and caller should not delete the key + # <false>:: Caller should delete the key, either no :delim was specific or value was empty + # after we removed the element. + def delete_element + return false unless new_resource.delim #no delim: delete the key + needs_delete = new_values.any? { |v| current_values.include?(v) } + if !needs_delete + logger.trace("#{new_resource} element '#{new_resource.value}' does not exist") + return true #do not delete the key + else + new_value = + current_values.select do |item| + not new_values.include?(item) + end.join(new_resource.delim) + + if new_value.empty? + return false #nothing left here, delete the key + else + old_value = new_resource.value(new_value) + create_env + logger.trace("#{new_resource} deleted #{old_value} element") + new_resource.updated_by_last_action(true) + return true #we removed the element and updated; do not delete the key + end + end + end + + def action_delete + if ( ENV[new_resource.key_name] || @key_exists ) && !delete_element + delete_env + logger.info("#{new_resource} deleted") + new_resource.updated_by_last_action(true) + end + end + + def action_modify + if @key_exists + if requires_modify_or_create? + modify_env + logger.info("#{new_resource} modified") + new_resource.updated_by_last_action(true) + end + else + raise Chef::Exceptions::WindowsEnv, "Cannot modify #{new_resource} - key does not exist!" + end + end + + def create_env + obj = env_obj(@new_resource.key_name) + unless obj + obj = WIN32OLE.connect("winmgmts://").get("Win32_Environment").spawninstance_ + obj.name = @new_resource.key_name + obj.username = new_resource.user + end + obj.variablevalue = @new_resource.value + obj.put_ + value = @new_resource.value + value = expand_path(value) if @new_resource.key_name.casecmp("PATH") == 0 + ENV[@new_resource.key_name] = value + broadcast_env_change + end + + def delete_env + obj = env_obj(@new_resource.key_name) + if obj + obj.delete_ + broadcast_env_change + end + if ENV[@new_resource.key_name] + ENV.delete(@new_resource.key_name) + end + end + + def modify_env + if new_resource.delim + new_resource.value((new_values + current_values).uniq.join(new_resource.delim)) + end + create_env + end + + # Returns the current values to split by delimiter + def current_values + @current_values ||= current_resource.value.split(new_resource.delim) + end + + # Returns the new values to split by delimiter + def new_values + @new_values ||= new_resource.value.split(new_resource.delim) + end + + def env_value(key_name) + obj = env_obj(key_name) + obj.variablevalue if obj + end + + def env_obj(key_name) + return @env_obj if @env_obj + wmi = WmiLite::Wmi.new + # Note that by design this query is case insensitive with regard to key_name + environment_variables = wmi.query("select * from Win32_Environment where name = '#{key_name}'") + if environment_variables && environment_variables.length > 0 + environment_variables.each do |env| + @env_obj = env.wmi_ole_object + return @env_obj if @env_obj.username.split('\\').last.casecmp(new_resource.user) == 0 + end + end + @env_obj = nil + end + end + end +end diff --git a/lib/chef/provider/windows_path.rb b/lib/chef/provider/windows_path.rb new file mode 100644 index 0000000000..1c78e20606 --- /dev/null +++ b/lib/chef/provider/windows_path.rb @@ -0,0 +1,61 @@ +# +# Author:: Nimisha Sharad (<nimisha.sharad@msystechnologies.com>) +# Copyright:: Copyright 2008-2017, 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/mixin/windows_env_helper" if Chef::Platform.windows? +require "chef/mixin/wide_string" +require "chef/exceptions" + +class Chef + class Provider + class WindowsPath < Chef::Provider + include Chef::Mixin::WindowsEnvHelper if Chef::Platform.windows? + + provides :windows_path + + def load_current_resource + @current_resource = Chef::Resource::WindowsPath.new(new_resource.name) + @current_resource.path(new_resource.path) + @current_resource + end + + action :add do + # The windows Env provider does not correctly expand variables in + # the PATH environment variable. Ruby expects these to be expanded. + # + path = expand_path(new_resource.path) + declare_resource(:env, "path") do + action :modify + delim ::File::PATH_SEPARATOR + value path.tr("/", '\\') + end + end + + action :remove do + # The windows Env provider does not correctly expand variables in + # the PATH environment variable. Ruby expects these to be expanded. + # + path = expand_path(new_resource.path) + declare_resource(:env, "path") do + action :delete + delim ::File::PATH_SEPARATOR + value path.tr("/", '\\') + end + end + end + end +end diff --git a/lib/chef/provider/windows_script.rb b/lib/chef/provider/windows_script.rb index 2de127addf..3b0202790c 100644 --- a/lib/chef/provider/windows_script.rb +++ b/lib/chef/provider/windows_script.rb @@ -33,8 +33,11 @@ class Chef super( new_resource, run_context ) @script_extension = script_extension - target_architecture = new_resource.architecture.nil? ? - node_windows_architecture(run_context.node) : new_resource.architecture + target_architecture = if new_resource.architecture.nil? + node_windows_architecture(run_context.node) + else + new_resource.architecture + end @is_wow64 = wow64_architecture_override_required?(run_context.node, target_architecture) diff --git a/lib/chef/provider/windows_task.rb b/lib/chef/provider/windows_task.rb new file mode 100644 index 0000000000..9a6fd39582 --- /dev/null +++ b/lib/chef/provider/windows_task.rb @@ -0,0 +1,587 @@ +# +# Author:: Nimisha Sharad (<nimisha.sharad@msystechnologies.com>) +# Copyright:: Copyright 2008-2018, 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/mixin/shell_out" +require "rexml/document" +require "iso8601" +require "chef/mixin/powershell_out" +require "chef/provider" +require "win32/taskscheduler" if Chef::Platform.windows? + +class Chef + class Provider + class WindowsTask < Chef::Provider + include Chef::Mixin::ShellOut + include Chef::Mixin::PowershellOut + + if Chef::Platform.windows? + include Win32 + + provides :windows_task + + MONTHS = { + JAN: TaskScheduler::JANUARY, + FEB: TaskScheduler::FEBRUARY, + MAR: TaskScheduler::MARCH, + APR: TaskScheduler::APRIL, + MAY: TaskScheduler::MAY, + JUN: TaskScheduler::JUNE, + JUL: TaskScheduler::JULY, + AUG: TaskScheduler::AUGUST, + SEP: TaskScheduler::SEPTEMBER, + OCT: TaskScheduler::OCTOBER, + NOV: TaskScheduler::NOVEMBER, + DEC: TaskScheduler::DECEMBER + } + + DAYS_OF_WEEK = { MON: TaskScheduler::MONDAY, + TUE: TaskScheduler::TUESDAY, + WED: TaskScheduler::WEDNESDAY, + THU: TaskScheduler::THURSDAY, + FRI: TaskScheduler::FRIDAY, + SAT: TaskScheduler::SATURDAY, + SUN: TaskScheduler::SUNDAY } + + WEEKS_OF_MONTH = { + FIRST: TaskScheduler::FIRST_WEEK, + SECOND: TaskScheduler::SECOND_WEEK, + THIRD: TaskScheduler::THIRD_WEEK, + FOURTH: TaskScheduler::FOURTH_WEEK + } + + DAYS_OF_MONTH = { + 1 => TaskScheduler::TASK_FIRST, + 2 => TaskScheduler::TASK_SECOND, + 3 => TaskScheduler::TASK_THIRD, + 4 => TaskScheduler::TASK_FOURTH, + 5 => TaskScheduler::TASK_FIFTH, + 6 => TaskScheduler::TASK_SIXTH, + 7 => TaskScheduler::TASK_SEVENTH, + 8 => TaskScheduler::TASK_EIGHTH, + 9 => TaskScheduler::TASK_NINETH, + 10 => TaskScheduler::TASK_TENTH, + 11 => TaskScheduler::TASK_ELEVENTH, + 12 => TaskScheduler::TASK_TWELFTH, + 13 => TaskScheduler::TASK_THIRTEENTH, + 14 => TaskScheduler::TASK_FOURTEENTH, + 15 => TaskScheduler::TASK_FIFTEENTH, + 16 => TaskScheduler::TASK_SIXTEENTH, + 17 => TaskScheduler::TASK_SEVENTEENTH, + 18 => TaskScheduler::TASK_EIGHTEENTH, + 19 => TaskScheduler::TASK_NINETEENTH, + 20 => TaskScheduler::TASK_TWENTIETH, + 21 => TaskScheduler::TASK_TWENTY_FIRST, + 22 => TaskScheduler::TASK_TWENTY_SECOND, + 23 => TaskScheduler::TASK_TWENTY_THIRD, + 24 => TaskScheduler::TASK_TWENTY_FOURTH, + 25 => TaskScheduler::TASK_TWENTY_FIFTH, + 26 => TaskScheduler::TASK_TWENTY_SIXTH, + 27 => TaskScheduler::TASK_TWENTY_SEVENTH, + 28 => TaskScheduler::TASK_TWENTY_EIGHTH, + 29 => TaskScheduler::TASK_TWENTY_NINTH, + 30 => TaskScheduler::TASK_THIRTYETH, + 31 => TaskScheduler::TASK_THIRTY_FIRST + } + + def load_current_resource + @current_resource = Chef::Resource::WindowsTask.new(new_resource.name) + task = TaskScheduler.new + if task.exists?(new_resource.task_name) + @current_resource.exists = true + task.get_task(new_resource.task_name) + @current_resource.task = task + pathed_task_name = new_resource.task_name.start_with?('\\') ? new_resource.task_name : "\\#{new_resource.task_name}" + @current_resource.task_name(pathed_task_name) + else + @current_resource.exists = false + end + @current_resource + end + + def action_create + if current_resource.exists + logger.trace "#{new_resource} task exist." + unless (task_needs_update?(current_resource.task)) || (new_resource.force) + logger.info "#{new_resource} task does not need updating and force is not specified - nothing to do" + return + end + + # if start_day and start_time is not set by user current date and time will be set while updating any property + set_start_day_and_time unless new_resource.frequency == :none + update_task(current_resource.task) + else + basic_validation + set_start_day_and_time + converge_by("#{new_resource} task created") do + task = TaskScheduler.new + if new_resource.frequency == :none + task.new_work_item(new_resource.task_name, {}) + task.activate(new_resource.task_name) + else + task.new_work_item(new_resource.task_name, trigger) + end + task.application_name = new_resource.command + task.working_directory = new_resource.cwd if new_resource.cwd + task.configure_settings(config_settings) + task.configure_principals(principal_settings) + task.set_account_information(new_resource.user, new_resource.password) + task.creator = new_resource.user + task.activate(new_resource.task_name) + end + end + end + + def action_run + if current_resource.exists + logger.trace "#{new_resource} task exists" + if current_resource.task.status == "running" + logger.info "#{new_resource} task is currently running, skipping run" + else + converge_by("run scheduled task #{new_resource}") do + current_resource.task.run + end + end + else + logger.warn "#{new_resource} task does not exist - nothing to do" + end + end + + def action_delete + if current_resource.exists + logger.trace "#{new_resource} task exists" + converge_by("delete scheduled task #{new_resource}") do + ts = TaskScheduler.new + ts.delete(current_resource.task_name) + end + else + logger.warn "#{new_resource} task does not exist - nothing to do" + end + end + + def action_end + if current_resource.exists + logger.trace "#{new_resource} task exists" + if current_resource.task.status != "running" + logger.trace "#{new_resource} is not running - nothing to do" + else + converge_by("#{new_resource} task ended") do + current_resource.task.stop + end + end + else + logger.warn "#{new_resource} task does not exist - nothing to do" + end + end + + def action_enable + if current_resource.exists + logger.trace "#{new_resource} task exists" + if current_resource.task.status == "not scheduled" + converge_by("#{new_resource} task enabled") do + #TODO wind32-taskscheduler currently not having any method to handle this so using schtasks.exe here + run_schtasks "CHANGE", "ENABLE" => "" + end + else + logger.trace "#{new_resource} already enabled - nothing to do" + end + else + logger.fatal "#{new_resource} task does not exist - nothing to do" + raise Errno::ENOENT, "#{new_resource}: task does not exist, cannot enable" + end + end + + def action_disable + if current_resource.exists + logger.info "#{new_resource} task exists" + if %w{ready running}.include?(current_resource.task.status) + converge_by("#{new_resource} task disabled") do + #TODO: in win32-taskscheduler there is no method whcih disbales the task so currently calling disable with schtasks.exe + run_schtasks "CHANGE", "DISABLE" => "" + end + else + logger.warn "#{new_resource} already disabled - nothing to do" + end + else + logger.warn "#{new_resource} task does not exist - nothing to do" + end + end + + alias_method :action_change, :action_create + + private + + def set_start_day_and_time + new_resource.start_day = Time.now.strftime("%m/%d/%Y") unless new_resource.start_day + new_resource.start_time = Time.now.strftime("%H:%M") unless new_resource.start_time + end + + def update_task(task) + converge_by("#{new_resource} task updated") do + task.set_account_information(new_resource.user, new_resource.password) + task.application_name = new_resource.command if new_resource.command + task.working_directory = new_resource.cwd if new_resource.cwd + task.trigger = trigger unless new_resource.frequency == :none + task.configure_settings(config_settings) + task.creator = new_resource.user + task.configure_principals(principal_settings) + end + end + + def trigger + start_month, start_day, start_year = new_resource.start_day.to_s.split("/") + start_hour, start_minute = new_resource.start_time.to_s.split(":") + #TODO currently end_month, end_year and end_year needs to be set to 0. If not set win32-taskscheduler throwing nil into integer error. + trigger_hash = { + start_year: start_year.to_i, + start_month: start_month.to_i, + start_day: start_day.to_i, + start_hour: start_hour.to_i, + start_minute: start_minute.to_i, + end_month: 0, + end_day: 0, + end_year: 0, + trigger_type: trigger_type, + type: type, + random_minutes_interval: new_resource.random_delay + } + + if new_resource.frequency == :minute + trigger_hash[:minutes_interval] = new_resource.frequency_modifier + end + + if new_resource.frequency == :hourly + minutes = convert_hours_in_minutes(new_resource.frequency_modifier.to_i) + trigger_hash[:minutes_interval] = minutes + end + + if new_resource.minutes_interval + trigger_hash[:minutes_interval] = new_resource.minutes_interval + end + + if new_resource.minutes_duration + trigger_hash[:minutes_duration] = new_resource.minutes_duration + end + + if trigger_type == TaskScheduler::MONTHLYDOW && frequency_modifier_contains_last_week?(new_resource.frequency_modifier) + trigger_hash[:run_on_last_week_of_month] = true + else + trigger_hash[:run_on_last_week_of_month] = false + end + + if trigger_type == TaskScheduler::MONTHLYDATE && day_includes_last_or_lastday?(new_resource.day) + trigger_hash[:run_on_last_day_of_month] = true + else + trigger_hash[:run_on_last_day_of_month] = false + end + trigger_hash + end + + def frequency_modifier_contains_last_week?(frequency_modifier) + frequency_modifier = frequency_modifier.to_s.split(",") + frequency_modifier.map! { |value| value.strip.upcase } + frequency_modifier.include?("LAST") + end + + def day_includes_last_or_lastday?(day) + day = day.to_s.split(",") + day.map! { |value| value.strip.upcase } + day.include?("LAST") || day.include?("LASTDAY") + end + + def convert_hours_in_minutes(hours) + hours.to_i * 60 if hours + end + + #TODO : Try to optimize this method + # known issue : Since start_day and time is not mandatory while updating weekly frequency for which start_day is not mentioned by user idempotency + # is not gettting maintained as new_resource.start_day is nil and we fetch the day of week from start_day to set and its currently coming as nil and don't match with current_task + def task_needs_update?(task) + flag = false + if new_resource.frequency == :none + flag = (task.account_information != new_resource.user || + task.application_name != new_resource.command || + task.principals[:run_level] != run_level) + else + current_task_trigger = task.trigger(0) + new_task_trigger = trigger + flag = (ISO8601::Duration.new(task.idle_settings[:idle_duration])) != (ISO8601::Duration.new(new_resource.idle_time * 60)) if new_resource.frequency == :on_idle + flag = (ISO8601::Duration.new(task.execution_time_limit)) != (ISO8601::Duration.new(new_resource.execution_time_limit * 60)) unless new_resource.execution_time_limit.nil? + + # if trigger not found updating the task to add the trigger + if current_task_trigger.nil? + flag = true + else + flag = true if start_day_updated?(current_task_trigger, new_task_trigger) == true || + start_time_updated?(current_task_trigger, new_task_trigger) == true || + current_task_trigger[:trigger_type] != new_task_trigger[:trigger_type] || + current_task_trigger[:type] != new_task_trigger[:type] || + current_task_trigger[:random_minutes_interval].to_i != new_task_trigger[:random_minutes_interval].to_i || + current_task_trigger[:minutes_interval].to_i != new_task_trigger[:minutes_interval].to_i || + task.account_information != new_resource.user || + task.application_name != new_resource.command || + task.working_directory != new_resource.cwd.to_s || + task.principals[:logon_type] != logon_type || + task.principals[:run_level] != run_level + + if trigger_type == TaskScheduler::MONTHLYDATE + flag = true if current_task_trigger[:run_on_last_day_of_month] != new_task_trigger[:run_on_last_day_of_month] + end + + if trigger_type == TaskScheduler::MONTHLYDOW + flag = true if current_task_trigger[:run_on_last_week_of_month] != new_task_trigger[:run_on_last_week_of_month] + end + end + end + flag + end + + def start_day_updated?(current_task_trigger, new_task_trigger) + ( new_resource.start_day && (current_task_trigger[:start_year].to_i != new_task_trigger[:start_year] || + current_task_trigger[:start_month].to_i != new_task_trigger[:start_month] || + current_task_trigger[:start_day].to_i != new_task_trigger[:start_day]) ) + end + + def start_time_updated?(current_task_trigger, new_task_trigger) + ( new_resource.start_time && ( current_task_trigger[:start_hour].to_i != new_task_trigger[:start_hour] || + current_task_trigger[:start_minute].to_i != new_task_trigger[:start_minute] ) ) + end + + def trigger_type + case new_resource.frequency + when :once, :minute, :hourly + TaskScheduler::ONCE + when :daily + TaskScheduler::DAILY + when :weekly + TaskScheduler::WEEKLY + when :monthly + # If frequency modifier is set with frequency :monthly we are setting taskscheduler as monthlydow + # Ref https://msdn.microsoft.com/en-us/library/windows/desktop/aa382061(v=vs.85).aspx + new_resource.frequency_modifier.to_i.between?(1, 12) ? TaskScheduler::MONTHLYDATE : TaskScheduler::MONTHLYDOW + when :on_idle + TaskScheduler::ON_IDLE + when :onstart + TaskScheduler::AT_SYSTEMSTART + when :on_logon + TaskScheduler::AT_LOGON + else + raise ArgumentError, "Please set frequency" + end + end + + def type + case trigger_type + when TaskScheduler::ONCE + { once: nil } + when TaskScheduler::DAILY + { days_interval: new_resource.frequency_modifier.to_i } + when TaskScheduler::WEEKLY + { weeks_interval: new_resource.frequency_modifier.to_i, days_of_week: days_of_week.to_i } + when TaskScheduler::MONTHLYDATE + { months: months_of_year.to_i, days: days_of_month.to_i } + when TaskScheduler::MONTHLYDOW + { months: months_of_year.to_i, days_of_week: days_of_week.to_i, weeks_of_month: weeks_of_month.to_i } + when TaskScheduler::ON_IDLE + # TODO: handle option for this trigger + when TaskScheduler::AT_LOGON + # TODO: handle option for this trigger + when TaskScheduler::AT_SYSTEMSTART + # TODO: handle option for this trigger + end + end + + # Deleting last from the array of weeks of month since last week is handled in :run_on_last_week_of_month parameter. + def weeks_of_month + weeks_of_month = [] + if new_resource.frequency_modifier + weeks = new_resource.frequency_modifier.split(",") + weeks.map! { |week| week.to_s.strip.upcase } + weeks.delete("LAST") if weeks.include?("LAST") + weeks_of_month = get_binary_values_from_constants(weeks, WEEKS_OF_MONTH) + end + weeks_of_month + end + + # Deleting the "LAST" and "LASTDAY" from days since last day is handled in :run_on_last_day_of_month parameter. + def days_of_month + days_of_month = [] + if new_resource.day + days = new_resource.day.split(",") + days.map! { |day| day.to_s.strip.upcase } + days.delete("LAST") if days.include?("LAST") + days.delete("LASTDAY") if days.include?("LASTDAY") + if days - (1..31).to_a + days.each do |day| + days_of_month << DAYS_OF_MONTH[day.to_i] + end + days_of_month = days_of_month.size > 1 ? days_of_month.inject(:|) : days_of_month[0] + end + else + days_of_month = DAYS_OF_MONTH[1] + end + days_of_month + end + + def days_of_week + if new_resource.day + #this line of code is just to support backward compatibility of wild card * + new_resource.day = "mon, tue, wed, thu, fri, sat, sun" if new_resource.day == "*" && new_resource.frequency == :weekly + days = new_resource.day.split(",") + days.map! { |day| day.to_s.strip.upcase } + weeks_days = get_binary_values_from_constants(days, DAYS_OF_WEEK) + else + # following condition will make the frequency :weekly idempotent if start_day is not provided by user setting day as the current_resource day + if (current_resource) && (current_resource.task) && (current_resource.task.trigger(0)[:type][:days_of_week]) && (new_resource.start_day.nil?) + weeks_days = current_resource.task.trigger(0)[:type][:days_of_week] + else + day = get_day(new_resource.start_day).to_sym if new_resource.start_day + DAYS_OF_WEEK[day] + end + end + end + + def months_of_year + months_of_year = [] + if new_resource.frequency_modifier.to_i.between?(1, 12) && !(new_resource.months) + new_resource.months = set_months(new_resource.frequency_modifier.to_i) + end + + if new_resource.months + #this line of code is just to support backward compatibility of wild card * + new_resource.months = "jan, feb, mar, apr, may, jun, jul, aug, sep, oct, nov, dec" if new_resource.months == "*" && new_resource.frequency == :monthly + months = new_resource.months.split(",") + months.map! { |month| month.to_s.strip.upcase } + months_of_year = get_binary_values_from_constants(months, MONTHS) + else + MONTHS.each do |key, value| + months_of_year << MONTHS[key] + end + months_of_year = months_of_year.inject(:|) + end + months_of_year + end + + # This values are set for frequency_modifier set as 1-12 + # This is to give backward compatibility validated this values with earlier code and running schtask.exe + # Used this as reference https://docs.microsoft.com/en-us/windows-server/administration/windows-commands/schtasks#d-dayday-- + def set_months(frequency_modifier) + case frequency_modifier + when 1 + "jan, feb, mar, apr, may, jun, jul, aug, sep, oct, nov, dec" + when 2 + "feb, apr, jun, aug, oct, dec" + when 3 + "mar, jun, sep, dec" + when 4 + "apr, aug, dec" + when 5 + "may, oct" + when 6 + "jun, dec" + when 7 + "jul" + when 8 + "aug" + when 9 + "sep" + when 10 + "oct" + when 11 + "nov" + when 12 + "dec" + end + end + + def get_binary_values_from_constants(array_values, constant) + data = [] + array_values.each do |value| + value = value.to_sym + data << constant[value] + end + data.size > 1 ? data.inject(:|) : data[0] + end + + def run_level + case new_resource.run_level + when :highest + TaskScheduler::TASK_RUNLEVEL_HIGHEST + when :limited + TaskScheduler::TASK_RUNLEVEL_LUA + end + end + + #TODO: while creating the configuration settings win32-taskscheduler it accepts execution time limit values in ISO8601 formata + def config_settings + settings = { + execution_time_limit: new_resource.execution_time_limit, + enabled: true + } + settings[:idle_duration] = new_resource.idle_time if new_resource.idle_time + settings[:run_only_if_idle] = true if new_resource.idle_time + settings + end + + def principal_settings + settings = {} + settings [:run_level] = run_level + settings[:logon_type] = logon_type + settings + end + + def logon_type + # Ref: https://msdn.microsoft.com/en-us/library/windows/desktop/aa383566(v=vs.85).aspx + # if nothing is passed as logon_type the TASK_LOGON_SERVICE_ACCOUNT is getting set as default so using that for comparision. + new_resource.password.nil? ? TaskScheduler::TASK_LOGON_SERVICE_ACCOUNT : TaskScheduler::TASK_LOGON_PASSWORD + end + + # This method checks if task and command attributes exist since those two are mandatory attributes to create a schedules task. + def basic_validation + validate = [] + validate << "Command" if new_resource.command.nil? || new_resource.command.empty? + validate << "Task Name" if new_resource.task_name.nil? || new_resource.task_name.empty? + return true if validate.empty? + raise Chef::Exceptions::ValidationFailed.new "Value for '#{validate.join(', ')}' option cannot be empty" + end + + # rubocop:disable Style/StringLiteralsInInterpolation + def run_schtasks(task_action, options = {}) + cmd = "schtasks /#{task_action} /TN \"#{new_resource.task_name}\" " + options.each_key do |option| + unless option == "TR" + cmd += "/#{option} " + cmd += "\"#{options[option].to_s.gsub('"', "\\\"")}\" " unless options[option] == "" + end + end + # Appending Task Run [TR] option at the end since appending causing sometimes to append other options in option["TR"] value + if options["TR"] + cmd += "/TR \"#{options["TR"]} \" " unless task_action == "DELETE" + end + logger.trace("running: ") + logger.trace(" #{cmd}") + shell_out!(cmd, returns: [0]) + end + # rubocop:enable Style/StringLiteralsInInterpolation + + def get_day(date) + Date.strptime(date, "%m/%d/%Y").strftime("%a").upcase + end + end + end + end +end diff --git a/lib/chef/provider/yum_repository.rb b/lib/chef/provider/yum_repository.rb new file mode 100644 index 0000000000..20a7a8e3d2 --- /dev/null +++ b/lib/chef/provider/yum_repository.rb @@ -0,0 +1,130 @@ +# +# Author:: Thom May (<thom@chef.io>) +# Copyright:: Copyright (c) 2016-2017, 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/resource" +require "chef/dsl/declare_resource" +require "chef/mixin/which" +require "chef/provider/noop" + +class Chef + class Provider + class YumRepository < Chef::Provider + extend Chef::Mixin::Which + + provides :yum_repository do + which "yum" + end + + def load_current_resource + end + + action :create do + declare_resource(:template, "/etc/yum.repos.d/#{new_resource.repositoryid}.repo") do + if template_available?(new_resource.source) + source new_resource.source + else + source ::File.expand_path("../support/yum_repo.erb", __FILE__) + local true + end + sensitive new_resource.sensitive + variables(config: new_resource) + mode new_resource.mode + if new_resource.make_cache + notifies :run, "execute[yum clean metadata #{new_resource.repositoryid}]", :immediately if new_resource.clean_metadata || new_resource.clean_headers + notifies :run, "execute[yum-makecache-#{new_resource.repositoryid}]", :immediately + notifies :create, "ruby_block[package-cache-reload-#{new_resource.repositoryid}]", :immediately + end + end + + declare_resource(:execute, "yum clean metadata #{new_resource.repositoryid}") do + command "yum clean metadata --disablerepo=* --enablerepo=#{new_resource.repositoryid}" + action :nothing + end + + # get the metadata for this repo only + declare_resource(:execute, "yum-makecache-#{new_resource.repositoryid}") do + command "yum -q -y makecache --disablerepo=* --enablerepo=#{new_resource.repositoryid}" + action :nothing + only_if { new_resource.enabled } + end + + # reload internal Chef yum/dnf cache + declare_resource(:ruby_block, "package-cache-reload-#{new_resource.repositoryid}") do + if ( platform?("fedora") && node["platform_version"].to_i >= 22 ) || + ( platform_family?("rhel") && node["platform_version"].to_i >= 8 ) + block { Chef::Provider::Package::Dnf::PythonHelper.instance.restart } + else + block { Chef::Provider::Package::Yum::YumCache.instance.reload } + end + action :nothing + end + end + + action :delete do + # clean the repo cache first + declare_resource(:execute, "yum clean all #{new_resource.repositoryid}") do + command "yum clean all --disablerepo=* --enablerepo=#{new_resource.repositoryid}" + only_if "yum repolist all | grep -P '^#{new_resource.repositoryid}([ \t]|$)'" + end + + declare_resource(:file, "/etc/yum.repos.d/#{new_resource.repositoryid}.repo") do + action :delete + notifies :create, "ruby_block[package-cache-reload-#{new_resource.repositoryid}]", :immediately + end + + declare_resource(:ruby_block, "package-cache-reload-#{new_resource.repositoryid}") do + if ( platform?("fedora") && node["platform_version"].to_i >= 22 ) || + ( platform_family?("rhel") && node["platform_version"].to_i >= 8 ) + block { Chef::Provider::Package::Dnf::PythonHelper.instance.restart } + else + block { Chef::Provider::Package::Yum::YumCache.instance.reload } + end + action :nothing + end + end + + action :makecache do + declare_resource(:execute, "yum-makecache-#{new_resource.repositoryid}") do + command "yum -q -y makecache --disablerepo=* --enablerepo=#{new_resource.repositoryid}" + action :run + only_if { new_resource.enabled } + end + + declare_resource(:ruby_block, "package-cache-reload-#{new_resource.repositoryid}") do + if ( platform?("fedora") && node["platform_version"].to_i >= 22 ) || + ( platform_family?("rhel") && node["platform_version"].to_i >= 8 ) + block { Chef::Provider::Package::Dnf::PythonHelper.instance.restart } + else + block { Chef::Provider::Package::Yum::YumCache.instance.reload } + end + action :run + end + end + + alias_method :action_add, :action_create + alias_method :action_remove, :action_delete + + def template_available?(path) + !path.nil? && run_context.has_template_in_cookbook?(new_resource.cookbook_name, path) + end + + end + end +end + +Chef::Provider::Noop.provides :yum_repository diff --git a/lib/chef/provider/zypper_repository.rb b/lib/chef/provider/zypper_repository.rb new file mode 100644 index 0000000000..369d23a396 --- /dev/null +++ b/lib/chef/provider/zypper_repository.rb @@ -0,0 +1,169 @@ +# +# Author:: Tim Smith (<tsmith@chef.io>) +# Copyright:: Copyright (c) 2017, 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/resource" +require "chef/dsl/declare_resource" +require "chef/provider/noop" +require "chef/mixin/shell_out" +require "shellwords" + +class Chef + class Provider + class ZypperRepository < Chef::Provider + provides :zypper_repository, platform_family: "suse" + + def load_current_resource + end + + action :create do + if new_resource.gpgautoimportkeys + install_gpg_key(new_resource.gpgkey) + else + logger.trace("'gpgautoimportkeys' property is set to false. Skipping key import.") + end + + declare_resource(:template, "/etc/zypp/repos.d/#{escaped_repo_name}.repo") do + if template_available?(new_resource.source) + source new_resource.source + else + source ::File.expand_path("../support/zypper_repo.erb", __FILE__) + local true + end + sensitive new_resource.sensitive + variables(config: new_resource) + mode new_resource.mode + notifies :refresh, new_resource, :immediately if new_resource.refresh_cache + end + end + + action :delete do + declare_resource(:execute, "zypper --quiet --non-interactive removerepo #{escaped_repo_name}") do + only_if "zypper --quiet lr #{escaped_repo_name}" + end + end + + action :refresh do + declare_resource(:execute, "zypper --quiet --non-interactive refresh --force #{escaped_repo_name}") do + only_if "zypper --quiet lr #{escaped_repo_name}" + end + end + + alias_method :action_add, :action_create + alias_method :action_remove, :action_delete + + # zypper repos are allowed to have spaces in the names + # @return [String] escaped repo string + def escaped_repo_name + Shellwords.escape(new_resource.repo_name) + end + + # return the specified cookbook name or the cookbook containing the + # resource. + # + # @return [String] name of the cookbook + def cookbook_name + new_resource.cookbook || new_resource.cookbook_name + end + + # determine if a template file is available in the current run + # @param [String] path the path to the template file + # + # @return [Boolean] template file exists or doesn't + def template_available?(path) + !path.nil? && run_context.has_template_in_cookbook?(cookbook_name, path) + end + + # determine if a cookbook file is available in the run + # @param [String] fn the path to the template file + # + # @return [Boolean] cookbook file exists or doesn't + def has_cookbook_file?(fn) + run_context.has_cookbook_file_in_cookbook?(cookbook_name, fn) + end + + # Given the provided key URI determine what kind of chef resource we need + # to fetch the key + # @param [String] uri the uri of the gpg key (local path or http URL) + # + # @raise [Chef::Exceptions::FileNotFound] Key isn't remote or found in the current run + # + # @return [Symbol] :remote_file or :cookbook_file + def key_type(uri) + if uri.start_with?("http") + logger.trace("Will use :remote_file resource to cache the gpg key locally") + :remote_file + elsif has_cookbook_file?(uri) + logger.trace("Will use :cookbook_file resource to cache the gpg key locally") + :cookbook_file + else + raise Chef::Exceptions::FileNotFound, "Cannot determine location of gpgkey. Must start with 'http' or be a file managed by Chef." + end + end + + # is the provided key already installed + # @param [String] key_path the path to the key on the local filesystem + # + # @return [boolean] is the key already known by rpm + def key_installed?(key_path) + so = shell_out("rpm -qa gpg-pubkey*") + # expected output & match: http://rubular.com/r/RdF7EcXEtb + status = /gpg-pubkey-#{key_fingerprint(key_path)}/.match(so.stdout) + logger.trace("GPG key at #{key_path} is known by rpm? #{status ? "true" : "false"}") + status + end + + # extract the gpg key fingerprint from a local file + # @param [String] key_path the path to the key on the local filesystem + # + # @return [String] the fingerprint of the key + def key_fingerprint(key_path) + so = shell_out!("gpg --with-fingerprint #{key_path}") + # expected output and match: http://rubular.com/r/BpfMjxySQM + fingerprint = /pub\s*\S*\/(\S*)/.match(so.stdout)[1].downcase + logger.trace("GPG fingerprint of key at #{key_path} is #{fingerprint}") + fingerprint + end + + # install the provided gpg key + # @param [String] uri the uri of the local or remote gpg key + def install_gpg_key(uri) + unless uri + logger.trace("'gpgkey' property not provided or set to nil. Skipping key import.") + return + end + + cached_keyfile = ::File.join(Chef::Config[:file_cache_path], uri.split("/")[-1]) + + declare_resource(key_type(new_resource.gpgkey), cached_keyfile) do + source uri + mode "0644" + sensitive new_resource.sensitive + action :create + end + + declare_resource(:execute, "import gpg key from #{new_resource.gpgkey}") do + command "/bin/rpm --import #{cached_keyfile}" + not_if { key_installed?(cached_keyfile) } + action :run + end + end + end + end +end + +Chef::Provider::Noop.provides :zypper_repository |