diff options
Diffstat (limited to 'lib')
250 files changed, 6083 insertions, 2903 deletions
diff --git a/lib/chef/api_client.rb b/lib/chef/api_client.rb index ce9ceb312c..ad31fb7d7b 100644 --- a/lib/chef/api_client.rb +++ b/lib/chef/api_client.rb @@ -1,7 +1,7 @@ # -# Author:: Adam Jacob (<adam@opscode.com>) -# Author:: Nuo Yan (<nuo@opscode.com>) -# Copyright:: Copyright (c) 2008 Opscode, Inc. +# Author:: Adam Jacob (<adam@chef.io>) +# Author:: Nuo Yan (<nuo@chef.io>) +# Copyright:: Copyright (c) 2008, 2015 Chef Software, Inc. # License:: Apache License, Version 2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -23,12 +23,18 @@ require 'chef/mixin/from_file' require 'chef/mash' require 'chef/json_compat' require 'chef/search/query' +require 'chef/exceptions' +require 'chef/mixin/api_version_request_handling' +require 'chef/server_api' class Chef class ApiClient include Chef::Mixin::FromFile include Chef::Mixin::ParamsValidate + include Chef::Mixin::ApiVersionRequestHandling + + SUPPORTED_API_VERSIONS = [0,1] # Create a new Chef::ApiClient object. def initialize @@ -37,6 +43,25 @@ class Chef @private_key = nil @admin = false @validator = false + @create_key = nil + end + + def chef_rest_v0 + @chef_rest_v0 ||= Chef::ServerAPI.new(Chef::Config[:chef_server_url], {:api_version => "0"}) + end + + def chef_rest_v1 + @chef_rest_v1 ||= Chef::ServerAPI.new(Chef::Config[:chef_server_url], {:api_version => "1"}) + end + + # will default to the current version (Chef::Authenticator::DEFAULT_SERVER_API_VERSION) + def http_api + @http_api ||= Chef::REST.new(Chef::Config[:chef_server_url]) + end + + # will default to the current version (Chef::Authenticator::DEFAULT_SERVER_API_VERSION) + def self.http_api + Chef::REST.new(Chef::Config[:chef_server_url]) end # Gets or sets the client name. @@ -88,7 +113,8 @@ class Chef ) end - # Gets or sets the private key. + # Private key. The server will return it as a string. + # Set to true under API V0 to have the server regenerate the default key. # # @params [Optional String] The string representation of the private key. # @return [String] The current value. @@ -96,7 +122,19 @@ class Chef set_or_return( :private_key, arg, - :kind_of => [String, FalseClass] + :kind_of => [String, TrueClass, FalseClass] + ) + end + + # Used to ask server to generate key pair under api V1 + # + # @params [Optional True/False] Should be true or false - default is false. + # @return [True/False] The current value + def create_key(arg=nil) + set_or_return( + :create_key, + arg, + :kind_of => [ TrueClass, FalseClass ] ) end @@ -107,13 +145,14 @@ class Chef def to_hash result = { "name" => @name, - "public_key" => @public_key, "validator" => @validator, "admin" => @admin, 'json_class' => self.class.name, "chef_type" => "client" } - result["private_key"] = @private_key if @private_key + result["private_key"] = @private_key unless @private_key.nil? + result["public_key"] = @public_key unless @public_key.nil? + result["create_key"] = @create_key unless @create_key.nil? result end @@ -127,10 +166,11 @@ class Chef def self.from_hash(o) client = Chef::ApiClient.new client.name(o["name"] || o["clientname"]) - client.private_key(o["private_key"]) if o.key?("private_key") - client.public_key(o["public_key"]) client.admin(o["admin"]) client.validator(o["validator"]) + client.private_key(o["private_key"]) if o.key?("private_key") + client.public_key(o["public_key"]) if o.key?("public_key") + client.create_key(o["create_key"]) if o.key?("create_key") client end @@ -142,10 +182,6 @@ class Chef from_hash(Chef::JSONCompat.parse(j)) end - def self.http_api - Chef::REST.new(Chef::Config[:chef_server_url]) - end - def self.reregister(name) api_client = load(name) api_client.reregister @@ -182,11 +218,11 @@ class Chef # Save this client via the REST API, returns a hash including the private key def save begin - http_api.put("clients/#{name}", { :name => self.name, :admin => self.admin, :validator => self.validator}) + update rescue Net::HTTPServerException => e # If that fails, go ahead and try and update it if e.response.code == "404" - http_api.post("clients", {:name => self.name, :admin => self.admin, :validator => self.validator }) + create else raise e end @@ -194,18 +230,95 @@ class Chef end def reregister - reregistered_self = http_api.put("clients/#{name}", { :name => name, :admin => admin, :validator => validator, :private_key => true }) + # Try API V0 and if it fails due to V0 not being supported, raise the proper error message. + # reregister only supported in API V0 or lesser. + reregistered_self = chef_rest_v0.put("clients/#{name}", { :name => name, :admin => admin, :validator => validator, :private_key => true }) if reregistered_self.respond_to?(:[]) private_key(reregistered_self["private_key"]) else private_key(reregistered_self.private_key) end self + rescue Net::HTTPServerException => e + # if there was a 406 related to versioning, give error explaining that + # only API version 0 is supported for reregister command + if e.response.code == "406" && e.response["x-ops-server-api-version"] + version_header = Chef::JSONCompat.from_json(e.response["x-ops-server-api-version"]) + min_version = version_header["min_version"] + max_version = version_header["max_version"] + error_msg = reregister_only_v0_supported_error_msg(max_version, min_version) + raise Chef::Exceptions::OnlyApiVersion0SupportedForAction.new(error_msg) + else + raise e + end + end + + # Updates the client via the REST API + def update + # NOTE: API V1 dropped support for updating client keys via update (aka PUT), + # but this code never supported key updating in the first place. Since + # it was never implemented, we will simply ignore that functionality + # as it is being deprecated. + # Delete this comment after V0 support is dropped. + payload = { :name => name } + payload[:validator] = validator unless validator.nil? + + # DEPRECATION + # This field is ignored in API V1, but left for backwards-compat, + # can remove after API V0 is no longer supported. + payload[:admin] = admin unless admin.nil? + + begin + new_client = chef_rest_v1.put("clients/#{name}", payload) + rescue Net::HTTPServerException => e + # rescue API V0 if 406 and the server supports V0 + supported_versions = server_client_api_version_intersection(e, SUPPORTED_API_VERSIONS) + raise e unless supported_versions && supported_versions.include?(0) + new_client = chef_rest_v0.put("clients/#{name}", payload) + end + + new_client end # Create the client via the REST API def create - http_api.post("clients", self) + payload = { + :name => name, + :validator => validator, + # this field is ignored in API V1, but left for backwards-compat, + # can remove after OSC 11 support is finished? + :admin => admin + } + begin + # try API V1 + raise Chef::Exceptions::InvalidClientAttribute, "You cannot set both public_key and create_key for create." if !create_key.nil? && !public_key.nil? + + payload[:public_key] = public_key unless public_key.nil? + payload[:create_key] = create_key unless create_key.nil? + + new_client = chef_rest_v1.post("clients", payload) + + # get the private_key out of the chef_key hash if it exists + if new_client['chef_key'] + if new_client['chef_key']['private_key'] + new_client['private_key'] = new_client['chef_key']['private_key'] + end + new_client['public_key'] = new_client['chef_key']['public_key'] + new_client.delete('chef_key') + end + + rescue Net::HTTPServerException => e + # rescue API V0 if 406 and the server supports V0 + supported_versions = server_client_api_version_intersection(e, SUPPORTED_API_VERSIONS) + raise e unless supported_versions && supported_versions.include?(0) + + # under API V0, a key pair will always be created unless public_key is + # passed on initial POST + payload[:public_key] = public_key unless public_key.nil? + + new_client = chef_rest_v0.post("clients", payload) + end + Chef::ApiClient.from_hash(self.to_hash.merge(new_client)) end # As a string @@ -213,14 +326,5 @@ class Chef "client[#{@name}]" end - def inspect - "Chef::ApiClient name:'#{name}' admin:'#{admin.inspect}' validator:'#{validator}' " + - "public_key:'#{public_key}' private_key:'#{private_key}'" - end - - def http_api - @http_api ||= Chef::REST.new(Chef::Config[:chef_server_url]) - end - end end diff --git a/lib/chef/application.rb b/lib/chef/application.rb index 297e46ef3c..0563822ede 100644 --- a/lib/chef/application.rb +++ b/lib/chef/application.rb @@ -93,7 +93,6 @@ class Chef if config[:config_file].nil? Chef::Log.warn("No config file found or specified on command line, using command line options.") elsif config_fetcher.config_missing? - pp config_missing: true Chef::Log.warn("*****************************************") Chef::Log.warn("Did not find config file: #{config[:config_file]}, using command line options.") Chef::Log.warn("*****************************************") diff --git a/lib/chef/application/client.rb b/lib/chef/application/client.rb index bd19afa914..409680b553 100644 --- a/lib/chef/application/client.rb +++ b/lib/chef/application/client.rb @@ -279,6 +279,12 @@ class Chef::Application::Client < Chef::Application Chef::Config[:chef_server_url] = config[:chef_server_url] if config.has_key? :chef_server_url Chef::Config.local_mode = config[:local_mode] if config.has_key?(:local_mode) + + if Chef::Config.has_key?(:chef_repo_path) && Chef::Config.chef_repo_path.nil? + Chef::Config.delete(:chef_repo_path) + Chef::Log.warn "chef_repo_path was set in a config file but was empty. Assuming #{Chef::Config.chef_repo_path}" + end + if Chef::Config.local_mode && !Chef::Config.has_key?(:cookbook_path) && !Chef::Config.has_key?(:chef_repo_path) Chef::Config.chef_repo_path = Chef::Config.find_chef_repo_path(Dir.pwd) end diff --git a/lib/chef/audit/audit_reporter.rb b/lib/chef/audit/audit_reporter.rb index a4f84ed7eb..d952d8a249 100644 --- a/lib/chef/audit/audit_reporter.rb +++ b/lib/chef/audit/audit_reporter.rb @@ -34,6 +34,7 @@ class Chef @rest_client = rest_client # Ruby 1.9.3 and above "enumerate their values in the order that the corresponding keys were inserted." @ordered_control_groups = Hash.new + @audit_phase_error = nil end def run_context @@ -46,7 +47,7 @@ class Chef @run_status = run_status end - def audit_phase_complete + def audit_phase_complete(audit_output) Chef::Log.debug("Audit Reporter completed successfully without errors.") ordered_control_groups.each do |name, control_group| audit_data.add_control_group(control_group) @@ -57,8 +58,9 @@ class Chef # that runs tests - normal errors are interpreted as EXAMPLE failures and captured. # We still want to send available audit information to the server so we process the # known control groups. - def audit_phase_failed(error) + def audit_phase_failed(error, audit_output) # The stacktrace information has already been logged elsewhere + @audit_phase_error = error Chef::Log.debug("Audit Reporter failed.") ordered_control_groups.each do |name, control_group| audit_data.add_control_group(control_group) @@ -70,7 +72,9 @@ class Chef end def run_failed(error) - post_auditing_data(error) + # Audit phase errors are captured when audit_phase_failed gets called. + # The error passed here isn't relevant to auditing, so we ignore it. + post_auditing_data end def control_group_started(name) @@ -98,7 +102,7 @@ class Chef private - def post_auditing_data(error = nil) + def post_auditing_data unless auditing_enabled? Chef::Log.debug("Audit Reports are disabled. Skipping sending reports.") return @@ -116,8 +120,10 @@ class Chef Chef::Log.debug("Sending audit report (run-id: #{audit_data.run_id})") run_data = audit_data.to_hash - if error - run_data[:error] = "#{error.class.to_s}: #{error.message}\n#{error.backtrace.join("\n")}" + if @audit_phase_error + error_info = "#{@audit_phase_error.class}: #{@audit_phase_error.message}" + error_info << "\n#{@audit_phase_error.backtrace.join("\n")}" if @audit_phase_error.backtrace + run_data[:error] = error_info end Chef::Log.debug "Audit Report:\n#{Chef::JSONCompat.to_json_pretty(run_data)}" @@ -163,7 +169,6 @@ class Chef def iso8601ify(time) time.utc.iso8601.to_s end - end end end diff --git a/lib/chef/audit/logger.rb b/lib/chef/audit/logger.rb new file mode 100644 index 0000000000..e46f54e582 --- /dev/null +++ b/lib/chef/audit/logger.rb @@ -0,0 +1,36 @@ +# +# 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 'stringio' + +class Chef + class Audit + class Logger + def self.puts(message="") + @buffer ||= StringIO.new + @buffer.puts(message) + + Chef::Log.info(message) + end + + def self.read_buffer + return "" if @buffer.nil? + @buffer.string + end + end + end +end diff --git a/lib/chef/audit/runner.rb b/lib/chef/audit/runner.rb index 801bf5ee58..234d83ab8f 100644 --- a/lib/chef/audit/runner.rb +++ b/lib/chef/audit/runner.rb @@ -16,6 +16,8 @@ # limitations under the License. # +require 'chef/audit/logger' + class Chef class Audit class Runner @@ -115,8 +117,8 @@ class Chef # the output stream to be changed for a formatter once the formatter has # been added. def set_streams - RSpec.configuration.output_stream = Chef::Config[:log_location] - RSpec.configuration.error_stream = Chef::Config[:log_location] + RSpec.configuration.output_stream = Chef::Audit::Logger + RSpec.configuration.error_stream = Chef::Audit::Logger end # Add formatters which we use to diff --git a/lib/chef/chef_class.rb b/lib/chef/chef_class.rb index d3f7ee55c7..f1dd797c04 100644 --- a/lib/chef/chef_class.rb +++ b/lib/chef/chef_class.rb @@ -26,6 +26,9 @@ # 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. +require 'chef/platform/provider_priority_map' +require 'chef/platform/resource_priority_map' + class Chef class << self @@ -33,50 +36,74 @@ class Chef # 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 + result = provider_priority_map.get_priority_array(node, resource_name) + result = result.dup if result + result 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 + result = resource_priority_map.get_priority_array(node, resource_name) + result = result.dup if result + result 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 priority_array [Class, Array<Class>] Class or 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 + # + def set_provider_priority_array(resource_name, priority_array, *filter, &block) + result = provider_priority_map.set_priority_array(resource_name, priority_array, *filter, &block) + result = result.dup if result + result 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 priority_array [Class, Array<Class>] Class or 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 + # + def set_resource_priority_array(resource_name, priority_array, *filter, &block) + result = resource_priority_map.set_priority_array(resource_name, priority_array, *filter, &block) + result = result.dup if result + result end # @@ -85,22 +112,27 @@ class Chef # *NOT* for public consumption ] # + # # Sets the resource_priority_map # - # @api private # @param resource_priority_map [Chef::Platform::ResourcePriorityMap] + # + # @api private 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] + # + # @api private def set_provider_priority_map(provider_priority_map) @provider_priority_map = provider_priority_map end + # # Sets the node object # # @api private @@ -109,14 +141,17 @@ class Chef @node = node end + # # Sets the run_context object # - # @api private # @param run_context [Chef::RunContext] + # + # @api private def set_run_context(run_context) @run_context = run_context end + # # Resets the internal state # # @api private @@ -126,5 +161,21 @@ class Chef @provider_priority_map = nil @resource_priority_map = nil end + + # @api private + def provider_priority_map + @provider_priority_map ||= begin + # these slurp in the resource+provider world, so be exceedingly lazy about requiring them + Chef::Platform::ProviderPriorityMap.instance + end + end + # @api private + def resource_priority_map + @resource_priority_map ||= begin + Chef::Platform::ResourcePriorityMap.instance + end + end end + + reset! end diff --git a/lib/chef/client.rb b/lib/chef/client.rb index d04a3dbbd5..86e92585e3 100644 --- a/lib/chef/client.rb +++ b/lib/chef/client.rb @@ -50,6 +50,7 @@ require 'chef/run_lock' require 'chef/policy_builder' require 'chef/request_id' require 'chef/platform/rebooter' +require 'chef/mixin/deprecation' require 'ohai' require 'rbconfig' @@ -60,121 +61,273 @@ 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 + extend Chef::Mixin::Deprecation - # Clears all notifications for client run status events. - # Primarily for testing purposes. - def self.clear_notifications - @run_start_notifications = nil - @run_completed_successfully_notifications = nil - @run_failed_notifications = nil - end - - # The list of notifications to be run when the client run starts. - def self.run_start_notifications - @run_start_notifications ||= [] - end - - # The list of notifications to be run when the client run completes - # successfully. - def self.run_completed_successfully_notifications - @run_completed_successfully_notifications ||= [] - end - - # The list of notifications to be run when the client run fails. - def self.run_failed_notifications - @run_failed_notifications ||= [] - end - - # Add a notification for the 'client run started' event. The notification - # is provided as a block. The current Chef::RunStatus object will be passed - # to the notification_block when the event is triggered. - def self.when_run_starts(¬ification_block) - run_start_notifications << notification_block - end - - # Add a notification for the 'client run success' event. The notification - # is provided as a block. The current Chef::RunStatus object will be passed - # to the notification_block when the event is triggered. - def self.when_run_completes_successfully(¬ification_block) - run_completed_successfully_notifications << notification_block - end + # + # The status of the Chef run. + # + # @return [Chef::RunStatus] + # + attr_reader :run_status - # Add a notification for the 'client run failed' event. The notification - # is provided as a block. The current Chef::RunStatus is passed to the - # notification_block when the event is triggered. - def self.when_run_fails(¬ification_block) - run_failed_notifications << notification_block + # + # The node represented by this client. + # + # @return [Chef::Node] + # + def node + run_status.node end - - # Callback to fire notifications that the Chef run is starting - def run_started - self.class.run_start_notifications.each do |notification| - notification.call(run_status) - end - @events.run_started(run_status) + def node=(value) + run_status.node = value end - # Callback to fire notifications that the run completed successfully - def run_completed_successfully - success_handlers = self.class.run_completed_successfully_notifications - success_handlers.each do |notification| - notification.call(run_status) - end - end + # + # The ohai system used by this client. + # + # @return [Ohai::System] + # + attr_reader :ohai - # Callback to fire notifications that the Chef run failed - def run_failed - failure_handlers = self.class.run_failed_notifications - failure_handlers.each do |notification| - notification.call(run_status) - end - end + # + # The rest object used to communicate with the Chef server. + # + # @return [Chef::REST] + # + attr_reader :rest - attr_accessor :node - attr_accessor :ohai - attr_accessor :rest + # + # The runner used to converge. + # + # @return [Chef::Runner] + # attr_accessor :runner + # + # Extra node attributes that were applied to the node. + # + # @return [Hash] + # attr_reader :json_attribs - attr_reader :run_status + + # + # The event dispatcher for the Chef run, including any configured output + # formatters and event loggers. + # + # @return [EventDispatch::Dispatcher] + # + # @see Chef::Formatters + # @see Chef::Config#formatters + # @see Chef::Config#stdout + # @see Chef::Config#stderr + # @see Chef::Config#force_logger + # @see Chef::Config#force_formatter + # TODO add stdout, stderr, and default formatters to Chef::Config so the + # defaults aren't calculated here. Remove force_logger and force_formatter + # from this code. + # @see Chef::EventLoggers + # @see Chef::Config#disable_event_logger + # @see Chef::Config#event_loggers + # @see Chef::Config#event_handlers + # attr_reader :events + # # Creates a new Chef::Client. + # + # @param json_attribs [Hash] Node attributes to layer into the node when it is + # fetched. + # @param args [Hash] Options: + # @option args [Array<RunList::RunListItem>] :override_runlist A runlist to + # use instead of the node's embedded run list. + # @option args [Array<String>] :specific_recipes A list of recipe file paths + # to load after the run list has been loaded. + # def initialize(json_attribs=nil, args={}) @json_attribs = json_attribs || {} - @node = nil - @run_status = nil - @runner = nil @ohai = Ohai::System.new event_handlers = configure_formatters + configure_event_loggers event_handlers += Array(Chef::Config[:event_handlers]) @events = EventDispatch::Dispatcher.new(*event_handlers) + # TODO it seems like a bad idea to be deletin' other peoples' hashes. @override_runlist = args.delete(:override_runlist) @specific_recipes = args.delete(:specific_recipes) + @run_status = Chef::RunStatus.new(nil, events) if new_runlist = args.delete(:runlist) @json_attribs["run_list"] = new_runlist end + 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 + # + # Do a full run for this Chef::Client. + # + # Locks the run while doing its job. + # + # Fires run_start before doing anything and fires run_completed or + # run_failed when finished. Also notifies client listeners of run_started + # at the beginning of Compile, and run_completed_successfully or run_failed + # when all is complete. + # + # Phase 1: Setup + # -------------- + # Gets information about the system and the run we are doing. + # + # 1. Run ohai to collect system information. + # 2. Register / connect to the Chef server (unless in solo mode). + # 3. Retrieve the node (or create a new one). + # 4. Merge in json_attribs, Chef::Config.environment, and override_run_list. + # + # @see #run_ohai + # @see #load_node + # @see #build_node + # @see Chef::Config#lockfile + # @see Chef::RunLock#acquire + # + # Phase 2: Compile + # ---------------- + # Decides *what* we plan to converge by compiling recipes. + # + # 1. Sync required cookbooks to the local cache. + # 2. Load libraries from all cookbooks. + # 3. Load attributes from all cookbooks. + # 4. Load LWRPs from all cookbooks. + # 5. Load resource definitions from all cookbooks. + # 6. Load recipes in the run list. + # 7. Load recipes from the command line. + # + # @see #setup_run_context Syncs and compiles cookbooks. + # @see Chef::CookbookCompiler#compile + # + # Phase 3: Converge + # ----------------- + # Brings the system up to date. + # + # 1. Converge the resources built from recipes in Phase 2. + # 2. Save the node. + # 3. Reboot if we were asked to. + # + # @see #converge_and_save + # @see Chef::Runner + # + # Phase 4: Audit + # -------------- + # Runs 'control_group' audits in recipes. This entire section can be enabled or disabled with config. + # + # 1. 'control_group' DSL collects audits during Phase 2 + # 2. Audits are run using RSpec + # 3. Errors are collected and reported using the formatters + # + # @see #run_audits + # @see Chef::Audit::Runner#run + # + # @raise [Chef::Exceptions::RunFailedWrappingError] If converge or audit failed. + # + # @see Chef::Config#enforce_path_sanity + # @see Chef::Config#solo + # @see Chef::Config#audit_mode + # + # @return Always returns true. + # + def run + run_error = nil - Chef.set_provider_priority_map(Chef::Platform::ProviderPriorityMap.instance) - Chef.set_resource_priority_map(Chef::Platform::ResourcePriorityMap.instance) + runlock = RunLock.new(Chef::Config.lockfile) + # TODO feels like acquire should have its own block arg for this + runlock.acquire + # don't add code that may fail before entering this section to be sure to release lock + begin + runlock.save_pid + + 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 + + register unless Chef::Config[:solo] + + load_node + + build_node + + run_status.run_id = request_id + run_status.start_clock + Chef::Log.info("Starting Chef Run for #{node.name}") + run_started + + do_windows_admin_check + + run_context = setup_run_context + + if Chef::Config[:audit_mode] != :audit_only + converge_error = converge_and_save(run_context) + end + + if Chef::Config[:why_run] == true + # why_run should probably be renamed to why_converge + Chef::Log.debug("Not running controls in 'why_run' mode - this mode is used to see potential converge changes") + elsif Chef::Config[:audit_mode] != :disabled + audit_error = run_audits(run_context) + end + + # Raise converge_error so run_failed reporters/events are processed. + raise converge_error if converge_error + + run_status.stop_clock + Chef::Log.info("Chef Run complete in #{run_status.elapsed_time} seconds") + run_completed_successfully + events.run_completed(node) + + # rebooting has to be the last thing we do, no exceptions. + Chef::Platform::Rebooter.reboot_if_needed!(node) + rescue Exception => run_error + # CHEF-3336: Send the error first in case something goes wrong below and we don't know why + Chef::Log.debug("Re-raising exception: #{run_error.class} - #{run_error.message}\n#{run_error.backtrace.join("\n ")}") + # If we failed really early, we may not have a run_status yet. Too early for these to be of much use. + if run_status + run_status.stop_clock + run_status.exception = run_error + run_failed + end + events.run_failed(run_error) + ensure + Chef::RequestID.instance.reset_request_id + request_id = nil + @run_status = nil + run_context = nil + runlock.release + GC.start + end + + # Raise audit, converge, and other errors here so that we exit + # with the proper exit status code and everything gets raised + # as a RunFailedWrappingError + if run_error || converge_error || audit_error + error = if run_error == converge_error + Chef::Exceptions::RunFailedWrappingError.new(converge_error, audit_error) + else + Chef::Exceptions::RunFailedWrappingError.new(run_error, converge_error, audit_error) + end + error.fill_backtrace + Chef::Application.debug_stacktrace(error) + raise error + end + + true end + # + # Private API + # TODO make this stuff protected or private + # + + # @api private def configure_formatters formatters_for_run.map do |formatter_name, output_path| if output_path.nil? @@ -187,6 +340,7 @@ class Chef end end + # @api private def formatters_for_run if Chef::Config.formatters.empty? [default_formatter] @@ -195,6 +349,7 @@ class Chef end end + # @api private def default_formatter if (STDOUT.tty? && !Chef::Config[:force_logger]) || Chef::Config[:force_formatter] [:doc] @@ -203,6 +358,7 @@ class Chef end end + # @api private def configure_event_loggers if Chef::Config.disable_event_logger [] @@ -219,8 +375,9 @@ class Chef end end - # Resource repoters send event information back to the chef server for processing. - # Can only be called after we have a @rest object + # Resource reporters send event information back to the chef server for + # processing. Can only be called after we have a @rest object + # @api private def register_reporters [ Chef::ResourceReporter.new(rest), @@ -230,43 +387,123 @@ class Chef end end + # + # Callback to fire notifications that the Chef run is starting + # + # @api private + # + def run_started + self.class.run_start_notifications.each do |notification| + notification.call(run_status) + end + events.run_started(run_status) + end + + # + # Callback to fire notifications that the run completed successfully + # + # @api private + # + def run_completed_successfully + success_handlers = self.class.run_completed_successfully_notifications + success_handlers.each do |notification| + notification.call(run_status) + end + end + + # + # Callback to fire notifications that the Chef run failed + # + # @api private + # + def run_failed + failure_handlers = self.class.run_failed_notifications + failure_handlers.each do |notification| + notification.call(run_status) + end + end + + # # Instantiates a Chef::Node object, possibly loading the node's prior state - # when using chef-client. Delegates to policy_builder. Injects the built node - # into the Chef class. + # when using chef-client. Sets Chef.node to the new node. # # @return [Chef::Node] The node object for this Chef run + # + # @see Chef::PolicyBuilder#load_node + # + # @api private + # def load_node policy_builder.load_node - @node = policy_builder.node - Chef.set_node(@node) + run_status.node = policy_builder.node + Chef.set_node(policy_builder.node) node end - # Mutates the `node` object to prepare it for the chef run. Delegates to - # policy_builder + # + # Mutates the `node` object to prepare it for the chef run. # # @return [Chef::Node] The updated node object + # + # @see Chef::PolicyBuilder#build_node + # + # @api private + # def build_node policy_builder.build_node - @run_status = Chef::RunStatus.new(node, events) + run_status.node = node node end + # + # Sync cookbooks to local cache. + # + # TODO this appears to be unused. + # + # @see Chef::PolicyBuilder#sync_cookbooks + # + # @api private + # + def sync_cookbooks + policy_builder.sync_cookbooks + end + + # + # Sets up the run context. + # + # @see Chef::PolicyBuilder#setup_run_context + # + # @return The newly set up run context + # + # @api private def setup_run_context - run_context = policy_builder.setup_run_context(@specific_recipes) + run_context = policy_builder.setup_run_context(specific_recipes) assert_cookbook_path_not_empty(run_context) run_status.run_context = run_context run_context end - def sync_cookbooks - policy_builder.sync_cookbooks - end - + # + # The PolicyBuilder strategy for figuring out run list and cookbooks. + # + # @return [Chef::PolicyBuilder::Policyfile, Chef::PolicyBuilder::ExpandNodeObject] + # + # @api private + # def policy_builder - @policy_builder ||= Chef::PolicyBuilder.strategy.new(node_name, ohai.data, json_attribs, @override_runlist, events) + @policy_builder ||= Chef::PolicyBuilder.strategy.new(node_name, ohai.data, json_attribs, override_runlist, events) end + # + # Save the updated node to Chef. + # + # Does not save if we are in solo mode or using override_runlist. + # + # @see Chef::Node#save + # @see Chef::Config#solo + # + # @api private + # def save_updated_node if Chef::Config[:solo] # nothing to do @@ -274,16 +511,46 @@ class Chef Chef::Log.warn("Skipping final node save because override_runlist was given") else Chef::Log.debug("Saving the current state of node #{node_name}") - @node.save + node.save end end + # + # Run ohai plugins. Runs all ohai plugins unless minimal_ohai is specified. + # + # Sends the ohai_completed event when finished. + # + # @see Chef::EventDispatcher# + # @see Chef::Config#minimal_ohai + # + # @api private + # def run_ohai filter = Chef::Config[:minimal_ohai] ? %w[fqdn machinename hostname platform platform_version os os_version] : nil ohai.all_plugins(filter) - @events.ohai_completed(node) + events.ohai_completed(node) end + # + # Figure out the node name we are working with. + # + # It tries these, in order: + # - Chef::Config.node_name + # - ohai[:fqdn] + # - ohai[:machinename] + # - ohai[:hostname] + # + # If we are running against a server with authentication protocol < 1.0, we + # *require* authentication protocol version 1.1. + # + # @raise [Chef::Exceptions::CannotDetermineNodeName] If the node name is not + # set and cannot be determined via ohai. + # + # @see Chef::Config#node_name + # @see Chef::Config#authentication_protocol_version + # + # @api private + # def node_name name = Chef::Config[:node_name] || ohai[:fqdn] || ohai[:machinename] || ohai[:hostname] Chef::Config[:node_name] = name @@ -292,6 +559,8 @@ class Chef # node names > 90 bytes only work with authentication protocol >= 1.1 # see discussion in config.rb. + # TODO use a computed default in Chef::Config to determine this instead of + # setting it. if name.bytesize > 90 Chef::Config[:authentication_protocol_version] = "1.1" end @@ -300,46 +569,86 @@ class Chef end # - # === Returns - # rest<Chef::REST>:: returns Chef::REST connection object + # Determine our private key and set up the connection to the Chef server. + # + # Skips registration and fires the `skipping_registration` event if + # Chef::Config.client_key is unspecified or already exists. + # + # If Chef::Config.client_key does not exist, we register the client with the + # Chef server and fire the registration_start and registration_completed events. + # + # @return [Chef::REST] The server connection object. + # + # @see Chef::Config#chef_server_url + # @see Chef::Config#client_key + # @see Chef::ApiClient::Registration#run + # @see Chef::EventDispatcher#skipping_registration + # @see Chef::EventDispatcher#registration_start + # @see Chef::EventDispatcher#registration_completed + # @see Chef::EventDispatcher#registration_failed + # + # @api private + # def register(client_name=node_name, config=Chef::Config) if !config[:client_key] - @events.skipping_registration(client_name, config) + events.skipping_registration(client_name, config) Chef::Log.debug("Client key is unspecified - skipping registration") elsif File.exists?(config[:client_key]) - @events.skipping_registration(client_name, config) + events.skipping_registration(client_name, config) Chef::Log.debug("Client key #{config[:client_key]} is present - skipping registration") else - @events.registration_start(node_name, config) + events.registration_start(node_name, config) Chef::Log.info("Client key #{config[:client_key]} is not present - registering") Chef::ApiClient::Registration.new(node_name, config[:client_key]).run - @events.registration_completed + events.registration_completed end # We now have the client key, and should use it from now on. @rest = Chef::REST.new(config[:chef_server_url], client_name, config[:client_key]) register_reporters rescue Exception => e + # TODO this should probably only ever fire if we *started* registration. + # Move it to the block above. # TODO: munge exception so a semantic failure message can be given to the # user - @events.registration_failed(client_name, e, config) + events.registration_failed(client_name, e, config) raise end - # Converges the node. # - # === Returns - # The thrown exception, if there was one. If this returns nil the converge was successful. + # Converges all compiled resources. + # + # Fires the converge_start, converge_complete and converge_failed events. + # + # If the exception `:end_client_run_early` is thrown during convergence, it + # does not mark the run complete *or* failed, and returns `nil` + # + # @param run_context The run context. + # + # @return The thrown exception, if we are in audit mode. `nil` means the + # converge was successful or ended early. + # + # @raise Any converge exception, unless we are in audit mode, in which case + # we *return* the exception. + # + # @see Chef::Runner#converge + # @see Chef::Config#audit_mode + # @see Chef::EventDispatch#converge_start + # @see Chef::EventDispatch#converge_complete + # @see Chef::EventDispatch#converge_failed + # + # @api private + # def converge(run_context) converge_exception = nil catch(:end_client_run_early) do begin - @events.converge_start(run_context) + events.converge_start(run_context) Chef::Log.debug("Converging node #{node_name}") @runner = Chef::Runner.new(run_context) - runner.converge - @events.converge_complete + @runner.converge + events.converge_complete rescue Exception => e - @events.converge_failed(e) + events.converge_failed(e) raise e if Chef::Config[:audit_mode] == :disabled converge_exception = e end @@ -347,8 +656,28 @@ class Chef converge_exception end + # + # Converge the node via and then save it if successful. + # + # @param run_context The run context. + # + # @return The thrown exception, if we are in audit mode. `nil` means the + # converge was successful or ended early. + # + # @raise Any converge or node save exception, unless we are in audit mode, + # in which case we *return* the exception. + # + # @see #converge + # @see #save_updated_mode + # @see Chef::Config#audit_mode + # + # @api private + # # We don't want to change the old API on the `converge` method to have it perform # saving. So we wrap it in this method. + # TODO given this seems to be pretty internal stuff, how badly do we need to + # split this stuff up? + # def converge_and_save(run_context) converge_exception = converge(run_context) unless converge_exception @@ -362,37 +691,67 @@ class Chef converge_exception end + # + # Run the audit phase. + # + # Triggers the audit_phase_start, audit_phase_complete and + # audit_phase_failed events. + # + # @param run_context The run context. + # + # @return Any thrown exceptions. `nil` if successful. + # + # @see Chef::Audit::Runner#run + # @see Chef::EventDispatch#audit_phase_start + # @see Chef::EventDispatch#audit_phase_complete + # @see Chef::EventDispatch#audit_phase_failed + # + # @api private + # def run_audits(run_context) - audit_exception = nil begin - @events.audit_phase_start(run_status) + events.audit_phase_start(run_status) Chef::Log.info("Starting audit phase") auditor = Chef::Audit::Runner.new(run_context) auditor.run if auditor.failed? - raise Chef::Exceptions::AuditsFailed.new(auditor.num_failed, auditor.num_total) + audit_exception = Chef::Exceptions::AuditsFailed.new(auditor.num_failed, auditor.num_total) + @events.audit_phase_failed(audit_exception, Chef::Audit::Logger.read_buffer) + else + @events.audit_phase_complete(Chef::Audit::Logger.read_buffer) end - @events.audit_phase_complete rescue Exception => e Chef::Log.error("Audit phase failed with error message: #{e.message}") - @events.audit_phase_failed(e) + @events.audit_phase_failed(e, Chef::Audit::Logger.read_buffer) audit_exception = e end audit_exception end - # Expands the run list. Delegates to the policy_builder. # - # Normally this does not need to be called from here, it will be called by - # build_node. This is provided so external users (like the chefspec - # project) can inject custom behavior into the run process. + # Expands the run list. + # + # @return [Chef::RunListExpansion] The expanded run list. + # + # @see Chef::PolicyBuilder#expand_run_list # - # === Returns - # RunListExpansion: A RunListExpansion or API compatible object. def expanded_run_list policy_builder.expand_run_list end + # + # Check if the user has Administrator privileges on windows. + # + # Throws an error if the user is not an admin, and + # `Chef::Config.fatal_windows_admin_check` is true. + # + # @raise [Chef::Exceptions::WindowsNotAdmin] If the user is not an admin. + # + # @see Chef::platform#windows? + # @see Chef::Config#fatal_windows_admin_check + # + # @api private + # def do_windows_admin_check if Chef::Platform.windows? Chef::Log.debug("Checking for administrator privileges....") @@ -412,99 +771,121 @@ class Chef end end - # Do a full run for this Chef::Client. Calls: - # - # * run_ohai - Collect information about the system - # * build_node - Get the last known state, merge with local changes - # * register - If not in solo mode, make sure the server knows about this client - # * sync_cookbooks - If not in solo mode, populate the local cache with the node's cookbooks - # * converge - Bring this system up to date - # - # === Returns - # true:: Always returns true. - def run - runlock = RunLock.new(Chef::Config.lockfile) - runlock.acquire - # don't add code that may fail before entering this section to be sure to release lock - begin - runlock.save_pid - - 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 - - register unless Chef::Config[:solo] - - load_node - - build_node + # Notification registration + class<<self + # + # Add a listener for the 'client run started' event. + # + # @param notification_block The callback (takes |run_status| parameter). + # @yieldparam [Chef::RunStatus] run_status The run status. + # + def when_run_starts(¬ification_block) + run_start_notifications << notification_block + end - run_status.run_id = request_id - run_status.start_clock - Chef::Log.info("Starting Chef Run for #{node.name}") - run_started + # + # Add a listener for the 'client run success' event. + # + # @param notification_block The callback (takes |run_status| parameter). + # @yieldparam [Chef::RunStatus] run_status The run status. + # + def when_run_completes_successfully(¬ification_block) + run_completed_successfully_notifications << notification_block + end - do_windows_admin_check + # + # Add a listener for the 'client run failed' event. + # + # @param notification_block The callback (takes |run_status| parameter). + # @yieldparam [Chef::RunStatus] run_status The run status. + # + def when_run_fails(¬ification_block) + run_failed_notifications << notification_block + end - run_context = setup_run_context + # + # Clears all listeners for client run status events. + # + # Primarily for testing purposes. + # + # @api private + # + def clear_notifications + @run_start_notifications = nil + @run_completed_successfully_notifications = nil + @run_failed_notifications = nil + end - if Chef::Config[:audit_mode] != :audit_only - converge_error = converge_and_save(run_context) - end + # + # TODO These seem protected to me. + # + + # + # Listeners to be run when the client run starts. + # + # @return [Array<Proc>] + # + # @api private + # + def run_start_notifications + @run_start_notifications ||= [] + end - if Chef::Config[:why_run] == true - # why_run should probably be renamed to why_converge - Chef::Log.debug("Not running controls in 'why_run' mode - this mode is used to see potential converge changes") - elsif Chef::Config[:audit_mode] != :disabled - audit_error = run_audits(run_context) - end + # + # Listeners to be run when the client run completes successfully. + # + # @return [Array<Proc>] + # + # @api private + # + def run_completed_successfully_notifications + @run_completed_successfully_notifications ||= [] + end - if converge_error || audit_error - e = Chef::Exceptions::RunFailedWrappingError.new(converge_error, audit_error) - e.fill_backtrace - raise e - end + # + # Listeners to be run when the client run fails. + # + # @return [Array<Proc>] + # + # @api private + # + def run_failed_notifications + @run_failed_notifications ||= [] + end + end - run_status.stop_clock - Chef::Log.info("Chef Run complete in #{run_status.elapsed_time} seconds") - run_completed_successfully - @events.run_completed(node) + # + # 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. + # + # @api private + # + STDOUT_FD = STDOUT - # rebooting has to be the last thing we do, no exceptions. - Chef::Platform::Rebooter.reboot_if_needed!(node) + # + # 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. + # + # @api private + # + STDERR_FD = STDERR - true + # + # Deprecated writers + # - rescue Exception => e - # CHEF-3336: Send the error first in case something goes wrong below and we don't know why - Chef::Log.debug("Re-raising exception: #{e.class} - #{e.message}\n#{e.backtrace.join("\n ")}") - # If we failed really early, we may not have a run_status yet. Too early for these to be of much use. - if run_status - run_status.stop_clock - run_status.exception = e - run_failed - end - Chef::Application.debug_stacktrace(e) - @events.run_failed(e) - raise - ensure - Chef::RequestID.instance.reset_request_id - request_id = nil - @run_status = nil - run_context = nil - runlock.release - GC.start - end - true - end + include Chef::Mixin::Deprecation + deprecated_attr_writer :ohai, "There is no alternative. Leave ohai alone!" + deprecated_attr_writer :rest, "There is no alternative. Leave rest alone!" + deprecated_attr :runner, "There is no alternative. Leave runner alone!" private + attr_reader :override_runlist + attr_reader :specific_recipes + def empty_directory?(path) !File.exists?(path) || (Dir.entries(path).size <= 2) end @@ -536,7 +917,6 @@ class Chef Chef::ReservedNames::Win32::Security.has_admin_privileges? end - end end diff --git a/lib/chef/config.rb b/lib/chef/config.rb index d2d3c736c2..9beb18b53e 100644 --- a/lib/chef/config.rb +++ b/lib/chef/config.rb @@ -20,739 +20,34 @@ # limitations under the License. require 'chef/log' -require 'chef/exceptions' -require 'mixlib/config' -require 'chef/util/selinux' -require 'chef/util/path_helper' -require 'pathname' -require 'chef/mixin/shell_out' +require 'chef-config/logger' -class Chef - class Config - - extend Mixlib::Config - extend Chef::Mixin::ShellOut - - PathHelper = Chef::Util::PathHelper - - # Evaluates the given string as config. - # - # +filename+ is used for context in stacktraces, but doesn't need to be the name of an actual file. - def self.from_string(string, filename) - self.instance_eval(string, filename, 1) - end - - # Manages the chef secret session key - # === Returns - # <newkey>:: A new or retrieved session key - # - def self.manage_secret_key - newkey = nil - if Chef::FileCache.has_key?("chef_server_cookie_id") - newkey = Chef::FileCache.load("chef_server_cookie_id") - else - chars = ("a".."z").to_a + ("A".."Z").to_a + ("0".."9").to_a - newkey = "" - 40.times { |i| newkey << chars[rand(chars.size-1)] } - Chef::FileCache.store("chef_server_cookie_id", newkey) - end - newkey - end +# DI our logger into ChefConfig before we load the config. Some defaults are +# auto-detected, and this emits log messages on some systems, all of which will +# occur at require-time. So we need to set the logger first. +ChefConfig.logger = Chef::Log - def self.inspect - configuration.inspect - end +require 'chef-config/config' - def self.platform_specific_path(path) - path = PathHelper.cleanpath(path) - if Chef::Platform.windows? - # turns \etc\chef\client.rb and \var\chef\client.rb into C:/chef/client.rb - if env['SYSTEMDRIVE'] && path[0] == '\\' && path.split('\\')[2] == 'chef' - path = PathHelper.join(env['SYSTEMDRIVE'], path.split('\\', 3)[2]) - end - end - path - end - - def self.add_formatter(name, file_path=nil) - formatters << [name, file_path] - end +require 'chef/platform/query_helpers' - def self.add_event_logger(logger) - event_handlers << logger - end - - # Config file to load (client.rb, knife.rb, etc. defaults set differently in knife, chef-client, etc.) - configurable(:config_file) - - default(:config_dir) do - if config_file - PathHelper.dirname(config_file) - else - PathHelper.join(user_home, ".chef", "") - end - end - - default :formatters, [] - - # Override the config dispatch to set the value of multiple server options simultaneously - # - # === Parameters - # url<String>:: String to be set for all of the chef-server-api URL's - # - configurable(:chef_server_url).writes_value { |url| url.to_s.strip } - - # When you are using ActiveSupport, they monkey-patch 'daemonize' into Kernel. - # So while this is basically identical to what method_missing would do, we pull - # it up here and get a real method written so that things get dispatched - # properly. - configurable(:daemonize).writes_value { |v| v } - - # The root where all local chef object data is stored. cookbooks, data bags, - # environments are all assumed to be in separate directories under this. - # chef-solo uses these directories for input data. knife commands - # that upload or download files (such as knife upload, knife role from file, - # etc.) work. - default :chef_repo_path do - if self.configuration[:cookbook_path] - if self.configuration[:cookbook_path].kind_of?(String) - File.expand_path('..', self.configuration[:cookbook_path]) - else - self.configuration[:cookbook_path].map do |path| - File.expand_path('..', path) - end - end - else - cache_path - end - end - - def self.find_chef_repo_path(cwd) - # In local mode, we auto-discover the repo root by looking for a path with "cookbooks" under it. - # This allows us to run config-free. - path = cwd - until File.directory?(PathHelper.join(path, "cookbooks")) - new_path = File.expand_path('..', path) - if new_path == path - Chef::Log.warn("No cookbooks directory found at or above current directory. Assuming #{Dir.pwd}.") - return Dir.pwd - end - path = new_path - end - Chef::Log.info("Auto-discovered chef repository at #{path}") - path - end - - def self.derive_path_from_chef_repo_path(child_path) - if chef_repo_path.kind_of?(String) - PathHelper.join(chef_repo_path, child_path) - else - chef_repo_path.map { |path| PathHelper.join(path, child_path)} - end - end - - # Location of acls on disk. String or array of strings. - # Defaults to <chef_repo_path>/acls. - # Only applies to Enterprise Chef commands. - default(:acl_path) { derive_path_from_chef_repo_path('acls') } - - # Location of clients on disk. String or array of strings. - # Defaults to <chef_repo_path>/acls. - default(:client_path) { derive_path_from_chef_repo_path('clients') } - - # Location of cookbooks on disk. String or array of strings. - # Defaults to <chef_repo_path>/cookbooks. If chef_repo_path - # is not specified, this is set to [/var/chef/cookbooks, /var/chef/site-cookbooks]). - default(:cookbook_path) do - if self.configuration[:chef_repo_path] - derive_path_from_chef_repo_path('cookbooks') - else - Array(derive_path_from_chef_repo_path('cookbooks')).flatten + - Array(derive_path_from_chef_repo_path('site-cookbooks')).flatten - end - end - - # Location of containers on disk. String or array of strings. - # Defaults to <chef_repo_path>/containers. - # Only applies to Enterprise Chef commands. - default(:container_path) { derive_path_from_chef_repo_path('containers') } - - # Location of data bags on disk. String or array of strings. - # Defaults to <chef_repo_path>/data_bags. - default(:data_bag_path) { derive_path_from_chef_repo_path('data_bags') } - - # Location of environments on disk. String or array of strings. - # Defaults to <chef_repo_path>/environments. - default(:environment_path) { derive_path_from_chef_repo_path('environments') } - - # Location of groups on disk. String or array of strings. - # Defaults to <chef_repo_path>/groups. - # Only applies to Enterprise Chef commands. - default(:group_path) { derive_path_from_chef_repo_path('groups') } - - # Location of nodes on disk. String or array of strings. - # Defaults to <chef_repo_path>/nodes. - default(:node_path) { derive_path_from_chef_repo_path('nodes') } - - # Location of roles on disk. String or array of strings. - # Defaults to <chef_repo_path>/roles. - default(:role_path) { derive_path_from_chef_repo_path('roles') } - - # Location of users on disk. String or array of strings. - # Defaults to <chef_repo_path>/users. - # Does not apply to Enterprise Chef commands. - default(:user_path) { derive_path_from_chef_repo_path('users') } - - # Location of policies on disk. String or array of strings. - # Defaults to <chef_repo_path>/policies. - default(:policy_path) { derive_path_from_chef_repo_path('policies') } - - # Turn on "path sanity" by default. See also: http://wiki.opscode.com/display/chef/User+Environment+PATH+Sanity - default :enforce_path_sanity, true - - # Formatted Chef Client output is a beta feature, disabled by default: - default :formatter, "null" - - # The number of times the client should retry when registering with the server - default :client_registration_retries, 5 - - # An array of paths to search for knife exec scripts if they aren't in the current directory - default :script_path, [] - - # The root of all caches (checksums, cache and backup). If local mode is on, - # this is under the user's home directory. - default(:cache_path) do - if local_mode - PathHelper.join(config_dir, 'local-mode-cache') - else - primary_cache_root = platform_specific_path("/var") - primary_cache_path = platform_specific_path("/var/chef") - # Use /var/chef as the cache path only if that folder exists and we can read and write - # into it, or /var exists and we can read and write into it (we'll create /var/chef later). - # Otherwise, we'll create .chef under the user's home directory and use that as - # the cache path. - unless path_accessible?(primary_cache_path) || path_accessible?(primary_cache_root) - secondary_cache_path = PathHelper.join(user_home, '.chef') - Chef::Log.info("Unable to access cache at #{primary_cache_path}. Switching cache to #{secondary_cache_path}") - secondary_cache_path - else - primary_cache_path - end - end - end - - # Returns true only if the path exists and is readable and writeable for the user. - def self.path_accessible?(path) - File.exists?(path) && File.readable?(path) && File.writable?(path) - end - - # Where cookbook files are stored on the server (by content checksum) - default(:checksum_path) { PathHelper.join(cache_path, "checksums") } - - # Where chef's cache files should be stored - default(:file_cache_path) { PathHelper.join(cache_path, "cache") } - - # Where backups of chef-managed files should go - default(:file_backup_path) { PathHelper.join(cache_path, "backup") } - - # The chef-client (or solo) lockfile. - # - # If your `file_cache_path` resides on a NFS (or non-flock()-supporting - # fs), it's recommended to set this to something like - # '/tmp/chef-client-running.pid' - default(:lockfile) { PathHelper.join(file_cache_path, "chef-client-running.pid") } - - ## Daemonization Settings ## - # What user should Chef run as? - default :user, nil - default :group, nil - default :umask, 0022 - - # Valid log_levels are: - # * :debug - # * :info - # * :warn - # * :fatal - # These work as you'd expect. There is also a special `:auto` setting. - # When set to :auto, Chef will auto adjust the log verbosity based on - # context. When a tty is available (usually because the user is running chef - # in a console), the log level is set to :warn, and output formatters are - # used as the primary mode of output. When a tty is not available, the - # logger is the primary mode of output, and the log level is set to :info - default :log_level, :auto - - # Logging location as either an IO stream or string representing log file path - default :log_location, STDOUT - - # Using `force_formatter` causes chef to default to formatter output when STDOUT is not a tty - default :force_formatter, false - - # Using `force_logger` causes chef to default to logger output when STDOUT is a tty - default :force_logger, false - - default :http_retry_count, 5 - default :http_retry_delay, 5 - default :interval, nil - default :once, nil - default :json_attribs, nil - # toggle info level log items that can create a lot of output - default :verbose_logging, true - default :node_name, nil - default :diff_disabled, false - default :diff_filesize_threshold, 10000000 - default :diff_output_threshold, 1000000 - default :local_mode, false - - default :pid_file, nil - - # Whether Chef Zero local mode should bind to a port. All internal requests - # will go through the socketless code path regardless, so the socket is - # only needed if other processes will connect to the local mode server. - # - # For compatibility this is set to true but it will be changed to false in - # the future. - default :listen, true - - config_context :chef_zero do - config_strict_mode true - default(:enabled) { Chef::Config.local_mode } - default :host, 'localhost' - default :port, 8889.upto(9999) # Will try ports from 8889-9999 until one works - end - - default :chef_server_url, "https://localhost:443" - default(:chef_server_root) do - # if the chef_server_url is a path to an organization, aka - # 'some_url.../organizations/*' then remove the '/organization/*' by default - if self.configuration[:chef_server_url] =~ /\/organizations\/\S*$/ - self.configuration[:chef_server_url].split('/')[0..-3].join('/') - elsif self.configuration[:chef_server_url] # default to whatever chef_server_url is - self.configuration[:chef_server_url] - else - "https://localhost:443" - end - end - - default :rest_timeout, 300 - default :yum_timeout, 900 - default :yum_lock_timeout, 30 - default :solo, false - default :splay, nil - default :why_run, false - default :color, false - default :client_fork, true - default :ez, false - default :enable_reporting, true - default :enable_reporting_url_fatals, false - # Possible values for :audit_mode - # :enabled, :disabled, :audit_only, - # - # TODO: 11 Dec 2014: Currently audit-mode is an experimental feature - # and is disabled by default. When users choose to enable audit-mode, - # a warning is issued in application/client#reconfigure. - # This can be removed when audit-mode is enabled by default. - default :audit_mode, :disabled - - # Chef only needs ohai to run the hostname plugin for the most basic - # functionality. If the rest of the ohai plugins are not needed (like in - # most of our testing scenarios) - default :minimal_ohai, false - - # Policyfile is an experimental feature where a node gets its run list and - # cookbook version set from a single document on the server instead of - # expanding the run list and having the server compute the cookbook version - # set based on environment constraints. - # - # Because this feature is experimental, it is not recommended for - # production use. Developent/release of this feature may not adhere to - # semver guidelines. - default :use_policyfile, false - - # Set these to enable SSL authentication / mutual-authentication - # with the server - - # Client side SSL cert/key for mutual auth - default :ssl_client_cert, nil - default :ssl_client_key, nil - - # Whether or not to verify the SSL cert for all HTTPS requests. When set to - # :verify_peer (default), all HTTPS requests will be validated regardless of other - # SSL verification settings. When set to :verify_none no HTTPS requests will - # be validated. - default :ssl_verify_mode, :verify_peer - - # Whether or not to verify the SSL cert for HTTPS requests to the Chef - # server API. If set to `true`, the server's cert will be validated - # regardless of the :ssl_verify_mode setting. This is set to `true` when - # running in local-mode. - # NOTE: This is a workaround until verify_peer is enabled by default. - default(:verify_api_cert) { Chef::Config.local_mode } - - # Path to the default CA bundle files. - default :ssl_ca_path, nil - default(:ssl_ca_file) do - if Chef::Platform.windows? and embedded_path = embedded_dir - cacert_path = File.join(embedded_path, "ssl/certs/cacert.pem") - cacert_path if File.exist?(cacert_path) - else - nil - end - end - - # A directory that contains additional SSL certificates to trust. Any - # certificates in this directory will be added to whatever CA bundle ruby - # is using. Use this to add self-signed certs for your Chef Server or local - # HTTP file servers. - default(:trusted_certs_dir) { PathHelper.join(config_dir, "trusted_certs") } - - # Where should chef-solo download recipes from? - default :recipe_url, nil - - # Sets the version of the signed header authentication protocol to use (see - # the 'mixlib-authorization' project for more detail). Currently, versions - # 1.0 and 1.1 are available; however, the chef-server must first be - # upgraded to support version 1.1 before clients can begin using it. - # - # Version 1.1 of the protocol is required when using a `node_name` greater - # than ~90 bytes (~90 ascii characters), so chef-client will automatically - # switch to using version 1.1 when `node_name` is too large for the 1.0 - # protocol. If you intend to use large node names, ensure that your server - # supports version 1.1. Automatic detection of large node names means that - # users will generally not need to manually configure this. - # - # In the future, this configuration option may be replaced with an - # automatic negotiation scheme. - default :authentication_protocol_version, "1.0" - - # This key will be used to sign requests to the Chef server. This location - # must be writable by Chef during initial setup when generating a client - # identity on the server. - # - # The chef-server will look up the public key for the client using the - # `node_name` of the client. - # - # If chef-zero is enabled, this defaults to nil (no authentication). - default(:client_key) { chef_zero.enabled ? nil : platform_specific_path("/etc/chef/client.pem") } - - # When registering the client, should we allow the client key location to - # be a symlink? eg: /etc/chef/client.pem -> /etc/chef/prod-client.pem - # If the path of the key goes through a directory like /tmp this should - # never be set to true or its possibly an easily exploitable security hole. - default :follow_client_key_symlink, false - - # This secret is used to decrypt encrypted data bag items. - default(:encrypted_data_bag_secret) do - if File.exist?(platform_specific_path("/etc/chef/encrypted_data_bag_secret")) - platform_specific_path("/etc/chef/encrypted_data_bag_secret") - else - nil - end - end - - # As of Chef 11.0, version "1" is the default encrypted data bag item - # format. Version "2" is available which adds encrypt-then-mac protection. - # To maintain compatibility, versions other than 1 must be opt-in. - # - # Set this to `2` if you have chef-client 11.6.0+ in your infrastructure. - # Set this to `3` if you have chef-client 11.?.0+, ruby 2 and OpenSSL >= 1.0.1 in your infrastructure. (TODO) - default :data_bag_encrypt_version, 1 - - # When reading data bag items, any supported version is accepted. However, - # if all encrypted data bags have been generated with the version 2 format, - # it is recommended to disable support for earlier formats to improve - # security. For example, the version 2 format is identical to version 1 - # except for the addition of an HMAC, so an attacker with MITM capability - # could downgrade an encrypted data bag to version 1 as part of an attack. - default :data_bag_decrypt_minimum_version, 0 - - # If there is no file in the location given by `client_key`, chef-client - # will temporarily use the "validator" identity to generate one. If the - # `client_key` is not present and the `validation_key` is also not present, - # chef-client will not be able to authenticate to the server. - # - # The `validation_key` is never used if the `client_key` exists. - # - # If chef-zero is enabled, this defaults to nil (no authentication). - 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 send 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 `true`. Set to `false` to disable client-side key - # generation (server generates client keys). - default(:local_key_generation) { true } - - # 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. - # Leaving this set to nil or false is a security hazard! - default :zypper_check_gpg, nil - - # Report Handlers - default :report_handlers, [] +class Chef + Config = ChefConfig::Config - # Event Handlers - default :event_handlers, [] + # We re-open ChefConfig::Config to add additional settings. Generally, + # everything should go in chef-config so it's shared with whoever uses that. + # We make execeptions to that rule when: + # * The functionality isn't likely to be useful outside of Chef + # * The functionality makes use of a dependency we don't want to add to chef-config + class Config - default :disable_event_loggers, false default :event_loggers do evt_loggers = [] - if Chef::Platform::windows? and not Chef::Platform::windows_server_2003? + if ChefConfig.windows? and not Chef::Platform.windows_server_2003? evt_loggers << :win_evt end evt_loggers end - # Exception Handlers - default :exception_handlers, [] - - # Start handlers - default :start_handlers, [] - - # Syntax Check Cache. Knife keeps track of files that is has already syntax - # checked by storing files in this directory. `syntax_check_cache_path` is - # the new (and preferred) configuration setting. If not set, knife will - # fall back to using cache_options[:path], which is deprecated but exists in - # many client configs generated by pre-Chef-11 bootstrappers. - default(:syntax_check_cache_path) { cache_options[:path] } - - # Deprecated: - # Move this to the default value of syntax_cache_path when this is removed. - default(:cache_options) { { :path => PathHelper.join(config_dir, "syntaxcache") } } - - # Whether errors should be raised for deprecation warnings. When set to - # `false` (the default setting), a warning is emitted but code using - # deprecated methods/features/etc. should work normally otherwise. When set - # to `true`, usage of deprecated methods/features will raise a - # `DeprecatedFeatureError`. This is used by Chef's tests to ensure that - # deprecated functionality is not used internally by Chef. End users - # should generally leave this at the default setting (especially in - # production), but it may be useful when testing cookbooks or other code if - # the user wishes to aggressively address deprecations. - default(:treat_deprecation_warnings_as_errors) do - # Using an environment variable allows this setting to be inherited in - # tests that spawn new processes. - ENV.key?("CHEF_TREAT_DEPRECATION_WARNINGS_AS_ERRORS") - end - - # knife configuration data - config_context :knife do - default :ssh_port, nil - default :ssh_user, nil - default :ssh_attribute, nil - default :ssh_gateway, nil - default :bootstrap_version, nil - default :bootstrap_proxy, nil - default :bootstrap_template, nil - default :secret, nil - default :secret_file, nil - default :identity_file, nil - default :host_key_verify, nil - default :forward_agent, nil - default :sort_status_reverse, nil - default :hints, {} - end - - def self.set_defaults_for_windows - # Those lists of regular expressions define what chef considers a - # valid user and group name - # From http://technet.microsoft.com/en-us/library/cc776019(WS.10).aspx - principal_valid_regex_part = '[^"\/\\\\\[\]\:;|=,+*?<>]+' - default :user_valid_regex, [ /^(#{principal_valid_regex_part}\\)?#{principal_valid_regex_part}$/ ] - default :group_valid_regex, [ /^(#{principal_valid_regex_part}\\)?#{principal_valid_regex_part}$/ ] - - default :fatal_windows_admin_check, false - end - - def self.set_defaults_for_nix - # Those lists of regular expressions define what chef considers a - # valid user and group name - # - # user/group cannot start with '-', '+' or '~' - # user/group cannot contain ':', ',' or non-space-whitespace or null byte - # everything else is allowed (UTF-8, spaces, etc) and we delegate to your O/S useradd program to barf or not - # copies: http://anonscm.debian.org/viewvc/pkg-shadow/debian/trunk/debian/patches/506_relaxed_usernames?view=markup - default :user_valid_regex, [ /^[^-+~:,\t\r\n\f\0]+[^:,\t\r\n\f\0]*$/ ] - default :group_valid_regex, [ /^[^-+~:,\t\r\n\f\0]+[^:,\t\r\n\f\0]*$/ ] - end - - # Those lists of regular expressions define what chef considers a - # valid user and group name - if Chef::Platform.windows? - set_defaults_for_windows - else - set_defaults_for_nix - end - - # This provides a hook which rspec can stub so that we can avoid twiddling - # global state in tests. - def self.env - ENV - end - - def self.windows_home_path - Chef::Log.deprecation("Chef::Config.windows_home_path is now deprecated. Consider using Chef::Util::PathHelper.home instead.") - PathHelper.home - end - - # returns a platform specific path to the user home dir if set, otherwise default to current directory. - default( :user_home ) { PathHelper.home || Dir.pwd } - - # Enable file permission fixup for selinux. Fixup will be done - # only if selinux is enabled in the system. - default :enable_selinux_file_permission_fixup, true - - # Use atomic updates (i.e. move operation) while updating contents - # of the files resources. When set to false copy operation is - # used to update files. - default :file_atomic_update, true - - # There are 3 possible values for this configuration setting. - # true => file staging is done in the destination directory - # false => file staging is done via tempfiles under ENV['TMP'] - # :auto => file staging will try using destination directory if possible and - # will fall back to ENV['TMP'] if destination directory is not usable. - default :file_staging_uses_destdir, :auto - - # Exit if another run is in progress and the chef-client is unable to - # get the lock before time expires. If nil, no timeout is enforced. (Exits - # immediately if 0.) - default :run_lock_timeout, nil - - # Number of worker threads for syncing cookbooks in parallel. Increasing - # this number can result in gateway errors from the server (namely 503 and 504). - # If you are seeing this behavior while using the default setting, reducing - # the number of threads will help. - default :cookbook_sync_threads, 10 - - # At the beginning of the Chef Client run, the cookbook manifests are downloaded which - # contain URLs for every file in every relevant cookbook. Most of the files - # (recipes, resources, providers, libraries, etc) are immediately synchronized - # at the start of the run. The handling of "files" and "templates" directories, - # however, have two modes of operation. They can either all be downloaded immediately - # at the start of the run (no_lazy_load==true) or else they can be lazily loaded as - # cookbook_file or template resources are converged which require them (no_lazy_load==false). - # - # The advantage of lazily loading these files is that unnecessary files are not - # synchronized. This may be useful to users with large files checked into cookbooks which - # are only selectively downloaded to a subset of clients which use the cookbook. However, - # better solutions are to either isolate large files into individual cookbooks and only - # include those cookbooks in the run lists of the servers that need them -- or move to - # using remote_file and a more appropriate backing store like S3 for large file - # distribution. - # - # The disadvantages of lazily loading files are that users some time find it - # confusing that their cookbooks are not fully synchronzied to the cache initially, - # and more importantly the time-sensitive URLs which are in the manifest may time - # out on long Chef runs before the resource that uses the file is converged - # (leading to many confusing 403 errors on template/cookbook_file resources). - # - default :no_lazy_load, true - - # Default for the chef_gem compile_time attribute. Nil is the same as true but will emit - # warnings on every use of chef_gem prompting the user to be explicit. If the user sets this to - # true then the user will get backcompat behavior but with a single nag warning that cookbooks - # may break with this setting in the future. The false setting is the recommended setting and - # will become the default. - default :chef_gem_compile_time, nil - - # A whitelisted array of attributes you want sent over the wire when node - # data is saved. - # The default setting is nil, which collects all data. Setting to [] will not - # collect any data for save. - default :automatic_attribute_whitelist, nil - default :default_attribute_whitelist, nil - default :normal_attribute_whitelist, nil - default :override_attribute_whitelist, nil - - config_context :windows_service do - # Set `watchdog_timeout` to the number of seconds to wait for a chef-client run - # to finish - default :watchdog_timeout, 2 * (60 * 60) # 2 hours - end - - # Chef requires an English-language UTF-8 locale to function properly. We attempt - # to use the 'locale -a' command and search through a list of preferences until we - # find one that we can use. On Ubuntu systems we should find 'C.UTF-8' and be - # able to use that even if there is no English locale on the server, but Mac, Solaris, - # AIX, etc do not have that locale. We then try to find an English locale and fall - # back to 'C' if we do not. The choice of fallback is pick-your-poison. If we try - # to do the work to return a non-US UTF-8 locale then we fail inside of providers when - # things like 'svn info' return Japanese and we can't parse them. OTOH, if we pick 'C' then - # we will blow up on UTF-8 characters. Between the warn we throw and the Encoding - # exception that ruby will throw it is more obvious what is broken if we drop UTF-8 by - # default rather than drop English. - # - # If there is no 'locale -a' then we return 'en_US.UTF-8' since that is the most commonly - # available English UTF-8 locale. However, all modern POSIXen should support 'locale -a'. - def self.guess_internal_locale - # https://github.com/opscode/chef/issues/2181 - # Some systems have the `locale -a` command, but the result has - # invalid characters for the default encoding. - # - # For example, on CentOS 6 with ENV['LANG'] = "en_US.UTF-8", - # `locale -a`.split fails with ArgumentError invalid UTF-8 encoding. - locales = shell_out_with_systems_locale!("locale -a").stdout.split - case - when locales.include?('C.UTF-8') - 'C.UTF-8' - when locales.include?('en_US.UTF-8'), locales.include?('en_US.utf8') - 'en_US.UTF-8' - when locales.include?('en.UTF-8') - 'en.UTF-8' - else - # Will match en_ZZ.UTF-8, en_ZZ.utf-8, en_ZZ.UTF8, en_ZZ.utf8 - guesses = locales.select { |l| l =~ /^en_.*UTF-?8$/i } - unless guesses.empty? - guessed_locale = guesses.first - # Transform into the form en_ZZ.UTF-8 - guessed_locale.gsub(/UTF-?8$/i, "UTF-8") - else - Chef::Log.warn "Please install an English UTF-8 locale for Chef to use, falling back to C locale and disabling UTF-8 support." - 'C' - end - end - rescue - if Chef::Platform.windows? - Chef::Log.debug "Defaulting to locale en_US.UTF-8 on Windows, until it matters that we do something else." - else - Chef::Log.debug "No usable locale -a command found, assuming you have en_US.UTF-8 installed." - end - 'en_US.UTF-8' - end - - default :internal_locale, guess_internal_locale - - # Force UTF-8 Encoding, for when we fire up in the 'C' locale or other strange locales (e.g. - # japanese windows encodings). If we do not do this, then knife upload will fail when a cookbook's - # README.md has UTF-8 characters that do not encode in whatever surrounding encoding we have been - # passed. Effectively, the Chef Ecosystem is globally UTF-8 by default. Anyone who wants to be - # able to upload Shift_JIS or ISO-8859-1 files needs to mark *those* files explicitly with - # magic tags to make ruby correctly identify the encoding being used. Changing this default will - # break Chef community cookbooks and is very highly discouraged. - default :ruby_encoding, Encoding::UTF_8 - - # If installed via an omnibus installer, this gives the path to the - # "embedded" directory which contains all of the software packaged with - # omnibus. This is used to locate the cacert.pem file on windows. - def self.embedded_dir - Pathname.new(_this_file).ascend do |path| - if path.basename.to_s == "embedded" - return path.to_s - end - end - - nil - end - - # Path to this file in the current install. - def self._this_file - File.expand_path(__FILE__) - end end end diff --git a/lib/chef/cookbook/metadata.rb b/lib/chef/cookbook/metadata.rb index 781d3b40b0..01a98fda39 100644 --- a/lib/chef/cookbook/metadata.rb +++ b/lib/chef/cookbook/metadata.rb @@ -286,9 +286,13 @@ class Chef # === Returns # versions<Array>:: Returns the list of versions for the platform def depends(cookbook, *version_args) - version = new_args_format(:depends, cookbook, version_args) - constraint = validate_version_constraint(:depends, cookbook, version) - @dependencies[cookbook] = constraint.to_s + if cookbook == name + Chef::Log.warn "Ignoring self-dependency in cookbook #{name}, please remove it (in the future this will be fatal)." + else + version = new_args_format(:depends, cookbook, version_args) + constraint = validate_version_constraint(:depends, cookbook, version) + @dependencies[cookbook] = constraint.to_s + end @dependencies[cookbook] end @@ -603,7 +607,7 @@ class Chef msg=<<-OBSOLETED The dependency specification syntax you are using is no longer valid. You may not specify more than one version constraint for a particular cookbook. -Consult http://wiki.opscode.com/display/chef/Metadata for the updated syntax. +Consult https://docs.chef.io/config_rb_metadata.html for the updated syntax. Called by: #{caller_name} '#{dep_name}', #{version_constraints.map {|vc| vc.inspect}.join(", ")} Called from: @@ -622,7 +626,7 @@ OBSOLETED The version constraint syntax you are using is not valid. If you recently upgraded to Chef 0.10.0, be aware that you no may longer use "<<" and ">>" for 'less than' and 'greater than'; use '<' and '>' instead. -Consult http://wiki.opscode.com/display/chef/Metadata for more information. +Consult https://docs.chef.io/config_rb_metadata.html for more information. Called by: #{caller_name} '#{dep_name}', '#{constraint_str}' Called from: diff --git a/lib/chef/cookbook_loader.rb b/lib/chef/cookbook_loader.rb index c05fedb141..79005b1569 100644 --- a/lib/chef/cookbook_loader.rb +++ b/lib/chef/cookbook_loader.rb @@ -106,7 +106,7 @@ class Chef if @cookbooks_by_name.has_key?(cookbook.to_sym) or load_cookbook(cookbook.to_sym) @cookbooks_by_name[cookbook.to_sym] else - raise Exceptions::CookbookNotFoundInRepo, "Cannot find a cookbook named #{cookbook.to_s}; did you forget to add metadata to a cookbook? (http://wiki.opscode.com/display/chef/Metadata)" + raise Exceptions::CookbookNotFoundInRepo, "Cannot find a cookbook named #{cookbook.to_s}; did you forget to add metadata to a cookbook? (https://docs.chef.io/config_rb_metadata.html)" end end diff --git a/lib/chef/cookbook_site_streaming_uploader.rb b/lib/chef/cookbook_site_streaming_uploader.rb index 9e7a55c772..0302a51165 100644 --- a/lib/chef/cookbook_site_streaming_uploader.rb +++ b/lib/chef/cookbook_site_streaming_uploader.rb @@ -106,7 +106,7 @@ class Chef url = URI.parse(to_url) - Chef::Log.logger.debug("Signing: method: #{http_verb}, path: #{url.path}, file: #{content_file}, User-id: #{user_id}, Timestamp: #{timestamp}") + Chef::Log.logger.debug("Signing: method: #{http_verb}, url: #{url}, file: #{content_file}, User-id: #{user_id}, Timestamp: #{timestamp}") # We use the body for signing the request if the file parameter # wasn't a valid file or wasn't included. Extract the body (with @@ -141,13 +141,8 @@ class Chef req.content_type = 'multipart/form-data; boundary=' + boundary unless parts.empty? req.body_stream = body_stream - http = Net::HTTP.new(url.host, url.port) - if url.scheme == "https" - http.use_ssl = true - http.verify_mode = verify_mode - end + http = Chef::HTTP::BasicClient.new(url).http_client res = http.request(req) - #res = http.start {|http_proc| http_proc.request(req) } # alias status to code and to_s to body for test purposes # TODO: stop the following madness! @@ -166,17 +161,6 @@ class Chef res end - private - - def verify_mode - verify_mode = Chef::Config[:ssl_verify_mode] - if verify_mode == :verify_none - OpenSSL::SSL::VERIFY_NONE - elsif verify_mode == :verify_peer - OpenSSL::SSL::VERIFY_PEER - end - end - end class StreamPart diff --git a/lib/chef/dsl/definitions.rb b/lib/chef/dsl/definitions.rb new file mode 100644 index 0000000000..1358f67720 --- /dev/null +++ b/lib/chef/dsl/definitions.rb @@ -0,0 +1,44 @@ +class Chef + module DSL + # + # Module containing a method for each declared definition + # + # Depends on declare_resource(name, created_at, &block) + # + # @api private + # + module Definitions + def self.add_definition(dsl_name) + module_eval <<-EOM, __FILE__, __LINE__+1 + def #{dsl_name}(*args, &block) + evaluate_resource_definition(#{dsl_name.inspect}, *args, &block) + end + EOM + end + + # @api private + def has_resource_definition?(name) + run_context.definitions.has_key?(name) + end + + # Processes the arguments and block as a resource definition. + # + # @api private + def evaluate_resource_definition(definition_name, *args, &block) + + # This dupes the high level object, but we still need to dup the params + new_def = run_context.definitions[definition_name].dup + + new_def.params = new_def.params.dup + new_def.node = run_context.node + # This sets up the parameter overrides + new_def.instance_eval(&block) if block + + new_recipe = Chef::Recipe.new(cookbook_name, recipe_name, run_context) + new_recipe.params = new_def.params + new_recipe.params[:name] = args[0] + new_recipe.instance_eval(&new_def.recipe) + end + end + end +end diff --git a/lib/chef/dsl/recipe.rb b/lib/chef/dsl/recipe.rb index c22f053292..d69f0a8f11 100644 --- a/lib/chef/dsl/recipe.rb +++ b/lib/chef/dsl/recipe.rb @@ -21,6 +21,10 @@ require 'chef/mixin/convert_to_class_name' require 'chef/exceptions' require 'chef/resource_builder' require 'chef/mixin/shell_out' +require 'chef/mixin/powershell_out' +require 'chef/dsl/resources' +require 'chef/dsl/definitions' +require 'chef/resource' class Chef module DSL @@ -31,48 +35,10 @@ class Chef module Recipe include Chef::Mixin::ShellOut - include Chef::Mixin::ConvertToClassName - - def method_missing(method_symbol, *args, &block) - # If we have a definition that matches, we want to use that instead. This should - # let you do some really crazy over-riding of "native" types, if you really want - # to. - if has_resource_definition?(method_symbol) - evaluate_resource_definition(method_symbol, *args, &block) - elsif have_resource_class_for?(method_symbol) - # Otherwise, we're rocking the regular resource call route. - declare_resource(method_symbol, args[0], caller[0], &block) - else - begin - super - rescue NoMethodError - raise NoMethodError, "No resource or method named `#{method_symbol}' for #{describe_self_for_error}" - rescue NameError - raise NameError, "No resource, method, or local variable named `#{method_symbol}' for #{describe_self_for_error}" - end - end - end + include Chef::Mixin::PowershellOut - def has_resource_definition?(name) - run_context.definitions.has_key?(name) - end - - # Processes the arguments and block as a resource definition. - def evaluate_resource_definition(definition_name, *args, &block) - - # This dupes the high level object, but we still need to dup the params - new_def = run_context.definitions[definition_name].dup - - new_def.params = new_def.params.dup - new_def.node = run_context.node - # This sets up the parameter overrides - new_def.instance_eval(&block) if block - - new_recipe = Chef::Recipe.new(cookbook_name, recipe_name, run_context) - new_recipe.params = new_def.params - new_recipe.params[:name] = args[0] - new_recipe.instance_eval(&new_def.recipe) - end + include Chef::DSL::Resources + include Chef::DSL::Definitions # # Instantiates a resource (via #build_resource), then adds it to the @@ -168,14 +134,52 @@ class Chef raise Chef::Exceptions::ResourceNotFound, "exec was called, but you probably meant to use an execute resource. If not, please call Kernel#exec explicitly. The exec block called was \"#{args}\"" end + # DEPRECATED: + # method_missing must live for backcompat purposes until Chef 13. + def method_missing(method_symbol, *args, &block) + # + # If there is already DSL for this, someone must have called + # method_missing manually. Not a fan. Not. A. Fan. + # + if respond_to?(method_symbol) + Chef::Log.deprecation("Calling method_missing(#{method_symbol.inspect}) directly is deprecated in Chef 12 and will be removed in Chef 13.") + Chef::Log.deprecation("Use public_send() or send() instead.") + return send(method_symbol, *args, &block) + end + + # + # If a definition exists, then Chef::DSL::Definitions.add_definition was + # never called. DEPRECATED. + # + if run_context.definitions.has_key?(method_symbol.to_sym) + Chef::Log.deprecation("Definition #{method_symbol} (#{run_context.definitions[method_symbol.to_sym]}) was added to the run_context without calling Chef::DSL::Definitions.add_definition(#{method_symbol.to_sym.inspect}). This will become required in Chef 13.") + Chef::DSL::Definitions.add_definition(method_symbol) + return send(method_symbol, *args, &block) + end + + # + # See if the resource exists anyway. If the user had set + # Chef::Resource::Blah = <resource>, a deprecation warning will be + # emitted and the DSL method 'blah' will be added to the DSL. + # + resource_class = Chef::ResourceResolver.resolve(method_symbol, node: run_context ? run_context.node : nil) + if resource_class + Chef::DSL::Resources.add_resource_dsl(method_symbol) + return send(method_symbol, *args, &block) + end + + begin + super + rescue NoMethodError + raise NoMethodError, "No resource or method named `#{method_symbol}' for #{describe_self_for_error}" + rescue NameError + raise NameError, "No resource, method, or local variable named `#{method_symbol}' for #{describe_self_for_error}" + end + end 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/resource' - # **DEPRECATED** # This used to be part of chef/mixin/recipe_definition_dsl_core. Load the file to activate the deprecation code. require 'chef/mixin/recipe_definition_dsl_core' diff --git a/lib/chef/dsl/resources.rb b/lib/chef/dsl/resources.rb new file mode 100644 index 0000000000..1ce12ed0a0 --- /dev/null +++ b/lib/chef/dsl/resources.rb @@ -0,0 +1,29 @@ +class Chef + module DSL + # + # Module containing a method for each globally declared Resource + # + # Depends on declare_resource(name, created_at, &block) + # + # @api private + module Resources + def self.add_resource_dsl(dsl_name) + begin + module_eval(<<-EOM, __FILE__, __LINE__+1) + def #{dsl_name}(name=nil, created_at=nil, &block) + declare_resource(#{dsl_name.inspect}, name, created_at || caller[0], &block) + end + EOM + rescue SyntaxError + # Handle the case where dsl_name has spaces, etc. + define_method(dsl_name.to_sym) do |name=nil, created_at=nil, &block| + declare_resource(dsl_name, name, created_at || caller[0], &block) + end + end + end + def self.remove_resource_dsl(dsl_name) + remove_method(dsl_name) if method_defined?(dsl_name) + end + end + end +end diff --git a/lib/chef/event_dispatch/base.rb b/lib/chef/event_dispatch/base.rb index 7274105802..73fe25ec13 100644 --- a/lib/chef/event_dispatch/base.rb +++ b/lib/chef/event_dispatch/base.rb @@ -82,6 +82,11 @@ class Chef def node_load_completed(node, expanded_run_list, config) end + # Called after the Policyfile was loaded. This event only occurs when + # chef is in policyfile mode. + def policyfile_loaded(policy) + end + # Called before the cookbook collection is fetched from the server. def cookbook_resolution_start(expanded_run_list) end @@ -239,13 +244,13 @@ class Chef end # Called when audit phase successfully finishes - def audit_phase_complete + def audit_phase_complete(audit_output) end # Called if there is an uncaught exception during the audit phase. The audit runner should # be catching and handling errors from the examples, so this is only uncaught errors (like # bugs in our handling code) - def audit_phase_failed(exception) + def audit_phase_failed(exception, audit_output) end # Signifies the start of a `control_group` block with a defined name diff --git a/lib/chef/event_dispatch/dispatcher.rb b/lib/chef/event_dispatch/dispatcher.rb index 9f43f14311..370f8c51b4 100644 --- a/lib/chef/event_dispatch/dispatcher.rb +++ b/lib/chef/event_dispatch/dispatcher.rb @@ -9,6 +9,8 @@ class Chef # the registered subscribers. class Dispatcher < Base + attr_reader :subscribers + def initialize(*subscribers) @subscribers = subscribers end diff --git a/lib/chef/event_loggers/windows_eventlog.rb b/lib/chef/event_loggers/windows_eventlog.rb index 37dcdc8693..7a3a28b61f 100644 --- a/lib/chef/event_loggers/windows_eventlog.rb +++ b/lib/chef/event_loggers/windows_eventlog.rb @@ -18,17 +18,7 @@ require 'chef/event_loggers/base' require 'chef/platform/query_helpers' - -if Chef::Platform::windows? and not Chef::Platform::windows_server_2003? - if defined? Windows::Constants - [:INFINITE, :WAIT_FAILED, :FORMAT_MESSAGE_IGNORE_INSERTS, :ERROR_INSUFFICIENT_BUFFER].each do |c| - # These are redefined in 'win32/eventlog' - Windows::Constants.send(:remove_const, c) if Windows::Constants.const_defined? c - end - end - - require 'win32/eventlog' -end +require 'chef/win32/eventlog' class Chef module EventLoggers diff --git a/lib/chef/exceptions.rb b/lib/chef/exceptions.rb index da562e70f4..dd0bac3cf9 100644 --- a/lib/chef/exceptions.rb +++ b/lib/chef/exceptions.rb @@ -73,10 +73,13 @@ class Chef class KeyCommandInputError < ArgumentError; end class InvalidKeyArgument < ArgumentError; end class InvalidKeyAttribute < ArgumentError; end + class InvalidUserAttribute < ArgumentError; end + class InvalidClientAttribute < ArgumentError; end class RedirectLimitExceeded < RuntimeError; end class AmbiguousRunlistSpecification < ArgumentError; end class CookbookFrozen < ArgumentError; end class CookbookNotFound < RuntimeError; end + class OnlyApiVersion0SupportedForAction < RuntimeError; end # Cookbook loader used to raise an argument error when cookbook not found. # for back compat, need to raise an error that inherits from ArgumentError class CookbookNotFoundInRepo < ArgumentError; end @@ -435,7 +438,7 @@ class Chef wrapped_errors.each_with_index do |e,i| backtrace << "#{i+1}) #{e.class} - #{e.message}" backtrace += e.backtrace if e.backtrace - backtrace << "" + backtrace << "" unless i == wrapped_errors.length - 1 end set_backtrace(backtrace) end diff --git a/lib/chef/file_access_control/unix.rb b/lib/chef/file_access_control/unix.rb index 472f30b752..c53d832414 100644 --- a/lib/chef/file_access_control/unix.rb +++ b/lib/chef/file_access_control/unix.rb @@ -197,6 +197,8 @@ class Chef # the user has specified a permission, and it does not match the file, so fix the permission Chef::Log.debug("found target_mode != current_mode, updating mode") return true + elsif suid_bit_set? and (should_update_group? or should_update_owner?) + return true else Chef::Log.debug("found target_mode == current_mode, not updating mode") # the user has specified a permission, but it matches the file, so behave idempotently @@ -280,6 +282,9 @@ class Chef return nil end + def suid_bit_set? + return target_mode & 04000 > 0 + end end end end diff --git a/lib/chef/file_content_management/deploy/mv_windows.rb b/lib/chef/file_content_management/deploy/mv_windows.rb index 7504123012..0d16da9717 100644 --- a/lib/chef/file_content_management/deploy/mv_windows.rb +++ b/lib/chef/file_content_management/deploy/mv_windows.rb @@ -63,12 +63,22 @@ class Chef raise Chef::Exceptions::WindowsNotAdmin, "can not get the security information for '#{dst}' due to missing Administrator privileges." end - if dst_sd.dacl_present? - apply_dacl = ACL.create(dst_sd.dacl.select { |ace| !ace.inherited? }) + dacl_present = dst_sd.dacl_present? + if dacl_present + if dst_sd.dacl.nil? + apply_dacl = nil + else + apply_dacl = ACL.create(dst_sd.dacl.select { |ace| !ace.inherited? }) + end end - if dst_sd.sacl_present? - apply_sacl = ACL.create(dst_sd.sacl.select { |ace| !ace.inherited? }) + sacl_present = dst_sd.sacl_present? + if sacl_present + if dst_sd.sacl.nil? + apply_sacl = nil + else + apply_sacl = ACL.create(dst_sd.sacl.select { |ace| !ace.inherited? }) + end end # @@ -84,8 +94,8 @@ class Chef dst_so = Security::SecurableObject.new(dst) dst_so.group = dst_sd.group dst_so.owner = dst_sd.owner - dst_so.set_dacl(apply_dacl, dst_sd.dacl_inherits?) if dst_sd.dacl_present? - dst_so.set_sacl(apply_sacl, dst_sd.sacl_inherits?) if dst_sd.sacl_present? + dst_so.set_dacl(apply_dacl, dst_sd.dacl_inherits?) if dacl_present + dst_so.set_sacl(apply_sacl, dst_sd.sacl_inherits?) if sacl_present end end diff --git a/lib/chef/formatters/doc.rb b/lib/chef/formatters/doc.rb index 7144d00b5d..e76a940c38 100644 --- a/lib/chef/formatters/doc.rb +++ b/lib/chef/formatters/doc.rb @@ -3,9 +3,9 @@ require 'chef/config' class Chef module Formatters - #-- - # TODO: not sold on the name, but the output is similar to what rspec calls - # "specdoc" + + # Formatter similar to RSpec's documentation formatter. Uses indentation to + # show context. class Doc < Formatters::Base attr_reader :start_time, :end_time, :successful_audits, :failed_audits @@ -93,6 +93,10 @@ class Chef def node_load_completed(node, expanded_run_list, config) end + def policyfile_loaded(policy) + puts_line "Using policy '#{policy["name"]}' at revision '#{policy["revision_id"]}'" + end + # Called before the cookbook collection is fetched from the server. def cookbook_resolution_start(expanded_run_list) puts_line "resolving cookbooks for run list: #{expanded_run_list.inspect}" @@ -175,17 +179,21 @@ class Chef puts_line "Starting audit phase" end - def audit_phase_complete + def audit_phase_complete(audit_output) + puts_line audit_output puts_line "Auditing complete" end - def audit_phase_failed(error) + def audit_phase_failed(error, audit_output) + puts_line audit_output puts_line "" puts_line "Audit phase exception:" indent puts_line "#{error.message}" - error.backtrace.each do |l| - puts_line l + if error.backtrace + error.backtrace.each do |l| + puts_line l + end end end diff --git a/lib/chef/formatters/error_inspectors/api_error_formatting.rb b/lib/chef/formatters/error_inspectors/api_error_formatting.rb index 652d478b40..05ee3132a7 100644 --- a/lib/chef/formatters/error_inspectors/api_error_formatting.rb +++ b/lib/chef/formatters/error_inspectors/api_error_formatting.rb @@ -16,6 +16,8 @@ # limitations under the License. # +require 'chef/http/authenticator' + class Chef module Formatters @@ -65,6 +67,24 @@ E error_description.section("Server Response:",format_rest_error) end + def describe_406_error(error_description, response) + if response["x-ops-server-api-version"] + version_header = Chef::JSONCompat.from_json(response["x-ops-server-api-version"]) + client_api_version = version_header["request_version"] + min_server_version = version_header["min_version"] + max_server_version = version_header["max_version"] + + error_description.section("Incompatible server API version:",<<-E) +This version of the API that this Chef request specified is not supported by the Chef server you sent this request to. +The server supports a min API version of #{min_server_version} and a max API version of #{max_server_version}. +Chef just made a request with an API version of #{client_api_version}. +Please either update your Chef client or server to be a compatible set. +E + else + describe_http_error(error_description) + end + end + def describe_500_error(error_description) error_description.section("Unknown Server Error:",<<-E) The server had a fatal error attempting to load the node data. diff --git a/lib/chef/formatters/error_inspectors/compile_error_inspector.rb b/lib/chef/formatters/error_inspectors/compile_error_inspector.rb index 93328adbe3..d64d5e7b01 100644 --- a/lib/chef/formatters/error_inspectors/compile_error_inspector.rb +++ b/lib/chef/formatters/error_inspectors/compile_error_inspector.rb @@ -30,15 +30,16 @@ class Chef def initialize(path, exception) @path, @exception = path, exception + @backtrace_lines_in_cookbooks = nil + @file_lines = nil + @culprit_backtrace_entry = nil + @culprit_line = nil end def add_explanation(error_description) - case exception - when Chef::Exceptions::RecipeNotFound - error_description.section(exception.class.name, exception.message) - else - error_description.section(exception.class.name, exception.message) + error_description.section(exception.class.name, exception.message) + if found_error_in_cookbooks? traceback = filtered_bt.map {|line| " #{line}"}.join("\n") error_description.section("Cookbook Trace:", traceback) error_description.section("Relevant File Content:", context) @@ -93,10 +94,21 @@ class Chef end def filtered_bt - filters = Array(Chef::Config.cookbook_path).map {|p| /^#{Regexp.escape(p)}/ } - r = exception.backtrace.select {|line| filters.any? {|filter| line =~ filter }} - Chef::Log.debug("filtered backtrace of compile error: #{r.join(",")}") - return r.count > 0 ? r : exception.backtrace + backtrace_lines_in_cookbooks.count > 0 ? backtrace_lines_in_cookbooks : exception.backtrace + end + + def found_error_in_cookbooks? + !backtrace_lines_in_cookbooks.empty? + end + + def backtrace_lines_in_cookbooks + @backtrace_lines_in_cookbooks ||= + begin + filters = Array(Chef::Config.cookbook_path).map {|p| /^#{Regexp.escape(p)}/i } + r = exception.backtrace.select {|line| filters.any? {|filter| line =~ filter }} + Chef::Log.debug("filtered backtrace of compile error: #{r.join(",")}") + r + end end end diff --git a/lib/chef/formatters/error_inspectors/cookbook_resolve_error_inspector.rb b/lib/chef/formatters/error_inspectors/cookbook_resolve_error_inspector.rb index aa5eb8485d..e011fa9d9b 100644 --- a/lib/chef/formatters/error_inspectors/cookbook_resolve_error_inspector.rb +++ b/lib/chef/formatters/error_inspectors/cookbook_resolve_error_inspector.rb @@ -72,6 +72,8 @@ E describe_500_error(error_description) when Net::HTTPBadGateway, Net::HTTPServiceUnavailable describe_503_error(error_description) + when Net::HTTPNotAcceptable + describe_406_error(error_description, response) else describe_http_error(error_description) end diff --git a/lib/chef/formatters/error_inspectors/cookbook_sync_error_inspector.rb b/lib/chef/formatters/error_inspectors/cookbook_sync_error_inspector.rb index 0cb849a17f..971dbd664e 100644 --- a/lib/chef/formatters/error_inspectors/cookbook_sync_error_inspector.rb +++ b/lib/chef/formatters/error_inspectors/cookbook_sync_error_inspector.rb @@ -67,6 +67,8 @@ class Chef describe_500_error(error_description) when Net::HTTPBadGateway, Net::HTTPServiceUnavailable, Net::HTTPGatewayTimeOut describe_503_error(error_description) + when Net::HTTPNotAcceptable + describe_406_error(error_description, response) else describe_http_error(error_description) end diff --git a/lib/chef/formatters/error_inspectors/node_load_error_inspector.rb b/lib/chef/formatters/error_inspectors/node_load_error_inspector.rb index e257ee30c0..d81a9f7cc8 100644 --- a/lib/chef/formatters/error_inspectors/node_load_error_inspector.rb +++ b/lib/chef/formatters/error_inspectors/node_load_error_inspector.rb @@ -84,6 +84,8 @@ E describe_500_error(error_description) when Net::HTTPBadGateway, Net::HTTPServiceUnavailable describe_503_error(error_description) + when Net::HTTPNotAcceptable + describe_406_error(error_description, response) else describe_http_error(error_description) end diff --git a/lib/chef/formatters/error_inspectors/registration_error_inspector.rb b/lib/chef/formatters/error_inspectors/registration_error_inspector.rb index f31b348278..dbd23f4a52 100644 --- a/lib/chef/formatters/error_inspectors/registration_error_inspector.rb +++ b/lib/chef/formatters/error_inspectors/registration_error_inspector.rb @@ -9,6 +9,8 @@ class Chef # TODO: Lots of duplication with the node_load_error_inspector, just # slightly tweaked to talk about validation keys instead of other keys. class RegistrationErrorInspector + include APIErrorFormatting + attr_reader :exception attr_reader :node_name attr_reader :config @@ -94,6 +96,8 @@ E error_description.section("Relevant Config Settings:",<<-E) chef_server_url "#{server_url}" E + when Net::HTTPNotAcceptable + describe_406_error(error_description, response) when Net::HTTPInternalServerError error_description.section("Unknown Server Error:",<<-E) The server had a fatal error attempting to load the node data. diff --git a/lib/chef/formatters/error_inspectors/resource_failure_inspector.rb b/lib/chef/formatters/error_inspectors/resource_failure_inspector.rb index 48572d909b..6e4d9322f9 100644 --- a/lib/chef/formatters/error_inspectors/resource_failure_inspector.rb +++ b/lib/chef/formatters/error_inspectors/resource_failure_inspector.rb @@ -63,7 +63,7 @@ class Chef def recipe_snippet return nil if dynamic_resource? @snippet ||= begin - if file = resource.source_line[/^(([\w]:)?[^:]+):([\d]+)/,1] and line = resource.source_line[/^#{file}:([\d]+)/,1].to_i + if file = parse_source and line = parse_line(file) return nil unless ::File.exists?(file) lines = IO.readlines(file) @@ -111,6 +111,16 @@ class Chef line_nr_string + line end + def parse_source + resource.source_line[/^(([\w]:)?[^:]+):([\d]+)/,1] + end + + def parse_line(source) + resource.source_line[/^#{Regexp.escape(source)}:([\d]+)/,1].to_i + end + + + end end end diff --git a/lib/chef/formatters/error_inspectors/run_list_expansion_error_inspector.rb b/lib/chef/formatters/error_inspectors/run_list_expansion_error_inspector.rb index ac19a983af..818228276e 100644 --- a/lib/chef/formatters/error_inspectors/run_list_expansion_error_inspector.rb +++ b/lib/chef/formatters/error_inspectors/run_list_expansion_error_inspector.rb @@ -98,6 +98,8 @@ E error_description.section("Possible Causes:",<<-E) * Your client (#{username}) may have misconfigured authorization permissions. E + when Net::HTTPNotAcceptable + describe_406_error(error_description, response) when Net::HTTPInternalServerError error_description.section("Unknown Server Error:",<<-E) The server had a fatal error attempting to load a role. diff --git a/lib/chef/guard_interpreter/default_guard_interpreter.rb b/lib/chef/guard_interpreter/default_guard_interpreter.rb index df91c2b1ad..fead9886b2 100644 --- a/lib/chef/guard_interpreter/default_guard_interpreter.rb +++ b/lib/chef/guard_interpreter/default_guard_interpreter.rb @@ -16,6 +16,8 @@ # limitations under the License. # +require 'chef/mixin/shell_out' + class Chef class GuardInterpreter class DefaultGuardInterpreter diff --git a/lib/chef/guard_interpreter/resource_guard_interpreter.rb b/lib/chef/guard_interpreter/resource_guard_interpreter.rb index 1e2a534c18..d4b386a15a 100644 --- a/lib/chef/guard_interpreter/resource_guard_interpreter.rb +++ b/lib/chef/guard_interpreter/resource_guard_interpreter.rb @@ -92,8 +92,11 @@ class Chef raise ArgumentError, "Specified guard interpreter class #{resource_class} must be a kind of Chef::Resource::Execute resource" end + # Duplicate the node below because the new RunContext + # overwrites the state of Node instances passed to it. + # See https://github.com/chef/chef/issues/3485. empty_events = Chef::EventDispatch::Dispatcher.new - anonymous_run_context = Chef::RunContext.new(parent_resource.node, {}, empty_events) + anonymous_run_context = Chef::RunContext.new(parent_resource.node.dup, {}, empty_events) interpreter_resource = resource_class.new('Guard resource', anonymous_run_context) interpreter_resource.is_guard_interpreter = true diff --git a/lib/chef/http/authenticator.rb b/lib/chef/http/authenticator.rb index 4255f18cbd..bffa9c4b3a 100644 --- a/lib/chef/http/authenticator.rb +++ b/lib/chef/http/authenticator.rb @@ -24,6 +24,8 @@ class Chef class HTTP class Authenticator + DEFAULT_SERVER_API_VERSION = "1" + attr_reader :signing_key_filename attr_reader :raw_key attr_reader :attr_names @@ -37,10 +39,16 @@ class Chef @signing_key_filename = opts[:signing_key_filename] @key = load_signing_key(opts[:signing_key_filename], opts[:raw_key]) @auth_credentials = AuthCredentials.new(opts[:client_name], @key) + if opts[:api_version] + @api_version = opts[:api_version] + else + @api_version = DEFAULT_SERVER_API_VERSION + end end def handle_request(method, url, headers={}, data=false) headers.merge!(authentication_headers(method, url, data)) if sign_requests? + headers.merge!({'X-Ops-Server-API-Version' => @api_version}) [method, url, headers, data] end diff --git a/lib/chef/http/basic_client.rb b/lib/chef/http/basic_client.rb index 076d152d16..de5e7c03a8 100644 --- a/lib/chef/http/basic_client.rb +++ b/lib/chef/http/basic_client.rb @@ -101,12 +101,16 @@ class Chef env["#{url.scheme.upcase}_PROXY"] || env["#{url.scheme}_proxy"] # Check if the proxy string contains a scheme. If not, add the url's scheme to the - # proxy before parsing. The regex /^.*:\/\// matches, for example, http://. - proxy = if proxy.match(/^.*:\/\//) - URI.parse(proxy) - else - URI.parse("#{url.scheme}://#{proxy}") - end if String === proxy + # proxy before parsing. The regex /^.*:\/\// matches, for example, http://. Reusing proxy + # here since we are really just trying to get the string built correctly. + if String === proxy && !proxy.strip.empty? + if proxy.match(/^.*:\/\//) + proxy = URI.parse(proxy.strip) + else + proxy = URI.parse("#{url.scheme}://#{proxy.strip}") + end + end + no_proxy = Chef::Config[:no_proxy] || env['NO_PROXY'] || env['no_proxy'] excludes = no_proxy.to_s.split(/\s*,\s*/).compact excludes = excludes.map { |exclude| exclude =~ /:\d+$/ ? exclude : "#{exclude}:*" } diff --git a/lib/chef/http/json_input.rb b/lib/chef/http/json_input.rb index 23ccc3a8a7..3296d8821f 100644 --- a/lib/chef/http/json_input.rb +++ b/lib/chef/http/json_input.rb @@ -25,14 +25,19 @@ class Chef # Middleware that takes json input and turns it into raw text class JSONInput + attr_accessor :opts + def initialize(opts={}) + @opts = opts end def handle_request(method, url, headers={}, data=false) if data && should_encode_as_json?(headers) headers.delete_if { |key, _value| key.downcase == 'content-type' } headers["Content-Type"] = 'application/json' - data = Chef::JSONCompat.to_json(data) + json_opts = {} + json_opts[:validate_utf8] = opts[:validate_utf8] if opts.has_key?(:validate_utf8) + data = Chef::JSONCompat.to_json(data, json_opts) # Force encoding to binary to fix SSL related EOFErrors # cf. http://tickets.opscode.com/browse/CHEF-2363 # http://redmine.ruby-lang.org/issues/5233 diff --git a/lib/chef/key.rb b/lib/chef/key.rb index 1ba0ab49c9..be4be7f230 100644 --- a/lib/chef/key.rb +++ b/lib/chef/key.rb @@ -99,6 +99,10 @@ class Chef @public_key = nil end + def delete_create_key + @create_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, @@ -159,12 +163,22 @@ class Chef 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" + # set @name and pass put_name if you wish to update the name of an existing key put_name to @name + def update(put_name=nil) + if @name.nil? && put_name.nil? + raise Chef::Exceptions::MissingKeyAttribute, "the name field must be populated or you must pass a name to update when update is called" end - new_key = chef_rest.put_rest("#{api_base}/#{@actor}/keys/#{@name}", to_hash) + # If no name was passed, fall back to using @name in the PUT URL, otherwise + # use the put_name passed. This will update the a key by the name put_name + # to @name. + put_name = @name if put_name.nil? + + new_key = chef_rest.put_rest("#{api_base}/#{@actor}/keys/#{put_name}", to_hash) + # if the server returned a public_key, remove the create_key field, as we now have a key + if new_key["public_key"] + self.delete_create_key + end Chef::Key.from_hash(self.to_hash.merge(new_key)) end @@ -190,8 +204,10 @@ class Chef def self.from_hash(key_hash) if key_hash.has_key?("user") key = Chef::Key.new(key_hash["user"], "user") - else + elsif key_hash.has_key?("client") key = Chef::Key.new(key_hash["client"], "client") + else + raise Chef::Exceptions::MissingKeyAttribute, "The hash passed to from_hash does not contain the key 'user' or 'client'. Please pass a hash that defines one of those keys." end key.name key_hash['name'] if key_hash.key?('name') key.public_key key_hash['public_key'] if key_hash.key?('public_key') @@ -221,12 +237,12 @@ class Chef 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) + Chef::Key.from_hash(response.merge({"user" => actor})) 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) + Chef::Key.from_hash(response.merge({"client" => actor})) end def self.generate_fingerprint(public_key) diff --git a/lib/chef/knife.rb b/lib/chef/knife.rb index 2e0694aebc..4a93697a1b 100644 --- a/lib/chef/knife.rb +++ b/lib/chef/knife.rb @@ -27,6 +27,7 @@ require 'chef/knife/core/subcommand_loader' require 'chef/knife/core/ui' require 'chef/local_mode' require 'chef/rest' +require 'chef/http/authenticator' require 'pp' class Chef @@ -358,7 +359,7 @@ class Chef case Chef::Config[:verbosity] when 0, nil - Chef::Config[:log_level] = :error + Chef::Config[:log_level] = :warn when 1 Chef::Config[:log_level] = :info else @@ -400,6 +401,8 @@ class Chef end def configure_chef + # knife needs to send logger output to STDERR by default + Chef::Config[:log_location] = STDERR config_loader = self.class.load_config(config[:config_file]) config[:config_file] = config_loader.config_location @@ -483,6 +486,15 @@ class Chef when Net::HTTPServiceUnavailable ui.error "Service temporarily unavailable" ui.info "Response: #{format_rest_error(response)}" + when Net::HTTPNotAcceptable + version_header = Chef::JSONCompat.from_json(response["x-ops-server-api-version"]) + client_api_version = version_header["request_version"] + min_server_version = version_header["min_version"] + max_server_version = version_header["max_version"] + ui.error "The version of Chef that Knife is using is not supported by the Chef server you sent this request to" + ui.info "The request that Knife sent was using API version #{client_api_version}" + ui.info "The Chef server you sent the request to supports a min API verson of #{min_server_version} and a max API version of #{max_server_version}" + ui.info "Please either update your Chef client or server to be a compatible set" else ui.error response.message ui.info "Response: #{format_rest_error(response)}" @@ -539,6 +551,16 @@ class Chef self.msg("Deleted #{obj_name}") end + # helper method for testing if a field exists + # and returning the usage and proper error if not + def test_mandatory_field(field, fieldname) + if field.nil? + show_usage + ui.fatal("You must specify a #{fieldname}") + exit 1 + end + end + def rest @rest ||= begin require 'chef/rest' diff --git a/lib/chef/knife/bootstrap.rb b/lib/chef/knife/bootstrap.rb index a4095e8402..5b29591fcc 100644 --- a/lib/chef/knife/bootstrap.rb +++ b/lib/chef/knife/bootstrap.rb @@ -316,6 +316,12 @@ class Chef # new client-side hawtness, just delete your validation key. if chef_vault_handler.doing_chef_vault? || (Chef::Config[:validation_key] && !File.exist?(File.expand_path(Chef::Config[:validation_key]))) + + unless config[:chef_node_name] + ui.error("You must pass a node name with -N when bootstrapping with user credentials") + exit 1 + end + client_builder.run chef_vault_handler.run(node_name: config[:chef_node_name]) diff --git a/lib/chef/knife/client_create.rb b/lib/chef/knife/client_create.rb index 477a400e8a..570c1ee950 100644 --- a/lib/chef/knife/client_create.rb +++ b/lib/chef/knife/client_create.rb @@ -28,58 +28,82 @@ class Chef end option :file, - :short => "-f FILE", - :long => "--file FILE", - :description => "Write the key to a file" + :short => "-f FILE", + :long => "--file FILE", + :description => "Write the private key to a file if the server generated one." option :admin, - :short => "-a", - :long => "--admin", - :description => "Create the client as an admin", - :boolean => true + :short => "-a", + :long => "--admin", + :description => "Open Source Chef 11 only. Create the client as an admin.", + :boolean => true option :validator, - :long => "--validator", - :description => "Create the client as a validator", - :boolean => true + :long => "--validator", + :description => "Create the client as a validator.", + :boolean => true - banner "knife client create CLIENT (options)" + option :public_key, + :short => "-p FILE", + :long => "--public-key", + :description => "Set the initial default key for the client from a file on disk (cannot pass with --prevent-keygen)." + + option :prevent_keygen, + :short => "-k", + :long => "--prevent-keygen", + :description => "API V1 only. Prevent server from generating a default key pair for you. Cannot be passed with --public-key.", + :boolean => true + + banner "knife client create CLIENTNAME (options)" + + def client + @client_field ||= Chef::ApiClient.new + end + + def create_client(client) + # should not be using save :( bad behavior + client.save + end def run - @client_name = @name_args[0] + test_mandatory_field(@name_args[0], "client name") + client.name @name_args[0] - if @client_name.nil? + if config[:public_key] && config[:prevent_keygen] show_usage - ui.fatal("You must specify a client name") + ui.fatal("You cannot pass --public-key and --prevent-keygen") exit 1 end - client_hash = { - "name" => @client_name, - "admin" => !!config[:admin], - "validator" => !!config[:validator] - } + if !config[:prevent_keygen] && !config[:public_key] + client.create_key(true) + end + + if config[:admin] + client.admin(true) + end - output = Chef::ApiClient.from_hash(edit_hash(client_hash)) + if config[:validator] + client.validator(true) + end - # Chef::ApiClient.save will try to create a client and if it - # exists will update it instead silently. - client = output.save + if config[:public_key] + client.public_key File.read(File.expand_path(config[:public_key])) + end - # We only get a private_key on client creation, not on client update. - if client['private_key'] - ui.info("Created #{output}") + output = edit_data(client) + final_client = create_client(output) + ui.info("Created #{output}") + # output private_key if one + if final_client.private_key if config[:file] File.open(config[:file], "w") do |f| - f.print(client['private_key']) + f.print(final_client.private_key) end else - puts client['private_key'] + puts final_client.private_key end - else - ui.error "Client '#{client['name']}' already exists" - exit 1 end end end diff --git a/lib/chef/knife/client_key_delete.rb b/lib/chef/knife/client_key_delete.rb new file mode 100644 index 0000000000..8ecdfe1ec8 --- /dev/null +++ b/lib/chef/knife/client_key_delete.rb @@ -0,0 +1,76 @@ +# +# 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/knife' + +class Chef + class Knife + # Implements knife client key delete using Chef::Knife::KeyDelete + # as a service class. + # + # @author Tyler Cloke + # + # @attr_reader [String] actor the name of the client that this key is for + class ClientKeyDelete < Knife + banner "knife client key delete CLIENT KEYNAME (options)" + + attr_reader :actor + + def initialize(argv=[]) + super(argv) + @service_object = nil + end + + def run + apply_params!(@name_args) + service_object.run + end + + def actor_field_name + 'client' + end + + def actor_missing_error + 'You must specify a client name' + end + + def keyname_missing_error + 'You must specify a key name' + end + + def service_object + @service_object ||= Chef::Knife::KeyDelete.new(@name, @actor, actor_field_name, ui) + end + + def apply_params!(params) + @actor = params[0] + if @actor.nil? + show_usage + ui.fatal(actor_missing_error) + exit 1 + end + @name = params[1] + if @name.nil? + show_usage + ui.fatal(keyname_missing_error) + exit 1 + end + end + end + end +end diff --git a/lib/chef/knife/client_key_edit.rb b/lib/chef/knife/client_key_edit.rb new file mode 100644 index 0000000000..1de45f4ca2 --- /dev/null +++ b/lib/chef/knife/client_key_edit.rb @@ -0,0 +1,80 @@ +# +# 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/knife' +require 'chef/knife/key_edit_base' + +class Chef + class Knife + # Implements knife client key edit using Chef::Knife::KeyEdit + # as a service class. + # + # @author Tyler Cloke + # + # @attr_reader [String] actor the name of the client that this key is for + class ClientKeyEdit < Knife + include Chef::Knife::KeyEditBase + + banner 'knife client key edit CLIENT KEYNAME (options)' + + attr_reader :actor + + def initialize(argv=[]) + super(argv) + @service_object = nil + end + + def run + apply_params!(@name_args) + service_object.run + end + + def actor_field_name + 'client' + end + + def service_object + @service_object ||= Chef::Knife::KeyEdit.new(@name, @actor, actor_field_name, ui, config) + end + + def actor_missing_error + 'You must specify a client name' + end + + def keyname_missing_error + 'You must specify a key name' + end + + def apply_params!(params) + @actor = params[0] + if @actor.nil? + show_usage + ui.fatal(actor_missing_error) + exit 1 + end + @name = params[1] + if @name.nil? + show_usage + ui.fatal(keyname_missing_error) + exit 1 + end + end + end + end +end + diff --git a/lib/chef/knife/client_key_list.rb b/lib/chef/knife/client_key_list.rb new file mode 100644 index 0000000000..f6f29ae03f --- /dev/null +++ b/lib/chef/knife/client_key_list.rb @@ -0,0 +1,69 @@ +# +# 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/knife' +require 'chef/knife/key_list_base' + +class Chef + class Knife + # Implements knife user key list using Chef::Knife::KeyList + # as a service class. + # + # @author Tyler Cloke + # + # @attr_reader [String] actor the name of the client that this key is for + class ClientKeyList < Knife + include Chef::Knife::KeyListBase + + banner "knife client key list CLIENT (options)" + + attr_reader :actor + + def initialize(argv=[]) + super(argv) + @service_object = nil + end + + def run + apply_params!(@name_args) + service_object.run + end + + def list_method + :list_by_client + end + + def actor_missing_error + 'You must specify a client name' + end + + def service_object + @service_object ||= Chef::Knife::KeyList.new(@actor, list_method, ui, config) + end + + def apply_params!(params) + @actor = params[0] + if @actor.nil? + show_usage + ui.fatal(actor_missing_error) + exit 1 + end + end + end + end +end diff --git a/lib/chef/knife/client_key_show.rb b/lib/chef/knife/client_key_show.rb new file mode 100644 index 0000000000..c39a279000 --- /dev/null +++ b/lib/chef/knife/client_key_show.rb @@ -0,0 +1,76 @@ +# +# 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/knife' + +class Chef + class Knife + # Implements knife client key show using Chef::Knife::KeyShow + # as a service class. + # + # @author Tyler Cloke + # + # @attr_reader [String] actor the name of the client that this key is for + class ClientKeyShow < Knife + banner "knife client key show CLIENT KEYNAME (options)" + + attr_reader :actor + + def initialize(argv=[]) + super(argv) + @service_object = nil + end + + def run + apply_params!(@name_args) + service_object.run + end + + def load_method + :load_by_client + end + + def actor_missing_error + 'You must specify a client name' + end + + def keyname_missing_error + 'You must specify a key name' + end + + def service_object + @service_object ||= Chef::Knife::KeyShow.new(@name, @actor, load_method, ui) + end + + def apply_params!(params) + @actor = params[0] + if @actor.nil? + show_usage + ui.fatal(actor_missing_error) + exit 1 + end + @name = params[1] + if @name.nil? + show_usage + ui.fatal(keyname_missing_error) + exit 1 + end + end + end + end +end diff --git a/lib/chef/knife/core/generic_presenter.rb b/lib/chef/knife/core/generic_presenter.rb index f3ea0f0d6c..2df9603faa 100644 --- a/lib/chef/knife/core/generic_presenter.rb +++ b/lib/chef/knife/core/generic_presenter.rb @@ -181,7 +181,7 @@ class Chef # Must check :[] before attr because spec can include # `keys` - want the key named `keys`, not a list of # available keys. - elsif data.respond_to?(:[]) + elsif data.respond_to?(:[]) && data.has_key?(attr) data = data[attr] elsif data.respond_to?(attr.to_sym) data = data.send(attr.to_sym) diff --git a/lib/chef/knife/core/subcommand_loader.rb b/lib/chef/knife/core/subcommand_loader.rb index 1f59515f44..a8705c724f 100644 --- a/lib/chef/knife/core/subcommand_loader.rb +++ b/lib/chef/knife/core/subcommand_loader.rb @@ -23,7 +23,7 @@ class Chef class SubcommandLoader MATCHES_CHEF_GEM = %r{/chef-[\d]+\.[\d]+\.[\d]+}.freeze - MATCHES_THIS_CHEF_GEM = %r{/chef-#{Chef::VERSION}/}.freeze + MATCHES_THIS_CHEF_GEM = %r{/chef-#{Chef::VERSION}(-\w+)?(-\w+)?/}.freeze attr_reader :chef_config_dir attr_reader :env diff --git a/lib/chef/knife/key_delete.rb b/lib/chef/knife/key_delete.rb new file mode 100644 index 0000000000..fb996cff17 --- /dev/null +++ b/lib/chef/knife/key_delete.rb @@ -0,0 +1,55 @@ +# +# 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/key' + +class Chef + class Knife + # Service class for UserKeyDelete and ClientKeyDelete, used to delete keys. + # Implements common functionality of knife [user | org client] key delete. + # + # @author Tyler Cloke + # + # @attr_accessor [Hash] cli input, see UserKeyDelete and ClientKeyDelete for what could populate it + class KeyDelete + def initialize(name, actor, actor_field_name, ui) + @name = name + @actor = actor + @actor_field_name = actor_field_name + @ui = ui + end + + def confirm! + @ui.confirm("Do you really want to delete the key named #{@name} for the #{@actor_field_name} named #{@actor}") + end + + def print_destroyed + @ui.info("Destroyed key named #{@name} for the #{@actor_field_name} named #{@actor}") + end + + def run + key = Chef::Key.new(@actor, @actor_field_name) + key.name(@name) + confirm! + key.destroy + print_destroyed + end + + end + end +end diff --git a/lib/chef/knife/key_edit.rb b/lib/chef/knife/key_edit.rb new file mode 100644 index 0000000000..48ae344e6e --- /dev/null +++ b/lib/chef/knife/key_edit.rb @@ -0,0 +1,114 @@ +# +# 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/key' +require 'chef/json_compat' +require 'chef/exceptions' + +class Chef + class Knife + # Service class for UserKeyEdit and ClientKeyEdit, + # Implements common functionality of knife [user | org client] key edit. + # + # @author Tyler Cloke + # + # @attr_accessor [Hash] cli input, see UserKeyEdit and ClientKeyEdit for what could populate it + class KeyEdit + + attr_accessor :config + + def initialize(original_name, actor, actor_field_name, ui, config) + @original_name = original_name + @actor = actor + @actor_field_name = actor_field_name + @ui = ui + @config = config + end + + def public_key_and_create_key_error_msg +<<EOS +You passed both --public-key and --create-key. Only pass one, or the other, or neither. +Do not pass either if you do not want to change the public_key field of your key. +Pass --public-key if you want to update the public_key field of your key from a specific public key. +Pass --create-key if you want the server to generate a new key and use that to update the public_key field of your key. +EOS + end + + def edit_data(key) + @ui.edit_data(key) + end + + def display_info(input) + @ui.info(input) + end + + def display_private_key(private_key) + @ui.msg(private_key) + end + + def output_private_key_to_file(private_key) + File.open(@config[:file], "w") do |f| + f.print(private_key) + end + end + + def update_key_from_hash(output) + Chef::Key.from_hash(output).update(@original_name) + end + + def run + key = Chef::Key.new(@actor, @actor_field_name) + if @config[:public_key] && @config[:create_key] + raise Chef::Exceptions::KeyCommandInputError, public_key_and_create_key_error_msg + end + + if @config[:create_key] + key.create_key(true) + end + + if @config[:public_key] + key.public_key(File.read(File.expand_path(@config[:public_key]))) + end + + if @config[:key_name] + key.name(@config[:key_name]) + else + key.name(@original_name) + end + + if @config[:expiration_date] + key.expiration_date(@config[:expiration_date]) + end + + output = edit_data(key) + key = update_key_from_hash(output) + + to_display = "Updated key: #{key.name}" + to_display << " (formally #{@original_name})" if key.name != @original_name + display_info(to_display) + if key.private_key + if @config[:file] + output_private_key_to_file(key.private_key) + else + display_private_key(key.private_key) + end + end + end + end + end +end diff --git a/lib/chef/knife/key_edit_base.rb b/lib/chef/knife/key_edit_base.rb new file mode 100644 index 0000000000..bb5a951a5b --- /dev/null +++ b/lib/chef/knife/key_edit_base.rb @@ -0,0 +1,55 @@ +# +# 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. +# + +class Chef + class Knife + # Extendable module that class_eval's common options into UserKeyEdit and ClientKeyEdit + # + # @author Tyler Cloke + module KeyEditBase + def self.included(includer) + includer.class_eval do + option :public_key, + :short => "-p FILENAME", + :long => "--public-key FILENAME", + :description => "Replace the public_key field from a file on disk. If not passed, the public_key field will not change." + + option :create_key, + :short => "-c", + :long => "--create-key", + :description => "Replace the public_key field with a key generated by the server. The private key will be returned." + + option :file, + :short => "-f FILE", + :long => "--file FILE", + :description => "Write the private key to a file, if you requested the server to create one via --create-key." + + option :key_name, + :short => "-k NAME", + :long => "--key-name NAME", + :description => "The new name for your key. Pass if you wish to update the name field of your key." + + option :expiration_date, + :short => "-e DATE", + :long => "--expiration-date DATE", + :description => "Updates the expiration_date field of your key if passed. Pass in ISO 8601 fomatted string: YYYY-MM-DDTHH:MM:SSZ e.g. 2013-12-24T21:00:00Z or infinity. UTC timezone assumed." + end + end + end + end +end diff --git a/lib/chef/knife/key_list.rb b/lib/chef/knife/key_list.rb new file mode 100644 index 0000000000..e96a27161f --- /dev/null +++ b/lib/chef/knife/key_list.rb @@ -0,0 +1,88 @@ +# +# 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/key' +require 'chef/json_compat' +require 'chef/exceptions' + +class Chef + class Knife + # Service class for UserKeyList and ClientKeyList, used to list keys. + # Implements common functionality of knife [user | org client] key list. + # + # @author Tyler Cloke + # + # @attr_accessor [Hash] cli input, see UserKeyList and ClientKeyList for what could populate it + class KeyList + + attr_accessor :config + + def initialize(actor, list_method, ui, config) + @actor = actor + @list_method = list_method + @ui = ui + @config = config + end + + def expired_and_non_expired_msg +<<EOS +You cannot pass both --only-expired and --only-non-expired. +Please pass one or none. +EOS + end + + def display_info(string) + @ui.output(string) + end + + def colorize(string) + @ui.color(string, :cyan) + end + + def run + if @config[:only_expired] && @config[:only_non_expired] + raise Chef::Exceptions::KeyCommandInputError, expired_and_non_expired_msg + end + + # call proper list function + keys = Chef::Key.send(@list_method, @actor) + if @config[:with_details] + max_length = 0 + keys.each do |key| + key['name'] = key['name'] + ":" + max_length = key['name'].length if key['name'].length > max_length + end + keys.each do |key| + next if !key['expired'] && @config[:only_expired] + next if key['expired'] && @config[:only_non_expired] + display = "#{colorize(key['name'].ljust(max_length))} #{key['uri']}" + display = "#{display} (expired)" if key["expired"] + display_info(display) + end + else + keys.each do |key| + next if !key['expired'] && @config[:only_expired] + next if key['expired'] && @config[:only_non_expired] + display_info(key['name']) + end + end + end + + end + end +end diff --git a/lib/chef/knife/key_list_base.rb b/lib/chef/knife/key_list_base.rb new file mode 100644 index 0000000000..861db0d75a --- /dev/null +++ b/lib/chef/knife/key_list_base.rb @@ -0,0 +1,45 @@ +# +# 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. +# + +class Chef + class Knife + # Extendable module that class_eval's common options into UserKeyList and ClientKeyList + # + # @author Tyler Cloke + module KeyListBase + def self.included(includer) + includer.class_eval do + option :with_details, + :short => "-w", + :long => "--with-details", + :description => "Show corresponding URIs and whether the key has expired or not." + + option :only_expired, + :short => "-e", + :long => "--only-expired", + :description => "Only show expired keys." + + option :only_non_expired, + :short => "-n", + :long => "--only-non-expired", + :description => "Only show non-expired keys." + end + end + end + end +end diff --git a/lib/chef/knife/key_show.rb b/lib/chef/knife/key_show.rb new file mode 100644 index 0000000000..522f7a1a77 --- /dev/null +++ b/lib/chef/knife/key_show.rb @@ -0,0 +1,53 @@ +# +# 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/key' +require 'chef/json_compat' +require 'chef/exceptions' + +class Chef + class Knife + # Service class for UserKeyShow and ClientKeyShow, used to show keys. + # Implements common functionality of knife [user | org client] key show. + # + # @author Tyler Cloke + # + # @attr_accessor [Hash] cli input, see UserKeyShow and ClientKeyShow for what could populate it + class KeyShow + + attr_accessor :config + + def initialize(name, actor, load_method, ui) + @name = name + @actor = actor + @load_method = load_method + @ui = ui + end + + def display_output(key) + @ui.output(@ui.format_for_display(key)) + end + + def run + key = Chef::Key.send(@load_method, @actor, @name) + key.public_key(key.public_key.strip) + display_output(key) + end + end + end +end diff --git a/lib/chef/knife/osc_user_create.rb b/lib/chef/knife/osc_user_create.rb new file mode 100644 index 0000000000..c368296040 --- /dev/null +++ b/lib/chef/knife/osc_user_create.rb @@ -0,0 +1,97 @@ +# +# Author:: Steven Danna (<steve@opscode.com>) +# Copyright:: Copyright (c) 2012 Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'chef/knife' + +# DEPRECATION NOTE +# This code only remains to support users still operating with +# Open Source Chef Server 11 and should be removed once support +# for OSC 11 ends. New development should occur in user_create.rb. +class Chef + class Knife + class OscUserCreate < Knife + + deps do + require 'chef/osc_user' + require 'chef/json_compat' + end + + option :file, + :short => "-f FILE", + :long => "--file FILE", + :description => "Write the private key to a file" + + option :admin, + :short => "-a", + :long => "--admin", + :description => "Create the user as an admin", + :boolean => true + + option :user_password, + :short => "-p PASSWORD", + :long => "--password PASSWORD", + :description => "Password for newly created user", + :default => "" + + option :user_key, + :long => "--user-key FILENAME", + :description => "Public key for newly created user. By default a key will be created for you." + + banner "knife osc_user create USER (options)" + + def run + @user_name = @name_args[0] + + if @user_name.nil? + show_usage + ui.fatal("You must specify a user name") + exit 1 + end + + if config[:user_password].length == 0 + show_usage + ui.fatal("You must specify a non-blank password") + exit 1 + end + + user = Chef::OscUser.new + user.name(@user_name) + user.admin(config[:admin]) + user.password config[:user_password] + + if config[:user_key] + user.public_key File.read(File.expand_path(config[:user_key])) + end + + output = edit_data(user) + user = Chef::OscUser.from_hash(output).create + + ui.info("Created #{user}") + if user.private_key + if config[:file] + File.open(config[:file], "w") do |f| + f.print(user.private_key) + end + else + ui.msg user.private_key + end + end + end + end + end +end diff --git a/lib/chef/knife/osc_user_delete.rb b/lib/chef/knife/osc_user_delete.rb new file mode 100644 index 0000000000..d6fbd4a6a9 --- /dev/null +++ b/lib/chef/knife/osc_user_delete.rb @@ -0,0 +1,51 @@ +# +# Author:: Steven Danna (<steve@opscode.com>) +# Copyright:: Copyright (c) 2012 Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'chef/knife' + +# DEPRECATION NOTE +# This code only remains to support users still operating with +# Open Source Chef Server 11 and should be removed once support +# for OSC 11 ends. New development should occur in the user_delete.rb. + +class Chef + class Knife + class OscUserDelete < Knife + + deps do + require 'chef/osc_user' + require 'chef/json_compat' + end + + banner "knife osc_user delete USER (options)" + + def run + @user_name = @name_args[0] + + if @user_name.nil? + show_usage + ui.fatal("You must specify a user name") + exit 1 + end + + delete_object(Chef::OscUser, @user_name) + end + + end + end +end diff --git a/lib/chef/knife/osc_user_edit.rb b/lib/chef/knife/osc_user_edit.rb new file mode 100644 index 0000000000..4c38674d08 --- /dev/null +++ b/lib/chef/knife/osc_user_edit.rb @@ -0,0 +1,58 @@ +# +# Author:: Steven Danna (<steve@opscode.com>) +# Copyright:: Copyright (c) 2012 Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'chef/knife' + +# DEPRECATION NOTE +# This code only remains to support users still operating with +# Open Source Chef Server 11 and should be removed once support +# for OSC 11 ends. New development should occur in user_edit.rb. + +class Chef + class Knife + class OscUserEdit < Knife + + deps do + require 'chef/osc_user' + require 'chef/json_compat' + end + + banner "knife osc_user edit USER (options)" + + def run + @user_name = @name_args[0] + + if @user_name.nil? + show_usage + ui.fatal("You must specify a user name") + exit 1 + end + + original_user = Chef::OscUser.load(@user_name).to_hash + edited_user = edit_data(original_user) + if original_user != edited_user + user = Chef::OscUser.from_hash(edited_user) + user.update + ui.msg("Saved #{user}.") + else + ui.msg("User unchaged, not saving.") + end + end + end + end +end diff --git a/lib/chef/knife/osc_user_list.rb b/lib/chef/knife/osc_user_list.rb new file mode 100644 index 0000000000..92f049cd19 --- /dev/null +++ b/lib/chef/knife/osc_user_list.rb @@ -0,0 +1,47 @@ +# +# Author:: Steven Danna (<steve@opscode.com>) +# Copyright:: Copyright (c) 2012 Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'chef/knife' + +# DEPRECATION NOTE +# This code only remains to support users still operating with +# Open Source Chef Server 11 and should be removed once support +# for OSC 11 ends. New development should occur in user_list.rb. + +class Chef + class Knife + class OscUserList < Knife + + deps do + require 'chef/osc_user' + require 'chef/json_compat' + end + + banner "knife osc_user list (options)" + + option :with_uri, + :short => "-w", + :long => "--with-uri", + :description => "Show corresponding URIs" + + def run + output(format_list_for_display(Chef::OscUser.list)) + end + end + end +end diff --git a/lib/chef/knife/osc_user_reregister.rb b/lib/chef/knife/osc_user_reregister.rb new file mode 100644 index 0000000000..a71e0aa677 --- /dev/null +++ b/lib/chef/knife/osc_user_reregister.rb @@ -0,0 +1,64 @@ +# +# Author:: Steven Danna (<steve@opscode.com>) +# Copyright:: Copyright (c) 2012 Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'chef/knife' + +# DEPRECATION NOTE +# This code only remains to support users still operating with +# Open Source Chef Server 11 and should be removed once support +# for OSC 11 ends. New development should occur in user_reregister.rb. + +class Chef + class Knife + class OscUserReregister < Knife + + deps do + require 'chef/osc_user' + require 'chef/json_compat' + end + + banner "knife osc_user reregister USER (options)" + + option :file, + :short => "-f FILE", + :long => "--file FILE", + :description => "Write the private key to a file" + + def run + @user_name = @name_args[0] + + if @user_name.nil? + show_usage + ui.fatal("You must specify a user name") + exit 1 + end + + user = Chef::OscUser.load(@user_name).reregister + Chef::Log.debug("Updated user data: #{user.inspect}") + key = user.private_key + if config[:file] + File.open(config[:file], "w") do |f| + f.print(key) + end + else + ui.msg key + end + end + end + end +end diff --git a/lib/chef/knife/osc_user_show.rb b/lib/chef/knife/osc_user_show.rb new file mode 100644 index 0000000000..6a41ddae88 --- /dev/null +++ b/lib/chef/knife/osc_user_show.rb @@ -0,0 +1,54 @@ +# +# Author:: Steven Danna (<steve@opscode.com>) +# Copyright:: Copyright (c) 2009 Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'chef/knife' + +# DEPRECATION NOTE +# This code only remains to support users still operating with +# Open Source Chef Server 11 and should be removed once support +# for OSC 11 ends. New development should occur in user_show.rb. + +class Chef + class Knife + class OscUserShow < Knife + + include Knife::Core::MultiAttributeReturnOption + + deps do + require 'chef/osc_user' + require 'chef/json_compat' + end + + banner "knife osc_user show USER (options)" + + def run + @user_name = @name_args[0] + + if @user_name.nil? + show_usage + ui.fatal("You must specify a user name") + exit 1 + end + + user = Chef::OscUser.load(@user_name) + output(format_for_display(user)) + end + + end + end +end diff --git a/lib/chef/knife/ssh.rb b/lib/chef/knife/ssh.rb index 656baf5e8f..68e01cf94f 100644 --- a/lib/chef/knife/ssh.rb +++ b/lib/chef/knife/ssh.rb @@ -160,6 +160,31 @@ class Chef session_from_list(list) end + def get_ssh_attribute(node) + # Order of precedence for ssh target + # 1) command line attribute + # 2) configuration file + # 3) cloud attribute + # 4) fqdn + if config[:attribute] + Chef::Log.debug("Using node attribute '#{config[:attribute]}' as the ssh target") + attribute = config[:attribute] + elsif Chef::Config[:knife][:ssh_attribute] + Chef::Log.debug("Using node attribute #{Chef::Config[:knife][:ssh_attribute]}") + attribute = Chef::Config[:knife][:ssh_attribute] + elsif node[:cloud] && + node[:cloud][:public_hostname] && + !node[:cloud][:public_hostname].empty? + Chef::Log.debug("Using node attribute 'cloud[:public_hostname]' automatically as the ssh target") + attribute = 'cloud.public_hostname' + else + # falling back to default of fqdn + Chef::Log.debug("Using node attribute 'fqdn' as the ssh target") + attribute = "fqdn" + end + attribute + end + def search_nodes list = Array.new query = Chef::Search::Query.new @@ -168,25 +193,9 @@ class Chef # 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[:attribute_from_cli] - if config[:attribute_from_cli] - Chef::Log.debug("Using node attribute '#{config[:attribute_from_cli]}' from the command line as the ssh target") - host = extract_nested_value(item, config[:attribute_from_cli]) - elsif item[:cloud] && - item[:cloud][:public_hostname] && - !item[:cloud][:public_hostname].empty? - Chef::Log.debug("Using node attribute 'cloud[:public_hostname]' automatically as the ssh target") - host = item[:cloud][:public_hostname] - else - # ssh attribute from a configuration file or the default will land here - Chef::Log.debug("Using node attribute '#{config[:attribute]}' as the ssh target") - host = extract_nested_value(item, config[:attribute]) - end # next if we couldn't find the specified attribute in the # returned node object + host = extract_nested_value(item,get_ssh_attribute(item)) next if host.nil? ssh_port = item[:cloud].nil? ? nil : item[:cloud][:public_ssh_port] srv = [host, ssh_port] @@ -416,16 +425,6 @@ class Chef end end - def configure_attribute - # Setting 'knife[:ssh_attribute] = "foo"' in knife.rb => Chef::Config[:knife][:ssh_attribute] == 'foo' - # Running 'knife ssh -a foo' => both Chef::Config[:knife][:ssh_attribute] && config[:attribute] == foo - # Thus we can differentiate between a config file value and a command line override at this point by checking config[:attribute] - # We can tell here if fqdn was passed from the command line, rather than being the default, by checking config[:attribute] - # However, after here, we cannot tell these things, so we must preserve config[:attribute] - config[:attribute_from_cli] = config[:attribute] - config[:attribute] = (config[:attribute_from_cli] || Chef::Config[:knife][:ssh_attribute] || "fqdn").strip - end - def cssh cssh_cmd = nil %w[csshX cssh].each do |cmd| @@ -499,7 +498,6 @@ class Chef @longest = 0 - configure_attribute configure_user configure_password configure_identity_file diff --git a/lib/chef/knife/user_create.rb b/lib/chef/knife/user_create.rb index 4130f06878..e73f6be8b6 100644 --- a/lib/chef/knife/user_create.rb +++ b/lib/chef/knife/user_create.rb @@ -1,6 +1,7 @@ # -# Author:: Steven Danna (<steve@opscode.com>) -# Copyright:: Copyright (c) 2012 Opscode, Inc. +# Author:: Steven Danna (<steve@chef.io>) +# Author:: Tyler Cloke (<tyler@chef.io>) +# Copyright:: Copyright (c) 2012, 2015 Chef Software, Inc. # License:: Apache License, Version 2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,11 +18,14 @@ # require 'chef/knife' +require 'chef/knife/osc_user_create' class Chef class Knife class UserCreate < Knife + attr_accessor :user_field + deps do require 'chef/user' require 'chef/json_compat' @@ -30,63 +34,118 @@ class Chef option :file, :short => "-f FILE", :long => "--file FILE", - :description => "Write the private key to a file" + :description => "Write the private key to a file if the server generated one." + + option :user_key, + :long => "--user-key FILENAME", + :description => "Set the initial default key for the user from a file on disk (cannot pass with --prevent-keygen)." + + option :prevent_keygen, + :short => "-k", + :long => "--prevent-keygen", + :description => "API V1 only. Prevent server from generating a default key pair for you. Cannot be passed with --user-key.", + :boolean => true option :admin, :short => "-a", :long => "--admin", - :description => "Create the user as an admin", + :description => "DEPRECATED: Open Source Chef 11 only. Create the user as an admin.", :boolean => true option :user_password, :short => "-p PASSWORD", :long => "--password PASSWORD", - :description => "Password for newly created user", + :description => "DEPRECATED: Open Source Chef 11 only. Password for newly created user.", :default => "" - option :user_key, - :long => "--user-key FILENAME", - :description => "Public key for newly created user. By default a key will be created for you." + banner "knife user create USERNAME DISPLAY_NAME FIRST_NAME LAST_NAME EMAIL PASSWORD (options)" + + def user + @user_field ||= Chef::User.new + end + + def create_user_from_hash(hash) + Chef::User.from_hash(hash).create + end + + def osc_11_warning +<<-EOF +IF YOU ARE USING CHEF SERVER 12+, PLEASE FOLLOW THE INSTRUCTIONS UNDER knife user create --help. +You only passed a single argument to knife user create. +For backwards compatibility, when only a single argument is passed, +knife user create assumes you want Open Source 11 Server user creation. +knife user create for Open Source 11 Server is being deprecated. +Open Source 11 Server user commands now live under the knife osc_user namespace. +For backwards compatibility, we will forward this request to knife osc_user create. +If you are using an Open Source 11 Server, please use that command to avoid this warning. +EOF + end - banner "knife user create USER (options)" + def run_osc_11_user_create + # run osc_user_create with our input + ARGV.delete("user") + ARGV.unshift("osc_user") + Chef::Knife.run(ARGV, Chef::Application::Knife.options) + end def run - @user_name = @name_args[0] + # DEPRECATION NOTE + # Remove this if statement and corrosponding code post OSC 11 support. + # + # If only 1 arg is passed, assume OSC 11 case. + if @name_args.length == 1 + ui.warn(osc_11_warning) + run_osc_11_user_create + else # EC / CS 12 user create - if @user_name.nil? - show_usage - ui.fatal("You must specify a user name") - exit 1 - end + test_mandatory_field(@name_args[0], "username") + user.username @name_args[0] - if config[:user_password].length == 0 - show_usage - ui.fatal("You must specify a non-blank password") - exit 1 - end + test_mandatory_field(@name_args[1], "display name") + user.display_name @name_args[1] - user = Chef::User.new - user.name(@user_name) - user.admin(config[:admin]) - user.password config[:user_password] + test_mandatory_field(@name_args[2], "first name") + user.first_name @name_args[2] - if config[:user_key] - user.public_key File.read(File.expand_path(config[:user_key])) - end + test_mandatory_field(@name_args[3], "last name") + user.last_name @name_args[3] + + test_mandatory_field(@name_args[4], "email") + user.email @name_args[4] + + test_mandatory_field(@name_args[5], "password") + user.password @name_args[5] + + if config[:user_key] && config[:prevent_keygen] + show_usage + ui.fatal("You cannot pass --user-key and --prevent-keygen") + exit 1 + end + + if !config[:prevent_keygen] && !config[:user_key] + user.create_key(true) + end - output = edit_data(user) - user = Chef::User.from_hash(output).create + if config[:user_key] + user.public_key File.read(File.expand_path(config[:user_key])) + end - ui.info("Created #{user}") - if user.private_key - if config[:file] - File.open(config[:file], "w") do |f| - f.print(user.private_key) + output = edit_data(user) + final_user = create_user_from_hash(output) + + ui.info("Created #{user}") + if final_user.private_key + if config[:file] + File.open(config[:file], "w") do |f| + f.print(final_user.private_key) + end + else + ui.msg final_user.private_key end - else - ui.msg user.private_key end end + + end end end diff --git a/lib/chef/knife/user_delete.rb b/lib/chef/knife/user_delete.rb index b7af11bec8..803be6b90c 100644 --- a/lib/chef/knife/user_delete.rb +++ b/lib/chef/knife/user_delete.rb @@ -29,6 +29,40 @@ class Chef banner "knife user delete USER (options)" + def osc_11_warning +<<-EOF +The Chef Server you are using does not support the username field. +This means it is an Open Source 11 Server. +knife user delete for Open Source 11 Server is being deprecated. +Open Source 11 Server user commands now live under the knife osc_user namespace. +For backwards compatibility, we will forward this request to knife osc_user delete. +If you are using an Open Source 11 Server, please use that command to avoid this warning. +EOF + end + + def run_osc_11_user_delete + # run osc_user_delete with our input + ARGV.delete("user") + ARGV.unshift("osc_user") + Chef::Knife.run(ARGV, Chef::Application::Knife.options) + end + + # DEPRECATION NOTE + # Delete this override method after OSC 11 support is dropped + def delete_object(user_name) + confirm("Do you really want to delete #{user_name}") + + if Kernel.block_given? + object = block.call + else + object = Chef::User.load(user_name) + object.destroy + end + + output(format_for_display(object)) if config[:print_after] + self.msg("Deleted #{user_name}") + end + def run @user_name = @name_args[0] @@ -38,9 +72,25 @@ class Chef exit 1 end - delete_object(Chef::User, @user_name) - end + # DEPRECATION NOTE + # + # Below is modification of Chef::Knife.delete_object to detect OSC 11 server. + # When OSC 11 is deprecated, simply delete all this and go back to: + # + # delete_object(Chef::User, @user_name) + # + # Also delete our override of delete_object above + object = Chef::User.load(@user_name) + # OSC 11 case + if object.username.nil? + ui.warn(osc_11_warning) + run_osc_11_user_delete + else # proceed with EC / CS delete + delete_object(@user_name) + end + + end end end end diff --git a/lib/chef/knife/user_edit.rb b/lib/chef/knife/user_edit.rb index ae319c8872..dd2fc02743 100644 --- a/lib/chef/knife/user_edit.rb +++ b/lib/chef/knife/user_edit.rb @@ -29,6 +29,24 @@ class Chef banner "knife user edit USER (options)" + def osc_11_warning +<<-EOF +The Chef Server you are using does not support the username field. +This means it is an Open Source 11 Server. +knife user edit for Open Source 11 Server is being deprecated. +Open Source 11 Server user commands now live under the knife oc_user namespace. +For backwards compatibility, we will forward this request to knife osc_user edit. +If you are using an Open Source 11 Server, please use that command to avoid this warning. +EOF + end + + def run_osc_11_user_edit + # run osc_user_create with our input + ARGV.delete("user") + ARGV.unshift("osc_user") + Chef::Knife.run(ARGV, Chef::Application::Knife.options) + end + def run @user_name = @name_args[0] @@ -39,14 +57,26 @@ class Chef end original_user = Chef::User.load(@user_name).to_hash - edited_user = edit_data(original_user) - if original_user != edited_user - user = Chef::User.from_hash(edited_user) - user.update - ui.msg("Saved #{user}.") - else - ui.msg("User unchaged, not saving.") + + # DEPRECATION NOTE + # Remove this if statement and corrosponding code post OSC 11 support. + # + # if username is nil, we are in the OSC 11 case, + # forward to deprecated command + if original_user["username"].nil? + ui.warn(osc_11_warning) + run_osc_11_user_edit + else # EC / CS 12 user create + edited_user = edit_data(original_user) + if original_user != edited_user + user = Chef::User.from_hash(edited_user) + user.update + ui.msg("Saved #{user}.") + else + ui.msg("User unchaged, not saving.") + end end + end end end diff --git a/lib/chef/knife/user_key_delete.rb b/lib/chef/knife/user_key_delete.rb new file mode 100644 index 0000000000..6db1c9f552 --- /dev/null +++ b/lib/chef/knife/user_key_delete.rb @@ -0,0 +1,76 @@ +# +# 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/knife' + +class Chef + class Knife + # Implements knife user key delete using Chef::Knife::KeyDelete + # as a service class. + # + # @author Tyler Cloke + # + # @attr_reader [String] actor the name of the client that this key is for + class UserKeyDelete < Knife + banner "knife user key delete USER KEYNAME (options)" + + attr_reader :actor + + def initialize(argv=[]) + super(argv) + @service_object = nil + end + + def run + apply_params!(@name_args) + service_object.run + end + + def actor_field_name + 'user' + end + + def actor_missing_error + 'You must specify a user name' + end + + def keyname_missing_error + 'You must specify a key name' + end + + def service_object + @service_object ||= Chef::Knife::KeyDelete.new(@name, @actor, actor_field_name, ui) + end + + def apply_params!(params) + @actor = params[0] + if @actor.nil? + show_usage + ui.fatal(actor_missing_error) + exit 1 + end + @name = params[1] + if @name.nil? + show_usage + ui.fatal(keyname_missing_error) + exit 1 + end + end + end + end +end diff --git a/lib/chef/knife/user_key_edit.rb b/lib/chef/knife/user_key_edit.rb new file mode 100644 index 0000000000..0c35332523 --- /dev/null +++ b/lib/chef/knife/user_key_edit.rb @@ -0,0 +1,80 @@ +# +# 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/knife' +require 'chef/knife/key_edit_base' + +class Chef + class Knife + # Implements knife user key edit using Chef::Knife::KeyEdit + # as a service class. + # + # @author Tyler Cloke + # + # @attr_reader [String] actor the name of the user that this key is for + class UserKeyEdit < Knife + include Chef::Knife::KeyEditBase + + banner 'knife user key edit USER KEYNAME (options)' + + attr_reader :actor + + def initialize(argv=[]) + super(argv) + @service_object = nil + end + + def run + apply_params!(@name_args) + service_object.run + end + + def actor_field_name + 'user' + end + + def service_object + @service_object ||= Chef::Knife::KeyEdit.new(@name, @actor, actor_field_name, ui, config) + end + + def actor_missing_error + 'You must specify a user name' + end + + def keyname_missing_error + 'You must specify a key name' + end + + def apply_params!(params) + @actor = params[0] + if @actor.nil? + show_usage + ui.fatal(actor_missing_error) + exit 1 + end + @name = params[1] + if @name.nil? + show_usage + ui.fatal(keyname_missing_error) + exit 1 + end + end + end + end +end + diff --git a/lib/chef/knife/user_key_list.rb b/lib/chef/knife/user_key_list.rb new file mode 100644 index 0000000000..a73f59c86f --- /dev/null +++ b/lib/chef/knife/user_key_list.rb @@ -0,0 +1,69 @@ +# +# 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/knife' +require 'chef/knife/key_list_base' + +class Chef + class Knife + # Implements knife user key list using Chef::Knife::KeyList + # as a service class. + # + # @author Tyler Cloke + # + # @attr_reader [String] actor the name of the client that this key is for + class UserKeyList < Knife + include Chef::Knife::KeyListBase + + banner "knife user key list USER (options)" + + attr_reader :actor + + def initialize(argv=[]) + super(argv) + @service_object = nil + end + + def run + apply_params!(@name_args) + service_object.run + end + + def list_method + :list_by_user + end + + def actor_missing_error + 'You must specify a user name' + end + + def service_object + @service_object ||= Chef::Knife::KeyList.new(@actor, list_method, ui, config) + end + + def apply_params!(params) + @actor = params[0] + if @actor.nil? + show_usage + ui.fatal(actor_missing_error) + exit 1 + end + end + end + end +end diff --git a/lib/chef/knife/user_key_show.rb b/lib/chef/knife/user_key_show.rb new file mode 100644 index 0000000000..b8453dd28e --- /dev/null +++ b/lib/chef/knife/user_key_show.rb @@ -0,0 +1,76 @@ +# +# 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/knife' + +class Chef + class Knife + # Implements knife user key show using Chef::Knife::KeyShow + # as a service class. + # + # @author Tyler Cloke + # + # @attr_reader [String] actor the name of the client that this key is for + class UserKeyShow < Knife + banner "knife user key show USER KEYNAME (options)" + + attr_reader :actor + + def initialize(argv=[]) + super(argv) + @service_object = nil + end + + def run + apply_params!(@name_args) + service_object.run + end + + def load_method + :load_by_user + end + + def actor_missing_error + 'You must specify a user name' + end + + def keyname_missing_error + 'You must specify a key name' + end + + def service_object + @service_object ||= Chef::Knife::KeyShow.new(@name, @actor, load_method, ui) + end + + def apply_params!(params) + @actor = params[0] + if @actor.nil? + show_usage + ui.fatal(actor_missing_error) + exit 1 + end + @name = params[1] + if @name.nil? + show_usage + ui.fatal(keyname_missing_error) + exit 1 + end + end + end + end +end diff --git a/lib/chef/knife/user_list.rb b/lib/chef/knife/user_list.rb index 5d2e735a73..7ae43dadc9 100644 --- a/lib/chef/knife/user_list.rb +++ b/lib/chef/knife/user_list.rb @@ -18,6 +18,8 @@ require 'chef/knife' +# NOTE: only knife user command that is backwards compatible with OSC 11, +# so no deprecation warnings are necessary. class Chef class Knife class UserList < Knife @@ -37,6 +39,7 @@ class Chef def run output(format_list_for_display(Chef::User.list)) end + end end end diff --git a/lib/chef/knife/user_reregister.rb b/lib/chef/knife/user_reregister.rb index 946150e6e4..eab2245025 100644 --- a/lib/chef/knife/user_reregister.rb +++ b/lib/chef/knife/user_reregister.rb @@ -29,6 +29,24 @@ class Chef banner "knife user reregister USER (options)" + def osc_11_warning +<<-EOF +The Chef Server you are using does not support the username field. +This means it is an Open Source 11 Server. +knife user reregister for Open Source 11 Server is being deprecated. +Open Source 11 Server user commands now live under the knife osc_user namespace. +For backwards compatibility, we will forward this request to knife osc_user reregister. +If you are using an Open Source 11 Server, please use that command to avoid this warning. +EOF + end + + def run_osc_11_user_reregister + # run osc_user_edit with our input + ARGV.delete("user") + ARGV.unshift("osc_user") + Chef::Knife.run(ARGV, Chef::Application::Knife.options) + end + option :file, :short => "-f FILE", :long => "--file FILE", @@ -43,16 +61,29 @@ class Chef exit 1 end - user = Chef::User.load(@user_name).reregister - Chef::Log.debug("Updated user data: #{user.inspect}") - key = user.private_key - if config[:file] - File.open(config[:file], "w") do |f| - f.print(key) + user = Chef::User.load(@user_name) + + # DEPRECATION NOTE + # Remove this if statement and corrosponding code post OSC 11 support. + # + # if username is nil, we are in the OSC 11 case, + # forward to deprecated command + if user.username.nil? + ui.warn(osc_11_warning) + run_osc_11_user_reregister + else # EC / CS 12 case + user.reregister + Chef::Log.debug("Updated user data: #{user.inspect}") + key = user.private_key + if config[:file] + File.open(config[:file], "w") do |f| + f.print(key) + end + else + ui.msg key end - else - ui.msg key end + end end end diff --git a/lib/chef/knife/user_show.rb b/lib/chef/knife/user_show.rb index 61ea101e4c..f5e81e9972 100644 --- a/lib/chef/knife/user_show.rb +++ b/lib/chef/knife/user_show.rb @@ -31,6 +31,24 @@ class Chef banner "knife user show USER (options)" + def osc_11_warning +<<-EOF +The Chef Server you are using does not support the username field. +This means it is an Open Source 11 Server. +knife user show for Open Source 11 Server is being deprecated. +Open Source 11 Server user commands now live under the knife osc_user namespace. +For backwards compatibility, we will forward this request to knife osc_user show. +If you are using an Open Source 11 Server, please use that command to avoid this warning. +EOF + end + + def run_osc_11_user_show + # run osc_user_edit with our input + ARGV.delete("user") + ARGV.unshift("osc_user") + Chef::Knife.run(ARGV, Chef::Application::Knife.options) + end + def run @user_name = @name_args[0] @@ -41,7 +59,18 @@ class Chef end user = Chef::User.load(@user_name) - output(format_for_display(user)) + + # DEPRECATION NOTE + # Remove this if statement and corrosponding code post OSC 11 support. + # + # if username is nil, we are in the OSC 11 case, + # forward to deprecated command + if user.username.nil? + ui.warn(osc_11_warning) + run_osc_11_user_show + else + output(format_for_display(user)) + end end end diff --git a/lib/chef/log.rb b/lib/chef/log.rb index 682afcea4b..9b27778a40 100644 --- a/lib/chef/log.rb +++ b/lib/chef/log.rb @@ -21,6 +21,8 @@ require 'logger' require 'chef/monologger' require 'chef/exceptions' require 'mixlib/log' +require 'chef/log/syslog' unless (RUBY_PLATFORM =~ /mswin|mingw|windows/) +require 'chef/log/winevt' class Chef class Log diff --git a/lib/chef/log/syslog.rb b/lib/chef/log/syslog.rb new file mode 100644 index 0000000000..0c8190797f --- /dev/null +++ b/lib/chef/log/syslog.rb @@ -0,0 +1,46 @@ +# +# Author:: Lamont Granquist (<lamont@chef.io>) +# Author:: SAWANOBORI Yukihiko (<sawanoboriyu@higanworks.com>) +# 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 'logger' +require 'syslog-logger' +require 'chef/mixin/unformatter' + +class Chef + class Log + # + # Chef::Log::Syslog class. + # usage in client.rb: + # log_location Chef::Log::Syslog.new("chef-client", ::Syslog::LOG_DAEMON) + # + class Syslog < Logger::Syslog + include Chef::Mixin::Unformatter + + attr_accessor :sync, :formatter + + def initialize(program_name = 'chef-client', facility = ::Syslog::LOG_DAEMON, logopts=nil) + super + return if defined? ::Logger::Syslog::SYSLOG + ::Logger::Syslog.const_set :SYSLOG, SYSLOG + end + + def close + end + end + end +end + diff --git a/lib/chef/log/winevt.rb b/lib/chef/log/winevt.rb new file mode 100644 index 0000000000..c5b7c3485a --- /dev/null +++ b/lib/chef/log/winevt.rb @@ -0,0 +1,99 @@ +# +# Author:: Jay Mundrawala (<jdm@chef.io>) +# +# Copyright:: 2015, Chef Software, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# 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/event_loggers/base' +require 'chef/platform/query_helpers' +require 'chef/mixin/unformatter' + +class Chef + class Log + # + # Chef::Log::WinEvt class. + # usage in client.rb: + # log_location Chef::Log::WinEvt.new + # + class WinEvt + # These must match those that are defined in the manifest file + INFO_EVENT_ID = 10100 + WARN_EVENT_ID = 10101 + DEBUG_EVENT_ID = 10102 + ERROR_EVENT_ID = 10103 + FATAL_EVENT_ID = 10104 + + # Since we must install the event logger, this is not really configurable + SOURCE = 'Chef' + + include Chef::Mixin::Unformatter + + attr_accessor :sync, :formatter, :level + + def initialize(eventlog=nil) + @eventlog = eventlog || ::Win32::EventLog::open('Application') + end + + def close + end + + def info(msg) + @eventlog.report_event( + :event_type => ::Win32::EventLog::INFO_TYPE, + :source => SOURCE, + :event_id => INFO_EVENT_ID, + :data => [msg] + ) + end + + def warn(msg) + @eventlog.report_event( + :event_type => ::Win32::EventLog::WARN_TYPE, + :source => SOURCE, + :event_id => WARN_EVENT_ID, + :data => [msg] + ) + end + + def debug(msg) + @eventlog.report_event( + :event_type => ::Win32::EventLog::INFO_TYPE, + :source => SOURCE, + :event_id => DEBUG_EVENT_ID, + :data => [msg] + ) + end + + def error(msg) + @eventlog.report_event( + :event_type => ::Win32::EventLog::ERROR_TYPE, + :source => SOURCE, + :event_id => ERROR_EVENT_ID, + :data => [msg] + ) + end + + def fatal(msg) + @eventlog.report_event( + :event_type => ::Win32::EventLog::ERROR_TYPE, + :source => SOURCE, + :event_id => FATAL_EVENT_ID, + :data => [msg] + ) + end + + end + end +end diff --git a/lib/chef/mixin/api_version_request_handling.rb b/lib/chef/mixin/api_version_request_handling.rb new file mode 100644 index 0000000000..20ab3bf452 --- /dev/null +++ b/lib/chef/mixin/api_version_request_handling.rb @@ -0,0 +1,66 @@ +# +# Author:: Tyler Cloke (tyler@chef.io) +# Copyright:: Copyright 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. +# + +class Chef + module Mixin + module ApiVersionRequestHandling + # Input: + # exeception: + # Net::HTTPServerException that may or may not contain the x-ops-server-api-version header + # supported_client_versions: + # An array of Integers that represent the API versions the client supports. + # + # Output: + # nil: + # If the execption was not a 406 or the server does not support versioning + # Array of length zero: + # If there was no intersection between supported client versions and supported server versions + # Arrary of Integers: + # If there was an intersection of supported versions, the array returns will contain that intersection + def server_client_api_version_intersection(exception, supported_client_versions) + # return empty array unless 406 Unacceptable with proper header + return nil if exception.response.code != "406" || exception.response["x-ops-server-api-version"].nil? + + # intersection of versions the server and client support, will be of length zero if no intersection + server_supported_client_versions = Array.new + + header = Chef::JSONCompat.from_json(exception.response["x-ops-server-api-version"]) + min_server_version = Integer(header["min_version"]) + max_server_version = Integer(header["max_version"]) + + supported_client_versions.each do |version| + if version >= min_server_version && version <= max_server_version + server_supported_client_versions.push(version) + end + end + server_supported_client_versions + end + + def reregister_only_v0_supported_error_msg(max_version, min_version) + <<-EOH +The reregister command only supports server API version 0. +The server that received the request supports a min version of #{min_version} and a max version of #{max_version}. +User keys are now managed via the key rotation commmands. +Please refer to the documentation on how to manage your keys via the key rotation commands: +https://docs.chef.io/server_security.html#key-rotation +EOH + end + + end + end +end diff --git a/lib/chef/mixin/convert_to_class_name.rb b/lib/chef/mixin/convert_to_class_name.rb index 19f229fdd3..14676e5ed4 100644 --- a/lib/chef/mixin/convert_to_class_name.rb +++ b/lib/chef/mixin/convert_to_class_name.rb @@ -23,9 +23,7 @@ class Chef extend self def convert_to_class_name(str) - str = str.dup - str.gsub!(/[^A-Za-z0-9_]/,'_') - str.gsub!(/^(_+)?/,'') + str = normalize_snake_case_name(str) rname = nil regexp = %r{^(.+?)(_(.+))?$} @@ -51,6 +49,13 @@ class Chef str end + def normalize_snake_case_name(str) + str = str.dup + str.gsub!(/[^A-Za-z0-9_]/,'_') + str.gsub!(/^(_+)?/,'') + str + end + def snake_case_basename(str) with_namespace = convert_to_snake_case(str) with_namespace.split("::").last.sub(/^_/, '') @@ -58,7 +63,8 @@ class Chef def filename_to_qualified_string(base, filename) file_base = File.basename(filename, ".rb") - base.to_s + (file_base == 'default' ? '' : "_#{file_base}") + str = base.to_s + (file_base == 'default' ? '' : "_#{file_base}") + normalize_snake_case_name(str) end # Copied from rails activesupport. In ruby >= 2.0 const_get will just do this, so this can diff --git a/lib/chef/mixin/deprecation.rb b/lib/chef/mixin/deprecation.rb index 489f27c339..a3eacf75cb 100644 --- a/lib/chef/mixin/deprecation.rb +++ b/lib/chef/mixin/deprecation.rb @@ -95,6 +95,30 @@ class Chef DeprecatedInstanceVariable.new(obj, name, level) end + def deprecated_attr(name, alternative) + deprecated_attr_reader(name, alternative) + deprecated_attr_writer(name, alternative) + end + + def deprecated_attr_reader(name, alternative, level=:warn) + define_method(name) do + Chef::Log.deprecation("#{self.class}.#{name} is deprecated. Support will be removed in a future release.") + Chef::Log.deprecation(alternative) + Chef::Log.deprecation("Called from:") + caller[0..3].each {|c| Chef::Log.deprecation(c)} + instance_variable_get("@#{name}") + end + end + + def deprecated_attr_writer(name, alternative, level=:warn) + define_method("#{name}=") do |value| + Chef::Log.deprecation("Writing to #{self.class}.#{name} with #{name}= is deprecated. Support will be removed in a future release.") + Chef::Log.deprecation(alternative) + Chef::Log.deprecation("Called from:") + caller[0..3].each {|c| Chef::Log.deprecation(c)} + instance_variable_set("@#{name}", value) + end + end end end end diff --git a/lib/chef/mixin/powershell_out.rb b/lib/chef/mixin/powershell_out.rb new file mode 100644 index 0000000000..e4f29c07c4 --- /dev/null +++ b/lib/chef/mixin/powershell_out.rb @@ -0,0 +1,98 @@ +#-- +# 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/mixin/shell_out' +require 'chef/mixin/windows_architecture_helper' + +class Chef + module Mixin + module PowershellOut + include Chef::Mixin::ShellOut + include Chef::Mixin::WindowsArchitectureHelper + + # Run a command under powershell with the same API as shell_out. The + # options hash is extended to take an "architecture" flag which + # can be set to :i386 or :x86_64 to force the windows architecture. + # + # @param script [String] script to run + # @param options [Hash] options hash + # @return [Mixlib::Shellout] mixlib-shellout object + def powershell_out(*command_args) + script = command_args.first + options = command_args.last.is_a?(Hash) ? command_args.last : nil + + run_command_with_os_architecture(script, options) + end + + # Run a command under powershell with the same API as shell_out! + # (raises exceptions on errors) + # + # @param script [String] script to run + # @param options [Hash] options hash + # @return [Mixlib::Shellout] mixlib-shellout object + def powershell_out!(*command_args) + cmd = powershell_out(*command_args) + cmd.error! + cmd + end + + private + + # Helper function to run shell_out and wrap it with the correct + # flags to possibly disable WOW64 redirection (which we often need + # because chef-client runs as a 32-bit app on 64-bit windows). + # + # @param script [String] script to run + # @param options [Hash] options hash + # @return [Mixlib::Shellout] mixlib-shellout object + def run_command_with_os_architecture(script, options) + options ||= {} + options = options.dup + arch = options.delete(:architecture) + + with_os_architecture(nil, architecture: arch) do + shell_out( + build_powershell_command(script), + options + ) + end + end + + # Helper to build a powershell command around the script to run. + # + # @param script [String] script to run + # @retrurn [String] powershell command to execute + def build_powershell_command(script) + flags = [ + # Hides the copyright banner at startup. + "-NoLogo", + # Does not present an interactive prompt to the user. + "-NonInteractive", + # Does not load the Windows PowerShell profile. + "-NoProfile", + # always set the ExecutionPolicy flag + # see http://technet.microsoft.com/en-us/library/ee176961.aspx + "-ExecutionPolicy Unrestricted", + # Powershell will hang if STDIN is redirected + # http://connect.microsoft.com/PowerShell/feedback/details/572313/powershell-exe-can-hang-if-stdin-is-redirected + "-InputFormat None" + ] + + "powershell.exe #{flags.join(' ')} -Command \"#{script}\"" + end + end + end +end diff --git a/lib/chef/mixin/provides.rb b/lib/chef/mixin/provides.rb index bc25af62b2..095e273dab 100644 --- a/lib/chef/mixin/provides.rb +++ b/lib/chef/mixin/provides.rb @@ -4,29 +4,23 @@ require 'chef/mixin/descendants_tracker' class Chef module Mixin module Provides + # TODO no longer needed, remove or deprecate? include Chef::Mixin::DescendantsTracker - def node_map - @node_map ||= Chef::NodeMap.new + def provides(short_name, opts={}, &block) + raise NotImplementedError, :provides 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) + # Check whether this resource provides the resource_name DSL for the given + # node. TODO remove this when we stop checking unregistered things. + def provides?(node, resource) + raise NotImplementedError, :provides? end - # provides a node on the resource (early binding) - def provides?(node, resource_name) - resource_name = resource_name.resource_name if resource_name.is_a?(Chef::Resource) - node_map.get(node, resource_name) + # Get the list of recipe DSL this resource is responsible for on the given + # node. + def provided_as(node) + node_map.list(node) end end end diff --git a/lib/chef/mixin/unformatter.rb b/lib/chef/mixin/unformatter.rb new file mode 100644 index 0000000000..aa5977edd7 --- /dev/null +++ b/lib/chef/mixin/unformatter.rb @@ -0,0 +1,32 @@ +# +# Author:: Jay Mundrawala (<jdm@chef.io>) +# Copyright:: Copyright (c) 2015 Chef Software +# 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 + module Mixin + module Unformatter + + def write(message) + data = message.match(/(\[.+?\] )?([\w]+):(.*)$/) + self.send(data[2].downcase.chomp.to_sym, data[3].strip) + rescue NoMethodError + self.send(:info, message) + end + + end + end +end diff --git a/lib/chef/mixin/uris.rb b/lib/chef/mixin/uris.rb new file mode 100644 index 0000000000..0136b55f6a --- /dev/null +++ b/lib/chef/mixin/uris.rb @@ -0,0 +1,44 @@ +# +# Author:: Jay Mundrawala (<jdm@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 'uri' + +class Chef + module Mixin + module Uris + # uri_scheme? returns true if the string starts with + # scheme:// + # For example, it will match http://foo.bar.com + def uri_scheme?(source) + # From open-uri + !!(%r{\A[A-Za-z][A-Za-z0-9+\-\.]*://} =~ source) + end + + + def as_uri(source) + begin + URI.parse(source) + rescue URI::InvalidURIError + Chef::Log.warn("#{source} was an invalid URI. Trying to escape invalid characters") + URI.parse(URI.escape(source)) + end + end + + end + end +end diff --git a/lib/chef/mixin/windows_architecture_helper.rb b/lib/chef/mixin/windows_architecture_helper.rb index a0ac34f627..c5f3e1bd79 100644 --- a/lib/chef/mixin/windows_architecture_helper.rb +++ b/lib/chef/mixin/windows_architecture_helper.rb @@ -42,7 +42,7 @@ class Chef is_i386_process_on_x86_64_windows? end - def with_os_architecture(node) + def with_os_architecture(node, architecture: nil) node ||= begin os_arch = ENV['PROCESSOR_ARCHITEW6432'] || ENV['PROCESSOR_ARCHITECTURE'] @@ -51,9 +51,12 @@ class Chef n[:kernel][:machine] = os_arch == 'AMD64' ? :x86_64 : :i386 end end + + architecture ||= node_windows_architecture(node) + wow64_redirection_state = nil - if wow64_architecture_override_required?(node, node_windows_architecture(node)) + if wow64_architecture_override_required?(node, architecture) wow64_redirection_state = disable_wow64_file_redirection(node) end diff --git a/lib/chef/mixin/windows_env_helper.rb b/lib/chef/mixin/windows_env_helper.rb index 490b235065..a126801a28 100644 --- a/lib/chef/mixin/windows_env_helper.rb +++ b/lib/chef/mixin/windows_env_helper.rb @@ -21,11 +21,11 @@ require 'chef/exceptions' require 'chef/platform/query_helpers' require 'chef/win32/error' if Chef::Platform.windows? require 'chef/win32/api/system' if Chef::Platform.windows? +require 'chef/win32/api/unicode' if Chef::Platform.windows? class Chef module Mixin module WindowsEnvHelper - if Chef::Platform.windows? include Chef::ReservedNames::Win32::API::System end @@ -39,7 +39,16 @@ class Chef def broadcast_env_change flags = SMTO_BLOCK | SMTO_ABORTIFHUNG | SMTO_NOTIMEOUTIFNOTHUNG - SendMessageTimeoutA(HWND_BROADCAST, WM_SETTINGCHANGE, 0, FFI::MemoryPointer.from_string('Environment').address, flags, 5000, nil) + # for why two calls, see: + # http://stackoverflow.com/questions/4968373/why-doesnt-sendmessagetimeout-update-the-environment-variables + if ( SendMessageTimeoutA(HWND_BROADCAST, WM_SETTINGCHANGE, 0, FFI::MemoryPointer.from_string('Environment').address, flags, 5000, nil) == 0 ) + Chef::ReservedNames::Win32::Error.raise! + end + if ( SendMessageTimeoutW(HWND_BROADCAST, WM_SETTINGCHANGE, 0, FFI::MemoryPointer.from_string( + Chef::ReservedNames::Win32::Unicode.utf8_to_wide('Environment') + ).address, flags, 5000, nil) == 0 ) + Chef::ReservedNames::Win32::Error.raise! + end end def expand_path(path) diff --git a/lib/chef/mixin/wstring.rb b/lib/chef/mixin/wstring.rb new file mode 100644 index 0000000000..bb6fdf4884 --- /dev/null +++ b/lib/chef/mixin/wstring.rb @@ -0,0 +1,31 @@ +# +# Author:: Jay Mundrawala(<jdm@chef.io>) +# Copyright:: Copyright 2015 Chef Software +# 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 + module Mixin + module WideString + def wstring(str) + if str.nil? || str.encoding == Encoding::UTF_16LE + str + else + str.to_wstring + end + end + end + end +end diff --git a/lib/chef/node.rb b/lib/chef/node.rb index 9823185ede..d5078371c5 100644 --- a/lib/chef/node.rb +++ b/lib/chef/node.rb @@ -77,13 +77,30 @@ class Chef @run_state = {} end + # after the run_context has been set on the node, go through the cookbook_collection + # and setup the node[:cookbooks] attribute so that it is published in the node object + def set_cookbook_attribute + return unless run_context.cookbook_collection + run_context.cookbook_collection.each do |cookbook_name, cookbook| + automatic_attrs[:cookbooks][cookbook_name][:version] = cookbook.version + end + end + # Used by DSL def node self end def chef_server_rest - @chef_server_rest ||= Chef::REST.new(Chef::Config[:chef_server_url]) + # for saving node data we use validate_utf8: false which will not + # raise an exception on bad utf8 data, but will replace the bad + # characters and render valid JSON. + @chef_server_rest ||= Chef::REST.new( + Chef::Config[:chef_server_url], + Chef::Config[:node_name], + Chef::Config[:client_key], + validate_utf8: false, + ) end # Set the name of this Node, or return the current name. @@ -244,6 +261,7 @@ class Chef # saved back to the node and be searchable def loaded_recipe(cookbook, recipe) fully_qualified_recipe = "#{cookbook}::#{recipe}" + automatic_attrs[:recipes] << fully_qualified_recipe unless Array(self[:recipes]).include?(fully_qualified_recipe) end @@ -354,7 +372,8 @@ class Chef self.tags # make sure they're defined - automatic_attrs[:recipes] = expansion.recipes + automatic_attrs[:recipes] = expansion.recipes.with_fully_qualified_names_and_version_constraints + automatic_attrs[:expanded_run_list] = expansion.recipes.with_fully_qualified_names_and_version_constraints automatic_attrs[:roles] = expansion.roles apply_expansion_attributes(expansion) diff --git a/lib/chef/node_map.rb b/lib/chef/node_map.rb index 2ca6d9ba17..f547018a38 100644 --- a/lib/chef/node_map.rb +++ b/lib/chef/node_map.rb @@ -19,128 +19,183 @@ class Chef class NodeMap - VALID_OPTS = [ - :on_platform, - :on_platforms, - :platform, - :os, - :platform_family, - ] - - DEPRECATED_OPTS = [ - :on_platform, - :on_platforms, - ] - + # # Create a new NodeMap # def initialize @map = {} end + # # Set a key/value pair on the map with a filter. The filter must be true # when applied to the node in order to retrieve the value. # # @param key [Object] Key to store # @param value [Object] Value associated with the key # @param filters [Hash] Node filter options to apply to key retrieval + # # @yield [node] Arbitrary node filter as a block which takes a node argument + # # @return [NodeMap] Returns self for possible chaining # - def set(key, value, filters = {}, &block) - validate_filter!(filters) - deprecate_filter!(filters) + def set(key, value, platform: nil, platform_version: nil, platform_family: nil, os: nil, on_platform: nil, on_platforms: nil, canonical: nil, &block) + Chef::Log.deprecation "The on_platform option to node_map has been deprecated" if on_platform + Chef::Log.deprecation "The on_platforms option to node_map has been deprecated" if on_platforms + platform ||= on_platform || on_platforms + filters = { platform: platform, platform_version: platform_version, platform_family: platform_family, os: os } + new_matcher = { filters: filters, block: block, value: value, canonical: canonical } @map[key] ||= [] - # we match on the first value we find, so we want to unshift so that the - # last setter wins - # FIXME: need a test for this behavior - @map[key].unshift({ filters: filters, block: block, value: value }) + # Decide where to insert the matcher; the new value is preferred over + # anything more specific (see `priority_of`) and is preferred over older + # values of the same specificity. (So all other things being equal, + # newest wins.) + insert_at = nil + @map[key].each_with_index do |matcher, index| + if specificity(new_matcher) >= specificity(matcher) + insert_at = index + break + end + end + if insert_at + @map[key].insert(insert_at, new_matcher) + else + @map[key] << new_matcher + end self end + # # Get a value from the NodeMap via applying the node to the filters that # were set on the key. # - # @param node [Chef::Node] The Chef::Node object for the run + # @param node [Chef::Node] The Chef::Node object for the run, or `nil` to + # ignore all filters. # @param key [Object] Key to look up + # @param canonical [Boolean] `true` or `false` to match canonical or + # non-canonical values only. `nil` to ignore canonicality. Default: `nil` + # # @return [Object] Value # - def get(node, key) - # FIXME: real exception - raise "first argument must be a Chef::Node" unless node.is_a?(Chef::Node) - return nil unless @map.has_key?(key) - @map[key].each do |matcher| - if filters_match?(node, matcher[:filters]) && - block_matches?(node, matcher[:block]) - return matcher[:value] + def get(node, key, canonical: nil) + raise ArgumentError, "first argument must be a Chef::Node" unless node.is_a?(Chef::Node) || node.nil? + list(node, key, canonical: canonical).first + end + + # + # List all matches for the given node and key from the NodeMap, from + # most-recently added to oldest. + # + # @param node [Chef::Node] The Chef::Node object for the run, or `nil` to + # ignore all filters. + # @param key [Object] Key to look up + # @param canonical [Boolean] `true` or `false` to match canonical or + # non-canonical values only. `nil` to ignore canonicality. Default: `nil` + # + # @return [Object] Value + # + def list(node, key, canonical: nil) + raise ArgumentError, "first argument must be a Chef::Node" unless node.is_a?(Chef::Node) || node.nil? + return [] unless @map.has_key?(key) + @map[key].select do |matcher| + node_matches?(node, matcher) && canonical_matches?(canonical, matcher) + end.map { |matcher| matcher[:value] } + end + + # Seriously, don't use this, it's nearly certain to change on you + # @return remaining + # @api private + def delete_canonical(key, value) + remaining = @map[key] + if remaining + remaining.delete_if { |matcher| matcher[:canonical] && Array(matcher[:value]) == Array(value) } + if remaining.empty? + @map.delete(key) + remaining = nil end end - nil + remaining end private - # only allow valid filter options - def validate_filter!(filters) - filters.each_key do |key| - # FIXME: real exception - raise "Bad key #{key} in Chef::NodeMap filter expression" unless VALID_OPTS.include?(key) + # + # Gives a value for "how specific" the matcher is. + # Things which specify more specific filters get a higher number + # (platform_version > platform > platform_family > os); things + # with a block have higher specificity than similar things without + # a block. + # + def specificity(matcher) + if matcher[:filters][:platform_version] + specificity = 8 + elsif matcher[:filters][:platform] + specificity = 6 + elsif matcher[:filters][:platform_family] + specificity = 4 + elsif matcher[:filters][:os] + specificity = 2 + else + specificity = 0 end + specificity += 1 if matcher[:block] + specificity end - # warn on deprecated filter options - def deprecate_filter!(filters) - filters.each_key do |key| - Chef::Log.warn "The #{key} option to node_map has been deprecated" if DEPRECATED_OPTS.include?(key) - end - end + # + # Succeeds if: + # - no negative matches (!value) + # - at least one positive match (value or :all), or no positive filters + # + def matches_black_white_list?(node, filters, attribute) + # It's super common for the filter to be nil. Catch that so we don't + # spend any time here. + return true if !filters[attribute] + filter_values = Array(filters[attribute]) + value = node[attribute] - # @todo: this works fine, but is probably hard to understand - def negative_match(filter, param) - # We support strings prefaced by '!' to mean 'not'. In particular, this is most useful - # for os matching on '!windows'. - negative_matches = filter.select { |f| f[0] == '!' } - return true if !negative_matches.empty? && negative_matches.include?('!' + param) + # Split the blacklist and whitelist + blacklist, whitelist = filter_values.partition { |v| v.is_a?(String) && v.start_with?('!') } - # We support the symbol :all to match everything, for backcompat, but this can and should - # simply be ommitted. - positive_matches = filter.reject { |f| f[0] == '!' || f == :all } - return true if !positive_matches.empty? && !positive_matches.include?(param) + # If any blacklist value matches, we don't match + return false if blacklist.any? { |v| v[1..-1] == value } - # sorry double-negative: this means we pass this filter. - false + # If the whitelist is empty, or anything matches, we match. + whitelist.empty? || whitelist.any? { |v| v == :all || v == value } end - def filters_match?(node, filters) - return true if filters.empty? + def matches_version_list?(node, filters, attribute) + # It's super common for the filter to be nil. Catch that so we don't + # spend any time here. + return true if !filters[attribute] + filter_values = Array(filters[attribute]) + value = node[attribute] - # each filter is applied in turn. if any fail, then it shortcuts and returns false. - # if it passes or does not exist it succeeds and continues on. so multiple filters are - # effectively joined by 'and'. all filters can be single strings, or arrays which are - # effectively joined by 'or'. - - os_filter = [ filters[:os] ].flatten.compact - unless os_filter.empty? - return false if negative_match(os_filter, node[:os]) - end - - platform_family_filter = [ filters[:platform_family] ].flatten.compact - unless platform_family_filter.empty? - return false if negative_match(platform_family_filter, node[:platform_family]) - end - - # :on_platform and :on_platforms here are synonyms which are deprecated - platform_filter = [ filters[:platform] || filters[:on_platform] || filters[:on_platforms] ].flatten.compact - unless platform_filter.empty? - return false if negative_match(platform_filter, node[:platform]) + filter_values.empty? || + Array(filter_values).any? do |v| + Chef::VersionConstraint::Platform.new(v).include?(value) end + end - return true + def filters_match?(node, filters) + matches_black_white_list?(node, filters, :os) && + matches_black_white_list?(node, filters, :platform_family) && + matches_black_white_list?(node, filters, :platform) && + matches_version_list?(node, filters, :platform_version) end def block_matches?(node, block) return true if block.nil? block.call node end + + def node_matches?(node, matcher) + return true if !node + filters_match?(node, matcher[:filters]) && block_matches?(node, matcher[:block]) + end + + def canonical_matches?(canonical, matcher) + return true if canonical.nil? + !!canonical == !!matcher[:canonical] + end end end diff --git a/lib/chef/osc_user.rb b/lib/chef/osc_user.rb new file mode 100644 index 0000000000..52bfd11108 --- /dev/null +++ b/lib/chef/osc_user.rb @@ -0,0 +1,194 @@ +# +# Author:: Steven Danna (steve@opscode.com) +# Copyright:: Copyright 2012 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/config' +require 'chef/mixin/params_validate' +require 'chef/mixin/from_file' +require 'chef/mash' +require 'chef/json_compat' +require 'chef/search/query' + +# TODO +# DEPRECATION NOTE +# This class was previously Chef::User. It is the code to support the User object +# corrosponding to the Open Source Chef Server 11 and only still exists to support +# users still on OSC 11. +# +# Chef::User now supports Chef Server 12. +# +# New development should occur in Chef::User. +# This file and corrosponding osc_user knife files +# should be removed once client support for Open Source Chef Server 11 expires. +class Chef + class OscUser + + include Chef::Mixin::FromFile + include Chef::Mixin::ParamsValidate + + def initialize + @name = '' + @public_key = nil + @private_key = nil + @password = nil + @admin = false + end + + def name(arg=nil) + set_or_return(:name, arg, + :regex => /^[a-z0-9\-_]+$/) + end + + def admin(arg=nil) + set_or_return(:admin, + arg, :kind_of => [TrueClass, FalseClass]) + end + + def public_key(arg=nil) + 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 password(arg=nil) + set_or_return(:password, + arg, :kind_of => String) + end + + def to_hash + result = { + "name" => @name, + "public_key" => @public_key, + "admin" => @admin + } + result["private_key"] = @private_key if @private_key + result["password"] = @password if @password + result + end + + def to_json(*a) + Chef::JSONCompat.to_json(to_hash, *a) + end + + def destroy + Chef::REST.new(Chef::Config[:chef_server_url]).delete_rest("users/#{@name}") + end + + def create + payload = {:name => self.name, :admin => self.admin, :password => self.password } + payload[:public_key] = public_key if public_key + new_user =Chef::REST.new(Chef::Config[:chef_server_url]).post_rest("users", payload) + Chef::OscUser.from_hash(self.to_hash.merge(new_user)) + end + + def update(new_key=false) + payload = {:name => name, :admin => admin} + payload[:private_key] = new_key if new_key + payload[:password] = password if password + updated_user = Chef::REST.new(Chef::Config[:chef_server_url]).put_rest("users/#{name}", payload) + Chef::OscUser.from_hash(self.to_hash.merge(updated_user)) + end + + def save(new_key=false) + begin + create + rescue Net::HTTPServerException => e + if e.response.code == "409" + update(new_key) + else + raise e + end + end + end + + def reregister + r = Chef::REST.new(Chef::Config[:chef_server_url]) + reregistered_self = r.put_rest("users/#{name}", { :name => name, :admin => admin, :private_key => true }) + private_key(reregistered_self["private_key"]) + self + end + + def to_s + "user[#{@name}]" + end + + def inspect + "Chef::OscUser name:'#{name}' admin:'#{admin.inspect}'" + + "public_key:'#{public_key}' private_key:#{private_key}" + end + + # Class Methods + + def self.from_hash(user_hash) + user = Chef::OscUser.new + user.name user_hash['name'] + user.private_key user_hash['private_key'] if user_hash.key?('private_key') + user.password user_hash['password'] if user_hash.key?('password') + user.public_key user_hash['public_key'] + user.admin user_hash['admin'] + user + end + + def self.from_json(json) + Chef::OscUser.from_hash(Chef::JSONCompat.from_json(json)) + end + + class << self + alias_method :json_create, :from_json + end + + def self.list(inflate=false) + response = Chef::REST.new(Chef::Config[:chef_server_url]).get_rest('users') + users = if response.is_a?(Array) + transform_ohc_list_response(response) # OHC/OPC + else + response # OSC + end + if inflate + users.inject({}) do |user_map, (name, _url)| + user_map[name] = Chef::OscUser.load(name) + user_map + end + else + users + end + end + + def self.load(name) + response = Chef::REST.new(Chef::Config[:chef_server_url]).get_rest("users/#{name}") + Chef::OscUser.from_hash(response) + end + + # Gross. Transforms an API response in the form of: + # [ { "user" => { "username" => USERNAME }}, ...] + # into the form + # { "USERNAME" => "URI" } + def self.transform_ohc_list_response(response) + new_response = Hash.new + response.each do |u| + name = u['user']['username'] + new_response[name] = Chef::Config[:chef_server_url] + "/users/#{name}" + end + new_response + end + + private_class_method :transform_ohc_list_response + end +end diff --git a/lib/chef/platform/provider_mapping.rb b/lib/chef/platform/provider_mapping.rb index 0d7285729f..e3a894c8ac 100644 --- a/lib/chef/platform/provider_mapping.rb +++ b/lib/chef/platform/provider_mapping.rb @@ -20,13 +20,8 @@ require 'chef/log' require 'chef/exceptions' require 'chef/mixin/params_validate' require 'chef/version_constraint/platform' - -# This file depends on nearly every provider in chef, but requiring them -# directly causes circular requires resulting in uninitialized constant errors. -# Therefore, we do the includes inline rather than up top. require 'chef/provider' - class Chef class Platform @@ -34,267 +29,7 @@ class Chef attr_writer :platforms def platforms - @platforms ||= begin - require 'chef/providers' - - { - :freebsd => { - :default => { - :group => Chef::Provider::Group::Pw, - :user => Chef::Provider::User::Pw, - } - }, - :ubuntu => { - :default => { - :package => Chef::Provider::Package::Apt, - :service => Chef::Provider::Service::Debian, - }, - ">= 11.10" => { - :ifconfig => Chef::Provider::Ifconfig::Debian - } - # Chef::Provider::Service::Upstart is a candidate to be used in - # ubuntu versions >= 13.10 but it currently requires all the - # services to have an entry under /etc/init. We need to update it - # to use the service ctl apis in order to migrate to using it on - # ubuntu >= 13.10. - }, - :gcel => { - :default => { - :package => Chef::Provider::Package::Apt, - :service => Chef::Provider::Service::Debian, - } - }, - :linaro => { - :default => { - :package => Chef::Provider::Package::Apt, - :service => Chef::Provider::Service::Debian, - } - }, - :raspbian => { - :default => { - :package => Chef::Provider::Package::Apt, - :service => Chef::Provider::Service::Debian, - } - }, - :linuxmint => { - :default => { - :package => Chef::Provider::Package::Apt, - :service => Chef::Provider::Service::Upstart, - } - }, - :debian => { - :default => { - :package => Chef::Provider::Package::Apt, - :service => Chef::Provider::Service::Debian, - }, - ">= 6.0" => { - :service => Chef::Provider::Service::Insserv - }, - ">= 7.0" => { - :ifconfig => Chef::Provider::Ifconfig::Debian - } - }, - :xenserver => { - :default => { - :service => Chef::Provider::Service::Redhat, - :package => Chef::Provider::Package::Yum, - } - }, - :xcp => { - :default => { - :service => Chef::Provider::Service::Redhat, - :package => Chef::Provider::Package::Yum, - } - }, - :centos => { - :default => { - :service => Chef::Provider::Service::Systemd, - :package => Chef::Provider::Package::Yum, - :ifconfig => Chef::Provider::Ifconfig::Redhat - }, - "< 7" => { - :service => Chef::Provider::Service::Redhat - } - }, - :amazon => { - :default => { - :service => Chef::Provider::Service::Redhat, - :package => Chef::Provider::Package::Yum, - } - }, - :scientific => { - :default => { - :service => Chef::Provider::Service::Systemd, - :package => Chef::Provider::Package::Yum, - }, - "< 7" => { - :service => Chef::Provider::Service::Redhat - } - }, - :fedora => { - :default => { - :service => Chef::Provider::Service::Systemd, - :package => Chef::Provider::Package::Yum, - :ifconfig => Chef::Provider::Ifconfig::Redhat - }, - "< 15" => { - :service => Chef::Provider::Service::Redhat - } - }, - :opensuse => { - :default => { - :service => Chef::Provider::Service::Redhat, - :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 - } - }, - :suse => { - :default => { - :service => Chef::Provider::Service::Systemd, - :package => Chef::Provider::Package::Zypper, - :group => Chef::Provider::Group::Gpasswd - }, - "< 12.0" => { - :group => Chef::Provider::Group::Suse, - :service => Chef::Provider::Service::Redhat - } - }, - :oracle => { - :default => { - :service => Chef::Provider::Service::Systemd, - :package => Chef::Provider::Package::Yum, - }, - "< 7" => { - :service => Chef::Provider::Service::Redhat - } - }, - :redhat => { - :default => { - :service => Chef::Provider::Service::Systemd, - :package => Chef::Provider::Package::Yum, - :ifconfig => Chef::Provider::Ifconfig::Redhat - }, - "< 7" => { - :service => Chef::Provider::Service::Redhat - } - }, - :ibm_powerkvm => { - :default => { - :service => Chef::Provider::Service::Redhat, - :package => Chef::Provider::Package::Yum, - :ifconfig => Chef::Provider::Ifconfig::Redhat - } - }, - :cloudlinux => { - :default => { - :service => Chef::Provider::Service::Redhat, - :package => Chef::Provider::Package::Yum, - :ifconfig => Chef::Provider::Ifconfig::Redhat - } - }, - :parallels => { - :default => { - :service => Chef::Provider::Service::Redhat, - :package => Chef::Provider::Package::Yum, - :ifconfig => Chef::Provider::Ifconfig::Redhat - } - }, - :gentoo => { - :default => { - :package => Chef::Provider::Package::Portage, - :service => Chef::Provider::Service::Gentoo, - } - }, - :arch => { - :default => { - :package => Chef::Provider::Package::Pacman, - :service => Chef::Provider::Service::Systemd, - } - }, - :solaris => {}, - :openindiana => { - :default => { - :mount => Chef::Provider::Mount::Solaris, - :package => Chef::Provider::Package::Ips, - :group => Chef::Provider::Group::Usermod - } - }, - :opensolaris => { - :default => { - :mount => Chef::Provider::Mount::Solaris, - :package => Chef::Provider::Package::Ips, - :group => Chef::Provider::Group::Usermod - } - }, - :nexentacore => { - :default => { - :mount => Chef::Provider::Mount::Solaris, - :package => Chef::Provider::Package::Solaris, - :group => Chef::Provider::Group::Usermod - } - }, - :omnios => { - :default => { - :mount => Chef::Provider::Mount::Solaris, - :package => Chef::Provider::Package::Ips, - :group => Chef::Provider::Group::Usermod, - :user => Chef::Provider::User::Solaris, - } - }, - :solaris2 => { - :default => { - :mount => Chef::Provider::Mount::Solaris, - :package => Chef::Provider::Package::Ips, - :group => Chef::Provider::Group::Usermod, - :user => Chef::Provider::User::Solaris, - }, - "< 5.11" => { - :mount => Chef::Provider::Mount::Solaris, - :package => Chef::Provider::Package::Solaris, - :group => Chef::Provider::Group::Usermod, - :user => Chef::Provider::User::Solaris, - } - }, - :smartos => { - :default => { - :mount => Chef::Provider::Mount::Solaris, - :package => Chef::Provider::Package::SmartOS, - :group => Chef::Provider::Group::Usermod - } - }, - :hpux => { - :default => { - :group => Chef::Provider::Group::Usermod - } - }, - :aix => { - :default => { - :group => Chef::Provider::Group::Aix, - :mount => Chef::Provider::Mount::Aix, - :ifconfig => Chef::Provider::Ifconfig::Aix, - :package => Chef::Provider::Package::Aix, - :user => Chef::Provider::User::Aix, - :service => Chef::Provider::Service::Aix - } - }, - :exherbo => { - :default => { - :package => Chef::Provider::Package::Paludis, - :service => Chef::Provider::Service::Systemd, - } - }, - :default => { - :mount => Chef::Provider::Mount::Mount, - :user => Chef::Provider::User::Useradd, - :group => Chef::Provider::Group::Gpasswd, - :ifconfig => Chef::Provider::Ifconfig, - } - } - end + @platforms ||= { default: {} } end include Chef::Mixin::ParamsValidate @@ -304,7 +39,7 @@ class Chef name_sym = name if name.kind_of?(String) - name.downcase! + name = name.downcase name.gsub!(/\s/, "_") name_sym = name.to_sym end @@ -325,8 +60,6 @@ class Chef Chef::Log.debug("Chef::Version::Comparable does not know how to parse the platform version: #{version}") end end - else - Chef::Log.debug("Platform #{name} not found, using all defaults. (Unsupported platform?)") end provider_map end @@ -460,16 +193,20 @@ class Chef pmap.has_key?(rtkey) ? pmap[rtkey] : nil end + include Chef::Mixin::ConvertToClassName + def resource_matching_provider(platform, version, resource_type) if resource_type.kind_of?(Chef::Resource) + class_name = resource_type.class.to_s.split('::').last + begin - Chef::Provider.const_get(resource_type.class.to_s.split('::').last) + result = Chef::Provider.const_get(class_name) + Chef::Log.warn("Class Chef::Provider::#{class_name} does not declare 'provides #{convert_to_snake_case(class_name).to_sym.inspect}'.") + Chef::Log.warn("This will no longer work in Chef 13: you must use 'provides' to provide DSL.") rescue NameError - nil end - else - nil end + result end end diff --git a/lib/chef/platform/provider_priority_map.rb b/lib/chef/platform/provider_priority_map.rb index 1539f61900..9d703c9178 100644 --- a/lib/chef/platform/provider_priority_map.rb +++ b/lib/chef/platform/provider_priority_map.rb @@ -1,88 +1,25 @@ +require 'singleton' class Chef class Platform class ProviderPriorityMap 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) + def set_priority_array(resource_name, priority_array, *filter, &block) + priority_map.set(resource_name.to_sym, Array(priority_array), *filter, &block) end - def priority(*args) - priority_map.set(*args) + # @api private + def list_handlers(node, resource_name) + priority_map.list(node, resource_name.to_sym).flatten(1).uniq end private - def load_default_map - require 'chef/providers' - - # - # Linux - # - - # default block for linux O/Sen must come before platform_family exceptions - priority :service, [ - Chef::Provider::Service::Systemd, - Chef::Provider::Service::Insserv, - Chef::Provider::Service::Redhat, - ], os: "linux" - - priority :service, [ - Chef::Provider::Service::Systemd, - Chef::Provider::Service::Arch, - ], platform_family: "arch" - - priority :service, [ - Chef::Provider::Service::Systemd, - Chef::Provider::Service::Gentoo, - ], platform_family: "gentoo" - - priority :service, [ - # we can determine what systemd supports accurately - Chef::Provider::Service::Systemd, - # on debian-ish system if an upstart script exists that must win over sysv types - Chef::Provider::Service::Upstart, - Chef::Provider::Service::Insserv, - Chef::Provider::Service::Debian, - Chef::Provider::Service::Invokercd, - ], platform_family: "debian" - - priority :service, [ - Chef::Provider::Service::Systemd, - Chef::Provider::Service::Insserv, - Chef::Provider::Service::Redhat, - ], platform_family: [ "rhel", "fedora", "suse" ] - - # - # BSDen - # - - priority :service, Chef::Provider::Service::Freebsd, os: [ "freebsd", "netbsd" ] - priority :service, Chef::Provider::Service::Openbsd, os: [ "openbsd" ] - - # - # Solaris-en - # - - priority :service, Chef::Provider::Service::Solaris, os: "solaris2" - - # - # Mac - # - - priority :service, Chef::Provider::Service::Macosx, os: "darwin" - priority :package, Chef::Provider::Package::Homebrew, os: "darwin" - end - def priority_map require 'chef/node_map' @priority_map ||= Chef::NodeMap.new diff --git a/lib/chef/platform/query_helpers.rb b/lib/chef/platform/query_helpers.rb index f7c85fbe23..b3948eac21 100644 --- a/lib/chef/platform/query_helpers.rb +++ b/lib/chef/platform/query_helpers.rb @@ -21,11 +21,7 @@ class Chef class << self def windows? - if RUBY_PLATFORM =~ /mswin|mingw|windows/ - true - else - false - end + ChefConfig.windows? end def windows_server_2003? @@ -43,6 +39,11 @@ class Chef is_server_2003 end + def supports_powershell_execution_bypass?(node) + node[:languages] && node[:languages][:powershell] && + node[:languages][:powershell][:version].to_i >= 3 + end + def supports_dsc?(node) node[:languages] && node[:languages][:powershell] && node[:languages][:powershell][:version].to_i >= 4 diff --git a/lib/chef/platform/resource_priority_map.rb b/lib/chef/platform/resource_priority_map.rb index fc43b3e7db..fb08debc53 100644 --- a/lib/chef/platform/resource_priority_map.rb +++ b/lib/chef/platform/resource_priority_map.rb @@ -1,33 +1,30 @@ +require 'singleton' + class Chef class Platform class ResourcePriorityMap include Singleton - def initialize - load_default_map + def get_priority_array(node, resource_name, canonical: nil) + priority_map.get(node, resource_name.to_sym, canonical: canonical) end - def get_priority_array(node, resource_name) - priority_map.get(node, resource_name.to_sym) + def set_priority_array(resource_name, priority_array, *filter, &block) + priority_map.set(resource_name.to_sym, Array(priority_array), *filter, &block) end - def set_priority_array(resource_name, priority_array, *filter) - priority resource_name.to_sym, priority_array.to_a, *filter + # @api private + def delete_canonical(resource_name, resource_class) + priority_map.delete_canonical(resource_name, resource_class) end - def priority(*args) - priority_map.set(*args) + # @api private + def list_handlers(*args) + priority_map.list(*args).flatten(1).uniq 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 diff --git a/lib/chef/platform/service_helpers.rb b/lib/chef/platform/service_helpers.rb index dc0a808c06..d50812e687 100644 --- a/lib/chef/platform/service_helpers.rb +++ b/lib/chef/platform/service_helpers.rb @@ -42,34 +42,34 @@ class Chef # different services is NOT a design concern of this module. # def service_resource_providers - service_resource_providers = [] + @service_resource_providers ||= [].tap do |service_resource_providers| - if ::File.exist?("/usr/sbin/update-rc.d") - service_resource_providers << :debian - end + if ::File.exist?("/usr/sbin/update-rc.d") + service_resource_providers << :debian + end - if ::File.exist?("/usr/sbin/invoke-rc.d") - service_resource_providers << :invokercd - end + if ::File.exist?("/usr/sbin/invoke-rc.d") + service_resource_providers << :invokercd + end - if ::File.exist?("/sbin/insserv") - service_resource_providers << :insserv - end + if ::File.exist?("/sbin/insserv") + service_resource_providers << :insserv + end - # debian >= 6.0 has /etc/init but does not have upstart - if ::File.exist?("/etc/init") && ::File.exist?("/sbin/start") - service_resource_providers << :upstart - end + # debian >= 6.0 has /etc/init but does not have upstart + if ::File.exist?("/etc/init") && ::File.exist?("/sbin/start") + service_resource_providers << :upstart + end - if ::File.exist?("/sbin/chkconfig") - service_resource_providers << :redhat - end + if ::File.exist?("/sbin/chkconfig") + service_resource_providers << :redhat + end - if systemd_sanity_check? - service_resource_providers << :systemd - end + if systemd_sanity_check? + service_resource_providers << :systemd + end - service_resource_providers + end end def config_for_service(service_name) diff --git a/lib/chef/policy_builder/policyfile.rb b/lib/chef/policy_builder/policyfile.rb index ac25b549be..5991e3ce10 100644 --- a/lib/chef/policy_builder/policyfile.rb +++ b/lib/chef/policy_builder/policyfile.rb @@ -119,6 +119,7 @@ class Chef @node = Chef::Node.find_or_create(node_name) validate_policyfile + events.policyfile_loaded(policy) node rescue Exception => e events.node_load_failed(node_name, e, Chef::Config) diff --git a/lib/chef/provider.rb b/lib/chef/provider.rb index 65a56cf726..e50e374804 100644 --- a/lib/chef/provider.rb +++ b/lib/chef/provider.rb @@ -22,14 +22,19 @@ 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/powershell_out' require 'chef/mixin/provides' require 'chef/platform/service_helpers' require 'chef/node_map' class Chef class Provider + require 'chef/mixin/why_run' + require 'chef/mixin/shell_out' + require 'chef/mixin/provides' include Chef::Mixin::WhyRun include Chef::Mixin::ShellOut + include Chef::Mixin::PowershellOut extend Chef::Mixin::Provides # supports the given resource and action (late binding) @@ -83,6 +88,9 @@ class Chef new_resource.cookbook_name end + def check_resource_semantics! + end + def load_current_resource raise Chef::Exceptions::Override, "You must override load_current_resource in #{self.to_s}" end @@ -108,6 +116,8 @@ class Chef # TODO: it would be preferable to get the action to be executed in the # constructor... + check_resource_semantics! + # user-defined LWRPs may include unsafe load_current_resource methods that cannot be run in whyrun mode if !whyrun_mode? || whyrun_supported? load_current_resource @@ -165,6 +175,14 @@ class Chef converge_actions.add_action(descriptions, &block) end + def self.provides(short_name, opts={}, &block) + Chef.set_provider_priority_array(short_name, self, opts, &block) + end + + def self.provides?(node, resource) + Chef::ProviderResolver.new(node, resource, :nothing).provided_by?(self) + end + protected def converge_actions @@ -191,5 +209,39 @@ class Chef end end + module DeprecatedLWRPClass + def const_missing(class_name) + if deprecated_constants[class_name.to_sym] + Chef::Log.deprecation("Using an LWRP provider by its name (#{class_name}) directly is no longer supported in Chef 12 and will be removed. Use Chef::ProviderResolver.new(node, resource, action) instead.") + deprecated_constants[class_name.to_sym] + else + raise NameError, "uninitialized constant Chef::Provider::#{class_name}" + end + end + + # @api private + def register_deprecated_lwrp_class(provider_class, class_name) + # Register Chef::Provider::MyProvider with deprecation warnings if you + # try to access it + if Chef::Provider.const_defined?(class_name, false) + Chef::Log.warn "Chef::Provider::#{class_name} already exists! Cannot create deprecation class for #{provider_class}" + else + deprecated_constants[class_name.to_sym] = provider_class + end + end + + private + + def deprecated_constants + @deprecated_constants ||= {} + end + end + extend DeprecatedLWRPClass end end + +# Requiring things at the bottom breaks cycles +require 'chef/chef_class' +require 'chef/mixin/why_run' +require 'chef/resource_collection' +require 'chef/runner' diff --git a/lib/chef/provider/cron/unix.rb b/lib/chef/provider/cron/unix.rb index 0750c0420b..01c61e4253 100644 --- a/lib/chef/provider/cron/unix.rb +++ b/lib/chef/provider/cron/unix.rb @@ -20,6 +20,7 @@ require 'chef/log' require 'chef/provider' +require 'chef/provider/cron' class Chef class Provider diff --git a/lib/chef/provider/directory.rb b/lib/chef/provider/directory.rb index 416393ac60..4d5423d0e8 100644 --- a/lib/chef/provider/directory.rb +++ b/lib/chef/provider/directory.rb @@ -43,6 +43,9 @@ class Chef end def define_resource_requirements + # deep inside FAC we have to assert requirements, so call FACs hook to set that up + access_controls.define_resource_requirements + requirements.assert(:create) do |a| # Make sure the parent dir exists, or else fail. # for why run, print a message explaining the potential error. diff --git a/lib/chef/provider/dsc_resource.rb b/lib/chef/provider/dsc_resource.rb index 2812c154c6..5fa84a21e9 100644 --- a/lib/chef/provider/dsc_resource.rb +++ b/lib/chef/provider/dsc_resource.rb @@ -121,7 +121,14 @@ class Chef # however Invoke-DscResource is not correctly writing to that # stream and instead just dumping to stdout @converge_description = result.stdout - result.return_value[0]["InDesiredState"] + + if result.return_value.is_a?(Array) + # WMF Feb 2015 Preview + result.return_value[0]["InDesiredState"] + else + # WMF April 2015 Preview + result.return_value["InDesiredState"] + end end def set_resource diff --git a/lib/chef/provider/file.rb b/lib/chef/provider/file.rb index c070d29458..5ed7c6ac5b 100644 --- a/lib/chef/provider/file.rb +++ b/lib/chef/provider/file.rb @@ -26,8 +26,10 @@ require 'fileutils' require 'chef/scan_access_control' require 'chef/mixin/checksum' require 'chef/mixin/file_class' +require 'chef/mixin/enforce_ownership_and_permissions' require 'chef/util/backup' require 'chef/util/diff' +require 'chef/util/selinux' require 'chef/deprecation/provider/file' require 'chef/deprecation/warnings' require 'chef/file_content_management/deploy' @@ -386,10 +388,11 @@ class Chef def update_file_contents do_backup unless needs_creating? - deployment_strategy.deploy(tempfile.path, ::File.realpath(@new_resource.path)) - Chef::Log.info("#{@new_resource} updated file contents #{@new_resource.path}") + deployment_strategy.deploy(tempfile.path, ::File.realpath(new_resource.path)) + Chef::Log.info("#{new_resource} updated file contents #{new_resource.path}") if managing_content? - @new_resource.checksum(checksum(@new_resource.path)) # for reporting + # save final checksum for reporting. + new_resource.final_checksum = checksum(new_resource.path) end end diff --git a/lib/chef/provider/group/aix.rb b/lib/chef/provider/group/aix.rb index 6ac9d03357..92bb8cb225 100644 --- a/lib/chef/provider/group/aix.rb +++ b/lib/chef/provider/group/aix.rb @@ -23,6 +23,7 @@ class Chef class Provider class Group class Aix < Chef::Provider::Group::Groupadd + provides :group, platform: 'aix' def required_binaries [ "/usr/bin/mkgroup", diff --git a/lib/chef/provider/group/dscl.rb b/lib/chef/provider/group/dscl.rb index d7e8f2e827..9775ac8270 100644 --- a/lib/chef/provider/group/dscl.rb +++ b/lib/chef/provider/group/dscl.rb @@ -21,7 +21,7 @@ class Chef class Group class Dscl < Chef::Provider::Group - provides :group, os: "darwin" + provides :group, os: 'darwin' def dscl(*args) host = "." diff --git a/lib/chef/provider/group/gpasswd.rb b/lib/chef/provider/group/gpasswd.rb index 521affac11..432c524acd 100644 --- a/lib/chef/provider/group/gpasswd.rb +++ b/lib/chef/provider/group/gpasswd.rb @@ -22,6 +22,7 @@ class Chef class Provider class Group class Gpasswd < Chef::Provider::Group::Groupadd + provides :group def load_current_resource super diff --git a/lib/chef/provider/group/groupmod.rb b/lib/chef/provider/group/groupmod.rb index f9299546c8..82b68b8672 100644 --- a/lib/chef/provider/group/groupmod.rb +++ b/lib/chef/provider/group/groupmod.rb @@ -21,7 +21,7 @@ class Chef class Group class Groupmod < Chef::Provider::Group - provides :group, os: "netbsd" + provides :group, os: 'netbsd' def load_current_resource super diff --git a/lib/chef/provider/group/pw.rb b/lib/chef/provider/group/pw.rb index 7a66ab4d69..f877ed2424 100644 --- a/lib/chef/provider/group/pw.rb +++ b/lib/chef/provider/group/pw.rb @@ -20,6 +20,7 @@ class Chef class Provider class Group class Pw < Chef::Provider::Group + provides :group, platform: 'freebsd' def load_current_resource super diff --git a/lib/chef/provider/group/suse.rb b/lib/chef/provider/group/suse.rb index 7ac2831d02..b47ea33e80 100644 --- a/lib/chef/provider/group/suse.rb +++ b/lib/chef/provider/group/suse.rb @@ -22,6 +22,8 @@ class Chef class Provider class Group class Suse < Chef::Provider::Group::Groupadd + provides :group, platform: 'opensuse', platform_version: '< 12.3' + provides :group, platform: 'suse', platform_version: '< 12.0' def load_current_resource super diff --git a/lib/chef/provider/group/usermod.rb b/lib/chef/provider/group/usermod.rb index e50e13c443..d78d42d6e1 100644 --- a/lib/chef/provider/group/usermod.rb +++ b/lib/chef/provider/group/usermod.rb @@ -23,7 +23,8 @@ class Chef class Group class Usermod < Chef::Provider::Group::Groupadd - provides :group, os: "openbsd" + provides :group, os: %w(openbsd solaris2 hpux) + provides :group, platform: "opensuse" def load_current_resource super diff --git a/lib/chef/provider/group/windows.rb b/lib/chef/provider/group/windows.rb index 54e49b0e06..46d8afc7f6 100644 --- a/lib/chef/provider/group/windows.rb +++ b/lib/chef/provider/group/windows.rb @@ -26,7 +26,7 @@ class Chef class Group class Windows < Chef::Provider::Group - provides :group, os: "windows" + provides :group, os: 'windows' def initialize(new_resource,run_context) super diff --git a/lib/chef/provider/ifconfig.rb b/lib/chef/provider/ifconfig.rb index 06080c90c3..468e1ec639 100644 --- a/lib/chef/provider/ifconfig.rb +++ b/lib/chef/provider/ifconfig.rb @@ -39,6 +39,8 @@ require 'erb' class Chef class Provider class Ifconfig < Chef::Provider + provides :ifconfig + include Chef::Mixin::ShellOut include Chef::Mixin::Command diff --git a/lib/chef/provider/ifconfig/aix.rb b/lib/chef/provider/ifconfig/aix.rb index 8fead44bc6..25c3de3040 100644 --- a/lib/chef/provider/ifconfig/aix.rb +++ b/lib/chef/provider/ifconfig/aix.rb @@ -22,6 +22,7 @@ class Chef class Provider class Ifconfig class Aix < Chef::Provider::Ifconfig + provides :ifconfig, platform: %w(aix) def load_current_resource @current_resource = Chef::Resource::Ifconfig.new(@new_resource.name) diff --git a/lib/chef/provider/ifconfig/debian.rb b/lib/chef/provider/ifconfig/debian.rb index 7589971143..1e6863c8b5 100644 --- a/lib/chef/provider/ifconfig/debian.rb +++ b/lib/chef/provider/ifconfig/debian.rb @@ -23,6 +23,8 @@ class Chef class Provider class Ifconfig class Debian < Chef::Provider::Ifconfig + provides :ifconfig, platform: %w(ubuntu), platform_version: '>= 11.10' + provides :ifconfig, platform: %w(debian), platform_version: '>= 7.0' INTERFACES_FILE = "/etc/network/interfaces" INTERFACES_DOT_D_DIR = "/etc/network/interfaces.d" diff --git a/lib/chef/provider/ifconfig/redhat.rb b/lib/chef/provider/ifconfig/redhat.rb index ef35b0e012..ee053d1e52 100644 --- a/lib/chef/provider/ifconfig/redhat.rb +++ b/lib/chef/provider/ifconfig/redhat.rb @@ -22,6 +22,7 @@ class Chef class Provider class Ifconfig class Redhat < Chef::Provider::Ifconfig + provides :ifconfig, platform_family: %w(fedora rhel) def initialize(new_resource, run_context) super(new_resource, run_context) diff --git a/lib/chef/provider/lwrp_base.rb b/lib/chef/provider/lwrp_base.rb index 492ddda6da..b5efbb284d 100644 --- a/lib/chef/provider/lwrp_base.rb +++ b/lib/chef/provider/lwrp_base.rb @@ -19,6 +19,7 @@ # require 'chef/provider' +require 'chef/dsl/include_recipe' class Chef class Provider @@ -69,9 +70,6 @@ class Chef end - extend Chef::Mixin::ConvertToClassName - extend Chef::Mixin::FromFile - include Chef::DSL::Recipe # These were previously provided by Chef::Mixin::RecipeDefinitionDSLCore. @@ -80,71 +78,95 @@ class Chef include Chef::DSL::PlatformIntrospection include Chef::DSL::DataQuery - def self.build_from_file(cookbook_name, filename, run_context) - provider_class = nil - provider_name = filename_to_qualified_string(cookbook_name, filename) + # Allow include_recipe from within LWRP provider code + include Chef::DSL::IncludeRecipe + + # no-op `load_current_resource`. Allows simple LWRP providers to work + # without defining this method explicitly (silences + # Chef::Exceptions::Override exception) + def load_current_resource + end + + # class methods + class <<self + include Chef::Mixin::ConvertToClassName + include Chef::Mixin::FromFile + + def build_from_file(cookbook_name, filename, run_context) + if LWRPBase.loaded_lwrps[filename] + Chef::Log.info("LWRP provider #{filename} from cookbook #{cookbook_name} has already been loaded! Skipping the reload.") + return loaded_lwrps[filename] + end - class_name = convert_to_class_name(provider_name) + resource_name = filename_to_qualified_string(cookbook_name, filename) - if Chef::Provider.const_defined?(class_name, false) - Chef::Log.info("#{class_name} light-weight provider is already initialized -- Skipping loading #{filename}!") - Chef::Log.debug("Overriding already defined LWRPs is not supported anymore starting with Chef 12.") - provider_class = Chef::Provider.const_get(class_name) - else + # We load the class first to give it a chance to set its own name provider_class = Class.new(self) - Chef::Provider.const_set(class_name, provider_class) + provider_class.provides resource_name.to_sym provider_class.class_from_file(filename) - Chef::Log.debug("Loaded contents of #{filename} into a provider named #{provider_name} defined in Chef::Provider::#{class_name}") - end - provider_class - end + # Respect resource_name set inside the LWRP + provider_class.instance_eval do + define_singleton_method(:to_s) do + "LWRP provider #{resource_name} from cookbook #{cookbook_name}" + end + define_singleton_method(:inspect) { to_s } + end - # Enables inline evaluation of resources in provider actions. - # - # Without this option, any resources declared inside the LWRP are added - # to the resource collection after the current position at the time the - # action is executed. Because they are added to the primary resource - # collection for the chef run, they can notify other resources outside - # the LWRP, and potentially be notified by resources outside the LWRP - # (but this is complicated by the fact that they don't exist until the - # provider executes). In this mode, it is impossible to correctly set the - # updated_by_last_action flag on the parent LWRP resource, since it - # executes and returns before its component resources are run. - # - # With this option enabled, each action creates a temporary run_context - # with its own resource collection, evaluates the action's code in that - # context, and then converges the resources created. If any resources - # were updated, then this provider's new_resource will be marked updated. - # - # In this mode, resources created within the LWRP cannot interact with - # external resources via notifies, though notifications to other - # resources within the LWRP will work. Delayed notifications are executed - # at the conclusion of the provider's action, *not* at the end of the - # main chef run. - # - # This mode of evaluation is experimental, but is believed to be a better - # set of tradeoffs than the append-after mode, so it will likely become - # the default in a future major release of Chef. - # - def self.use_inline_resources - extend InlineResources::ClassMethods - include InlineResources - end + Chef::Log.debug("Loaded contents of #{filename} into provider #{resource_name} (#{provider_class})") + + LWRPBase.loaded_lwrps[filename] = true - # DSL for defining a provider's actions. - def self.action(name, &block) - define_method("action_#{name}") do - instance_eval(&block) + Chef::Provider.register_deprecated_lwrp_class(provider_class, convert_to_class_name(resource_name)) + + provider_class end - end - # no-op `load_current_resource`. Allows simple LWRP providers to work - # without defining this method explicitly (silences - # Chef::Exceptions::Override exception) - def load_current_resource - end + # Enables inline evaluation of resources in provider actions. + # + # Without this option, any resources declared inside the LWRP are added + # to the resource collection after the current position at the time the + # action is executed. Because they are added to the primary resource + # collection for the chef run, they can notify other resources outside + # the LWRP, and potentially be notified by resources outside the LWRP + # (but this is complicated by the fact that they don't exist until the + # provider executes). In this mode, it is impossible to correctly set the + # updated_by_last_action flag on the parent LWRP resource, since it + # executes and returns before its component resources are run. + # + # With this option enabled, each action creates a temporary run_context + # with its own resource collection, evaluates the action's code in that + # context, and then converges the resources created. If any resources + # were updated, then this provider's new_resource will be marked updated. + # + # In this mode, resources created within the LWRP cannot interact with + # external resources via notifies, though notifications to other + # resources within the LWRP will work. Delayed notifications are executed + # at the conclusion of the provider's action, *not* at the end of the + # main chef run. + # + # This mode of evaluation is experimental, but is believed to be a better + # set of tradeoffs than the append-after mode, so it will likely become + # the default in a future major release of Chef. + # + def use_inline_resources + extend InlineResources::ClassMethods + include InlineResources + end + + # DSL for defining a provider's actions. + def action(name, &block) + define_method("action_#{name}") do + instance_eval(&block) + end + end + + protected + def loaded_lwrps + @loaded_lwrps ||= {} + end + end end end end diff --git a/lib/chef/provider/mount.rb b/lib/chef/provider/mount.rb index 1631d87033..2039e9ae51 100644 --- a/lib/chef/provider/mount.rb +++ b/lib/chef/provider/mount.rb @@ -24,7 +24,6 @@ require 'chef/provider' class Chef class Provider class Mount < Chef::Provider - include Chef::Mixin::ShellOut attr_accessor :unmount_retries diff --git a/lib/chef/provider/mount/aix.rb b/lib/chef/provider/mount/aix.rb index 0d7e11a1b8..4ad7b24c15 100644 --- a/lib/chef/provider/mount/aix.rb +++ b/lib/chef/provider/mount/aix.rb @@ -22,6 +22,7 @@ class Chef class Provider class Mount class Aix < Chef::Provider::Mount::Mount + provides :mount, platform: %w(aix) # Override for aix specific handling def initialize(new_resource, run_context) diff --git a/lib/chef/provider/mount/mount.rb b/lib/chef/provider/mount/mount.rb index 0a6e269d2d..ef074166a9 100644 --- a/lib/chef/provider/mount/mount.rb +++ b/lib/chef/provider/mount/mount.rb @@ -24,6 +24,8 @@ class Chef class Mount class Mount < Chef::Provider::Mount + provides :mount + def initialize(new_resource, run_context) super @real_device = nil diff --git a/lib/chef/provider/mount/solaris.rb b/lib/chef/provider/mount/solaris.rb index d8cec24138..deb04d4d7b 100644 --- a/lib/chef/provider/mount/solaris.rb +++ b/lib/chef/provider/mount/solaris.rb @@ -27,6 +27,8 @@ class Chef class Mount # Mount Solaris File systems class Solaris < Chef::Provider::Mount + provides :mount, platform: %w(openindiana opensolaris nexentacore omnios solaris2 smartos) + extend Forwardable VFSTAB = '/etc/vfstab'.freeze diff --git a/lib/chef/provider/ohai.rb b/lib/chef/provider/ohai.rb index a6b5ab5daa..b7f4aa704b 100644 --- a/lib/chef/provider/ohai.rb +++ b/lib/chef/provider/ohai.rb @@ -21,6 +21,7 @@ require 'ohai' class Chef class Provider class Ohai < Chef::Provider + provides :ohai def whyrun_supported? true diff --git a/lib/chef/provider/package.rb b/lib/chef/provider/package.rb index 2e8e29981b..9d534ec414 100644 --- a/lib/chef/provider/package.rb +++ b/lib/chef/provider/package.rb @@ -43,6 +43,12 @@ class Chef true end + def check_resource_semantics! + if new_resource.package_name.is_a?(Array) && new_resource.source != nil + raise Chef::Exceptions::InvalidResourceSpecification, "You may not specify both multipackage and source" + end + end + def load_current_resource end @@ -464,10 +470,7 @@ class Chef # @return [Array] new_version(s) as an array def new_version_array - @new_version_array ||= - [ new_resource.version ].flatten.map do |v| - ( v.nil? || v.empty? ) ? nil : v - end + [ new_resource.version ].flatten.map { |v| v.to_s.empty? ? nil : v } end # @todo: extract apt/dpkg specific preseeding to a helper class @@ -487,6 +490,61 @@ class Chef false end end + + # Set provider priority + require 'chef/chef_class' + require 'chef/provider/package/dpkg' + require 'chef/provider/package/homebrew' + require 'chef/provider/package/macports' + require 'chef/provider/package/apt' + require 'chef/provider/package/yum' + require 'chef/provider/package/zypper' + require 'chef/provider/package/portage' + require 'chef/provider/package/pacman' + require 'chef/provider/package/ips' + require 'chef/provider/package/solaris' + require 'chef/provider/package/smartos' + require 'chef/provider/package/aix' + require 'chef/provider/package/paludis' + + Chef.set_provider_priority_array :package, [ Homebrew, Macports ], os: "darwin" + + Chef.set_provider_priority_array :package, Apt, platform_family: "debian" + Chef.set_provider_priority_array :package, Yum, platform_family: %w(rhel fedora) + Chef.set_provider_priority_array :package, Zypper, platform_family: "suse" + Chef.set_provider_priority_array :package, Portage, platform: "gentoo" + Chef.set_provider_priority_array :package, Pacman, platform: "arch" + Chef.set_provider_priority_array :package, Ips, platform: %w(openindiana opensolaris omnios solaris2) + Chef.set_provider_priority_array :package, Solaris, platform: "nexentacore" + Chef.set_provider_priority_array :package, Solaris, platform: "solaris2", platform_version: '< 5.11' + + Chef.set_provider_priority_array :package, SmartOS, platform: "smartos" + Chef.set_provider_priority_array :package, Aix, platform: "aix" + Chef.set_provider_priority_array :package, Paludis, platform: "exherbo" + + private + + def shell_out_with_timeout(*command_args) + shell_out(*add_timeout_option(command_args)) + end + + def shell_out_with_timeout!(*command_args) + shell_out!(*add_timeout_option(command_args)) + end + + def add_timeout_option(command_args) + args = command_args.dup + if args.last.is_a?(Hash) + options = args.pop.dup + options[:timeout] = new_resource.timeout if new_resource.timeout + options[:timeout] = 900 unless options.has_key?(:timeout) + args << options + else + args << { :timeout => new_resource.timeout ? new_resource.timeout : 900 } + end + args + end + end end end diff --git a/lib/chef/provider/package/aix.rb b/lib/chef/provider/package/aix.rb index 107f914c05..b97db9d061 100644 --- a/lib/chef/provider/package/aix.rb +++ b/lib/chef/provider/package/aix.rb @@ -52,7 +52,7 @@ class Chef @package_source_found = ::File.exists?(@new_resource.source) if @package_source_found Chef::Log.debug("#{@new_resource} checking pkg status") - ret = shell_out("installp -L -d #{@new_resource.source}") + ret = shell_out_with_timeout("installp -L -d #{@new_resource.source}") ret.stdout.each_line do | line | case line when /#{@new_resource.package_name}:/ @@ -60,11 +60,12 @@ class Chef @new_resource.version(fields[2]) end end + raise Chef::Exceptions::Package, "package source #{@new_resource.source} does not provide package #{@new_resource.package_name}" unless @new_resource.version end end Chef::Log.debug("#{@new_resource} checking install state") - ret = shell_out("lslpp -lcq #{@current_resource.package_name}") + ret = shell_out_with_timeout("lslpp -lcq #{@current_resource.package_name}") ret.stdout.each_line do | line | case line when /#{@current_resource.package_name}/ @@ -83,7 +84,7 @@ class Chef def candidate_version return @candidate_version if @candidate_version - ret = shell_out("installp -L -d #{@new_resource.source}") + ret = shell_out_with_timeout("installp -L -d #{@new_resource.source}") ret.stdout.each_line do | line | case line when /\w:#{Regexp.escape(@new_resource.package_name)}:(.*)/ @@ -109,10 +110,10 @@ class Chef def install_package(name, version) Chef::Log.debug("#{@new_resource} package install options: #{@new_resource.options}") if @new_resource.options.nil? - shell_out!( "installp -aYF -d #{@new_resource.source} #{@new_resource.package_name}" ) + shell_out_with_timeout!( "installp -aYF -d #{@new_resource.source} #{@new_resource.package_name}" ) Chef::Log.debug("#{@new_resource} installed version #{@new_resource.version} from: #{@new_resource.source}") else - shell_out!( "installp -aYF #{expand_options(@new_resource.options)} -d #{@new_resource.source} #{@new_resource.package_name}" ) + shell_out_with_timeout!( "installp -aYF #{expand_options(@new_resource.options)} -d #{@new_resource.source} #{@new_resource.package_name}" ) Chef::Log.debug("#{@new_resource} installed version #{@new_resource.version} from: #{@new_resource.source}") end end @@ -121,10 +122,10 @@ class Chef def remove_package(name, version) if @new_resource.options.nil? - shell_out!( "installp -u #{name}" ) + shell_out_with_timeout!( "installp -u #{name}" ) Chef::Log.debug("#{@new_resource} removed version #{@new_resource.version}") else - shell_out!( "installp -u #{expand_options(@new_resource.options)} #{name}" ) + shell_out_with_timeout!( "installp -u #{expand_options(@new_resource.options)} #{name}" ) Chef::Log.debug("#{@new_resource} removed version #{@new_resource.version}") end end diff --git a/lib/chef/provider/package/apt.rb b/lib/chef/provider/package/apt.rb index e426b51992..bd6ed283bf 100644 --- a/lib/chef/provider/package/apt.rb +++ b/lib/chef/provider/package/apt.rb @@ -62,7 +62,7 @@ class Chef installed_version = nil candidate_version = nil - shell_out!("apt-cache#{expand_options(default_release_options)} policy #{pkg}", {:timeout=>900}).stdout.each_line do |line| + shell_out_with_timeout!("apt-cache#{expand_options(default_release_options)} policy #{pkg}").stdout.each_line do |line| case line when /^\s{2}Installed: (.+)$/ installed_version = $1 @@ -78,7 +78,7 @@ class Chef if candidate_version == '(none)' # This may not be an appropriate assumption, but it shouldn't break anything that already worked -- btm is_virtual_package = true - showpkg = shell_out!("apt-cache showpkg #{pkg}", {:timeout => 900}).stdout + showpkg = shell_out_with_timeout!("apt-cache showpkg #{pkg}").stdout providers = Hash.new showpkg.rpartition(/Reverse Provides: ?#{$/}/)[2].each_line do |line| provider, version = line.split @@ -175,7 +175,7 @@ class Chef # interactive prompts. Command is run with default localization rather # than forcing locale to "C", so command output may not be stable. def run_noninteractive(command) - shell_out!(command, :env => { "DEBIAN_FRONTEND" => "noninteractive", "LC_ALL" => nil }, :timeout => @new_resource.timeout) + shell_out_with_timeout!(command, :env => { "DEBIAN_FRONTEND" => "noninteractive", "LC_ALL" => nil }) end end diff --git a/lib/chef/provider/package/dpkg.rb b/lib/chef/provider/package/dpkg.rb index 11691a2479..a262f1ab1a 100644 --- a/lib/chef/provider/package/dpkg.rb +++ b/lib/chef/provider/package/dpkg.rb @@ -62,7 +62,7 @@ class Chef # Get information from the package if supplied Chef::Log.debug("#{@new_resource} checking dpkg status") - shell_out("dpkg-deb -W #{@new_resource.source}").stdout.each_line do |line| + shell_out_with_timeout("dpkg-deb -W #{@new_resource.source}").stdout.each_line do |line| if pkginfo = DPKG_INFO.match(line) @current_resource.package_name(pkginfo[1]) @new_resource.version(pkginfo[2]) @@ -79,7 +79,7 @@ class Chef # Check to see if it is installed package_installed = nil Chef::Log.debug("#{@new_resource} checking install state") - status = shell_out("dpkg -s #{@current_resource.package_name}") + status = shell_out_with_timeout("dpkg -s #{@current_resource.package_name}") status.stdout.each_line do |line| case line when DPKG_INSTALLED @@ -134,13 +134,13 @@ class Chef run_noninteractive("dpkg-reconfigure #{name}") end - # Runs command via shell_out with magic environment to disable + # Runs command via shell_out_with_timeout with magic environment to disable # interactive prompts. Command is run with default localization rather # than forcing locale to "C", so command output may not be stable. # # FIXME: This should be "LC_ALL" => "en_US.UTF-8" in order to stabilize the output and get UTF-8 def run_noninteractive(command) - shell_out!(command, :env => { "DEBIAN_FRONTEND" => "noninteractive", "LC_ALL" => nil }) + shell_out_with_timeout!(command, :env => { "DEBIAN_FRONTEND" => "noninteractive", "LC_ALL" => nil }) end end diff --git a/lib/chef/provider/package/easy_install.rb b/lib/chef/provider/package/easy_install.rb index 90727b738d..2f7880bf08 100644 --- a/lib/chef/provider/package/easy_install.rb +++ b/lib/chef/provider/package/easy_install.rb @@ -32,10 +32,10 @@ class Chef begin # first check to see if we can import it - output = shell_out!("#{python_binary_path} -c \"import #{name}\"", :returns=>[0,1]).stderr + output = shell_out_with_timeout!("#{python_binary_path} -c \"import #{name}\"", :returns=>[0,1]).stderr if output.include? "ImportError" # then check to see if its on the path - output = shell_out!("#{python_binary_path} -c \"import sys; print sys.path\"", :returns=>[0,1]).stdout + output = shell_out_with_timeout!("#{python_binary_path} -c \"import sys; print sys.path\"", :returns=>[0,1]).stdout if output.downcase.include? "#{name.downcase}" check = true end @@ -73,10 +73,10 @@ class Chef package_version = nil if install_check(module_name) begin - output = shell_out!("#{python_binary_path} -c \"import #{module_name}; print #{module_name}.__version__\"").stdout + output = shell_out_with_timeout!("#{python_binary_path} -c \"import #{module_name}; print #{module_name}.__version__\"").stdout package_version = output.strip rescue - output = shell_out!("#{python_binary_path} -c \"import sys; print sys.path\"", :returns=>[0,1]).stdout + output = shell_out_with_timeout!("#{python_binary_path} -c \"import sys; print sys.path\"", :returns=>[0,1]).stdout output_array = output.gsub(/[\[\]]/,'').split(/\s*,\s*/) package_path = "" @@ -107,7 +107,7 @@ class Chef return @candidate_version if @candidate_version # do a dry run to get the latest version - result = shell_out!("#{easy_install_binary_path} -n #{@new_resource.package_name}", :returns=>[0,1]) + result = shell_out_with_timeout!("#{easy_install_binary_path} -n #{@new_resource.package_name}", :returns=>[0,1]) @candidate_version = result.stdout[/(.*)Best match: (.*) (.*)$/, 3] @candidate_version end diff --git a/lib/chef/provider/package/freebsd/base.rb b/lib/chef/provider/package/freebsd/base.rb index 6a3b97a4fd..7c032b3787 100644 --- a/lib/chef/provider/package/freebsd/base.rb +++ b/lib/chef/provider/package/freebsd/base.rb @@ -47,7 +47,7 @@ class Chef # Otherwise look up the path to the ports directory using 'whereis' else - whereis = shell_out!("whereis -s #{port}", :env => nil) + whereis = shell_out_with_timeout!("whereis -s #{port}", :env => nil) unless path = whereis.stdout[/^#{Regexp.escape(port)}:\s+(.+)$/, 1] raise Chef::Exceptions::Package, "Could not find port with the name #{port}" end @@ -57,7 +57,7 @@ class Chef def makefile_variable_value(variable, dir = nil) options = dir ? { :cwd => dir } : {} - make_v = shell_out!("make -V #{variable}", options.merge!(:env => nil, :returns => [0,1])) + make_v = shell_out_with_timeout!("make -V #{variable}", options.merge!(:env => nil, :returns => [0,1])) make_v.exitstatus.zero? ? make_v.stdout.strip.split($\).first : nil # $\ is the line separator, i.e. newline. end end diff --git a/lib/chef/provider/package/freebsd/pkg.rb b/lib/chef/provider/package/freebsd/pkg.rb index ebbfbb19b4..33a8c2c108 100644 --- a/lib/chef/provider/package/freebsd/pkg.rb +++ b/lib/chef/provider/package/freebsd/pkg.rb @@ -34,24 +34,24 @@ class Chef case @new_resource.source when /^http/, /^ftp/ if @new_resource.source =~ /\/$/ - shell_out!("pkg_add -r #{package_name}", :env => { "PACKAGESITE" => @new_resource.source, 'LC_ALL' => nil }).status + shell_out_with_timeout!("pkg_add -r #{package_name}", :env => { "PACKAGESITE" => @new_resource.source, 'LC_ALL' => nil }).status else - shell_out!("pkg_add -r #{package_name}", :env => { "PACKAGEROOT" => @new_resource.source, 'LC_ALL' => nil }).status + shell_out_with_timeout!("pkg_add -r #{package_name}", :env => { "PACKAGEROOT" => @new_resource.source, 'LC_ALL' => nil }).status end Chef::Log.debug("#{@new_resource} installed from: #{@new_resource.source}") when /^\// - shell_out!("pkg_add #{file_candidate_version_path}", :env => { "PKG_PATH" => @new_resource.source , 'LC_ALL'=>nil}).status + shell_out_with_timeout!("pkg_add #{file_candidate_version_path}", :env => { "PKG_PATH" => @new_resource.source , 'LC_ALL'=>nil}).status Chef::Log.debug("#{@new_resource} installed from: #{@new_resource.source}") else - shell_out!("pkg_add -r #{latest_link_name}", :env => nil).status + shell_out_with_timeout!("pkg_add -r #{latest_link_name}", :env => nil).status end end end def remove_package(name, version) - shell_out!("pkg_delete #{package_name}-#{version || @current_resource.version}", :env => nil).status + shell_out_with_timeout!("pkg_delete #{package_name}-#{version || @current_resource.version}", :env => nil).status end # The name of the package (without the version number) as understood by pkg_add and pkg_info. @@ -72,7 +72,7 @@ class Chef end def current_installed_version - pkg_info = shell_out!("pkg_info -E \"#{package_name}*\"", :env => nil, :returns => [0,1]) + pkg_info = shell_out_with_timeout!("pkg_info -E \"#{package_name}*\"", :env => nil, :returns => [0,1]) pkg_info.stdout[/^#{Regexp.escape(package_name)}-(.+)/, 1] end diff --git a/lib/chef/provider/package/freebsd/pkgng.rb b/lib/chef/provider/package/freebsd/pkgng.rb index bfe6dca617..2fdc9dda71 100644 --- a/lib/chef/provider/package/freebsd/pkgng.rb +++ b/lib/chef/provider/package/freebsd/pkgng.rb @@ -28,11 +28,11 @@ class Chef unless @current_resource.version case @new_resource.source when /^(http|ftp|\/)/ - shell_out!("pkg add#{expand_options(@new_resource.options)} #{@new_resource.source}", :env => { 'LC_ALL' => nil }).status + shell_out_with_timeout!("pkg add#{expand_options(@new_resource.options)} #{@new_resource.source}", :env => { 'LC_ALL' => nil }).status Chef::Log.debug("#{@new_resource} installed from: #{@new_resource.source}") else - shell_out!("pkg install -y#{expand_options(@new_resource.options)} #{name}", :env => { 'LC_ALL' => nil }).status + shell_out_with_timeout!("pkg install -y#{expand_options(@new_resource.options)} #{name}", :env => { 'LC_ALL' => nil }).status end end end @@ -40,11 +40,11 @@ class Chef def remove_package(name, version) options = @new_resource.options && @new_resource.options.sub(repo_regex, '') options && !options.empty? || options = nil - shell_out!("pkg delete -y#{expand_options(options)} #{name}#{version ? '-' + version : ''}", :env => nil).status + shell_out_with_timeout!("pkg delete -y#{expand_options(options)} #{name}#{version ? '-' + version : ''}", :env => nil).status end def current_installed_version - pkg_info = shell_out!("pkg info \"#{@new_resource.package_name}\"", :env => nil, :returns => [0,70]) + pkg_info = shell_out_with_timeout!("pkg info \"#{@new_resource.package_name}\"", :env => nil, :returns => [0,70]) pkg_info.stdout[/^Version +: (.+)$/, 1] end @@ -63,7 +63,7 @@ class Chef options = $1 end - pkg_query = shell_out!("pkg rquery#{expand_options(options)} '%v' #{@new_resource.package_name}", :env => nil) + pkg_query = shell_out_with_timeout!("pkg rquery#{expand_options(options)} '%v' #{@new_resource.package_name}", :env => nil) pkg_query.exitstatus.zero? ? pkg_query.stdout.strip.split(/\n/).last : nil end diff --git a/lib/chef/provider/package/freebsd/port.rb b/lib/chef/provider/package/freebsd/port.rb index 8b191179f0..3fbd002214 100644 --- a/lib/chef/provider/package/freebsd/port.rb +++ b/lib/chef/provider/package/freebsd/port.rb @@ -26,18 +26,18 @@ class Chef include PortsHelper def install_package(name, version) - shell_out!("make -DBATCH install clean", :timeout => 1800, :env => nil, :cwd => port_dir).status + shell_out_with_timeout!("make -DBATCH install clean", :timeout => 1800, :env => nil, :cwd => port_dir).status end def remove_package(name, version) - shell_out!("make deinstall", :timeout => 300, :env => nil, :cwd => port_dir).status + shell_out_with_timeout!("make deinstall", :timeout => 300, :env => nil, :cwd => port_dir).status end def current_installed_version pkg_info = if @new_resource.supports_pkgng? - shell_out!("pkg info \"#{@new_resource.package_name}\"", :env => nil, :returns => [0,70]) + shell_out_with_timeout!("pkg info \"#{@new_resource.package_name}\"", :env => nil, :returns => [0,70]) else - shell_out!("pkg_info -E \"#{@new_resource.package_name}*\"", :env => nil, :returns => [0,1]) + shell_out_with_timeout!("pkg_info -E \"#{@new_resource.package_name}*\"", :env => nil, :returns => [0,1]) end pkg_info.stdout[/^#{Regexp.escape(@new_resource.package_name)}-(.+)/, 1] end diff --git a/lib/chef/provider/package/homebrew.rb b/lib/chef/provider/package/homebrew.rb index 603899646f..beede1c916 100644 --- a/lib/chef/provider/package/homebrew.rb +++ b/lib/chef/provider/package/homebrew.rb @@ -27,7 +27,6 @@ class Chef class Homebrew < Chef::Provider::Package provides :homebrew_package - provides :package, os: "darwin" include Chef::Mixin::HomebrewUser @@ -126,7 +125,8 @@ class Chef homebrew_user = Etc.getpwuid(homebrew_uid) Chef::Log.debug "Executing '#{command}' as user '#{homebrew_user.name}'" - output = shell_out!(command, :timeout => 1800, :user => homebrew_uid, :environment => { 'HOME' => homebrew_user.dir, 'RUBYOPT' => nil }) + # FIXME: this 1800 second default timeout should be deprecated + output = shell_out_with_timeout!(command, :timeout => 1800, :user => homebrew_uid, :environment => { 'HOME' => homebrew_user.dir, 'RUBYOPT' => nil }) output.stdout.chomp end diff --git a/lib/chef/provider/package/ips.rb b/lib/chef/provider/package/ips.rb index 87022d770a..4d7f4a3583 100644 --- a/lib/chef/provider/package/ips.rb +++ b/lib/chef/provider/package/ips.rb @@ -42,14 +42,14 @@ class Chef end def get_current_version - shell_out("pkg info #{@new_resource.package_name}").stdout.each_line do |line| + shell_out_with_timeout("pkg info #{@new_resource.package_name}").stdout.each_line do |line| return $1.split[0] if line =~ /^\s+Version: (.*)/ end return nil end def get_candidate_version - shell_out!("pkg info -r #{new_resource.package_name}").stdout.each_line do |line| + shell_out_with_timeout!("pkg info -r #{new_resource.package_name}").stdout.each_line do |line| return $1.split[0] if line =~ /Version: (.*)/ end return nil @@ -73,7 +73,7 @@ class Chef else normal_command end - shell_out(command) + shell_out_with_timeout(command) end def upgrade_package(name, version) @@ -82,7 +82,7 @@ class Chef def remove_package(name, version) package_name = "#{name}@#{version}" - shell_out!( "pkg#{expand_options(@new_resource.options)} uninstall -q #{package_name}" ) + shell_out_with_timeout!( "pkg#{expand_options(@new_resource.options)} uninstall -q #{package_name}" ) end end end diff --git a/lib/chef/provider/package/macports.rb b/lib/chef/provider/package/macports.rb index b252344c99..e945211540 100644 --- a/lib/chef/provider/package/macports.rb +++ b/lib/chef/provider/package/macports.rb @@ -4,7 +4,6 @@ class Chef class Macports < Chef::Provider::Package provides :macports_package - provides :package, os: "darwin" def load_current_resource @current_resource = Chef::Resource::Package.new(@new_resource.name) @@ -49,21 +48,21 @@ class Chef unless @current_resource.version == version command = "port#{expand_options(@new_resource.options)} install #{name}" command << " @#{version}" if version and !version.empty? - shell_out!(command) + shell_out_with_timeout!(command) end end def purge_package(name, version) command = "port#{expand_options(@new_resource.options)} uninstall #{name}" command << " @#{version}" if version and !version.empty? - shell_out!(command) + shell_out_with_timeout!(command) end def remove_package(name, version) command = "port#{expand_options(@new_resource.options)} deactivate #{name}" command << " @#{version}" if version and !version.empty? - shell_out!(command) + shell_out_with_timeout!(command) end def upgrade_package(name, version) @@ -76,14 +75,14 @@ class Chef # that hasn't been installed. install_package(name, version) elsif current_version != version - shell_out!( "port#{expand_options(@new_resource.options)} upgrade #{name} @#{version}" ) + shell_out_with_timeout!( "port#{expand_options(@new_resource.options)} upgrade #{name} @#{version}" ) end end private def get_response_from_command(command) output = nil - status = shell_out(command) + status = shell_out_with_timeout(command) begin output = status.stdout rescue Exception diff --git a/lib/chef/provider/package/openbsd.rb b/lib/chef/provider/package/openbsd.rb index 82048c3bd4..f231101390 100644 --- a/lib/chef/provider/package/openbsd.rb +++ b/lib/chef/provider/package/openbsd.rb @@ -22,7 +22,6 @@ require 'chef/resource/package' require 'chef/provider/package' -require 'chef/mixin/shell_out' require 'chef/mixin/get_source_from_package' require 'chef/exceptions' @@ -72,7 +71,7 @@ class Chef if parts = name.match(/^(.+?)--(.+)/) # use double-dash for stems with flavors, see man page for pkg_add name = parts[1] end - shell_out!("pkg_add -r #{name}#{version_string}", :env => {"PKG_PATH" => pkg_path}).status + shell_out_with_timeout!("pkg_add -r #{name}#{version_string}", :env => {"PKG_PATH" => pkg_path}).status Chef::Log.debug("#{new_resource.package_name} installed") end end @@ -83,7 +82,7 @@ class Chef if parts = name.match(/^(.+?)--(.+)/) name = parts[1] end - shell_out!("pkg_delete #{name}#{version_string}", :env => nil).status + shell_out_with_timeout!("pkg_delete #{name}#{version_string}", :env => nil).status end private @@ -94,7 +93,7 @@ class Chef else name = new_resource.package_name end - pkg_info = shell_out!("pkg_info -e \"#{name}->0\"", :env => nil, :returns => [0,1]) + pkg_info = shell_out_with_timeout!("pkg_info -e \"#{name}->0\"", :env => nil, :returns => [0,1]) result = pkg_info.stdout[/^inst:#{Regexp.escape(name)}-(.+?)\s/, 1] Chef::Log.debug("installed_version of '#{new_resource.package_name}' is '#{result}'") result @@ -103,7 +102,7 @@ class Chef def candidate_version @candidate_version ||= begin results = [] - shell_out!("pkg_info -I \"#{new_resource.package_name}#{version_string}\"", :env => nil, :returns => [0,1]).stdout.each_line do |line| + shell_out_with_timeout!("pkg_info -I \"#{new_resource.package_name}#{version_string}\"", :env => nil, :returns => [0,1]).stdout.each_line do |line| if parts = new_resource.package_name.match(/^(.+?)--(.+)/) results << line[/^#{Regexp.escape(parts[1])}-(.+?)\s/, 1] else diff --git a/lib/chef/provider/package/pacman.rb b/lib/chef/provider/package/pacman.rb index f16fc811f5..bf03e54656 100644 --- a/lib/chef/provider/package/pacman.rb +++ b/lib/chef/provider/package/pacman.rb @@ -34,7 +34,7 @@ class Chef @current_resource.version(nil) Chef::Log.debug("#{@new_resource} checking pacman for #{@new_resource.package_name}") - status = shell_out("pacman -Qi #{@new_resource.package_name}") + status = shell_out_with_timeout("pacman -Qi #{@new_resource.package_name}") status.stdout.each_line do |line| case line when /^Version(\s?)*: (.+)$/ @@ -62,7 +62,7 @@ class Chef package_repos = repos.map {|r| Regexp.escape(r) }.join('|') - status = shell_out("pacman -Sl") + status = shell_out_with_timeout("pacman -Sl") status.stdout.each_line do |line| case line when /^(#{package_repos}) #{Regexp.escape(@new_resource.package_name)} (.+)$/ @@ -85,7 +85,7 @@ class Chef end def install_package(name, version) - shell_out!( "pacman --sync --noconfirm --noprogressbar#{expand_options(@new_resource.options)} #{name}" ) + shell_out_with_timeout!( "pacman --sync --noconfirm --noprogressbar#{expand_options(@new_resource.options)} #{name}" ) end def upgrade_package(name, version) @@ -93,7 +93,7 @@ class Chef end def remove_package(name, version) - shell_out!( "pacman --remove --noconfirm --noprogressbar#{expand_options(@new_resource.options)} #{name}" ) + shell_out_with_timeout!( "pacman --remove --noconfirm --noprogressbar#{expand_options(@new_resource.options)} #{name}" ) end def purge_package(name, version) diff --git a/lib/chef/provider/package/portage.rb b/lib/chef/provider/package/portage.rb index bb047ad2fa..4ba0160bb0 100644 --- a/lib/chef/provider/package/portage.rb +++ b/lib/chef/provider/package/portage.rb @@ -25,6 +25,8 @@ class Chef class Provider class Package class Portage < Chef::Provider::Package + provides :portage_package + PACKAGE_NAME_PATTERN = %r{(?:([^/]+)/)?([^/]+)} def load_current_resource diff --git a/lib/chef/provider/package/rpm.rb b/lib/chef/provider/package/rpm.rb index f10fe23c71..21c39752d1 100644 --- a/lib/chef/provider/package/rpm.rb +++ b/lib/chef/provider/package/rpm.rb @@ -17,7 +17,6 @@ # require 'chef/provider/package' require 'chef/mixin/command' -require 'chef/mixin/shell_out' require 'chef/resource/package' require 'chef/mixin/get_source_from_package' @@ -60,7 +59,7 @@ class Chef end Chef::Log.debug("#{@new_resource} checking rpm status") - shell_out!("rpm -qp --queryformat '%{NAME} %{VERSION}-%{RELEASE}\n' #{@new_resource.source}").stdout.each_line do |line| + shell_out_with_timeout!("rpm -qp --queryformat '%{NAME} %{VERSION}-%{RELEASE}\n' #{@new_resource.source}").stdout.each_line do |line| case line when /^([\w\d+_.-]+)\s([\w\d_.-]+)$/ @current_resource.package_name($1) @@ -76,7 +75,7 @@ class Chef end Chef::Log.debug("#{@new_resource} checking install state") - @rpm_status = shell_out("rpm -q --queryformat '%{NAME} %{VERSION}-%{RELEASE}\n' #{@current_resource.package_name}") + @rpm_status = shell_out_with_timeout("rpm -q --queryformat '%{NAME} %{VERSION}-%{RELEASE}\n' #{@current_resource.package_name}") @rpm_status.stdout.each_line do |line| case line when /^([\w\d+_.-]+)\s([\w\d_.-]+)$/ @@ -90,12 +89,12 @@ class Chef def install_package(name, version) unless @current_resource.version - shell_out!( "rpm #{@new_resource.options} -i #{@new_resource.source}" ) + shell_out_with_timeout!( "rpm #{@new_resource.options} -i #{@new_resource.source}" ) else if allow_downgrade - shell_out!( "rpm #{@new_resource.options} -U --oldpackage #{@new_resource.source}" ) + shell_out_with_timeout!( "rpm #{@new_resource.options} -U --oldpackage #{@new_resource.source}" ) else - shell_out!( "rpm #{@new_resource.options} -U #{@new_resource.source}" ) + shell_out_with_timeout!( "rpm #{@new_resource.options} -U #{@new_resource.source}" ) end end end @@ -104,9 +103,9 @@ class Chef def remove_package(name, version) if version - shell_out!( "rpm #{@new_resource.options} -e #{name}-#{version}" ) + shell_out_with_timeout!( "rpm #{@new_resource.options} -e #{name}-#{version}" ) else - shell_out!( "rpm #{@new_resource.options} -e #{name}" ) + shell_out_with_timeout!( "rpm #{@new_resource.options} -e #{name}" ) end end diff --git a/lib/chef/provider/package/rubygems.rb b/lib/chef/provider/package/rubygems.rb index c53aa8934a..b5f7dbdd80 100644 --- a/lib/chef/provider/package/rubygems.rb +++ b/lib/chef/provider/package/rubygems.rb @@ -32,14 +32,7 @@ require 'rubygems/version' require 'rubygems/dependency' require 'rubygems/spec_fetcher' require 'rubygems/platform' - -# Compatibility note: Rubygems 2.0 removes rubygems/format in favor of -# rubygems/package. -begin - require 'rubygems/format' -rescue LoadError - require 'rubygems/package' -end +require 'rubygems/package' require 'rubygems/dependency_installer' require 'rubygems/uninstaller' require 'rubygems/specification' @@ -545,9 +538,9 @@ class Chef src = @new_resource.source && " --source=#{@new_resource.source} --source=https://rubygems.org" end if !version.nil? && version.length > 0 - shell_out!("#{gem_binary_path} install #{name} -q --no-rdoc --no-ri -v \"#{version}\"#{src}#{opts}", :env=>nil) + shell_out_with_timeout!("#{gem_binary_path} install #{name} -q --no-rdoc --no-ri -v \"#{version}\"#{src}#{opts}", :env=>nil) else - shell_out!("#{gem_binary_path} install \"#{name}\" -q --no-rdoc --no-ri #{src}#{opts}", :env=>nil) + shell_out_with_timeout!("#{gem_binary_path} install \"#{name}\" -q --no-rdoc --no-ri #{src}#{opts}", :env=>nil) end end @@ -571,9 +564,9 @@ class Chef def uninstall_via_gem_command(name, version) if version - shell_out!("#{gem_binary_path} uninstall #{name} -q -x -I -v \"#{version}\"#{opts}", :env=>nil) + shell_out_with_timeout!("#{gem_binary_path} uninstall #{name} -q -x -I -v \"#{version}\"#{opts}", :env=>nil) else - shell_out!("#{gem_binary_path} uninstall #{name} -q -x -I -a#{opts}", :env=>nil) + shell_out_with_timeout!("#{gem_binary_path} uninstall #{name} -q -x -I -a#{opts}", :env=>nil) end end diff --git a/lib/chef/provider/package/smartos.rb b/lib/chef/provider/package/smartos.rb index 7cef91953a..0d5b801c96 100644 --- a/lib/chef/provider/package/smartos.rb +++ b/lib/chef/provider/package/smartos.rb @@ -43,7 +43,7 @@ class Chef def check_package_state(name) Chef::Log.debug("#{@new_resource} checking package #{name}") version = nil - info = shell_out!("/opt/local/sbin/pkg_info -E \"#{name}*\"", :env => nil, :returns => [0,1]) + info = shell_out_with_timeout!("/opt/local/sbin/pkg_info", "-E", "#{name}*", :env => nil, :returns => [0,1]) if info.stdout version = info.stdout[/^#{@new_resource.package_name}-(.+)/, 1] @@ -60,11 +60,11 @@ class Chef return @candidate_version if @candidate_version name = nil version = nil - pkg = shell_out!("/opt/local/bin/pkgin se #{new_resource.package_name}", :env => nil, :returns => [0,1]) + pkg = shell_out_with_timeout!("/opt/local/bin/pkgin", "se", new_resource.package_name, :env => nil, :returns => [0,1]) pkg.stdout.each_line do |line| case line when /^#{new_resource.package_name}/ - name, version = line.split[0].split(/-([^-]+)$/) + name, version = line.split(/[; ]/)[0].split(/-([^-]+)$/) end end @candidate_version = version @@ -74,7 +74,7 @@ class Chef def install_package(name, version) Chef::Log.debug("#{@new_resource} installing package #{name} version #{version}") package = "#{name}-#{version}" - out = shell_out!("/opt/local/bin/pkgin -y install #{package}", :env => nil) + out = shell_out_with_timeout!("/opt/local/bin/pkgin", "-y", "install", package, :env => nil) end def upgrade_package(name, version) @@ -85,7 +85,7 @@ class Chef def remove_package(name, version) Chef::Log.debug("#{@new_resource} removing package #{name} version #{version}") package = "#{name}" - out = shell_out!("/opt/local/bin/pkgin -y remove #{package}", :env => nil) + out = shell_out_with_timeout!("/opt/local/bin/pkgin", "-y", "remove", package, :env => nil) end end diff --git a/lib/chef/provider/package/solaris.rb b/lib/chef/provider/package/solaris.rb index a2cfd93ef6..9b10403344 100644 --- a/lib/chef/provider/package/solaris.rb +++ b/lib/chef/provider/package/solaris.rb @@ -55,7 +55,7 @@ class Chef @package_source_found = ::File.exists?(@new_resource.source) if @package_source_found Chef::Log.debug("#{@new_resource} checking pkg status") - shell_out("pkginfo -l -d #{@new_resource.source} #{@new_resource.package_name}").stdout.each_line do |line| + shell_out_with_timeout("pkginfo -l -d #{@new_resource.source} #{@new_resource.package_name}").stdout.each_line do |line| case line when /VERSION:\s+(.+)/ @new_resource.version($1) @@ -65,7 +65,7 @@ class Chef end Chef::Log.debug("#{@new_resource} checking install state") - status = shell_out("pkginfo -l #{@current_resource.package_name}") + status = shell_out_with_timeout("pkginfo -l #{@current_resource.package_name}") status.stdout.each_line do |line| case line when /VERSION:\s+(.+)/ @@ -87,7 +87,7 @@ class Chef def candidate_version return @candidate_version if @candidate_version - status = shell_out("pkginfo -l -d #{@new_resource.source} #{new_resource.package_name}") + status = shell_out_with_timeout("pkginfo -l -d #{@new_resource.source} #{new_resource.package_name}") status.stdout.each_line do |line| case line when /VERSION:\s+(.+)/ @@ -110,7 +110,7 @@ class Chef else command = "pkgadd -n -d #{@new_resource.source} all" end - shell_out!(command) + shell_out_with_timeout!(command) Chef::Log.debug("#{@new_resource} installed version #{@new_resource.version} from: #{@new_resource.source}") else if ::File.directory?(@new_resource.source) # CHEF-4469 @@ -118,17 +118,17 @@ class Chef else command = "pkgadd -n#{expand_options(@new_resource.options)} -d #{@new_resource.source} all" end - shell_out!(command) + shell_out_with_timeout!(command) Chef::Log.debug("#{@new_resource} installed version #{@new_resource.version} from: #{@new_resource.source}") end end def remove_package(name, version) if @new_resource.options.nil? - shell_out!( "pkgrm -n #{name}" ) + shell_out_with_timeout!( "pkgrm -n #{name}" ) Chef::Log.debug("#{@new_resource} removed version #{@new_resource.version}") else - shell_out!( "pkgrm -n#{expand_options(@new_resource.options)} #{name}" ) + shell_out_with_timeout!( "pkgrm -n#{expand_options(@new_resource.options)} #{name}" ) Chef::Log.debug("#{@new_resource} removed version #{@new_resource.version}") end end diff --git a/lib/chef/provider/package/windows.rb b/lib/chef/provider/package/windows.rb index 143d82f111..7ff0b71807 100644 --- a/lib/chef/provider/package/windows.rb +++ b/lib/chef/provider/package/windows.rb @@ -16,14 +16,18 @@ # limitations under the License. # +require 'chef/mixin/uris' require 'chef/resource/windows_package' require 'chef/provider/package' require 'chef/util/path_helper' +require 'chef/mixin/checksum' class Chef class Provider class Package class Windows < Chef::Provider::Package + include Chef::Mixin::Uris + include Chef::Mixin::Checksum provides :package, os: "windows" provides :windows_package, os: "windows" @@ -36,19 +40,23 @@ class Chef # load_current_resource is run in Chef::Provider#run_action when not in whyrun_mode? def load_current_resource - @new_resource.source(Chef::Util::PathHelper.validate_path(@new_resource.source)) - @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 + if downloadable_file_missing? + Chef::Log.debug("We do not know the version of #{new_resource.source} because the file is not downloaded") + current_resource.version(:unknown.to_s) + else + current_resource.version(package_provider.installed_version) + new_resource.version(package_provider.package_version) + end + + current_resource end def package_provider @package_provider ||= begin case installer_type when :msi - Chef::Provider::Package::Windows::MSI.new(@new_resource) + Chef::Provider::Package::Windows::MSI.new(resource_for_provider) else raise "Unable to find a Chef::Provider::Package::Windows provider for installer_type '#{installer_type}'" end @@ -71,6 +79,17 @@ class Chef end end + def action_install + if uri_scheme?(new_resource.source) + download_source_file + load_current_resource + else + validate_content! + end + + super + 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) @@ -80,6 +99,71 @@ class Chef def remove_package(name, version) package_provider.remove_package(name, version) end + + # @return [Array] new_version(s) as an array + def new_version_array + # Because the one in the parent caches things + [new_resource.version] + end + + private + + def downloadable_file_missing? + uri_scheme?(new_resource.source) && !::File.exists?(source_location) + end + + def resource_for_provider + @resource_for_provider = Chef::Resource::WindowsPackage.new(new_resource.name).tap do |r| + r.source(Chef::Util::PathHelper.validate_path(source_location)) + r.timeout(new_resource.timeout) + r.returns(new_resource.returns) + r.options(new_resource.options) + end + end + + def download_source_file + source_resource.run_action(:create) + Chef::Log.debug("#{@new_resource} fetched source file to #{source_resource.path}") + end + + def source_resource + @source_resource ||= Chef::Resource::RemoteFile.new(default_download_cache_path, run_context).tap do |r| + r.source(new_resource.source) + r.checksum(new_resource.checksum) + r.backup(false) + + if new_resource.remote_file_attributes + new_resource.remote_file_attributes.each do |(k,v)| + r.send(k.to_sym, v) + end + end + end + end + + def default_download_cache_path + uri = ::URI.parse(new_resource.source) + filename = ::File.basename(::URI.unescape(uri.path)) + file_cache_dir = Chef::FileCache.create_cache_path("package/") + Chef::Util::PathHelper.cleanpath("#{file_cache_dir}/#{filename}") + end + + def source_location + if uri_scheme?(new_resource.source) + source_resource.path + else + Chef::Util::PathHelper.cleanpath(new_resource.source) + end + end + + def validate_content! + if new_resource.checksum + source_checksum = checksum(source_location) + if new_resource.checksum != source_checksum + raise Chef::Exceptions::ChecksumMismatch.new(short_cksum(new_resource.checksum), short_cksum(source_checksum)) + end + end + end + end end end diff --git a/lib/chef/provider/package/windows/msi.rb b/lib/chef/provider/package/windows/msi.rb index 938452945e..31faa78215 100644 --- a/lib/chef/provider/package/windows/msi.rb +++ b/lib/chef/provider/package/windows/msi.rb @@ -56,7 +56,7 @@ class Chef 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}'") diff --git a/lib/chef/provider/package/yum.rb b/lib/chef/provider/package/yum.rb index b3d3d72844..85c2ba683c 100644 --- a/lib/chef/provider/package/yum.rb +++ b/lib/chef/provider/package/yum.rb @@ -1,4 +1,4 @@ -# + # Author:: Adam Jacob (<adam@opscode.com>) # Copyright:: Copyright (c) 2008 Opscode, Inc. # License:: Apache License, Version 2.0 @@ -18,7 +18,6 @@ require 'chef/config' require 'chef/provider/package' -require 'chef/mixin/shell_out' require 'chef/mixin/which' require 'chef/resource/package' require 'singleton' @@ -647,7 +646,6 @@ class Chef # Cache for our installed and available packages, pulled in from yum-dump.py class YumCache - include Chef::Mixin::Command include Chef::Mixin::Which include Chef::Mixin::ShellOut include Singleton @@ -986,6 +984,17 @@ class Chef # Extra attributes # + def arch_for_name(n) + if @new_resource.respond_to?("arch") + @new_resource.arch + elsif @arch + idx = package_name_array.index(n) + as_array(@arch)[idx] + else + nil + end + end + def arch if @new_resource.respond_to?("arch") @new_resource.arch @@ -994,6 +1003,12 @@ class Chef end end + def set_arch(arch) + if @new_resource.respond_to?("arch") + @new_resource.arch(arch) + end + end + def flush_cache if @new_resource.respond_to?("flush_cache") @new_resource.flush_cache @@ -1005,12 +1020,13 @@ class Chef # Helpers # - def yum_arch + def yum_arch(arch) arch ? ".#{arch}" : nil end def yum_command(command) - status = shell_out(command, {:timeout => Chef::Config[:yum_timeout]}) + Chef::Log.debug("#{@new_resource}: yum command: \"#{command}\"") + status = shell_out_with_timeout(command, {:timeout => Chef::Config[:yum_timeout]}) # This is fun: rpm can encounter errors in the %post/%postun scripts which aren't # considered fatal - meaning the rpm is still successfully installed. These issue @@ -1027,7 +1043,7 @@ class Chef if l =~ %r{^error: %(post|postun)\(.*\) scriptlet failed, exit status \d+$} Chef::Log.warn("#{@new_resource} caught non-fatal scriptlet issue: \"#{l}\". Can't trust yum exit status " + "so running install again to verify.") - status = shell_out(command, {:timeout => Chef::Config[:yum_timeout]}) + status = shell_out_with_timeout(command, {:timeout => Chef::Config[:yum_timeout]}) break end end @@ -1087,23 +1103,20 @@ class Chef end end - # Don't overwrite an existing arch - unless arch - parse_arch - end @current_resource = Chef::Resource::Package.new(@new_resource.name) @current_resource.package_name(@new_resource.package_name) installed_version = [] @candidate_version = [] + @arch = [] if @new_resource.source unless ::File.exists?(@new_resource.source) raise Chef::Exceptions::Package, "Package #{@new_resource.name} not found: #{@new_resource.source}" end Chef::Log.debug("#{@new_resource} checking rpm status") - shell_out!("rpm -qp --queryformat '%{NAME} %{VERSION}-%{RELEASE}\n' #{@new_resource.source}", :timeout => Chef::Config[:yum_timeout]).stdout.each_line do |line| + shell_out_with_timeout!("rpm -qp --queryformat '%{NAME} %{VERSION}-%{RELEASE}\n' #{@new_resource.source}", :timeout => Chef::Config[:yum_timeout]).stdout.each_line do |line| case line when /([\w\d_.-]+)\s([\w\d_.-]+)/ @current_resource.package_name($1) @@ -1113,24 +1126,43 @@ class Chef @candidate_version << @new_resource.version installed_version << @yum.installed_version(@current_resource.package_name, arch) else - if @new_resource.version - new_resource = "#{@new_resource.package_name}-#{@new_resource.version}#{yum_arch}" - else - new_resource = "#{@new_resource.package_name}#{yum_arch}" - end - Chef::Log.debug("#{@new_resource} checking yum info for #{new_resource}") + package_name_array.each_with_index do |pkg, idx| + # Don't overwrite an existing arch + if arch + name, parch = pkg, arch + else + name, parch = parse_arch(pkg) + # if we parsed an arch from the name, update the name + # to be just the package name. + if parch + if @new_resource.package_name.is_a?(Array) + @new_resource.package_name[idx] = name + else + @new_resource.package_name(name) + # only set the arch if it's a single package + set_arch(parch) + end + end + end - package_name_array.each do |pkg| - installed_version << @yum.installed_version(pkg, arch) - @candidate_version << @yum.candidate_version(pkg, arch) + if @new_resource.version + new_resource = + "#{@new_resource.package_name}-#{@new_resource.version}#{yum_arch(parch)}" + else + new_resource = "#{@new_resource.package_name}#{yum_arch(parch)}" + end + Chef::Log.debug("#{@new_resource} checking yum info for #{new_resource}") + installed_version << @yum.installed_version(name, parch) + @candidate_version << @yum.candidate_version(name, parch) + @arch << parch end - end if installed_version.size == 1 @current_resource.version(installed_version[0]) @candidate_version = @candidate_version[0] + @arch = @arch[0] else @current_resource.version(installed_version) end @@ -1145,7 +1177,7 @@ class Chef # Work around yum not exiting with an error if a package doesn't exist # for CHEF-2062 all_avail = as_array(name).zip(as_array(version)).any? do |n, v| - @yum.version_available?(n, v, arch) + @yum.version_available?(n, v, arch_for_name(n)) end method = log_method = nil methods = [] @@ -1187,16 +1219,16 @@ class Chef repos = [] pkg_string_bits = [] - index = 0 as_array(name).zip(as_array(version)).each do |n, v| + idx = package_name_array.index(n) + a = arch_for_name(n) s = '' - unless v == current_version_array[index] - s = "#{n}-#{v}#{yum_arch}" - repo = @yum.package_repository(n, v, arch) + unless v == current_version_array[idx] + s = "#{n}-#{v}#{yum_arch(a)}" + repo = @yum.package_repository(n, v, a) repos << "#{s} from #{repo} repository" pkg_string_bits << s end - index += 1 end pkg_string = pkg_string_bits.join(' ') Chef::Log.info("#{@new_resource} #{log_method} #{repos.join(' ')}") @@ -1247,11 +1279,15 @@ class Chef def remove_package(name, version) if version - remove_str = as_array(name).zip(as_array(version)).map do |x| - "#{x.join('-')}#{yum_arch}" + remove_str = as_array(name).zip(as_array(version)).map do |n, v| + a = arch_for_name(n) + "#{[n, v].join('-')}#{yum_arch(a)}" end.join(' ') else - remove_str = as_array(name).map { |n| "#{n}#{yum_arch}" }.join(' ') + remove_str = as_array(name).map do |n| + a = arch_for_name(n) + "#{n}#{yum_arch(a)}" + end.join(' ') end yum_command("yum -d0 -e0 -y#{expand_options(@new_resource.options)} remove #{remove_str}") @@ -1268,22 +1304,26 @@ class Chef private - def parse_arch + def parse_arch(package_name) # Allow for foo.x86_64 style package_name like yum uses in it's output # - if @new_resource.package_name =~ %r{^(.*)\.(.*)$} + if package_name =~ %r{^(.*)\.(.*)$} new_package_name = $1 new_arch = $2 # foo.i386 and foo.beta1 are both valid package names or expressions of an arch. # Ensure we don't have an existing package matching package_name, then ensure we at # least have a match for the new_package+new_arch before we overwrite. If neither # then fall through to standard package handling. - if (@yum.installed_version(@new_resource.package_name).nil? and @yum.candidate_version(@new_resource.package_name).nil?) and - (@yum.installed_version(new_package_name, new_arch) or @yum.candidate_version(new_package_name, new_arch)) - @new_resource.package_name(new_package_name) - @new_resource.arch(new_arch) + old_installed = @yum.installed_version(package_name) + old_candidate = @yum.candidate_version(package_name) + new_installed = @yum.installed_version(new_package_name, new_arch) + new_candidate = @yum.candidate_version(new_package_name, new_arch) + if (old_installed.nil? and old_candidate.nil?) and (new_installed or new_candidate) + Chef::Log.debug("Parsed out arch #{new_arch}, new package name is #{new_package_name}") + return new_package_name, new_arch end end + return package_name, nil end # If we don't have the package we could have been passed a 'whatprovides' feature @@ -1328,7 +1368,7 @@ class Chef new_package_name = packages.first.name new_package_version = packages.first.version.to_s debug_msg = "#{name}: Unable to match package '#{name}' but matched #{packages.size} " - debug_msg << packages.size == 1 ? "package" : "packages" + debug_msg << (packages.size == 1 ? "package" : "packages") debug_msg << ", selected '#{new_package_name}' version '#{new_package_version}'" Chef::Log.debug(debug_msg) diff --git a/lib/chef/provider/package/zypper.rb b/lib/chef/provider/package/zypper.rb index 2cd321660b..c2a3ac4ba8 100644 --- a/lib/chef/provider/package/zypper.rb +++ b/lib/chef/provider/package/zypper.rb @@ -29,46 +29,48 @@ class Chef class Package class Zypper < Chef::Provider::Package + provides :zypper_package, os: "linux" + def load_current_resource - @current_resource = Chef::Resource::Package.new(@new_resource.name) - @current_resource.package_name(@new_resource.package_name) + @current_resource = Chef::Resource::ZypperPackage.new(new_resource.name) + current_resource.package_name(new_resource.package_name) is_installed=false is_out_of_date=false version='' oud_version='' - Chef::Log.debug("#{@new_resource} checking zypper") - status = shell_out("zypper --non-interactive info #{@new_resource.package_name}") + Chef::Log.debug("#{new_resource} checking zypper") + status = shell_out_with_timeout("zypper --non-interactive info #{new_resource.package_name}") status.stdout.each_line do |line| case line when /^Version: (.+)$/ version = $1 - Chef::Log.debug("#{@new_resource} version #{$1}") + Chef::Log.debug("#{new_resource} version #{$1}") when /^Installed: Yes$/ is_installed=true - Chef::Log.debug("#{@new_resource} is installed") + Chef::Log.debug("#{new_resource} is installed") when /^Installed: No$/ is_installed=false - Chef::Log.debug("#{@new_resource} is not installed") + Chef::Log.debug("#{new_resource} is not installed") when /^Status: out-of-date \(version (.+) installed\)$/ is_out_of_date=true oud_version=$1 - Chef::Log.debug("#{@new_resource} out of date version #{$1}") + Chef::Log.debug("#{new_resource} out of date version #{$1}") end end if is_installed==false @candidate_version=version - @current_resource.version(nil) + current_resource.version(nil) end if is_installed==true if is_out_of_date==true - @current_resource.version(oud_version) + current_resource.version(oud_version) @candidate_version=version else - @current_resource.version(version) + current_resource.version(version) @candidate_version=version end end @@ -77,7 +79,7 @@ class Chef raise Chef::Exceptions::Package, "zypper failed - #{status.inspect}!" end - @current_resource + current_resource end def zypper_version() @@ -104,9 +106,9 @@ class Chef def zypper_package(command, pkgname, version) version = "=#{version}" unless version.nil? || version.empty? if zypper_version < 1.0 - shell_out!("zypper#{gpg_checks} #{command} -y #{pkgname}") + shell_out_with_timeout!("zypper#{gpg_checks} #{command} -y #{pkgname}") else - shell_out!("zypper --non-interactive#{gpg_checks} "+ + shell_out_with_timeout!("zypper --non-interactive#{gpg_checks} "+ "#{command} #{pkgname}#{version}") end end diff --git a/lib/chef/provider/powershell_script.rb b/lib/chef/provider/powershell_script.rb index f9dcd6d80c..ed44dee6ae 100644 --- a/lib/chef/provider/powershell_script.rb +++ b/lib/chef/provider/powershell_script.rb @@ -24,71 +24,153 @@ class Chef provides :powershell_script, os: "windows" + def initialize (new_resource, run_context) + super(new_resource, run_context, '.ps1') + add_exit_status_wrapper + end + + def action_run + valid_syntax = validate_script_syntax! + super if valid_syntax + end + + def flags + # Must use -File rather than -Command to launch the script + # file created by the base class that contains the script + # code -- otherwise, powershell.exe does not propagate the + # error status of a failed Windows process that ran at the + # end of the script, it gets changed to '1'. + interpreter_flags = [default_interpreter_flags, '-File'].join(' ') + + if ! (@new_resource.flags.nil?) + interpreter_flags = [@new_resource.flags, interpreter_flags].join(' ') + end + + interpreter_flags + end + protected - EXIT_STATUS_EXCEPTION_HANDLER = "\ntrap [Exception] {write-error -exception ($_.Exception.Message);exit 1}".freeze - EXIT_STATUS_NORMALIZATION_SCRIPT = "\nif ($? -ne $true) { if ( $LASTEXITCODE ) {exit $LASTEXITCODE} else { exit 1 }}".freeze - EXIT_STATUS_RESET_SCRIPT = "\n$global:LASTEXITCODE=$null".freeze - # Process exit codes are strange with PowerShell. Unless you - # explicitly call exit in Powershell, the powershell.exe - # interpreter returns only 0 for success or 1 for failure. Since - # we'd like to get specific exit codes from executable tools run - # with Powershell, we do some work using the automatic variables - # $? and $LASTEXITCODE to return the process exit code of the - # 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 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 - self.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 - Chef::Log.debug("powershell_script provider called with script code:\n\n#{code}\n") + # Process exit codes are strange with PowerShell and require + # special handling to cover common use cases. + def add_exit_status_wrapper + self.code = wrapper_script + Chef::Log.debug("powershell_script provider called with script code:\n\n#{@new_resource.code}\n") Chef::Log.debug("powershell_script provider will execute transformed code:\n\n#{self.code}\n") end - public + def validate_script_syntax! + interpreter_arguments = default_interpreter_flags.join(' ') + Tempfile.open(['chef_powershell_script-user-code', '.ps1']) do | user_script_file | + user_script_file.puts("{#{@new_resource.code}}") + user_script_file.close - def initialize (new_resource, run_context) - super(new_resource, run_context, '.ps1') - normalize_script_exit_status(new_resource.code) + validation_command = "\"#{interpreter}\" #{interpreter_arguments} -Command #{user_script_file.path}" + + # For consistency with other script resources, allow even syntax errors + # to be suppressed if the returns attribute would have suppressed it + # at converge. + valid_returns = [0] + specified_returns = @new_resource.returns.is_a?(Integer) ? + [@new_resource.returns] : + @new_resource.returns + valid_returns.concat([1]) if specified_returns.include?(1) + + result = shell_out!(validation_command, {returns: valid_returns}) + result.exitstatus == 0 + end end - def flags - default_flags = [ + def default_interpreter_flags + # 'Bypass' is preferable since it doesn't require user input confirmation + # for files such as PowerShell modules downloaded from the + # Internet. However, 'Bypass' is not supported prior to + # PowerShell 3.0, so the fallback is 'Unrestricted' + execution_policy = Chef::Platform.supports_powershell_execution_bypass?(run_context.node) ? 'Bypass' : 'Unrestricted' + + [ "-NoLogo", "-NonInteractive", "-NoProfile", - "-ExecutionPolicy Unrestricted", + "-ExecutionPolicy #{execution_policy}", # Powershell will hang if STDIN is redirected # http://connect.microsoft.com/PowerShell/feedback/details/572313/powershell-exe-can-hang-if-stdin-is-redirected - "-InputFormat None", - # Must use -File rather than -Command to launch the script - # file created by the base class that contains the script - # code -- otherwise, powershell.exe does not propagate the - # error status of a failed Windows process that ran at the - # end of the script, it gets changed to '1'. - "-File" + "-InputFormat None" ] + end - interpreter_flags = default_flags.join(' ') + # A wrapper script is used to launch user-supplied script while + # still obtaining useful process exit codes. Unless you + # explicitly call exit in Powershell, the powershell.exe + # interpreter returns only 0 for success or 1 for failure. Since + # we'd like to get specific exit codes from executable tools run + # with Powershell, we do some work using the automatic variables + # $? and $LASTEXITCODE to return the process exit code of the + # 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 wrapper_script +<<-EOH +# Chef Client wrapper for powershell_script resources - if ! (@new_resource.flags.nil?) - interpreter_flags = [@new_resource.flags, interpreter_flags].join(' ') - end +# LASTEXITCODE can be uninitialized -- make it explictly 0 +# to avoid incorrect detection of failure (non-zero) codes +$global:LASTEXITCODE = 0 - interpreter_flags +# Catch any exceptions -- without this, exceptions will result +# In a zero return code instead of the desired non-zero code +# that indicates a failure +trap [Exception] {write-error ($_.Exception.Message);exit 1} + +# Variable state that should not be accessible to the user code +new-variable -name interpolatedexitcode -visibility private -value $#{@new_resource.convert_boolean_return} +new-variable -name chefscriptresult -visibility private + +# Initialize a variable we use to capture $? inside a block +$global:lastcmdlet = $null + +# Execute the user's code in a script block -- +$chefscriptresult = +{ + #{@new_resource.code} + + # This assignment doesn't affect the block's return value + $global:lastcmdlet = $? +}.invokereturnasis() + +# Assume failure status of 1 -- success cases +# will have to override this +$exitstatus = 1 + +# If convert_boolean_return is enabled, the block's return value +# gets precedence in determining our exit status +if ($interpolatedexitcode -and $chefscriptresult -ne $null -and $chefscriptresult.gettype().name -eq 'boolean') +{ + $exitstatus = [int32](!$chefscriptresult) +} +elseif ($lastcmdlet) +{ + # Otherwise, a successful cmdlet execution defines the status + $exitstatus = 0 +} +elseif ( $LASTEXITCODE -ne $null -and $LASTEXITCODE -ne 0 ) +{ + # If the cmdlet status is failed, allow the Win32 status + # in $LASTEXITCODE to define exit status. This handles the case + # where no cmdlets, only Win32 processes have run since $? + # will be set to $false whenever a Win32 process returns a non-zero + # status. + $exitstatus = $LASTEXITCODE +} + +# If this script is launched with -File, the process exit +# status of PowerShell.exe will be $exitstatus. If it was +# launched with -Command, it will be 0 if $exitstatus was 0, +# 1 (i.e. failed) otherwise. +exit $exitstatus +EOH end + end end end diff --git a/lib/chef/provider/reboot.rb b/lib/chef/provider/reboot.rb index 8dde4653ec..22e77dcc13 100644 --- a/lib/chef/provider/reboot.rb +++ b/lib/chef/provider/reboot.rb @@ -22,6 +22,7 @@ require 'chef/provider' class Chef class Provider class Reboot < Chef::Provider + provides :reboot def whyrun_supported? true diff --git a/lib/chef/provider/registry_key.rb b/lib/chef/provider/registry_key.rb index 94f4e2655b..cd62f7c56f 100644 --- a/lib/chef/provider/registry_key.rb +++ b/lib/chef/provider/registry_key.rb @@ -31,6 +31,8 @@ class Chef class Provider class RegistryKey < Chef::Provider + provides :registry_key + include Chef::Mixin::Checksum def whyrun_supported? diff --git a/lib/chef/provider/remote_file.rb b/lib/chef/provider/remote_file.rb index da2573dacb..c4643edc0b 100644 --- a/lib/chef/provider/remote_file.rb +++ b/lib/chef/provider/remote_file.rb @@ -24,6 +24,7 @@ require 'chef/deprecation/warnings' class Chef class Provider class RemoteFile < Chef::Provider::File + provides :remote_file extend Chef::Deprecation::Warnings include Chef::Deprecation::Provider::RemoteFile diff --git a/lib/chef/provider/remote_file/content.rb b/lib/chef/provider/remote_file/content.rb index ef55dd77cd..4f450ce333 100644 --- a/lib/chef/provider/remote_file/content.rb +++ b/lib/chef/provider/remote_file/content.rb @@ -20,6 +20,7 @@ require 'uri' require 'tempfile' require 'chef/file_content_management/content_base' +require 'chef/mixin/uris' class Chef class Provider @@ -28,6 +29,8 @@ class Chef private + include Chef::Mixin::Uris + def file_for_provider Chef::Log.debug("#{@new_resource} checking for changes") @@ -45,7 +48,11 @@ class Chef sources = sources.dup source = sources.shift begin - uri = URI.parse(source) + uri = if Chef::Provider::RemoteFile::Fetcher.network_share?(source) + source + else + as_uri(source) + end raw_file = grab_file_from_uri(uri) rescue SocketError, Errno::ECONNREFUSED, Errno::ENOENT, Errno::EACCES, Timeout::Error, Net::HTTPServerException, Net::HTTPFatalError, Net::FTPError => e Chef::Log.warn("#{@new_resource} cannot be downloaded from #{source}: #{e.to_s}") diff --git a/lib/chef/provider/remote_file/fetcher.rb b/lib/chef/provider/remote_file/fetcher.rb index 249b29186f..53bfe9935c 100644 --- a/lib/chef/provider/remote_file/fetcher.rb +++ b/lib/chef/provider/remote_file/fetcher.rb @@ -23,15 +23,29 @@ class Chef class Fetcher def self.for_resource(uri, new_resource, current_resource) - case uri.scheme - when "http", "https" - Chef::Provider::RemoteFile::HTTP.new(uri, new_resource, current_resource) - when "ftp" - Chef::Provider::RemoteFile::FTP.new(uri, new_resource, current_resource) - when "file" - Chef::Provider::RemoteFile::LocalFile.new(uri, new_resource, current_resource) + if network_share?(uri) + Chef::Provider::RemoteFile::NetworkFile.new(uri, new_resource, current_resource) else - raise ArgumentError, "Invalid uri, Only http(s), ftp, and file are currently supported" + case uri.scheme + when "http", "https" + Chef::Provider::RemoteFile::HTTP.new(uri, new_resource, current_resource) + when "ftp" + Chef::Provider::RemoteFile::FTP.new(uri, new_resource, current_resource) + when "file" + Chef::Provider::RemoteFile::LocalFile.new(uri, new_resource, current_resource) + else + raise ArgumentError, "Invalid uri, Only http(s), ftp, and file are currently supported" + end + end + end + + # Windows network share: \\computername\share\file + def self.network_share?(source) + case source + when String + !!(%r{\A\\\\[A-Za-z0-9+\-\.]+} =~ source) + else + false end end diff --git a/lib/chef/provider/remote_file/local_file.rb b/lib/chef/provider/remote_file/local_file.rb index e78311f2c3..026206b64e 100644 --- a/lib/chef/provider/remote_file/local_file.rb +++ b/lib/chef/provider/remote_file/local_file.rb @@ -32,15 +32,21 @@ class Chef @new_resource = new_resource @uri = uri end - + # CHEF-4472: Remove the leading slash from windows paths that we receive from a file:// URI - def fix_windows_path(path) - path.gsub(/^\/([a-zA-Z]:)/,'\1') + def fix_windows_path(path) + path.gsub(/^\/([a-zA-Z]:)/,'\1') + end + + def source_path + @source_path ||= begin + path = URI.unescape(uri.path) + Chef::Platform.windows? ? fix_windows_path(path) : path + end end # Fetches the file at uri, returning a Tempfile-like File handle def fetch - source_path = Chef::Platform.windows? ? fix_windows_path(uri.path) : uri.path tempfile = Chef::FileContentManagement::Tempfile.new(new_resource).tempfile Chef::Log.debug("#{new_resource} staging #{source_path} to #{tempfile.path}") FileUtils.cp(source_path, tempfile.path) diff --git a/lib/chef/provider/remote_file/network_file.rb b/lib/chef/provider/remote_file/network_file.rb new file mode 100644 index 0000000000..093a388d2a --- /dev/null +++ b/lib/chef/provider/remote_file/network_file.rb @@ -0,0 +1,48 @@ +# +# Author:: Jesse Campbell (<hikeit@gmail.com>) +# Copyright:: Copyright (c) 2013 Jesse Campbell +# 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 'uri' +require 'tempfile' +require 'chef/provider/remote_file' + +class Chef + class Provider + class RemoteFile + class NetworkFile + + attr_reader :new_resource + + def initialize(source, new_resource, current_resource) + @new_resource = new_resource + @source = source + end + + # Fetches the file on a network share, returning a Tempfile-like File handle + # windows only + def fetch + tempfile = Chef::FileContentManagement::Tempfile.new(new_resource).tempfile + Chef::Log.debug("#{new_resource} staging #{@source} to #{tempfile.path}") + FileUtils.cp(@source, tempfile.path) + tempfile.close if tempfile + tempfile + end + + end + end + end +end diff --git a/lib/chef/provider/service.rb b/lib/chef/provider/service.rb index 75da2ddb31..9c523b5e66 100644 --- a/lib/chef/provider/service.rb +++ b/lib/chef/provider/service.rb @@ -168,6 +168,50 @@ class Chef @new_resource.respond_to?(method_name) && !!@new_resource.send(method_name) end + + module ServicePriorityInit + + # + # Platform-specific versions + # + + # + # Linux + # + + require 'chef/chef_class' + require 'chef/provider/service/systemd' + require 'chef/provider/service/insserv' + require 'chef/provider/service/redhat' + require 'chef/provider/service/arch' + require 'chef/provider/service/gentoo' + require 'chef/provider/service/upstart' + require 'chef/provider/service/debian' + require 'chef/provider/service/invokercd' + require 'chef/provider/service/freebsd' + require 'chef/provider/service/openbsd' + require 'chef/provider/service/solaris' + require 'chef/provider/service/macosx' + + def self.os(os, *providers) + Chef.set_provider_priority_array(:service, providers, os: os) + end + def self.platform_family(platform_family, *providers) + Chef.set_provider_priority_array(:service, providers, platform_family: platform_family) + end + + os %w(freebsd netbsd), Freebsd + os %w(openbsd), Openbsd + os %w(solaris2), Solaris + os %w(darwin), Macosx + os %w(linux), Systemd, Insserv, Redhat + + platform_family %w(arch), Systemd, Arch + platform_family %w(gentoo), Systemd, Gentoo + platform_family %w(debian), Systemd, Upstart, Insserv, Debian, Invokercd + platform_family %w(rhel fedora suse), Systemd, Insserv, Redhat + + end end end end diff --git a/lib/chef/provider/service/aix.rb b/lib/chef/provider/service/aix.rb index 0aef62c62e..09ed4bbf01 100644 --- a/lib/chef/provider/service/aix.rb +++ b/lib/chef/provider/service/aix.rb @@ -91,15 +91,18 @@ class Chef protected def determine_current_status! - Chef::Log.debug "#{@new_resource} using lssrc to check the status " + Chef::Log.debug "#{@new_resource} using lssrc to check the status" begin - services = shell_out!("lssrc -a | grep -w #{@new_resource.service_name}").stdout.split("\n") - is_resource_group?(services) - - if services.length == 1 && services[0].split(' ').last == "active" - @current_resource.running true - else + if is_resource_group? + # Groups as a whole have no notion of whether they're running @current_resource.running false + else + service = shell_out!("lssrc -s #{@new_resource.service_name}").stdout + if service.split(' ').last == 'active' + @current_resource.running true + else + @current_resource.running false + end end Chef::Log.debug "#{@new_resource} running: #{@current_resource.running}" # ShellOut sometimes throws different types of Exceptions than ShellCommandFailed. @@ -112,11 +115,9 @@ class Chef end end - def is_resource_group? (services) - if services.length > 1 - Chef::Log.debug("#{@new_resource.service_name} is a group") - @is_resource_group = true - elsif services[0].split(' ')[1] == @new_resource.service_name + def is_resource_group? + so = shell_out!("lssrc -g #{@new_resource.service_name}") + if so.exitstatus == 0 Chef::Log.debug("#{@new_resource.service_name} is a group") @is_resource_group = true end diff --git a/lib/chef/provider/service/freebsd.rb b/lib/chef/provider/service/freebsd.rb index 9204e3ef92..6c78f86fe0 100644 --- a/lib/chef/provider/service/freebsd.rb +++ b/lib/chef/provider/service/freebsd.rb @@ -147,7 +147,7 @@ class Chef # some scripts support multiple instances through symlinks such as openvpn. # We should get the service name from rcvar. Chef::Log.debug("name=\"service\" not found at #{init_command}. falling back to rcvar") - sn = shell_out!("#{init_command} rcvar").stdout[/(\w+_enable)=/, 1] + shell_out!("#{init_command} rcvar").stdout[/(\w+_enable)=/, 1] else # for why-run mode when the rcd_script is not there yet new_resource.service_name diff --git a/lib/chef/provider/service/init.rb b/lib/chef/provider/service/init.rb index 0a219a69e1..355e98a0eb 100644 --- a/lib/chef/provider/service/init.rb +++ b/lib/chef/provider/service/init.rb @@ -18,6 +18,7 @@ require 'chef/provider/service/simple' require 'chef/mixin/command' +require 'chef/platform/service_helpers' class Chef class Provider diff --git a/lib/chef/provider/service/macosx.rb b/lib/chef/provider/service/macosx.rb index 7cfe57a92a..7324822eff 100644 --- a/lib/chef/provider/service/macosx.rb +++ b/lib/chef/provider/service/macosx.rb @@ -28,8 +28,8 @@ class Chef class Service class Macosx < Chef::Provider::Service::Simple - provides :service, os: "darwin" provides :macosx_service, os: "darwin" + provides :service, os: "darwin" def self.gather_plist_dirs locations = %w{/Library/LaunchAgents diff --git a/lib/chef/provider/service/windows.rb b/lib/chef/provider/service/windows.rb index ba53f0a3c3..355ffafc2a 100644 --- a/lib/chef/provider/service/windows.rb +++ b/lib/chef/provider/service/windows.rb @@ -25,7 +25,6 @@ if RUBY_PLATFORM =~ /mswin|mingw32|windows/ end class Chef::Provider::Service::Windows < Chef::Provider::Service - provides :service, os: "windows" provides :windows_service, os: "windows" diff --git a/lib/chef/provider/user.rb b/lib/chef/provider/user.rb index f6ac72448e..ad92a72a0a 100644 --- a/lib/chef/provider/user.rb +++ b/lib/chef/provider/user.rb @@ -23,6 +23,7 @@ require 'etc' class Chef class Provider class User < Chef::Provider + provides :user include Chef::Mixin::Command @@ -208,7 +209,6 @@ class Chef def unlock_user raise NotImplementedError end - end end end diff --git a/lib/chef/provider/user/aix.rb b/lib/chef/provider/user/aix.rb index af08ab4364..a575a41e54 100644 --- a/lib/chef/provider/user/aix.rb +++ b/lib/chef/provider/user/aix.rb @@ -18,9 +18,10 @@ class Chef class Provider class User class Aix < Chef::Provider::User::Useradd + provides :user, platform: %w(aix) UNIVERSAL_OPTIONS = [[:comment, "-c"], [:gid, "-g"], [:shell, "-s"], [:uid, "-u"]] - + def create_user super add_password @@ -88,7 +89,7 @@ class Chef end end end - + end end end diff --git a/lib/chef/provider/user/pw.rb b/lib/chef/provider/user/pw.rb index fe71e93561..810ffb9a8d 100644 --- a/lib/chef/provider/user/pw.rb +++ b/lib/chef/provider/user/pw.rb @@ -22,6 +22,7 @@ class Chef class Provider class User class Pw < Chef::Provider::User + provides :user, platform: %w(freebsd) def load_current_resource super diff --git a/lib/chef/provider/user/solaris.rb b/lib/chef/provider/user/solaris.rb index d480acaced..b242095f0c 100644 --- a/lib/chef/provider/user/solaris.rb +++ b/lib/chef/provider/user/solaris.rb @@ -22,6 +22,8 @@ class Chef class Provider class User class Solaris < Chef::Provider::User::Useradd + provides :user, platform: %w(omnios solaris2) + UNIVERSAL_OPTIONS = [[:comment, "-c"], [:gid, "-g"], [:shell, "-s"], [:uid, "-u"]] attr_writer :password_file diff --git a/lib/chef/provider/user/useradd.rb b/lib/chef/provider/user/useradd.rb index cc770c0be2..a1b5b3459c 100644 --- a/lib/chef/provider/user/useradd.rb +++ b/lib/chef/provider/user/useradd.rb @@ -23,6 +23,7 @@ class Chef class Provider class User class Useradd < Chef::Provider::User + provides :user UNIVERSAL_OPTIONS = [[:comment, "-c"], [:gid, "-g"], [:password, "-p"], [:shell, "-s"], [:uid, "-u"]] diff --git a/lib/chef/provider_resolver.rb b/lib/chef/provider_resolver.rb index 5e887225e4..5bfee343d1 100644 --- a/lib/chef/provider_resolver.rb +++ b/lib/chef/provider_resolver.rb @@ -20,6 +20,30 @@ require 'chef/exceptions' require 'chef/platform/provider_priority_map' class Chef + # + # Provider Resolution + # =================== + # + # Provider resolution is the process of taking a Resource object and an + # action, and determining the Provider class that should be instantiated to + # handle the action. + # + # If the resource has its `provider` set, that is used. + # + # Otherwise, we take the lists of Providers that have registered as + # providing the DSL through `provides :dsl_name, <filters>` or + # `Chef.set_resource_priority_array :dsl_name, <filters>`. We filter each + # list of Providers through: + # + # 1. The filters it was registered with (such as `os: 'linux'` or + # `platform_family: 'debian'`) + # 2. `provides?(node, resource)` + # 3. `supports?(resource, action)` + # + # Anything that passes the filter and returns `true` to provides and supports, + # is considered a match. The first matching Provider in the *most recently + # registered list* is selected and returned. + # class ProviderResolver attr_reader :node @@ -32,33 +56,14 @@ class Chef @action = action end - # return a deterministically sorted list of Chef::Provider subclasses - def providers - @providers ||= Chef::Provider.descendants - end - def resolve maybe_explicit_provider(resource) || maybe_dynamic_provider_resolution(resource, action) || maybe_chef_platform_lookup(resource) end - # this cut looks at if the provider can handle the resource type on the node - def enabled_handlers - @enabled_handlers ||= - providers.select do |klass| - # NB: this is different from resource_resolver which must pass a resource_name - # FIXME: deprecate this and normalize on passing resource_name here - klass.provides?(node, resource) - end.sort {|a,b| a.to_s <=> b.to_s } - end - - # this cut looks at if the provider can handle the specific resource and action - def supported_handlers - @supported_handlers ||= - enabled_handlers.select do |klass| - klass.supports?(resource, action) - end + def provided_by?(provider_class) + prioritized_handlers.include?(provider_class) end private @@ -71,40 +76,37 @@ class Chef # try dynamically finding a provider based on querying the providers to see what they support def maybe_dynamic_provider_resolution(resource, action) - # log this so we know what providers will work for the generic resource on the node (early cut) - Chef::Log.debug "providers for generic #{resource.resource_name} resource enabled on node include: #{enabled_handlers}" - - # what providers were excluded by machine state (late cut) - Chef::Log.debug "providers that refused resource #{resource} were: #{enabled_handlers - supported_handlers}" - Chef::Log.debug "providers that support resource #{resource} include: #{supported_handlers}" - - # if none of the providers specifically support the resource, we still need to pick one of the providers that are - # enabled on the node to handle the why-run use case. - handlers = supported_handlers.empty? ? enabled_handlers : supported_handlers - Chef::Log.debug "no providers supported the resource, falling back to enabled handlers" if supported_handlers.empty? - - 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_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 ] + Chef::Log.debug "Providers for generic #{resource.resource_name} resource enabled on node include: #{enabled_handlers}" + + # Get all the handlers in the priority bucket + handlers = prioritized_handlers + + # Narrow it down to handlers that return `true` to `provides?` + # TODO deprecate this and don't bother calling--the fact that they said + # `provides` should be enough. But we need to do it right now because + # some classes implement additional handling. + enabled_handlers = prioritized_handlers.select { |handler| handler.provides?(node, resource) } + + # Narrow it down to handlers that return `true` to `supports?` + # TODO deprecate this and allow actions to be passed as a filter to + # `provides` so we don't have to have two separate things. + supported_handlers = enabled_handlers.select { |handler| handler.supports?(resource, action) } + if supported_handlers.empty? + # if none of the providers specifically support the resource, we still need to pick one of the providers that are + # enabled on the node to handle the why-run use case. FIXME we should only do this in why-run mode then. + Chef::Log.debug "No providers responded true to `supports?` for action #{action} on resource #{resource}, falling back to enabled handlers so we can return something anyway." + handler = enabled_handlers.first + else + handler = supported_handlers.first end - Chef::Log.debug "providers that survived replacement include: #{handlers}" - - raise Chef::Exceptions::AmbiguousProviderResolution.new(resource, handlers) if handlers.count >= 2 - - Chef::Log.debug "dynamic provider resolver FAILED to resolve a provider" if handlers.empty? - - return nil if handlers.empty? + if handler + Chef::Log.debug "Provider for action #{action} on resource #{resource} is #{handler}" + else + Chef::Log.debug "Dynamic provider resolver FAILED to resolve a provider for action #{action} on resource #{resource}" + end - handlers[0] + handler end # try the old static lookup of providers by platform @@ -112,13 +114,51 @@ class Chef Chef::Platform.find_provider_for_node(node, resource) end - # dep injection hooks - 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 end + + def prioritized_handlers + @prioritized_handlers ||= + provider_priority_map.list_handlers(node, resource.resource_name).flatten(1).uniq + end + + module Deprecated + # return a deterministically sorted list of Chef::Provider subclasses + def providers + @providers ||= Chef::Provider.descendants + end + + # this cut looks at if the provider can handle the resource type on the node + def enabled_handlers + @enabled_handlers ||= + providers.select do |klass| + # NB: this is different from resource_resolver which must pass a resource_name + # FIXME: deprecate this and normalize on passing resource_name here + klass.provides?(node, resource) + end.sort {|a,b| a.to_s <=> b.to_s } + end + + # this cut looks at if the provider can handle the specific resource and action + def supported_handlers + @supported_handlers ||= + enabled_handlers.select do |klass| + klass.supports?(resource, action) + end + end + + # If there are no providers for a DSL, we search through the + def prioritized_handlers + @prioritized_handlers ||= super || begin + result = providers.select { |handler| handler.provides?(node, resource) }.sort_by(:name) + if !result.empty? + Chef::Log.deprecation("#{resource.resource_name.to_sym} is marked as providing DSL #{method_symbol}, but provides #{resource.resource_name.to_sym.inspect} was never called!") + Chef::Log.deprecation("In Chef 13, this will break: you must call provides to mark the names you provide, even if you also override provides? yourself.") + end + result + end + end + end + prepend Deprecated end end diff --git a/lib/chef/providers.rb b/lib/chef/providers.rb index a5f5386de3..18500d4669 100644 --- a/lib/chef/providers.rb +++ b/lib/chef/providers.rb @@ -122,6 +122,7 @@ require 'chef/provider/deploy/timestamped' require 'chef/provider/remote_file/ftp' require 'chef/provider/remote_file/http' require 'chef/provider/remote_file/local_file' +require 'chef/provider/remote_file/network_file' require 'chef/provider/remote_file/fetcher' require "chef/provider/lwrp_base" diff --git a/lib/chef/resource.rb b/lib/chef/resource.rb index d934ec8c47..7fe8a52d95 100644 --- a/lib/chef/resource.rb +++ b/lib/chef/resource.rb @@ -22,6 +22,7 @@ require 'chef/dsl/platform_introspection' require 'chef/dsl/data_query' require 'chef/dsl/registry_helper' require 'chef/dsl/reboot_pending' +require 'chef/dsl/resources' require 'chef/mixin/convert_to_class_name' require 'chef/guard_interpreter/resource_guard_interpreter' require 'chef/resource/conditional' @@ -31,9 +32,14 @@ require 'chef/node_map' require 'chef/node' require 'chef/platform' require 'chef/resource/resource_notification' +require 'chef/provider_resolver' +require 'chef/resource_resolver' +require 'set' require 'chef/mixin/deprecation' require 'chef/mixin/provides' +require 'chef/mixin/shell_out' +require 'chef/mixin/powershell_out' class Chef class Resource @@ -48,6 +54,12 @@ class Chef include Chef::DSL::RebootPending extend Chef::Mixin::Provides + # This lets user code do things like `not_if { shell_out!("command") }` + include Chef::Mixin::ShellOut + include Chef::Mixin::PowershellOut + + NULL_ARG = Object.new + # # The node the current Chef run is using. # @@ -79,7 +91,6 @@ class Chef run_context.resource_collection.find(*args) end - # # Resource User Interface (for users) # @@ -98,8 +109,8 @@ class Chef @before = nil @params = Hash.new @provider = nil - @allowed_actions = [ :nothing ] - @action = :nothing + @allowed_actions = self.class.allowed_actions.to_a + @action = self.class.default_action @updated = false @updated_by_last_action = false @supports = {} @@ -160,19 +171,24 @@ class Chef # @param arg [Array[Symbol], Symbol] A list of actions (e.g. `:create`) # @return [Array[Symbol]] the list of actions. # + attr_accessor :action def action(arg=nil) if arg - action_list = arg.kind_of?(Array) ? arg : [ arg ] - action_list = action_list.collect { |a| a.to_sym } - action_list.each do |action| + if arg.is_a?(Array) + arg = arg.map { |a| a.to_sym } + else + arg = arg.to_sym + end + Array(arg).each do |action| validate( { action: action }, - { action: { kind_of: Symbol, equal_to: @allowed_actions } } + { action: { kind_of: Symbol, equal_to: allowed_actions } } ) end - @action = action_list + self.action = arg else - @action + # Pull the action from the class if it's not set + @action || self.class.default_action end end @@ -180,8 +196,7 @@ class Chef # Sets up a notification that will run a particular action on another resource # if and when *this* resource is updated by an action. # - # If the action does nothing--does not update this resource, the - # notification never triggers.) + # If the action does not update this resource, the notification never triggers. # # Only one resource may be specified per notification. # @@ -467,7 +482,7 @@ class Chef # # @return [Hash{Symbol => Object}] A Hash of attribute => value for the # Resource class's `state_attrs`. - def state + def state_for_resource_reporter self.class.state_attrs.inject({}) do |state_attrs, attr_name| state_attrs[attr_name] = send(attr_name) state_attrs @@ -475,6 +490,15 @@ class Chef end # + # Since there are collisions with LWRP parameters named 'state' this + # method is not used by the resource_reporter and is most likely unused. + # It certainly cannot be relied upon and cannot be fixed. + # + # @deprecated + # + alias_method :state, :state_for_resource_reporter + + # # The value of the identity attribute, if declared. Falls back to #name if # no identity attribute is declared. # @@ -588,14 +612,14 @@ class Chef # def to_s - "#{@resource_name}[#{@name}]" + "#{resource_name}[#{name}]" end def to_text return "suppressed sensitive resource output" if sensitive ivars = instance_variables.map { |ivar| ivar.to_sym } - HIDDEN_IVARS text = "# Declared in #{@source_line}\n\n" - text << self.class.dsl_name + "(\"#{name}\") do\n" + text << "#{resource_name}(\"#{name}\") do\n" ivars.each do |ivar| if (value = instance_variable_get(ivar)) && !(value.respond_to?(:empty?) && value.empty?) value_string = value.respond_to?(:to_text) ? value.to_text : value.inspect @@ -749,6 +773,12 @@ class Chef # have. # attr_accessor :allowed_actions + def allowed_actions(value=NULL_ARG) + if value != NULL_ARG + self.allowed_actions = value + end + @allowed_actions + end # # Whether or not this resource was updated during an action. If multiple @@ -807,19 +837,15 @@ class Chef end # - # The DSL name of this resource (e.g. `package` or `yum_package`) + # The display name of this resource type, for printing purposes. # - # @return [String] The DSL name of this resource. - def self.dsl_name - convert_to_snake_case(name, 'Chef::Resource') - end - + # Will be used to print out the resource in messages, e.g. resource_name[name] # - # The name of this resource (e.g. `file`) + # @return [Symbol] The name of this resource type (e.g. `:execute`). # - # @return [String] The name of this resource. - # - attr_reader :resource_name + def resource_name + @resource_name || self.class.resource_name + end # # Sets a list of capabilities of the real resource. For example, `:remount` @@ -852,6 +878,66 @@ class Chef end # + # The DSL name of this resource (e.g. `package` or `yum_package`) + # + # @return [String] The DSL name of this resource. + # + # @deprecated Use resource_name instead. + # + def self.dsl_name + Chef::Log.deprecation "Resource.dsl_name is deprecated and will be removed in Chef 13. Use resource_name instead." + if name + name = self.name.split('::')[-1] + convert_to_snake_case(name) + end + end + + # + # The display name of this resource type, for printing purposes. + # + # This also automatically calls "provides" to provide DSL with the given + # name. + # + # resource_name defaults to your class name. + # + # Call `resource_name nil` to remove the resource name (and any + # corresponding DSL). + # + # @param value [Symbol] The desired name of this resource type (e.g. + # `execute`), or `nil` if this class is abstract and has no resource_name. + # + # @return [Symbol] The name of this resource type (e.g. `:execute`). + # + def self.resource_name(name=NULL_ARG) + # Setter + if name != NULL_ARG + remove_canonical_dsl + + # Set the resource_name and call provides + if name + name = name.to_sym + # If our class is not already providing this name, provide it. + if !Chef::ResourceResolver.list(name).include?(self) + provides name, canonical: true + end + @resource_name = name + else + @resource_name = nil + end + else + # set resource_name automatically if it's not set + if !instance_variable_defined?(:@resource_name) && self.name + resource_name convert_to_snake_case(self.name.split('::')[-1]) + end + end + + @resource_name + end + def self.resource_name=(name) + resource_name(name) + end + + # # The module where Chef should look for providers for this resource. # The provider for `MyResource` will be looked up using # `provider_base::MyResource`. Defaults to `Chef::Provider`. @@ -865,11 +951,70 @@ class Chef # # ...other stuff # end # + # @deprecated Use `provides` on the provider, or `provider` on the resource, instead. + # def self.provider_base(arg=nil) - @provider_base ||= arg - @provider_base ||= Chef::Provider + if arg + Chef::Log.deprecation("Resource.provider_base is deprecated and will be removed in Chef 13. Use provides on the provider, or provider on the resource, instead.") + end + @provider_base ||= arg || Chef::Provider + end + + # + # The list of allowed actions for the resource. + # + # @param actions [Array<Symbol>] The list of actions to add to allowed_actions. + # + # @return [Arrau<Symbol>] The list of actions, as symbols. + # + def self.allowed_actions(*actions) + @allowed_actions ||= + if superclass.respond_to?(:allowed_actions) + superclass.allowed_actions.dup + else + [ :nothing ] + end + @allowed_actions |= actions end + def self.allowed_actions=(value) + @allowed_actions = value + end + + # + # The action that will be run if no other action is specified. + # + # Setting default_action will automatially add the action to + # allowed_actions, if it isn't already there. + # + # Defaults to :nothing. + # + # @param action_name [Symbol,Array<Symbol>] The default action (or series + # of actions) to use. + # + # @return [Symbol,Array<Symbol>] The default actions for the resource. + # + def self.default_action(action_name=NULL_ARG) + unless action_name.equal?(NULL_ARG) + if action_name.is_a?(Array) + @default_action = action_name.map { |arg| arg.to_sym } + else + @default_action = action_name.to_sym + end + self.allowed_actions |= Array(@default_action) + end + + if @default_action + @default_action + elsif superclass.respond_to?(:default_action) + superclass.default_action + else + :nothing + end + end + def self.default_action=(action_name) + default_action(action_name) + end # # Internal Resource Interface (for Chef) @@ -945,10 +1090,31 @@ class Chef # NOTE: that we do not support unregistering classes as descendents like # we used to for LWRP unloading because that was horrible and removed in # Chef-12. + # @deprecated + # @api private alias :resource_classes :descendants + # @deprecated + # @api private alias :find_subclass_by_name :find_descendants_by_name end + # @deprecated + # @api private + # We memoize a sorted version of descendants so that resource lookups don't + # have to sort all the things, all the time. + # This was causing performance issues in test runs, and probably in real + # life as well. + @@sorted_descendants = nil + def self.sorted_descendants + @@sorted_descendants ||= descendants.sort_by { |x| x.to_s } + end + def self.inherited(child) + super + @sorted_descendants = nil + child.resource_name + end + + # If an unknown method is invoked, determine whether the enclosing Provider's # lexical scope can fulfill the request. E.g. This happens when the Resource's # block invokes new_resource. @@ -960,6 +1126,32 @@ class Chef end end + # + # Mark this resource as providing particular DSL. + # + # Resources have an automatic DSL based on their resource_name, equivalent to + # `provides :resource_name` (providing the resource on all OS's). If you + # declare a `provides` with the given resource_name, it *replaces* that + # provides (so that you can provide your resource DSL only on certain OS's). + # + def self.provides(name, **options, &block) + name = name.to_sym + + # `provides :resource_name, os: 'linux'`) needs to remove the old + # canonical DSL before adding the new one. + if @resource_name && name == @resource_name + remove_canonical_dsl + end + + result = Chef.set_resource_priority_array(name, self, options, &block) + Chef::DSL::Resources.add_resource_dsl(name) + result + end + + def self.provides?(node, resource) + Chef::ResourceResolver.resolve(resource, node: node).provided_by?(self) + end + # Helper for #notifies def validate_resource_spec!(resource_spec) run_context.resource_collection.validate_lookup_spec!(resource_spec) @@ -1016,7 +1208,6 @@ 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 @@ -1090,30 +1281,90 @@ class Chef # === Returns # <Chef::Resource>:: returns the proper Chef::Resource class def self.resource_for_node(short_name, node) - require 'chef/resource_resolver' - klass = Chef::ResourceResolver.new(node, short_name).resolve + klass = Chef::ResourceResolver.resolve(short_name, node: node) raise Chef::Exceptions::NoSuchResourceType.new(short_name, node) if klass.nil? klass end - # Returns the class of a Chef::Resource based on the short name + # + # Returns the class with the given resource_name. + # # ==== Parameters # short_name<Symbol>:: short_name of the resource (ie :directory) # # === Returns # <Chef::Resource>:: returns the proper Chef::Resource class + # def self.resource_matching_short_name(short_name) - begin - rname = convert_to_class_name(short_name.to_s) - Chef::Resource.const_get(rname) - rescue NameError - nil + Chef::ResourceResolver.resolve(short_name, canonical: true) + end + + # @api private + def self.register_deprecated_lwrp_class(resource_class, class_name) + if Chef::Resource.const_defined?(class_name, false) + Chef::Log.warn "#{class_name} already exists! Deprecation class overwrites #{resource_class}" + Chef::Resource.send(:remove_const, class_name) end + + # In order to generate deprecation warnings when you use Chef::Resource::MyLwrp, + # we make a special subclass (identical in nearly all respects) of the + # actual LWRP. When you say any of these, a deprecation warning will be + # generated: + # + # - Chef::Resource::MyLwrp.new(...) + # - resource.is_a?(Chef::Resource::MyLwrp) + # - resource.kind_of?(Chef::Resource::MyLwrp) + # - case resource + # when Chef::Resource::MyLwrp + # end + # + resource_subclass = class_eval <<-EOM, __FILE__, __LINE__+1 + class Chef::Resource::#{class_name} < resource_class + resource_name nil # we do not actually provide anything + def initialize(*args, &block) + Chef::Log.deprecation("Using an LWRP by its name (#{class_name}) directly is no longer supported in Chef 13 and will be removed. Use Chef::Resource.resource_for_node(node, name) instead.") + super + end + def self.resource_name(*args) + if args.empty? + @resource_name ||= superclass.resource_name + else + super + end + end + self + end + EOM + # Make case, is_a and kind_of work with the new subclass, for backcompat. + # Any subclass of Chef::Resource::ResourceClass is already a subclass of resource_class + # Any subclass of resource_class is considered a subclass of Chef::Resource::ResourceClass + resource_class.class_eval do + define_method(:is_a?) do |other| + other.is_a?(Module) && other === self + end + define_method(:kind_of?) do |other| + other.is_a?(Module) && other === self + end + end + resource_subclass.class_eval do + define_singleton_method(:===) do |other| + Chef::Log.deprecation("Using an LWRP by its name (#{class_name}) directly is no longer supported in Chef 13 and will be removed. Use Chef::Resource.resource_for_node(node, name) instead.") + # resource_subclass is a superclass of all resource_class descendants. + if self == resource_subclass && other.class <= resource_class + return true + end + super(other) + end + end + deprecated_constants[class_name.to_sym] = resource_subclass end - private + def self.deprecated_constants + @deprecated_constants ||= {} + end - def lookup_provider_constant(name) + # @api private + def lookup_provider_constant(name, action=:nothing) begin self.class.provider_base.const_get(convert_to_class_name(name.to_s)) rescue NameError => e @@ -1124,5 +1375,19 @@ class Chef end end end + + private + + def self.remove_canonical_dsl + if @resource_name + remaining = Chef.resource_priority_map.delete_canonical(@resource_name, self) + if !remaining + Chef::DSL::Resources.remove_resource_dsl(@resource_name) + end + end + end end end + +# Requiring things at the bottom breaks cycles +require 'chef/chef_class' diff --git a/lib/chef/resource/apt_package.rb b/lib/chef/resource/apt_package.rb index f944825ac3..ca119b50c4 100644 --- a/lib/chef/resource/apt_package.rb +++ b/lib/chef/resource/apt_package.rb @@ -23,12 +23,10 @@ class Chef class Resource class AptPackage < Chef::Resource::Package - provides :apt_package provides :package, os: "linux", platform_family: [ "debian" ] def initialize(name, run_context=nil) super - @resource_name = :apt_package @default_release = nil end diff --git a/lib/chef/resource/bash.rb b/lib/chef/resource/bash.rb index 0add0ce501..025687e879 100644 --- a/lib/chef/resource/bash.rb +++ b/lib/chef/resource/bash.rb @@ -25,7 +25,6 @@ class Chef def initialize(name, run_context=nil) super - @resource_name = :bash @interpreter = "bash" end diff --git a/lib/chef/resource/batch.rb b/lib/chef/resource/batch.rb index c091ec56b6..efe3f2205f 100644 --- a/lib/chef/resource/batch.rb +++ b/lib/chef/resource/batch.rb @@ -25,7 +25,7 @@ class Chef provides :batch, os: "windows" def initialize(name, run_context=nil) - super(name, run_context, :batch, "cmd.exe") + super(name, run_context, nil, "cmd.exe") end end diff --git a/lib/chef/resource/bff_package.rb b/lib/chef/resource/bff_package.rb index 917f0d1d50..7c1496a46b 100644 --- a/lib/chef/resource/bff_package.rb +++ b/lib/chef/resource/bff_package.rb @@ -22,14 +22,6 @@ require 'chef/provider/package/aix' class Chef class Resource class BffPackage < Chef::Resource::Package - - def initialize(name, run_context=nil) - super - @resource_name = :bff_package - end - end end end - - diff --git a/lib/chef/resource/breakpoint.rb b/lib/chef/resource/breakpoint.rb index b2210262d2..69dbc48050 100644 --- a/lib/chef/resource/breakpoint.rb +++ b/lib/chef/resource/breakpoint.rb @@ -22,14 +22,12 @@ require 'chef/resource' class Chef class Resource class Breakpoint < Chef::Resource + default_action :break def initialize(action="break", *args) - @name = caller.first - super(@name, *args) - @action = "break" - @allowed_actions << :break - @resource_name = :breakpoint + super(caller.first, *args) end + end end end diff --git a/lib/chef/resource/chef_gem.rb b/lib/chef/resource/chef_gem.rb index 59f575a524..0c2fdfa819 100644 --- a/lib/chef/resource/chef_gem.rb +++ b/lib/chef/resource/chef_gem.rb @@ -23,11 +23,8 @@ class Chef class Resource class ChefGem < Chef::Resource::Package::GemPackage - provides :chef_gem - def initialize(name, run_context=nil) super - @resource_name = :chef_gem @compile_time = Chef::Config[:chef_gem_compile_time] @gem_binary = RbConfig::CONFIG['bindir'] + "/gem" end diff --git a/lib/chef/resource/cookbook_file.rb b/lib/chef/resource/cookbook_file.rb index 7be353b648..42f16e6db6 100644 --- a/lib/chef/resource/cookbook_file.rb +++ b/lib/chef/resource/cookbook_file.rb @@ -27,13 +27,11 @@ class Chef class CookbookFile < Chef::Resource::File include Chef::Mixin::Securable - provides :cookbook_file + default_action :create def initialize(name, run_context=nil) super @provider = Chef::Provider::CookbookFile - @resource_name = :cookbook_file - @action = "create" @source = ::File.basename(name) @cookbook = nil end diff --git a/lib/chef/resource/cron.rb b/lib/chef/resource/cron.rb index cb16506012..93cf41bc37 100644 --- a/lib/chef/resource/cron.rb +++ b/lib/chef/resource/cron.rb @@ -27,13 +27,11 @@ class Chef state_attrs :minute, :hour, :day, :month, :weekday, :user - provides :cron + default_action :create + allowed_actions :create, :delete def initialize(name, run_context=nil) super - @resource_name = :cron - @action = :create - @allowed_actions.push(:create, :delete) @minute = "*" @hour = "*" @day = "*" diff --git a/lib/chef/resource/csh.rb b/lib/chef/resource/csh.rb index 36659c349b..d5e9c910b1 100644 --- a/lib/chef/resource/csh.rb +++ b/lib/chef/resource/csh.rb @@ -25,7 +25,6 @@ class Chef def initialize(name, run_context=nil) super - @resource_name = :csh @interpreter = "csh" end diff --git a/lib/chef/resource/deploy.rb b/lib/chef/resource/deploy.rb index 4252aa230f..3e5255bced 100644 --- a/lib/chef/resource/deploy.rb +++ b/lib/chef/resource/deploy.rb @@ -51,15 +51,15 @@ class Chef # class Deploy < Chef::Resource - provider_base Chef::Provider::Deploy - identity_attr :repository state_attrs :deploy_to, :revision + default_action :deploy + allowed_actions :force_deploy, :deploy, :rollback + def initialize(name, run_context=nil) super - @resource_name = :deploy @deploy_to = name @environment = nil @repository_cache = 'cached-copy' @@ -69,7 +69,6 @@ class Chef @symlink_before_migrate = {"config/database.yml" => "config/database.yml"} @symlinks = {"system" => "public/system", "pids" => "tmp/pids", "log" => "log"} @revision = 'HEAD' - @action = :deploy @migrate = false @rollback_on_error = false @remote = "origin" @@ -77,7 +76,6 @@ class Chef @shallow_clone = false @scm_provider = Chef::Provider::Git @svn_force_export = false - @allowed_actions.push(:force_deploy, :deploy, :rollback) @additional_remotes = Hash[] @keep_releases = 5 @enable_checkout = true @@ -281,6 +279,12 @@ class Chef ) end + # This is to support "provider :revision" without deprecation warnings. + # Do NOT copy this. + def self.provider_base + Chef::Provider::Deploy + end + def svn_force_export(arg=nil) set_or_return( :svn_force_export, diff --git a/lib/chef/resource/deploy_revision.rb b/lib/chef/resource/deploy_revision.rb index e144ce2162..1397359ac8 100644 --- a/lib/chef/resource/deploy_revision.rb +++ b/lib/chef/resource/deploy_revision.rb @@ -22,23 +22,9 @@ class Chef # Convenience class for using the deploy resource with the revision # deployment strategy (provider) class DeployRevision < Chef::Resource::Deploy - - provides :deploy_revision - - def initialize(*args, &block) - super - @resource_name = :deploy_revision - end end class DeployBranch < Chef::Resource::DeployRevision - - provides :deploy_branch - - def initialize(*args, &block) - super - @resource_name = :deploy_branch - end end end diff --git a/lib/chef/resource/directory.rb b/lib/chef/resource/directory.rb index 1ab7f0d16d..9cac2ce243 100644 --- a/lib/chef/resource/directory.rb +++ b/lib/chef/resource/directory.rb @@ -32,15 +32,13 @@ class Chef include Chef::Mixin::Securable - provides :directory + default_action :create + allowed_actions :create, :delete def initialize(name, run_context=nil) super - @resource_name = :directory @path = name - @action = :create @recursive = false - @allowed_actions.push(:create, :delete) end def recursive(arg=nil) diff --git a/lib/chef/resource/dpkg_package.rb b/lib/chef/resource/dpkg_package.rb index 35a47e8a82..38adf24cf6 100644 --- a/lib/chef/resource/dpkg_package.rb +++ b/lib/chef/resource/dpkg_package.rb @@ -25,11 +25,6 @@ class Chef provides :dpkg_package, os: "linux" - def initialize(name, run_context=nil) - super - @resource_name = :dpkg_package - end - end end end diff --git a/lib/chef/resource/dsc_resource.rb b/lib/chef/resource/dsc_resource.rb index 912b683434..5db00f49ca 100644 --- a/lib/chef/resource/dsc_resource.rb +++ b/lib/chef/resource/dsc_resource.rb @@ -25,13 +25,12 @@ class Chef include Chef::DSL::Powershell
+ default_action :run
+
def initialize(name, run_context)
super
@properties = {}
- @resource_name = :dsc_resource
@resource = nil
- @allowed_actions.push(:run)
- @action = :run
end
def resource(value=nil)
diff --git a/lib/chef/resource/dsc_script.rb b/lib/chef/resource/dsc_script.rb index cf96ef6b7f..2fcf183375 100644 --- a/lib/chef/resource/dsc_script.rb +++ b/lib/chef/resource/dsc_script.rb @@ -24,11 +24,10 @@ class Chef provides :dsc_script, platform: "windows" + default_action :run + def initialize(name, run_context=nil) super - @allowed_actions.push(:run) - @action = :run - @resource_name = :dsc_script @imports = {} end diff --git a/lib/chef/resource/easy_install_package.rb b/lib/chef/resource/easy_install_package.rb index 5286e9a289..df4cee1ab3 100644 --- a/lib/chef/resource/easy_install_package.rb +++ b/lib/chef/resource/easy_install_package.rb @@ -22,13 +22,6 @@ class Chef class Resource class EasyInstallPackage < Chef::Resource::Package - provides :easy_install_package - - def initialize(name, run_context=nil) - super - @resource_name = :easy_install_package - end - def easy_install_binary(arg=nil) set_or_return( :easy_install_binary, diff --git a/lib/chef/resource/env.rb b/lib/chef/resource/env.rb index 2072ae5d80..025bfc72b7 100644 --- a/lib/chef/resource/env.rb +++ b/lib/chef/resource/env.rb @@ -27,14 +27,14 @@ class Chef provides :env, os: "windows" + default_action :create + allowed_actions :create, :delete, :modify + def initialize(name, run_context=nil) super - @resource_name = :env @key_name = name @value = nil - @action = :create @delim = nil - @allowed_actions.push(:create, :delete, :modify) end def key_name(arg=nil) diff --git a/lib/chef/resource/erl_call.rb b/lib/chef/resource/erl_call.rb index 24009d51c7..1976c54c45 100644 --- a/lib/chef/resource/erl_call.rb +++ b/lib/chef/resource/erl_call.rb @@ -28,18 +28,16 @@ class Chef identity_attr :code + default_action :run + def initialize(name, run_context=nil) super - @resource_name = :erl_call @code = "q()." # your erlang code goes here @cookie = nil # cookie of the erlang node @distributed = false # if you want to have a distributed erlang node @name_type = "sname" # type of erlang hostname name or sname @node_name = "chef@localhost" # the erlang node hostname - - @action = "run" - @allowed_actions.push(:run) end def code(arg=nil) diff --git a/lib/chef/resource/execute.rb b/lib/chef/resource/execute.rb index 9f8b629fb8..ec669a75d3 100644 --- a/lib/chef/resource/execute.rb +++ b/lib/chef/resource/execute.rb @@ -32,12 +32,12 @@ class Chef # Only execute resources (and subclasses) can be guard interpreters. attr_accessor :is_guard_interpreter + default_action :run + def initialize(name, run_context=nil) super - @resource_name = :execute @command = name @backup = 5 - @action = "run" @creates = nil @cwd = nil @environment = nil @@ -46,7 +46,6 @@ class Chef @returns = 0 @timeout = nil @user = nil - @allowed_actions.push(:run) @umask = nil @default_guard_interpreter = :execute @is_guard_interpreter = false diff --git a/lib/chef/resource/file.rb b/lib/chef/resource/file.rb index 53a6a160af..d278652cc3 100644 --- a/lib/chef/resource/file.rb +++ b/lib/chef/resource/file.rb @@ -38,15 +38,22 @@ class Chef attr_writer :checksum - provides :file + # + # The checksum of the rendered file. This has to be saved on the + # new_resource for the 'after' state for reporting but we cannot + # mutate the new_resource.checksum which would change the + # user intent in the new_resource if the resource is reused. + # + # @returns [String] Checksum of the file we actually rendered + attr_accessor :final_checksum + + default_action :create + allowed_actions :create, :delete, :touch, :create_if_missing def initialize(name, run_context=nil) super - @resource_name = :file @path = name @backup = 5 - @action = "create" - @allowed_actions.push(:create, :delete, :touch, :create_if_missing) @atomic_update = Chef::Config[:file_atomic_update] @force_unlink = false @manage_symlink_source = nil @@ -129,6 +136,15 @@ class Chef @verifications end end + + def state_for_resource_reporter + state_attrs = super() + # fix up checksum state with final_checksum saved by the provider + if checksum.nil? && final_checksum + state_attrs[:checksum] = final_checksum + end + state_attrs + end end end end diff --git a/lib/chef/resource/freebsd_package.rb b/lib/chef/resource/freebsd_package.rb index 9c8db506f8..c7c43450ba 100644 --- a/lib/chef/resource/freebsd_package.rb +++ b/lib/chef/resource/freebsd_package.rb @@ -31,11 +31,6 @@ class Chef provides :package, platform: "freebsd" - def initialize(name, run_context=nil) - super - @resource_name = :freebsd_package - end - def after_created assign_provider end diff --git a/lib/chef/resource/gem_package.rb b/lib/chef/resource/gem_package.rb index 0e838ca040..b981797876 100644 --- a/lib/chef/resource/gem_package.rb +++ b/lib/chef/resource/gem_package.rb @@ -22,11 +22,8 @@ class Chef class Resource class GemPackage < Chef::Resource::Package - provides :gem_package - def initialize(name, run_context=nil) super - @resource_name = :gem_package @clear_sources = false end diff --git a/lib/chef/resource/git.rb b/lib/chef/resource/git.rb index 7156873315..393a0689fe 100644 --- a/lib/chef/resource/git.rb +++ b/lib/chef/resource/git.rb @@ -22,11 +22,8 @@ class Chef class Resource class Git < Chef::Resource::Scm - provides :git - def initialize(name, run_context=nil) super - @resource_name = :git @additional_remotes = Hash[] end diff --git a/lib/chef/resource/group.rb b/lib/chef/resource/group.rb index 9e8f1309b0..2e80f32fea 100644 --- a/lib/chef/resource/group.rb +++ b/lib/chef/resource/group.rb @@ -25,19 +25,17 @@ class Chef state_attrs :members - provides :group + allowed_actions :create, :remove, :modify, :manage + default_action :create def initialize(name, run_context=nil) super - @resource_name = :group @group_name = name @gid = nil @members = [] @excluded_members = [] - @action = :create @append = false @non_unique = false - @allowed_actions.push(:create, :remove, :modify, :manage) end def group_name(arg=nil) diff --git a/lib/chef/resource/homebrew_package.rb b/lib/chef/resource/homebrew_package.rb index 73409b13ac..048ba6b3aa 100644 --- a/lib/chef/resource/homebrew_package.rb +++ b/lib/chef/resource/homebrew_package.rb @@ -25,12 +25,10 @@ class Chef class Resource class HomebrewPackage < Chef::Resource::Package - provides :homebrew_package provides :package, os: "darwin" def initialize(name, run_context=nil) super - @resource_name = :homebrew_package @homebrew_user = nil end diff --git a/lib/chef/resource/http_request.rb b/lib/chef/resource/http_request.rb index ccb0a26629..f9f056325a 100644 --- a/lib/chef/resource/http_request.rb +++ b/lib/chef/resource/http_request.rb @@ -26,14 +26,14 @@ class Chef identity_attr :url + default_action :get + allowed_actions :get, :put, :post, :delete, :head, :options + def initialize(name, run_context=nil) super - @resource_name = :http_request @message = name @url = nil - @action = :get @headers = {} - @allowed_actions.push(:get, :put, :post, :delete, :head, :options) end def url(args=nil) diff --git a/lib/chef/resource/ifconfig.rb b/lib/chef/resource/ifconfig.rb index c289ddadbe..527eb0e515 100644 --- a/lib/chef/resource/ifconfig.rb +++ b/lib/chef/resource/ifconfig.rb @@ -27,12 +27,12 @@ class Chef state_attrs :inet_addr, :mask + default_action :add + allowed_actions :add, :delete, :enable, :disable + def initialize(name, run_context=nil) super - @resource_name = :ifconfig @target = name - @action = :add - @allowed_actions.push(:add, :delete, :enable, :disable) @hwaddr = nil @mask = nil @inet_addr = nil @@ -145,5 +145,3 @@ class Chef end end - - diff --git a/lib/chef/resource/ips_package.rb b/lib/chef/resource/ips_package.rb index c0e699e31a..8d720dd411 100644 --- a/lib/chef/resource/ips_package.rb +++ b/lib/chef/resource/ips_package.rb @@ -25,10 +25,10 @@ class Chef provides :ips_package, os: "solaris2" + allowed_actions :install, :remove, :upgrade + def initialize(name, run_context = nil) super(name, run_context) - @resource_name = :ips_package - @allowed_actions.push(:install, :remove, :upgrade) @accept_license = false end diff --git a/lib/chef/resource/link.rb b/lib/chef/resource/link.rb index 30f8ec86d1..f932383cc1 100644 --- a/lib/chef/resource/link.rb +++ b/lib/chef/resource/link.rb @@ -25,21 +25,19 @@ class Chef class Link < Chef::Resource include Chef::Mixin::Securable - provides :link - identity_attr :target_file state_attrs :to, :owner, :group + default_action :create + allowed_actions :create, :delete + def initialize(name, run_context=nil) verify_links_supported! super - @resource_name = :link @to = nil - @action = :create @link_type = :symbolic @target_file = name - @allowed_actions.push(:create, :delete) end def to(arg=nil) diff --git a/lib/chef/resource/log.rb b/lib/chef/resource/log.rb index 7f970a87a4..9adffb26bb 100644 --- a/lib/chef/resource/log.rb +++ b/lib/chef/resource/log.rb @@ -26,6 +26,8 @@ class Chef identity_attr :message + default_action :write + # Sends a string from a recipe to a log provider # # log "some string to log" do @@ -48,10 +50,7 @@ class Chef # node<Chef::Node>:: Node where resource will be used def initialize(name, run_context=nil) super - @resource_name = :log @level = :info - @action = :write - @allowed_actions.push(:write) @message = name end @@ -75,5 +74,3 @@ class Chef end end end - - diff --git a/lib/chef/resource/lwrp_base.rb b/lib/chef/resource/lwrp_base.rb index ce72e98028..c486233020 100644 --- a/lib/chef/resource/lwrp_base.rb +++ b/lib/chef/resource/lwrp_base.rb @@ -19,6 +19,13 @@ # require 'chef/resource' +require 'chef/resource_resolver' +require 'chef/node' +require 'chef/log' +require 'chef/exceptions' +require 'chef/mixin/convert_to_class_name' +require 'chef/mixin/from_file' +require 'chef/mixin/params_validate' # for DelayedEvaluator class Chef class Resource @@ -28,138 +35,99 @@ class Chef # so attributes, default action, etc. can be defined with pleasing syntax. class LWRPBase < Resource - NULL_ARG = Object.new + # Class methods + class <<self - extend Chef::Mixin::ConvertToClassName - extend Chef::Mixin::FromFile + include Chef::Mixin::ConvertToClassName + include Chef::Mixin::FromFile - # Evaluates the LWRP resource file and instantiates a new Resource class. - def self.build_from_file(cookbook_name, filename, run_context) - resource_class = nil - rname = filename_to_qualified_string(cookbook_name, filename) + attr_accessor :loaded_lwrps - class_name = convert_to_class_name(rname) - if Resource.const_defined?(class_name, false) - Chef::Log.info("#{class_name} light-weight resource is already initialized -- Skipping loading #{filename}!") - Chef::Log.debug("Overriding already defined LWRPs is not supported anymore starting with Chef 12.") - resource_class = Resource.const_get(class_name) - else - resource_class = Class.new(self) + def build_from_file(cookbook_name, filename, run_context) + if LWRPBase.loaded_lwrps[filename] + Chef::Log.info("LWRP resource #{filename} from cookbook #{cookbook_name} has already been loaded! Skipping the reload.") + return loaded_lwrps[filename] + end - Chef::Resource.const_set(class_name, resource_class) - resource_class.resource_name = rname + resource_name = filename_to_qualified_string(cookbook_name, filename) + + # We load the class first to give it a chance to set its own name + resource_class = Class.new(self) + resource_class.resource_name resource_name.to_sym resource_class.run_context = run_context resource_class.class_from_file(filename) - Chef::Log.debug("Loaded contents of #{filename} into a resource named #{rname} defined in Chef::Resource::#{class_name}") - end + # Make a useful string for the class (rather than <Class:312894723894>) + resource_class.instance_eval do + define_singleton_method(:to_s) do + "LWRP resource #{resource_name} from cookbook #{cookbook_name}" + end + define_singleton_method(:inspect) { to_s } + end - resource_class - end + Chef::Log.debug("Loaded contents of #{filename} into resource #{resource_name} (#{resource_class})") - # Set the resource name for this LWRP - def self.resource_name(arg = NULL_ARG) - if arg.equal?(NULL_ARG) - @resource_name - else - @resource_name = arg - end - end - - class << self - alias_method :resource_name=, :resource_name - end + LWRPBase.loaded_lwrps[filename] = true - # Define an attribute on this resource, including optional validation - # parameters. - def self.attribute(attr_name, validation_opts={}) - define_method(attr_name) do |arg=nil| - set_or_return(attr_name.to_sym, arg, validation_opts) + # Create the deprecated Chef::Resource::LwrpFoo class + Chef::Resource.register_deprecated_lwrp_class(resource_class, convert_to_class_name(resource_name)) + resource_class end - end - # Sets the default action - def self.default_action(action_name=NULL_ARG) - unless action_name.equal?(NULL_ARG) - @actions ||= [] - if action_name.is_a?(Array) - action = action_name.map { |arg| arg.to_sym } - @actions = actions | action - @default_action = action - else - action = action_name.to_sym - @actions.push(action) unless @actions.include?(action) - @default_action = action + # Define an attribute on this resource, including optional validation + # parameters. + def attribute(attr_name, validation_opts={}) + define_method(attr_name) do |arg=nil| + set_or_return(attr_name.to_sym, arg, validation_opts) end end - @default_action ||= from_superclass(:default_action) - end - - # Adds +action_names+ to the list of valid actions for this resource. - def self.actions(*action_names) - if action_names.empty? - defined?(@actions) ? @actions : from_superclass(:actions, []).dup - else - # BC-compat way for checking if actions have already been defined - if defined?(@actions) - @actions.push(*action_names) + # Adds +action_names+ to the list of valid actions for this resource. + # Does not include superclass's action list when appending. + def actions(*action_names) + if !action_names.empty? && !@allowed_actions + self.allowed_actions = action_names else - @actions = action_names + allowed_actions(*action_names) end end - end - - # @deprecated - def self.valid_actions(*args) - Chef::Log.warn("`valid_actions' is deprecated, please use actions `instead'!") - actions(*args) - end + alias :actions= :allowed_actions= - # Set the run context on the class. Used to provide access to the node - # during class definition. - def self.run_context=(run_context) - @run_context = run_context - end + # @deprecated + def valid_actions(*args) + Chef::Log.warn("`valid_actions' is deprecated, please use allowed_actions `instead'!") + allowed_actions(*args) + end - def self.run_context - @run_context - end + # Set the run context on the class. Used to provide access to the node + # during class definition. + attr_accessor :run_context - def self.node - run_context.node - end + def node + run_context ? run_context.node : nil + end - def self.lazy(&block) - DelayedEvaluator.new(&block) - end + def lazy(&block) + DelayedEvaluator.new(&block) + end - private + protected - # Get the value from the superclass, if it responds, otherwise return - # +nil+. Since class instance variables are **not** inherited upon - # subclassing, this is a required check to ensure Chef pulls the - # +default_action+ and other DSL-y methods when extending LWRP::Base. - def self.from_superclass(m, default = nil) - return default if superclass == Chef::Resource::LWRPBase - superclass.respond_to?(m) ? superclass.send(m) : default - end + def loaded_lwrps + @loaded_lwrps ||= {} + end - # Default initializer. Sets the default action and allowed actions. - def initialize(name, run_context=nil) - super(name, run_context) + private - # Raise an exception if the resource_name was not defined - if self.class.resource_name.nil? - raise Chef::Exceptions::InvalidResourceSpecification, - "You must specify `resource_name'!" + # Get the value from the superclass, if it responds, otherwise return + # +nil+. Since class instance variables are **not** inherited upon + # subclassing, this is a required check to ensure Chef pulls the + # +default_action+ and other DSL-y methods when extending LWRP::Base. + def from_superclass(m, default = nil) + return default if superclass == Chef::Resource::LWRPBase + superclass.respond_to?(m) ? superclass.send(m) : default end - - @resource_name = self.class.resource_name.to_sym - @action = self.class.default_action - allowed_actions.push(self.class.actions).flatten! end - end end end diff --git a/lib/chef/resource/macosx_service.rb b/lib/chef/resource/macosx_service.rb index 879ea99cf8..f1ed4051cb 100644 --- a/lib/chef/resource/macosx_service.rb +++ b/lib/chef/resource/macosx_service.rb @@ -22,8 +22,8 @@ class Chef class Resource class MacosxService < Chef::Resource::Service - provides :service, os: "darwin" provides :macosx_service, os: "darwin" + provides :service, os: "darwin" identity_attr :service_name @@ -31,7 +31,6 @@ class Chef def initialize(name, run_context=nil) super - @resource_name = :macosx_service @plist = nil @session_type = nil end diff --git a/lib/chef/resource/macports_package.rb b/lib/chef/resource/macports_package.rb index 0d4e5dec65..937839b6e1 100644 --- a/lib/chef/resource/macports_package.rb +++ b/lib/chef/resource/macports_package.rb @@ -19,14 +19,7 @@ class Chef class Resource class MacportsPackage < Chef::Resource::Package - - provides :macports_package provides :package, os: "darwin" - - def initialize(name, run_context=nil) - super - @resource_name = :macports_package - end end end end diff --git a/lib/chef/resource/mdadm.rb b/lib/chef/resource/mdadm.rb index 971b6c51b4..b789fab155 100644 --- a/lib/chef/resource/mdadm.rb +++ b/lib/chef/resource/mdadm.rb @@ -27,11 +27,11 @@ class Chef state_attrs :devices, :level, :chunk - provides :mdadm + default_action :create + allowed_actions :create, :assemble, :stop def initialize(name, run_context=nil) super - @resource_name = :mdadm @chunk = 16 @devices = [] @@ -40,9 +40,6 @@ class Chef @metadata = "0.90" @bitmap = nil @raid_device = name - - @action = :create - @allowed_actions.push(:create, :assemble, :stop) end def chunk(arg=nil) diff --git a/lib/chef/resource/mount.rb b/lib/chef/resource/mount.rb index 142dec87f7..79986d127f 100644 --- a/lib/chef/resource/mount.rb +++ b/lib/chef/resource/mount.rb @@ -27,11 +27,11 @@ class Chef state_attrs :mount_point, :device_type, :fstype, :username, :password, :domain - provides :mount + default_action :mount + allowed_actions :mount, :umount, :remount, :enable, :disable def initialize(name, run_context=nil) super - @resource_name = :mount @mount_point = name @device = nil @device_type = :device @@ -42,9 +42,7 @@ class Chef @pass = 2 @mounted = false @enabled = false - @action = :mount @supports = { :remount => false } - @allowed_actions.push(:mount, :umount, :remount, :enable, :disable) @username = nil @password = nil @domain = nil diff --git a/lib/chef/resource/ohai.rb b/lib/chef/resource/ohai.rb index b567db40f9..9425e55c0c 100644 --- a/lib/chef/resource/ohai.rb +++ b/lib/chef/resource/ohai.rb @@ -25,12 +25,11 @@ class Chef state_attrs :plugin + default_action :reload + def initialize(name, run_context=nil) super - @resource_name = :ohai @name = name - @allowed_actions.push(:reload) - @action = :reload @plugin = nil end diff --git a/lib/chef/resource/openbsd_package.rb b/lib/chef/resource/openbsd_package.rb index 20a2523e3a..f91fdb37e0 100644 --- a/lib/chef/resource/openbsd_package.rb +++ b/lib/chef/resource/openbsd_package.rb @@ -30,11 +30,6 @@ class Chef provides :package, os: "openbsd" - def initialize(name, run_context=nil) - super - @resource_name = :openbsd_package - end - def after_created assign_provider end @@ -48,4 +43,3 @@ class Chef end end end - diff --git a/lib/chef/resource/package.rb b/lib/chef/resource/package.rb index f4f49b543b..1c6da75678 100644 --- a/lib/chef/resource/package.rb +++ b/lib/chef/resource/package.rb @@ -22,24 +22,23 @@ require 'chef/resource' class Chef class Resource class Package < Chef::Resource - identity_attr :package_name state_attrs :version, :options + default_action :install + allowed_actions :install, :upgrade, :remove, :purge, :reconfig + def initialize(name, run_context=nil) super - @action = :install - @allowed_actions.push(:install, :upgrade, :remove, :purge, :reconfig) @candidate_version = nil @options = nil @package_name = name - @resource_name = :package @response_file = nil @response_file_variables = Hash.new @source = nil @version = nil - @timeout = 900 + @timeout = nil end def package_name(arg=nil) @@ -101,3 +100,8 @@ class Chef end end end + +require 'chef/chef_class' +require 'chef/resource/homebrew_package' + +Chef.set_resource_priority_array :package, Chef::Resource::HomebrewPackage, os: "darwin" diff --git a/lib/chef/resource/pacman_package.rb b/lib/chef/resource/pacman_package.rb index 4c45dd004f..54b8efc4c2 100644 --- a/lib/chef/resource/pacman_package.rb +++ b/lib/chef/resource/pacman_package.rb @@ -21,14 +21,7 @@ require 'chef/resource/package' class Chef class Resource class PacmanPackage < Chef::Resource::Package - provides :pacman_package, os: "linux" - - def initialize(name, run_context=nil) - super - @resource_name = :pacman_package - end - end end end diff --git a/lib/chef/resource/paludis_package.rb b/lib/chef/resource/paludis_package.rb index 552c96857a..56c47bc141 100644 --- a/lib/chef/resource/paludis_package.rb +++ b/lib/chef/resource/paludis_package.rb @@ -22,13 +22,12 @@ require 'chef/provider/package/paludis' class Chef class Resource class PaludisPackage < Chef::Resource::Package - provides :paludis_package, os: "linux" + allowed_actions :install, :remove, :upgrade + def initialize(name, run_context=nil) super(name, run_context) - @resource_name = :paludis_package - @allowed_actions.push(:install, :remove, :upgrade) @timeout = 3600 end end diff --git a/lib/chef/resource/perl.rb b/lib/chef/resource/perl.rb index c4bdb6e130..773eba6571 100644 --- a/lib/chef/resource/perl.rb +++ b/lib/chef/resource/perl.rb @@ -22,10 +22,8 @@ require 'chef/provider/script' class Chef class Resource class Perl < Chef::Resource::Script - def initialize(name, run_context=nil) super - @resource_name = :perl @interpreter = "perl" end diff --git a/lib/chef/resource/portage_package.rb b/lib/chef/resource/portage_package.rb index 42c03560b6..1af48702fa 100644 --- a/lib/chef/resource/portage_package.rb +++ b/lib/chef/resource/portage_package.rb @@ -21,10 +21,8 @@ require 'chef/resource/package' class Chef class Resource class PortagePackage < Chef::Resource::Package - def initialize(name, run_context=nil) super - @resource_name = :portage_package @provider = Chef::Provider::Package::Portage end diff --git a/lib/chef/resource/powershell_script.rb b/lib/chef/resource/powershell_script.rb index 43aafe4df2..7d432883e4 100644 --- a/lib/chef/resource/powershell_script.rb +++ b/lib/chef/resource/powershell_script.rb @@ -20,11 +20,10 @@ require 'chef/resource/windows_script' class Chef class Resource class PowershellScript < Chef::Resource::WindowsScript - provides :powershell_script, os: "windows" def initialize(name, run_context=nil) - super(name, run_context, :powershell_script, "powershell.exe") + super(name, run_context, nil, "powershell.exe") @convert_boolean_return = false end diff --git a/lib/chef/resource/python.rb b/lib/chef/resource/python.rb index b1f23d13ce..432ee46b85 100644 --- a/lib/chef/resource/python.rb +++ b/lib/chef/resource/python.rb @@ -21,10 +21,8 @@ require 'chef/provider/script' class Chef class Resource class Python < Chef::Resource::Script - def initialize(name, run_context=nil) super - @resource_name = :python @interpreter = "python" end diff --git a/lib/chef/resource/reboot.rb b/lib/chef/resource/reboot.rb index c111b23d2e..401f2f338f 100644 --- a/lib/chef/resource/reboot.rb +++ b/lib/chef/resource/reboot.rb @@ -24,11 +24,11 @@ require 'chef/resource' class Chef class Resource class Reboot < Chef::Resource + allowed_actions :request_reboot, :reboot_now, :cancel + def initialize(name, run_context=nil) super - @resource_name = :reboot @provider = Chef::Provider::Reboot - @allowed_actions.push(:request_reboot, :reboot_now, :cancel) @reason = "Reboot by Chef" @delay_mins = 0 diff --git a/lib/chef/resource/registry_key.rb b/lib/chef/resource/registry_key.rb index 8126ccf126..4ed0d4a4e0 100644 --- a/lib/chef/resource/registry_key.rb +++ b/lib/chef/resource/registry_key.rb @@ -22,10 +22,12 @@ require 'chef/digester' class Chef class Resource class RegistryKey < Chef::Resource - identity_attr :key state_attrs :values + default_action :create + allowed_actions :create, :create_if_missing, :delete, :delete_key + # Some registry key data types may not be safely reported as json. # Example (CHEF-5323): # @@ -59,13 +61,10 @@ class Chef def initialize(name, run_context=nil) super - @resource_name = :registry_key - @action = :create @architecture = :machine @recursive = false @key = name @values, @unscrubbed_values = [], [] - @allowed_actions.push(:create, :create_if_missing, :delete, :delete_key) end def key(arg=nil) diff --git a/lib/chef/resource/remote_directory.rb b/lib/chef/resource/remote_directory.rb index d4108da47a..b731f7b201 100644 --- a/lib/chef/resource/remote_directory.rb +++ b/lib/chef/resource/remote_directory.rb @@ -26,19 +26,18 @@ class Chef class RemoteDirectory < Chef::Resource::Directory include Chef::Mixin::Securable - provides :remote_directory - identity_attr :path state_attrs :files_owner, :files_group, :files_mode + default_action :create + allowed_actions :create, :create_if_missing, :delete + def initialize(name, run_context=nil) super - @resource_name = :remote_directory @path = name @source = ::File.basename(name) @delete = false - @action = :create @recursive = true @purge = false @files_backup = 5 @@ -46,7 +45,6 @@ class Chef @files_group = nil @files_mode = 0644 unless Chef::Platform.windows? @overwrite = true - @allowed_actions.push(:create, :create_if_missing, :delete) @cookbook = nil end diff --git a/lib/chef/resource/remote_file.rb b/lib/chef/resource/remote_file.rb index e56f69941d..b7a553cbe8 100644 --- a/lib/chef/resource/remote_file.rb +++ b/lib/chef/resource/remote_file.rb @@ -21,18 +21,15 @@ require 'uri' require 'chef/resource/file' require 'chef/provider/remote_file' require 'chef/mixin/securable' +require 'chef/mixin/uris' class Chef class Resource class RemoteFile < Chef::Resource::File include Chef::Mixin::Securable - provides :remote_file - def initialize(name, run_context=nil) super - @resource_name = :remote_file - @action = "create" @source = [] @use_etag = true @use_last_modified = true @@ -127,6 +124,8 @@ class Chef private + include Chef::Mixin::Uris + def validate_source(source) source = Array(source).flatten raise ArgumentError, "#{resource_name} has an empty source" if source.empty? @@ -140,7 +139,7 @@ class Chef end def absolute_uri?(source) - source.kind_of?(String) and URI.parse(source).absolute? + Chef::Provider::RemoteFile::Fetcher.network_share?(source) or (source.kind_of?(String) and as_uri(source).absolute?) rescue URI::InvalidURIError false end diff --git a/lib/chef/resource/route.rb b/lib/chef/resource/route.rb index 942905d138..3ba8f6215b 100644 --- a/lib/chef/resource/route.rb +++ b/lib/chef/resource/route.rb @@ -22,17 +22,16 @@ require 'chef/resource' class Chef class Resource class Route < Chef::Resource - identity_attr :target state_attrs :netmask, :gateway + default_action :add + allowed_actions :add, :delete + def initialize(name, run_context=nil) super - @resource_name = :route @target = name - @action = [:add] - @allowed_actions.push(:add, :delete) @netmask = nil @gateway = nil @metric = nil @@ -136,5 +135,3 @@ class Chef end end end - - diff --git a/lib/chef/resource/rpm_package.rb b/lib/chef/resource/rpm_package.rb index f00121dd69..b8b5144a42 100644 --- a/lib/chef/resource/rpm_package.rb +++ b/lib/chef/resource/rpm_package.rb @@ -22,12 +22,10 @@ require 'chef/provider/package/rpm' class Chef class Resource class RpmPackage < Chef::Resource::Package - provides :rpm_package, os: [ "linux", "aix" ] def initialize(name, run_context=nil) super - @resource_name = :rpm_package @allow_downgrade = false end diff --git a/lib/chef/resource/ruby.rb b/lib/chef/resource/ruby.rb index 2b2aa0249d..3c3909043d 100644 --- a/lib/chef/resource/ruby.rb +++ b/lib/chef/resource/ruby.rb @@ -22,13 +22,10 @@ require 'chef/provider/script' class Chef class Resource class Ruby < Chef::Resource::Script - def initialize(name, run_context=nil) super - @resource_name = :ruby @interpreter = "ruby" end - end end end diff --git a/lib/chef/resource/ruby_block.rb b/lib/chef/resource/ruby_block.rb index a9cbf234cf..ae8e4cb7cd 100644 --- a/lib/chef/resource/ruby_block.rb +++ b/lib/chef/resource/ruby_block.rb @@ -23,14 +23,13 @@ require 'chef/provider/ruby_block' class Chef class Resource class RubyBlock < Chef::Resource + default_action :run + allowed_actions :create, :run identity_attr :block_name def initialize(name, run_context=nil) super - @resource_name = :ruby_block - @action = "run" - @allowed_actions << :create << :run @block_name = name end diff --git a/lib/chef/resource/scm.rb b/lib/chef/resource/scm.rb index 87c217b4cc..85028c266b 100644 --- a/lib/chef/resource/scm.rb +++ b/lib/chef/resource/scm.rb @@ -22,23 +22,22 @@ require 'chef/resource' class Chef class Resource class Scm < Chef::Resource - identity_attr :destination state_attrs :revision + default_action :sync + allowed_actions :checkout, :export, :sync, :diff, :log + def initialize(name, run_context=nil) super @destination = name - @resource_name = :scm @enable_submodules = false @enable_checkout = true @revision = "HEAD" @remote = "origin" @ssh_wrapper = nil @depth = nil - @allowed_actions.push(:checkout, :export, :sync, :diff, :log) - @action = [:sync] @checkout_branch = "deploy" @environment = nil end diff --git a/lib/chef/resource/script.rb b/lib/chef/resource/script.rb index fd0fd5a7fd..30bed367cb 100644 --- a/lib/chef/resource/script.rb +++ b/lib/chef/resource/script.rb @@ -23,13 +23,11 @@ require 'chef/provider/script' class Chef class Resource class Script < Chef::Resource::Execute - # Chef-13: go back to using :name as the identity attr identity_attr :command def initialize(name, run_context=nil) super - @resource_name = :script # Chef-13: the command variable should be initialized to nil @command = name @code = nil diff --git a/lib/chef/resource/service.rb b/lib/chef/resource/service.rb index 36df7c859a..aa59b543be 100644 --- a/lib/chef/resource/service.rb +++ b/lib/chef/resource/service.rb @@ -22,14 +22,15 @@ require 'chef/resource' class Chef class Resource class Service < Chef::Resource - identity_attr :service_name state_attrs :enabled, :running + default_action :nothing + allowed_actions :enable, :disable, :start, :stop, :restart, :reload + def initialize(name, run_context=nil) super - @resource_name = :service @service_name = name @enabled = nil @running = nil @@ -43,9 +44,7 @@ class Chef @init_command = nil @priority = nil @timeout = nil - @action = "nothing" @supports = { :restart => false, :reload => false, :status => false } - @allowed_actions.push(:enable, :disable, :start, :stop, :restart, :reload) end def service_name(arg=nil) diff --git a/lib/chef/resource/smartos_package.rb b/lib/chef/resource/smartos_package.rb index 99b3b953b0..b8bd940c24 100644 --- a/lib/chef/resource/smartos_package.rb +++ b/lib/chef/resource/smartos_package.rb @@ -22,16 +22,7 @@ require 'chef/provider/package/smartos' class Chef class Resource class SmartosPackage < Chef::Resource::Package - - provides :smartos_package provides :package, os: "solaris2", platform_family: "smartos" - - def initialize(name, run_context=nil) - super - @resource_name = :smartos_package - end - end end end - diff --git a/lib/chef/resource/solaris_package.rb b/lib/chef/resource/solaris_package.rb index 94be4693b6..2dc72d5c47 100644 --- a/lib/chef/resource/solaris_package.rb +++ b/lib/chef/resource/solaris_package.rb @@ -23,21 +23,11 @@ require 'chef/provider/package/solaris' class Chef class Resource class SolarisPackage < Chef::Resource::Package - - provides :solaris_package provides :package, os: "solaris2", platform_family: "nexentacore" provides :package, os: "solaris2", platform_family: "solaris2" do |node| # on >= Solaris 11 we default to IPS packages instead node[:platform_version].to_f <= 5.10 end - - def initialize(name, run_context=nil) - super - @resource_name = :solaris_package - end - end end end - - diff --git a/lib/chef/resource/subversion.rb b/lib/chef/resource/subversion.rb index 3afbe0baaf..ae6a37caa2 100644 --- a/lib/chef/resource/subversion.rb +++ b/lib/chef/resource/subversion.rb @@ -22,13 +22,12 @@ require "chef/resource/scm" class Chef class Resource class Subversion < Chef::Resource::Scm + allowed_actions :force_export def initialize(name, run_context=nil) super @svn_arguments = '--no-auth-cache' @svn_info_args = '--no-auth-cache' - @resource_name = :subversion - allowed_actions << :force_export end # Override exception to strip password if any, so it won't appear in logs and different Chef notifications diff --git a/lib/chef/resource/template.rb b/lib/chef/resource/template.rb index 67a9e6a418..5a7f7efd6f 100644 --- a/lib/chef/resource/template.rb +++ b/lib/chef/resource/template.rb @@ -27,15 +27,11 @@ class Chef class Template < Chef::Resource::File include Chef::Mixin::Securable - provides :template - attr_reader :inline_helper_blocks attr_reader :inline_helper_modules def initialize(name, run_context=nil) super - @resource_name = :template - @action = "create" @source = "#{::File.basename(name)}.erb" @cookbook = nil @local = false diff --git a/lib/chef/resource/timestamped_deploy.rb b/lib/chef/resource/timestamped_deploy.rb index b2109db85c..344f8b0a5e 100644 --- a/lib/chef/resource/timestamped_deploy.rb +++ b/lib/chef/resource/timestamped_deploy.rb @@ -21,10 +21,6 @@ class Chef # Convenience class for using the deploy resource with the timestamped # deployment strategy (provider) class TimestampedDeploy < Chef::Resource::Deploy - provides :timestamped_deploy - def initialize(*args, &block) - super(*args, &block) - end end end end diff --git a/lib/chef/resource/user.rb b/lib/chef/resource/user.rb index 7d2ec25596..b85b648c92 100644 --- a/lib/chef/resource/user.rb +++ b/lib/chef/resource/user.rb @@ -21,16 +21,15 @@ require 'chef/resource' class Chef class Resource class User < Chef::Resource - identity_attr :username state_attrs :uid, :gid, :home - provides :user + default_action :create + allowed_actions :create, :remove, :modify, :manage, :lock, :unlock def initialize(name, run_context=nil) super - @resource_name = :user @username = name @comment = nil @uid = nil @@ -42,14 +41,12 @@ class Chef @manage_home = false @force = false @non_unique = false - @action = :create @supports = { :manage_home => false, :non_unique => false } @iterations = 27855 @salt = nil - @allowed_actions.push(:create, :remove, :modify, :manage, :lock, :unlock) end def username(arg=nil) diff --git a/lib/chef/resource/whyrun_safe_ruby_block.rb b/lib/chef/resource/whyrun_safe_ruby_block.rb index 6fa5383f5d..f289f15001 100644 --- a/lib/chef/resource/whyrun_safe_ruby_block.rb +++ b/lib/chef/resource/whyrun_safe_ruby_block.rb @@ -19,12 +19,6 @@ class Chef class Resource class WhyrunSafeRubyBlock < Chef::Resource::RubyBlock - - def initialize(name, run_context=nil) - super - @resource_name = :whyrun_safe_ruby_block - end - end end end diff --git a/lib/chef/resource/windows_package.rb b/lib/chef/resource/windows_package.rb index 16cfcf865e..a76765cc36 100644 --- a/lib/chef/resource/windows_package.rb +++ b/lib/chef/resource/windows_package.rb @@ -16,6 +16,7 @@ # limitations under the License. # +require 'chef/mixin/uris' require 'chef/resource/package' require 'chef/provider/package/windows' require 'chef/win32/error' if RUBY_PLATFORM =~ /mswin|mingw|windows/ @@ -23,14 +24,15 @@ require 'chef/win32/error' if RUBY_PLATFORM =~ /mswin|mingw|windows/ class Chef class Resource class WindowsPackage < Chef::Resource::Package + include Chef::Mixin::Uris - provides :package, os: "windows" provides :windows_package, os: "windows" + provides :package, os: "windows" + + allowed_actions :install, :remove def initialize(name, run_context=nil) super - @allowed_actions.push(:install, :remove) - @resource_name = :windows_package @source ||= source(@package_name) # Unique to this resource @@ -69,10 +71,30 @@ class Chef @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) + if uri_scheme?(arg) + @source = arg + else + @source = Chef::Util::PathHelper.canonical_path(arg, false) + end end end + + def checksum(arg=nil) + set_or_return( + :checksum, + arg, + :kind_of => [ String ] + ) + end + + def remote_file_attributes(arg=nil) + set_or_return( + :remote_file_attributes, + arg, + :kind_of => [ Hash ] + ) + end + end end end diff --git a/lib/chef/resource/windows_script.rb b/lib/chef/resource/windows_script.rb index 6b0827b77c..48e2b535a8 100644 --- a/lib/chef/resource/windows_script.rb +++ b/lib/chef/resource/windows_script.rb @@ -22,6 +22,7 @@ require 'chef/mixin/windows_architecture_helper' class Chef class Resource class WindowsScript < Chef::Resource::Script + # This is an abstract resource meant to be subclasses; thus no 'provides' set_guard_inherited_attributes(:architecture) @@ -30,8 +31,8 @@ class Chef def initialize(name, run_context, resource_name, interpreter_command) super(name, run_context) @interpreter = interpreter_command - @resource_name = resource_name - @default_guard_interpreter = resource_name + @resource_name = resource_name if resource_name + @default_guard_interpreter = self.resource_name end include Chef::Mixin::WindowsArchitectureHelper diff --git a/lib/chef/resource/windows_service.rb b/lib/chef/resource/windows_service.rb index 8090adceb0..a77690652e 100644 --- a/lib/chef/resource/windows_service.rb +++ b/lib/chef/resource/windows_service.rb @@ -25,8 +25,10 @@ class Chef # Until #1773 is resolved, you need to manually specify the windows_service resource # to use action :configure_startup and attribute startup_type - provides :service, os: "windows" provides :windows_service, os: "windows" + provides :service, os: "windows" + + allowed_actions :configure_startup identity_attr :service_name @@ -34,8 +36,6 @@ class Chef def initialize(name, run_context=nil) super - @resource_name = :windows_service - @allowed_actions.push(:configure_startup) @startup_type = :automatic @run_as_user = "" @run_as_password = "" diff --git a/lib/chef/resource/yum_package.rb b/lib/chef/resource/yum_package.rb index 8fbca9b097..4d54f6051f 100644 --- a/lib/chef/resource/yum_package.rb +++ b/lib/chef/resource/yum_package.rb @@ -22,13 +22,10 @@ require 'chef/provider/package/yum' class Chef class Resource class YumPackage < Chef::Resource::Package - - provides :yum_package provides :package, os: "linux", platform_family: [ "rhel", "fedora" ] def initialize(name, run_context=nil) super - @resource_name = :yum_package @flush_cache = { :before => false, :after => false } @allow_downgrade = false end @@ -38,7 +35,7 @@ class Chef set_or_return( :arch, arg, - :kind_of => [ String ] + :kind_of => [ String, Array ] ) end diff --git a/lib/chef/resource/zypper_package.rb b/lib/chef/resource/zypper_package.rb new file mode 100644 index 0000000000..f09a20e2c6 --- /dev/null +++ b/lib/chef/resource/zypper_package.rb @@ -0,0 +1,27 @@ +# +# Author:: Joe Williams (<joe@joetify.com>) +# Copyright:: Copyright (c) 2009 Joe Williams +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'chef/resource/package' + +class Chef + class Resource + class ZypperPackage < Chef::Resource::Package + provides :package, platform_family: "suse" + end + end +end diff --git a/lib/chef/resource_builder.rb b/lib/chef/resource_builder.rb index bb0962d128..9e9f7047a4 100644 --- a/lib/chef/resource_builder.rb +++ b/lib/chef/resource_builder.rb @@ -18,6 +18,10 @@ # NOTE: this was extracted from the Recipe DSL mixin, relevant specs are in spec/unit/recipe_spec.rb +require 'chef/exceptions' +require 'chef/resource' +require 'chef/log' + class Chef class ResourceBuilder attr_reader :type @@ -46,6 +50,9 @@ class Chef raise ArgumentError, "You must supply a name when declaring a #{type} resource" if name.nil? @resource = resource_class.new(name, run_context) + if resource.resource_name.nil? + raise Chef::Exceptions::InvalidResourceSpecification, "#{resource}.resource_name is `nil`! Did you forget to put `provides :blah` or `resource_name :blah` in your resource class?" + end resource.source_line = created_at resource.declared_type = type diff --git a/lib/chef/resource_definition.rb b/lib/chef/resource_definition.rb index 9d6844129c..cffabb6786 100644 --- a/lib/chef/resource_definition.rb +++ b/lib/chef/resource_definition.rb @@ -50,6 +50,7 @@ class Chef else raise ArgumentError, "You must pass a block to a definition." end + Chef::DSL::Definitions.add_definition(name) true end diff --git a/lib/chef/resource_reporter.rb b/lib/chef/resource_reporter.rb index 1816fc857d..7d13a5a5ce 100644 --- a/lib/chef/resource_reporter.rb +++ b/lib/chef/resource_reporter.rb @@ -59,11 +59,11 @@ class Chef # attrs. def for_json as_hash = {} - as_hash["type"] = new_resource.class.dsl_name + as_hash["type"] = new_resource.resource_name.to_sym as_hash["name"] = new_resource.name.to_s as_hash["id"] = new_resource.identity.to_s - as_hash["after"] = state(new_resource) - as_hash["before"] = current_resource ? state(current_resource) : {} + as_hash["after"] = new_resource.state_for_resource_reporter + as_hash["before"] = current_resource ? current_resource.state_for_resource_reporter : {} as_hash["duration"] = (elapsed_time * 1000).to_i.to_s as_hash["delta"] = new_resource.diff if new_resource.respond_to?("diff") as_hash["delta"] = "" if as_hash["delta"].nil? @@ -89,13 +89,6 @@ class Chef def success? !self.exception end - - def state(r) - r.class.state_attrs.inject({}) do |state_attrs, attr_name| - state_attrs[attr_name] = r.send(attr_name) - state_attrs - end - end end # End class ResouceReport attr_reader :updated_resources @@ -220,7 +213,7 @@ class Chef # If we failed before we received the run_started callback, there's not much we can do # in terms of reporting if @run_status - post_reporting_data + post_reporting_data end end diff --git a/lib/chef/resource_resolver.rb b/lib/chef/resource_resolver.rb index ff9d7aeb9f..31b39f7e24 100644 --- a/lib/chef/resource_resolver.rb +++ b/lib/chef/resource_resolver.rb @@ -21,81 +21,137 @@ require 'chef/platform/resource_priority_map' class Chef class ResourceResolver + # + # Resolve a resource by name. + # + # @param resource_name [Symbol] The resource DSL name (e.g. `:file`). + # @param node [Chef::Node] The node against which to resolve. `nil` causes + # platform filters to be ignored. + # + def self.resolve(resource_name, node: nil, canonical: nil) + new(node, resource_name, canonical: canonical).resolve + end + + # + # Resolve a list of all resources that implement the given DSL (in order of + # preference). + # + # @param resource_name [Symbol] The resource DSL name (e.g. `:file`). + # @param node [Chef::Node] The node against which to resolve. `nil` causes + # platform filters to be ignored. + # @param canonical [Boolean] `true` or `false` to match canonical or + # non-canonical values only. `nil` to ignore canonicality. + # + def self.list(resource_name, node: nil, canonical: nil) + new(node, resource_name, canonical: canonical).list + end + + include Chef::Mixin::ConvertToClassName + + # @api private attr_reader :node - attr_reader :resource + # @api private + attr_reader :resource_name + # @api private + def resource + Chef::Log.deprecation("Chef::ResourceResolver.resource deprecated. Use resource_name instead.") + resource_name + end + # @api private attr_reader :action - - def initialize(node, resource) + # @api private + attr_reader :canonical + + # + # Create a resolver. + # + # @param node [Chef::Node] The node against which to resolve. `nil` causes + # platform filters to be ignored. + # @param resource_name [Symbol] The resource DSL name (e.g. `:file`). + # @param canonical [Boolean] `true` or `false` to match canonical or + # non-canonical values only. `nil` to ignore canonicality. Default: `nil` + # + # @api private use Chef::ResourceResolver.resolve or .list instead. + def initialize(node, resource_name, canonical: nil) @node = node - @resource = resource - end - - # return a deterministically sorted list of Chef::Resource subclasses - def resources - @resources ||= Chef::Resource.descendants + @resource_name = resource_name.to_sym + @canonical = canonical end + # @api private use Chef::ResourceResolver.resolve instead. 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 + # 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_name} resource enabled on node include: #{prioritized_handlers}" - private + handler = prioritized_handlers.first - # 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 ] + if handler + Chef::Log.debug "Resource for #{resource_name} is #{handler}" + else + Chef::Log.debug "Dynamic resource resolver FAILED to resolve a resource for #{resource_name}" end - Chef::Log.debug "resources that survived replacement include: #{handlers}" + handler + end - raise Chef::Exceptions::AmbiguousResourceResolution.new(resource, handlers) if handlers.count >= 2 + # @api private + def list + Chef::Log.debug "Resources for generic #{resource_name} resource enabled on node include: #{prioritized_handlers}" + prioritized_handlers + end - Chef::Log.debug "dynamic resource resolver FAILED to resolve a resouce" if handlers.empty? + # + # Whether this DSL is provided by the given resource_class. + # + # @api private + def provided_by?(resource_class) + !prioritized_handlers.include?(resource_class) + end - return nil if handlers.empty? + protected - handlers[0] + def priority_map + Chef::Platform::ResourcePriorityMap.instance 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) + def prioritized_handlers + @prioritized_handlers ||= + priority_map.list_handlers(node, resource_name, canonical: canonical) end - # dep injection hooks - def get_priority_array(node, resource_name) - resource_priority_map.get_priority_array(node, resource_name) - end + module Deprecated + # return a deterministically sorted list of Chef::Resource subclasses + # @deprecated Now prioritized_handlers does its own work (more efficiently) + def resources + Chef::Resource.sorted_descendants + end - def resource_priority_map - Chef::Platform::ResourcePriorityMap.instance + # A list of all handlers + # @deprecated Now prioritized_handlers does its own work + def enabled_handlers + Chef::Log.deprecation("enabled_handlers is deprecated. If you are implementing a ResourceResolver, use provided_handlers. If you are not, use Chef::ResourceResolver.list(#{resource_name.inspect}, node: <node>)") + resources.select { |klass| klass.provides?(node, resource_name) } + end + + protected + + # A list of all handlers for the given DSL. If there are no handlers in + # the map, we still check all descendants of Chef::Resource for backwards + # compatibility purposes. + def prioritized_handlers + @prioritized_handlers ||= super || + resources.select do |klass| + # Don't bother calling provides? unless it's overridden. We already + # know prioritized_handlers + if klass.method(:provides?).owner != Chef::Resource && klass.provides?(node, resource_name) + Chef::Log.deprecation("Resources #{provided.join(", ")} are marked as providing DSL #{resource_name}, but provides #{resource_name.inspect} was never called!") + Chef::Log.deprecation("In Chef 13, this will break: you must call provides to mark the names you provide, even if you also override provides? yourself.") + true + end + end + end end + prepend Deprecated end end diff --git a/lib/chef/resources.rb b/lib/chef/resources.rb index 40b12a7c5f..af4dd8a642 100644 --- a/lib/chef/resources.rb +++ b/lib/chef/resources.rb @@ -80,6 +80,7 @@ require 'chef/resource/windows_package' require 'chef/resource/yum_package' require 'chef/resource/lwrp_base' require 'chef/resource/bff_package' +require 'chef/resource/zypper_package' begin # Optional resources chef_node, chef_client, machine, machine_image, etc. diff --git a/lib/chef/rest.rb b/lib/chef/rest.rb index 2612714a19..f87cec9b76 100644 --- a/lib/chef/rest.rb +++ b/lib/chef/rest.rb @@ -64,6 +64,7 @@ class Chef options = options.dup options[:client_name] = client_name options[:signing_key_filename] = signing_key_filename + super(url, options) @decompressor = Decompressor.new(options) diff --git a/lib/chef/run_context.rb b/lib/chef/run_context.rb index 4f0215bfd4..44b05f0cc0 100644 --- a/lib/chef/run_context.rb +++ b/lib/chef/run_context.rb @@ -86,6 +86,7 @@ class Chef @reboot_info = {} @node.run_context = self + @node.set_cookbook_attribute @cookbook_compiler = nil end diff --git a/lib/chef/run_list/versioned_recipe_list.rb b/lib/chef/run_list/versioned_recipe_list.rb index 0eefded964..7cce6fa48c 100644 --- a/lib/chef/run_list/versioned_recipe_list.rb +++ b/lib/chef/run_list/versioned_recipe_list.rb @@ -63,6 +63,24 @@ class Chef end end end + + # Get an array of strings of the fully-qualified recipe names (with ::default appended) and + # with the versions in "NAME@VERSION" format. + # + # @return [Array] Array of strings with fully-qualified recipe names + def with_fully_qualified_names_and_version_constraints + self.map do |recipe_name| + ret = if recipe_name.include?('::') + recipe_name + else + "#{recipe_name}::default" + end + if @versions[recipe_name] + ret << "@#{@versions[recipe_name]}" + end + ret + end + end end end end diff --git a/lib/chef/run_status.rb b/lib/chef/run_status.rb index 0f181426b0..ce8ff296f4 100644 --- a/lib/chef/run_status.rb +++ b/lib/chef/run_status.rb @@ -39,15 +39,13 @@ class Chef::RunStatus attr_accessor :run_id + attr_accessor :node + def initialize(node, events) @node = node @events = events end - def node - @node - end - # sets +start_time+ to the current time. def start_clock @start_time = Time.now diff --git a/lib/chef/server_api.rb b/lib/chef/server_api.rb index ec4a864cb3..764296f8c8 100644 --- a/lib/chef/server_api.rb +++ b/lib/chef/server_api.rb @@ -42,3 +42,5 @@ class Chef use Chef::HTTP::RemoteRequestID end end + +require 'chef/config' diff --git a/lib/chef/shell.rb b/lib/chef/shell.rb index ee4fe78808..3a68785ce9 100644 --- a/lib/chef/shell.rb +++ b/lib/chef/shell.rb @@ -110,7 +110,7 @@ module Shell conf.prompt_c = "chef#{leader(m)} > " conf.return_format = " => %s \n" - conf.prompt_i = "chef#{leader(m)} > " + conf.prompt_i = "chef#{leader(m)} (#{Chef::VERSION})> " conf.prompt_n = "chef#{leader(m)} ?> " conf.prompt_s = "chef#{leader(m)}%l> " conf.use_tracer = false diff --git a/lib/chef/user.rb b/lib/chef/user.rb index 42fa6b5fa1..717deb63c3 100644 --- a/lib/chef/user.rb +++ b/lib/chef/user.rb @@ -21,29 +21,85 @@ require 'chef/mixin/from_file' require 'chef/mash' require 'chef/json_compat' require 'chef/search/query' +require 'chef/mixin/api_version_request_handling' +require 'chef/exceptions' +require 'chef/server_api' +# OSC 11 BACKWARDS COMPATIBILITY NOTE (remove after OSC 11 support ends) +# +# In general, Chef::User is no longer expected to support Open Source Chef 11 Server requests. +# The object that handles those requests has been moved to the Chef::OscUser namespace. +# +# Exception: self.list is backwards compatible with OSC 11 class Chef class User include Chef::Mixin::FromFile include Chef::Mixin::ParamsValidate + include Chef::Mixin::ApiVersionRequestHandling + + SUPPORTED_API_VERSIONS = [0,1] def initialize - @name = '' + @username = nil + @display_name = nil + @first_name = nil + @middle_name = nil + @last_name = nil + @email = nil + @password = nil @public_key = nil @private_key = nil + @create_key = nil @password = nil - @admin = false end - def name(arg=nil) - set_or_return(:name, arg, + def chef_root_rest_v0 + @chef_root_rest_v0 ||= Chef::ServerAPI.new(Chef::Config[:chef_server_root], {:api_version => "0"}) + end + + def chef_root_rest_v1 + @chef_root_rest_v1 ||= Chef::ServerAPI.new(Chef::Config[:chef_server_root], {:api_version => "1"}) + end + + def username(arg=nil) + set_or_return(:username, arg, :regex => /^[a-z0-9\-_]+$/) end - def admin(arg=nil) - set_or_return(:admin, - arg, :kind_of => [TrueClass, FalseClass]) + def display_name(arg=nil) + set_or_return(:display_name, + arg, :kind_of => String) + end + + def first_name(arg=nil) + set_or_return(:first_name, + arg, :kind_of => String) + end + + def middle_name(arg=nil) + set_or_return(:middle_name, + arg, :kind_of => String) + end + + def last_name(arg=nil) + set_or_return(:last_name, + arg, :kind_of => String) + end + + def email(arg=nil) + set_or_return(:email, + arg, :kind_of => String) + end + + def password(arg=nil) + set_or_return(:password, + arg, :kind_of => String) + end + + def create_key(arg=nil) + set_or_return(:create_key, arg, + :kind_of => [TrueClass, FalseClass]) end def public_key(arg=nil) @@ -63,12 +119,17 @@ class Chef def to_hash result = { - "name" => @name, - "public_key" => @public_key, - "admin" => @admin + "username" => @username } - result["private_key"] = @private_key if @private_key - result["password"] = @password if @password + result["display_name"] = @display_name unless @display_name.nil? + result["first_name"] = @first_name unless @first_name.nil? + result["middle_name"] = @middle_name unless @middle_name.nil? + result["last_name"] = @last_name unless @last_name.nil? + result["email"] = @email unless @email.nil? + result["password"] = @password unless @password.nil? + result["public_key"] = @public_key unless @public_key.nil? + result["private_key"] = @private_key unless @private_key.nil? + result["create_key"] = @create_key unless @create_key.nil? result end @@ -77,21 +138,86 @@ class Chef end def destroy - Chef::REST.new(Chef::Config[:chef_server_url]).delete_rest("users/#{@name}") + # will default to the current API version (Chef::Authenticator::DEFAULT_SERVER_API_VERSION) + Chef::REST.new(Chef::Config[:chef_server_url]).delete("users/#{@username}") end def create - payload = {:name => self.name, :admin => self.admin, :password => self.password } - payload[:public_key] = public_key if public_key - new_user =Chef::REST.new(Chef::Config[:chef_server_url]).post_rest("users", payload) + # try v1, fail back to v0 if v1 not supported + begin + payload = { + :username => @username, + :display_name => @display_name, + :first_name => @first_name, + :last_name => @last_name, + :email => @email, + :password => @password + } + payload[:public_key] = @public_key unless @public_key.nil? + payload[:create_key] = @create_key unless @create_key.nil? + payload[:middle_name] = @middle_name unless @middle_name.nil? + raise Chef::Exceptions::InvalidUserAttribute, "You cannot set both public_key and create_key for create." if !@create_key.nil? && !@public_key.nil? + new_user = chef_root_rest_v1.post("users", payload) + + # get the private_key out of the chef_key hash if it exists + if new_user['chef_key'] + if new_user['chef_key']['private_key'] + new_user['private_key'] = new_user['chef_key']['private_key'] + end + new_user['public_key'] = new_user['chef_key']['public_key'] + new_user.delete('chef_key') + end + rescue Net::HTTPServerException => e + # rescue API V0 if 406 and the server supports V0 + supported_versions = server_client_api_version_intersection(e, SUPPORTED_API_VERSIONS) + raise e unless supported_versions && supported_versions.include?(0) + payload = { + :username => @username, + :display_name => @display_name, + :first_name => @first_name, + :last_name => @last_name, + :email => @email, + :password => @password + } + payload[:middle_name] = @middle_name unless @middle_name.nil? + payload[:public_key] = @public_key unless @public_key.nil? + # under API V0, the server will create a key pair if public_key isn't passed + new_user = chef_root_rest_v0.post("users", payload) + end + Chef::User.from_hash(self.to_hash.merge(new_user)) end def update(new_key=false) - payload = {:name => name, :admin => admin} - payload[:private_key] = new_key if new_key - payload[:password] = password if password - updated_user = Chef::REST.new(Chef::Config[:chef_server_url]).put_rest("users/#{name}", payload) + begin + payload = {:username => username} + payload[:display_name] = display_name unless display_name.nil? + payload[:first_name] = first_name unless first_name.nil? + payload[:middle_name] = middle_name unless middle_name.nil? + payload[:last_name] = last_name unless last_name.nil? + payload[:email] = email unless email.nil? + payload[:password] = password unless password.nil? + + # API V1 will fail if these key fields are defined, and try V0 below if relevant 400 is returned + payload[:public_key] = public_key unless public_key.nil? + payload[:private_key] = new_key if new_key + + updated_user = chef_root_rest_v1.put("users/#{username}", payload) + rescue Net::HTTPServerException => e + if e.response.code == "400" + # if a 400 is returned but the error message matches the error related to private / public key fields, try V0 + # else, raise the 400 + error = Chef::JSONCompat.from_json(e.response.body)["error"].first + error_match = /Since Server API v1, all keys must be updated via the keys endpoint/.match(error) + if error_match.nil? + raise e + end + else # for other types of errors, test for API versioning errors right away + supported_versions = server_client_api_version_intersection(e, SUPPORTED_API_VERSIONS) + raise e unless supported_versions && supported_versions.include?(0) + end + updated_user = chef_root_rest_v0.put("users/#{username}", payload) + end Chef::User.from_hash(self.to_hash.merge(updated_user)) end @@ -107,31 +233,47 @@ class Chef end end + # Note: remove after API v0 no longer supported by client (and knife command). def reregister - r = Chef::REST.new(Chef::Config[:chef_server_url]) - reregistered_self = r.put_rest("users/#{name}", { :name => name, :admin => admin, :private_key => true }) - private_key(reregistered_self["private_key"]) + begin + payload = self.to_hash.merge({"private_key" => true}) + reregistered_self = chef_root_rest_v0.put("users/#{username}", payload) + private_key(reregistered_self["private_key"]) + # only V0 supported for reregister + rescue Net::HTTPServerException => e + # if there was a 406 related to versioning, give error explaining that + # only API version 0 is supported for reregister command + if e.response.code == "406" && e.response["x-ops-server-api-version"] + version_header = Chef::JSONCompat.from_json(e.response["x-ops-server-api-version"]) + min_version = version_header["min_version"] + max_version = version_header["max_version"] + error_msg = reregister_only_v0_supported_error_msg(max_version, min_version) + raise Chef::Exceptions::OnlyApiVersion0SupportedForAction.new(error_msg) + else + raise e + end + end self end def to_s - "user[#{@name}]" - end - - def inspect - "Chef::User name:'#{name}' admin:'#{admin.inspect}'" + - "public_key:'#{public_key}' private_key:#{private_key}" + "user[#{@username}]" end # Class Methods def self.from_hash(user_hash) user = Chef::User.new - user.name user_hash['name'] - user.private_key user_hash['private_key'] if user_hash.key?('private_key') + user.username user_hash['username'] + user.display_name user_hash['display_name'] if user_hash.key?('display_name') + user.first_name user_hash['first_name'] if user_hash.key?('first_name') + user.middle_name user_hash['middle_name'] if user_hash.key?('middle_name') + user.last_name user_hash['last_name'] if user_hash.key?('last_name') + user.email user_hash['email'] if user_hash.key?('email') user.password user_hash['password'] if user_hash.key?('password') - user.public_key user_hash['public_key'] - user.admin user_hash['admin'] + user.public_key user_hash['public_key'] if user_hash.key?('public_key') + user.private_key user_hash['private_key'] if user_hash.key?('private_key') + user.create_key user_hash['create_key'] if user_hash.key?('create_key') user end @@ -144,12 +286,19 @@ class Chef end def self.list(inflate=false) - response = Chef::REST.new(Chef::Config[:chef_server_url]).get_rest('users') + response = Chef::REST.new(Chef::Config[:chef_server_url]).get('users') users = if response.is_a?(Array) - transform_ohc_list_response(response) # OHC/OPC - else - response # OSC - end + # EC 11 / CS 12 V0, V1 + # GET /organizations/<org>/users + transform_list_response(response) + else + # OSC 11 + # GET /users + # EC 11 / CS 12 V0, V1 + # GET /users + response # OSC + end + if inflate users.inject({}) do |user_map, (name, _url)| user_map[name] = Chef::User.load(name) @@ -160,8 +309,9 @@ class Chef end end - def self.load(name) - response = Chef::REST.new(Chef::Config[:chef_server_url]).get_rest("users/#{name}") + def self.load(username) + # will default to the current API version (Chef::Authenticator::DEFAULT_SERVER_API_VERSION) + response = Chef::REST.new(Chef::Config[:chef_server_url]).get("users/#{username}") Chef::User.from_hash(response) end @@ -169,7 +319,7 @@ class Chef # [ { "user" => { "username" => USERNAME }}, ...] # into the form # { "USERNAME" => "URI" } - def self.transform_ohc_list_response(response) + def self.transform_list_response(response) new_response = Hash.new response.each do |u| name = u['user']['username'] @@ -178,6 +328,7 @@ class Chef new_response end - private_class_method :transform_ohc_list_response + private_class_method :transform_list_response + end end diff --git a/lib/chef/util/backup.rb b/lib/chef/util/backup.rb index 0cc009ca1f..6c95cedad7 100644 --- a/lib/chef/util/backup.rb +++ b/lib/chef/util/backup.rb @@ -78,8 +78,16 @@ class Chef Chef::Log.info("#{@new_resource} removed backup at #{backup_file}") end + def unsorted_backup_files + # If you replace this with Dir[], you will probably break Windows. + fn = Regexp.escape(::File.basename(path)) + Dir.entries(::File.dirname(backup_path)).select do |f| + !!(f =~ /\A#{fn}.chef-[0-9.]*\B/) + end.map {|f| ::File.join(::File.dirname(backup_path), f)} + end + def sorted_backup_files - Dir[Chef::Util::PathHelper.escape_glob(prefix, ".#{path}") + ".chef-*"].sort { |a,b| b <=> a } + unsorted_backup_files.sort { |a,b| b <=> a } end end end diff --git a/lib/chef/util/path_helper.rb b/lib/chef/util/path_helper.rb index 66c2e3f19f..9ebc9319b8 100644 --- a/lib/chef/util/path_helper.rb +++ b/lib/chef/util/path_helper.rb @@ -16,212 +16,10 @@ # limitations under the License. # +require 'chef-config/path_helper' + class Chef class Util - class PathHelper - # Maximum characters in a standard Windows path (260 including drive letter and NUL) - WIN_MAX_PATH = 259 - - def self.dirname(path) - if Chef::Platform.windows? - # Find the first slash, not counting trailing slashes - end_slash = path.size - loop do - slash = path.rindex(/[#{Regexp.escape(File::SEPARATOR)}#{Regexp.escape(path_separator)}]/, end_slash - 1) - if !slash - return end_slash == path.size ? '.' : path_separator - elsif slash == end_slash - 1 - end_slash = slash - else - return path[0..slash-1] - end - end - else - ::File.dirname(path) - end - end - - BACKSLASH = '\\'.freeze - - def self.path_separator - if Chef::Platform.windows? - File::ALT_SEPARATOR || BACKSLASH - else - File::SEPARATOR - end - end - - def self.join(*args) - args.flatten.inject do |joined_path, component| - # Joined path ends with / - joined_path = joined_path.sub(/[#{Regexp.escape(File::SEPARATOR)}#{Regexp.escape(path_separator)}]+$/, '') - component = component.sub(/^[#{Regexp.escape(File::SEPARATOR)}#{Regexp.escape(path_separator)}]+/, '') - joined_path += "#{path_separator}#{component}" - end - end - - def self.validate_path(path) - if Chef::Platform.windows? - unless printable?(path) - msg = "Path '#{path}' contains non-printable characters. Check that backslashes are escaped with another backslash (e.g. C:\\\\Windows) in double-quoted strings." - Chef::Log.error(msg) - raise Chef::Exceptions::ValidationFailed, msg - end - - if windows_max_length_exceeded?(path) - Chef::Log.debug("Path '#{path}' is longer than #{WIN_MAX_PATH}, prefixing with'\\\\?\\'") - path.insert(0, "\\\\?\\") - end - end - - path - end - - def self.windows_max_length_exceeded?(path) - # Check to see if paths without the \\?\ prefix are over the maximum allowed length for the Windows API - # http://msdn.microsoft.com/en-us/library/windows/desktop/aa365247%28v=vs.85%29.aspx - unless path =~ /^\\\\?\\/ - if path.length > WIN_MAX_PATH - return true - end - end - - false - end - - def self.printable?(string) - # returns true if string is free of non-printable characters (escape sequences) - # this returns false for whitespace escape sequences as well, e.g. \n\t - if string =~ /[^[:print:]]/ - false - else - true - end - end - - # Produces a comparable path. - def self.canonical_path(path, add_prefix=true) - # First remove extra separators and resolve any relative paths - abs_path = File.absolute_path(path) - - if Chef::Platform.windows? - # Add the \\?\ API prefix on Windows unless add_prefix is false - # Downcase on Windows where paths are still case-insensitive - abs_path.gsub!(::File::SEPARATOR, path_separator) - if add_prefix && abs_path !~ /^\\\\?\\/ - abs_path.insert(0, "\\\\?\\") - end - - abs_path.downcase! - end - - abs_path - end - - def self.cleanpath(path) - path = Pathname.new(path).cleanpath.to_s - # ensure all forward slashes are backslashes - if Chef::Platform.windows? - path = path.gsub(File::SEPARATOR, path_separator) - end - path - end - - def self.paths_eql?(path1, path2) - canonical_path(path1) == canonical_path(path2) - end - - # Paths which may contain glob-reserved characters need - # to be escaped before globbing can be done. - # http://stackoverflow.com/questions/14127343 - def self.escape_glob(*parts) - path = cleanpath(join(*parts)) - path.gsub(/[\\\{\}\[\]\*\?]/) { |x| "\\"+x } - end - - def self.relative_path_from(from, to) - pathname = Pathname.new(Chef::Util::PathHelper.cleanpath(to)).relative_path_from(Pathname.new(Chef::Util::PathHelper.cleanpath(from))) - end - - # Retrieves the "home directory" of the current user while trying to ascertain the existence - # of said directory. The path returned uses / for all separators (the ruby standard format). - # If the home directory doesn't exist or an error is otherwise encountered, nil is returned. - # - # If a set of path elements is provided, they are appended as-is to the home path if the - # homepath exists. - # - # If an optional block is provided, the joined path is passed to that block if the home path is - # valid and the result of the block is returned instead. - # - # Home-path discovery is performed once. If a path is discovered, that value is memoized so - # that subsequent calls to home_dir don't bounce around. - # - # See self.all_homes. - def self.home(*args) - @@home_dir ||= self.all_homes { |p| break p } - if @@home_dir - path = File.join(@@home_dir, *args) - block_given? ? (yield path) : path - end - end - - # See self.home. This method performs a similar operation except that it yields all the different - # possible values of 'HOME' that one could have on this platform. Hence, on windows, if - # HOMEDRIVE\HOMEPATH and USERPROFILE are different, the provided block will be called twice. - # This method goes out and checks the existence of each location at the time of the call. - # - # The return is a list of all the returned values from each block invocation or a list of paths - # if no block is provided. - def self.all_homes(*args) - paths = [] - if Chef::Platform.windows? - # By default, Ruby uses the the following environment variables to determine Dir.home: - # HOME - # HOMEDRIVE HOMEPATH - # USERPROFILE - # Ruby only checks to see if the variable is specified - not if the directory actually exists. - # On Windows, HOMEDRIVE HOMEPATH can point to a different location (such as an unavailable network mounted drive) - # while USERPROFILE points to the location where the user application settings and profile are stored. HOME - # is not defined as an environment variable (usually). If the home path actually uses UNC, then the prefix is - # HOMESHARE instead of HOMEDRIVE. - # - # We instead walk down the following and only include paths that actually exist. - # HOME - # HOMEDRIVE HOMEPATH - # HOMESHARE HOMEPATH - # USERPROFILE - - paths << ENV['HOME'] - paths << ENV['HOMEDRIVE'] + ENV['HOMEPATH'] if ENV['HOMEDRIVE'] && ENV['HOMEPATH'] - paths << ENV['HOMESHARE'] + ENV['HOMEPATH'] if ENV['HOMESHARE'] && ENV['HOMEPATH'] - paths << ENV['USERPROFILE'] - end - paths << Dir.home if ENV['HOME'] - - # Depending on what environment variables we're using, the slashes can go in any which way. - # Just change them all to / to keep things consistent. - # Note: Maybe this is a bad idea on some unixy systems where \ might be a valid character depending on - # the particular brand of kool-aid you consume. This code assumes that \ and / are both - # path separators on any system being used. - paths = paths.map { |home_path| home_path.gsub(path_separator, ::File::SEPARATOR) if home_path } - - # Filter out duplicate paths and paths that don't exist. - valid_paths = paths.select { |home_path| home_path && Dir.exists?(home_path) } - valid_paths = valid_paths.uniq - - # Join all optional path elements at the end. - # If a block is provided, invoke it - otherwise just return what we've got. - joined_paths = valid_paths.map { |home_path| File.join(home_path, *args) } - if block_given? - joined_paths.each { |p| yield p } - else - joined_paths - end - end - end + PathHelper = ChefConfig::PathHelper end end - -# Break a require loop when require chef/util/path_helper -require 'chef/platform' -require 'chef/exceptions' diff --git a/lib/chef/util/windows/net_user.rb b/lib/chef/util/windows/net_user.rb index 5df1a8aaa4..26fbe53db6 100644 --- a/lib/chef/util/windows/net_user.rb +++ b/lib/chef/util/windows/net_user.rb @@ -18,98 +18,69 @@ require 'chef/util/windows' require 'chef/exceptions' +require 'chef/win32/net' +require 'chef/win32/security' #wrapper around a subset of the NetUser* APIs. #nothing Chef specific, but not complete enough to be its own gem, so util for now. class Chef::Util::Windows::NetUser < Chef::Util::Windows private - - LogonUser = Windows::API.new('LogonUser', 'SSSLLP', 'I', 'advapi32') - - DOMAIN_GROUP_RID_USERS = 0x00000201 - - UF_SCRIPT = 0x000001 - UF_ACCOUNTDISABLE = 0x000002 - UF_PASSWD_CANT_CHANGE = 0x000040 - UF_NORMAL_ACCOUNT = 0x000200 - UF_DONT_EXPIRE_PASSWD = 0x010000 - - #[:symbol_name, default_val] - #default_val duals as field type - #array index duals as structure offset - - #OC-8391 - #Changing [:password, nil], to [:password, ""], - #if :password is set to nil, windows user creation api ignores the password policy applied - #thus initializing it with empty string value. - USER_INFO_3 = [ - [:name, nil], - [:password, ""], - [:password_age, 0], - [:priv, 0], #"The NetUserAdd and NetUserSetInfo functions ignore this member" - [:home_dir, nil], - [:comment, nil], - [:flags, UF_SCRIPT|UF_DONT_EXPIRE_PASSWD|UF_NORMAL_ACCOUNT], - [:script_path, nil], - [:auth_flags, 0], - [:full_name, nil], - [:user_comment, nil], - [:parms, nil], - [:workstations, nil], - [:last_logon, 0], - [:last_logoff, 0], - [:acct_expires, -1], - [:max_storage, -1], - [:units_per_week, 0], - [:logon_hours, nil], - [:bad_pw_count, 0], - [:num_logons, 0], - [:logon_server, nil], - [:country_code, 0], - [:code_page, 0], - [:user_id, 0], - [:primary_group_id, DOMAIN_GROUP_RID_USERS], - [:profile, nil], - [:home_dir_drive, nil], - [:password_expired, 0] - ] - - USER_INFO_3_TEMPLATE = - USER_INFO_3.collect { |field| field[1].class == Fixnum ? 'i' : 'L' }.join - - SIZEOF_USER_INFO_3 = #sizeof(USER_INFO_3) - USER_INFO_3.inject(0){|sum,item| - sum + (item[1].class == Fixnum ? 4 : PTR_SIZE) - } - - def user_info_3(args) - USER_INFO_3.collect { |field| - args.include?(field[0]) ? args[field[0]] : field[1] - } - end - - def user_info_3_pack(user) - user.collect { |v| - v.class == Fixnum ? v : str_to_ptr(multi_to_wide(v)) - }.pack(USER_INFO_3_TEMPLATE) + NetUser = Chef::ReservedNames::Win32::NetUser + Security = Chef::ReservedNames::Win32::Security + + USER_INFO_3_TRANSFORM = { + name: :usri3_name, + password: :usri3_password, + password_age: :usri3_password_age, + priv: :usri3_priv, + home_dir: :usri3_home_dir, + comment: :usri3_comment, + flags: :usri3_flags, + script_path: :usri3_script_path, + auth_flags: :usri3_auth_flags, + full_name: :usri3_full_name, + user_comment: :usri3_usr_comment, + parms: :usri3_parms, + workstations: :usri3_workstations, + last_logon: :usri3_last_logon, + last_logoff: :usri3_last_logoff, + acct_expires: :usri3_acct_expires, + max_storage: :usri3_max_storage, + units_per_week: :usri3_units_per_week, + logon_hours: :usri3_logon_hours, + bad_pw_count: :usri3_bad_pw_count, + num_logons: :usri3_num_logons, + logon_server: :usri3_logon_server, + country_code: :usri3_country_code, + code_page: :usri3_code_page, + user_id: :usri3_user_id, + primary_group_id: :usri3_primary_group_id, + profile: :usri3_profile, + home_dir_drive: :usri3_home_dir_drive, + password_expired: :usri3_password_expired, + } + + def transform_usri3(args) + args.inject({}) do |memo, (k,v)| + memo[USER_INFO_3_TRANSFORM[k]] = v + memo + end end - def user_info_3_unpack(buffer) - user = Hash.new - USER_INFO_3.each_with_index do |field,offset| - user[field[0]] = field[1].class == Fixnum ? - dword_to_i(buffer, offset) : lpwstr_to_s(buffer, offset) + def usri3_to_hash(usri3) + t = USER_INFO_3_TRANSFORM.invert + usri3.inject({}) do |memo, (k,v)| + memo[t[k]] = v + memo end - user end def set_info(args) - user = user_info_3(args) - buffer = user_info_3_pack(user) - rc = NetUserSetInfo.call(nil, @name, 3, buffer, nil) - if rc != NERR_Success - raise ArgumentError, get_last_error(rc) + begin + rc = NetUser::net_user_set_info_l3(nil, @username, transform_usri3(args)) + rescue Chef::Exceptions::Win32APIError => e + raise ArgumentError, e end end @@ -120,49 +91,32 @@ class Chef::Util::Windows::NetUser < Chef::Util::Windows @name = multi_to_wide(username) end - LOGON32_PROVIDER_DEFAULT = 0 - LOGON32_LOGON_NETWORK = 3 + LOGON32_PROVIDER_DEFAULT = Security::LOGON32_PROVIDER_DEFAULT + LOGON32_LOGON_NETWORK = Security::LOGON32_LOGON_NETWORK #XXX for an extra painful alternative, see: http://support.microsoft.com/kb/180548 def validate_credentials(passwd) - token = 0.chr * PTR_SIZE - res = LogonUser.call(@username, nil, passwd, - LOGON32_LOGON_NETWORK, LOGON32_PROVIDER_DEFAULT, token) - if res == 0 + begin + token = Security::logon_user(@username, nil, passwd, + LOGON32_LOGON_NETWORK, LOGON32_PROVIDER_DEFAULT) + return true + rescue Chef::Exceptions::Win32APIError return false end - ::Windows::Handle::CloseHandle.call(token.unpack('L')[0]) - return true end def get_info - ptr = 0.chr * PTR_SIZE - rc = NetUserGetInfo.call(nil, @name, 3, ptr) - - if rc == NERR_UserNotFound - raise Chef::Exceptions::UserIDNotFound, get_last_error(rc) - elsif rc != NERR_Success - raise ArgumentError, get_last_error(rc) + begin + ui3 = NetUser::net_user_get_info_l3(nil, @username) + rescue Chef::Exceptions::Win32APIError => e + raise ArgumentError, e end - - ptr = ptr.unpack('L')[0] - buffer = 0.chr * SIZEOF_USER_INFO_3 - memcpy(buffer, ptr, buffer.size) - NetApiBufferFree(ptr) - user_info_3_unpack(buffer) + usri3_to_hash(ui3) end def add(args) - user = user_info_3(args) - buffer = user_info_3_pack(user) - - rc = NetUserAdd.call(nil, 3, buffer, rc) - if rc != NERR_Success - raise ArgumentError, get_last_error(rc) - end - - #usri3_primary_group_id: - #"When you call the NetUserAdd function, this member must be DOMAIN_GROUP_RID_USERS" - NetLocalGroupAddMembers(nil, multi_to_wide("Users"), 3, buffer[0,PTR_SIZE], 1) + transformed_args = transform_usri3(args) + NetUser::net_user_add_l3(nil, transformed_args) + NetUser::net_local_group_add_member(nil, "Users", args[:name]) end def user_modify(&proc) @@ -182,15 +136,16 @@ class Chef::Util::Windows::NetUser < Chef::Util::Windows end def delete - rc = NetUserDel.call(nil, @name) - if rc != NERR_Success - raise ArgumentError, get_last_error(rc) + begin + NetUser::net_user_del(nil, @username) + rescue Chef::Exceptions::Win32APIError => e + raise ArgumentError, e end end def disable_account user_modify do |user| - user[:flags] |= UF_ACCOUNTDISABLE + user[:flags] |= NetUser::UF_ACCOUNTDISABLE #This does not set the password to nil. It (for some reason) means to ignore updating the field. #See similar behavior for the logon_hours field documented at #http://msdn.microsoft.com/en-us/library/windows/desktop/aa371338%28v=vs.85%29.aspx @@ -200,7 +155,7 @@ class Chef::Util::Windows::NetUser < Chef::Util::Windows def enable_account user_modify do |user| - user[:flags] &= ~UF_ACCOUNTDISABLE + user[:flags] &= ~NetUser::UF_ACCOUNTDISABLE #This does not set the password to nil. It (for some reason) means to ignore updating the field. #See similar behavior for the logon_hours field documented at #http://msdn.microsoft.com/en-us/library/windows/desktop/aa371338%28v=vs.85%29.aspx @@ -209,6 +164,6 @@ class Chef::Util::Windows::NetUser < Chef::Util::Windows end def check_enabled - (get_info()[:flags] & UF_ACCOUNTDISABLE) != 0 + (get_info()[:flags] & NetUser::UF_ACCOUNTDISABLE) != 0 end end diff --git a/lib/chef/version.rb b/lib/chef/version.rb index 30ede54095..f4cf71c002 100644 --- a/lib/chef/version.rb +++ b/lib/chef/version.rb @@ -1,6 +1,4 @@ - -# Author:: Daniel DeLeo (<dan@opscode.com>) -# Copyright:: Copyright (c) 2010-2011 Opscode, Inc. +# Copyright:: Copyright (c) 2010-2015 Chef Software, Inc. # License:: Apache License, Version 2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -15,9 +13,15 @@ # See the License for the specific language governing permissions and # limitations under the License. +#!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! +# NOTE: This file is generated by running `rake version` in the top level of +# this repo. Do not edit this manually. Edit the VERSION file and run the rake +# task instead. +#!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + class Chef CHEF_ROOT = File.dirname(File.expand_path(File.dirname(__FILE__))) - VERSION = '12.4.0.dev.0' + VERSION = '12.4.0.rc.0' end # diff --git a/lib/chef/win32/api.rb b/lib/chef/win32/api.rb index efa632f454..e9d273808a 100644 --- a/lib/chef/win32/api.rb +++ b/lib/chef/win32/api.rb @@ -67,7 +67,7 @@ class Chef # BaseTsd.h: #ifdef (_WIN64) host.typedef int HALF_PTR; #else host.typedef short HALF_PTR; host.typedef :ulong, :HACCEL # (L) Handle to an accelerator table. WinDef.h: #host.typedef HANDLE HACCEL; # See http://msdn.microsoft.com/en-us/library/ms645526%28VS.85%29.aspx - host.typedef :ulong, :HANDLE # (L) Handle to an object. WinNT.h: #host.typedef PVOID HANDLE; + host.typedef :size_t, :HANDLE # (L) Handle to an object. WinNT.h: #host.typedef PVOID HANDLE; # todo: Platform-dependent! Need to change to :uint64 for Win64 host.typedef :ulong, :HBITMAP # (L) Handle to a bitmap: http://msdn.microsoft.com/en-us/library/dd183377%28VS.85%29.aspx host.typedef :ulong, :HBRUSH # (L) Handle to a brush. http://msdn.microsoft.com/en-us/library/dd183394%28VS.85%29.aspx @@ -117,6 +117,7 @@ class Chef host.typedef :uint32, :LCID # Locale identifier. For more information, see Locales. host.typedef :uint32, :LCTYPE # Locale information type. For a list, see Locale Information Constants. host.typedef :uint32, :LGRPID # Language group identifier. For a list, see EnumLanguageGroupLocales. + host.typedef :pointer, :LMSTR # Pointer to null termiated string of unicode characters host.typedef :long, :LONG # 32-bit signed integer. The range is -2,147,483,648 through +...647 decimal. host.typedef :int32, :LONG32 # 32-bit signed integer. The range is -2,147,483,648 through +...647 decimal. host.typedef :int64, :LONG64 # 64-bit signed integer. The range is –9,223,372,036,854,775,808 through +...807 diff --git a/lib/chef/win32/api/installer.rb b/lib/chef/win32/api/installer.rb index 6864a26e7d..b4851eccf1 100644 --- a/lib/chef/win32/api/installer.rb +++ b/lib/chef/win32/api/installer.rb @@ -158,7 +158,7 @@ UINT MsiCloseHandle( raise Chef::Exceptions::Package, msg end - version + version.chomp(0.chr) end end end diff --git a/lib/chef/win32/api/net.rb b/lib/chef/win32/api/net.rb index eeb2b078a4..72caf46628 100644 --- a/lib/chef/win32/api/net.rb +++ b/lib/chef/win32/api/net.rb @@ -32,8 +32,24 @@ class Chef MAX_PREFERRED_LENGTH = 0xFFFF - NERR_Success = 0 - NERR_UserNotFound = 2221 + DOMAIN_GROUP_RID_USERS = 0x00000201 + + UF_SCRIPT = 0x000001 + UF_ACCOUNTDISABLE = 0x000002 + UF_PASSWD_CANT_CHANGE = 0x000040 + UF_NORMAL_ACCOUNT = 0x000200 + UF_DONT_EXPIRE_PASSWD = 0x010000 + + NERR_Success = 0 + NERR_InvalidComputer = 2351 + NERR_NotPrimary = 2226 + NERR_SpeGroupOp = 2234 + NERR_LastAdmin = 2452 + NERR_BadUsername = 2202 + NERR_BadPassword = 2203 + NERR_PasswordTooShort = 2245 + NERR_UserNotFound = 2221 + ERROR_ACCESS_DENIED = 5 ffi_lib "netapi32" @@ -67,6 +83,57 @@ class Chef :usri3_profile, :LPWSTR, :usri3_home_dir_drive, :LPWSTR, :usri3_password_expired, :DWORD + + def set(key, val) + val = if val.is_a? String + encoded = if val.encoding == Encoding::UTF_16LE + val + else + val.to_wstring + end + FFI::MemoryPointer.from_string(encoded) + else + val + end + self[key] = val + end + + def get(key) + if respond_to? key + send(key) + else + val = self[key] + if val.is_a? FFI::Pointer + if val.null? + nil + else + val.read_wstring + end + else + val + end + end + end + + def usri3_logon_hours + val = self[:usri3_logon_hours] + if !val.nil? && !val.null? + val.read_bytes(21) + else + nil + end + end + + def as_ruby + members.inject({}) do |memo, key| + memo[key] = get(key) + memo + end + end + end + + class LOCALGROUP_MEMBERS_INFO_3 < FFI::Struct + layout :lgrmi3_domainandname, :LPWSTR end # NET_API_STATUS NetUserEnum( @@ -85,6 +152,52 @@ class Chef # _In_ LPVOID Buffer # ); safe_attach_function :NetApiBufferFree, [ :LPVOID ], :DWORD + +#NET_API_STATUS NetUserAdd( + #_In_ LMSTR servername, + #_In_ DWORD level, + #_In_ LPBYTE buf, + #_Out_ LPDWORD parm_err +#); + safe_attach_function :NetUserAdd, [:LMSTR, :DWORD, :LPBYTE, :LPDWORD ], :DWORD + +#NET_API_STATUS NetLocalGroupAddMembers( +# _In_ LPCWSTR servername, +# _In_ LPCWSTR groupname, +# _In_ DWORD level, +# _In_ LPBYTE buf, +# _In_ DWORD totalentries +#); + safe_attach_function :NetLocalGroupAddMembers, [:LPCWSTR, :LPCWSTR, :DWORD, :LPBYTE, :DWORD ], :DWORD + +#NET_API_STATUS NetUserGetInfo( +# _In_ LPCWSTR servername, +# _In_ LPCWSTR username, +# _In_ DWORD level, +# _Out_ LPBYTE *bufptr +#); + safe_attach_function :NetUserGetInfo, [:LPCWSTR, :LPCWSTR, :DWORD, :LPBYTE], :DWORD + +#NET_API_STATUS NetApiBufferFree( +# _In_ LPVOID Buffer +#); + safe_attach_function :NetApiBufferFree, [:LPVOID], :DWORD + +#NET_API_STATUS NetUserSetInfo( +# _In_ LPCWSTR servername, +# _In_ LPCWSTR username, +# _In_ DWORD level, +# _In_ LPBYTE buf, +# _Out_ LPDWORD parm_err +#); + safe_attach_function :NetUserSetInfo, [:LPCWSTR, :LPCWSTR, :DWORD, :LPBYTE, :LPDWORD], :DWORD + +#NET_API_STATUS NetUserDel( +# _In_ LPCWSTR servername, +# _In_ LPCWSTR username +#); + safe_attach_function :NetUserDel, [:LPCWSTR, :LPCWSTR], :DWORD + end end end diff --git a/lib/chef/win32/api/security.rb b/lib/chef/win32/api/security.rb index 229f2ace10..4c352a3554 100644 --- a/lib/chef/win32/api/security.rb +++ b/lib/chef/win32/api/security.rb @@ -193,6 +193,20 @@ class Chef MAXDWORD = 0xffffffff + # LOGON32 constants for LogonUser + LOGON32_LOGON_INTERACTIVE = 2; + LOGON32_LOGON_NETWORK = 3; + LOGON32_LOGON_BATCH = 4; + LOGON32_LOGON_SERVICE = 5; + LOGON32_LOGON_UNLOCK = 7; + LOGON32_LOGON_NETWORK_CLEARTEXT = 8; + LOGON32_LOGON_NEW_CREDENTIALS = 9; + + LOGON32_PROVIDER_DEFAULT = 0; + LOGON32_PROVIDER_WINNT35 = 1; + LOGON32_PROVIDER_WINNT40 = 2; + LOGON32_PROVIDER_WINNT50 = 3; + ############################################### # Win32 API Bindings ############################################### @@ -270,6 +284,14 @@ class Chef :MaxTokenInfoClass ] + class TOKEN_OWNER < FFI::Struct + layout :Owner, :pointer + end + + class TOKEN_PRIMARY_GROUP < FFI::Struct + layout :PrimaryGroup, :pointer + end + # https://msdn.microsoft.com/en-us/library/windows/desktop/aa379572%28v=vs.85%29.aspx SECURITY_IMPERSONATION_LEVEL = enum :SECURITY_IMPERSONATION_LEVEL, [ :SecurityAnonymous, @@ -405,6 +427,8 @@ class Chef safe_attach_function :SetSecurityDescriptorOwner, [ :pointer, :pointer, :BOOL ], :BOOL safe_attach_function :SetSecurityDescriptorSacl, [ :pointer, :BOOL, :pointer, :BOOL ], :BOOL safe_attach_function :GetTokenInformation, [ :HANDLE, :TOKEN_INFORMATION_CLASS, :pointer, :DWORD, :PDWORD ], :BOOL + safe_attach_function :LogonUserW, [:LPTSTR, :LPTSTR, :LPTSTR, :DWORD, :DWORD, :PHANDLE], :BOOL + end end end diff --git a/lib/chef/win32/api/unicode.rb b/lib/chef/win32/api/unicode.rb index 0b2cb09a6b..2e3a599f0a 100644 --- a/lib/chef/win32/api/unicode.rb +++ b/lib/chef/win32/api/unicode.rb @@ -139,7 +139,7 @@ int WideCharToMultiByte( ustring = (ustring + "").force_encoding('UTF-8') if ustring.respond_to?(:force_encoding) && ustring.encoding.name != "UTF-8" # ensure we have the double-null termination Windows Wide likes - ustring = ustring + "\000\000" if ustring[-1].chr != "\000" + ustring = ustring + "\000\000" if ustring.length == 0 or ustring[-1].chr != "\000" # encode it all as UTF-16LE AKA Windows Wide Character AKA Windows Unicode ustring = begin diff --git a/lib/chef/win32/eventlog.rb b/lib/chef/win32/eventlog.rb new file mode 100644 index 0000000000..24af2da0d6 --- /dev/null +++ b/lib/chef/win32/eventlog.rb @@ -0,0 +1,31 @@ +# +# Author:: Jay Mundrawala (<jdm@chef.io>) +# +# Copyright:: 2015, Chef Software, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# 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. +# + +if Chef::Platform::windows? and not Chef::Platform::windows_server_2003? + if !defined? Chef::Win32EventLogLoaded + if defined? Windows::Constants + [:INFINITE, :WAIT_FAILED, :FORMAT_MESSAGE_IGNORE_INSERTS, :ERROR_INSUFFICIENT_BUFFER].each do |c| + # These are redefined in 'win32/eventlog' + Windows::Constants.send(:remove_const, c) if Windows::Constants.const_defined? c + end + end + + require 'win32/eventlog' + Chef::Win32EventLogLoaded = true + end +end diff --git a/lib/chef/win32/net.rb b/lib/chef/win32/net.rb new file mode 100644 index 0000000000..1349091eb9 --- /dev/null +++ b/lib/chef/win32/net.rb @@ -0,0 +1,190 @@ +# +# Author:: Jay Mundrawala(<jdm@chef.io>) +# Copyright:: Copyright 2015 Chef Software +# 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/win32/api/net' +require 'chef/win32/error' +require 'chef/mixin/wstring' + +class Chef + module ReservedNames::Win32 + class NetUser + include Chef::ReservedNames::Win32::API::Error + extend Chef::ReservedNames::Win32::API::Error + + include Chef::ReservedNames::Win32::API::Net + extend Chef::ReservedNames::Win32::API::Net + + include Chef::Mixin::WideString + extend Chef::Mixin::WideString + + def self.default_user_info_3 + ui3 = USER_INFO_3.new.tap do |s| + { usri3_name: nil, + usri3_password: nil, + usri3_password_age: 0, + usri3_priv: 0, + usri3_home_dir: nil, + usri3_comment: nil, + usri3_flags: UF_SCRIPT|UF_DONT_EXPIRE_PASSWD|UF_NORMAL_ACCOUNT, + usri3_script_path: nil, + usri3_auth_flags: 0, + usri3_full_name: nil, + usri3_usr_comment: nil, + usri3_parms: nil, + usri3_workstations: nil, + usri3_last_logon: 0, + usri3_last_logoff: 0, + usri3_acct_expires: -1, + usri3_max_storage: -1, + usri3_units_per_week: 0, + usri3_logon_hours: nil, + usri3_bad_pw_count: 0, + usri3_num_logons: 0, + usri3_logon_server: nil, + usri3_country_code: 0, + usri3_code_page: 0, + usri3_user_id: 0, + usri3_primary_group_id: DOMAIN_GROUP_RID_USERS, + usri3_profile: nil, + usri3_home_dir_drive: nil, + usri3_password_expired: 0 + }.each do |(k,v)| + s.set(k, v) + end + end + end + + def self.net_api_error!(code) + msg = case code + when NERR_InvalidComputer + "The user does not have access to the requested information." + when NERR_NotPrimary + "The operation is allowed only on the primary domain controller of the domain." + when NERR_SpeGroupOp + "This operation is not allowed on this special group." + when NERR_LastAdmin + "This operation is not allowed on the last administrative account." + when NERR_BadUsername + "The user name or group name parameter is invalid." + when NERR_BadPassword + "The password parameter is invalid." + when NERR_UserNotFound + raise Chef::Exceptions::UserIDNotFound, code + when NERR_PasswordTooShort + <<END +The password is shorter than required. (The password could also be too +long, be too recent in its change history, not have enough unique characters, +or not meet another password policy requirement.) +END + when ERROR_ACCESS_DENIED + "The user does not have access to the requested information." + else + "Received unknown error code (#{code})" + end + + formatted_message = "" + formatted_message << "---- Begin Win32 API output ----\n" + formatted_message << "Net Api Error Code: #{code}\n" + formatted_message << "Net Api Error Message: #{msg}\n" + formatted_message << "---- End Win32 API output ----\n" + + raise Chef::Exceptions::Win32APIError, msg + "\n" + formatted_message + end + + def self.net_user_add_l3(server_name, args) + buf = default_user_info_3 + + args.each do |k, v| + buf.set(k, v) + end + + server_name = wstring(server_name) + + rc = NetUserAdd(server_name, 3, buf, nil) + if rc != NERR_Success + net_api_error!(rc) + end + end + + def self.net_user_get_info_l3(server_name, user_name) + server_name = wstring(server_name) + user_name = wstring(user_name) + + ui3_p = FFI::MemoryPointer.new(:pointer) + + rc = NetUserGetInfo(server_name, user_name, 3, ui3_p) + + if rc != NERR_Success + net_api_error!(rc) + end + + ui3 = USER_INFO_3.new(ui3_p.read_pointer).as_ruby + + rc = NetApiBufferFree(ui3_p.read_pointer) + + if rc != NERR_Success + net_api_error!(rc) + end + + ui3 + end + + def self.net_user_set_info_l3(server_name, user_name, info) + buf = default_user_info_3 + + info.each do |k, v| + buf.set(k, v) + end + + server_name = wstring(server_name) + user_name = wstring(user_name) + + rc = NetUserSetInfo(server_name, user_name, 3, buf, nil) + if rc != NERR_Success + net_api_error!(rc) + end + end + + def self.net_user_del(server_name, user_name) + server_name = wstring(server_name) + user_name = wstring(user_name) + + rc = NetUserDel(server_name, user_name) + if rc != NERR_Success + net_api_error!(rc) + end + end + + def self.net_local_group_add_member(server_name, group_name, domain_user) + server_name = wstring(server_name) + group_name = wstring(group_name) + domain_user = wstring(domain_user) + + buf = LOCALGROUP_MEMBERS_INFO_3.new + buf[:lgrmi3_domainandname] = FFI::MemoryPointer.from_string(domain_user) + + rc = NetLocalGroupAddMembers(server_name, group_name, 3, buf, 1) + + if rc != NERR_Success + net_api_error!(rc) + end + end + + end + end +end diff --git a/lib/chef/win32/security.rb b/lib/chef/win32/security.rb index 3902d8caaf..5c83180bc0 100644 --- a/lib/chef/win32/security.rb +++ b/lib/chef/win32/security.rb @@ -22,6 +22,7 @@ require 'chef/win32/memory' require 'chef/win32/process' require 'chef/win32/unicode' require 'chef/win32/security/token' +require 'chef/mixin/wstring' class Chef module ReservedNames::Win32 @@ -31,6 +32,8 @@ class Chef include Chef::ReservedNames::Win32::API::Security extend Chef::ReservedNames::Win32::API::Security extend Chef::ReservedNames::Win32::API::Macros + include Chef::Mixin::WideString + extend Chef::Mixin::WideString def self.access_check(security_descriptor, token, desired_access, generic_mapping) token_handle = token.handle.handle @@ -270,6 +273,36 @@ class Chef [ present.read_char != 0, acl.null? ? nil : ACL.new(acl, security_descriptor), defaulted.read_char != 0 ] end + def self.get_token_information_owner(token) + owner_result_size = FFI::MemoryPointer.new(:ulong) + if GetTokenInformation(token.handle.handle, :TokenOwner, nil, 0, owner_result_size) + raise "Expected ERROR_INSUFFICIENT_BUFFER from GetTokenInformation, and got no error!" + elsif FFI::LastError.error != ERROR_INSUFFICIENT_BUFFER + Chef::ReservedNames::Win32::Error.raise! + end + owner_result_storage = FFI::MemoryPointer.new owner_result_size.read_ulong + unless GetTokenInformation(token.handle.handle, :TokenOwner, owner_result_storage, owner_result_size.read_ulong, owner_result_size) + Chef::ReservedNames::Win32::Error.raise! + end + owner_result = TOKEN_OWNER.new owner_result_storage + SID.new(owner_result[:Owner], owner_result_storage) + end + + def self.get_token_information_primary_group(token) + group_result_size = FFI::MemoryPointer.new(:ulong) + if GetTokenInformation(token.handle.handle, :TokenPrimaryGroup, nil, 0, group_result_size) + raise "Expected ERROR_INSUFFICIENT_BUFFER from GetTokenInformation, and got no error!" + elsif FFI::LastError.error != ERROR_INSUFFICIENT_BUFFER + Chef::ReservedNames::Win32::Error.raise! + end + group_result_storage = FFI::MemoryPointer.new group_result_size.read_ulong + unless GetTokenInformation(token.handle.handle, :TokenPrimaryGroup, group_result_storage, group_result_size.read_ulong, group_result_size) + Chef::ReservedNames::Win32::Error.raise! + end + group_result = TOKEN_PRIMARY_GROUP.new group_result_storage + SID.new(group_result[:PrimaryGroup], group_result_storage) + end + def self.initialize_acl(acl_size) acl = FFI::MemoryPointer.new acl_size unless InitializeAcl(acl, acl_size, ACL_REVISION) @@ -415,6 +448,10 @@ class Chef [ SecurityDescriptor.new(absolute_sd), SID.new(owner), SID.new(group), ACL.new(dacl), ACL.new(sacl) ] end + def self.open_current_process_token(desired_access = TOKEN_READ) + open_process_token(Chef::ReservedNames::Win32::Process.get_current_process, desired_access) + end + def self.open_process_token(process, desired_access) process = process.handle if process.respond_to?(:handle) process = process.handle if process.respond_to?(:handle) @@ -513,7 +550,7 @@ class Chef def self.with_privileges(*privilege_names) # Set privileges - token = open_process_token(Chef::ReservedNames::Win32::Process.get_current_process, TOKEN_READ | TOKEN_ADJUST_PRIVILEGES) + token = open_current_process_token(TOKEN_READ | TOKEN_ADJUST_PRIVILEGES) old_privileges = token.enable_privileges(*privilege_names) # Let the caller do their privileged stuff @@ -533,7 +570,7 @@ class Chef true else - process_token = open_process_token(Chef::ReservedNames::Win32::Process.get_current_process, TOKEN_READ) + process_token = open_current_process_token(TOKEN_READ) elevation_result = FFI::Buffer.new(:ulong) elevation_result_size = FFI::MemoryPointer.new(:uint32) success = GetTokenInformation(process_token.handle.handle, :TokenElevation, elevation_result, 4, elevation_result_size) @@ -543,6 +580,18 @@ class Chef success && (elevation_result.read_ulong != 0) end end + + def self.logon_user(username, domain, password, logon_type, logon_provider) + username = wstring(username) + domain = wstring(domain) + password = wstring(password) + + token = FFI::Buffer.new(:pointer) + unless LogonUserW(username, domain, password, logon_type, logon_provider, token) + Chef::ReservedNames::Win32::Error.raise! + end + Token.new(Handle.new(token.read_pointer)) + end end end end diff --git a/lib/chef/win32/security/sid.rb b/lib/chef/win32/security/sid.rb index 8e9407dc80..f8bd934876 100644 --- a/lib/chef/win32/security/sid.rb +++ b/lib/chef/win32/security/sid.rb @@ -203,6 +203,23 @@ class Chef SID.from_account("#{::ENV['USERDOMAIN']}\\#{::ENV['USERNAME']}") end + # See https://technet.microsoft.com/en-us/library/cc961992.aspx + # In practice, this is SID.Administrators if the current_user is an admin (even if not + # running elevated), and is current_user otherwise. On win2k3, it technically can be + # current_user in all cases if a certain group policy is set. + def self.default_security_object_owner + token = Chef::ReservedNames::Win32::Security.open_current_process_token + Chef::ReservedNames::Win32::Security.get_token_information_owner(token) + end + + # See https://technet.microsoft.com/en-us/library/cc961996.aspx + # In practice, this seems to be SID.current_user for Microsoft Accounts, the current + # user's Domain Users group for domain accounts, and SID.None otherwise. + def self.default_security_object_group + token = Chef::ReservedNames::Win32::Security.open_current_process_token + Chef::ReservedNames::Win32::Security.get_token_information_primary_group(token) + end + def self.admin_account_name @admin_account_name ||= begin admin_account_name = nil |