diff options
Diffstat (limited to 'lib')
209 files changed, 5783 insertions, 2650 deletions
diff --git a/lib/chef.rb b/lib/chef.rb index 6bce976439..1a0b802adb 100644 --- a/lib/chef.rb +++ b/lib/chef.rb @@ -31,5 +31,5 @@ require 'chef/daemon' require 'chef/run_status' require 'chef/handler' require 'chef/handler/json_file' - +require 'chef/event_dispatch/dsl' require 'chef/chef_class' diff --git a/lib/chef/api_client.rb b/lib/chef/api_client.rb index ad31fb7d7b..b7b9f7dc43 100644 --- a/lib/chef/api_client.rb +++ b/lib/chef/api_client.rb @@ -1,7 +1,7 @@ # -# Author:: Adam Jacob (<adam@chef.io>) -# Author:: Nuo Yan (<nuo@chef.io>) -# Copyright:: Copyright (c) 2008, 2015 Chef Software, Inc. +# Author:: Adam Jacob (<adam@opscode.com>) +# Author:: Nuo Yan (<nuo@opscode.com>) +# Copyright:: Copyright (c) 2008 Opscode, Inc. # License:: Apache License, Version 2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -23,18 +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' +# DEPRECATION NOTE +# +# This code will be removed in Chef 13 in favor of the code in Chef::ApiClientV1, +# which will be moved to this namespace. New development should occur in +# Chef::ApiClientV1 until the time before Chef 13. 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 @@ -43,25 +43,6 @@ 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. @@ -113,8 +94,7 @@ class Chef ) end - # Private key. The server will return it as a string. - # Set to true under API V0 to have the server regenerate the default key. + # Gets or sets the private key. # # @params [Optional String] The string representation of the private key. # @return [String] The current value. @@ -122,19 +102,7 @@ class Chef set_or_return( :private_key, arg, - :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 ] + :kind_of => [String, FalseClass] ) end @@ -145,14 +113,13 @@ 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 unless @private_key.nil? - result["public_key"] = @public_key unless @public_key.nil? - result["create_key"] = @create_key unless @create_key.nil? + result["private_key"] = @private_key if @private_key result end @@ -166,11 +133,10 @@ 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 @@ -182,6 +148,10 @@ class Chef from_hash(Chef::JSONCompat.parse(j)) end + def self.http_api + Chef::ServerAPI.new(Chef::Config[:chef_server_url], {:api_version => "0"}) + end + def self.reregister(name) api_client = load(name) api_client.reregister @@ -218,11 +188,11 @@ class Chef # Save this client via the REST API, returns a hash including the private key def save begin - update + http_api.put("clients/#{name}", { :name => self.name, :admin => self.admin, :validator => self.validator}) rescue Net::HTTPServerException => e # If that fails, go ahead and try and update it if e.response.code == "404" - create + http_api.post("clients", {:name => self.name, :admin => self.admin, :validator => self.validator }) else raise e end @@ -230,95 +200,18 @@ class Chef end def reregister - # 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 }) + reregistered_self = http_api.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 - 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)) + http_api.post("clients", self) end # As a string @@ -326,5 +219,14 @@ 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::ServerAPI.new(Chef::Config[:chef_server_url], {:api_version => "0"}) + end + end end diff --git a/lib/chef/api_client_v1.rb b/lib/chef/api_client_v1.rb new file mode 100644 index 0000000000..80f0d2517c --- /dev/null +++ b/lib/chef/api_client_v1.rb @@ -0,0 +1,325 @@ +# +# 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"); +# 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' +require 'chef/exceptions' +require 'chef/mixin/api_version_request_handling' +require 'chef/server_api' +require 'chef/api_client' + +# COMPATIBILITY NOTE +# +# This ApiClientV1 code attempts to make API V1 requests and falls back to +# API V0 requests when it fails. New development should occur here instead +# of Chef::ApiClient as this will replace that namespace when Chef 13 is released. +# +# If you need to default to API V0 behavior (i.e. you need GET client to return +# a public key, etc), please use Chef::ApiClient and update your code to support +# API V1 before you pull in Chef 13. +class Chef + class ApiClientV1 + + include Chef::Mixin::FromFile + include Chef::Mixin::ParamsValidate + include Chef::Mixin::ApiVersionRequestHandling + + SUPPORTED_API_VERSIONS = [0,1] + + # Create a new Chef::ApiClientV1 object. + def initialize + @name = '' + @public_key = nil + @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", :inflate_json_class => false}) + end + + def chef_rest_v1 + @chef_rest_v1 ||= Chef::ServerAPI.new(Chef::Config[:chef_server_url], {:api_version => "1", :inflate_json_class => false}) + end + + def self.http_api + Chef::ServerAPI.new(Chef::Config[:chef_server_url], {:api_version => "1", :inflate_json_class => false}) + end + + # Gets or sets the client name. + # + # @params [Optional String] The name must be alpha-numeric plus - and _. + # @return [String] The current value of the name. + def name(arg=nil) + set_or_return( + :name, + arg, + :regex => /^[\-[:alnum:]_\.]+$/ + ) + end + + # Gets or sets whether this client is an admin. + # + # @params [Optional True/False] Should be true or false - default is false. + # @return [True/False] The current value + def admin(arg=nil) + set_or_return( + :admin, + arg, + :kind_of => [ TrueClass, FalseClass ] + ) + end + + # Gets or sets the public key. + # + # @params [Optional String] The string representation of the public key. + # @return [String] The current value. + def public_key(arg=nil) + set_or_return( + :public_key, + arg, + :kind_of => String + ) + end + + # Gets or sets whether this client is a validator. + # + # @params [Boolean] whether or not the client is a validator. If + # `nil`, retrieves the already-set value. + # @return [Boolean] The current value + def validator(arg=nil) + set_or_return( + :validator, + arg, + :kind_of => [TrueClass, FalseClass] + ) + end + + # 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. + def private_key(arg=nil) + set_or_return( + :private_key, + arg, + :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 + + # The hash representation of the object. Includes the name and public_key. + # Private key is included if available. + # + # @return [Hash] + def to_hash + result = { + "name" => @name, + "validator" => @validator, + "admin" => @admin, + "chef_type" => "client" + } + 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 + + # The JSON representation of the object. + # + # @return [String] the JSON string. + def to_json(*a) + Chef::JSONCompat.to_json(to_hash, *a) + end + + def self.from_hash(o) + client = Chef::ApiClientV1.new + client.name(o["name"] || o["clientname"]) + 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 + + def self.from_json(j) + Chef::ApiClientV1.from_hash(Chef::JSONCompat.from_json(j)) + end + + def self.reregister(name) + api_client = Chef::ApiClientV1.load(name) + api_client.reregister + end + + def self.list(inflate=false) + if inflate + response = Hash.new + Chef::Search::Query.new.search(:client) do |n| + n = self.from_hash(n) if n.instance_of?(Hash) + response[n.name] = n + end + response + else + http_api.get("clients") + end + end + + # Load a client by name via the API + def self.load(name) + response = http_api.get("clients/#{name}") + Chef::ApiClientV1.from_hash(response) + end + + # Remove this client via the REST API + def destroy + chef_rest_v1.delete("clients/#{@name}") + end + + # Save this client via the REST API, returns a hash including the private key + def save + begin + update + rescue Net::HTTPServerException => e + # If that fails, go ahead and try and update it + if e.response.code == "404" + create + else + raise e + end + end + end + + def reregister + # 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 + + Chef::ApiClientV1.from_hash(new_client) + end + + # Create the client via the REST API + def create + 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::ApiClientV1.from_hash(self.to_hash.merge(new_client)) + end + + # As a string + def to_s + "client[#{@name}]" + end + + end +end diff --git a/lib/chef/application.rb b/lib/chef/application.rb index 0563822ede..970544c068 100644 --- a/lib/chef/application.rb +++ b/lib/chef/application.rb @@ -382,7 +382,7 @@ class Chef def emit_warnings if Chef::Config[:chef_gem_compile_time] - Chef::Log.deprecation "setting chef_gem_compile_time to true is deprecated" + Chef.log_deprecation "setting chef_gem_compile_time to true is deprecated" end end diff --git a/lib/chef/application/apply.rb b/lib/chef/application/apply.rb index e9768b218c..243b441119 100644 --- a/lib/chef/application/apply.rb +++ b/lib/chef/application/apply.rb @@ -49,6 +49,24 @@ class Chef::Application::Apply < Chef::Application :description => "Load attributes from a JSON file or URL", :proc => nil + option :force_logger, + :long => "--force-logger", + :description => "Use logger output instead of formatter output", + :boolean => true, + :default => false + + option :force_formatter, + :long => "--force-formatter", + :description => "Use formatter output instead of logger output", + :boolean => true, + :default => false + + option :formatter, + :short => "-F FORMATTER", + :long => "--format FORMATTER", + :description => "output format to use", + :proc => lambda { |format| Chef::Config.add_formatter(format) } + option :log_level, :short => "-l LEVEL", :long => "--log_level LEVEL", diff --git a/lib/chef/application/client.rb b/lib/chef/application/client.rb index 409680b553..73eda81343 100644 --- a/lib/chef/application/client.rb +++ b/lib/chef/application/client.rb @@ -449,9 +449,9 @@ class Chef::Application::Client < Chef::Application end def audit_mode_settings_explanation - "\n* To enable audit mode after converge, use command line option `--audit-mode enabled` or set `:audit_mode = :enabled` in your config file." + - "\n* To disable audit mode, use command line option `--audit-mode disabled` or set `:audit_mode = :disabled` in your config file." + - "\n* To only run audit mode, use command line option `--audit-mode audit-only` or set `:audit_mode = :audit_only` in your config file." + + "\n* To enable audit mode after converge, use command line option `--audit-mode enabled` or set `audit_mode :enabled` in your config file." + + "\n* To disable audit mode, use command line option `--audit-mode disabled` or set `audit_mode :disabled` in your config file." + + "\n* To only run audit mode, use command line option `--audit-mode audit-only` or set `audit_mode :audit_only` in your config file." + "\nAudit mode is disabled by default." end diff --git a/lib/chef/application/solo.rb b/lib/chef/application/solo.rb index dd09d65b42..5bb2a1ceb0 100644 --- a/lib/chef/application/solo.rb +++ b/lib/chef/application/solo.rb @@ -214,7 +214,7 @@ class Chef::Application::Solo < Chef::Application FileUtils.mkdir_p(recipes_path) tarball_path = File.join(recipes_path, 'recipes.tgz') fetch_recipe_tarball(Chef::Config[:recipe_url], tarball_path) - Chef::Mixin::Command.run_command(:command => "tar zxvf #{tarball_path} -C #{recipes_path}") + Mixlib::ShellOut.new("tar zxvf #{tarball_path} -C #{recipes_path}").run_command end # json_attribs shuld be fetched after recipe_url tarball is unpacked. diff --git a/lib/chef/application/windows_service_manager.rb b/lib/chef/application/windows_service_manager.rb index de8ed657c2..44526c1720 100644 --- a/lib/chef/application/windows_service_manager.rb +++ b/lib/chef/application/windows_service_manager.rb @@ -78,7 +78,7 @@ class Chef raise ArgumentError, "Service definition is not provided" if service_options.nil? - required_options = [:service_name, :service_display_name, :service_name, :service_description, :service_file_path] + required_options = [:service_name, :service_display_name, :service_description, :service_file_path] required_options.each do |req_option| if !service_options.has_key?(req_option) @@ -92,6 +92,8 @@ class Chef @service_file_path = service_options[:service_file_path] @service_start_name = service_options[:run_as_user] @password = service_options[:run_as_password] + @delayed_start = service_options[:delayed_start] + @dependencies = service_options[:dependencies] end def run(params = ARGV) @@ -113,17 +115,22 @@ class Chef cmd = "\"#{ruby}\" \"#{@service_file_path}\" #{opts}".gsub(File::SEPARATOR, File::ALT_SEPARATOR) ::Win32::Service.new( - :service_name => @service_name, - :display_name => @service_display_name, - :description => @service_description, - # Prior to 0.8.5, win32-service creates interactive services by default, - # and we don't want that, so we need to override the service type. - :service_type => ::Win32::Service::SERVICE_WIN32_OWN_PROCESS, - :start_type => ::Win32::Service::SERVICE_AUTO_START, - :binary_path_name => cmd, - :service_start_name => @service_start_name, - :password => @password, - ) + :service_name => @service_name, + :display_name => @service_display_name, + :description => @service_description, + # Prior to 0.8.5, win32-service creates interactive services by default, + # and we don't want that, so we need to override the service type. + :service_type => ::Win32::Service::SERVICE_WIN32_OWN_PROCESS, + :start_type => ::Win32::Service::SERVICE_AUTO_START, + :binary_path_name => cmd, + :service_start_name => @service_start_name, + :password => @password, + :dependencies => @dependencies + ) + ::Win32::Service.configure( + :service_name => @service_name, + :delayed_start => @delayed_start + ) unless @delayed_start.nil? puts "Service '#{@service_name}' has successfully been installed." end when 'status' diff --git a/lib/chef/chef_class.rb b/lib/chef/chef_class.rb index f1dd797c04..c2cb9e2b24 100644 --- a/lib/chef/chef_class.rb +++ b/lib/chef/chef_class.rb @@ -28,6 +28,8 @@ require 'chef/platform/provider_priority_map' require 'chef/platform/resource_priority_map' +require 'chef/platform/provider_handler_map' +require 'chef/platform/resource_handler_map' class Chef class << self @@ -50,7 +52,14 @@ class Chef # attr_reader :run_context + # Register an event handler with user specified block # + # @return[Chef::EventDispatch::Base] handler object + def event_handler(&block) + dsl = Chef::EventDispatch::DSL.new('Chef client DSL') + dsl.instance_eval(&block) + end + # 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 @@ -58,7 +67,7 @@ class Chef # @return [Array<Class>] Priority Array of Provider Classes to use for the resource_name on the node # def get_provider_priority_array(resource_name) - result = provider_priority_map.get_priority_array(node, resource_name) + result = provider_priority_map.get_priority_array(node, resource_name.to_sym) result = result.dup if result result end @@ -71,7 +80,7 @@ class Chef # @return [Array<Class>] Priority Array of Resource Classes to use for the resource_name on the node # def get_resource_priority_array(resource_name) - result = resource_priority_map.get_priority_array(node, resource_name) + result = resource_priority_map.get_priority_array(node, resource_name.to_sym) result = result.dup if result result end @@ -86,7 +95,7 @@ class Chef # @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, &block) - result = provider_priority_map.set_priority_array(resource_name, priority_array, *filter, &block) + result = provider_priority_map.set_priority_array(resource_name.to_sym, priority_array, *filter, &block) result = result.dup if result result end @@ -101,7 +110,7 @@ class Chef # @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, &block) - result = resource_priority_map.set_priority_array(resource_name, priority_array, *filter, &block) + result = resource_priority_map.set_priority_array(resource_name.to_sym, priority_array, *filter, &block) result = result.dup if result result end @@ -160,19 +169,48 @@ class Chef @node = nil @provider_priority_map = nil @resource_priority_map = nil + @provider_handler_map = nil + @resource_handler_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 + # these slurp in the resource+provider world, so be exceedingly lazy about requiring them + @provider_priority_map ||= Chef::Platform::ProviderPriorityMap.instance end # @api private def resource_priority_map - @resource_priority_map ||= begin - Chef::Platform::ResourcePriorityMap.instance + @resource_priority_map ||= Chef::Platform::ResourcePriorityMap.instance + end + # @api private + def provider_handler_map + @provider_handler_map ||= Chef::Platform::ProviderHandlerMap.instance + end + # @api private + def resource_handler_map + @resource_handler_map ||= Chef::Platform::ResourceHandlerMap.instance + end + + # + # Emit a deprecation message. + # + # @param message The message to send. + # @param location The location. Defaults to the caller who called you (since + # generally the person who triggered the check is the one that needs to be + # fixed). + # + # @example + # Chef.deprecation("Deprecated!") + # + # @api private this will likely be removed in favor of an as-yet unwritten + # `Chef.log` + def log_deprecation(message, location=caller(2..2)[0]) + # `run_context.events` is the primary deprecation target if we're in a + # run. If we are not yet in a run, print to `Chef::Log`. + if run_context && run_context.events + run_context.events.deprecation(message, location) + else + Chef::Log.deprecation(message, location) end end end diff --git a/lib/chef/chef_fs/config.rb b/lib/chef/chef_fs/config.rb index 6666a3deee..40cbb36530 100644 --- a/lib/chef/chef_fs/config.rb +++ b/lib/chef/chef_fs/config.rb @@ -111,7 +111,7 @@ class Chef # def initialize(chef_config = Chef::Config, cwd = Dir.pwd, options = {}, ui = nil) @chef_config = chef_config - @cwd = cwd + @cwd = File.expand_path(cwd) @cookbook_version = options[:cookbook_version] if @chef_config[:repo_mode] == 'everything' && is_hosted? && !ui.nil? @@ -166,34 +166,37 @@ class Chef # server_path('/home/jkeiser/chef_repo/cookbooks/blah') == '/cookbooks/blah' # server_path('/home/*/chef_repo/cookbooks/blah') == nil # - # If there are multiple paths (cookbooks, roles, data bags, etc. can all - # have separate paths), and cwd+the path reaches into one of them, we will - # return a path relative to that. Otherwise we will return a path to - # chef_repo. + # If there are multiple different, manually specified paths to object locations + # (cookbooks, roles, data bags, etc. can all have separate paths), and cwd+the + # path reaches into one of them, we will return a path relative to the first + # one to match it. Otherwise we expect the path provided to be to the chef + # repo path itself. Paths that are not available on the server are not supported. # # Globs are allowed as well, but globs outside server paths are NOT # (presently) supported. See above examples. TODO support that. # # If the path does not reach into ANY specified directory, nil is returned. def server_path(file_path) - pwd = File.expand_path(Dir.pwd) - absolute_pwd = Chef::ChefFS::PathUtils.realest_path(File.expand_path(file_path, pwd)) + target_path = Chef::ChefFS::PathUtils.realest_path(file_path, @cwd) # Check all object paths (cookbooks_dir, data_bags_dir, etc.) + # These are either manually specified by the user or autogenerated relative + # to chef_repo_path. object_paths.each_pair do |name, paths| paths.each do |path| - realest_path = Chef::ChefFS::PathUtils.realest_path(path) - if PathUtils.descendant_of?(absolute_pwd, realest_path) - relative_path = Chef::ChefFS::PathUtils::relative_to(absolute_pwd, realest_path) - return relative_path == '.' ? "/#{name}" : "/#{name}/#{relative_path}" + object_abs_path = Chef::ChefFS::PathUtils.realest_path(path, @cwd) + if relative_path = PathUtils.descendant_path(target_path, object_abs_path) + return Chef::ChefFS::PathUtils.join("/#{name}", relative_path) end end end # Check chef_repo_path Array(@chef_config[:chef_repo_path]).flatten.each do |chef_repo_path| - realest_chef_repo_path = Chef::ChefFS::PathUtils.realest_path(chef_repo_path) - if absolute_pwd == realest_chef_repo_path + # We're using realest_path here but we really don't need to - we can just expand the + # path and use realpath because a repo_path if provided *must* exist. + realest_chef_repo_path = Chef::ChefFS::PathUtils.realest_path(chef_repo_path, @cwd) + if Chef::ChefFS::PathUtils.os_path_eq?(target_path, realest_chef_repo_path) return '/' end end @@ -201,15 +204,10 @@ class Chef nil end - # The current directory, relative to server root + # The current directory, relative to server root. This is a case-sensitive server path. + # It only exists if the current directory is a child of one of the recognized object_paths below. def base_path - @base_path ||= begin - if @chef_config[:chef_repo_path] - server_path(File.expand_path(@cwd)) - else - nil - end - end + @base_path ||= server_path(@cwd) end # Print the given server path, relative to the current directory @@ -217,10 +215,10 @@ class Chef server_path = entry.path if base_path && server_path[0,base_path.length] == base_path if server_path == base_path - return "." - elsif server_path[base_path.length,1] == "/" + return '.' + elsif server_path[base_path.length,1] == '/' return server_path[base_path.length + 1, server_path.length - base_path.length - 1] - elsif base_path == "/" && server_path[0,1] == "/" + elsif base_path == '/' && server_path[0,1] == '/' return server_path[1, server_path.length - 1] end end diff --git a/lib/chef/chef_fs/file_pattern.rb b/lib/chef/chef_fs/file_pattern.rb index 134d22cbd5..b2351dac68 100644 --- a/lib/chef/chef_fs/file_pattern.rb +++ b/lib/chef/chef_fs/file_pattern.rb @@ -72,7 +72,7 @@ class Chef def could_match_children?(path) return false if path == '' # Empty string is not a path - argument_is_absolute = !!(path =~ /^#{Chef::ChefFS::PathUtils::regexp_path_separator}/) + argument_is_absolute = Chef::ChefFS::PathUtils::is_absolute?(path) return false if is_absolute != argument_is_absolute path = path[1,path.length-1] if argument_is_absolute @@ -111,7 +111,7 @@ class Chef # # This method assumes +could_match_children?(path)+ is +true+. def exact_child_name_under(path) - path = path[1,path.length-1] if !!(path =~ /^#{Chef::ChefFS::PathUtils::regexp_path_separator}/) + path = path[1,path.length-1] if Chef::ChefFS::PathUtils::is_absolute?(path) dirs_in_path = Chef::ChefFS::PathUtils::split(path).length return nil if exact_parts.length <= dirs_in_path return exact_parts[dirs_in_path] @@ -149,7 +149,7 @@ class Chef # abc/*/def.match?('abc/foo/def') == true # abc/*/def.match?('abc/foo') == false def match?(path) - argument_is_absolute = !!(path =~ /^#{Chef::ChefFS::PathUtils::regexp_path_separator}/) + argument_is_absolute = Chef::ChefFS::PathUtils::is_absolute?(path) return false if is_absolute != argument_is_absolute path = path[1,path.length-1] if argument_is_absolute !!regexp.match(path) @@ -160,17 +160,6 @@ class Chef pattern end - # Given a relative file pattern and a directory, makes a new file pattern - # starting with the directory. - # - # FilePattern.relative_to('/usr/local', 'bin/*grok') == FilePattern.new('/usr/local/bin/*grok') - # - # BUG: this does not support patterns starting with <tt>..</tt> - def self.relative_to(dir, pattern) - return FilePattern.new(pattern) if pattern =~ /^#{Chef::ChefFS::PathUtils::regexp_path_separator}/ - FilePattern.new(Chef::ChefFS::PathUtils::join(dir, pattern)) - end - private def regexp @@ -195,7 +184,7 @@ class Chef def calculate if !@regexp - @is_absolute = !!(@pattern =~ /^#{Chef::ChefFS::PathUtils::regexp_path_separator}/) + @is_absolute = Chef::ChefFS::PathUtils::is_absolute?(@pattern) full_regexp_parts = [] normalized_parts = [] diff --git a/lib/chef/chef_fs/file_system/acl_dir.rb b/lib/chef/chef_fs/file_system/acl_dir.rb index c2354d478d..9f68d7cda7 100644 --- a/lib/chef/chef_fs/file_system/acl_dir.rb +++ b/lib/chef/chef_fs/file_system/acl_dir.rb @@ -28,10 +28,9 @@ class Chef parent.parent.child(name).api_path end - def child(name) + def make_child_entry(name, exists = nil) result = @children.select { |child| child.name == name }.first if @children - result ||= can_have_child?(name, false) ? - AclEntry.new(name, self) : NonexistentFSObject.new(name, self) + result || AclEntry.new(name, self, exists) end def can_have_child?(name, is_dir) @@ -42,7 +41,7 @@ class Chef if @children.nil? # Grab the ACTUAL children (/nodes, /containers, etc.) and get their names names = parent.parent.child(name).children.map { |child| child.dir? ? "#{child.name}.json" : child.name } - @children = names.map { |name| AclEntry.new(name, self, true) } + @children = names.map { |name| make_child_entry(name, true) } end @children end diff --git a/lib/chef/chef_fs/file_system/acls_dir.rb b/lib/chef/chef_fs/file_system/acls_dir.rb index 938bf73fb2..a8c63726b7 100644 --- a/lib/chef/chef_fs/file_system/acls_dir.rb +++ b/lib/chef/chef_fs/file_system/acls_dir.rb @@ -40,8 +40,12 @@ class Chef parent.api_path end + def make_child_entry(name) + children.select { |child| child.name == name }.first + end + def can_have_child?(name, is_dir) - is_dir ? ENTITY_TYPES.include(name) : name == 'organization.json' + is_dir ? ENTITY_TYPES.include?(name) : name == 'organization.json' end def children diff --git a/lib/chef/chef_fs/file_system/base_fs_dir.rb b/lib/chef/chef_fs/file_system/base_fs_dir.rb index 8cc277facc..47e33f961a 100644 --- a/lib/chef/chef_fs/file_system/base_fs_dir.rb +++ b/lib/chef/chef_fs/file_system/base_fs_dir.rb @@ -31,11 +31,6 @@ class Chef true end - # Override child(name) to provide a child object by name without the network read - def child(name) - children.select { |child| child.name == name }.first || NonexistentFSObject.new(name, self) - end - def can_have_child?(name, is_dir) true end diff --git a/lib/chef/chef_fs/file_system/base_fs_object.rb b/lib/chef/chef_fs/file_system/base_fs_object.rb index 43e6a513d7..916ab8297d 100644 --- a/lib/chef/chef_fs/file_system/base_fs_object.rb +++ b/lib/chef/chef_fs/file_system/base_fs_object.rb @@ -95,7 +95,10 @@ class Chef # directly perform a network request to retrieve the y.json data bag. No # network request was necessary to retrieve def child(name) - NonexistentFSObject.new(name, self) + if can_have_child?(name, true) || can_have_child?(name, false) + result = make_child_entry(name) + end + result || NonexistentFSObject.new(name, self) end # Override children to report your *actual* list of children as an array. @@ -171,7 +174,7 @@ class Chef # Important directory attributes: name, parent, path, root # Overridable attributes: dir?, child(name), path_for_printing - # Abstract: read, write, delete, children, can_have_child?, create_child, compare_to + # Abstract: read, write, delete, children, can_have_child?, create_child, compare_to, make_child_entry end # class BaseFsObject end end diff --git a/lib/chef/chef_fs/file_system/chef_repository_file_system_cookbook_dir.rb b/lib/chef/chef_fs/file_system/chef_repository_file_system_cookbook_dir.rb index a7f1d733b1..4391bdbfcd 100644 --- a/lib/chef/chef_fs/file_system/chef_repository_file_system_cookbook_dir.rb +++ b/lib/chef/chef_fs/file_system/chef_repository_file_system_cookbook_dir.rb @@ -58,14 +58,7 @@ class Chef end def children - begin - Dir.entries(file_path).sort. - select { |child_name| can_have_child?(child_name, File.directory?(File.join(file_path, child_name))) }. - map { |child_name| make_child(child_name) }. - select { |entry| !(entry.dir? && entry.children.size == 0) } - rescue Errno::ENOENT - raise Chef::ChefFS::FileSystem::NotFoundError.new(self, $!) - end + super.select { |entry| !(entry.dir? && entry.children.size == 0 ) } end def can_have_child?(name, is_dir) @@ -99,7 +92,7 @@ class Chef protected - def make_child(child_name) + def make_child_entry(child_name) segment_info = CookbookDir::COOKBOOK_SEGMENT_INFO[child_name.to_sym] || {} ChefRepositoryFileSystemCookbookEntry.new(child_name, self, nil, segment_info[:ruby_only], segment_info[:recursive]) end diff --git a/lib/chef/chef_fs/file_system/chef_repository_file_system_cookbook_entry.rb b/lib/chef/chef_fs/file_system/chef_repository_file_system_cookbook_entry.rb index 66709ccf68..914412f839 100644 --- a/lib/chef/chef_fs/file_system/chef_repository_file_system_cookbook_entry.rb +++ b/lib/chef/chef_fs/file_system/chef_repository_file_system_cookbook_entry.rb @@ -34,14 +34,7 @@ class Chef attr_reader :recursive def children - begin - Dir.entries(file_path).sort. - select { |child_name| can_have_child?(child_name, File.directory?(File.join(file_path, child_name))) }. - map { |child_name| make_child(child_name) }. - select { |entry| !(entry.dir? && entry.children.size == 0) } - rescue Errno::ENOENT - raise Chef::ChefFS::FileSystem::NotFoundError.new(self, $!) - end + super.select { |entry| !(entry.dir? && entry.children.size == 0 ) } end def can_have_child?(name, is_dir) @@ -78,7 +71,7 @@ class Chef protected - def make_child(child_name) + def make_child_entry(child_name) ChefRepositoryFileSystemCookbookEntry.new(child_name, self, nil, ruby_only, recursive) end end diff --git a/lib/chef/chef_fs/file_system/chef_repository_file_system_cookbooks_dir.rb b/lib/chef/chef_fs/file_system/chef_repository_file_system_cookbooks_dir.rb index 7c60b51114..5b495666c3 100644 --- a/lib/chef/chef_fs/file_system/chef_repository_file_system_cookbooks_dir.rb +++ b/lib/chef/chef_fs/file_system/chef_repository_file_system_cookbooks_dir.rb @@ -37,21 +37,14 @@ class Chef attr_reader :chefignore def children - begin - Dir.entries(file_path).sort. - select { |child_name| can_have_child?(child_name, File.directory?(File.join(file_path, child_name))) }. - map { |child_name| make_child(child_name) }. - select do |entry| - # empty cookbooks and cookbook directories are ignored - if !entry.can_upload? - Chef::Log.warn("Cookbook '#{entry.name}' is empty or entirely chefignored at #{entry.path_for_printing}") - false - else - true - end - end - rescue Errno::ENOENT - raise Chef::ChefFS::FileSystem::NotFoundError.new(self, $!) + super.select do |entry| + # empty cookbooks and cookbook directories are ignored + if !entry.can_upload? + Chef::Log.warn("Cookbook '#{entry.name}' is empty or entirely chefignored at #{entry.path_for_printing}") + false + else + true + end end end @@ -61,7 +54,7 @@ class Chef def write_cookbook(cookbook_path, cookbook_version_json, from_fs) cookbook_name = File.basename(cookbook_path) - child = make_child(cookbook_name) + child = make_child_entry(cookbook_name) # Use the copy/diff algorithm to copy it down so we don't destroy # chefignored data. This is terribly un-thread-safe. @@ -80,7 +73,7 @@ class Chef protected - def make_child(child_name) + def make_child_entry(child_name) ChefRepositoryFileSystemCookbookDir.new(child_name, self) end end diff --git a/lib/chef/chef_fs/file_system/chef_repository_file_system_entry.rb b/lib/chef/chef_fs/file_system/chef_repository_file_system_entry.rb index 0b14750744..39172e7ab9 100644 --- a/lib/chef/chef_fs/file_system/chef_repository_file_system_entry.rb +++ b/lib/chef/chef_fs/file_system/chef_repository_file_system_entry.rb @@ -70,20 +70,9 @@ class Chef Chef::JSONCompat.to_json_pretty(object) end - def children - # Except cookbooks and data bag dirs, all things must be json files - begin - Dir.entries(file_path).sort. - select { |child_name| can_have_child?(child_name, File.directory?(File.join(file_path, child_name))) }. - map { |child_name| make_child(child_name) } - rescue Errno::ENOENT - raise Chef::ChefFS::FileSystem::NotFoundError.new(self, $!) - end - end - protected - def make_child(child_name) + def make_child_entry(child_name) ChefRepositoryFileSystemEntry.new(child_name, self) end end diff --git a/lib/chef/chef_fs/file_system/chef_repository_file_system_root_dir.rb b/lib/chef/chef_fs/file_system/chef_repository_file_system_root_dir.rb index d03baf91fe..267fe30456 100644 --- a/lib/chef/chef_fs/file_system/chef_repository_file_system_root_dir.rb +++ b/lib/chef/chef_fs/file_system/chef_repository_file_system_root_dir.rb @@ -68,13 +68,13 @@ class Chef attr_reader :child_paths attr_reader :versioned_cookbooks - CHILDREN = %w(invitations.json members.json org.json) + CHILDREN = %w(org.json invitations.json members.json) def children @children ||= begin - result = child_paths.keys.sort.map { |name| make_child_entry(name) }.select { |child| !child.nil? } - result += root_dir.children.select { |c| CHILDREN.include?(c.name) } if root_dir - result.sort_by { |c| c.name } + result = child_paths.keys.sort.map { |name| make_child_entry(name) } + result += CHILDREN.map { |name| make_child_entry(name) } + result.select { |c| c && c.exists? }.sort_by { |c| c.name } end end @@ -149,19 +149,23 @@ class Chef # cookbooks from all of them when you list or grab them). # def make_child_entry(name) - paths = child_paths[name].select do |path| - File.exists?(path) + if CHILDREN.include?(name) + return nil if !root_dir + return root_dir.child(name) end + + paths = (child_paths[name] || []).select { |path| File.exists?(path) } if paths.size == 0 - return nil + return NonexistentFSObject.new(name, self) end - if name == 'cookbooks' + case name + when 'cookbooks' dirs = paths.map { |path| ChefRepositoryFileSystemCookbooksDir.new(name, self, path) } - elsif name == 'data_bags' + when 'data_bags' dirs = paths.map { |path| ChefRepositoryFileSystemDataBagsDir.new(name, self, path) } - elsif name == 'policies' + when 'policies' dirs = paths.map { |path| ChefRepositoryFileSystemPoliciesDir.new(name, self, path) } - elsif name == 'acls' + when 'acls' dirs = paths.map { |path| ChefRepositoryFileSystemAclsDir.new(name, self, path) } else data_handler = case name diff --git a/lib/chef/chef_fs/file_system/chef_server_root_dir.rb b/lib/chef/chef_fs/file_system/chef_server_root_dir.rb index 370308ee0a..e3ffd644ad 100644 --- a/lib/chef/chef_fs/file_system/chef_server_root_dir.rb +++ b/lib/chef/chef_fs/file_system/chef_server_root_dir.rb @@ -90,11 +90,11 @@ class Chef end def rest - Chef::ServerAPI.new(chef_server_url, :client_name => chef_username, :signing_key_filename => chef_private_key, :raw_output => true) + Chef::ServerAPI.new(chef_server_url, :client_name => chef_username, :signing_key_filename => chef_private_key, :raw_output => true, :api_version => "0") end def get_json(path) - Chef::ServerAPI.new(chef_server_url, :client_name => chef_username, :signing_key_filename => chef_private_key).get(path) + Chef::ServerAPI.new(chef_server_url, :client_name => chef_username, :signing_key_filename => chef_private_key, :api_version => "0").get(path) end def chef_rest @@ -110,7 +110,8 @@ class Chef end def can_have_child?(name, is_dir) - is_dir && children.any? { |child| child.name == name } + result = children.select { |child| child.name == name }.first + result && !!result.dir? == !!is_dir end def org @@ -124,6 +125,10 @@ class Chef end end + def make_child_entry(name) + children.select { |child| child.name == name }.first + end + def children @children ||= begin result = [ diff --git a/lib/chef/chef_fs/file_system/cookbook_dir.rb b/lib/chef/chef_fs/file_system/cookbook_dir.rb index 03652dc376..c0f0390e98 100644 --- a/lib/chef/chef_fs/file_system/cookbook_dir.rb +++ b/lib/chef/chef_fs/file_system/cookbook_dir.rb @@ -16,6 +16,7 @@ # limitations under the License. # +require 'chef/chef_fs/command_line' require 'chef/chef_fs/file_system/rest_list_dir' require 'chef/chef_fs/file_system/cookbook_subdir' require 'chef/chef_fs/file_system/cookbook_file' @@ -71,16 +72,15 @@ class Chef "#{parent.api_path}/#{cookbook_name}/#{version || "_latest"}" end - def child(name) + def make_child_entry(name) # Since we're ignoring the rules and doing a network request here, # we need to make sure we don't rethrow the exception. (child(name) # is not supposed to fail.) begin - result = children.select { |child| child.name == name }.first - return result if result + children.select { |child| child.name == name }.first rescue Chef::ChefFS::FileSystem::NotFoundError + nil end - return NonexistentFSObject.new(name, self) end def can_have_child?(name, is_dir) diff --git a/lib/chef/chef_fs/file_system/cookbooks_acl_dir.rb b/lib/chef/chef_fs/file_system/cookbooks_acl_dir.rb index d6246f1e60..560ceb4886 100644 --- a/lib/chef/chef_fs/file_system/cookbooks_acl_dir.rb +++ b/lib/chef/chef_fs/file_system/cookbooks_acl_dir.rb @@ -31,7 +31,7 @@ class Chef def children if @children.nil? names = parent.parent.child(name).children.map { |child| "#{child.cookbook_name}.json" } - @children = names.uniq.map { |name| AclEntry.new(name, self, true) } + @children = names.uniq.map { |name| make_child_entry(name, true) } end @children end diff --git a/lib/chef/chef_fs/file_system/cookbooks_dir.rb b/lib/chef/chef_fs/file_system/cookbooks_dir.rb index 27bedd3827..6f49c28996 100644 --- a/lib/chef/chef_fs/file_system/cookbooks_dir.rb +++ b/lib/chef/chef_fs/file_system/cookbooks_dir.rb @@ -36,17 +36,9 @@ class Chef super("cookbooks", parent) end - def child(name) - if @children - result = self.children.select { |child| child.name == name }.first - if result - result - else - NonexistentFSObject.new(name, self) - end - else - CookbookDir.new(name, self) - end + def make_child_entry(name) + result = @children.select { |child| child.name == name }.first if @children + result || CookbookDir.new(name, self) end def children diff --git a/lib/chef/chef_fs/file_system/data_bags_dir.rb b/lib/chef/chef_fs/file_system/data_bags_dir.rb index 6d0685d3b7..1cb61bbd1a 100644 --- a/lib/chef/chef_fs/file_system/data_bags_dir.rb +++ b/lib/chef/chef_fs/file_system/data_bags_dir.rb @@ -27,16 +27,14 @@ class Chef super("data_bags", parent, "data") end - def child(name) + def make_child_entry(name, exists = false) result = @children.select { |child| child.name == name }.first if @children - result || DataBagDir.new(name, self) + result || DataBagDir.new(name, self, exists) end def children begin - @children ||= root.get_json(api_path).keys.sort.map do |entry| - DataBagDir.new(entry, self, true) - end + @children ||= root.get_json(api_path).keys.sort.map { |entry| make_child_entry(entry, true) } rescue Timeout::Error => e raise Chef::ChefFS::FileSystem::OperationFailedError.new(:children, self, e), "Timeout getting children: #{e}" rescue Net::HTTPServerException => e diff --git a/lib/chef/chef_fs/file_system/environments_dir.rb b/lib/chef/chef_fs/file_system/environments_dir.rb index 559dd6af86..3aee3ee5af 100644 --- a/lib/chef/chef_fs/file_system/environments_dir.rb +++ b/lib/chef/chef_fs/file_system/environments_dir.rb @@ -30,7 +30,7 @@ class Chef super("environments", parent, nil, Chef::ChefFS::DataHandler::EnvironmentDataHandler.new) end - def _make_child_entry(name, exists = nil) + def make_child_entry(name, exists = nil) if name == '_default.json' DefaultEnvironmentEntry.new(name, self, exists) else diff --git a/lib/chef/chef_fs/file_system/file_system_entry.rb b/lib/chef/chef_fs/file_system/file_system_entry.rb index 1af7e618de..8611aa2e0f 100644 --- a/lib/chef/chef_fs/file_system/file_system_entry.rb +++ b/lib/chef/chef_fs/file_system/file_system_entry.rb @@ -40,15 +40,18 @@ class Chef end def children + # Except cookbooks and data bag dirs, all things must be json files begin - Dir.entries(file_path).sort.select { |entry| entry != '.' && entry != '..' }.map { |entry| make_child(entry) } + Dir.entries(file_path).sort. + map { |child_name| make_child_entry(child_name) }. + select { |child| child && can_have_child?(child.name, child.dir?) } rescue Errno::ENOENT raise Chef::ChefFS::FileSystem::NotFoundError.new(self, $!) end end def create_child(child_name, file_contents=nil) - child = make_child(child_name) + child = make_child_entry(child_name) if child.exists? raise Chef::ChefFS::FileSystem::AlreadyExistsError.new(:create_child, child) end @@ -80,7 +83,7 @@ class Chef end def exists? - File.exists?(file_path) + File.exists?(file_path) && parent.can_have_child?(name, dir?) end def read @@ -99,7 +102,7 @@ class Chef protected - def make_child(child_name) + def make_child_entry(child_name) FileSystemEntry.new(child_name, self) end end diff --git a/lib/chef/chef_fs/file_system/memory_dir.rb b/lib/chef/chef_fs/file_system/memory_dir.rb index a7eda3c654..260a91693c 100644 --- a/lib/chef/chef_fs/file_system/memory_dir.rb +++ b/lib/chef/chef_fs/file_system/memory_dir.rb @@ -1,5 +1,4 @@ require 'chef/chef_fs/file_system/base_fs_dir' -require 'chef/chef_fs/file_system/nonexistent_fs_object' require 'chef/chef_fs/file_system/memory_file' class Chef @@ -13,8 +12,8 @@ class Chef attr_reader :children - def child(name) - @children.select { |child| child.name == name }.first || Chef::ChefFS::FileSystem::NonexistentFSObject.new(name, self) + def make_child_entry(name) + @children.select { |child| child.name == name }.first end def add_child(child) diff --git a/lib/chef/chef_fs/file_system/multiplexed_dir.rb b/lib/chef/chef_fs/file_system/multiplexed_dir.rb index 06d4af705d..70b827f85f 100644 --- a/lib/chef/chef_fs/file_system/multiplexed_dir.rb +++ b/lib/chef/chef_fs/file_system/multiplexed_dir.rb @@ -35,6 +35,21 @@ class Chef end end + def make_child_entry(name) + result = nil + multiplexed_dirs.each do |dir| + child_entry = dir.child(name) + if child_entry.exists? + if result + Chef::Log.warn("Child with name '#{child_entry.name}' found in multiple directories: #{result.parent.path_for_printing} and #{child_entry.parent.path_for_printing}") + else + result = child_entry + end + end + end + result + end + def can_have_child?(name, is_dir) write_dir.can_have_child?(name, is_dir) end diff --git a/lib/chef/chef_fs/file_system/nodes_dir.rb b/lib/chef/chef_fs/file_system/nodes_dir.rb index c3c48377cd..2610b06a82 100644 --- a/lib/chef/chef_fs/file_system/nodes_dir.rb +++ b/lib/chef/chef_fs/file_system/nodes_dir.rb @@ -33,7 +33,7 @@ class Chef def children begin @children ||= root.get_json(env_api_path).keys.sort.map do |key| - _make_child_entry("#{key}.json", true) + make_child_entry("#{key}.json", true) end rescue Timeout::Error => e raise Chef::ChefFS::FileSystem::OperationFailedError.new(:children, self, e), "Timeout retrieving children: #{e}" diff --git a/lib/chef/chef_fs/file_system/rest_list_dir.rb b/lib/chef/chef_fs/file_system/rest_list_dir.rb index 672fa444f1..0ac735a2c4 100644 --- a/lib/chef/chef_fs/file_system/rest_list_dir.rb +++ b/lib/chef/chef_fs/file_system/rest_list_dir.rb @@ -33,12 +33,6 @@ class Chef attr_reader :api_path attr_reader :data_handler - def child(name) - result = @children.select { |child| child.name == name }.first if @children - result ||= can_have_child?(name, false) ? - _make_child_entry(name) : NonexistentFSObject.new(name, self) - end - def can_have_child?(name, is_dir) name =~ /\.json$/ && !is_dir end @@ -46,7 +40,7 @@ class Chef def children begin @children ||= root.get_json(api_path).keys.sort.map do |key| - _make_child_entry("#{key}.json", true) + make_child_entry("#{key}.json", true) end rescue Timeout::Error => e raise Chef::ChefFS::FileSystem::OperationFailedError.new(:children, self, e), "Timeout retrieving children: #{e}" @@ -66,7 +60,7 @@ class Chef raise Chef::ChefFS::FileSystem::OperationFailedError.new(:create_child, self, e), "Parse error reading JSON creating child '#{name}': #{e}" end - result = _make_child_entry(name, true) + result = make_child_entry(name, true) if data_handler object = data_handler.normalize_for_post(object, result) @@ -106,7 +100,8 @@ class Chef parent.rest end - def _make_child_entry(name, exists = nil) + def make_child_entry(name, exists = nil) + @children.select { |child| child.name == name }.first if @children RestListEntry.new(name, self, exists) end end diff --git a/lib/chef/chef_fs/knife.rb b/lib/chef/chef_fs/knife.rb index 86872dab71..9101e455f8 100644 --- a/lib/chef/chef_fs/knife.rb +++ b/lib/chef/chef_fs/knife.rb @@ -17,6 +17,7 @@ # require 'chef/knife' +require 'pathname' class Chef module ChefFS @@ -63,7 +64,7 @@ class Chef # --chef-repo-path forcibly overrides all other paths if config[:chef_repo_path] Chef::Config[:chef_repo_path] = config[:chef_repo_path] - %w(acl client cookbook container data_bag environment group node role user).each do |variable_name| + Chef::ChefFS::Config::INFLECTIONS.each_value do |variable_name| Chef::Config.delete("#{variable_name}_path".to_sym) end end @@ -98,14 +99,41 @@ class Chef end def pattern_arg_from(arg) - # TODO support absolute file paths and not just patterns? Too much? - # Could be super useful in a world with multiple repo paths - if !@chef_fs_config.base_path && !Chef::ChefFS::PathUtils.is_absolute?(arg) - # Check if chef repo path is specified to give a better error message - ui.error("Attempt to use relative path '#{arg}' when current directory is outside the repository path") + inferred_path = nil + if Chef::ChefFS::PathUtils.is_absolute?(arg) + # We should be able to use this as-is - but the user might have incorrectly provided + # us with a path that is based off of the OS root path instead of the Chef-FS root. + # Do a quick and dirty sanity check. + if possible_server_path = @chef_fs_config.server_path(arg) + ui.warn("The absolute path provided is suspicious: #{arg}") + ui.warn("If you wish to refer to a file location, please provide a path that is rooted at the chef-repo.") + ui.warn("Consider writing '#{possible_server_path}' instead of '#{arg}'") + end + # Use the original path because we can't be sure. + inferred_path = arg + elsif arg[0,1] == '~' + # Let's be nice and fix it if possible - but warn the user. + ui.warn("A path relative to a user home directory has been provided: #{arg}") + ui.warn("Paths provided need to be rooted at the chef-repo being considered or be relative paths.") + inferred_path = @chef_fs_config.server_path(arg) + ui.warn("Using '#{inferred_path}' as the path instead of '#{arg}'.") + elsif Pathname.new(arg).absolute? + # It is definitely a system absolute path (such as C:\ or \\foo\bar) but it cannot be + # interpreted as a Chef-FS absolute path. Again attempt to be nice but warn the user. + ui.warn("An absolute file system path that isn't a server path was provided: #{arg}") + ui.warn("Paths provided need to be rooted at the chef-repo being considered or be relative paths.") + inferred_path = @chef_fs_config.server_path(arg) + ui.warn("Using '#{inferred_path}' as the path instead of '#{arg}'.") + elsif @chef_fs_config.base_path.nil? + # These are all relative paths. We can't resolve and root paths unless we are in the + # chef repo. + ui.error("Attempt to use relative path '#{arg}' when current directory is outside the repository path.") + ui.error("Current working directory is '#{@chef_fs_config.cwd}'.") exit(1) + else + inferred_path = Chef::ChefFS::PathUtils::join(@chef_fs_config.base_path, arg) end - Chef::ChefFS::FilePattern.relative_to(@chef_fs_config.base_path, arg) + Chef::ChefFS::FilePattern.new(inferred_path) end def format_path(entry) diff --git a/lib/chef/chef_fs/path_utils.rb b/lib/chef/chef_fs/path_utils.rb index 9ef75ce2e5..595f966378 100644 --- a/lib/chef/chef_fs/path_utils.rb +++ b/lib/chef/chef_fs/path_utils.rb @@ -23,31 +23,31 @@ class Chef module ChefFS class PathUtils - # If you are in 'source', this is what you would have to type to reach 'dest' - # relative_to('/a/b/c/d/e', '/a/b/x/y') == '../../c/d/e' - # relative_to('/a/b', '/a/b') == '.' - def self.relative_to(dest, source) - # Skip past the common parts - source_parts = Chef::ChefFS::PathUtils.split(source) - dest_parts = Chef::ChefFS::PathUtils.split(dest) - i = 0 - until i >= source_parts.length || i >= dest_parts.length || source_parts[i] != dest_parts[i] - i+=1 - end - # dot-dot up from 'source' to the common ancestor, then - # descend to 'dest' from the common ancestor - result = Chef::ChefFS::PathUtils.join(*(['..']*(source_parts.length-i) + dest_parts[i,dest.length-i])) - result == '' ? '.' : result - end + # A Chef-FS path is a path in a chef-repository that can be used to address + # both files on a local file-system as well as objects on a chef server. + # These paths are stricter than file-system paths allowed on various OSes. + # Absolute Chef-FS paths begin with "/" (on windows, "\" is acceptable as well). + # "/" is used as the path element separator (on windows, "\" is acceptable as well). + # No directory/path element may contain a literal "\" character. Any such characters + # encountered are either dealt with as separators (on windows) or as escape + # characters (on POSIX systems). Relative Chef-FS paths may use ".." or "." but + # may never use these to back-out of the root of a Chef-FS path. Any such extraneous + # ".."s are ignored. + # Chef-FS paths are case sensitive (since the paths on the server are). + # On OSes with case insensitive paths, you may be unable to locally deal with two + # objects whose server paths only differ by case. OTOH, the case of path segments + # that are outside the Chef-FS root (such as when looking at a file-system absolute + # path to discover the Chef-FS root path) are handled in accordance to the rules + # of the local file-system and OS. def self.join(*parts) return "" if parts.length == 0 # Determine if it started with a slash absolute = parts[0].length == 0 || parts[0].length > 0 && parts[0] =~ /^#{regexp_path_separator}/ # Remove leading and trailing slashes from each part so that the join will work (and the slash at the end will go away) - parts = parts.map { |part| part.gsub(/^\/|\/$/, "") } + parts = parts.map { |part| part.gsub(/^#{regexp_path_separator}+|#{regexp_path_separator}+$/, '') } # Don't join empty bits - result = parts.select { |part| part != "" }.join("/") + result = parts.select { |part| part != '' }.join('/') # Put the / back on absolute ? "/#{result}" : result end @@ -60,36 +60,67 @@ class Chef Chef::ChefFS::windows? ? '[\/\\\\]' : '/' end + # Given a server path, determines if it is absolute. + def self.is_absolute?(path) + !!(path =~ /^#{regexp_path_separator}/) + end # Given a path which may only be partly real (i.e. /x/y/z when only /x exists, # or /x/y/*/blah when /x/y/z/blah exists), call File.realpath on the biggest - # part that actually exists. + # part that actually exists. The paths operated on here are not Chef-FS paths. + # These are OS paths that may contain symlinks but may not also fully exist. # # If /x is a symlink to /blarghle, and has no subdirectories, then: # PathUtils.realest_path('/x/y/z') == '/blarghle/y/z' # PathUtils.realest_path('/x/*/z') == '/blarghle/*/z' # PathUtils.realest_path('/*/y/z') == '/*/y/z' - def self.realest_path(path) - path = Pathname.new(path) - begin - path.realpath.to_s - rescue Errno::ENOENT - dirname = path.dirname - if dirname - PathUtils.join(realest_path(dirname), path.basename.to_s) - else - path.to_s + # + # TODO: Move this to wherever util/path_helper is these days. + def self.realest_path(path, cwd = Dir.pwd) + path = File.expand_path(path, cwd) + parent_path = File.dirname(path) + suffix = [] + + # File.dirname happens to return the path as its own dirname if you're + # at the root (such as at \\foo\bar, C:\ or /) + until parent_path == path do + # This can occur if a path such as "C:" is given. Ruby gives the parent as "C:." + # for reasons only it knows. + raise ArgumentError "Invalid path segment #{path}" if parent_path.length > path.length + begin + path = File.realpath(path) + break + rescue Errno::ENOENT + suffix << File.basename(path) + path = parent_path + parent_path = File.dirname(path) end end + File.join(path, *suffix.reverse) end - def self.descendant_of?(path, ancestor) - path[0,ancestor.length] == ancestor && - (ancestor.length == path.length || path[ancestor.length,1] =~ /#{PathUtils.regexp_path_separator}/) + # Compares two path fragments according to the case-sentitivity of the host platform. + def self.os_path_eq?(left, right) + Chef::ChefFS::windows? ? left.casecmp(right) == 0 : left == right end - def self.is_absolute?(path) - path =~ /^#{regexp_path_separator}/ + # Given two general OS-dependent file paths, determines the relative path of the + # child with respect to the ancestor. Both child and ancestor must exist and be + # fully resolved - this is strictly a lexical comparison. No trailing slashes + # and other shenanigans are allowed. + # + # TODO: Move this to util/path_helper. + def self.descendant_path(path, ancestor) + candidate_fragment = path[0, ancestor.length] + return nil unless PathUtils.os_path_eq?(candidate_fragment, ancestor) + if ancestor.length == path.length + '' + elsif path[ancestor.length,1] =~ /#{PathUtils.regexp_path_separator}/ + path[ancestor.length+1..-1] + else + nil + end end + end end end diff --git a/lib/chef/client.rb b/lib/chef/client.rb index 86e92585e3..621ce3d489 100644 --- a/lib/chef/client.rb +++ b/lib/chef/client.rb @@ -3,7 +3,7 @@ # Author:: Christopher Walters (<cw@opscode.com>) # Author:: Christopher Brown (<cb@opscode.com>) # Author:: Tim Hinderliter (<tim@opscode.com>) -# Copyright:: Copyright (c) 2008-2011 Opscode, Inc. +# Copyright:: Copyright (c) 2008-2015 Chef Software, Inc. # License:: Apache License, Version 2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -302,19 +302,24 @@ class Chef @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 + error = if Chef::Config[:audit_mode] == :disabled + run_error || converge_error + else + e = 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 + e.fill_backtrace + e + end + Chef::Application.debug_stacktrace(error) raise error end diff --git a/lib/chef/config.rb b/lib/chef/config.rb index 9beb18b53e..6382af14c2 100644 --- a/lib/chef/config.rb +++ b/lib/chef/config.rb @@ -28,9 +28,21 @@ require 'chef-config/logger' ChefConfig.logger = Chef::Log require 'chef-config/config' - require 'chef/platform/query_helpers' +# Ohai::Config defines its own log_level and log_location. When loaded, it will +# override the default ChefConfig::Config values. We save them here before +# loading ohai/config so that we can override them again inside Chef::Config. +# +# REMOVEME once these configurables are removed from the top level of Ohai. +LOG_LEVEL = ChefConfig::Config[:log_level] unless defined? LOG_LEVEL +LOG_LOCATION = ChefConfig::Config[:log_location] unless defined? LOG_LOCATION + +# Load the ohai config into the chef config. We can't have an empty ohai +# configuration context because `ohai.plugins_path << some_path` won't work, +# and providing default ohai config values here isn't DRY. +require 'ohai/config' + class Chef Config = ChefConfig::Config @@ -49,5 +61,24 @@ class Chef evt_loggers end + # Override the default values that were set by Ohai. + # + # REMOVEME once these configurables are removed from the top level of Ohai. + default :log_level, LOG_LEVEL + default :log_location, LOG_LOCATION + + # Ohai::Config[:log_level] is deprecated and warns when set. Unfortunately, + # there is no way to distinguish between setting log_level and setting + # Ohai::Config[:log_level]. Since log_level and log_location are used by + # chef-client and other tools (e.g., knife), we will mute the warnings here + # by redefining the config_attr_writer to not warn for these options. + # + # REMOVEME once the warnings for these configurables are removed from Ohai. + [ :log_level, :log_location ].each do |option| + config_attr_writer option do |value| + value + end + end + end end diff --git a/lib/chef/constants.rb b/lib/chef/constants.rb new file mode 100644 index 0000000000..d39ce4c68d --- /dev/null +++ b/lib/chef/constants.rb @@ -0,0 +1,27 @@ +# +# Author:: John Keiser <jkeiser@chef.io> +# Copyright:: Copyright (c) 2015 Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +class Chef + NOT_PASSED = Object.new + def NOT_PASSED.to_s + "NOT_PASSED" + end + def NOT_PASSED.inspect + to_s + end + NOT_PASSED.freeze +end diff --git a/lib/chef/cookbook/metadata.rb b/lib/chef/cookbook/metadata.rb index 01a98fda39..9822920a7d 100644 --- a/lib/chef/cookbook/metadata.rb +++ b/lib/chef/cookbook/metadata.rb @@ -54,12 +54,13 @@ class Chef VERSION = 'version'.freeze SOURCE_URL = 'source_url'.freeze ISSUES_URL = 'issues_url'.freeze + PRIVACY = 'privacy'.freeze COMPARISON_FIELDS = [ :name, :description, :long_description, :maintainer, :maintainer_email, :license, :platforms, :dependencies, :recommendations, :suggestions, :conflicting, :providing, :replacing, :attributes, :groupings, :recipes, :version, - :source_url, :issues_url ] + :source_url, :issues_url, :privacy ] VERSION_CONSTRAINTS = {:depends => DEPENDENCIES, :recommends => RECOMMENDATIONS, @@ -116,6 +117,7 @@ class Chef @version = Version.new("0.0.0") @source_url = '' @issues_url = '' + @privacy = false @errors = [] end @@ -454,7 +456,8 @@ class Chef :recipes => { :kind_of => [ Array ], :default => [] }, :default => { :kind_of => [ String, Array, Hash, Symbol, Numeric, TrueClass, FalseClass ] }, :source_url => { :kind_of => String }, - :issues_url => { :kind_of => String } + :issues_url => { :kind_of => String }, + :privacy => { :kind_of => [ TrueClass, FalseClass ] } } ) options[:required] = remap_required_attribute(options[:required]) unless options[:required].nil? @@ -498,7 +501,8 @@ class Chef RECIPES => self.recipes, VERSION => self.version, SOURCE_URL => self.source_url, - ISSUES_URL => self.issues_url + ISSUES_URL => self.issues_url, + PRIVACY => self.privacy } end @@ -532,6 +536,7 @@ class Chef @version = o[VERSION] if o.has_key?(VERSION) @source_url = o[SOURCE_URL] if o.has_key?(SOURCE_URL) @issues_url = o[ISSUES_URL] if o.has_key?(ISSUES_URL) + @privacy = o[PRIVACY] if o.has_key?(PRIVACY) self end @@ -590,6 +595,23 @@ class Chef ) end + # + # Sets the cookbook's privacy flag, or returns it. + # + # === Parameters + # privacy<TrueClass,FalseClass>:: Whether this cookbook is private or not + # + # === Returns + # privacy<TrueClass,FalseClass>:: Whether this cookbook is private or not + # + def privacy(arg=nil) + set_or_return( + :privacy, + arg, + :kind_of => [ TrueClass, FalseClass ] + ) + end + private def run_validation diff --git a/lib/chef/cookbook/synchronizer.rb b/lib/chef/cookbook/synchronizer.rb index 1b96d0510b..fc8e739d73 100644 --- a/lib/chef/cookbook/synchronizer.rb +++ b/lib/chef/cookbook/synchronizer.rb @@ -131,7 +131,7 @@ class Chef files_remaining_by_cookbook[file.cookbook] -= 1 if files_remaining_by_cookbook[file.cookbook] == 0 - @events.synchronized_cookbook(file.cookbook.name) + @events.synchronized_cookbook(file.cookbook.name, file.cookbook) end end diff --git a/lib/chef/cookbook_version.rb b/lib/chef/cookbook_version.rb index 8d302eeec2..bff3146572 100644 --- a/lib/chef/cookbook_version.rb +++ b/lib/chef/cookbook_version.rb @@ -51,12 +51,12 @@ class Chef attr_accessor :metadata_filenames def status=(new_status) - Chef::Log.deprecation("Deprecated method `status' called from #{caller(1).first}. This method will be removed") + Chef.log_deprecation("Deprecated method `status' called. This method will be removed.", caller(1..1)) @status = new_status end def status - Chef::Log.deprecation("Deprecated method `status' called from #{caller(1).first}. This method will be removed") + Chef.log_deprecation("Deprecated method `status' called. This method will be removed.", caller(1..1)) @status end @@ -480,7 +480,7 @@ class Chef # @deprecated This method was used by the Ruby Chef Server and is no longer # needed. There is no replacement. def generate_manifest_with_urls(&url_generator) - Chef::Log.deprecation("Deprecated method #generate_manifest_with_urls called from #{caller(1).first}") + Chef.log_deprecation("Deprecated method #generate_manifest_with_urls.", caller(1..1)) rendered_manifest = manifest.dup COOKBOOK_SEGMENTS.each do |segment| diff --git a/lib/chef/delayed_evaluator.rb b/lib/chef/delayed_evaluator.rb new file mode 100644 index 0000000000..9f18a53445 --- /dev/null +++ b/lib/chef/delayed_evaluator.rb @@ -0,0 +1,21 @@ +# +# Author:: John Keiser <jkeiser@chef.io> +# Copyright:: Copyright (c) 2015 Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +class Chef + class DelayedEvaluator < Proc + end +end diff --git a/lib/chef/deprecation/mixin/template.rb b/lib/chef/deprecation/mixin/template.rb index 36d18ad90d..58a661c4bd 100644 --- a/lib/chef/deprecation/mixin/template.rb +++ b/lib/chef/deprecation/mixin/template.rb @@ -25,7 +25,7 @@ class Chef # == Deprecation::Provider::Mixin::Template # This module contains the deprecated functions of # Chef::Mixin::Template. These functions are refactored to different - # components. They are frozen and will be removed in Chef 12. + # components. They are frozen and will be removed in Chef 13. # module Template @@ -46,4 +46,3 @@ class Chef end end end - diff --git a/lib/chef/deprecation/provider/cookbook_file.rb b/lib/chef/deprecation/provider/cookbook_file.rb index dfbf4a39a4..92f5ce3623 100644 --- a/lib/chef/deprecation/provider/cookbook_file.rb +++ b/lib/chef/deprecation/provider/cookbook_file.rb @@ -24,7 +24,7 @@ class Chef # == Deprecation::Provider::CookbookFile # This module contains the deprecated functions of # Chef::Provider::CookbookFile. These functions are refactored to - # different components. They are frozen and will be removed in Chef 12. + # different components. They are frozen and will be removed in Chef 13. # module CookbookFile diff --git a/lib/chef/deprecation/provider/file.rb b/lib/chef/deprecation/provider/file.rb index 125f31fe10..31038ab3d8 100644 --- a/lib/chef/deprecation/provider/file.rb +++ b/lib/chef/deprecation/provider/file.rb @@ -25,7 +25,7 @@ class Chef # == Deprecation::Provider::File # This module contains the deprecated functions of # Chef::Provider::File. These functions are refactored to different - # components. They are frozen and will be removed in Chef 12. + # components. They are frozen and will be removed in Chef 13. # module File diff --git a/lib/chef/deprecation/provider/remote_file.rb b/lib/chef/deprecation/provider/remote_file.rb index 4452de67cd..c06a5cc695 100644 --- a/lib/chef/deprecation/provider/remote_file.rb +++ b/lib/chef/deprecation/provider/remote_file.rb @@ -23,7 +23,7 @@ class Chef # == Deprecation::Provider::RemoteFile # This module contains the deprecated functions of # Chef::Provider::RemoteFile. These functions are refactored to different - # components. They are frozen and will be removed in Chef 12. + # components. They are frozen and will be removed in Chef 13. # module RemoteFile @@ -83,4 +83,3 @@ class Chef end end end - diff --git a/lib/chef/deprecation/provider/template.rb b/lib/chef/deprecation/provider/template.rb index d7a228e97a..34e5f54b7e 100644 --- a/lib/chef/deprecation/provider/template.rb +++ b/lib/chef/deprecation/provider/template.rb @@ -25,7 +25,7 @@ class Chef # == Deprecation::Provider::Template # This module contains the deprecated functions of # Chef::Provider::Template. These functions are refactored to different - # components. They are frozen and will be removed in Chef 12. + # components. They are frozen and will be removed in Chef 13. # module Template diff --git a/lib/chef/deprecation/warnings.rb b/lib/chef/deprecation/warnings.rb index 34f468ff53..376629710e 100644 --- a/lib/chef/deprecation/warnings.rb +++ b/lib/chef/deprecation/warnings.rb @@ -25,10 +25,9 @@ class Chef m = instance_method(name) define_method(name) do |*args| message = [] - message << "Method '#{name}' of '#{self.class}' is deprecated. It will be removed in Chef 12." - message << "Please update your cookbooks accordingly. Accessed from:" - caller[0..3].each {|l| message << l} - Chef::Log.deprecation message + message << "Method '#{name}' of '#{self.class}' is deprecated. It will be removed in Chef 13." + message << "Please update your cookbooks accordingly." + Chef.log_deprecation(message, caller(0..3)) super(*args) end end diff --git a/lib/chef/dsl/reboot_pending.rb b/lib/chef/dsl/reboot_pending.rb index 7af67e94a5..c577118dd4 100644 --- a/lib/chef/dsl/reboot_pending.rb +++ b/lib/chef/dsl/reboot_pending.rb @@ -45,7 +45,7 @@ class Chef registry_key_exists?('HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Auto Update\RebootRequired') || # Vista + Server 2008 and newer may have reboots pending from CBS - registry_key_exists?('HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Component Based Servicing\RebootRequired') || + registry_key_exists?('HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Component Based Servicing\RebootPending') || # The mere existence of the UpdateExeVolatile key should indicate a pending restart for certain updates # http://support.microsoft.com/kb/832475 diff --git a/lib/chef/dsl/recipe.rb b/lib/chef/dsl/recipe.rb index d69f0a8f11..26c0ec6768 100644 --- a/lib/chef/dsl/recipe.rb +++ b/lib/chef/dsl/recipe.rb @@ -19,12 +19,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 @@ -122,9 +120,9 @@ class Chef def describe_self_for_error if respond_to?(:name) - %Q[`#{self.class.name} "#{name}"'] + %Q[`#{self.class} "#{name}"'] elsif respond_to?(:recipe_name) - %Q[`#{self.class.name} "#{recipe_name}"'] + %Q[`#{self.class} "#{recipe_name}"'] else to_s end @@ -142,8 +140,7 @@ class Chef # 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.") + Chef.log_deprecation("Calling method_missing(#{method_symbol.inspect}) directly is deprecated in Chef 12 and will be removed in Chef 13. Use public_send() or send() instead.") return send(method_symbol, *args, &block) end @@ -152,7 +149,7 @@ class Chef # 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.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 @@ -176,10 +173,32 @@ class Chef raise NameError, "No resource, method, or local variable named `#{method_symbol}' for #{describe_self_for_error}" end end + + module FullDSL + require 'chef/dsl/data_query' + require 'chef/dsl/platform_introspection' + require 'chef/dsl/include_recipe' + require 'chef/dsl/registry_helper' + require 'chef/dsl/reboot_pending' + require 'chef/dsl/audit' + require 'chef/dsl/powershell' + include Chef::DSL::DataQuery + include Chef::DSL::PlatformIntrospection + include Chef::DSL::IncludeRecipe + include Chef::DSL::Recipe + include Chef::DSL::RegistryHelper + include Chef::DSL::RebootPending + include Chef::DSL::Audit + include Chef::DSL::Powershell + end end end end +# Avoid circular references for things that are only used in instance methods +require 'chef/resource_builder' +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 index 1ce12ed0a0..49588ed516 100644 --- a/lib/chef/dsl/resources.rb +++ b/lib/chef/dsl/resources.rb @@ -10,14 +10,16 @@ class Chef 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) + def #{dsl_name}(*args, &block) + Chef.log_deprecation("Cannot create resource #{dsl_name} with more than one argument. All arguments except the name (\#{args[0].inspect}) will be ignored. This will cause an error in Chef 13. Arguments: \#{args}") if args.size > 1 + declare_resource(#{dsl_name.inspect}, args[0], 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) + define_method(dsl_name.to_sym) do |*args, &block| + Chef.log_deprecation("Cannot create resource #{dsl_name} with more than one argument. All arguments except the name (#{args[0].inspect}) will be ignored. This will cause an error in Chef 13. Arguments: #{args}") if args.size > 1 + declare_resource(dsl_name, args[0], caller[0], &block) end end end diff --git a/lib/chef/event_dispatch/base.rb b/lib/chef/event_dispatch/base.rb index 73fe25ec13..1c9a58be23 100644 --- a/lib/chef/event_dispatch/base.rb +++ b/lib/chef/event_dispatch/base.rb @@ -47,14 +47,19 @@ class Chef def ohai_completed(node) end - # Already have a client key, assuming this node has registered. + # Announce that we're not going to register the client. Generally because + # we already have the private key, or because we're deliberately not using + # a key. def skipping_registration(node_name, config) end - # About to attempt to register as +node_name+ + # About to attempt to create a private key registered to the server with + # client +node_name+. def registration_start(node_name, config) end + # Successfully created the private key and registered this client with the + # server. def registration_completed end @@ -118,8 +123,8 @@ class Chef def cookbook_sync_start(cookbook_count) end - # Called when cookbook +cookbook_name+ has been sync'd - def synchronized_cookbook(cookbook_name) + # Called when cookbook +cookbook+ has been sync'd + def synchronized_cookbook(cookbook_name, cookbook) end # Called when an individual file in a cookbook has been updated @@ -269,26 +274,37 @@ class Chef # def notifications_resolved # end + # + # Resource events and ordering: + # + # 1. Start the action + # - resource_action_start + # 2. Check the guard + # - resource_skipped: (goto 7) if only_if/not_if say to skip + # 3. Load the current resource + # - resource_current_state_loaded + # - resource_current_state_load_bypassed (if not why-run safe) + # 4. Check if why-run safe + # - resource_bypassed: (goto 7) if not why-run safe + # 5. During processing: + # - resource_update_applied: For each actual change (many per action) + # 6. Processing complete status: + # - resource_failed if the resource threw an exception while running + # - resource_failed_retriable: (goto 3) if resource failed and will be retried + # - resource_updated if the resource was updated (resource_update_applied will have been called) + # - resource_up_to_date if the resource was up to date (no resource_update_applied) + # 7. Processing complete: + # - resource_completed + # + # Called before action is executed on a resource. def resource_action_start(resource, action, notification_type=nil, notifier=nil) end - # Called when a resource fails, but will retry. - def resource_failed_retriable(resource, action, retry_count, exception) - end - - # Called when a resource fails and will not be retried. - def resource_failed(resource, action, exception) - end - # Called when a resource action has been skipped b/c of a conditional def resource_skipped(resource, action, conditional) end - # Called when a resource action has been completed - def resource_completed(resource) - end - # Called after #load_current_resource has run. def resource_current_state_loaded(resource, action, current_resource) end @@ -302,21 +318,33 @@ class Chef def resource_bypassed(resource, action, current_resource) end - # Called when a resource has no converge actions, e.g., it was already correct. - def resource_up_to_date(resource, action) - end - # Called when a change has been made to a resource. May be called multiple # times per resource, e.g., a file may have its content updated, and then # its permissions updated. def resource_update_applied(resource, action, update) end + # Called when a resource fails, but will retry. + def resource_failed_retriable(resource, action, retry_count, exception) + end + + # Called when a resource fails and will not be retried. + def resource_failed(resource, action, exception) + end + # Called after a resource has been completely converged, but only if # modifications were made. def resource_updated(resource, action) end + # Called when a resource has no converge actions, e.g., it was already correct. + def resource_up_to_date(resource, action) + end + + # Called when a resource action has been completed + def resource_completed(resource) + end + # A stream has opened. def stream_opened(stream, options = {}) end @@ -352,8 +380,9 @@ class Chef def whyrun_assumption(action, resource, message) end - ## TODO: deprecation warning. this way we can queue them up and present - # them all at once. + # Emit a message about something being deprecated. + def deprecation(message, location=caller(2..2)[0]) + end # An uncategorized message. This supports the case that a user needs to # pass output that doesn't fit into one of the callbacks above. Note that diff --git a/lib/chef/event_dispatch/dispatcher.rb b/lib/chef/event_dispatch/dispatcher.rb index 370f8c51b4..966a3f32ec 100644 --- a/lib/chef/event_dispatch/dispatcher.rb +++ b/lib/chef/event_dispatch/dispatcher.rb @@ -25,18 +25,30 @@ class Chef # define the forwarding in one go: # - # Define a method that will be forwarded to all - def self.def_forwarding_method(method_name) - define_method(method_name) do |*args| - @subscribers.each { |s| s.send(method_name, *args) } + def call_subscribers(method_name, *args) + @subscribers.each do |s| + # Skip new/unsupported event names. + next if !s.respond_to?(method_name) + mth = s.method(method_name) + # Trim arguments to match what the subscriber expects to allow + # adding new arguments without breaking compat. + args = args.take(mth.arity) if mth.arity < args.size && mth.arity >= 0 + mth.call(*args) end end (Base.instance_methods - Object.instance_methods).each do |method_name| - def_forwarding_method(method_name) + class_eval <<-EOM + def #{method_name}(*args) + call_subscribers(#{method_name.inspect}, *args) + end + EOM end + # Special case deprecation, since it needs to know its caller + def deprecation(message, location=caller(2..2)[0]) + call_subscribers(:deprecation, message, location) + end end end end - diff --git a/lib/chef/event_dispatch/dsl.rb b/lib/chef/event_dispatch/dsl.rb new file mode 100644 index 0000000000..c6f21c9b45 --- /dev/null +++ b/lib/chef/event_dispatch/dsl.rb @@ -0,0 +1,64 @@ +# +# Author:: Ranjib Dey (<ranjib@linux.com>) +# Copyright:: Copyright (c) 2015 Ranjib Dey +# 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/event_dispatch/base' +require 'chef/exceptions' +require 'chef/config' + +class Chef + module EventDispatch + class DSL + attr_reader :handler + + def initialize(name) + klass = Class.new(Chef::EventDispatch::Base) do + attr_reader :name + end + @handler = klass.new + @handler.instance_variable_set(:@name, name) + + # Use event.register API to add anonymous handler if Chef.run_context + # and associated event dispatcher is set, else fallback to + # Chef::Config[:hanlder] + if Chef.run_context && Chef.run_context.events + Chef::Log.debug("Registering handler '#{name}' using events api") + Chef.run_context.events.register(handler) + else + Chef::Log.debug("Registering handler '#{name}' using global config") + Chef::Config[:event_handlers] << handler + end + end + + # Adds a new event handler derived from base handler + # with user defined block against a chef event + # + # @return [Chef::EventDispatch::Base] a base handler object + def on(event_type, &block) + validate!(event_type) + handler.define_singleton_method(event_type) do |*args| + instance_exec(*args, &block) + end + end + + private + def validate!(event_type) + all_event_types = (Chef::EventDispatch::Base.instance_methods - Object.instance_methods) + raise Chef::Exceptions::InvalidEventType, "Invalid event type: #{event_type}" unless all_event_types.include?(event_type) + end + end + end +end diff --git a/lib/chef/exceptions.rb b/lib/chef/exceptions.rb index dd0bac3cf9..e3649c068b 100644 --- a/lib/chef/exceptions.rb +++ b/lib/chef/exceptions.rb @@ -17,12 +17,16 @@ # See the License for the specific language governing permissions and # limitations under the License. +require 'chef-config/exceptions' + class Chef # == Chef::Exceptions # Chef's custom exceptions are all contained within the Chef::Exceptions # namespace. class Exceptions + ConfigurationError = ChefConfig::ConfigurationError + # Backcompat with Chef::ShellOut code: require 'mixlib/shellout/exceptions' @@ -68,7 +72,6 @@ class Chef class DuplicateRole < RuntimeError; end class ValidationFailed < ArgumentError; end class InvalidPrivateKey < ArgumentError; end - class ConfigurationError < ArgumentError; end class MissingKeyAttribute < ArgumentError; end class KeyCommandInputError < ArgumentError; end class InvalidKeyArgument < ArgumentError; end @@ -97,7 +100,14 @@ class Chef class ConflictingMembersInGroup < ArgumentError; end class InvalidResourceReference < RuntimeError; end class ResourceNotFound < RuntimeError; end + class ProviderNotFound < RuntimeError; end + NoProviderAvailable = ProviderNotFound class VerificationNotFound < RuntimeError; end + class InvalidEventType < ArgumentError; end + class MultipleIdentityError < RuntimeError; end + # Used in Resource::ActionProvider#load_current_resource to denote that + # the resource doesn't actually exist (for example, the file does not exist) + class CurrentValueDoesNotExist < RuntimeError; end # Can't find a Resource of this type that is valid on this platform. class NoSuchResourceType < NameError @@ -119,6 +129,23 @@ class Chef class EnclosingDirectoryDoesNotExist < ArgumentError; end # Errors originating from calls to the Win32 API class Win32APIError < RuntimeError; end + + class Win32NetAPIError < Win32APIError + attr_reader :msg, :error_code + def initialize(msg, error_code) + @msg = msg + @error_code = error_code + + formatted_message = "" + formatted_message << "---- Begin Win32 API output ----\n" + formatted_message << "Net Api Error Code: #{error_code}\n" + formatted_message << "Net Api Error Message: #{msg}\n" + formatted_message << "---- End Win32 API output ----\n" + + super(formatted_message) + end + end + # Thrown when Win32 API layer binds to non-existent Win32 function. Occurs # when older versions of Windows don't support newer Win32 API functions. class Win32APIFunctionNotImplemented < NotImplementedError; end @@ -218,8 +245,6 @@ class Chef class ChildConvergeError < RuntimeError; end - class NoProviderAvailable < RuntimeError; end - class DeprecatedFeatureError < RuntimeError; def initalize(message) super("#{message} (raising error due to treat_deprecation_warnings_as_errors being set)") diff --git a/lib/chef/formatters/base.rb b/lib/chef/formatters/base.rb index c901068aa0..d3756ef00c 100644 --- a/lib/chef/formatters/base.rb +++ b/lib/chef/formatters/base.rb @@ -212,6 +212,9 @@ class Chef file_load_failed(path, exception) end + def deprecation(message, location=caller(2..2)[0]) + Chef::Log.deprecation("#{message} at #{location}") + end end diff --git a/lib/chef/formatters/doc.rb b/lib/chef/formatters/doc.rb index e76a940c38..614cc44e6d 100644 --- a/lib/chef/formatters/doc.rb +++ b/lib/chef/formatters/doc.rb @@ -22,6 +22,7 @@ class Chef @failed_audits = 0 @start_time = Time.now @end_time = @start_time + @skipped_resources = 0 end def elapsed_time @@ -33,7 +34,7 @@ class Chef end def total_resources - @up_to_date_resources + @updated_resources + @up_to_date_resources + @updated_resources + @skipped_resources end def total_audits @@ -42,6 +43,26 @@ class Chef def run_completed(node) @end_time = Time.now + # Print out deprecations. + if !deprecations.empty? + puts_line "" + puts_line "Deprecated features used!" + deprecations.each do |message, locations| + if locations.size == 1 + puts_line " #{message} at #{locations.size} location:" + else + puts_line " #{message} at #{locations.size} locations:" + end + locations.each do |location| + prefix = " - " + Array(location).each do |line| + puts_line "#{prefix}#{line}" + prefix = " " + end + end + end + puts_line "" + end if Chef::Config[:why_run] puts_line "Chef Client finished, #{@updated_resources}/#{total_resources} resources would have been updated" else @@ -132,9 +153,9 @@ class Chef indent end - # Called when cookbook +cookbook_name+ has been sync'd - def synchronized_cookbook(cookbook_name) - puts_line "- #{cookbook_name}" + # Called when cookbook +cookbook+ has been sync'd + def synchronized_cookbook(cookbook_name, cookbook) + puts_line "- #{cookbook.name} (#{cookbook.version})" end # Called when an individual file in a cookbook has been updated @@ -236,6 +257,7 @@ class Chef # Called when a resource action has been skipped b/c of a conditional def resource_skipped(resource, action, conditional) + @skipped_resources += 1 # TODO: more info about conditional puts " (skipped due to #{conditional.short_description})", :stream => resource unindent @@ -334,6 +356,16 @@ class Chef end end + def deprecation(message, location=caller(2..2)[0]) + if Chef::Config[:treat_deprecation_warnings_as_errors] + super + end + + # Save deprecations to the screen until the end + deprecations[message] ||= Set.new + deprecations[message] << location + end + def indent indent_by(2) end @@ -341,6 +373,12 @@ class Chef def unindent indent_by(-2) end + + protected + + def deprecations + @deprecations ||= {} + end end end end diff --git a/lib/chef/formatters/error_inspectors/compile_error_inspector.rb b/lib/chef/formatters/error_inspectors/compile_error_inspector.rb index d64d5e7b01..fe418ed485 100644 --- a/lib/chef/formatters/error_inspectors/compile_error_inspector.rb +++ b/lib/chef/formatters/error_inspectors/compile_error_inspector.rb @@ -44,6 +44,18 @@ class Chef error_description.section("Cookbook Trace:", traceback) error_description.section("Relevant File Content:", context) end + + if exception_message_modifying_frozen? + msg = <<-MESSAGE + Chef calls the freeze method on certain ruby objects to prevent + pollution across multiple instances. Specifically, resource + properties have frozen default values to avoid modifying the + property for all instances of a resource. Try modifying the + particular instance variable or using an instance accessor instead. + MESSAGE + + error_description.section("Additional information:", msg.gsub(/^ {6}/, '')) + end end def context @@ -111,6 +123,10 @@ class Chef end end + def exception_message_modifying_frozen? + exception.message.include?("can't modify frozen") + end + end end diff --git a/lib/chef/formatters/minimal.rb b/lib/chef/formatters/minimal.rb index a189cc67eb..3862951f76 100644 --- a/lib/chef/formatters/minimal.rb +++ b/lib/chef/formatters/minimal.rb @@ -109,8 +109,8 @@ class Chef puts "Synchronizing cookbooks" end - # Called when cookbook +cookbook_name+ has been sync'd - def synchronized_cookbook(cookbook_name) + # Called when cookbook +cookbook+ has been sync'd + def synchronized_cookbook(cookbook_name, cookbook) print "." end diff --git a/lib/chef/guard_interpreter/resource_guard_interpreter.rb b/lib/chef/guard_interpreter/resource_guard_interpreter.rb index d4b386a15a..8cff3bc032 100644 --- a/lib/chef/guard_interpreter/resource_guard_interpreter.rb +++ b/lib/chef/guard_interpreter/resource_guard_interpreter.rb @@ -68,7 +68,10 @@ class Chef run_action = action || @resource.action begin - @resource.run_action(run_action) + # Coerce to an array to be safe. This could happen with a legacy + # resource or something overriding the default_action code in a + # subclass. + Array(run_action).each {|action_to_run| @resource.run_action(action_to_run) } resource_updated = @resource.updated rescue Mixlib::ShellOut::ShellCommandFailed resource_updated = nil diff --git a/lib/chef/http/http_request.rb b/lib/chef/http/http_request.rb index 7582f4458f..1baf5724ae 100644 --- a/lib/chef/http/http_request.rb +++ b/lib/chef/http/http_request.rb @@ -40,7 +40,7 @@ class Chef engine = defined?(RUBY_ENGINE) ? RUBY_ENGINE : "ruby" - UA_COMMON = "/#{::Chef::VERSION} (#{engine}-#{RUBY_VERSION}-p#{RUBY_PATCHLEVEL}; ohai-#{Ohai::VERSION}; #{RUBY_PLATFORM}; +http://opscode.com)" + UA_COMMON = "/#{::Chef::VERSION} (#{engine}-#{RUBY_VERSION}-p#{RUBY_PATCHLEVEL}; ohai-#{Ohai::VERSION}; #{RUBY_PLATFORM}; +https://chef.io)" DEFAULT_UA = "Chef Client" << UA_COMMON USER_AGENT = "User-Agent".freeze diff --git a/lib/chef/knife.rb b/lib/chef/knife.rb index 4a93697a1b..46e968827e 100644 --- a/lib/chef/knife.rb +++ b/lib/chef/knife.rb @@ -87,6 +87,7 @@ class Chef def self.inherited(subclass) unless subclass.unnamed? subcommands[subclass.snake_case_name] = subclass + subcommand_files[subclass.snake_case_name] += [caller[0].split(/:\d+/).first] end end @@ -121,17 +122,29 @@ class Chef end def self.subcommand_loader - @subcommand_loader ||= Knife::SubcommandLoader.new(chef_config_dir) + @subcommand_loader ||= Chef::Knife::SubcommandLoader.for_config(chef_config_dir) end def self.load_commands @commands_loaded ||= subcommand_loader.load_commands end + def self.guess_category(args) + subcommand_loader.guess_category(args) + end + + def self.subcommand_class_from(args) + subcommand_loader.command_class_from(args) || subcommand_not_found!(args) + end + def self.subcommands @@subcommands ||= {} end + def self.subcommand_files + @@subcommand_files ||= Hash.new([]) + end + def self.subcommands_by_category unless @subcommands_by_category @subcommands_by_category = Hash.new { |hash, key| hash[key] = [] } @@ -142,30 +155,6 @@ class Chef @subcommands_by_category end - # Print the list of subcommands knife knows about. If +preferred_category+ - # is given, only subcommands in that category are shown - def self.list_commands(preferred_category=nil) - load_commands - - category_desc = preferred_category ? preferred_category + " " : '' - msg "Available #{category_desc}subcommands: (for details, knife SUB-COMMAND --help)\n\n" - - if preferred_category && subcommands_by_category.key?(preferred_category) - commands_to_show = {preferred_category => subcommands_by_category[preferred_category]} - else - commands_to_show = subcommands_by_category - end - - commands_to_show.sort.each do |category, commands| - next if category =~ /deprecated/i - msg "** #{category.upcase} COMMANDS **" - commands.sort.each do |command| - msg subcommands[command].banner if subcommands[command] - end - msg - end - end - # Shared with subclasses @@chef_config_dir = nil @@ -206,7 +195,6 @@ class Chef Chef::Log.level(:debug) end - load_commands subcommand_class = subcommand_class_from(args) subcommand_class.options = options.merge!(subcommand_class.options) subcommand_class.load_deps @@ -215,34 +203,6 @@ class Chef instance.run_with_pretty_exceptions end - def self.guess_category(args) - category_words = args.select {|arg| arg =~ /^(([[:alnum:]])[[:alnum:]\_\-]+)$/ } - category_words.map! {|w| w.split('-')}.flatten! - matching_category = nil - while (!matching_category) && (!category_words.empty?) - candidate_category = category_words.join(' ') - matching_category = candidate_category if subcommands_by_category.key?(candidate_category) - matching_category || category_words.pop - end - matching_category - end - - def self.subcommand_class_from(args) - command_words = args.select {|arg| arg =~ /^(([[:alnum:]])[[:alnum:]\_\-]+)$/ } - - subcommand_class = nil - - while ( !subcommand_class ) && ( !command_words.empty? ) - snake_case_class_name = command_words.join("_") - unless subcommand_class = subcommands[snake_case_class_name] - command_words.pop - end - end - # see if we got the command as e.g., knife node-list - subcommand_class ||= subcommands[args.first.gsub('-', '_')] - subcommand_class || subcommand_not_found!(args) - end - def self.dependency_loaders @dependency_loaders ||= [] end @@ -265,7 +225,13 @@ class Chef # Error out and print usage. probably because the arguments given by the # user could not be resolved to a subcommand. def self.subcommand_not_found!(args) - ui.fatal("Cannot find sub command for: '#{args.join(' ')}'") + ui.fatal("Cannot find subcommand for: '#{args.join(' ')}'") + + # Mention rehash when the subcommands cache(plugin_manifest.json) is used + if subcommand_loader.is_a?(Chef::Knife::SubcommandLoader::HashedCommandLoader) || + subcommand_loader.is_a?(Chef::Knife::SubcommandLoader::CustomManifestLoader) + ui.info("If this is a recently installed plugin, please run 'knife rehash' to update the subcommands cache.") + end if category_commands = guess_category(args) list_commands(category_commands) @@ -280,6 +246,20 @@ class Chef exit 10 end + def self.list_commands(preferred_category=nil) + category_desc = preferred_category ? preferred_category + " " : '' + msg "Available #{category_desc}subcommands: (for details, knife SUB-COMMAND --help)\n\n" + subcommand_loader.list_commands(preferred_category).sort.each do |category, commands| + next if category =~ /deprecated/i + msg "** #{category.upcase} COMMANDS **" + commands.sort.each do |command| + subcommand_loader.load_command(command) + msg subcommands[command].banner if subcommands[command] + end + msg + end + end + def self.reset_config_path! @@chef_config_dir = nil end diff --git a/lib/chef/knife/bootstrap.rb b/lib/chef/knife/bootstrap.rb index 5b29591fcc..f173b6b909 100644 --- a/lib/chef/knife/bootstrap.rb +++ b/lib/chef/knife/bootstrap.rb @@ -143,6 +143,12 @@ class Chef :proc => lambda { |o| o.split(/[\s,]+/) }, :default => [] + option :tags, + :long => "--tags TAGS", + :description => "Comma separated list of tags to apply to the node", + :proc => lambda { |o| o.split(/[\s,]+/) }, + :default => [] + option :first_boot_attributes, :short => "-j JSON_ATTRIBS", :long => "--json-attributes", diff --git a/lib/chef/knife/bootstrap/chef_vault_handler.rb b/lib/chef/knife/bootstrap/chef_vault_handler.rb index 749f61e6da..f658957499 100644 --- a/lib/chef/knife/bootstrap/chef_vault_handler.rb +++ b/lib/chef/knife/bootstrap/chef_vault_handler.rb @@ -15,6 +15,7 @@ # See the License for the specific language governing permissions and # limitations under the License. # +require 'chef/knife/bootstrap' class Chef class Knife diff --git a/lib/chef/knife/bootstrap/client_builder.rb b/lib/chef/knife/bootstrap/client_builder.rb index b9c1d98bec..59b0cabd49 100644 --- a/lib/chef/knife/bootstrap/client_builder.rb +++ b/lib/chef/knife/bootstrap/client_builder.rb @@ -20,6 +20,7 @@ require 'chef/node' require 'chef/rest' require 'chef/api_client/registration' require 'chef/api_client' +require 'chef/knife/bootstrap' require 'tmpdir' class Chef @@ -140,6 +141,9 @@ class Chef node.run_list(normalized_run_list) node.normal_attrs = first_boot_attributes if first_boot_attributes node.environment(environment) if environment + (knife_config[:tags] || []).each do |tag| + node.tags << tag + end node end end diff --git a/lib/chef/knife/bootstrap/templates/archlinux-gems.erb b/lib/chef/knife/bootstrap/templates/archlinux-gems.erb deleted file mode 100644 index 55d2c0cc12..0000000000 --- a/lib/chef/knife/bootstrap/templates/archlinux-gems.erb +++ /dev/null @@ -1,76 +0,0 @@ -bash -c ' -<%= "export http_proxy=\"#{knife_config[:bootstrap_proxy]}\"" if knife_config[:bootstrap_proxy] -%> - -if [ ! -f /usr/bin/chef-client ]; then - pacman -Syy - pacman -S --noconfirm ruby ntp base-devel - ntpdate -u pool.ntp.org - gem install ohai --no-user-install --no-document --verbose - gem install chef --no-user-install --no-document --verbose <%= Chef::VERSION %> -fi - -mkdir -p /etc/chef - -<% if validation_key -%> -cat > /etc/chef/validation.pem <<'EOP' -<%= validation_key %> -EOP -chmod 0600 /etc/chef/validation.pem -<% end -%> - -<% if encrypted_data_bag_secret -%> -cat > /etc/chef/encrypted_data_bag_secret <<'EOP' -<%= encrypted_data_bag_secret %> -EOP -chmod 0600 /etc/chef/encrypted_data_bag_secret -<% end -%> - -<% unless trusted_certs.empty? -%> -mkdir -p /etc/chef/trusted_certs -<%= trusted_certs %> -<% end -%> - -<%# Generate Ohai Hints -%> -<% unless @chef_config[:knife][:hints].nil? || @chef_config[:knife][:hints].empty? -%> -mkdir -p /etc/chef/ohai/hints - -<% @chef_config[:knife][:hints].each do |name, hash| -%> -cat > /etc/chef/ohai/hints/<%= name %>.json <<'EOP' -<%= Chef::JSONCompat.to_json(hash) %> -EOP -<% end -%> -<% end -%> - -<% if client_pem -%> -cat > /etc/chef/client.pem <<'EOP' -<%= ::File.read(::File.expand_path(client_pem)) %> -EOP -chmod 0600 /etc/chef/client.pem -<% end -%> - -cat > /etc/chef/client.rb <<'EOP' -log_level :info -log_location STDOUT -chef_server_url "<%= @chef_config[:chef_server_url] %>" -validation_client_name "<%= @chef_config[:validation_client_name] %>" -<% if @config[:chef_node_name] -%> -node_name "<%= @config[:chef_node_name] %>" -<% else -%> -# Using default node name (fqdn) -<% end -%> -# ArchLinux follows the Filesystem Hierarchy Standard -file_cache_path "/var/cache/chef" -file_backup_path "/var/lib/chef/backup" -pid_file "/var/run/chef/client.pid" -cache_options({ :path => "/var/cache/chef/checksums", :skip_expires => true}) -<% if knife_config[:bootstrap_proxy] %> -http_proxy "<%= knife_config[:bootstrap_proxy] %>" -https_proxy "<%= knife_config[:bootstrap_proxy] %>" -<% end -%> -EOP - -cat > /etc/chef/first-boot.json <<'EOP' -<%= Chef::JSONCompat.to_json(first_boot) %> -EOP - -<%= start_chef %>' diff --git a/lib/chef/knife/bootstrap/templates/chef-aix.erb b/lib/chef/knife/bootstrap/templates/chef-aix.erb deleted file mode 100644 index 45fbba7b48..0000000000 --- a/lib/chef/knife/bootstrap/templates/chef-aix.erb +++ /dev/null @@ -1,72 +0,0 @@ -ksh -c ' - -function exists { - if type $1 >/dev/null 2>&1 - then - return 0 - else - return 1 - fi -} - -if ! exists /usr/bin/chef-client; then - <% if @chef_config[:aix_package] -%> - # Read the download URL/location from knife.rb with option aix_package - rm -rf /tmp/chef_installer # ensure there no older pkg - echo "<%= @chef_config[:aix_package] %>" - perl -e '\''use LWP::Simple; getprint($ARGV[0]);'\'' <%= @chef_config[:aix_package] %> > /tmp/chef_installer - installp -aYF -d /tmp/chef_installer chef - <% else -%> - echo ":aix_package location is not set in knife.rb" - exit - <% end -%> -fi - -mkdir -p /etc/chef - -<% if client_pem -%> -cat > /etc/chef/client.pem <<'EOP' -<%= ::File.read(::File.expand_path(client_pem)) %> -EOP -chmod 0600 /etc/chef/client.pem -<% end -%> - -<% if validation_key -%> -cat > /etc/chef/validation.pem <<'EOP' -<%= validation_key %> -EOP -chmod 0600 /etc/chef/validation.pem -<% end -%> - -<% if encrypted_data_bag_secret -%> -cat > /etc/chef/encrypted_data_bag_secret <<'EOP' -<%= encrypted_data_bag_secret %> -EOP -chmod 0600 /etc/chef/encrypted_data_bag_secret -<% end -%> - -<% unless trusted_certs.empty? -%> -mkdir -p /etc/chef/trusted_certs -<%= trusted_certs %> -<% end -%> - -<%# Generate Ohai Hints -%> -<% unless @chef_config[:knife][:hints].nil? || @chef_config[:knife][:hints].empty? -%> -mkdir -p /etc/chef/ohai/hints - -<% @chef_config[:knife][:hints].each do |name, hash| -%> -cat > /etc/chef/ohai/hints/<%= name %>.json <<'EOP' -<%= Chef::JSONCompat.to_json(hash) %> -EOP -<% end -%> -<% end -%> - -cat > /etc/chef/client.rb <<'EOP' -<%= config_content %> -EOP - -cat > /etc/chef/first-boot.json <<'EOP' -<%= Chef::JSONCompat.to_json(first_boot) %> -EOP - -<%= start_chef %>' diff --git a/lib/chef/knife/bootstrap/templates/chef-full.erb b/lib/chef/knife/bootstrap/templates/chef-full.erb index 335b1f181c..575aec0f50 100644 --- a/lib/chef/knife/bootstrap/templates/chef-full.erb +++ b/lib/chef/knife/bootstrap/templates/chef-full.erb @@ -12,7 +12,7 @@ tmp_dir="$tmp/install.sh.$$" (umask 077 && mkdir $tmp_dir) || exit 1 exists() { - if command -v $1 &>/dev/null + if command -v $1 >/dev/null 2>&1 then return 0 else @@ -166,12 +166,12 @@ do_download() { <%= knife_config[:bootstrap_install_command] %> <% else %> install_sh="<%= knife_config[:bootstrap_url] ? knife_config[:bootstrap_url] : "https://www.opscode.com/chef/install.sh" %>" - if ! exists /usr/bin/chef-client; then + if test -f /usr/bin/chef-client; then + echo "-----> Existing Chef installation detected" + else echo "-----> Installing Chef Omnibus (<%= latest_current_chef_version_string %>)" do_download ${install_sh} $tmp_dir/install.sh sh $tmp_dir/install.sh -P chef <%= latest_current_chef_version_string %> - else - echo "-----> Existing Chef installation detected" fi <% end %> @@ -226,6 +226,6 @@ cat > /etc/chef/first-boot.json <<EOP <%= Chef::JSONCompat.to_json(first_boot) %> EOP -echo "Starting first Chef Client run..." +echo "Starting the first Chef Client run..." <%= start_chef %>' diff --git a/lib/chef/knife/client_bulk_delete.rb b/lib/chef/knife/client_bulk_delete.rb index f2be772759..b439e6f995 100644 --- a/lib/chef/knife/client_bulk_delete.rb +++ b/lib/chef/knife/client_bulk_delete.rb @@ -23,7 +23,7 @@ class Chef class ClientBulkDelete < Knife deps do - require 'chef/api_client' + require 'chef/api_client_v1' require 'chef/json_compat' end @@ -39,7 +39,7 @@ class Chef ui.fatal("You must supply a regular expression to match the results against") exit 42 end - all_clients = Chef::ApiClient.list(true) + all_clients = Chef::ApiClientV1.list(true) matcher = /#{name_args[0]}/ clients_to_delete = {} diff --git a/lib/chef/knife/client_create.rb b/lib/chef/knife/client_create.rb index 570c1ee950..fa9a1a7e32 100644 --- a/lib/chef/knife/client_create.rb +++ b/lib/chef/knife/client_create.rb @@ -23,7 +23,7 @@ class Chef class ClientCreate < Knife deps do - require 'chef/api_client' + require 'chef/api_client_v1' require 'chef/json_compat' end @@ -57,12 +57,12 @@ class Chef banner "knife client create CLIENTNAME (options)" def client - @client_field ||= Chef::ApiClient.new + @client_field ||= Chef::ApiClientV1.new end def create_client(client) # should not be using save :( bad behavior - client.save + Chef::ApiClientV1.from_hash(client).save end def run @@ -93,7 +93,7 @@ class Chef output = edit_data(client) final_client = create_client(output) - ui.info("Created #{output}") + ui.info("Created #{final_client}") # output private_key if one if final_client.private_key diff --git a/lib/chef/knife/client_delete.rb b/lib/chef/knife/client_delete.rb index d7d302ee1d..a49c0867a8 100644 --- a/lib/chef/knife/client_delete.rb +++ b/lib/chef/knife/client_delete.rb @@ -23,7 +23,7 @@ class Chef class ClientDelete < Knife deps do - require 'chef/api_client' + require 'chef/api_client_v1' require 'chef/json_compat' end @@ -43,8 +43,8 @@ class Chef exit 1 end - delete_object(Chef::ApiClient, @client_name, 'client') { - object = Chef::ApiClient.load(@client_name) + delete_object(Chef::ApiClientV1, @client_name, 'client') { + object = Chef::ApiClientV1.load(@client_name) if object.validator unless config[:delete_validators] ui.fatal("You must specify --delete-validators to delete the validator client #{@client_name}") diff --git a/lib/chef/knife/client_edit.rb b/lib/chef/knife/client_edit.rb index c81bce902a..5dcd8f212b 100644 --- a/lib/chef/knife/client_edit.rb +++ b/lib/chef/knife/client_edit.rb @@ -23,7 +23,7 @@ class Chef class ClientEdit < Knife deps do - require 'chef/api_client' + require 'chef/api_client_v1' require 'chef/json_compat' end @@ -38,7 +38,15 @@ class Chef exit 1 end - edit_object(Chef::ApiClient, @client_name) + original_data = Chef::ApiClientV1.load(@client_name).to_hash + edited_client = edit_data(original_data) + if original_data != edited_client + client = Chef::ApiClientV1.from_hash(edited_client) + client.save + ui.msg("Saved #{client}.") + else + ui.msg("Client unchanged, not saving.") + end end end end diff --git a/lib/chef/knife/client_list.rb b/lib/chef/knife/client_list.rb index da0bf12dc3..d8a3698b6a 100644 --- a/lib/chef/knife/client_list.rb +++ b/lib/chef/knife/client_list.rb @@ -23,7 +23,7 @@ class Chef class ClientList < Knife deps do - require 'chef/api_client' + require 'chef/api_client_v1' require 'chef/json_compat' end @@ -35,7 +35,7 @@ class Chef :description => "Show corresponding URIs" def run - output(format_list_for_display(Chef::ApiClient.list)) + output(format_list_for_display(Chef::ApiClientV1.list)) end end end diff --git a/lib/chef/knife/client_reregister.rb b/lib/chef/knife/client_reregister.rb index 666fd09fd2..b94761e718 100644 --- a/lib/chef/knife/client_reregister.rb +++ b/lib/chef/knife/client_reregister.rb @@ -23,7 +23,7 @@ class Chef class ClientReregister < Knife deps do - require 'chef/api_client' + require 'chef/api_client_v1' require 'chef/json_compat' end @@ -43,7 +43,7 @@ class Chef exit 1 end - client = Chef::ApiClient.reregister(@client_name) + client = Chef::ApiClientV1.reregister(@client_name) Chef::Log.debug("Updated client data: #{client.inspect}") key = client.private_key if config[:file] diff --git a/lib/chef/knife/client_show.rb b/lib/chef/knife/client_show.rb index 822848fdc2..bdac3f9758 100644 --- a/lib/chef/knife/client_show.rb +++ b/lib/chef/knife/client_show.rb @@ -25,7 +25,7 @@ class Chef include Knife::Core::MultiAttributeReturnOption deps do - require 'chef/api_client' + require 'chef/api_client_v1' require 'chef/json_compat' end @@ -40,7 +40,7 @@ class Chef exit 1 end - client = Chef::ApiClient.load(@client_name) + client = Chef::ApiClientV1.load(@client_name) output(format_for_display(client)) end diff --git a/lib/chef/knife/core/bootstrap_context.rb b/lib/chef/knife/core/bootstrap_context.rb index 7197653489..867b6fe366 100644 --- a/lib/chef/knife/core/bootstrap_context.rb +++ b/lib/chef/knife/core/bootstrap_context.rb @@ -163,11 +163,14 @@ CONFIG end def first_boot - (@config[:first_boot_attributes] || {}).merge(:run_list => @run_list) + (@config[:first_boot_attributes] || {}).tap do |attributes| + attributes.merge!(:run_list => @run_list) + attributes.merge!(:tags => @config[:tags]) if @config[:tags] && !@config[:tags].empty? + end end private - + # Returns a string for copying the trusted certificates on the workstation to the system being bootstrapped # This string should contain both the commands necessary to both create the files, as well as their content def trusted_certs_content diff --git a/lib/chef/knife/core/custom_manifest_loader.rb b/lib/chef/knife/core/custom_manifest_loader.rb new file mode 100644 index 0000000000..c19e749f32 --- /dev/null +++ b/lib/chef/knife/core/custom_manifest_loader.rb @@ -0,0 +1,69 @@ +# 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/version' +class Chef + class Knife + class SubcommandLoader + + # + # Load a subcommand from a user-supplied + # manifest file + # + class CustomManifestLoader < Chef::Knife::SubcommandLoader + attr_accessor :manifest + def initialize(chef_config_dir, plugin_manifest) + super(chef_config_dir) + @manifest = plugin_manifest + end + + # If the user has created a ~/.chef/plugin_manifest.json file, we'll use + # that instead of inspecting the on-system gems to find the plugins. The + # file format is expected to look like: + # + # { "plugins": { + # "knife-ec2": { + # "paths": [ + # "/home/alice/.rubymanagerthing/gems/knife-ec2-x.y.z/lib/chef/knife/ec2_server_create.rb", + # "/home/alice/.rubymanagerthing/gems/knife-ec2-x.y.z/lib/chef/knife/ec2_server_delete.rb" + # ] + # } + # } + # } + # + # Extraneous content in this file is ignored. This is intentional so that we + # can adapt the file format for potential behavior changes to knife in + # the future. + def find_subcommands_via_manifest + # Format of subcommand_files is "relative_path" (something you can + # Kernel.require()) => full_path. The relative path isn't used + # currently, so we just map full_path => full_path. + subcommand_files = {} + manifest["plugins"].each do |plugin_name, plugin_manifest| + plugin_manifest["paths"].each do |cmd_path| + subcommand_files[cmd_path] = cmd_path + end + end + subcommand_files.merge(find_subcommands_via_dirglob) + end + + def subcommand_files + subcommand_files ||= (find_subcommands_via_manifest.values + site_subcommands).flatten.uniq + end + end + end + end +end diff --git a/lib/chef/knife/core/gem_glob_loader.rb b/lib/chef/knife/core/gem_glob_loader.rb new file mode 100644 index 0000000000..d09131aacb --- /dev/null +++ b/lib/chef/knife/core/gem_glob_loader.rb @@ -0,0 +1,138 @@ +# Author:: Christopher Brown (<cb@opscode.com>) +# Author:: Daniel DeLeo (<dan@opscode.com>) +# Copyright:: Copyright (c) 2009-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/version' +require 'chef/util/path_helper' +class Chef + class Knife + class SubcommandLoader + class GemGlobLoader < Chef::Knife::SubcommandLoader + MATCHES_CHEF_GEM = %r{/chef-[\d]+\.[\d]+\.[\d]+}.freeze + MATCHES_THIS_CHEF_GEM = %r{/chef-#{Chef::VERSION}(-\w+)?(-\w+)?/}.freeze + + def subcommand_files + @subcommand_files ||= (gem_and_builtin_subcommands.values + site_subcommands).flatten.uniq + end + + # Returns a Hash of paths to knife commands built-in to chef, or installed via gem. + # If rubygems is not installed, falls back to globbing the knife directory. + # The Hash is of the form {"relative/path" => "/absolute/path"} + #-- + # Note: the "right" way to load the plugins is to require the relative path, i.e., + # require 'chef/knife/command' + # but we're getting frustrated by bugs at every turn, and it's slow besides. So + # subcommand loader has been modified to load the plugins by using Kernel.load + # with the absolute path. + def gem_and_builtin_subcommands + require 'rubygems' + find_subcommands_via_rubygems + rescue LoadError + find_subcommands_via_dirglob + end + + def find_subcommands_via_dirglob + # The "require paths" of the core knife subcommands bundled with chef + files = Dir[File.join(Chef::Util::PathHelper.escape_glob(File.expand_path('../../../knife', __FILE__)), '*.rb')] + subcommand_files = {} + files.each do |knife_file| + rel_path = knife_file[/#{CHEF_ROOT}#{Regexp.escape(File::SEPARATOR)}(.*)\.rb/,1] + subcommand_files[rel_path] = knife_file + end + subcommand_files + end + + def find_subcommands_via_rubygems + files = find_files_latest_gems 'chef/knife/*.rb' + subcommand_files = {} + files.each do |file| + rel_path = file[/(#{Regexp.escape File.join('chef', 'knife', '')}.*)\.rb/, 1] + + # When not installed as a gem (ChefDK/appbundler in particular), AND + # a different version of Chef is installed via gems, `files` will + # include some files from the 'other' Chef install. If this contains + # a knife command that doesn't exist in this version of Chef, we will + # get a LoadError later when we try to require it. + next if from_different_chef_version?(file) + + subcommand_files[rel_path] = file + end + + subcommand_files.merge(find_subcommands_via_dirglob) + end + + private + + def find_files_latest_gems(glob, check_load_path=true) + files = [] + + if check_load_path + files = $LOAD_PATH.map { |load_path| + Dir["#{File.expand_path glob, Chef::Util::PathHelper.escape_glob(load_path)}#{Gem.suffix_pattern}"] + }.flatten.select { |file| File.file? file.untaint } + end + + gem_files = latest_gem_specs.map do |spec| + # Gem::Specification#matches_for_glob wasn't added until RubyGems 1.8 + if spec.respond_to? :matches_for_glob + spec.matches_for_glob("#{glob}#{Gem.suffix_pattern}") + else + check_spec_for_glob(spec, glob) + end + end.flatten + + files.concat gem_files + files.uniq! if check_load_path + + return files + end + + def latest_gem_specs + @latest_gem_specs ||= if Gem::Specification.respond_to? :latest_specs + Gem::Specification.latest_specs(true) # find prerelease gems + else + Gem.source_index.latest_specs(true) + end + end + + def check_spec_for_glob(spec, glob) + dirs = if spec.require_paths.size > 1 then + "{#{spec.require_paths.join(',')}}" + else + spec.require_paths.first + end + + glob = File.join(Chef::Util::PathHelper.escape_glob(spec.full_gem_path, dirs), glob) + + Dir[glob].map { |f| f.untaint } + end + + def from_different_chef_version?(path) + matches_any_chef_gem?(path) && !matches_this_chef_gem?(path) + end + + def matches_any_chef_gem?(path) + path =~ MATCHES_CHEF_GEM + end + + def matches_this_chef_gem?(path) + path =~ MATCHES_THIS_CHEF_GEM + end + end + end + end +end diff --git a/lib/chef/knife/core/hashed_command_loader.rb b/lib/chef/knife/core/hashed_command_loader.rb new file mode 100644 index 0000000000..6eb3635726 --- /dev/null +++ b/lib/chef/knife/core/hashed_command_loader.rb @@ -0,0 +1,80 @@ +# Author:: Steven Danna (<steve@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/version' +class Chef + class Knife + class SubcommandLoader + # + # Load a subcommand from a pre-computed path + # for the given command. + # + class HashedCommandLoader < Chef::Knife::SubcommandLoader + KEY = '_autogenerated_command_paths' + + attr_accessor :manifest + def initialize(chef_config_dir, plugin_manifest) + super(chef_config_dir) + @manifest = plugin_manifest + end + + def guess_category(args) + category_words = positional_arguments(args) + category_words.map! { |w| w.split('-') }.flatten! + find_longest_key(manifest[KEY]["plugins_by_category"], category_words, ' ') + end + + def list_commands(pref_category=nil) + if pref_category || manifest[KEY]["plugins_by_category"].key?(pref_category) + { pref_category => manifest[KEY]["plugins_by_category"][pref_category] } + else + manifest[KEY]["plugins_by_category"] + end + end + + def subcommand_files + manifest[KEY]["plugins_paths"].values.flatten + end + + def load_command(args) + paths = manifest[KEY]["plugins_paths"][subcommand_for_args(args)] + if paths.nil? || paths.empty? || (! paths.is_a? Array) + false + else + paths.each do |sc| + if File.exists?(sc) + Kernel.load sc + else + Chef::Log.error "The file #{sc} is missing for subcommand '#{subcommand_for_args(args)}'. Please rehash to update the subcommands cache." + return false + end + end + true + end + end + + def subcommand_for_args(args) + if manifest[KEY]["plugins_paths"].key?(args) + args + else + find_longest_key(manifest[KEY]["plugins_paths"], args, "_") + end + end + end + end + end +end diff --git a/lib/chef/knife/core/object_loader.rb b/lib/chef/knife/core/object_loader.rb index 698b09ac84..97ca381471 100644 --- a/lib/chef/knife/core/object_loader.rb +++ b/lib/chef/knife/core/object_loader.rb @@ -18,6 +18,7 @@ require 'ffi_yajl' require 'chef/util/path_helper' +require 'chef/data_bag_item' class Chef class Knife diff --git a/lib/chef/knife/core/subcommand_loader.rb b/lib/chef/knife/core/subcommand_loader.rb index a8705c724f..808e053c40 100644 --- a/lib/chef/knife/core/subcommand_loader.rb +++ b/lib/chef/knife/core/subcommand_loader.rb @@ -18,105 +18,120 @@ require 'chef/version' require 'chef/util/path_helper' +require 'chef/knife/core/gem_glob_loader' +require 'chef/knife/core/hashed_command_loader' +require 'chef/knife/core/custom_manifest_loader' + class Chef class Knife + # + # Public Methods of a Subcommand Loader + # + # load_commands - loads all available subcommands + # load_command(args) - loads subcommands for the given args + # list_commands(args) - lists all available subcommands, + # optionally filtering by category + # subcommand_files - returns an array of all subcommand files + # that could be loaded + # commnad_class_from(args) - returns the subcommand class for the + # user-requested command + # class SubcommandLoader - - MATCHES_CHEF_GEM = %r{/chef-[\d]+\.[\d]+\.[\d]+}.freeze - MATCHES_THIS_CHEF_GEM = %r{/chef-#{Chef::VERSION}(-\w+)?(-\w+)?/}.freeze - attr_reader :chef_config_dir attr_reader :env - def initialize(chef_config_dir, env=nil) + # A small factory method. Eventually, this is the only place + # where SubcommandLoader should know about its subclasses, but + # to maintain backwards compatibility many of the instance + # methods in this base class contain default implementations + # of the functions sub classes should otherwise provide + # or directly instantiate the appropriate subclass + def self.for_config(chef_config_dir) + if autogenerated_manifest? + Chef::Log.debug("Using autogenerated hashed command manifest #{plugin_manifest_path}") + Knife::SubcommandLoader::HashedCommandLoader.new(chef_config_dir, plugin_manifest) + elsif custom_manifest? + Chef.log_deprecation("Using custom manifest #{plugin_manifest_path} is deprecated. Please use a `knife rehash` autogenerated manifest instead.") + Knife::SubcommandLoader::CustomManifestLoader.new(chef_config_dir, plugin_manifest) + else + Knife::SubcommandLoader::GemGlobLoader.new(chef_config_dir) + end + end + + def self.plugin_manifest? + plugin_manifest_path && File.exist?(plugin_manifest_path) + end + + def self.autogenerated_manifest? + plugin_manifest? && plugin_manifest.key?(HashedCommandLoader::KEY) + end + + def self.custom_manifest? + plugin_manifest? && plugin_manifest.key?("plugins") + end + + def self.plugin_manifest + Chef::JSONCompat.from_json(File.read(plugin_manifest_path)) + end + + def self.plugin_manifest_path + Chef::Util::PathHelper.home('.chef', 'plugin_manifest.json') + end + + def initialize(chef_config_dir, env = nil) @chef_config_dir = chef_config_dir - @forced_activate = {} # Deprecated and un-used instance variable. @env = env unless env.nil? - Chef::Log.deprecation("The env argument to Chef::Knife::SubcommandLoader is deprecated. If you are using env to inject/mock HOME, consider mocking Chef::Util::PathHelper.home instead.") + Chef.log_deprecation("The env argument to Chef::Knife::SubcommandLoader is deprecated. If you are using env to inject/mock HOME, consider mocking Chef::Util::PathHelper.home instead.") end end # Load all the sub-commands def load_commands + return true if @loaded subcommand_files.each { |subcommand| Kernel.load subcommand } - true + @loaded = true end - # Returns an Array of paths to knife commands located in chef_config_dir/plugins/knife/ - # and ~/.chef/plugins/knife/ - def site_subcommands - user_specific_files = [] - - if chef_config_dir - user_specific_files.concat Dir.glob(File.expand_path("plugins/knife/*.rb", Chef::Util::PathHelper.escape_glob(chef_config_dir))) - end - - # finally search ~/.chef/plugins/knife/*.rb - Chef::Util::PathHelper.home('.chef', 'plugins', 'knife') do |p| - user_specific_files.concat Dir.glob(File.join(Chef::Util::PathHelper.escape_glob(p), '*.rb')) - end + def force_load + @loaded=false + load_commands + end - user_specific_files + def load_command(_command_args) + load_commands end - # Returns a Hash of paths to knife commands built-in to chef, or installed via gem. - # If rubygems is not installed, falls back to globbing the knife directory. - # The Hash is of the form {"relative/path" => "/absolute/path"} - #-- - # Note: the "right" way to load the plugins is to require the relative path, i.e., - # require 'chef/knife/command' - # but we're getting frustrated by bugs at every turn, and it's slow besides. So - # subcommand loader has been modified to load the plugins by using Kernel.load - # with the absolute path. - def gem_and_builtin_subcommands - if have_plugin_manifest? - find_subcommands_via_manifest + def list_commands(pref_cat = nil) + load_commands + if pref_cat && Chef::Knife.subcommands_by_category.key?(pref_cat) + { pref_cat => Chef::Knife.subcommands_by_category[pref_cat] } else - # search all gems for chef/knife/*.rb - require 'rubygems' - find_subcommands_via_rubygems + Chef::Knife.subcommands_by_category end - rescue LoadError - find_subcommands_via_dirglob end - def subcommand_files - @subcommand_files ||= (gem_and_builtin_subcommands.values + site_subcommands).flatten.uniq + def command_class_from(args) + cmd_words = positional_arguments(args) + load_command(cmd_words) + result = Chef::Knife.subcommands[find_longest_key(Chef::Knife.subcommands, + cmd_words, '_')] + result || Chef::Knife.subcommands[args.first.gsub('-', '_')] end - # If the user has created a ~/.chef/plugin_manifest.json file, we'll use - # that instead of inspecting the on-system gems to find the plugins. The - # file format is expected to look like: - # - # { "plugins": { - # "knife-ec2": { - # "paths": [ - # "/home/alice/.rubymanagerthing/gems/knife-ec2-x.y.z/lib/chef/knife/ec2_server_create.rb", - # "/home/alice/.rubymanagerthing/gems/knife-ec2-x.y.z/lib/chef/knife/ec2_server_delete.rb" - # ] - # } - # } - # } - # - # Extraneous content in this file is ignored. This intentional so that we - # can adapt the file format for potential behavior changes to knife in - # the future. - def find_subcommands_via_manifest - # Format of subcommand_files is "relative_path" (something you can - # Kernel.require()) => full_path. The relative path isn't used - # currently, so we just map full_path => full_path. - subcommand_files = {} - plugin_manifest["plugins"].each do |plugin_name, plugin_manifest| - plugin_manifest["paths"].each do |cmd_path| - subcommand_files[cmd_path] = cmd_path - end - end - subcommand_files.merge(find_subcommands_via_dirglob) + def guess_category(args) + category_words = positional_arguments(args) + category_words.map! { |w| w.split('-') }.flatten! + find_longest_key(Chef::Knife.subcommands_by_category, + category_words, ' ') end + + # + # This is shared between the custom_manifest_loader and the gem_glob_loader + # def find_subcommands_via_dirglob # The "require paths" of the core knife subcommands bundled with chef files = Dir[File.join(Chef::Util::PathHelper.escape_glob(File.expand_path('../../../knife', __FILE__)), '*.rb')] @@ -128,95 +143,65 @@ class Chef subcommand_files end - def find_subcommands_via_rubygems - files = find_files_latest_gems 'chef/knife/*.rb' - subcommand_files = {} - files.each do |file| - rel_path = file[/(#{Regexp.escape File.join('chef', 'knife', '')}.*)\.rb/, 1] - - # When not installed as a gem (ChefDK/appbundler in particular), AND - # a different version of Chef is installed via gems, `files` will - # include some files from the 'other' Chef install. If this contains - # a knife command that doesn't exist in this version of Chef, we will - # get a LoadError later when we try to require it. - next if from_different_chef_version?(file) - - subcommand_files[rel_path] = file - end - - subcommand_files.merge(find_subcommands_via_dirglob) - end - - def have_plugin_manifest? - plugin_manifest_path && File.exist?(plugin_manifest_path) - end - - def plugin_manifest - Chef::JSONCompat.from_json(File.read(plugin_manifest_path)) - end - - def plugin_manifest_path - Chef::Util::PathHelper.home('.chef', 'plugin_manifest.json') + # + # Subclassses should define this themselves. Eventually, this will raise a + # NotImplemented error, but for now, we mimic the behavior the user was likely + # to get in the past. + # + def subcommand_files + Chef.log_deprecation "Using Chef::Knife::SubcommandLoader directly is deprecated. +Please use Chef::Knife::SubcommandLoader.for_config(chef_config_dir, env)" + @subcommand_files ||= if Chef::Knife::SubcommandLoader.plugin_manifest? + Chef::Knife::SubcommandLoader::CustomManifestLoader.new(chef_config_dir, env).subcommand_files + else + Chef::Knife::SubcommandLoader::GemGlobLoader.new(chef_config_dir, env).subcommand_files + end end - private - - def find_files_latest_gems(glob, check_load_path=true) - files = [] - - if check_load_path - files = $LOAD_PATH.map { |load_path| - Dir["#{File.expand_path glob, Chef::Util::PathHelper.escape_glob(load_path)}#{Gem.suffix_pattern}"] - }.flatten.select { |file| File.file? file.untaint } - end - - gem_files = latest_gem_specs.map do |spec| - # Gem::Specification#matches_for_glob wasn't added until RubyGems 1.8 - if spec.respond_to? :matches_for_glob - spec.matches_for_glob("#{glob}#{Gem.suffix_pattern}") + # + # Utility function for finding an element in a hash given an array + # of words and a separator. We find the the longest key in the + # hash composed of the given words joined by the separator. + # + def find_longest_key(hash, words, sep = '_') + match = nil + until match || words.empty? + candidate = words.join(sep) + if hash.key?(candidate) + match = candidate else - check_spec_for_glob(spec, glob) + words.pop end - end.flatten - - files.concat gem_files - files.uniq! if check_load_path - - return files - end - - def latest_gem_specs - @latest_gem_specs ||= if Gem::Specification.respond_to? :latest_specs - Gem::Specification.latest_specs(true) # find prerelease gems - else - Gem.source_index.latest_specs(true) end + match end - def check_spec_for_glob(spec, glob) - dirs = if spec.require_paths.size > 1 then - "{#{spec.require_paths.join(',')}}" - else - spec.require_paths.first - end - - glob = File.join(Chef::Util::PathHelper.escape_glob(spec.full_gem_path, dirs), glob) - - Dir[glob].map { |f| f.untaint } + # + # The positional arguments from the argument list provided by the + # users. Used to search for subcommands and categories. + # + # @return [Array<String>] + # + def positional_arguments(args) + args.select { |arg| arg =~ /^(([[:alnum:]])[[:alnum:]\_\-]+)$/ } end - def from_different_chef_version?(path) - matches_any_chef_gem?(path) && !matches_this_chef_gem?(path) - end + # Returns an Array of paths to knife commands located in + # chef_config_dir/plugins/knife/ and ~/.chef/plugins/knife/ + def site_subcommands + user_specific_files = [] - def matches_any_chef_gem?(path) - path =~ MATCHES_CHEF_GEM - end + if chef_config_dir + user_specific_files.concat Dir.glob(File.expand_path("plugins/knife/*.rb", Chef::Util::PathHelper.escape_glob(chef_config_dir))) + end - def matches_this_chef_gem?(path) - path =~ MATCHES_THIS_CHEF_GEM - end + # finally search ~/.chef/plugins/knife/*.rb + Chef::Util::PathHelper.home('.chef', 'plugins', 'knife') do |p| + user_specific_files.concat Dir.glob(File.join(Chef::Util::PathHelper.escape_glob(p), '*.rb')) + end + user_specific_files + end end end end diff --git a/lib/chef/knife/node_run_list_remove.rb b/lib/chef/knife/node_run_list_remove.rb index 4b8953a264..ef03c176b8 100644 --- a/lib/chef/knife/node_run_list_remove.rb +++ b/lib/chef/knife/node_run_list_remove.rb @@ -42,7 +42,18 @@ class Chef entries = @name_args[1].split(',').map { |e| e.strip } end - entries.each { |e| node.run_list.remove(e) } + # iterate over the list of things to remove, + # warning if one of them was not found + entries.each do |e| + if node.run_list.find { |rli| e == rli.to_s } + node.run_list.remove(e) + else + ui.warn "#{e} is not in the run list" + unless e =~ /^(recipe|role)\[/ + ui.warn '(did you forget recipe[] or role[] around it?)' + end + end + end node.save diff --git a/lib/chef/knife/null.rb b/lib/chef/knife/null.rb new file mode 100644 index 0000000000..0b5058e8ea --- /dev/null +++ b/lib/chef/knife/null.rb @@ -0,0 +1,10 @@ +class Chef + class Knife + class Null < Chef::Knife + banner "knife null" + + def run + end + end + end +end diff --git a/lib/chef/knife/osc_user_create.rb b/lib/chef/knife/osc_user_create.rb index c368296040..6c3415473f 100644 --- a/lib/chef/knife/osc_user_create.rb +++ b/lib/chef/knife/osc_user_create.rb @@ -27,7 +27,7 @@ class Chef class OscUserCreate < Knife deps do - require 'chef/osc_user' + require 'chef/user' require 'chef/json_compat' end @@ -69,7 +69,7 @@ class Chef exit 1 end - user = Chef::OscUser.new + user = Chef::User.new user.name(@user_name) user.admin(config[:admin]) user.password config[:user_password] @@ -79,7 +79,7 @@ class Chef end output = edit_data(user) - user = Chef::OscUser.from_hash(output).create + user = Chef::User.from_hash(output).create ui.info("Created #{user}") if user.private_key diff --git a/lib/chef/knife/osc_user_delete.rb b/lib/chef/knife/osc_user_delete.rb index d6fbd4a6a9..5cd4f10413 100644 --- a/lib/chef/knife/osc_user_delete.rb +++ b/lib/chef/knife/osc_user_delete.rb @@ -28,7 +28,7 @@ class Chef class OscUserDelete < Knife deps do - require 'chef/osc_user' + require 'chef/user' require 'chef/json_compat' end @@ -43,7 +43,7 @@ class Chef exit 1 end - delete_object(Chef::OscUser, @user_name) + delete_object(Chef::User, @user_name) end end diff --git a/lib/chef/knife/osc_user_edit.rb b/lib/chef/knife/osc_user_edit.rb index 4c38674d08..526475db05 100644 --- a/lib/chef/knife/osc_user_edit.rb +++ b/lib/chef/knife/osc_user_edit.rb @@ -28,7 +28,7 @@ class Chef class OscUserEdit < Knife deps do - require 'chef/osc_user' + require 'chef/user' require 'chef/json_compat' end @@ -43,10 +43,10 @@ class Chef exit 1 end - original_user = Chef::OscUser.load(@user_name).to_hash + original_user = Chef::User.load(@user_name).to_hash edited_user = edit_data(original_user) if original_user != edited_user - user = Chef::OscUser.from_hash(edited_user) + user = Chef::User.from_hash(edited_user) user.update ui.msg("Saved #{user}.") else diff --git a/lib/chef/knife/osc_user_list.rb b/lib/chef/knife/osc_user_list.rb index 92f049cd19..84fca31899 100644 --- a/lib/chef/knife/osc_user_list.rb +++ b/lib/chef/knife/osc_user_list.rb @@ -28,7 +28,7 @@ class Chef class OscUserList < Knife deps do - require 'chef/osc_user' + require 'chef/user' require 'chef/json_compat' end @@ -40,7 +40,7 @@ class Chef :description => "Show corresponding URIs" def run - output(format_list_for_display(Chef::OscUser.list)) + output(format_list_for_display(Chef::User.list)) end end end diff --git a/lib/chef/knife/osc_user_reregister.rb b/lib/chef/knife/osc_user_reregister.rb index a71e0aa677..163b286fe0 100644 --- a/lib/chef/knife/osc_user_reregister.rb +++ b/lib/chef/knife/osc_user_reregister.rb @@ -28,7 +28,7 @@ class Chef class OscUserReregister < Knife deps do - require 'chef/osc_user' + require 'chef/user' require 'chef/json_compat' end @@ -48,7 +48,7 @@ class Chef exit 1 end - user = Chef::OscUser.load(@user_name).reregister + user = Chef::User.load(@user_name).reregister Chef::Log.debug("Updated user data: #{user.inspect}") key = user.private_key if config[:file] diff --git a/lib/chef/knife/osc_user_show.rb b/lib/chef/knife/osc_user_show.rb index 6a41ddae88..cb3a77585a 100644 --- a/lib/chef/knife/osc_user_show.rb +++ b/lib/chef/knife/osc_user_show.rb @@ -30,7 +30,7 @@ class Chef include Knife::Core::MultiAttributeReturnOption deps do - require 'chef/osc_user' + require 'chef/user' require 'chef/json_compat' end @@ -45,7 +45,7 @@ class Chef exit 1 end - user = Chef::OscUser.load(@user_name) + user = Chef::User.load(@user_name) output(format_for_display(user)) end diff --git a/lib/chef/knife/rehash.rb b/lib/chef/knife/rehash.rb new file mode 100644 index 0000000000..6f1fd91911 --- /dev/null +++ b/lib/chef/knife/rehash.rb @@ -0,0 +1,62 @@ +# +# Author:: Steven Danna <steve@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/core/subcommand_loader' + +class Chef + class Knife + class Rehash < Chef::Knife + banner "knife rehash" + + def run + if ! Chef::Knife::SubcommandLoader.autogenerated_manifest? + ui.msg "Using knife-rehash will speed up knife's load time by caching the location of subcommands on disk." + ui.msg "However, you will need to update the cache by running `knife rehash` anytime you install a new knife plugin." + else + reload_plugins + end + write_hash(generate_hash) + end + + def reload_plugins + Chef::Knife::SubcommandLoader::GemGlobLoader.new(@@chef_config_dir).load_commands + end + + def generate_hash + output = if Chef::Knife::SubcommandLoader.plugin_manifest? + Chef::Knife::SubcommandLoader.plugin_manifest + else + { Chef::Knife::SubcommandLoader::HashedCommandLoader::KEY => {}} + end + output[Chef::Knife::SubcommandLoader::HashedCommandLoader::KEY]['plugins_paths'] = Chef::Knife.subcommand_files + output[Chef::Knife::SubcommandLoader::HashedCommandLoader::KEY]['plugins_by_category'] = Chef::Knife.subcommands_by_category + output + end + + def write_hash(data) + plugin_manifest_dir = File.expand_path('..', Chef::Knife::SubcommandLoader.plugin_manifest_path) + FileUtils.mkdir_p(plugin_manifest_dir) unless File.directory?(plugin_manifest_dir) + File.open(Chef::Knife::SubcommandLoader.plugin_manifest_path, 'w') do |f| + f.write(Chef::JSONCompat.to_json_pretty(data)) + ui.msg "Knife subcommands are cached in #{Chef::Knife::SubcommandLoader.plugin_manifest_path}. Delete this file to disable the caching." + end + end + end + end +end diff --git a/lib/chef/knife/ssl_check.rb b/lib/chef/knife/ssl_check.rb index c5fe4fc1aa..d71eacfc7e 100644 --- a/lib/chef/knife/ssl_check.rb +++ b/lib/chef/knife/ssl_check.rb @@ -73,11 +73,12 @@ class Chef exit 1 end - def verify_peer_socket @verify_peer_socket ||= begin tcp_connection = TCPSocket.new(host, port) - OpenSSL::SSL::SSLSocket.new(tcp_connection, verify_peer_ssl_context) + ssl_client = OpenSSL::SSL::SSLSocket.new(tcp_connection, verify_peer_ssl_context) + ssl_client.hostname = host + ssl_client end end diff --git a/lib/chef/knife/user_create.rb b/lib/chef/knife/user_create.rb index e73f6be8b6..995573cd03 100644 --- a/lib/chef/knife/user_create.rb +++ b/lib/chef/knife/user_create.rb @@ -27,7 +27,7 @@ class Chef attr_accessor :user_field deps do - require 'chef/user' + require 'chef/user_v1' require 'chef/json_compat' end @@ -61,11 +61,11 @@ class Chef banner "knife user create USERNAME DISPLAY_NAME FIRST_NAME LAST_NAME EMAIL PASSWORD (options)" def user - @user_field ||= Chef::User.new + @user_field ||= Chef::UserV1.new end def create_user_from_hash(hash) - Chef::User.from_hash(hash).create + Chef::UserV1.from_hash(hash).create end def osc_11_warning diff --git a/lib/chef/knife/user_delete.rb b/lib/chef/knife/user_delete.rb index 803be6b90c..828cd51588 100644 --- a/lib/chef/knife/user_delete.rb +++ b/lib/chef/knife/user_delete.rb @@ -23,7 +23,7 @@ class Chef class UserDelete < Knife deps do - require 'chef/user' + require 'chef/user_v1' require 'chef/json_compat' end @@ -55,7 +55,7 @@ EOF if Kernel.block_given? object = block.call else - object = Chef::User.load(user_name) + object = Chef::UserV1.load(user_name) object.destroy end @@ -77,10 +77,10 @@ EOF # 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) + # delete_object(Chef::UserV1, @user_name) # # Also delete our override of delete_object above - object = Chef::User.load(@user_name) + object = Chef::UserV1.load(@user_name) # OSC 11 case if object.username.nil? diff --git a/lib/chef/knife/user_edit.rb b/lib/chef/knife/user_edit.rb index dd2fc02743..c3a4326ee8 100644 --- a/lib/chef/knife/user_edit.rb +++ b/lib/chef/knife/user_edit.rb @@ -23,7 +23,7 @@ class Chef class UserEdit < Knife deps do - require 'chef/user' + require 'chef/user_v1' require 'chef/json_compat' end @@ -56,8 +56,7 @@ EOF exit 1 end - original_user = Chef::User.load(@user_name).to_hash - + original_user = Chef::UserV1.load(@user_name).to_hash # DEPRECATION NOTE # Remove this if statement and corrosponding code post OSC 11 support. # @@ -69,11 +68,11 @@ EOF 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 = Chef::UserV1.from_hash(edited_user) user.update ui.msg("Saved #{user}.") else - ui.msg("User unchaged, not saving.") + ui.msg("User unchanged, not saving.") end end diff --git a/lib/chef/knife/user_list.rb b/lib/chef/knife/user_list.rb index 7ae43dadc9..6a130392b9 100644 --- a/lib/chef/knife/user_list.rb +++ b/lib/chef/knife/user_list.rb @@ -25,7 +25,7 @@ class Chef class UserList < Knife deps do - require 'chef/user' + require 'chef/user_v1' require 'chef/json_compat' end @@ -37,7 +37,7 @@ class Chef :description => "Show corresponding URIs" def run - output(format_list_for_display(Chef::User.list)) + output(format_list_for_display(Chef::UserV1.list)) end end diff --git a/lib/chef/knife/user_reregister.rb b/lib/chef/knife/user_reregister.rb index eab2245025..09fd1cd2d6 100644 --- a/lib/chef/knife/user_reregister.rb +++ b/lib/chef/knife/user_reregister.rb @@ -23,7 +23,7 @@ class Chef class UserReregister < Knife deps do - require 'chef/user' + require 'chef/user_v1' require 'chef/json_compat' end @@ -61,7 +61,7 @@ EOF exit 1 end - user = Chef::User.load(@user_name) + user = Chef::UserV1.load(@user_name) # DEPRECATION NOTE # Remove this if statement and corrosponding code post OSC 11 support. diff --git a/lib/chef/knife/user_show.rb b/lib/chef/knife/user_show.rb index f5e81e9972..3a2443471a 100644 --- a/lib/chef/knife/user_show.rb +++ b/lib/chef/knife/user_show.rb @@ -25,7 +25,7 @@ class Chef include Knife::Core::MultiAttributeReturnOption deps do - require 'chef/user' + require 'chef/user_v1' require 'chef/json_compat' end @@ -58,7 +58,7 @@ EOF exit 1 end - user = Chef::User.load(@user_name) + user = Chef::UserV1.load(@user_name) # DEPRECATION NOTE # Remove this if statement and corrosponding code post OSC 11 support. diff --git a/lib/chef/log.rb b/lib/chef/log.rb index 9b27778a40..bf846c2072 100644 --- a/lib/chef/log.rb +++ b/lib/chef/log.rb @@ -37,7 +37,11 @@ class Chef end end - def self.deprecation(msg=nil, &block) + def self.deprecation(msg=nil, location=caller(2..2)[0], &block) + if msg + msg << " at #{Array(location).join("\n")}" + msg = msg.join("") if msg.respond_to?(:join) + end if Chef::Config[:treat_deprecation_warnings_as_errors] error(msg, &block) raise Chef::Exceptions::DeprecatedFeatureError.new(msg) diff --git a/lib/chef/mixin/deprecation.rb b/lib/chef/mixin/deprecation.rb index a3eacf75cb..562af541bd 100644 --- a/lib/chef/mixin/deprecation.rb +++ b/lib/chef/mixin/deprecation.rb @@ -102,20 +102,20 @@ class Chef 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)} + 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)} + 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 diff --git a/lib/chef/mixin/params_validate.rb b/lib/chef/mixin/params_validate.rb index 78d72dc801..e3c7657b1b 100644 --- a/lib/chef/mixin/params_validate.rb +++ b/lib/chef/mixin/params_validate.rb @@ -15,9 +15,11 @@ # See the License for the specific language governing permissions and # limitations under the License. +require 'chef/constants' +require 'chef/property' +require 'chef/delayed_evaluator' + class Chef - class DelayedEvaluator < Proc - end module Mixin module ParamsValidate @@ -32,20 +34,55 @@ class Chef # Would raise an exception if the value of :one above is not a kind_of? string. Valid # map options are: # - # :default:: Sets the default value for this parameter. - # :callbacks:: Takes a hash of Procs, which should return true if the argument is valid. - # The key will be inserted into the error message if the Proc does not return true: - # "Option #{key}'s value #{value} #{message}!" - # :kind_of:: Ensure that the value is a kind_of?(Whatever). If passed an array, it will ensure - # that the value is one of those types. - # :respond_to:: Ensure that the value has a given method. Takes one method name or an array of - # method names. - # :required:: Raise an exception if this parameter is missing. Valid values are true or false, - # by default, options are not required. - # :regex:: Match the value of the parameter against a regular expression. - # :equal_to:: Match the value of the parameter with ==. An array means it can be equal to any - # of the values. + # @param opts [Hash<Symbol,Object>] Validation opts. + # @option opts [Object,Array] :is An object, or list of + # objects, that must match the value using Ruby's `===` operator + # (`opts[:is].any? { |v| v === value }`). (See #_pv_is.) + # @option opts [Object,Array] :equal_to An object, or list + # of objects, that must be equal to the value using Ruby's `==` + # operator (`opts[:is].any? { |v| v == value }`) (See #_pv_equal_to.) + # @option opts [Regexp,Array<Regexp>] :regex An object, or + # list of objects, that must match the value with `regex.match(value)`. + # (See #_pv_regex) + # @option opts [Class,Array<Class>] :kind_of A class, or + # list of classes, that the value must be an instance of. (See + # #_pv_kind_of.) + # @option opts [Hash<String,Proc>] :callbacks A hash of + # messages -> procs, all of which match the value. The proc must + # return a truthy or falsey value (true means it matches). (See + # #_pv_callbacks.) + # @option opts [Symbol,Array<Symbol>] :respond_to A method + # name, or list of method names, the value must respond to. (See + # #_pv_respond_to.) + # @option opts [Symbol,Array<Symbol>] :cannot_be A property, + # or a list of properties, that the value cannot have (such as `:nil` or + # `:empty`). The method with a questionmark at the end is called on the + # value (e.g. `value.empty?`). If the value does not have this method, + # it is considered valid (i.e. if you don't respond to `empty?` we + # assume you are not empty). (See #_pv_cannot_be.) + # @option opts [Proc] :coerce A proc which will be called to + # transform the user input to canonical form. The value is passed in, + # and the transformed value returned as output. Lazy values will *not* + # be passed to this method until after they are evaluated. Called in the + # context of the resource (meaning you can access other properties). + # (See #_pv_coerce.) (See #_pv_coerce.) + # @option opts [Boolean] :required `true` if this property + # must be present and not `nil`; `false` otherwise. This is checked + # after the resource is fully initialized. (See #_pv_required.) + # @option opts [Boolean] :name_property `true` if this + # property defaults to the same value as `name`. Equivalent to + # `default: lazy { name }`, except that #property_is_set? will + # return `true` if the property is set *or* if `name` is set. (See + # #_pv_name_property.) + # @option opts [Boolean] :name_attribute Same as `name_property`. + # @option opts [Object] :default The value this property + # will return if the user does not set one. If this is `lazy`, it will + # be run in the context of the instance (and able to access other + # properties). (See #_pv_default.) + # def validate(opts, map) + map = map.validation_options if map.is_a?(Property) + #-- # validate works by taking the keys in the validation map, assuming it's a hash, and # looking for _pv_:symbol as methods. Assuming it find them, it calls the right @@ -65,7 +102,7 @@ class Chef true when Hash validation.each do |check, carg| - check_method = "_pv_#{check.to_s}" + check_method = "_pv_#{check}" if self.respond_to?(check_method, true) self.send(check_method, opts, key, carg) else @@ -81,162 +118,352 @@ class Chef DelayedEvaluator.new(&block) end - def set_or_return(symbol, arg, validation) - iv_symbol = "@#{symbol.to_s}".to_sym - if arg == nil && self.instance_variable_defined?(iv_symbol) == true - ivar = self.instance_variable_get(iv_symbol) - if(ivar.is_a?(DelayedEvaluator)) - validate({ symbol => ivar.call }, { symbol => validation })[symbol] - else - ivar - end - else - if(arg.is_a?(DelayedEvaluator)) - val = arg - else - val = validate({ symbol => arg }, { symbol => validation })[symbol] + def set_or_return(symbol, value, validation) + property = SetOrReturnProperty.new(name: symbol, **validation) + property.call(self, value) + end - # Handle the case where the "default" was a DelayedEvaluator. In - # this case, the block yields an optional parameter of +self+, - # which is the equivalent of "new_resource" - if val.is_a?(DelayedEvaluator) - val = val.call(self) - end - end - self.instance_variable_set(iv_symbol, val) + private + + def explicitly_allows_nil?(key, validation) + validation.has_key?(:is) && _pv_is({ key => nil }, key, validation[:is], raise_error: false) + end + + # Return the value of a parameter, or nil if it doesn't exist. + def _pv_opts_lookup(opts, key) + if opts.has_key?(key.to_s) + opts[key.to_s] + elsif opts.has_key?(key.to_sym) + opts[key.to_sym] + else + nil end end - private + # Raise an exception if the parameter is not found. + def _pv_required(opts, key, is_required=true, explicitly_allows_nil=false) + if is_required + return true if opts.has_key?(key.to_s) && (explicitly_allows_nil || !opts[key.to_s].nil?) + return true if opts.has_key?(key.to_sym) && (explicitly_allows_nil || !opts[key.to_sym].nil?) + raise Exceptions::ValidationFailed, "Required argument #{key.inspect} is missing!" + end + true + end - # Return the value of a parameter, or nil if it doesn't exist. - def _pv_opts_lookup(opts, key) - if opts.has_key?(key.to_s) - opts[key.to_s] - elsif opts.has_key?(key.to_sym) - opts[key.to_sym] - else - nil + # + # List of things values must be equal to. + # + # Uses Ruby's `==` to evaluate (equal_to == value). At least one must + # match for the value to be valid. + # + # `nil` passes this validation automatically. + # + # @return [Array,nil] List of things values must be equal to, or nil if + # equal_to is unspecified. + # + def _pv_equal_to(opts, key, to_be) + value = _pv_opts_lookup(opts, key) + unless value.nil? + to_be = Array(to_be) + to_be.each do |tb| + return true if value == tb end + raise Exceptions::ValidationFailed, "Option #{key} must be equal to one of: #{to_be.join(", ")}! You passed #{value.inspect}." end + end - # Raise an exception if the parameter is not found. - def _pv_required(opts, key, is_required=true) - if is_required - if (opts.has_key?(key.to_s) && !opts[key.to_s].nil?) || - (opts.has_key?(key.to_sym) && !opts[key.to_sym].nil?) - true - else - raise Exceptions::ValidationFailed, "Required argument #{key} is missing!" - end + # + # List of things values must be instances of. + # + # Uses value.kind_of?(kind_of) to evaluate. At least one must match for + # the value to be valid. + # + # `nil` automatically passes this validation. + # + def _pv_kind_of(opts, key, to_be) + value = _pv_opts_lookup(opts, key) + unless value.nil? + to_be = Array(to_be) + to_be.each do |tb| + return true if value.kind_of?(tb) end + raise Exceptions::ValidationFailed, "Option #{key} must be a kind of #{to_be}! You passed #{value.inspect}." end + end - def _pv_equal_to(opts, key, to_be) - value = _pv_opts_lookup(opts, key) - unless value.nil? - passes = false - Array(to_be).each do |tb| - passes = true if value == tb - end - unless passes - raise Exceptions::ValidationFailed, "Option #{key} must be equal to one of: #{to_be.join(", ")}! You passed #{value.inspect}." + # + # List of method names values must respond to. + # + # Uses value.respond_to?(respond_to) to evaluate. At least one must match + # for the value to be valid. + # + def _pv_respond_to(opts, key, method_name_list) + value = _pv_opts_lookup(opts, key) + unless value.nil? + Array(method_name_list).each do |method_name| + unless value.respond_to?(method_name) + raise Exceptions::ValidationFailed, "Option #{key} must have a #{method_name} method!" end end end + end - # Raise an exception if the parameter is not a kind_of?(to_be) - def _pv_kind_of(opts, key, to_be) - value = _pv_opts_lookup(opts, key) - unless value.nil? - passes = false - Array(to_be).each do |tb| - passes = true if value.kind_of?(tb) - end - unless passes - raise Exceptions::ValidationFailed, "Option #{key} must be a kind of #{to_be}! You passed #{value.inspect}." + # + # List of things that must not be true about the value. + # + # Calls `value.<thing>?` All responses must be false for the value to be + # valid. + # Values which do not respond to <thing>? are considered valid (because if + # a value doesn't respond to `:readable?`, then it probably isn't + # readable.) + # + # @example + # ```ruby + # property :x, cannot_be: [ :nil, :empty ] + # x [ 1, 2 ] #=> valid + # x 1 #=> valid + # x [] #=> invalid + # x nil #=> invalid + # ``` + # + def _pv_cannot_be(opts, key, predicate_method_base_name) + value = _pv_opts_lookup(opts, key) + if !value.nil? + Array(predicate_method_base_name).each do |method_name| + predicate_method = :"#{method_name}?" + + if value.respond_to?(predicate_method) + if value.send(predicate_method) + raise Exceptions::ValidationFailed, "Option #{key} cannot be #{predicate_method_base_name}" + end end end end + end - # Raise an exception if the parameter does not respond to a given set of methods. - def _pv_respond_to(opts, key, method_name_list) - value = _pv_opts_lookup(opts, key) - unless value.nil? - Array(method_name_list).each do |method_name| - unless value.respond_to?(method_name) - raise Exceptions::ValidationFailed, "Option #{key} must have a #{method_name} method!" - end - end + # + # The default value for a property. + # + # When the property is not assigned, this will be used. + # + # If this is a lazy value, it will either be passed the resource as a value, + # or if the lazy proc does not take parameters, it will be run in the + # context of the instance with instance_eval. + # + # @example + # ```ruby + # property :x, default: 10 + # ``` + # + # @example + # ```ruby + # property :x + # property :y, default: lazy { x+2 } + # ``` + # + # @example + # ```ruby + # property :x + # property :y, default: lazy { |r| r.x+2 } + # ``` + # + def _pv_default(opts, key, default_value) + value = _pv_opts_lookup(opts, key) + if value.nil? + default_value = default_value.freeze if !default_value.is_a?(DelayedEvaluator) + opts[key] = default_value + end + end + + # + # List of regexes values that must match. + # + # Uses regex.match() to evaluate. At least one must match for the value to + # be valid. + # + # `nil` passes regex validation automatically. + # + # @example + # ```ruby + # property :x, regex: [ /abc/, /xyz/ ] + # ``` + # + def _pv_regex(opts, key, regex) + value = _pv_opts_lookup(opts, key) + if !value.nil? + Array(regex).flatten.each do |r| + return true if r.match(value.to_s) end + raise Exceptions::ValidationFailed, "Option #{key}'s value #{value} does not match regular expression #{regex.inspect}" end + end - # Assert that parameter returns false when passed a predicate method. - # For example, :cannot_be => :blank will raise a Exceptions::ValidationFailed - # error value.blank? returns a 'truthy' (not nil or false) value. - # - # Note, this will *PASS* if the object doesn't respond to the method. - # So, to make sure a value is not nil and not blank, you need to do - # both :cannot_be => :blank *and* :cannot_be => :nil (or :required => true) - def _pv_cannot_be(opts, key, predicate_method_base_name) - value = _pv_opts_lookup(opts, key) - predicate_method = (predicate_method_base_name.to_s + "?").to_sym - - if value.respond_to?(predicate_method) - if value.send(predicate_method) - raise Exceptions::ValidationFailed, "Option #{key} cannot be #{predicate_method_base_name}" + # + # List of procs we pass the value to. + # + # All procs must return true for the value to be valid. If any procs do + # not return true, the key will be used for the message: `"Property x's + # value :y <message>"`. + # + # @example + # ```ruby + # property :x, callbacks: { "is bigger than 10" => proc { |v| v <= 10 }, "is not awesome" => proc { |v| !v.awesome }} + # ``` + # + def _pv_callbacks(opts, key, callbacks) + raise ArgumentError, "Callback list must be a hash!" unless callbacks.kind_of?(Hash) + value = _pv_opts_lookup(opts, key) + if !value.nil? + callbacks.each do |message, zeproc| + unless zeproc.call(value) + raise Exceptions::ValidationFailed, "Option #{key}'s value #{value} #{message}!" end end end + end - # Assign a default value to a parameter. - def _pv_default(opts, key, default_value) - value = _pv_opts_lookup(opts, key) - if value == nil - opts[key] = default_value + # + # Allows a parameter to default to the value of the resource name. + # + # @example + # ```ruby + # property :x, name_property: true + # ``` + # + def _pv_name_property(opts, key, is_name_property=true) + if is_name_property + if opts[key].nil? + opts[key] = self.instance_variable_get(:"@name") end end + end + alias :_pv_name_attribute :_pv_name_property - # Check a parameter against a regular expression. - def _pv_regex(opts, key, regex) - value = _pv_opts_lookup(opts, key) - if value != nil - passes = false - [ regex ].flatten.each do |r| - if value != nil - if r.match(value.to_s) - passes = true - end - end - end - unless passes - raise Exceptions::ValidationFailed, "Option #{key}'s value #{value} does not match regular expression #{regex.inspect}" - end + # + # List of valid things values can be. + # + # Uses Ruby's `===` to evaluate (is === value). At least one must match + # for the value to be valid. + # + # If a proc is passed, it is instance_eval'd in the resource, passed the + # value, and must return a truthy or falsey value. + # + # @example Class + # ```ruby + # property :x, String + # x 'valid' #=> valid + # x 1 #=> invalid + # x nil #=> invalid + # + # @example Value + # ```ruby + # property :x, [ :a, :b, :c, nil ] + # x :a #=> valid + # x nil #=> valid + # ``` + # + # @example Regex + # ```ruby + # property :x, /bar/ + # x 'foobar' #=> valid + # x 'foo' #=> invalid + # x nil #=> invalid + # ``` + # + # @example Proc + # ```ruby + # property :x, proc { |x| x > y } + # property :y, default: 2 + # x 3 #=> valid + # x 1 #=> invalid + # ``` + # + # @example Property + # ```ruby + # type = Property.new(is: String) + # property :x, type + # x 'foo' #=> valid + # x 1 #=> invalid + # x nil #=> invalid + # ``` + # + # @example RSpec Matcher + # ```ruby + # include RSpec::Matchers + # property :x, a_string_matching /bar/ + # x 'foobar' #=> valid + # x 'foo' #=> invalid + # x nil #=> invalid + # ``` + # + def _pv_is(opts, key, to_be, raise_error: true) + return true if !opts.has_key?(key.to_s) && !opts.has_key?(key.to_sym) + value = _pv_opts_lookup(opts, key) + to_be = [ to_be ].flatten(1) + to_be.each do |tb| + case tb + when Proc + return true if instance_exec(value, &tb) + when Property + validate(opts, { key => tb.validation_options }) + return true + else + return true if tb === value end end - # Check a parameter against a hash of proc's. - def _pv_callbacks(opts, key, callbacks) - raise ArgumentError, "Callback list must be a hash!" unless callbacks.kind_of?(Hash) - value = _pv_opts_lookup(opts, key) - if value != nil - callbacks.each do |message, zeproc| - if zeproc.call(value) != true - raise Exceptions::ValidationFailed, "Option #{key}'s value #{value} #{message}!" - end - end + if raise_error + raise Exceptions::ValidationFailed, "Option #{key} must be one of: #{to_be.join(", ")}! You passed #{value.inspect}." + else + false + end + end + + # + # Method to mess with a value before it is validated and stored. + # + # Allows you to transform values into a canonical form that is easy to + # work with. + # + # This is passed the value to transform, and is run in the context of the + # instance (so it has access to other resource properties). It must return + # the value that will be stored in the instance. + # + # @example + # ```ruby + # property :x, Integer, coerce: { |v| v.to_i } + # ``` + # + def _pv_coerce(opts, key, coercer) + if opts.has_key?(key.to_s) + opts[key.to_s] = instance_exec(opts[key], &coercer) + elsif opts.has_key?(key.to_sym) + opts[key.to_sym] = instance_exec(opts[key], &coercer) + end + end + + # Used by #set_or_return to avoid emitting a deprecation warning for + # "value nil" and to keep default stickiness working exactly the same + # @api private + class SetOrReturnProperty < Chef::Property + def get(resource) + value = super + # All values are sticky, frozen or not + if !is_set?(resource) + set_value(resource, value) end + value end - # Allow a parameter to default to @name - def _pv_name_attribute(opts, key, is_name_attribute=true) - if is_name_attribute - if opts[key] == nil - opts[key] = self.instance_variable_get("@name") - end + def call(resource, value=NOT_PASSED) + # setting to nil does a get + if value.nil? && !explicitly_accepts_nil?(resource) + get(resource) + else + super end end + end end end end - diff --git a/lib/chef/mixin/template.rb b/lib/chef/mixin/template.rb index 9b35bbcc33..db9a2f6d91 100644 --- a/lib/chef/mixin/template.rb +++ b/lib/chef/mixin/template.rb @@ -44,6 +44,52 @@ class Chef attr_reader :_extension_modules + # + # Helpers for adding context of which resource is rendering the template (CHEF-5012) + # + + # name of the cookbook containing the template resource, e.g.: + # test + # + # @return [String] cookbook name + attr_reader :cookbook_name + + # name of the recipe containing the template resource, e.g.: + # default + # + # @return [String] recipe name + attr_reader :recipe_name + + # string representation of the line in the recipe containing the template resource, e.g.: + # /Users/lamont/solo/cookbooks/test/recipes/default.rb:2:in `from_file' + # + # @return [String] recipe line + attr_reader :recipe_line_string + + # path to the recipe containing the template resource, e.g.: + # /Users/lamont/solo/cookbooks/test/recipes/default.rb + # + # @return [String] recipe path + attr_reader :recipe_path + + # line in the recipe containing the template reosurce, e.g.: + # 2 + # + # @return [String] recipe line + attr_reader :recipe_line + + # name of the template source itself, e.g.: + # foo.erb + # + # @return [String] template name + attr_reader :template_name + + # path to the template source itself, e.g.: + # /Users/lamont/solo/cookbooks/test/templates/default/foo.erb + # + # @return [String] template path + attr_reader :template_path + def initialize(variables) super @_extension_modules = [] @@ -62,6 +108,7 @@ class Chef "include a node variable if you plan to use it." end + # # Takes the name of the partial, plus a hash of options. Returns a # string that contains the result of the evaluation of the partial. diff --git a/lib/chef/mixin/wide_string.rb b/lib/chef/mixin/wide_string.rb new file mode 100644 index 0000000000..0c32b76365 --- /dev/null +++ b/lib/chef/mixin/wide_string.rb @@ -0,0 +1,72 @@ +# +# 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 + utf8_to_wide(str) + end + end + + def utf8_to_wide(ustring) + # ensure it is actually UTF-8 + # Ruby likes to mark binary data as ASCII-8BIT + 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.length == 0 or ustring[-1].chr != "\000" + + # encode it all as UTF-16LE AKA Windows Wide Character AKA Windows Unicode + ustring = begin + if ustring.respond_to?(:encode) + ustring.encode('UTF-16LE') + else + require 'iconv' + Iconv.conv("UTF-16LE", "UTF-8", ustring) + end + end + ustring + end + + def wide_to_utf8(wstring) + # ensure it is actually UTF-16LE + # Ruby likes to mark binary data as ASCII-8BIT + wstring = wstring.force_encoding('UTF-16LE') if wstring.respond_to?(:force_encoding) + + # encode it all as UTF-8 + wstring = begin + if wstring.respond_to?(:encode) + wstring.encode('UTF-8') + else + require 'iconv' + Iconv.conv("UTF-8", "UTF-16LE", wstring) + end + end + # remove trailing CRLF and NULL characters + wstring.strip! + wstring + end + + end + end +end diff --git a/lib/chef/mixin/windows_architecture_helper.rb b/lib/chef/mixin/windows_architecture_helper.rb index c5f3e1bd79..744001f8a2 100644 --- a/lib/chef/mixin/windows_architecture_helper.rb +++ b/lib/chef/mixin/windows_architecture_helper.rb @@ -19,19 +19,13 @@ require 'chef/exceptions' require 'chef/platform/query_helpers' -require 'win32/api' if Chef::Platform.windows? -require 'chef/win32/api/process' if Chef::Platform.windows? -require 'chef/win32/api/error' if Chef::Platform.windows? +require 'chef/win32/process' if Chef::Platform.windows? +require 'chef/win32/system' if Chef::Platform.windows? class Chef module Mixin module WindowsArchitectureHelper - if Chef::Platform.windows? - include Chef::ReservedNames::Win32::API::Process - include Chef::ReservedNames::Win32::API::Error - end - def node_windows_architecture(node) node[:kernel][:machine].to_sym end @@ -42,6 +36,16 @@ class Chef is_i386_process_on_x86_64_windows? end + def forced_32bit_override_required?(node, desired_architecture) + desired_architecture == :i386 && + node_windows_architecture(node) == :x86_64 && + !is_i386_process_on_x86_64_windows? + end + + def wow64_directory + Chef::ReservedNames::Win32::System.get_system_wow64_directory + end + def with_os_architecture(node, architecture: nil) node ||= begin os_arch = ENV['PROCESSOR_ARCHITEW6432'] || @@ -88,49 +92,21 @@ class Chef def is_i386_process_on_x86_64_windows? if Chef::Platform.windows? - is_64_bit_process_result = FFI::MemoryPointer.new(:int) - - # The return value of IsWow64Process is nonzero value if the API call succeeds. - # The result data are returned in the last parameter, not the return value. - call_succeeded = IsWow64Process(GetCurrentProcess(), is_64_bit_process_result) - - # The result is nonzero if IsWow64Process's calling process, in the case here - # this process, is running under WOW64, i.e. the result is nonzero if this - # process is 32-bit (aka :i386). - result = (call_succeeded != 0) && (is_64_bit_process_result.get_int(0) != 0) + Chef::ReservedNames::Win32::Process.is_wow64_process else false end end def disable_wow64_file_redirection( node ) - original_redirection_state = ['0'].pack('P') - if ( ( node_windows_architecture(node) == :x86_64) && ::Chef::Platform.windows?) - win32_wow_64_disable_wow_64_fs_redirection = - ::Win32::API.new('Wow64DisableWow64FsRedirection', 'P', 'L', 'kernel32') - - succeeded = win32_wow_64_disable_wow_64_fs_redirection.call(original_redirection_state) - - if succeeded == 0 - raise Win32APIError "Failed to disable Wow64 file redirection" - end - + Chef::ReservedNames::Win32::System.wow64_disable_wow64_fs_redirection end - - original_redirection_state end def restore_wow64_file_redirection( node, original_redirection_state ) if ( (node_windows_architecture(node) == :x86_64) && ::Chef::Platform.windows?) - win32_wow_64_revert_wow_64_fs_redirection = - ::Win32::API.new('Wow64RevertWow64FsRedirection', 'P', 'L', 'kernel32') - - succeeded = win32_wow_64_revert_wow_64_fs_redirection.call(original_redirection_state) - - if succeeded == 0 - raise Win32APIError "Failed to revert Wow64 file redirection" - end + Chef::ReservedNames::Win32::System.wow64_revert_wow64_fs_redirection(original_redirection_state) end end diff --git a/lib/chef/mixin/windows_env_helper.rb b/lib/chef/mixin/windows_env_helper.rb index a126801a28..cd12b4254a 100644 --- a/lib/chef/mixin/windows_env_helper.rb +++ b/lib/chef/mixin/windows_env_helper.rb @@ -18,6 +18,7 @@ require 'chef/exceptions' +require 'chef/mixin/wide_string' require 'chef/platform/query_helpers' require 'chef/win32/error' if Chef::Platform.windows? require 'chef/win32/api/system' if Chef::Platform.windows? @@ -26,6 +27,8 @@ require 'chef/win32/api/unicode' if Chef::Platform.windows? class Chef module Mixin module WindowsEnvHelper + include Chef::Mixin::WideString + if Chef::Platform.windows? include Chef::ReservedNames::Win32::API::System end @@ -45,7 +48,7 @@ class Chef 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') + utf8_to_wide('Environment') ).address, flags, 5000, nil) == 0 ) Chef::ReservedNames::Win32::Error.raise! end diff --git a/lib/chef/node.rb b/lib/chef/node.rb index d5078371c5..22c7d5bd8e 100644 --- a/lib/chef/node.rb +++ b/lib/chef/node.rb @@ -315,6 +315,7 @@ class Chef # Consumes the combined run_list and other attributes in +attrs+ def consume_attributes(attrs) normal_attrs_to_merge = consume_run_list(attrs) + normal_attrs_to_merge = consume_chef_environment(normal_attrs_to_merge) Chef::Log.debug("Applying attributes from json file") self.normal_attrs = Chef::Mixin::DeepMerge.merge(normal_attrs,normal_attrs_to_merge) self.tags # make sure they're defined @@ -347,6 +348,24 @@ class Chef attrs end + # chef_environment when set in -j JSON will take precedence over + # -E ENVIRONMENT. Ideally, IMO, the order of precedence should be (lowest to + # highest): + # config_file + # -j JSON + # -E ENVIRONMENT + # so that users could reuse their JSON and override the chef_environment + # configured within it with -E ENVIRONMENT. Because command line options are + # merged with Chef::Config there is currently no way to distinguish between + # an environment set via config from an environment set via command line. + def consume_chef_environment(attrs) + attrs = attrs ? attrs.dup : {} + if env = attrs.delete("chef_environment") + chef_environment(env) + end + attrs + end + # Clear defaults and overrides, so that any deleted attributes # between runs are still gone. def reset_defaults_and_overrides diff --git a/lib/chef/node_map.rb b/lib/chef/node_map.rb index f547018a38..751f9576f6 100644 --- a/lib/chef/node_map.rb +++ b/lib/chef/node_map.rb @@ -20,13 +20,6 @@ class Chef class NodeMap # - # 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. # @@ -38,30 +31,34 @@ class Chef # # @return [NodeMap] Returns self for possible chaining # - 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 + def set(key, value, platform: nil, platform_version: nil, platform_family: nil, os: nil, on_platform: nil, on_platforms: nil, canonical: nil, override: 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] ||= [] - # 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.) + filters = {} + filters[:platform] = platform if platform + filters[:platform_version] = platform_version if platform_version + filters[:platform_family] = platform_family if platform_family + filters[:os] = os if os + new_matcher = { value: value, filters: filters } + new_matcher[:block] = block if block + new_matcher[:canonical] = canonical if canonical + new_matcher[:override] = override if override + + # The map is sorted in order of preference already; we just need to find + # our place in it (just before the first value with the same preference level). insert_at = nil - @map[key].each_with_index do |matcher, index| - if specificity(new_matcher) >= specificity(matcher) - insert_at = index - break - end + map[key] ||= [] + map[key].each_with_index do |matcher,index| + cmp = compare_matchers(key, new_matcher, matcher) + insert_at ||= index if cmp && cmp <= 0 end if insert_at - @map[key].insert(insert_at, new_matcher) + map[key].insert(insert_at, new_matcher) else - @map[key] << new_matcher + map[key] << new_matcher end - self + map end # @@ -95,8 +92,8 @@ class Chef # 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| + 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 @@ -105,41 +102,18 @@ class Chef # @return remaining # @api private def delete_canonical(key, value) - remaining = @map[key] + remaining = map[key] if remaining remaining.delete_if { |matcher| matcher[:canonical] && Array(matcher[:value]) == Array(value) } if remaining.empty? - @map.delete(key) + map.delete(key) remaining = nil end end remaining end - private - - # - # 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 + protected # # Succeeds if: @@ -197,5 +171,52 @@ class Chef return true if canonical.nil? !!canonical == !!matcher[:canonical] end + + def compare_matchers(key, new_matcher, matcher) + cmp = compare_matcher_properties(new_matcher, matcher) { |m| m[:block] } + return cmp if cmp != 0 + cmp = compare_matcher_properties(new_matcher, matcher) { |m| m[:filters][:platform_version] } + return cmp if cmp != 0 + cmp = compare_matcher_properties(new_matcher, matcher) { |m| m[:filters][:platform] } + return cmp if cmp != 0 + cmp = compare_matcher_properties(new_matcher, matcher) { |m| m[:filters][:platform_family] } + return cmp if cmp != 0 + cmp = compare_matcher_properties(new_matcher, matcher) { |m| m[:filters][:os] } + return cmp if cmp != 0 + cmp = compare_matcher_properties(new_matcher, matcher) { |m| m[:override] } + return cmp if cmp != 0 + # If all things are identical, return 0 + 0 + end + + def compare_matcher_properties(new_matcher, matcher) + a = yield(new_matcher) + b = yield(matcher) + + # Check for blcacklists ('!windows'). Those always come *after* positive + # whitelists. + a_negated = Array(a).any? { |f| f.is_a?(String) && f.start_with?('!') } + b_negated = Array(b).any? { |f| f.is_a?(String) && f.start_with?('!') } + if a_negated != b_negated + return 1 if a_negated + return -1 if b_negated + end + + # We treat false / true and nil / not-nil with the same comparison + a = nil if a == false + b = nil if b == false + cmp = a <=> b + # This is the case where one is non-nil, and one is nil. The one that is + # nil is "greater" (i.e. it should come last). + if cmp.nil? + return 1 if a.nil? + return -1 if b.nil? + end + cmp + end + + def map + @map ||= {} + end end end diff --git a/lib/chef/osc_user.rb b/lib/chef/osc_user.rb deleted file mode 100644 index 52bfd11108..0000000000 --- a/lib/chef/osc_user.rb +++ /dev/null @@ -1,194 +0,0 @@ -# -# 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/handler_map.rb b/lib/chef/platform/handler_map.rb new file mode 100644 index 0000000000..a9551a344b --- /dev/null +++ b/lib/chef/platform/handler_map.rb @@ -0,0 +1,40 @@ +# +# Author:: John Keiser (<jkeiser@chef.io>) +# Copyright:: Copyright (c) 2015 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/node_map' + +class Chef + class Platform + class HandlerMap < Chef::NodeMap + # + # "provides" lines with identical filters sort by class name (ascending). + # + def compare_matchers(key, new_matcher, matcher) + cmp = super + if cmp == 0 + # Sort by class name (ascending) as well, if all other properties + # are exactly equal + if new_matcher[:value].is_a?(Class) && !new_matcher[:override] + cmp = compare_matcher_properties(new_matcher, matcher) { |m| m[:value].name } + end + end + cmp + end + end + end +end diff --git a/lib/chef/platform/priority_map.rb b/lib/chef/platform/priority_map.rb new file mode 100644 index 0000000000..0b050deb59 --- /dev/null +++ b/lib/chef/platform/priority_map.rb @@ -0,0 +1,41 @@ +# +# Author:: John Keiser (<jkeiser@chef.io>) +# Copyright:: Copyright (c) 2015 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/node_map' + +class Chef + class Platform + class PriorityMap < Chef::NodeMap + def priority(resource_name, priority_array, *filter) + set_priority_array(resource_name.to_sym, priority_array, *filter) + end + + # @api private + def get_priority_array(node, key) + get(node, key) + end + + # @api private + def set_priority_array(key, priority_array, *filter, &block) + priority_array = Array(priority_array) + set(key, priority_array, *filter, &block) + priority_array + end + end + end +end diff --git a/lib/chef/mixin/wstring.rb b/lib/chef/platform/provider_handler_map.rb index bb6fdf4884..4549d7994e 100644 --- a/lib/chef/mixin/wstring.rb +++ b/lib/chef/platform/provider_handler_map.rb @@ -1,6 +1,6 @@ # -# Author:: Jay Mundrawala(<jdm@chef.io>) -# Copyright:: Copyright 2015 Chef Software +# Author:: John Keiser (<jkeiser@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"); @@ -16,16 +16,14 @@ # limitations under the License. # +require 'singleton' +require 'chef/platform/handler_map' + class Chef - module Mixin - module WideString - def wstring(str) - if str.nil? || str.encoding == Encoding::UTF_16LE - str - else - str.to_wstring - end - end + class Platform + # @api private + class ProviderHandlerMap < Chef::Platform::HandlerMap + include Singleton end end end diff --git a/lib/chef/platform/provider_mapping.rb b/lib/chef/platform/provider_mapping.rb index e3a894c8ac..9b511f0237 100644 --- a/lib/chef/platform/provider_mapping.rb +++ b/lib/chef/platform/provider_mapping.rb @@ -176,7 +176,7 @@ class Chef platform_provider(platform, version, resource_type) || resource_matching_provider(platform, version, resource_type) - raise ArgumentError, "Cannot find a provider for #{resource_type} on #{platform} version #{version}" if provider_klass.nil? + raise Chef::Exceptions::ProviderNotFound, "Cannot find a provider for #{resource_type} on #{platform} version #{version}" if provider_klass.nil? provider_klass end @@ -197,16 +197,16 @@ class Chef def resource_matching_provider(platform, version, resource_type) if resource_type.kind_of?(Chef::Resource) - class_name = resource_type.class.to_s.split('::').last + class_name = resource_type.class.name ? resource_type.class.name.split('::').last : + convert_to_class_name(resource_type.resource_name.to_s) - begin - result = Chef::Provider.const_get(class_name) + if Chef::Provider.const_defined?(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 + Chef::Log.warn("This will no longer work in Chef 13: you must use 'provides' to use the resource's DSL.") + return Chef::Provider.const_get(class_name) end end - result + nil end end diff --git a/lib/chef/platform/provider_priority_map.rb b/lib/chef/platform/provider_priority_map.rb index 9d703c9178..5599c74c2d 100644 --- a/lib/chef/platform/provider_priority_map.rb +++ b/lib/chef/platform/provider_priority_map.rb @@ -1,29 +1,11 @@ require 'singleton' +require 'chef/platform/priority_map' class Chef class Platform - class ProviderPriorityMap + # @api private + class ProviderPriorityMap < Chef::Platform::PriorityMap include Singleton - - 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, &block) - priority_map.set(resource_name.to_sym, Array(priority_array), *filter, &block) - end - - # @api private - def list_handlers(node, resource_name) - priority_map.list(node, resource_name.to_sym).flatten(1).uniq - end - - private - - def priority_map - require 'chef/node_map' - @priority_map ||= Chef::NodeMap.new - end end end end diff --git a/lib/chef/platform/query_helpers.rb b/lib/chef/platform/query_helpers.rb index b3948eac21..e64189fbd6 100644 --- a/lib/chef/platform/query_helpers.rb +++ b/lib/chef/platform/query_helpers.rb @@ -25,13 +25,10 @@ class Chef end def windows_server_2003? + # WMI startup shouldn't be performed unless we're on Windows. return false unless windows? require 'wmi-lite/wmi' - # CHEF-4888: Work around ruby #2618, expected to be fixed in Ruby 2.1.0 - # https://github.com/ruby/ruby/commit/588504b20f5cc880ad51827b93e571e32446e5db - # https://github.com/ruby/ruby/commit/27ed294c7134c0de582007af3c915a635a6506cd - wmi = WmiLite::Wmi.new host = wmi.first_of('Win32_OperatingSystem') is_server_2003 = (host['version'] && host['version'].start_with?("5.2")) diff --git a/lib/chef/platform/rebooter.rb b/lib/chef/platform/rebooter.rb index b46f0e394c..b78ac38f0c 100644 --- a/lib/chef/platform/rebooter.rb +++ b/lib/chef/platform/rebooter.rb @@ -32,7 +32,7 @@ class Chef cmd = if Chef::Platform.windows? # should this do /f as well? do we then need a minimum delay to let apps quit? - "shutdown /r /t #{reboot_info[:delay_mins]} /c \"#{reboot_info[:reason]}\"" + "shutdown /r /t #{reboot_info[:delay_mins]*60} /c \"#{reboot_info[:reason]}\"" else # probably Linux-only. "shutdown -r +#{reboot_info[:delay_mins]} \"#{reboot_info[:reason]}\"" diff --git a/lib/chef/platform/resource_handler_map.rb b/lib/chef/platform/resource_handler_map.rb new file mode 100644 index 0000000000..27a7bb1342 --- /dev/null +++ b/lib/chef/platform/resource_handler_map.rb @@ -0,0 +1,29 @@ +# +# Author:: John Keiser (<jkeiser@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 'singleton' +require 'chef/platform/handler_map' + +class Chef + class Platform + # @api private + class ResourceHandlerMap < Chef::Platform::HandlerMap + include Singleton + end + end +end diff --git a/lib/chef/platform/resource_priority_map.rb b/lib/chef/platform/resource_priority_map.rb index fb08debc53..5cc86fd2e7 100644 --- a/lib/chef/platform/resource_priority_map.rb +++ b/lib/chef/platform/resource_priority_map.rb @@ -1,34 +1,11 @@ require 'singleton' +require 'chef/platform/priority_map' class Chef class Platform - class ResourcePriorityMap + # @api private + class ResourcePriorityMap < Chef::Platform::PriorityMap include Singleton - - def get_priority_array(node, resource_name, canonical: nil) - priority_map.get(node, resource_name.to_sym, canonical: canonical) - end - - def set_priority_array(resource_name, priority_array, *filter, &block) - priority_map.set(resource_name.to_sym, Array(priority_array), *filter, &block) - end - - # @api private - def delete_canonical(resource_name, resource_class) - priority_map.delete_canonical(resource_name, resource_class) - end - - # @api private - def list_handlers(*args) - priority_map.list(*args).flatten(1).uniq - end - - private - - def priority_map - require 'chef/node_map' - @priority_map ||= Chef::NodeMap.new - end end end end diff --git a/lib/chef/property.rb b/lib/chef/property.rb new file mode 100644 index 0000000000..09198d90f1 --- /dev/null +++ b/lib/chef/property.rb @@ -0,0 +1,536 @@ +# +# Author:: John Keiser <jkeiser@chef.io> +# Copyright:: Copyright (c) 2015 John Keiser. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'chef/exceptions' +require 'chef/delayed_evaluator' + +class Chef + # + # Type and validation information for a property on a resource. + # + # A property named "x" manipulates the "@x" instance variable on a + # resource. The *presence* of the variable (`instance_variable_defined?(@x)`) + # tells whether the variable is defined; it may have any actual value, + # constrained only by validation. + # + # Properties may have validation, defaults, and coercion, and have full + # support for lazy values. + # + # @see Chef::Resource.property + # @see Chef::DelayedEvaluator + # + class Property + # + # Create a reusable property type that can be used in multiple properties + # in different resources. + # + # @param options [Hash<Symbol,Object>] Validation options. See Chef::Resource.property for + # the list of options. + # + # @example + # Property.derive(default: 'hi') + # + def self.derive(**options) + new(**options) + end + + # + # Create a new property. + # + # @param options [Hash<Symbol,Object>] Property options, including + # control options here, as well as validation options (see + # Chef::Mixin::ParamsValidate#validate for a description of validation + # options). + # @option options [Symbol] :name The name of this property. + # @option options [Class] :declared_in The class this property comes from. + # @option options [Symbol] :instance_variable_name The instance variable + # tied to this property. Must include a leading `@`. Defaults to `@<name>`. + # `nil` means the property is opaque and not tied to a specific instance + # variable. + # @option options [Boolean] :desired_state `true` if this property is part of desired + # state. Defaults to `true`. + # @option options [Boolean] :identity `true` if this property is part of object + # identity. Defaults to `false`. + # @option options [Boolean] :name_property `true` if this + # property defaults to the same value as `name`. Equivalent to + # `default: lazy { name }`, except that #property_is_set? will + # return `true` if the property is set *or* if `name` is set. + # @option options [Object] :default The value this property + # will return if the user does not set one. If this is `lazy`, it will + # be run in the context of the instance (and able to access other + # properties) and cached. If not, the value will be frozen with Object#freeze + # to prevent users from modifying it in an instance. + # @option options [Proc] :coerce A proc which will be called to + # transform the user input to canonical form. The value is passed in, + # and the transformed value returned as output. Lazy values will *not* + # be passed to this method until after they are evaluated. Called in the + # context of the resource (meaning you can access other properties). + # @option options [Boolean] :required `true` if this property + # must be present; `false` otherwise. This is checked after the resource + # is fully initialized. + # + def initialize(**options) + options.each { |k,v| options[k.to_sym] = v if k.is_a?(String) } + options[:name_property] = options.delete(:name_attribute) if options.has_key?(:name_attribute) && !options.has_key?(:name_property) + @options = options + + options[:name] = options[:name].to_sym if options[:name] + options[:instance_variable_name] = options[:instance_variable_name].to_sym if options[:instance_variable_name] + end + + # + # The name of this property. + # + # @return [String] + # + def name + options[:name] + end + + # + # The class this property was defined in. + # + # @return [Class] + # + def declared_in + options[:declared_in] + end + + # + # The instance variable associated with this property. + # + # Defaults to `@<name>` + # + # @return [Symbol] + # + def instance_variable_name + if options.has_key?(:instance_variable_name) + options[:instance_variable_name] + elsif name + :"@#{name}" + end + end + + # + # The raw default value for this resource. + # + # Does not coerce or validate the default. Does not evaluate lazy values. + # + # Defaults to `lazy { name }` if name_property is true; otherwise defaults to + # `nil` + # + def default + return options[:default] if options.has_key?(:default) + return Chef::DelayedEvaluator.new { name } if name_property? + nil + end + + # + # Whether this is part of the resource's natural identity or not. + # + # @return [Boolean] + # + def identity? + options[:identity] + end + + # + # Whether this is part of desired state or not. + # + # Defaults to true. + # + # @return [Boolean] + # + def desired_state? + return true if !options.has_key?(:desired_state) + options[:desired_state] + end + + # + # Whether this is name_property or not. + # + # @return [Boolean] + # + def name_property? + options[:name_property] + end + + # + # Whether this property has a default value. + # + # @return [Boolean] + # + def has_default? + options.has_key?(:default) || name_property? + end + + # + # Whether this property is required or not. + # + # @return [Boolean] + # + def required? + options[:required] + end + + # + # Validation options. (See Chef::Mixin::ParamsValidate#validate.) + # + # @return [Hash<Symbol,Object>] + # + def validation_options + @validation_options ||= options.reject { |k,v| + [:declared_in,:name,:instance_variable_name,:desired_state,:identity,:default,:name_property,:coerce,:required].include?(k) + } + end + + # + # Handle the property being called. + # + # The base implementation does the property get-or-set: + # + # ```ruby + # resource.myprop # get + # resource.myprop value # set + # ``` + # + # Subclasses may implement this with any arguments they want, as long as + # the corresponding DSL calls it correctly. + # + # @param resource [Chef::Resource] The resource to get the property from. + # @param value The value to set (or NOT_PASSED if it is a get). + # + # @return The current value of the property. If it is a `set`, lazy values + # will be returned without running, validating or coercing. If it is a + # `get`, the non-lazy, coerced, validated value will always be returned. + # + def call(resource, value=NOT_PASSED) + if value == NOT_PASSED + return get(resource) + end + + # myprop nil is sometimes a get (backcompat) + if value.nil? && !explicitly_accepts_nil?(resource) + # If you say "my_property nil" and the property explicitly accepts + # nil values, we consider this a get. + Chef.log_deprecation("#{name} nil currently does not overwrite the value of #{name}. This will change in Chef 13, and the value will be set to nil instead. Please change your code to explicitly accept nil using \"property :#{name}, [MyType, nil]\", or stop setting this value to nil.") + return get(resource) + end + + # Anything else (myprop value) is a set + set(resource, value) + end + + # + # Get the property value from the resource, handling lazy values, + # defaults, and validation. + # + # - If the property's value is lazy, it is evaluated, coerced and validated. + # - If the property has no value, and is required, raises ValidationFailed. + # - If the property has no value, but has a lazy default, it is evaluated, + # coerced and validated. If the evaluated value is frozen, the resulting + # - If the property has no value, but has a default, the default value + # will be returned and frozen. If the default value is lazy, it will be + # evaluated, coerced and validated, and the result stored in the property. + # - If the property has no value, but is name_property, `resource.name` + # is retrieved, coerced, validated and stored in the property. + # - Otherwise, `nil` is returned. + # + # @param resource [Chef::Resource] The resource to get the property from. + # + # @return The value of the property. + # + # @raise Chef::Exceptions::ValidationFailed If the value is invalid for + # this property, or if the value is required and not set. + # + def get(resource) + if is_set?(resource) + value = get_value(resource) + if value.is_a?(DelayedEvaluator) + value = exec_in_resource(resource, value) + value = coerce(resource, value) + validate(resource, value) + end + value + + else + if has_default? + value = default + if value.is_a?(DelayedEvaluator) + value = exec_in_resource(resource, value) + end + + value = coerce(resource, value) + + # We don't validate defaults + + # If the value is mutable (non-frozen), we set it on the instance + # so that people can mutate it. (All constant default values are + # frozen.) + if !value.frozen? && !value.nil? + set_value(resource, value) + end + + value + + elsif required? + raise Chef::Exceptions::ValidationFailed, "#{name} is required" + end + end + end + + # + # Set the value of this property in the given resource. + # + # Non-lazy values are coerced and validated before being set. Coercion + # and validation of lazy values is delayed until they are first retrieved. + # + # @param resource [Chef::Resource] The resource to set this property in. + # @param value The value to set. + # + # @return The value that was set, after coercion (if lazy, still returns + # the lazy value) + # + # @raise Chef::Exceptions::ValidationFailed If the value is invalid for + # this property. + # + def set(resource, value) + unless value.is_a?(DelayedEvaluator) + value = coerce(resource, value) + validate(resource, value) + end + set_value(resource, value) + end + + # + # Find out whether this property has been set. + # + # This will be true if: + # - The user explicitly set the value + # - The property has a default, and the value was retrieved. + # + # From this point of view, it is worth looking at this as "what does the + # user think this value should be." In order words, if the user grabbed + # the value, even if it was a default, they probably based calculations on + # it. If they based calculations on it and the value changes, the rest of + # the world gets inconsistent. + # + # @param resource [Chef::Resource] The resource to get the property from. + # + # @return [Boolean] + # + def is_set?(resource) + value_is_set?(resource) + end + + # + # Reset the value of this property so that is_set? will return false and the + # default will be returned in the future. + # + # @param resource [Chef::Resource] The resource to get the property from. + # + def reset(resource) + reset_value(resource) + end + + # + # Coerce an input value into canonical form for the property. + # + # After coercion, the value is suitable for storage in the resource. + # You must validate values after coercion, however. + # + # Does no special handling for lazy values. + # + # @param resource [Chef::Resource] The resource we're coercing against + # (to provide context for the coerce). + # @param value The value to coerce. + # + # @return The coerced value. + # + # @raise Chef::Exceptions::ValidationFailed If the value is invalid for + # this property. + # + def coerce(resource, value) + if options.has_key?(:coerce) + value = exec_in_resource(resource, options[:coerce], value) + end + value + end + + # + # Validate a value. + # + # Calls Chef::Mixin::ParamsValidate#validate with #validation_options as + # options. + # + # @param resource [Chef::Resource] The resource we're validating against + # (to provide context for the validate). + # @param value The value to validate. + # + # @raise Chef::Exceptions::ValidationFailed If the value is invalid for + # this property. + # + def validate(resource, value) + resource.validate({ name => value }, { name => validation_options }) + end + + # + # Derive a new Property that is just like this one, except with some added or + # changed options. + # + # @param options [Hash<Symbol,Object>] List of options that would be passed + # to #initialize. + # + # @return [Property] The new property type. + # + def derive(**modified_options) + Property.new(**options.merge(**modified_options)) + end + + # + # Emit the DSL for this property into the resource class (`declared_in`). + # + # Creates a getter and setter for the property. + # + def emit_dsl + # We don't create the getter/setter if it's a custom property; we will + # be using the existing getter/setter to manipulate it instead. + return if !instance_variable_name + + # We prefer this form because the property name won't show up in the + # stack trace if you use `define_method`. + declared_in.class_eval <<-EOM, __FILE__, __LINE__+1 + def #{name}(value=NOT_PASSED) + self.class.properties[#{name.inspect}].call(self, value) + end + def #{name}=(value) + self.class.properties[#{name.inspect}].set(self, value) + end + EOM + rescue SyntaxError + # If the name is not a valid ruby name, we use define_method. + resource_class.define_method(name) do |value=NOT_PASSED| + self.class.properties[name].call(self, value) + end + resource_class.define_method("#{name}=") do |value| + self.class.properties[name].set(self, value) + end + end + + protected + + # + # The options this Property will use for get/set behavior and validation. + # + # @see #initialize for a list of valid options. + # + attr_reader :options + + # + # Find out whether this type accepts nil explicitly. + # + # A type accepts nil explicitly if "is" allows nil, it validates as nil, *and* is not simply + # an empty type. + # + # These examples accept nil explicitly: + # ```ruby + # property :a, [ String, nil ] + # property :a, [ String, NilClass ] + # property :a, [ String, proc { |v| v.nil? } ] + # ``` + # + # This does not (because the "is" doesn't exist or doesn't have nil): + # + # ```ruby + # property :x, String + # ``` + # + # These do not, even though nil would validate fine (because they do not + # have "is"): + # + # ```ruby + # property :a + # property :a, equal_to: [ 1, 2, 3, nil ] + # property :a, kind_of: [ String, NilClass ] + # property :a, respond_to: [ ] + # property :a, callbacks: { "a" => proc { |v| v.nil? } } + # ``` + # + # @param resource [Chef::Resource] The resource we're coercing against + # (to provide context for the coerce). + # + # @return [Boolean] Whether this value explicitly accepts nil. + # + # @api private + def explicitly_accepts_nil?(resource) + options.has_key?(:is) && resource.send(:_pv_is, { name => nil }, name, options[:is], raise_error: false) + end + + def get_value(resource) + if instance_variable_name + resource.instance_variable_get(instance_variable_name) + else + resource.send(name) + end + end + + def set_value(resource, value) + if instance_variable_name + resource.instance_variable_set(instance_variable_name, value) + else + resource.send(name, value) + end + end + + def value_is_set?(resource) + if instance_variable_name + resource.instance_variable_defined?(instance_variable_name) + else + true + end + end + + def reset_value(resource) + if instance_variable_name + if value_is_set?(resource) + resource.remove_instance_variable(instance_variable_name) + end + else + raise ArgumentError, "Property #{name} has no instance variable defined and cannot be reset" + end + end + + def exec_in_resource(resource, proc, *args) + if resource + if proc.arity > args.size + value = proc.call(resource, *args) + else + value = resource.instance_exec(*args, &proc) + end + else + value = proc.call + end + + if value.is_a?(DelayedEvaluator) + value = coerce(resource, value) + validate(resource, value) + end + value + end + end +end diff --git a/lib/chef/provider.rb b/lib/chef/provider.rb index e50e374804..3138704a55 100644 --- a/lib/chef/provider.rb +++ b/lib/chef/provider.rb @@ -26,6 +26,7 @@ require 'chef/mixin/powershell_out' require 'chef/mixin/provides' require 'chef/platform/service_helpers' require 'chef/node_map' +require 'forwardable' class Chef class Provider @@ -65,6 +66,7 @@ class Chef @recipe_name = nil @cookbook_name = nil + self.class.include_resource_dsl_module(new_resource) end def whyrun_mode? @@ -119,11 +121,11 @@ class Chef 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? + if whyrun_mode? && !whyrun_supported? + events.resource_current_state_load_bypassed(@new_resource, @action, @current_resource) + else load_current_resource events.resource_current_state_loaded(@new_resource, @action, @current_resource) - elsif whyrun_mode? && !whyrun_supported? - events.resource_current_state_load_bypassed(@new_resource, @action, @current_resource) end define_resource_requirements @@ -136,9 +138,7 @@ class Chef # we can't execute the action. # in non-whyrun mode, this will still cause the action to be # executed normally. - if whyrun_supported? && !requirements.action_blocked?(@action) - send("action_#{@action}") - elsif whyrun_mode? + if whyrun_mode? && (!whyrun_supported? || requirements.action_blocked?(@action)) events.resource_bypassed(@new_resource, @action, self) else send("action_#{@action}") @@ -175,14 +175,221 @@ class Chef converge_actions.add_action(descriptions, &block) end + # + # Handle patchy convergence safely. + # + # - Does *not* call the block if the current_resource's properties match + # the properties the user specified on the resource. + # - Calls the block if current_resource does not exist + # - Calls the block if the user has specified any properties in the resource + # whose values are *different* from current_resource. + # - Does *not* call the block if why-run is enabled (just prints out text). + # - Prints out automatic green text saying what properties have changed. + # + # @param properties An optional list of property names (symbols). If not + # specified, `new_resource.class.state_properties` will be used. + # @param converge_block The block to do the converging in. + # + # @return [Boolean] whether the block was executed. + # + def converge_if_changed(*properties, &converge_block) + if !converge_block + raise ArgumentError, "converge_if_changed must be passed a block!" + end + + properties = new_resource.class.state_properties.map { |p| p.name } if properties.empty? + properties = properties.map { |p| p.to_sym } + if current_resource + # Collect the list of modified properties + specified_properties = properties.select { |property| new_resource.property_is_set?(property) } + modified = specified_properties.select { |p| new_resource.send(p) != current_resource.send(p) } + if modified.empty? + Chef::Log.debug("Skipping update of #{new_resource.to_s}: has not changed any of the specified properties #{specified_properties.map { |p| "#{p}=#{new_resource.send(p).inspect}" }.join(", ")}.") + return false + end + + # Print the pretty green text and run the block + property_size = modified.map { |p| p.size }.max + modified = modified.map { |p| " set #{p.to_s.ljust(property_size)} to #{new_resource.send(p).inspect} (was #{current_resource.send(p).inspect})" } + converge_by([ "update #{current_resource.identity}" ] + modified, &converge_block) + + else + # The resource doesn't exist. Mark that we are *creating* this, and + # write down any properties we are setting. + property_size = properties.map { |p| p.size }.max + created = [] + properties.each do |property| + if new_resource.property_is_set?(property) + created << " set #{property.to_s.ljust(property_size)} to #{new_resource.send(property).inspect}" + else + created << " set #{property.to_s.ljust(property_size)} to #{new_resource.send(property).inspect} (default value)" + end + end + + converge_by([ "create #{new_resource.identity}" ] + created, &converge_block) + end + true + end + def self.provides(short_name, opts={}, &block) - Chef.set_provider_priority_array(short_name, self, opts, &block) + Chef.provider_handler_map.set(short_name, self, opts, &block) end def self.provides?(node, resource) Chef::ProviderResolver.new(node, resource, :nothing).provided_by?(self) end + # + # Include attributes, public and protected methods from this Resource in + # the provider. + # + # If this is set to true, delegate methods are included in the provider so + # that you can call (for example) `attrname` and it will call + # `new_resource.attrname`. + # + # The actual include does not happen until the first time the Provider + # is instantiated (so that we don't have to worry about load order issues). + # + # @param include_resource_dsl [Boolean] Whether to include resource DSL or + # not (defaults to `false`). + # + def self.include_resource_dsl(include_resource_dsl) + @include_resource_dsl = include_resource_dsl + end + + # Create the resource DSL module that forwards resource methods to new_resource + # + # @api private + def self.include_resource_dsl_module(resource) + if @include_resource_dsl && !defined?(@included_resource_dsl_module) + provider_class = self + @included_resource_dsl_module = Module.new do + extend Forwardable + define_singleton_method(:to_s) { "#{resource_class} forwarder module" } + define_singleton_method(:inspect) { to_s } + # Add a delegator for each explicit property that will get the *current* value + # of the property by default instead of the *actual* value. + resource.class.properties.each do |name, property| + class_eval(<<-EOM, __FILE__, __LINE__) + def #{name}(*args, &block) + # If no arguments were passed, we process "get" by defaulting + # the value to current_resource, not new_resource. This helps + # avoid issues where resources accidentally overwrite perfectly + # valid stuff with default values. + if args.empty? && !block + if !new_resource.property_is_set?(__method__) && current_resource + return current_resource.public_send(__method__) + end + end + new_resource.public_send(__method__, *args, &block) + end + EOM + end + dsl_methods = + resource.class.public_instance_methods + + resource.class.protected_instance_methods - + provider_class.instance_methods - + resource.class.properties.keys + def_delegators(:new_resource, *dsl_methods) + end + include @included_resource_dsl_module + end + end + + # Enables inline evaluation of resources in provider actions. + # + # Without this option, any resources declared inside the Provider 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 Provider, and potentially be notified by resources outside the Provider + # (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 Provider 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 Provider cannot interact with + # external resources via notifies, though notifications to other + # resources within the Provider 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::Provider::InlineResources + # Implementation of inline resource convergence for providers. See + # Provider.use_inline_resources for a longer explanation. + # + # This code is restricted to a module so that it can be selectively + # applied to providers on an opt-in basis. + # + # @api private + module InlineResources + + # Our run context is a child of the main run context; that gives us a + # whole new resource collection and notification set. + def initialize(resource, run_context) + super(resource, run_context.create_child) + end + + # Class methods for InlineResources. Overrides the `action` DSL method + # with one that enables inline resource convergence. + # + # @api private + module ClassMethods + # Defines an action method on the provider, running the block to + # compile the resources, converging them, and then checking if any + # were updated (and updating new-resource if so) + def action(name, &block) + # We first try to create the method using "def method_name", which is + # preferred because it actually shows up in stack traces. If that + # fails, we try define_method. + begin + class_eval <<-EOM, __FILE__, __LINE__+1 + def action_#{name} + return_value = compile_action_#{name} + Chef::Runner.new(run_context).converge + return_value + ensure + if run_context.resource_collection.any? {|r| r.updated? } + new_resource.updated_by_last_action(true) + end + end + EOM + rescue SyntaxError + define_method("action_#{name}") do + begin + return_value = send("compile_action_#{name}") + Chef::Runner.new(run_context).converge + return_value + ensure + if run_context.resource_collection.any? {|r| r.updated? } + new_resource.updated_by_last_action(true) + end + end + end + end + # We put the action in its own method so that super() works. + define_method("compile_action_#{name}", &block) + end + end + + require 'chef/dsl/recipe' + include Chef::DSL::Recipe::FullDSL + end + protected def converge_actions @@ -200,19 +407,21 @@ class Chef # manipulating notifies. converge_by ("evaluate block and run any associated actions") do - saved_run_context = @run_context - @run_context = @run_context.dup - @run_context.resource_collection = Chef::ResourceCollection.new - instance_eval(&block) - Chef::Runner.new(@run_context).converge - @run_context = saved_run_context + saved_run_context = run_context + begin + @run_context = run_context.create_child + instance_eval(&block) + Chef::Runner.new(run_context).converge + ensure + @run_context = saved_run_context + end 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.") + 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}" diff --git a/lib/chef/provider/batch.rb b/lib/chef/provider/batch.rb index b6b386e5a8..5f0134443d 100644 --- a/lib/chef/provider/batch.rb +++ b/lib/chef/provider/batch.rb @@ -28,6 +28,14 @@ class Chef super(new_resource, run_context, '.bat') end + def command + basepath = is_forced_32bit ? wow64_directory : run_context.node.kernel.os_info.system_directory + + interpreter_path = Chef::Util::PathHelper.join(basepath, interpreter) + + "\"#{interpreter_path}\" #{flags} \"#{script_file.path}\"" + end + def flags @new_resource.flags.nil? ? '/c' : new_resource.flags + ' /c' end diff --git a/lib/chef/provider/deploy.rb b/lib/chef/provider/deploy.rb index 19e7c01ab1..77a0410593 100644 --- a/lib/chef/provider/deploy.rb +++ b/lib/chef/provider/deploy.rb @@ -201,7 +201,7 @@ class Chef converge_by("execute migration command #{@new_resource.migration_command}") do Chef::Log.info "#{@new_resource} migrating #{@new_resource.user} with environment #{env_info}" - run_command(run_options(:command => @new_resource.migration_command, :cwd=>release_path, :log_level => :info)) + shell_out!(@new_resource.migration_command,run_options(:cwd=>release_path, :log_level => :info)) end end end @@ -221,7 +221,7 @@ class Chef else converge_by("restart app using command #{@new_resource.restart_command}") do Chef::Log.info("#{@new_resource} restarting app") - run_command(run_options(:command => @new_resource.restart_command, :cwd => @new_resource.current_path)) + shell_out!(@new_resource.restart_command,run_options(:cwd=>@new_resource.current_path)) end end end @@ -373,11 +373,9 @@ class Chef end def gem_resource_collection_runner - gems_collection = Chef::ResourceCollection.new - gem_packages.each { |rbgem| gems_collection.insert(rbgem) } - gems_run_context = run_context.dup - gems_run_context.resource_collection = gems_collection - Chef::Runner.new(gems_run_context) + child_context = run_context.create_child + gem_packages.each { |rbgem| child_context.resource_collection.insert(rbgem) } + Chef::Runner.new(child_context) end def gem_packages diff --git a/lib/chef/provider/directory.rb b/lib/chef/provider/directory.rb index 4d5423d0e8..8892d3a73d 100644 --- a/lib/chef/provider/directory.rb +++ b/lib/chef/provider/directory.rb @@ -64,7 +64,13 @@ class Chef is_parent_writable = lambda do |base_dir| base_dir = ::File.dirname(base_dir) if ::File.exists?(base_dir) - Chef::FileAccessControl.writable?(base_dir) + if Chef::FileAccessControl.writable?(base_dir) + true + elsif Chef::Util::PathHelper.is_sip_path?(base_dir, node) + Chef::Util::PathHelper.writable_sip_path?(base_dir) + else + false + end else is_parent_writable.call(base_dir) end @@ -74,7 +80,13 @@ class Chef # in why run mode & parent directory does not exist no permissions check is required # If not in why run, permissions must be valid and we rely on prior assertion that dir exists if !whyrun_mode? || ::File.exists?(parent_directory) - Chef::FileAccessControl.writable?(parent_directory) + if Chef::FileAccessControl.writable?(parent_directory) + true + elsif Chef::Util::PathHelper.is_sip_path?(parent_directory, node) + Chef::Util::PathHelper.writable_sip_path?(@new_resource.path) + else + false + end else true end diff --git a/lib/chef/provider/dsc_resource.rb b/lib/chef/provider/dsc_resource.rb index 5fa84a21e9..379369ba6e 100644 --- a/lib/chef/provider/dsc_resource.rb +++ b/lib/chef/provider/dsc_resource.rb @@ -53,7 +53,7 @@ class Chef requirements.assert(:run) do |a| a.assertion { supports_dsc_invoke_resource? } err = ["You must have Powershell version >= 5.0.10018.0 to use dsc_resource."] - a.failure_message Chef::Exceptions::NoProviderAvailable, + a.failure_message Chef::Exceptions::ProviderNotFound, err a.whyrun err + ["Assuming a previous resource installs Powershell 5.0.10018.0 or higher."] a.block_action! @@ -63,7 +63,7 @@ class Chef meta_configuration['RefreshMode'] == 'Disabled' } err = ["The LCM must have its RefreshMode set to Disabled. "] - a.failure_message Chef::Exceptions::NoProviderAvailable, err.join(' ') + a.failure_message Chef::Exceptions::ProviderNotFound, err.join(' ') a.whyrun err + ["Assuming a previous resource sets the RefreshMode."] a.block_action! end diff --git a/lib/chef/provider/dsc_script.rb b/lib/chef/provider/dsc_script.rb index a75e68a475..b2432132b7 100644 --- a/lib/chef/provider/dsc_script.rb +++ b/lib/chef/provider/dsc_script.rb @@ -70,7 +70,7 @@ class Chef "Powershell 4.0 or higher was not detected on your system and is required to use the dsc_script resource.", ] a.assertion { supports_dsc? } - a.failure_message Chef::Exceptions::NoProviderAvailable, err.join(' ') + a.failure_message Chef::Exceptions::ProviderNotFound, err.join(' ') a.whyrun err + ["Assuming a previous resource installs Powershell 4.0 or higher."] a.block_action! end diff --git a/lib/chef/provider/group/pw.rb b/lib/chef/provider/group/pw.rb index f877ed2424..5b5c8136f1 100644 --- a/lib/chef/provider/group/pw.rb +++ b/lib/chef/provider/group/pw.rb @@ -109,7 +109,7 @@ class Chef else # Append is not set so we're resetting the membership of # the group to the given members. - members_to_be_added = @new_resource.members + members_to_be_added = @new_resource.members.dup @current_resource.members.each do |member| # No need to re-add a member if it's present in the new # list of members diff --git a/lib/chef/provider/ifconfig.rb b/lib/chef/provider/ifconfig.rb index 468e1ec639..7869917307 100644 --- a/lib/chef/provider/ifconfig.rb +++ b/lib/chef/provider/ifconfig.rb @@ -194,7 +194,7 @@ class Chef private def add_command - command = "ifconfig #{@new_resource.device} #{@new_resource.name}" + command = "ifconfig #{@new_resource.device} #{@new_resource.target}" command << " netmask #{@new_resource.mask}" if @new_resource.mask command << " metric #{@new_resource.metric}" if @new_resource.metric command << " mtu #{@new_resource.mtu}" if @new_resource.mtu @@ -202,7 +202,7 @@ class Chef end def enable_command - command = "ifconfig #{@new_resource.device} #{@new_resource.name}" + command = "ifconfig #{@new_resource.device} #{@new_resource.target}" command << " netmask #{@new_resource.mask}" if @new_resource.mask command << " metric #{@new_resource.metric}" if @new_resource.metric command << " mtu #{@new_resource.mtu}" if @new_resource.mtu diff --git a/lib/chef/provider/lwrp_base.rb b/lib/chef/provider/lwrp_base.rb index b5efbb284d..a96c382a01 100644 --- a/lib/chef/provider/lwrp_base.rb +++ b/lib/chef/provider/lwrp_base.rb @@ -28,52 +28,10 @@ class Chef # Base class from which LWRP providers inherit. class LWRPBase < Provider - # Chef::Provider::LWRPBase::InlineResources - # Implementation of inline resource convergence for LWRP providers. See - # Provider::LWRPBase.use_inline_resources for a longer explanation. - # - # This code is restricted to a module so that it can be selectively - # applied to providers on an opt-in basis. - module InlineResources - - # Class methods for InlineResources. Overrides the `action` DSL method - # with one that enables inline resource convergence. - module ClassMethods - # Defines an action method on the provider, using - # recipe_eval_with_update_check to execute the given block. - def action(name, &block) - define_method("action_#{name}") do - recipe_eval_with_update_check(&block) - end - end - end - - # Executes the given block in a temporary run_context with its own - # resource collection. After the block is executed, any resources - # declared inside are converged, and if any are updated, the - # new_resource will be marked updated. - def recipe_eval_with_update_check(&block) - saved_run_context = @run_context - temp_run_context = @run_context.dup - @run_context = temp_run_context - @run_context.resource_collection = Chef::ResourceCollection.new - - return_value = instance_eval(&block) - Chef::Runner.new(@run_context).converge - return_value - ensure - @run_context = saved_run_context - if temp_run_context.resource_collection.any? {|r| r.updated? } - new_resource.updated_by_last_action(true) - end - end - - end - include Chef::DSL::Recipe # These were previously provided by Chef::Mixin::RecipeDefinitionDSLCore. - # They are not included by its replacment, Chef::DSL::Recipe, but + # They are not included by its replacement, Chef::DSL::Recipe, but # they may be used in existing LWRPs. include Chef::DSL::PlatformIntrospection include Chef::DSL::DataQuery @@ -122,38 +80,6 @@ class Chef provider_class 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 diff --git a/lib/chef/provider/mount.rb b/lib/chef/provider/mount.rb index 2039e9ae51..6bdfd5b867 100644 --- a/lib/chef/provider/mount.rb +++ b/lib/chef/provider/mount.rb @@ -42,13 +42,17 @@ class Chef end def action_mount - unless current_resource.mounted + if current_resource.mounted + if mount_options_unchanged? + Chef::Log.debug("#{new_resource} is already mounted") + else + action_remount + end + else converge_by("mount #{current_resource.device} to #{current_resource.mount_point}") do mount_fs Chef::Log.info("#{new_resource} mounted") end - else - Chef::Log.debug("#{new_resource} is already mounted") end end diff --git a/lib/chef/provider/mount/aix.rb b/lib/chef/provider/mount/aix.rb index 4ad7b24c15..510dfde46d 100644 --- a/lib/chef/provider/mount/aix.rb +++ b/lib/chef/provider/mount/aix.rb @@ -32,7 +32,7 @@ class Chef @new_resource.options.clear end if @new_resource.fstype == "auto" - @new_resource.fstype = nil + @new_resource.send(:clear_fstype) end end diff --git a/lib/chef/provider/package.rb b/lib/chef/provider/package.rb index 9d534ec414..880104bff7 100644 --- a/lib/chef/provider/package.rb +++ b/lib/chef/provider/package.rb @@ -142,7 +142,7 @@ class Chef def action_remove if removing_package? description = @new_resource.version ? "version #{@new_resource.version} of " : "" - converge_by("remove #{description} package #{@current_resource.package_name}") do + converge_by("remove #{description}package #{@current_resource.package_name}") do remove_package(@current_resource.package_name, @new_resource.version) Chef::Log.info("#{@new_resource} removed") end @@ -491,37 +491,6 @@ class Chef 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) diff --git a/lib/chef/provider/package/aix.rb b/lib/chef/provider/package/aix.rb index b97db9d061..5165f4b4ea 100644 --- a/lib/chef/provider/package/aix.rb +++ b/lib/chef/provider/package/aix.rb @@ -26,6 +26,7 @@ class Chef class Package class Aix < Chef::Provider::Package + provides :package, os: "aix" provides :bff_package, os: "aix" include Chef::Mixin::GetSourceFromPackage diff --git a/lib/chef/provider/package/apt.rb b/lib/chef/provider/package/apt.rb index bd6ed283bf..e109c9966a 100644 --- a/lib/chef/provider/package/apt.rb +++ b/lib/chef/provider/package/apt.rb @@ -25,6 +25,7 @@ class Chef class Package class Apt < Chef::Provider::Package + provides :package, platform_family: "debian" provides :apt_package, os: "linux" # return [Hash] mapping of package name to Boolean value diff --git a/lib/chef/provider/package/dpkg.rb b/lib/chef/provider/package/dpkg.rb index a262f1ab1a..67e9b903c6 100644 --- a/lib/chef/provider/package/dpkg.rb +++ b/lib/chef/provider/package/dpkg.rb @@ -25,8 +25,6 @@ class Chef class Provider class Package class Dpkg < Chef::Provider::Package - # http://www.debian.org/doc/debian-policy/ch-controlfields.html#s-f-Version - DPKG_INFO = /([a-z\d\-\+\.]+)\t([\w\d.~:-]+)/ DPKG_INSTALLED = /^Status: install ok installed/ DPKG_VERSION = /^Version: (.+)$/ @@ -54,26 +52,22 @@ class Chef @source_exists = true @current_resource = Chef::Resource::Package.new(@new_resource.name) @current_resource.package_name(@new_resource.package_name) - @new_resource.version(nil) if @new_resource.source @source_exists = ::File.exists?(@new_resource.source) if @source_exists # Get information from the package if supplied Chef::Log.debug("#{@new_resource} checking dpkg status") - - 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]) - @candidate_version = pkginfo[2] - end + status = shell_out_with_timeout("dpkg-deb -W #{@new_resource.source}") + pkginfo = status.stdout.split("\t") + unless pkginfo.empty? + @current_resource.package_name(pkginfo[0]) + @candidate_version = pkginfo[1].strip end else # Source provided but not valid means we can't safely do further processing return end - end # Check to see if it is installed diff --git a/lib/chef/provider/package/homebrew.rb b/lib/chef/provider/package/homebrew.rb index beede1c916..e5c45f0a62 100644 --- a/lib/chef/provider/package/homebrew.rb +++ b/lib/chef/provider/package/homebrew.rb @@ -26,6 +26,7 @@ class Chef class Package class Homebrew < Chef::Provider::Package + provides :package, os: "darwin", override: true provides :homebrew_package include Chef::Mixin::HomebrewUser diff --git a/lib/chef/provider/package/ips.rb b/lib/chef/provider/package/ips.rb index 4d7f4a3583..96c2e711d4 100644 --- a/lib/chef/provider/package/ips.rb +++ b/lib/chef/provider/package/ips.rb @@ -27,6 +27,7 @@ class Chef class Package class Ips < Chef::Provider::Package + provides :package, platform: %w(openindiana opensolaris omnios solaris2) provides :ips_package, os: "solaris2" attr_accessor :virtual diff --git a/lib/chef/provider/package/macports.rb b/lib/chef/provider/package/macports.rb index e945211540..c7ea71ac8c 100644 --- a/lib/chef/provider/package/macports.rb +++ b/lib/chef/provider/package/macports.rb @@ -3,6 +3,7 @@ class Chef class Package class Macports < Chef::Provider::Package + provides :package, os: "darwin" provides :macports_package def load_current_resource diff --git a/lib/chef/provider/package/openbsd.rb b/lib/chef/provider/package/openbsd.rb index f231101390..83fc09c8ae 100644 --- a/lib/chef/provider/package/openbsd.rb +++ b/lib/chef/provider/package/openbsd.rb @@ -31,6 +31,7 @@ class Chef class Openbsd < Chef::Provider::Package provides :package, os: "openbsd" + provides :openbsd_package include Chef::Mixin::ShellOut include Chef::Mixin::GetSourceFromPackage diff --git a/lib/chef/provider/package/pacman.rb b/lib/chef/provider/package/pacman.rb index bf03e54656..01e3a9cc01 100644 --- a/lib/chef/provider/package/pacman.rb +++ b/lib/chef/provider/package/pacman.rb @@ -25,6 +25,7 @@ class Chef class Package class Pacman < Chef::Provider::Package + provides :package, platform: "arch" provides :pacman_package, os: "linux" def load_current_resource diff --git a/lib/chef/provider/package/paludis.rb b/lib/chef/provider/package/paludis.rb index 407e0d0110..2d6302515b 100644 --- a/lib/chef/provider/package/paludis.rb +++ b/lib/chef/provider/package/paludis.rb @@ -24,6 +24,7 @@ class Chef class Package class Paludis < Chef::Provider::Package + provides :package, platform: "exherbo" provides :paludis_package, os: "linux" def load_current_resource diff --git a/lib/chef/provider/package/portage.rb b/lib/chef/provider/package/portage.rb index 4ba0160bb0..95782a6774 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 :package, platform: "gentoo" provides :portage_package PACKAGE_NAME_PATTERN = %r{(?:([^/]+)/)?([^/]+)} diff --git a/lib/chef/provider/package/rpm.rb b/lib/chef/provider/package/rpm.rb index 21c39752d1..c5d52a8384 100644 --- a/lib/chef/provider/package/rpm.rb +++ b/lib/chef/provider/package/rpm.rb @@ -61,7 +61,7 @@ class Chef Chef::Log.debug("#{@new_resource} checking rpm status") 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_.-]+)$/ + when /^([\w\d+_.-]+)\s([\w\d~_.-]+)$/ @current_resource.package_name($1) @new_resource.version($2) @candidate_version = $2 @@ -78,7 +78,7 @@ class Chef @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_.-]+)$/ + when /^([\w\d+_.-]+)\s([\w\d~_.-]+)$/ Chef::Log.debug("#{@new_resource} current version is #{$2}") @current_resource.version($2) end diff --git a/lib/chef/provider/package/rubygems.rb b/lib/chef/provider/package/rubygems.rb index b5f7dbdd80..729f755b2a 100644 --- a/lib/chef/provider/package/rubygems.rb +++ b/lib/chef/provider/package/rubygems.rb @@ -394,7 +394,7 @@ class Chef end def is_omnibus? - if RbConfig::CONFIG['bindir'] =~ %r!/opt/(opscode|chef)/embedded/bin! + if RbConfig::CONFIG['bindir'] =~ %r!/(opscode|chef|chefdk)/embedded/bin! Chef::Log.debug("#{@new_resource} detected omnibus installation in #{RbConfig::CONFIG['bindir']}") # Omnibus installs to a static path because of linking on unix, find it. true diff --git a/lib/chef/provider/package/smartos.rb b/lib/chef/provider/package/smartos.rb index 0d5b801c96..71b8a9b9e1 100644 --- a/lib/chef/provider/package/smartos.rb +++ b/lib/chef/provider/package/smartos.rb @@ -29,6 +29,7 @@ class Chef class SmartOS < Chef::Provider::Package attr_accessor :is_virtual_package + provides :package, platform: "smartos" provides :smartos_package, os: "solaris2", platform_family: "smartos" def load_current_resource diff --git a/lib/chef/provider/package/solaris.rb b/lib/chef/provider/package/solaris.rb index 9b10403344..e62f37d27b 100644 --- a/lib/chef/provider/package/solaris.rb +++ b/lib/chef/provider/package/solaris.rb @@ -27,6 +27,8 @@ class Chef include Chef::Mixin::GetSourceFromPackage + provides :package, platform: "nexentacore" + provides :package, platform: "solaris2", platform_version: '< 5.11' provides :solaris_package, os: "solaris2" # def initialize(*args) diff --git a/lib/chef/provider/package/yum.rb b/lib/chef/provider/package/yum.rb index 85c2ba683c..81454380a3 100644 --- a/lib/chef/provider/package/yum.rb +++ b/lib/chef/provider/package/yum.rb @@ -1,6 +1,6 @@ # Author:: Adam Jacob (<adam@opscode.com>) -# Copyright:: Copyright (c) 2008 Opscode, Inc. +# Copyright:: Copyright (c) 2008-2015 Chef Software, Inc. # License:: Apache License, Version 2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -28,6 +28,7 @@ class Chef class Package class Yum < Chef::Provider::Package + provides :package, platform_family: %w(rhel fedora) provides :yum_package, os: "linux" class RPMUtils @@ -650,6 +651,8 @@ class Chef include Chef::Mixin::ShellOut include Singleton + attr_accessor :yum_binary + def initialize @rpmdb = RPMDb.new @@ -780,7 +783,7 @@ class Chef end def python_bin - yum_executable = which("yum") + yum_executable = which(yum_binary) if yum_executable && shabang?(yum_executable) extract_interpreter(yum_executable) else @@ -979,6 +982,15 @@ class Chef super @yum = YumCache.instance + @yum.yum_binary = yum_binary + end + + def yum_binary + @yum_binary ||= + begin + yum_binary = new_resource.yum_binary if new_resource.is_a?(Chef::Resource::YumPackage) + yum_binary ||= ::File.exist?("/usr/bin/yum-deprecated") ? "yum-deprecated" : "yum" + end end # Extra attributes @@ -1025,6 +1037,7 @@ class Chef end def yum_command(command) + command = "#{yum_binary} #{command}" Chef::Log.debug("#{@new_resource}: yum command: \"#{command}\"") status = shell_out_with_timeout(command, {:timeout => Chef::Config[:yum_timeout]}) @@ -1232,7 +1245,7 @@ class Chef end pkg_string = pkg_string_bits.join(' ') Chef::Log.info("#{@new_resource} #{log_method} #{repos.join(' ')}") - yum_command("yum -d0 -e0 -y#{expand_options(@new_resource.options)} #{method} #{pkg_string}") + yum_command("-d0 -e0 -y#{expand_options(@new_resource.options)} #{method} #{pkg_string}") else raise Chef::Exceptions::Package, "Version #{version} of #{name} not found. Did you specify both version " + "and release? (version-release, e.g. 1.84-10.fc6)" @@ -1241,7 +1254,7 @@ class Chef def install_package(name, version) if @new_resource.source - yum_command("yum -d0 -e0 -y#{expand_options(@new_resource.options)} localinstall #{@new_resource.source}") + yum_command("-d0 -e0 -y#{expand_options(@new_resource.options)} localinstall #{@new_resource.source}") else install_remote_package(name, version) end @@ -1289,7 +1302,7 @@ class Chef "#{n}#{yum_arch(a)}" end.join(' ') end - yum_command("yum -d0 -e0 -y#{expand_options(@new_resource.options)} remove #{remove_str}") + yum_command("-d0 -e0 -y#{expand_options(@new_resource.options)} remove #{remove_str}") if flush_cache[:after] @yum.reload diff --git a/lib/chef/provider/package/zypper.rb b/lib/chef/provider/package/zypper.rb index c2a3ac4ba8..ac42304ffb 100644 --- a/lib/chef/provider/package/zypper.rb +++ b/lib/chef/provider/package/zypper.rb @@ -29,6 +29,7 @@ class Chef class Package class Zypper < Chef::Provider::Package + provides :package, platform_family: "suse" provides :zypper_package, os: "linux" def load_current_resource diff --git a/lib/chef/provider/powershell_script.rb b/lib/chef/provider/powershell_script.rb index ed44dee6ae..b876b6d8ee 100644 --- a/lib/chef/provider/powershell_script.rb +++ b/lib/chef/provider/powershell_script.rb @@ -30,8 +30,17 @@ class Chef end def action_run - valid_syntax = validate_script_syntax! - super if valid_syntax + validate_script_syntax! + super + end + + def command + basepath = is_forced_32bit ? wow64_directory : run_context.node.kernel.os_info.system_directory + + # Powershell.exe is always in "v1.0" folder (for backwards compatibility) + interpreter_path = Chef::Util::PathHelper.join(basepath, "WindowsPowerShell", "v1.0", interpreter) + + "\"#{interpreter_path}\" #{flags} \"#{script_file.path}\"" end def flags @@ -62,30 +71,46 @@ class Chef 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 + # Wrap the user's code in a PowerShell script block so that + # it isn't executed. However, syntactically invalid script + # in that block will still trigger a syntax error which is + # exactly what we want here -- verify the syntax without + # actually running the script. + user_code_wrapped_in_powershell_script_block = <<-EOH +{ + #{@new_resource.code} +} +EOH + user_script_file.puts user_code_wrapped_in_powershell_script_block + # A .close or explicit .flush required to ensure the file is + # written to the file system at this point, which is required since + # the intent is to execute the code just written to it. + user_script_file.close 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 + # Note that other script providers like bash allow syntax errors + # to be suppressed by setting 'returns' to a value that the + # interpreter would return as a status code in the syntax + # error case. We explicitly don't do this here -- syntax + # errors will not be suppressed, since doing so could make + # it harder for users to detect / debug invalid scripts. + + # Therefore, the only return value for a syntactically valid + # script is 0. If an exception is raised by shellout, this + # means a non-zero return and thus a syntactically invalid script. + + with_os_architecture(node, architecture: new_resource.architecture) do + shell_out!(validation_command, {returns: [0]}) + end end end 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 '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' [ diff --git a/lib/chef/provider/registry_key.rb b/lib/chef/provider/registry_key.rb index cd62f7c56f..948fa6c63f 100644 --- a/lib/chef/provider/registry_key.rb +++ b/lib/chef/provider/registry_key.rb @@ -64,7 +64,7 @@ class Chef def values_to_hash(values) if values - @name_hash = Hash[values.map { |val| [val[:name], val] }] + @name_hash = Hash[values.map { |val| [val[:name].downcase, val] }] else @name_hash = {} end @@ -100,8 +100,8 @@ class Chef end end @new_resource.unscrubbed_values.each do |value| - if @name_hash.has_key?(value[:name]) - current_value = @name_hash[value[:name]] + if @name_hash.has_key?(value[:name].downcase) + current_value = @name_hash[value[:name].downcase] unless current_value[:type] == value[:type] && current_value[:data] == value[:data] converge_by("set value #{value}") do registry.set_value(@new_resource.key, value) @@ -122,7 +122,7 @@ class Chef end end @new_resource.unscrubbed_values.each do |value| - unless @name_hash.has_key?(value[:name]) + unless @name_hash.has_key?(value[:name].downcase) converge_by("create value #{value}") do registry.set_value(@new_resource.key, value) end @@ -133,7 +133,7 @@ class Chef def action_delete if registry.key_exists?(@new_resource.key) @new_resource.unscrubbed_values.each do |value| - if @name_hash.has_key?(value[:name]) + if @name_hash.has_key?(value[:name].downcase) converge_by("delete value #{value}") do registry.delete_value(@new_resource.key, value) end diff --git a/lib/chef/provider/remote_directory.rb b/lib/chef/provider/remote_directory.rb index eaccce46cf..85ceb5cdae 100644 --- a/lib/chef/provider/remote_directory.rb +++ b/lib/chef/provider/remote_directory.rb @@ -67,7 +67,7 @@ class Chef ::File::FNM_DOTMATCH) # Remove current directory and previous directory - files.reject! do |name| + files = files.reject do |name| basename = Pathname.new(name).basename().to_s ['.', '..'].include?(basename) end diff --git a/lib/chef/provider/service.rb b/lib/chef/provider/service.rb index 9c523b5e66..e7bb2a76d7 100644 --- a/lib/chef/provider/service.rb +++ b/lib/chef/provider/service.rb @@ -1,6 +1,6 @@ # # Author:: AJ Christensen (<aj@hjksolutions.com>) -# Copyright:: Copyright (c) 2008 Opscode, Inc. +# Copyright:: Copyright (c) 2008-2015 Chef Software, Inc. # License:: Apache License, Version 2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -25,6 +25,10 @@ class Chef include Chef::Mixin::Command + def supports + @supports ||= new_resource.supports.dup + end + def initialize(new_resource, run_context) super @enabled = nil @@ -34,6 +38,12 @@ class Chef true end + def load_current_resource + supports[:status] = false if supports[:status].nil? + supports[:reload] = false if supports[:reload].nil? + supports[:restart] = false if supports[:restart].nil? + end + def load_new_resource_state # If the user didn't specify a change in enabled state, # it will be the same as the old resource @@ -50,7 +60,7 @@ class Chef def define_resource_requirements requirements.assert(:reload) do |a| - a.assertion { @new_resource.supports[:reload] || @new_resource.reload_command } + a.assertion { supports[:reload] || @new_resource.reload_command } a.failure_message Chef::Exceptions::UnsupportedAction, "#{self.to_s} does not support :reload" # if a service is not declared to support reload, that won't # typically change during the course of a run - so no whyrun @@ -188,29 +198,11 @@ class Chef 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 + Chef.set_provider_priority_array :service, [ Systemd, Arch ], platform_family: 'arch' + Chef.set_provider_priority_array :service, [ Systemd, Gentoo ], platform_family: 'gentoo' + Chef.set_provider_priority_array :service, [ Systemd, Upstart, Insserv, Debian, Invokercd ], platform_family: 'debian' + Chef.set_provider_priority_array :service, [ Systemd, Insserv, Redhat ], platform_family: %w(rhel fedora suse) end end end diff --git a/lib/chef/provider/service/aix.rb b/lib/chef/provider/service/aix.rb index 09ed4bbf01..0c95ce2c8e 100644 --- a/lib/chef/provider/service/aix.rb +++ b/lib/chef/provider/service/aix.rb @@ -116,7 +116,7 @@ class Chef end def is_resource_group? - so = shell_out!("lssrc -g #{@new_resource.service_name}") + 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 diff --git a/lib/chef/provider/service/debian.rb b/lib/chef/provider/service/debian.rb index 01505924cb..7d23e4ac77 100644 --- a/lib/chef/provider/service/debian.rb +++ b/lib/chef/provider/service/debian.rb @@ -22,15 +22,13 @@ class Chef class Provider class Service class Debian < Chef::Provider::Service::Init + provides :service, platform_family: 'debian' do |node| + Chef::Platform::ServiceHelpers.service_resource_providers.include?(:debian) + end + UPDATE_RC_D_ENABLED_MATCHES = /\/rc[\dS].d\/S|not installed/i UPDATE_RC_D_PRIORITIES = /\/rc([\dS]).d\/([SK])(\d\d)/i - provides :service, platform_family: "debian" - - def self.provides?(node, resource) - super && Chef::Platform::ServiceHelpers.service_resource_providers.include?(:debian) - end - def self.supports?(resource, action) Chef::Platform::ServiceHelpers.config_for_service(resource.service_name).include?(:initd) end diff --git a/lib/chef/provider/service/freebsd.rb b/lib/chef/provider/service/freebsd.rb index 6c78f86fe0..78ca0be235 100644 --- a/lib/chef/provider/service/freebsd.rb +++ b/lib/chef/provider/service/freebsd.rb @@ -99,7 +99,7 @@ class Chef def restart_service if new_resource.restart_command super - elsif new_resource.supports[:restart] + elsif supports[:restart] shell_out_with_systems_locale!("#{init_command} fastrestart") else stop_service diff --git a/lib/chef/provider/service/gentoo.rb b/lib/chef/provider/service/gentoo.rb index 3dab920f06..903c55af7a 100644 --- a/lib/chef/provider/service/gentoo.rb +++ b/lib/chef/provider/service/gentoo.rb @@ -1,7 +1,7 @@ # # Author:: Lee Jensen (<ljensen@engineyard.com>) # Author:: AJ Christensen (<aj@opscode.com>) -# Copyright:: Copyright (c) 2008 Opscode, Inc. +# Copyright:: Copyright (c) 2008-2015 Chef Software, Inc. # License:: Apache License, Version 2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -26,9 +26,9 @@ class Chef::Provider::Service::Gentoo < Chef::Provider::Service::Init provides :service, platform_family: "gentoo" def load_current_resource + supports[:status] = true if supports[:status].nil? + supports[:restart] = true if supports[:restart].nil? - @new_resource.supports[:status] = true - @new_resource.supports[:restart] = true @found_script = false super diff --git a/lib/chef/provider/service/init.rb b/lib/chef/provider/service/init.rb index 355e98a0eb..8fe5b0281f 100644 --- a/lib/chef/provider/service/init.rb +++ b/lib/chef/provider/service/init.rb @@ -1,6 +1,6 @@ # # Author:: AJ Christensen (<aj@hjksolutions.com>) -# Copyright:: Copyright (c) 2008 Opscode, Inc. +# Copyright:: Copyright (c) 2008-2015 Chef Software, Inc. # License:: Apache License, Version 2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -72,7 +72,7 @@ class Chef def restart_service if @new_resource.restart_command super - elsif @new_resource.supports[:restart] + elsif supports[:restart] shell_out_with_systems_locale!("#{default_init_command} restart") else stop_service @@ -84,7 +84,7 @@ class Chef def reload_service if @new_resource.reload_command super - elsif @new_resource.supports[:reload] + elsif supports[:reload] shell_out_with_systems_locale!("#{default_init_command} reload") end end diff --git a/lib/chef/provider/service/insserv.rb b/lib/chef/provider/service/insserv.rb index 31965a4bc6..dd01f9ab87 100644 --- a/lib/chef/provider/service/insserv.rb +++ b/lib/chef/provider/service/insserv.rb @@ -24,10 +24,8 @@ class Chef class Service class Insserv < Chef::Provider::Service::Init - provides :service, os: "linux" - - def self.provides?(node, resource) - super && Chef::Platform::ServiceHelpers.service_resource_providers.include?(:insserv) + provides :service, platform_family: %w(debian rhel fedora suse) do |node| + Chef::Platform::ServiceHelpers.service_resource_providers.include?(:insserv) end def self.supports?(resource, action) diff --git a/lib/chef/provider/service/invokercd.rb b/lib/chef/provider/service/invokercd.rb index 5ff24e0dbb..2b045e0e60 100644 --- a/lib/chef/provider/service/invokercd.rb +++ b/lib/chef/provider/service/invokercd.rb @@ -23,10 +23,8 @@ class Chef class Service class Invokercd < Chef::Provider::Service::Init - provides :service, platform_family: "debian" - - def self.provides?(node, resource) - super && Chef::Platform::ServiceHelpers.service_resource_providers.include?(:invokercd) + provides :service, platform_family: 'debian', override: true do |node| + Chef::Platform::ServiceHelpers.service_resource_providers.include?(:invokercd) end def self.supports?(resource, action) diff --git a/lib/chef/provider/service/macosx.rb b/lib/chef/provider/service/macosx.rb index 7324822eff..0a8fca4262 100644 --- a/lib/chef/provider/service/macosx.rb +++ b/lib/chef/provider/service/macosx.rb @@ -42,6 +42,10 @@ class Chef PLIST_DIRS = gather_plist_dirs + def this_version_or_newer?(this_version) + Gem::Version.new(node['platform_version']) >= Gem::Version.new(this_version) + end + def load_current_resource @current_resource = Chef::Resource::MacosxService.new(@new_resource.name) @current_resource.service_name(@new_resource.service_name) @@ -56,7 +60,7 @@ class Chef @console_user = Etc.getlogin Chef::Log.debug("#{new_resource} console_user: '#{@console_user}'") cmd = "su " - param = !node['platform_version'].include?('10.10') ? '-l ' : '' + param = this_version_or_newer?('10.10') ? '' : '-l ' @base_user_cmd = cmd + param + "#{@console_user} -c" # Default LauchAgent session should be Aqua @session_type = 'Aqua' if @session_type.nil? diff --git a/lib/chef/provider/service/openbsd.rb b/lib/chef/provider/service/openbsd.rb index d509ee10ff..36c9e8141e 100644 --- a/lib/chef/provider/service/openbsd.rb +++ b/lib/chef/provider/service/openbsd.rb @@ -26,7 +26,7 @@ class Chef class Service class Openbsd < Chef::Provider::Service::Init - provides :service, os: [ "openbsd" ] + provides :service, os: "openbsd" include Chef::Mixin::ShellOut @@ -40,11 +40,12 @@ class Chef @rc_conf = ::File.read(RC_CONF_PATH) rescue '' @rc_conf_local = ::File.read(RC_CONF_LOCAL_PATH) rescue '' @init_command = ::File.exist?(rcd_script_path) ? rcd_script_path : nil - new_resource.supports[:status] = true new_resource.status_command("#{default_init_command} check") end def load_current_resource + supports[:status] = true if supports[:status].nil? + @current_resource = Chef::Resource::Service.new(new_resource.name) current_resource.service_name(new_resource.service_name) diff --git a/lib/chef/provider/service/redhat.rb b/lib/chef/provider/service/redhat.rb index 850953125e..33a9778715 100644 --- a/lib/chef/provider/service/redhat.rb +++ b/lib/chef/provider/service/redhat.rb @@ -1,6 +1,6 @@ # # Author:: AJ Christensen (<aj@hjksolutions.com>) -# Copyright:: Copyright (c) 2008 Opscode, Inc. +# 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,24 +23,32 @@ class Chef class Service class Redhat < Chef::Provider::Service::Init - CHKCONFIG_ON = /\d:on/ - CHKCONFIG_MISSING = /No such/ - - provides :service, platform_family: [ "rhel", "fedora", "suse" ] + # @api private + attr_accessor :service_missing + # @api private + attr_accessor :current_run_levels - def self.provides?(node, resource) - super && Chef::Platform::ServiceHelpers.service_resource_providers.include?(:redhat) + provides :service, platform_family: %w(rhel fedora suse) do |node| + Chef::Platform::ServiceHelpers.service_resource_providers.include?(:redhat) end + CHKCONFIG_ON = /\d:on/ + CHKCONFIG_MISSING = /No such/ + def self.supports?(resource, action) Chef::Platform::ServiceHelpers.config_for_service(resource.service_name).include?(:initd) end def initialize(new_resource, run_context) super - @init_command = "/sbin/service #{@new_resource.service_name}" - @new_resource.supports[:status] = true + @init_command = "/sbin/service #{new_resource.service_name}" @service_missing = false + @current_run_levels = [] + end + + # @api private + def run_levels + new_resource.run_levels end def define_resource_requirements @@ -49,34 +57,60 @@ class Chef requirements.assert(:all_actions) do |a| chkconfig_file = "/sbin/chkconfig" a.assertion { ::File.exists? chkconfig_file } - a.failure_message Chef::Exceptions::Service, "#{chkconfig_file} does not exist!" + a.failure_message Chef::Exceptions::Service, "#{chkconfig_file} dbleoes not exist!" end requirements.assert(:start, :enable, :reload, :restart) do |a| a.assertion { !@service_missing } - a.failure_message Chef::Exceptions::Service, "#{@new_resource}: unable to locate the init.d script!" + a.failure_message Chef::Exceptions::Service, "#{new_resource}: unable to locate the init.d script!" a.whyrun "Assuming service would be disabled. The init script is not presently installed." end end def load_current_resource + supports[:status] = true if supports[:status].nil? + super if ::File.exists?("/sbin/chkconfig") - chkconfig = shell_out!("/sbin/chkconfig --list #{@current_resource.service_name}", :returns => [0,1]) - @current_resource.enabled(!!(chkconfig.stdout =~ CHKCONFIG_ON)) + chkconfig = shell_out!("/sbin/chkconfig --list #{current_resource.service_name}", :returns => [0,1]) + unless run_levels.nil? or run_levels.empty? + all_levels_match = true + chkconfig.stdout.split(/\s+/)[1..-1].each do |level| + index = level.split(':').first + status = level.split(':').last + if level =~ CHKCONFIG_ON + @current_run_levels << index.to_i + all_levels_match = false unless run_levels.include?(index.to_i) + else + all_levels_match = false if run_levels.include?(index.to_i) + end + end + current_resource.enabled(all_levels_match) + else + current_resource.enabled(!!(chkconfig.stdout =~ CHKCONFIG_ON)) + end @service_missing = !!(chkconfig.stderr =~ CHKCONFIG_MISSING) end - @current_resource + current_resource + end + + # @api private + def levels + (run_levels.nil? or run_levels.empty?) ? "" : "--level #{run_levels.join('')} " end def enable_service() - shell_out! "/sbin/chkconfig #{@new_resource.service_name} on" + unless run_levels.nil? or run_levels.empty? + disable_levels = current_run_levels - run_levels + shell_out! "/sbin/chkconfig --level #{disable_levels.join('')} #{new_resource.service_name} off" unless disable_levels.empty? + end + shell_out! "/sbin/chkconfig #{levels}#{new_resource.service_name} on" end def disable_service() - shell_out! "/sbin/chkconfig #{@new_resource.service_name} off" + shell_out! "/sbin/chkconfig #{levels}#{new_resource.service_name} off" end end end diff --git a/lib/chef/provider/service/simple.rb b/lib/chef/provider/service/simple.rb index ee403ee163..d295513b42 100644 --- a/lib/chef/provider/service/simple.rb +++ b/lib/chef/provider/service/simple.rb @@ -76,7 +76,7 @@ class Chef end requirements.assert(:all_actions) do |a| - a.assertion { @new_resource.status_command or @new_resource.supports[:status] or + a.assertion { @new_resource.status_command or supports[:status] or (!ps_cmd.nil? and !ps_cmd.empty?) } a.failure_message Chef::Exceptions::Service, "#{@new_resource} could not determine how to inspect the process table, please set this node's 'command.ps' attribute" end @@ -127,7 +127,7 @@ class Chef nil end - elsif @new_resource.supports[:status] + elsif supports[:status] Chef::Log.debug("#{@new_resource} supports status, running") begin if shell_out("#{default_init_command} status").exitstatus == 0 diff --git a/lib/chef/provider/service/systemd.rb b/lib/chef/provider/service/systemd.rb index 9085ffde2e..d41f6248c2 100644 --- a/lib/chef/provider/service/systemd.rb +++ b/lib/chef/provider/service/systemd.rb @@ -24,14 +24,12 @@ class Chef::Provider::Service::Systemd < Chef::Provider::Service::Simple include Chef::Mixin::Which - provides :service, os: "linux" + provides :service, os: "linux" do |node| + Chef::Platform::ServiceHelpers.service_resource_providers.include?(:systemd) + end attr_accessor :status_check_success - def self.provides?(node, resource) - super && Chef::Platform::ServiceHelpers.service_resource_providers.include?(:systemd) - end - def self.supports?(resource, action) Chef::Platform::ServiceHelpers.config_for_service(resource.service_name).include?(:systemd) end diff --git a/lib/chef/provider/service/upstart.rb b/lib/chef/provider/service/upstart.rb index 8d4aa41035..c08a5f8636 100644 --- a/lib/chef/provider/service/upstart.rb +++ b/lib/chef/provider/service/upstart.rb @@ -25,14 +25,13 @@ class Chef class Provider class Service class Upstart < Chef::Provider::Service::Simple - UPSTART_STATE_FORMAT = /\w+ \(?(\w+)\)?[\/ ](\w+)/ - - provides :service, os: "linux" - def self.provides?(node, resource) - super && Chef::Platform::ServiceHelpers.service_resource_providers.include?(:upstart) + provides :service, platform_family: 'debian', override: true do |node| + Chef::Platform::ServiceHelpers.service_resource_providers.include?(:upstart) end + UPSTART_STATE_FORMAT = /\w+ \(?(\w+)\)?[\/ ](\w+)/ + def self.supports?(resource, action) Chef::Platform::ServiceHelpers.config_for_service(resource.service_name).include?(:upstart) end @@ -107,7 +106,7 @@ class Chef Chef::Log.debug("#{@new_resource} you have specified a status command, running..") begin - if shell_out!(@new_resource.status_command) == 0 + if shell_out!(@new_resource.status_command).exitstatus == 0 @current_resource.running true end rescue diff --git a/lib/chef/provider/template/content.rb b/lib/chef/provider/template/content.rb index 7fc680ec85..a231bd509e 100644 --- a/lib/chef/provider/template/content.rb +++ b/lib/chef/provider/template/content.rb @@ -39,6 +39,16 @@ class Chef context = TemplateContext.new(@new_resource.variables) context[:node] = @run_context.node context[:template_finder] = template_finder + + # helper variables + context[:cookbook_name] = @new_resource.cookbook_name unless context.keys.include?(:coookbook_name) + context[:recipe_name] = @new_resource.recipe_name unless context.keys.include?(:recipe_name) + context[:recipe_line_string] = @new_resource.source_line unless context.keys.include?(:recipe_line_string) + context[:recipe_path] = @new_resource.source_line_file unless context.keys.include?(:recipe_path) + context[:recipe_line] = @new_resource.source_line_number unless context.keys.include?(:recipe_line) + context[:template_name] = @new_resource.source unless context.keys.include?(:template_name) + context[:template_path] = template_location unless context.keys.include?(:template_path) + context._extend_modules(@new_resource.helper_modules) output = context.render_template(template_location) diff --git a/lib/chef/provider/user.rb b/lib/chef/provider/user.rb index ad92a72a0a..244b11db98 100644 --- a/lib/chef/provider/user.rb +++ b/lib/chef/provider/user.rb @@ -23,8 +23,6 @@ require 'etc' class Chef class Provider class User < Chef::Provider - provides :user - include Chef::Mixin::Command attr_accessor :user_exists, :locked diff --git a/lib/chef/provider/windows_script.rb b/lib/chef/provider/windows_script.rb index e600bb2837..62b49bd833 100644 --- a/lib/chef/provider/windows_script.rb +++ b/lib/chef/provider/windows_script.rb @@ -23,6 +23,8 @@ class Chef class Provider class WindowsScript < Chef::Provider::Script + attr_reader :is_forced_32bit + protected include Chef::Mixin::WindowsArchitectureHelper @@ -36,11 +38,7 @@ class Chef @is_wow64 = wow64_architecture_override_required?(run_context.node, target_architecture) - # if the user wants to run the script 32 bit && we are on a 64bit windows system && we are running a 64bit ruby ==> fail - if ( target_architecture == :i386 ) && node_windows_architecture(run_context.node) == :x86_64 && !is_i386_process_on_x86_64_windows? - raise Chef::Exceptions::Win32ArchitectureIncorrect, - "Support for the i386 architecture from a 64-bit Ruby runtime is not yet implemented" - end + @is_forced_32bit = forced_32bit_override_required?(run_context.node, target_architecture) end public diff --git a/lib/chef/provider_resolver.rb b/lib/chef/provider_resolver.rb index 5bfee343d1..82a24fc078 100644 --- a/lib/chef/provider_resolver.rb +++ b/lib/chef/provider_resolver.rb @@ -17,7 +17,7 @@ # require 'chef/exceptions' -require 'chef/platform/provider_priority_map' +require 'chef/platform/priority_map' class Chef # @@ -62,12 +62,47 @@ class Chef maybe_chef_platform_lookup(resource) end + # Does NOT call provides? on the resource (it is assumed this is being + # called *from* provides?). def provided_by?(provider_class) - prioritized_handlers.include?(provider_class) + potential_handlers.include?(provider_class) + end + + def enabled_handlers + @enabled_handlers ||= potential_handlers.select { |handler| !overrode_provides?(handler) || handler.provides?(node, resource) } + end + + # TODO deprecate this and allow actions to be passed as a filter to + # `provides` so we don't have to have two separate things. + # @api private + def supported_handlers + enabled_handlers.select { |handler| handler.supports?(resource, action) } end private + def potential_handlers + handler_map.list(node, resource.resource_name).uniq + end + + # The list of handlers, with any in the priority_map moved to the front + def prioritized_handlers + @prioritized_handlers ||= begin + supported_handlers = self.supported_handlers + 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." + supported_handlers = enabled_handlers + end + + prioritized = priority_map.list(node, resource.resource_name).flatten(1) + prioritized &= supported_handlers # Filter the priority map by the actual enabled handlers + prioritized |= supported_handlers # Bring back any handlers that aren't in the priority map, at the *end* (ordered set) + prioritized + end + end + # if resource.provider is set, just return one of those objects def maybe_explicit_provider(resource) return nil unless resource.provider @@ -78,27 +113,7 @@ class Chef def maybe_dynamic_provider_resolution(resource, action) 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 + handler = prioritized_handlers.first if handler Chef::Log.debug "Provider for action #{action} on resource #{resource} is #{handler}" @@ -114,13 +129,16 @@ class Chef Chef::Platform.find_provider_for_node(node, resource) end - def provider_priority_map - Chef::Platform::ProviderPriorityMap.instance + def priority_map + Chef.provider_priority_map end - def prioritized_handlers - @prioritized_handlers ||= - provider_priority_map.list_handlers(node, resource.resource_name).flatten(1).uniq + def handler_map + Chef.provider_handler_map + end + + def overrode_provides?(handler) + handler.method(:provides?).owner != Chef::Provider.method(:provides?).owner end module Deprecated @@ -129,33 +147,21 @@ class Chef @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.") + @enabled_handlers ||= begin + handlers = super + if handlers.empty? + # Look through all providers, and find ones that return true to provides. + # Don't bother with ones that don't override provides?, since they + # would have been in enabled_handlers already if that were so. (It's a + # perf concern otherwise.) + handlers = providers.select { |handler| overrode_provides?(handler) && handler.provides?(node, resource) } + handlers.each do |handler| + Chef.log_deprecation("#{handler}.provides? returned true when asked if it provides DSL #{resource.resource_name}, but provides #{resource.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.") + end end - result + handlers end end end diff --git a/lib/chef/recipe.rb b/lib/chef/recipe.rb index b4d37c2d61..262560f754 100644 --- a/lib/chef/recipe.rb +++ b/lib/chef/recipe.rb @@ -36,14 +36,7 @@ class Chef # A Recipe object is the context in which Chef recipes are evaluated. class Recipe - include Chef::DSL::DataQuery - include Chef::DSL::PlatformIntrospection - include Chef::DSL::IncludeRecipe - include Chef::DSL::Recipe - include Chef::DSL::RegistryHelper - include Chef::DSL::RebootPending - include Chef::DSL::Audit - include Chef::DSL::Powershell + include Chef::DSL::Recipe::FullDSL include Chef::Mixin::FromFile include Chef::Mixin::Deprecation diff --git a/lib/chef/resource.rb b/lib/chef/resource.rb index 7fe8a52d95..ee75dec3b9 100644 --- a/lib/chef/resource.rb +++ b/lib/chef/resource.rb @@ -1,7 +1,8 @@ # # Author:: Adam Jacob (<adam@opscode.com>) # Author:: Christopher Walters (<cw@opscode.com>) -# Copyright:: Copyright (c) 2008 Opscode, Inc. +# Author:: John Keiser (<jkeiser@chef.io) +# Copyright:: Copyright (c) 2008-2015 Chef, Inc. # License:: Apache License, Version 2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,6 +18,7 @@ # limitations under the License. # +require 'chef/exceptions' require 'chef/mixin/params_validate' require 'chef/dsl/platform_introspection' require 'chef/dsl/data_query' @@ -27,6 +29,7 @@ require 'chef/mixin/convert_to_class_name' require 'chef/guard_interpreter/resource_guard_interpreter' require 'chef/resource/conditional' require 'chef/resource/conditional_action_not_nothing' +require 'chef/resource/action_provider' require 'chef/resource_collection' require 'chef/node_map' require 'chef/node' @@ -58,8 +61,6 @@ class Chef include Chef::Mixin::ShellOut include Chef::Mixin::PowershellOut - NULL_ARG = Object.new - # # The node the current Chef run is using. # @@ -103,7 +104,7 @@ class Chef # @param run_context The context of the Chef run. Corresponds to #run_context. # def initialize(name, run_context=nil) - name(name) + name(name) unless name.nil? @run_context = run_context @noop = nil @before = nil @@ -132,37 +133,27 @@ class Chef end # - # The name of this particular resource. - # - # This special resource attribute is set automatically from the declaration - # of the resource, e.g. + # The list of properties defined on this resource. # - # execute 'Vitruvius' do - # command 'ls' - # end + # Everything defined with `property` is in this list. # - # Will set the name to "Vitruvius". - # - # This is also used in to_s to show the resource name, e.g. `execute[Vitruvius]`. - # - # This is also used for resource notifications and subscribes in the same manner. - # - # This will coerce any object into a string via #to_s. Arrays are a special case - # so that `package ["foo", "bar"]` becomes package[foo, bar] instead of the more - # awkward `package[["foo", "bar"]]` that #to_s would produce. + # @param include_superclass [Boolean] `true` to include properties defined + # on superclasses; `false` or `nil` to return the list of properties + # directly on this class. # - # @param name [Object] The name to set, typically a String or Array - # @return [String] The name of this Resource. + # @return [Hash<Symbol,Property>] The list of property names and types. # - def name(name=nil) - if !name.nil? - if name.is_a?(Array) - @name = name.join(', ') + def self.properties(include_superclass=true) + @properties ||= {} + if include_superclass + if superclass.respond_to?(:properties) + superclass.properties.merge(@properties) else - @name = name.to_s + @properties.dup end + else + @properties end - @name end # @@ -171,27 +162,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 - if arg.is_a?(Array) - arg = arg.map { |a| a.to_sym } - else - arg = arg.to_sym - end - Array(arg).each do |action| + arg = Array(arg).map(&:to_sym) + arg.each do |action| validate( { action: action }, { action: { kind_of: Symbol, equal_to: allowed_actions } } ) end - self.action = arg + @action = arg else - # Pull the action from the class if it's not set - @action || self.class.default_action + @action end end + # Alias for normal assigment syntax. + alias_method :action=, :action + # # Sets up a notification that will run a particular action on another resource # if and when *this* resource is updated by an action. @@ -480,13 +468,21 @@ class Chef # # Get the value of the state attributes in this resource as a hash. # + # Does not include properties that are not set (unless they are identity + # properties). + # # @return [Hash{Symbol => Object}] A Hash of attribute => value for the # Resource class's `state_attrs`. + # def state_for_resource_reporter - self.class.state_attrs.inject({}) do |state_attrs, attr_name| - state_attrs[attr_name] = send(attr_name) - state_attrs + state = {} + state_properties = self.class.state_properties + state_properties.each do |property| + if property.identity? || property.is_set?(self) + state[property.name] = send(property.name) + end end + state end # @@ -499,17 +495,22 @@ class Chef 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. + # The value of the identity of this resource. + # + # - If there are no identity properties on the resource, `name` is returned. + # - If there is exactly one identity property on the resource, it is returned. + # - If there are more than one, they are returned in a hash. # - # @return The value of the identity attribute. + # @return [Object,Hash<Symbol,Object>] The identity of this resource. # def identity - if identity_attr = self.class.identity_attr - send(identity_attr) - else - name + result = {} + identity_properties = self.class.identity_properties + identity_properties.each do |property| + result[property.name] = send(property.name) end + return result.values.first if identity_properties.size == 1 + result end # @@ -531,9 +532,7 @@ class Chef # # Equivalent to #ignore_failure. # - def epic_fail(arg=nil) - ignore_failure(arg) - end + alias :epic_fail :ignore_failure # # Make this resource into an exact (shallow) copy of the other resource. @@ -688,66 +687,393 @@ class Chef # # The provider class for this resource. # + # If `action :x do ... end` has been declared on this resource or its + # superclasses, this will return the `action_provider_class`. + # # If this is not set, `provider_for_action` will dynamically determine the # provider. # # @param arg [String, Symbol, Class] Sets the provider class for this resource. # If passed a String or Symbol, e.g. `:file` or `"file"`, looks up the # provider based on the name. + # # @return The provider class for this resource. # + # @see Chef::Resource.action_provider_class + # def provider(arg=nil) klass = if arg.kind_of?(String) || arg.kind_of?(Symbol) lookup_provider_constant(arg) else arg end - set_or_return(:provider, klass, kind_of: [ Class ]) + set_or_return(:provider, klass, kind_of: [ Class ]) || + self.class.action_provider_class end def provider=(arg) provider(arg) end - # Set or return the list of "state attributes" implemented by the Resource - # subclass. State attributes are attributes that describe the desired state - # of the system, such as file permissions or ownership. In general, state - # attributes are attributes that could be populated by examining the state - # of the system (e.g., File.stat can tell you the permissions on an - # existing file). Contrarily, attributes that are not "state attributes" - # usually modify the way Chef itself behaves, for example by providing - # additional options for a package manager to use when installing a - # package. + # + # Create a property on this resource class. + # + # If a superclass has this property, or if this property has already been + # defined by this resource, this will *override* the previous value. + # + # @param name [Symbol] The name of the property. + # @param type [Object,Array<Object>] The type(s) of this property. + # If present, this is prepended to the `is` validation option. + # @param options [Hash<Symbol,Object>] Validation options. + # @option options [Object,Array] :is An object, or list of + # objects, that must match the value using Ruby's `===` operator + # (`options[:is].any? { |v| v === value }`). + # @option options [Object,Array] :equal_to An object, or list + # of objects, that must be equal to the value using Ruby's `==` + # operator (`options[:is].any? { |v| v == value }`) + # @option options [Regexp,Array<Regexp>] :regex An object, or + # list of objects, that must match the value with `regex.match(value)`. + # @option options [Class,Array<Class>] :kind_of A class, or + # list of classes, that the value must be an instance of. + # @option options [Hash<String,Proc>] :callbacks A hash of + # messages -> procs, all of which match the value. The proc must + # return a truthy or falsey value (true means it matches). + # @option options [Symbol,Array<Symbol>] :respond_to A method + # name, or list of method names, the value must respond to. + # @option options [Symbol,Array<Symbol>] :cannot_be A property, + # or a list of properties, that the value cannot have (such as `:nil` or + # `:empty`). The method with a questionmark at the end is called on the + # value (e.g. `value.empty?`). If the value does not have this method, + # it is considered valid (i.e. if you don't respond to `empty?` we + # assume you are not empty). + # @option options [Proc] :coerce A proc which will be called to + # transform the user input to canonical form. The value is passed in, + # and the transformed value returned as output. Lazy values will *not* + # be passed to this method until after they are evaluated. Called in the + # context of the resource (meaning you can access other properties). + # @option options [Boolean] :required `true` if this property + # must be present; `false` otherwise. This is checked after the resource + # is fully initialized. + # @option options [Boolean] :name_property `true` if this + # property defaults to the same value as `name`. Equivalent to + # `default: lazy { name }`, except that #property_is_set? will + # return `true` if the property is set *or* if `name` is set. + # @option options [Boolean] :name_attribute Same as `name_property`. + # @option options [Object] :default The value this property + # will return if the user does not set one. If this is `lazy`, it will + # be run in the context of the instance (and able to access other + # properties). + # @option options [Boolean] :desired_state `true` if this property is + # part of desired state. Defaults to `true`. + # @option options [Boolean] :identity `true` if this property + # is part of object identity. Defaults to `false`. + # + # @example Bare property + # property :x + # + # @example With just a type + # property :x, String + # + # @example With just options + # property :x, default: 'hi' + # + # @example With type and options + # property :x, String, default: 'hi' + # + def self.property(name, type=NOT_PASSED, **options) + name = name.to_sym + + options[:instance_variable_name] = :"@#{name}" if !options.has_key?(:instance_variable_name) + options.merge!(name: name, declared_in: self) + + if type == NOT_PASSED + # If a type is not passed, the property derives from the + # superclass property (if any) + if properties.has_key?(name) + property = properties[name].derive(**options) + else + property = property_type(**options) + end + + # If a Property is specified, derive a new one from that. + elsif type.is_a?(Property) || (type.is_a?(Class) && type <= Property) + property = type.derive(**options) + + # If a primitive type was passed, combine it with "is" + else + if options[:is] + options[:is] = ([ type ] + [ options[:is] ]).flatten(1) + else + options[:is] = type + end + property = property_type(**options) + end + + if !options[:default].frozen? && (options[:default].is_a?(Array) || options[:default].is_a?(Hash)) + Chef.log_deprecation("Property #{self}.#{name} has an array or hash default (#{options[:default]}). This means that if one resource modifies or appends to it, all other resources of the same type will also see the changes. Either freeze the constant with `.freeze` to prevent appending, or use lazy { #{options[:default].inspect} }.") + end + + local_properties = properties(false) + local_properties[name] = property + + property.emit_dsl + end + + # + # Create a reusable property type that can be used in multiple properties + # in different resources. + # + # @param options [Hash<Symbol,Object>] Validation options. see #property for + # the list of options. + # + # @example + # property_type(default: 'hi') + # + def self.property_type(**options) + Property.derive(**options) + end + + # + # The name of this particular resource. + # + # This special resource attribute is set automatically from the declaration + # of the resource, e.g. + # + # execute 'Vitruvius' do + # command 'ls' + # end + # + # Will set the name to "Vitruvius". + # + # This is also used in to_s to show the resource name, e.g. `execute[Vitruvius]`. + # + # This is also used for resource notifications and subscribes in the same manner. + # + # This will coerce any object into a string via #to_s. Arrays are a special case + # so that `package ["foo", "bar"]` becomes package[foo, bar] instead of the more + # awkward `package[["foo", "bar"]]` that #to_s would produce. + # + # @param name [Object] The name to set, typically a String or Array + # @return [String] The name of this Resource. + # + property :name, String, coerce: proc { |v| v.is_a?(Array) ? v.join(', ') : v.to_s }, desired_state: false + + # + # Whether this property has been set (or whether it has a default that has + # been retrieved). + # + # @param name [Symbol] The name of the property. + # @return [Boolean] `true` if the property has been set. + # + def property_is_set?(name) + property = self.class.properties[name.to_sym] + raise ArgumentError, "Property #{name} is not defined in class #{self}" if !property + property.is_set?(self) + end + + # + # Clear this property as if it had never been set. It will thereafter return + # the default. + # been retrieved). + # + # @param name [Symbol] The name of the property. + # + def reset_property(name) + property = self.class.properties[name.to_sym] + raise ArgumentError, "Property #{name} is not defined in class #{self}" if !property + property.reset(self) + end + + # + # Create a lazy value for assignment to a default value. + # + # @param block The block to run when the value is retrieved. + # + # @return [Chef::DelayedEvaluator] The lazy value + # + def self.lazy(&block) + DelayedEvaluator.new(&block) + end + + # + # Get or set the list of desired state properties for this resource. + # + # State properties are properties that describe the desired state + # of the system, such as file permissions or ownership. + # In general, state properties are properties that could be populated by + # examining the state of the system (e.g., File.stat can tell you the + # permissions on an existing file). Contrarily, properties that are not + # "state properties" usually modify the way Chef itself behaves, for example + # by providing additional options for a package manager to use when + # installing a package. # # This list is used by the Chef client auditing system to extract # information from resources to describe changes made to the system. - def self.state_attrs(*attr_names) - @state_attrs ||= [] - @state_attrs = attr_names unless attr_names.empty? + # + # This method is unnecessary when declaring properties with `property`; + # properties are added to state_properties by default, and can be turned off + # with `desired_state: false`. + # + # ```ruby + # property :x # part of desired state + # property :y, desired_state: false # not part of desired state + # ``` + # + # @param names [Array<Symbol>] A list of property names to set as desired + # state. + # + # @return [Array<Property>] All properties in desired state. + # + def self.state_properties(*names) + if !names.empty? + names = names.map { |name| name.to_sym }.uniq - # Return *all* state_attrs that this class has, including inherited ones - if superclass.respond_to?(:state_attrs) - superclass.state_attrs + @state_attrs - else - @state_attrs + local_properties = properties(false) + # Add new properties to the list. + names.each do |name| + property = properties[name] + if !property + self.property name, instance_variable_name: false, desired_state: true + elsif !property.desired_state? + self.property name, desired_state: true + end + end + + # If state_attrs *excludes* something which is currently desired state, + # mark it as desired_state: false. + local_properties.each do |name,property| + if property.desired_state? && !names.include?(name) + self.property name, desired_state: false + end + end end + + properties.values.select { |property| property.desired_state? } + end + + # + # Set or return the list of "state properties" implemented by the Resource + # subclass. + # + # Equivalent to calling #state_properties and getting `state_properties.keys`. + # + # @deprecated Use state_properties.keys instead. Note that when you declare + # properties with `property`: properties are added to state_properties by + # default, and can be turned off with `desired_state: false` + # + # ```ruby + # property :x # part of desired state + # property :y, desired_state: false # not part of desired state + # ``` + # + # @param names [Array<Symbol>] A list of property names to set as desired + # state. + # + # @return [Array<Symbol>] All property names with desired state. + # + def self.state_attrs(*names) + state_properties(*names).map { |property| property.name } end - # Set or return the "identity attribute" for this resource class. This is - # generally going to be the "name attribute" for this resource. In other - # words, the resource type plus this attribute uniquely identify a given - # bit of state that chef manages. For a File resource, this would be the - # path, for a package resource, it will be the package name. This will show - # up in chef-client's audit records as a searchable field. - def self.identity_attr(attr_name=nil) - @identity_attr ||= nil - @identity_attr = attr_name if attr_name + # + # Set the identity of this resource to a particular set of properties. + # + # This drives #identity, which returns data that uniquely refers to a given + # resource on the given node (in such a way that it can be correlated + # across Chef runs). + # + # This method is unnecessary when declaring properties with `property`; + # properties can be added to identity during declaration with + # `identity: true`. + # + # ```ruby + # property :x, identity: true # part of identity + # property :y # not part of identity + # ``` + # + # If no properties are marked as identity, "name" is considered the identity. + # + # @param names [Array<Symbol>] A list of property names to set as the identity. + # + # @return [Array<Property>] All identity properties. + # + def self.identity_properties(*names) + if !names.empty? + names = names.map { |name| name.to_sym } - # If this class doesn't have an identity attr, we'll defer to the superclass: - if @identity_attr || !superclass.respond_to?(:identity_attr) - @identity_attr - else - superclass.identity_attr + # Add or change properties that are not part of the identity. + names.each do |name| + property = properties[name] + if !property + self.property name, instance_variable_name: false, identity: true + elsif !property.identity? + self.property name, identity: true + end + end + + # If identity_properties *excludes* something which is currently part of + # the identity, mark it as identity: false. + properties.each do |name,property| + if property.identity? && !names.include?(name) + self.property name, identity: false + end + end end + + result = properties.values.select { |property| property.identity? } + result = [ properties[:name] ] if result.empty? + result + end + + # + # Set the identity of this resource to a particular property. + # + # This drives #identity, which returns data that uniquely refers to a given + # resource on the given node (in such a way that it can be correlated + # across Chef runs). + # + # This method is unnecessary when declaring properties with `property`; + # properties can be added to identity during declaration with + # `identity: true`. + # + # ```ruby + # property :x, identity: true # part of identity + # property :y # not part of identity + # ``` + # + # @param name [Symbol] A list of property names to set as the identity. + # + # @return [Symbol] The identity property if there is only one; or `nil` if + # there are more than one. + # + # @raise [ArgumentError] If no arguments are passed and the resource has + # more than one identity property. + # + def self.identity_property(name=nil) + result = identity_properties(*Array(name)) + if result.size > 1 + raise Chef::Exceptions::MultipleIdentityError, "identity_property cannot be called on an object with more than one identity property (#{result.map { |r| r.name }.join(", ")})." + end + result.first + end + + # + # Set a property as the "identity attribute" for this resource. + # + # Identical to calling #identity_property.first.key. + # + # @param name [Symbol] The name of the property to set. + # + # @return [Symbol] + # + # @deprecated `identity_property` should be used instead. + # + # @raise [ArgumentError] If no arguments are passed and the resource has + # more than one identity property. + # + def self.identity_attr(name=nil) + property = identity_property(name) + return nil if !property + property.name end # @@ -773,8 +1099,8 @@ class Chef # have. # attr_accessor :allowed_actions - def allowed_actions(value=NULL_ARG) - if value != NULL_ARG + def allowed_actions(value=NOT_PASSED) + if value != NOT_PASSED self.allowed_actions = value end @allowed_actions @@ -885,7 +1211,7 @@ class Chef # @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." + 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) @@ -908,29 +1234,23 @@ class Chef # # @return [Symbol] The name of this resource type (e.g. `:execute`). # - def self.resource_name(name=NULL_ARG) + def self.resource_name(name=NOT_PASSED) # Setter - if name != NULL_ARG + if name != NOT_PASSED 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) + if !Chef::ResourceResolver.includes_handler?(name, 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) @@ -938,6 +1258,19 @@ class Chef end # + # Use the class name as the resource name. + # + # Munges the last part of the class name from camel case to snake case, + # and sets the resource_name to that: + # + # A::B::BlahDBlah -> blah_d_blah + # + def self.use_automatic_resource_name + automatic_name = convert_to_snake_case(self.name.split('::')[-1]) + resource_name automatic_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`. @@ -955,7 +1288,7 @@ class Chef # def self.provider_base(arg=nil) 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.") + 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 @@ -965,7 +1298,7 @@ class Chef # # @param actions [Array<Symbol>] The list of actions to add to allowed_actions. # - # @return [Arrau<Symbol>] The list of actions, as symbols. + # @return [Array<Symbol>] The list of actions, as symbols. # def self.allowed_actions(*actions) @allowed_actions ||= @@ -974,10 +1307,10 @@ class Chef else [ :nothing ] end - @allowed_actions |= actions + @allowed_actions |= actions.flatten end def self.allowed_actions=(value) - @allowed_actions = value + @allowed_actions = value.uniq end # @@ -986,22 +1319,17 @@ class Chef # Setting default_action will automatially add the action to # allowed_actions, if it isn't already there. # - # Defaults to :nothing. + # 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. + # @return [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) + def self.default_action(action_name=NOT_PASSED) + unless action_name.equal?(NOT_PASSED) + @default_action = Array(action_name).map(&:to_sym) + self.allowed_actions |= @default_action end if @default_action @@ -1009,11 +1337,132 @@ class Chef elsif superclass.respond_to?(:default_action) superclass.default_action else - :nothing + [:nothing] end end def self.default_action=(action_name) - default_action(action_name) + default_action action_name + end + + # + # Define an action on this resource. + # + # The action is defined as a *recipe* block that will be compiled and then + # converged when the action is taken (when Resource is converged). The recipe + # has access to the resource's attributes and methods, as well as the Chef + # recipe DSL. + # + # Resources in the action recipe may notify and subscribe to other resources + # within the action recipe, but cannot notify or subscribe to resources + # in the main Chef run. + # + # Resource actions are *inheritable*: if resource A defines `action :create` + # and B is a subclass of A, B gets all of A's actions. Additionally, + # resource B can define `action :create` and call `super()` to invoke A's + # action code. + # + # The first action defined (besides `:nothing`) will become the default + # action for the resource. + # + # @param name [Symbol] The action name to define. + # @param recipe_block The recipe to run when the action is taken. This block + # takes no parameters, and will be evaluated in a new context containing: + # + # - The resource's public and protected methods (including attributes) + # - The Chef Recipe DSL (file, etc.) + # - super() referring to the parent version of the action (if any) + # + # @return The Action class implementing the action + # + def self.action(action, &recipe_block) + action = action.to_sym + new_action_provider_class.action(action, &recipe_block) + self.allowed_actions += [ action ] + default_action action if Array(default_action) == [:nothing] + end + + # + # Define a method to load up this resource's properties with the current + # actual values. + # + # @param load_block The block to load. Will be run in the context of a newly + # created resource with its identity values filled in. + # + def self.load_current_value(&load_block) + define_method(:load_current_value!, &load_block) + end + + # + # Call this in `load_current_value` to indicate that the value does not + # exist and that `current_resource` should therefore be `nil`. + # + # @raise Chef::Exceptions::CurrentValueDoesNotExist + # + def current_value_does_not_exist! + raise Chef::Exceptions::CurrentValueDoesNotExist + end + + # + # Get the current actual value of this resource. + # + # This does not cache--a new value will be returned each time. + # + # @return A new copy of the resource, with values filled in from the actual + # current value. + # + def current_resource + provider = provider_for_action(Array(action).first) + if provider.whyrun_mode? && !provider.whyrun_supported? + raise "Cannot retrieve #{self.class.current_resource} in why-run mode: #{provider} does not support why-run" + end + provider.load_current_resource + provider.current_resource + end + + # + # The action provider class is an automatic `Provider` created to handle + # actions declared by `action :x do ... end`. + # + # This class will be returned by `resource.provider` if `resource.provider` + # is not set. `provider_for_action` will also use this instead of calling + # out to `Chef::ProviderResolver`. + # + # If the user has not declared actions on this class or its superclasses + # using `action :x do ... end`, then there is no need for this class and + # `action_provider_class` will be `nil`. + # + # @api private + # + def self.action_provider_class + @action_provider_class || + # If the superclass needed one, then we need one as well. + if superclass.respond_to?(:action_provider_class) && superclass.action_provider_class + new_action_provider_class + end + end + + # + # Ensure the action provider class actually gets created. This is called + # when the user does `action :x do ... end`. + # + # @api private + def self.new_action_provider_class + return @action_provider_class if @action_provider_class + + if superclass.respond_to?(:action_provider_class) + base_provider = superclass.action_provider_class + end + base_provider ||= Chef::Provider + + resource_class = self + @action_provider_class = Class.new(base_provider) do + include ActionProvider + define_singleton_method(:to_s) { "#{resource_class} action provider" } + def self.inspect + to_s + end + end + @action_provider_class end # @@ -1087,7 +1536,7 @@ class Chef class << self # back-compat - # NOTE: that we do not support unregistering classes as descendents like + # NOTE: that we do not support unregistering classes as descendants like # we used to for LWRP unloading because that was horrible and removed in # Chef-12. # @deprecated @@ -1110,8 +1559,13 @@ class Chef end def self.inherited(child) super - @sorted_descendants = nil - child.resource_name + @@sorted_descendants = nil + # set resource_name automatically if it's not set + if child.name && !child.resource_name + if child.name =~ /^Chef::Resource::(\w+)$/ + child.resource_name(convert_to_snake_case($1)) + end + end end @@ -1143,13 +1597,13 @@ class Chef remove_canonical_dsl end - result = Chef.set_resource_priority_array(name, self, options, &block) + result = Chef.resource_handler_map.set(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) + def self.provides?(node, resource_name) + Chef::ResourceResolver.new(node, resource_name).provided_by?(self) end # Helper for #notifies @@ -1173,16 +1627,31 @@ class Chef run_context.delayed_notifications(self) end + def source_line_file + if source_line + source_line.match(/(.*):(\d+):?.*$/).to_a[1] + else + nil + end + end + + def source_line_number + if source_line + source_line.match(/(.*):(\d+):?.*$/).to_a[2] + else + nil + end + end + def defined_at # The following regexp should match these two sourceline formats: # /some/path/to/file.rb:80:in `wombat_tears' # C:/some/path/to/file.rb:80 in 1`wombat_tears' # extracting the path to the source file and the line number. - (file, line_no) = source_line.match(/(.*):(\d+):?.*$/).to_a[1,2] if source_line if cookbook_name && recipe_name && source_line - "#{cookbook_name}::#{recipe_name} line #{line_no}" + "#{cookbook_name}::#{recipe_name} line #{source_line_number}" elsif source_line - "#{file} line #{line_no}" + "#{source_line_file} line #{source_line_number}" else "dynamically defined" end @@ -1208,7 +1677,8 @@ class Chef end def provider_for_action(action) - provider = Chef::ProviderResolver.new(node, self, action).resolve.new(self, run_context) + provider_class = Chef::ProviderResolver.new(node, self, action).resolve + provider = provider_class.new(self, run_context) provider.action = action provider end @@ -1306,57 +1776,11 @@ class Chef 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 + if !Chef::Config[:treat_deprecation_warnings_as_errors] + Chef::Resource.const_set(class_name, resource_class) + deprecated_constants[class_name.to_sym] = resource_class 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 def self.deprecated_constants @@ -1380,7 +1804,7 @@ class Chef def self.remove_canonical_dsl if @resource_name - remaining = Chef.resource_priority_map.delete_canonical(@resource_name, self) + remaining = Chef.resource_handler_map.delete_canonical(@resource_name, self) if !remaining Chef::DSL::Resources.remove_resource_dsl(@resource_name) end diff --git a/lib/chef/resource/action_provider.rb b/lib/chef/resource/action_provider.rb new file mode 100644 index 0000000000..d71b54ef4d --- /dev/null +++ b/lib/chef/resource/action_provider.rb @@ -0,0 +1,69 @@ +# +# Author:: John Keiser (<jkeiser@chef.io) +# Copyright:: Copyright (c) 2015 Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'chef/exceptions' + +class Chef + class Resource + module ActionProvider + # + # If load_current_value! is defined on the resource, use that. + # + def load_current_resource + if new_resource.respond_to?(:load_current_value!) + # dup the resource and then reset desired-state properties. + current_resource = new_resource.dup + + # We clear desired state in the copy, because it is supposed to be actual state. + # We keep identity properties and non-desired-state, which are assumed to be + # "control" values like `recurse: true` + current_resource.class.properties.each do |name,property| + if property.desired_state? && !property.identity? && !property.name_property? + property.reset(current_resource) + end + end + + # Call the actual load_current_value! method. If it raises + # CurrentValueDoesNotExist, set current_resource to `nil`. + begin + # If the user specifies load_current_value do |desired_resource|, we + # pass in the desired resource as well as the current one. + if current_resource.method(:load_current_value!).arity > 0 + current_resource.load_current_value!(new_resource) + else + current_resource.load_current_value! + end + rescue Chef::Exceptions::CurrentValueDoesNotExist + current_resource = nil + end + end + + @current_resource = current_resource + end + + def self.included(other) + other.extend(ClassMethods) + other.use_inline_resources + other.include_resource_dsl true + end + + module ClassMethods + end + end + end +end diff --git a/lib/chef/resource/chef_gem.rb b/lib/chef/resource/chef_gem.rb index 0c2fdfa819..7e9d21ebd2 100644 --- a/lib/chef/resource/chef_gem.rb +++ b/lib/chef/resource/chef_gem.rb @@ -50,9 +50,9 @@ class Chef # Chef::Resource.run_action: Caveat: this skips Chef::Runner.run_action, where notifications are handled # Action could be an array of symbols, but probably won't (think install + enable for a package) if compile_time.nil? - Chef::Log.deprecation "#{self} chef_gem compile_time installation is deprecated" - Chef::Log.deprecation "#{self} Please set `compile_time false` on the resource to use the new behavior." - Chef::Log.deprecation "#{self} or set `compile_time true` on the resource if compile_time behavior is required." + Chef.log_deprecation "#{self} chef_gem compile_time installation is deprecated" + Chef.log_deprecation "#{self} Please set `compile_time false` on the resource to use the new behavior." + Chef.log_deprecation "#{self} or set `compile_time true` on the resource if compile_time behavior is required." end if compile_time || compile_time.nil? diff --git a/lib/chef/resource/deploy.rb b/lib/chef/resource/deploy.rb index 3e5255bced..5df46fff60 100644 --- a/lib/chef/resource/deploy.rb +++ b/lib/chef/resource/deploy.rb @@ -27,6 +27,7 @@ # migration_command "rake db:migrate" # environment "RAILS_ENV" => "production", "OTHER_ENV" => "foo" # shallow_clone true +# depth 1 # action :deploy # or :rollback # restart_command "touch tmp/restart.txt" # git_ssh_wrapper "wrap-ssh4git.sh" @@ -74,6 +75,7 @@ class Chef @remote = "origin" @enable_submodules = false @shallow_clone = false + @depth = nil @scm_provider = Chef::Provider::Git @svn_force_export = false @additional_remotes = Hash[] @@ -97,8 +99,12 @@ class Chef @current_path ||= @deploy_to + "/current" end - def depth - @shallow_clone ? "5" : nil + def depth(arg=@shallow_clone ? 5 : nil) + set_or_return( + :depth, + arg, + :kind_of => [ Integer ] + ) end # note: deploy_to is your application "meta-root." diff --git a/lib/chef/resource/dsc_script.rb b/lib/chef/resource/dsc_script.rb index 2fcf183375..c3602fa60e 100644 --- a/lib/chef/resource/dsc_script.rb +++ b/lib/chef/resource/dsc_script.rb @@ -17,12 +17,14 @@ # require 'chef/exceptions' +require 'chef/dsl/powershell' class Chef class Resource class DscScript < Chef::Resource + include Chef::DSL::Powershell - provides :dsc_script, platform: "windows" + provides :dsc_script, os: "windows" default_action :run diff --git a/lib/chef/resource/file/verification.rb b/lib/chef/resource/file/verification.rb index f1ca0f1883..9b0788fad3 100644 --- a/lib/chef/resource/file/verification.rb +++ b/lib/chef/resource/file/verification.rb @@ -106,7 +106,14 @@ class Chef # We reuse Chef::GuardInterpreter in order to support # the same set of options that the not_if/only_if blocks do def verify_command(path, opts) - command = @command % {:file => path} + # First implementation interpolated `file`; docs & RFC claim `path` + # is interpolated. Until `file` can be deprecated, interpolate both. + Chef.log_deprecation( + '%{file} is deprecated in verify command and will not be '\ + 'supported in Chef 13. Please use %{path} instead.', + caller(2..2)[0] + ) if @command.include?('%{file}') + command = @command % {:file => path, :path => path} interpreter = Chef::GuardInterpreter.for_resource(@parent_resource, command, @command_opts) interpreter.evaluate end diff --git a/lib/chef/resource/ips_package.rb b/lib/chef/resource/ips_package.rb index 8d720dd411..2bf8e1dba8 100644 --- a/lib/chef/resource/ips_package.rb +++ b/lib/chef/resource/ips_package.rb @@ -23,6 +23,7 @@ class Chef class Resource class IpsPackage < ::Chef::Resource::Package + provides :package, os: "solaris2" provides :ips_package, os: "solaris2" allowed_actions :install, :remove, :upgrade diff --git a/lib/chef/resource/lwrp_base.rb b/lib/chef/resource/lwrp_base.rb index c486233020..443e0ed819 100644 --- a/lib/chef/resource/lwrp_base.rb +++ b/lib/chef/resource/lwrp_base.rb @@ -74,19 +74,14 @@ class Chef resource_class end - # 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 + alias :attribute :property # 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) + action_names = action_names.flatten if !action_names.empty? && !@allowed_actions - self.allowed_actions = action_names + self.allowed_actions = ([ :nothing ] + action_names).uniq else allowed_actions(*action_names) end diff --git a/lib/chef/resource/macports_package.rb b/lib/chef/resource/macports_package.rb index 937839b6e1..5843016897 100644 --- a/lib/chef/resource/macports_package.rb +++ b/lib/chef/resource/macports_package.rb @@ -16,10 +16,11 @@ # limitations under the License. # +require 'chef/resource/package' + class Chef class Resource class MacportsPackage < Chef::Resource::Package - provides :package, os: "darwin" end end end diff --git a/lib/chef/resource/mount.rb b/lib/chef/resource/mount.rb index 79986d127f..a5da0ba329 100644 --- a/lib/chef/resource/mount.rb +++ b/lib/chef/resource/mount.rb @@ -174,6 +174,14 @@ class Chef ) end + private + + # Used by the AIX provider to set fstype to nil. + # TODO use property to make nil a valid value for fstype + def clear_fstype + @fstype = nil + end + end end end diff --git a/lib/chef/resource/openbsd_package.rb b/lib/chef/resource/openbsd_package.rb index f91fdb37e0..9ae8813d69 100644 --- a/lib/chef/resource/openbsd_package.rb +++ b/lib/chef/resource/openbsd_package.rb @@ -29,17 +29,6 @@ class Chef include Chef::Mixin::ShellOut provides :package, os: "openbsd" - - def after_created - assign_provider - end - - private - - def assign_provider - @provider = Chef::Provider::Package::Openbsd - end - end end end diff --git a/lib/chef/resource/package.rb b/lib/chef/resource/package.rb index 1c6da75678..5be1c34b89 100644 --- a/lib/chef/resource/package.rb +++ b/lib/chef/resource/package.rb @@ -100,8 +100,3 @@ 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/registry_key.rb b/lib/chef/resource/registry_key.rb index 4ed0d4a4e0..d2e5c4b94c 100644 --- a/lib/chef/resource/registry_key.rb +++ b/lib/chef/resource/registry_key.rb @@ -93,7 +93,7 @@ class Chef raise ArgumentError, "Bad key #{key} in RegistryKey values hash" unless [:name,:type,:data].include?(key) end raise ArgumentError, "Type of name => #{v[:name]} should be string" unless v[:name].is_a?(String) - raise Argument Error "Type of type => #{v[:name]} should be symbol" unless v[:type].is_a?(Symbol) + raise ArgumentError, "Type of type => #{v[:type]} should be symbol" unless v[:type].is_a?(Symbol) end @unscrubbed_values = @values elsif self.instance_variable_defined?(:@values) diff --git a/lib/chef/resource/service.rb b/lib/chef/resource/service.rb index aa59b543be..6d1b81f9cb 100644 --- a/lib/chef/resource/service.rb +++ b/lib/chef/resource/service.rb @@ -1,7 +1,7 @@ # # Author:: AJ Christensen (<aj@hjksolutions.com>) # Author:: Tyler Cloke (<tyler@opscode.com>) -# Copyright:: Copyright (c) 2008 Opscode, Inc. +# Copyright:: Copyright (c) 2008-2015 Chef Software, Inc. # License:: Apache License, Version 2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -44,7 +44,8 @@ class Chef @init_command = nil @priority = nil @timeout = nil - @supports = { :restart => false, :reload => false, :status => false } + @run_levels = nil + @supports = { :restart => nil, :reload => nil, :status => nil } end def service_name(arg=nil) @@ -174,6 +175,13 @@ class Chef ) end + def run_levels(arg=nil) + set_or_return( + :run_levels, + arg, + :kind_of => [ Array ] ) + end + def supports(args={}) if args.is_a? Array args.each { |arg| @supports[arg] = true } diff --git a/lib/chef/resource/solaris_package.rb b/lib/chef/resource/solaris_package.rb index 2dc72d5c47..a98fb8b4fa 100644 --- a/lib/chef/resource/solaris_package.rb +++ b/lib/chef/resource/solaris_package.rb @@ -24,10 +24,7 @@ class Chef class Resource class SolarisPackage < Chef::Resource::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 + provides :package, os: "solaris2", platform_family: "solaris2", platform_version: "<= 5.10" end end end diff --git a/lib/chef/resource/yum_package.rb b/lib/chef/resource/yum_package.rb index 4d54f6051f..50ba13ce65 100644 --- a/lib/chef/resource/yum_package.rb +++ b/lib/chef/resource/yum_package.rb @@ -1,6 +1,6 @@ # # Author:: AJ Christensen (<aj@opscode.com>) -# Copyright:: Copyright (c) 2008 Opscode, Inc. +# Copyright:: Copyright (c) 2008-2015 Chef Software, Inc. # License:: Apache License, Version 2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -28,6 +28,7 @@ class Chef super @flush_cache = { :before => false, :after => false } @allow_downgrade = false + @yum_binary = nil end # Install a specific arch @@ -57,6 +58,14 @@ class Chef ) end + def yum_binary(arg=nil) + set_or_return( + :yum_binary, + arg, + :kind_of => [ String ] + ) + end + end end end diff --git a/lib/chef/resource_resolver.rb b/lib/chef/resource_resolver.rb index 31b39f7e24..67cf134c62 100644 --- a/lib/chef/resource_resolver.rb +++ b/lib/chef/resource_resolver.rb @@ -18,6 +18,7 @@ require 'chef/exceptions' require 'chef/platform/resource_priority_map' +require 'chef/mixin/convert_to_class_name' class Chef class ResourceResolver @@ -55,7 +56,7 @@ class Chef attr_reader :resource_name # @api private def resource - Chef::Log.deprecation("Chef::ResourceResolver.resource deprecated. Use resource_name instead.") + Chef.log_deprecation("Chef::ResourceResolver.resource deprecated. Use resource_name instead.") resource_name end # @api private @@ -104,52 +105,80 @@ class Chef # # Whether this DSL is provided by the given resource_class. # + # Does NOT call provides? on the resource (it is assumed this is being + # called *from* provides?). + # # @api private def provided_by?(resource_class) - !prioritized_handlers.include?(resource_class) + potential_handlers.include?(resource_class) + end + + # + # Whether the given handler attempts to provide the resource class at all. + # + # @api private + def self.includes_handler?(resource_name, resource_class) + handler_map.list(nil, resource_name).include?(resource_class) end protected + def self.priority_map + Chef.resource_priority_map + end + + def self.handler_map + Chef.resource_handler_map + end + def priority_map - Chef::Platform::ResourcePriorityMap.instance + Chef.resource_priority_map + end + + def handler_map + Chef.resource_handler_map + end + + # @api private + def potential_handlers + handler_map.list(node, resource_name, canonical: canonical).uniq + end + + def enabled_handlers + potential_handlers.select { |handler| !overrode_provides?(handler) || handler.provides?(node, resource_name) } end def prioritized_handlers - @prioritized_handlers ||= - priority_map.list_handlers(node, resource_name, canonical: canonical) + @prioritized_handlers ||= begin + enabled_handlers = self.enabled_handlers + + prioritized = priority_map.list(node, resource_name, canonical: canonical).flatten(1) + prioritized &= enabled_handlers # Filter the priority map by the actual enabled handlers + prioritized |= enabled_handlers # Bring back any handlers that aren't in the priority map, at the *end* (ordered set) + prioritized + end + end + + def overrode_provides?(handler) + handler.method(:provides?).owner != Chef::Resource.method(:provides?).owner 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 - # 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 + handlers = super + if handlers.empty? + handlers = resources.select { |handler| overrode_provides?(handler) && handler.provides?(node, resource_name) } + handlers.each do |handler| + Chef.log_deprecation("#{handler}.provides? returned true when asked if it provides 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.") end + end + handlers end end prepend Deprecated diff --git a/lib/chef/run_context.rb b/lib/chef/run_context.rb index 44b05f0cc0..0c8d3d1a48 100644 --- a/lib/chef/run_context.rb +++ b/lib/chef/run_context.rb @@ -25,118 +25,223 @@ require 'chef/log' require 'chef/recipe' require 'chef/run_context/cookbook_compiler' require 'chef/event_dispatch/events_output_stream' +require 'forwardable' class Chef # == Chef::RunContext # Value object that loads and tracks the context of a Chef run class RunContext + # + # Global state + # - # Chef::Node object for this run + # + # The node for this run + # + # @return [Chef::Node] + # attr_reader :node - # Chef::CookbookCollection for this run + # + # The set of cookbooks involved in this run + # + # @return [Chef::CookbookCollection] + # attr_reader :cookbook_collection + # # Resource Definitions for this run. Populated when the files in # +definitions/+ are evaluated (this is triggered by #load). + # + # @return [Array[Chef::ResourceDefinition]] + # attr_reader :definitions - ### - # These need to be settable so deploy can run a resource_collection - # independent of any cookbooks via +recipe_eval+ + # + # Event dispatcher for this run. + # + # @return [Chef::EventDispatch::Dispatcher] + # + attr_reader :events + + # + # Hash of factoids for a reboot request. + # + # @return [Hash] + # + attr_accessor :reboot_info + + # + # Scoped state + # - # The Chef::ResourceCollection for this run. Populated by evaluating - # recipes, which is triggered by #load. (See also: CookbookCompiler) - attr_accessor :resource_collection + # + # The parent run context. + # + # @return [Chef::RunContext] The parent run context, or `nil` if this is the + # root context. + # + attr_reader :parent_run_context + # + # The collection of resources intended to be converged (and able to be + # notified). + # + # @return [Chef::ResourceCollection] + # + # @see CookbookCompiler + # + attr_reader :resource_collection + + # # The list of control groups to execute during the audit phase - attr_accessor :audits + # + attr_reader :audits + + # + # Notification handling + # + # # A Hash containing the immediate notifications triggered by resources # during the converge phase of the chef run. - attr_accessor :immediate_notification_collection + # + # @return [Hash[String, Array[Chef::Resource::Notification]]] A hash from + # <notifying resource name> => <list of notifications it sent> + # + attr_reader :immediate_notification_collection + # # A Hash containing the delayed (end of run) notifications triggered by # resources during the converge phase of the chef run. - attr_accessor :delayed_notification_collection - - # Event dispatcher for this run. - attr_reader :events - - # Hash of factoids for a reboot request. - attr_reader :reboot_info + # + # @return [Hash[String, Array[Chef::Resource::Notification]]] A hash from + # <notifying resource name> => <list of notifications it sent> + # + attr_reader :delayed_notification_collection # Creates a new Chef::RunContext object and populates its fields. This object gets # used by the Chef Server to generate a fully compiled recipe list for a node. # - # === Returns - # object<Chef::RunContext>:: Duh. :) + # @param node [Chef::Node] The node to run against. + # @param cookbook_collection [Chef::CookbookCollection] The cookbooks + # involved in this run. + # @param events [EventDispatch::Dispatcher] The event dispatcher for this + # run. + # def initialize(node, cookbook_collection, events) @node = node @cookbook_collection = cookbook_collection - @resource_collection = Chef::ResourceCollection.new - @audits = {} - @immediate_notification_collection = Hash.new {|h,k| h[k] = []} - @delayed_notification_collection = Hash.new {|h,k| h[k] = []} - @definitions = Hash.new - @loaded_recipes = {} - @loaded_attributes = {} @events = events - @reboot_info = {} - @node.run_context = self - @node.set_cookbook_attribute + node.run_context = self + node.set_cookbook_attribute + + @definitions = Hash.new + @loaded_recipes_hash = {} + @loaded_attributes_hash = {} + @reboot_info = {} @cookbook_compiler = nil + + initialize_child_state end - # Triggers the compile phase of the chef run. Implemented by - # Chef::RunContext::CookbookCompiler + # + # Triggers the compile phase of the chef run. + # + # @param run_list_expansion [Chef::RunList::RunListExpansion] The run list. + # @see Chef::RunContext::CookbookCompiler + # def load(run_list_expansion) @cookbook_compiler = CookbookCompiler.new(self, run_list_expansion, events) - @cookbook_compiler.compile + cookbook_compiler.compile end - # Adds an immediate notification to the - # +immediate_notification_collection+. The notification should be a - # Chef::Resource::Notification or duck type. + # + # Initialize state that applies to both Chef::RunContext and Chef::ChildRunContext + # + def initialize_child_state + @audits = {} + @resource_collection = Chef::ResourceCollection.new + @immediate_notification_collection = Hash.new {|h,k| h[k] = []} + @delayed_notification_collection = Hash.new {|h,k| h[k] = []} + end + + # + # Adds an immediate notification to the +immediate_notification_collection+. + # + # @param [Chef::Resource::Notification] The notification to add. + # def notifies_immediately(notification) nr = notification.notifying_resource if nr.instance_of?(Chef::Resource) - @immediate_notification_collection[nr.name] << notification + immediate_notification_collection[nr.name] << notification else - @immediate_notification_collection[nr.declared_key] << notification + immediate_notification_collection[nr.declared_key] << notification end end - # Adds a delayed notification to the +delayed_notification_collection+. The - # notification should be a Chef::Resource::Notification or duck type. + # + # Adds a delayed notification to the +delayed_notification_collection+. + # + # @param [Chef::Resource::Notification] The notification to add. + # def notifies_delayed(notification) nr = notification.notifying_resource if nr.instance_of?(Chef::Resource) - @delayed_notification_collection[nr.name] << notification + delayed_notification_collection[nr.name] << notification else - @delayed_notification_collection[nr.declared_key] << notification + delayed_notification_collection[nr.declared_key] << notification end end + # + # Get the list of immediate notifications sent by the given resource. + # + # TODO seriously, this is actually wrong. resource.name is not unique, + # you need the type as well. + # + # @return [Array[Notification]] + # def immediate_notifications(resource) if resource.instance_of?(Chef::Resource) - return @immediate_notification_collection[resource.name] + return immediate_notification_collection[resource.name] else - return @immediate_notification_collection[resource.declared_key] + return immediate_notification_collection[resource.declared_key] end end + # + # Get the list of delayed (end of run) notifications sent by the given + # resource. + # + # TODO seriously, this is actually wrong. resource.name is not unique, + # you need the type as well. + # + # @return [Array[Notification]] + # def delayed_notifications(resource) if resource.instance_of?(Chef::Resource) - return @delayed_notification_collection[resource.name] + return delayed_notification_collection[resource.name] else - return @delayed_notification_collection[resource.declared_key] + return delayed_notification_collection[resource.declared_key] end end + # + # Cookbook and recipe loading + # + + # # Evaluates the recipes +recipe_names+. Used by DSL::IncludeRecipe + # + # @param recipe_names [Array[String]] The list of recipe names (e.g. + # 'my_cookbook' or 'my_cookbook::my_resource'). + # @param current_cookbook The cookbook we are currently running in. + # + # @see DSL::IncludeRecipe#include_recipe + # def include_recipe(*recipe_names, current_cookbook: nil) result_recipes = Array.new recipe_names.flatten.each do |recipe_name| @@ -147,7 +252,21 @@ class Chef result_recipes end + # # Evaluates the recipe +recipe_name+. Used by DSL::IncludeRecipe + # + # TODO I am sort of confused why we have both this and include_recipe ... + # I don't see anything different beyond accepting and returning an + # array of recipes. + # + # @param recipe_names [Array[String]] The recipe name (e.g 'my_cookbook' or + # 'my_cookbook::my_resource'). + # @param current_cookbook The cookbook we are currently running in. + # + # @return A truthy value if the load occurred; `false` if already loaded. + # + # @see DSL::IncludeRecipe#load_recipe + # def load_recipe(recipe_name, current_cookbook: nil) Chef::Log.debug("Loading Recipe #{recipe_name} via include_recipe") @@ -175,6 +294,15 @@ ERROR_MESSAGE end end + # + # Load the given recipe from a filename. + # + # @param recipe_file [String] The recipe filename. + # + # @return [Chef::Recipe] The loaded recipe. + # + # @raise [Chef::Exceptions::RecipeNotFound] If the file does not exist. + # def load_recipe_file(recipe_file) if !File.exist?(recipe_file) raise Chef::Exceptions::RecipeNotFound, "could not find recipe file #{recipe_file}" @@ -186,8 +314,19 @@ ERROR_MESSAGE recipe end - # Looks up an attribute file given the +cookbook_name+ and - # +attr_file_name+. Used by DSL::IncludeAttribute + # + # Look up an attribute filename. + # + # @param cookbook_name [String] The cookbook name of the attribute file. + # @param attr_file_name [String] The attribute file's name (not path). + # + # @return [String] The filename. + # + # @see DSL::IncludeAttribute#include_attribute + # + # @raise [Chef::Exceptions::CookbookNotFound] If the cookbook could not be found. + # @raise [Chef::Exceptions::AttributeNotFound] If the attribute file could not be found. + # def resolve_attribute(cookbook_name, attr_file_name) cookbook = cookbook_collection[cookbook_name] raise Chef::Exceptions::CookbookNotFound, "could not find cookbook #{cookbook_name} while loading attribute #{name}" unless cookbook @@ -198,76 +337,152 @@ ERROR_MESSAGE attribute_filename end - # An Array of all recipes that have been loaded. This is stored internally - # as a Hash, so ordering is predictable. # - # Recipe names are given in fully qualified form, e.g., the recipe "nginx" - # will be given as "nginx::default" + # A list of all recipes that have been loaded. + # + # This is stored internally as a Hash, so ordering is predictable. + # + # TODO is the above statement true in a 1.9+ ruby world? Is it relevant? + # + # @return [Array[String]] A list of recipes in fully qualified form, e.g. + # the recipe "nginx" will be given as "nginx::default". + # + # @see #loaded_recipe? To determine if a particular recipe has been loaded. # - # To determine if a particular recipe has been loaded, use #loaded_recipe? def loaded_recipes - @loaded_recipes.keys + loaded_recipes_hash.keys end - # An Array of all attributes files that have been loaded. Stored internally - # using a Hash, so order is predictable. # - # Attribute file names are given in fully qualified form, e.g., - # "nginx::default" instead of "nginx". + # A list of all attributes files that have been loaded. + # + # Stored internally using a Hash, so order is predictable. + # + # TODO is the above statement true in a 1.9+ ruby world? Is it relevant? + # + # @return [Array[String]] A list of attribute file names in fully qualified + # form, e.g. the "nginx" will be given as "nginx::default". + # def loaded_attributes - @loaded_attributes.keys + loaded_attributes_hash.keys end + # + # Find out if a given recipe has been loaded. + # + # @param cookbook [String] Cookbook name. + # @param recipe [String] Recipe name. + # + # @return [Boolean] `true` if the recipe has been loaded, `false` otherwise. + # def loaded_fully_qualified_recipe?(cookbook, recipe) - @loaded_recipes.has_key?("#{cookbook}::#{recipe}") + loaded_recipes_hash.has_key?("#{cookbook}::#{recipe}") end - # Returns true if +recipe+ has been loaded, false otherwise. Default recipe - # names are expanded, so `loaded_recipe?("nginx")` and - # `loaded_recipe?("nginx::default")` are valid and give identical results. + # + # Find out if a given recipe has been loaded. + # + # @param recipe [String] Recipe name. "nginx" and "nginx::default" yield + # the same results. + # + # @return [Boolean] `true` if the recipe has been loaded, `false` otherwise. + # def loaded_recipe?(recipe) cookbook, recipe_name = Chef::Recipe.parse_recipe_name(recipe) loaded_fully_qualified_recipe?(cookbook, recipe_name) end + # + # Mark a given recipe as having been loaded. + # + # @param cookbook [String] Cookbook name. + # @param recipe [String] Recipe name. + # + def loaded_recipe(cookbook, recipe) + loaded_recipes_hash["#{cookbook}::#{recipe}"] = true + end + + # + # Find out if a given attribute file has been loaded. + # + # @param cookbook [String] Cookbook name. + # @param attribute_file [String] Attribute file name. + # + # @return [Boolean] `true` if the recipe has been loaded, `false` otherwise. + # def loaded_fully_qualified_attribute?(cookbook, attribute_file) - @loaded_attributes.has_key?("#{cookbook}::#{attribute_file}") + loaded_attributes_hash.has_key?("#{cookbook}::#{attribute_file}") end + # + # Mark a given attribute file as having been loaded. + # + # @param cookbook [String] Cookbook name. + # @param attribute_file [String] Attribute file name. + # def loaded_attribute(cookbook, attribute_file) - @loaded_attributes["#{cookbook}::#{attribute_file}"] = true + loaded_attributes_hash["#{cookbook}::#{attribute_file}"] = true end ## # Cookbook File Introspection + # + # Find out if the cookbook has the given template. + # + # @param cookbook [String] Cookbook name. + # @param template_name [String] Template name. + # + # @return [Boolean] `true` if the template is in the cookbook, `false` + # otherwise. + # @see Chef::CookbookVersion#has_template_for_node? + # def has_template_in_cookbook?(cookbook, template_name) cookbook = cookbook_collection[cookbook] cookbook.has_template_for_node?(node, template_name) end + # + # Find out if the cookbook has the given file. + # + # @param cookbook [String] Cookbook name. + # @param cb_file_name [String] File name. + # + # @return [Boolean] `true` if the file is in the cookbook, `false` + # otherwise. + # @see Chef::CookbookVersion#has_cookbook_file_for_node? + # def has_cookbook_file_in_cookbook?(cookbook, cb_file_name) cookbook = cookbook_collection[cookbook] cookbook.has_cookbook_file_for_node?(node, cb_file_name) end - # Delegates to CookbookCompiler#unreachable_cookbook? - # Used to raise an error when attempting to load a recipe belonging to a - # cookbook that is not in the dependency graph. See also: CHEF-4367 + # + # Find out whether the given cookbook is in the cookbook dependency graph. + # + # @param cookbook_name [String] Cookbook name. + # + # @return [Boolean] `true` if the cookbook is reachable, `false` otherwise. + # + # @see Chef::CookbookCompiler#unreachable_cookbook? def unreachable_cookbook?(cookbook_name) - @cookbook_compiler.unreachable_cookbook?(cookbook_name) + cookbook_compiler.unreachable_cookbook?(cookbook_name) end + # # Open a stream object that can be printed into and will dispatch to events # - # == Arguments - # options is a hash with these possible options: - # - name: a string that identifies the stream to the user. Preferably short. + # @param name [String] The name of the stream. + # @param options [Hash] Other options for the stream. + # + # @return [EventDispatch::EventsOutputStream] The created stream. + # + # @yield If a block is passed, it will be run and the stream will be closed + # afterwards. + # @yieldparam stream [EventDispatch::EventsOutputStream] The created stream. # - # Pass a block and the stream will be yielded to it, and close on its own - # at the end of the block. - def open_stream(options = {}) - stream = EventDispatch::EventsOutputStream.new(events, options) + def open_stream(name: nil, **options) + stream = EventDispatch::EventsOutputStream.new(events, name: name, **options) if block_given? begin yield stream @@ -280,31 +495,137 @@ ERROR_MESSAGE end # there are options for how to handle multiple calls to these functions: - # 1. first call always wins (never change @reboot_info once set). - # 2. last call always wins (happily change @reboot_info whenever). + # 1. first call always wins (never change reboot_info once set). + # 2. last call always wins (happily change reboot_info whenever). # 3. raise an exception on the first conflict. # 4. disable reboot after this run if anyone ever calls :cancel. # 5. raise an exception on any second call. # 6. ? def request_reboot(reboot_info) - Chef::Log::info "Changing reboot status from #{@reboot_info.inspect} to #{reboot_info.inspect}" + Chef::Log::info "Changing reboot status from #{self.reboot_info.inspect} to #{reboot_info.inspect}" @reboot_info = reboot_info end def cancel_reboot - Chef::Log::info "Changing reboot status from #{@reboot_info.inspect} to {}" + Chef::Log::info "Changing reboot status from #{reboot_info.inspect} to {}" @reboot_info = {} end def reboot_requested? - @reboot_info.size > 0 + reboot_info.size > 0 end - private + # + # Create a child RunContext. + # + def create_child + ChildRunContext.new(self) + end - def loaded_recipe(cookbook, recipe) - @loaded_recipes["#{cookbook}::#{recipe}"] = true + protected + + attr_reader :cookbook_compiler + attr_reader :loaded_attributes_hash + attr_reader :loaded_recipes_hash + + module Deprecated + ### + # These need to be settable so deploy can run a resource_collection + # independent of any cookbooks via +recipe_eval+ + def resource_collection=(value) + Chef.log_deprecation("Setting run_context.resource_collection will be removed in a future Chef. Use run_context.create_child to create a new RunContext instead.") + @resource_collection = value + end + + def audits=(value) + Chef.log_deprecation("Setting run_context.audits will be removed in a future Chef. Use run_context.create_child to create a new RunContext instead.") + @audits = value + end + + def immediate_notification_collection=(value) + Chef.log_deprecation("Setting run_context.immediate_notification_collection will be removed in a future Chef. Use run_context.create_child to create a new RunContext instead.") + @immediate_notification_collection = value + end + + def delayed_notification_collection=(value) + Chef.log_deprecation("Setting run_context.delayed_notification_collection will be removed in a future Chef. Use run_context.create_child to create a new RunContext instead.") + @delayed_notification_collection = value + end end + prepend Deprecated + + + # + # A child run context. Delegates all root context calls to its parent. + # + # @api private + # + class ChildRunContext < RunContext + extend Forwardable + def_delegators :parent_run_context, *%w( + cancel_reboot + config + cookbook_collection + cookbook_compiler + definitions + events + has_cookbook_file_in_cookbook? + has_template_in_cookbook? + load + loaded_attribute + loaded_attributes + loaded_attributes_hash + loaded_fully_qualified_attribute? + loaded_fully_qualified_recipe? + loaded_recipe + loaded_recipe? + loaded_recipes + loaded_recipes_hash + node + open_stream + reboot_info + reboot_info= + reboot_requested? + request_reboot + resolve_attribute + unreachable_cookbook? + ) + + def initialize(parent_run_context) + @parent_run_context = parent_run_context + + # We don't call super, because we don't bother initializing stuff we're + # going to delegate to the parent anyway. Just initialize things that + # every instance needs. + initialize_child_state + end + CHILD_STATE = %w( + audits + audits= + create_child + delayed_notification_collection + delayed_notification_collection= + delayed_notifications + immediate_notification_collection + immediate_notification_collection= + immediate_notifications + include_recipe + initialize_child_state + load_recipe + load_recipe_file + notifies_immediately + notifies_delayed + parent_run_context + resource_collection + resource_collection= + ).map { |x| x.to_sym } + + # Verify that we didn't miss any methods + missing_methods = superclass.instance_methods(false) - instance_methods(false) - CHILD_STATE + if !missing_methods.empty? + raise "ERROR: not all methods of RunContext accounted for in ChildRunContext! All methods must be marked as child methods with CHILD_STATE or delegated to the parent_run_context. Missing #{missing_methods.join(", ")}." + end + end end end diff --git a/lib/chef/run_list/versioned_recipe_list.rb b/lib/chef/run_list/versioned_recipe_list.rb index 7cce6fa48c..2824f08f31 100644 --- a/lib/chef/run_list/versioned_recipe_list.rb +++ b/lib/chef/run_list/versioned_recipe_list.rb @@ -70,15 +70,16 @@ class Chef # @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?('::') + qualified_recipe = if recipe_name.include?('::') recipe_name else "#{recipe_name}::default" end - if @versions[recipe_name] - ret << "@#{@versions[recipe_name]}" - end - ret + + version = @versions[recipe_name] + qualified_recipe = "#{qualified_recipe}@#{version}" if version + + qualified_recipe end end end diff --git a/lib/chef/user.rb b/lib/chef/user.rb index 717deb63c3..31ebeda86f 100644 --- a/lib/chef/user.rb +++ b/lib/chef/user.rb @@ -21,85 +21,45 @@ 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) +# TODO +# DEPRECATION NOTE +# This class will be replaced by Chef::UserV1 in Chef 13. 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. # -# 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. +# Chef::UserV1 now supports Chef Server 12 and will be moved to this namespace in Chef 13. # -# Exception: self.list is backwards compatible with OSC 11 +# New development should occur in Chef::UserV1. +# This file and corrosponding osc_user knife files +# should be removed once client support for Open Source Chef Server 11 expires. class Chef class User include Chef::Mixin::FromFile include Chef::Mixin::ParamsValidate - include Chef::Mixin::ApiVersionRequestHandling - - SUPPORTED_API_VERSIONS = [0,1] def initialize - @username = nil - @display_name = nil - @first_name = nil - @middle_name = nil - @last_name = nil - @email = nil - @password = nil + @name = '' @public_key = nil @private_key = nil - @create_key = nil @password = nil + @admin = false end - 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"}) + def chef_rest_v0 + @chef_rest_v0 ||= Chef::ServerAPI.new(Chef::Config[:chef_server_url], {:api_version => "0"}) end - def username(arg=nil) - set_or_return(:username, arg, + def name(arg=nil) + set_or_return(:name, arg, :regex => /^[a-z0-9\-_]+$/) end - 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]) + def admin(arg=nil) + set_or_return(:admin, + arg, :kind_of => [TrueClass, FalseClass]) end def public_key(arg=nil) @@ -119,17 +79,12 @@ class Chef def to_hash result = { - "username" => @username + "name" => @name, + "public_key" => @public_key, + "admin" => @admin } - 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["private_key"] = @private_key if @private_key + result["password"] = @password if @password result end @@ -138,86 +93,21 @@ class Chef end def destroy - # will default to the current API version (Chef::Authenticator::DEFAULT_SERVER_API_VERSION) - Chef::REST.new(Chef::Config[:chef_server_url]).delete("users/#{@username}") + chef_rest_v0.delete("users/#{@name}") end def create - # 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 - + payload = {:name => self.name, :admin => self.admin, :password => self.password } + payload[:public_key] = public_key if public_key + new_user = chef_rest_v0.post("users", payload) Chef::User.from_hash(self.to_hash.merge(new_user)) end def update(new_key=false) - 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 + payload = {:name => name, :admin => admin} + payload[:private_key] = new_key if new_key + payload[:password] = password if password + updated_user = chef_rest_v0.put("users/#{name}", payload) Chef::User.from_hash(self.to_hash.merge(updated_user)) end @@ -233,47 +123,30 @@ class Chef end end - # Note: remove after API v0 no longer supported by client (and knife command). def reregister - 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 + reregistered_self = chef_rest_v0.put("users/#{name}", { :name => name, :admin => admin, :private_key => true }) + private_key(reregistered_self["private_key"]) self end def to_s - "user[#{@username}]" + "user[#{@name}]" + end + + def inspect + "Chef::User name:'#{name}' admin:'#{admin.inspect}'" + + "public_key:'#{public_key}' private_key:#{private_key}" end # Class Methods def self.from_hash(user_hash) user = Chef::User.new - 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'] if user_hash.key?('public_key') + user.name user_hash['name'] 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.password user_hash['password'] if user_hash.key?('password') + user.public_key user_hash['public_key'] + user.admin user_hash['admin'] user end @@ -286,19 +159,12 @@ class Chef end def self.list(inflate=false) - response = Chef::REST.new(Chef::Config[:chef_server_url]).get('users') + response = Chef::ServerAPI.new(Chef::Config[:chef_server_url], {:api_version => "0"}).get('users') users = if response.is_a?(Array) - # 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 - + transform_ohc_list_response(response) # OHC/OPC + else + response # OSC + end if inflate users.inject({}) do |user_map, (name, _url)| user_map[name] = Chef::User.load(name) @@ -309,9 +175,8 @@ class Chef end end - 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}") + def self.load(name) + response = Chef::ServerAPI.new(Chef::Config[:chef_server_url], {:api_version => "0"}).get("users/#{name}") Chef::User.from_hash(response) end @@ -319,7 +184,7 @@ class Chef # [ { "user" => { "username" => USERNAME }}, ...] # into the form # { "USERNAME" => "URI" } - def self.transform_list_response(response) + def self.transform_ohc_list_response(response) new_response = Hash.new response.each do |u| name = u['user']['username'] @@ -328,7 +193,6 @@ class Chef new_response end - private_class_method :transform_list_response - + private_class_method :transform_ohc_list_response end end diff --git a/lib/chef/user_v1.rb b/lib/chef/user_v1.rb new file mode 100644 index 0000000000..31cb0576a2 --- /dev/null +++ b/lib/chef/user_v1.rb @@ -0,0 +1,335 @@ +# +# 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' +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::UserV1 is no longer expected to support Open Source Chef 11 Server requests. +# The object that handles those requests remain in the Chef::User namespace. +# This code will be moved to the Chef::User namespace as of Chef 13. +# +# Exception: self.list is backwards compatible with OSC 11 +class Chef + class UserV1 + + include Chef::Mixin::FromFile + include Chef::Mixin::ParamsValidate + include Chef::Mixin::ApiVersionRequestHandling + + SUPPORTED_API_VERSIONS = [0,1] + + def initialize + @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 + end + + 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 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) + 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 = { + "username" => @username + } + 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 + + def to_json(*a) + Chef::JSONCompat.to_json(to_hash, *a) + end + + def destroy + # 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 + # 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::UserV1.from_hash(self.to_hash.merge(new_user)) + end + + def update(new_key=false) + 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::UserV1.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 + + # Note: remove after API v0 no longer supported by client (and knife command). + def reregister + 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[#{@username}]" + end + + # Class Methods + + def self.from_hash(user_hash) + user = Chef::UserV1.new + 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'] 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 + + def self.from_json(json) + Chef::UserV1.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('users') + users = if response.is_a?(Array) + # 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::UserV1.load(name) + user_map + end + else + users + end + end + + 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::UserV1.from_hash(response) + end + + # Gross. Transforms an API response in the form of: + # [ { "user" => { "username" => USERNAME }}, ...] + # into the form + # { "USERNAME" => "URI" } + def self.transform_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_list_response + + end +end diff --git a/lib/chef/util/powershell/ps_credential.rb b/lib/chef/util/powershell/ps_credential.rb index 01f8c27b6c..3f4558a77c 100644 --- a/lib/chef/util/powershell/ps_credential.rb +++ b/lib/chef/util/powershell/ps_credential.rb @@ -29,6 +29,10 @@ class Chef::Util::Powershell "New-Object System.Management.Automation.PSCredential('#{@username}',('#{encrypt(@password)}' | ConvertTo-SecureString))" end + def to_s + to_psobject + end + private def encrypt(str) diff --git a/lib/chef/util/windows/net_group.rb b/lib/chef/util/windows/net_group.rb index 924bd392f9..2085747eb9 100644 --- a/lib/chef/util/windows/net_group.rb +++ b/lib/chef/util/windows/net_group.rb @@ -1,106 +1,85 @@ -# -# Author:: Doug MacEachern (<dougm@vmware.com>) -# Copyright:: Copyright (c) 2010 VMware, 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/util/windows' - -#wrapper around a subset of the NetGroup* APIs. -#nothing Chef specific, but not complete enough to be its own gem, so util for now. -class Chef::Util::Windows::NetGroup < Chef::Util::Windows - - private - - def pack_str(s) - [str_to_ptr(s)].pack('L') - end - - def modify_members(members, func) - buffer = 0.chr * (members.size * PTR_SIZE) - members.each_with_index do |member,offset| - buffer[offset*PTR_SIZE,PTR_SIZE] = pack_str(multi_to_wide(member)) - end - rc = func.call(nil, @name, 3, buffer, members.size) - if rc != NERR_Success - raise ArgumentError, get_last_error(rc) - end - end - - public - - def initialize(groupname) - @name = multi_to_wide(groupname) - end - - def local_get_members - group_members = [] - handle = 0.chr * PTR_SIZE - rc = ERROR_MORE_DATA - - while rc == ERROR_MORE_DATA - ptr = 0.chr * PTR_SIZE - nread = 0.chr * PTR_SIZE - total = 0.chr * PTR_SIZE - - rc = NetLocalGroupGetMembers.call(nil, @name, 0, ptr, -1, - nread, total, handle) - if (rc == NERR_Success) || (rc == ERROR_MORE_DATA) - ptr = ptr.unpack('L')[0] - nread = nread.unpack('i')[0] - members = 0.chr * (nread * PTR_SIZE ) #nread * sizeof(LOCALGROUP_MEMBERS_INFO_0) - memcpy(members, ptr, members.size) - - # 1 pointer field in LOCALGROUP_MEMBERS_INFO_0, offset 0 is lgrmi0_sid - nread.times do |i| - sid_address = members[i * PTR_SIZE, PTR_SIZE].unpack('L')[0] - sid_ptr = FFI::Pointer.new(sid_address) - member_sid = Chef::ReservedNames::Win32::Security::SID.new(sid_ptr) - group_members << member_sid.to_s - end - NetApiBufferFree(ptr) - else - raise ArgumentError, get_last_error(rc) - end - end - group_members - end - - def local_add - rc = NetLocalGroupAdd.call(nil, 0, pack_str(@name), nil) - if rc != NERR_Success - raise ArgumentError, get_last_error(rc) - end - end - - def local_set_members(members) - modify_members(members, NetLocalGroupSetMembers) - end - - def local_add_members(members) - modify_members(members, NetLocalGroupAddMembers) - end - - def local_delete_members(members) - modify_members(members, NetLocalGroupDelMembers) - end - - def local_delete - rc = NetLocalGroupDel.call(nil, @name) - if rc != NERR_Success - raise ArgumentError, get_last_error(rc) - end - end -end +#
+# Author:: Doug MacEachern (<dougm@vmware.com>)
+# Copyright:: Copyright (c) 2010 VMware, 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/util/windows'
+require 'chef/win32/net'
+
+#wrapper around a subset of the NetGroup* APIs.
+class Chef::Util::Windows::NetGroup
+
+ private
+
+ def groupname
+ @groupname
+ end
+
+ public
+
+ def initialize(groupname)
+ @groupname = groupname
+ end
+
+ def local_get_members
+ begin
+ Chef::ReservedNames::Win32::NetUser::net_local_group_get_members(nil, groupname)
+ rescue Chef::Exceptions::Win32NetAPIError => e
+ raise ArgumentError, e.msg
+ end
+ end
+
+ def local_add
+ begin
+ Chef::ReservedNames::Win32::NetUser::net_local_group_add(nil, groupname)
+ rescue Chef::Exceptions::Win32NetAPIError => e
+ raise ArgumentError, e.msg
+ end
+ end
+
+ def local_set_members(members)
+ begin
+ Chef::ReservedNames::Win32::NetUser::net_local_group_set_members(nil, groupname, members)
+ rescue Chef::Exceptions::Win32NetAPIError => e
+ raise ArgumentError, e.msg
+ end
+ end
+
+ def local_add_members(members)
+ begin
+ Chef::ReservedNames::Win32::NetUser::net_local_group_add_members(nil, groupname, members)
+ rescue Chef::Exceptions::Win32NetAPIError => e
+ raise ArgumentError, e.msg
+ end
+ end
+
+ def local_delete_members(members)
+ begin
+ Chef::ReservedNames::Win32::NetUser::net_local_group_del_members(nil, groupname, members)
+ rescue Chef::Exceptions::Win32NetAPIError => e
+ raise ArgumentError, e.msg
+ end
+
+ end
+
+ def local_delete
+ begin
+ Chef::ReservedNames::Win32::NetUser::net_local_group_del(nil, groupname)
+ rescue Chef::Exceptions::Win32NetAPIError => e
+ raise ArgumentError, e.msg
+ end
+ end
+end
diff --git a/lib/chef/util/windows/net_use.rb b/lib/chef/util/windows/net_use.rb index 62d7e169dc..b94576e702 100644 --- a/lib/chef/util/windows/net_use.rb +++ b/lib/chef/util/windows/net_use.rb @@ -21,61 +21,18 @@ #see also cmd.exe: net use /? require 'chef/util/windows' +require 'chef/win32/net' class Chef::Util::Windows::NetUse < Chef::Util::Windows - - private - - USE_NOFORCE = 0 - USE_FORCE = 1 - USE_LOTS_OF_FORCE = 2 #every windows API should support this flag - - USE_INFO_2 = [ - [:local, nil], - [:remote, nil], - [:password, nil], - [:status, 0], - [:asg_type, 0], - [:refcount, 0], - [:usecount, 0], - [:username, nil], - [:domainname, nil] - ] - - USE_INFO_2_TEMPLATE = - USE_INFO_2.collect { |field| field[1].class == Fixnum ? 'i' : 'L' }.join - - SIZEOF_USE_INFO_2 = #sizeof(USE_INFO_2) - USE_INFO_2.inject(0) do |sum, item| - sum + (item[1].class == Fixnum ? 4 : PTR_SIZE) - end - - def use_info_2(args) - USE_INFO_2.collect { |field| - args.include?(field[0]) ? args[field[0]] : field[1] - } - end - - def use_info_2_pack(use) - use.collect { |v| - v.class == Fixnum ? v : str_to_ptr(multi_to_wide(v)) - }.pack(USE_INFO_2_TEMPLATE) + def initialize(localname) + @use_name = localname end - def use_info_2_unpack(buffer) - use = Hash.new - USE_INFO_2.each_with_index do |field,offset| - use[field[0]] = field[1].class == Fixnum ? - dword_to_i(buffer, offset) : lpwstr_to_s(buffer, offset) + def to_ui2_struct(use_info) + use_info.inject({}) do |memo, (k,v)| + memo["ui2_#{k}".to_sym] = v + memo end - use - end - - public - - def initialize(localname) - @localname = localname - @name = multi_to_wide(localname) end def add(args) @@ -84,38 +41,45 @@ class Chef::Util::Windows::NetUse < Chef::Util::Windows args = Hash.new args[:remote] = remote end - args[:local] ||= @localname - use = use_info_2(args) - buffer = use_info_2_pack(use) - rc = NetUseAdd.call(nil, 2, buffer, nil) - if rc != NERR_Success - raise ArgumentError, get_last_error(rc) + args[:local] ||= use_name + ui2_hash = to_ui2_struct(args) + + begin + Chef::ReservedNames::Win32::Net.net_use_add_l2(nil, ui2_hash) + rescue Chef::Exceptions::Win32APIError => e + raise ArgumentError, e end end - def get_info - ptr = 0.chr * PTR_SIZE - rc = NetUseGetInfo.call(nil, @name, 2, ptr) - - if rc != NERR_Success - raise ArgumentError, get_last_error(rc) + def from_use_info_struct(ui2_hash) + ui2_hash.inject({}) do |memo, (k,v)| + memo[k.to_s.sub('ui2_', '').to_sym] = v + memo end + end - ptr = ptr.unpack('L')[0] - buffer = 0.chr * SIZEOF_USE_INFO_2 - memcpy(buffer, ptr, buffer.size) - NetApiBufferFree(ptr) - use_info_2_unpack(buffer) + def get_info + begin + ui2 = Chef::ReservedNames::Win32::Net.net_use_get_info_l2(nil, use_name) + from_use_info_struct(ui2) + rescue Chef::Exceptions::Win32APIError => e + raise ArgumentError, e + end end def device get_info()[:remote] end - #XXX should we use some FORCE here? + def delete - rc = NetUseDel.call(nil, @name, USE_NOFORCE) - if rc != NERR_Success - raise ArgumentError, get_last_error(rc) + begin + Chef::ReservedNames::Win32::Net.net_use_del(nil, use_name, :use_noforce) + rescue Chef::Exceptions::Win32APIError => e + raise ArgumentError, e end end + + def use_name + @use_name + end end diff --git a/lib/chef/util/windows/volume.rb b/lib/chef/util/windows/volume.rb index 08c3a53793..6e45594ba6 100644 --- a/lib/chef/util/windows/volume.rb +++ b/lib/chef/util/windows/volume.rb @@ -18,42 +18,42 @@ #simple wrapper around Volume APIs. might be possible with WMI, but possibly more complex. +require 'chef/win32/api/file' require 'chef/util/windows' -require 'windows/volume' class Chef::Util::Windows::Volume < Chef::Util::Windows - - private - include Windows::Volume - #XXX not defined in the current windows-pr release - DeleteVolumeMountPoint = - Windows::API.new('DeleteVolumeMountPoint', 'S', 'B') unless defined? DeleteVolumeMountPoint - - public + attr_reader :mount_point def initialize(name) name += "\\" unless name =~ /\\$/ #trailing slash required - @name = name + @mount_point = name end def device - buffer = 0.chr * 256 - if GetVolumeNameForVolumeMountPoint(@name, buffer, buffer.size) - return buffer[0,buffer.size].unpack("Z*")[0] - else - raise ArgumentError, get_last_error + begin + Chef::ReservedNames::Win32::File.get_volume_name_for_volume_mount_point(mount_point) + rescue Chef::Exceptions::Win32APIError => e + raise ArgumentError, e end end def delete - unless DeleteVolumeMountPoint.call(@name) - raise ArgumentError, get_last_error + begin + Chef::ReservedNames::Win32::File.delete_volume_mount_point(mount_point) + rescue Chef::Exceptions::Win32APIError => e + raise ArgumentError, e end end def add(args) - unless SetVolumeMountPoint(@name, args[:remote]) - raise ArgumentError, get_last_error + begin + Chef::ReservedNames::Win32::File.set_volume_mount_point(mount_point, args[:remote]) + rescue Chef::Exceptions::Win32APIError => e + raise ArgumentError, e end end + + def mount_point + @mount_point + end end diff --git a/lib/chef/version.rb b/lib/chef/version.rb index 80fd422c55..faa61aee54 100644 --- a/lib/chef/version.rb +++ b/lib/chef/version.rb @@ -21,7 +21,7 @@ class Chef CHEF_ROOT = File.dirname(File.expand_path(File.dirname(__FILE__))) - VERSION = '12.4.0.rc.2' + VERSION = '12.5.0.current.0' end # @@ -29,6 +29,6 @@ end # # NOTE: DO NOT Use the Chef::Version class on Chef::VERSIONs. The # Chef::Version class is for _cookbooks_ only, and cannot handle -# pre-release chef-client versions like "10.14.0.rc.2". Please -# use Rubygem's Gem::Version class instead. +# pre-release versions like "10.14.0.rc.2". Please use Rubygem's +# Gem::Version class instead. # diff --git a/lib/chef/win32/api.rb b/lib/chef/win32/api.rb index e9d273808a..4786222bd4 100644 --- a/lib/chef/win32/api.rb +++ b/lib/chef/win32/api.rb @@ -188,6 +188,7 @@ class Chef host.typedef :pointer, :PCRYPTPROTECT_PROMPTSTRUCT # Pointer to a CRYPTOPROTECT_PROMPTSTRUCT. host.typedef :pointer, :PDATA_BLOB # Pointer to a DATA_BLOB. host.typedef :pointer, :PTSTR # A PWSTR if UNICODE is defined, a PSTR otherwise. + host.typedef :pointer, :PSID host.typedef :pointer, :PUCHAR # Pointer to a UCHAR. host.typedef :pointer, :PUHALF_PTR # Pointer to a UHALF_PTR. host.typedef :pointer, :PUINT # Pointer to a UINT. diff --git a/lib/chef/win32/api/file.rb b/lib/chef/win32/api/file.rb index 86b2b942c2..9ff1ad40d6 100644 --- a/lib/chef/win32/api/file.rb +++ b/lib/chef/win32/api/file.rb @@ -20,6 +20,7 @@ require 'chef/win32/api' require 'chef/win32/api/security' require 'chef/win32/api/system' +require 'chef/win32/unicode' class Chef module ReservedNames::Win32 @@ -450,6 +451,25 @@ BOOL WINAPI DeviceIoControl( =end safe_attach_function :DeviceIoControl, [:HANDLE, :DWORD, :LPVOID, :DWORD, :LPVOID, :DWORD, :LPDWORD, :pointer], :BOOL + +#BOOL WINAPI DeleteVolumeMountPoint( + #_In_ LPCTSTR lpszVolumeMountPoint +#); + safe_attach_function :DeleteVolumeMountPointW, [:LPCTSTR], :BOOL + +#BOOL WINAPI SetVolumeMountPoint( + #_In_ LPCTSTR lpszVolumeMountPoint, + #_In_ LPCTSTR lpszVolumeName +#); + safe_attach_function :SetVolumeMountPointW, [:LPCTSTR, :LPCTSTR], :BOOL + +#BOOL WINAPI GetVolumeNameForVolumeMountPoint( + #_In_ LPCTSTR lpszVolumeMountPoint, + #_Out_ LPTSTR lpszVolumeName, + #_In_ DWORD cchBufferLength +#); + safe_attach_function :GetVolumeNameForVolumeMountPointW, [:LPCTSTR, :LPTSTR, :DWORD], :BOOL + ############################################### # Helpers ############################################### diff --git a/lib/chef/win32/api/net.rb b/lib/chef/win32/api/net.rb index 72caf46628..b173987a05 100644 --- a/lib/chef/win32/api/net.rb +++ b/lib/chef/win32/api/net.rb @@ -17,6 +17,7 @@ # require 'chef/win32/api' +require 'chef/win32/unicode' class Chef module ReservedNames::Win32 @@ -40,6 +41,10 @@ class Chef UF_NORMAL_ACCOUNT = 0x000200 UF_DONT_EXPIRE_PASSWD = 0x010000 + USE_NOFORCE = 0 + USE_FORCE = 1 + USE_LOTS_OF_FORCE = 2 #every windows API should support this flag + NERR_Success = 0 NERR_InvalidComputer = 2351 NERR_NotPrimary = 2226 @@ -49,41 +54,13 @@ class Chef NERR_BadPassword = 2203 NERR_PasswordTooShort = 2245 NERR_UserNotFound = 2221 + NERR_GroupNotFound = 2220 ERROR_ACCESS_DENIED = 5 + ERROR_MORE_DATA = 234 ffi_lib "netapi32" - class USER_INFO_3 < FFI::Struct - layout :usri3_name, :LPWSTR, - :usri3_password, :LPWSTR, - :usri3_password_age, :DWORD, - :usri3_priv, :DWORD, - :usri3_home_dir, :LPWSTR, - :usri3_comment, :LPWSTR, - :usri3_flags, :DWORD, - :usri3_script_path, :LPWSTR, - :usri3_auth_flags, :DWORD, - :usri3_full_name, :LPWSTR, - :usri3_usr_comment, :LPWSTR, - :usri3_parms, :LPWSTR, - :usri3_workstations, :LPWSTR, - :usri3_last_logon, :DWORD, - :usri3_last_logoff, :DWORD, - :usri3_acct_expires, :DWORD, - :usri3_max_storage, :DWORD, - :usri3_units_per_week, :DWORD, - :usri3_logon_hours, :PBYTE, - :usri3_bad_pw_count, :DWORD, - :usri3_num_logons, :DWORD, - :usri3_logon_server, :LPWSTR, - :usri3_country_code, :DWORD, - :usri3_code_page, :DWORD, - :usri3_user_id, :DWORD, - :usri3_primary_group_id, :DWORD, - :usri3_profile, :LPWSTR, - :usri3_home_dir_drive, :LPWSTR, - :usri3_password_expired, :DWORD - + module StructHelpers def set(key, val) val = if val.is_a? String encoded = if val.encoding == Encoding::UTF_16LE @@ -115,6 +92,47 @@ class Chef end end + def as_ruby + members.inject({}) do |memo, key| + memo[key] = get(key) + memo + end + end + end + + + class USER_INFO_3 < FFI::Struct + include StructHelpers + layout :usri3_name, :LPWSTR, + :usri3_password, :LPWSTR, + :usri3_password_age, :DWORD, + :usri3_priv, :DWORD, + :usri3_home_dir, :LPWSTR, + :usri3_comment, :LPWSTR, + :usri3_flags, :DWORD, + :usri3_script_path, :LPWSTR, + :usri3_auth_flags, :DWORD, + :usri3_full_name, :LPWSTR, + :usri3_usr_comment, :LPWSTR, + :usri3_parms, :LPWSTR, + :usri3_workstations, :LPWSTR, + :usri3_last_logon, :DWORD, + :usri3_last_logoff, :DWORD, + :usri3_acct_expires, :DWORD, + :usri3_max_storage, :DWORD, + :usri3_units_per_week, :DWORD, + :usri3_logon_hours, :PBYTE, + :usri3_bad_pw_count, :DWORD, + :usri3_num_logons, :DWORD, + :usri3_logon_server, :LPWSTR, + :usri3_country_code, :DWORD, + :usri3_code_page, :DWORD, + :usri3_user_id, :DWORD, + :usri3_primary_group_id, :DWORD, + :usri3_profile, :LPWSTR, + :usri3_home_dir_drive, :LPWSTR, + :usri3_password_expired, :DWORD + def usri3_logon_hours val = self[:usri3_logon_hours] if !val.nil? && !val.null? @@ -123,19 +141,66 @@ class Chef nil end end + end - def as_ruby - members.inject({}) do |memo, key| - memo[key] = get(key) - memo - end - end + class LOCALGROUP_MEMBERS_INFO_0 < FFI::Struct + layout :lgrmi0_sid, :PSID end class LOCALGROUP_MEMBERS_INFO_3 < FFI::Struct layout :lgrmi3_domainandname, :LPWSTR end + class LOCALGROUP_INFO_0 < FFI::Struct + layout :lgrpi0_name, :LPWSTR + end + + class USE_INFO_2 < FFI::Struct + include StructHelpers + + layout :ui2_local, :LMSTR, + :ui2_remote, :LMSTR, + :ui2_password, :LMSTR, + :ui2_status, :DWORD, + :ui2_asg_type, :DWORD, + :ui2_refcount, :DWORD, + :ui2_usecount, :DWORD, + :ui2_username, :LPWSTR, + :ui2_domainname, :LMSTR + end + + +#NET_API_STATUS NetLocalGroupAdd( + #_In_ LPCWSTR servername, + #_In_ DWORD level, + #_In_ LPBYTE buf, + #_Out_ LPDWORD parm_err +#); + safe_attach_function :NetLocalGroupAdd, [ + :LPCWSTR, :DWORD, :LPBYTE, :LPDWORD + ], :DWORD + +#NET_API_STATUS NetLocalGroupDel( + #_In_ LPCWSTR servername, + #_In_ LPCWSTR groupname +#); + safe_attach_function :NetLocalGroupDel, [:LPCWSTR, :LPCWSTR], :DWORD + +#NET_API_STATUS NetLocalGroupGetMembers( + #_In_ LPCWSTR servername, + #_In_ LPCWSTR localgroupname, + #_In_ DWORD level, + #_Out_ LPBYTE *bufptr, + #_In_ DWORD prefmaxlen, + #_Out_ LPDWORD entriesread, + #_Out_ LPDWORD totalentries, + #_Inout_ PDWORD_PTR resumehandle +#); + safe_attach_function :NetLocalGroupGetMembers, [ + :LPCWSTR, :LPCWSTR, :DWORD, :LPBYTE, :DWORD, + :LPDWORD, :LPDWORD, :PDWORD_PTR + ], :DWORD + # NET_API_STATUS NetUserEnum( # _In_ LPCWSTR servername, # _In_ DWORD level, @@ -146,12 +211,15 @@ class Chef # _Out_ LPDWORD totalentries, # _Inout_ LPDWORD resume_handle # ); - safe_attach_function :NetUserEnum, [ :LPCWSTR, :DWORD, :DWORD, :LPBYTE, :DWORD, :LPDWORD, :LPDWORD, :LPDWORD ], :DWORD + safe_attach_function :NetUserEnum, [ + :LPCWSTR, :DWORD, :DWORD, :LPBYTE, + :DWORD, :LPDWORD, :LPDWORD, :LPDWORD + ], :DWORD # NET_API_STATUS NetApiBufferFree( # _In_ LPVOID Buffer # ); - safe_attach_function :NetApiBufferFree, [ :LPVOID ], :DWORD + safe_attach_function :NetApiBufferFree, [:LPVOID], :DWORD #NET_API_STATUS NetUserAdd( #_In_ LMSTR servername, @@ -159,7 +227,9 @@ class Chef #_In_ LPBYTE buf, #_Out_ LPDWORD parm_err #); - safe_attach_function :NetUserAdd, [:LMSTR, :DWORD, :LPBYTE, :LPDWORD ], :DWORD + safe_attach_function :NetUserAdd, [ + :LMSTR, :DWORD, :LPBYTE, :LPDWORD + ], :DWORD #NET_API_STATUS NetLocalGroupAddMembers( # _In_ LPCWSTR servername, @@ -168,7 +238,31 @@ class Chef # _In_ LPBYTE buf, # _In_ DWORD totalentries #); - safe_attach_function :NetLocalGroupAddMembers, [:LPCWSTR, :LPCWSTR, :DWORD, :LPBYTE, :DWORD ], :DWORD + safe_attach_function :NetLocalGroupAddMembers, [ + :LPCWSTR, :LPCWSTR, :DWORD, :LPBYTE, :DWORD + ], :DWORD + +#NET_API_STATUS NetLocalGroupSetMembers( +# _In_ LPCWSTR servername, +# _In_ LPCWSTR groupname, +# _In_ DWORD level, +# _In_ LPBYTE buf, +# _In_ DWORD totalentries +#); + safe_attach_function :NetLocalGroupSetMembers, [ + :LPCWSTR, :LPCWSTR, :DWORD, :LPBYTE, :DWORD + ], :DWORD + +#NET_API_STATUS NetLocalGroupDelMembers( +# _In_ LPCWSTR servername, +# _In_ LPCWSTR groupname, +# _In_ DWORD level, +# _In_ LPBYTE buf, +# _In_ DWORD totalentries +#); + safe_attach_function :NetLocalGroupDelMembers, [ + :LPCWSTR, :LPCWSTR, :DWORD, :LPBYTE, :DWORD + ], :DWORD #NET_API_STATUS NetUserGetInfo( # _In_ LPCWSTR servername, @@ -176,7 +270,9 @@ class Chef # _In_ DWORD level, # _Out_ LPBYTE *bufptr #); - safe_attach_function :NetUserGetInfo, [:LPCWSTR, :LPCWSTR, :DWORD, :LPBYTE], :DWORD + safe_attach_function :NetUserGetInfo, [ + :LPCWSTR, :LPCWSTR, :DWORD, :LPBYTE + ], :DWORD #NET_API_STATUS NetApiBufferFree( # _In_ LPVOID Buffer @@ -190,7 +286,9 @@ class Chef # _In_ LPBYTE buf, # _Out_ LPDWORD parm_err #); - safe_attach_function :NetUserSetInfo, [:LPCWSTR, :LPCWSTR, :DWORD, :LPBYTE, :LPDWORD], :DWORD + safe_attach_function :NetUserSetInfo, [ + :LPCWSTR, :LPCWSTR, :DWORD, :LPBYTE, :LPDWORD + ], :DWORD #NET_API_STATUS NetUserDel( # _In_ LPCWSTR servername, @@ -198,6 +296,28 @@ class Chef #); safe_attach_function :NetUserDel, [:LPCWSTR, :LPCWSTR], :DWORD +#NET_API_STATUS NetUseDel( + #_In_ LMSTR UncServerName, + #_In_ LMSTR UseName, + #_In_ DWORD ForceCond +#); + safe_attach_function :NetUseDel, [:LMSTR, :LMSTR, :DWORD], :DWORD + +#NET_API_STATUS NetUseGetInfo( + #_In_ LMSTR UncServerName, + #_In_ LMSTR UseName, + #_In_ DWORD Level, + #_Out_ LPBYTE *BufPtr +#); + safe_attach_function :NetUseGetInfo, [:LMSTR, :LMSTR, :DWORD, :pointer], :DWORD + +#NET_API_STATUS NetUseAdd( + #_In_ LMSTR UncServerName, + #_In_ DWORD Level, + #_In_ LPBYTE Buf, + #_Out_ LPDWORD ParmError +#); + safe_attach_function :NetUseAdd, [:LMSTR, :DWORD, :LPBYTE, :LPDWORD], :DWORD end end end diff --git a/lib/chef/win32/api/registry.rb b/lib/chef/win32/api/registry.rb new file mode 100644 index 0000000000..45b91d7d32 --- /dev/null +++ b/lib/chef/win32/api/registry.rb @@ -0,0 +1,45 @@ +# +# Author:: Salim Alam (<salam@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. +# + +require 'chef/win32/api' + +class Chef + module ReservedNames::Win32 + module API + module Registry + extend Chef::ReservedNames::Win32::API + + ############################################### + # Win32 API Bindings + ############################################### + + ffi_lib 'advapi32' + + # LONG WINAPI RegDeleteKeyEx( + # _In_ HKEY hKey, + # _In_ LPCTSTR lpSubKey, + # _In_ REGSAM samDesired, + # _Reserved_ DWORD Reserved + # ); + safe_attach_function :RegDeleteKeyExW, [ :HKEY, :LPCTSTR, :LONG, :DWORD ], :LONG + safe_attach_function :RegDeleteKeyExA, [ :HKEY, :LPCTSTR, :LONG, :DWORD ], :LONG + + end + end + end +end
\ No newline at end of file diff --git a/lib/chef/win32/api/system.rb b/lib/chef/win32/api/system.rb index d57699acb4..a485f89708 100644 --- a/lib/chef/win32/api/system.rb +++ b/lib/chef/win32/api/system.rb @@ -187,6 +187,29 @@ int WINAPI GetSystemMetrics( safe_attach_function :GetSystemMetrics, [:int], :int =begin +UINT WINAPI GetSystemWow64Directory( + _Out_ LPTSTR lpBuffer, + _In_ UINT uSize +); +=end + safe_attach_function :GetSystemWow64DirectoryW, [:LPTSTR, :UINT], :UINT + safe_attach_function :GetSystemWow64DirectoryA, [:LPTSTR, :UINT], :UINT + +=begin +BOOL WINAPI Wow64DisableWow64FsRedirection( + _Out_ PVOID *OldValue +); +=end + safe_attach_function :Wow64DisableWow64FsRedirection, [:PVOID], :BOOL + +=begin +BOOL WINAPI Wow64RevertWow64FsRedirection( + _In_ PVOID OldValue +); +=end + safe_attach_function :Wow64RevertWow64FsRedirection, [:PVOID], :BOOL + +=begin LRESULT WINAPI SendMessageTimeout( _In_ HWND hWnd, _In_ UINT Msg, diff --git a/lib/chef/win32/api/unicode.rb b/lib/chef/win32/api/unicode.rb index 2e3a599f0a..2a9166aa99 100644 --- a/lib/chef/win32/api/unicode.rb +++ b/lib/chef/win32/api/unicode.rb @@ -129,49 +129,6 @@ int WideCharToMultiByte( =end safe_attach_function :WideCharToMultiByte, [:UINT, :DWORD, :LPCWSTR, :int, :LPSTR, :int, :LPCSTR, :LPBOOL], :int - ############################################### - # Helpers - ############################################### - - def utf8_to_wide(ustring) - # ensure it is actually UTF-8 - # Ruby likes to mark binary data as ASCII-8BIT - 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.length == 0 or ustring[-1].chr != "\000" - - # encode it all as UTF-16LE AKA Windows Wide Character AKA Windows Unicode - ustring = begin - if ustring.respond_to?(:encode) - ustring.encode('UTF-16LE') - else - require 'iconv' - Iconv.conv("UTF-16LE", "UTF-8", ustring) - end - end - ustring - end - - def wide_to_utf8(wstring) - # ensure it is actually UTF-16LE - # Ruby likes to mark binary data as ASCII-8BIT - wstring = wstring.force_encoding('UTF-16LE') if wstring.respond_to?(:force_encoding) - - # encode it all as UTF-8 - wstring = begin - if wstring.respond_to?(:encode) - wstring.encode('UTF-8') - else - require 'iconv' - Iconv.conv("UTF-8", "UTF-16LE", wstring) - end - end - # remove trailing CRLF and NULL characters - wstring.strip! - wstring - end - end end end diff --git a/lib/chef/win32/crypto.rb b/lib/chef/win32/crypto.rb index 79cf51b002..aa20c2dfd4 100644 --- a/lib/chef/win32/crypto.rb +++ b/lib/chef/win32/crypto.rb @@ -19,6 +19,7 @@ require 'chef/win32/error'
require 'chef/win32/api/memory'
require 'chef/win32/api/crypto'
+require 'chef/win32/unicode'
require 'digest'
class Chef
diff --git a/lib/chef/win32/file.rb b/lib/chef/win32/file.rb index e6640caa3c..700ddb24d3 100644 --- a/lib/chef/win32/file.rb +++ b/lib/chef/win32/file.rb @@ -17,9 +17,11 @@ # limitations under the License. # +require 'chef/mixin/wide_string' require 'chef/win32/api/file' require 'chef/win32/api/security' require 'chef/win32/error' +require 'chef/win32/unicode' class Chef module ReservedNames::Win32 @@ -27,6 +29,9 @@ class Chef include Chef::ReservedNames::Win32::API::File extend Chef::ReservedNames::Win32::API::File + include Chef::Mixin::WideString + extend Chef::Mixin::WideString + # Creates a symbolic link called +new_name+ for the file or directory # +old_name+. # @@ -157,9 +162,9 @@ class Chef def self.file_access_check(path, desired_access) security_descriptor = Chef::ReservedNames::Win32::Security.get_file_security(path) - token_rights = Chef::ReservedNames::Win32::Security::TOKEN_IMPERSONATE | + token_rights = Chef::ReservedNames::Win32::Security::TOKEN_IMPERSONATE | Chef::ReservedNames::Win32::Security::TOKEN_QUERY | - Chef::ReservedNames::Win32::Security::TOKEN_DUPLICATE | + Chef::ReservedNames::Win32::Security::TOKEN_DUPLICATE | Chef::ReservedNames::Win32::Security::STANDARD_RIGHTS_READ token = Chef::ReservedNames::Win32::Security.open_process_token( Chef::ReservedNames::Win32::Process.get_current_process, @@ -172,10 +177,30 @@ class Chef mapping[:GenericExecute] = Chef::ReservedNames::Win32::Security::FILE_GENERIC_EXECUTE mapping[:GenericAll] = Chef::ReservedNames::Win32::Security::FILE_ALL_ACCESS - Chef::ReservedNames::Win32::Security.access_check(security_descriptor, duplicate_token, + Chef::ReservedNames::Win32::Security.access_check(security_descriptor, duplicate_token, desired_access, mapping) end + def self.delete_volume_mount_point(mount_point) + unless DeleteVolumeMountPointW(wstring(mount_point)) + Chef::ReservedNames::Win32::Error.raise! + end + end + + def self.set_volume_mount_point(mount_point, name) + unless SetVolumeMountPointW(wstring(mount_point), wstring(name)) + Chef::ReservedNames::Win32::Error.raise! + end + end + + def self.get_volume_name_for_volume_mount_point(mount_point) + buffer = FFI::MemoryPointer.new(2, 128) + unless GetVolumeNameForVolumeMountPointW(wstring(mount_point), buffer, buffer.size/buffer.type_size) + Chef::ReservedNames::Win32::Error.raise! + end + buffer.read_wstring + end + # ::File compat class << self alias :stat :info diff --git a/lib/chef/win32/mutex.rb b/lib/chef/win32/mutex.rb index 0b7d99f111..f4755e9019 100644 --- a/lib/chef/win32/mutex.rb +++ b/lib/chef/win32/mutex.rb @@ -17,6 +17,7 @@ # require 'chef/win32/api/synchronization' +require 'chef/win32/unicode' class Chef module ReservedNames::Win32 @@ -113,5 +114,3 @@ if the mutex is attempted to be acquired by other threads.") end end end - - diff --git a/lib/chef/win32/net.rb b/lib/chef/win32/net.rb index 1349091eb9..59f29c4d1b 100644 --- a/lib/chef/win32/net.rb +++ b/lib/chef/win32/net.rb @@ -18,11 +18,11 @@ require 'chef/win32/api/net' require 'chef/win32/error' -require 'chef/mixin/wstring' +require 'chef/mixin/wide_string' class Chef module ReservedNames::Win32 - class NetUser + class Net include Chef::ReservedNames::Win32::API::Error extend Chef::ReservedNames::Win32::API::Error @@ -91,19 +91,72 @@ 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 NERR_GroupNotFound + "The group name could not be found." 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::Win32NetAPIError.new(msg, code) + end + + def self.net_local_group_add(server_name, group_name) + server_name = wstring(server_name) + group_name = wstring(group_name) + + buf = LOCALGROUP_INFO_0.new + buf[:lgrpi0_name] = FFI::MemoryPointer.from_string(group_name) + + rc = NetLocalGroupAdd(server_name, 0, buf, nil) + if rc != NERR_Success + net_api_error!(rc) + end + end + + def self.net_local_group_del(server_name, group_name) + server_name = wstring(server_name) + group_name = wstring(group_name) - raise Chef::Exceptions::Win32APIError, msg + "\n" + formatted_message + rc = NetLocalGroupDel(server_name, group_name) + if rc != NERR_Success + net_api_error!(rc) + end + end + + def self.net_local_group_get_members(server_name, group_name) + server_name = wstring(server_name) + group_name = wstring(group_name) + + buf = FFI::MemoryPointer.new(:pointer) + entries_read_ptr = FFI::MemoryPointer.new(:long) + total_read_ptr = FFI::MemoryPointer.new(:long) + resume_handle_ptr = FFI::MemoryPointer.new(:pointer) + + rc = ERROR_MORE_DATA + group_members = [] + while rc == ERROR_MORE_DATA + rc = NetLocalGroupGetMembers( + server_name, group_name, 0, buf, -1, + entries_read_ptr, total_read_ptr, resume_handle_ptr + ) + + nread = entries_read_ptr.read_long + nread.times do |i| + member = LOCALGROUP_MEMBERS_INFO_0.new(buf.read_pointer + + (i * LOCALGROUP_MEMBERS_INFO_0.size)) + member_sid = Chef::ReservedNames::Win32::Security::SID.new(member[:lgrmi0_sid]) + group_members << member_sid.to_s + end + NetApiBufferFree(buf.read_pointer) + end + + if rc != NERR_Success + net_api_error!(rc) + end + + group_members end def self.net_user_add_l3(server_name, args) @@ -185,6 +238,107 @@ END end end + def self.members_to_lgrmi3(members) + buf = FFI::MemoryPointer.new(LOCALGROUP_MEMBERS_INFO_3, members.size) + members.size.times.collect do |i| + member_info = LOCALGROUP_MEMBERS_INFO_3.new( + buf + i * LOCALGROUP_MEMBERS_INFO_3.size) + member_info[:lgrmi3_domainandname] = FFI::MemoryPointer.from_string(wstring(members[i])) + member_info + end + end + + def self.net_local_group_add_members(server_name, group_name, members) + server_name = wstring(server_name) + group_name = wstring(group_name) + + lgrmi3s = members_to_lgrmi3(members) + rc = NetLocalGroupAddMembers( + server_name, group_name, 3, lgrmi3s[0], members.size) + + if rc != NERR_Success + net_api_error!(rc) + end + end + + def self.net_local_group_set_members(server_name, group_name, members) + server_name = wstring(server_name) + group_name = wstring(group_name) + + lgrmi3s = members_to_lgrmi3(members) + rc = NetLocalGroupSetMembers( + server_name, group_name, 3, lgrmi3s[0], members.size) + + if rc != NERR_Success + net_api_error!(rc) + end + end + + def self.net_local_group_del_members(server_name, group_name, members) + server_name = wstring(server_name) + group_name = wstring(group_name) + + lgrmi3s = members_to_lgrmi3(members) + rc = NetLocalGroupDelMembers( + server_name, group_name, 3, lgrmi3s[0], members.size) + + if rc != NERR_Success + net_api_error!(rc) + end + end + + def self.net_use_del(server_name, use_name, force=:use_noforce) + server_name = wstring(server_name) + use_name = wstring(use_name) + force_const = case force + when :use_noforce + USE_NOFORCE + when :use_force + USE_FORCE + when :use_lots_of_force + USE_LOTS_OF_FORCE + else + raise ArgumentError, "force must be one of [:use_noforce, :use_force, or :use_lots_of_force]" + end + + rc = NetUseDel(server_name, use_name, force_const) + if rc != NERR_Success + net_api_error!(rc) + end + end + + def self.net_use_get_info_l2(server_name, use_name) + server_name = wstring(server_name) + use_name = wstring(use_name) + ui2_p = FFI::MemoryPointer.new(:pointer) + + rc = NetUseGetInfo(server_name, use_name, 2, ui2_p) + if rc != NERR_Success + net_api_error!(rc) + end + + ui2 = USE_INFO_2.new(ui2_p.read_pointer).as_ruby + NetApiBufferFree(ui2_p.read_pointer) + + ui2 + end + + def self.net_use_add_l2(server_name, ui2_hash) + server_name = wstring(server_name) + group_name = wstring(group_name) + + buf = USE_INFO_2.new + + ui2_hash.each do |(k,v)| + buf.set(k,v) + end + + rc = NetUseAdd(server_name, 2, buf, nil) + if rc != NERR_Success + net_api_error!(rc) + end + end end + NetUser = Net # For backwards compatibility end end diff --git a/lib/chef/win32/process.rb b/lib/chef/win32/process.rb index 2df39bb918..767d4f390c 100644 --- a/lib/chef/win32/process.rb +++ b/lib/chef/win32/process.rb @@ -69,6 +69,19 @@ class Chef result end + def self.is_wow64_process + is_64_bit_process_result = FFI::MemoryPointer.new(:int) + + # The return value of IsWow64Process is nonzero value if the API call succeeds. + # The result data are returned in the last parameter, not the return value. + call_succeeded = IsWow64Process(GetCurrentProcess(), is_64_bit_process_result) + + # The result is nonzero if IsWow64Process's calling process, in the case here + # this process, is running under WOW64, i.e. the result is nonzero if this + # process is 32-bit (aka :i386). + (call_succeeded != 0) && (is_64_bit_process_result.get_int(0) != 0) + end + # Must have PROCESS_QUERY_INFORMATION or PROCESS_QUERY_LIMITED_INFORMATION rights, # AND the PROCESS_VM_READ right def self.get_process_memory_info(handle) diff --git a/lib/chef/win32/registry.rb b/lib/chef/win32/registry.rb index 18f12d26b8..b25ce7937e 100644 --- a/lib/chef/win32/registry.rb +++ b/lib/chef/win32/registry.rb @@ -17,8 +17,11 @@ # limitations under the License. # require 'chef/reserved_names' +require 'chef/win32/api' +require 'chef/mixin/wide_string' if RUBY_PLATFORM =~ /mswin|mingw32|windows/ + require 'chef/win32/api/registry' require 'win32/registry' require 'win32/api' end @@ -27,6 +30,14 @@ class Chef class Win32 class Registry + if RUBY_PLATFORM =~ /mswin|mingw32|windows/ + include Chef::ReservedNames::Win32::API::Registry + extend Chef::ReservedNames::Win32::API::Registry + end + + include Chef::Mixin::WideString + extend Chef::Mixin::WideString + attr_accessor :run_context attr_accessor :architecture @@ -142,9 +153,8 @@ class Chef #Using the 'RegDeleteKeyEx' Windows API that correctly supports WOW64 systems (Win2003) #instead of the 'RegDeleteKey' def delete_key_ex(hive, key) - regDeleteKeyEx = ::Win32::API.new('RegDeleteKeyEx', 'LPLL', 'L', 'advapi32') hive_num = hive.hkey - (1 << 32) - regDeleteKeyEx.call(hive_num, key, ::Win32::Registry::KEY_WRITE | registry_system_architecture, 0) + RegDeleteKeyExW(hive_num, wstring(key), ::Win32::Registry::KEY_WRITE | registry_system_architecture, 0) == 0 end def key_exists?(key_path) @@ -203,7 +213,7 @@ class Chef key_exists!(key_path) hive, key = get_hive_and_key(key_path) hive.open(key, ::Win32::Registry::KEY_READ | registry_system_architecture) do |reg| - return true if reg.any? {|val| val == value[:name] } + return true if reg.any? {|val| safely_downcase(val) == safely_downcase(value[:name]) } end return false end @@ -213,7 +223,7 @@ class Chef hive, key = get_hive_and_key(key_path) hive.open(key, ::Win32::Registry::KEY_READ | registry_system_architecture) do |reg| reg.each do |val_name, val_type, val_data| - if val_name == value[:name] && + if safely_downcase(val_name) == safely_downcase(value[:name]) && val_type == get_type_from_name(value[:type]) && val_data == value[:data] return true @@ -289,6 +299,14 @@ class Chef private + + def safely_downcase(val) + if val.is_a? String + return val.downcase + end + return val + end + def node run_context && run_context.node end diff --git a/lib/chef/win32/security.rb b/lib/chef/win32/security.rb index 5c83180bc0..bc80517d80 100644 --- a/lib/chef/win32/security.rb +++ b/lib/chef/win32/security.rb @@ -22,7 +22,7 @@ require 'chef/win32/memory' require 'chef/win32/process' require 'chef/win32/unicode' require 'chef/win32/security/token' -require 'chef/mixin/wstring' +require 'chef/mixin/wide_string' class Chef module ReservedNames::Win32 diff --git a/lib/chef/win32/security/token.rb b/lib/chef/win32/security/token.rb index 9e494a73b9..8d4e54ad8c 100644 --- a/lib/chef/win32/security/token.rb +++ b/lib/chef/win32/security/token.rb @@ -18,7 +18,7 @@ require 'chef/win32/security' require 'chef/win32/api/security' - +require 'chef/win32/unicode' require 'ffi' class Chef diff --git a/lib/chef/win32/system.rb b/lib/chef/win32/system.rb new file mode 100755 index 0000000000..cdd063f174 --- /dev/null +++ b/lib/chef/win32/system.rb @@ -0,0 +1,62 @@ +# +# Author:: Salim Alam (<salam@chef.io>) +# Copyright:: Copyright 2015 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/win32/api/system' +require 'chef/win32/error' +require 'ffi' + +class Chef + module ReservedNames::Win32 + class System + include Chef::ReservedNames::Win32::API::System + extend Chef::ReservedNames::Win32::API::System + + def self.get_system_wow64_directory + ptr = FFI::MemoryPointer.new(:char, 255, true) + succeeded = GetSystemWow64DirectoryA(ptr, 255) + + if succeeded == 0 + raise Win32APIError, "Failed to get Wow64 system directory" + end + + ptr.read_string.strip + end + + def self.wow64_disable_wow64_fs_redirection + original_redirection_state = FFI::MemoryPointer.new(:pointer) + + succeeded = Wow64DisableWow64FsRedirection(original_redirection_state) + + if succeeded == 0 + raise Win32APIError, "Failed to disable Wow64 file redirection" + end + + original_redirection_state + end + + def self.wow64_revert_wow64_fs_redirection(original_redirection_state) + succeeded = Wow64RevertWow64FsRedirection(original_redirection_state) + + if succeeded == 0 + raise Win32APIError, "Failed to revert Wow64 file redirection" + end + end + + end + end +end diff --git a/lib/chef/win32/unicode.rb b/lib/chef/win32/unicode.rb index e7399d5255..562301a040 100644 --- a/lib/chef/win32/unicode.rb +++ b/lib/chef/win32/unicode.rb @@ -17,6 +17,7 @@ # limitations under the License. # +require 'chef/mixin/wide_string' require 'chef/win32/api/unicode' class Chef @@ -30,6 +31,8 @@ end module FFI class Pointer + include Chef::Mixin::WideString + def read_wstring(num_wchars = nil) if num_wchars.nil? # Find the length of the string @@ -43,13 +46,42 @@ module FFI num_wchars = length end - Chef::ReservedNames::Win32::Unicode.wide_to_utf8(self.get_bytes(0, num_wchars*2)) + wide_to_utf8(self.get_bytes(0, num_wchars*2)) end end end class String + include Chef::Mixin::WideString + def to_wstring - Chef::ReservedNames::Win32::Unicode.utf8_to_wide(self) + utf8_to_wide(self) end end + +# https://bugs.ruby-lang.org/issues/11439 +if RUBY_VERSION =~ /^2\.1/ + module Win32 + class Registry + def write(name, type, data) + case type + when REG_SZ, REG_EXPAND_SZ + data = data.to_s.encode(WCHAR) + WCHAR_NUL + when REG_MULTI_SZ + data = data.to_a.map {|s| s.encode(WCHAR)}.join(WCHAR_NUL) << WCHAR_NUL << WCHAR_NUL + when REG_BINARY + data = data.to_s + when REG_DWORD + data = API.packdw(data.to_i) + when REG_DWORD_BIG_ENDIAN + data = [data.to_i].pack('N') + when REG_QWORD + data = API.packqw(data.to_i) + else + raise TypeError, "Unsupported type #{type}" + end + API.SetValue(@hkey, name, type, data, data.bytesize) + end + end + end +end
\ No newline at end of file diff --git a/lib/chef/win32/version.rb b/lib/chef/win32/version.rb index 17c27e4780..6a7a65b01b 100644 --- a/lib/chef/win32/version.rb +++ b/lib/chef/win32/version.rb @@ -122,10 +122,6 @@ class Chef # WMI always returns the truth. See article at # http://msdn.microsoft.com/en-us/library/windows/desktop/ms724439(v=vs.85).aspx - # CHEF-4888: Work around ruby #2618, expected to be fixed in Ruby 2.1.0 - # https://github.com/ruby/ruby/commit/588504b20f5cc880ad51827b93e571e32446e5db - # https://github.com/ruby/ruby/commit/27ed294c7134c0de582007af3c915a635a6506cd - wmi = WmiLite::Wmi.new os_info = wmi.first_of('Win32_OperatingSystem') os_version = os_info['version'] diff --git a/lib/chef/workstation_config_loader.rb b/lib/chef/workstation_config_loader.rb index 2454c9cccf..8398c5d616 100644 --- a/lib/chef/workstation_config_loader.rb +++ b/lib/chef/workstation_config_loader.rb @@ -1,5 +1,5 @@ # -# Author:: Daniel DeLeo (<dan@getchef.com>) +# Author:: Claire McQuin (<claire@chef.io>) # Copyright:: Copyright (c) 2014 Chef Software, Inc. # License:: Apache License, Version 2.0 # @@ -16,163 +16,8 @@ # limitations under the License. # -require 'chef/config_fetcher' -require 'chef/config' -require 'chef/null_logger' -require 'chef/util/path_helper' +require 'chef-config/workstation_config_loader' class Chef - - class WorkstationConfigLoader - - # Path to a config file requested by user, (e.g., via command line option). Can be nil - attr_accessor :explicit_config_file - - # TODO: initialize this with a logger for Chef and Knife - def initialize(explicit_config_file, logger=nil) - @explicit_config_file = explicit_config_file - @config_location = nil - @logger = logger || NullLogger.new - end - - def no_config_found? - config_location.nil? - end - - def config_location - @config_location ||= (explicit_config_file || locate_local_config) - end - - def chef_config_dir - if @chef_config_dir.nil? - @chef_config_dir = false - full_path = working_directory.split(File::SEPARATOR) - (full_path.length - 1).downto(0) do |i| - candidate_directory = File.join(full_path[0..i] + [".chef" ]) - if File.exist?(candidate_directory) && File.directory?(candidate_directory) - @chef_config_dir = candidate_directory - break - end - end - end - @chef_config_dir - end - - def load - # Ignore it if there's no explicit_config_file and can't find one at a - # default path. - return false if config_location.nil? - - if explicit_config_file && !path_exists?(config_location) - raise Exceptions::ConfigurationError, "Specified config file #{config_location} does not exist" - end - - # Have to set Chef::Config.config_file b/c other config is derived from it. - Chef::Config.config_file = config_location - read_config(IO.read(config_location), config_location) - end - - # (Private API, public for test purposes) - def env - ENV - end - - # (Private API, public for test purposes) - def path_exists?(path) - Pathname.new(path).expand_path.exist? - end - - private - - def have_config?(path) - if path_exists?(path) - logger.info("Using config at #{path}") - true - else - logger.debug("Config not found at #{path}, trying next option") - false - end - end - - def locate_local_config - candidate_configs = [] - - # Look for $KNIFE_HOME/knife.rb (allow multiple knives config on same machine) - if env['KNIFE_HOME'] - candidate_configs << File.join(env['KNIFE_HOME'], 'config.rb') - candidate_configs << File.join(env['KNIFE_HOME'], 'knife.rb') - end - # Look for $PWD/knife.rb - if Dir.pwd - candidate_configs << File.join(Dir.pwd, 'config.rb') - candidate_configs << File.join(Dir.pwd, 'knife.rb') - end - # Look for $UPWARD/.chef/knife.rb - if chef_config_dir - candidate_configs << File.join(chef_config_dir, 'config.rb') - candidate_configs << File.join(chef_config_dir, 'knife.rb') - end - # Look for $HOME/.chef/knife.rb - Chef::Util::PathHelper.home('.chef') do |dot_chef_dir| - candidate_configs << File.join(dot_chef_dir, 'config.rb') - candidate_configs << File.join(dot_chef_dir, 'knife.rb') - end - - candidate_configs.find do | candidate_config | - have_config?(candidate_config) - end - end - - def working_directory - a = if Chef::Platform.windows? - env['CD'] - else - env['PWD'] - end || Dir.pwd - - a - end - - def read_config(config_content, config_file_path) - Chef::Config.from_string(config_content, config_file_path) - rescue SignalException - raise - rescue SyntaxError => e - message = "" - message << "You have invalid ruby syntax in your config file #{config_file_path}\n\n" - message << "#{e.class.name}: #{e.message}\n" - if file_line = e.message[/#{Regexp.escape(config_file_path)}:[\d]+/] - line = file_line[/:([\d]+)$/, 1].to_i - message << highlight_config_error(config_file_path, line) - end - raise Exceptions::ConfigurationError, message - rescue Exception => e - message = "You have an error in your config file #{config_file_path}\n\n" - message << "#{e.class.name}: #{e.message}\n" - filtered_trace = e.backtrace.grep(/#{Regexp.escape(config_file_path)}/) - filtered_trace.each {|bt_line| message << " " << bt_line << "\n" } - if !filtered_trace.empty? - line_nr = filtered_trace.first[/#{Regexp.escape(config_file_path)}:([\d]+)/, 1] - message << highlight_config_error(config_file_path, line_nr.to_i) - end - raise Exceptions::ConfigurationError, message - end - - - def highlight_config_error(file, line) - config_file_lines = [] - IO.readlines(file).each_with_index {|l, i| config_file_lines << "#{(i + 1).to_s.rjust(3)}: #{l.chomp}"} - if line == 1 - lines = config_file_lines[0..3] - else - lines = config_file_lines[Range.new(line - 2, line)] - end - "Relevant file content:\n" + lines.join("\n") + "\n" - end - - def logger - @logger - end - - end + WorkstationConfigLoader = ChefConfig::WorkstationConfigLoader end |