summaryrefslogtreecommitdiff
path: root/lib/chef
diff options
context:
space:
mode:
authorsersut <serdar@opscode.com>2014-03-30 13:45:00 -0700
committersersut <serdar@opscode.com>2014-03-30 13:45:00 -0700
commit7a1778fb309423114462d578e01ba0e00108010f (patch)
tree792e1fbafdca12725edf923cdad22536f3124fc3 /lib/chef
parent0d097217dda26ac5551d1ad24132d9e53a62e0fb (diff)
parentcacf2a53a3b789829dd6b5b2956e07cc1aa42931 (diff)
downloadchef-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')
-rw-r--r--lib/chef/api_client.rb4
-rw-r--r--lib/chef/api_client/registration.rb55
-rw-r--r--lib/chef/application.rb3
-rw-r--r--lib/chef/application/client.rb61
-rw-r--r--lib/chef/client.rb67
-rw-r--r--lib/chef/config.rb11
-rw-r--r--lib/chef/cookbook/chefignore.rb12
-rw-r--r--lib/chef/cookbook/metadata.rb34
-rw-r--r--lib/chef/cookbook/synchronizer.rb4
-rw-r--r--lib/chef/cookbook/syntax_check.rb121
-rw-r--r--lib/chef/dsl/reboot_pending.rb61
-rw-r--r--lib/chef/encrypted_data_bag_item.rb38
-rw-r--r--lib/chef/exceptions.rb14
-rw-r--r--lib/chef/formatters/error_descriptor.rb2
-rw-r--r--lib/chef/guard_interpreter/default_guard_interpreter.rb42
-rw-r--r--lib/chef/guard_interpreter/resource_guard_interpreter.rb122
-rw-r--r--lib/chef/http.rb1
-rw-r--r--lib/chef/http/decompressor.rb11
-rw-r--r--lib/chef/http/remote_request_id.rb46
-rw-r--r--lib/chef/http/simple.rb5
-rw-r--r--lib/chef/http/validate_content_length.rb40
-rw-r--r--lib/chef/knife.rb1
-rw-r--r--lib/chef/knife/bootstrap.rb2
-rw-r--r--lib/chef/knife/bootstrap/README.md12
-rw-r--r--lib/chef/knife/bootstrap/chef-full.erb3
-rw-r--r--lib/chef/knife/client_bulk_delete.rb57
-rw-r--r--lib/chef/knife/client_create.rb6
-rw-r--r--lib/chef/knife/client_delete.rb16
-rw-r--r--lib/chef/knife/cookbook_bulk_delete.rb2
-rw-r--r--lib/chef/knife/cookbook_upload.rb24
-rw-r--r--lib/chef/knife/core/bootstrap_context.rb2
-rw-r--r--lib/chef/knife/core/ui.rb47
-rw-r--r--lib/chef/knife/node_run_list_add.rb33
-rw-r--r--lib/chef/knife/raw.rb1
-rw-r--r--lib/chef/knife/ssh.rb75
-rw-r--r--lib/chef/knife/ssl_check.rb213
-rw-r--r--lib/chef/knife/ssl_fetch.rb145
-rw-r--r--lib/chef/mixin/deep_merge.rb18
-rw-r--r--lib/chef/mixin/shell_out.rb12
-rw-r--r--lib/chef/node.rb29
-rw-r--r--lib/chef/node/attribute_collections.rb9
-rw-r--r--lib/chef/node/immutable_collections.rb41
-rw-r--r--lib/chef/platform/provider_mapping.rb39
-rw-r--r--lib/chef/platform/query_helpers.rb12
-rw-r--r--lib/chef/policy_builder/expand_node_object.rb9
-rw-r--r--lib/chef/provider/cron.rb28
-rw-r--r--lib/chef/provider/deploy.rb2
-rw-r--r--lib/chef/provider/group.rb2
-rw-r--r--lib/chef/provider/ifconfig/debian.rb27
-rw-r--r--lib/chef/provider/mount/mount.rb2
-rw-r--r--lib/chef/provider/ohai.rb11
-rw-r--r--lib/chef/provider/package/dpkg.rb3
-rw-r--r--lib/chef/provider/package/windows.rb80
-rw-r--r--lib/chef/provider/package/windows/msi.rb69
-rw-r--r--lib/chef/provider/powershell_script.rb25
-rw-r--r--lib/chef/provider/service/macosx.rb82
-rw-r--r--lib/chef/provider/service/solaris.rb18
-rw-r--r--lib/chef/recipe.rb2
-rw-r--r--lib/chef/request_id.rb37
-rw-r--r--lib/chef/resource.rb25
-rw-r--r--lib/chef/resource/conditional.rb27
-rw-r--r--lib/chef/resource/cron.rb20
-rw-r--r--lib/chef/resource/execute.rb2
-rw-r--r--lib/chef/resource/powershell_script.rb24
-rw-r--r--lib/chef/resource/script.rb25
-rw-r--r--lib/chef/resource/subversion.rb4
-rw-r--r--lib/chef/resource/windows_package.rb79
-rw-r--r--lib/chef/resource/windows_script.rb5
-rw-r--r--lib/chef/resource_reporter.rb11
-rw-r--r--lib/chef/resources.rb1
-rw-r--r--lib/chef/rest.rb13
-rw-r--r--lib/chef/run_context.rb24
-rw-r--r--lib/chef/run_context/cookbook_compiler.rb12
-rw-r--r--lib/chef/run_status.rb5
-rw-r--r--lib/chef/server_api.rb4
-rw-r--r--lib/chef/util/editor.rb92
-rw-r--r--lib/chef/util/file_edit.rb76
-rw-r--r--lib/chef/version.rb2
-rw-r--r--lib/chef/win32/api/installer.rb166
-rw-r--r--lib/chef/win32/version.rb8
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