diff options
author | sersut <serdar@opscode.com> | 2014-03-30 13:45:00 -0700 |
---|---|---|
committer | sersut <serdar@opscode.com> | 2014-03-30 13:45:00 -0700 |
commit | 7a1778fb309423114462d578e01ba0e00108010f (patch) | |
tree | 792e1fbafdca12725edf923cdad22536f3124fc3 /lib/chef | |
parent | 0d097217dda26ac5551d1ad24132d9e53a62e0fb (diff) | |
parent | cacf2a53a3b789829dd6b5b2956e07cc1aa42931 (diff) | |
download | chef-11.12.0.rc.0.tar.gz |
Merge branch 'master' into 11-stable11.12.0.rc.0
Merging mater branch for RC version.
Diffstat (limited to 'lib/chef')
80 files changed, 2258 insertions, 317 deletions
diff --git a/lib/chef/api_client.rb b/lib/chef/api_client.rb index 66cbd3f30e..7b7fd99ff7 100644 --- a/lib/chef/api_client.rb +++ b/lib/chef/api_client.rb @@ -162,9 +162,7 @@ class Chef if response.kind_of?(Chef::ApiClient) response else - client = Chef::ApiClient.new - client.name(response['clientname']) - client + json_create(response) end end diff --git a/lib/chef/api_client/registration.rb b/lib/chef/api_client/registration.rb index f44c326d5d..213d0b7f49 100644 --- a/lib/chef/api_client/registration.rb +++ b/lib/chef/api_client/registration.rb @@ -30,14 +30,13 @@ class Chef # a new client/node identity by borrowing the validator client identity # when creating a new client. class Registration - attr_reader :private_key attr_reader :destination attr_reader :name def initialize(name, destination) @name = name @destination = destination - @private_key = nil + @server_generated_private_key = nil end # Runs the client registration process, including creating the client on @@ -90,29 +89,67 @@ class Chef end def create - response = http_api.post("clients", :name => name, :admin => false) - @private_key = response["private_key"] + response = http_api.post("clients", post_data) + @server_generated_private_key = response["private_key"] response end def update - response = http_api.put("clients/#{name}", :name => name, - :admin => false, - :private_key => true) + response = http_api.put("clients/#{name}", put_data) if response.respond_to?(:private_key) # Chef 11 - @private_key = response.private_key + @server_generated_private_key = response.private_key else # Chef 10 - @private_key = response["private_key"] + @server_generated_private_key = response["private_key"] end response end + def put_data + base_put_data = { :name => name, :admin => false } + if self_generate_keys? + base_put_data[:public_key] = generated_public_key + else + base_put_data[:private_key] = true + end + base_put_data + end + + def post_data + post_data = { :name => name, :admin => false } + post_data[:public_key] = generated_public_key if self_generate_keys? + post_data + end + + def http_api @http_api_as_validator ||= Chef::REST.new(Chef::Config[:chef_server_url], Chef::Config[:validation_client_name], Chef::Config[:validation_key]) end + # Whether or not to generate keys locally and post the public key to the + # server. Delegates to `Chef::Config.local_key_generation`. Servers + # before 11.0 do not support this feature. + def self_generate_keys? + Chef::Config.local_key_generation + end + + def private_key + if self_generate_keys? + generated_private_key.to_pem + else + @server_generated_private_key + end + end + + def generated_private_key + @generated_key ||= OpenSSL::PKey::RSA.generate(2048) + end + + def generated_public_key + generated_private_key.public_key.to_pem + end + def file_flags base_flags = File::CREAT|File::TRUNC|File::RDWR # Windows doesn't have symlinks, so it doesn't have NOFOLLOW diff --git a/lib/chef/application.rb b/lib/chef/application.rb index 04e88de2ce..601bbd91f1 100644 --- a/lib/chef/application.rb +++ b/lib/chef/application.rb @@ -208,7 +208,8 @@ class Chef::Application @chef_client = Chef::Client.new( @chef_client_json, :override_runlist => config[:override_runlist], - :specific_recipes => specific_recipes + :specific_recipes => specific_recipes, + :runlist => config[:runlist] ) @chef_client_json = nil diff --git a/lib/chef/application/client.rb b/lib/chef/application/client.rb index de644b5f31..c579fe4ba1 100644 --- a/lib/chef/application/client.rb +++ b/lib/chef/application/client.rb @@ -25,7 +25,6 @@ require 'chef/log' require 'chef/config_fetcher' require 'chef/handler/error_report' - class Chef::Application::Client < Chef::Application # Mimic self_pipe sleep from Unicorn to capture signals safely @@ -170,7 +169,7 @@ class Chef::Application::Client < Chef::Application option :override_runlist, :short => "-o RunlistItem,RunlistItem...", :long => "--override-runlist RunlistItem,RunlistItem...", - :description => "Replace current run list with specified items", + :description => "Replace current run list with specified items for a single run", :proc => lambda{|items| items = items.split(',') items.compact.map{|item| @@ -178,6 +177,16 @@ class Chef::Application::Client < Chef::Application } } + option :runlist, + :short => "-r RunlistItem,RunlistItem...", + :long => "--runlist RunlistItem,RunlistItem...", + :description => "Permanently replace current run list with specified items", + :proc => lambda{|items| + items = items.split(',') + items.compact.map{|item| + Chef::RunList::RunListItem.new(item) + } + } option :why_run, :short => '-W', :long => '--why-run', @@ -218,12 +227,10 @@ class Chef::Application::Client < Chef::Application :boolean => true end - attr_reader :chef_client_json + IMMEDIATE_RUN_SIGNAL = "1".freeze + GRACEFUL_EXIT_SIGNAL = "2".freeze - def initialize - super - @exit_gracefully = false - end + attr_reader :chef_client_json # Reconfigure the chef client # Re-open the JSON attributes and load them into the node @@ -285,13 +292,12 @@ class Chef::Application::Client < Chef::Application trap("USR1") do Chef::Log.info("SIGUSR1 received, waking up") - SELF_PIPE[1].putc('.') # wakeup master process from select + SELF_PIPE[1].putc(IMMEDIATE_RUN_SIGNAL) # wakeup master process from select end trap("TERM") do Chef::Log.info("SIGTERM received, exiting gracefully") - @exit_gracefully = true - SELF_PIPE[1].putc('.') + SELF_PIPE[1].putc(GRACEFUL_EXIT_SIGNAL) end end @@ -303,23 +309,24 @@ class Chef::Application::Client < Chef::Application Chef::Daemon.daemonize("chef-client") end + signal = nil + loop do begin - Chef::Application.exit!("Exiting", 0) if @exit_gracefully - if Chef::Config[:splay] + 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 end + + signal = nil run_chef_client(Chef::Config[:specific_recipes]) + if Chef::Config[:interval] Chef::Log.debug("Sleeping for #{Chef::Config[:interval]} seconds") - unless SELF_PIPE.empty? - client_sleep Chef::Config[:interval] - else - # Windows - sleep Chef::Config[:interval] - end + signal = interval_sleep else Chef::Application.exit! "Exiting", 0 end @@ -329,12 +336,7 @@ class Chef::Application::Client < Chef::Application if Chef::Config[:interval] Chef::Log.error("#{e.class}: #{e}") Chef::Log.error("Sleeping for #{Chef::Config[:interval]} seconds before trying again") - unless SELF_PIPE.empty? - client_sleep Chef::Config[:interval] - else - # Windows - sleep Chef::Config[:interval] - end + signal = interval_sleep retry else Chef::Application.fatal!("#{e.class}: #{e.message}", 1) @@ -345,8 +347,17 @@ class Chef::Application::Client < Chef::Application private + def interval_sleep + unless SELF_PIPE.empty? + client_sleep Chef::Config[:interval] + else + # Windows + sleep Chef::Config[:interval] + end + end + def client_sleep(sec) IO.select([ SELF_PIPE[0] ], nil, nil, sec) or return - SELF_PIPE[0].getc + SELF_PIPE[0].getc.chr end end diff --git a/lib/chef/client.rb b/lib/chef/client.rb index 722c9915e9..2e5963e996 100644 --- a/lib/chef/client.rb +++ b/lib/chef/client.rb @@ -44,6 +44,7 @@ require 'chef/version' require 'chef/resource_reporter' require 'chef/run_lock' require 'chef/policy_builder' +require 'chef/request_id' require 'ohai' require 'rbconfig' @@ -54,6 +55,16 @@ class Chef class Client include Chef::Mixin::PathSanity + # IO stream that will be used as 'STDOUT' for formatters. Formatters are + # configured during `initialize`, so this provides a convenience for + # setting alternative IO stream during tests. + STDOUT_FD = STDOUT + + # IO stream that will be used as 'STDERR' for formatters. Formatters are + # configured during `initialize`, so this provides a convenience for + # setting alternative IO stream during tests. + STDERR_FD = STDERR + # Clears all notifications for client run status events. # Primarily for testing purposes. def self.clear_notifications @@ -128,15 +139,13 @@ class Chef attr_accessor :rest attr_accessor :runner - #-- - # TODO: timh/cw: 5-19-2010: json_attribs should be moved to RunContext? attr_reader :json_attribs attr_reader :run_status attr_reader :events # Creates a new Chef::Client. def initialize(json_attribs=nil, args={}) - @json_attribs = json_attribs + @json_attribs = json_attribs || {} @node = nil @run_status = nil @runner = nil @@ -148,12 +157,16 @@ class Chef @events = EventDispatch::Dispatcher.new(*event_handlers) @override_runlist = args.delete(:override_runlist) @specific_recipes = args.delete(:specific_recipes) + + if new_runlist = args.delete(:runlist) + @json_attribs["run_list"] = new_runlist + end end def configure_formatters formatters_for_run.map do |formatter_name, output_path| if output_path.nil? - Chef::Formatters.new(formatter_name, STDOUT, STDERR) + Chef::Formatters.new(formatter_name, STDOUT_FD, STDERR_FD) else io = File.open(output_path, "a+") io.sync = true @@ -280,13 +293,10 @@ class Chef end def node_name - name = Chef::Config[:node_name] || ohai[:fqdn] || ohai[:hostname] + name = Chef::Config[:node_name] || ohai[:fqdn] || ohai[:machinename] || ohai[:hostname] Chef::Config[:node_name] = name - unless name - msg = "Unable to determine node name: configure node_name or configure the system's hostname and fqdn" - raise Chef::Exceptions::CannotDetermineNodeName, msg - end + raise Chef::Exceptions::CannotDetermineNodeName unless name # node names > 90 bytes only work with authentication protocol >= 1.1 # see discussion in config.rb. @@ -391,10 +401,15 @@ class Chef # don't add code that may fail before entering this section to be sure to release lock begin runlock.save_pid + + check_ssl_config + + request_id = Chef::RequestID.instance.request_id run_context = nil @events.run_start(Chef::VERSION) Chef::Log.info("*** Chef #{Chef::VERSION} ***") Chef::Log.info "Chef-client pid: #{Process.pid}" + Chef::Log.debug("Chef-client request_id: #{request_id}") enforce_path_sanity run_ohai @events.ohai_completed(node) @@ -404,6 +419,7 @@ class Chef build_node + run_status.run_id = request_id run_status.start_clock Chef::Log.info("Starting Chef Run for #{node.name}") run_started @@ -434,6 +450,8 @@ class Chef @events.run_failed(e) raise ensure + Chef::RequestID.instance.reset_request_id + request_id = nil @run_status = nil run_context = nil runlock.release @@ -474,6 +492,37 @@ class Chef Chef::ReservedNames::Win32::Security.has_admin_privileges? end + def check_ssl_config + if Chef::Config[:ssl_verify_mode] == :verify_none and !Chef::Config[:verify_api_cert] + Chef::Log.warn(<<-WARN) + +* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +SSL validation of HTTPS requests is disabled. HTTPS connections are still +encrypted, but chef is not able to detect forged replies or man in the middle +attacks. + +To fix this issue add an entry like this to your configuration file: + +``` + # Verify all HTTPS connections (recommended) + ssl_verify_mode :verify_peer + + # OR, Verify only connections to chef-server + verify_api_cert true +``` + +To check your SSL configuration, or troubleshoot errors, you can use the +`knife ssl check` command like so: + +``` + knife ssl check -c #{Chef::Config.config_file} +``` + +* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +WARN + end + end + end end diff --git a/lib/chef/config.rb b/lib/chef/config.rb index da3f3790f6..3099d876c1 100644 --- a/lib/chef/config.rb +++ b/lib/chef/config.rb @@ -432,6 +432,17 @@ class Chef default(:validation_key) { chef_zero.enabled ? nil : platform_specific_path("/etc/chef/validation.pem") } default :validation_client_name, "chef-validator" + # When creating a new client via the validation_client account, Chef 11 + # servers allow the client to generate a key pair locally and sent the + # public key to the server. This is more secure and helps offload work from + # the server, enhancing scalability. If enabled and the remote server + # implements only the Chef 10 API, client registration will not work + # properly. + # + # The default value is `false` (Server generates client keys). Set to + # `true` to enable client-side key generation. + default(:local_key_generation) { false } + # Zypper package provider gpg checks. Set to true to enable package # gpg signature checking. This will be default in the # future. Setting to false disables the warnings. diff --git a/lib/chef/cookbook/chefignore.rb b/lib/chef/cookbook/chefignore.rb index 17c000350d..aa9345e64e 100644 --- a/lib/chef/cookbook/chefignore.rb +++ b/lib/chef/cookbook/chefignore.rb @@ -25,7 +25,11 @@ class Chef attr_reader :ignores def initialize(ignore_file_or_repo) + # Check the 'ignore_file_or_repo' path first and then look in the parent directory + # to handle both the chef repo cookbook layout and a standalone cookbook @ignore_file = find_ignore_file(ignore_file_or_repo) + @ignore_file = find_ignore_file(File.dirname(ignore_file_or_repo)) unless readable_file_or_symlink?(@ignore_file) + @ignores = parse_ignore_file end @@ -43,8 +47,7 @@ class Chef def parse_ignore_file ignore_globs = [] - if File.exist?(@ignore_file) && File.readable?(@ignore_file) && - (File.file?(@ignore_file) || File.symlink?(@ignore_file)) + if readable_file_or_symlink?(@ignore_file) File.foreach(@ignore_file) do |line| ignore_globs << line.strip unless line =~ COMMENTS_AND_WHITESPACE end @@ -61,6 +64,11 @@ class Chef File.join(path, 'chefignore') end end + + def readable_file_or_symlink?(path) + File.exist?(@ignore_file) && File.readable?(@ignore_file) && + (File.file?(@ignore_file) || File.symlink?(@ignore_file)) + end end end end diff --git a/lib/chef/cookbook/metadata.rb b/lib/chef/cookbook/metadata.rb index b9b32c8224..32597490d3 100644 --- a/lib/chef/cookbook/metadata.rb +++ b/lib/chef/cookbook/metadata.rb @@ -391,14 +391,14 @@ class Chef :description => { :kind_of => String }, :choice => { :kind_of => [ Array ], :default => [] }, :calculated => { :equal_to => [ true, false ], :default => false }, - :type => { :equal_to => [ "string", "array", "hash", "symbol" ], :default => "string" }, + :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 ] } + :default => { :kind_of => [ String, Array, Hash, Symbol, Numeric, TrueClass, FalseClass ] } } ) options[:required] = remap_required_attribute(options[:required]) unless options[:required].nil? - validate_string_array(options[:choice]) + validate_choice_array(options) validate_calculated_default_rule(options) validate_choice_default_rule(options) @@ -546,6 +546,34 @@ INVALID end end + # Validate the choice of the options hash + # + # Raise an exception if the members of the array do not match the defaults + # === Parameters + # opts<Hash>:: The options hash + def validate_choice_array(opts) + if opts[:choice].kind_of?(Array) + case opts[:type] + when "string" + validator = [ String ] + when "array" + validator = [ Array ] + when "hash" + validator = [ Hash ] + when "symbol" + validator = [ Symbol ] + when "boolean" + validator = [ TrueClass, FalseClass ] + when "numeric" + validator = [ Numeric ] + end + + opts[:choice].each do |choice| + validate( {:choice => choice}, {:choice => {:kind_of => validator}} ) + end + end + end + # For backwards compatibility, remap Boolean values to String # true is mapped to "required" # false is mapped to "optional" diff --git a/lib/chef/cookbook/synchronizer.rb b/lib/chef/cookbook/synchronizer.rb index 4522323fac..fc5d16617c 100644 --- a/lib/chef/cookbook/synchronizer.rb +++ b/lib/chef/cookbook/synchronizer.rb @@ -92,7 +92,7 @@ class Chef # === Returns # true:: Always returns true def sync_cookbooks - Chef::Log.info("Loading cookbooks [#{cookbook_names.sort.join(', ')}]") + Chef::Log.info("Loading cookbooks [#{cookbooks.map {|ckbk| ckbk.name + '@' + ckbk.version}.join(', ')}]") Chef::Log.debug("Cookbooks detail: #{cookbooks.inspect}") clear_obsoleted_cookbooks @@ -136,7 +136,7 @@ class Chef # valid_cache_entries<Hash>:: Out-param; Added to this hash are the files that # were referred to by this cookbook def sync_cookbook(cookbook) - Chef::Log.debug("Synchronizing cookbook #{cookbook.name}") + Chef::Log.debug("Synchronizing cookbook #{cookbook.name} #{cookbook.version}") # files and templates are lazily loaded, and will be done later. diff --git a/lib/chef/cookbook/syntax_check.rb b/lib/chef/cookbook/syntax_check.rb index 59888e2ba3..effc7dd01d 100644 --- a/lib/chef/cookbook/syntax_check.rb +++ b/lib/chef/cookbook/syntax_check.rb @@ -17,6 +17,8 @@ # require 'pathname' +require 'stringio' +require 'erubis' require 'chef/mixin/shell_out' require 'chef/mixin/checksum' @@ -75,6 +77,8 @@ class Chef # validated. attr_reader :validated_files + attr_reader :chefignore + # Creates a new SyntaxCheck given the +cookbook_name+ and a +cookbook_path+. # If no +cookbook_path+ is given, +Chef::Config.cookbook_path+ is used. def self.for_cookbook(cookbook_name, cookbook_path=nil) @@ -90,11 +94,9 @@ class Chef # cookbook_path::: the (on disk) path to the cookbook def initialize(cookbook_path) @cookbook_path = cookbook_path - @validated_files = PersistentSet.new - end + @chefignore ||= Chefignore.new(cookbook_path) - def chefignore - @chefignore ||= Chefignore.new(File.dirname(cookbook_path)) + @validated_files = PersistentSet.new end def remove_ignored_files(file_list) @@ -161,28 +163,127 @@ class Chef def validate_template(erb_file) Chef::Log.debug("Testing template #{erb_file} for syntax errors...") - result = shell_out("erubis -x #{erb_file} | ruby -c") + if validate_inline? + validate_erb_file_inline(erb_file) + else + validate_erb_via_subcommand(erb_file) + end + end + + def validate_ruby_file(ruby_file) + Chef::Log.debug("Testing #{ruby_file} for syntax errors...") + if validate_inline? + validate_ruby_file_inline(ruby_file) + else + validate_ruby_by_subcommand(ruby_file) + end + end + + # Whether or not we're running on a version of ruby that can support + # inline validation. Inline validation relies on the `RubyVM` features + # introduced with ruby 1.9, so 1.8 cannot be supported. + def validate_inline? + defined?(RubyVM::InstructionSequence) + end + + # Validate the ruby code in an erb template. Uses RubyVM to do syntax + # checking, so callers should check #validate_inline? before calling. + def validate_erb_file_inline(erb_file) + old_stderr = $stderr + + engine = Erubis::Eruby.new + engine.convert!(IO.read(erb_file)) + + ruby_code = engine.src + + # Even when we're compiling the code w/ RubyVM, syntax errors just + # print to $stderr. We want to capture this and handle the printing + # ourselves, so we must temporarily swap $stderr to capture the output. + tmp_stderr = $stderr = StringIO.new + + abs_path = File.expand_path(erb_file) + RubyVM::InstructionSequence.new(ruby_code, erb_file, abs_path, 0) + + true + rescue SyntaxError + $stderr = old_stderr + invalid_erb_file(erb_file, tmp_stderr.string) + false + ensure + # be paranoid about setting stderr back to the old value. + $stderr = old_stderr if defined?(old_stderr) && old_stderr + end + + # Validate the ruby code in an erb template. Pipes the output of `erubis + # -x` to `ruby -c`, so it works with any ruby version, but is much slower + # than the inline version. + # -- + # TODO: This can be removed when ruby 1.8 support is dropped. + def validate_erb_via_subcommand(erb_file) + result = shell_out("erubis -x #{erb_file} | #{ruby} -c") result.error! true rescue Mixlib::ShellOut::ShellCommandFailed + invalid_erb_file(erb_file, result.stderr) + false + end + + # Debug a syntax error in a template. + def invalid_erb_file(erb_file, error_message) file_relative_path = erb_file[/^#{Regexp.escape(cookbook_path+File::Separator)}(.*)/, 1] Chef::Log.fatal("Erb template #{file_relative_path} has a syntax error:") - result.stderr.each_line { |l| Chef::Log.fatal(l.chomp) } + error_message.each_line { |l| Chef::Log.fatal(l.chomp) } + nil + end + + # Validate the syntax of a ruby file. Uses (Ruby 1.9+ only) RubyVM to + # compile the code without evaluating it or spawning a new process. + # Callers should check #validate_inline? before calling. + def validate_ruby_file_inline(ruby_file) + # Even when we're compiling the code w/ RubyVM, syntax errors just + # print to $stderr. We want to capture this and handle the printing + # ourselves, so we must temporarily swap $stderr to capture the output. + old_stderr = $stderr + tmp_stderr = $stderr = StringIO.new + abs_path = File.expand_path(ruby_file) + file_content = IO.read(abs_path) + RubyVM::InstructionSequence.new(file_content, ruby_file, abs_path, 0) + true + rescue SyntaxError + $stderr = old_stderr + invalid_ruby_file(ruby_file, tmp_stderr.string) false + ensure + # be paranoid about setting stderr back to the old value. + $stderr = old_stderr if defined?(old_stderr) && old_stderr end - def validate_ruby_file(ruby_file) - Chef::Log.debug("Testing #{ruby_file} for syntax errors...") - result = shell_out("ruby -c #{ruby_file}") + # Validate the syntax of a ruby file by shelling out to `ruby -c`. Should + # work for all ruby versions, but is slower and uses more resources than + # the inline strategy. + def validate_ruby_by_subcommand(ruby_file) + result = shell_out("#{ruby} -c #{ruby_file}") result.error! true rescue Mixlib::ShellOut::ShellCommandFailed + invalid_ruby_file(ruby_file, result.stderr) + false + end + + # Debugs ruby syntax errors by printing the path to the file and any + # diagnostic info given in +error_message+ + def invalid_ruby_file(ruby_file, error_message) file_relative_path = ruby_file[/^#{Regexp.escape(cookbook_path+File::Separator)}(.*)/, 1] Chef::Log.fatal("Cookbook file #{file_relative_path} has a ruby syntax error:") - result.stderr.each_line { |l| Chef::Log.fatal(l.chomp) } + error_message.each_line { |l| Chef::Log.fatal(l.chomp) } false end + # Returns the full path to the running ruby. + def ruby + Gem.ruby + end + end end end diff --git a/lib/chef/dsl/reboot_pending.rb b/lib/chef/dsl/reboot_pending.rb new file mode 100644 index 0000000000..9f80d38c61 --- /dev/null +++ b/lib/chef/dsl/reboot_pending.rb @@ -0,0 +1,61 @@ +# Author:: Bryan McLellan <btm@loftninjas.org> +# Author:: Seth Chisamore <schisamo@opscode.com> +# Copyright:: Copyright (c) 2011,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/dsl/platform_introspection' +require 'chef/dsl/registry_helper' + +class Chef + module DSL + module RebootPending + + include Chef::DSL::RegistryHelper + include Chef::DSL::PlatformIntrospection + + # Returns true if the system needs a reboot or is expected to reboot + # Raises UnsupportedPlatform if this functionality isn't provided yet + def reboot_pending? + + if platform?("windows") + # PendingFileRenameOperations contains pairs (REG_MULTI_SZ) of filenames that cannot be updated + # due to a file being in use (usually a temporary file and a system file) + # \??\c:\temp\test.sys!\??\c:\winnt\system32\test.sys + # http://technet.microsoft.com/en-us/library/cc960241.aspx + registry_value_exists?('HKLM\SYSTEM\CurrentControlSet\Control\Session Manager', { :name => 'PendingFileRenameOperations' }) || + + # RebootRequired key contains Update IDs with a value of 1 if they require a reboot. + # The existence of RebootRequired alone is sufficient on my Windows 8.1 workstation in Windows Update + registry_key_exists?('HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Auto Update\RebootRequired') || + + # Vista + Server 2008 and newer may have reboots pending from CBS + registry_key_exists?('HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Component Based Servicing\RebootRequired') || + + # The mere existance of the UpdateExeVolatile key should indicate a pending restart for certain updates + # http://support.microsoft.com/kb/832475 + (registry_key_exists?('HKLM\SOFTWARE\Microsoft\Updates\UpdateExeVolatile') && + !registry_get_values('HKLM\SOFTWARE\Microsoft\Updates\UpdateExeVolatile').select { |v| v[:name] == "Flags" }[0].nil? && + [1,2,3].include?(registry_get_values('HKLM\SOFTWARE\Microsoft\Updates\UpdateExeVolatile').select { |v| v[:name] == "Flags" }[0][:data])) + elsif platform?("ubuntu") + # This should work for Debian as well if update-notifier-common happens to be installed. We need an API for that. + File.exists?('/var/run/reboot-required') + else + raise Chef::Exceptions::UnsupportedPlatform.new(node[:platform]) + end + end + end + end +end diff --git a/lib/chef/encrypted_data_bag_item.rb b/lib/chef/encrypted_data_bag_item.rb index b38a6f3512..b0d9337212 100644 --- a/lib/chef/encrypted_data_bag_item.rb +++ b/lib/chef/encrypted_data_bag_item.rb @@ -26,7 +26,7 @@ require 'open-uri' # all values, except for the value associated with the id key, have # been encrypted. # -# EncrypedDataBagItem can be used in recipes to decrypt data bag item +# EncryptedDataBagItem can be used in recipes to decrypt data bag item # members. # # Data bag item values are assumed to have been encrypted using the @@ -49,6 +49,22 @@ require 'open-uri' class Chef::EncryptedDataBagItem ALGORITHM = 'aes-256-cbc' + # + # === Synopsis + # + # EncryptedDataBagItem.new(hash, secret) + # + # === Args + # + # +enc_hash+:: + # The encrypted hash to be decrypted + # +secret+:: + # The raw secret key + # + # === Description + # + # Create a new encrypted data bag item for reading (decryption) + # def initialize(enc_hash, secret) @enc_hash = enc_hash @secret = secret @@ -82,6 +98,26 @@ class Chef::EncryptedDataBagItem end end + # + # === Synopsis + # + # EncryptedDataBagItem.load(data_bag, name, secret = nil) + # + # === Args + # + # +data_bag+:: + # The name of the data bag to fetch + # +name+:: + # The name of the data bag item to fetch + # +secret+:: + # The raw secret key. If the +secret+ is nil, the value of the file at + # +Chef::Config[:encrypted_data_bag_secret]+ is loaded. See +load_secret+ + # for more information. + # + # === Description + # + # Loads and decrypts the data bag item with the given name. + # def self.load(data_bag, name, secret = nil) raw_hash = Chef::DataBagItem.load(data_bag, name) secret = secret || self.load_secret diff --git a/lib/chef/exceptions.rb b/lib/chef/exceptions.rb index afd42885f9..bd99cb3ebd 100644 --- a/lib/chef/exceptions.rb +++ b/lib/chef/exceptions.rb @@ -50,7 +50,13 @@ class Chef class Override < RuntimeError; end class UnsupportedAction < RuntimeError; end class MissingLibrary < RuntimeError; end - class CannotDetermineNodeName < RuntimeError; end + + class CannotDetermineNodeName < RuntimeError + def initialize + super "Unable to determine node name: configure node_name or configure the system's hostname and fqdn" + end + end + class User < RuntimeError; end class Group < RuntimeError; end class Link < RuntimeError; end @@ -70,6 +76,7 @@ class Chef class CookbookNotFoundInRepo < ArgumentError; end class RecipeNotFound < ArgumentError; end class AttributeNotFound < RuntimeError; end + class MissingCookbookDependency < StandardError; end # CHEF-5120 class InvalidCommandOption < RuntimeError; end class CommandTimeout < RuntimeError; end class RequestedUIDUnavailable < RuntimeError; end @@ -309,5 +316,10 @@ class Chef end end + class UnsupportedPlatform < RuntimeError + def initialize(platform) + super "This functionality is not supported on platform #{platform}." + end + end end end diff --git a/lib/chef/formatters/error_descriptor.rb b/lib/chef/formatters/error_descriptor.rb index 3f0756df73..c2e656f167 100644 --- a/lib/chef/formatters/error_descriptor.rb +++ b/lib/chef/formatters/error_descriptor.rb @@ -31,7 +31,7 @@ class Chef end def section(heading, text) - @sections << {heading => text} + @sections << {heading => (text or "")} end def display(out) diff --git a/lib/chef/guard_interpreter/default_guard_interpreter.rb b/lib/chef/guard_interpreter/default_guard_interpreter.rb new file mode 100644 index 0000000000..df91c2b1ad --- /dev/null +++ b/lib/chef/guard_interpreter/default_guard_interpreter.rb @@ -0,0 +1,42 @@ +# +# Author:: Adam Edwards (<adamed@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 GuardInterpreter + class DefaultGuardInterpreter + include Chef::Mixin::ShellOut + + protected + + def initialize(command, opts) + @command = command + @command_opts = opts + end + + public + + def evaluate + shell_out(@command, @command_opts).status.success? + rescue Chef::Exceptions::CommandTimeout + Chef::Log.warn "Command '#{@command}' timed out" + false + end + end + end +end + diff --git a/lib/chef/guard_interpreter/resource_guard_interpreter.rb b/lib/chef/guard_interpreter/resource_guard_interpreter.rb new file mode 100644 index 0000000000..229a8502c7 --- /dev/null +++ b/lib/chef/guard_interpreter/resource_guard_interpreter.rb @@ -0,0 +1,122 @@ +# +# Author:: Adam Edwards (<adamed@getchef.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 'chef/guard_interpreter/default_guard_interpreter' + +class Chef + class GuardInterpreter + class ResourceGuardInterpreter < DefaultGuardInterpreter + + def initialize(parent_resource, command, opts, &block) + super(command, opts) + @parent_resource = parent_resource + @resource = get_interpreter_resource(parent_resource) + end + + def evaluate + # Add attributes inherited from the parent class + # 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}) + + # Handles cases like powershell_script where default + # attributes are different when used in a guard vs. not. For + # powershell_script in particular, this will go away when + # the one attribue that causes this changes its default to be + # the same after some period to prepare for deprecation + if @resource.class.respond_to?(:get_default_attributes) + block_attributes = @resource.class.send(:get_default_attributes, @command_opts).merge(block_attributes) + end + + resource_block = block_from_attributes(block_attributes) + evaluate_action(nil, &resource_block) + end + + protected + + def evaluate_action(action=nil, &block) + @resource.instance_eval(&block) + + run_action = action || @resource.action + + begin + @resource.run_action(run_action) + resource_updated = @resource.updated + rescue Mixlib::ShellOut::ShellCommandFailed + resource_updated = nil + end + + resource_updated + end + + def get_interpreter_resource(parent_resource) + if parent_resource.nil? || parent_resource.node.nil? + raise ArgumentError, "Node for guard resource parent must not be nil" + end + + resource_class = Chef::Resource.resource_for_node(parent_resource.guard_interpreter, parent_resource.node) + + if resource_class.nil? + 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" + end + + empty_events = Chef::EventDispatch::Dispatcher.new + anonymous_run_context = Chef::RunContext.new(parent_resource.node, {}, empty_events) + interpreter_resource = resource_class.new('Guard resource', anonymous_run_context) + + interpreter_resource + end + + def block_from_attributes(attributes) + Proc.new do + attributes.keys.each do |attribute_name| + send(attribute_name, attributes[attribute_name]) if respond_to?(attribute_name) + end + end + end + + def merge_inherited_attributes + inherited_attributes = [] + + if @parent_resource.class.respond_to?(:guard_inherited_attributes) + inherited_attributes = @parent_resource.class.send(:guard_inherited_attributes) + end + + if inherited_attributes && !inherited_attributes.empty? + inherited_attributes.each do |attribute| + if @parent_resource.respond_to?(attribute) && @resource.respond_to?(attribute) + parent_value = @parent_resource.send(attribute) + child_value = @resource.send(attribute) + if parent_value || child_value + @resource.send(attribute, parent_value) + end + end + end + end + end + end + end +end diff --git a/lib/chef/http.rb b/lib/chef/http.rb index 78c47735d2..42b5decd6b 100644 --- a/lib/chef/http.rb +++ b/lib/chef/http.rb @@ -393,4 +393,3 @@ class Chef end end - diff --git a/lib/chef/http/decompressor.rb b/lib/chef/http/decompressor.rb index 78af47798c..e1d776da60 100644 --- a/lib/chef/http/decompressor.rb +++ b/lib/chef/http/decompressor.rb @@ -94,16 +94,21 @@ class Chef # object you can use to unzip/inflate a streaming response. def stream_response_handler(response) if gzip_disabled? + Chef::Log.debug "disable_gzip is set. \ + Not using #{response[CONTENT_ENCODING]} \ + and initializing noop stream deflator." NoopInflater.new else case response[CONTENT_ENCODING] when GZIP - Chef::Log.debug "decompressing gzip stream" + Chef::Log.debug "Initializing gzip stream deflator" GzipInflater.new when DEFLATE - Chef::Log.debug "decompressing inflate stream" + Chef::Log.debug "Initializing deflate stream deflator" DeflateInflater.new else + Chef::Log.debug "content_encoding = '#{response[CONTENT_ENCODING]}' \ + initializing noop stream deflator." NoopInflater.new end end @@ -137,5 +142,3 @@ class Chef end end end - - diff --git a/lib/chef/http/remote_request_id.rb b/lib/chef/http/remote_request_id.rb new file mode 100644 index 0000000000..6bec5dba4f --- /dev/null +++ b/lib/chef/http/remote_request_id.rb @@ -0,0 +1,46 @@ +# Author:: Prajakta Purohit (<prajakta@opscode.com>) +# Copyright:: Copyright (c) 2009, 2010, 2013, 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/request_id' + +class Chef + class HTTP + class RemoteRequestID + + def initialize(opts={}) + end + + def handle_request(method, url, headers={}, data=false) + headers.merge!({'X-REMOTE-REQUEST-ID' => Chef::RequestID.instance.request_id}) + [method, url, headers, data] + end + + def handle_response(http_response, rest_request, return_value) + [http_response, rest_request, return_value] + end + + def stream_response_handler(response) + nil + end + + def handle_stream_complete(http_response, rest_request, return_value) + [http_response, rest_request, return_value] + end + + end + end +end diff --git a/lib/chef/http/simple.rb b/lib/chef/http/simple.rb index 0ecb28846c..d675a17ee8 100644 --- a/lib/chef/http/simple.rb +++ b/lib/chef/http/simple.rb @@ -11,6 +11,11 @@ class Chef use Decompressor use CookieManager + # ValidateContentLength should come after Decompressor + # because the order of middlewares is reversed when handling + # responses. + use ValidateContentLength + end end end diff --git a/lib/chef/http/validate_content_length.rb b/lib/chef/http/validate_content_length.rb index 49f1738d42..076194e31a 100644 --- a/lib/chef/http/validate_content_length.rb +++ b/lib/chef/http/validate_content_length.rb @@ -49,22 +49,20 @@ class Chef end def handle_response(http_response, rest_request, return_value) - unless http_response['content-length'] - Chef::Log.debug("HTTP server did not include a Content-Length header in response, cannot identify truncated downloads.") - return [http_response, rest_request, return_value] - end - validate(response_content_length(http_response), http_response.body.bytesize) + validate(http_response, http_response.body.bytesize) if http_response && http_response.body return [http_response, rest_request, return_value] end def handle_stream_complete(http_response, rest_request, return_value) - if http_response['content-length'].nil? - Chef::Log.debug("HTTP server did not include a Content-Length header in response, cannot idenfity streamed download.") - elsif @content_length_counter.nil? + if @content_length_counter.nil? Chef::Log.debug("No content-length information collected for the streamed download, cannot identify streamed download.") else - validate(response_content_length(http_response), @content_length_counter.content_length) + validate(http_response, @content_length_counter.content_length) end + + # Make sure the counter is reset since this object might get used + # again. See CHEF-5100 + @content_length_counter = nil return [http_response, rest_request, return_value] end @@ -73,7 +71,9 @@ class Chef end private + def response_content_length(response) + return nil if response['content-length'].nil? if response['content-length'].is_a?(Array) response['content-length'].first.to_i else @@ -81,12 +81,28 @@ class Chef end end - def validate(content_length, response_length) - Chef::Log.debug "Content-Length header = #{content_length}" - Chef::Log.debug "Response body length = #{response_length}" + def validate(http_response, response_length) + content_length = response_content_length(http_response) + transfer_encoding = http_response['transfer-encoding'] + content_encoding = http_response['content-encoding'] + + if content_length.nil? + Chef::Log.debug "HTTP server did not include a Content-Length header in response, cannot identify truncated downloads." + return true + end + + # if Transfer-Encoding is set the RFC states that we must ignore the Content-Length field + # CHEF-5041: some proxies uncompress gzip content, leave the incorrect content-length, but set the transfer-encoding field + unless transfer_encoding.nil? + Chef::Log.debug "Transfer-Encoding header is set, skipping Content-Length check." + return true + end + if response_length != content_length raise Chef::Exceptions::ContentLengthMismatch.new(response_length, content_length) end + + Chef::Log.debug "Content-Length validated correctly." true end end diff --git a/lib/chef/knife.rb b/lib/chef/knife.rb index eb2c321cab..5cbc968980 100644 --- a/lib/chef/knife.rb +++ b/lib/chef/knife.rb @@ -421,6 +421,7 @@ class Chef # Don't try to load a knife.rb if it wasn't specified. if config[:config_file] + Chef::Config.config_file = config[:config_file] fetcher = Chef::ConfigFetcher.new(config[:config_file], Chef::Config.config_file_jail) if fetcher.config_missing? ui.error("Specified config file #{config[:config_file]} does not exist#{Chef::Config.config_file_jail ? " or is not under config file jail #{Chef::Config.config_file_jail}" : ""}!") diff --git a/lib/chef/knife/bootstrap.rb b/lib/chef/knife/bootstrap.rb index 14dccb3892..a7c10fc608 100644 --- a/lib/chef/knife/bootstrap.rb +++ b/lib/chef/knife/bootstrap.rb @@ -201,7 +201,7 @@ class Chef $stdout.sync = true - ui.info("Bootstrapping Chef on #{ui.color(@node_name, :bold)}") + ui.info("Connecting to #{ui.color(@node_name, :bold)}") begin knife_ssh.run diff --git a/lib/chef/knife/bootstrap/README.md b/lib/chef/knife/bootstrap/README.md new file mode 100644 index 0000000000..13a0fe7ada --- /dev/null +++ b/lib/chef/knife/bootstrap/README.md @@ -0,0 +1,12 @@ +This directory contains bootstrap templates which can be used with the -d flag +to 'knife bootstrap' to install Chef in different ways. To simplify installation, +and reduce the matrix of common installation patterns to support, we have +standardized on the [Omnibus](https://github.com/opscode/omnibus-ruby) built installation +packages. + +The 'chef-full' template downloads a script which is used to determine the correct +Omnibus package for this system from the [Omnitruck](https://github.com/opscode/opscode-omnitruck) API. All other templates in this directory are deprecated and will be removed +in the future. + +You can still utilize custom bootstrap templates on your system if your installation +needs are unique. Additional information can be found on the [docs site](http://docs.opscode.com/knife_bootstrap.html#custom-templates).
\ No newline at end of file diff --git a/lib/chef/knife/bootstrap/chef-full.erb b/lib/chef/knife/bootstrap/chef-full.erb index 24ffca2c69..1d75117b72 100644 --- a/lib/chef/knife/bootstrap/chef-full.erb +++ b/lib/chef/knife/bootstrap/chef-full.erb @@ -23,6 +23,7 @@ install_sh="https://www.opscode.com/chef/install.sh" version_string="-v <%= chef_version %>" if ! exists /usr/bin/chef-client; then + echo "Installing Chef Client..." if exists wget; then bash <(wget <%= "--proxy=on " if knife_config[:bootstrap_proxy] %> ${install_sh} -O -) ${version_string} elif exists curl; then @@ -66,4 +67,6 @@ cat > /etc/chef/first-boot.json <<'EOP' <%= first_boot.to_json %> EOP +echo "Starting first Chef Client run..." + <%= start_chef %>' diff --git a/lib/chef/knife/client_bulk_delete.rb b/lib/chef/knife/client_bulk_delete.rb index 8bf2c2f116..f2be772759 100644 --- a/lib/chef/knife/client_bulk_delete.rb +++ b/lib/chef/knife/client_bulk_delete.rb @@ -27,6 +27,11 @@ class Chef require 'chef/json_compat' end + option :delete_validators, + :short => "-D", + :long => "--delete-validators", + :description => "Force deletion of clients if they're validators" + banner "knife client bulk delete REGEX (options)" def run @@ -38,28 +43,62 @@ class Chef matcher = /#{name_args[0]}/ clients_to_delete = {} + validators_to_delete = {} all_clients.each do |name, client| next unless name =~ matcher - clients_to_delete[client.name] = client + if client.validator + validators_to_delete[client.name] = client + else + clients_to_delete[client.name] = client + end end - if clients_to_delete.empty? + if clients_to_delete.empty? && validators_to_delete.empty? ui.info "No clients match the expression /#{name_args[0]}/" exit 0 end - ui.msg("The following clients will be deleted:") - ui.msg("") - ui.msg(ui.list(clients_to_delete.keys.sort, :columns_down)) - ui.msg("") - ui.confirm("Are you sure you want to delete these clients") + check_and_delete_validators(validators_to_delete) + check_and_delete_clients(clients_to_delete) + end - clients_to_delete.sort.each do |name, client| + def check_and_delete_validators(validators) + unless validators.empty? + unless config[:delete_validators] + ui.msg("Following clients are validators and will not be deleted.") + print_clients(validators) + ui.msg("You must specify --delete-validators to delete the validator clients") + else + ui.msg("The following validators will be deleted:") + print_clients(validators) + if ui.confirm_without_exit("Are you sure you want to delete these validators") + destroy_clients(validators) + end + end + end + end + + def check_and_delete_clients(clients) + unless clients.empty? + ui.msg("The following clients will be deleted:") + print_clients(clients) + ui.confirm("Are you sure you want to delete these clients") + destroy_clients(clients) + end + end + + def destroy_clients(clients) + clients.sort.each do |name, client| client.destroy ui.msg("Deleted client #{name}") end end + + def print_clients(clients) + ui.msg("") + ui.msg(ui.list(clients.keys.sort, :columns_down)) + ui.msg("") + end end end end - diff --git a/lib/chef/knife/client_create.rb b/lib/chef/knife/client_create.rb index 285254aef0..b2bac36081 100644 --- a/lib/chef/knife/client_create.rb +++ b/lib/chef/knife/client_create.rb @@ -38,6 +38,11 @@ class Chef :description => "Create the client as an admin", :boolean => true + option :validator, + :long => "--validator", + :description => "Create the client as a validator", + :boolean => true + banner "knife client create CLIENT (options)" def run @@ -52,6 +57,7 @@ class Chef client = Chef::ApiClient.new client.name(@client_name) client.admin(config[:admin]) + client.validator(config[:validator]) output = edit_data(client) diff --git a/lib/chef/knife/client_delete.rb b/lib/chef/knife/client_delete.rb index 6a6fae7ea0..1902145c8d 100644 --- a/lib/chef/knife/client_delete.rb +++ b/lib/chef/knife/client_delete.rb @@ -27,6 +27,11 @@ class Chef require 'chef/json_compat' end + option :delete_validators, + :short => "-D", + :long => "--delete-validators", + :description => "Force deletion of client if it's a validator" + banner "knife client delete CLIENT (options)" def run @@ -38,7 +43,16 @@ class Chef exit 1 end - delete_object(Chef::ApiClient, @client_name) + delete_object(Chef::ApiClient, @client_name, 'client') { + object = Chef::ApiClient.load(@client_name) + if object.validator + unless config[:delete_validators] + ui.fatal("You must specify --force to delete the validator client #{@client_name}") + exit 2 + end + end + object.destroy + } end end diff --git a/lib/chef/knife/cookbook_bulk_delete.rb b/lib/chef/knife/cookbook_bulk_delete.rb index f8ad74d856..65fa888486 100644 --- a/lib/chef/knife/cookbook_bulk_delete.rb +++ b/lib/chef/knife/cookbook_bulk_delete.rb @@ -49,7 +49,7 @@ class Chef ui.msg "" unless config[:yes] - ui.confirm("Do you really want to delete these cookbooks? (Y/N) ", false) + ui.confirm("Do you really want to delete these cookbooks") if config[:purge] ui.msg("Files that are common to multiple cookbooks are shared, so purging the files may break other cookbooks.") diff --git a/lib/chef/knife/cookbook_upload.rb b/lib/chef/knife/cookbook_upload.rb index a882cd7109..9d6e0d438d 100644 --- a/lib/chef/knife/cookbook_upload.rb +++ b/lib/chef/knife/cookbook_upload.rb @@ -93,6 +93,7 @@ class Chef end assert_environment_valid! + warn_about_cookbook_shadowing version_constraints_to_update = {} upload_failures = 0 upload_ok = 0 @@ -139,6 +140,7 @@ class Chef end end + upload_failures += @name_args.length - @cookbooks_to_upload.length if upload_failures == 0 @@ -199,6 +201,10 @@ class Chef end def warn_about_cookbook_shadowing + # because cookbooks are lazy-loaded, we have to force the loader + # to load the cookbooks the user intends to upload here: + cookbooks_to_upload + unless cookbook_repo.merged_cookbooks.empty? ui.warn "* " * 40 ui.warn(<<-WARNING) @@ -257,14 +263,18 @@ WARNING end def check_for_dependencies!(cookbook) - # for each dependency, check if the version is on the server, or + # for all dependencies, check if the version is on the server, or # the version is in the cookbooks being uploaded. If not, exit and warn the user. - cookbook.metadata.dependencies.each do |cookbook_name, version| - unless check_server_side_cookbooks(cookbook_name, version) || check_uploading_cookbooks(cookbook_name, version) - ui.error "Cookbook #{cookbook.name} depends on cookbook '#{cookbook_name}' version '#{version}'," - ui.error "which is not currently being uploaded and cannot be found on the server." - exit 1 - end + missing_dependencies = cookbook.metadata.dependencies.reject do |cookbook_name, version| + check_server_side_cookbooks(cookbook_name, version) || check_uploading_cookbooks(cookbook_name, version) + end + + unless missing_dependencies.empty? + missing_cookbook_names = missing_dependencies.map { |cookbook_name, version| "'#{cookbook_name}' version '#{version}'"} + ui.error "Cookbook #{cookbook.name} depends on cookbooks which are not currently" + ui.error "being uploaded and cannot be found on the server." + ui.error "The missing cookbook(s) are: #{missing_cookbook_names.join(', ')}" + exit 1 end end diff --git a/lib/chef/knife/core/bootstrap_context.rb b/lib/chef/knife/core/bootstrap_context.rb index e1ad606c80..dc10bbb3d3 100644 --- a/lib/chef/knife/core/bootstrap_context.rb +++ b/lib/chef/knife/core/bootstrap_context.rb @@ -62,7 +62,6 @@ class Chef def config_content client_rb = <<-CONFIG -log_level :auto log_location STDOUT chef_server_url "#{@chef_config[:chef_server_url]}" validation_client_name "#{@chef_config[:validation_client_name]}" @@ -93,6 +92,7 @@ CONFIG # If the user doesn't have a client path configure, let bash use the PATH for what it was designed for client_path = @chef_config[:chef_client_path] || 'chef-client' s = "#{client_path} -j /etc/chef/first-boot.json" + s << ' -l debug' if @config[:verbosity] and @config[:verbosity] >= 2 s << " -E #{bootstrap_environment}" if chef_version.to_f != 0.9 # only use the -E option on Chef 0.10+ s end diff --git a/lib/chef/knife/core/ui.rb b/lib/chef/knife/core/ui.rb index dfa8c11644..ff2545cfed 100644 --- a/lib/chef/knife/core/ui.rb +++ b/lib/chef/knife/core/ui.rb @@ -205,24 +205,61 @@ class Chef output(format_for_display(object)) if config[:print_after] end - def confirm(question, append_instructions=true) + def confirmation_instructions(default_choice) + case default_choice + when true + '? (Y/n)' + when false + '? (y/N)' + else + '? (Y/N)' + end + end + + # See confirm method for argument information + def confirm_without_exit(question, append_instructions=true, default_choice=nil) return true if config[:yes] stdout.print question - stdout.print "? (Y/N) " if append_instructions + stdout.print confirmation_instructions(default_choice) if append_instructions + answer = stdin.readline answer.chomp! + case answer when "Y", "y" true when "N", "n" self.msg("You said no, so I'm done here.") - exit 3 + false + when "" + unless default_choice.nil? + default_choice + else + self.msg("I have no idea what to do with '#{answer}'") + self.msg("Just say Y or N, please.") + confirm_without_exit(question, append_instructions, default_choice) + end else - self.msg("I have no idea what to do with #{answer}") + self.msg("I have no idea what to do with '#{answer}'") self.msg("Just say Y or N, please.") - confirm(question) + confirm_without_exit(question, append_instructions, default_choice) + end + end + + # + # Not the ideal signature for a function but we need to stick with this + # for now until we get a chance to break our API in Chef 12. + # + # question => Question to print before asking for confirmation + # append_instructions => Should print '? (Y/N)' as instructions + # default_choice => Set to true for 'Y', and false for 'N' as default answer + # + def confirm(question, append_instructions=true, default_choice=nil) + unless confirm_without_exit(question, append_instructions, default_choice) + exit 3 end + true end end diff --git a/lib/chef/knife/node_run_list_add.rb b/lib/chef/knife/node_run_list_add.rb index dcd41ae997..519c280400 100644 --- a/lib/chef/knife/node_run_list_add.rb +++ b/lib/chef/knife/node_run_list_add.rb @@ -34,6 +34,11 @@ class Chef :long => "--after ITEM", :description => "Place the ENTRY in the run list after ITEM" + option :before, + :short => "-b ITEM", + :long => "--before ITEM", + :description => "Place the ENTRY in the run list before ITEM" + def run node = Chef::Node.load(@name_args[0]) if @name_args.size > 2 @@ -46,7 +51,18 @@ class Chef entries = @name_args[1].split(',').map { |e| e.strip } end - add_to_run_list(node, entries, config[:after]) + if config[:after] && config[:before] + ui.fatal("You cannot specify both --before and --after!") + exit 1 + end + + if config[:after] + add_to_run_list_after(node, entries, config[:after]) + elsif config[:before] + add_to_run_list_before(node, entries, config[:before]) + else + add_to_run_list_after(node, entries) + end node.save @@ -55,7 +71,9 @@ class Chef output(format_for_display(node)) end - def add_to_run_list(node, entries, after=nil) + private + + def add_to_run_list_after(node, entries, after=nil) if after nlist = [] node.run_list.each do |entry| @@ -70,6 +88,17 @@ class Chef end end + def add_to_run_list_before(node, entries, before) + nlist = [] + node.run_list.each do |entry| + if entry == before + entries.each { |e| nlist << e } + end + nlist << entry + end + node.run_list.reset!(nlist) + end + end end end diff --git a/lib/chef/knife/raw.rb b/lib/chef/knife/raw.rb index 2756de1a5a..954d46beee 100644 --- a/lib/chef/knife/raw.rb +++ b/lib/chef/knife/raw.rb @@ -42,6 +42,7 @@ class Chef use Chef::HTTP::CookieManager use Chef::HTTP::Decompressor use Chef::HTTP::Authenticator + use Chef::HTTP::RemoteRequestID end def run diff --git a/lib/chef/knife/ssh.rb b/lib/chef/knife/ssh.rb index 83c1735b4a..d32b3309ed 100644 --- a/lib/chef/knife/ssh.rb +++ b/lib/chef/knife/ssh.rb @@ -114,7 +114,7 @@ class Chef end case config[:on_error] when :skip - ui.warn "Failed to connect to #{node_name} -- #{$!.class.name}: #{$!.message}" + ui.warn "Failed to connect to #{server.host} -- #{$!.class.name}: #{$!.message}" $!.backtrace.each { |l| Chef::Log.debug(l) } when :raise #Net::SSH::Multi magic to force exception to be re-raised. @@ -142,31 +142,9 @@ class Chef end def configure_session - list = case config[:manual] - when true - @name_args[0].split(" ") - when false - r = Array.new - q = Chef::Search::Query.new - @action_nodes = q.search(:node, @name_args[0])[0] - @action_nodes.each do |item| - # we should skip the loop to next iteration if the item returned by the search is nil - next if item.nil? - # if a command line attribute was not passed, and we have a cloud public_hostname, use that. - # see #configure_attribute for the source of config[:attribute] and config[:override_attribute] - if !config[:override_attribute] && item[:cloud] and item[:cloud][:public_hostname] - i = item[:cloud][:public_hostname] - elsif config[:override_attribute] - i = extract_nested_value(item, config[:override_attribute]) - else - i = extract_nested_value(item, config[:attribute]) - end - # next if we couldn't find the specified attribute in the returned node object - next if i.nil? - r.push(i) - end - r - end + list = config[:manual] ? + @name_args[0].split(" ") : + search_nodes if list.length == 0 if @action_nodes.length == 0 ui.fatal("No nodes returned from search!") @@ -180,21 +158,54 @@ class Chef session_from_list(list) end + def search_nodes + list = Array.new + query = Chef::Search::Query.new + @action_nodes = query.search(:node, @name_args[0])[0] + @action_nodes.each do |item| + # we should skip the loop to next iteration if the item + # returned by the search is nil + next if item.nil? + # if a command line attribute was not passed, and we have a + # cloud public_hostname, use that. see #configure_attribute + # for the source of config[:attribute] and + # config[:override_attribute] + if config[:override_attribute] + host = extract_nested_value(item, config[:override_attribute]) + elsif item[:cloud] && item[:cloud][:public_hostname] + host = item[:cloud][:public_hostname] + else + host = extract_nested_value(item, config[:attribute]) + end + # next if we couldn't find the specified attribute in the + # returned node object + next if host.nil? + ssh_port = item[:cloud].nil? ? nil : item[:cloud][:public_ssh_port] + srv = [host, ssh_port] + list.push(srv) + end + list + end + def session_from_list(list) list.each do |item| - Chef::Log.debug("Adding #{item}") + host, ssh_port = item + Chef::Log.debug("Adding #{host}") session_opts = {} - ssh_config = Net::SSH.configuration_for(item) + ssh_config = Net::SSH.configuration_for(host) # Chef::Config[:knife][:ssh_user] is parsed in #configure_user and written to config[:ssh_user] user = config[:ssh_user] || ssh_config[:user] - hostspec = user ? "#{user}@#{item}" : item + hostspec = user ? "#{user}@#{host}" : host session_opts[:keys] = File.expand_path(config[:identity_file]) if config[:identity_file] session_opts[:keys_only] = true if config[:identity_file] session_opts[:password] = config[:ssh_password] if config[:ssh_password] session_opts[:forward_agent] = config[:forward_agent] - session_opts[:port] = config[:ssh_port] || Chef::Config[:knife][:ssh_port] || ssh_config[:port] + session_opts[:port] = config[:ssh_port] || + ssh_port || # Use cloud port if available + Chef::Config[:knife][:ssh_port] || + ssh_config[:port] session_opts[:logger] = Chef::Log.logger if Chef::Log.level == :debug if !config[:host_key_verify] @@ -204,7 +215,7 @@ class Chef session.use(hostspec, session_opts) - @longest = item.length if item.length > @longest + @longest = host.length if host.length > @longest end session @@ -510,6 +521,8 @@ class Chef end end + private :search_nodes + end end end diff --git a/lib/chef/knife/ssl_check.rb b/lib/chef/knife/ssl_check.rb new file mode 100644 index 0000000000..e98469d5aa --- /dev/null +++ b/lib/chef/knife/ssl_check.rb @@ -0,0 +1,213 @@ +# +# Author:: Daniel DeLeo (<dan@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/knife' +require 'chef/config' + +class Chef + class Knife + class SslCheck < Chef::Knife + + deps do + require 'pp' + require 'socket' + require 'uri' + require 'chef/http/ssl_policies' + require 'openssl' + end + + banner "knife ssl check [URL] (options)" + + def initialize(*args) + @host = nil + @verify_peer_socket = nil + @ssl_policy = HTTP::DefaultSSLPolicy + super + end + + def uri + @uri ||= begin + Chef::Log.debug("Checking SSL cert on #{given_uri}") + URI.parse(given_uri) + end + end + + def given_uri + (name_args[0] or Chef::Config.chef_server_url) + end + + def host + uri.host + end + + def port + uri.port + end + + def validate_uri + unless host && port + invalid_uri! + end + rescue URI::Error + invalid_uri! + end + + def invalid_uri! + ui.error("Given URI: `#{given_uri}' is invalid") + show_usage + exit 1 + end + + + def verify_peer_socket + @verify_peer_socket ||= begin + tcp_connection = TCPSocket.new(host, port) + OpenSSL::SSL::SSLSocket.new(tcp_connection, verify_peer_ssl_context) + end + end + + def verify_peer_ssl_context + @verify_peer_ssl_context ||= begin + verify_peer_context = OpenSSL::SSL::SSLContext.new + @ssl_policy.apply_to(verify_peer_context) + verify_peer_context.verify_mode = OpenSSL::SSL::VERIFY_PEER + verify_peer_context + end + end + + def noverify_socket + @noverify_socket ||= begin + tcp_connection = TCPSocket.new(host, port) + OpenSSL::SSL::SSLSocket.new(tcp_connection, noverify_peer_ssl_context) + end + end + + def noverify_peer_ssl_context + @noverify_peer_ssl_context ||= begin + noverify_peer_context = OpenSSL::SSL::SSLContext.new + @ssl_policy.apply_to(noverify_peer_context) + noverify_peer_context.verify_mode = OpenSSL::SSL::VERIFY_NONE + noverify_peer_context + end + end + + def verify_cert + ui.msg("Connecting to host #{host}:#{port}") + verify_peer_socket.connect + true + rescue OpenSSL::SSL::SSLError => e + ui.error "The SSL certificate of #{host} could not be verified" + Chef::Log.debug e.message + debug_invalid_cert + false + end + + def verify_cert_host + verify_peer_socket.post_connection_check(host) + true + rescue OpenSSL::SSL::SSLError => e + ui.error "The SSL cert is signed by a trusted authority but is not valid for the given hostname" + Chef::Log.debug(e) + debug_invalid_host + false + end + + def debug_invalid_cert + noverify_socket.connect + issuer_info = noverify_socket.peer_cert.issuer + ui.msg("Certificate issuer data: #{issuer_info}") + + ui.msg("\n#{ui.color("Configuration Info:", :bold)}\n\n") + debug_ssl_settings + debug_chef_ssl_config + + ui.err(<<-ADVICE) + +#{ui.color("TO FIX THIS ERROR:", :bold)} + +If the server you are connecting to uses a self-signed certificate, you must +configure chef to trust that server's certificate. + +By default, the certificate is stored in the following location on the host +where your chef-server runs: + + /var/opt/chef-server/nginx/ca/SERVER_HOSTNAME.crt + +Copy that file to you trusted_certs_dir (currently: #{configuration.trusted_certs_dir}) +using SSH/SCP or some other secure method, then re-run this command to confirm +that the server's certificate is now trusted. + +ADVICE + end + + def debug_invalid_host + noverify_socket.connect + subject = noverify_socket.peer_cert.subject + cn_field_tuple = subject.to_a.find {|field| field[0] == "CN" } + cn = cn_field_tuple[1] + + ui.error("You are attempting to connect to: '#{host}'") + ui.error("The server's certificate belongs to '#{cn}'") + ui.err(<<-ADVICE) + +#{ui.color("TO FIX THIS ERROR:", :bold)} + +The solution for this issue depends on your networking configuration. If you +are able to connect to this server using the hostname #{cn} +instead of #{host}, then you can resolve this issue by updating chef_server_url +in your configuration file. + +If you are not able to connect to the server using the hostname #{cn} +you will have to update the certificate on the server to use the correct hostname. +ADVICE + end + + def debug_ssl_settings + ui.err "OpenSSL Configuration:" + ui.err "* Version: #{OpenSSL::OPENSSL_VERSION}" + ui.err "* Certificate file: #{OpenSSL::X509::DEFAULT_CERT_FILE}" + ui.err "* Certificate directory: #{OpenSSL::X509::DEFAULT_CERT_DIR}" + end + + def debug_chef_ssl_config + ui.err "Chef SSL Configuration:" + ui.err "* ssl_ca_path: #{configuration.ssl_ca_path.inspect}" + ui.err "* ssl_ca_file: #{configuration.ssl_ca_file.inspect}" + ui.err "* trusted_certs_dir: #{configuration.trusted_certs_dir.inspect}" + end + + def configuration + Chef::Config + end + + def run + validate_uri + if verify_cert && verify_cert_host + ui.msg "Successfully verified certificates from `#{host}'" + else + exit 1 + end + end + + end + end +end + + + + diff --git a/lib/chef/knife/ssl_fetch.rb b/lib/chef/knife/ssl_fetch.rb new file mode 100644 index 0000000000..5626a5610d --- /dev/null +++ b/lib/chef/knife/ssl_fetch.rb @@ -0,0 +1,145 @@ +# +# Author:: Daniel DeLeo (<dan@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/knife/ssl_fetch' +require 'chef/config' + +class Chef + class Knife + class SslFetch < Chef::Knife + + deps do + require 'pp' + require 'socket' + require 'uri' + require 'openssl' + end + + banner "knife ssl fetch [URL] (options)" + + def initialize(*args) + super + @uri = nil + end + + def uri + @uri ||= begin + Chef::Log.debug("Checking SSL cert on #{given_uri}") + URI.parse(given_uri) + end + end + + def given_uri + (name_args[0] or Chef::Config.chef_server_url) + end + + def host + uri.host + end + + def port + uri.port + end + + def validate_uri + unless host && port + invalid_uri! + end + rescue URI::Error + invalid_uri! + end + + def invalid_uri! + ui.error("Given URI: `#{given_uri}' is invalid") + show_usage + exit 1 + end + + def remote_cert_chain + tcp_connection = TCPSocket.new(host, port) + shady_ssl_connection = OpenSSL::SSL::SSLSocket.new(tcp_connection, noverify_peer_ssl_context) + shady_ssl_connection.connect + shady_ssl_connection.peer_cert_chain + end + + def noverify_peer_ssl_context + @noverify_peer_ssl_context ||= begin + noverify_peer_context = OpenSSL::SSL::SSLContext.new + noverify_peer_context.verify_mode = OpenSSL::SSL::VERIFY_NONE + noverify_peer_context + end + end + + + def cn_of(certificate) + subject = certificate.subject + cn_field_tuple = subject.to_a.find {|field| field[0] == "CN" } + cn_field_tuple[1] + end + + # Convert the CN of a certificate into something that will work well as a + # filename. To do so, all `*` characters are converted to the string + # "wildcard" and then all characters other than alphanumeric and hypen + # characters are converted to underscores. + # NOTE: There is some confustion about what the CN will contain when + # using internationalized domain names. RFC 6125 mandates that the ascii + # representation be used, but it is not clear whether this is followed in + # practice. + # https://tools.ietf.org/html/rfc6125#section-6.4.2 + def normalize_cn(cn) + cn.gsub("*", "wildcard").gsub(/[^[:alnum:]\-]/, '_') + end + + def configuration + Chef::Config + end + + def trusted_certs_dir + configuration.trusted_certs_dir + end + + def write_cert(cert) + FileUtils.mkdir_p(trusted_certs_dir) + cn = cn_of(cert) + filename = File.join(trusted_certs_dir, "#{normalize_cn(cn)}.crt") + ui.msg("Adding certificate for #{cn} in #{filename}") + File.open(filename, File::CREAT|File::TRUNC|File::RDWR, 0644) do |f| + f.print(cert.to_s) + end + end + + def run + validate_uri + ui.warn(<<-TRUST_TRUST) +Certificates from #{host} will be fetched and placed in your trusted_cert +directory (#{trusted_certs_dir}). + +Knife has no means to verify these are the correct certificates. You should +verify the authenticity of these certificates after downloading. + +TRUST_TRUST + remote_cert_chain.each do |cert| + write_cert(cert) + end + end + + + end + end +end + diff --git a/lib/chef/mixin/deep_merge.rb b/lib/chef/mixin/deep_merge.rb index ad3e5803fd..a8a4737758 100644 --- a/lib/chef/mixin/deep_merge.rb +++ b/lib/chef/mixin/deep_merge.rb @@ -111,7 +111,13 @@ class Chef end # deep_merge! def hash_only_merge(merge_onto, merge_with) - hash_only_merge!(merge_onto.dup, merge_with.dup) + hash_only_merge!(safe_dup(merge_onto), safe_dup(merge_with)) + end + + def safe_dup(thing) + thing.dup + rescue TypeError + thing end # Deep merge without Array merge. @@ -122,7 +128,11 @@ class Chef # If there are two Hashes, recursively merge. if merge_onto.kind_of?(Hash) && merge_with.kind_of?(Hash) merge_with.each do |key, merge_with_value| - merge_onto[key] = hash_only_merge!(merge_onto[key], merge_with_value) + merge_onto[key] = if merge_onto.has_key?(key) + hash_only_merge(merge_onto[key], merge_with_value) + else + merge_with_value + end end merge_onto @@ -158,11 +168,9 @@ class Chef end def deep_merge(source, dest) - deep_merge!(source.dup, dest.dup) + deep_merge!(safe_dup(source), safe_dup(dest)) end end end end - - diff --git a/lib/chef/mixin/shell_out.rb b/lib/chef/mixin/shell_out.rb index f0c2ba2000..56b02d780f 100644 --- a/lib/chef/mixin/shell_out.rb +++ b/lib/chef/mixin/shell_out.rb @@ -33,9 +33,7 @@ class Chef def shell_out(*command_args) cmd = Mixlib::ShellOut.new(*run_command_compatible_options(command_args)) - if STDOUT.tty? && !Chef::Config[:daemon] && Chef::Log.debug? - cmd.live_stream = STDOUT - end + cmd.live_stream = io_for_live_stream cmd.run_command cmd end @@ -73,6 +71,14 @@ class Chef def deprecate_option(old_option, new_option) Chef::Log.logger.warn "DEPRECATION: Chef::Mixin::ShellOut option :#{old_option} is deprecated. Use :#{new_option}" end + + def io_for_live_stream + if STDOUT.tty? && !Chef::Config[:daemon] && Chef::Log.debug? + STDOUT + else + nil + end + end end end end diff --git a/lib/chef/node.rb b/lib/chef/node.rb index 69e5e05b01..4992ec2430 100644 --- a/lib/chef/node.rb +++ b/lib/chef/node.rb @@ -42,7 +42,7 @@ class Chef def_delegators :attributes, :keys, :each_key, :each_value, :key?, :has_key? - attr_accessor :recipe_list, :run_state, :run_list + attr_accessor :recipe_list, :run_state, :override_runlist # RunContext will set itself as run_context via this setter when # initialized. This is needed so DSL::IncludeAttribute (in particular, @@ -63,7 +63,8 @@ class Chef @name = nil @chef_environment = '_default' - @run_list = Chef::RunList.new + @primary_runlist = Chef::RunList.new + @override_runlist = Chef::RunList.new @attributes = Chef::Node::Attribute.new({}, {}, {}, {}) @@ -259,10 +260,28 @@ class Chef run_list.include?("role[#{role_name}]") end + def primary_runlist + @primary_runlist + end + + def override_runlist(*args) + args.length > 0 ? @override_runlist.reset!(args) : @override_runlist + end + + def select_run_list + @override_runlist.empty? ? @primary_runlist : @override_runlist + end + # Returns an Array of roles and recipes, in the order they will be applied. # If you call it with arguments, they will become the new list of roles and recipes. def run_list(*args) - args.length > 0 ? @run_list.reset!(args) : @run_list + rl = select_run_list + args.length > 0 ? rl.reset!(args) : rl + end + + def run_list=(list) + rl = select_run_list + rl = list end # Returns true if this Node expects a given role, false if not. @@ -312,7 +331,7 @@ class Chef if attrs.key?("recipes") || attrs.key?("run_list") raise Chef::Exceptions::AmbiguousRunlistSpecification, "please set the node's run list using the 'run_list' attribute only." end - Chef::Log.info("Setting the run_list to #{new_run_list.inspect} from JSON") + Chef::Log.info("Setting the run_list to #{new_run_list.inspect} from CLI options") run_list(new_run_list) end attrs @@ -410,7 +429,7 @@ class Chef "default" => attributes.combined_default, "override" => attributes.combined_override, #Render correctly for run_list items so malformed json does not result - "run_list" => run_list.run_list.map { |item| item.to_s } + "run_list" => @primary_runlist.run_list.map { |item| item.to_s } } result end diff --git a/lib/chef/node/attribute_collections.rb b/lib/chef/node/attribute_collections.rb index d5d496fd60..f09b02b106 100644 --- a/lib/chef/node/attribute_collections.rb +++ b/lib/chef/node/attribute_collections.rb @@ -76,8 +76,15 @@ class Chef super(data) end + # For elements like Fixnums, true, nil... + def safe_dup(e) + e.dup + rescue TypeError + e + end + def dup - Array.new(map {|e| e.dup}) + Array.new(map {|e| safe_dup(e)}) end end diff --git a/lib/chef/node/immutable_collections.rb b/lib/chef/node/immutable_collections.rb index f5b3a5121d..3558ba3a86 100644 --- a/lib/chef/node/immutable_collections.rb +++ b/lib/chef/node/immutable_collections.rb @@ -85,8 +85,31 @@ class Chef METHOD_DEFN end + # For elements like Fixnums, true, nil... + def safe_dup(e) + e.dup + rescue TypeError + e + end + def dup - Array.new(map {|e| e.dup }) + Array.new(map {|e| safe_dup(e)}) + end + + def to_a + a = Array.new + each do |v| + a << + case v + when ImmutableArray + v.to_a + when ImmutableMash + v.to_hash + else + v + end + end + a end end @@ -180,6 +203,22 @@ class Chef Mash.new(self) end + def to_hash + h = Hash.new + each_pair do |k, v| + h[k] = + case v + when ImmutableMash + v.to_hash + when ImmutableArray + v.to_a + else + v + end + end + h + end + end end diff --git a/lib/chef/platform/provider_mapping.rb b/lib/chef/platform/provider_mapping.rb index 92a7278d2f..a773da550e 100644 --- a/lib/chef/platform/provider_mapping.rb +++ b/lib/chef/platform/provider_mapping.rb @@ -180,6 +180,7 @@ class Chef :package => Chef::Provider::Package::Zypper, :group => Chef::Provider::Group::Suse }, + # Only OpenSuSE 12.3+ should use the Usermod group provider: ">= 12.3" => { :group => Chef::Provider::Group::Usermod } @@ -190,19 +191,6 @@ class Chef :cron => Chef::Provider::Cron, :package => Chef::Provider::Package::Zypper, :group => Chef::Provider::Group::Suse - }, - ############################################### - # TODO: Remove this after ohai update is released. - # Only OpenSuSE 12.3+ should use the Usermod group provider: - # Ohai before OHAI-339 is applied reports both OpenSuSE and SuSE - # Enterprise as "suse", Ohai after OHAI-339 will report OpenSuSE as - # "opensuse". - # - # In order to support OpenSuSE both before and after the Ohai - # change, I'm leaving this here. It needs to get removed before - # SuSE enterprise 12.3 ships. - ">= 12.3" => { - :group => Chef::Provider::Group::Usermod } }, :oracle => { @@ -222,6 +210,15 @@ class Chef :ifconfig => Chef::Provider::Ifconfig::Redhat } }, + :ibm_powerkvm => { + :default => { + :service => Chef::Provider::Service::Redhat, + :cron => Chef::Provider::Cron, + :package => Chef::Provider::Package::Yum, + :mdadm => Chef::Provider::Mdadm, + :ifconfig => Chef::Provider::Ifconfig::Redhat + } + }, :gentoo => { :default => { :package => Chef::Provider::Package::Portage, @@ -233,7 +230,7 @@ class Chef :arch => { :default => { :package => Chef::Provider::Package::Pacman, - :service => Chef::Provider::Service::Arch, + :service => Chef::Provider::Service::Systemd, :cron => Chef::Provider::Cron, :mdadm => Chef::Provider::Mdadm } @@ -244,7 +241,9 @@ class Chef :service => Chef::Provider::Service::Windows, :user => Chef::Provider::User::Windows, :group => Chef::Provider::Group::Windows, - :mount => Chef::Provider::Mount::Windows + :mount => Chef::Provider::Mount::Windows, + :batch => Chef::Provider::Batch, + :powershell_script => Chef::Provider::PowershellScript } }, :mingw32 => { @@ -253,7 +252,9 @@ class Chef :service => Chef::Provider::Service::Windows, :user => Chef::Provider::User::Windows, :group => Chef::Provider::Group::Windows, - :mount => Chef::Provider::Mount::Windows + :mount => Chef::Provider::Mount::Windows, + :batch => Chef::Provider::Batch, + :powershell_script => Chef::Provider::PowershellScript } }, :windows => { @@ -262,7 +263,9 @@ class Chef :service => Chef::Provider::Service::Windows, :user => Chef::Provider::User::Windows, :group => Chef::Provider::Group::Windows, - :mount => Chef::Provider::Mount::Windows + :mount => Chef::Provider::Mount::Windows, + :batch => Chef::Provider::Batch, + :powershell_script => Chef::Provider::PowershellScript } }, :solaris => {}, @@ -307,7 +310,7 @@ class Chef :group => Chef::Provider::Group::Usermod, :user => Chef::Provider::User::Solaris, }, - ">= 5.9" => { + "< 5.11" => { :service => Chef::Provider::Service::Solaris, :package => Chef::Provider::Package::Solaris, :cron => Chef::Provider::Cron::Solaris, diff --git a/lib/chef/platform/query_helpers.rb b/lib/chef/platform/query_helpers.rb index 028a220a5d..f9f7af0343 100644 --- a/lib/chef/platform/query_helpers.rb +++ b/lib/chef/platform/query_helpers.rb @@ -30,11 +30,19 @@ class Chef def windows_server_2003? return false unless windows? - require 'ruby-wmi' + # CHEF-4888: Work around ruby #2618, expected to be fixed in Ruby 2.1.0 + # https://github.com/ruby/ruby/commit/588504b20f5cc880ad51827b93e571e32446e5db + # https://github.com/ruby/ruby/commit/27ed294c7134c0de582007af3c915a635a6506cd + WIN32OLE.ole_initialize + host = WMI::Win32_OperatingSystem.find(:first) - (host.version && host.version.start_with?("5.2")) + is_server_2003 = (host.version && host.version.start_with?("5.2")) + + WIN32OLE.ole_uninitialize + + is_server_2003 end end diff --git a/lib/chef/policy_builder/expand_node_object.rb b/lib/chef/policy_builder/expand_node_object.rb index 38b8b7551b..269e722797 100644 --- a/lib/chef/policy_builder/expand_node_object.rb +++ b/lib/chef/policy_builder/expand_node_object.rb @@ -40,7 +40,6 @@ class Chef attr_reader :ohai_data attr_reader :json_attribs attr_reader :override_runlist - attr_reader :original_runlist attr_reader :run_context attr_reader :run_list_expansion @@ -52,7 +51,6 @@ class Chef @events = events @node = nil - @original_runlist = nil @run_list_expansion = nil end @@ -190,7 +188,7 @@ class Chef # override_runlist was provided. Chef::Client uses this to decide whether # to do the final node save at the end of the run or not. def temporary_policy? - !!@original_runlist + !node.override_runlist.empty? end ######################################## @@ -200,10 +198,9 @@ class Chef def setup_run_list_override runlist_override_sanity_check! unless(override_runlist.empty?) - @original_runlist = node.run_list.run_list_items.dup - node.run_list(*override_runlist) + node.override_runlist(*override_runlist) Chef::Log.warn "Run List override has been provided." - Chef::Log.warn "Original Run List: [#{original_runlist.join(', ')}]" + Chef::Log.warn "Original Run List: [#{node.primary_runlist}]" Chef::Log.warn "Overridden Run List: [#{node.run_list}]" end end diff --git a/lib/chef/provider/cron.rb b/lib/chef/provider/cron.rb index 87452b4872..1be15f9f5f 100644 --- a/lib/chef/provider/cron.rb +++ b/lib/chef/provider/cron.rb @@ -25,11 +25,14 @@ class Chef class Cron < Chef::Provider include Chef::Mixin::Command + SPECIAL_TIME_VALUES = [:reboot, :yearly, :annually, :monthly, :weekly, :daily, :midnight, :hourly] + CRON_ATTRIBUTES = [:minute, :hour, :day, :month, :weekday, :time, :command, :mailto, :path, :shell, :home, :environment] + WEEKDAY_SYMBOLS = [:sunday, :monday, :tuesday, :wednesday, :thursday, :friday, :saturday] + CRON_PATTERN = /\A([-0-9*,\/]+)\s([-0-9*,\/]+)\s([-0-9*,\/]+)\s([-0-9*,\/]+|[a-zA-Z]{3})\s([-0-9*,\/]+|[a-zA-Z]{3})\s(.*)/ + SPECIAL_PATTERN = /\A(@(#{SPECIAL_TIME_VALUES.join('|')}))\s(.*)/ ENV_PATTERN = /\A(\S+)=(\S*)/ - CRON_ATTRIBUTES = [:minute, :hour, :day, :month, :weekday, :command, :mailto, :path, :shell, :home, :environment] - def initialize(new_resource, run_context) super(new_resource, run_context) @cron_exists = false @@ -58,6 +61,12 @@ class Chef when ENV_PATTERN set_environment_var($1, $2) if cron_found next + when SPECIAL_PATTERN + if cron_found + @current_resource.time($2.to_sym) + @current_resource.command($3) + cron_found=false + end when CRON_PATTERN if cron_found @current_resource.minute($1) @@ -220,9 +229,22 @@ class Chef @new_resource.environment.each do |name, value| newcron << "#{name}=#{value}\n" end - newcron << "#{@new_resource.minute} #{@new_resource.hour} #{@new_resource.day} #{@new_resource.month} #{@new_resource.weekday} #{@new_resource.command}\n" + if @new_resource.time + newcron << "@#{@new_resource.time} #{@new_resource.command}\n" + else + newcron << "#{@new_resource.minute} #{@new_resource.hour} #{@new_resource.day} #{@new_resource.month} #{@new_resource.weekday} #{@new_resource.command}\n" + end newcron end + + def weekday_in_crontab + weekday_in_crontab = WEEKDAY_SYMBOLS.index(@new_resource.weekday) + if weekday_in_crontab.nil? + @new_resource.weekday + else + weekday_in_crontab.to_s + end + end end end end diff --git a/lib/chef/provider/deploy.rb b/lib/chef/provider/deploy.rb index d1017dba62..516aee6159 100644 --- a/lib/chef/provider/deploy.rb +++ b/lib/chef/provider/deploy.rb @@ -266,7 +266,7 @@ class Chef def copy_cached_repo target_dir_path = @new_resource.deploy_to + "/releases" - converge_by("deploy from repo to #{@target_dir_path} ") do + converge_by("deploy from repo to #{target_dir_path} ") do FileUtils.rm_rf(release_path) if ::File.exist?(release_path) FileUtils.mkdir_p(target_dir_path) FileUtils.cp_r(::File.join(@new_resource.destination, "."), release_path, :preserve => true) diff --git a/lib/chef/provider/group.rb b/lib/chef/provider/group.rb index f01677b3ac..35a16c870c 100644 --- a/lib/chef/provider/group.rb +++ b/lib/chef/provider/group.rb @@ -84,7 +84,7 @@ class Chef # <false>:: If a change is not required def compare_group @change_desc = [ ] - if @new_resource.gid != @current_resource.gid + if @new_resource.gid.to_s != @current_resource.gid.to_s @change_desc << "change gid #{@current_resource.gid} to #{@new_resource.gid}" end diff --git a/lib/chef/provider/ifconfig/debian.rb b/lib/chef/provider/ifconfig/debian.rb index 821f4fe924..7589971143 100644 --- a/lib/chef/provider/ifconfig/debian.rb +++ b/lib/chef/provider/ifconfig/debian.rb @@ -24,6 +24,9 @@ class Chef class Ifconfig class Debian < Chef::Provider::Ifconfig + INTERFACES_FILE = "/etc/network/interfaces" + INTERFACES_DOT_D_DIR = "/etc/network/interfaces.d" + def initialize(new_resource, run_context) super(new_resource, run_context) @config_template = %{ @@ -46,22 +49,30 @@ iface <%= @new_resource.device %> inet static <% end %> <% end %> } - @config_path = "/etc/network/interfaces.d/ifcfg-#{@new_resource.device}" + @config_path = "#{INTERFACES_DOT_D_DIR}/ifcfg-#{@new_resource.device}" end def generate_config - check_interfaces_config + enforce_interfaces_dot_d_sanity super end protected - def check_interfaces_config - converge_by ('modify configuration file : /etc/network/interfaces') do - Dir.mkdir('/etc/network/interfaces.d') unless ::File.directory?('/etc/network/interfaces.d') - conf = Chef::Util::FileEdit.new('/etc/network/interfaces') - conf.insert_line_if_no_match('^\s*source\s+/etc/network/interfaces[.]d/[*]\s*$', 'source /etc/network/interfaces.d/*') - conf.write_file + def enforce_interfaces_dot_d_sanity + # create /etc/network/interfaces.d via dir resource (to get reporting, etc) + dir = Chef::Resource::Directory.new(INTERFACES_DOT_D_DIR, run_context) + dir.run_action(:create) + new_resource.updated_by_last_action(true) if dir.updated_by_last_action? + # roll our own file_edit resource, this will not get reported until we have a file_edit resource + interfaces_dot_d_for_regexp = INTERFACES_DOT_D_DIR.gsub(/\./, '\.') # escape dots for the regexp + regexp = %r{^\s*source\s+#{interfaces_dot_d_for_regexp}/\*\s*$} + unless ::File.exists?(INTERFACES_FILE) && regexp.match(IO.read(INTERFACES_FILE)) + converge_by("modifying #{INTERFACES_FILE} to source #{INTERFACES_DOT_D_DIR}") do + conf = Chef::Util::FileEdit.new(INTERFACES_FILE) + conf.insert_line_if_no_match(regexp, "source #{INTERFACES_DOT_D_DIR}/*") + conf.write_file + end end end diff --git a/lib/chef/provider/mount/mount.rb b/lib/chef/provider/mount/mount.rb index 25dfd42725..22d61a9236 100644 --- a/lib/chef/provider/mount/mount.rb +++ b/lib/chef/provider/mount/mount.rb @@ -244,7 +244,7 @@ class Chef # So given a symlink like this: # /dev/mapper/vgroot-tmp.vol -> /dev/dm-9 # First it will try to match "/dev/mapper/vgroot-tmp.vol". If there is no match it will try matching for "/dev/dm-9". - "(?:#{Regexp.escape(device_real)}|#{Regexp.escape(::File.readlink(device_real))})" + "(?:#{Regexp.escape(device_real)}|#{Regexp.escape(::File.expand_path(::File.readlink(device_real),::File.dirname(device_real)))})" else Regexp.escape(device_real) end diff --git a/lib/chef/provider/ohai.rb b/lib/chef/provider/ohai.rb index c686f67450..a6b5ab5daa 100644 --- a/lib/chef/provider/ohai.rb +++ b/lib/chef/provider/ohai.rb @@ -33,11 +33,12 @@ class Chef def action_reload converge_by("re-run ohai and merge results into node attributes") do ohai = ::Ohai::System.new - if @new_resource.plugin - ohai.require_plugin @new_resource.plugin - else - ohai.all_plugins - end + + # If @new_resource.plugin is nil, ohai will reload all the plugins + # Otherwise it will only reload the specified plugin + # Note that any changes to plugins, or new plugins placed on + # the path are picked up by ohai. + ohai.all_plugins @new_resource.plugin node.automatic_attrs.merge! ohai.data Chef::Log.info("#{@new_resource} reloaded") end diff --git a/lib/chef/provider/package/dpkg.rb b/lib/chef/provider/package/dpkg.rb index 8ec1ad5878..fb366fb6eb 100644 --- a/lib/chef/provider/package/dpkg.rb +++ b/lib/chef/provider/package/dpkg.rb @@ -25,7 +25,8 @@ class Chef class Provider class Package class Dpkg < Chef::Provider::Package::Apt - DPKG_INFO = /([a-z\d\-\+\.]+)\t([\w\d.~-]+)/ + # http://www.debian.org/doc/debian-policy/ch-controlfields.html#s-f-Version + DPKG_INFO = /([a-z\d\-\+\.]+)\t([\w\d.~:-]+)/ DPKG_INSTALLED = /^Status: install ok installed/ DPKG_VERSION = /^Version: (.+)$/ diff --git a/lib/chef/provider/package/windows.rb b/lib/chef/provider/package/windows.rb new file mode 100644 index 0000000000..be1de0b969 --- /dev/null +++ b/lib/chef/provider/package/windows.rb @@ -0,0 +1,80 @@ +# +# Author:: Bryan McLellan <btm@loftninjas.org> +# 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/windows_package' +require 'chef/provider/package' + +class Chef + class Provider + class Package + class Windows < Chef::Provider::Package + + # Depending on the installer, we may need to examine installer_type or + # source attributes, or search for text strings in the installer file + # binary to determine the installer type for the user. Since the file + # must be on disk to do so, we have to make this choice in the provider. + require 'chef/provider/package/windows/msi.rb' + + # load_current_resource is run in Chef::Provider#run_action when not in whyrun_mode? + def load_current_resource + @current_resource = Chef::Resource::WindowsPackage.new(@new_resource.name) + @current_resource.version(package_provider.installed_version) + @new_resource.version(package_provider.package_version) + @current_resource + end + + def package_provider + @package_provider ||= begin + case installer_type + when :msi + Chef::Provider::Package::Windows::MSI.new(@new_resource) + else + raise "Unable to find a Chef::Provider::Package::Windows provider for installer_type '#{installer_type}'" + end + end + end + + def installer_type + @installer_type ||= begin + if @new_resource.installer_type + @new_resource.installer_type + else + file_extension = ::File.basename(@new_resource.source).split(".").last.downcase + + if file_extension == "msi" + :msi + else + raise ArgumentError, "Installer type for Windows Package '#{@new_resource.name}' not specified and cannot be determined from file extension '#{file_extension}'" + end + end + end + end + + # Chef::Provider::Package action_install + action_remove call install_package + remove_package + # Pass those calls to the correct sub-provider + def install_package(name, version) + package_provider.install_package(name, version) + end + + def remove_package(name, version) + package_provider.remove_package(name, version) + end + end + end + end +end diff --git a/lib/chef/provider/package/windows/msi.rb b/lib/chef/provider/package/windows/msi.rb new file mode 100644 index 0000000000..a342600678 --- /dev/null +++ b/lib/chef/provider/package/windows/msi.rb @@ -0,0 +1,69 @@ +# +# Author:: Bryan McLellan <btm@loftninjas.org> +# 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. +# + +# TODO: Allow @new_resource.source to be a Product Code as a GUID for uninstall / network install + +require 'chef/mixin/shell_out' +require 'chef/win32/api/installer' if RUBY_PLATFORM =~ /mswin|mingw32|windows/ + +class Chef + class Provider + class Package + class Windows + class MSI + include Chef::ReservedNames::Win32::API::Installer if RUBY_PLATFORM =~ /mswin|mingw32|windows/ + include Chef::Mixin::ShellOut + + def initialize(resource) + @new_resource = resource + end + + # From Chef::Provider::Package + def expand_options(options) + options ? " #{options}" : "" + end + + # Returns a version if the package is installed or nil if it is not. + def installed_version + Chef::Log.debug("#{@new_resource} getting product code for package at #{@new_resource.source}") + product_code = get_product_property(@new_resource.source, "ProductCode") + Chef::Log.debug("#{@new_resource} checking package status and verion for #{product_code}") + get_installed_version(product_code) + end + + def package_version + Chef::Log.debug("#{@new_resource} getting product version for package at #{@new_resource.source}") + get_product_property(@new_resource.source, "ProductVersion") + end + + def install_package(name, version) + # We could use MsiConfigureProduct here, but we'll start off with msiexec + Chef::Log.debug("#{@new_resource} installing MSI package '#{@new_resource.source}'") + shell_out!("msiexec /qn /i \"#{@new_resource.source}\" #{expand_options(@new_resource.options)}", {:timeout => @new_resource.timeout, :returns => @new_resource.returns}) + end + + def remove_package(name, version) + # We could use MsiConfigureProduct here, but we'll start off with msiexec + Chef::Log.debug("#{@new_resource} removing MSI package '#{@new_resource.source}'") + shell_out!("msiexec /qn /x \"#{@new_resource.source}\" #{expand_options(@new_resource.options)}", {:timeout => @new_resource.timeout, :returns => @new_resource.returns}) + end + end + end + end + end +end diff --git a/lib/chef/provider/powershell_script.rb b/lib/chef/provider/powershell_script.rb index c459cdf678..967b2d822b 100644 --- a/lib/chef/provider/powershell_script.rb +++ b/lib/chef/provider/powershell_script.rb @@ -23,9 +23,9 @@ class Chef class PowershellScript < Chef::Provider::WindowsScript protected - - EXIT_STATUS_NORMALIZATION_SCRIPT = "\nif ($? -eq $true) {exit 0} elseif ( $LASTEXITCODE -ne 0) {exit $LASTEXITCODE} else { exit 1 }" - EXIT_STATUS_RESET_SCRIPT = "$LASTEXITCODE=0\n" + EXIT_STATUS_EXCEPTION_HANDLER = "\ntrap [Exception] {write-error -exception ($_.Exception.Message);exit 1}".freeze + EXIT_STATUS_NORMALIZATION_SCRIPT = "\nif ($? -ne $true) { if ( $LASTEXITCODE -ne 0) {exit $LASTEXITCODE} else { exit 1 }}".freeze + EXIT_STATUS_RESET_SCRIPT = "\n$LASTEXITCODE=0".freeze # Process exit codes are strange with PowerShell. Unless you # explicitly call exit in Powershell, the powershell.exe @@ -36,15 +36,28 @@ class Chef # last process run in the script if it is the last command # executed, otherwise 0 or 1 based on whether $? is set to true # (success, where we return 0) or false (where we return 1). - def NormalizeScriptExitStatus( code ) - @code = (! code.nil?) ? ( EXIT_STATUS_RESET_SCRIPT + code + EXIT_STATUS_NORMALIZATION_SCRIPT ) : nil + def normalize_script_exit_status( code ) + target_code = ( EXIT_STATUS_EXCEPTION_HANDLER + + EXIT_STATUS_RESET_SCRIPT + + "\n" + + code.to_s + + EXIT_STATUS_NORMALIZATION_SCRIPT ) + convert_boolean_return = @new_resource.convert_boolean_return + @code = <<EOH +new-variable -name interpolatedexitcode -visibility private -value $#{convert_boolean_return} +new-variable -name chefscriptresult -visibility private +$chefscriptresult = { +#{target_code} +}.invokereturnasis() +if ($interpolatedexitcode -and $chefscriptresult.gettype().name -eq 'boolean') { exit [int32](!$chefscriptresult) } else { exit 0 } +EOH end public def initialize (new_resource, run_context) super(new_resource, run_context, '.ps1') - NormalizeScriptExitStatus(new_resource.code) + normalize_script_exit_status(new_resource.code) end def flags diff --git a/lib/chef/provider/service/macosx.rb b/lib/chef/provider/service/macosx.rb index 4f2de2ccbf..ca78c2eaee 100644 --- a/lib/chef/provider/service/macosx.rb +++ b/lib/chef/provider/service/macosx.rb @@ -17,6 +17,7 @@ # require 'chef/provider/service' +require 'rexml/document' class Chef class Provider @@ -41,6 +42,7 @@ class Chef @current_resource.service_name(@new_resource.service_name) @plist_size = 0 @plist = find_service_plist + @service_label = find_service_label set_service_status @current_resource @@ -48,14 +50,6 @@ class Chef def define_resource_requirements #super - requirements.assert(:enable) do |a| - a.failure_message Chef::Exceptions::UnsupportedAction, "#{self.to_s} does not support :enable" - end - - requirements.assert(:disable) do |a| - a.failure_message Chef::Exceptions::UnsupportedAction, "#{self.to_s} does not support :disable" - end - requirements.assert(:reload) do |a| a.failure_message Chef::Exceptions::UnsupportedAction, "#{self.to_s} does not support :reload" end @@ -66,6 +60,12 @@ class Chef end requirements.assert(:all_actions) do |a| + a.assertion { !@service_label.to_s.empty? } + a.failure_message Chef::Exceptions::Service, + "Could not find service's label in plist file '#{@plist}'!" + end + + requirements.assert(:all_actions) do |a| a.assertion { @plist_size > 0 } # No failrue here in original code - so we also will not # fail. Instead warn that the service is potentially missing @@ -74,7 +74,6 @@ class Chef @current_resource.running(false) end end - end def start_service @@ -111,19 +110,56 @@ class Chef end end + # On OS/X, enabling a service has the side-effect of starting it, + # and disabling a service has the side-effect of stopping it. + # + # This makes some sense on OS/X since launchctl is an "init"-style + # supervisor that will restart daemons that are crashing, etc. + def enable_service + if @current_resource.enabled + Chef::Log.debug("#{@new_resource} already enabled, not enabling") + else + shell_out!( + "launchctl load -w '#{@plist}'", + :user => @owner_uid, :group => @owner_gid + ) + end + end + + def disable_service + unless @current_resource.enabled + Chef::Log.debug("#{@new_resource} not enabled, not disabling") + else + shell_out!( + "launchctl unload -w '#{@plist}'", + :user => @owner_uid, :group => @owner_gid + ) + end + end def set_service_status - return if @plist == nil + return if @plist == nil or @service_label.to_s.empty? - @current_resource.enabled(!@plist.nil?) + cmd = shell_out( + "launchctl list #{@service_label}", + :user => @owner_uid, :group => @owner_gid + ) + + if cmd.exitstatus == 0 + @current_resource.enabled(true) + else + @current_resource.enabled(false) + end if @current_resource.enabled @owner_uid = ::File.stat(@plist).uid @owner_gid = ::File.stat(@plist).gid - shell_out!("launchctl list", :user => @owner_uid, :group => @owner_gid).stdout.each_line do |line| + shell_out!( + "launchctl list", :user => @owner_uid, :group => @owner_gid + ).stdout.each_line do |line| case line - when /(\d+|-)\s+(?:\d+|-)\s+(.*\.?)#{@current_resource.service_name}/ + when /(\d+|-)\s+(?:\d+|-)\s+(.*\.?)#{@service_label}/ pid = $1 @current_resource.running(!pid.to_i.zero?) end @@ -135,9 +171,27 @@ class Chef private + def find_service_label + # Most services have the same internal label as the name of the + # plist file. However, there is no rule saying that *has* to be + # the case, and some core services (notably, ssh) do not follow + # this rule. + + # plist files can come in XML or Binary formats. this command + # will make sure we get XML every time. + plist_xml = shell_out!("plutil -convert xml1 -o - #{@plist}").stdout + + plist_doc = REXML::Document.new(plist_xml) + plist_doc.elements[ + "/plist/dict/key[text()='Label']/following::string[1]/text()"] + end + def find_service_plist plists = PLIST_DIRS.inject([]) do |results, dir| - entries = Dir.glob("#{::File.expand_path(dir)}/*#{@current_resource.service_name}*.plist") + edir = ::File.expand_path(dir) + entries = Dir.glob( + "#{edir}/*#{@current_resource.service_name}*.plist" + ) entries.any? ? results << entries : results end plists.flatten! diff --git a/lib/chef/provider/service/solaris.rb b/lib/chef/provider/service/solaris.rb index 4bdb6fbfd1..7f06ac561b 100644 --- a/lib/chef/provider/service/solaris.rb +++ b/lib/chef/provider/service/solaris.rb @@ -25,11 +25,13 @@ class Chef class Service class Solaris < Chef::Provider::Service include Chef::Mixin::ShellOut + attr_reader :maintenance def initialize(new_resource, run_context=nil) super @init_command = "/usr/sbin/svcadm" @status_command = "/bin/svcs -l" + @maintenace = false end @@ -44,6 +46,7 @@ class Chef end def enable_service + shell_out!("#{default_init_command} clear #{@new_resource.service_name}") if @maintenance shell_out!("#{default_init_command} enable -s #{@new_resource.service_name}") end @@ -65,13 +68,14 @@ class Chef end def service_status - status = popen4("#{@status_command} #{@current_resource.service_name}") do |pid, stdin, stdout, stderr| - stdout.each do |line| - case line - when /state\s+online/ - @current_resource.enabled(true) - @current_resource.running(true) - end + status = shell_out!("#{@status_command} #{@current_resource.service_name}") + status.stdout.each_line do |line| + case line + when /state\s+online/ + @current_resource.enabled(true) + @current_resource.running(true) + when /state\s+maintenance/ + @maintenance = true end end unless @current_resource.enabled diff --git a/lib/chef/recipe.rb b/lib/chef/recipe.rb index 0c688cb5f8..5b95d80590 100644 --- a/lib/chef/recipe.rb +++ b/lib/chef/recipe.rb @@ -23,6 +23,7 @@ require 'chef/dsl/data_query' require 'chef/dsl/platform_introspection' require 'chef/dsl/include_recipe' require 'chef/dsl/registry_helper' +require 'chef/dsl/reboot_pending' require 'chef/mixin/from_file' @@ -38,6 +39,7 @@ class Chef include Chef::DSL::IncludeRecipe include Chef::DSL::Recipe include Chef::DSL::RegistryHelper + include Chef::DSL::RebootPending include Chef::Mixin::FromFile include Chef::Mixin::Deprecation diff --git a/lib/chef/request_id.rb b/lib/chef/request_id.rb new file mode 100644 index 0000000000..7fc177c633 --- /dev/null +++ b/lib/chef/request_id.rb @@ -0,0 +1,37 @@ +# Author:: Prajakta Purohit (<prajakta@opscode.com>) +# Copyright:: Copyright (c) 2009, 2010, 2013, 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/monkey_patches/securerandom' +require 'singleton' + +class Chef + class RequestID + include Singleton + + def reset_request_id + @request_id = nil + end + + def request_id + @request_id ||= generate_request_id + end + + def generate_request_id + SecureRandom.uuid + end + end +end diff --git a/lib/chef/resource.rb b/lib/chef/resource.rb index 997c614171..7d96b26b4b 100644 --- a/lib/chef/resource.rb +++ b/lib/chef/resource.rb @@ -21,7 +21,9 @@ require 'chef/mixin/params_validate' require 'chef/dsl/platform_introspection' require 'chef/dsl/data_query' require 'chef/dsl/registry_helper' +require 'chef/dsl/reboot_pending' require 'chef/mixin/convert_to_class_name' +require 'chef//guard_interpreter/resource_guard_interpreter' require 'chef/resource/conditional' require 'chef/resource/conditional_action_not_nothing' require 'chef/resource_collection' @@ -125,6 +127,7 @@ F include Chef::Mixin::ParamsValidate include Chef::DSL::PlatformIntrospection include Chef::DSL::RegistryHelper + include Chef::DSL::RebootPending include Chef::Mixin::ConvertToClassName include Chef::Mixin::Deprecation @@ -247,6 +250,7 @@ F @not_if = [] @only_if = [] @source_line = nil + @guard_interpreter = :default @elapsed_time = 0 @node = run_context ? deprecated_ivar(run_context.node, :node, :warn) : nil @@ -399,6 +403,14 @@ F ignore_failure(arg) end + def guard_interpreter(arg=nil) + set_or_return( + :guard_interpreter, + arg, + :kind_of => Symbol + ) + end + # Sets up a notification from this resource to the resource specified by +resource_spec+. def notifies(action, resource_spec, timing=:delayed) # when using old-style resources(:template => "/foo.txt") style, you @@ -550,7 +562,7 @@ F # * evaluates to false if the block is false, or if the command returns a non-zero exit code. def only_if(command=nil, opts={}, &block) if command || block_given? - @only_if << Conditional.only_if(command, opts, &block) + @only_if << Conditional.only_if(self, command, opts, &block) end @only_if end @@ -571,7 +583,7 @@ F # * evaluates to false if the block is true, or if the command returns a 0 exit status. def not_if(command=nil, opts={}, &block) if command || block_given? - @not_if << Conditional.not_if(command, opts, &block) + @not_if << Conditional.not_if(self, command, opts, &block) end @not_if end @@ -625,7 +637,7 @@ F provider_for_action(action).run_action rescue Exception => e if ignore_failure - Chef::Log.error("#{self} (#{defined_at}) had an error: #{e.message}; ignore_failure is set, continuing") + 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) @@ -660,8 +672,12 @@ F end end + def custom_exception_message(e) + "#{self} (#{defined_at}) had an error: #{e.class.name}: #{e.message}" + end + def customize_exception(e) - new_exception = e.exception("#{self} (#{defined_at}) had an error: #{e.class.name}: #{e.message}") + new_exception = e.exception(custom_exception_message(e)) new_exception.set_backtrace(e.backtrace) new_exception end @@ -813,6 +829,5 @@ F end end end - end end diff --git a/lib/chef/resource/conditional.rb b/lib/chef/resource/conditional.rb index 60f65e14e2..e6623be5dd 100644 --- a/lib/chef/resource/conditional.rb +++ b/lib/chef/resource/conditional.rb @@ -17,6 +17,7 @@ # require 'chef/mixin/shell_out' +require 'chef/guard_interpreter/resource_guard_interpreter' class Chef class Resource @@ -29,12 +30,12 @@ class Chef private :new end - def self.not_if(command=nil, command_opts={}, &block) - new(:not_if, command, command_opts, &block) + def self.not_if(parent_resource, command=nil, command_opts={}, &block) + new(:not_if, parent_resource, command, command_opts, &block) end - def self.only_if(command=nil, command_opts={}, &block) - new(:only_if, command, command_opts, &block) + def self.only_if(parent_resource, command=nil, command_opts={}, &block) + new(:only_if, parent_resource, command, command_opts, &block) end attr_reader :positivity @@ -42,14 +43,16 @@ class Chef attr_reader :command_opts attr_reader :block - def initialize(positivity, command=nil, command_opts={}, &block) + def initialize(positivity, parent_resource, command=nil, command_opts={}, &block) @positivity = positivity case command when String + @guard_interpreter = new_guard_interpreter(parent_resource, command, command_opts, &block) @command, @command_opts = command, command_opts @block = nil when nil raise ArgumentError, "only_if/not_if requires either a command or a block" unless block_given? + @guard_interpreter = nil @command, @command_opts = nil, nil @block = block else @@ -69,11 +72,11 @@ class Chef end def evaluate - @command ? evaluate_command : evaluate_block + @guard_interpreter ? evaluate_command : evaluate_block end def evaluate_command - shell_out(@command, @command_opts).status.success? + @guard_interpreter.evaluate rescue Chef::Exceptions::CommandTimeout Chef::Log.warn "Command '#{@command}' timed out" false @@ -100,6 +103,16 @@ class Chef end end + private + + def new_guard_interpreter(parent_resource, command, opts) + if parent_resource.guard_interpreter == :default + guard_interpreter = Chef::GuardInterpreter::DefaultGuardInterpreter.new(command, opts) + else + guard_interpreter = Chef::GuardInterpreter::ResourceGuardInterpreter.new(parent_resource, command, opts) + end + end + end end end diff --git a/lib/chef/resource/cron.rb b/lib/chef/resource/cron.rb index dfbb91f80c..9c04658bf3 100644 --- a/lib/chef/resource/cron.rb +++ b/lib/chef/resource/cron.rb @@ -43,6 +43,7 @@ class Chef @path = nil @shell = nil @home = nil + @time = nil @environment = {} end @@ -121,13 +122,28 @@ class Chef converted_arg = arg end begin - if integerize(arg) > 7 then raise RangeError end + error_message = "You provided '#{arg}' as a weekday, acceptable values are " + error_message << Provider::Cron::WEEKDAY_SYMBOLS.map {|sym| ":#{sym.to_s}"}.join(', ') + error_message << " and a string in crontab format" + if (arg.is_a?(Symbol) && !Provider::Cron::WEEKDAY_SYMBOLS.include?(arg)) || + (!arg.is_a?(Symbol) && integerize(arg) > 7) || + (!arg.is_a?(Symbol) && integerize(arg) < 0) + raise RangeError, error_message + end rescue ArgumentError end set_or_return( :weekday, converted_arg, - :kind_of => String + :kind_of => [String, Symbol] + ) + end + + def time(arg=nil) + set_or_return( + :time, + arg, + :equal_to => Chef::Provider::Cron::SPECIAL_TIME_VALUES ) end diff --git a/lib/chef/resource/execute.rb b/lib/chef/resource/execute.rb index 6c07bf9352..7c4fa48c0a 100644 --- a/lib/chef/resource/execute.rb +++ b/lib/chef/resource/execute.rb @@ -125,8 +125,6 @@ class Chef ) end - - end end end diff --git a/lib/chef/resource/powershell_script.rb b/lib/chef/resource/powershell_script.rb index cbd81b1259..1b47e7411a 100644 --- a/lib/chef/resource/powershell_script.rb +++ b/lib/chef/resource/powershell_script.rb @@ -15,17 +15,39 @@ # See the License for the specific language governing permissions and # limitations under the License. # - require 'chef/resource/windows_script' class Chef class Resource class PowershellScript < Chef::Resource::WindowsScript + set_guard_inherited_attributes(:architecture) + def initialize(name, run_context=nil) super(name, run_context, :powershell_script, "powershell.exe") + @convert_boolean_return = false + end + + def convert_boolean_return(arg=nil) + set_or_return( + :convert_boolean_return, + arg, + :kind_of => [ FalseClass, TrueClass ] + ) end + protected + + # Allow callers evaluating guards to request default + # attribute values. This is needed to allow + # convert_boolean_return to be true in guard context by default, + # and false by default otherwise. When this mode becomes the + # default for this resource, this method can be removed since + # guard context and recipe resource context will have the + # same behavior. + def self.get_default_attributes(opts) + {:convert_boolean_return => true} + end end end end diff --git a/lib/chef/resource/script.rb b/lib/chef/resource/script.rb index 8cc9c6f0c5..6f66fb9094 100644 --- a/lib/chef/resource/script.rb +++ b/lib/chef/resource/script.rb @@ -58,6 +58,31 @@ 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/subversion.rb b/lib/chef/resource/subversion.rb index 04fec9b1d8..44158cb080 100644 --- a/lib/chef/resource/subversion.rb +++ b/lib/chef/resource/subversion.rb @@ -32,6 +32,10 @@ class Chef allowed_actions << :force_export end + # Override exception to strip password if any, so it won't appear in logs and different Chef notifications + def custom_exception_message(e) + "#{self} (#{defined_at}) had an error: #{e.class.name}: #{svn_password ? e.message.gsub(svn_password, "[hidden_password]") : e.message}" + end end end end diff --git a/lib/chef/resource/windows_package.rb b/lib/chef/resource/windows_package.rb new file mode 100644 index 0000000000..8bd41e0cb7 --- /dev/null +++ b/lib/chef/resource/windows_package.rb @@ -0,0 +1,79 @@ +# +# Author:: Bryan McLellan <btm@loftninjas.org> +# 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/package' +require 'chef/provider/package/windows' +require 'chef/win32/error' if RUBY_PLATFORM =~ /mswin|mingw|windows/ + +class Chef + class Resource + class WindowsPackage < Chef::Resource::Package + + provides :package, :on_platforms => ["windows"] + + def initialize(name, run_context=nil) + super + @allowed_actions = [ :install, :remove ] + @provider = Chef::Provider::Package::Windows + @resource_name = :windows_package + @source ||= source(@package_name) + + # Unique to this resource + @installer_type = nil + @timeout = 600 + # In the past we accepted return code 127 for an unknown reason and 42 because of a bug + @returns = [ 0 ] + end + + def installer_type(arg=nil) + set_or_return( + :installer_type, + arg, + :kind_of => [ String ] + ) + end + + def timeout(arg=nil) + set_or_return( + :timeout, + arg, + :kind_of => [ String, Integer ] + ) + end + + def returns(arg=nil) + set_or_return( + :returns, + arg, + :kind_of => [ String, Integer, Array ] + ) + end + + def source(arg=nil) + if arg == nil && self.instance_variable_defined?(:@source) == true + @source + else + raise ArgumentError, "Bad type for WindowsPackage resource, use a String" unless arg.is_a?(String) + Chef::Log.debug("#{package_name}: sanitizing source path '#{arg}'") + @source = ::File.absolute_path(arg).gsub(::File::SEPARATOR, ::File::ALT_SEPARATOR) + end + end + end + end +end + diff --git a/lib/chef/resource/windows_script.rb b/lib/chef/resource/windows_script.rb index 2b563f5bec..108891e9ba 100644 --- a/lib/chef/resource/windows_script.rb +++ b/lib/chef/resource/windows_script.rb @@ -52,11 +52,6 @@ class Chef "cannot execute script with requested architecture '#{desired_architecture.to_s}' on a system with architecture '#{node_windows_architecture(node)}'" end end - - def node - run_context && run_context.node - end - end end end diff --git a/lib/chef/resource_reporter.rb b/lib/chef/resource_reporter.rb index 04f4ee26de..d191710cb4 100644 --- a/lib/chef/resource_reporter.rb +++ b/lib/chef/resource_reporter.rb @@ -107,7 +107,6 @@ class Chef @pending_update = nil @status = "success" @exception = nil - @run_id = SecureRandom.uuid @rest_client = rest_client @error_descriptions = {} end @@ -118,7 +117,7 @@ class Chef if reporting_enabled? begin resource_history_url = "reports/nodes/#{node_name}/runs" - server_response = @rest_client.post_rest(resource_history_url, {:action => :start, :run_id => @run_id, + server_response = @rest_client.post_rest(resource_history_url, {:action => :start, :run_id => run_id, :start_time => start_time.to_s}, headers) rescue Timeout::Error, Errno::EINVAL, Errno::ECONNRESET, EOFError, Net::HTTPBadResponse, Net::HTTPHeaderSyntaxError, Net::ProtocolError => e handle_error_starting_run(e, resource_history_url) @@ -158,6 +157,10 @@ class Chef @reporting_enabled = false end + def run_id + @run_status.run_id + end + def resource_current_state_loaded(new_resource, action, current_resource) unless nested_resource?(new_resource) @pending_update = ResourceReport.new_with_current_state(new_resource, action, current_resource) @@ -214,8 +217,8 @@ class Chef def post_reporting_data if reporting_enabled? run_data = prepare_run_data - resource_history_url = "reports/nodes/#{node_name}/runs/#{@run_id}" - Chef::Log.info("Sending resource update report (run-id: #{@run_id})") + resource_history_url = "reports/nodes/#{node_name}/runs/#{run_id}" + Chef::Log.info("Sending resource update report (run-id: #{run_id})") Chef::Log.debug run_data.inspect compressed_data = encode_gzip(run_data.to_json) begin diff --git a/lib/chef/resources.rb b/lib/chef/resources.rb index 76adb6f1e1..711becef8c 100644 --- a/lib/chef/resources.rb +++ b/lib/chef/resources.rb @@ -69,6 +69,7 @@ require 'chef/resource/template' require 'chef/resource/timestamped_deploy' require 'chef/resource/user' require 'chef/resource/whyrun_safe_ruby_block' +require 'chef/resource/windows_package' require 'chef/resource/yum_package' require 'chef/resource/lwrp_base' require 'chef/resource/bff_package' diff --git a/lib/chef/rest.rb b/lib/chef/rest.rb index a1139d7fa2..f0de443058 100644 --- a/lib/chef/rest.rb +++ b/lib/chef/rest.rb @@ -36,6 +36,7 @@ require 'chef/http/validate_content_length' require 'chef/config' require 'chef/exceptions' require 'chef/platform/query_helpers' +require 'chef/http/remote_request_id' class Chef # == Chef::REST @@ -56,19 +57,27 @@ class Chef # http://localhost:4000, a call to +get_rest+ with 'nodes' will make an # HTTP GET request to http://localhost:4000/nodes def initialize(url, client_name=Chef::Config[:node_name], signing_key_filename=Chef::Config[:client_key], options={}) + options = options.dup options[:client_name] = client_name options[:signing_key_filename] = signing_key_filename super(url, options) @decompressor = Decompressor.new(options) @authenticator = Authenticator.new(options) + @request_id = RemoteRequestID.new(options) - @middlewares << ValidateContentLength.new(options) @middlewares << JSONInput.new(options) @middlewares << JSONToModelOutput.new(options) @middlewares << CookieManager.new(options) @middlewares << @decompressor @middlewares << @authenticator + @middlewares << @request_id + + # ValidateContentLength should come after Decompressor + # because the order of middlewares is reversed when handling + # responses. + @middlewares << ValidateContentLength.new(options) + end def signing_key_filename @@ -132,7 +141,7 @@ class Chef def raw_http_request(method, path, headers, data) url = create_url(path) method, url, headers, data = @authenticator.handle_request(method, url, headers, data) - + method, url, headers, data = @request_id.handle_request(method, url, headers, data) response, rest_request, return_value = send_http_request(method, url, headers, data) response.error! unless success_response?(response) return_value diff --git a/lib/chef/run_context.rb b/lib/chef/run_context.rb index 05a954ad15..a102ef4692 100644 --- a/lib/chef/run_context.rb +++ b/lib/chef/run_context.rb @@ -77,13 +77,15 @@ class Chef @events = events @node.run_context = self + + @cookbook_compiler = nil end # Triggers the compile phase of the chef run. Implemented by # Chef::RunContext::CookbookCompiler def load(run_list_expansion) - compiler = CookbookCompiler.new(self, run_list_expansion, events) - compiler.compile + @cookbook_compiler = CookbookCompiler.new(self, run_list_expansion, events) + @cookbook_compiler.compile end # Adds an immediate notification to the @@ -141,6 +143,18 @@ class Chef Chef::Log.debug("Loading Recipe #{recipe_name} via include_recipe") cookbook_name, recipe_short_name = Chef::Recipe.parse_recipe_name(recipe_name) + + if unreachable_cookbook?(cookbook_name) # CHEF-4367 + Chef::Log.warn(<<-ERROR_MESSAGE) +MissingCookbookDependency: +Recipe `#{recipe_name}` is not in the run_list, and cookbook '#{cookbook_name}' +is not a dependency of any cookbook in the run_list. To load this recipe, +first add a dependency on cookbook '#{cookbook_name}' in the cookbook you're +including it from in that cookbook's metadata. +ERROR_MESSAGE + end + + if loaded_fully_qualified_recipe?(cookbook_name, recipe_short_name) Chef::Log.debug("I am not loading #{recipe_name}, because I have already seen it.") false @@ -228,6 +242,12 @@ class Chef cookbook.has_cookbook_file_for_node?(node, cb_file_name) end + # Delegates to CookbookCompiler#unreachable_cookbook? + # Used to raise an error when attempting to load a recipe belonging to a + # cookbook that is not in the dependency graph. See also: CHEF-4367 + def unreachable_cookbook?(cookbook_name) + @cookbook_compiler.unreachable_cookbook?(cookbook_name) + end private diff --git a/lib/chef/run_context/cookbook_compiler.rb b/lib/chef/run_context/cookbook_compiler.rb index 0a05061152..abe5afa7ae 100644 --- a/lib/chef/run_context/cookbook_compiler.rb +++ b/lib/chef/run_context/cookbook_compiler.rb @@ -16,6 +16,7 @@ # limitations under the License. # +require 'set' require 'chef/log' require 'chef/recipe' require 'chef/resource/lwrp_base' @@ -149,6 +150,17 @@ class Chef @events.recipe_load_complete end + # Whether or not a cookbook is reachable from the set of cookbook given + # by the run_list plus those cookbooks' dependencies. + def unreachable_cookbook?(cookbook_name) + !reachable_cookbooks.include?(cookbook_name) + end + + # All cookbooks in the dependency graph, returned as a Set. + def reachable_cookbooks + @reachable_cookbooks ||= Set.new(cookbook_order) + end + private def load_attributes_from_cookbook(cookbook_name) diff --git a/lib/chef/run_status.rb b/lib/chef/run_status.rb index 9354f7872a..0f181426b0 100644 --- a/lib/chef/run_status.rb +++ b/lib/chef/run_status.rb @@ -37,6 +37,8 @@ class Chef::RunStatus attr_writer :exception + attr_accessor :run_id + def initialize(node, events) @node = node @events = events @@ -112,7 +114,8 @@ class Chef::RunStatus :all_resources => all_resources, :updated_resources => updated_resources, :exception => formatted_exception, - :backtrace => backtrace} + :backtrace => backtrace, + :run_id => run_id} end # Returns a string of the format "ExceptionClass: message" or +nil+ if no diff --git a/lib/chef/server_api.rb b/lib/chef/server_api.rb index e9e7593dd6..8cdcd7a09d 100644 --- a/lib/chef/server_api.rb +++ b/lib/chef/server_api.rb @@ -22,6 +22,7 @@ require 'chef/http/cookie_manager' require 'chef/http/decompressor' require 'chef/http/json_input' require 'chef/http/json_output' +require 'chef/http/remote_request_id' class Chef class ServerAPI < Chef::HTTP @@ -37,5 +38,6 @@ class Chef use Chef::HTTP::CookieManager use Chef::HTTP::Decompressor use Chef::HTTP::Authenticator + use Chef::HTTP::RemoteRequestID end -end
\ No newline at end of file +end diff --git a/lib/chef/util/editor.rb b/lib/chef/util/editor.rb new file mode 100644 index 0000000000..973cf48e30 --- /dev/null +++ b/lib/chef/util/editor.rb @@ -0,0 +1,92 @@ +# +# Author:: Chris Bandy (<bandy.chris@gmail.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. +# + +class Chef + class Util + class Editor + attr_reader :lines + + def initialize(lines) + @lines = lines.to_a.clone + end + + def append_line_after(search, line_to_append) + lines = [] + + @lines.each do |line| + lines << line + lines << line_to_append if line.match(search) + end + + (lines.length - @lines.length).tap { @lines = lines } + end + + def append_line_if_missing(search, line_to_append) + count = 0 + + unless @lines.find { |line| line.match(search) } + count = 1 + @lines << line_to_append + end + + count + end + + def remove_lines(search) + count = 0 + + @lines.delete_if do |line| + count += 1 if line.match(search) + end + + count + end + + def replace(search, replace) + count = 0 + + @lines.map! do |line| + if line.match(search) + count += 1 + line.gsub!(search, replace) + else + line + end + end + + count + end + + def replace_lines(search, replace) + count = 0 + + @lines.map! do |line| + if line.match(search) + count += 1 + replace + else + line + end + end + + count + end + end + end +end + diff --git a/lib/chef/util/file_edit.rb b/lib/chef/util/file_edit.rb index bb19435a12..92cefb4bb4 100644 --- a/lib/chef/util/file_edit.rb +++ b/lib/chef/util/file_edit.rb @@ -15,8 +15,8 @@ # See the License for the specific language governing permissions and # limitations under the License. +require 'chef/util/editor' require 'fileutils' -require 'tempfile' class Chef class Util @@ -24,108 +24,76 @@ class Chef private - attr_accessor :original_pathname, :contents, :file_edited + attr_reader :editor, :original_pathname public def initialize(filepath) + raise ArgumentError, "File '#{filepath}' does not exist" unless File.exist?(filepath) + @editor = Editor.new(File.open(filepath, &:readlines)) @original_pathname = filepath @file_edited = false + end - raise ArgumentError, "File doesn't exist" unless File.exist? @original_pathname - @contents = File.open(@original_pathname) { |f| f.readlines } + # return if file has been edited + def file_edited? + @file_edited end #search the file line by line and match each line with the given regex #if matched, replace the whole line with newline. def search_file_replace_line(regex, newline) - search_match(regex, newline, 'r', 1) + @changes = (editor.replace_lines(regex, newline) > 0) || @changes end #search the file line by line and match each line with the given regex #if matched, replace the match (all occurances) with the replace parameter def search_file_replace(regex, replace) - search_match(regex, replace, 'r', 2) + @changes = (editor.replace(regex, replace) > 0) || @changes end #search the file line by line and match each line with the given regex #if matched, delete the line def search_file_delete_line(regex) - search_match(regex, " ", 'd', 1) + @changes = (editor.remove_lines(regex) > 0) || @changes end #search the file line by line and match each line with the given regex #if matched, delete the match (all occurances) from the line def search_file_delete(regex) - search_match(regex, " ", 'd', 2) + search_file_replace(regex, '') end #search the file line by line and match each line with the given regex #if matched, insert newline after each matching line def insert_line_after_match(regex, newline) - search_match(regex, newline, 'i', 1) + @changes = (editor.append_line_after(regex, newline) > 0) || @changes end #search the file line by line and match each line with the given regex #if not matched, insert newline at the end of the file def insert_line_if_no_match(regex, newline) - search_match(regex, newline, 'i', 2) + @changes = (editor.append_line_if_missing(regex, newline) > 0) || @changes + end + + def unwritten_changes? + !!@changes end #Make a copy of old_file and write new file out (only if file changed) def write_file - - # file_edited is false when there was no match in the whole file and thus no contents have changed. - if file_edited + if @changes backup_pathname = original_pathname + ".old" FileUtils.cp(original_pathname, backup_pathname, :preserve => true) File.open(original_pathname, "w") do |newfile| - contents.each do |line| + editor.lines.each do |line| newfile.puts(line) end newfile.flush end + @file_edited = true end - self.file_edited = false - end - - private - - #helper method to do the match, replace, delete, and insert operations - #command is the switch of delete, replace, and insert ('d', 'r', 'i') - #method is to control operation on whole line or only the match (1 for line, 2 for match) - def search_match(regex, replace, command, method) - - #convert regex to a Regexp object (if not already is one) and store it in exp. - exp = Regexp.new(regex) - - #loop through contents and do the appropriate operation depending on 'command' and 'method' - new_contents = [] - - contents.each do |line| - if line.match(exp) - self.file_edited = true - case - when command == 'r' - new_contents << ((method == 1) ? replace : line.gsub!(exp, replace)) - when command == 'd' - if method == 2 - new_contents << line.gsub!(exp, "") - end - when command == 'i' - new_contents << line - new_contents << replace unless method == 2 - end - else - new_contents << line - end - end - if command == 'i' && method == 2 && ! file_edited - new_contents << replace - self.file_edited = true - end - - self.contents = new_contents + @changes = false end end end diff --git a/lib/chef/version.rb b/lib/chef/version.rb index 3c3972d0b4..f55160b56c 100644 --- a/lib/chef/version.rb +++ b/lib/chef/version.rb @@ -17,7 +17,7 @@ class Chef CHEF_ROOT = File.dirname(File.expand_path(File.dirname(__FILE__))) - VERSION = '11.10.4' + VERSION = '11.12.0.rc.0' end # NOTE: the Chef::Version class is defined in version_class.rb diff --git a/lib/chef/win32/api/installer.rb b/lib/chef/win32/api/installer.rb new file mode 100644 index 0000000000..745802d260 --- /dev/null +++ b/lib/chef/win32/api/installer.rb @@ -0,0 +1,166 @@ +# +# Author:: Bryan McLellan <btm@loftninjas.org> +# 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/exceptions' +require 'chef/win32/api' +require 'chef/win32/error' +require 'pathname' + +class Chef + module ReservedNames::Win32 + module API + module Installer + extend Chef::ReservedNames::Win32 + extend Chef::ReservedNames::Win32::API + + ############################################### + # Win32 API Constants + ############################################### + + + ############################################### + # Win32 API Bindings + ############################################### + + ffi_lib 'msi' + +=begin +UINT MsiOpenPackage( + _In_ LPCTSTR szPackagePath, + _Out_ MSIHANDLE *hProduct +); +=end + safe_attach_function :msi_open_package, :MsiOpenPackageExA, [ :string, :int, :pointer ], :int + +=begin +UINT MsiGetProductProperty( + _In_ MSIHANDLE hProduct, + _In_ LPCTSTR szProperty, + _Out_ LPTSTR lpValueBuf, + _Inout_ DWORD *pcchValueBuf +); +=end + safe_attach_function :msi_get_product_property, :MsiGetProductPropertyA, [ :pointer, :pointer, :pointer, :pointer ], :int + +=begin +UINT MsiGetProductInfo( + _In_ LPCTSTR szProduct, + _In_ LPCTSTR szProperty, + _Out_ LPTSTR lpValueBuf, + _Inout_ DWORD *pcchValueBuf +); +=end + safe_attach_function :msi_get_product_info, :MsiGetProductInfoA, [ :pointer, :pointer, :pointer, :pointer ], :int + +=begin +UINT MsiCloseHandle( + _In_ MSIHANDLE hAny +); +=end + safe_attach_function :msi_close_handle, :MsiCloseHandle, [ :pointer ], :int + + ############################################### + # Helpers + ############################################### + + # Opens a Microsoft Installer (MSI) file from an absolute path and returns the specified property + def get_product_property(package_path, property_name) + pkg_ptr = open_package(package_path) + + buffer = 0.chr + buffer_length = FFI::Buffer.new(:long).write_long(0) + + # Fetch the length of the property + status = msi_get_product_property(pkg_ptr.read_pointer, property_name, buffer, buffer_length) + + # We expect error ERROR_MORE_DATA (234) here because we passed a buffer length of 0 + if status != 234 + msg = "msi_get_product_property: returned unknown error #{status} when retrieving #{property_name}: " + msg << Chef::ReservedNames::Win32::Error.format_message(status) + raise Chef::Exceptions::Package, msg + end + + buffer_length = FFI::Buffer.new(:long).write_long(buffer_length.read_long + 1) + buffer = 0.chr * buffer_length.read_long + + # Fetch the property + status = msi_get_product_property(pkg_ptr.read_pointer, property_name, buffer, buffer_length) + + if status != 0 + msg = "msi_get_product_property: returned unknown error #{status} when retrieving #{property_name}: " + msg << Chef::ReservedNames::Win32::Error.format_message(status) + raise Chef::Exceptions::Package, msg + end + + msi_close_handle(pkg_ptr.read_pointer) + return buffer + end + + # Opens a Microsoft Installer (MSI) file from an absolute path and returns a pointer to a handle + # Remember to close the handle with msi_close_handle() + def open_package(package_path) + # MsiOpenPackage expects a perfect absolute Windows path to the MSI + raise ArgumentError, "Provided path '#{package_path}' must be an absolute path" unless Pathname.new(package_path).absolute? + + pkg_ptr = FFI::MemoryPointer.new(:pointer, 4) + status = msi_open_package(package_path, 1, pkg_ptr) + case status + when 0 + # success + else + raise Chef::Exceptions::Package, "msi_open_package: unexpected status #{status}: #{Chef::ReservedNames::Win32::Error.format_message(status)}" + end + return pkg_ptr + end + + # All installed product_codes should have a VersionString + # Returns a version if installed, nil if not installed + def get_installed_version(product_code) + version = 0.chr + version_length = FFI::Buffer.new(:long).write_long(0) + + status = msi_get_product_info(product_code, "VersionString", version, version_length) + + return nil if status == 1605 # ERROR_UNKNOWN_PRODUCT (0x645) + + # We expect error ERROR_MORE_DATA (234) here because we passed a buffer length of 0 + if status != 234 + msg = "msi_get_product_info: product code '#{product_code}' returned unknown error #{status} when retrieving VersionString: " + msg << Chef::ReservedNames::Win32::Error.format_message(status) + raise Chef::Exceptions::Package, msg + end + + # We could fetch the product version now that we know the variable length, but we don't need it here. + + version_length = FFI::Buffer.new(:long).write_long(version_length.read_long + 1) + version = 0.chr * version_length.read_long + + status = msi_get_product_info(product_code, "VersionString", version, version_length) + + if status != 0 + msg = "msi_get_product_info: product code '#{product_code}' returned unknown error #{status} when retrieving VersionString: " + msg << Chef::ReservedNames::Win32::Error.format_message(status) + raise Chef::Exceptions::Package, msg + end + + version + end + end + end + end +end diff --git a/lib/chef/win32/version.rb b/lib/chef/win32/version.rb index e008ff15e8..7f5fcceead 100644 --- a/lib/chef/win32/version.rb +++ b/lib/chef/win32/version.rb @@ -116,9 +116,17 @@ class Chef # http://msdn.microsoft.com/en-us/library/windows/desktop/ms724439(v=vs.85).aspx require 'ruby-wmi' + # CHEF-4888: Work around ruby #2618, expected to be fixed in Ruby 2.1.0 + # https://github.com/ruby/ruby/commit/588504b20f5cc880ad51827b93e571e32446e5db + # https://github.com/ruby/ruby/commit/27ed294c7134c0de582007af3c915a635a6506cd + + WIN32OLE.ole_initialize + os_info = WMI::Win32_OperatingSystem.find(:first) os_version = os_info.send('Version') + WIN32OLE.ole_uninitialize + # The operating system version is a string in the following form # that can be split into components based on the '.' delimiter: # MajorVersionNumber.MinorVersionNumber.BuildNumber |