diff options
author | Noah Kantrowitz <noah@coderanger.net> | 2015-05-29 18:03:37 -0700 |
---|---|---|
committer | Noah Kantrowitz <noah@coderanger.net> | 2015-05-29 18:03:37 -0700 |
commit | 0bf9fda2b7224db4005aeda9cf2efb00ba6fb51b (patch) | |
tree | 8a20227ac9cacb776af3eb14daaa0080f629271a | |
parent | dec356f59e9aaa37e30aee49b0e23ce1e3f41a12 (diff) | |
parent | bcb812deffae37b15513bfcff448b95c7be4b265 (diff) | |
download | chef-0bf9fda2b7224db4005aeda9cf2efb00ba6fb51b.tar.gz |
Merge branch 'master' into nameless
66 files changed, 2045 insertions, 987 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index 6e94990631..29abb09ca5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,23 @@ ## Unreleased +* [**Yukihiko SAWANOBORI**](https://github.com/sawanoboly): Pass name by + knife cil attribute [pr#3195](https://github.com/chef/chef/pull/3195) +* [**Torben Knerr**](https://github.com/tknerr): + Allow knife sub-command loader to match platform specific gems. [pr#3281](https://github.com/chef/chef/pull/3281) + +* [Issue #2247](https://github.com/chef/chef/issues/2247): powershell_script returns 0 for scripts with syntax errors +* [pr#3080](https://github.com/chef/chef/pull/3080): Issue 2247: powershell_script exit status should be nonzero for syntax errors +* [pr#3441](https://github.com/chef/chef/pull/3441): Add powershell_out mixin to core chef +* [pr#3448](https://github.com/chef/chef/pull/3448): Fix dsc_resource to work with wmf5 april preview +* [pr#3392](https://github.com/chef/chef/pull/3392): Comment up Chef::Client and privatize/deprecate unused things +* [pr#3419](https://github.com/chef/chef/pull/3419): Fix cli issue with chef_repo_path when ENV variable is unset +* [pr#3358](https://github.com/chef/chef/pull/3358): Separate audit and converge failures +* [pr#3431](https://github.com/chef/chef/pull/3431): Fix backups on windows for the file resource +* [pr#3397](https://github.com/chef/chef/pull/3397): Validate owner exists in directory resources +* [pr#3418](https://github.com/chef/chef/pull/3418): Add `shell_out` mixin to Chef::Resource class for use in `not_if`/`only_if` conditionals, etc. +* [pr#3406](https://github.com/chef/chef/pull/3406): Add wide-char 'Environment' to `broadcast_env_change` mixin for setting windows environment variables + +## 12.4.0 + * [**Phil Dibowitz**](https://github.com/jaymzh): Fix multipackage and architectures * [**Igor Shpakov**](https://github.com/Igorshp): @@ -28,24 +47,24 @@ * Add an integration test of chef-client with empty ENV. #3321 * Switch over Windows builds to universal builds. #3278 * Convert bootstrap template to use sh #2877 -* [Issue #3316](https://github.com/chef/chef/issues/3316) Fix idempotency issues with the `windows_package` resource +* [Issue #3316](https://github.com/chef/chef/issues/3316): Fix idempotency issues with the `windows_package` resource * [pr#3295](https://github.com/chef/chef/pull/3295): Stop mutating `new_resource.checksum` in file providers. Fixes some ChecksumMismatch exceptions like [issue#3168](https://github.com/chef/chef/issues/3168) * [pr#3320] Sanitize non-UTF8 characters in the node data before doing node.save(). Works around many UTF8 exception issues reported on node.save(). * Implemented X-Ops-Server-API-Version with a API version of 0, as well as error handling when the Chef server does not support the API version that the client supports. * [pr#3327](https://github.com/chef/chef/pull/3327): Fix unreliable AIX service group parsing mechanism. * [pr#3333](https://github.com/chef/chef/pull/3333): Fix SSL errors when connecting to private Supermarkets * [pr#3340](https://github.com/chef/chef/pull/3340): Allow Event dispatch subscribers to be inspected. -* [Issue #3055](https://github.com/chef/chef/issues/3055) Fix regex parsing for recipe failures on Windows -* [pr#3345](https://github.com/chef/chef/pull/3345) Windows Event log logger -* [pr#3336](https://github.com/chef/chef/pull/3336) Remote file understands UNC paths +* [Issue #3055](https://github.com/chef/chef/issues/3055): Fix regex parsing for recipe failures on Windows +* [pr#3345](https://github.com/chef/chef/pull/3345): Windows Event log logger +* [pr#3336](https://github.com/chef/chef/pull/3336): Remote file understands UNC paths * [pr#3269](https://github.com/chef/chef/pull/3269): Deprecate automatic recipe DSL for classes in `Chef::Resource` * [pr#3360](https://github.com/chef/chef/pull/3360): Add check_resource_semantics! lifecycle method to provider * [pr#3344](https://github.com/chef/chef/pull/3344): Rewrite Windows user resouce code to use ffi instead of win32-api -* [pr#3318](https://github.com/chef/chef/pull/3318) Modify Windows package provider to allow for url source -* [pr#3381](https://github.com/chef/chef/pull/3381) warn on cookbook self-deps +* [pr#3318](https://github.com/chef/chef/pull/3318): Modify Windows package provider to allow for url source +* [pr#3381](https://github.com/chef/chef/pull/3381): warn on cookbook self-deps * [pr#2312](https://github.com/chef/chef/pull/2312): fix `node[:recipes]` duplication, add `node[:cookbooks]` and `node[:expanded_run_list]` -* [pr#3325](https://github.com/chef/chef/pull/3325) enforce passing a node name with validatorless bootstrapping -* [pr#3398](https://github.com/chef/chef/pull/3398) Allow spaces in files for the `remote_file` resource +* [pr#3325](https://github.com/chef/chef/pull/3325): enforce passing a node name with validatorless bootstrapping +* [pr#3398](https://github.com/chef/chef/pull/3398): Allow spaces in files for the `remote_file` resource ## 12.3.0 @@ -108,9 +108,19 @@ Dir[File.expand_path("../*gemspec", __FILE__)].reverse.each do |gemspec_path| Gem::PackageTask.new(gemspec).define end +def with_clean_env(&block) + if defined?(Bundler) + Bundler.with_clean_env(&block) + else + block.call + end +end + desc "Build and install a chef gem" task :install => [:package] do - sh %{gem install pkg/#{GEM_NAME}-#{VERSION}.gem --no-rdoc --no-ri} + with_clean_env do + sh %{gem install pkg/#{GEM_NAME}-#{VERSION}.gem --no-rdoc --no-ri} + end end task :uninstall do @@ -1 +1 @@ -12.4.0.dev.0 +12.4.0.rc.0 diff --git a/chef-config/Rakefile b/chef-config/Rakefile index 6eb195f672..10b6010de3 100644 --- a/chef-config/Rakefile +++ b/chef-config/Rakefile @@ -8,9 +8,19 @@ Dir[File.expand_path("../*gemspec", __FILE__)].reverse.each do |gemspec_path| Gem::PackageTask.new(gemspec).define end +def with_clean_env(&block) + if defined?(Bundler) + Bundler.with_clean_env(&block) + else + block.call + end +end + desc "Build and install a chef-config gem" task :install => [:package] do - sh %{gem install pkg/chef-config-#{ChefConfig::VERSION}.gem --no-rdoc --no-ri} + with_clean_env do + sh(%{gem install pkg/chef-config-#{ChefConfig::VERSION}.gem --no-rdoc --no-ri}, verbose: true) + end end task :default => :spec diff --git a/chef-config/lib/chef-config/version.rb b/chef-config/lib/chef-config/version.rb index 2371f31d98..42f2b7b991 100644 --- a/chef-config/lib/chef-config/version.rb +++ b/chef-config/lib/chef-config/version.rb @@ -20,6 +20,6 @@ #!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! module ChefConfig - VERSION = '12.4.0.dev.0' + VERSION = '12.4.0.rc.0' end diff --git a/lib/chef/application/client.rb b/lib/chef/application/client.rb index a5faee9d35..551e26e303 100644 --- a/lib/chef/application/client.rb +++ b/lib/chef/application/client.rb @@ -279,6 +279,12 @@ class Chef::Application::Client < Chef::Application Chef::Config[:chef_server_url] = config[:chef_server_url] if config.has_key? :chef_server_url Chef::Config.local_mode = config[:local_mode] if config.has_key?(:local_mode) + + if Chef::Config.has_key?(:chef_repo_path) && Chef::Config.chef_repo_path.nil? + Chef::Config.delete(:chef_repo_path) + Chef::Log.warn "chef_repo_path was set in a config file but was empty. Assuming #{Chef::Config.chef_repo_path}" + end + if Chef::Config.local_mode && !Chef::Config.has_key?(:cookbook_path) && !Chef::Config.has_key?(:chef_repo_path) Chef::Config.chef_repo_path = Chef::Config.find_chef_repo_path(Dir.pwd) end diff --git a/lib/chef/audit/audit_reporter.rb b/lib/chef/audit/audit_reporter.rb index a4f84ed7eb..b74bf07b8b 100644 --- a/lib/chef/audit/audit_reporter.rb +++ b/lib/chef/audit/audit_reporter.rb @@ -34,6 +34,7 @@ class Chef @rest_client = rest_client # Ruby 1.9.3 and above "enumerate their values in the order that the corresponding keys were inserted." @ordered_control_groups = Hash.new + @audit_phase_error = nil end def run_context @@ -59,6 +60,7 @@ class Chef # known control groups. def audit_phase_failed(error) # The stacktrace information has already been logged elsewhere + @audit_phase_error = error Chef::Log.debug("Audit Reporter failed.") ordered_control_groups.each do |name, control_group| audit_data.add_control_group(control_group) @@ -70,7 +72,9 @@ class Chef end def run_failed(error) - post_auditing_data(error) + # Audit phase errors are captured when audit_phase_failed gets called. + # The error passed here isn't relevant to auditing, so we ignore it. + post_auditing_data end def control_group_started(name) @@ -98,7 +102,7 @@ class Chef private - def post_auditing_data(error = nil) + def post_auditing_data unless auditing_enabled? Chef::Log.debug("Audit Reports are disabled. Skipping sending reports.") return @@ -116,8 +120,10 @@ class Chef Chef::Log.debug("Sending audit report (run-id: #{audit_data.run_id})") run_data = audit_data.to_hash - if error - run_data[:error] = "#{error.class.to_s}: #{error.message}\n#{error.backtrace.join("\n")}" + if @audit_phase_error + error_info = "#{@audit_phase_error.class}: #{@audit_phase_error.message}" + error_info << "\n#{@audit_phase_error.backtrace.join("\n")}" if @audit_phase_error.backtrace + run_data[:error] = error_info end Chef::Log.debug "Audit Report:\n#{Chef::JSONCompat.to_json_pretty(run_data)}" @@ -163,7 +169,6 @@ class Chef def iso8601ify(time) time.utc.iso8601.to_s end - end end end diff --git a/lib/chef/client.rb b/lib/chef/client.rb index 0764d3f3ba..51e78e60a9 100644 --- a/lib/chef/client.rb +++ b/lib/chef/client.rb @@ -50,6 +50,7 @@ require 'chef/run_lock' require 'chef/policy_builder' require 'chef/request_id' require 'chef/platform/rebooter' +require 'chef/mixin/deprecation' require 'ohai' require 'rbconfig' @@ -60,108 +61,100 @@ class Chef class Client include Chef::Mixin::PathSanity - # IO stream that will be used as 'STDOUT' for formatters. Formatters are - # configured during `initialize`, so this provides a convenience for - # setting alternative IO stream during tests. - STDOUT_FD = STDOUT - - # IO stream that will be used as 'STDERR' for formatters. Formatters are - # configured during `initialize`, so this provides a convenience for - # setting alternative IO stream during tests. - STDERR_FD = STDERR + extend Chef::Mixin::Deprecation - # Clears all notifications for client run status events. - # Primarily for testing purposes. - def self.clear_notifications - @run_start_notifications = nil - @run_completed_successfully_notifications = nil - @run_failed_notifications = nil - end - - # The list of notifications to be run when the client run starts. - def self.run_start_notifications - @run_start_notifications ||= [] - end - - # The list of notifications to be run when the client run completes - # successfully. - def self.run_completed_successfully_notifications - @run_completed_successfully_notifications ||= [] - end - - # The list of notifications to be run when the client run fails. - def self.run_failed_notifications - @run_failed_notifications ||= [] - end - - # Add a notification for the 'client run started' event. The notification - # is provided as a block. The current Chef::RunStatus object will be passed - # to the notification_block when the event is triggered. - def self.when_run_starts(¬ification_block) - run_start_notifications << notification_block - end - - # Add a notification for the 'client run success' event. The notification - # is provided as a block. The current Chef::RunStatus object will be passed - # to the notification_block when the event is triggered. - def self.when_run_completes_successfully(¬ification_block) - run_completed_successfully_notifications << notification_block - end + # + # The status of the Chef run. + # + # @return [Chef::RunStatus] + # + attr_reader :run_status - # Add a notification for the 'client run failed' event. The notification - # is provided as a block. The current Chef::RunStatus is passed to the - # notification_block when the event is triggered. - def self.when_run_fails(¬ification_block) - run_failed_notifications << notification_block + # + # The node represented by this client. + # + # @return [Chef::Node] + # + def node + run_status.node end - - # Callback to fire notifications that the Chef run is starting - def run_started - self.class.run_start_notifications.each do |notification| - notification.call(run_status) - end - @events.run_started(run_status) + def node=(value) + run_status.node = value end - # Callback to fire notifications that the run completed successfully - def run_completed_successfully - success_handlers = self.class.run_completed_successfully_notifications - success_handlers.each do |notification| - notification.call(run_status) - end - end + # + # The ohai system used by this client. + # + # @return [Ohai::System] + # + attr_reader :ohai - # Callback to fire notifications that the Chef run failed - def run_failed - failure_handlers = self.class.run_failed_notifications - failure_handlers.each do |notification| - notification.call(run_status) - end - end + # + # The rest object used to communicate with the Chef server. + # + # @return [Chef::REST] + # + attr_reader :rest - attr_accessor :node - attr_accessor :ohai - attr_accessor :rest + # + # The runner used to converge. + # + # @return [Chef::Runner] + # attr_accessor :runner + # + # Extra node attributes that were applied to the node. + # + # @return [Hash] + # attr_reader :json_attribs - attr_reader :run_status + + # + # The event dispatcher for the Chef run, including any configured output + # formatters and event loggers. + # + # @return [EventDispatch::Dispatcher] + # + # @see Chef::Formatters + # @see Chef::Config#formatters + # @see Chef::Config#stdout + # @see Chef::Config#stderr + # @see Chef::Config#force_logger + # @see Chef::Config#force_formatter + # TODO add stdout, stderr, and default formatters to Chef::Config so the + # defaults aren't calculated here. Remove force_logger and force_formatter + # from this code. + # @see Chef::EventLoggers + # @see Chef::Config#disable_event_logger + # @see Chef::Config#event_loggers + # @see Chef::Config#event_handlers + # attr_reader :events + # # Creates a new Chef::Client. + # + # @param json_attribs [Hash] Node attributes to layer into the node when it is + # fetched. + # @param args [Hash] Options: + # @option args [Array<RunList::RunListItem>] :override_runlist A runlist to + # use instead of the node's embedded run list. + # @option args [Array<String>] :specific_recipes A list of recipe file paths + # to load after the run list has been loaded. + # def initialize(json_attribs=nil, args={}) @json_attribs = json_attribs || {} - @node = nil - @runner = nil @ohai = Ohai::System.new event_handlers = configure_formatters + configure_event_loggers event_handlers += Array(Chef::Config[:event_handlers]) @events = EventDispatch::Dispatcher.new(*event_handlers) + # TODO it seems like a bad idea to be deletin' other peoples' hashes. @override_runlist = args.delete(:override_runlist) @specific_recipes = args.delete(:specific_recipes) - @run_status = Chef::RunStatus.new(node, events) + @run_status = Chef::RunStatus.new(nil, events) if new_runlist = args.delete(:runlist) @json_attribs["run_list"] = new_runlist @@ -175,6 +168,173 @@ class Chef Chef.set_resource_priority_map(Chef::Platform::ResourcePriorityMap.instance) end + # + # Do a full run for this Chef::Client. + # + # Locks the run while doing its job. + # + # Fires run_start before doing anything and fires run_completed or + # run_failed when finished. Also notifies client listeners of run_started + # at the beginning of Compile, and run_completed_successfully or run_failed + # when all is complete. + # + # Phase 1: Setup + # -------------- + # Gets information about the system and the run we are doing. + # + # 1. Run ohai to collect system information. + # 2. Register / connect to the Chef server (unless in solo mode). + # 3. Retrieve the node (or create a new one). + # 4. Merge in json_attribs, Chef::Config.environment, and override_run_list. + # + # @see #run_ohai + # @see #load_node + # @see #build_node + # @see Chef::Config#lockfile + # @see Chef::RunLock#acquire + # + # Phase 2: Compile + # ---------------- + # Decides *what* we plan to converge by compiling recipes. + # + # 1. Sync required cookbooks to the local cache. + # 2. Load libraries from all cookbooks. + # 3. Load attributes from all cookbooks. + # 4. Load LWRPs from all cookbooks. + # 5. Load resource definitions from all cookbooks. + # 6. Load recipes in the run list. + # 7. Load recipes from the command line. + # + # @see #setup_run_context Syncs and compiles cookbooks. + # @see Chef::CookbookCompiler#compile + # + # Phase 3: Converge + # ----------------- + # Brings the system up to date. + # + # 1. Converge the resources built from recipes in Phase 2. + # 2. Save the node. + # 3. Reboot if we were asked to. + # + # @see #converge_and_save + # @see Chef::Runner + # + # Phase 4: Audit + # -------------- + # Runs 'control_group' audits in recipes. This entire section can be enabled or disabled with config. + # + # 1. 'control_group' DSL collects audits during Phase 2 + # 2. Audits are run using RSpec + # 3. Errors are collected and reported using the formatters + # + # @see #run_audits + # @see Chef::Audit::Runner#run + # + # @raise [Chef::Exceptions::RunFailedWrappingError] If converge or audit failed. + # + # @see Chef::Config#enforce_path_sanity + # @see Chef::Config#solo + # @see Chef::Config#audit_mode + # + # @return Always returns true. + # + def run + run_error = nil + + runlock = RunLock.new(Chef::Config.lockfile) + # TODO feels like acquire should have its own block arg for this + runlock.acquire + # don't add code that may fail before entering this section to be sure to release lock + begin + runlock.save_pid + + request_id = Chef::RequestID.instance.request_id + run_context = nil + events.run_start(Chef::VERSION) + Chef::Log.info("*** Chef #{Chef::VERSION} ***") + Chef::Log.info "Chef-client pid: #{Process.pid}" + Chef::Log.debug("Chef-client request_id: #{request_id}") + enforce_path_sanity + run_ohai + + register unless Chef::Config[:solo] + + load_node + + build_node + + run_status.run_id = request_id + run_status.start_clock + Chef::Log.info("Starting Chef Run for #{node.name}") + run_started + + do_windows_admin_check + + run_context = setup_run_context + + if Chef::Config[:audit_mode] != :audit_only + converge_error = converge_and_save(run_context) + end + + if Chef::Config[:why_run] == true + # why_run should probably be renamed to why_converge + Chef::Log.debug("Not running controls in 'why_run' mode - this mode is used to see potential converge changes") + elsif Chef::Config[:audit_mode] != :disabled + audit_error = run_audits(run_context) + end + + # Raise converge_error so run_failed reporters/events are processed. + raise converge_error if converge_error + + run_status.stop_clock + Chef::Log.info("Chef Run complete in #{run_status.elapsed_time} seconds") + run_completed_successfully + events.run_completed(node) + + # rebooting has to be the last thing we do, no exceptions. + Chef::Platform::Rebooter.reboot_if_needed!(node) + rescue Exception => run_error + # CHEF-3336: Send the error first in case something goes wrong below and we don't know why + Chef::Log.debug("Re-raising exception: #{run_error.class} - #{run_error.message}\n#{run_error.backtrace.join("\n ")}") + # If we failed really early, we may not have a run_status yet. Too early for these to be of much use. + if run_status + run_status.stop_clock + run_status.exception = run_error + run_failed + end + events.run_failed(run_error) + ensure + Chef::RequestID.instance.reset_request_id + request_id = nil + @run_status = nil + run_context = nil + runlock.release + GC.start + end + + # Raise audit, converge, and other errors here so that we exit + # with the proper exit status code and everything gets raised + # as a RunFailedWrappingError + if run_error || converge_error || audit_error + error = if run_error == converge_error + Chef::Exceptions::RunFailedWrappingError.new(converge_error, audit_error) + else + Chef::Exceptions::RunFailedWrappingError.new(run_error, converge_error, audit_error) + end + error.fill_backtrace + Chef::Application.debug_stacktrace(error) + raise error + end + + true + end + + # + # Private API + # TODO make this stuff protected or private + # + + # @api private def configure_formatters formatters_for_run.map do |formatter_name, output_path| if output_path.nil? @@ -187,6 +347,7 @@ class Chef end end + # @api private def formatters_for_run if Chef::Config.formatters.empty? [default_formatter] @@ -195,6 +356,7 @@ class Chef end end + # @api private def default_formatter if (STDOUT.tty? && !Chef::Config[:force_logger]) || Chef::Config[:force_formatter] [:doc] @@ -203,6 +365,7 @@ class Chef end end + # @api private def configure_event_loggers if Chef::Config.disable_event_logger [] @@ -219,8 +382,9 @@ class Chef end end - # Resource repoters send event information back to the chef server for processing. - # Can only be called after we have a @rest object + # Resource reporters send event information back to the chef server for + # processing. Can only be called after we have a @rest object + # @api private def register_reporters [ Chef::ResourceReporter.new(rest), @@ -230,43 +394,123 @@ class Chef end end + # + # Callback to fire notifications that the Chef run is starting + # + # @api private + # + def run_started + self.class.run_start_notifications.each do |notification| + notification.call(run_status) + end + events.run_started(run_status) + end + + # + # Callback to fire notifications that the run completed successfully + # + # @api private + # + def run_completed_successfully + success_handlers = self.class.run_completed_successfully_notifications + success_handlers.each do |notification| + notification.call(run_status) + end + end + + # + # Callback to fire notifications that the Chef run failed + # + # @api private + # + def run_failed + failure_handlers = self.class.run_failed_notifications + failure_handlers.each do |notification| + notification.call(run_status) + end + end + + # # Instantiates a Chef::Node object, possibly loading the node's prior state - # when using chef-client. Delegates to policy_builder. Injects the built node - # into the Chef class. + # when using chef-client. Sets Chef.node to the new node. # # @return [Chef::Node] The node object for this Chef run + # + # @see Chef::PolicyBuilder#load_node + # + # @api private + # def load_node policy_builder.load_node - @node = policy_builder.node - Chef.set_node(@node) + run_status.node = policy_builder.node + Chef.set_node(policy_builder.node) node end - # Mutates the `node` object to prepare it for the chef run. Delegates to - # policy_builder + # + # Mutates the `node` object to prepare it for the chef run. # # @return [Chef::Node] The updated node object + # + # @see Chef::PolicyBuilder#build_node + # + # @api private + # def build_node policy_builder.build_node - @run_status.node = node + run_status.node = node node end + # + # Sync cookbooks to local cache. + # + # TODO this appears to be unused. + # + # @see Chef::PolicyBuilder#sync_cookbooks + # + # @api private + # + def sync_cookbooks + policy_builder.sync_cookbooks + end + + # + # Sets up the run context. + # + # @see Chef::PolicyBuilder#setup_run_context + # + # @return The newly set up run context + # + # @api private def setup_run_context - run_context = policy_builder.setup_run_context(@specific_recipes) + run_context = policy_builder.setup_run_context(specific_recipes) assert_cookbook_path_not_empty(run_context) run_status.run_context = run_context run_context end - def sync_cookbooks - policy_builder.sync_cookbooks - end - + # + # The PolicyBuilder strategy for figuring out run list and cookbooks. + # + # @return [Chef::PolicyBuilder::Policyfile, Chef::PolicyBuilder::ExpandNodeObject] + # + # @api private + # def policy_builder - @policy_builder ||= Chef::PolicyBuilder.strategy.new(node_name, ohai.data, json_attribs, @override_runlist, events) + @policy_builder ||= Chef::PolicyBuilder.strategy.new(node_name, ohai.data, json_attribs, override_runlist, events) end + # + # Save the updated node to Chef. + # + # Does not save if we are in solo mode or using override_runlist. + # + # @see Chef::Node#save + # @see Chef::Config#solo + # + # @api private + # def save_updated_node if Chef::Config[:solo] # nothing to do @@ -274,16 +518,46 @@ class Chef Chef::Log.warn("Skipping final node save because override_runlist was given") else Chef::Log.debug("Saving the current state of node #{node_name}") - @node.save + node.save end end + # + # Run ohai plugins. Runs all ohai plugins unless minimal_ohai is specified. + # + # Sends the ohai_completed event when finished. + # + # @see Chef::EventDispatcher# + # @see Chef::Config#minimal_ohai + # + # @api private + # def run_ohai filter = Chef::Config[:minimal_ohai] ? %w[fqdn machinename hostname platform platform_version os os_version] : nil ohai.all_plugins(filter) - @events.ohai_completed(node) + events.ohai_completed(node) end + # + # Figure out the node name we are working with. + # + # It tries these, in order: + # - Chef::Config.node_name + # - ohai[:fqdn] + # - ohai[:machinename] + # - ohai[:hostname] + # + # If we are running against a server with authentication protocol < 1.0, we + # *require* authentication protocol version 1.1. + # + # @raise [Chef::Exceptions::CannotDetermineNodeName] If the node name is not + # set and cannot be determined via ohai. + # + # @see Chef::Config#node_name + # @see Chef::Config#authentication_protocol_version + # + # @api private + # def node_name name = Chef::Config[:node_name] || ohai[:fqdn] || ohai[:machinename] || ohai[:hostname] Chef::Config[:node_name] = name @@ -292,6 +566,8 @@ class Chef # node names > 90 bytes only work with authentication protocol >= 1.1 # see discussion in config.rb. + # TODO use a computed default in Chef::Config to determine this instead of + # setting it. if name.bytesize > 90 Chef::Config[:authentication_protocol_version] = "1.1" end @@ -300,46 +576,86 @@ class Chef end # - # === Returns - # rest<Chef::REST>:: returns Chef::REST connection object + # Determine our private key and set up the connection to the Chef server. + # + # Skips registration and fires the `skipping_registration` event if + # Chef::Config.client_key is unspecified or already exists. + # + # If Chef::Config.client_key does not exist, we register the client with the + # Chef server and fire the registration_start and registration_completed events. + # + # @return [Chef::REST] The server connection object. + # + # @see Chef::Config#chef_server_url + # @see Chef::Config#client_key + # @see Chef::ApiClient::Registration#run + # @see Chef::EventDispatcher#skipping_registration + # @see Chef::EventDispatcher#registration_start + # @see Chef::EventDispatcher#registration_completed + # @see Chef::EventDispatcher#registration_failed + # + # @api private + # def register(client_name=node_name, config=Chef::Config) if !config[:client_key] - @events.skipping_registration(client_name, config) + events.skipping_registration(client_name, config) Chef::Log.debug("Client key is unspecified - skipping registration") elsif File.exists?(config[:client_key]) - @events.skipping_registration(client_name, config) + events.skipping_registration(client_name, config) Chef::Log.debug("Client key #{config[:client_key]} is present - skipping registration") else - @events.registration_start(node_name, config) + events.registration_start(node_name, config) Chef::Log.info("Client key #{config[:client_key]} is not present - registering") Chef::ApiClient::Registration.new(node_name, config[:client_key]).run - @events.registration_completed + events.registration_completed end # We now have the client key, and should use it from now on. @rest = Chef::REST.new(config[:chef_server_url], client_name, config[:client_key]) register_reporters rescue Exception => e + # TODO this should probably only ever fire if we *started* registration. + # Move it to the block above. # TODO: munge exception so a semantic failure message can be given to the # user - @events.registration_failed(client_name, e, config) + events.registration_failed(client_name, e, config) raise end - # Converges the node. # - # === Returns - # The thrown exception, if there was one. If this returns nil the converge was successful. + # Converges all compiled resources. + # + # Fires the converge_start, converge_complete and converge_failed events. + # + # If the exception `:end_client_run_early` is thrown during convergence, it + # does not mark the run complete *or* failed, and returns `nil` + # + # @param run_context The run context. + # + # @return The thrown exception, if we are in audit mode. `nil` means the + # converge was successful or ended early. + # + # @raise Any converge exception, unless we are in audit mode, in which case + # we *return* the exception. + # + # @see Chef::Runner#converge + # @see Chef::Config#audit_mode + # @see Chef::EventDispatch#converge_start + # @see Chef::EventDispatch#converge_complete + # @see Chef::EventDispatch#converge_failed + # + # @api private + # def converge(run_context) converge_exception = nil catch(:end_client_run_early) do begin - @events.converge_start(run_context) + events.converge_start(run_context) Chef::Log.debug("Converging node #{node_name}") @runner = Chef::Runner.new(run_context) - runner.converge - @events.converge_complete + @runner.converge + events.converge_complete rescue Exception => e - @events.converge_failed(e) + events.converge_failed(e) raise e if Chef::Config[:audit_mode] == :disabled converge_exception = e end @@ -347,8 +663,28 @@ class Chef converge_exception end + # + # Converge the node via and then save it if successful. + # + # @param run_context The run context. + # + # @return The thrown exception, if we are in audit mode. `nil` means the + # converge was successful or ended early. + # + # @raise Any converge or node save exception, unless we are in audit mode, + # in which case we *return* the exception. + # + # @see #converge + # @see #save_updated_mode + # @see Chef::Config#audit_mode + # + # @api private + # # We don't want to change the old API on the `converge` method to have it perform # saving. So we wrap it in this method. + # TODO given this seems to be pretty internal stuff, how badly do we need to + # split this stuff up? + # def converge_and_save(run_context) converge_exception = converge(run_context) unless converge_exception @@ -362,37 +698,67 @@ class Chef converge_exception end + # + # Run the audit phase. + # + # Triggers the audit_phase_start, audit_phase_complete and + # audit_phase_failed events. + # + # @param run_context The run context. + # + # @return Any thrown exceptions. `nil` if successful. + # + # @see Chef::Audit::Runner#run + # @see Chef::EventDispatch#audit_phase_start + # @see Chef::EventDispatch#audit_phase_complete + # @see Chef::EventDispatch#audit_phase_failed + # + # @api private + # def run_audits(run_context) - audit_exception = nil begin - @events.audit_phase_start(run_status) + events.audit_phase_start(run_status) Chef::Log.info("Starting audit phase") auditor = Chef::Audit::Runner.new(run_context) auditor.run if auditor.failed? - raise Chef::Exceptions::AuditsFailed.new(auditor.num_failed, auditor.num_total) + audit_exception = Chef::Exceptions::AuditsFailed.new(auditor.num_failed, auditor.num_total) + events.audit_phase_failed(audit_exception) + else + events.audit_phase_complete end - @events.audit_phase_complete rescue Exception => e Chef::Log.error("Audit phase failed with error message: #{e.message}") - @events.audit_phase_failed(e) + events.audit_phase_failed(e) audit_exception = e end audit_exception end - # Expands the run list. Delegates to the policy_builder. # - # Normally this does not need to be called from here, it will be called by - # build_node. This is provided so external users (like the chefspec - # project) can inject custom behavior into the run process. + # Expands the run list. + # + # @return [Chef::RunListExpansion] The expanded run list. + # + # @see Chef::PolicyBuilder#expand_run_list # - # === Returns - # RunListExpansion: A RunListExpansion or API compatible object. def expanded_run_list policy_builder.expand_run_list end + # + # Check if the user has Administrator privileges on windows. + # + # Throws an error if the user is not an admin, and + # `Chef::Config.fatal_windows_admin_check` is true. + # + # @raise [Chef::Exceptions::WindowsNotAdmin] If the user is not an admin. + # + # @see Chef::platform#windows? + # @see Chef::Config#fatal_windows_admin_check + # + # @api private + # def do_windows_admin_check if Chef::Platform.windows? Chef::Log.debug("Checking for administrator privileges....") @@ -412,99 +778,121 @@ class Chef end end - # Do a full run for this Chef::Client. Calls: - # - # * run_ohai - Collect information about the system - # * build_node - Get the last known state, merge with local changes - # * register - If not in solo mode, make sure the server knows about this client - # * sync_cookbooks - If not in solo mode, populate the local cache with the node's cookbooks - # * converge - Bring this system up to date - # - # === Returns - # true:: Always returns true. - def run - runlock = RunLock.new(Chef::Config.lockfile) - runlock.acquire - # don't add code that may fail before entering this section to be sure to release lock - begin - runlock.save_pid - - request_id = Chef::RequestID.instance.request_id - run_context = nil - @events.run_start(Chef::VERSION) - Chef::Log.info("*** Chef #{Chef::VERSION} ***") - Chef::Log.info "Chef-client pid: #{Process.pid}" - Chef::Log.debug("Chef-client request_id: #{request_id}") - enforce_path_sanity - run_ohai - - register unless Chef::Config[:solo] - - load_node - - build_node + # Notification registration + class<<self + # + # Add a listener for the 'client run started' event. + # + # @param notification_block The callback (takes |run_status| parameter). + # @yieldparam [Chef::RunStatus] run_status The run status. + # + def when_run_starts(¬ification_block) + run_start_notifications << notification_block + end - run_status.run_id = request_id - run_status.start_clock - Chef::Log.info("Starting Chef Run for #{node.name}") - run_started + # + # Add a listener for the 'client run success' event. + # + # @param notification_block The callback (takes |run_status| parameter). + # @yieldparam [Chef::RunStatus] run_status The run status. + # + def when_run_completes_successfully(¬ification_block) + run_completed_successfully_notifications << notification_block + end - do_windows_admin_check + # + # Add a listener for the 'client run failed' event. + # + # @param notification_block The callback (takes |run_status| parameter). + # @yieldparam [Chef::RunStatus] run_status The run status. + # + def when_run_fails(¬ification_block) + run_failed_notifications << notification_block + end - run_context = setup_run_context + # + # Clears all listeners for client run status events. + # + # Primarily for testing purposes. + # + # @api private + # + def clear_notifications + @run_start_notifications = nil + @run_completed_successfully_notifications = nil + @run_failed_notifications = nil + end - if Chef::Config[:audit_mode] != :audit_only - converge_error = converge_and_save(run_context) - end + # + # TODO These seem protected to me. + # + + # + # Listeners to be run when the client run starts. + # + # @return [Array<Proc>] + # + # @api private + # + def run_start_notifications + @run_start_notifications ||= [] + end - if Chef::Config[:why_run] == true - # why_run should probably be renamed to why_converge - Chef::Log.debug("Not running controls in 'why_run' mode - this mode is used to see potential converge changes") - elsif Chef::Config[:audit_mode] != :disabled - audit_error = run_audits(run_context) - end + # + # Listeners to be run when the client run completes successfully. + # + # @return [Array<Proc>] + # + # @api private + # + def run_completed_successfully_notifications + @run_completed_successfully_notifications ||= [] + end - if converge_error || audit_error - e = Chef::Exceptions::RunFailedWrappingError.new(converge_error, audit_error) - e.fill_backtrace - raise e - end + # + # Listeners to be run when the client run fails. + # + # @return [Array<Proc>] + # + # @api private + # + def run_failed_notifications + @run_failed_notifications ||= [] + end + end - run_status.stop_clock - Chef::Log.info("Chef Run complete in #{run_status.elapsed_time} seconds") - run_completed_successfully - @events.run_completed(node) + # + # IO stream that will be used as 'STDOUT' for formatters. Formatters are + # configured during `initialize`, so this provides a convenience for + # setting alternative IO stream during tests. + # + # @api private + # + STDOUT_FD = STDOUT - # rebooting has to be the last thing we do, no exceptions. - Chef::Platform::Rebooter.reboot_if_needed!(node) + # + # IO stream that will be used as 'STDERR' for formatters. Formatters are + # configured during `initialize`, so this provides a convenience for + # setting alternative IO stream during tests. + # + # @api private + # + STDERR_FD = STDERR - true + # + # Deprecated writers + # - rescue Exception => e - # CHEF-3336: Send the error first in case something goes wrong below and we don't know why - Chef::Log.debug("Re-raising exception: #{e.class} - #{e.message}\n#{e.backtrace.join("\n ")}") - # If we failed really early, we may not have a run_status yet. Too early for these to be of much use. - if run_status - run_status.stop_clock - run_status.exception = e - run_failed - end - Chef::Application.debug_stacktrace(e) - @events.run_failed(e) - raise - ensure - Chef::RequestID.instance.reset_request_id - request_id = nil - @run_status = nil - run_context = nil - runlock.release - GC.start - end - true - end + include Chef::Mixin::Deprecation + deprecated_attr_writer :ohai, "There is no alternative. Leave ohai alone!" + deprecated_attr_writer :rest, "There is no alternative. Leave rest alone!" + deprecated_attr :runner, "There is no alternative. Leave runner alone!" private + attr_reader :override_runlist + attr_reader :specific_recipes + def empty_directory?(path) !File.exists?(path) || (Dir.entries(path).size <= 2) end @@ -536,7 +924,6 @@ class Chef Chef::ReservedNames::Win32::Security.has_admin_privileges? end - end end diff --git a/lib/chef/config.rb b/lib/chef/config.rb index 629553c8ab..9beb18b53e 100644 --- a/lib/chef/config.rb +++ b/lib/chef/config.rb @@ -51,4 +51,3 @@ class Chef end end - diff --git a/lib/chef/dsl/recipe.rb b/lib/chef/dsl/recipe.rb index 97f5088b5d..77646376ba 100644 --- a/lib/chef/dsl/recipe.rb +++ b/lib/chef/dsl/recipe.rb @@ -21,6 +21,7 @@ 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' @@ -33,6 +34,7 @@ class Chef module Recipe include Chef::Mixin::ShellOut + include Chef::Mixin::PowershellOut # method_missing must live for backcompat purposes until Chef 13. def method_missing(method_symbol, *args, &block) diff --git a/lib/chef/event_dispatch/base.rb b/lib/chef/event_dispatch/base.rb index 7274105802..93caa62a65 100644 --- a/lib/chef/event_dispatch/base.rb +++ b/lib/chef/event_dispatch/base.rb @@ -82,6 +82,11 @@ class Chef def node_load_completed(node, expanded_run_list, config) end + # Called after the Policyfile was loaded. This event only occurs when + # chef is in policyfile mode. + def policyfile_loaded(policy) + end + # Called before the cookbook collection is fetched from the server. def cookbook_resolution_start(expanded_run_list) end diff --git a/lib/chef/exceptions.rb b/lib/chef/exceptions.rb index da562e70f4..c0f4158db4 100644 --- a/lib/chef/exceptions.rb +++ b/lib/chef/exceptions.rb @@ -435,7 +435,7 @@ class Chef wrapped_errors.each_with_index do |e,i| backtrace << "#{i+1}) #{e.class} - #{e.message}" backtrace += e.backtrace if e.backtrace - backtrace << "" + backtrace << "" unless i == wrapped_errors.length - 1 end set_backtrace(backtrace) end diff --git a/lib/chef/formatters/doc.rb b/lib/chef/formatters/doc.rb index 7144d00b5d..e63c764cbf 100644 --- a/lib/chef/formatters/doc.rb +++ b/lib/chef/formatters/doc.rb @@ -3,9 +3,9 @@ require 'chef/config' class Chef module Formatters - #-- - # TODO: not sold on the name, but the output is similar to what rspec calls - # "specdoc" + + # Formatter similar to RSpec's documentation formatter. Uses indentation to + # show context. class Doc < Formatters::Base attr_reader :start_time, :end_time, :successful_audits, :failed_audits @@ -93,6 +93,10 @@ class Chef def node_load_completed(node, expanded_run_list, config) end + def policyfile_loaded(policy) + puts_line "Using policy '#{policy["name"]}' at revision '#{policy["revision_id"]}'" + end + # Called before the cookbook collection is fetched from the server. def cookbook_resolution_start(expanded_run_list) puts_line "resolving cookbooks for run list: #{expanded_run_list.inspect}" @@ -184,8 +188,10 @@ class Chef puts_line "Audit phase exception:" indent puts_line "#{error.message}" - error.backtrace.each do |l| - puts_line l + if error.backtrace + error.backtrace.each do |l| + puts_line l + end end end diff --git a/lib/chef/knife/core/generic_presenter.rb b/lib/chef/knife/core/generic_presenter.rb index f3ea0f0d6c..2df9603faa 100644 --- a/lib/chef/knife/core/generic_presenter.rb +++ b/lib/chef/knife/core/generic_presenter.rb @@ -181,7 +181,7 @@ class Chef # Must check :[] before attr because spec can include # `keys` - want the key named `keys`, not a list of # available keys. - elsif data.respond_to?(:[]) + elsif data.respond_to?(:[]) && data.has_key?(attr) data = data[attr] elsif data.respond_to?(attr.to_sym) data = data.send(attr.to_sym) diff --git a/lib/chef/knife/core/subcommand_loader.rb b/lib/chef/knife/core/subcommand_loader.rb index 1f59515f44..a8705c724f 100644 --- a/lib/chef/knife/core/subcommand_loader.rb +++ b/lib/chef/knife/core/subcommand_loader.rb @@ -23,7 +23,7 @@ class Chef class SubcommandLoader MATCHES_CHEF_GEM = %r{/chef-[\d]+\.[\d]+\.[\d]+}.freeze - MATCHES_THIS_CHEF_GEM = %r{/chef-#{Chef::VERSION}/}.freeze + MATCHES_THIS_CHEF_GEM = %r{/chef-#{Chef::VERSION}(-\w+)?(-\w+)?/}.freeze attr_reader :chef_config_dir attr_reader :env diff --git a/lib/chef/mixin/deprecation.rb b/lib/chef/mixin/deprecation.rb index 489f27c339..a3eacf75cb 100644 --- a/lib/chef/mixin/deprecation.rb +++ b/lib/chef/mixin/deprecation.rb @@ -95,6 +95,30 @@ class Chef DeprecatedInstanceVariable.new(obj, name, level) end + def deprecated_attr(name, alternative) + deprecated_attr_reader(name, alternative) + deprecated_attr_writer(name, alternative) + end + + def deprecated_attr_reader(name, alternative, level=:warn) + define_method(name) do + Chef::Log.deprecation("#{self.class}.#{name} is deprecated. Support will be removed in a future release.") + Chef::Log.deprecation(alternative) + Chef::Log.deprecation("Called from:") + caller[0..3].each {|c| Chef::Log.deprecation(c)} + instance_variable_get("@#{name}") + end + end + + def deprecated_attr_writer(name, alternative, level=:warn) + define_method("#{name}=") do |value| + Chef::Log.deprecation("Writing to #{self.class}.#{name} with #{name}= is deprecated. Support will be removed in a future release.") + Chef::Log.deprecation(alternative) + Chef::Log.deprecation("Called from:") + caller[0..3].each {|c| Chef::Log.deprecation(c)} + instance_variable_set("@#{name}", value) + end + end end end end diff --git a/lib/chef/mixin/powershell_out.rb b/lib/chef/mixin/powershell_out.rb new file mode 100644 index 0000000000..e4f29c07c4 --- /dev/null +++ b/lib/chef/mixin/powershell_out.rb @@ -0,0 +1,98 @@ +#-- +# Copyright:: Copyright (c) 2015 Chef Software, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +require 'chef/mixin/shell_out' +require 'chef/mixin/windows_architecture_helper' + +class Chef + module Mixin + module PowershellOut + include Chef::Mixin::ShellOut + include Chef::Mixin::WindowsArchitectureHelper + + # Run a command under powershell with the same API as shell_out. The + # options hash is extended to take an "architecture" flag which + # can be set to :i386 or :x86_64 to force the windows architecture. + # + # @param script [String] script to run + # @param options [Hash] options hash + # @return [Mixlib::Shellout] mixlib-shellout object + def powershell_out(*command_args) + script = command_args.first + options = command_args.last.is_a?(Hash) ? command_args.last : nil + + run_command_with_os_architecture(script, options) + end + + # Run a command under powershell with the same API as shell_out! + # (raises exceptions on errors) + # + # @param script [String] script to run + # @param options [Hash] options hash + # @return [Mixlib::Shellout] mixlib-shellout object + def powershell_out!(*command_args) + cmd = powershell_out(*command_args) + cmd.error! + cmd + end + + private + + # Helper function to run shell_out and wrap it with the correct + # flags to possibly disable WOW64 redirection (which we often need + # because chef-client runs as a 32-bit app on 64-bit windows). + # + # @param script [String] script to run + # @param options [Hash] options hash + # @return [Mixlib::Shellout] mixlib-shellout object + def run_command_with_os_architecture(script, options) + options ||= {} + options = options.dup + arch = options.delete(:architecture) + + with_os_architecture(nil, architecture: arch) do + shell_out( + build_powershell_command(script), + options + ) + end + end + + # Helper to build a powershell command around the script to run. + # + # @param script [String] script to run + # @retrurn [String] powershell command to execute + def build_powershell_command(script) + flags = [ + # Hides the copyright banner at startup. + "-NoLogo", + # Does not present an interactive prompt to the user. + "-NonInteractive", + # Does not load the Windows PowerShell profile. + "-NoProfile", + # always set the ExecutionPolicy flag + # see http://technet.microsoft.com/en-us/library/ee176961.aspx + "-ExecutionPolicy Unrestricted", + # Powershell will hang if STDIN is redirected + # http://connect.microsoft.com/PowerShell/feedback/details/572313/powershell-exe-can-hang-if-stdin-is-redirected + "-InputFormat None" + ] + + "powershell.exe #{flags.join(' ')} -Command \"#{script}\"" + end + end + end +end diff --git a/lib/chef/mixin/windows_architecture_helper.rb b/lib/chef/mixin/windows_architecture_helper.rb index a0ac34f627..c5f3e1bd79 100644 --- a/lib/chef/mixin/windows_architecture_helper.rb +++ b/lib/chef/mixin/windows_architecture_helper.rb @@ -42,7 +42,7 @@ class Chef is_i386_process_on_x86_64_windows? end - def with_os_architecture(node) + def with_os_architecture(node, architecture: nil) node ||= begin os_arch = ENV['PROCESSOR_ARCHITEW6432'] || ENV['PROCESSOR_ARCHITECTURE'] @@ -51,9 +51,12 @@ class Chef n[:kernel][:machine] = os_arch == 'AMD64' ? :x86_64 : :i386 end end + + architecture ||= node_windows_architecture(node) + wow64_redirection_state = nil - if wow64_architecture_override_required?(node, node_windows_architecture(node)) + if wow64_architecture_override_required?(node, architecture) wow64_redirection_state = disable_wow64_file_redirection(node) end diff --git a/lib/chef/mixin/windows_env_helper.rb b/lib/chef/mixin/windows_env_helper.rb index 490b235065..a126801a28 100644 --- a/lib/chef/mixin/windows_env_helper.rb +++ b/lib/chef/mixin/windows_env_helper.rb @@ -21,11 +21,11 @@ require 'chef/exceptions' require 'chef/platform/query_helpers' require 'chef/win32/error' if Chef::Platform.windows? require 'chef/win32/api/system' if Chef::Platform.windows? +require 'chef/win32/api/unicode' if Chef::Platform.windows? class Chef module Mixin module WindowsEnvHelper - if Chef::Platform.windows? include Chef::ReservedNames::Win32::API::System end @@ -39,7 +39,16 @@ class Chef def broadcast_env_change flags = SMTO_BLOCK | SMTO_ABORTIFHUNG | SMTO_NOTIMEOUTIFNOTHUNG - SendMessageTimeoutA(HWND_BROADCAST, WM_SETTINGCHANGE, 0, FFI::MemoryPointer.from_string('Environment').address, flags, 5000, nil) + # for why two calls, see: + # http://stackoverflow.com/questions/4968373/why-doesnt-sendmessagetimeout-update-the-environment-variables + if ( SendMessageTimeoutA(HWND_BROADCAST, WM_SETTINGCHANGE, 0, FFI::MemoryPointer.from_string('Environment').address, flags, 5000, nil) == 0 ) + Chef::ReservedNames::Win32::Error.raise! + end + if ( SendMessageTimeoutW(HWND_BROADCAST, WM_SETTINGCHANGE, 0, FFI::MemoryPointer.from_string( + Chef::ReservedNames::Win32::Unicode.utf8_to_wide('Environment') + ).address, flags, 5000, nil) == 0 ) + Chef::ReservedNames::Win32::Error.raise! + end end def expand_path(path) diff --git a/lib/chef/platform/query_helpers.rb b/lib/chef/platform/query_helpers.rb index 03b1c9ca1e..b3948eac21 100644 --- a/lib/chef/platform/query_helpers.rb +++ b/lib/chef/platform/query_helpers.rb @@ -39,6 +39,11 @@ class Chef is_server_2003 end + def supports_powershell_execution_bypass?(node) + node[:languages] && node[:languages][:powershell] && + node[:languages][:powershell][:version].to_i >= 3 + end + def supports_dsc?(node) node[:languages] && node[:languages][:powershell] && node[:languages][:powershell][:version].to_i >= 4 diff --git a/lib/chef/policy_builder/policyfile.rb b/lib/chef/policy_builder/policyfile.rb index ac25b549be..5991e3ce10 100644 --- a/lib/chef/policy_builder/policyfile.rb +++ b/lib/chef/policy_builder/policyfile.rb @@ -119,6 +119,7 @@ class Chef @node = Chef::Node.find_or_create(node_name) validate_policyfile + events.policyfile_loaded(policy) node rescue Exception => e events.node_load_failed(node_name, e, Chef::Config) diff --git a/lib/chef/provider.rb b/lib/chef/provider.rb index 99d09d0507..42347a230e 100644 --- a/lib/chef/provider.rb +++ b/lib/chef/provider.rb @@ -22,6 +22,7 @@ require 'chef/mixin/convert_to_class_name' require 'chef/mixin/enforce_ownership_and_permissions' require 'chef/mixin/why_run' require 'chef/mixin/shell_out' +require 'chef/mixin/powershell_out' require 'chef/mixin/provides' require 'chef/platform/service_helpers' require 'chef/node_map' @@ -30,6 +31,7 @@ class Chef class Provider include Chef::Mixin::WhyRun include Chef::Mixin::ShellOut + include Chef::Mixin::PowershellOut extend Chef::Mixin::Provides # supports the given resource and action (late binding) diff --git a/lib/chef/provider/directory.rb b/lib/chef/provider/directory.rb index 416393ac60..4d5423d0e8 100644 --- a/lib/chef/provider/directory.rb +++ b/lib/chef/provider/directory.rb @@ -43,6 +43,9 @@ class Chef end def define_resource_requirements + # deep inside FAC we have to assert requirements, so call FACs hook to set that up + access_controls.define_resource_requirements + requirements.assert(:create) do |a| # Make sure the parent dir exists, or else fail. # for why run, print a message explaining the potential error. diff --git a/lib/chef/provider/dsc_resource.rb b/lib/chef/provider/dsc_resource.rb index 2812c154c6..5fa84a21e9 100644 --- a/lib/chef/provider/dsc_resource.rb +++ b/lib/chef/provider/dsc_resource.rb @@ -121,7 +121,14 @@ class Chef # however Invoke-DscResource is not correctly writing to that # stream and instead just dumping to stdout @converge_description = result.stdout - result.return_value[0]["InDesiredState"] + + if result.return_value.is_a?(Array) + # WMF Feb 2015 Preview + result.return_value[0]["InDesiredState"] + else + # WMF April 2015 Preview + result.return_value["InDesiredState"] + end end def set_resource diff --git a/lib/chef/provider/powershell_script.rb b/lib/chef/provider/powershell_script.rb index f9dcd6d80c..ed44dee6ae 100644 --- a/lib/chef/provider/powershell_script.rb +++ b/lib/chef/provider/powershell_script.rb @@ -24,71 +24,153 @@ class Chef provides :powershell_script, os: "windows" + def initialize (new_resource, run_context) + super(new_resource, run_context, '.ps1') + add_exit_status_wrapper + end + + def action_run + valid_syntax = validate_script_syntax! + super if valid_syntax + end + + def flags + # Must use -File rather than -Command to launch the script + # file created by the base class that contains the script + # code -- otherwise, powershell.exe does not propagate the + # error status of a failed Windows process that ran at the + # end of the script, it gets changed to '1'. + interpreter_flags = [default_interpreter_flags, '-File'].join(' ') + + if ! (@new_resource.flags.nil?) + interpreter_flags = [@new_resource.flags, interpreter_flags].join(' ') + end + + interpreter_flags + end + protected - EXIT_STATUS_EXCEPTION_HANDLER = "\ntrap [Exception] {write-error -exception ($_.Exception.Message);exit 1}".freeze - EXIT_STATUS_NORMALIZATION_SCRIPT = "\nif ($? -ne $true) { if ( $LASTEXITCODE ) {exit $LASTEXITCODE} else { exit 1 }}".freeze - EXIT_STATUS_RESET_SCRIPT = "\n$global:LASTEXITCODE=$null".freeze - # Process exit codes are strange with PowerShell. Unless you - # explicitly call exit in Powershell, the powershell.exe - # interpreter returns only 0 for success or 1 for failure. Since - # we'd like to get specific exit codes from executable tools run - # with Powershell, we do some work using the automatic variables - # $? and $LASTEXITCODE to return the process exit code of the - # last process run in the script if it is the last command - # executed, otherwise 0 or 1 based on whether $? is set to true - # (success, where we return 0) or false (where we return 1). - def normalize_script_exit_status( code ) - target_code = ( EXIT_STATUS_EXCEPTION_HANDLER + - EXIT_STATUS_RESET_SCRIPT + - "\n" + - code.to_s + - EXIT_STATUS_NORMALIZATION_SCRIPT ) - convert_boolean_return = @new_resource.convert_boolean_return - self.code = <<EOH -new-variable -name interpolatedexitcode -visibility private -value $#{convert_boolean_return} -new-variable -name chefscriptresult -visibility private -$chefscriptresult = { -#{target_code} -}.invokereturnasis() -if ($interpolatedexitcode -and $chefscriptresult.gettype().name -eq 'boolean') { exit [int32](!$chefscriptresult) } else { exit 0 } -EOH - Chef::Log.debug("powershell_script provider called with script code:\n\n#{code}\n") + # Process exit codes are strange with PowerShell and require + # special handling to cover common use cases. + def add_exit_status_wrapper + self.code = wrapper_script + Chef::Log.debug("powershell_script provider called with script code:\n\n#{@new_resource.code}\n") Chef::Log.debug("powershell_script provider will execute transformed code:\n\n#{self.code}\n") end - public + def validate_script_syntax! + interpreter_arguments = default_interpreter_flags.join(' ') + Tempfile.open(['chef_powershell_script-user-code', '.ps1']) do | user_script_file | + user_script_file.puts("{#{@new_resource.code}}") + user_script_file.close - def initialize (new_resource, run_context) - super(new_resource, run_context, '.ps1') - normalize_script_exit_status(new_resource.code) + validation_command = "\"#{interpreter}\" #{interpreter_arguments} -Command #{user_script_file.path}" + + # For consistency with other script resources, allow even syntax errors + # to be suppressed if the returns attribute would have suppressed it + # at converge. + valid_returns = [0] + specified_returns = @new_resource.returns.is_a?(Integer) ? + [@new_resource.returns] : + @new_resource.returns + valid_returns.concat([1]) if specified_returns.include?(1) + + result = shell_out!(validation_command, {returns: valid_returns}) + result.exitstatus == 0 + end end - def flags - default_flags = [ + def default_interpreter_flags + # 'Bypass' is preferable since it doesn't require user input confirmation + # for files such as PowerShell modules downloaded from the + # Internet. However, 'Bypass' is not supported prior to + # PowerShell 3.0, so the fallback is 'Unrestricted' + execution_policy = Chef::Platform.supports_powershell_execution_bypass?(run_context.node) ? 'Bypass' : 'Unrestricted' + + [ "-NoLogo", "-NonInteractive", "-NoProfile", - "-ExecutionPolicy Unrestricted", + "-ExecutionPolicy #{execution_policy}", # Powershell will hang if STDIN is redirected # http://connect.microsoft.com/PowerShell/feedback/details/572313/powershell-exe-can-hang-if-stdin-is-redirected - "-InputFormat None", - # Must use -File rather than -Command to launch the script - # file created by the base class that contains the script - # code -- otherwise, powershell.exe does not propagate the - # error status of a failed Windows process that ran at the - # end of the script, it gets changed to '1'. - "-File" + "-InputFormat None" ] + end - interpreter_flags = default_flags.join(' ') + # A wrapper script is used to launch user-supplied script while + # still obtaining useful process exit codes. Unless you + # explicitly call exit in Powershell, the powershell.exe + # interpreter returns only 0 for success or 1 for failure. Since + # we'd like to get specific exit codes from executable tools run + # with Powershell, we do some work using the automatic variables + # $? and $LASTEXITCODE to return the process exit code of the + # last process run in the script if it is the last command + # executed, otherwise 0 or 1 based on whether $? is set to true + # (success, where we return 0) or false (where we return 1). + def wrapper_script +<<-EOH +# Chef Client wrapper for powershell_script resources - if ! (@new_resource.flags.nil?) - interpreter_flags = [@new_resource.flags, interpreter_flags].join(' ') - end +# LASTEXITCODE can be uninitialized -- make it explictly 0 +# to avoid incorrect detection of failure (non-zero) codes +$global:LASTEXITCODE = 0 - interpreter_flags +# Catch any exceptions -- without this, exceptions will result +# In a zero return code instead of the desired non-zero code +# that indicates a failure +trap [Exception] {write-error ($_.Exception.Message);exit 1} + +# Variable state that should not be accessible to the user code +new-variable -name interpolatedexitcode -visibility private -value $#{@new_resource.convert_boolean_return} +new-variable -name chefscriptresult -visibility private + +# Initialize a variable we use to capture $? inside a block +$global:lastcmdlet = $null + +# Execute the user's code in a script block -- +$chefscriptresult = +{ + #{@new_resource.code} + + # This assignment doesn't affect the block's return value + $global:lastcmdlet = $? +}.invokereturnasis() + +# Assume failure status of 1 -- success cases +# will have to override this +$exitstatus = 1 + +# If convert_boolean_return is enabled, the block's return value +# gets precedence in determining our exit status +if ($interpolatedexitcode -and $chefscriptresult -ne $null -and $chefscriptresult.gettype().name -eq 'boolean') +{ + $exitstatus = [int32](!$chefscriptresult) +} +elseif ($lastcmdlet) +{ + # Otherwise, a successful cmdlet execution defines the status + $exitstatus = 0 +} +elseif ( $LASTEXITCODE -ne $null -and $LASTEXITCODE -ne 0 ) +{ + # If the cmdlet status is failed, allow the Win32 status + # in $LASTEXITCODE to define exit status. This handles the case + # where no cmdlets, only Win32 processes have run since $? + # will be set to $false whenever a Win32 process returns a non-zero + # status. + $exitstatus = $LASTEXITCODE +} + +# If this script is launched with -File, the process exit +# status of PowerShell.exe will be $exitstatus. If it was +# launched with -Command, it will be 0 if $exitstatus was 0, +# 1 (i.e. failed) otherwise. +exit $exitstatus +EOH end + end end end diff --git a/lib/chef/provider/service/freebsd.rb b/lib/chef/provider/service/freebsd.rb index 9204e3ef92..6c78f86fe0 100644 --- a/lib/chef/provider/service/freebsd.rb +++ b/lib/chef/provider/service/freebsd.rb @@ -147,7 +147,7 @@ class Chef # some scripts support multiple instances through symlinks such as openvpn. # We should get the service name from rcvar. Chef::Log.debug("name=\"service\" not found at #{init_command}. falling back to rcvar") - sn = shell_out!("#{init_command} rcvar").stdout[/(\w+_enable)=/, 1] + shell_out!("#{init_command} rcvar").stdout[/(\w+_enable)=/, 1] else # for why-run mode when the rcd_script is not there yet new_resource.service_name diff --git a/lib/chef/resource.rb b/lib/chef/resource.rb index 2f5c2b7798..18759b29f7 100644 --- a/lib/chef/resource.rb +++ b/lib/chef/resource.rb @@ -37,6 +37,8 @@ require 'chef/resource_resolver' require 'chef/mixin/deprecation' require 'chef/mixin/provides' +require 'chef/mixin/shell_out' +require 'chef/mixin/powershell_out' class Chef class Resource @@ -51,6 +53,10 @@ class Chef include Chef::DSL::RebootPending extend Chef::Mixin::Provides + # This lets user code do things like `not_if { shell_out!("command") }` + include Chef::Mixin::ShellOut + include Chef::Mixin::PowershellOut + # # The node the current Chef run is using. # diff --git a/lib/chef/resource_reporter.rb b/lib/chef/resource_reporter.rb index 7829bb4d70..7d13a5a5ce 100644 --- a/lib/chef/resource_reporter.rb +++ b/lib/chef/resource_reporter.rb @@ -213,7 +213,7 @@ class Chef # If we failed before we received the run_started callback, there's not much we can do # in terms of reporting if @run_status - post_reporting_data + post_reporting_data end end diff --git a/lib/chef/util/backup.rb b/lib/chef/util/backup.rb index 0cc009ca1f..6c95cedad7 100644 --- a/lib/chef/util/backup.rb +++ b/lib/chef/util/backup.rb @@ -78,8 +78,16 @@ class Chef Chef::Log.info("#{@new_resource} removed backup at #{backup_file}") end + def unsorted_backup_files + # If you replace this with Dir[], you will probably break Windows. + fn = Regexp.escape(::File.basename(path)) + Dir.entries(::File.dirname(backup_path)).select do |f| + !!(f =~ /\A#{fn}.chef-[0-9.]*\B/) + end.map {|f| ::File.join(::File.dirname(backup_path), f)} + end + def sorted_backup_files - Dir[Chef::Util::PathHelper.escape_glob(prefix, ".#{path}") + ".chef-*"].sort { |a,b| b <=> a } + unsorted_backup_files.sort { |a,b| b <=> a } end end end diff --git a/lib/chef/version.rb b/lib/chef/version.rb index f7466084b6..f4cf71c002 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.dev.0' + VERSION = '12.4.0.rc.0' end # diff --git a/spec/functional/mixin/powershell_out_spec.rb b/spec/functional/mixin/powershell_out_spec.rb new file mode 100644 index 0000000000..9cc8aeed7e --- /dev/null +++ b/spec/functional/mixin/powershell_out_spec.rb @@ -0,0 +1,43 @@ +# +# Copyright:: Copyright (c) 2014 Chef Software, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'spec_helper' +require 'chef/mixin/powershell_out' + +describe Chef::Mixin::PowershellOut, windows_only: true do + include Chef::Mixin::PowershellOut + + describe "#powershell_out" do + it "runs a powershell command and collects stdout" do + expect(powershell_out("get-process").run_command.stdout).to match /Handles\s+NPM\(K\)\s+PM\(K\)\s+WS\(K\)\s+VM\(M\)\s+CPU\(s\)\s+Id\s+ProcessName/ + end + + it "does not raise exceptions when the command is invalid" do + powershell_out("this-is-not-a-valid-command").run_command + end + end + + describe "#powershell_out!" do + it "runs a powershell command and collects stdout" do + expect(powershell_out!("get-process").run_command.stdout).to match /Handles\s+NPM\(K\)\s+PM\(K\)\s+WS\(K\)\s+VM\(M\)\s+CPU\(s\)\s+Id\s+ProcessName/ + end + + it "raises exceptions when the command is invalid" do + expect { powershell_out!("this-is-not-a-valid-command").run_command }.to raise_exception(Mixlib::ShellOut::ShellCommandFailed) + end + end +end diff --git a/spec/functional/resource/execute_spec.rb b/spec/functional/resource/execute_spec.rb index ffa4628cb2..692ccfb796 100644 --- a/spec/functional/resource/execute_spec.rb +++ b/spec/functional/resource/execute_spec.rb @@ -137,9 +137,16 @@ describe Chef::Resource::Execute do end end + # Ensure that CommandTimeout is raised, and is caused by resource.timeout really expiring. + # https://github.com/chef/chef/issues/2985 + # + # resource.timeout should be short, this is what we're testing + # resource.command ruby sleep timer should be longer than resource.timeout to give us something to timeout + # Timeout::timeout should be longer than resource.timeout, but less than the resource.command ruby sleep timer, + # so we fail if we finish on resource.command instead of resource.timeout, but raise CommandTimeout anyway (#2175). it "times out when a timeout is set on the resource" do - Timeout::timeout(5) do - resource.command %{ruby -e 'sleep 600'} + Timeout::timeout(30) do + resource.command %{ruby -e 'sleep 300'} resource.timeout 0.1 expect { resource.run_action(:run) }.to raise_error(Mixlib::ShellOut::CommandTimeout) end diff --git a/spec/functional/resource/file_spec.rb b/spec/functional/resource/file_spec.rb index f1a290dea4..9e30e62111 100644 --- a/spec/functional/resource/file_spec.rb +++ b/spec/functional/resource/file_spec.rb @@ -86,6 +86,31 @@ describe Chef::Resource::File do end end + + describe "when using backup" do + before do + Chef::Config[:file_backup_path] = CHEF_SPEC_BACKUP_PATH + resource_without_content.backup(1) + resource_without_content.run_action(:create) + end + + let(:backup_glob) { File.join(CHEF_SPEC_BACKUP_PATH, test_file_dir.sub(/^([A-Za-z]:)/, ""), "#{file_base}*") } + + let(:path) do + # Use native system path + ChefConfig::PathHelper.canonical_path(File.join(test_file_dir, make_tmpname(file_base)), false) + end + + it "only stores the number of requested backups" do + resource_without_content.content('foo') + resource_without_content.run_action(:create) + resource_without_content.content('bar') + resource_without_content.run_action(:create) + expect(Dir.glob(backup_glob).length).to eq(1) + end + + end + # github issue 1842. describe "when running action :create on a relative path" do before do diff --git a/spec/functional/resource/group_spec.rb b/spec/functional/resource/group_spec.rb index 6676aa32e9..529af52d4e 100644 --- a/spec/functional/resource/group_spec.rb +++ b/spec/functional/resource/group_spec.rb @@ -372,6 +372,11 @@ downthestreetalwayshadagoodsmileonhisfacetheoldmanwalkingdownthestreeQQQQQQ" } let(:tested_action) { :manage } describe "when there is no group" do + before(:each) do + group_resource.run_action(:remove) + group_should_not_exist(group_name) + end + it "raises an error on modify" do expect { group_resource.run_action(:modify) }.to raise_error end diff --git a/spec/functional/resource/link_spec.rb b/spec/functional/resource/link_spec.rb index d39a0c2ef6..7e903b30b4 100644 --- a/spec/functional/resource/link_spec.rb +++ b/spec/functional/resource/link_spec.rb @@ -348,8 +348,7 @@ describe Chef::Resource::Link do end it_behaves_like 'delete errors out' end - context 'and the link already exists and is not writeable to this user', :skip => true do - end + it_behaves_like 'a securable resource without existing target' do let(:path) { target_file } def allowed_acl(sid, expected_perms) @@ -360,7 +359,7 @@ describe Chef::Resource::Link do end def parent_inheritable_acls dummy_file_path = File.join(test_file_dir, "dummy_file") - dummy_file = FileUtils.touch(dummy_file_path) + FileUtils.touch(dummy_file_path) dummy_desc = get_security_descriptor(dummy_file_path) FileUtils.rm_rf(dummy_file_path) dummy_desc @@ -416,8 +415,6 @@ describe Chef::Resource::Link do end end end - context "when the link destination is not readable to this user", :skip => true do - end context "when the link destination does not exist" do include_context 'create symbolic link succeeds' include_context 'delete is noop' @@ -518,8 +515,6 @@ describe Chef::Resource::Link do end it_behaves_like 'delete errors out' end - context "and the link already exists and is not writeable to this user", :skip => true do - end context "and specifies security attributes" do before(:each) do resource.owner(windows? ? 'Guest' : 'nobody') @@ -559,10 +554,10 @@ describe Chef::Resource::Link do end context 'and the link does not yet exist' do it 'links to the target file' do + skip('OS X/FreeBSD/AIX symlink? and readlink working on hard links to symlinks') if (os_x? or freebsd? or aix?) resource.run_action(:create) expect(File.exists?(target_file)).to be_truthy # OS X gets angry about this sort of link. Bug in OS X, IMO. - pending('OS X/FreeBSD/AIX symlink? and readlink working on hard links to symlinks') if (os_x? or freebsd? or aix?) expect(symlink?(target_file)).to be_truthy expect(readlink(target_file)).to eq(canonicalize(@other_target)) end @@ -578,7 +573,7 @@ describe Chef::Resource::Link do end context 'and the link does not yet exist' do it 'links to the target file' do - pending('OS X/FreeBSD/AIX fails to create hardlinks to broken symlinks') if (os_x? or freebsd? or aix?) + skip('OS X/FreeBSD/AIX fails to create hardlinks to broken symlinks') if (os_x? or freebsd? or aix?) resource.run_action(:create) # Windows and Unix have different definitions of exists? here, and that's OK. if windows? @@ -593,8 +588,7 @@ describe Chef::Resource::Link do end end end - context "when the link destination is not readable to this user", :skip => true do - end + context "when the link destination does not exist" do context 'and the link does not yet exist' do it 'create errors out' do diff --git a/spec/functional/resource/powershell_spec.rb b/spec/functional/resource/powershell_spec.rb index 56a905efe7..17ae8cbd2a 100644 --- a/spec/functional/resource/powershell_spec.rb +++ b/spec/functional/resource/powershell_spec.rb @@ -56,14 +56,13 @@ describe Chef::Resource::WindowsScript::PowershellScript, :windows_only do resource.run_action(:run) end - it "returns the -27 for a powershell script that exits with -27", :windows_powershell_dsc_only do - # This is broken on Powershell < 4.0 + it "returns the exit status 27 for a powershell script that exits with 27" do file = Tempfile.new(['foo', '.ps1']) begin - file.write "exit -27" + file.write "exit 27" file.close resource.code(". \"#{file.path}\"") - resource.returns(-27) + resource.returns(27) resource.run_action(:run) ensure file.close @@ -71,6 +70,30 @@ describe Chef::Resource::WindowsScript::PowershellScript, :windows_only do end end + let (:negative_exit_status) { -27 } + let (:unsigned_exit_status) { (-negative_exit_status ^ 65535) + 1 } + it "returns the exit status -27 as a signed integer or an unsigned 16-bit 2's complement value of 65509 for a powershell script that exits with -27" do + # Versions of PowerShell prior to 4.0 return a 16-bit unsigned value -- + # PowerShell 4.0 and later versions return a 32-bit signed value. + file = Tempfile.new(['foo', '.ps1']) + begin + file.write "exit #{negative_exit_status.to_s}" + file.close + resource.code(". \"#{file.path}\"") + + # PowerShell earlier than 4.0 takes negative exit codes + # and returns them as the underlying unsigned 16-bit + # 2's complement representation. We cover multiple versions + # of PowerShell in this example by including both the signed + # exit code and its converted counterpart as permitted return values. + # See http://support.microsoft.com/en-us/kb/2646183/zh-cn + resource.returns([negative_exit_status, unsigned_exit_status]) + expect { resource.run_action(:run) }.not_to raise_error + ensure + file.close + file.unlink + end + end it "returns the process exit code" do resource.code(arbitrary_nonzero_process_exit_code_content) @@ -99,7 +122,19 @@ describe Chef::Resource::WindowsScript::PowershellScript, :windows_only do it "returns 1 if the last command was a cmdlet that failed and was preceded by a successfully executed non-cmdlet Windows binary" do resource.code([windows_process_exit_code_success_content, cmdlet_exit_code_not_found_content].join(';')) resource.returns(1) - resource.run_action(:run) + expect { resource.run_action(:run) }.not_to raise_error + end + + it "raises an error if the script is not syntactically correct and returns is not set to 1" do + resource.code('if({)') + resource.returns(0) + expect { resource.run_action(:run) }.to raise_error(Mixlib::ShellOut::ShellCommandFailed) + end + + it "returns 1 if the script provided to the code attribute is not syntactically correct" do + resource.code('if({)') + resource.returns(1) + expect { resource.run_action(:run) }.not_to raise_error end # This somewhat ambiguous case, two failures of different types, diff --git a/spec/functional/resource/user/useradd_spec.rb b/spec/functional/resource/user/useradd_spec.rb index 3e4e4e7604..474f6a4ecf 100644 --- a/spec/functional/resource/user/useradd_spec.rb +++ b/spec/functional/resource/user/useradd_spec.rb @@ -65,8 +65,12 @@ describe Chef::Provider::User::Useradd, metadata do end end - def supports_quote_in_username? - OHAI_SYSTEM["platform_family"] == "debian" + def self.quote_in_username_unsupported? + if OHAI_SYSTEM["platform_family"] == "debian" + false + else + "Only debian family systems support quotes in username" + end end def password_should_be_set @@ -108,7 +112,7 @@ describe Chef::Provider::User::Useradd, metadata do break if status.exitstatus != 8 sleep 1 - max_retries = max_retries -1 + max_retries = max_retries - 1 rescue UserNotFound break end @@ -162,15 +166,10 @@ describe Chef::Provider::User::Useradd, metadata do end end - let(:skip) { false } - describe "action :create" do context "when the user does not exist beforehand" do before do - if reason = skip - pending(reason) - end user_resource.run_action(:create) expect(user_resource).to be_updated_by_last_action end @@ -186,14 +185,7 @@ describe Chef::Provider::User::Useradd, metadata do # tabulation: '\t', etc.). Note that using a slash ('/') may break the # default algorithm for the definition of the user's home directory. - context "and the username contains a single quote" do - let(:skip) do - if supports_quote_in_username? - false - else - "Platform #{OHAI_SYSTEM["platform"]} not expected to support username w/ quote" - end - end + context "and the username contains a single quote", skip: quote_in_username_unsupported? do let(:username) { "t'bilisi" } @@ -342,7 +334,7 @@ describe Chef::Provider::User::Useradd, metadata do before do if reason = skip - pending(reason) + skip(reason) end existing_user.run_action(:create) expect(existing_user).to be_updated_by_last_action @@ -535,7 +527,7 @@ describe Chef::Provider::User::Useradd, metadata do def aix_user_lock_status lock_info = shell_out!("lsuser -a account_locked #{username}") - status = /\S+\s+account_locked=(\S+)/.match(lock_info.stdout)[1] + /\S+\s+account_locked=(\S+)/.match(lock_info.stdout)[1] end def user_account_should_be_locked diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 8b5e42e5b6..dcf244c3cc 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -151,7 +151,7 @@ RSpec.configure do |config| config.filter_run_excluding :aes_256_gcm_only => true unless aes_256_gcm? config.filter_run_excluding :broken => true - running_platform_arch = `uname -m`.strip + running_platform_arch = `uname -m`.strip unless windows? config.filter_run_excluding :arch => lambda {|target_arch| running_platform_arch != target_arch diff --git a/spec/support/shared/context/client.rb b/spec/support/shared/context/client.rb new file mode 100644 index 0000000000..e625185f7c --- /dev/null +++ b/spec/support/shared/context/client.rb @@ -0,0 +1,275 @@ + +require 'spec_helper' + +# Stubs a basic client object +shared_context "client" do + let(:fqdn) { "hostname.example.org" } + let(:hostname) { "hostname" } + let(:machinename) { "machinename.example.org" } + let(:platform) { "example-platform" } + let(:platform_version) { "example-platform-1.0" } + + let(:ohai_data) do + { + :fqdn => fqdn, + :hostname => hostname, + :machinename => machinename, + :platform => platform, + :platform_version => platform_version + } + end + + let(:ohai_system) do + ohai = instance_double("Ohai::System", :all_plugins => true, :data => ohai_data) + allow(ohai).to receive(:[]) do |k| + ohai_data[k] + end + ohai + end + + let(:node) do + Chef::Node.new.tap do |n| + n.name(fqdn) + n.chef_environment("_default") + end + end + + let(:json_attribs) { nil } + let(:client_opts) { {} } + + let(:client) do + Chef::Config[:event_loggers] = [] + Chef::Client.new(json_attribs, client_opts).tap do |c| + c.node = node + end + end + + before do + Chef::Log.logger = Logger.new(StringIO.new) + + # Node/Ohai data + #Chef::Config[:node_name] = fqdn + allow(Ohai::System).to receive(:new).and_return(ohai_system) + end +end + +# Stubs a client for a client run. +# Requires a client object be defined in the scope of this included context. +# e.g.: +# describe "some functionality" do +# include_context "client" +# include_context "a client run" +# ... +# end +shared_context "a client run" do + let(:stdout) { StringIO.new } + let(:stderr) { StringIO.new } + + let(:api_client_exists?) { false } + let(:enable_fork) { false } + + let(:http_cookbook_sync) { double("Chef::REST (cookbook sync)") } + let(:http_node_load) { double("Chef::REST (node)") } + let(:http_node_save) { double("Chef::REST (node save)") } + + let(:runner) { instance_double("Chef::Runner") } + let(:audit_runner) { instance_double("Chef::Audit::Runner", :failed? => false) } + + def stub_for_register + # --Client.register + # Make sure Client#register thinks the client key doesn't + # exist, so it tries to register and create one. + allow(File).to receive(:exists?).and_call_original + expect(File).to receive(:exists?). + with(Chef::Config[:client_key]). + exactly(:once). + and_return(api_client_exists?) + + unless api_client_exists? + # Client.register will register with the validation client name. + expect_any_instance_of(Chef::ApiClient::Registration).to receive(:run) + end + end + + def stub_for_node_load + # Client.register will then turn around create another + # Chef::REST object, this time with the client key it got from the + # previous step. + expect(Chef::REST).to receive(:new). + with(Chef::Config[:chef_server_url], fqdn, Chef::Config[:client_key]). + exactly(:once). + and_return(http_node_load) + + # --Client#build_node + # looks up the node, which we will return, then later saves it. + expect(Chef::Node).to receive(:find_or_create).with(fqdn).and_return(node) + + # --ResourceReporter#node_load_completed + # gets a run id from the server for storing resource history + # (has its own tests, so stubbing it here.) + expect_any_instance_of(Chef::ResourceReporter).to receive(:node_load_completed) + end + + def stub_for_sync_cookbooks + # --Client#setup_run_context + # ---Client#sync_cookbooks -- downloads the list of cookbooks to sync + # + expect_any_instance_of(Chef::CookbookSynchronizer).to receive(:sync_cookbooks) + expect(Chef::REST).to receive(:new).with(Chef::Config[:chef_server_url]).and_return(http_cookbook_sync) + expect(http_cookbook_sync).to receive(:post). + with("environments/_default/cookbook_versions", {:run_list => []}). + and_return({}) + end + + def stub_for_converge + # define me + end + + def stub_for_audit + # define me + end + + def stub_for_node_save + # define me + end + + def stub_for_run + # define me + end + + before do + Chef::Config[:client_fork] = enable_fork + Chef::Config[:cache_path] = windows? ? 'C:\chef' : '/var/chef' + Chef::Config[:why_run] = false + Chef::Config[:audit_mode] = :enabled + + stub_const("Chef::Client::STDOUT_FD", stdout) + stub_const("Chef::Client::STDERR_FD", stderr) + + stub_for_register + stub_for_node_load + stub_for_sync_cookbooks + stub_for_converge + stub_for_audit + stub_for_node_save + + expect_any_instance_of(Chef::RunLock).to receive(:acquire) + expect_any_instance_of(Chef::RunLock).to receive(:save_pid) + expect_any_instance_of(Chef::RunLock).to receive(:release) + + # Post conditions: check that node has been filled in correctly + expect(client).to receive(:run_started) + + stub_for_run + end +end + +shared_context "converge completed" do + def stub_for_converge + # --Client#converge + expect(Chef::Runner).to receive(:new).and_return(runner) + expect(runner).to receive(:converge).and_return(true) + end + + def stub_for_node_save + allow(node).to receive(:data_for_save).and_return(node.for_json) + + # --Client#save_updated_node + expect(Chef::REST).to receive(:new).with(Chef::Config[:chef_server_url], fqdn, Chef::Config[:client_key], validate_utf8: false).and_return(http_node_save) + expect(http_node_save).to receive(:put_rest).with("nodes/#{fqdn}", node.for_json).and_return(true) + end +end + +shared_context "converge failed" do + let(:converge_error) do + err = Chef::Exceptions::UnsupportedAction.new("Action unsupported") + err.set_backtrace([ "/path/recipe.rb:15", "/path/recipe.rb:12" ]) + err + end + + def stub_for_converge + expect(Chef::Runner).to receive(:new).and_return(runner) + expect(runner).to receive(:converge).and_raise(converge_error) + end + + def stub_for_node_save + expect(client).to_not receive(:save_updated_node) + end +end + +shared_context "audit phase completed" do + def stub_for_audit + # -- Client#run_audits + expect(Chef::Audit::Runner).to receive(:new).and_return(audit_runner) + expect(audit_runner).to receive(:run).and_return(true) + expect(client.events).to receive(:audit_phase_complete) + end +end + +shared_context "audit phase failed with error" do + let(:audit_error) do + err = RuntimeError.new("Unexpected audit error") + err.set_backtrace([ "/path/recipe.rb:57", "/path/recipe.rb:55" ]) + err + end + + def stub_for_audit + expect(Chef::Audit::Runner).to receive(:new).and_return(audit_runner) + expect(audit_runner).to receive(:run).and_raise(audit_error) + expect(client.events).to receive(:audit_phase_failed).with(audit_error) + end +end + +shared_context "audit phase completed with failed controls" do + let(:audit_runner) { instance_double("Chef::Audit::Runner", :failed? => true, + :num_failed => 1, :num_total => 3) } + + let(:audit_error) do + err = Chef::Exceptions::AuditsFailed.new(audit_runner.num_failed, audit_runner.num_total) + err.set_backtrace([ "/path/recipe.rb:108", "/path/recipe.rb:103" ]) + err + end + + def stub_for_audit + expect(Chef::Audit::Runner).to receive(:new).and_return(audit_runner) + expect(audit_runner).to receive(:run) + expect(Chef::Exceptions::AuditsFailed).to receive(:new).with( + audit_runner.num_failed, audit_runner.num_total + ).and_return(audit_error) + expect(client.events).to receive(:audit_phase_failed).with(audit_error) + end +end + +shared_context "run completed" do + def stub_for_run + expect(client).to receive(:run_completed_successfully) + + # --ResourceReporter#run_completed + # updates the server with the resource history + # (has its own tests, so stubbing it here.) + expect_any_instance_of(Chef::ResourceReporter).to receive(:run_completed) + # --AuditReporter#run_completed + # posts the audit data to server. + # (has its own tests, so stubbing it here.) + expect_any_instance_of(Chef::Audit::AuditReporter).to receive(:run_completed) + end +end + +shared_context "run failed" do + def stub_for_run + expect(client).to receive(:run_failed) + + # --ResourceReporter#run_completed + # updates the server with the resource history + # (has its own tests, so stubbing it here.) + expect_any_instance_of(Chef::ResourceReporter).to receive(:run_failed) + # --AuditReporter#run_completed + # posts the audit data to server. + # (has its own tests, so stubbing it here.) + expect_any_instance_of(Chef::Audit::AuditReporter).to receive(:run_failed) + end + + before do + expect(Chef::Application).to receive(:debug_stacktrace).with an_instance_of(Chef::Exceptions::RunFailedWrappingError) + end +end diff --git a/spec/support/shared/examples/client.rb b/spec/support/shared/examples/client.rb new file mode 100644 index 0000000000..330cb40ac6 --- /dev/null +++ b/spec/support/shared/examples/client.rb @@ -0,0 +1,53 @@ + +require 'spec_helper' +require 'spec/support/shared/context/client' + +# requires platform and platform_version be defined +shared_examples "a completed run" do + include_context "run completed" + + it "runs ohai, sets up authentication, loads node state, synchronizes policy, converges, and runs audits" do + # This is what we're testing. + expect(client.run).to be true + + # fork is stubbed, so we can see the outcome of the run + expect(node.automatic_attrs[:platform]).to eq(platform) + expect(node.automatic_attrs[:platform_version]).to eq(platform_version) + end +end + +shared_examples "a completed run with audit failure" do + include_context "run completed" + + before do + expect(Chef::Application).to receive(:debug_stacktrace).with an_instance_of(Chef::Exceptions::RunFailedWrappingError) + end + + it "converges, runs audits, saves the node and raises the error in a wrapping error" do + expect{ client.run }.to raise_error(Chef::Exceptions::RunFailedWrappingError) do |error| + expect(error.wrapped_errors.size).to eq(run_errors.size) + run_errors.each do |run_error| + expect(error.wrapped_errors).to include(run_error) + expect(error.backtrace).to include(*run_error.backtrace) + end + end + + # fork is stubbed, so we can see the outcome of the run + expect(node.automatic_attrs[:platform]).to eq(platform) + expect(node.automatic_attrs[:platform_version]).to eq(platform_version) + end +end + +shared_examples "a failed run" do + include_context "run failed" + + it "skips node save and raises the error in a wrapping error" do + expect{ client.run }.to raise_error(Chef::Exceptions::RunFailedWrappingError) do |error| + expect(error.wrapped_errors.size).to eq(run_errors.size) + run_errors.each do |run_error| + expect(error.wrapped_errors).to include(run_error) + expect(error.backtrace).to include(*run_error.backtrace) + end + end + end +end diff --git a/spec/support/shared/functional/file_resource.rb b/spec/support/shared/functional/file_resource.rb index 4f8e2f5b71..3ce3c9c94e 100644 --- a/spec/support/shared/functional/file_resource.rb +++ b/spec/support/shared/functional/file_resource.rb @@ -592,10 +592,6 @@ shared_examples_for "a configured file resource" do File.open(path, "wb") { |f| f.write(wrong_content) } end - it "updates the source file content" do - skip - end - it "marks the resource as updated" do resource.run_action(:create) expect(resource).to be_updated_by_last_action diff --git a/spec/support/shared/functional/securable_resource.rb b/spec/support/shared/functional/securable_resource.rb index bde508b14d..b3c32356aa 100644 --- a/spec/support/shared/functional/securable_resource.rb +++ b/spec/support/shared/functional/securable_resource.rb @@ -163,9 +163,6 @@ shared_examples_for "a securable resource with existing target" do let(:desired_gid) { 1337 } let(:expected_gid) { 1337 } - skip "should set an owner (Rerun specs under root)", :requires_unprivileged_user => true - skip "should set a group (Rerun specs under root)", :requires_unprivileged_user => true - describe "when setting the owner", :requires_root do before do resource.owner expected_user_name @@ -205,11 +202,6 @@ shared_examples_for "a securable resource with existing target" do resource.run_action(:create) end - it "should set permissions as specified" do - pending("Linux does not support lchmod") - expect{ File.lstat(path).mode & 007777 }.to eq(@mode_string.oct & 007777) - end - it "is marked as updated only if changes are made" do expect(resource.updated_by_last_action?).to eq(expect_updated?) end @@ -222,11 +214,6 @@ shared_examples_for "a securable resource with existing target" do resource.run_action(:create) end - it "should set permissions in numeric form as a ruby-interpreted octal" do - pending('Linux does not support lchmod') - expect{ File.lstat(path).mode & 007777 }.to eq(@mode_integer & 007777) - end - it "is marked as updated only if changes are made" do expect(resource.updated_by_last_action?).to eq(expect_updated?) end @@ -306,10 +293,6 @@ shared_examples_for "a securable resource without existing target" do include_context "diff disabled" - context "on Unix", :unix_only do - skip "if we need any securable resource tests on Unix without existing target resource." - end - context "on Windows", :windows_only do include_context "use Windows permissions" @@ -366,13 +349,6 @@ shared_examples_for "a securable resource without existing target" do expect { resource.group 'Lance "The Nose" Glindenberry III' }.to raise_error(Chef::Exceptions::ValidationFailed) end - it "sets group when group is specified with a \\" do - pending("Need to find a group containing a backslash that is on most peoples' machines") - resource.group "#{ENV['COMPUTERNAME']}\\Administrators" - resource.run_action(:create) - expect{ descriptor.group }.to eq(SID.Everyone) - end - it "leaves group alone if group is not specified and resource already exists" do arbitrary_non_default_group = SID.Everyone expect(arbitrary_non_default_group).not_to eq(SID.default_security_object_group) diff --git a/spec/support/shared/functional/securable_resource_with_reporting.rb b/spec/support/shared/functional/securable_resource_with_reporting.rb index 37fc538801..3176ebba0d 100644 --- a/spec/support/shared/functional/securable_resource_with_reporting.rb +++ b/spec/support/shared/functional/securable_resource_with_reporting.rb @@ -279,14 +279,14 @@ shared_examples_for "a securable resource with reporting" do end it "has empty values for file metadata in 'current_resource'" do - pending "windows reporting not yet fully supported" + skip "windows reporting not yet fully supported" expect(current_resource.owner).to be_nil expect(current_resource.expanded_rights).to be_nil end context "and no security metadata is specified in new_resource" do before do - pending "windows reporting not yet fully supported" + skip "windows reporting not yet fully supported" end it "sets the metadata values on the new_resource as strings after creating" do @@ -322,7 +322,7 @@ shared_examples_for "a securable resource with reporting" do let(:expected_user_name) { 'domain\user' } before do - pending "windows reporting not yet fully supported" + skip "windows reporting not yet fully supported" resource.owner(expected_user_name) resource.run_action(:create) end @@ -336,7 +336,7 @@ shared_examples_for "a securable resource with reporting" do context "when the target file exists" do before do - pending "windows reporting not yet fully supported" + skip "windows reporting not yet fully supported" FileUtils.touch(resource.path) resource.action(:create) end diff --git a/spec/support/shared/functional/windows_script.rb b/spec/support/shared/functional/windows_script.rb index 35b86dc4e8..3499cc98ec 100644 --- a/spec/support/shared/functional/windows_script.rb +++ b/spec/support/shared/functional/windows_script.rb @@ -114,7 +114,7 @@ shared_context Chef::Resource::WindowsScript do describe "when the run action is invoked on Windows" do it "executes the script code" do - resource.code("@whoami > #{script_output_path}") + resource.code("whoami > #{script_output_path}") resource.returns(0) resource.run_action(:run) end diff --git a/spec/unit/audit/audit_reporter_spec.rb b/spec/unit/audit/audit_reporter_spec.rb index 4bf889510a..75c96155da 100644 --- a/spec/unit/audit/audit_reporter_spec.rb +++ b/spec/unit/audit/audit_reporter_spec.rb @@ -88,6 +88,29 @@ describe Chef::Audit::AuditReporter do reporter.run_completed(node) end + context "when audit phase failed" do + + let(:audit_error) { double("AuditError", :class => "Chef::Exceptions::AuditError", + :message => "Audit phase failed with error message: derpderpderp", + :backtrace => ["/path/recipe.rb:57", "/path/library.rb:106"]) } + + before do + reporter.instance_variable_set(:@audit_phase_error, audit_error) + end + + it "reports an error" do + reporter.run_completed(node) + expect(run_data).to have_key(:error) + expect(run_data).to have_key(:error) + expect(run_data[:error]).to eq <<-EOM.strip! +Chef::Exceptions::AuditError: Audit phase failed with error message: derpderpderp +/path/recipe.rb:57 +/path/library.rb:106 +EOM + end + + end + context "when unable to post to server" do let(:error) do @@ -215,9 +238,13 @@ describe Chef::Audit::AuditReporter do let(:audit_data) { Chef::Audit::AuditData.new(node.name, run_id) } let(:run_data) { audit_data.to_hash } - let(:error) { double("AuditError", :class => "Chef::Exception::AuditError", - :message => "Well that certainly didn't work", - :backtrace => ["line 0", "line 1", "line 2"]) } + let(:audit_error) { double("AuditError", :class => "Chef::Exceptions::AuditError", + :message => "Audit phase failed with error message: derpderpderp", + :backtrace => ["/path/recipe.rb:57", "/path/library.rb:106"]) } + + let(:run_error) { double("RunError", :class => "Chef::Exceptions::RunError", + :message => "This error shouldn't be reported.", + :backtrace => ["fix it", "fix it", "fix it"]) } before do allow(reporter).to receive(:auditing_enabled?).and_return(true) @@ -226,15 +253,32 @@ describe Chef::Audit::AuditReporter do allow(audit_data).to receive(:to_hash).and_return(run_data) end - it "adds the error information to the reported data" do - expect(rest).to receive(:create_url) - expect(rest).to receive(:post) - reporter.run_failed(error) - expect(run_data).to have_key(:error) - expect(run_data[:error]).to eq "Chef::Exception::AuditError: Well that certainly didn't work\n" + - "line 0\nline 1\nline 2" + context "when no prior exception is stored" do + it "reports no error" do + expect(rest).to receive(:create_url) + expect(rest).to receive(:post) + reporter.run_failed(run_error) + expect(run_data).to_not have_key(:error) + end end + context "when some prior exception is stored" do + before do + reporter.instance_variable_set(:@audit_phase_error, audit_error) + end + + it "reports the prior error" do + expect(rest).to receive(:create_url) + expect(rest).to receive(:post) + reporter.run_failed(run_error) + expect(run_data).to have_key(:error) + expect(run_data[:error]).to eq <<-EOM.strip! +Chef::Exceptions::AuditError: Audit phase failed with error message: derpderpderp +/path/recipe.rb:57 +/path/library.rb:106 +EOM + end + end end shared_context "audit data" do diff --git a/spec/unit/chef_fs/file_pattern_spec.rb b/spec/unit/chef_fs/file_pattern_spec.rb index a9f06e8424..ed5f314605 100644 --- a/spec/unit/chef_fs/file_pattern_spec.rb +++ b/spec/unit/chef_fs/file_pattern_spec.rb @@ -157,7 +157,7 @@ describe Chef::ChefFS::FilePattern do end end - context 'with simple pattern "a\*\b"', :pending => (Chef::Platform.windows?) do + context 'with simple pattern "a\*\b"', :skip => (Chef::Platform.windows?) do let(:pattern) { Chef::ChefFS::FilePattern.new('a\*\b') } it 'match?' do expect(pattern.match?('a*b')).to be_truthy @@ -264,7 +264,7 @@ describe Chef::ChefFS::FilePattern do end end - context 'with star pattern "/abc/d[a-z][0-9]f/ghi"', :pending => (Chef::Platform.windows?) do + context 'with star pattern "/abc/d[a-z][0-9]f/ghi"', :skip => (Chef::Platform.windows?) do let(:pattern) { Chef::ChefFS::FilePattern.new('/abc/d[a-z][0-9]f/ghi') } it 'match?' do expect(pattern.match?('/abc/de1f/ghi')).to be_truthy @@ -352,11 +352,7 @@ describe Chef::ChefFS::FilePattern do expect(pattern.could_match_children?('/abc/def/ghi')).to be_truthy expect(pattern.could_match_children?('abc')).to be_falsey end - it 'could_match_children? /abc** returns false for /xyz' do - pending 'Make could_match_children? more rigorous' - # At the moment, we return false for this, but in the end it would be nice to return true: - expect(pattern.could_match_children?('/xyz')).to be_falsey - end + it 'exact_child_name_under' do expect(pattern.exact_child_name_under('/')).to eq(nil) expect(pattern.exact_child_name_under('/abc')).to eq(nil) @@ -440,14 +436,6 @@ describe Chef::ChefFS::FilePattern do expect(p('/.').exact_path).to eq('/') expect(p('/.').match?('/')).to be_truthy end - it 'handles dot by itself', :pending => "decide what to do with dot by itself" do - expect(p('.').normalized_pattern).to eq('.') - expect(p('.').exact_path).to eq('.') - expect(p('.').match?('.')).to be_truthy - expect(p('./').normalized_pattern).to eq('.') - expect(p('./').exact_path).to eq('.') - expect(p('./').match?('.')).to be_truthy - end it 'handles dotdot' do expect(p('abc/../def').normalized_pattern).to eq('def') expect(p('abc/../def').exact_path).to eq('def') diff --git a/spec/unit/client_spec.rb b/spec/unit/client_spec.rb index d8c4ede796..1e4bbb5c56 100644 --- a/spec/unit/client_spec.rb +++ b/spec/unit/client_spec.rb @@ -19,6 +19,8 @@ # require 'spec_helper' +require 'spec/support/shared/context/client' +require 'spec/support/shared/examples/client' require 'chef/run_context' require 'chef/rest' @@ -28,55 +30,7 @@ class FooError < RuntimeError end describe Chef::Client do - - let(:hostname) { "hostname" } - let(:machinename) { "machinename.example.org" } - let(:fqdn) { "hostname.example.org" } - - let(:ohai_data) do - { :fqdn => fqdn, - :hostname => hostname, - :machinename => machinename, - :platform => 'example-platform', - :platform_version => 'example-platform-1.0', - :data => {} - } - end - - let(:ohai_system) do - ohai_system = double( "Ohai::System", - :all_plugins => true, - :data => ohai_data) - allow(ohai_system).to receive(:[]) do |key| - ohai_data[key] - end - ohai_system - end - - let(:node) do - Chef::Node.new.tap do |n| - n.name(fqdn) - n.chef_environment("_default") - end - end - - let(:json_attribs) { nil } - let(:client_opts) { {} } - - let(:client) do - Chef::Config[:event_loggers] = [] - Chef::Client.new(json_attribs, client_opts).tap do |c| - c.node = node - end - end - - before do - Chef::Log.logger = Logger.new(StringIO.new) - - # Node/Ohai data - #Chef::Config[:node_name] = fqdn - allow(Ohai::System).to receive(:new).and_return(ohai_system) - end + include_context "client" context "when minimal ohai is configured" do before do @@ -88,7 +42,6 @@ describe Chef::Client do expect(ohai_system).to receive(:all_plugins).with(expected_filter) client.run_ohai end - end describe "authentication protocol selection" do @@ -117,7 +70,6 @@ describe Chef::Client do describe "configuring output formatters" do context "when no formatter has been configured" do - context "and STDOUT is a TTY" do before do allow(STDOUT).to receive(:tty?).and_return(true) @@ -203,135 +155,12 @@ describe Chef::Client do end describe "a full client run" do - shared_context "a client run" do - let(:http_node_load) { double("Chef::REST (node)") } - let(:http_cookbook_sync) { double("Chef::REST (cookbook sync)") } - let(:http_node_save) { double("Chef::REST (node save)") } - let(:runner) { double("Chef::Runner") } - let(:audit_runner) { instance_double("Chef::Audit::Runner", :failed? => false) } - - let(:api_client_exists?) { false } - - let(:stdout) { StringIO.new } - let(:stderr) { StringIO.new } - - let(:enable_fork) { false } - - def stub_for_register - # --Client.register - # Make sure Client#register thinks the client key doesn't - # exist, so it tries to register and create one. - allow(File).to receive(:exists?).and_call_original - expect(File).to receive(:exists?). - with(Chef::Config[:client_key]). - exactly(:once). - and_return(api_client_exists?) - - unless api_client_exists? - # Client.register will register with the validation client name. - expect_any_instance_of(Chef::ApiClient::Registration).to receive(:run) - end - end - - def stub_for_node_load - # Client.register will then turn around create another - # Chef::REST object, this time with the client key it got from the - # previous step. - expect(Chef::REST).to receive(:new). - with(Chef::Config[:chef_server_url], fqdn, Chef::Config[:client_key]). - exactly(:once). - and_return(http_node_load) - - # --Client#build_node - # looks up the node, which we will return, then later saves it. - expect(Chef::Node).to receive(:find_or_create).with(fqdn).and_return(node) - - # --ResourceReporter#node_load_completed - # gets a run id from the server for storing resource history - # (has its own tests, so stubbing it here.) - expect_any_instance_of(Chef::ResourceReporter).to receive(:node_load_completed) - end - - def stub_for_sync_cookbooks - # --Client#setup_run_context - # ---Client#sync_cookbooks -- downloads the list of cookbooks to sync - # - expect_any_instance_of(Chef::CookbookSynchronizer).to receive(:sync_cookbooks) - expect(Chef::REST).to receive(:new).with(Chef::Config[:chef_server_url]).and_return(http_cookbook_sync) - expect(http_cookbook_sync).to receive(:post). - with("environments/_default/cookbook_versions", {:run_list => []}). - and_return({}) - end - - def stub_for_converge - # --Client#converge - expect(Chef::Runner).to receive(:new).and_return(runner) - expect(runner).to receive(:converge).and_return(true) - end - - def stub_for_audit - # -- Client#run_audits - expect(Chef::Audit::Runner).to receive(:new).and_return(audit_runner) - expect(audit_runner).to receive(:run).and_return(true) - end - - def stub_for_node_save - allow(node).to receive(:data_for_save).and_return(node.for_json) - - # --Client#save_updated_node - expect(Chef::REST).to receive(:new).with(Chef::Config[:chef_server_url], fqdn, Chef::Config[:client_key], validate_utf8: false).and_return(http_node_save) - expect(http_node_save).to receive(:put_rest).with("nodes/#{fqdn}", node.for_json).and_return(true) - end - - def stub_for_run - expect_any_instance_of(Chef::RunLock).to receive(:acquire) - expect_any_instance_of(Chef::RunLock).to receive(:save_pid) - expect_any_instance_of(Chef::RunLock).to receive(:release) - - # Post conditions: check that node has been filled in correctly - expect(client).to receive(:run_started) - expect(client).to receive(:run_completed_successfully) - - # --ResourceReporter#run_completed - # updates the server with the resource history - # (has its own tests, so stubbing it here.) - expect_any_instance_of(Chef::ResourceReporter).to receive(:run_completed) - # --AuditReporter#run_completed - # posts the audit data to server. - # (has its own tests, so stubbing it here.) - expect_any_instance_of(Chef::Audit::AuditReporter).to receive(:run_completed) - end - - before do - Chef::Config[:client_fork] = enable_fork - Chef::Config[:cache_path] = windows? ? 'C:\chef' : '/var/chef' - Chef::Config[:why_run] = false - Chef::Config[:audit_mode] = :enabled - - stub_const("Chef::Client::STDOUT_FD", stdout) - stub_const("Chef::Client::STDERR_FD", stderr) - - stub_for_register - stub_for_node_load - stub_for_sync_cookbooks - stub_for_converge - stub_for_audit - stub_for_node_save - stub_for_run - end - end - shared_examples_for "a successful client run" do include_context "a client run" + include_context "converge completed" + include_context "audit phase completed" - it "runs ohai, sets up authentication, loads node state, synchronizes policy, converges, and runs audits" do - # This is what we're testing. - client.run - - # fork is stubbed, so we can see the outcome of the run - expect(node.automatic_attrs[:platform]).to eq("example-platform") - expect(node.automatic_attrs[:platform_version]).to eq("example-platform-1.0") - end + include_examples "a completed run" end describe "when running chef-client without fork" do @@ -339,24 +168,19 @@ describe Chef::Client do end describe "when the client key already exists" do - let(:api_client_exists?) { true } - include_examples "a successful client run" + include_examples "a successful client run" do + let(:api_client_exists?) { true } + end end - describe "when an override run list is given" do - let(:client_opts) { {:override_runlist => "recipe[override_recipe]"} } - - it "should permit spaces in overriding run list" do + context "when an override run list is given" do + it "permits spaces in overriding run list" do Chef::Client.new(nil, :override_runlist => 'role[a], role[b]') end - describe "when running the client" do + describe "calling run" do include_examples "a successful client run" do - - before do - # Client will try to compile and run override_recipe - expect_any_instance_of(Chef::RunContext::CookbookCompiler).to receive(:compile) - end + let(:client_opts) { {:override_runlist => "recipe[override_recipe]"} } def stub_for_sync_cookbooks # --Client#setup_run_context @@ -373,13 +197,22 @@ describe Chef::Client do # Expect NO node save expect(node).not_to receive(:save) end + + before do + # Client will try to compile and run override_recipe + expect_any_instance_of(Chef::RunContext::CookbookCompiler).to receive(:compile) + end end end end describe "when a permanent run list is passed as an option" do - include_examples "a successful client run" do + it "sets the new run list on the node" do + client.run + expect(node.run_list).to eq(Chef::RunList.new(new_runlist)) + end + include_examples "a successful client run" do let(:new_runlist) { "recipe[new_run_list_recipe]" } let(:client_opts) { {:runlist => new_runlist} } @@ -399,214 +232,61 @@ describe Chef::Client do # do not create a fixture for this. expect_any_instance_of(Chef::RunContext::CookbookCompiler).to receive(:compile) end - - it "sets the new run list on the node" do - client.run - expect(node.run_list).to eq(Chef::RunList.new(new_runlist)) - end end end - describe "when converge fails" do - include_context "a client run" do - let(:e) { Exception.new } - def stub_for_converge - expect(Chef::Runner).to receive(:new).and_return(runner) - expect(runner).to receive(:converge).and_raise(e) - expect(Chef::Application).to receive(:debug_stacktrace).with an_instance_of(Chef::Exceptions::RunFailedWrappingError) - end - - def stub_for_node_save - expect(client).to_not receive(:save_updated_node) - end - - def stub_for_run - expect_any_instance_of(Chef::RunLock).to receive(:acquire) - expect_any_instance_of(Chef::RunLock).to receive(:save_pid) - expect_any_instance_of(Chef::RunLock).to receive(:release) - - # Post conditions: check that node has been filled in correctly - expect(client).to receive(:run_started) - expect(client).to receive(:run_failed) - - expect_any_instance_of(Chef::ResourceReporter).to receive(:run_failed) - expect_any_instance_of(Chef::Audit::AuditReporter).to receive(:run_failed) - end - end + describe "when converge completes successfully" do + include_context "a client run" + include_context "converge completed" - it "runs the audits and raises the error" do - expect{ client.run }.to raise_error(Chef::Exceptions::RunFailedWrappingError) do |error| - expect(error.wrapped_errors.size).to eq(1) - expect(error.wrapped_errors[0]).to eq(e) + describe "when audit phase errors" do + include_context "audit phase failed with error" + include_examples "a completed run with audit failure" do + let(:run_errors) { [audit_error] } end end - end - - describe "when the audit phase fails" do - context "with an exception" do - context "when audit mode is enabled" do - include_context "a client run" do - let(:e) { Exception.new } - def stub_for_audit - expect(Chef::Audit::Runner).to receive(:new).and_return(audit_runner) - expect(audit_runner).to receive(:run).and_raise(e) - expect(Chef::Application).to receive(:debug_stacktrace).with an_instance_of(Chef::Exceptions::RunFailedWrappingError) - end - - def stub_for_run - expect_any_instance_of(Chef::RunLock).to receive(:acquire) - expect_any_instance_of(Chef::RunLock).to receive(:save_pid) - expect_any_instance_of(Chef::RunLock).to receive(:release) - - # Post conditions: check that node has been filled in correctly - expect(client).to receive(:run_started) - expect(client).to receive(:run_failed) - - expect_any_instance_of(Chef::ResourceReporter).to receive(:run_failed) - expect_any_instance_of(Chef::Audit::AuditReporter).to receive(:run_failed) - end - end - - it "should save the node after converge and raise exception" do - expect{ client.run }.to raise_error(Chef::Exceptions::RunFailedWrappingError) do |error| - expect(error.wrapped_errors.size).to eq(1) - expect(error.wrapped_errors[0]).to eq(e) - end - end - end - - context "when audit mode is disabled" do - include_context "a client run" do - before do - Chef::Config[:audit_mode] = :disabled - end - - let(:e) { FooError.new } - - def stub_for_audit - expect(Chef::Audit::Runner).to_not receive(:new) - end - - def stub_for_converge - expect(Chef::Runner).to receive(:new).and_return(runner) - expect(runner).to receive(:converge).and_raise(e) - expect(Chef::Application).to receive(:debug_stacktrace).with an_instance_of(FooError) - end - - def stub_for_node_save - expect(client).to_not receive(:save_updated_node) - end - - def stub_for_run - expect_any_instance_of(Chef::RunLock).to receive(:acquire) - expect_any_instance_of(Chef::RunLock).to receive(:save_pid) - expect_any_instance_of(Chef::RunLock).to receive(:release) - - - # Post conditions: check that node has been filled in correctly - expect(client).to receive(:run_started) - expect(client).to receive(:run_failed) - - expect_any_instance_of(Chef::ResourceReporter).to receive(:run_failed) - - end - - it "re-raises an unwrapped exception" do - expect { client.run }.to raise_error(FooError) - end - end - end - + describe "when audit phase completed" do + include_context "audit phase completed" + include_examples "a completed run" end - context "with failed audits" do - include_context "a client run" do - let(:audit_runner) do - instance_double("Chef::Audit::Runner", :run => true, :failed? => true, :num_failed => 1, :num_total => 1) - end - - def stub_for_audit - expect(Chef::Audit::Runner).to receive(:new).and_return(audit_runner) - expect(Chef::Application).to receive(:debug_stacktrace).with an_instance_of(Chef::Exceptions::RunFailedWrappingError) - end - - def stub_for_run - expect_any_instance_of(Chef::RunLock).to receive(:acquire) - expect_any_instance_of(Chef::RunLock).to receive(:save_pid) - expect_any_instance_of(Chef::RunLock).to receive(:release) - - # Post conditions: check that node has been filled in correctly - expect(client).to receive(:run_started) - expect(client).to receive(:run_failed) - - expect_any_instance_of(Chef::ResourceReporter).to receive(:run_failed) - expect_any_instance_of(Chef::Audit::AuditReporter).to receive(:run_failed) - end - end - - it "should save the node after converge and raise exception" do - expect{ client.run }.to raise_error(Chef::Exceptions::RunFailedWrappingError) do |error| - expect(error.wrapped_errors.size).to eq(1) - expect(error.wrapped_errors[0]).to be_instance_of(Chef::Exceptions::AuditsFailed) - end + describe "when audit phase completed with failed controls" do + include_context "audit phase completed with failed controls" + include_examples "a completed run with audit failure" do + let(:run_errors) { [audit_error] } end end end - describe "when why_run mode is enabled" do - include_context "a client run" do - - before do - Chef::Config[:why_run] = true - end - - def stub_for_audit - expect(Chef::Audit::Runner).to_not receive(:new) - end - - def stub_for_node_save - # This is how we should be mocking external calls - not letting it fall all the way through to the - # REST call - expect(node).to receive(:save) - end - - it "runs successfully without enabling the audit runner" do - client.run + describe "when converge errors" do + include_context "a client run" + include_context "converge failed" - # fork is stubbed, so we can see the outcome of the run - expect(node.automatic_attrs[:platform]).to eq("example-platform") - expect(node.automatic_attrs[:platform_version]).to eq("example-platform-1.0") + describe "when audit phase errors" do + include_context "audit phase failed with error" + include_examples "a failed run" do + let(:run_errors) { [converge_error, audit_error] } end end - end - - describe "when audits are disabled" do - include_context "a client run" do - - before do - Chef::Config[:audit_mode] = :disabled - end - def stub_for_audit - expect(Chef::Audit::Runner).to_not receive(:new) + describe "when audit phase completed" do + include_context "audit phase completed" + include_examples "a failed run" do + let(:run_errors) { [converge_error] } end + end - it "runs successfully without enabling the audit runner" do - client.run - - # fork is stubbed, so we can see the outcome of the run - expect(node.automatic_attrs[:platform]).to eq("example-platform") - expect(node.automatic_attrs[:platform_version]).to eq("example-platform-1.0") + describe "when audit phase completed with failed controls" do + include_context "audit phase completed with failed controls" + include_examples "a failed run" do + let(:run_errors) { [converge_error, audit_error] } end end end - end - describe "when handling run failures" do - it "should remove the run_lock on failure of #load_node" do @run_lock = double("Chef::RunLock", :acquire => true) allow(Chef::RunLock).to receive(:new).and_return(@run_lock) @@ -779,6 +459,7 @@ describe Chef::Client do Chef::Config[:solo] = true Chef::Config[:cookbook_path] = ["/path/to/invalid/cookbook_path"] end + context "when any directory of cookbook_path contains no cookbook" do it "raises CookbookNotFound error" do expect do @@ -833,7 +514,10 @@ describe Chef::Client do it "should run exception handlers on early fail" do expect(subject).to receive(:run_failed) - expect { subject.run }.to raise_error(NoMethodError) + expect { subject.run }.to raise_error(Chef::Exceptions::RunFailedWrappingError) do |error| + expect(error.wrapped_errors.size).to eq 1 + expect(error.wrapped_errors).to include(NoMethodError) + end end end end diff --git a/spec/unit/cookbook_spec.rb b/spec/unit/cookbook_spec.rb index 7b3cda2af1..f36b031309 100644 --- a/spec/unit/cookbook_spec.rb +++ b/spec/unit/cookbook_spec.rb @@ -59,15 +59,6 @@ describe Chef::CookbookVersion do expect(@cookbook.fully_qualified_recipe_names.include?("openldap::three")).to eq(true) end - it "should find a preferred file" do - skip - end - - it "should not return an unchanged preferred file" do - pending - expect(@cookbook.preferred_filename(@node, :files, 'a-filename', 'the-checksum')).to be_nil - end - it "should raise an ArgumentException if you try to load a bad recipe name" do expect { @cookbook.load_recipe("doesnt_exist", @node) }.to raise_error(ArgumentError) end diff --git a/spec/unit/cookbook_version_spec.rb b/spec/unit/cookbook_version_spec.rb index 440dd9da6c..4990aef004 100644 --- a/spec/unit/cookbook_version_spec.rb +++ b/spec/unit/cookbook_version_spec.rb @@ -306,26 +306,6 @@ describe Chef::CookbookVersion do subject(:cbv) { Chef::CookbookVersion.new("version validation", '/tmp/blah') } - describe "HTTP Resource behaviors", pending: "will be deprected when CookbookManifest API is stablized" do - - it "errors on #save_url" do - expect { cbv.save_url }.to raise_error(Chef::Exceptions::DeprecatedFeatureError) - end - - it "errors on #force_save_url" do - expect { cbv.force_save_url }.to raise_error(Chef::Exceptions::DeprecatedFeatureError) - end - - it "errors on #to_hash" do - expect { cbv.to_hash }.to raise_error(Chef::Exceptions::DeprecatedFeatureError) - end - - it "errors on #to_json" do - expect { cbv.to_json }.to raise_error(Chef::Exceptions::DeprecatedFeatureError) - end - - end - it "errors on #status and #status=" do expect { cbv.status = :wat }.to raise_error(Chef::Exceptions::DeprecatedFeatureError) expect { cbv.status }.to raise_error(Chef::Exceptions::DeprecatedFeatureError) diff --git a/spec/unit/deprecation_spec.rb b/spec/unit/deprecation_spec.rb index f824cb7c76..2e1f3c39f3 100644 --- a/spec/unit/deprecation_spec.rb +++ b/spec/unit/deprecation_spec.rb @@ -95,4 +95,59 @@ describe Chef::Deprecation do expect { test_instance.deprecated_method(10) }.to raise_error(Chef::Exceptions::DeprecatedFeatureError) end + context "When a class has deprecated_attr, _reader and _writer" do + before(:context) do + class DeprecatedAttrTest + extend Chef::Mixin::Deprecation + def initialize + @a = @r = @w = 1 + end + deprecated_attr :a, "a" + deprecated_attr_reader :r, "r" + deprecated_attr_writer :w, "w" + end + end + + it "The deprecated_attr emits warnings" do + test = DeprecatedAttrTest.new + expect { test.a = 10 }.to raise_error(Chef::Exceptions::DeprecatedFeatureError) + expect { test.a }.to raise_error(Chef::Exceptions::DeprecatedFeatureError) + end + + it "The deprecated_attr_writer emits warnings, and does not create a reader" do + test = DeprecatedAttrTest.new + expect { test.w = 10 }.to raise_error(Chef::Exceptions::DeprecatedFeatureError) + expect { test.w }.to raise_error(NoMethodError) + end + + it "The deprecated_attr_reader emits warnings, and does not create a writer" do + test = DeprecatedAttrTest.new + expect { test.r = 10 }.to raise_error(NoMethodError) + expect { test.r }.to raise_error(Chef::Exceptions::DeprecatedFeatureError) + end + + context "With deprecation warnings not throwing exceptions" do + before do + Chef::Config[:treat_deprecation_warnings_as_errors] = false + end + + it "The deprecated_attr can be written to and read from" do + test = DeprecatedAttrTest.new + test.a = 10 + expect(test.a).to eq 10 + end + + it "The deprecated_attr_reader can be read from" do + test = DeprecatedAttrTest.new + expect(test.r).to eq 1 + end + + it "The deprecated_attr_writer can be written to" do + test = DeprecatedAttrTest.new + test.w = 10 + expect(test.instance_eval { @w }).to eq 10 + end + end + end + end diff --git a/spec/unit/exceptions_spec.rb b/spec/unit/exceptions_spec.rb index d35ecc8ec8..fd90aeab71 100644 --- a/spec/unit/exceptions_spec.rb +++ b/spec/unit/exceptions_spec.rb @@ -113,7 +113,7 @@ describe Chef::Exceptions do context "initialized with 1 error and nil" do let(:e) { Chef::Exceptions::RunFailedWrappingError.new(RuntimeError.new("foo"), nil) } let(:num_errors) { 1 } - let(:backtrace) { ["1) RuntimeError - foo", ""] } + let(:backtrace) { ["1) RuntimeError - foo"] } include_examples "RunFailedWrappingError expectations" end @@ -121,7 +121,7 @@ describe Chef::Exceptions do context "initialized with 2 errors" do let(:e) { Chef::Exceptions::RunFailedWrappingError.new(RuntimeError.new("foo"), RuntimeError.new("bar")) } let(:num_errors) { 2 } - let(:backtrace) { ["1) RuntimeError - foo", "", "2) RuntimeError - bar", ""] } + let(:backtrace) { ["1) RuntimeError - foo", "", "2) RuntimeError - bar"] } include_examples "RunFailedWrappingError expectations" end diff --git a/spec/unit/formatters/doc_spec.rb b/spec/unit/formatters/doc_spec.rb new file mode 100644 index 0000000000..d018207f49 --- /dev/null +++ b/spec/unit/formatters/doc_spec.rb @@ -0,0 +1,46 @@ +# +# Author:: Daniel DeLeo (<dan@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 'spec_helper' + +describe Chef::Formatters::Base do + + let(:out) { StringIO.new } + let(:err) { StringIO.new } + + subject(:formatter) { Chef::Formatters::Doc.new(out, err) } + + it "prints a policyfile's name and revision ID" do + minimal_policyfile = { + "revision_id"=> "613f803bdd035d574df7fa6da525b38df45a74ca82b38b79655efed8a189e073", + "name"=> "jenkins", + "run_list"=> [ + "recipe[apt::default]", + "recipe[java::default]", + "recipe[jenkins::master]", + "recipe[policyfile_demo::default]" + ], + "cookbook_locks"=> { } + } + + formatter.policyfile_loaded(minimal_policyfile) + expect(out.string).to include("Using policy 'jenkins' at revision '613f803bdd035d574df7fa6da525b38df45a74ca82b38b79655efed8a189e073'") + end + +end diff --git a/spec/unit/knife/core/subcommand_loader_spec.rb b/spec/unit/knife/core/subcommand_loader_spec.rb index 76ebf154db..219a1f2906 100644 --- a/spec/unit/knife/core/subcommand_loader_spec.rb +++ b/spec/unit/knife/core/subcommand_loader_spec.rb @@ -22,14 +22,14 @@ describe Chef::Knife::SubcommandLoader do let(:loader) { Chef::Knife::SubcommandLoader.new(File.join(CHEF_SPEC_DATA, 'knife-site-subcommands')) } let(:home) { File.join(CHEF_SPEC_DATA, 'knife-home') } let(:plugin_dir) { File.join(home, '.chef', 'plugins', 'knife') } - + before do allow(ChefConfig).to receive(:windows?) { false } - Chef::Util::PathHelper.class_variable_set(:@@home_dir, home) + Chef::Util::PathHelper.class_variable_set(:@@home_dir, home) end after do - Chef::Util::PathHelper.class_variable_set(:@@home_dir, nil) + Chef::Util::PathHelper.class_variable_set(:@@home_dir, nil) end it "builds a list of the core subcommand file require paths" do @@ -106,6 +106,18 @@ describe Chef::Knife::SubcommandLoader do # Chef 12.0.0.rc.0 gem also: "/opt/chefdk/embedded/lib/ruby/gems/2.1.0/gems/chef-#{Chef::VERSION}.rc.0/lib/chef/knife/thing.rb", + # Test that we ignore the platform suffix when checking for different + # gem versions. + "/opt/chefdk/embedded/lib/ruby/gems/2.1.0/gems/chef-#{Chef::VERSION}-x86-mingw32/lib/chef/knife/valid.rb", + "/opt/chefdk/embedded/lib/ruby/gems/2.1.0/gems/chef-#{Chef::VERSION}-i386-mingw64/lib/chef/knife/valid-too.rb", + "/opt/chefdk/embedded/lib/ruby/gems/2.1.0/gems/chef-#{Chef::VERSION}-mswin32/lib/chef/knife/also-valid.rb", + "/opt/chefdk/embedded/lib/ruby/gems/2.1.0/gems/chef-#{Chef::VERSION}-universal-mingw32/lib/chef/knife/universal-is-valid.rb", + # ...but don't ignore the .rc / .dev parts in the case when we have + # platform suffixes + "/opt/chefdk/embedded/lib/ruby/gems/2.1.0/gems/chef-#{Chef::VERSION}.rc.0-x86-mingw32/lib/chef/knife/invalid.rb", + "/opt/chefdk/embedded/lib/ruby/gems/2.1.0/gems/chef-#{Chef::VERSION}.dev-mswin32/lib/chef/knife/invalid-too.rb", + "/opt/chefdk/embedded/lib/ruby/gems/2.1.0/gems/chef-#{Chef::VERSION}.dev.0-x86-mingw64/lib/chef/knife/still-invalid.rb", + # This command is "extra" compared to what's in the embedded/apps/chef install: "/opt/chefdk/embedded/lib/ruby/gems/2.1.0/gems/chef-1.0.0/lib/chef/knife/data_bag_secret_options.rb", "/opt/chefdk/embedded/lib/ruby/gems/2.1.0/gems/chef-vault-2.2.4/lib/chef/knife/decrypt.rb", @@ -133,6 +145,10 @@ describe Chef::Knife::SubcommandLoader do "/opt/chefdk/embedded/apps/chef/lib/chef/knife/bootstrap.rb", "/opt/chefdk/embedded/apps/chef/lib/chef/knife/client_bulk_delete.rb", "/opt/chefdk/embedded/apps/chef/lib/chef/knife/client_create.rb", + "/opt/chefdk/embedded/lib/ruby/gems/2.1.0/gems/chef-#{Chef::VERSION}-x86-mingw32/lib/chef/knife/valid.rb", + "/opt/chefdk/embedded/lib/ruby/gems/2.1.0/gems/chef-#{Chef::VERSION}-i386-mingw64/lib/chef/knife/valid-too.rb", + "/opt/chefdk/embedded/lib/ruby/gems/2.1.0/gems/chef-#{Chef::VERSION}-mswin32/lib/chef/knife/also-valid.rb", + "/opt/chefdk/embedded/lib/ruby/gems/2.1.0/gems/chef-#{Chef::VERSION}-universal-mingw32/lib/chef/knife/universal-is-valid.rb", "/opt/chefdk/embedded/lib/ruby/gems/2.1.0/gems/chef-vault-2.2.4/lib/chef/knife/decrypt.rb", "/opt/chefdk/embedded/lib/ruby/gems/2.1.0/gems/knife-spork-1.4.1/lib/chef/knife/spork-bump.rb", "/opt/chefdk/embedded/lib/ruby/gems/2.1.0/gems/chef-foo-#{Chef::VERSION}/lib/chef/knife/chef-foo.rb", diff --git a/spec/unit/knife/core/ui_spec.rb b/spec/unit/knife/core/ui_spec.rb index 4f48d4ff0d..ab420518a3 100644 --- a/spec/unit/knife/core/ui_spec.rb +++ b/spec/unit/knife/core/ui_spec.rb @@ -368,6 +368,20 @@ EOM @ui.config[:attribute] = "keys.keys" expect(@ui.format_for_display(input)).to eq({ "sample-data-bag-item" => { "keys.keys" => "values" } }) end + + it "should return the name attribute" do + allow_any_instance_of(Chef::Node).to receive(:name).and_return("chef.localdomain") + input = Chef::Node.new + @ui.config[:attribute] = "name" + expect(@ui.format_for_display(input)).to eq( {"chef.localdomain"=>{"name"=>"chef.localdomain"} }) + end + + it "returns nil when given an attribute path that isn't a name or attribute" do + input = { "keys" => {"keys" => "values"}, "hi" => "ho", "id" => "sample-data-bag-item" } + non_existing_path = "nope.nada.nothingtoseehere" + @ui.config[:attribute] = non_existing_path + expect(@ui.format_for_display(input)).to eq({ "sample-data-bag-item" => { non_existing_path => nil } }) + end end describe "with --run-list passed" do diff --git a/spec/unit/mixin/command_spec.rb b/spec/unit/mixin/command_spec.rb index e198e3addd..050b261256 100644 --- a/spec/unit/mixin/command_spec.rb +++ b/spec/unit/mixin/command_spec.rb @@ -22,7 +22,7 @@ describe Chef::Mixin::Command, :volatile do if windows? - pending("TODO MOVE: this is a platform specific integration test.") + skip("TODO MOVE: this is a platform specific integration test.") else @@ -61,7 +61,6 @@ describe Chef::Mixin::Command, :volatile do it "returns immediately after the first child process exits" do expect {Timeout.timeout(10) do - pid, stdin,stdout,stderr = nil,nil,nil,nil evil_forker="exit if fork; 10.times { sleep 1}" popen4("ruby -e '#{evil_forker}'") do |pid,stdin,stdout,stderr| end diff --git a/spec/unit/mixin/powershell_out_spec.rb b/spec/unit/mixin/powershell_out_spec.rb new file mode 100644 index 0000000000..0fede582fa --- /dev/null +++ b/spec/unit/mixin/powershell_out_spec.rb @@ -0,0 +1,70 @@ +# +# 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 'spec_helper' +require 'chef/mixin/powershell_out' + +describe Chef::Mixin::PowershellOut do + let(:shell_out_class) { Class.new { include Chef::Mixin::PowershellOut } } + subject(:object) { shell_out_class.new } + let(:architecture) { "something" } + let(:flags) { + "-NoLogo -NonInteractive -NoProfile -ExecutionPolicy Unrestricted -InputFormat None" + } + + describe "#powershell_out" do + it "runs a command and returns the shell_out object" do + ret = double("Mixlib::ShellOut") + expect(object).to receive(:shell_out).with( + "powershell.exe #{flags} -Command \"Get-Process\"", + {} + ).and_return(ret) + expect(object.powershell_out("Get-Process")).to eql(ret) + end + + it "passes options" do + ret = double("Mixlib::ShellOut") + expect(object).to receive(:shell_out).with( + "powershell.exe #{flags} -Command \"Get-Process\"", + timeout: 600 + ).and_return(ret) + expect(object.powershell_out("Get-Process", timeout: 600)).to eql(ret) + end + end + + describe "#powershell_out!" do + it "runs a command and returns the shell_out object" do + mixlib_shellout = double("Mixlib::ShellOut") + expect(object).to receive(:shell_out).with( + "powershell.exe #{flags} -Command \"Get-Process\"", + {} + ).and_return(mixlib_shellout) + expect(mixlib_shellout).to receive(:error!) + expect(object.powershell_out!("Get-Process")).to eql(mixlib_shellout) + end + + it "passes options" do + mixlib_shellout = double("Mixlib::ShellOut") + expect(object).to receive(:shell_out).with( + "powershell.exe #{flags} -Command \"Get-Process\"", + timeout: 600 + ).and_return(mixlib_shellout) + expect(mixlib_shellout).to receive(:error!) + expect(object.powershell_out!("Get-Process", timeout: 600)).to eql(mixlib_shellout) + end + end +end diff --git a/spec/unit/provider/directory_spec.rb b/spec/unit/provider/directory_spec.rb index 6489f2ca50..38d6db8320 100644 --- a/spec/unit/provider/directory_spec.rb +++ b/spec/unit/provider/directory_spec.rb @@ -16,173 +16,237 @@ # limitations under the License. # -require 'ostruct' - require 'spec_helper' require 'tmpdir' describe Chef::Provider::Directory do - before(:each) do - @new_resource = Chef::Resource::Directory.new(Dir.tmpdir) - if !windows? - @new_resource.owner(500) - @new_resource.group(500) - @new_resource.mode(0644) + let(:tmp_dir) { Dir.mktmpdir } + let(:new_resource) { Chef::Resource::Directory.new(tmp_dir) } + let(:node) { Chef::Node.new } + let(:events) { Chef::EventDispatch::Dispatcher.new } + let(:run_context) { Chef::RunContext.new(node, {}, events) } + let(:directory) { Chef::Provider::Directory.new(new_resource, run_context) } + + describe "#load_current_resource" do + describe "scanning file security metadata" + describe "on unix", unix_only: true do + describe "when the directory exists" do + let(:dir_stat) { File::Stat.new(tmp_dir) } + let(:expected_uid) { dir_stat.uid } + let(:expected_gid) { dir_stat.gid } + let(:expected_mode) { "0%o" % ( dir_stat.mode & 007777 ) } + let(:expected_pwnam) { Etc.getpwuid(expected_uid).name } + let(:expected_grnam) { Etc.getgrgid(expected_gid).name } + + it "describes the access mode as a String of octal integers" do + directory.load_current_resource + expect(directory.current_resource.mode).to eq(expected_mode) + end + + it "when the new_resource.owner is numeric, describes the owner as a numeric uid" do + new_resource.owner(500) + directory.load_current_resource + expect(directory.current_resource.owner).to eql(expected_uid) + end + + it "when the new_resource.group is numeric, describes the group as a numeric gid" do + new_resource.group(500) + directory.load_current_resource + expect(directory.current_resource.group).to eql(expected_gid) + end + + it "when the new_resource.owner is a string, describes the owner as a string" do + new_resource.owner("foo") + directory.load_current_resource + expect(directory.current_resource.owner).to eql(expected_pwnam) + end + + it "when the new_resource.group is a string, describes the group as a string" do + new_resource.group("bar") + directory.load_current_resource + expect(directory.current_resource.group).to eql(expected_grnam) + end + end end - @node = Chef::Node.new - @events = Chef::EventDispatch::Dispatcher.new - @run_context = Chef::RunContext.new(@node, {}, @events) - @directory = Chef::Provider::Directory.new(@new_resource, @run_context) - end + describe "on windows", windows_only: true do + describe "when the directory exists" do + it "the mode is always nil" do + directory.load_current_resource + expect(directory.current_resource.mode).to be nil + end + + it "the owner is always nil" do + directory.load_current_resource + expect(directory.current_resource.owner).to be nil + end + + it "the group is always nil" do + directory.load_current_resource + expect(directory.current_resource.group).to be nil + end + + it "rights are always nil (incorrectly)" do + directory.load_current_resource + expect(directory.current_resource.rights).to be nil + end + + it "inherits is always nil (incorrectly)" do + directory.load_current_resource + expect(directory.current_resource.inherits).to be nil + end + end + end + describe "when the directory does not exist" do + before do + FileUtils.rmdir tmp_dir + end - describe "scanning file security metadata on windows" do - before do + it "sets the mode, group and owner to nil" do + directory.load_current_resource + expect(directory.current_resource.mode).to eq(nil) + expect(directory.current_resource.group).to eq(nil) + expect(directory.current_resource.owner).to eq(nil) + end end - it "describes the directory's access rights" do - skip - end end - describe "scanning file security metadata on unix" do - before do - allow(ChefConfig).to receive(:windows?).and_return(false) - end - let(:mock_stat) do - cstats = double("stats") - allow(cstats).to receive(:uid).and_return(500) - allow(cstats).to receive(:gid).and_return(500) - allow(cstats).to receive(:mode).and_return(0755) - cstats - end + describe "#define_resource_requirements" do + describe "on unix", unix_only: true do + it "raises an exception if the user does not exist" do + new_resource.owner("arglebargle_iv") + expect(Etc).to receive(:getpwnam).with("arglebargle_iv").and_raise(ArgumentError) + directory.action = :create + directory.load_current_resource + expect(directory.access_controls).to receive(:define_resource_requirements).and_call_original + directory.define_resource_requirements + expect { directory.process_resource_requirements }.to raise_error(ArgumentError) + end - it "describes the access mode as a String of octal integers" do - allow(File).to receive(:exists?).and_return(true) - expect(File).to receive(:stat).and_return(mock_stat) - @directory.load_current_resource - expect(@directory.current_resource.mode).to eq("0755") + it "raises an exception if the group does not exist" do + new_resource.group("arglebargle_iv") + expect(Etc).to receive(:getgrnam).with("arglebargle_iv").and_raise(ArgumentError) + directory.action = :create + directory.load_current_resource + expect(directory.access_controls).to receive(:define_resource_requirements).and_call_original + directory.define_resource_requirements + expect { directory.process_resource_requirements }.to raise_error(ArgumentError) + end end + end - context "when user and group are specified with UID/GID" do - it "describes the current owner and group as UID and GID" do - allow(File).to receive(:exists?).and_return(true) - expect(File).to receive(:stat).and_return(mock_stat) - @directory.load_current_resource - expect(@directory.current_resource.path).to eql(@new_resource.path) - expect(@directory.current_resource.owner).to eql(500) - expect(@directory.current_resource.group).to eql(500) + describe "#run_action(:create)" do + describe "when the directory exists" do + it "does not create the directory" do + expect(Dir).not_to receive(:mkdir).with(new_resource.path) + directory.run_action(:create) + end + + it "should not set the resource as updated" do + directory.run_action(:create) + expect(new_resource).not_to be_updated end end - context "when user/group are specified with user/group names" do + describe "when the directory does not exist" do + before do + FileUtils.rmdir tmp_dir + end + + it "creates the directory" do + directory.run_action(:create) + expect(File.exist?(tmp_dir)).to be true + end + + it "sets the new resource as updated" do + directory.run_action(:create) + expect(new_resource).to be_updated + end end - end - # Unix only for now. While file security attribute reporting for windows is - # disabled, unix and windows differ in the number of exists? calls that are - # made by the provider. - it "should create a new directory on create, setting updated to true", :unix_only do - @new_resource.path "/tmp/foo" + describe "when the parent directory does not exist" do + before do + new_resource.path "#{tmp_dir}/foobar" + FileUtils.rmdir tmp_dir + end - expect(File).to receive(:exists?).at_least(:once).and_return(false) - expect(File).to receive(:directory?).with("/tmp").and_return(true) - expect(Dir).to receive(:mkdir).with(@new_resource.path).once.and_return(true) + it "raises an exception when recursive is false" do + new_resource.recursive false + expect { directory.run_action(:create) }.to raise_error(Chef::Exceptions::EnclosingDirectoryDoesNotExist) + end - expect(@directory).to receive(:do_acl_changes) - allow(@directory).to receive(:do_selinux) - @directory.run_action(:create) - expect(@directory.new_resource).to be_updated - end + it "creates the directories when recursive is true" do + new_resource.recursive true + directory.run_action(:create) + expect(new_resource).to be_updated + expect(File.exist?("#{tmp_dir}/foobar")).to be true + end - it "should raise an exception if the parent directory does not exist and recursive is false" do - @new_resource.path "/tmp/some/dir" - @new_resource.recursive false - expect { @directory.run_action(:create) }.to raise_error(Chef::Exceptions::EnclosingDirectoryDoesNotExist) - end + it "raises an exception when the parent directory is a file and recursive is true" do + FileUtils.touch tmp_dir + new_resource.recursive true + expect { directory.run_action(:create) }.to raise_error + end - # Unix only for now. While file security attribute reporting for windows is - # disabled, unix and windows differ in the number of exists? calls that are - # made by the provider. - it "should create a new directory when parent directory does not exist if recursive is true and permissions are correct", :unix_only do - @new_resource.path "/path/to/dir" - @new_resource.recursive true - expect(File).to receive(:exists?).with(@new_resource.path).ordered.and_return(false) - - expect(File).to receive(:exists?).with('/path/to').ordered.and_return(false) - expect(File).to receive(:exists?).with('/path').ordered.and_return(true) - expect(Chef::FileAccessControl).to receive(:writable?).with('/path').ordered.and_return(true) - expect(File).to receive(:exists?).with(@new_resource.path).ordered.and_return(false) - - expect(FileUtils).to receive(:mkdir_p).with(@new_resource.path).and_return(true) - expect(@directory).to receive(:do_acl_changes) - allow(@directory).to receive(:do_selinux) - @directory.run_action(:create) - expect(@new_resource).to be_updated + it "raises the right exception when the parent directory is a file and recursive is true" do + pending "this seems to return the wrong error" # FIXME + FileUtils.touch tmp_dir + new_resource.recursive true + expect { directory.run_action(:create) }.to raise_error(Chef::Exceptions::EnclosingDirectoryDoesNotExist) + end + end end + describe "#run_action(:create)" do + describe "when the directory exists" do + it "deletes the directory" do + directory.run_action(:delete) + expect(File.exist?(tmp_dir)).to be false + end - it "should raise an error when creating a directory when parent directory is a file" do - expect(File).to receive(:directory?).and_return(false) - expect(Dir).not_to receive(:mkdir).with(@new_resource.path) - expect { @directory.run_action(:create) }.to raise_error(Chef::Exceptions::EnclosingDirectoryDoesNotExist) - expect(@directory.new_resource).not_to be_updated - end + it "sets the new resource as updated" do + directory.run_action(:delete) + expect(new_resource).to be_updated + end + end - # Unix only for now. While file security attribute reporting for windows is - # disabled, unix and windows differ in the number of exists? calls that are - # made by the provider. - it "should not create the directory if it already exists", :unix_only do - stub_file_cstats - @new_resource.path "/tmp/foo" - expect(File).to receive(:directory?).at_least(:once).and_return(true) - expect(Chef::FileAccessControl).to receive(:writable?).with("/tmp").and_return(true) - expect(File).to receive(:exists?).at_least(:once).and_return(true) - expect(Dir).not_to receive(:mkdir).with(@new_resource.path) - expect(@directory).to receive(:do_acl_changes) - @directory.run_action(:create) - end + describe "when the directory does not exist" do + before do + FileUtils.rmdir tmp_dir + end - it "should delete the directory if it exists, and is writable with action_delete" do - expect(File).to receive(:directory?).and_return(true) - expect(Chef::FileAccessControl).to receive(:writable?).once.and_return(true) - expect(Dir).to receive(:delete).with(@new_resource.path).once.and_return(true) - @directory.run_action(:delete) - end + it "does not delete the directory" do + expect(Dir).not_to receive(:delete).with(new_resource.path) + directory.run_action(:delete) + end - it "should raise an exception if it cannot delete the directory due to bad permissions" do - allow(File).to receive(:exists?).and_return(true) - allow(Chef::FileAccessControl).to receive(:writable?).and_return(false) - expect { @directory.run_action(:delete) }.to raise_error(RuntimeError) - end + it "sets the new resource as updated" do + directory.run_action(:delete) + expect(new_resource).not_to be_updated + end + end - it "should take no action when deleting a target directory that does not exist" do - @new_resource.path "/an/invalid/path" - allow(File).to receive(:exists?).and_return(false) - expect(Dir).not_to receive(:delete).with(@new_resource.path) - @directory.run_action(:delete) - expect(@directory.new_resource).not_to be_updated - end + describe "when the directory is not writable" do + before do + allow(Chef::FileAccessControl).to receive(:writable?).and_return(false) + end - it "should raise an exception when deleting a directory when target directory is a file" do - stub_file_cstats - @new_resource.path "/an/invalid/path" - allow(File).to receive(:exists?).and_return(true) - expect(File).to receive(:directory?).and_return(false) - expect(Dir).not_to receive(:delete).with(@new_resource.path) - expect { @directory.run_action(:delete) }.to raise_error(RuntimeError) - expect(@directory.new_resource).not_to be_updated - end + it "cannot delete it and raises an exception" do + expect { directory.run_action(:delete) }.to raise_error(RuntimeError) + end + end + + describe "when the target directory is a file" do + before do + FileUtils.rmdir tmp_dir + FileUtils.touch tmp_dir + end - def stub_file_cstats - cstats = double("stats") - allow(cstats).to receive(:uid).and_return(500) - allow(cstats).to receive(:gid).and_return(500) - allow(cstats).to receive(:mode).and_return(0755) - # File.stat is called in: - # - Chef::Provider::File.load_current_resource_attrs - # - Chef::ScanAccessControl via Chef::Provider::File.setup_acl - allow(File).to receive(:stat).and_return(cstats) + it "cannot delete it and raises an exception" do + expect { directory.run_action(:delete) }.to raise_error(RuntimeError) + end + end end end diff --git a/spec/unit/provider/ifconfig/debian_spec.rb b/spec/unit/provider/ifconfig/debian_spec.rb index 351e734040..0c02ae9cd4 100644 --- a/spec/unit/provider/ifconfig/debian_spec.rb +++ b/spec/unit/provider/ifconfig/debian_spec.rb @@ -144,11 +144,6 @@ EOF expect(IO.read(tempfile.path)).to eq(expected_string) end - it "should not mark the resource as updated" do - provider.run_action(:add) - pending "superclass ifconfig provider is not idempotent" - expect(new_resource.updated_by_last_action?).to be_falsey - end end context "when the /etc/network/interfaces file does not have the source line" do @@ -280,11 +275,6 @@ another line expect(IO.read(tempfile.path)).to eq(expected_string) end - it "should not mark the resource as updated" do - provider.run_action(:add) - pending "superclass ifconfig provider is not idempotent" - expect(new_resource.updated_by_last_action?).to be_falsey - end end context "when the /etc/network/interfaces file does not have the source line" do diff --git a/spec/unit/provider/package/openbsd_spec.rb b/spec/unit/provider/package/openbsd_spec.rb index b0cdb9969d..abe94474a4 100644 --- a/spec/unit/provider/package/openbsd_spec.rb +++ b/spec/unit/provider/package/openbsd_spec.rb @@ -50,18 +50,6 @@ describe Chef::Provider::Package::Openbsd do context 'when there is a single candidate' do - context 'when installing from source' do - it 'should run the installation command' do - pending('Installing from source is not supported yet') - # This is a consequence of load_current_resource being called before define_resource_requirements - # It can be deleted once an implementation is provided - allow(provider).to receive(:shell_out!).with("pkg_info -I \"#{name}\"", anything()).and_return( - instance_double('shellout', :stdout => "#{name}-#{version}\n")) - new_resource.source('/some/path/on/disk.tgz') - provider.run_action(:install) - end - end - context 'when source is not provided' do it 'should run the installation command' do expect(provider).to receive(:shell_out!).with("pkg_info -I \"#{name}\"", anything()).and_return( @@ -106,15 +94,6 @@ describe Chef::Provider::Package::Openbsd do end end - context 'if a version is specified' do - it 'runs the installation command' do - pending('Specifying both a version and flavor is not supported') - new_resource.version(version) - allow(provider).to receive(:shell_out!).with(/pkg_info -e/, anything()).and_return(instance_double('shellout', :stdout => '')) - allow(provider).to receive(:candidate_version).and_return("#{package_name}-#{version}-#{flavor}") - provider.run_action(:install) - end - end end context 'if a version is specified' do diff --git a/spec/unit/provider/package/rubygems_spec.rb b/spec/unit/provider/package/rubygems_spec.rb index 380572499c..3ccc69652f 100644 --- a/spec/unit/provider/package/rubygems_spec.rb +++ b/spec/unit/provider/package/rubygems_spec.rb @@ -222,8 +222,6 @@ describe Chef::Provider::Package::Rubygems::AlternateGemEnvironment do end it "uses the cached result for gem paths when available" do - gem_env_output = ['/path/to/gems', '/another/path/to/gems'].join(File::PATH_SEPARATOR) - shell_out_result = OpenStruct.new(:stdout => gem_env_output) expect(@gem_env).not_to receive(:shell_out!) expected = ['/path/to/gems', '/another/path/to/gems'] Chef::Provider::Package::Rubygems::AlternateGemEnvironment.gempath_cache['/usr/weird/bin/gem']= expected @@ -261,7 +259,7 @@ describe Chef::Provider::Package::Rubygems::AlternateGemEnvironment do else `which gem`.strip end - pending("cant find your gem executable") if path_to_gem.empty? + skip("cant find your gem executable") if path_to_gem.empty? gem_env = Chef::Provider::Package::Rubygems::AlternateGemEnvironment.new(path_to_gem) expected = ['rspec-core', Gem::Version.new(RSpec::Core::Version::STRING)] actual = gem_env.installed_versions(Gem::Dependency.new('rspec-core', nil)).map { |s| [s.name, s.version] } diff --git a/spec/unit/provider/powershell_spec.rb b/spec/unit/provider/powershell_spec.rb index 60dbcf80b0..855c18af9b 100644 --- a/spec/unit/provider/powershell_spec.rb +++ b/spec/unit/provider/powershell_spec.rb @@ -19,20 +19,62 @@ require 'spec_helper' describe Chef::Provider::PowershellScript, "action_run" do - before(:each) do - @node = Chef::Node.new + let(:powershell_version) { nil } + let(:node) { + node = Chef::Node.new + node.default["kernel"] = Hash.new + node.default["kernel"][:machine] = :x86_64.to_s + if ! powershell_version.nil? + node.default[:languages] = { :powershell => { :version => powershell_version } } + end + node + } - @node.default["kernel"] = Hash.new - @node.default["kernel"][:machine] = :x86_64.to_s + let(:provider) { + empty_events = Chef::EventDispatch::Dispatcher.new + run_context = Chef::RunContext.new(node, {}, empty_events) + new_resource = Chef::Resource::PowershellScript.new('run some powershell code', run_context) + Chef::Provider::PowershellScript.new(new_resource, run_context) + } - @run_context = Chef::RunContext.new(@node, {}, @events) - @new_resource = Chef::Resource::PowershellScript.new('run some powershell code', @run_context) + context 'when setting interpreter flags' do + it "should set the -File flag as the last flag" do + expect(provider.flags.split(' ').pop).to eq("-File") + end - @provider = Chef::Provider::PowershellScript.new(@new_resource, @run_context) - end + let(:execution_policy_flag) do + execution_policy_index = 0 + provider_flags = provider.flags.split(' ') + execution_policy_specified = false - it "should set the -File flag as the last flag" do - expect(@provider.flags.split(' ').pop).to eq("-File") - end + provider_flags.find do | value | + execution_policy_index += 1 + execution_policy_specified = value.downcase == '-ExecutionPolicy'.downcase + end + + execution_policy = execution_policy_specified ? provider_flags[execution_policy_index] : nil + end + context 'when running with an unspecified PowerShell version' do + let(:powershell_version) { nil } + it "should set the -ExecutionPolicy flag to 'Unrestricted' by default" do + expect(execution_policy_flag.downcase).to eq('unrestricted'.downcase) + end + end + + { '2.0' => 'Unrestricted', + '2.5' => 'Unrestricted', + '3.0' => 'Bypass', + '3.6' => 'Bypass', + '4.0' => 'Bypass', + '5.0' => 'Bypass' }.each do | version_policy | + let(:powershell_version) { version_policy[0].to_f } + context "when running PowerShell version #{version_policy[0]}" do + let(:powershell_version) { version_policy[0].to_f } + it "should set the -ExecutionPolicy flag to '#{version_policy[1]}'" do + expect(execution_policy_flag.downcase).to eq(version_policy[1].downcase) + end + end + end + end end diff --git a/spec/unit/provider/remote_directory_spec.rb b/spec/unit/provider/remote_directory_spec.rb index 4434714ebc..99e2fe285c 100644 --- a/spec/unit/provider/remote_directory_spec.rb +++ b/spec/unit/provider/remote_directory_spec.rb @@ -194,8 +194,8 @@ describe Chef::Provider::RemoteDirectory do expect(::File.exist?(symlinked_dir_path)).to be_falsey expect(::File.exist?(tmp_dir)).to be_truthy - rescue Chef::Exceptions::Win32APIError => e - pending "This must be run as an Administrator to create symlinks" + rescue Chef::Exceptions::Win32APIError + skip "This must be run as an Administrator to create symlinks" end end end diff --git a/spec/unit/provider/service/freebsd_service_spec.rb b/spec/unit/provider/service/freebsd_service_spec.rb index 5a55425d87..cfc28c94d5 100644 --- a/spec/unit/provider/service/freebsd_service_spec.rb +++ b/spec/unit/provider/service/freebsd_service_spec.rb @@ -189,18 +189,6 @@ PS_SAMPLE expect(provider.status_load_success).to be_nil end - context "when ps command is nil" do - before do - node.automatic_attrs[:command] = {:ps => nil} - end - - it "should set running to nil" do - pending "superclass raises no conversion of nil to string which seems broken" - provider.determine_current_status! - expect(current_resource.running).to be_nil - end - end - context "when ps is empty string" do before do node.automatic_attrs[:command] = {:ps => ""} diff --git a/spec/unit/provider/user_spec.rb b/spec/unit/provider/user_spec.rb index 381168647b..2345ce18fb 100644 --- a/spec/unit/provider/user_spec.rb +++ b/spec/unit/provider/user_spec.rb @@ -143,8 +143,8 @@ describe Chef::Provider::User do begin require 'rubygems' require 'shadow' - rescue LoadError => e - pending "ruby-shadow gem not installed for dynamic load test" + rescue LoadError + skip "ruby-shadow gem not installed for dynamic load test" true else false @@ -161,7 +161,7 @@ describe Chef::Provider::User do unless shadow_lib_unavail? context "and we have the ruby-shadow gem" do - pending "and we are not root (rerun this again as root)", :requires_unprivileged_user => true + skip "and we are not root (rerun this again as root)", :requires_unprivileged_user => true context "and we are root", :requires_root => true do it "should pass assertions when ruby-shadow can be loaded" do diff --git a/spec/unit/resource/template_spec.rb b/spec/unit/resource/template_spec.rb index df5ca94b8a..2fd951b72d 100644 --- a/spec/unit/resource/template_spec.rb +++ b/spec/unit/resource/template_spec.rb @@ -98,7 +98,7 @@ describe Chef::Resource::Template do context "on windows", :windows_only do # according to Chef::Resource::File, windows state attributes are rights + deny_rights - pending "it describes its state" + skip "it describes its state" end it "returns the file path as its identity" do diff --git a/spec/unit/resource_spec.rb b/spec/unit/resource_spec.rb index 0479778f55..3bfd63f5ab 100644 --- a/spec/unit/resource_spec.rb +++ b/spec/unit/resource_spec.rb @@ -35,6 +35,18 @@ describe Chef::Resource do @resource = Chef::Resource.new("funk", @run_context) end + it "should mixin shell_out" do + expect(@resource.respond_to?(:shell_out)).to be true + end + + it "should mixin shell_out!" do + expect(@resource.respond_to?(:shell_out!)).to be true + end + + it "should mixin shell_out_with_systems_locale" do + expect(@resource.respond_to?(:shell_out_with_systems_locale)).to be true + end + describe "when inherited" do it "adds an entry to a list of subclasses" do |