diff options
Diffstat (limited to 'lib/chef')
80 files changed, 1720 insertions, 1013 deletions
diff --git a/lib/chef/application.rb b/lib/chef/application.rb index 0430d4acfa..abcc81c290 100644 --- a/lib/chef/application.rb +++ b/lib/chef/application.rb @@ -62,6 +62,10 @@ class Chef::Application Chef::Application.fatal!("SIGINT received, stopping", 2) end + trap("TERM") do + Chef::Application.fatal!("SIGTERM received, stopping", 3) + end + unless Chef::Platform.windows? trap("QUIT") do Chef::Log.info("SIGQUIT received, call stack:\n " + caller.join("\n ")) @@ -206,12 +210,78 @@ class Chef::Application ) @chef_client_json = nil - @chef_client.run + if can_fork? + fork_chef_client # allowed to run client in forked process + else + # Unforked interval runs are disabled, so this runs chef-client + # once and then exits. If TERM signal is received, will "ignore" + # the signal to finish converge. + run_with_graceful_exit_option + end @chef_client = nil end end private + def can_fork? + # win32-process gem exposes some form of :fork for Process + # class. So we are seperately ensuring that the platform we're + # running on is not windows before forking. + Chef::Config[:client_fork] && Process.respond_to?(:fork) && !Chef::Platform.windows? + end + + # Run chef-client once and then exit. If TERM signal is received, ignores the + # signal to finish the converge and exists. + def run_with_graceful_exit_option + # Override the TERM signal. + trap('TERM') do + Chef::Log.debug("SIGTERM received during converge," + + " finishing converge to exit normally (send SIGINT to terminate immediately)") + end + + @chef_client.run + true + end + + def fork_chef_client + Chef::Log.info "Forking chef instance to converge..." + pid = fork do + # Want to allow forked processes to finish converging when + # TERM singal is received (exit gracefully) + trap('TERM') do + Chef::Log.debug("SIGTERM received during converge," + + " finishing converge to exit normally (send SIGINT to terminate immediately)") + end + + client_solo = Chef::Config[:solo] ? "chef-solo" : "chef-client" + $0 = "#{client_solo} worker: ppid=#{Process.ppid};start=#{Time.new.strftime("%R:%S")};" + begin + Chef::Log.debug "Forked instance now converging" + @chef_client.run + rescue Exception => e + Chef::Log.error(e.to_s) + exit 1 + else + exit 0 + end + end + Chef::Log.debug "Fork successful. Waiting for new chef pid: #{pid}" + result = Process.waitpid2(pid) + handle_child_exit(result) + Chef::Log.debug "Forked instance successfully reaped (pid: #{pid})" + true + end + + def handle_child_exit(pid_and_status) + status = pid_and_status[1] + return true if status.success? + message = if status.signaled? + "Chef run process terminated by signal #{status.termsig} (#{Signal.list.invert[status.termsig]})" + else + "Chef run process exited unsuccessfully (exit code #{status.exitstatus})" + end + raise Exceptions::ChildConvergeError, message + end def apply_config(config_content, config_file_path) Chef::Config.from_string(config_content, config_file_path) diff --git a/lib/chef/application/apply.rb b/lib/chef/application/apply.rb index ea9154c6f2..22d835e876 100644 --- a/lib/chef/application/apply.rb +++ b/lib/chef/application/apply.rb @@ -31,7 +31,6 @@ class Chef::Application::Apply < Chef::Application banner "Usage: chef-apply [RECIPE_FILE] [-e RECIPE_TEXT] [-s]" - option :execute, :short => "-e RECIPE_TEXT", :long => "--execute RECIPE_TEXT", @@ -92,14 +91,17 @@ class Chef::Application::Apply < Chef::Application end def read_recipe_file(file_name) - recipe_path = file_name - unless File.exist?(recipe_path) - Chef::Application.fatal!("No file exists at #{recipe_path}", 1) + if file_name.nil? + Chef::Application.fatal!("No recipe file was provided", 1) + else + recipe_path = File.expand_path(file_name) + unless File.exist?(recipe_path) + Chef::Application.fatal!("No file exists at #{recipe_path}", 1) + end + recipe_fh = open(recipe_path) + recipe_text = recipe_fh.read + [recipe_text, recipe_fh] end - recipe_path = File.expand_path(recipe_path) - recipe_fh = open(recipe_path) - recipe_text = recipe_fh.read - [recipe_text, recipe_fh] end def get_recipe_and_run_context diff --git a/lib/chef/application/client.rb b/lib/chef/application/client.rb index 6c06ad656d..5463f504bc 100644 --- a/lib/chef/application/client.rb +++ b/lib/chef/application/client.rb @@ -238,8 +238,12 @@ class Chef::Application::Client < Chef::Application :boolean => true end + option :audit_mode, + :long => "--[no-]audit-mode", + :description => "If not specified, run converge and audit phase. If true, run only audit phase. If false, run only converge phase.", + :boolean => true + IMMEDIATE_RUN_SIGNAL = "1".freeze - GRACEFUL_EXIT_SIGNAL = "2".freeze attr_reader :chef_client_json @@ -268,6 +272,8 @@ class Chef::Application::Client < Chef::Application Chef::Config[:splay] = nil end + Chef::Application.fatal!(unforked_interval_error_message) if !Chef::Config[:client_fork] && Chef::Config[:interval] + if Chef::Config[:json_attribs] config_fetcher = Chef::ConfigFetcher.new(Chef::Config[:json_attribs]) @chef_client_json = config_fetcher.fetch_json @@ -295,8 +301,9 @@ class Chef::Application::Client < Chef::Application Chef::Daemon.change_privilege end - # Run the chef client, optionally daemonizing or looping at intervals. - def run_application + def setup_signal_handlers + super + unless Chef::Platform.windows? SELF_PIPE.replace IO.pipe @@ -304,52 +311,54 @@ class Chef::Application::Client < Chef::Application Chef::Log.info("SIGUSR1 received, waking up") SELF_PIPE[1].putc(IMMEDIATE_RUN_SIGNAL) # wakeup master process from select end - - # see CHEF-5172 - if Chef::Config[:daemonize] || Chef::Config[:interval] - trap("TERM") do - Chef::Log.info("SIGTERM received, exiting gracefully") - SELF_PIPE[1].putc(GRACEFUL_EXIT_SIGNAL) - end - end end + end + # Run the chef client, optionally daemonizing or looping at intervals. + def run_application if Chef::Config[:version] puts "Chef version: #{::Chef::VERSION}" end + if !Chef::Config[:client_fork] || Chef::Config[:once] + begin + # run immediately without interval sleep, or splay + run_chef_client(Chef::Config[:specific_recipes]) + rescue SystemExit + raise + rescue Exception => e + Chef::Application.fatal!("#{e.class}: #{e.message}", 1) + end + else + interval_run_chef_client + end + end + + private + def interval_run_chef_client if Chef::Config[:daemonize] Chef::Daemon.daemonize("chef-client") end - signal = nil - loop do begin - Chef::Application.exit!("Exiting", 0) if signal == GRACEFUL_EXIT_SIGNAL - - if Chef::Config[:splay] and signal != IMMEDIATE_RUN_SIGNAL - splay = rand Chef::Config[:splay] - Chef::Log.debug("Splay sleep #{splay} seconds") - sleep splay + @signal = test_signal + if @signal != IMMEDIATE_RUN_SIGNAL + sleep_sec = time_to_sleep + Chef::Log.debug("Sleeping for #{sleep_sec} seconds") + interval_sleep(sleep_sec) end - signal = nil + @signal = nil run_chef_client(Chef::Config[:specific_recipes]) - if Chef::Config[:interval] - Chef::Log.debug("Sleeping for #{Chef::Config[:interval]} seconds") - signal = interval_sleep - else - Chef::Application.exit! "Exiting", 0 - end + Chef::Application.exit!("Exiting", 0) if !Chef::Config[:interval] rescue SystemExit => e raise rescue Exception => e if Chef::Config[:interval] Chef::Log.error("#{e.class}: #{e}") - Chef::Log.error("Sleeping for #{Chef::Config[:interval]} seconds before trying again") - signal = interval_sleep + Chef::Log.debug("#{e.class}: #{e}\n#{e.backtrace.join("\n")}") retry else Chef::Application.fatal!("#{e.class}: #{e.message}", 1) @@ -358,19 +367,35 @@ class Chef::Application::Client < Chef::Application end end - private + def test_signal + @signal = interval_sleep(0) + end - def interval_sleep + def time_to_sleep + duration = 0 + duration += rand(Chef::Config[:splay]) if Chef::Config[:splay] + duration += Chef::Config[:interval] if Chef::Config[:interval] + duration + end + + def interval_sleep(sec) unless SELF_PIPE.empty? - client_sleep Chef::Config[:interval] + client_sleep(sec) else # Windows - sleep Chef::Config[:interval] + sleep(sec) end end def client_sleep(sec) IO.select([ SELF_PIPE[0] ], nil, nil, sec) or return - SELF_PIPE[0].getc.chr + @signal = SELF_PIPE[0].getc.chr + end + + def unforked_interval_error_message + "Unforked chef-client interval runs are disabled in Chef 12." + + "\nConfiguration settings:" + + "#{"\n interval = #{Chef::Config[:interval]} seconds" if Chef::Config[:interval]}" + + "\nEnable chef-client interval runs by setting `:client_fork = true` in your config file or adding `--fork` to your command line options." end end diff --git a/lib/chef/application/solo.rb b/lib/chef/application/solo.rb index f0e578d5ef..474bbf3f6c 100644 --- a/lib/chef/application/solo.rb +++ b/lib/chef/application/solo.rb @@ -185,6 +185,8 @@ class Chef::Application::Solo < Chef::Application Chef::Config[:interval] ||= 1800 end + Chef::Application.fatal!(unforked_interval_error_message) if !Chef::Config[:client_fork] && Chef::Config[:interval] + if Chef::Config[:recipe_url] cookbooks_path = Array(Chef::Config[:cookbook_path]).detect{|e| e =~ /\/cookbooks\/*$/ } recipes_path = File.expand_path(File.join(cookbooks_path, '..')) @@ -209,23 +211,39 @@ class Chef::Application::Solo < Chef::Application end def run_application + if !Chef::Config[:client_fork] || Chef::Config[:once] + # Run immediately without interval sleep or splay + begin + run_chef_client(Chef::Config[:specific_recipes]) + rescue SystemExit + raise + rescue Exception => e + Chef::Application.fatal!("#{e.class}: #{e.message}", 1) + end + else + interval_run_chef_client + end + end + + private + def interval_run_chef_client if Chef::Config[:daemonize] Chef::Daemon.daemonize("chef-client") end loop do begin - if Chef::Config[:splay] - splay = rand Chef::Config[:splay] - Chef::Log.debug("Splay sleep #{splay} seconds") - sleep splay + + sleep_sec = 0 + sleep_sec += rand(Chef::Config[:splay]) if Chef::Config[:splay] + sleep_sec += Chef::Config[:interval] if Chef::Config[:interval] + if sleep_sec != 0 + Chef::Log.debug("Sleeping for #{sleep_sec} seconds") + sleep(sleep_sec) end run_chef_client - if Chef::Config[:interval] - Chef::Log.debug("Sleeping for #{Chef::Config[:interval]} seconds") - sleep Chef::Config[:interval] - else + if !Chef::Config[:interval] Chef::Application.exit! "Exiting", 0 end rescue SystemExit => e @@ -234,8 +252,6 @@ class Chef::Application::Solo < Chef::Application if Chef::Config[:interval] Chef::Log.error("#{e.class}: #{e}") Chef::Log.debug("#{e.class}: #{e}\n#{e.backtrace.join("\n")}") - Chef::Log.fatal("Sleeping for #{Chef::Config[:interval]} seconds before trying again") - sleep Chef::Config[:interval] retry else Chef::Application.fatal!("#{e.class}: #{e.message}", 1) @@ -244,8 +260,6 @@ class Chef::Application::Solo < Chef::Application end end - private - def fetch_recipe_tarball(url, path) Chef::Log.debug("Download recipes tarball from #{url} to #{path}") File.open(path, 'wb') do |f| @@ -254,4 +268,11 @@ class Chef::Application::Solo < Chef::Application end end end + + def unforked_interval_error_message + "Unforked chef-client interval runs are disabled in Chef 12." + + "\nConfiguration settings:" + + "#{"\n interval = #{Chef::Config[:interval]} seconds" if Chef::Config[:interval]}" + + "\nEnable chef-client interval runs by setting `:client_fork = true` in your config file or adding `--fork` to your command line options." + end end diff --git a/lib/chef/chef_fs/chef_fs_data_store.rb b/lib/chef/chef_fs/chef_fs_data_store.rb index 484ab07390..3813d0edb4 100644 --- a/lib/chef/chef_fs/chef_fs_data_store.rb +++ b/lib/chef/chef_fs/chef_fs_data_store.rb @@ -168,7 +168,7 @@ class Chef end end end - JSON.pretty_generate(result) + Chef::JSONCompat.to_json_pretty(result) else begin diff --git a/lib/chef/chef_fs/command_line.rb b/lib/chef/chef_fs/command_line.rb index 43e8b276e0..8a205eef78 100644 --- a/lib/chef/chef_fs/command_line.rb +++ b/lib/chef/chef_fs/command_line.rb @@ -253,7 +253,7 @@ class Chef def self.canonicalize_json(json_text) parsed_json = Chef::JSONCompat.parse(json_text) sorted_json = sort_keys(parsed_json) - JSON.pretty_generate(sorted_json) + Chef::JSONCompat.to_json_pretty(sorted_json) end def self.diff_text(old_path, new_path, old_value, new_value) diff --git a/lib/chef/chef_fs/file_system/organization_invites_entry.rb b/lib/chef/chef_fs/file_system/organization_invites_entry.rb index cb26326050..5df37085cb 100644 --- a/lib/chef/chef_fs/file_system/organization_invites_entry.rb +++ b/lib/chef/chef_fs/file_system/organization_invites_entry.rb @@ -1,5 +1,6 @@ require 'chef/chef_fs/file_system/rest_list_entry' require 'chef/chef_fs/data_handler/organization_invites_data_handler' +require 'chef/json_compat' class Chef module ChefFS @@ -34,7 +35,7 @@ class Chef end def write(contents) - desired_invites = minimize_value(JSON.parse(contents, :create_additions => false)) + desired_invites = minimize_value(Chef::JSONCompat.parse(contents, :create_additions => false)) actual_invites = _read_json.inject({}) { |h,val| h[val['username']] = val['id']; h } invites = actual_invites.keys (desired_invites - invites).each do |invite| diff --git a/lib/chef/chef_fs/file_system/organization_members_entry.rb b/lib/chef/chef_fs/file_system/organization_members_entry.rb index eb524d5ea2..94393b341f 100644 --- a/lib/chef/chef_fs/file_system/organization_members_entry.rb +++ b/lib/chef/chef_fs/file_system/organization_members_entry.rb @@ -1,5 +1,6 @@ require 'chef/chef_fs/file_system/rest_list_entry' require 'chef/chef_fs/data_handler/organization_members_data_handler' +require 'chef/json_compat' class Chef module ChefFS @@ -34,7 +35,7 @@ class Chef end def write(contents) - desired_members = minimize_value(JSON.parse(contents, :create_additions => false)) + desired_members = minimize_value(Chef::JSONCompat.parse(contents, :create_additions => false)) members = minimize_value(_read_json) (desired_members - members).each do |member| begin diff --git a/lib/chef/chef_fs/file_system/rest_list_entry.rb b/lib/chef/chef_fs/file_system/rest_list_entry.rb index ac47ff4f25..f68794cb0d 100644 --- a/lib/chef/chef_fs/file_system/rest_list_entry.rb +++ b/lib/chef/chef_fs/file_system/rest_list_entry.rb @@ -21,6 +21,7 @@ require 'chef/chef_fs/file_system/not_found_error' require 'chef/chef_fs/file_system/operation_failed_error' require 'chef/role' require 'chef/node' +require 'chef/json_compat' class Chef module ChefFS @@ -172,7 +173,7 @@ class Chef def api_error_text(response) begin - JSON.parse(response.body)['error'].join("\n") + Chef::JSONCompat.parse(response.body)['error'].join("\n") rescue response.body end diff --git a/lib/chef/client.rb b/lib/chef/client.rb index 161ecddb0f..e8ff352116 100644 --- a/lib/chef/client.rb +++ b/lib/chef/client.rb @@ -191,54 +191,6 @@ class Chef end end - # Do a full run for this Chef::Client. Calls: - # * do_run - # - # This provides a wrapper around #do_run allowing the - # run to be optionally forked. - # === Returns - # boolean:: Return value from #do_run. Should always returns true. - def run - # win32-process gem exposes some form of :fork for Process - # class. So we are seperately ensuring that the platform we're - # running on is not windows before forking. - if(Chef::Config[:client_fork] && Process.respond_to?(:fork) && !Chef::Platform.windows?) - Chef::Log.info "Forking chef instance to converge..." - pid = fork do - [:INT, :TERM].each {|s| trap(s, "EXIT") } - client_solo = Chef::Config[:solo] ? "chef-solo" : "chef-client" - $0 = "#{client_solo} worker: ppid=#{Process.ppid};start=#{Time.new.strftime("%R:%S")};" - begin - Chef::Log.debug "Forked instance now converging" - do_run - rescue Exception => e - Chef::Log.error(e.to_s) - exit 1 - else - exit 0 - end - end - Chef::Log.debug "Fork successful. Waiting for new chef pid: #{pid}" - result = Process.waitpid2(pid) - handle_child_exit(result) - Chef::Log.debug "Forked instance successfully reaped (pid: #{pid})" - true - else - do_run - end - end - - def handle_child_exit(pid_and_status) - status = pid_and_status[1] - return true if status.success? - message = if status.signaled? - "Chef run process terminated by signal #{status.termsig} (#{Signal.list.invert[status.termsig]})" - else - "Chef run process exited unsuccessfully (exit code #{status.exitstatus})" - end - raise Exceptions::ChildConvergeError, message - end - # Instantiates a Chef::Node object, possibly loading the node's prior state # when using chef-client. Delegates to policy_builder # @@ -330,7 +282,7 @@ class Chef rescue Exception => e # TODO: munge exception so a semantic failure message can be given to the # user - @events.registration_failed(node_name, e, config) + @events.registration_failed(client_name, e, config) raise end @@ -383,8 +335,6 @@ class Chef end end - private - # Do a full run for this Chef::Client. Calls: # # * run_ohai - Collect information about the system @@ -395,7 +345,7 @@ class Chef # # === Returns # true:: Always returns true. - def do_run + def run runlock = RunLock.new(Chef::Config.lockfile) runlock.acquire # don't add code that may fail before entering this section to be sure to release lock @@ -428,7 +378,7 @@ class Chef run_context = setup_run_context - catch (:end_client_run_early) do + catch(:end_client_run_early) do converge(run_context) end @@ -466,6 +416,8 @@ class Chef true end + private + def empty_directory?(path) !File.exists?(path) || (Dir.entries(path).size <= 2) end diff --git a/lib/chef/config.rb b/lib/chef/config.rb index 107b50ee85..9a8117d2c2 100644 --- a/lib/chef/config.rb +++ b/lib/chef/config.rb @@ -25,11 +25,13 @@ require 'mixlib/config' require 'chef/util/selinux' require 'chef/util/path_helper' require 'pathname' +require 'chef/mixin/shell_out' class Chef class Config extend Mixlib::Config + extend Chef::Mixin::ShellOut PathHelper = Chef::Util::PathHelper @@ -604,25 +606,37 @@ class Chef # available English UTF-8 locale. However, all modern POSIXen should support 'locale -a'. default :internal_locale do begin - locales = `locale -a`.split + # https://github.com/opscode/chef/issues/2181 + # Some systems have the `locale -a` command, but the result has + # invalid characters for the default encoding. + # + # For example, on CentOS 6 with ENV['LANG'] = "en_US.UTF-8", + # `locale -a`.split fails with ArgumentError invalid UTF-8 encoding. + locales = shell_out_with_systems_locale("locale -a").stdout.split case when locales.include?('C.UTF-8') 'C.UTF-8' - when locales.include?('en_US.UTF-8') + when locales.include?('en_US.UTF-8'), locales.include?('en_US.utf8') 'en_US.UTF-8' when locales.include?('en.UTF-8') 'en.UTF-8' - when guesses = locales.select { |l| l =~ /^en_.*UTF-8$'/ } - guesses.first else - Chef::Log.warn "Please install an English UTF-8 locale for Chef to use, falling back to C locale and disabling UTF-8 support." - 'C' + # Will match en_ZZ.UTF-8, en_ZZ.utf-8, en_ZZ.UTF8, en_ZZ.utf8 + guesses = locales.select { |l| l =~ /^en_.*UTF-?8$/i } + unless guesses.empty? + guessed_locale = guesses.first + # Transform into the form en_ZZ.UTF-8 + guessed_locale.gsub(/UTF-?8$/i, "UTF-8") + else + Chef::Log.warn "Please install an English UTF-8 locale for Chef to use, falling back to C locale and disabling UTF-8 support." + 'C' + end end rescue if Chef::Platform.windows? Chef::Log.debug "Defaulting to locale en_US.UTF-8 on Windows, until it matters that we do something else." else - Chef::Log.warn "No usable locale -a command found, assuming you have en_US.UTF-8 installed." + Chef::Log.debug "No usable locale -a command found, assuming you have en_US.UTF-8 installed." end 'en_US.UTF-8' end diff --git a/lib/chef/cookbook/cookbook_version_loader.rb b/lib/chef/cookbook/cookbook_version_loader.rb index 5c9de6b8ca..bcbfcbeec8 100644 --- a/lib/chef/cookbook/cookbook_version_loader.rb +++ b/lib/chef/cookbook/cookbook_version_loader.rb @@ -81,7 +81,7 @@ class Chef load_as(:attribute_filenames, 'attributes', '*.rb') load_as(:definition_filenames, 'definitions', '*.rb') load_as(:recipe_filenames, 'recipes', '*.rb') - load_as(:library_filenames, 'libraries', '*.rb') + load_recursively_as(:library_filenames, 'libraries', '*.rb') load_recursively_as(:template_filenames, "templates", "*") load_recursively_as(:file_filenames, "files", "*") load_recursively_as(:resource_filenames, "resources", "*.rb") @@ -214,23 +214,29 @@ class Chef def load_root_files Dir.glob(File.join(Chef::Util::PathHelper.escape_glob(cookbook_path), '*'), File::FNM_DOTMATCH).each do |file| + file = Chef::Util::PathHelper.cleanpath(file) next if File.directory?(file) next if File.basename(file) == UPLOADED_COOKBOOK_VERSION_FILE - cookbook_settings[:root_filenames][file[@relative_path, 1]] = file + name = Chef::Util::PathHelper.relative_path_from(@cookbook_path, file) + cookbook_settings[:root_filenames][name] = file end end def load_recursively_as(category, category_dir, glob) file_spec = File.join(Chef::Util::PathHelper.escape_glob(cookbook_path, category_dir), '**', glob) Dir.glob(file_spec, File::FNM_DOTMATCH).each do |file| + file = Chef::Util::PathHelper.cleanpath(file) next if File.directory?(file) - cookbook_settings[category][file[@relative_path, 1]] = file + name = Chef::Util::PathHelper.relative_path_from(@cookbook_path, file) + cookbook_settings[category][name] = file end end def load_as(category, *path_glob) Dir[File.join(Chef::Util::PathHelper.escape_glob(cookbook_path), *path_glob)].each do |file| - cookbook_settings[category][file[@relative_path, 1]] = file + file = Chef::Util::PathHelper.cleanpath(file) + name = Chef::Util::PathHelper.relative_path_from(@cookbook_path, file) + cookbook_settings[category][name] = file end end diff --git a/lib/chef/cookbook/metadata.rb b/lib/chef/cookbook/metadata.rb index 3964354d50..54e930135d 100644 --- a/lib/chef/cookbook/metadata.rb +++ b/lib/chef/cookbook/metadata.rb @@ -35,28 +35,31 @@ class Chef # about Chef Cookbooks. class Metadata - NAME = 'name'.freeze - DESCRIPTION = 'description'.freeze - LONG_DESCRIPTION = 'long_description'.freeze - MAINTAINER = 'maintainer'.freeze - MAINTAINER_EMAIL = 'maintainer_email'.freeze - LICENSE = 'license'.freeze - PLATFORMS = 'platforms'.freeze - DEPENDENCIES = 'dependencies'.freeze - RECOMMENDATIONS = 'recommendations'.freeze - SUGGESTIONS = 'suggestions'.freeze - CONFLICTING = 'conflicting'.freeze - PROVIDING = 'providing'.freeze - REPLACING = 'replacing'.freeze - ATTRIBUTES = 'attributes'.freeze - GROUPINGS = 'groupings'.freeze - RECIPES = 'recipes'.freeze - VERSION = 'version'.freeze + NAME = 'name'.freeze + DESCRIPTION = 'description'.freeze + LONG_DESCRIPTION = 'long_description'.freeze + MAINTAINER = 'maintainer'.freeze + MAINTAINER_EMAIL = 'maintainer_email'.freeze + LICENSE = 'license'.freeze + PLATFORMS = 'platforms'.freeze + DEPENDENCIES = 'dependencies'.freeze + RECOMMENDATIONS = 'recommendations'.freeze + SUGGESTIONS = 'suggestions'.freeze + CONFLICTING = 'conflicting'.freeze + PROVIDING = 'providing'.freeze + REPLACING = 'replacing'.freeze + ATTRIBUTES = 'attributes'.freeze + GROUPINGS = 'groupings'.freeze + RECIPES = 'recipes'.freeze + VERSION = 'version'.freeze + SOURCE_URL = 'source_url'.freeze + ISSUES_URL = 'issues_url'.freeze COMPARISON_FIELDS = [ :name, :description, :long_description, :maintainer, :maintainer_email, :license, :platforms, :dependencies, :recommendations, :suggestions, :conflicting, :providing, - :replacing, :attributes, :groupings, :recipes, :version] + :replacing, :attributes, :groupings, :recipes, :version, + :source_url, :issues_url ] VERSION_CONSTRAINTS = {:depends => DEPENDENCIES, :recommends => RECOMMENDATIONS, @@ -111,6 +114,8 @@ class Chef @groupings = Mash.new @recipes = Mash.new @version = Version.new("0.0.0") + @source_url = '' + @issues_url = '' @errors = [] end @@ -413,7 +418,7 @@ class Chef end end - # Adds an attribute )hat a user needs to configure for this cookbook. Takes + # Adds an attribute that a user needs to configure for this cookbook. Takes # a name (with the / notation for a nested attribute), followed by any of # these options # @@ -443,7 +448,9 @@ class Chef :type => { :equal_to => [ "string", "array", "hash", "symbol", "boolean", "numeric" ], :default => "string" }, :required => { :equal_to => [ "required", "recommended", "optional", true, false ], :default => "optional" }, :recipes => { :kind_of => [ Array ], :default => [] }, - :default => { :kind_of => [ String, Array, Hash, Symbol, Numeric, TrueClass, FalseClass ] } + :default => { :kind_of => [ String, Array, Hash, Symbol, Numeric, TrueClass, FalseClass ] }, + :source_url => { :kind_of => String }, + :issues_url => { :kind_of => String } } ) options[:required] = remap_required_attribute(options[:required]) unless options[:required].nil? @@ -469,23 +476,25 @@ class Chef def to_hash { - NAME => self.name, - DESCRIPTION => self.description, - LONG_DESCRIPTION => self.long_description, - MAINTAINER => self.maintainer, - MAINTAINER_EMAIL => self.maintainer_email, - LICENSE => self.license, - PLATFORMS => self.platforms, - DEPENDENCIES => self.dependencies, - RECOMMENDATIONS => self.recommendations, - SUGGESTIONS => self.suggestions, - CONFLICTING => self.conflicting, - PROVIDING => self.providing, - REPLACING => self.replacing, - ATTRIBUTES => self.attributes, - GROUPINGS => self.groupings, - RECIPES => self.recipes, - VERSION => self.version + NAME => self.name, + DESCRIPTION => self.description, + LONG_DESCRIPTION => self.long_description, + MAINTAINER => self.maintainer, + MAINTAINER_EMAIL => self.maintainer_email, + LICENSE => self.license, + PLATFORMS => self.platforms, + DEPENDENCIES => self.dependencies, + RECOMMENDATIONS => self.recommendations, + SUGGESTIONS => self.suggestions, + CONFLICTING => self.conflicting, + PROVIDING => self.providing, + REPLACING => self.replacing, + ATTRIBUTES => self.attributes, + GROUPINGS => self.groupings, + RECIPES => self.recipes, + VERSION => self.version, + SOURCE_URL => self.source_url, + ISSUES_URL => self.issues_url } end @@ -517,6 +526,8 @@ class Chef @groupings = o[GROUPINGS] if o.has_key?(GROUPINGS) @recipes = o[RECIPES] if o.has_key?(RECIPES) @version = o[VERSION] if o.has_key?(VERSION) + @source_url = o[SOURCE_URL] if o.has_key?(SOURCE_URL) + @issues_url = o[ISSUES_URL] if o.has_key?(ISSUES_URL) self end @@ -531,7 +542,9 @@ class Chef VERSION_CONSTRAINTS.each do |dependency_type, hash_key| if dependency_group = o[hash_key] dependency_group.each do |cb_name, constraints| - metadata.send(method_name, cb_name, *Array(constraints)) + if metadata.respond_to?(method_name) + metadata.public_send(method_name, cb_name, *Array(constraints)) + end end end end @@ -543,6 +556,36 @@ class Chef from_hash(o) end + # Sets the cookbook's source URL, or returns it. + # + # === Parameters + # maintainer<String>:: The source URL + # + # === Returns + # source_url<String>:: Returns the current source URL. + def source_url(arg=nil) + set_or_return( + :source_url, + arg, + :kind_of => [ String ] + ) + end + + # Sets the cookbook's issues URL, or returns it. + # + # === Parameters + # issues_url<String>:: The issues URL + # + # === Returns + # issues_url<String>:: Returns the current issues URL. + def issues_url(arg=nil) + set_or_return( + :issues_url, + arg, + :kind_of => [ String ] + ) + end + private def run_validation diff --git a/lib/chef/cookbook/syntax_check.rb b/lib/chef/cookbook/syntax_check.rb index 69c194516b..96fc7e7b84 100644 --- a/lib/chef/cookbook/syntax_check.rb +++ b/lib/chef/cookbook/syntax_check.rb @@ -101,17 +101,24 @@ class Chef end def remove_ignored_files(file_list) - return file_list unless chefignore.ignores.length > 0 + return file_list if chefignore.ignores.empty? + file_list.reject do |full_path| - cookbook_pn = Pathname.new cookbook_path - full_pn = Pathname.new full_path - relative_pn = full_pn.relative_path_from cookbook_pn - chefignore.ignored? relative_pn.to_s + relative_pn = Chef::Util::PathHelper.relative_path_from(cookbook_path, full_path) + chefignore.ignored?(relative_pn.to_s) end end + def remove_uninteresting_ruby_files(file_list) + file_list.reject { |f| f =~ %r{#{cookbook_path}/(files|templates)/} } + end + def ruby_files - remove_ignored_files Dir[File.join(Chef::Util::PathHelper.escape_glob(cookbook_path), '**', '*.rb')] + path = Chef::Util::PathHelper.escape_glob(cookbook_path) + files = Dir[File.join(path, '**', '*.rb')] + files = remove_ignored_files(files) + files = remove_uninteresting_ruby_files(files) + files end def untested_ruby_files diff --git a/lib/chef/cookbook_version.rb b/lib/chef/cookbook_version.rb index 76e6d152b2..505b403e65 100644 --- a/lib/chef/cookbook_version.rb +++ b/lib/chef/cookbook_version.rb @@ -315,13 +315,20 @@ class Chef else if segment == :files || segment == :templates error_message = "Cookbook '#{name}' (#{version}) does not contain a file at any of these locations:\n" - error_locations = [ - " #{segment}/#{node[:platform]}-#{node[:platform_version]}/#{filename}", - " #{segment}/#{node[:platform]}/#{filename}", - " #{segment}/default/#{filename}", - ] + error_locations = if filename.is_a?(Array) + filename.map{|name| " #{File.join(segment.to_s, name)}"} + else + [ + " #{segment}/#{node[:platform]}-#{node[:platform_version]}/#{filename}", + " #{segment}/#{node[:platform]}/#{filename}", + " #{segment}/default/#{filename}", + " #{segment}/#{filename}", + ] + end error_message << error_locations.join("\n") existing_files = segment_filenames(segment) + # Strip the root_dir prefix off all files for readability + existing_files.map!{|path| path[root_dir.length+1..-1]} if root_dir # Show the files that the cookbook does have. If the user made a typo, # hopefully they'll see it here. unless existing_files.empty? @@ -421,38 +428,44 @@ class Chef def preferences_for_path(node, segment, path) # only files and templates can be platform-specific if segment.to_sym == :files || segment.to_sym == :templates - begin - platform, version = Chef::Platform.find_platform_and_version(node) - rescue ArgumentError => e - # Skip platform/version if they were not found by find_platform_and_version - if e.message =~ /Cannot find a (?:platform|version)/ - platform = "/unknown_platform/" - version = "/unknown_platform_version/" - else - raise + relative_search_path = if path.is_a?(Array) + path + else + begin + platform, version = Chef::Platform.find_platform_and_version(node) + rescue ArgumentError => e + # Skip platform/version if they were not found by find_platform_and_version + if e.message =~ /Cannot find a (?:platform|version)/ + platform = "/unknown_platform/" + version = "/unknown_platform_version/" + else + raise + end end - end - fqdn = node[:fqdn] + fqdn = node[:fqdn] - # Break version into components, eg: "5.7.1" => [ "5.7.1", "5.7", "5" ] - search_versions = [] - parts = version.to_s.split('.') + # Break version into components, eg: "5.7.1" => [ "5.7.1", "5.7", "5" ] + search_versions = [] + parts = version.to_s.split('.') - parts.size.times do - search_versions << parts.join('.') - parts.pop - end + parts.size.times do + search_versions << parts.join('.') + parts.pop + end - # Most specific to least specific places to find the path - search_path = [ File.join(segment.to_s, "host-#{fqdn}", path) ] - search_versions.each do |v| - search_path << File.join(segment.to_s, "#{platform}-#{v}", path) - end - search_path << File.join(segment.to_s, platform.to_s, path) - search_path << File.join(segment.to_s, "default", path) + # Most specific to least specific places to find the path + search_path = [ File.join("host-#{fqdn}", path) ] + search_versions.each do |v| + search_path << File.join("#{platform}-#{v}", path) + end + search_path << File.join(platform.to_s, path) + search_path << File.join("default", path) + search_path << path - search_path + search_path + end + relative_search_path.map {|relative_path| File.join(segment.to_s, relative_path)} else [File.join(segment, path)] end @@ -479,7 +492,7 @@ class Chef cookbook_version.manifest = o # We don't need the following step when we decide to stop supporting deprecated operators in the metadata (e.g. <<, >>) - cookbook_version.manifest["metadata"] = Chef::JSONCompat.from_json(cookbook_version.metadata.to_json) + cookbook_version.manifest["metadata"] = Chef::JSONCompat.from_json(Chef::JSONCompat.to_json(cookbook_version.metadata)) cookbook_version.freeze_version if o["frozen?"] cookbook_version @@ -552,6 +565,11 @@ class Chef chef_server_rest.get_rest('cookbooks') end + # Alias latest_cookbooks as list + class << self + alias :latest_cookbooks :list + end + def self.list_all_versions chef_server_rest.get_rest('cookbooks?num_versions=all') end @@ -575,11 +593,6 @@ class Chef end end - # Get the newest version of all cookbooks - def self.latest_cookbooks - chef_server_rest.get_rest('cookbooks/_latest') - end - def <=>(o) raise Chef::Exceptions::CookbookVersionNameMismatch if self.name != o.name # FIXME: can we change the interface to the Metadata class such @@ -660,13 +673,19 @@ class Chef def parse_segment_file_from_root_paths(segment, segment_file) root_paths.each do |root_path| - pathname = Pathname.new(segment_file).relative_path_from(Pathname.new(root_path)) + pathname = Chef::Util::PathHelper.relative_path_from(root_path, segment_file) parts = pathname.each_filename.take(2) # Check if path is actually under root_path next if parts[0] == '..' if segment == :templates || segment == :files - return [ pathname.to_s, parts[1] ] + # Check if pathname looks like files/foo or templates/foo (unscoped) + if pathname.each_filename.to_a.length == 2 + # Use root_default in case the same path exists at root_default and default + return [ pathname.to_s, 'root_default' ] + else + return [ pathname.to_s, parts[1] ] + end else return [ pathname.to_s, 'default' ] end diff --git a/lib/chef/dsl/data_query.rb b/lib/chef/dsl/data_query.rb index 3dafbca6bf..e36784271a 100644 --- a/lib/chef/dsl/data_query.rb +++ b/lib/chef/dsl/data_query.rb @@ -20,6 +20,7 @@ require 'chef/search/query' require 'chef/data_bag' require 'chef/data_bag_item' require 'chef/encrypted_data_bag_item' +require 'chef/encrypted_data_bag_item/check_encrypted' class Chef module DSL @@ -28,6 +29,7 @@ class Chef # Provides DSL for querying data from the chef-server via search or data # bag. module DataQuery + include Chef::EncryptedDataBagItem::CheckEncrypted def search(*args, &block) # If you pass a block, or have at least the start argument, do raw result parsing @@ -78,35 +80,6 @@ class Chef raise end - private - - # Tries to autodetect if the item's raw hash appears to be encrypted. - def encrypted?(raw_data) - data = raw_data.reject { |k, _| k == "id" } # Remove the "id" key. - # Assume hashes containing only the "id" key are not encrypted. - # Otherwise, remove the keys that don't appear to be encrypted and compare - # the result with the hash. If some entry has been removed, then some entry - # doesn't appear to be encrypted and we assume the entire hash is not encrypted. - data.empty? ? false : data.reject { |_, v| !looks_like_encrypted?(v) } == data - end - - # Checks if data looks like it has been encrypted by - # Chef::EncryptedDataBagItem::Encryptor::VersionXEncryptor. Returns - # true only when there is an exact match between the VersionXEncryptor - # keys and the hash's keys. - def looks_like_encrypted?(data) - return false unless data.is_a?(Hash) && data.has_key?("version") - case data["version"] - when 1 - Chef::EncryptedDataBagItem::Encryptor::Version1Encryptor.encryptor_keys.sort == data.keys.sort - when 2 - Chef::EncryptedDataBagItem::Encryptor::Version2Encryptor.encryptor_keys.sort == data.keys.sort - when 3 - Chef::EncryptedDataBagItem::Encryptor::Version3Encryptor.encryptor_keys.sort == data.keys.sort - else - false # version means something else... assume not encrypted. - end - end end end end diff --git a/lib/chef/dsl/recipe.rb b/lib/chef/dsl/recipe.rb index 3282320b8c..94b0d2d18b 100644 --- a/lib/chef/dsl/recipe.rb +++ b/lib/chef/dsl/recipe.rb @@ -19,6 +19,7 @@ require 'chef/resource_platform_map' require 'chef/mixin/convert_to_class_name' +require 'chef/exceptions' class Chef module DSL @@ -52,9 +53,7 @@ class Chef end def has_resource_definition?(name) - yes_or_no = run_context.definitions.has_key?(name) - - yes_or_no + run_context.definitions.has_key?(name) end # Processes the arguments and block as a resource definition. @@ -68,12 +67,10 @@ class Chef # This sets up the parameter overrides new_def.instance_eval(&block) if block - new_recipe = Chef::Recipe.new(cookbook_name, recipe_name, run_context) new_recipe.params = new_def.params new_recipe.params[:name] = args[0] new_recipe.instance_eval(&new_def.recipe) - new_recipe end # Instantiates a resource (via #build_resource), then adds it to the @@ -85,21 +82,7 @@ class Chef resource = build_resource(type, name, created_at, &resource_attrs_block) - # Some resources (freebsd_package) can be invoked with multiple names - # (package || freebsd_package). - # https://github.com/opscode/chef/issues/1773 - # For these resources we want to make sure - # their key in resource collection is same as the name they are declared - # as. Since this might be a breaking change for resources that define - # customer to_s methods, we are working around the issue by letting - # resources know of their created_as_type until this issue is fixed in - # Chef 12: - # https://github.com/opscode/chef/issues/1817 - if resource.respond_to?(:created_as_type=) - resource.created_as_type = type - end - - run_context.resource_collection.insert(resource) + run_context.resource_collection.insert(resource, resource_type:type, instance_name:name) resource end @@ -123,7 +106,7 @@ class Chef # This behavior is very counter-intuitive and should be removed. # See CHEF-3694, https://tickets.opscode.com/browse/CHEF-3694 # Moved to this location to resolve CHEF-5052, https://tickets.opscode.com/browse/CHEF-5052 - resource.load_prior_resource + resource.load_prior_resource(type, name) resource.cookbook_name = cookbook_name resource.recipe_name = recipe_name # Determine whether this resource is being created in the context of an enclosing Provider @@ -162,6 +145,10 @@ class Chef end end + def exec(args) + raise Chef::Exceptions::ResourceNotFound, "exec was called, but you probably meant to use an execute resource. If not, please call Kernel#exec explicitly. The exec block called was \"#{args}\"" + end + end end end diff --git a/lib/chef/encrypted_data_bag_item/assertions.rb b/lib/chef/encrypted_data_bag_item/assertions.rb index e9acebbd29..0f9416e7b6 100644 --- a/lib/chef/encrypted_data_bag_item/assertions.rb +++ b/lib/chef/encrypted_data_bag_item/assertions.rb @@ -45,7 +45,7 @@ class Chef::EncryptedDataBagItem def assert_aead_requirements_met!(algorithm) unless OpenSSL::Cipher.method_defined?(:auth_data=) - raise EncryptedDataBagRequirementsFailure, "The used Encrypted Data Bags version requires Ruby >= 1.9" + raise EncryptedDataBagRequirementsFailure, "The used Encrypted Data Bags version requires Ruby >= 2.0" end unless OpenSSL::Cipher.ciphers.include?(algorithm) raise EncryptedDataBagRequirementsFailure, "The used Encrypted Data Bags version requires an OpenSSL version with \"#{algorithm}\" algorithm support" diff --git a/lib/chef/encrypted_data_bag_item/check_encrypted.rb b/lib/chef/encrypted_data_bag_item/check_encrypted.rb new file mode 100644 index 0000000000..b7cb5841b3 --- /dev/null +++ b/lib/chef/encrypted_data_bag_item/check_encrypted.rb @@ -0,0 +1,56 @@ +# +# Author:: Tyler Ball (<tball@getchef.com>) +# Copyright:: Copyright (c) 2010-2014 Opscode, 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/encrypted_data_bag_item/encryptor' + +class Chef::EncryptedDataBagItem + # Common code for checking if a data bag appears encrypted + module CheckEncrypted + + # Tries to autodetect if the item's raw hash appears to be encrypted. + def encrypted?(raw_data) + data = raw_data.reject { |k, _| k == "id" } # Remove the "id" key. + # Assume hashes containing only the "id" key are not encrypted. + # Otherwise, remove the keys that don't appear to be encrypted and compare + # the result with the hash. If some entry has been removed, then some entry + # doesn't appear to be encrypted and we assume the entire hash is not encrypted. + data.empty? ? false : data.reject { |_, v| !looks_like_encrypted?(v) } == data + end + + private + + # Checks if data looks like it has been encrypted by + # Chef::EncryptedDataBagItem::Encryptor::VersionXEncryptor. Returns + # true only when there is an exact match between the VersionXEncryptor + # keys and the hash's keys. + def looks_like_encrypted?(data) + return false unless data.is_a?(Hash) && data.has_key?("version") + case data["version"] + when 1 + Chef::EncryptedDataBagItem::Encryptor::Version1Encryptor.encryptor_keys.sort == data.keys.sort + when 2 + Chef::EncryptedDataBagItem::Encryptor::Version2Encryptor.encryptor_keys.sort == data.keys.sort + when 3 + Chef::EncryptedDataBagItem::Encryptor::Version3Encryptor.encryptor_keys.sort == data.keys.sort + else + false # version means something else... assume not encrypted. + end + end + + end +end diff --git a/lib/chef/encrypted_data_bag_item/encrypted_data_bag_item_assertions.rb b/lib/chef/encrypted_data_bag_item/encrypted_data_bag_item_assertions.rb index 3567925844..9a54396174 100644 --- a/lib/chef/encrypted_data_bag_item/encrypted_data_bag_item_assertions.rb +++ b/lib/chef/encrypted_data_bag_item/encrypted_data_bag_item_assertions.rb @@ -25,7 +25,7 @@ class Chef::EncryptedDataBagItem def assert_requirements_met! unless OpenSSL::Cipher.method_defined?(:auth_data=) - raise EncryptedDataBagRequirementsFailure, "The used Encrypted Data Bags version requires Ruby >= 1.9" + raise EncryptedDataBagRequirementsFailure, "The used Encrypted Data Bags version requires Ruby >= 2.0" end unless OpenSSL::Cipher.ciphers.include?(algorithm) raise EncryptedDataBagRequirementsFailure, "The used Encrypted Data Bags version requires an OpenSSL version with \"#{algorithm}\" algorithm support" diff --git a/lib/chef/guard_interpreter/resource_guard_interpreter.rb b/lib/chef/guard_interpreter/resource_guard_interpreter.rb index 229a8502c7..346b585d8c 100644 --- a/lib/chef/guard_interpreter/resource_guard_interpreter.rb +++ b/lib/chef/guard_interpreter/resource_guard_interpreter.rb @@ -33,10 +33,19 @@ class Chef # to the resource merge_inherited_attributes - # Script resources have a code attribute, which is - # what is used to execute the command, so include - # that with attributes specified by caller in opts - block_attributes = @command_opts.merge({:code => @command}) + # Only execute and script resources and use guard attributes. + # The command to be executed on them are passed via different attributes. + # Script resources use code attribute and execute resources use + # command attribute. Moreover script resources are also execute + # resources. Here we make sure @command is assigned to the right + # attribute by checking the type of the resources. + # We need to make sure we check for Script first because any resource + # that can get to here is an Execute resource. + if @parent_resource.is_a? Chef::Resource::Script + block_attributes = @command_opts.merge({:code => @command}) + else + block_attributes = @command_opts.merge({:command => @command}) + end # Handles cases like powershell_script where default # attributes are different when used in a guard vs. not. For @@ -79,8 +88,8 @@ class Chef raise ArgumentError, "Specified guard_interpreter resource #{parent_resource.guard_interpreter.to_s} unknown for this platform" end - if ! resource_class.ancestors.include?(Chef::Resource::Script) - raise ArgumentError, "Specified guard interpreter class #{resource_class} must be a kind of Chef::Resource::Script resource" + if ! resource_class.ancestors.include?(Chef::Resource::Execute) + raise ArgumentError, "Specified guard interpreter class #{resource_class} must be a kind of Chef::Resource::Execute resource" end empty_events = Chef::EventDispatch::Dispatcher.new diff --git a/lib/chef/http.rb b/lib/chef/http.rb index abc47f636e..ee951bd675 100644 --- a/lib/chef/http.rb +++ b/lib/chef/http.rb @@ -271,7 +271,7 @@ class Chef elsif redirect_location = redirected_to(response) if [:GET, :HEAD].include?(method) follow_redirect do - send_http_request(method, create_url(redirect_location), headers, body, &response_handler) + send_http_request(method, url+redirect_location, headers, body, &response_handler) end else raise Exceptions::InvalidRedirect, "#{method} request was redirected from #{url} to #{redirect_location}. Only GET and HEAD support redirects." @@ -289,11 +289,26 @@ class Chef def retrying_http_errors(url) http_attempts = 0 begin - http_attempts += 1 - - yield - + loop do + http_attempts += 1 + response, request, return_value = yield + # handle HTTP 50X Error + if response.kind_of?(Net::HTTPServerError) + if http_retry_count - http_attempts + 1 > 0 + sleep_time = 1 + (2 ** http_attempts) + rand(2 ** http_attempts) + Chef::Log.error("Server returned error #{response.code} for #{url}, retrying #{http_attempts}/#{http_retry_count} in #{sleep_time}s") + sleep(sleep_time) + redo + end + end + return [response, request, return_value] + end rescue SocketError, Errno::ETIMEDOUT => e + if http_retry_count - http_attempts + 1 > 0 + Chef::Log.error("Error connecting to #{url}, retry #{http_attempts}/#{http_retry_count}") + sleep(http_retry_delay) + retry + end e.message.replace "Error connecting to #{url} - #{e.message}" raise e rescue Errno::ECONNREFUSED @@ -310,14 +325,6 @@ class Chef retry end raise Timeout::Error, "Timeout connecting to #{url}, giving up" - rescue Net::HTTPFatalError => e - if http_retry_count - http_attempts + 1 > 0 - sleep_time = 1 + (2 ** http_attempts) + rand(2 ** http_attempts) - Chef::Log.error("Server returned error for #{url}, retrying #{http_attempts}/#{http_retry_count} in #{sleep_time}s") - sleep(sleep_time) - retry - end - raise end end diff --git a/lib/chef/json_compat.rb b/lib/chef/json_compat.rb index e92d5c36ae..0796984ab2 100644 --- a/lib/chef/json_compat.rb +++ b/lib/chef/json_compat.rb @@ -18,9 +18,9 @@ # Wrapper class for interacting with JSON. require 'ffi_yajl' -require 'json' -require 'ffi_yajl/json_gem' # XXX: parts of chef require JSON gem's Hash#to_json monkeypatch require 'chef/exceptions' +# We're requiring this to prevent breaking consumers using Hash.to_json +require 'json' class Chef class JSONCompat @@ -39,6 +39,8 @@ class Chef CHEF_SANDBOX = "Chef::Sandbox".freeze CHEF_RESOURCE = "Chef::Resource".freeze CHEF_RESOURCECOLLECTION = "Chef::ResourceCollection".freeze + CHEF_RESOURCESET = "Chef::ResourceCollection::ResourceSet".freeze + CHEF_RESOURCELIST = "Chef::ResourceCollection::ResourceList".freeze class <<self @@ -145,6 +147,10 @@ class Chef Chef::Resource when CHEF_RESOURCECOLLECTION Chef::ResourceCollection + when CHEF_RESOURCESET + Chef::ResourceCollection::ResourceSet + when CHEF_RESOURCELIST + Chef::ResourceCollection::ResourceList when /^Chef::Resource/ Chef::Resource.find_subclass_by_name(json_class) else diff --git a/lib/chef/knife.rb b/lib/chef/knife.rb index 6421384f01..0c079792a4 100644 --- a/lib/chef/knife.rb +++ b/lib/chef/knife.rb @@ -164,7 +164,6 @@ class Chef def self.load_config(explicit_config_file) config_loader = WorkstationConfigLoader.new(explicit_config_file, Chef::Log) - Chef::Log.debug("Using configuration from #{config_loader.config_location}") config_loader.load ui.warn("No knife configuration file found") if config_loader.no_config_found? @@ -393,6 +392,8 @@ class Chef merge_configs apply_computed_config + # This has to be after apply_computed_config so that Mixlib::Log is configured + Chef::Log.info("Using configuration from #{config[:config_file]}") if config[:config_file] end def show_usage diff --git a/lib/chef/knife/bootstrap.rb b/lib/chef/knife/bootstrap.rb index 36a0fc1e47..a992cf5779 100644 --- a/lib/chef/knife/bootstrap.rb +++ b/lib/chef/knife/bootstrap.rb @@ -17,11 +17,13 @@ # require 'chef/knife' +require 'chef/knife/data_bag_secret_options' require 'erubis' class Chef class Knife class Bootstrap < Knife + include DataBagSecretOptions deps do require 'chef/knife/core/bootstrap_context' @@ -157,17 +159,6 @@ class Chef Chef::Config[:knife][:hints][name] = path ? Chef::JSONCompat.parse(::File.read(path)) : Hash.new } - option :secret, - :short => "-s SECRET", - :long => "--secret ", - :description => "The secret key to use to encrypt data bag item values", - :proc => Proc.new { |s| Chef::Config[:knife][:secret] = s } - - option :secret_file, - :long => "--secret-file SECRET_FILE", - :description => "A file containing the secret key to use to encrypt data bag item values", - :proc => Proc.new { |sf| Chef::Config[:knife][:secret_file] = sf } - option :bootstrap_url, :long => "--bootstrap-url URL", :description => "URL to a custom installation script", @@ -248,7 +239,8 @@ class Chef def render_template template_file = find_template template = IO.read(template_file).chomp - context = Knife::Core::BootstrapContext.new(config, config[:run_list], Chef::Config) + secret = encryption_secret_provided_ignore_encrypt_flag? ? read_secret : nil + context = Knife::Core::BootstrapContext.new(config, config[:run_list], Chef::Config, secret) Erubis::Eruby.new(template).evaluate(context) end diff --git a/lib/chef/knife/bootstrap/archlinux-gems.erb b/lib/chef/knife/bootstrap/archlinux-gems.erb index eb134b90d5..581293daa3 100644 --- a/lib/chef/knife/bootstrap/archlinux-gems.erb +++ b/lib/chef/knife/bootstrap/archlinux-gems.erb @@ -34,7 +34,7 @@ mkdir -p /etc/chef/ohai/hints <% @chef_config[:knife][:hints].each do |name, hash| -%> cat > /etc/chef/ohai/hints/<%= name %>.json <<'EOP' -<%= hash.to_json %> +<%= Chef::JSONCompat.to_json(hash) %> EOP <% end -%> <% end -%> @@ -61,7 +61,7 @@ https_proxy "<%= knife_config[:bootstrap_proxy] %>" EOP cat > /etc/chef/first-boot.json <<'EOP' -<%= first_boot.to_json %> +<%= Chef::JSONCompat.to_json(first_boot) %> EOP <%= start_chef %>' diff --git a/lib/chef/knife/bootstrap/chef-aix.erb b/lib/chef/knife/bootstrap/chef-aix.erb index 3a031ee738..013ad1decb 100644 --- a/lib/chef/knife/bootstrap/chef-aix.erb +++ b/lib/chef/knife/bootstrap/chef-aix.erb @@ -47,7 +47,7 @@ mkdir -p /etc/chef/ohai/hints <% @chef_config[:knife][:hints].each do |name, hash| -%> cat > /etc/chef/ohai/hints/<%= name %>.json <<'EOP' -<%= hash.to_json %> +<%= Chef::JSONCompat.to_json(hash) %> EOP <% end -%> <% end -%> @@ -57,7 +57,7 @@ cat > /etc/chef/client.rb <<'EOP' EOP cat > /etc/chef/first-boot.json <<'EOP' -<%= first_boot.to_json %> +<%= Chef::JSONCompat.to_json(first_boot) %> EOP <%= start_chef %>' diff --git a/lib/chef/knife/bootstrap/chef-full.erb b/lib/chef/knife/bootstrap/chef-full.erb index 6edb485f44..dfd5df0071 100644 --- a/lib/chef/knife/bootstrap/chef-full.erb +++ b/lib/chef/knife/bootstrap/chef-full.erb @@ -61,7 +61,7 @@ mkdir -p /etc/chef/ohai/hints <% @chef_config[:knife][:hints].each do |name, hash| -%> cat > /etc/chef/ohai/hints/<%= name %>.json <<'EOP' -<%= hash.to_json %> +<%= Chef::JSONCompat.to_json(hash) %> EOP <% end -%> <% end -%> @@ -71,7 +71,7 @@ cat > /etc/chef/client.rb <<'EOP' EOP cat > /etc/chef/first-boot.json <<'EOP' -<%= first_boot.to_json %> +<%= Chef::JSONCompat.to_json(first_boot) %> EOP echo "Starting first Chef Client run..." diff --git a/lib/chef/knife/cookbook_create.rb b/lib/chef/knife/cookbook_create.rb index f4183a7245..e17a54079f 100644 --- a/lib/chef/knife/cookbook_create.rb +++ b/lib/chef/knife/cookbook_create.rb @@ -80,7 +80,7 @@ class Chef end def create_cookbook(dir, cookbook_name, copyright, license) - msg("** Creating cookbook #{cookbook_name}") + msg("** Creating cookbook #{cookbook_name} in #{dir}") FileUtils.mkdir_p "#{File.join(dir, cookbook_name, "attributes")}" FileUtils.mkdir_p "#{File.join(dir, cookbook_name, "recipes")}" FileUtils.mkdir_p "#{File.join(dir, cookbook_name, "definitions")}" diff --git a/lib/chef/knife/cookbook_site_share.rb b/lib/chef/knife/cookbook_site_share.rb index 7204ccdb1c..b4a7873c71 100644 --- a/lib/chef/knife/cookbook_site_share.rb +++ b/lib/chef/knife/cookbook_site_share.rb @@ -31,7 +31,7 @@ class Chef require 'chef/cookbook_site_streaming_uploader' end - banner "knife cookbook site share COOKBOOK CATEGORY (options)" + banner "knife cookbook site share COOKBOOK [CATEGORY] (options)" category "cookbook site" option :cookbook_path, @@ -40,17 +40,28 @@ class Chef :description => "A colon-separated path to look for cookbooks in", :proc => lambda { |o| Chef::Config.cookbook_path = o.split(":") } + option :dry_run, + :long => '--dry-run', + :short => '-n', + :boolean => true, + :default => false, + :description => "Don't take action, only print what files will be upload to SuperMarket." + def run - if @name_args.length < 2 + config[:cookbook_path] ||= Chef::Config[:cookbook_path] + + if @name_args.length < 1 show_usage - ui.fatal("You must specify the cookbook name and the category you want to share this cookbook to.") - exit 1 + ui.fatal("You must specify the cookbook name.") + exit(1) + elsif @name_args.length < 2 + cookbook_name = @name_args[0] + category = get_category(cookbook_name) + else + cookbook_name = @name_args[0] + category = @name_args[1] end - config[:cookbook_path] ||= Chef::Config[:cookbook_path] - - cookbook_name = @name_args[0] - category = @name_args[1] cl = Chef::CookbookLoader.new(config[:cookbook_path]) if cl.cookbook_exists?(cookbook_name) cookbook = cl[cookbook_name] @@ -66,6 +77,14 @@ class Chef exit(1) end + if config[:dry_run] + ui.info("Not uploading #{cookbook_name}.tgz due to --dry-run flag.") + result = shell_out!("tar -tzf #{cookbook_name}.tgz", :cwd => tmp_cookbook_dir) + ui.info(result.stdout) + FileUtils.rm_rf tmp_cookbook_dir + return + end + begin do_upload("#{tmp_cookbook_dir}/#{cookbook_name}.tgz", category, Chef::Config[:node_name], Chef::Config[:client_key]) ui.info("Upload complete!") @@ -84,6 +103,22 @@ class Chef end + def get_category(cookbook_name) + begin + data = noauth_rest.get_rest("http://cookbooks.opscode.com/api/v1/cookbooks/#{@name_args[0]}") + if !data["category"] && data["error_code"] + ui.fatal("Received an error from the Opscode Cookbook site: #{data["error_code"]}. On the first time you upload it, you are required to specify the category you want to share this cookbook to.") + exit(1) + else + data['category'] + end + rescue => e + ui.fatal("Unable to reach Opscode Cookbook Site: #{e.message}. Increase log verbosity (-VV) for more information.") + Chef::Log.debug("\n#{e.backtrace.join("\n")}") + exit(1) + end + end + def do_upload(cookbook_filename, cookbook_category, user_id, user_secret_filename) uri = "https://supermarket.getchef.com/api/v1/cookbooks" diff --git a/lib/chef/knife/core/bootstrap_context.rb b/lib/chef/knife/core/bootstrap_context.rb index 03e3a42e4a..e681d7a49b 100644 --- a/lib/chef/knife/core/bootstrap_context.rb +++ b/lib/chef/knife/core/bootstrap_context.rb @@ -30,10 +30,11 @@ class Chef # class BootstrapContext - def initialize(config, run_list, chef_config) + def initialize(config, run_list, chef_config, secret) @config = config @run_list = run_list @chef_config = chef_config + @secret = secret end def bootstrap_environment @@ -45,15 +46,7 @@ class Chef end def encrypted_data_bag_secret - knife_config[:secret] || begin - secret_file_path = knife_config[:secret_file] - expanded_secret_file_path = File.expand_path(secret_file_path.to_s) - if secret_file_path && File.exist?(expanded_secret_file_path) - IO.read(expanded_secret_file_path) - else - nil - end - end + @secret end def trusted_certs diff --git a/lib/chef/knife/core/status_presenter.rb b/lib/chef/knife/core/status_presenter.rb new file mode 100644 index 0000000000..3298d5e4ac --- /dev/null +++ b/lib/chef/knife/core/status_presenter.rb @@ -0,0 +1,156 @@ +# +# Author:: Nicolas DUPEUX (<nicolas.dupeux@arkea.com>) +# Copyright:: Copyright (c) 2011 Opscode, 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/knife/core/text_formatter' +require 'chef/knife/core/generic_presenter' + +class Chef + class Knife + module Core + + # This module may be included into a knife subcommand class to automatically + # add configuration options used by the StatusPresenter + module StatusFormattingOptions + # :nodoc: + # Would prefer to do this in a rational way, but can't be done b/c of + # Mixlib::CLI's design :( + def self.included(includer) + includer.class_eval do + option :medium_output, + :short => '-m', + :long => '--medium', + :boolean => true, + :default => false, + :description => 'Include normal attributes in the output' + + option :long_output, + :short => '-l', + :long => '--long', + :boolean => true, + :default => false, + :description => 'Include all attributes in the output' + end + end + end + + #==Chef::Knife::Core::StatusPresenter + # A customized presenter for Chef::Node objects. Supports variable-length + # output formats for displaying node data + class StatusPresenter < GenericPresenter + + def format(data) + if parse_format_option == :json + summarize_json(data) + else + super + end + end + + def summarize_json(list) + result_list = [] + list.each do |node| + result = {} + + result["name"] = node.name + result["chef_environment"] = node.chef_environment + ip = (node[:ec2] && node[:ec2][:public_ipv4]) || node[:ipaddress] + fqdn = (node[:ec2] && node[:ec2][:public_hostname]) || node[:fqdn] + result["ip"] = ip if ip + result["fqdn"] = fqdn if fqdn + result["run_list"] = node.run_list if config[:run_list] + result["ohai_time"] = node[:ohai_time] + result["platform"] = node[:platform] if node[:platform] + result["platform_version"] = node[:platform_version] if node[:platform_version] + + if config[:long_output] + result["default"] = node.default_attrs + result["override"] = node.override_attrs + result["automatic"] = node.automatic_attrs + end + result_list << result + end + + Chef::JSONCompat.to_json_pretty(result_list) + end + + # Converts a Chef::Node object to a string suitable for output to a + # terminal. If config[:medium_output] or config[:long_output] are set + # the volume of output is adjusted accordingly. Uses colors if enabled + # in the ui object. + def summarize(list) + summarized='' + list.each do |data| + node = data + # special case ec2 with their split horizon whatsis. + ip = (node[:ec2] && node[:ec2][:public_ipv4]) || node[:ipaddress] + fqdn = (node[:ec2] && node[:ec2][:public_hostname]) || node[:fqdn] + + hours, minutes, seconds = time_difference_in_hms(node["ohai_time"]) + hours_text = "#{hours} hour#{hours == 1 ? ' ' : 's'}" + minutes_text = "#{minutes} minute#{minutes == 1 ? ' ' : 's'}" + run_list = "#{node.run_list}" if config[:run_list] + if hours > 24 + color = :red + text = hours_text + elsif hours >= 1 + color = :yellow + text = hours_text + else + color = :green + text = minutes_text + end + + line_parts = Array.new + line_parts << @ui.color(text, color) + ' ago' << node.name + line_parts << fqdn if fqdn + line_parts << ip if ip + line_parts << run_list if run_list + + if node['platform'] + platform = node['platform'] + if node['platform_version'] + platform << " #{node['platform_version']}" + end + line_parts << platform + end + + summarized=summarized + line_parts.join(', ') + ".\n" + end + summarized + end + + def key(key_text) + ui.color(key_text, :cyan) + end + + # :nodoc: + # TODO: this is duplicated from StatusHelper in the Webui. dedup. + def time_difference_in_hms(unix_time) + now = Time.now.to_i + difference = now - unix_time.to_i + hours = (difference / 3600).to_i + difference = difference % 3600 + minutes = (difference / 60).to_i + seconds = (difference % 60) + return [hours, minutes, seconds] + end + + end + end + end +end diff --git a/lib/chef/knife/core/ui.rb b/lib/chef/knife/core/ui.rb index 0007480ea2..f3ecfbcae8 100644 --- a/lib/chef/knife/core/ui.rb +++ b/lib/chef/knife/core/ui.rb @@ -113,7 +113,7 @@ class Chef # determined by the value of `config[:color]`. When output is not to a # terminal, colored output is never used def color? - Chef::Config[:color] && stdout.tty? && !Chef::Platform.windows? + Chef::Config[:color] && stdout.tty? end def ask(*args, &block) diff --git a/lib/chef/knife/data_bag_create.rb b/lib/chef/knife/data_bag_create.rb index bc49c68448..f8a7619a8a 100644 --- a/lib/chef/knife/data_bag_create.rb +++ b/lib/chef/knife/data_bag_create.rb @@ -18,10 +18,12 @@ # require 'chef/knife' +require 'chef/knife/data_bag_secret_options' class Chef class Knife class DataBagCreate < Knife + include DataBagSecretOptions deps do require 'chef/data_bag' @@ -31,33 +33,6 @@ class Chef banner "knife data bag create BAG [ITEM] (options)" category "data bag" - option :secret, - :short => "-s SECRET", - :long => "--secret ", - :description => "The secret key to use to encrypt data bag item values", - :proc => Proc.new { |s| Chef::Config[:knife][:secret] = s } - - option :secret_file, - :long => "--secret-file SECRET_FILE", - :description => "A file containing the secret key to use to encrypt data bag item values", - :proc => Proc.new { |sf| Chef::Config[:knife][:secret_file] = sf } - - def read_secret - if config[:secret] - config[:secret] - else - Chef::EncryptedDataBagItem.load_secret(config[:secret_file]) - end - end - - def use_encryption - if config[:secret] && config[:secret_file] - ui.fatal("please specify only one of --secret, --secret-file") - exit(1) - end - config[:secret] || config[:secret_file] - end - def run @data_bag_name, @data_bag_item_name = @name_args @@ -87,11 +62,11 @@ class Chef if @data_bag_item_name create_object({ "id" => @data_bag_item_name }, "data_bag_item[#{@data_bag_item_name}]") do |output| item = Chef::DataBagItem.from_hash( - if use_encryption - Chef::EncryptedDataBagItem.encrypt_data_bag_item(output, read_secret) - else - output - end) + if encryption_secret_provided? + Chef::EncryptedDataBagItem.encrypt_data_bag_item(output, read_secret) + else + output + end) item.data_bag(@data_bag_name) rest.post_rest("data/#{@data_bag_name}", item) end diff --git a/lib/chef/knife/data_bag_edit.rb b/lib/chef/knife/data_bag_edit.rb index b3f53af919..6ef4b33f59 100644 --- a/lib/chef/knife/data_bag_edit.rb +++ b/lib/chef/knife/data_bag_edit.rb @@ -18,10 +18,12 @@ # require 'chef/knife' +require 'chef/knife/data_bag_secret_options' class Chef class Knife class DataBagEdit < Knife + include DataBagSecretOptions deps do require 'chef/data_bag_item' @@ -31,48 +33,17 @@ class Chef banner "knife data bag edit BAG ITEM (options)" category "data bag" - option :secret, - :short => "-s SECRET", - :long => "--secret ", - :description => "The secret key to use to encrypt data bag item values", - :proc => Proc.new { |s| Chef::Config[:knife][:secret] = s } - - option :secret_file, - :long => "--secret-file SECRET_FILE", - :description => "A file containing the secret key to use to encrypt data bag item values", - :proc => Proc.new { |sf| Chef::Config[:knife][:secret_file] = sf } - - def read_secret - if config[:secret] - config[:secret] - else - Chef::EncryptedDataBagItem.load_secret(config[:secret_file]) - end - end - - def use_encryption - if config[:secret] && config[:secret_file] - stdout.puts "please specify only one of --secret, --secret-file" - exit(1) - end - config[:secret] || config[:secret_file] - end - def load_item(bag, item_name) item = Chef::DataBagItem.load(bag, item_name) - if use_encryption - Chef::EncryptedDataBagItem.new(item, read_secret).to_hash + if encrypted?(item.raw_data) + if encryption_secret_provided_ignore_encrypt_flag? + return Chef::EncryptedDataBagItem.new(item, read_secret).to_hash, true + else + ui.fatal("You cannot edit an encrypted data bag without providing the secret.") + exit(1) + end else - item - end - end - - def edit_item(item) - output = edit_data(item) - if use_encryption - Chef::EncryptedDataBagItem.encrypt_data_bag_item(output, read_secret) - else - output + return item, false end end @@ -82,11 +53,21 @@ class Chef stdout.puts opt_parser exit 1 end - item = load_item(@name_args[0], @name_args[1]) - output = edit_item(item) - rest.put_rest("data/#{@name_args[0]}/#{@name_args[1]}", output) + + item, was_encrypted = load_item(@name_args[0], @name_args[1]) + edited_item = edit_data(item) + + if was_encrypted || encryption_secret_provided? + ui.info("Encrypting data bag using provided secret.") + item_to_save = Chef::EncryptedDataBagItem.encrypt_data_bag_item(edited_item, read_secret) + else + ui.info("Saving data bag unencrypted. To encrypt it, provide an appropriate secret.") + item_to_save = edited_item + end + + rest.put_rest("data/#{@name_args[0]}/#{@name_args[1]}", item_to_save) stdout.puts("Saved data_bag_item[#{@name_args[1]}]") - ui.output(output) if config[:print_after] + ui.output(edited_item) if config[:print_after] end end end diff --git a/lib/chef/knife/data_bag_from_file.rb b/lib/chef/knife/data_bag_from_file.rb index 2ff79b6256..d1b7daa4a2 100644 --- a/lib/chef/knife/data_bag_from_file.rb +++ b/lib/chef/knife/data_bag_from_file.rb @@ -19,10 +19,12 @@ require 'chef/knife' require 'chef/util/path_helper' +require 'chef/knife/data_bag_secret_options' class Chef class Knife class DataBagFromFile < Knife + include DataBagSecretOptions deps do require 'chef/data_bag' @@ -35,38 +37,11 @@ class Chef banner "knife data bag from file BAG FILE|FOLDER [FILE|FOLDER..] (options)" category "data bag" - option :secret, - :short => "-s SECRET", - :long => "--secret ", - :description => "The secret key to use to encrypt data bag item values", - :proc => Proc.new { |s| Chef::Config[:knife][:secret] = s } - - option :secret_file, - :long => "--secret-file SECRET_FILE", - :description => "A file containing the secret key to use to encrypt data bag item values", - :proc => Proc.new { |sf| Chef::Config[:knife][:secret_file] = sf } - option :all, :short => "-a", :long => "--all", :description => "Upload all data bags or all items for specified data bags" - def read_secret - if config[:secret] - config[:secret] - else - Chef::EncryptedDataBagItem.load_secret(config[:secret_file]) - end - end - - def use_encryption - if config[:secret] && config[:secret_file] - ui.fatal("please specify only one of --secret, --secret-file") - exit(1) - end - config[:secret] || config[:secret_file] - end - def loader @loader ||= Knife::Core::ObjectLoader.new(DataBagItem, ui) end @@ -109,9 +84,8 @@ class Chef item_paths = normalize_item_paths(items) item_paths.each do |item_path| item = loader.load_from("#{data_bags_path}", data_bag, item_path) - item = if use_encryption - secret = read_secret - Chef::EncryptedDataBagItem.encrypt_data_bag_item(item, secret) + item = if encryption_secret_provided? + Chef::EncryptedDataBagItem.encrypt_data_bag_item(item, read_secret) else item end diff --git a/lib/chef/knife/data_bag_secret_options.rb b/lib/chef/knife/data_bag_secret_options.rb new file mode 100644 index 0000000000..766006089e --- /dev/null +++ b/lib/chef/knife/data_bag_secret_options.rb @@ -0,0 +1,142 @@ +# +# Author:: Tyler Ball (<tball@opscode.com>) +# Copyright:: Copyright (c) 2014 Opscode, 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 'mixlib/cli' +require 'chef/config' +require 'chef/encrypted_data_bag_item/check_encrypted' + +class Chef + class Knife + module DataBagSecretOptions + include Mixlib::CLI + include Chef::EncryptedDataBagItem::CheckEncrypted + + # The config object is populated by knife#merge_configs with knife.rb `knife[:*]` config values, but they do + # not overwrite the command line properties. It does mean, however, that `knife[:secret]` and `--secret-file` + # passed at the same time populate both `config[:secret]` and `config[:secret_file]`. We cannot differentiate + # the valid case (`knife[:secret]` in config file and `--secret-file` on CL) and the invalid case (`--secret` + # and `--secret-file` on the CL) - thats why I'm storing the CL options in a different config key if they + # are provided. + + def self.included(base) + base.option :secret, + :short => "-s SECRET", + :long => "--secret ", + :description => "The secret key to use to encrypt data bag item values. Can also be defaulted in your config with the key 'secret'", + # Need to store value from command line in separate variable - knife#merge_configs populates same keys + # on config object from + :proc => Proc.new { |s| set_cl_secret(s) } + + base.option :secret_file, + :long => "--secret-file SECRET_FILE", + :description => "A file containing the secret key to use to encrypt data bag item values. Can also be defaulted in your config with the key 'secret_file'", + :proc => Proc.new { |sf| set_cl_secret_file(sf) } + + base.option :encrypt, + :long => "--encrypt", + :description => "If 'secret' or 'secret_file' is present in your config, then encrypt data bags using it", + :boolean => true, + :default => false + end + + def encryption_secret_provided? + base_encryption_secret_provided? + end + + def encryption_secret_provided_ignore_encrypt_flag? + base_encryption_secret_provided?(false) + end + + def read_secret + # Moving the non 'compile-time' requires into here to speed up knife command loading + # IE, if we are not running 'knife data bag *' we don't need to load 'chef/encrypted_data_bag_item' + require 'chef/encrypted_data_bag_item' + + if has_cl_secret? + config[:secret] + elsif has_cl_secret_file? + Chef::EncryptedDataBagItem.load_secret(config[:secret_file]) + elsif secret = knife_config[:secret] + secret + else + secret_file = knife_config[:secret_file] + Chef::EncryptedDataBagItem.load_secret(secret_file) + end + end + + def validate_secrets + if has_cl_secret? && has_cl_secret_file? + ui.fatal("Please specify only one of --secret, --secret-file") + exit(1) + end + + if knife_config[:secret] && knife_config[:secret_file] + ui.fatal("Please specify only one of 'secret' or 'secret_file' in your config file") + exit(1) + end + end + + private + + ## + # Determine if the user has specified an appropriate secret for encrypting data bag items. + # @returns boolean + def base_encryption_secret_provided?(need_encrypt_flag = true) + validate_secrets + + return true if has_cl_secret? || has_cl_secret_file? + + if need_encrypt_flag + if config[:encrypt] + unless knife_config[:secret] || knife_config[:secret_file] + ui.fatal("No secret or secret_file specified in config, unable to encrypt item.") + exit(1) + end + return true + end + return false + elsif knife_config[:secret] || knife_config[:secret_file] + # Certain situations (show and bootstrap) don't need a --encrypt flag to use the config file secret + return true + end + return false + end + + def has_cl_secret? + Chef::Config[:knife].has_key?(:cl_secret) + end + + def self.set_cl_secret(s) + Chef::Config[:knife][:cl_secret] = s + end + + def has_cl_secret_file? + Chef::Config[:knife].has_key?(:cl_secret_file) + end + + def self.set_cl_secret_file(sf) + Chef::Config[:knife][:cl_secret_file] = sf + end + + def knife_config + Chef::Config.key?(:knife) ? Chef::Config[:knife] : {} + end + + end + end +end diff --git a/lib/chef/knife/data_bag_show.rb b/lib/chef/knife/data_bag_show.rb index 519859ca2d..36715286e8 100644 --- a/lib/chef/knife/data_bag_show.rb +++ b/lib/chef/knife/data_bag_show.rb @@ -18,10 +18,12 @@ # require 'chef/knife' +require 'chef/knife/data_bag_secret_options' class Chef class Knife class DataBagShow < Knife + include DataBagSecretOptions deps do require 'chef/data_bag' @@ -31,45 +33,29 @@ class Chef banner "knife data bag show BAG [ITEM] (options)" category "data bag" - option :secret, - :short => "-s SECRET", - :long => "--secret ", - :description => "The secret key to use to decrypt data bag item values", - :proc => Proc.new { |s| Chef::Config[:knife][:secret] = s } - - option :secret_file, - :long => "--secret-file SECRET_FILE", - :description => "A file containing the secret key to use to decrypt data bag item values", - :proc => Proc.new { |sf| Chef::Config[:knife][:secret_file] = sf } - - def read_secret - if config[:secret] - config[:secret] - else - Chef::EncryptedDataBagItem.load_secret(config[:secret_file]) - end - end - - def use_encryption - if config[:secret] && config[:secret_file] - stdout.puts "please specify only one of --secret, --secret-file" - exit(1) - end - config[:secret] || config[:secret_file] - end - def run display = case @name_args.length - when 2 - if use_encryption + when 2 # Bag and Item names provided + secret = encryption_secret_provided_ignore_encrypt_flag? ? read_secret : nil + raw_data = Chef::DataBagItem.load(@name_args[0], @name_args[1]).raw_data + encrypted = encrypted?(raw_data) + + if encrypted && secret + # Users do not need to pass --encrypt to read data, we simply try to use the provided secret + ui.info("Encrypted data bag detected, decrypting with provided secret.") raw = Chef::EncryptedDataBagItem.load(@name_args[0], @name_args[1], - read_secret) + secret) format_for_display(raw.to_hash) + elsif encrypted && !secret + ui.warn("Encrypted data bag detected, but no secret provided for decoding. Displaying encrypted data.") + format_for_display(raw_data) else - format_for_display(Chef::DataBagItem.load(@name_args[0], @name_args[1]).raw_data) + ui.info("Unencrypted data bag detected, ignoring any provided secret options.") + format_for_display(raw_data) end - when 1 + + when 1 # Only Bag name provided format_list_for_display(Chef::DataBag.load(@name_args[0])) else stdout.puts opt_parser @@ -77,7 +63,7 @@ class Chef end output(display) end + end end end - diff --git a/lib/chef/knife/status.rb b/lib/chef/knife/status.rb index 5906a4a624..93e81f8f03 100644 --- a/lib/chef/knife/status.rb +++ b/lib/chef/knife/status.rb @@ -17,13 +17,13 @@ # require 'chef/knife' +require 'chef/knife/core/status_presenter' class Chef class Knife class Status < Knife deps do - require 'highline' require 'chef/search/query' end @@ -44,74 +44,27 @@ class Chef :long => "--hide-healthy", :description => "Hide nodes that have run chef in the last hour" - def highline - @h ||= HighLine.new - end - def run + ui.use_presenter Knife::Core::StatusPresenter all_nodes = [] q = Chef::Search::Query.new - query = @name_args[0] || "*:*" + query = @name_args[0] ? @name_args[0].dup : '*:*' + if config[:hide_healthy] + time = Time.now.to_i + query_unhealthy = "NOT ohai_time:[" << (time - 60*60).to_s << " TO " << time.to_s << "]" + query << ' AND ' << query_unhealthy << @name_args[0] if @name_args[0] + query = query_unhealthy unless @name_args[0] + end q.search(:node, query) do |node| all_nodes << node end - all_nodes.sort { |n1, n2| + output(all_nodes.sort { |n1, n2| if (config[:sort_reverse] || Chef::Config[:knife][:sort_status_reverse]) (n2["ohai_time"] or 0) <=> (n1["ohai_time"] or 0) else (n1["ohai_time"] or 0) <=> (n2["ohai_time"] or 0) end - }.each do |node| - if node.has_key?("ec2") - fqdn = node['ec2']['public_hostname'] - ipaddress = node['ec2']['public_ipv4'] - else - fqdn = node['fqdn'] - ipaddress = node['ipaddress'] - end - hours, minutes, seconds = time_difference_in_hms(node["ohai_time"]) - hours_text = "#{hours} hour#{hours == 1 ? ' ' : 's'}" - minutes_text = "#{minutes} minute#{minutes == 1 ? ' ' : 's'}" - run_list = "#{node.run_list}" if config[:run_list] - if hours > 24 - color = :red - text = hours_text - elsif hours >= 1 - color = :yellow - text = hours_text - else - color = :green - text = minutes_text - end - - line_parts = Array.new - line_parts << @ui.color(text, color) + " ago" << node.name - line_parts << fqdn if fqdn - line_parts << ipaddress if ipaddress - line_parts << run_list if run_list - - if node['platform'] - platform = node['platform'] - if node['platform_version'] - platform << " #{node['platform_version']}" - end - line_parts << platform - end - highline.say(line_parts.join(', ') + '.') unless (config[:hide_healthy] && hours < 1) - end - - end - - # :nodoc: - # TODO: this is duplicated from StatusHelper in the Webui. dedup. - def time_difference_in_hms(unix_time) - now = Time.now.to_i - difference = now - unix_time.to_i - hours = (difference / 3600).to_i - difference = difference % 3600 - minutes = (difference / 60).to_i - seconds = (difference % 60) - return [hours, minutes, seconds] + }) end end diff --git a/lib/chef/mixin/command/unix.rb b/lib/chef/mixin/command/unix.rb index b63a02663b..2bad4e6bcf 100644 --- a/lib/chef/mixin/command/unix.rb +++ b/lib/chef/mixin/command/unix.rb @@ -100,9 +100,9 @@ class Chef begin if cmd.kind_of?(Array) - exec(*cmd) + Kernel.exec(*cmd) else - exec(cmd) + Kernel.exec(cmd) end raise 'forty-two' rescue Exception => e diff --git a/lib/chef/mixin/homebrew_owner.rb b/lib/chef/mixin/homebrew_owner.rb deleted file mode 100644 index 73bb22ddf5..0000000000 --- a/lib/chef/mixin/homebrew_owner.rb +++ /dev/null @@ -1,58 +0,0 @@ -# -# Author:: Joshua Timberman (<joshua@getchef.com>) -# Author:: Graeme Mathieson (<mathie@woss.name>) -# -# Copyright 2011-2013, Opscode, Inc. -# Copyright 2014, Chef Software, Inc <legal@getchef.com> -# -# 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. -# -# Ported from the homebrew cookbook's Homebrew::Mixin owner helpers -# -# This lives here in Chef::Mixin because Chef's namespacing makes it -# awkward to use modules elsewhere (e.g., chef/provider/package/homebrew/owner) - -class Chef - module Mixin - module HomebrewOwner - def homebrew_owner(node) - @homebrew_owner ||= calculate_owner(node) - end - - private - - def calculate_owner(node) - owner = homebrew_owner_attr(node) || sudo_user || current_user - if owner == 'root' - raise Chef::Exceptions::CannotDetermineHomebrewOwner, - 'The homebrew owner is not specified and the current user is \"root\"' + - 'Homebrew does not support root installs, please specify the homebrew' + - 'owner by setting the attribute `node[\'homebrew\'][\'owner\']`.' - end - owner - end - - def homebrew_owner_attr(node) - node['homebrew']['owner'] if node.attribute?('homebrew') && node['homebrew'].attribute?('owner') - end - - def sudo_user - ENV['SUDO_USER'] - end - - def current_user - ENV['USER'] - end - end - end -end diff --git a/lib/chef/mixin/homebrew_user.rb b/lib/chef/mixin/homebrew_user.rb new file mode 100644 index 0000000000..ab6fb19563 --- /dev/null +++ b/lib/chef/mixin/homebrew_user.rb @@ -0,0 +1,68 @@ +# +# Author:: Joshua Timberman (<joshua@getchef.com>) +# Author:: Graeme Mathieson (<mathie@woss.name>) +# +# Copyright 2011-2013, Opscode, Inc. +# Copyright 2014, Chef Software, Inc <legal@getchef.com> +# +# 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. +# +# Ported from the homebrew cookbook's Homebrew::Mixin owner helpers +# +# This lives here in Chef::Mixin because Chef's namespacing makes it +# awkward to use modules elsewhere (e.g., chef/provider/package/homebrew/owner) + +require 'chef/mixin/shell_out' +require 'etc' + +class Chef + module Mixin + module HomebrewUser + include Chef::Mixin::ShellOut + + ## + # This tries to find the user to execute brew as. If a user is provided, that overrides the brew + # executable user. It is an error condition if the brew executable owner is root or we cannot find + # the brew executable. + def find_homebrew_uid(provided_user = nil) + # They could provide us a user name or a UID + if provided_user + return provided_user if provided_user.is_a? Integer + return Etc.getpwnam(provided_user).uid + end + + @homebrew_owner ||= calculate_owner + @homebrew_owner + end + + private + + def calculate_owner + default_brew_path = '/usr/local/bin/brew' + if ::File.exist?(default_brew_path) + # By default, this follows symlinks which is what we want + owner = ::File.stat(default_brew_path).uid + elsif (brew_path = shell_out("which brew").stdout.strip) && !brew_path.empty? + owner = ::File.stat(brew_path).uid + else + raise Chef::Exceptions::CannotDetermineHomebrewOwner, + 'Could not find the "brew" executable in /usr/local/bin or anywhere on the path.' + end + + Chef::Log.debug "Found Homebrew owner #{Etc.getpwuid(owner).name}; executing `brew` commands as them" + owner + end + + end + end +end diff --git a/lib/chef/mixin/shell_out.rb b/lib/chef/mixin/shell_out.rb index 82772b584a..5b05e788db 100644 --- a/lib/chef/mixin/shell_out.rb +++ b/lib/chef/mixin/shell_out.rb @@ -15,10 +15,6 @@ # See the License for the specific language governing permissions and # limitations under the License. -# chef/shell_out has been deprecated in favor of mixlib/shellout -# chef/shell_out is still required here to ensure backward compatibility -require 'chef/shell_out' - require 'mixlib/shellout' class Chef diff --git a/lib/chef/mixin/windows_env_helper.rb b/lib/chef/mixin/windows_env_helper.rb new file mode 100644 index 0000000000..490b235065 --- /dev/null +++ b/lib/chef/mixin/windows_env_helper.rb @@ -0,0 +1,56 @@ +# +# Author:: Adam Edwards (<adamed@opscode.com>) +# Copyright:: Copyright (c) 2013 Opscode, 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/exceptions' +require 'chef/platform/query_helpers' +require 'chef/win32/error' if Chef::Platform.windows? +require 'chef/win32/api/system' if Chef::Platform.windows? + +class Chef + module Mixin + module WindowsEnvHelper + + if Chef::Platform.windows? + include Chef::ReservedNames::Win32::API::System + end + + #see: http://msdn.microsoft.com/en-us/library/ms682653%28VS.85%29.aspx + HWND_BROADCAST = 0xffff + WM_SETTINGCHANGE = 0x001A + SMTO_BLOCK = 0x0001 + SMTO_ABORTIFHUNG = 0x0002 + SMTO_NOTIMEOUTIFNOTHUNG = 0x0008 + + def broadcast_env_change + flags = SMTO_BLOCK | SMTO_ABORTIFHUNG | SMTO_NOTIMEOUTIFNOTHUNG + SendMessageTimeoutA(HWND_BROADCAST, WM_SETTINGCHANGE, 0, FFI::MemoryPointer.from_string('Environment').address, flags, 5000, nil) + end + + def expand_path(path) + # http://msdn.microsoft.com/en-us/library/windows/desktop/ms724265%28v=vs.85%29.aspx + # Max size of env block on windows is 32k + buf = 0.chr * 32 * 1024 + if ExpandEnvironmentStringsA(path, buf, buf.length) == 0 + Chef::ReservedNames::Win32::Error.raise! + end + buf.strip + end + end + end +end diff --git a/lib/chef/platform/provider_mapping.rb b/lib/chef/platform/provider_mapping.rb index 0766ccffa7..9e15ea5658 100644 --- a/lib/chef/platform/provider_mapping.rb +++ b/lib/chef/platform/provider_mapping.rb @@ -197,10 +197,14 @@ class Chef }, :suse => { :default => { - :service => Chef::Provider::Service::Redhat, + :service => Chef::Provider::Service::Systemd, :cron => Chef::Provider::Cron, :package => Chef::Provider::Package::Zypper, - :group => Chef::Provider::Group::Suse + :group => Chef::Provider::Group::Gpasswd + }, + "< 12.0" => { + :group => Chef::Provider::Group::Suse, + :service => Chef::Provider::Service::Redhat } }, :oracle => { diff --git a/lib/chef/provider/cron/unix.rb b/lib/chef/provider/cron/unix.rb index 5cb1bcda41..350f8bda18 100644 --- a/lib/chef/provider/cron/unix.rb +++ b/lib/chef/provider/cron/unix.rb @@ -25,18 +25,21 @@ class Chef class Provider class Cron class Unix < Chef::Provider::Cron + include Chef::Mixin::ShellOut private def read_crontab - crontab = nil - status = popen4("crontab -l #{@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}" + 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 + + if status > 1 + raise Chef::Exceptions::Cron, "Error determining state of #{@new_resource.name}, exit: #{status}" end - crontab + return nil if status > 0 + crontab.stdout.chomp << "\n" end def write_crontab(crontab) @@ -47,8 +50,9 @@ class Chef exit_status = 0 error_message = "" begin - status, stdout, stderr = run_command_and_return_stdout_stderr(:command => "/usr/bin/crontab #{tempcron.path}",:user => @new_resource.user) - exit_status = status.exitstatus + crontab_write = shell_out("/usr/bin/crontab #{tempcron.path}", :user => @new_resource.user) + stderr = crontab_write.stderr + exit_status = crontab_write.status.exitstatus # solaris9, 10 on some failures for example invalid 'mins' in crontab fails with exit code of zero :( if stderr && stderr.include?("errors detected in input, no crontab file generated") error_message = stderr diff --git a/lib/chef/provider/deploy.rb b/lib/chef/provider/deploy.rb index db147278c2..b30f7ed17e 100644 --- a/lib/chef/provider/deploy.rb +++ b/lib/chef/provider/deploy.rb @@ -375,7 +375,7 @@ class Chef def gem_resource_collection_runner gems_collection = Chef::ResourceCollection.new - gem_packages.each { |rbgem| gems_collection << rbgem } + gem_packages.each { |rbgem| gems_collection.insert(rbgem) } gems_run_context = run_context.dup gems_run_context.resource_collection = gems_collection Chef::Runner.new(gems_run_context) diff --git a/lib/chef/provider/dsc_script.rb b/lib/chef/provider/dsc_script.rb index c979800cba..a70f4b5048 100644 --- a/lib/chef/provider/dsc_script.rb +++ b/lib/chef/provider/dsc_script.rb @@ -47,9 +47,11 @@ class Chef end def load_current_resource - @dsc_resources_info = run_configuration(:test) - @resource_converged = @dsc_resources_info.all? do |resource| - !resource.changes_state? + if supports_dsc? + @dsc_resources_info = run_configuration(:test) + @resource_converged = @dsc_resources_info.all? do |resource| + !resource.changes_state? + end end end @@ -57,8 +59,26 @@ class Chef true end + def define_resource_requirements + requirements.assert(:run) do |a| + err = [ + 'Could not find PowerShell DSC support on the system', + powershell_info_str, + "Powershell 4.0 or higher was not detected on your system and is required to use the dsc_script resource.", + ] + a.assertion { supports_dsc? } + a.failure_message Chef::Exceptions::NoProviderAvailable, err.join(' ') + a.whyrun err + ["Assuming a previous resource installs Powershell 4.0 or higher."] + a.block_action! + end + end + protected + def supports_dsc? + run_context && Chef::Platform.supports_dsc?(node) + end + def run_configuration(operation) config_directory = ::Dir.mktmpdir("chef-dsc-script") configuration_data_path = get_configuration_data_path(config_directory) @@ -78,9 +98,8 @@ class Chef end def get_augmented_configuration_flags(configuration_data_path) - updated_flags = nil + updated_flags = @dsc_resource.flags.nil? ? {} : @dsc_resource.flags.dup if configuration_data_path - updated_flags = @dsc_resource.flags.nil? ? {} : @dsc_resource.flags.dup Chef::Util::PathHelper.validate_path(configuration_data_path) updated_flags[:configurationdata] = configuration_data_path end @@ -144,6 +163,14 @@ class Chef end end end + + def powershell_info_str + if run_context && run_context.node[:languages] && run_context.node[:languages][:powershell] + install_info = "Powershell #{run_context.node[:languages][:powershell][:version]} was found on the system." + else + install_info = 'Powershell was not found.' + end + end end end end diff --git a/lib/chef/provider/env/windows.rb b/lib/chef/provider/env/windows.rb index f73cb42f7e..572ec5c633 100644 --- a/lib/chef/provider/env/windows.rb +++ b/lib/chef/provider/env/windows.rb @@ -16,13 +16,13 @@ # limitations under the License. # -require 'chef/win32/api/system' if RUBY_PLATFORM =~ /mswin|mingw32|windows/ +require 'chef/mixin/windows_env_helper' class Chef class Provider class Env class Windows < Chef::Provider::Env - include Chef::ReservedNames::Win32::API::System if RUBY_PLATFORM =~ /mswin|mingw32|windows/ + include Chef::Mixin::WindowsEnvHelper def create_env obj = env_obj(@new_resource.key_name) @@ -33,7 +33,9 @@ class Chef end obj.variablevalue = @new_resource.value obj.put_ - ENV[@new_resource.key_name] = @new_resource.value + value = @new_resource.value + value = expand_path(value) if @new_resource.key_name.upcase == 'PATH' + ENV[@new_resource.key_name] = value broadcast_env_change end @@ -60,17 +62,6 @@ class Chef end end - #see: http://msdn.microsoft.com/en-us/library/ms682653%28VS.85%29.aspx - HWND_BROADCAST = 0xffff - WM_SETTINGCHANGE = 0x001A - SMTO_BLOCK = 0x0001 - SMTO_ABORTIFHUNG = 0x0002 - SMTO_NOTIMEOUTIFNOTHUNG = 0x0008 - - def broadcast_env_change - flags = SMTO_BLOCK | SMTO_ABORTIFHUNG | SMTO_NOTIMEOUTIFNOTHUNG - SendMessageTimeoutA(HWND_BROADCAST, WM_SETTINGCHANGE, 0, FFI::MemoryPointer.from_string('Environment').address, flags, 5000, nil) - end end end end diff --git a/lib/chef/provider/execute.rb b/lib/chef/provider/execute.rb index d469bea769..54632c0684 100644 --- a/lib/chef/provider/execute.rb +++ b/lib/chef/provider/execute.rb @@ -50,10 +50,11 @@ class Chef opts[:umask] = @new_resource.umask if @new_resource.umask opts[:log_level] = :info opts[:log_tag] = @new_resource.to_s - if STDOUT.tty? && !Chef::Config[:daemon] && Chef::Log.info? + if STDOUT.tty? && !Chef::Config[:daemon] && Chef::Log.info? && !@new_resource.sensitive opts[:live_stream] = STDOUT end - converge_by("execute #{@new_resource.command}") do + description = @new_resource.sensitive ? "sensitive resource" : @new_resource.command + converge_by("execute #{description}") do result = shell_out!(@new_resource.command, opts) Chef::Log.info("#{@new_resource} ran successfully") end diff --git a/lib/chef/provider/git.rb b/lib/chef/provider/git.rb index aa58fdc4a1..2ef119e839 100644 --- a/lib/chef/provider/git.rb +++ b/lib/chef/provider/git.rb @@ -64,7 +64,7 @@ class Chef a.failure_message Chef::Exceptions::UnresolvableGitReference, "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` output: #{@resolved_reference}" + "`git ls-remote '#{@new_resource.repository}' '#{rev_search_pattern}'` output: #{@resolved_reference}" end end @@ -102,6 +102,10 @@ class Chef end end + def git_minor_version + @git_minor_version ||= Gem::Version.new(shell_out!('git --version', run_options).stdout.split.last) + end + def existing_git_clone? ::File.exist?(::File.join(@new_resource.destination, ".git")) end @@ -137,6 +141,7 @@ class Chef args = [] args << "-o #{remote}" unless remote == 'origin' args << "--depth #{@new_resource.depth}" if @new_resource.depth + args << "--no-single-branch" if @new_resource.depth and git_minor_version >= Gem::Version.new('1.7.10') Chef::Log.info "#{@new_resource} cloning repo #{@new_resource.repository} to #{@new_resource.destination}" @@ -150,7 +155,8 @@ class Chef converge_by("checkout ref #{sha_ref} branch #{@new_resource.revision}") do # checkout into a local branch rather than a detached HEAD - shell_out!("git checkout -B #{@new_resource.checkout_branch} #{sha_ref}", run_options(:cwd => @new_resource.destination)) + 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}" end end @@ -234,35 +240,55 @@ class Chef # annotated tags, we have to search for "revision*" and # post-process. Special handling for 'HEAD' to ignore a tag # named 'HEAD'. - rev_pattern = case @new_resource.revision - when '', 'HEAD' - 'HEAD' - else - @new_resource.revision + '*' - end - command = git("ls-remote \"#{@new_resource.repository}\" \"#{rev_pattern}\"") - @resolved_reference = shell_out!(command, run_options).stdout - ref_lines = @resolved_reference.split("\n") - refs = ref_lines.map { |line| line.split("\t") } - # first try for ^{} indicating the commit pointed to by an - # annotated tag - tagged_commit = refs.find { |m| m[1].end_with?("#{@new_resource.revision}^{}") } + @resolved_reference = git_ls_remote(rev_search_pattern) + refs = @resolved_reference.split("\n").map { |line| line.split("\t") } + # First try for ^{} indicating the commit pointed to by an + # annotated tag. # It is possible for a user to create a tag named 'HEAD'. # Using such a degenerate annotated tag would be very # confusing. We avoid the issue by disallowing the use of # annotated tags named 'HEAD'. - if tagged_commit && rev_pattern != 'HEAD' - tagged_commit[0] + if rev_search_pattern != 'HEAD' + found = find_revision(refs, @new_resource.revision, '^{}') else - found = refs.find { |m| m[1].end_with?(@new_resource.revision) } - if found - found[0] - else - nil - end + found = refs_search(refs, 'HEAD') + end + found = find_revision(refs, @new_resource.revision) if found.empty? + found.size == 1 ? found.first[0] : nil + end + + def find_revision(refs, revision, suffix="") + found = refs_search(refs, rev_match_pattern('refs/tags/', revision) + suffix) + found = refs_search(refs, rev_match_pattern('refs/heads/', revision) + suffix) if found.empty? + found = refs_search(refs, revision + suffix) if found.empty? + found + end + + def rev_match_pattern(prefix, revision) + if revision.start_with?(prefix) + revision + else + prefix + revision + end + end + + def rev_search_pattern + if ['', 'HEAD'].include? @new_resource.revision + 'HEAD' + else + @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 + end + + def refs_search(refs, pattern) + refs.find_all { |m| m[1] == pattern } + end + private def run_options(run_opts={}) diff --git a/lib/chef/provider/lwrp_base.rb b/lib/chef/provider/lwrp_base.rb index 90ce70ae61..135a3f6b7c 100644 --- a/lib/chef/provider/lwrp_base.rb +++ b/lib/chef/provider/lwrp_base.rb @@ -81,22 +81,24 @@ class Chef include Chef::DSL::DataQuery def self.build_from_file(cookbook_name, filename, run_context) + provider_class = nil provider_name = filename_to_qualified_string(cookbook_name, filename) - # Add log entry if we override an existing light-weight provider. class_name = convert_to_class_name(provider_name) if Chef::Provider.const_defined?(class_name) - Chef::Log.info("#{class_name} light-weight provider already initialized -- overriding!") + Chef::Log.info("#{class_name} light-weight provider is already initialized -- Skipping loading #{filename}!") + Chef::Log.debug("Overriding already defined LWRPs is not supported anymore starting with Chef 12.") + provider_class = Chef::Provider.const_get(class_name) + else + provider_class = Class.new(self) + provider_class.class_from_file(filename) + + class_name = convert_to_class_name(provider_name) + Chef::Provider.const_set(class_name, provider_class) + Chef::Log.debug("Loaded contents of #{filename} into a provider named #{provider_name} defined in Chef::Provider::#{class_name}") end - provider_class = Class.new(self) - provider_class.class_from_file(filename) - - class_name = convert_to_class_name(provider_name) - Chef::Provider.const_set(class_name, provider_class) - Chef::Log.debug("Loaded contents of #{filename} into a provider named #{provider_name} defined in Chef::Provider::#{class_name}") - provider_class end diff --git a/lib/chef/provider/package/freebsd/pkgng.rb b/lib/chef/provider/package/freebsd/pkgng.rb index 0741a4d95f..bfe6dca617 100644 --- a/lib/chef/provider/package/freebsd/pkgng.rb +++ b/lib/chef/provider/package/freebsd/pkgng.rb @@ -45,7 +45,7 @@ class Chef def current_installed_version pkg_info = shell_out!("pkg info \"#{@new_resource.package_name}\"", :env => nil, :returns => [0,70]) - pkg_info.stdout[/^#{Regexp.escape(@new_resource.package_name)}-(.+)/, 1] + pkg_info.stdout[/^Version +: (.+)$/, 1] end def candidate_version diff --git a/lib/chef/provider/package/freebsd/port.rb b/lib/chef/provider/package/freebsd/port.rb index 4b3510f0e9..8b191179f0 100644 --- a/lib/chef/provider/package/freebsd/port.rb +++ b/lib/chef/provider/package/freebsd/port.rb @@ -34,7 +34,7 @@ class Chef end def current_installed_version - pkg_info = if supports_pkgng? + pkg_info = if @new_resource.supports_pkgng? shell_out!("pkg info \"#{@new_resource.package_name}\"", :env => nil, :returns => [0,70]) else shell_out!("pkg_info -E \"#{@new_resource.package_name}*\"", :env => nil, :returns => [0,1]) @@ -53,14 +53,6 @@ class Chef def port_dir super(@new_resource.package_name) end - - private - - def supports_pkgng? - with_pkgng = makefile_variable_value('WITH_PKGNG') - with_pkgng && with_pkgng =~ /yes/i - end - end end end diff --git a/lib/chef/provider/package/homebrew.rb b/lib/chef/provider/package/homebrew.rb index d964703c87..a9aeea1415 100644 --- a/lib/chef/provider/package/homebrew.rb +++ b/lib/chef/provider/package/homebrew.rb @@ -19,13 +19,13 @@ # require 'etc' -require 'chef/mixin/homebrew_owner' +require 'chef/mixin/homebrew_user' class Chef class Provider class Package class Homebrew < Chef::Provider::Package - include Chef::Mixin::HomebrewOwner + include Chef::Mixin::HomebrewUser def load_current_resource self.current_resource = Chef::Resource::Package.new(new_resource.name) current_resource.package_name(new_resource.package_name) @@ -109,12 +109,14 @@ class Chef private def get_response_from_command(command) - home_dir = Etc.getpwnam(homebrew_owner(node)).dir + 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_owner(node)}'" - output = shell_out!(command, :timeout => 1800, :user => homebrew_owner(node), :environment => { 'HOME' => home_dir, 'RUBYOPT' => nil }) + Chef::Log.debug "Executing '#{command}' as user '#{homebrew_user.name}'" + output = shell_out!(command, :timeout => 1800, :user => homebrew_uid, :environment => { 'HOME' => homebrew_user.dir, 'RUBYOPT' => nil }) output.stdout.chomp end + end end end diff --git a/lib/chef/provider/package/pacman.rb b/lib/chef/provider/package/pacman.rb index 1014ebcaa5..a9ff0edf7f 100644 --- a/lib/chef/provider/package/pacman.rb +++ b/lib/chef/provider/package/pacman.rb @@ -62,11 +62,11 @@ class Chef package_repos = repos.map {|r| Regexp.escape(r) }.join('|') - status = popen4("pacman -Ss #{@new_resource.package_name}") do |pid, stdin, stdout, stderr| + status = popen4("pacman -Sl") do |pid, stdin, stdout, stderr| stdout.each do |line| case line - when /^(#{package_repos})\/#{Regexp.escape(@new_resource.package_name)} (.+)$/ - # $2 contains a string like "4.4.0-1 (kde kdenetwork)" or "3.10-4 (base)" + 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 diff --git a/lib/chef/provider/package/paludis.rb b/lib/chef/provider/package/paludis.rb index 7c5245fc97..f363b38e50 100644 --- a/lib/chef/provider/package/paludis.rb +++ b/lib/chef/provider/package/paludis.rb @@ -34,7 +34,7 @@ class Chef installed = false re = Regexp.new('(.*)[[:blank:]](.*)[[:blank:]](.*)$') - shell_out!("cave -L warning print-ids -M none -m \"*/#{@new_resource.package_name.split('/').last}\" -f \"%c/%p %v %r\n\"").stdout.each_line do |line| + 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| res = re.match(line) unless res.nil? case res[3] diff --git a/lib/chef/provider/package/rubygems.rb b/lib/chef/provider/package/rubygems.rb index be0022f4aa..6c7e1c066e 100644 --- a/lib/chef/provider/package/rubygems.rb +++ b/lib/chef/provider/package/rubygems.rb @@ -493,6 +493,7 @@ class Chef def target_version_already_installed? return false unless @current_resource && @current_resource.version return false if @current_resource.version.nil? + return false if @new_resource.version.nil? Gem::Requirement.new(@new_resource.version).satisfied_by?(Gem::Version.new(@current_resource.version)) end diff --git a/lib/chef/provider/remote_directory.rb b/lib/chef/provider/remote_directory.rb index 553eb14ae5..5bd1cb5493 100644 --- a/lib/chef/provider/remote_directory.rb +++ b/lib/chef/provider/remote_directory.rb @@ -36,21 +36,18 @@ class Chef def action_create super - files_to_purge = Set.new(Dir.glob(::File.join(Chef::Util::PathHelper.escape_glob(@new_resource.path), '**', '*'), - ::File::FNM_DOTMATCH).select do |name| - name !~ /(?:^|#{Regexp.escape(::File::SEPARATOR)})\.\.?$/ - end) + # Mark all files as needing to be purged + files_to_purge = Set.new(ls(@new_resource.path)) # Make sure each path is clean + # Transfer files files_to_transfer.each do |cookbook_file_relative_path| create_cookbook_file(cookbook_file_relative_path) - # the file is removed from the purge list - files_to_purge.delete(::File.join(@new_resource.path, cookbook_file_relative_path)) - # parent directories are also removed from the purge list - directories=::File.dirname(::File.join(@new_resource.path, cookbook_file_relative_path)).split(::File::SEPARATOR) - for i in 0..directories.length-1 - files_to_purge.delete(::File.join(directories[0..i])) + # parent directories and file being transfered are removed from the purge list + Pathname.new(Chef::Util::PathHelper.cleanpath(::File.join(@new_resource.path, cookbook_file_relative_path))).descend do |d| + files_to_purge.delete(d.to_s) end end + purge_unmanaged_files(files_to_purge) end @@ -62,6 +59,21 @@ class Chef protected + # List all excluding . and .. + def ls(path) + files = Dir.glob(::File.join(Chef::Util::PathHelper.escape_glob(path), '**', '*'), + ::File::FNM_DOTMATCH) + + # Remove current directory and previous directory + files.reject! do |name| + basename = Pathname.new(name).basename().to_s + ['.', '..'].include?(basename) + end + + # Clean all the paths... this is required because of the join + files.map {|f| Chef::Util::PathHelper.cleanpath(f)} + end + def purge_unmanaged_files(unmanaged_files) if @new_resource.purge unmanaged_files.sort.reverse.each do |f| diff --git a/lib/chef/recipe.rb b/lib/chef/recipe.rb index 32578da5ab..de72a8d0c4 100644 --- a/lib/chef/recipe.rb +++ b/lib/chef/recipe.rb @@ -96,6 +96,8 @@ class Chef # true<TrueClass>:: If all the parameters are present # false<FalseClass>:: If any of the parameters are missing def tagged?(*tags) + return false if run_context.node[:tags].nil? + tags.each do |tag| return false unless run_context.node[:tags].include?(tag) end diff --git a/lib/chef/resource.rb b/lib/chef/resource.rb index 70abfbcdb0..e92ea28c69 100644 --- a/lib/chef/resource.rb +++ b/lib/chef/resource.rb @@ -229,6 +229,8 @@ F attr_reader :elapsed_time + attr_reader :default_guard_interpreter + # Each notify entry is a resource/action pair, modeled as an # Struct with a #resource and #action member @@ -250,7 +252,13 @@ F @not_if = [] @only_if = [] @source_line = nil - @guard_interpreter = :default + # We would like to raise an error when the user gives us a guard + # interpreter and a ruby_block to the guard. In order to achieve this + # we need to understand when the user overrides the default guard + # interpreter. Therefore we store the default separately in a different + # attribute. + @guard_interpreter = nil + @default_guard_interpreter = :default @elapsed_time = 0 @sensitive = false end @@ -297,12 +305,13 @@ F end end - def load_prior_resource + def load_prior_resource(resource_type, instance_name) begin - prior_resource = run_context.resource_collection.lookup(self.to_s) + key = ::Chef::ResourceCollection::ResourceSet.create_key(resource_type, instance_name) + prior_resource = run_context.resource_collection.lookup(key) # if we get here, there is a prior resource (otherwise we'd have jumped # to the rescue clause). - Chef::Log.warn("Cloning resource attributes for #{self.to_s} from prior resource (CHEF-3694)") + Chef::Log.warn("Cloning resource attributes for #{key} from prior resource (CHEF-3694)") Chef::Log.warn("Previous #{prior_resource}: #{prior_resource.source_line}") if prior_resource.source_line Chef::Log.warn("Current #{self}: #{self.source_line}") if self.source_line prior_resource.instance_variables.each do |iv| @@ -410,11 +419,15 @@ F end def guard_interpreter(arg=nil) - set_or_return( - :guard_interpreter, - arg, - :kind_of => Symbol - ) + if arg.nil? + @guard_interpreter || @default_guard_interpreter + else + set_or_return( + :guard_interpreter, + arg, + :kind_of => Symbol + ) + end end # Sets up a notification from this resource to the resource specified by +resource_spec+. @@ -639,6 +652,11 @@ F # on accident updated_by_last_action(false) + # Don't modify @retries directly and keep it intact, so that the + # recipe_snippet from ResourceFailureInspector can print the value + # that was set in the resource block initially. + remaining_retries = retries + begin return if should_skip?(action) provider_for_action(action).run_action @@ -646,10 +664,10 @@ F if ignore_failure Chef::Log.error("#{custom_exception_message(e)}; ignore_failure is set, continuing") events.resource_failed(self, action, e) - elsif retries > 0 - events.resource_failed_retriable(self, action, retries, e) - @retries -= 1 - Chef::Log.info("Retrying execution of #{self}, #{retries} attempt(s) left") + elsif remaining_retries > 0 + events.resource_failed_retriable(self, action, remaining_retries, e) + remaining_retries -= 1 + Chef::Log.info("Retrying execution of #{self}, #{remaining_retries} attempt(s) left") sleep retry_delay retry else diff --git a/lib/chef/resource/conditional.rb b/lib/chef/resource/conditional.rb index e6623be5dd..8960a4d57f 100644 --- a/lib/chef/resource/conditional.rb +++ b/lib/chef/resource/conditional.rb @@ -45,22 +45,42 @@ class Chef def initialize(positivity, parent_resource, command=nil, command_opts={}, &block) @positivity = positivity - case command + @command, @command_opts = command, command_opts + @block = block + @block_given = block_given? + @parent_resource = parent_resource + + raise ArgumentError, "only_if/not_if requires either a command or a block" unless command || block_given? + end + + def configure + case @command when String - @guard_interpreter = new_guard_interpreter(parent_resource, command, command_opts, &block) - @command, @command_opts = command, command_opts + @guard_interpreter = new_guard_interpreter(@parent_resource, @command, @command_opts, &@block) @block = nil when nil - raise ArgumentError, "only_if/not_if requires either a command or a block" unless block_given? + # We should have a block if we get here + # Check to see if the user set the guard_interpreter on the parent resource. Note that + # this error will not be raised when using the default_guard_interpreter + if @parent_resource.guard_interpreter != @parent_resource.default_guard_interpreter + msg = "#{@parent_resource.name} was given a guard_interpreter of #{@parent_resource.guard_interpreter}, " + msg << "but not given a command as a string. guard_interpreter does not support blocks (because they just contain ruby)." + raise ArgumentError, msg + end + @guard_interpreter = nil @command, @command_opts = nil, nil - @block = block else - raise ArgumentError, "Invalid only_if/not_if command: #{command.inspect} (#{command.class})" + # command was passed, but it wasn't a String + raise ArgumentError, "Invalid only_if/not_if command, expected a string: #{command.inspect} (#{command.class})" end end + # this is run during convergence via Chef::Resource#run_action -> Chef::Resource#should_skip? def continue? + # configure late in case guard_interpreter is specified on the resource after the conditional + configure + case @positivity when :only_if evaluate diff --git a/lib/chef/resource/cookbook_file.rb b/lib/chef/resource/cookbook_file.rb index de758aef71..2709cf64f4 100644 --- a/lib/chef/resource/cookbook_file.rb +++ b/lib/chef/resource/cookbook_file.rb @@ -40,7 +40,7 @@ class Chef end def source(source_filename=nil) - set_or_return(:source, source_filename, :kind_of => String) + set_or_return(:source, source_filename, :kind_of => [ String, Array ]) end def cookbook(cookbook_name=nil) diff --git a/lib/chef/resource/dsc_script.rb b/lib/chef/resource/dsc_script.rb index 2972ace1aa..76ac6659d6 100644 --- a/lib/chef/resource/dsc_script.rb +++ b/lib/chef/resource/dsc_script.rb @@ -28,12 +28,8 @@ class Chef super @allowed_actions.push(:run) @action = :run - if(run_context && Chef::Platform.supports_dsc?(run_context.node)) - @provider = Chef::Provider::DscScript - else - raise Chef::Exceptions::NoProviderAvailable, - "#{powershell_info_str(run_context)}\nPowershell 4.0 or higher was not detected on your system and is required to use the dsc_script resource." - end + @provider = Chef::Provider::DscScript + @resource_name = :dsc_script end def code(arg=nil) @@ -125,16 +121,6 @@ class Chef :kind_of => [ Integer ] ) end - - private - - def powershell_info_str(run_context) - if run_context && run_context.node[:languages] && run_context.node[:languages][:powershell] - install_info = "Powershell #{run_context.node[:languages][:powershell][:version]} was found on the system." - else - install_info = 'Powershell was not found.' - end - end end end end diff --git a/lib/chef/resource/execute.rb b/lib/chef/resource/execute.rb index 7c4fa48c0a..ae118b1c9e 100644 --- a/lib/chef/resource/execute.rb +++ b/lib/chef/resource/execute.rb @@ -35,12 +35,12 @@ class Chef @cwd = nil @environment = nil @group = nil - @path = nil @returns = 0 @timeout = nil @user = nil @allowed_actions.push(:run) @umask = nil + @default_guard_interpreter = :execute end def umask(arg=nil) @@ -93,14 +93,6 @@ class Chef ) end - def path(arg=nil) - set_or_return( - :path, - arg, - :kind_of => [ Array ] - ) - end - def returns(arg=nil) set_or_return( :returns, @@ -125,6 +117,30 @@ class Chef ) end + def self.set_guard_inherited_attributes(*inherited_attributes) + @class_inherited_attributes = inherited_attributes + end + + def self.guard_inherited_attributes(*inherited_attributes) + # Similar to patterns elsewhere, return attributes from this + # class and superclasses as a form of inheritance + ancestor_attributes = [] + + if superclass.respond_to?(:guard_inherited_attributes) + ancestor_attributes = superclass.guard_inherited_attributes + end + + ancestor_attributes.concat(@class_inherited_attributes ? @class_inherited_attributes : []).uniq + end + + set_guard_inherited_attributes( + :cwd, + :environment, + :group, + :user, + :umask + ) + end end end diff --git a/lib/chef/resource/freebsd_package.rb b/lib/chef/resource/freebsd_package.rb index 70ef62ae8a..957c25caf1 100644 --- a/lib/chef/resource/freebsd_package.rb +++ b/lib/chef/resource/freebsd_package.rb @@ -31,49 +31,36 @@ class Chef provides :package, :on_platforms => ["freebsd"] - attr_accessor :created_as_type - def initialize(name, run_context=nil) super @resource_name = :freebsd_package - @created_as_type = "freebsd_package" end def after_created assign_provider end - # This resource can be invoked with multiple names package & freebsd_package. - # We override the to_s method to ensure the key in resource collection - # matches the type resource is declared as using created_as_type. This - # logic can be removed once Chef does this for all resource in Chef 12: - # https://github.com/opscode/chef/issues/1817 - def to_s - "#{created_as_type}[#{name}]" + def supports_pkgng? + ships_with_pkgng? || !!shell_out!("make -V WITH_PKGNG", :env => nil).stdout.match(/yes/i) end private + def ships_with_pkgng? + # It was not until __FreeBSD_version 1000017 that pkgng became + # the default binary package manager. See '/usr/ports/Mk/bsd.port.mk'. + node.automatic[:os_version].to_i >= 1000017 + end + def assign_provider @provider = if @source.to_s =~ /^ports$/i Chef::Provider::Package::Freebsd::Port - elsif ships_with_pkgng? || supports_pkgng? + elsif supports_pkgng? Chef::Provider::Package::Freebsd::Pkgng else Chef::Provider::Package::Freebsd::Pkg end end - - def ships_with_pkgng? - # It was not until __FreeBSD_version 1000017 that pkgng became - # the default binary package manager. See '/usr/ports/Mk/bsd.port.mk'. - node[:os_version].to_i >= 1000017 - end - - def supports_pkgng? - !!shell_out!("make -V WITH_PKGNG", :env => nil).stdout.match(/yes/i) - end - end end end diff --git a/lib/chef/resource/homebrew_package.rb b/lib/chef/resource/homebrew_package.rb index c3fa9ddffc..e1d50c1739 100644 --- a/lib/chef/resource/homebrew_package.rb +++ b/lib/chef/resource/homebrew_package.rb @@ -24,11 +24,22 @@ require 'chef/resource/package' class Chef class Resource class HomebrewPackage < Chef::Resource::Package + def initialize(name, run_context=nil) super @resource_name = :homebrew_package @provider = Chef::Provider::Package::Homebrew + @homebrew_user = nil + end + + def homebrew_user(arg=nil) + set_or_return( + :homebrew_user, + arg, + :kind_of => [ String, Integer ] + ) end + end end end diff --git a/lib/chef/resource/lwrp_base.rb b/lib/chef/resource/lwrp_base.rb index 5b67941a8b..a4606be842 100644 --- a/lib/chef/resource/lwrp_base.rb +++ b/lib/chef/resource/lwrp_base.rb @@ -35,26 +35,24 @@ class Chef # Evaluates the LWRP resource file and instantiates a new Resource class. def self.build_from_file(cookbook_name, filename, run_context) + resource_class = nil rname = filename_to_qualified_string(cookbook_name, filename) - # Add log entry if we override an existing lightweight resource. class_name = convert_to_class_name(rname) if Resource.strict_const_defined?(class_name) - old_class = Resource.send(:remove_const, class_name) - # CHEF-3432 -- Chef::Resource keeps a list of subclasses; need to - # remove old ones from the list when replacing. - resource_classes.delete(old_class) - Chef::Log.info("#{class_name} lightweight resource already initialized -- overriding!") - end - - resource_class = Class.new(self) + Chef::Log.info("#{class_name} light-weight resource is already initialized -- Skipping loading #{filename}!") + Chef::Log.debug("Overriding already defined LWRPs is not supported anymore starting with Chef 12.") + resource_class = Resource.const_get(class_name) + else + resource_class = Class.new(self) - resource_class.resource_name = rname - resource_class.run_context = run_context - resource_class.class_from_file(filename) + resource_class.resource_name = rname + resource_class.run_context = run_context + resource_class.class_from_file(filename) - Chef::Resource.const_set(class_name, resource_class) - Chef::Log.debug("Loaded contents of #{filename} into a resource named #{rname} defined in Chef::Resource::#{class_name}") + Chef::Resource.const_set(class_name, resource_class) + Chef::Log.debug("Loaded contents of #{filename} into a resource named #{rname} defined in Chef::Resource::#{class_name}") + end resource_class end @@ -112,10 +110,21 @@ class Chef if action_names.empty? defined?(@actions) ? @actions : from_superclass(:actions, []).dup else - @actions = action_names + # BC-compat way for checking if actions have already been defined + if defined?(@actions) + @actions.push(*action_names) + else + @actions = action_names + end end end + # @deprecated + def self.valid_actions(*args) + Chef::Log.warn("`valid_actions' is deprecated, please use actions `instead'!") + actions(*args) + end + # Set the run context on the class. Used to provide access to the node # during class definition. def self.run_context=(run_context) diff --git a/lib/chef/resource/script.rb b/lib/chef/resource/script.rb index 6f66fb9094..ff7d23f075 100644 --- a/lib/chef/resource/script.rb +++ b/lib/chef/resource/script.rb @@ -32,6 +32,7 @@ class Chef @code = nil @interpreter = nil @flags = nil + @default_guard_interpreter = :default end def code(arg=nil) @@ -58,31 +59,6 @@ class Chef ) end - def self.set_guard_inherited_attributes(*inherited_attributes) - @class_inherited_attributes = inherited_attributes - end - - def self.guard_inherited_attributes(*inherited_attributes) - # Similar to patterns elsewhere, return attributes from this - # class and superclasses as a form of inheritance - ancestor_attributes = [] - - if superclass.respond_to?(:guard_inherited_attributes) - ancestor_attributes = superclass.guard_inherited_attributes - end - - ancestor_attributes.concat(@class_inherited_attributes ? @class_inherited_attributes : []).uniq - end - - set_guard_inherited_attributes( - :cwd, - :environment, - :group, - :path, - :user, - :umask - ) - end end end diff --git a/lib/chef/resource/template.rb b/lib/chef/resource/template.rb index 9cba6f1c38..8473f5b677 100644 --- a/lib/chef/resource/template.rb +++ b/lib/chef/resource/template.rb @@ -50,7 +50,7 @@ class Chef set_or_return( :source, file, - :kind_of => [ String ] + :kind_of => [ String, Array ] ) end diff --git a/lib/chef/resource_collection.rb b/lib/chef/resource_collection.rb index cc14a03962..30520cff7e 100644 --- a/lib/chef/resource_collection.rb +++ b/lib/chef/resource_collection.rb @@ -17,250 +17,75 @@ # limitations under the License. # -require 'chef/resource' -require 'chef/resource_collection/stepable_iterator' - +require 'chef/resource_collection/resource_set' +require 'chef/resource_collection/resource_list' +require 'chef/resource_collection/resource_collection_serialization' +require 'chef/log' +require 'forwardable' + +## +# ResourceCollection currently handles two tasks: +# 1) Keeps an ordered list of resources to use when converging the node +# 2) Keeps a unique list of resources (keyed as `type[name]`) used for notifications class Chef class ResourceCollection - include Enumerable - - # Matches a multiple resource lookup specification, - # e.g., "service[nginx,unicorn]" - MULTIPLE_RESOURCE_MATCH = /^(.+)\[(.+?),(.+)\]$/ + include ResourceCollectionSerialization + extend Forwardable - # Matches a single resource lookup specification, - # e.g., "service[nginx]" - SINGLE_RESOURCE_MATCH = /^(.+)\[(.+)\]$/ - - attr_reader :iterator + attr_reader :resource_set, :resource_list + private :resource_set, :resource_list def initialize - @resources = Array.new - @resources_by_name = Hash.new - @insert_after_idx = nil - end - - def all_resources - @resources - end - - def [](index) - @resources[index] - end - - def []=(index, arg) - is_chef_resource(arg) - @resources[index] = arg - @resources_by_name[arg.to_s] = index - end - - def <<(*args) - args.flatten.each do |a| - is_chef_resource(a) - @resources << a - @resources_by_name[a.to_s] = @resources.length - 1 - end - self - end - - # 'push' is an alias method to << - alias_method :push, :<< - - def insert(resource) - if @insert_after_idx - # in the middle of executing a run, so any resources inserted now should - # be placed after the most recent addition done by the currently executing - # resource - insert_at(@insert_after_idx + 1, resource) - @insert_after_idx += 1 + @resource_set = ResourceSet.new + @resource_list = ResourceList.new + end + + # @param resource [Chef::Resource] The resource to insert + # @param resource_type [String,Symbol] If known, the resource type used in the recipe, Eg `package`, `execute` + # @param instance_name [String] If known, the recource name as used in the recipe, IE `vim` in `package 'vim'` + # @param at_location [Integer] If know, a location in the @resource_list to insert resource + # If you know the at_location but not the resource_type or instance_name, pass them in as nil + # This method is meant to be the 1 insert method necessary in the future. It should support all known use cases + # for writing into the ResourceCollection. + def insert(resource, opts={}) + resource_type ||= opts[:resource_type] # Would rather use Ruby 2.x syntax, but oh well + instance_name ||= opts[:instance_name] + resource_list.insert(resource) + if !(resource_type.nil? && instance_name.nil?) + resource_set.insert_as(resource, resource_type, instance_name) else - is_chef_resource(resource) - @resources << resource - @resources_by_name[resource.to_s] = @resources.length - 1 + resource_set.insert_as(resource) end end - def insert_at(insert_at_index, *resources) - resources.each do |resource| - is_chef_resource(resource) - end - @resources.insert(insert_at_index, *resources) - # update name -> location mappings and register new resource - @resources_by_name.each_key do |key| - @resources_by_name[key] += resources.size if @resources_by_name[key] >= insert_at_index - end - resources.each_with_index do |resource, i| - @resources_by_name[resource.to_s] = insert_at_index + i - end + # @deprecated + def []=(index, resource) + Chef::Log.warn("`[]=` is deprecated, use `insert` with the `at_location` parameter") + resource_list[index] = resource + resource_set.insert_as(resource) end - def each - @resources.each do |resource| - yield resource + # @deprecated + def push(*resources) + Chef::Log.warn("`push` is deprecated, use `insert`") + resources.flatten.each do |res| + insert(res) end + self end - def execute_each_resource(&resource_exec_block) - @iterator = StepableIterator.for_collection(@resources) - @iterator.each_with_index do |resource, idx| - @insert_after_idx = idx - yield resource - end - end - - def each_index - @resources.each_index do |i| - yield i - end - end - - def empty? - @resources.empty? - end - - def lookup(resource) - lookup_by = nil - if resource.kind_of?(Chef::Resource) - lookup_by = resource.to_s - elsif resource.kind_of?(String) - lookup_by = resource - else - raise ArgumentError, "Must pass a Chef::Resource or String to lookup" - end - res = @resources_by_name[lookup_by] - unless res - raise Chef::Exceptions::ResourceNotFound, "Cannot find a resource matching #{lookup_by} (did you define it first?)" - end - @resources[res] - end - - # Find existing resources by searching the list of existing resources. Possible - # forms are: - # - # find(:file => "foobar") - # find(:file => [ "foobar", "baz" ]) - # find("file[foobar]", "file[baz]") - # find("file[foobar,baz]") - # - # Returns the matching resource, or an Array of matching resources. - # - # Raises an ArgumentError if you feed it bad lookup information - # Raises a Runtime Error if it can't find the resources you are looking for. - def find(*args) - results = Array.new - args.each do |arg| - case arg - when Hash - results << find_resource_by_hash(arg) - when String - results << find_resource_by_string(arg) - else - msg = "arguments to #{self.class.name}#find should be of the form :resource => 'name' or resource[name]" - raise Chef::Exceptions::InvalidResourceSpecification, msg - end - end - flat_results = results.flatten - flat_results.length == 1 ? flat_results[0] : flat_results - end - - # resources is a poorly named, but we have to maintain it for back - # compat. - alias_method :resources, :find - - # Returns true if +query_object+ is a valid string for looking up a - # resource, or raises InvalidResourceSpecification if not. - # === Arguments - # * query_object should be a string of the form - # "resource_type[resource_name]", a single element Hash (e.g., :service => - # "apache2"), or a Chef::Resource (this is the happy path). Other arguments - # will raise an exception. - # === Returns - # * true returns true for all valid input. - # === Raises - # * Chef::Exceptions::InvalidResourceSpecification for all invalid input. - def validate_lookup_spec!(query_object) - case query_object - when Chef::Resource - true - when SINGLE_RESOURCE_MATCH, MULTIPLE_RESOURCE_MATCH - true - when Hash - true - when String - raise Chef::Exceptions::InvalidResourceSpecification, - "The string `#{query_object}' is not valid for resource collection lookup. Correct syntax is `resource_type[resource_name]'" - else - raise Chef::Exceptions::InvalidResourceSpecification, - "The object `#{query_object.inspect}' is not valid for resource collection lookup. " + - "Use a String like `resource_type[resource_name]' or a Chef::Resource object" - end - end - - # Serialize this object as a hash - def to_hash - instance_vars = Hash.new - self.instance_variables.each do |iv| - instance_vars[iv] = self.instance_variable_get(iv) - end - { - 'json_class' => self.class.name, - 'instance_vars' => instance_vars - } - end - - def to_json(*a) - Chef::JSONCompat.to_json(to_hash, *a) - end - - def self.json_create(o) - collection = self.new() - o["instance_vars"].each do |k,v| - collection.instance_variable_set(k.to_sym, v) - end - collection - end + # @deprecated + alias_method :<<, :insert - private + # Read-only methods are simple to delegate - doing that below - def find_resource_by_hash(arg) - results = Array.new - arg.each do |resource_name, name_list| - names = name_list.kind_of?(Array) ? name_list : [ name_list ] - names.each do |name| - res_name = "#{resource_name.to_s}[#{name}]" - results << lookup(res_name) - end - end - return results - end + resource_list_methods = Enumerable.instance_methods + + [:iterator, :all_resources, :[], :each, :execute_each_resource, :each_index, :empty?] - + [:find] # find needs to run on the set + resource_set_methods = [:lookup, :find, :resources, :keys, :validate_lookup_spec!] - def find_resource_by_string(arg) - results = Array.new - case arg - when MULTIPLE_RESOURCE_MATCH - resource_type = $1 - arg =~ /^.+\[(.+)\]$/ - resource_list = $1 - resource_list.split(",").each do |name| - resource_name = "#{resource_type}[#{name}]" - results << lookup(resource_name) - end - when SINGLE_RESOURCE_MATCH - resource_type = $1 - name = $2 - resource_name = "#{resource_type}[#{name}]" - results << lookup(resource_name) - else - raise ArgumentError, "Bad string format #{arg}, you must have a string like resource_type[name]!" - end - return results - end + def_delegators :resource_list, *resource_list_methods + def_delegators :resource_set, *resource_set_methods - def is_chef_resource(arg) - unless arg.kind_of?(Chef::Resource) - raise ArgumentError, "Cannot insert a #{arg.class} into a resource collection: must be a subclass of Chef::Resource" - end - true - end end end diff --git a/lib/chef/resource_collection/resource_collection_serialization.rb b/lib/chef/resource_collection/resource_collection_serialization.rb new file mode 100644 index 0000000000..3651fb2a2a --- /dev/null +++ b/lib/chef/resource_collection/resource_collection_serialization.rb @@ -0,0 +1,59 @@ +# +# Author:: Tyler Ball (<tball@getchef.com>) +# Copyright:: Copyright (c) 2014 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 ResourceCollection + module ResourceCollectionSerialization + # Serialize this object as a hash + def to_hash + instance_vars = Hash.new + self.instance_variables.each do |iv| + instance_vars[iv] = self.instance_variable_get(iv) + end + { + 'json_class' => self.class.name, + 'instance_vars' => instance_vars + } + end + + def to_json(*a) + Chef::JSONCompat.to_json(to_hash, *a) + end + + def self.included(base) + base.extend(ClassMethods) + end + + module ClassMethods + def json_create(o) + collection = self.new() + o["instance_vars"].each do |k,v| + collection.instance_variable_set(k.to_sym, v) + end + collection + end + end + + def is_chef_resource!(arg) + unless arg.kind_of?(Chef::Resource) + raise ArgumentError, "Cannot insert a #{arg.class} into a resource collection: must be a subclass of Chef::Resource" + end + true + end + end + end +end diff --git a/lib/chef/resource_collection/resource_list.rb b/lib/chef/resource_collection/resource_list.rb new file mode 100644 index 0000000000..e083cc0a9f --- /dev/null +++ b/lib/chef/resource_collection/resource_list.rb @@ -0,0 +1,101 @@ +# +# Author:: Tyler Ball (<tball@getchef.com>) +# Copyright:: Copyright (c) 2014 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/resource_collection/stepable_iterator' +require 'chef/resource_collection/resource_collection_serialization' + +# This class keeps the list of all known Resources in the order they are to be executed in. It also keeps a pointer +# to the most recently executed resource so we can add resources-to-execute after this point. +class Chef + class ResourceCollection + class ResourceList + include ResourceCollection::ResourceCollectionSerialization + include Enumerable + + attr_reader :iterator + + def initialize + @resources = Array.new + @insert_after_idx = nil + end + + # @param resource [Chef::Resource] The resource to insert + # If @insert_after_idx is nil, we are not currently executing a converge so the Resource is appended to the + # end of the list. If @insert_after_idx is NOT nil, we ARE currently executing a converge so the resource + # is inserted into the middle of the list after the last resource that was converged. If it is called multiple + # times (when an LWRP contains multiple resources) it keeps track of that. See this example ResourceList: + # [File1, LWRP1, File2] # The iterator starts and points to File1. It is executed and @insert_after_idx=0 + # [File1, LWRP1, File2] # The iterator moves to LWRP1. It is executed and @insert_after_idx=1 + # [File1, LWRP1, Service1, File2] # The LWRP execution inserts Service1 and @insert_after_idx=2 + # [File1, LWRP1, Service1, Service2, File2] # The LWRP inserts Service2 and @insert_after_idx=3. The LWRP + # finishes executing + # [File1, LWRP1, Service1, Service2, File2] # The iterator moves to Service1 since it is the next non-executed + # resource. The execute_each_resource call below resets @insert_after_idx=2 + # If Service1 was another LWRP, it would insert its resources between Service1 and Service2. The iterator keeps + # track of executed resources and @insert_after_idx keeps track of where the next resource to insert should be. + def insert(resource) + is_chef_resource!(resource) + if @insert_after_idx + @resources.insert(@insert_after_idx += 1, resource) + else + @resources << resource + end + end + + # @deprecated - can be removed when it is removed from resource_collection.rb + def []=(index, resource) + @resources[index] = resource + end + + def all_resources + @resources + end + + def [](index) + @resources[index] + end + + def each + @resources.each do |resource| + yield resource + end + end + + def execute_each_resource(&resource_exec_block) + @iterator = ResourceCollection::StepableIterator.for_collection(@resources) + @iterator.each_with_index do |resource, idx| + @insert_after_idx = idx + yield resource + end + end + + def each_index + @resources.each_index do |i| + yield i + end + end + + def empty? + @resources.empty? + end + + end + end +end + diff --git a/lib/chef/resource_collection/resource_set.rb b/lib/chef/resource_collection/resource_set.rb new file mode 100644 index 0000000000..6425c2ab08 --- /dev/null +++ b/lib/chef/resource_collection/resource_set.rb @@ -0,0 +1,170 @@ +# +# Author:: Tyler Ball (<tball@getchef.com>) +# Copyright:: Copyright (c) 2014 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/resource_collection/resource_collection_serialization' + +class Chef + class ResourceCollection + class ResourceSet + include ResourceCollection::ResourceCollectionSerialization + + # Matches a multiple resource lookup specification, + # e.g., "service[nginx,unicorn]" + MULTIPLE_RESOURCE_MATCH = /^(.+)\[(.+?),(.+)\]$/ + + # Matches a single resource lookup specification, + # e.g., "service[nginx]" + SINGLE_RESOURCE_MATCH = /^(.+)\[(.+)\]$/ + + def initialize + @resources_by_key = Hash.new + end + + def keys + @resources_by_key.keys + end + + def insert_as(resource, resource_type=nil, instance_name=nil) + is_chef_resource!(resource) + resource_type ||= resource.resource_name + instance_name ||= resource.name + key = ResourceSet.create_key(resource_type, instance_name) + @resources_by_key[key] = resource + end + + def lookup(key) + case + when key.kind_of?(String) + lookup_by = key + when key.kind_of?(Chef::Resource) + lookup_by = ResourceSet.create_key(key.resource_name, key.name) + else + raise ArgumentError, "Must pass a Chef::Resource or String to lookup" + end + + res = @resources_by_key[lookup_by] + unless res + raise Chef::Exceptions::ResourceNotFound, "Cannot find a resource matching #{lookup_by} (did you define it first?)" + end + res + end + + # Find existing resources by searching the list of existing resources. Possible + # forms are: + # + # find(:file => "foobar") + # find(:file => [ "foobar", "baz" ]) + # find("file[foobar]", "file[baz]") + # find("file[foobar,baz]") + # + # Returns the matching resource, or an Array of matching resources. + # + # Raises an ArgumentError if you feed it bad lookup information + # Raises a Runtime Error if it can't find the resources you are looking for. + def find(*args) + results = Array.new + args.each do |arg| + case arg + when Hash + results << find_resource_by_hash(arg) + when String + results << find_resource_by_string(arg) + else + msg = "arguments to #{self.class.name}#find should be of the form :resource => 'name' or 'resource[name]'" + raise Chef::Exceptions::InvalidResourceSpecification, msg + end + end + flat_results = results.flatten + flat_results.length == 1 ? flat_results[0] : flat_results + end + + # @deprecated + # resources is a poorly named, but we have to maintain it for back + # compat. + alias_method :resources, :find + + # Returns true if +query_object+ is a valid string for looking up a + # resource, or raises InvalidResourceSpecification if not. + # === Arguments + # * query_object should be a string of the form + # "resource_type[resource_name]", a single element Hash (e.g., :service => + # "apache2"), or a Chef::Resource (this is the happy path). Other arguments + # will raise an exception. + # === Returns + # * true returns true for all valid input. + # === Raises + # * Chef::Exceptions::InvalidResourceSpecification for all invalid input. + def validate_lookup_spec!(query_object) + case query_object + when Chef::Resource + true + when SINGLE_RESOURCE_MATCH, MULTIPLE_RESOURCE_MATCH + true + when Hash + true + when String + raise Chef::Exceptions::InvalidResourceSpecification, + "The string `#{query_object}' is not valid for resource collection lookup. Correct syntax is `resource_type[resource_name]'" + else + raise Chef::Exceptions::InvalidResourceSpecification, + "The object `#{query_object.inspect}' is not valid for resource collection lookup. " + + "Use a String like `resource_type[resource_name]' or a Chef::Resource object" + end + end + + def self.create_key(resource_type, instance_name) + "#{resource_type}[#{instance_name}]" + end + + private + + def find_resource_by_hash(arg) + results = Array.new + arg.each do |resource_type, name_list| + instance_names = name_list.kind_of?(Array) ? name_list : [ name_list ] + instance_names.each do |instance_name| + results << lookup(ResourceSet.create_key(resource_type, instance_name)) + end + end + return results + end + + def find_resource_by_string(arg) + results = Array.new + case arg + when MULTIPLE_RESOURCE_MATCH + resource_type = $1 + arg =~ /^.+\[(.+)\]$/ + resource_list = $1 + resource_list.split(",").each do |instance_name| + results << lookup(ResourceSet.create_key(resource_type, instance_name)) + end + when SINGLE_RESOURCE_MATCH + resource_type = $1 + name = $2 + results << lookup(ResourceSet.create_key(resource_type, name)) + else + raise ArgumentError, "Bad string format #{arg}, you must have a string like resource_type[name]!" + end + return results + end + + end + end +end diff --git a/lib/chef/shell/ext.rb b/lib/chef/shell/ext.rb index bc4e955169..fd785e2f79 100644 --- a/lib/chef/shell/ext.rb +++ b/lib/chef/shell/ext.rb @@ -547,7 +547,7 @@ E desc "list all the resources on the current recipe" def resources(*args) if args.empty? - pp run_context.resource_collection.instance_variable_get(:@resources_by_name).keys + pp run_context.resource_collection.keys else pp resources = original_resources(*args) resources diff --git a/lib/chef/util/dsc/local_configuration_manager.rb b/lib/chef/util/dsc/local_configuration_manager.rb index 4a56b6a397..7395dd5bbf 100644 --- a/lib/chef/util/dsc/local_configuration_manager.rb +++ b/lib/chef/util/dsc/local_configuration_manager.rb @@ -29,7 +29,7 @@ class Chef::Util::DSC def test_configuration(configuration_document) status = run_configuration_cmdlet(configuration_document) - handle_what_if_exception!(status.stderr) unless status.succeeded? + log_what_if_exception(status.stderr) unless status.succeeded? configuration_update_required?(status.return_value) end @@ -78,14 +78,14 @@ $ProgressPreference = 'SilentlyContinue';start-dscconfiguration -path #{@configu EOH end - def handle_what_if_exception!(what_if_exception_output) + def log_what_if_exception(what_if_exception_output) if what_if_exception_output.gsub(/\s+/, ' ') =~ /A parameter cannot be found that matches parameter name 'Whatif'/i # LCM returns an error if any of the resources do not support the opptional What-If Chef::Log::warn("Received error while testing configuration due to resource not supporting 'WhatIf'") elsif output_has_dsc_module_failure?(what_if_exception_output) Chef::Log::warn("Received error while testing configuration due to a module for an imported resource possibly not being fully installed:\n#{what_if_exception_output.gsub(/\s+/, ' ')}") else - raise Chef::Exceptions::PowershellCmdletException, "Powershell Cmdlet failed: #{what_if_exception_output.gsub(/\s+/, ' ')}" + Chef::Log::warn("Received error while testing configuration:\n#{what_if_exception_output.gsub(/\s+/, ' ')}") end end diff --git a/lib/chef/util/path_helper.rb b/lib/chef/util/path_helper.rb index 8ca8279593..26c9c76fe5 100644 --- a/lib/chef/util/path_helper.rb +++ b/lib/chef/util/path_helper.rb @@ -141,6 +141,10 @@ class Chef path = cleanpath(join(*parts)) path.gsub(/[\\\{\}\[\]\*\?]/) { |x| "\\"+x } end + + def self.relative_path_from(from, to) + pathname = Pathname.new(Chef::Util::PathHelper.cleanpath(to)).relative_path_from(Pathname.new(Chef::Util::PathHelper.cleanpath(from))) + end end end end diff --git a/lib/chef/util/powershell/cmdlet_result.rb b/lib/chef/util/powershell/cmdlet_result.rb index af7b3607cd..246701a7bc 100644 --- a/lib/chef/util/powershell/cmdlet_result.rb +++ b/lib/chef/util/powershell/cmdlet_result.rb @@ -16,7 +16,7 @@ # limitations under the License. # -require 'json' +require 'chef/json_compat' class Chef::Util::Powershell class CmdletResult @@ -33,7 +33,7 @@ class Chef::Util::Powershell def return_value if output_format == :object - JSON.parse(@status.stdout) + Chef::JSONCompat.parse(@status.stdout) else @status.stdout end diff --git a/lib/chef/win32/api/system.rb b/lib/chef/win32/api/system.rb index a58c0f38f4..d57699acb4 100644 --- a/lib/chef/win32/api/system.rb +++ b/lib/chef/win32/api/system.rb @@ -200,6 +200,15 @@ LRESULT WINAPI SendMessageTimeout( safe_attach_function :SendMessageTimeoutW, [:HWND, :UINT, :WPARAM, :LPARAM, :UINT, :UINT, :PDWORD_PTR], :LRESULT safe_attach_function :SendMessageTimeoutA, [:HWND, :UINT, :WPARAM, :LPARAM, :UINT, :UINT, :PDWORD_PTR], :LRESULT +=begin +DWORD WINAPI ExpandEnvironmentStrings( + _In_ LPCTSTR lpSrc, + _Out_opt_ LPTSTR lpDst, + _In_ DWORD nSize +); +=end + safe_attach_function :ExpandEnvironmentStringsW, [:pointer, :pointer, :DWORD], :DWORD + safe_attach_function :ExpandEnvironmentStringsA, [:pointer, :pointer, :DWORD], :DWORD end end end diff --git a/lib/chef/win32/version.rb b/lib/chef/win32/version.rb index d2138289f5..d16bd8c12f 100644 --- a/lib/chef/win32/version.rb +++ b/lib/chef/win32/version.rb @@ -48,6 +48,8 @@ class Chef public WIN_VERSIONS = { + "Windows 10" => {:major => 6, :minor => 4, :callable => lambda{ |product_type, suite_mask| product_type == VER_NT_WORKSTATION }}, + "Windows Server 10" => {:major => 6, :minor => 4, :callable => lambda {|product_type, suite_mask| product_type != VER_NT_WORKSTATION }}, "Windows 8.1" => {:major => 6, :minor => 3, :callable => lambda{ |product_type, suite_mask| product_type == VER_NT_WORKSTATION }}, "Windows Server 2012 R2" => {:major => 6, :minor => 3, :callable => lambda {|product_type, suite_mask| product_type != VER_NT_WORKSTATION }}, "Windows 8" => {:major => 6, :minor => 2, :callable => lambda{ |product_type, suite_mask| product_type == VER_NT_WORKSTATION }}, |