diff options
Diffstat (limited to 'lib')
-rw-r--r-- | lib/chef.rb | 1 | ||||
-rw-r--r-- | lib/chef/chef_class.rb | 130 | ||||
-rw-r--r-- | lib/chef/client.rb | 19 | ||||
-rw-r--r-- | lib/chef/event_loggers/windows_eventlog.rb | 16 | ||||
-rw-r--r-- | lib/chef/exceptions.rb | 3 | ||||
-rw-r--r-- | lib/chef/key.rb | 251 | ||||
-rw-r--r-- | lib/chef/knife/bootstrap/templates/chef-full.erb | 189 | ||||
-rw-r--r-- | lib/chef/mixin/provides.rb | 32 | ||||
-rw-r--r-- | lib/chef/platform/provider_priority_map.rb | 23 | ||||
-rw-r--r-- | lib/chef/platform/resource_priority_map.rb | 37 | ||||
-rw-r--r-- | lib/chef/policy_builder/expand_node_object.rb | 14 | ||||
-rw-r--r-- | lib/chef/policy_builder/policyfile.rb | 1 | ||||
-rw-r--r-- | lib/chef/provider.rb | 25 | ||||
-rw-r--r-- | lib/chef/provider_resolver.rb | 15 | ||||
-rw-r--r-- | lib/chef/resource.rb | 44 | ||||
-rw-r--r-- | lib/chef/resource_resolver.rb | 101 | ||||
-rw-r--r-- | lib/chef/search/query.rb | 2 |
17 files changed, 796 insertions, 107 deletions
diff --git a/lib/chef.rb b/lib/chef.rb index 7f54b91f14..6bce976439 100644 --- a/lib/chef.rb +++ b/lib/chef.rb @@ -32,3 +32,4 @@ require 'chef/run_status' require 'chef/handler' require 'chef/handler/json_file' +require 'chef/chef_class' diff --git a/lib/chef/chef_class.rb b/lib/chef/chef_class.rb new file mode 100644 index 0000000000..d3f7ee55c7 --- /dev/null +++ b/lib/chef/chef_class.rb @@ -0,0 +1,130 @@ +# +# Author:: Lamont Granquist (<lamont@chef.io>) +# Copyright:: Copyright (c) 2015 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. + +# NOTE: This class is not intended for internal use by the chef-client code. Classes in +# chef-client should still have objects like the node and run_context injected into them +# via their initializers. This class is still global state and will complicate writing +# unit tests for internal Chef objects. It is intended to be used only by recipe code. + +# NOTE: When adding require lines here you are creating tight coupling on a global that may be +# included in many different situations and ultimately that ends in tears with circular requires. +# Note the way that the run_context, provider_priority_map and resource_priority_map are "dependency +# injected" into this class by other objects and do not reference the class symbols in those files +# directly and we do not need to require those files here. + +class Chef + class << self + + # + # Public API + # + + # Get the node object + # + # @return [Chef::Node] node object of the chef-client run + attr_reader :node + + # Get the run context + # + # @return [Chef::RunContext] run_context of the chef-client run + attr_reader :run_context + + # Get the array of providers associated with a resource_name for the current node + # + # @param resource_name [Symbol] name of the resource as a symbol + # @return [Array<Class>] Priority Array of Provider Classes to use for the resource_name on the node + def get_provider_priority_array(resource_name) + @provider_priority_map.get_priority_array(node, resource_name).dup + end + + # Get the array of resources associated with a resource_name for the current node + # + # @param resource_name [Symbol] name of the resource as a symbol + # @return [Array<Class>] Priority Array of Resource Classes to use for the resource_name on the node + def get_resource_priority_array(resource_name) + @resource_priority_map.get_priority_array(node, resource_name).dup + end + + # Set the array of providers associated with a resource_name for the current node + # + # @param resource_name [Symbol] name of the resource as a symbol + # @param priority_array [Array<Class>] Array of Classes to set as the priority for resource_name on the node + # @param filter [Hash] Chef::Nodearray-style filter + # @return [Array<Class>] Modified Priority Array of Provider Classes to use for the resource_name on the node + def set_provider_priority_array(resource_name, priority_array, *filter) + @provider_priority_map.set_priority_array(resource_name, priority_array, *filter).dup + end + + # Get the array of resources associated with a resource_name for the current node + # + # @param resource_name [Symbol] name of the resource as a symbol + # @param priority_array [Array<Class>] Array of Classes to set as the priority for resource_name on the node + # @param filter [Hash] Chef::Nodearray-style filter + # @return [Array<Class>] Modified Priority Array of Resource Classes to use for the resource_name on the node + def set_resource_priority_array(resource_name, priority_array, *filter) + @resource_priority_map.set_priority_array(resource_name, priority_array, *filter).dup + end + + # + # Dependency Injection API (Private not Public) + # [ in the ruby sense these have to be public methods, but they are + # *NOT* for public consumption ] + # + + # Sets the resource_priority_map + # + # @api private + # @param resource_priority_map [Chef::Platform::ResourcePriorityMap] + def set_resource_priority_map(resource_priority_map) + @resource_priority_map = resource_priority_map + end + + # Sets the provider_priority_map + # + # @api private + # @param provider_priority_map [Chef::Platform::providerPriorityMap] + def set_provider_priority_map(provider_priority_map) + @provider_priority_map = provider_priority_map + end + + # Sets the node object + # + # @api private + # @param node [Chef::Node] + def set_node(node) + @node = node + end + + # Sets the run_context object + # + # @api private + # @param run_context [Chef::RunContext] + def set_run_context(run_context) + @run_context = run_context + end + + # Resets the internal state + # + # @api private + def reset! + @run_context = nil + @node = nil + @provider_priority_map = nil + @resource_priority_map = nil + end + end +end diff --git a/lib/chef/client.rb b/lib/chef/client.rb index a4f15c271f..d04a3dbbd5 100644 --- a/lib/chef/client.rb +++ b/lib/chef/client.rb @@ -166,6 +166,13 @@ class Chef if new_runlist = args.delete(:runlist) @json_attribs["run_list"] = new_runlist end + + # these slurp in the resource+provider world, so be exceedingly lazy about requiring them + require 'chef/platform/provider_priority_map' unless defined? Chef::Platform::ProviderPriorityMap + require 'chef/platform/resource_priority_map' unless defined? Chef::Platform::ResourcePriorityMap + + Chef.set_provider_priority_map(Chef::Platform::ProviderPriorityMap.instance) + Chef.set_resource_priority_map(Chef::Platform::ResourcePriorityMap.instance) end def configure_formatters @@ -224,21 +231,21 @@ class Chef end # Instantiates a Chef::Node object, possibly loading the node's prior state - # when using chef-client. Delegates to policy_builder - # + # when using chef-client. Delegates to policy_builder. Injects the built node + # into the Chef class. # - # === Returns - # Chef::Node:: The node object for this chef run + # @return [Chef::Node] The node object for this Chef run def load_node policy_builder.load_node @node = policy_builder.node + Chef.set_node(@node) + node end # Mutates the `node` object to prepare it for the chef run. Delegates to # policy_builder # - # === Returns - # Chef::Node:: The updated node object + # @return [Chef::Node] The updated node object def build_node policy_builder.build_node @run_status = Chef::RunStatus.new(node, events) diff --git a/lib/chef/event_loggers/windows_eventlog.rb b/lib/chef/event_loggers/windows_eventlog.rb index 6f5ef627fb..37dcdc8693 100644 --- a/lib/chef/event_loggers/windows_eventlog.rb +++ b/lib/chef/event_loggers/windows_eventlog.rb @@ -88,15 +88,21 @@ class Chef #Exception message: %4 #Exception backtrace: %5 def run_failed(e) + data = + if @run_status + [@run_status.run_id, + @run_status.elapsed_time.to_s] + else + ["UNKNOWN", "UNKNOWN"] + end + @eventlog.report_event( :event_type => ::Win32::EventLog::ERROR_TYPE, :source => SOURCE, :event_id => RUN_FAILED_EVENT_ID, - :data => [@run_status.run_id, - @run_status.elapsed_time.to_s, - e.class.name, - e.message, - e.backtrace.join("\n")] + :data => data + [e.class.name, + e.message, + e.backtrace.join("\n")] ) end diff --git a/lib/chef/exceptions.rb b/lib/chef/exceptions.rb index eea6a2f239..cfedbfd0d9 100644 --- a/lib/chef/exceptions.rb +++ b/lib/chef/exceptions.rb @@ -69,6 +69,9 @@ class Chef class ValidationFailed < ArgumentError; end class InvalidPrivateKey < ArgumentError; end class ConfigurationError < ArgumentError; end + class MissingKeyAttribute < ArgumentError; end + class InvalidKeyArgument < ArgumentError; end + class InvalidKeyAttribute < ArgumentError; end class RedirectLimitExceeded < RuntimeError; end class AmbiguousRunlistSpecification < ArgumentError; end class CookbookFrozen < ArgumentError; end diff --git a/lib/chef/key.rb b/lib/chef/key.rb new file mode 100644 index 0000000000..1828713386 --- /dev/null +++ b/lib/chef/key.rb @@ -0,0 +1,251 @@ +# +# Author:: Tyler Cloke (tyler@chef.io) +# Copyright:: Copyright (c) 2015 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/json_compat' +require 'chef/mixin/params_validate' +require 'chef/exceptions' + +class Chef + # Class for interacting with a chef key object. Can be used to create new keys, + # save to server, load keys from server, list keys, delete keys, etc. + # + # @author Tyler Cloke + # + # @attr [String] actor the name of the client or user that this key is for + # @attr [String] name the name of the key + # @attr [String] public_key the RSA string of this key + # @attr [String] private_key the RSA string of the private key if returned via a POST or PUT + # @attr [String] expiration_date the ISO formatted string YYYY-MM-DDTHH:MM:SSZ, i.e. 2020-12-24T21:00:00Z + # @attr [String] rest Chef::REST object, initialized and cached via chef_rest method + # @attr [string] api_base either "users" or "clients", initialized and cached via api_base method + # + # @attr_reader [String] actor_field_name must be either 'client' or 'user' + class Key + + include Chef::Mixin::ParamsValidate + + attr_reader :actor_field_name + + def initialize(actor, actor_field_name) + # Actor that the key is for, either a client or a user. + @actor = actor + + unless actor_field_name == "user" || actor_field_name == "client" + raise Chef::Exceptions::InvalidKeyArgument, "the second argument to initialize must be either 'user' or 'client'" + end + + @actor_field_name = actor_field_name + + @name = nil + @public_key = nil + @private_key = nil + @expiration_date = nil + @create_key = nil + end + + def chef_rest + @rest ||= if @actor_field_name == "user" + Chef::REST.new(Chef::Config[:chef_server_root]) + else + Chef::REST.new(Chef::Config[:chef_server_url]) + end + end + + def api_base + @api_base ||= if @actor_field_name == "user" + "users" + else + "clients" + end + end + + def actor(arg=nil) + set_or_return(:actor, arg, + :regex => /^[a-z0-9\-_]+$/) + end + + def name(arg=nil) + set_or_return(:name, arg, + :kind_of => String) + end + + def public_key(arg=nil) + raise Chef::Exceptions::InvalidKeyAttribute, "you cannot set the public_key if create_key is true" if !arg.nil? && @create_key + set_or_return(:public_key, arg, + :kind_of => String) + end + + def private_key(arg=nil) + set_or_return(:private_key, arg, + :kind_of => String) + end + + def delete_public_key + @public_key = nil + end + + def create_key(arg=nil) + raise Chef::Exceptions::InvalidKeyAttribute, "you cannot set create_key to true if the public_key field exists" if arg == true && !@public_key.nil? + set_or_return(:create_key, arg, + :kind_of => [TrueClass, FalseClass]) + end + + def expiration_date(arg=nil) + set_or_return(:expiration_date, arg, + :regex => /^(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z|infinity)$/) + end + + def to_hash + result = { + @actor_field_name => @actor + } + result["name"] = @name if @name + result["public_key"] = @public_key if @public_key + result["private_key"] = @private_key if @private_key + result["expiration_date"] = @expiration_date if @expiration_date + result["create_key"] = @create_key if @create_key + result + end + + def to_json(*a) + Chef::JSONCompat.to_json(to_hash, *a) + end + + def create + # if public_key is undefined and create_key is false, we cannot create + if @public_key.nil? && !@create_key + raise Chef::Exceptions::MissingKeyAttribute, "either public_key must be defined or create_key must be true" + end + + # defaults the key name to the fingerprint of the key + if @name.nil? + # if they didn't pass a public_key, + #then they must supply a name because we can't generate a fingerprint + unless @public_key.nil? + @name = fingerprint + else + raise Chef::Exceptions::MissingKeyAttribute, "a name cannot be auto-generated if no public key passed, either pass a public key or supply a name" + end + end + + payload = {"name" => @name} + payload['public_key'] = @public_key unless @public_key.nil? + payload['create_key'] = @create_key if @create_key + payload['expiration_date'] = @expiration_date unless @expiration_date.nil? + new_key = chef_rest.post_rest("#{api_base}/#{@actor}/keys", payload) + Chef::Key.from_hash(new_key) + end + + def fingerprint + self.class.generate_fingerprint(@public_key) + end + + def update + if @name.nil? + raise Chef::Exceptions::MissingKeyAttribute, "the name field must be populated when update is called" + end + + new_key = chef_rest.put_rest("#{api_base}/#{@actor}/keys/#{@name}", to_hash) + Chef::Key.from_hash(self.to_hash.merge(new_key)) + end + + def save + create + rescue Net::HTTPServerException => e + if e.response.code == "409" + update + else + raise e + end + end + + def destroy + if @name.nil? + raise Chef::Exceptions::MissingKeyAttribute, "the name field must be populated when delete is called" + end + + chef_rest.delete_rest("#{api_base}/#{@actor}/keys/#{@name}") + end + + # Class methods + def self.from_hash(key_hash) + if key_hash.has_key?("user") + key = Chef::Key.new(key_hash["user"], "user") + else + key = Chef::Key.new(key_hash["client"], "client") + end + key.name key_hash['name'] if key_hash.key?('name') + key.public_key key_hash['public_key'] if key_hash.key?('public_key') + key.private_key key_hash['private_key'] if key_hash.key?('private_key') + key.create_key key_hash['create_key'] if key_hash.key?('create_key') + key.expiration_date key_hash['expiration_date'] if key_hash.key?('expiration_date') + key + end + + def self.from_json(json) + Chef::Key.from_hash(Chef::JSONCompat.from_json(json)) + end + + class << self + alias_method :json_create, :from_json + end + + def self.list_by_user(actor, inflate=false) + keys = Chef::REST.new(Chef::Config[:chef_server_root]).get_rest("users/#{actor}/keys") + self.list(keys, actor, :load_by_user, inflate) + end + + def self.list_by_client(actor, inflate=false) + keys = Chef::REST.new(Chef::Config[:chef_server_url]).get_rest("clients/#{actor}/keys") + self.list(keys, actor, :load_by_client, inflate) + end + + def self.load_by_user(actor, key_name) + response = Chef::REST.new(Chef::Config[:chef_server_root]).get_rest("users/#{actor}/keys/#{key_name}") + Chef::Key.from_hash(response) + end + + def self.load_by_client(actor, key_name) + response = Chef::REST.new(Chef::Config[:chef_server_url]).get_rest("clients/#{actor}/keys/#{key_name}") + Chef::Key.from_hash(response) + end + + def self.generate_fingerprint(public_key) + openssl_key_object = OpenSSL::PKey::RSA.new(public_key) + data_string = OpenSSL::ASN1::Sequence([ + OpenSSL::ASN1::Integer.new(openssl_key_object.public_key.n), + OpenSSL::ASN1::Integer.new(openssl_key_object.public_key.e) + ]) + OpenSSL::Digest::SHA1.hexdigest(data_string.to_der).scan(/../).join(':') + end + + private + + def self.list(keys, actor, load_method_symbol, inflate) + if inflate + keys.inject({}) do |key_map, result| + name = result["name"] + key_map[name] = Chef::Key.send(load_method_symbol, actor, name) + key_map + end + else + keys + end + end + end +end diff --git a/lib/chef/knife/bootstrap/templates/chef-full.erb b/lib/chef/knife/bootstrap/templates/chef-full.erb index a87ab8e544..335b1f181c 100644 --- a/lib/chef/knife/bootstrap/templates/chef-full.erb +++ b/lib/chef/knife/bootstrap/templates/chef-full.erb @@ -1,15 +1,16 @@ -bash -c ' +sh -c ' <%= "export https_proxy=\"#{knife_config[:bootstrap_proxy]}\"" if knife_config[:bootstrap_proxy] -%> -distro=`uname -s` - -if test "x$distro" = "xSunOS"; then - if test -d "/usr/sfw/bin"; then - PATH=/usr/sfw/bin:$PATH - export PATH - fi +if test "x$TMPDIR" = "x"; then + tmp="/tmp" +else + tmp=$TMPDIR fi +# secure-ish temp dir creation without having mktemp available (DDoS-able but not exploitable) +tmp_dir="$tmp/install.sh.$$" +(umask 077 && mkdir $tmp_dir) || exit 1 + exists() { if command -v $1 &>/dev/null then @@ -19,41 +20,183 @@ exists() { fi } +http_404_error() { + echo "ERROR 404: Could not retrieve a valid install.sh!" + exit 1 +} + +capture_tmp_stderr() { + # spool up /tmp/stderr from all the commands we called + if test -f "$tmp_dir/stderr"; then + output=`cat $tmp_dir/stderr` + stderr_results="${stderr_results}\nSTDERR from $1:\n\n$output\n" + rm $tmp_dir/stderr + fi +} + +# do_wget URL FILENAME +do_wget() { + echo "trying wget..." + wget <%= "--proxy=on " if knife_config[:bootstrap_proxy] %> <%= knife_config[:bootstrap_wget_options] %> -O "$2" "$1" 2>$tmp_dir/stderr + rc=$? + # check for 404 + grep "ERROR 404" $tmp_dir/stderr 2>&1 >/dev/null + if test $? -eq 0; then + http_404_error + fi + + # check for bad return status or empty output + if test $rc -ne 0 || test ! -s "$2"; then + capture_tmp_stderr "wget" + return 1 + fi + + return 0 +} + +# do_curl URL FILENAME +do_curl() { + echo "trying curl..." + curl -sL <%= "--proxy \"#{knife_config[:bootstrap_proxy]}\" " if knife_config[:bootstrap_proxy] %> <%= knife_config[:bootstrap_curl_options] %> -D $tmp_dir/stderr -o "$2" "$1" 2>$tmp_dir/stderr + rc=$? + # check for 404 + grep "404 Not Found" $tmp_dir/stderr 2>&1 >/dev/null + if test $? -eq 0; then + http_404_error + fi + + # check for bad return status or empty output + if test $rc -ne 0 || test ! -s "$2"; then + capture_tmp_stderr "curl" + return 1 + fi + + return 0 +} + +# do_fetch URL FILENAME +do_fetch() { + echo "trying fetch..." + fetch -o "$2" "$1" 2>$tmp_dir/stderr + # check for bad return status + test $? -ne 0 && return 1 + return 0 +} + +# do_perl URL FILENAME +do_perl() { + echo "trying perl..." + perl -e "use LWP::Simple; getprint(shift @ARGV);" "$1" > "$2" 2>$tmp_dir/stderr + rc=$? + # check for 404 + grep "404 Not Found" $tmp_dir/stderr 2>&1 >/dev/null + if test $? -eq 0; then + http_404_error + fi + + # check for bad return status or empty output + if test $rc -ne 0 || test ! -s "$2"; then + capture_tmp_stderr "perl" + return 1 + fi + + return 0 +} + +# do_python URL FILENAME +do_python() { + echo "trying python..." + python -c "import sys,urllib2 ; sys.stdout.write(urllib2.urlopen(sys.argv[1]).read())" "$1" > "$2" 2>$tmp_dir/stderr + rc=$? + # check for 404 + grep "HTTP Error 404" $tmp_dir/stderr 2>&1 >/dev/null + if test $? -eq 0; then + http_404_error + fi + + # check for bad return status or empty output + if test $rc -ne 0 || test ! -s "$2"; then + capture_tmp_stderr "python" + return 1 + fi + return 0 +} + +# do_download URL FILENAME +do_download() { + PATH=/opt/local/bin:/usr/local/sbin:/usr/local/bin:/usr/sfw/bin:/sbin:/bin:/usr/sbin:/usr/bin + export PATH + + echo "downloading $1" + echo " to file $2" + + # we try all of these until we get success. + # perl, in particular may be present but LWP::Simple may not be installed + + if exists wget; then + do_wget $1 $2 && return 0 + fi + + if exists curl; then + do_curl $1 $2 && return 0 + fi + + if exists fetch; then + do_fetch $1 $2 && return 0 + fi + + if exists perl; then + do_perl $1 $2 && return 0 + fi + + if exists python; then + do_python $1 $2 && return 0 + fi + + echo ">>>>>> wget, curl, fetch, perl, or python not found on this instance." + + if test "x$stderr_results" != "x"; then + echo "\nDEBUG OUTPUT FOLLOWS:\n$stderr_results" + fi + + return 16 +} + <% if knife_config[:bootstrap_install_command] %> <%= knife_config[:bootstrap_install_command] %> <% else %> install_sh="<%= knife_config[:bootstrap_url] ? knife_config[:bootstrap_url] : "https://www.opscode.com/chef/install.sh" %>" if ! exists /usr/bin/chef-client; then - echo "Installing Chef Client..." - if exists wget; then - bash <(wget <%= "--proxy=on " if knife_config[:bootstrap_proxy] %> <%= knife_config[:bootstrap_wget_options] %> ${install_sh} -O -) <%= latest_current_chef_version_string %> - elif exists curl; then - bash <(curl -L <%= "--proxy \"#{knife_config[:bootstrap_proxy]}\" " if knife_config[:bootstrap_proxy] %> <%= knife_config[:bootstrap_curl_options] %> ${install_sh}) <%= latest_current_chef_version_string %> - else - echo "Neither wget nor curl found. Please install one and try again." >&2 - exit 1 - fi + echo "-----> Installing Chef Omnibus (<%= latest_current_chef_version_string %>)" + do_download ${install_sh} $tmp_dir/install.sh + sh $tmp_dir/install.sh -P chef <%= latest_current_chef_version_string %> + else + echo "-----> Existing Chef installation detected" fi <% end %> +if test "x$tmp_dir" != "x"; then + rm -r "$tmp_dir" +fi + mkdir -p /etc/chef <% if client_pem -%> -cat > /etc/chef/client.pem <<'EOP' +cat > /etc/chef/client.pem <<EOP <%= ::File.read(::File.expand_path(client_pem)) %> EOP chmod 0600 /etc/chef/client.pem <% end -%> <% if validation_key -%> -cat > /etc/chef/validation.pem <<'EOP' +cat > /etc/chef/validation.pem <<EOP <%= validation_key %> EOP chmod 0600 /etc/chef/validation.pem <% end -%> <% if encrypted_data_bag_secret -%> -cat > /etc/chef/encrypted_data_bag_secret <<'EOP' +cat > /etc/chef/encrypted_data_bag_secret <<EOP <%= encrypted_data_bag_secret %> EOP chmod 0600 /etc/chef/encrypted_data_bag_secret @@ -69,17 +212,17 @@ mkdir -p /etc/chef/trusted_certs mkdir -p /etc/chef/ohai/hints <% @chef_config[:knife][:hints].each do |name, hash| -%> -cat > /etc/chef/ohai/hints/<%= name %>.json <<'EOP' +cat > /etc/chef/ohai/hints/<%= name %>.json <<EOP <%= Chef::JSONCompat.to_json(hash) %> EOP <% end -%> <% end -%> -cat > /etc/chef/client.rb <<'EOP' +cat > /etc/chef/client.rb <<EOP <%= config_content %> EOP -cat > /etc/chef/first-boot.json <<'EOP' +cat > /etc/chef/first-boot.json <<EOP <%= Chef::JSONCompat.to_json(first_boot) %> EOP diff --git a/lib/chef/mixin/provides.rb b/lib/chef/mixin/provides.rb new file mode 100644 index 0000000000..e5bb2c2005 --- /dev/null +++ b/lib/chef/mixin/provides.rb @@ -0,0 +1,32 @@ + +require 'chef/mixin/descendants_tracker' + +class Chef + module Mixin + module Provides + include Chef::Mixin::DescendantsTracker + + def node_map + @node_map ||= Chef::NodeMap.new + end + + def provides(short_name, opts={}, &block) + if !short_name.kind_of?(Symbol) + # YAGNI: this is probably completely unnecessary and can be removed? + Chef::Log.deprecation "Passing a non-Symbol to Chef::Resource#provides will be removed" + if short_name.kind_of?(String) + short_name.downcase! + short_name.gsub!(/\s/, "_") + end + short_name = short_name.to_sym + end + node_map.set(short_name, true, opts, &block) + end + + # provides a node on the resource (early binding) + def provides?(node, resource_name) + node_map.get(node, resource_name) + end + end + end +end diff --git a/lib/chef/platform/provider_priority_map.rb b/lib/chef/platform/provider_priority_map.rb index 2517f46dfd..1539f61900 100644 --- a/lib/chef/platform/provider_priority_map.rb +++ b/lib/chef/platform/provider_priority_map.rb @@ -1,6 +1,4 @@ -require 'chef/providers' - class Chef class Platform class ProviderPriorityMap @@ -10,7 +8,22 @@ class Chef load_default_map end + def get_priority_array(node, resource_name) + priority_map.get(node, resource_name.to_sym) + end + + def set_priority_array(resource_name, priority_array, *filter) + priority(resource_name.to_sym, priority_array.to_a, *filter) + end + + def priority(*args) + priority_map.set(*args) + end + + private + def load_default_map + require 'chef/providers' # # Linux @@ -71,13 +84,9 @@ class Chef end def priority_map + require 'chef/node_map' @priority_map ||= Chef::NodeMap.new end - - def priority(*args) - priority_map.set(*args) - end - end end end diff --git a/lib/chef/platform/resource_priority_map.rb b/lib/chef/platform/resource_priority_map.rb new file mode 100644 index 0000000000..fc43b3e7db --- /dev/null +++ b/lib/chef/platform/resource_priority_map.rb @@ -0,0 +1,37 @@ +class Chef + class Platform + class ResourcePriorityMap + include Singleton + + def initialize + load_default_map + end + + def get_priority_array(node, resource_name) + priority_map.get(node, resource_name.to_sym) + end + + def set_priority_array(resource_name, priority_array, *filter) + priority resource_name.to_sym, priority_array.to_a, *filter + end + + def priority(*args) + priority_map.set(*args) + end + + private + + def load_default_map + require 'chef/resources' + + # MacOSX + priority :package, Chef::Resource::HomebrewPackage, os: "darwin" + end + + def priority_map + require 'chef/node_map' + @priority_map ||= Chef::NodeMap.new + end + end + end +end diff --git a/lib/chef/policy_builder/expand_node_object.rb b/lib/chef/policy_builder/expand_node_object.rb index 2ee8a23258..524bdd95b1 100644 --- a/lib/chef/policy_builder/expand_node_object.rb +++ b/lib/chef/policy_builder/expand_node_object.rb @@ -24,6 +24,7 @@ require 'chef/rest' require 'chef/run_context' require 'chef/config' require 'chef/node' +require 'chef/chef_class' class Chef module PolicyBuilder @@ -54,6 +55,15 @@ class Chef @run_list_expansion = nil end + # This method injects the run_context and provider and resource priority + # maps into the Chef class. The run_context has to be injected here, the provider and + # resource maps could be moved if a better place can be found to do this work. + # + # @param run_context [Chef::RunContext] the run_context to inject + def setup_chef_class(run_context) + Chef.set_run_context(run_context) + end + def setup_run_context(specific_recipes=nil) if Chef::Config[:solo] Chef::Cookbook::FileVendor.fetch_from_disk(Chef::Config[:cookbook_path]) @@ -68,6 +78,10 @@ class Chef run_context = Chef::RunContext.new(node, cookbook_collection, @events) end + # TODO: this is really obviously not the place for this + # FIXME: need same edits + setup_chef_class(run_context) + # TODO: this is not the place for this. It should be in Runner or # CookbookCompiler or something. run_context.load(@run_list_expansion) diff --git a/lib/chef/policy_builder/policyfile.rb b/lib/chef/policy_builder/policyfile.rb index 0c7c07f9f3..ac25b549be 100644 --- a/lib/chef/policy_builder/policyfile.rb +++ b/lib/chef/policy_builder/policyfile.rb @@ -385,4 +385,3 @@ class Chef end end end - diff --git a/lib/chef/provider.rb b/lib/chef/provider.rb index 680fe9782f..65a56cf726 100644 --- a/lib/chef/provider.rb +++ b/lib/chef/provider.rb @@ -22,7 +22,7 @@ require 'chef/mixin/convert_to_class_name' require 'chef/mixin/enforce_ownership_and_permissions' require 'chef/mixin/why_run' require 'chef/mixin/shell_out' -require 'chef/mixin/descendants_tracker' +require 'chef/mixin/provides' require 'chef/platform/service_helpers' require 'chef/node_map' @@ -30,26 +30,11 @@ class Chef class Provider include Chef::Mixin::WhyRun include Chef::Mixin::ShellOut - extend Chef::Mixin::DescendantsTracker + extend Chef::Mixin::Provides - class << self - def node_map - @node_map ||= Chef::NodeMap.new - end - - def provides(resource_name, opts={}, &block) - node_map.set(resource_name.to_sym, true, opts, &block) - end - - # provides a node on the resource (early binding) - def provides?(node, resource) - node_map.get(node, resource.resource_name) - end - - # supports the given resource and action (late binding) - def supports?(resource, action) - true - end + # supports the given resource and action (late binding) + def self.supports?(resource, action) + true end attr_accessor :new_resource diff --git a/lib/chef/provider_resolver.rb b/lib/chef/provider_resolver.rb index d83a3e0468..867c3deca8 100644 --- a/lib/chef/provider_resolver.rb +++ b/lib/chef/provider_resolver.rb @@ -47,7 +47,7 @@ class Chef def enabled_handlers @enabled_handlers ||= providers.select do |klass| - klass.provides?(node, resource) + klass.provides?(node, resource.resource_name) end.sort {|a,b| a.to_s <=> b.to_s } end @@ -84,8 +84,13 @@ class Chef if handlers.count >= 2 # this magic stack ranks the providers by where they appear in the provider_priority_map, it is mostly used # to pick amongst N different ways to start init scripts on different debian/ubuntu systems. - priority_list = [ get_provider_priority_map(resource.resource_name, node) ].flatten.compact + priority_list = [ get_priority_array(node, resource.resource_name) ].flatten.compact handlers = handlers.sort_by { |x| i = priority_list.index x; i.nil? ? Float::INFINITY : i } + if priority_list.index(handlers.first).nil? + # if we had more than one and we picked one with a precidence of infinity that means that the resource_priority_map + # entry for this resource is missing -- we should probably raise here and force resolution of the ambiguity. + Chef::Log.warn "Ambiguous provider precedence: #{handlers}, please use Chef.set_provider_priority_array to provide determinism" + end handlers = [ handlers.first ] end @@ -106,12 +111,12 @@ class Chef end # dep injection hooks - def get_provider_priority_map(resource_name, node) - provider_priority_map.get(node, resource_name) + def get_priority_array(node, resource_name) + provider_priority_map.get_priority_array(node, resource_name) end def provider_priority_map - Chef::Platform::ProviderPriorityMap.instance.priority_map + Chef::Platform::ProviderPriorityMap.instance end end end diff --git a/lib/chef/resource.rb b/lib/chef/resource.rb index ea220b6c70..d934ec8c47 100644 --- a/lib/chef/resource.rb +++ b/lib/chef/resource.rb @@ -33,7 +33,7 @@ require 'chef/platform' require 'chef/resource/resource_notification' require 'chef/mixin/deprecation' -require 'chef/mixin/descendants_tracker' +require 'chef/mixin/provides' class Chef class Resource @@ -46,6 +46,7 @@ class Chef include Chef::DSL::PlatformIntrospection include Chef::DSL::RegistryHelper include Chef::DSL::RebootPending + extend Chef::Mixin::Provides # # The node the current Chef run is using. @@ -879,7 +880,6 @@ class Chef include Chef::Mixin::ConvertToClassName extend Chef::Mixin::ConvertToClassName - extend Chef::Mixin::DescendantsTracker # XXX: this is required for definition params inside of the scope of a # subresource to work correctly. @@ -1016,6 +1016,7 @@ class Chef end def provider_for_action(action) + require 'chef/provider_resolver' provider = Chef::ProviderResolver.new(node, self, action).resolve.new(self, run_context) provider.action = action provider @@ -1080,33 +1081,6 @@ class Chef end end - # Maps a short_name (and optionally a platform and version) to a - # Chef::Resource. This allows finer grained per platform resource - # attributes and the end of overloaded resource definitions - # (I'm looking at you Chef::Resource::Package) - # Ex: - # class WindowsFile < Chef::Resource - # provides :file, os: "linux", platform_family: "rhel", platform: "redhat" - # provides :file, os: "!windows - # provides :file, os: [ "linux", "aix" ] - # provides :file, os: "solaris2" do |node| - # node['platform_version'].to_f <= 5.11 - # end - # # ...other stuff - # end - # - def self.provides(short_name, opts={}, &block) - short_name_sym = short_name - if short_name.kind_of?(String) - # YAGNI: this is probably completely unnecessary and can be removed? - Chef::Log.warn "[DEPRECATION] Passing a String to Chef::Resource#provides will be removed" - short_name.downcase! - short_name.gsub!(/\s/, "_") - short_name_sym = short_name.to_sym - end - node_map.set(short_name_sym, constantize(self.name), opts, &block) - end - # Returns a resource based on a short_name and node # # ==== Parameters @@ -1116,16 +1090,12 @@ class Chef # === Returns # <Chef::Resource>:: returns the proper Chef::Resource class def self.resource_for_node(short_name, node) - klass = node_map.get(node, short_name) || - resource_matching_short_name(short_name) + require 'chef/resource_resolver' + klass = Chef::ResourceResolver.new(node, short_name).resolve raise Chef::Exceptions::NoSuchResourceType.new(short_name, node) if klass.nil? klass end - def self.node_map - @@node_map ||= NodeMap.new - end - # Returns the class of a Chef::Resource based on the short name # ==== Parameters # short_name<Symbol>:: short_name of the resource (ie :directory) @@ -1156,7 +1126,3 @@ class Chef end end end - -# We require this at the BOTTOM of this file to avoid circular requires (it is used -# at runtime but not load time) -require 'chef/provider_resolver' diff --git a/lib/chef/resource_resolver.rb b/lib/chef/resource_resolver.rb new file mode 100644 index 0000000000..ff9d7aeb9f --- /dev/null +++ b/lib/chef/resource_resolver.rb @@ -0,0 +1,101 @@ +# +# Author:: Lamont Granquist (<lamont@chef.io>) +# Copyright:: Copyright (c) 2015 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/platform/resource_priority_map' + +class Chef + class ResourceResolver + + attr_reader :node + attr_reader :resource + attr_reader :action + + def initialize(node, resource) + @node = node + @resource = resource + end + + # return a deterministically sorted list of Chef::Resource subclasses + def resources + @resources ||= Chef::Resource.descendants + end + + def resolve + maybe_dynamic_resource_resolution(resource) || + maybe_chef_platform_lookup(resource) + end + + # this cut looks at if the resource can handle the resource type on the node + def enabled_handlers + @enabled_handlers ||= + resources.select do |klass| + klass.provides?(node, resource) + end.sort {|a,b| a.to_s <=> b.to_s } + @enabled_handlers + end + + private + + # try dynamically finding a resource based on querying the resources to see what they support + def maybe_dynamic_resource_resolution(resource) + # log this so we know what resources will work for the generic resource on the node (early cut) + Chef::Log.debug "resources for generic #{resource} resource enabled on node include: #{enabled_handlers}" + + # if none of the resources specifically support the resource, we still need to pick one of the resources that are + # enabled on the node to handle the why-run use case. + handlers = enabled_handlers + + if handlers.count >= 2 + # this magic stack ranks the resources by where they appear in the resource_priority_map + priority_list = [ get_priority_array(node, resource) ].flatten.compact + handlers = handlers.sort_by { |x| i = priority_list.index x; i.nil? ? Float::INFINITY : i } + if priority_list.index(handlers.first).nil? + # if we had more than one and we picked one with a precidence of infinity that means that the resource_priority_map + # entry for this resource is missing -- we should probably raise here and force resolution of the ambiguity. + Chef::Log.warn "Ambiguous resource precedence: #{handlers}, please use Chef.set_resource_priority_array to provide determinism" + end + handlers = [ handlers.first ] + end + + Chef::Log.debug "resources that survived replacement include: #{handlers}" + + raise Chef::Exceptions::AmbiguousResourceResolution.new(resource, handlers) if handlers.count >= 2 + + Chef::Log.debug "dynamic resource resolver FAILED to resolve a resouce" if handlers.empty? + + return nil if handlers.empty? + + handlers[0] + end + + # try the old static lookup of resources by mangling name to resource klass + def maybe_chef_platform_lookup(resource) + Chef::Resource.resource_matching_short_name(resource) + end + + # dep injection hooks + def get_priority_array(node, resource_name) + resource_priority_map.get_priority_array(node, resource_name) + end + + def resource_priority_map + Chef::Platform::ResourcePriorityMap.instance + end + end +end diff --git a/lib/chef/search/query.rb b/lib/chef/search/query.rb index 8656e810db..6469a18c49 100644 --- a/lib/chef/search/query.rb +++ b/lib/chef/search/query.rb @@ -89,7 +89,7 @@ WARNDEP if block response["rows"].each { |row| block.call(row) if row } unless (response["start"] + response["rows"].length) >= response["total"] - args_h[:start] = response["start"] + (args_h[:rows] || 0) + args_h[:start] = response["start"] + response["rows"].length search(type, query, args_h, &block) end true |