diff options
83 files changed, 2885 insertions, 843 deletions
diff --git a/.travis.yml b/.travis.yml index c083a31ba9..699d8237ad 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,3 +1,6 @@ +language: ruby + +sudo: false # Early warning system to catch if Rubygems breaks something before_install: gem update --system diff --git a/CHANGELOG.md b/CHANGELOG.md index 2107c460b8..0a96ec8d28 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,34 @@ * [**Phil Dibowitz**](https://github.com/jaymzh): Drop SSL warnings now that we have a safe default * [Pull 2684](https://github.com/chef/chef/pull/2684) Remove ole_initialize/uninitialize which cause problems with Ruby >= 2 +* [**BinaryBabel**](https://github.com/binarybabel) + Make knife cookbook site share prefer gnutar when packaging +* [**Dave Eddy**](https://github.com/bahamas10) + Support arrays for not_if and only_if +* [**Scott Bonds**](https://github.com/bonds) + Add service provider for OpenBSD +* [**Alex Slynko**](https://github.com/alex-slynko-wonga) + Change env provider to preserve ordering +* [**Rob Redpath**](https://github.com/robredpath) + Add --lockfile opt for chef-client and chef-solo +* [**Josh Murphy**](https://github.com/jdmurphy) + Check cookbooks exist in path(s) before attempting to upload them with --all +* [**Vasiliy Tolstov**](https://github.com/vtolstov) + add ability to fetch recipes like in chef-solo when using local-mode +* [**Jan**](https://github.com/habermann24) + FIX data_bag_item.rb:161: warning: circular argument reference - data_bag +* [**David Radcliffe**](https://github.com/dwradcliffe) + add banner for knife serve command +* [**Yukihiko Sawanobori**](https://github.com/sawanoboly) + use Chef::JSONCompat.parse for file_contents +* [**Xabier de Zuazo**] (https://github.com/zuazo) + Remove some simple Ruby 1.8 and 1.9 code +* [**Xabier de Zuazo**] (https://github.com/zuazo) + Remove all RSpec test filters related to Ruby 1.8 and 1.9 +* [**Xabier de Zuazo**] (https://github.com/zuazo) + Fix knife cookbook upload messages +* [**David Crowder**] (https://github.com/david-crowder) + refactor to use shell_out in rpm provider ### Chef Contributions * ruby 1.9.3 support is dropped @@ -34,6 +62,11 @@ * suppress 3694 warnings on the most trivial resource cloning * fixed bugs in the deep_merge_cache logic introduced in 12.0.0 around `node['foo']` vs `node[:foo]` vs. `node.foo` * add `include_recipe "::recipe"` sugar to reference a recipe in the current cookbook +* Add --proxy-auth option to `knife raw` +* added Chef::Org model class for Chef Organizations in Chef 12 Server +* `powershell_script` should now correctly get the exit code for scripts that it runs. See [Issue 2348](https://github.com/chef/chef/issues/2348) +* Useradd functional tests fail randomly +* Add comments to trusted_certs_content ## 12.0.3 * [**Phil Dibowitz**](https://github.com/jaymzh): @@ -1,13 +1,13 @@ # Chef [![Code Climate](https://codeclimate.com/github/opscode/chef.png)](https://codeclimate.com/github/opscode/chef) [![Build Status Master](https://travis-ci.org/chef/chef.svg?branch=master)](https://travis-ci.org/chef/chef) -[![Build Status Master](https://ci.appveyor.com/api/projects/status/github/opscode/chef?branch=master&svg=true&passingText=master%20-%20Ok&pendingText=master%20-%20Pending&failingText=master%20-%20Failing)](https://ci.appveyor.com/project/Chef/chef/branch/master) +[![Build Status Master](https://ci.appveyor.com/api/projects/status/github/chef/chef?branch=master&svg=true&passingText=master%20-%20Ok&pendingText=master%20-%20Pending&failingText=master%20-%20Failing)](https://ci.appveyor.com/project/Chef/chef/branch/master) -Want to try Chef? Get started with [learnchef](https://learnchef.opscode.com) +Want to try Chef? Get started with [learnchef](https://learn.chef.io) -* Documentation: [http://docs.opscode.com](http://docs.opscode.com) -* Source: [http://github.com/opscode/chef/tree/master](http://github.com/opscode/chef/tree/master) -* Tickets/Issues: [https://github.com/opscode/chef/issues](https://github.com/opscode/chef/issues) +* Documentation: [http://docs.chef.io](http://docs.chef.io) +* Source: [http://github.com/chef/chef/tree/master](http://github.com/chef/chef/tree/master) +* Tickets/Issues: [https://github.com/chef/chef/issues](https://github.com/chef/chef/issues) * IRC: `#chef` and `#chef-hacking` on Freenode * Mailing list: [http://lists.opscode.com](http://lists.opscode.com) @@ -17,15 +17,15 @@ entire infrastructure. This README focuses on developers who want to modify Chef source code. If you just want to use Chef, check out these resources: -* [learnchef](https://learnchef.opscode.com): Getting started guide -* [http://docs.opscode.com](http://docs.opscode.com): Comprehensive User Docs -* [Installer Downloads](http://www.getchef.com/chef/install/): Install Chef as a complete package +* [learnchef](https://learn.chef.io): Getting started guide +* [http://docs.chef.io](http://docs.chef.io): Comprehensive User Docs +* [Installer Downloads](https://www.chef.io/download-chef-client/): Install Chef as a complete package ## Installing From Git **NOTE:** Unless you have a specific reason to install from source (to try a new feature, contribute a patch, or run chef on an OS for which no -package is available), you should head to the [installer page](http://www.getchef.com/chef/install/) +package is available), you should head to the [installer page](https://www.chef.io/download-chef-client/) to get a prebuilt package. ### Prerequisites @@ -45,7 +45,7 @@ emerge, etc.): Then get the source and install it: # Clone this repo - git clone https://github.com/opscode/chef.git + git clone https://github.com/chef/chef.git # cd into the source tree cd chef @@ -64,12 +64,15 @@ Then get the source and install it: Before working on the code, if you plan to contribute your changes, you need to read the -[Chef Contributions document](http://docs.opscode.com/community_contributions.html). +[Chef Contributions document](http://docs.chef.io/community_contributions.html). -You will also need to set up the repository with the appropriate branches. We -document the process on the -[Working with Git](http://wiki.opscode.com/display/chef/Working+with+git) page -of the Chef wiki. +The general development process is: + +1. Fork this repo and clone it to your workstation +2. Create a feature branch for your change +3. Write code and tests +4. Push your feature branch to github and open a pull request against + master Once your repository is set up, you can start working on the code. We do use TDD with RSpec, so you'll need to get a development environment running. @@ -78,9 +81,9 @@ copy of the source running. ## Reporting Issues -Issues can be reported by using [GitHub issues](https://github.com/opscode/chef/issues). +Issues can be reported by using [GitHub issues](https://github.com/chef/chef/issues). -Full details on how to report issues can be found in the [CONTRIBUTING](https://github.com/opscode/chef/blob/master/CONTRIBUTING.md#-chef-issue-tracking) doc. +Full details on how to report issues can be found in the [CONTRIBUTING](https://github.com/chef/chef/blob/master/CONTRIBUTING.md#-chef-issue-tracking) doc. Note that this repository is primarily for reporting chef-client issues. For reporting issues against other Chef projects, please look up the appropriate repository @@ -114,8 +117,8 @@ Chef - A configuration management system | | | |:---------------------|:-----------------------------------------| -| **Author:** | Adam Jacob (<adam@opscode.com>) -| **Copyright:** | Copyright (c) 2008-2014 Chef Software, Inc. +| **Author:** | Adam Jacob (<adam@chef.io>) +| **Copyright:** | Copyright (c) 2008-2015 Chef Software, Inc. | **License:** | Apache License, Version 2.0 Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/lib/chef/application/client.rb b/lib/chef/application/client.rb index 40772c0f8f..c0635e1cb5 100644 --- a/lib/chef/application/client.rb +++ b/lib/chef/application/client.rb @@ -27,6 +27,7 @@ require 'chef/handler/error_report' require 'chef/workstation_config_loader' class Chef::Application::Client < Chef::Application + include Chef::Mixin::ShellOut # Mimic self_pipe sleep from Unicorn to capture signals safely SELF_PIPE = [] @@ -63,7 +64,7 @@ class Chef::Application::Client < Chef::Application option :log_level, :short => "-l LEVEL", :long => "--log_level LEVEL", - :description => "Set the log level (debug, info, warn, error, fatal)", + :description => "Set the log level (auto, debug, info, warn, error, fatal)", :proc => lambda { |l| l.to_sym } option :log_location, @@ -104,7 +105,12 @@ class Chef::Application::Client < Chef::Application option :pid_file, :short => "-P PID_FILE", :long => "--pid PIDFILE", - :description => "Set the PID file location, defaults to /tmp/chef-client.pid", + :description => "Set the PID file location, for the chef-client daemon process. Defaults to /tmp/chef-client.pid", + :proc => nil + + option :lockfile, + :long => "--lockfile LOCKFILE", + :description => "Set the lockfile location. Prevents multiple client processes from converging at the same time", :proc => nil option :interval, @@ -200,6 +206,10 @@ class Chef::Application::Client < Chef::Application :description => "Fork client", :boolean => true + option :recipe_url, + :long => "--recipe-url=RECIPE_URL", + :description => "Pull down a remote archive of recipes and unpack it to the cookbook cache. Only used in local mode." + option :enable_reporting, :short => "-R", :long => "--enable-reporting", @@ -252,6 +262,8 @@ class Chef::Application::Client < Chef::Application def reconfigure super + raise Chef::Exceptions::PIDFileLockfileMatch if Chef::Util::PathHelper.paths_eql? (Chef::Config[:pid_file] || '' ), (Chef::Config[:lockfile] || '') + Chef::Config[:specific_recipes] = cli_arguments.map { |file| File.expand_path(file) } Chef::Config[:chef_server_url] = config[:chef_server_url] if config.has_key? :chef_server_url @@ -260,6 +272,18 @@ class Chef::Application::Client < Chef::Application 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 + + if !Chef::Config.local_mode && Chef::Config.has_key?(:recipe_url) + Chef::Application.fatal!("chef-client recipe-url can be used only in local-mode", 1) + elsif Chef::Config.local_mode && Chef::Config.has_key?(:recipe_url) + Chef::Log.debug "Creating path #{Chef::Config.chef_repo_path} to extract recipes into" + FileUtils.mkdir_p(Chef::Config.chef_repo_path) + tarball_path = File.join(Chef::Config.chef_repo_path, 'recipes.tgz') + fetch_recipe_tarball(Chef::Config[:recipe_url], tarball_path) + result = shell_out!("tar zxvf #{tarball_path} -C #{Chef::Config.chef_repo_path}") + Chef::Log.debug "#{result.stdout}" + end + Chef::Config.chef_zero.host = config[:chef_zero_host] if config[:chef_zero_host] Chef::Config.chef_zero.port = config[:chef_zero_port] if config[:chef_zero_port] @@ -435,4 +459,13 @@ class Chef::Application::Client < Chef::Application msg += audit_mode_settings_explaination return msg end + + def fetch_recipe_tarball(url, path) + Chef::Log.debug("Download recipes tarball from #{url} to #{path}") + File.open(path, 'wb') do |f| + open(url) do |r| + f.write(r.read) + end + end + end end diff --git a/lib/chef/application/solo.rb b/lib/chef/application/solo.rb index c3f5444ef7..c919c2b48b 100644 --- a/lib/chef/application/solo.rb +++ b/lib/chef/application/solo.rb @@ -99,6 +99,11 @@ class Chef::Application::Solo < Chef::Application :proc => lambda { |p| true } end + option :lockfile, + :long => "--lockfile LOCKFILE", + :description => "Set the lockfile location. Prevents multiple processes from converging at the same time", + :proc => nil + option :interval, :short => "-i SECONDS", :long => "--interval SECONDS", diff --git a/lib/chef/chef_fs/chef_fs_data_store.rb b/lib/chef/chef_fs/chef_fs_data_store.rb index 3813d0edb4..9b4f7320b8 100644 --- a/lib/chef/chef_fs/chef_fs_data_store.rb +++ b/lib/chef/chef_fs/chef_fs_data_store.rb @@ -369,6 +369,11 @@ class Chef if path.length >= 3 path[2] = "#{path[2]}.json" end + elsif path[0] == 'policies' + path = path.dup + if path.length >= 3 + path[2] = "#{path[2]}.json" + end elsif path[0] == 'cookbooks' if path.length == 2 raise ChefZero::DataStore::DataNotFoundError.new(path) @@ -445,10 +450,13 @@ class Chef def with_dir(path) # Do not automatically create data bags create = !(path[0] == 'data' && path.size >= 2) + begin yield get_dir(_to_chef_fs_path(path), create) rescue Chef::ChefFS::FileSystem::NotFoundError => e - raise ChefZero::DataStore::DataNotFoundError.new(to_zero_path(e.entry), e) + err = ChefZero::DataStore::DataNotFoundError.new(to_zero_path(e.entry), e) + err.set_backtrace(e.backtrace) + raise err end end diff --git a/lib/chef/chef_fs/config.rb b/lib/chef/chef_fs/config.rb index fcad6c919f..6666a3deee 100644 --- a/lib/chef/chef_fs/config.rb +++ b/lib/chef/chef_fs/config.rb @@ -26,6 +26,25 @@ class Chef # objects representing the server and local repository, respectively). # class Config + + # Not all of our object types pluralize by adding an 's', so we map them + # out here: + INFLECTIONS = { + "acls" => "acl", + "clients" => "client", + "cookbooks" => "cookbook", + "containers" => "container", + "data_bags" => "data_bag", + "environments" => "environment", + "groups" => "group", + "nodes" => "node", + "roles" => "role", + "users" => "user", + "policies" => "policy" + } + INFLECTIONS.each { |k,v| k.freeze; v.freeze } + INFLECTIONS.freeze + # # Create a new Config object which can produce a chef_fs and local_fs. # @@ -215,14 +234,16 @@ class Chef result = {} case @chef_config[:repo_mode] when 'static' - object_names = %w(cookbooks data_bags environments roles) + object_names = %w(cookbooks data_bags environments roles policies) when 'hosted_everything' - object_names = %w(acls clients cookbooks containers data_bags environments groups nodes roles) + object_names = %w(acls clients cookbooks containers data_bags environments groups nodes roles policies) else - object_names = %w(clients cookbooks data_bags environments nodes roles users) + object_names = %w(clients cookbooks data_bags environments nodes roles users policies) end object_names.each do |object_name| - variable_name = "#{object_name[0..-2]}_path" # cookbooks -> cookbook_path + # cookbooks -> cookbook_path + singular_name = INFLECTIONS[object_name] or raise "Unknown object name #{object_name}" + variable_name = "#{singular_name}_path" paths = Array(@chef_config[variable_name]).flatten result[object_name] = paths.map { |path| File.expand_path(path) } end diff --git a/lib/chef/chef_fs/data_handler/policy_data_handler.rb b/lib/chef/chef_fs/data_handler/policy_data_handler.rb new file mode 100644 index 0000000000..769c13c364 --- /dev/null +++ b/lib/chef/chef_fs/data_handler/policy_data_handler.rb @@ -0,0 +1,15 @@ +require 'chef/chef_fs/data_handler/data_handler_base' + +class Chef + module ChefFS + module DataHandler + class PolicyDataHandler < DataHandlerBase + + def normalize(policy, entry) + policy + end + end + end + end +end + diff --git a/lib/chef/chef_fs/data_handler/user_data_handler.rb b/lib/chef/chef_fs/data_handler/user_data_handler.rb index 2b50ce38d8..f6859faccd 100644 --- a/lib/chef/chef_fs/data_handler/user_data_handler.rb +++ b/lib/chef/chef_fs/data_handler/user_data_handler.rb @@ -8,6 +8,7 @@ class Chef normalize_hash(user, { 'name' => remove_dot_json(entry.name), 'username' => remove_dot_json(entry.name), + 'display_name' => remove_dot_json(entry.name), 'admin' => false, 'json_class' => 'Chef::WebUIUser', 'chef_type' => 'webui_user', diff --git a/lib/chef/chef_fs/file_system.rb b/lib/chef/chef_fs/file_system.rb index 730fa0e5cc..3ab59046cc 100644 --- a/lib/chef/chef_fs/file_system.rb +++ b/lib/chef/chef_fs/file_system.rb @@ -379,7 +379,7 @@ class Chef should_copy = true src_value = nil else - are_same, src_value, dest_value = compare(src_entry, dest_entry) + are_same, src_value, _dest_value = compare(src_entry, dest_entry) should_copy = !are_same end if should_copy diff --git a/lib/chef/chef_fs/file_system/chef_repository_file_system_policies_dir.rb b/lib/chef/chef_fs/file_system/chef_repository_file_system_policies_dir.rb new file mode 100644 index 0000000000..42768f10b7 --- /dev/null +++ b/lib/chef/chef_fs/file_system/chef_repository_file_system_policies_dir.rb @@ -0,0 +1,38 @@ +# +# Author:: John Keiser (<jkeiser@opscode.com>) +# Copyright:: Copyright (c) 2012 Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'chef/chef_fs/file_system/chef_repository_file_system_entry' +require 'chef/chef_fs/data_handler/policy_data_handler' + +class Chef + module ChefFS + module FileSystem + + class ChefRepositoryFileSystemPoliciesDir < ChefRepositoryFileSystemEntry + def initialize(name, parent, path = nil) + super(name, parent, path, Chef::ChefFS::DataHandler::PolicyDataHandler.new) + end + + def can_have_child?(name, is_dir) + is_dir && !name.start_with?('.') + end + end + end + end +end + diff --git a/lib/chef/chef_fs/file_system/chef_repository_file_system_root_dir.rb b/lib/chef/chef_fs/file_system/chef_repository_file_system_root_dir.rb index ac272d4c1a..d03baf91fe 100644 --- a/lib/chef/chef_fs/file_system/chef_repository_file_system_root_dir.rb +++ b/lib/chef/chef_fs/file_system/chef_repository_file_system_root_dir.rb @@ -21,6 +21,7 @@ require 'chef/chef_fs/file_system/chef_repository_file_system_entry' require 'chef/chef_fs/file_system/chef_repository_file_system_acls_dir' require 'chef/chef_fs/file_system/chef_repository_file_system_cookbooks_dir' require 'chef/chef_fs/file_system/chef_repository_file_system_data_bags_dir' +require 'chef/chef_fs/file_system/chef_repository_file_system_policies_dir' require 'chef/chef_fs/file_system/multiplexed_dir' require 'chef/chef_fs/data_handler/client_data_handler' require 'chef/chef_fs/data_handler/environment_data_handler' @@ -33,6 +34,7 @@ require 'chef/chef_fs/data_handler/container_data_handler' class Chef module ChefFS module FileSystem + # # Represents the root of a local Chef repository, with directories for # nodes, cookbooks, roles, etc. under it. @@ -157,6 +159,8 @@ class Chef dirs = paths.map { |path| ChefRepositoryFileSystemCookbooksDir.new(name, self, path) } elsif name == 'data_bags' dirs = paths.map { |path| ChefRepositoryFileSystemDataBagsDir.new(name, self, path) } + elsif name == 'policies' + dirs = paths.map { |path| ChefRepositoryFileSystemPoliciesDir.new(name, self, path) } elsif name == 'acls' dirs = paths.map { |path| ChefRepositoryFileSystemAclsDir.new(name, self, path) } else diff --git a/lib/chef/client.rb b/lib/chef/client.rb index 3d9678ea31..3b4f8d4683 100644 --- a/lib/chef/client.rb +++ b/lib/chef/client.rb @@ -39,6 +39,7 @@ require 'chef/cookbook/remote_file_vendor' require 'chef/event_dispatch/dispatcher' require 'chef/event_loggers/base' require 'chef/event_loggers/windows_eventlog' +require 'chef/exceptions' require 'chef/formatters/base' require 'chef/formatters/doc' require 'chef/formatters/minimal' diff --git a/lib/chef/config.rb b/lib/chef/config.rb index 453a8f83da..bf0901e182 100644 --- a/lib/chef/config.rb +++ b/lib/chef/config.rb @@ -203,6 +203,10 @@ class Chef # Does not apply to Enterprise Chef commands. default(:user_path) { derive_path_from_chef_repo_path('users') } + # Location of policies on disk. String or array of strings. + # Defaults to <chef_repo_path>/policies. + default(:policy_path) { derive_path_from_chef_repo_path('policies') } + # Turn on "path sanity" by default. See also: http://wiki.opscode.com/display/chef/User+Environment+PATH+Sanity default :enforce_path_sanity, true diff --git a/lib/chef/data_bag_item.rb b/lib/chef/data_bag_item.rb index 6f0ae65478..fc0ee74c0c 100644 --- a/lib/chef/data_bag_item.rb +++ b/lib/chef/data_bag_item.rb @@ -158,7 +158,7 @@ class Chef end end - def destroy(data_bag=data_bag, databag_item=name) + def destroy(data_bag=data_bag(), databag_item=name) chef_server_rest.delete_rest("data/#{data_bag}/#{databag_item}") end diff --git a/lib/chef/encrypted_data_bag_item/assertions.rb b/lib/chef/encrypted_data_bag_item/assertions.rb index 0f9416e7b6..ab93f46c10 100644 --- a/lib/chef/encrypted_data_bag_item/assertions.rb +++ b/lib/chef/encrypted_data_bag_item/assertions.rb @@ -44,9 +44,6 @@ class Chef::EncryptedDataBagItem end def assert_aead_requirements_met!(algorithm) - unless OpenSSL::Cipher.method_defined?(:auth_data=) - raise EncryptedDataBagRequirementsFailure, "The used Encrypted Data Bags version requires Ruby >= 2.0" - end unless OpenSSL::Cipher.ciphers.include?(algorithm) raise EncryptedDataBagRequirementsFailure, "The used Encrypted Data Bags version requires an OpenSSL version with \"#{algorithm}\" algorithm support" end diff --git a/lib/chef/event_dispatch/dispatcher.rb b/lib/chef/event_dispatch/dispatcher.rb index c172a406d8..9f43f14311 100644 --- a/lib/chef/event_dispatch/dispatcher.rb +++ b/lib/chef/event_dispatch/dispatcher.rb @@ -25,11 +25,9 @@ class Chef # Define a method that will be forwarded to all def self.def_forwarding_method(method_name) - class_eval(<<-END_OF_METHOD, __FILE__, __LINE__) - def #{method_name}(*args) - @subscribers.each {|s| s.#{method_name}(*args)} - end - END_OF_METHOD + define_method(method_name) do |*args| + @subscribers.each { |s| s.send(method_name, *args) } + end end (Base.instance_methods - Object.instance_methods).each do |method_name| diff --git a/lib/chef/exceptions.rb b/lib/chef/exceptions.rb index 18b8ee5d3f..0fa8696584 100644 --- a/lib/chef/exceptions.rb +++ b/lib/chef/exceptions.rb @@ -429,5 +429,11 @@ class Chef set_backtrace(backtrace) end end + + class PIDFileLockfileMatch < RuntimeError + def initialize + super "PID file and lockfile are not permitted to match. Specify a different location with --pid or --lockfile" + end + end end end diff --git a/lib/chef/guard_interpreter/resource_guard_interpreter.rb b/lib/chef/guard_interpreter/resource_guard_interpreter.rb index 346b585d8c..7d9bccb6ca 100644 --- a/lib/chef/guard_interpreter/resource_guard_interpreter.rb +++ b/lib/chef/guard_interpreter/resource_guard_interpreter.rb @@ -95,6 +95,7 @@ class Chef empty_events = Chef::EventDispatch::Dispatcher.new anonymous_run_context = Chef::RunContext.new(parent_resource.node, {}, empty_events) interpreter_resource = resource_class.new('Guard resource', anonymous_run_context) + interpreter_resource.is_guard_interpreter = true interpreter_resource end diff --git a/lib/chef/knife.rb b/lib/chef/knife.rb index 51ccb99955..10774acc46 100644 --- a/lib/chef/knife.rb +++ b/lib/chef/knife.rb @@ -269,7 +269,8 @@ class Chef list_commands(category_commands) elsif missing_plugin = ( OFFICIAL_PLUGINS.find {|plugin| plugin == args[0]} ) ui.info("The #{missing_plugin} commands were moved to plugins in Chef 0.10") - ui.info("You can install the plugin with `(sudo) gem install knife-#{missing_plugin}") + ui.info("You can install the plugin with `(sudo) gem install knife-#{missing_plugin}`") + ui.info("Use `chef gem install knife-#{missing_plugin}` instead if using ChefDK") else list_commands end @@ -308,7 +309,7 @@ class Chef exit 1 end - # copy Mixlib::CLI over so that it cab be configured in knife.rb + # copy Mixlib::CLI over so that it can be configured in knife.rb # config file Chef::Config[:verbosity] = config[:verbosity] end diff --git a/lib/chef/knife/cookbook_site_share.rb b/lib/chef/knife/cookbook_site_share.rb index b4a7873c71..3ea130ba9f 100644 --- a/lib/chef/knife/cookbook_site_share.rb +++ b/lib/chef/knife/cookbook_site_share.rb @@ -29,8 +29,11 @@ class Chef require 'chef/cookbook_loader' require 'chef/cookbook_uploader' require 'chef/cookbook_site_streaming_uploader' + require 'mixlib/shellout' end + include Chef::Mixin::ShellOut + banner "knife cookbook site share COOKBOOK [CATEGORY] (options)" category "cookbook site" @@ -70,7 +73,15 @@ class Chef begin Chef::Log.debug("Temp cookbook directory is #{tmp_cookbook_dir.inspect}") ui.info("Making tarball #{cookbook_name}.tgz") - shell_out!("tar -czf #{cookbook_name}.tgz #{cookbook_name}", :cwd => tmp_cookbook_dir) + tar_cmd = "tar" + begin + # Unix and Mac only - prefer gnutar + if shell_out("which gnutar").exitstatus.equal?(0) + tar_cmd = "gnutar" + end + rescue Errno::ENOENT + end + shell_out!("#{tar_cmd} -czf #{cookbook_name}.tgz #{cookbook_name}", :cwd => tmp_cookbook_dir) rescue => e ui.error("Error making tarball #{cookbook_name}.tgz: #{e.message}. Increase log verbosity (-VV) for more information.") Chef::Log.debug("\n#{e.backtrace.join("\n")}") diff --git a/lib/chef/knife/cookbook_upload.rb b/lib/chef/knife/cookbook_upload.rb index f5002be3a7..b2acd74b4b 100644 --- a/lib/chef/knife/cookbook_upload.rb +++ b/lib/chef/knife/cookbook_upload.rb @@ -104,18 +104,23 @@ class Chef justify_width = @server_side_cookbooks.map {|name| name.size}.max.to_i + 2 if config[:all] cookbook_repo.load_cookbooks - cbs = [] + cookbooks_for_upload = [] cookbook_repo.each do |cookbook_name, cookbook| - cbs << cookbook + cookbooks_for_upload << cookbook cookbook.freeze_version if config[:freeze] version_constraints_to_update[cookbook_name] = cookbook.version end - begin - upload(cbs, justify_width) - rescue Exceptions::CookbookFrozen - ui.warn("Not updating version constraints for some cookbooks in the environment as the cookbook is frozen.") + if cookbooks_for_upload.any? + begin + upload(cookbooks_for_upload, justify_width) + rescue Exceptions::CookbookFrozen + ui.warn("Not updating version constraints for some cookbooks in the environment as the cookbook is frozen.") + end + ui.info("Uploaded all cookbooks.") + else + cookbook_path = config[:cookbook_path].respond_to?(:join) ? config[:cookbook_path].join(', ') : config[:cookbook_path] + ui.warn("Could not find any cookbooks in your cookbook path: #{cookbook_path}. Use --cookbook-path to specify the desired path.") end - ui.info("Uploaded all cookbooks.") else if @name_args.empty? show_usage @@ -204,7 +209,7 @@ class Chef # because cookbooks are lazy-loaded, we have to force the loader # to load the cookbooks the user intends to upload here: cookbooks_to_upload - + unless cookbook_repo.merged_cookbooks.empty? ui.warn "* " * 40 ui.warn(<<-WARNING) diff --git a/lib/chef/knife/core/bootstrap_context.rb b/lib/chef/knife/core/bootstrap_context.rb index f7fee023de..60db34c8d0 100644 --- a/lib/chef/knife/core/bootstrap_context.rb +++ b/lib/chef/knife/core/bootstrap_context.rb @@ -49,6 +49,8 @@ class Chef @secret end + # Contains commands and content, see trusted_certs_content + # TODO: Rename to trusted_certs_script def trusted_certs @trusted_certs ||= trusted_certs_content end @@ -159,6 +161,9 @@ CONFIG end private + + # Returns a string for copying the trusted certificates on the workstation to the system being bootstrapped + # This string should contain both the commands necessary to both create the files, as well as their content def trusted_certs_content content = "" if @chef_config[:trusted_certs_dir] diff --git a/lib/chef/knife/raw.rb b/lib/chef/knife/raw.rb index 954d46beee..601cfcef9b 100644 --- a/lib/chef/knife/raw.rb +++ b/lib/chef/knife/raw.rb @@ -32,6 +32,12 @@ class Chef :short => '-i FILE', :description => "Name of file to use for PUT or POST" + option :proxy_auth, + :long => '--proxy-auth', + :boolean => true, + :default => false, + :description => "Use webui proxy authentication. Client key must be the webui key." + class RawInputServerAPI < Chef::HTTP def initialize(options = {}) options[:client_name] ||= Chef::Config[:node_name] @@ -64,15 +70,21 @@ class Chef begin method = config[:method].to_sym + headers = {'Content-Type' => 'application/json'} + + if config[:proxy_auth] + headers['x-ops-request-source'] = 'web' + end + if config[:pretty] chef_rest = RawInputServerAPI.new - result = chef_rest.request(method, name_args[0], {'Content-Type' => 'application/json'}, data) + result = chef_rest.request(method, name_args[0], headers, data) unless result.is_a?(String) result = Chef::JSONCompat.to_json_pretty(result) end else chef_rest = RawInputServerAPI.new(:raw_output => true) - result = chef_rest.request(method, name_args[0], {'Content-Type' => 'application/json'}, data) + result = chef_rest.request(method, name_args[0], headers, data) end output result rescue Timeout::Error => e @@ -88,4 +100,3 @@ class Chef end # class Raw end end - diff --git a/lib/chef/knife/serve.rb b/lib/chef/knife/serve.rb index 870177e0be..84918e2ef2 100644 --- a/lib/chef/knife/serve.rb +++ b/lib/chef/knife/serve.rb @@ -4,6 +4,9 @@ require 'chef/local_mode' class Chef class Knife class Serve < Knife + + banner 'knife serve (options)' + option :repo_mode, :long => '--repo-mode MODE', :description => "Specifies the local repository layout. Values: static (only environments/roles/data_bags/cookbooks), everything (includes nodes/clients/users), hosted_everything (includes acls/groups/etc. for Enterprise/Hosted Chef). Default: everything/hosted_everything" diff --git a/lib/chef/knife/ssl_fetch.rb b/lib/chef/knife/ssl_fetch.rb index 745aca5786..fd7d101fd8 100644 --- a/lib/chef/knife/ssl_fetch.rb +++ b/lib/chef/knife/ssl_fetch.rb @@ -16,7 +16,7 @@ # limitations under the License. # -require 'chef/knife/ssl_fetch' +require 'chef/knife' require 'chef/config' class Chef diff --git a/lib/chef/mixin/command/windows.rb b/lib/chef/mixin/command/windows.rb index e3d0cfdb18..0147d58039 100644 --- a/lib/chef/mixin/command/windows.rb +++ b/lib/chef/mixin/command/windows.rb @@ -18,11 +18,7 @@ # limitations under the License. # -if RUBY_VERSION =~ /^1\.8/ - require 'win32/open3' -else - require 'open3' -end +require 'open3' class Chef module Mixin diff --git a/lib/chef/mixin/securable.rb b/lib/chef/mixin/securable.rb index f77703f21a..aaedf0b9ba 100644 --- a/lib/chef/mixin/securable.rb +++ b/lib/chef/mixin/securable.rb @@ -111,13 +111,7 @@ class Chef # equivalent to something like: # def rights(permissions=nil, principals=nil, args_hash=nil) - define_method(name) do |*args| - # Ruby 1.8 compat: default the arguments - permissions = args.length >= 1 ? args[0] : nil - principals = args.length >= 2 ? args[1] : nil - args_hash = args.length >= 3 ? args[2] : nil - raise ArgumentError.new("wrong number of arguments (#{args.length} for 3)") if args.length >= 4 - + define_method(name) do |permissions=nil, principals=nil, args_hash=nil| rights = self.instance_variable_get("@#{name.to_s}".to_sym) unless permissions.nil? input = { diff --git a/lib/chef/mixin/template.rb b/lib/chef/mixin/template.rb index ae23336581..d705a9e7be 100644 --- a/lib/chef/mixin/template.rb +++ b/lib/chef/mixin/template.rb @@ -23,18 +23,6 @@ class Chef module Mixin module Template - # A compatibility wrapper around IO.binread so it works on Ruby 1.8.7. - # -- - # Used in the TemplateContext class, but that method namespace is shared - # with user code, so we want to avoid adding methods there when possible. - def self.binread(file) - if IO.respond_to?(:binread) - IO.binread(file) - else - File.open(file, "rb") {|f| f.read } - end - end - # == ChefContext # ChefContext was previously used to mix behavior into Erubis::Context so # that it would be available to templates. This behavior has now moved to @@ -105,11 +93,11 @@ class Chef partial_context._extend_modules(@_extension_modules) template_location = @template_finder.find(partial_name, options) - _render_template(Mixin::Template.binread(template_location), partial_context) + _render_template(IO.binread(template_location), partial_context) end def render_template(template_location) - _render_template(Mixin::Template.binread(template_location), self) + _render_template(IO.binread(template_location), self) end def render_template_from_string(template) diff --git a/lib/chef/mixin/why_run.rb b/lib/chef/mixin/why_run.rb index d650e3332f..d3acea5490 100644 --- a/lib/chef/mixin/why_run.rb +++ b/lib/chef/mixin/why_run.rb @@ -48,7 +48,7 @@ class Chef # block/proc that implements the action. def add_action(descriptions, &block) @actions << [descriptions, block] - if !Chef::Config[:why_run] + if (@resource.respond_to?(:is_guard_interpreter) && @resource.is_guard_interpreter) || !Chef::Config[:why_run] block.call end events.resource_update_applied(@resource, @action, descriptions) diff --git a/lib/chef/node/attribute.rb b/lib/chef/node/attribute.rb index 80f5ac4f8d..9d8738f637 100644 --- a/lib/chef/node/attribute.rb +++ b/lib/chef/node/attribute.rb @@ -138,11 +138,9 @@ class Chef :values, :values_at, :zip].each do |delegated_method| - class_eval(<<-METHOD_DEFN) - def #{delegated_method}(*args, &block) - merged_attributes.send(:#{delegated_method}, *args, &block) - end - METHOD_DEFN + define_method(delegated_method) do |*args, &block| + merged_attributes.send(delegated_method, *args, &block) + end end # return the cookbook level default attribute component diff --git a/lib/chef/node/attribute_collections.rb b/lib/chef/node/attribute_collections.rb index 333f4864c6..b912904534 100644 --- a/lib/chef/node/attribute_collections.rb +++ b/lib/chef/node/attribute_collections.rb @@ -61,12 +61,10 @@ class Chef # also invalidate the cached merged_attributes on the root # Node::Attribute object. MUTATOR_METHODS.each do |mutator| - class_eval(<<-METHOD_DEFN, __FILE__, __LINE__) - def #{mutator}(*args, &block) - root.reset_cache(root.top_level_breadcrumb) - super - end - METHOD_DEFN + define_method(mutator) do |*args, &block| + root.reset_cache(root.top_level_breadcrumb) + super(*args, &block) + end end attr_reader :root @@ -126,12 +124,10 @@ class Chef # also invalidate the cached `merged_attributes` on the root Attribute # object. MUTATOR_METHODS.each do |mutator| - class_eval(<<-METHOD_DEFN, __FILE__, __LINE__) - def #{mutator}(*args, &block) - root.reset_cache(root.top_level_breadcrumb) - super - end - METHOD_DEFN + define_method(mutator) do |*args, &block| + root.reset_cache(root.top_level_breadcrumb) + super(*args, &block) + end end def initialize(root, data={}) diff --git a/lib/chef/node/immutable_collections.rb b/lib/chef/node/immutable_collections.rb index 56b8fed3b7..0e2800641a 100644 --- a/lib/chef/node/immutable_collections.rb +++ b/lib/chef/node/immutable_collections.rb @@ -75,12 +75,9 @@ class Chef # Redefine all of the methods that mutate a Hash to raise an error when called. # This is the magic that makes this object "Immutable" DISALLOWED_MUTATOR_METHODS.each do |mutator_method_name| - # Ruby 1.8 blocks can't have block arguments, so we must use string eval: - class_eval(<<-METHOD_DEFN, __FILE__, __LINE__) - def #{mutator_method_name}(*args, &block) - raise Exceptions::ImmutableAttributeModification - end - METHOD_DEFN + define_method(mutator_method_name) do |*args, &block| + raise Exceptions::ImmutableAttributeModification + end end # For elements like Fixnums, true, nil... @@ -164,12 +161,9 @@ class Chef # Redefine all of the methods that mutate a Hash to raise an error when called. # This is the magic that makes this object "Immutable" DISALLOWED_MUTATOR_METHODS.each do |mutator_method_name| - # Ruby 1.8 blocks can't have block arguments, so we must use string eval: - class_eval(<<-METHOD_DEFN, __FILE__, __LINE__) - def #{mutator_method_name}(*args, &block) + define_method(mutator_method_name) do |*args, &block| raise Exceptions::ImmutableAttributeModification end - METHOD_DEFN end def method_missing(symbol, *args) diff --git a/lib/chef/org.rb b/lib/chef/org.rb new file mode 100644 index 0000000000..41d74b6186 --- /dev/null +++ b/lib/chef/org.rb @@ -0,0 +1,148 @@ +# +# Author:: Steven Danna (steve@opscode.com) +# Copyright:: Copyright (c) 2014 Chef Software, Inc +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'chef/json_compat' +require 'chef/mixin/params_validate' +require 'chef/rest' + +class Chef + class Org + + include Chef::Mixin::ParamsValidate + + def initialize(name) + @name = name + @full_name = '' + # The Chef API returns the private key of the validator + # client on create + @private_key = nil + @guid = nil + end + + def chef_rest + @chef_rest ||= Chef::REST.new(Chef::Config[:chef_server_root]) + end + + def name(arg=nil) + set_or_return(:name, arg, + :regex => /^[a-z0-9\-_]+$/) + end + + def full_name(arg=nil) + set_or_return(:full_name, + arg, :kind_of => String) + end + + def private_key(arg=nil) + set_or_return(:private_key, + arg, :kind_of => String) + end + + def guid(arg=nil) + set_or_return(:guid, + arg, :kind_of => String) + end + + def to_hash + result = { + "name" => @name, + "full_name" => @full_name + } + result["private_key"] = @private_key if @private_key + result["guid"] = @guid if @guid + result + end + + def to_json(*a) + Chef::JSONCompat.to_json(to_hash, *a) + end + + def create + payload = {:name => self.name, :full_name => self.full_name} + new_org = chef_rest.post_rest("organizations", payload) + Chef::Org.from_hash(self.to_hash.merge(new_org)) + end + + def update + payload = {:name => self.name, :full_name => self.full_name} + new_org = chef_rest.put_rest("organizations/#{name}", payload) + Chef::Org.from_hash(self.to_hash.merge(new_org)) + end + + def destroy + chef_rest.delete_rest("organizations/#{@name}") + end + + def save + begin + create + rescue Net::HTTPServerException => e + if e.response.code == "409" + update + else + raise e + end + end + end + + def associate_user(username) + request_body = {:user => username} + response = chef_rest.post_rest "organizations/#{@name}/association_requests", request_body + association_id = response["uri"].split("/").last + chef_rest.put_rest "users/#{username}/association_requests/#{association_id}", { :response => 'accept' } + end + + def dissociate_user(username) + chef_rest.delete_rest "organizations/#{name}/users/#{username}" + end + + # Class methods + def self.from_hash(org_hash) + org = Chef::Org.new(org_hash['name']) + org.full_name org_hash['full_name'] + org.private_key org_hash['private_key'] if org_hash.key?('private_key') + org.guid org_hash['guid'] if org_hash.key?('guid') + org + end + + def self.from_json(json) + Chef::Org.from_hash(Chef::JSONCompat.from_json(json)) + end + + class <<self + alias_method :json_create, :from_json + end + + def self.load(org_name) + response = Chef::REST.new(Chef::Config[:chef_server_root]).get_rest("organizations/#{org_name}") + Chef::Org.from_hash(response) + end + + def self.list(inflate=false) + orgs = Chef::REST.new(Chef::Config[:chef_server_root]).get_rest('organizations') + if inflate + orgs.inject({}) do |org_map, (name, _url)| + org_map[name] = Chef::Org.load(name) + org_map + end + else + orgs + end + end + end +end diff --git a/lib/chef/platform/provider_mapping.rb b/lib/chef/platform/provider_mapping.rb index 271e72f761..3c7ecf038c 100644 --- a/lib/chef/platform/provider_mapping.rb +++ b/lib/chef/platform/provider_mapping.rb @@ -366,7 +366,8 @@ class Chef :openbsd => { :default => { :group => Chef::Provider::Group::Usermod, - :package => Chef::Provider::Package::Openbsd + :package => Chef::Provider::Package::Openbsd, + :service => Chef::Provider::Service::Openbsd } }, :hpux => { diff --git a/lib/chef/platform/provider_priority_map.rb b/lib/chef/platform/provider_priority_map.rb index aa69761012..2517f46dfd 100644 --- a/lib/chef/platform/provider_priority_map.rb +++ b/lib/chef/platform/provider_priority_map.rb @@ -54,6 +54,7 @@ class Chef # priority :service, Chef::Provider::Service::Freebsd, os: [ "freebsd", "netbsd" ] + priority :service, Chef::Provider::Service::Openbsd, os: [ "openbsd" ] # # Solaris-en diff --git a/lib/chef/policy_builder/policyfile.rb b/lib/chef/policy_builder/policyfile.rb index 0df3dd5dd2..ff8e067bf6 100644 --- a/lib/chef/policy_builder/policyfile.rb +++ b/lib/chef/policy_builder/policyfile.rb @@ -237,7 +237,12 @@ class Chef end def policyfile_location - "data/policyfiles/#{deployment_group}" + if Chef::Config[:policy_document_native_api] + validate_policy_config! + "policies/#{policy_group}/#{policy_name}" + else + "data/policyfiles/#{deployment_group}" + end end # Do some mimimal validation of the policyfile we fetched from the @@ -281,6 +286,22 @@ class Chef raise ConfigurationError, "Setting `deployment_group` is not configured." end + def validate_policy_config! + policy_group or + raise ConfigurationError, "Setting `policy_group` is not configured." + + policy_name or + raise ConfigurationError, "Setting `policy_name` is not configured." + end + + def policy_group + Chef::Config[:policy_group] + end + + def policy_name + Chef::Config[:policy_name] + end + # Builds a 'cookbook_hash' map of the form # "COOKBOOK_NAME" => "IDENTIFIER" # diff --git a/lib/chef/provider/dsc_script.rb b/lib/chef/provider/dsc_script.rb index 5db50e74b3..d1f62d7f0d 100644 --- a/lib/chef/provider/dsc_script.rb +++ b/lib/chef/provider/dsc_script.rb @@ -161,7 +161,7 @@ class Chef cleaned_messages = resource.change_log[0..-2].map { |c| c.sub(/^#{Regexp.escape(resource.name)}/, '').strip } "converge DSC resource #{resource.name} by #{cleaned_messages.find_all{ |c| c != ''}.join("\n")}" else - # This is needed because a dsc script can have resouces that are both converged and not + # This is needed because a dsc script can have resources that are both converged and not "converge DSC resource #{resource.name} by doing nothing because it is already converged" end end diff --git a/lib/chef/provider/env.rb b/lib/chef/provider/env.rb index 600cac1e6b..970131c407 100644 --- a/lib/chef/provider/env.rb +++ b/lib/chef/provider/env.rb @@ -61,9 +61,12 @@ class Chef def requires_modify_or_create? if @new_resource.delim #e.g. check for existing value within PATH - not new_values.all? do |val| - current_values.include? val + new_values.inject(0) do |index, val| + next_index = current_values.find_index val + return true if next_index.nil? || next_index < index + next_index end + false else @new_resource.value != @current_resource.value end @@ -145,10 +148,7 @@ class Chef def modify_env if @new_resource.delim - values = new_values.reject do |v| - current_values.include?(v) - end - @new_resource.value((values + [@current_resource.value]).join(@new_resource.delim)) + @new_resource.value((new_values + current_values).uniq.join(@new_resource.delim)) end create_env end diff --git a/lib/chef/provider/package/rpm.rb b/lib/chef/provider/package/rpm.rb index 131587e066..1201431fed 100644 --- a/lib/chef/provider/package/rpm.rb +++ b/lib/chef/provider/package/rpm.rb @@ -17,6 +17,7 @@ # require 'chef/provider/package' require 'chef/mixin/command' +require 'chef/mixin/shell_out' require 'chef/resource/package' require 'chef/mixin/get_source_from_package' @@ -59,14 +60,13 @@ class Chef end Chef::Log.debug("#{@new_resource} checking rpm status") - status = popen4("rpm -qp --queryformat '%{NAME} %{VERSION}-%{RELEASE}\n' #{@new_resource.source}") do |pid, stdin, stdout, stderr| - stdout.each do |line| - case line - when /^([\w\d+_.-]+)\s([\w\d_.-]+)$/ - @current_resource.package_name($1) - @new_resource.version($2) - @candidate_version = $2 - end + status = shell_out!("rpm -qp --queryformat '%{NAME} %{VERSION}-%{RELEASE}\n' #{@new_resource.source}") + status.stdout.each_line do |line| + case line + when /^([\w\d+_.-]+)\s([\w\d_.-]+)$/ + @current_resource.package_name($1) + @new_resource.version($2) + @candidate_version = $2 end end else @@ -77,13 +77,12 @@ class Chef end Chef::Log.debug("#{@new_resource} checking install state") - @rpm_status = popen4("rpm -q --queryformat '%{NAME} %{VERSION}-%{RELEASE}\n' #{@current_resource.package_name}") do |pid, stdin, stdout, stderr| - stdout.each do |line| - case line - when /^([\w\d+_.-]+)\s([\w\d_.-]+)$/ - Chef::Log.debug("#{@new_resource} current version is #{$2}") - @current_resource.version($2) - end + @rpm_status = shell_out!("rpm -q --queryformat '%{NAME} %{VERSION}-%{RELEASE}\n' #{@current_resource.package_name}") + @rpm_status.stdout.each_line do |line| + case line + when /^([\w\d+_.-]+)\s([\w\d_.-]+)$/ + Chef::Log.debug("#{@new_resource} current version is #{$2}") + @current_resource.version($2) end end diff --git a/lib/chef/provider/powershell_script.rb b/lib/chef/provider/powershell_script.rb index 96955f73b6..8c79b384e9 100644 --- a/lib/chef/provider/powershell_script.rb +++ b/lib/chef/provider/powershell_script.rb @@ -24,8 +24,8 @@ class Chef protected EXIT_STATUS_EXCEPTION_HANDLER = "\ntrap [Exception] {write-error -exception ($_.Exception.Message);exit 1}".freeze - EXIT_STATUS_NORMALIZATION_SCRIPT = "\nif ($? -ne $true) { if ( $LASTEXITCODE -ne 0) {exit $LASTEXITCODE} else { exit 1 }}".freeze - EXIT_STATUS_RESET_SCRIPT = "\n$LASTEXITCODE=0".freeze + 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 diff --git a/lib/chef/provider/service/openbsd.rb b/lib/chef/provider/service/openbsd.rb new file mode 100644 index 0000000000..d509ee10ff --- /dev/null +++ b/lib/chef/provider/service/openbsd.rb @@ -0,0 +1,216 @@ +# +# Author:: Scott Bonds (<scott@ggr.com>) +# Copyright:: Copyright (c) 2014 Scott Bonds +# 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/command' +require 'chef/mixin/shell_out' +require 'chef/provider/service/init' +require 'chef/resource/service' + +class Chef + class Provider + class Service + class Openbsd < Chef::Provider::Service::Init + + provides :service, os: [ "openbsd" ] + + include Chef::Mixin::ShellOut + + attr_reader :init_command, :rc_conf, :rc_conf_local, :enabled_state_found + + RC_CONF_PATH = '/etc/rc.conf' + RC_CONF_LOCAL_PATH = '/etc/rc.conf.local' + + def initialize(new_resource, run_context) + super + @rc_conf = ::File.read(RC_CONF_PATH) rescue '' + @rc_conf_local = ::File.read(RC_CONF_LOCAL_PATH) rescue '' + @init_command = ::File.exist?(rcd_script_path) ? rcd_script_path : nil + new_resource.supports[:status] = true + new_resource.status_command("#{default_init_command} check") + end + + def load_current_resource + @current_resource = Chef::Resource::Service.new(new_resource.name) + current_resource.service_name(new_resource.service_name) + + Chef::Log.debug("#{current_resource} found at #{init_command}") + + determine_current_status! + determine_enabled_status! + current_resource + end + + def define_resource_requirements + shared_resource_requirements + + requirements.assert(:start, :enable, :reload, :restart) do |a| + a.assertion { init_command } + a.failure_message Chef::Exceptions::Service, "#{new_resource}: unable to locate the rc.d script" + end + + requirements.assert(:all_actions) do |a| + a.assertion { enabled_state_found } + # for consistency with original behavior, this will not fail in non-whyrun mode; + # rather it will silently set enabled state=>false + a.whyrun "Unable to determine enabled/disabled state, assuming this will be correct for an actual run. Assuming disabled." + end + + requirements.assert(:start, :enable, :reload, :restart) do |a| + a.assertion { init_command && builtin_service_enable_variable_name != nil } + a.failure_message Chef::Exceptions::Service, "Could not find the service name in #{init_command} and rcvar" + # No recovery in whyrun mode - the init file is present but not correct. + end + end + + def enable_service + if !is_enabled? + if is_builtin? + if is_enabled_by_default? + update_rcl rc_conf_local.sub(/^#{Regexp.escape(builtin_service_enable_variable_name)}=.*/, '') + else + # add line with blank string, which means enable + update_rcl rc_conf_local + "\n" + "#{builtin_service_enable_variable_name}=\"\"\n" + end + else + # add to pkg_scripts, most recent addition goes last + old_services_list = rc_conf_local.match(/^pkg_scripts="(.*)"/) + old_services_list = old_services_list ? old_services_list[1].split(' ') : [] + new_services_list = old_services_list + [new_resource.service_name] + if rc_conf_local.match(/^pkg_scripts="(.*)"/) + new_rcl = rc_conf_local.sub(/^pkg_scripts="(.*)"/, "pkg_scripts=\"#{new_services_list.join(' ')}\"") + else + new_rcl = rc_conf_local + "\n" + "pkg_scripts=\"#{new_services_list.join(' ')}\"\n" + end + update_rcl new_rcl + end + end + end + + def disable_service + if is_enabled? + if is_builtin? + if is_enabled_by_default? + # add line to disable + update_rcl rc_conf_local + "\n" + "#{builtin_service_enable_variable_name}=\"NO\"\n" + else + # remove line to disable + update_rcl rc_conf_local.sub(/^#{Regexp.escape(builtin_service_enable_variable_name)}=.*/, '') + end + else + # remove from pkg_scripts + old_list = rc_conf_local.match(/^pkg_scripts="(.*)"/) + old_list = old_list ? old_list[1].split(' ') : [] + new_list = old_list - [new_resource.service_name] + update_rcl rc_conf_local.sub(/^pkg_scripts="(.*)"/, pkg_scripts="#{new_list.join(' ')}") + end + end + end + + private + + def rcd_script_found? + !init_command.nil? + end + + def rcd_script_path + "/etc/rc.d/#{new_resource.service_name}" + end + + def update_rcl(value) + FileUtils.touch RC_CONF_LOCAL_PATH if !::File.exists? RC_CONF_LOCAL_PATH + ::File.write(RC_CONF_LOCAL_PATH, value) + @rc_conf_local = value + end + + # The variable name used in /etc/rc.conf.local for enabling this service + def builtin_service_enable_variable_name + @bsevn ||= begin + result = nil + if rcd_script_found? + ::File.open(init_command) do |rcscript| + if m = rcscript.read.match(/^# \$OpenBSD: (\w+)[(.rc),]?/) + result = m[1] + "_flags" + end + end + end + # Fallback allows us to keep running in whyrun mode when + # the script does not exist. + result || new_resource.service_name + end + end + + def is_builtin? + result = false + var_name = builtin_service_enable_variable_name + if var_name + if rc_conf.match(/^#{Regexp.escape(var_name)}=(.*)/) + result = true + end + end + result + end + + def is_enabled_by_default? + result = false + var_name = builtin_service_enable_variable_name + if var_name + if m = rc_conf.match(/^#{Regexp.escape(var_name)}=(.*)/) + if !(m[1] =~ /"?[Nn][Oo]"?/) + result = true + end + end + end + result + end + + def determine_enabled_status! + result = false # Default to disabled if the service doesn't currently exist at all + @enabled_state_found = false + if is_builtin? + var_name = builtin_service_enable_variable_name + if var_name + if m = rc_conf_local.match(/^#{Regexp.escape(var_name)}=(.*)/) + @enabled_state_found = true + if !(m[1] =~ /"?[Nn][Oo]"?/) # e.g. looking for httpd_flags=NO + result = true + end + end + end + if !@enabled_state_found + result = is_enabled_by_default? + end + else + var_name = @new_resource.service_name + if var_name + if m = rc_conf_local.match(/^pkg_scripts="(.*)"/) + @enabled_state_found = true + if m[1].include?(var_name) # e.g. looking for 'gdm' in pkg_scripts="gdm unbound" + result = true + end + end + end + end + + current_resource.enabled result + end + alias :is_enabled? :determine_enabled_status! + + end + end + end +end diff --git a/lib/chef/provider/user.rb b/lib/chef/provider/user.rb index e554dc7f2f..f6ac72448e 100644 --- a/lib/chef/provider/user.rb +++ b/lib/chef/provider/user.rb @@ -67,7 +67,7 @@ class Chef @current_resource.shell(user_info.shell) @current_resource.password(user_info.passwd) - if @new_resource.comment && user_info.gecos.respond_to?(:force_encoding) + if @new_resource.comment user_info.gecos.force_encoding(@new_resource.comment.encoding) end @current_resource.comment(user_info.gecos) diff --git a/lib/chef/providers.rb b/lib/chef/providers.rb index 38c0e6fc9a..796a0f8fa6 100644 --- a/lib/chef/providers.rb +++ b/lib/chef/providers.rb @@ -81,6 +81,7 @@ require 'chef/provider/service/gentoo' require 'chef/provider/service/init' require 'chef/provider/service/invokercd' require 'chef/provider/service/debian' +require 'chef/provider/service/openbsd' require 'chef/provider/service/redhat' require 'chef/provider/service/insserv' require 'chef/provider/service/simple' diff --git a/lib/chef/resource.rb b/lib/chef/resource.rb index bf49cd9d26..3e9d119cee 100644 --- a/lib/chef/resource.rb +++ b/lib/chef/resource.rb @@ -38,110 +38,58 @@ require 'chef/mixin/descendants_tracker' class Chef class Resource - FORBIDDEN_IVARS = [:@run_context, :@not_if, :@only_if, :@enclosing_provider] - HIDDEN_IVARS = [:@allowed_actions, :@resource_name, :@source_line, :@run_context, :@name, :@not_if, :@only_if, :@elapsed_time, :@enclosing_provider] + # + # Generic User DSL (not resource-specific) + # include Chef::DSL::DataQuery - include Chef::Mixin::ParamsValidate include Chef::DSL::PlatformIntrospection include Chef::DSL::RegistryHelper include Chef::DSL::RebootPending - include Chef::Mixin::ConvertToClassName - include Chef::Mixin::Deprecation - extend Chef::Mixin::ConvertToClassName - extend Chef::Mixin::DescendantsTracker - - if Module.method(:const_defined?).arity == 1 - def self.strict_const_defined?(const) - const_defined?(const) - end - else - def self.strict_const_defined?(const) - const_defined?(const, false) - end - end - - class << self - # back-compat - # NOTE: that we do not support unregistering classes as descendents like - # we used to for LWRP unloading because that was horrible and removed in - # Chef-12. - alias :resource_classes :descendants - alias :find_subclass_by_name :find_descendants_by_name - end - - # Set or return the list of "state attributes" implemented by the Resource - # subclass. State attributes are attributes that describe the desired state - # of the system, such as file permissions or ownership. In general, state - # attributes are attributes that could be populated by examining the state - # of the system (e.g., File.stat can tell you the permissions on an - # existing file). Contrarily, attributes that are not "state attributes" - # usually modify the way Chef itself behaves, for example by providing - # additional options for a package manager to use when installing a - # package. # - # This list is used by the Chef client auditing system to extract - # information from resources to describe changes made to the system. - def self.state_attrs(*attr_names) - @state_attrs ||= [] - @state_attrs = attr_names unless attr_names.empty? - - # Return *all* state_attrs that this class has, including inherited ones - if superclass.respond_to?(:state_attrs) - superclass.state_attrs + @state_attrs - else - @state_attrs - end - end - - # Set or return the "identity attribute" for this resource class. This is - # generally going to be the "name attribute" for this resource. In other - # words, the resource type plus this attribute uniquely identify a given - # bit of state that chef manages. For a File resource, this would be the - # path, for a package resource, it will be the package name. This will show - # up in chef-client's audit records as a searchable field. - def self.identity_attr(attr_name=nil) - @identity_attr ||= nil - @identity_attr = attr_name if attr_name - - # If this class doesn't have an identity attr, we'll defer to the superclass: - if @identity_attr || !superclass.respond_to?(:identity_attr) - @identity_attr - else - superclass.identity_attr - end + # The node the current Chef run is using. + # + # Corresponds to `run_context.node`. + # + # @return [Chef::Node] The node the current Chef run is using. + # + def node + run_context && run_context.node end - def self.dsl_name - convert_to_snake_case(name, 'Chef::Resource') + # + # Find existing resources by searching the list of existing resources. Possible + # forms are: + # + # find(:file => "foobar") + # find(:file => [ "foobar", "baz" ]) + # find("file[foobar]", "file[baz]") + # find("file[foobar,baz]") + # + # Calls `run_context.resource_collection.find(*args)` + # + # @return the matching resource, or an Array of matching resources. + # + # @raise ArgumentError if you feed it bad lookup information + # @raise RuntimeError if it can't find the resources you are looking for. + # + def resources(*args) + run_context.resource_collection.find(*args) end - attr_accessor :params - attr_accessor :provider - attr_accessor :allowed_actions - attr_accessor :run_context - attr_accessor :cookbook_name - attr_accessor :recipe_name - attr_accessor :enclosing_provider - attr_accessor :source_line - attr_accessor :retries - attr_accessor :retry_delay - attr_accessor :declared_type - attr_reader :updated - - attr_reader :resource_name - attr_reader :not_if_args - attr_reader :only_if_args - - attr_reader :elapsed_time - - attr_reader :default_guard_interpreter - - # Each notify entry is a resource/action pair, modeled as an - # Struct with a #resource and #action member + # + # Resource User Interface (for users) + # + # + # Create a new Resource. + # + # @param name The name of this resource (corresponds to the #name attribute, + # used for notifications to this resource). + # @param run_context The context of the Chef run. Corresponds to #run_context. + # def initialize(name, run_context=nil) @name = name @run_context = run_context @@ -171,89 +119,45 @@ class Chef @sensitive = false end - # Returns a Hash of attribute => value for the state attributes declared in - # the resource's class definition. - def state - self.class.state_attrs.inject({}) do |state_attrs, attr_name| - state_attrs[attr_name] = send(attr_name) - state_attrs - end - end - - # Returns the value of the identity attribute, if declared. Falls back to - # #name if no identity attribute is declared. - def identity - if identity_attr = self.class.identity_attr - send(identity_attr) - else - name - end - end - - def updated=(true_or_false) - Chef::Log.warn("Chef::Resource#updated=(true|false) is deprecated. Please call #updated_by_last_action(true|false) instead.") - Chef::Log.warn("Called from:") - caller[0..3].each {|line| Chef::Log.warn(line)} - updated_by_last_action(true_or_false) - @updated = true_or_false - end - - def node - run_context && run_context.node - end - - # If an unknown method is invoked, determine whether the enclosing Provider's - # lexical scope can fulfill the request. E.g. This happens when the Resource's - # block invokes new_resource. - def method_missing(method_symbol, *args, &block) - if enclosing_provider && enclosing_provider.respond_to?(method_symbol) - enclosing_provider.send(method_symbol, *args, &block) - else - raise NoMethodError, "undefined method `#{method_symbol.to_s}' for #{self.class.to_s}" - end - end - - def load_from(resource) - resource.instance_variables.each do |iv| - unless iv == :@source_line || iv == :@action || iv == :@not_if || iv == :@only_if - self.instance_variable_set(iv, resource.instance_variable_get(iv)) - end - end - end - - def supports(args={}) - if args.any? - @supports = args - else - @supports + # + # The name of this particular resource. + # + # This special resource attribute is set automatically from the declaration + # of the resource, e.g. + # + # execute 'Vitruvius' do + # command 'ls' + # end + # + # Will set the name to "Vitruvius". + # + # This is also used in to_s to show the resource name, e.g. `execute[Vitruvius]`. + # + # @param name [String] The name to set. + # @return [String] The name of this Resource. + # + def name(name=nil) + if !name.nil? + raise ArgumentError, "name must be a string!" unless name.kind_of?(String) + @name = name end + @name end - def provider(arg=nil) - klass = if arg.kind_of?(String) || arg.kind_of?(Symbol) - lookup_provider_constant(arg) - else - arg - end - set_or_return( - :provider, - klass, - :kind_of => [ Class ] - ) - end - + # + # The action or actions that will be taken when this resource is run. + # + # @param arg [Array[Symbol], Symbol] A list of actions (e.g. `:create`) + # @return [Array[Symbol]] the list of actions. + # def action(arg=nil) if arg action_list = arg.kind_of?(Array) ? arg : [ arg ] action_list = action_list.collect { |a| a.to_sym } action_list.each do |action| validate( - { - :action => action, - }, - { - :action => { :kind_of => Symbol, :equal_to => @allowed_actions }, - } + { action: action }, + { action: { kind_of: Symbol, equal_to: @allowed_actions } } ) end @action = action_list @@ -262,71 +166,60 @@ class Chef end end - def name(name=nil) - if !name.nil? - raise ArgumentError, "name must be a string!" unless name.kind_of?(String) - @name = name - end - @name - end - - def noop(tf=nil) - if !tf.nil? - raise ArgumentError, "noop must be true or false!" unless tf == true || tf == false - @noop = tf - end - @noop - end - - def ignore_failure(arg=nil) - set_or_return( - :ignore_failure, - arg, - :kind_of => [ TrueClass, FalseClass ] - ) - end - - def retries(arg=nil) - set_or_return( - :retries, - arg, - :kind_of => Integer - ) - end - - def retry_delay(arg=nil) - set_or_return( - :retry_delay, - arg, - :kind_of => Integer - ) - end - - def sensitive(arg=nil) - set_or_return( - :sensitive, - arg, - :kind_of => [ TrueClass, FalseClass ] - ) - end - - def epic_fail(arg=nil) - ignore_failure(arg) - end - - def guard_interpreter(arg=nil) - if arg.nil? - @guard_interpreter || @default_guard_interpreter - else - set_or_return( - :guard_interpreter, - arg, - :kind_of => Symbol - ) - end - end - - # Sets up a notification from this resource to the resource specified by +resource_spec+. + # + # Sets up a notification that will run a particular action on another resource + # if and when *this* resource is updated by an action. + # + # If the action does nothing--does not update this resource, the + # notification never triggers.) + # + # Only one resource may be specified per notification. + # + # `delayed` notifications will only *ever* happen once per resource, so if + # multiple resources all notify a single resource to perform the same action, + # the action will only happen once. However, if they ask for different + # actions, each action will happen once, in the order they were updated. + # + # `immediate` notifications will cause the action to be triggered once per + # notification, regardless of how many other resources have triggered the + # notification as well. + # + # @param action The action to run on the other resource. + # @param resource_spec [String, Hash, Chef::Resource] The resource to run. + # @param timing [String, Symbol] When to notify. Has these values: + # - `delayed`: Will run the action on the other resource after all other + # actions have been run. This is the default. + # - `immediate`, `immediately`: Will run the action on the other resource + # immediately (before any other action is run). + # + # @example Resource by string + # file '/foo.txt' do + # content 'hi' + # notifies :create, 'file[/bar.txt]' + # end + # file '/bar.txt' do + # action :nothing + # content 'hi' + # end + # @example Resource by hash + # file '/foo.txt' do + # content 'hi' + # notifies :create, file: '/bar.txt' + # end + # file '/bar.txt' do + # action :nothing + # content 'hi' + # end + # @example Direct Resource + # bar = file '/bar.txt' do + # action :nothing + # content 'hi' + # end + # file '/foo.txt' do + # content 'hi' + # notifies :create, bar + # end + # def notifies(action, resource_spec, timing=:delayed) # when using old-style resources(:template => "/foo.txt") style, you # could end up with multiple resources. @@ -342,79 +235,348 @@ class Chef notifies_immediately(action, resource) else raise ArgumentError, "invalid timing: #{timing} for notifies(#{action}, #{resources.inspect}, #{timing}) resource #{self} "\ - "Valid timings are: :delayed, :immediate, :immediately" + "Valid timings are: :delayed, :immediate, :immediately" end end true end - # Iterates over all immediate and delayed notifications, calling - # resolve_resource_reference on each in turn, causing them to - # resolve lazy/forward references. - def resolve_notification_references - run_context.immediate_notifications(self).each { |n| - n.resolve_resource_reference(run_context.resource_collection) - } - run_context.delayed_notifications(self).each {|n| - n.resolve_resource_reference(run_context.resource_collection) - } + # + # Subscribes to updates from other resources, causing a particular action to + # run on *this* resource when the other resource is updated. + # + # If multiple resources are specified, this resource action will be run if + # *any* of them change. + # + # This notification will only trigger *once*, no matter how many other + # resources are updated (or how many actions are run by a particular resource). + # + # @param action The action to run on the other resource. + # @param resources [String, Resource, Array[String, Resource]] The resources to subscribe to. + # @param timing [String, Symbol] When to notify. Has these values: + # - `delayed`: An update will cause the action to run after all other + # actions have been run. This is the default. + # - `immediate`, `immediately`: The action will run immediately following + # the other resource being updated. + # + # @example Resources by string + # file '/foo.txt' do + # content 'hi' + # action :nothing + # subscribes :create, 'file[/bar.txt]' + # end + # file '/bar.txt' do + # content 'hi' + # end + # @example Direct resource + # bar = file '/bar.txt' do + # content 'hi' + # end + # file '/foo.txt' do + # content 'hi' + # action :nothing + # subscribes :create, '/bar.txt' + # end + # @example Multiple resources by string + # file '/foo.txt' do + # content 'hi' + # action :nothing + # subscribes :create, [ 'file[/bar.txt]', 'file[/baz.txt]' ] + # end + # file '/bar.txt' do + # content 'hi' + # end + # file '/baz.txt' do + # content 'hi' + # end + # @example Multiple resources + # bar = file '/bar.txt' do + # content 'hi' + # end + # baz = file '/bar.txt' do + # content 'hi' + # end + # file '/foo.txt' do + # content 'hi' + # action :nothing + # subscribes :create, [ bar, baz ] + # end + # + def subscribes(action, resources, timing=:delayed) + resources = [resources].flatten + resources.each do |resource| + if resource.is_a?(String) + resource = Chef::Resource.new(resource, run_context) + end + if resource.run_context.nil? + resource.run_context = run_context + end + resource.notifies(action, self, timing) + end + true end - def notifies_immediately(action, resource_spec) - run_context.notifies_immediately(Notification.new(resource_spec, action, self)) + # + # A command or block that indicates whether to actually run this resource. + # The command or block is run just before the action actually executes, and + # the action will be skipped if the block returns false. + # + # If a block is specified, it must return `true` in order for the Resource + # to be executed. + # + # If a command is specified, the resource's #guard_interpreter will run the + # command and interpret the results according to `opts`. For example, the + # default `execute` resource will be treated as `false` if the command + # returns a non-zero exit code, and `true` if it returns 0. Thus, in the + # default case: + # + # - `only_if "your command"` will perform the action only if `your command` + # returns 0. + # - `only_if "your command", valid_exit_codes: [ 1, 2, 3 ]` will perform the + # action only if `your command` returns 1, 2, or 3. + # + # @param command [String] A string to execute. + # @param opts [Hash] Options control the execution of the command + # @param block [Proc] A ruby block to run. Ignored if a command is given. + # + def only_if(command=nil, opts={}, &block) + if command || block_given? + @only_if << Conditional.only_if(self, command, opts, &block) + end + @only_if end - def notifies_delayed(action, resource_spec) - run_context.notifies_delayed(Notification.new(resource_spec, action, self)) + # + # A command or block that indicates whether to actually run this resource. + # The command or block is run just before the action actually executes, and + # the action will be skipped if the block returns true. + # + # If a block is specified, it must return `false` in order for the Resource + # to be executed. + # + # If a command is specified, the resource's #guard_interpreter will run the + # command and interpret the results according to `opts`. For example, the + # default `execute` resource will be treated as `false` if the command + # returns a non-zero exit code, and `true` if it returns 0. Thus, in the + # default case: + # + # - `not_if "your command"` will perform the action only if `your command` + # returns a non-zero code. + # - `only_if "your command", valid_exit_codes: [ 1, 2, 3 ]` will perform the + # action only if `your command` returns something other than 1, 2, or 3. + # + # @param command [String] A string to execute. + # @param opts [Hash] Options control the execution of the command + # @param block [Proc] A ruby block to run. Ignored if a command is given. + # + def not_if(command=nil, opts={}, &block) + if command || block_given? + @not_if << Conditional.not_if(self, command, opts, &block) + end + @not_if end - def immediate_notifications - run_context.immediate_notifications(self) + # + # The number of times to retry this resource if it fails by throwing an + # exception while running an action. Default: 0 + # + # When the retries have run out, the Resource will throw the last + # exception. + # + # @param arg [Integer] The number of retries. + # @return [Integer] The number of retries. + # + def retries(arg=nil) + set_or_return(:retries, arg, kind_of: Integer) end + attr_writer :retries - def delayed_notifications - run_context.delayed_notifications(self) + # + # The number of seconds to wait between retries. Default: 2. + # + # @param arg [Integer] The number of seconds to wait between retries. + # @return [Integer] The number of seconds to wait between retries. + # + def retry_delay(arg=nil) + set_or_return(:retry_delay, arg, kind_of: Integer) end + attr_writer :retry_delay - def resources(*args) - run_context.resource_collection.find(*args) + # + # Whether to treat this resource's data as sensitive. If set, no resource + # data will be displayed in log output. + # + # @param arg [Boolean] Whether this resource is sensitive or not. + # @return [Boolean] Whether this resource is sensitive or not. + # + def sensitive(arg=nil) + set_or_return(:sensitive, arg, :kind_of => [ TrueClass, FalseClass ]) end + attr_writer :sensitive - def subscribes(action, resources, timing=:delayed) - resources = [resources].flatten - resources.each do |resource| - if resource.is_a?(String) - resource = Chef::Resource.new(resource, run_context) - end - if resource.run_context.nil? - resource.run_context = run_context - end - resource.notifies(action, self, timing) + # ??? TODO unreferenced. Delete? + attr_reader :not_if_args + # ??? TODO unreferenced. Delete? + attr_reader :only_if_args + + # + # The time it took (in seconds) to run the most recently-run action. Not + # cumulative across actions. This is set to 0 as soon as a new action starts + # running, and set to the elapsed time at the end of the action. + # + # @return [Integer] The time (in seconds) it took to process the most recent + # action. Not cumulative. + # + attr_reader :elapsed_time + + # + # The guard interpreter that will be used to process `only_if` and `not_if` + # statements. If left unset, the #default_guard_interpreter will be used. + # + # Must be a resource class like `Chef::Resource::Execute`, or + # a corresponding to the name of a resource. The resource must descend from + # `Chef::Resource::Execute`. + # + # TODO this needs to be coerced on input so that retrieval is consistent. + # + # @param arg [Class, Symbol, String] The Guard interpreter resource class/ + # symbol/name. + # @return [Class, Symbol, String] The Guard interpreter resource. + # + def guard_interpreter(arg=nil) + if arg.nil? + @guard_interpreter || @default_guard_interpreter + else + set_or_return(:guard_interpreter, arg, :kind_of => Symbol) end - true end - def validate_resource_spec!(resource_spec) - run_context.resource_collection.validate_lookup_spec!(resource_spec) + # + # Get the value of the state attributes in this resource as a hash. + # + # @return [Hash{Symbol => Object}] A Hash of attribute => value for the + # Resource class's `state_attrs`. + def state + self.class.state_attrs.inject({}) do |state_attrs, attr_name| + state_attrs[attr_name] = send(attr_name) + state_attrs + end end - def is(*args) - if args.size == 1 - args.first + # + # The value of the identity attribute, if declared. Falls back to #name if + # no identity attribute is declared. + # + # @return The value of the identity attribute. + # + def identity + if identity_attr = self.class.identity_attr + send(identity_attr) else - return *args + name end end - # We usually want to store and reference resources by their declared type and not the actual type that - # was looked up by the Resolver (IE, "package" becomes YumPackage class). If we have not been provided - # the declared key we want to fall back on the old to_s key. - def declared_key - return to_s if declared_type.nil? - "#{declared_type}[#{@name}]" + # + # Whether to ignore failures. If set to `true`, and this resource when an + # action is run, the resource will be marked as failed but no exception will + # be thrown (and no error will be output). Defaults to `false`. + # + # TODO ignore_failure and retries seem to be mutually exclusive; I doubt + # that was intended. + # + # @param arg [Boolean] Whether to ignore failures. + # @return Whether this resource will ignore failures. + # + def ignore_failure(arg=nil) + set_or_return(:ignore_failure, arg, kind_of: [ TrueClass, FalseClass ]) + end + attr_writer :ignore_failure + + # + # Equivalent to #ignore_failure. + # + def epic_fail(arg=nil) + ignore_failure(arg) + end + + # + # Make this resource into an exact (shallow) copy of the other resource. + # + # @param resource [Chef::Resource] The resource to copy from. + # + def load_from(resource) + resource.instance_variables.each do |iv| + unless iv == :@source_line || iv == :@action || iv == :@not_if || iv == :@only_if + self.instance_variable_set(iv, resource.instance_variable_get(iv)) + end + end + end + + # + # Runs the given action on this resource, immediately. + # + # @param action The action to run (e.g. `:create`) + # @param notification_type The notification type that triggered this (if any) + # @param notifying_resource The resource that triggered this notification (if any) + # + # @raise Any error that occurs during the actual action. + # + def run_action(action, notification_type=nil, notifying_resource=nil) + # reset state in case of multiple actions on the same resource. + @elapsed_time = 0 + start_time = Time.now + events.resource_action_start(self, action, notification_type, notifying_resource) + # Try to resolve lazy/forward references in notifications again to handle + # the case where the resource was defined lazily (ie. in a ruby_block) + resolve_notification_references + validate_action(action) + + if Chef::Config[:verbose_logging] || Chef::Log.level == :debug + # This can be noisy + Chef::Log.info("Processing #{self} action #{action} (#{defined_at})") + end + + # ensure that we don't leave @updated_by_last_action set to true + # on accident + updated_by_last_action(false) + + # Don't modify @retries directly and keep it intact, so that the + # recipe_snippet from ResourceFailureInspector can print the value + # that was set in the resource block initially. + remaining_retries = retries + + begin + return if should_skip?(action) + provider_for_action(action).run_action + rescue Exception => e + if ignore_failure + Chef::Log.error("#{custom_exception_message(e)}; ignore_failure is set, continuing") + events.resource_failed(self, action, e) + elsif remaining_retries > 0 + events.resource_failed_retriable(self, action, remaining_retries, e) + remaining_retries -= 1 + Chef::Log.info("Retrying execution of #{self}, #{remaining_retries} attempt(s) left") + sleep retry_delay + retry + else + events.resource_failed(self, action, e) + raise customize_exception(e) + end + ensure + @elapsed_time = Time.now - start_time + # Reporting endpoint doesn't accept a negative resource duration so set it to 0. + # A negative value can occur when a resource changes the system time backwards + @elapsed_time = 0 if @elapsed_time < 0 + events.resource_completed(self) + end end + # + # Generic Ruby and Data Structure Stuff (for user) + # + def to_s "#{@resource_name}[#{@name}]" end @@ -474,46 +636,344 @@ class Chef instance_vars end - # If command is a block, returns true if the block returns true, false if it returns false. - # ("Only run this resource if the block is true") + def self.json_create(o) + resource = self.new(o["instance_vars"]["@name"]) + o["instance_vars"].each do |k,v| + resource.instance_variable_set("@#{k}".to_sym, v) + end + resource + end + + # + # Resource Definition Interface (for resource developers) + # + + include Chef::Mixin::ParamsValidate + include Chef::Mixin::Deprecation + # - # If the command is not a block, executes the command. If it returns any status other than - # 0, it returns false (clearly, a 0 status code is true) + # The provider class for this resource. # - # === Parameters - # command<String>:: A a string to execute. - # opts<Hash>:: Options control the execution of the command - # block<Proc>:: A ruby block to run. Ignored if a command is given. + # If this is not set, `provider_for_action` will dynamically determine the + # provider. # - # === Evaluation - # * evaluates to true if the block is true, or if the command returns 0 - # * evaluates to false if the block is false, or if the command returns a non-zero exit code. - def only_if(command=nil, opts={}, &block) - if command || block_given? - @only_if << Conditional.only_if(self, command, opts, &block) + # @param arg [String, Symbol, Class] Sets the provider class for this resource. + # If passed a String or Symbol, e.g. `:file` or `"file"`, looks up the + # provider based on the name. + # @return The provider class for this resource. + # + def provider(arg=nil) + klass = if arg.kind_of?(String) || arg.kind_of?(Symbol) + lookup_provider_constant(arg) + else + arg + end + set_or_return(:provider, klass, kind_of: [ Class ]) + end + def provider=(arg) + provider(arg) + end + + # Set or return the list of "state attributes" implemented by the Resource + # subclass. State attributes are attributes that describe the desired state + # of the system, such as file permissions or ownership. In general, state + # attributes are attributes that could be populated by examining the state + # of the system (e.g., File.stat can tell you the permissions on an + # existing file). Contrarily, attributes that are not "state attributes" + # usually modify the way Chef itself behaves, for example by providing + # additional options for a package manager to use when installing a + # package. + # + # This list is used by the Chef client auditing system to extract + # information from resources to describe changes made to the system. + def self.state_attrs(*attr_names) + @state_attrs ||= [] + @state_attrs = attr_names unless attr_names.empty? + + # Return *all* state_attrs that this class has, including inherited ones + if superclass.respond_to?(:state_attrs) + superclass.state_attrs + @state_attrs + else + @state_attrs + end + end + + # Set or return the "identity attribute" for this resource class. This is + # generally going to be the "name attribute" for this resource. In other + # words, the resource type plus this attribute uniquely identify a given + # bit of state that chef manages. For a File resource, this would be the + # path, for a package resource, it will be the package name. This will show + # up in chef-client's audit records as a searchable field. + def self.identity_attr(attr_name=nil) + @identity_attr ||= nil + @identity_attr = attr_name if attr_name + + # If this class doesn't have an identity attr, we'll defer to the superclass: + if @identity_attr || !superclass.respond_to?(:identity_attr) + @identity_attr + else + superclass.identity_attr end - @only_if end - # If command is a block, returns false if the block returns true, true if it returns false. - # ("Do not run this resource if the block is true") # - # If the command is not a block, executes the command. If it returns a 0 exitstatus, returns false. - # ("Do not run this resource if the command returns 0") + # The guard interpreter that will be used to process `only_if` and `not_if` + # statements by default. If left unset, or set to `:default`, the guard + # interpreter used will be Chef::GuardInterpreter::DefaultGuardInterpreter. # - # === Parameters - # command<String>:: A a string to execute. - # opts<Hash>:: Options control the execution of the command - # block<Proc>:: A ruby block to run. Ignored if a command is given. + # Must be a resource class like `Chef::Resource::Execute`, or + # a corresponding to the name of a resource. The resource must descend from + # `Chef::Resource::Execute`. # - # === Evaluation - # * evaluates to true if the block is false, or if the command returns a non-zero exit status. - # * evaluates to false if the block is true, or if the command returns a 0 exit status. - def not_if(command=nil, opts={}, &block) - if command || block_given? - @not_if << Conditional.not_if(self, command, opts, &block) + # TODO this needs to be coerced on input so that retrieval is consistent. + # + # @return [Class, Symbol, String] the default Guard interpreter resource. + # + attr_reader :default_guard_interpreter + + # + # The list of actions this Resource is allowed to have. Setting `action` + # will fail unless it is in this list. Default: [ :nothing ] + # + # @return [Array<Symbol>] The list of actions this Resource is allowed to + # have. + # + attr_accessor :allowed_actions + + # + # Whether or not this resource was updated during an action. If multiple + # actions are set on the resource, this will be `true` if *any* action + # caused an update to happen. + # + # @return [Boolean] Whether the resource was updated during an action. + # + attr_reader :updated + + # + # Whether or not this resource was updated during an action. If multiple + # actions are set on the resource, this will be `true` if *any* action + # caused an update to happen. + # + # @return [Boolean] Whether the resource was updated during an action. + # + def updated? + updated + end + + # + # Whether or not this resource was updated during the most recent action. + # This is set to `false` at the beginning of each action. + # + # @param true_or_false [Boolean] Whether the resource was updated during the + # current / most recent action. + # @return [Boolean] Whether the resource was updated during the most recent action. + # + def updated_by_last_action(true_or_false) + @updated ||= true_or_false + @updated_by_last_action = true_or_false + end + + # + # Whether or not this resource was updated during the most recent action. + # This is set to `false` at the beginning of each action. + # + # @return [Boolean] Whether the resource was updated during the most recent action. + # + def updated_by_last_action? + @updated_by_last_action + end + + # + # Set whether this class was updated during an action. + # + # @deprecated Multiple actions are supported by resources. Please call {}#updated_by_last_action} instead. + # + def updated=(true_or_false) + Chef::Log.warn("Chef::Resource#updated=(true|false) is deprecated. Please call #updated_by_last_action(true|false) instead.") + Chef::Log.warn("Called from:") + caller[0..3].each {|line| Chef::Log.warn(line)} + updated_by_last_action(true_or_false) + @updated = true_or_false + end + + # + # The DSL name of this resource (e.g. `package` or `yum_package`) + # + # @return [String] The DSL name of this resource. + def self.dsl_name + convert_to_snake_case(name, 'Chef::Resource') + end + + # + # The name of this resource (e.g. `file`) + # + # @return [String] The name of this resource. + # + attr_reader :resource_name + + # + # Sets a list of capabilities of the real resource. For example, `:remount` + # (for filesystems) and `:restart` (for services). + # + # TODO Calling resource.supports({}) will not set this to empty; it will do + # a get instead. That's wrong. + # + # @param args Hash{Symbol=>Boolean} If non-empty, sets the capabilities of + # this resource. Default: {} + # @return Hash{Symbol=>Boolean} An array of things this resource supports. + # + def supports(args={}) + if args.any? + @supports = args + else + @supports end - @not_if + end + def supports=(args) + supports(args) + end + + # + # A hook called after a resource is created. Meant to be overriden by + # subclasses. + # + def after_created + nil + end + + # + # The module where Chef should look for providers for this resource. + # The provider for `MyResource` will be looked up using + # `provider_base::MyResource`. Defaults to `Chef::Provider`. + # + # @param arg [Module] The module containing providers for this resource + # @return [Module] The module containing providers for this resource + # + # @example + # class MyResource < Chef::Resource + # provider_base Chef::Provider::Deploy + # # ...other stuff + # end + # + def self.provider_base(arg=nil) + @provider_base ||= arg + @provider_base ||= Chef::Provider + end + + + # + # Internal Resource Interface (for Chef) + # + + FORBIDDEN_IVARS = [:@run_context, :@not_if, :@only_if, :@enclosing_provider] + HIDDEN_IVARS = [:@allowed_actions, :@resource_name, :@source_line, :@run_context, :@name, :@not_if, :@only_if, :@elapsed_time, :@enclosing_provider] + + include Chef::Mixin::ConvertToClassName + extend Chef::Mixin::ConvertToClassName + extend Chef::Mixin::DescendantsTracker + + # XXX: this is required for definition params inside of the scope of a + # subresource to work correctly. + attr_accessor :params + + # @return [Chef::RunContext] The run context for this Resource. This is + # where the context for the current Chef run is stored, including the node + # and the resource collection. + attr_accessor :run_context + + # @return [String] The cookbook this resource was declared in. + attr_accessor :cookbook_name + + # @return [String] The recipe this resource was declared in. + attr_accessor :recipe_name + + # @return [Chef::Provider] The provider this resource was declared in (if + # it was declared in an LWRP). When you call methods that do not exist + # on this Resource, Chef will try to call the method on the provider + # as well before giving up. + attr_accessor :enclosing_provider + + # @return [String] The source line where this resource was declared. + # Expected to come from caller() or a stack trace, it usually follows one + # of these formats: + # /some/path/to/file.rb:80:in `wombat_tears' + # C:/some/path/to/file.rb:80 in 1`wombat_tears' + attr_accessor :source_line + + # @return [String] The actual name that was used to create this resource. + # Sometimes, when you say something like `package 'blah'`, the system will + # create a different resource (i.e. `YumPackage`). When this happens, the + # user will expect to see the thing they wrote, not the type that was + # returned. May be `nil`, in which case callers should read #resource_name. + # See #declared_key. + attr_accessor :declared_type + + # + # Iterates over all immediate and delayed notifications, calling + # resolve_resource_reference on each in turn, causing them to + # resolve lazy/forward references. + def resolve_notification_references + run_context.immediate_notifications(self).each { |n| + n.resolve_resource_reference(run_context.resource_collection) + } + run_context.delayed_notifications(self).each {|n| + n.resolve_resource_reference(run_context.resource_collection) + } + end + + # Helper for #notifies + def notifies_immediately(action, resource_spec) + run_context.notifies_immediately(Notification.new(resource_spec, action, self)) + end + + # Helper for #notifies + def notifies_delayed(action, resource_spec) + run_context.notifies_delayed(Notification.new(resource_spec, action, self)) + end + + def self.strict_const_defined?(const) + const_defined?(const, false) + end + + class << self + # back-compat + # NOTE: that we do not support unregistering classes as descendents like + # we used to for LWRP unloading because that was horrible and removed in + # Chef-12. + alias :resource_classes :descendants + alias :find_subclass_by_name :find_descendants_by_name + end + + # If an unknown method is invoked, determine whether the enclosing Provider's + # lexical scope can fulfill the request. E.g. This happens when the Resource's + # block invokes new_resource. + def method_missing(method_symbol, *args, &block) + if enclosing_provider && enclosing_provider.respond_to?(method_symbol) + enclosing_provider.send(method_symbol, *args, &block) + else + raise NoMethodError, "undefined method `#{method_symbol.to_s}' for #{self.class.to_s}" + end + end + + # Helper for #notifies + def validate_resource_spec!(resource_spec) + run_context.resource_collection.validate_lookup_spec!(resource_spec) + end + + # We usually want to store and reference resources by their declared type and not the actual type that + # was looked up by the Resolver (IE, "package" becomes YumPackage class). If we have not been provided + # the declared key we want to fall back on the old to_s key. + def declared_key + return to_s if declared_type.nil? + "#{declared_type}[#{@name}]" + end + + def immediate_notifications + run_context.immediate_notifications(self) + end + + def delayed_notifications + run_context.delayed_notifications(self) end def defined_at @@ -531,6 +991,11 @@ class Chef end end + # + # The cookbook in which this Resource was defined (if any). + # + # @return Chef::CookbookVersion The cookbook in which this Resource was defined. + # def cookbook_version if cookbook_name run_context.cookbook_collection[cookbook_name] @@ -541,56 +1006,6 @@ class Chef run_context.events end - def run_action(action, notification_type=nil, notifying_resource=nil) - # reset state in case of multiple actions on the same resource. - @elapsed_time = 0 - start_time = Time.now - events.resource_action_start(self, action, notification_type, notifying_resource) - # Try to resolve lazy/forward references in notifications again to handle - # the case where the resource was defined lazily (ie. in a ruby_block) - resolve_notification_references - validate_action(action) - - if Chef::Config[:verbose_logging] || Chef::Log.level == :debug - # This can be noisy - Chef::Log.info("Processing #{self} action #{action} (#{defined_at})") - end - - # ensure that we don't leave @updated_by_last_action set to true - # on accident - updated_by_last_action(false) - - # Don't modify @retries directly and keep it intact, so that the - # recipe_snippet from ResourceFailureInspector can print the value - # that was set in the resource block initially. - remaining_retries = retries - - begin - return if should_skip?(action) - provider_for_action(action).run_action - rescue Exception => e - if ignore_failure - Chef::Log.error("#{custom_exception_message(e)}; ignore_failure is set, continuing") - events.resource_failed(self, action, e) - elsif remaining_retries > 0 - events.resource_failed_retriable(self, action, remaining_retries, e) - remaining_retries -= 1 - Chef::Log.info("Retrying execution of #{self}, #{remaining_retries} attempt(s) left") - sleep retry_delay - retry - else - events.resource_failed(self, action, e) - raise customize_exception(e) - end - ensure - @elapsed_time = Time.now - start_time - # Reporting endpoint doesn't accept a negative resource duration so set it to 0. - # A negative value can occur when a resource changes the system time backwards - @elapsed_time = 0 if @elapsed_time < 0 - events.resource_completed(self) - end - end - def validate_action(action) raise ArgumentError, "nil is not a valid action for resource #{self}" if action.nil? end @@ -601,6 +1016,30 @@ class Chef provider end + # ??? TODO Seems unused. Delete? + def noop(tf=nil) + if !tf.nil? + raise ArgumentError, "noop must be true or false!" unless tf == true || tf == false + @noop = tf + end + @noop + end + + # TODO Seems unused. Delete? + def is(*args) + if args.size == 1 + args.first + else + return *args + end + end + + # + # Preface an exception message with generic Resource information. + # + # @param e [StandardError] An exception with `e.message` + # @return [String] An exception message customized with class name. + # def custom_exception_message(e) "#{self} (#{defined_at}) had an error: #{e.class.name}: #{e.message}" end @@ -610,6 +1049,7 @@ class Chef new_exception.set_backtrace(e.backtrace) new_exception end + # Evaluates not_if and only_if conditionals. Returns a falsey value if any # of the conditionals indicate that this resource should be skipped, i.e., # if an only_if evaluates to false or a not_if evaluates to true. @@ -635,48 +1075,6 @@ class Chef end end - def updated_by_last_action(true_or_false) - @updated ||= true_or_false - @updated_by_last_action = true_or_false - end - - def updated_by_last_action? - @updated_by_last_action - end - - def updated? - updated - end - - def self.json_create(o) - resource = self.new(o["instance_vars"]["@name"]) - o["instance_vars"].each do |k,v| - resource.instance_variable_set("@#{k}".to_sym, v) - end - resource - end - - # Hook to allow a resource to run specific code after creation - def after_created - nil - end - - # Resources that want providers namespaced somewhere other than - # Chef::Provider can set the namespace with +provider_base+ - # Ex: - # class MyResource < Chef::Resource - # provider_base Chef::Provider::Deploy - # # ...other stuff - # end - def self.provider_base(arg=nil) - @provider_base ||= arg - @provider_base ||= Chef::Provider - end - - def self.node_map - @@node_map ||= NodeMap.new - end - # Maps a short_name (and optionally a platform and version) to a # Chef::Resource. This allows finer grained per platform resource # attributes and the end of overloaded resource definitions @@ -719,6 +1117,10 @@ class Chef klass end + def self.node_map + @@node_map ||= NodeMap.new + end + # Returns the class of a Chef::Resource based on the short name # ==== Parameters # short_name<Symbol>:: short_name of the resource (ie :directory) diff --git a/lib/chef/resource/conditional.rb b/lib/chef/resource/conditional.rb index 8960a4d57f..cdc2638ef0 100644 --- a/lib/chef/resource/conditional.rb +++ b/lib/chef/resource/conditional.rb @@ -55,7 +55,7 @@ class Chef def configure case @command - when String + when String,Array @guard_interpreter = new_guard_interpreter(@parent_resource, @command, @command_opts, &@block) @block = nil when nil diff --git a/lib/chef/resource/execute.rb b/lib/chef/resource/execute.rb index 6853b62887..9f8b629fb8 100644 --- a/lib/chef/resource/execute.rb +++ b/lib/chef/resource/execute.rb @@ -26,6 +26,12 @@ class Chef identity_attr :command + # The ResourceGuardInterpreter wraps a resource's guards in another resource. That inner resource + # needs to behave differently during (for example) why_run mode, so we flag it here. For why_run mode + # we still want to execute the guard resource even if we are not executing the wrapping resource. + # Only execute resources (and subclasses) can be guard interpreters. + attr_accessor :is_guard_interpreter + def initialize(name, run_context=nil) super @resource_name = :execute @@ -43,6 +49,7 @@ class Chef @allowed_actions.push(:run) @umask = nil @default_guard_interpreter = :execute + @is_guard_interpreter = false end def umask(arg=nil) diff --git a/lib/chef/resource/lwrp_base.rb b/lib/chef/resource/lwrp_base.rb index 20d177f507..0a1253780c 100644 --- a/lib/chef/resource/lwrp_base.rb +++ b/lib/chef/resource/lwrp_base.rb @@ -73,16 +73,7 @@ class Chef # Define an attribute on this resource, including optional validation # parameters. def self.attribute(attr_name, validation_opts={}) - # Ruby 1.8 doesn't support default arguments to blocks, but we have to - # use define_method with a block to capture +validation_opts+. - # Workaround this by defining two methods :( - class_eval(<<-SHIM, __FILE__, __LINE__) - def #{attr_name}(arg=nil) - _set_or_return_#{attr_name}(arg) - end - SHIM - - define_method("_set_or_return_#{attr_name.to_s}".to_sym) do |arg| + define_method(attr_name) do |arg=nil| set_or_return(attr_name.to_sym, arg, validation_opts) end end diff --git a/lib/chef/resource/template.rb b/lib/chef/resource/template.rb index 8c9607ee07..67a9e6a418 100644 --- a/lib/chef/resource/template.rb +++ b/lib/chef/resource/template.rb @@ -102,9 +102,8 @@ class Chef # # ==== Method Arguments: # Helper methods can also take arguments. The syntax available for - # argument specification will be dependent on ruby version. Ruby 1.8 only - # supports a subset of the argument specification syntax available for - # method definition, whereas 1.9 supports the full syntax. + # argument specification supports full syntax available for method + # definition. # # Continuing the above example of simplifying attribute access, we can # define a helper to look up app-specific attributes like this: diff --git a/lib/chef/run_context.rb b/lib/chef/run_context.rb index bb1bf28ad7..fc54506407 100644 --- a/lib/chef/run_context.rb +++ b/lib/chef/run_context.rb @@ -198,7 +198,7 @@ ERROR_MESSAGE end # An Array of all recipes that have been loaded. This is stored internally - # as a Hash, so ordering is not preserved when using ruby 1.8. + # as a Hash, so ordering is predictable. # # Recipe names are given in fully qualified form, e.g., the recipe "nginx" # will be given as "nginx::default" @@ -209,7 +209,7 @@ ERROR_MESSAGE end # An Array of all attributes files that have been loaded. Stored internally - # using a Hash, so order is not preserved on ruby 1.8. + # using a Hash, so order is predictable. # # Attribute file names are given in fully qualified form, e.g., # "nginx::default" instead of "nginx". diff --git a/lib/chef/util/diff.rb b/lib/chef/util/diff.rb index 3117484a71..c2dc6e045c 100644 --- a/lib/chef/util/diff.rb +++ b/lib/chef/util/diff.rb @@ -176,10 +176,7 @@ class Chef end def encode_diff_for_json(diff_str) - if Object.const_defined? :Encoding - diff_str.encode!('UTF-8', :invalid => :replace, :undef => :replace, :replace => '?') - end - return diff_str + diff_str.encode!('UTF-8', :invalid => :replace, :undef => :replace, :replace => '?') end end diff --git a/lib/chef/util/path_helper.rb b/lib/chef/util/path_helper.rb index 26c9c76fe5..a5485a864b 100644 --- a/lib/chef/util/path_helper.rb +++ b/lib/chef/util/path_helper.rb @@ -101,9 +101,6 @@ class Chef # Produces a comparable path. def self.canonical_path(path, add_prefix=true) - # Rather than find an equivalent for File.absolute_path on 1.8.7, just bail out - raise NotImplementedError, "This feature is not supported on Ruby versions < 1.9" if RUBY_VERSION.to_f < 1.9 - # First remove extra separators and resolve any relative paths abs_path = File.absolute_path(path) diff --git a/spec/data/recipes.tgz b/spec/data/recipes.tgz Binary files differnew file mode 100644 index 0000000000..a6c172a001 --- /dev/null +++ b/spec/data/recipes.tgz diff --git a/spec/functional/file_content_management/deploy_strategies_spec.rb b/spec/functional/file_content_management/deploy_strategies_spec.rb index bcd171eb73..03a6c504c1 100644 --- a/spec/functional/file_content_management/deploy_strategies_spec.rb +++ b/spec/functional/file_content_management/deploy_strategies_spec.rb @@ -20,15 +20,6 @@ require 'spec_helper' shared_examples_for "a content deploy strategy" do - # Ruby 1.8 has no binread - def binread(file) - if IO.respond_to?(:binread) - IO.binread(file) - else - IO.read(file) - end - end - def normalize_mode(mode_int) ( mode_int & 07777).to_s(8) end @@ -160,7 +151,7 @@ shared_examples_for "a content deploy strategy" do it "updates the target with content from staged" do content_deployer.deploy(staging_file_path, target_file_path) - expect(binread(target_file_path)).to eq(staging_file_content) + expect(IO.binread(target_file_path)).to eq(staging_file_content) end context "when the owner of the target file is not the owner of the staging file", :requires_root do diff --git a/spec/functional/knife/exec_spec.rb b/spec/functional/knife/exec_spec.rb index 0a9177b5e8..6262094a9f 100644 --- a/spec/functional/knife/exec_spec.rb +++ b/spec/functional/knife/exec_spec.rb @@ -41,9 +41,7 @@ describe Chef::Knife::Exec do @server.stop end - skip "executes a script in the context of the chef-shell main context", :ruby_18_only - - it "executes a script in the context of the chef-shell main context", :ruby_gte_19_only do + it "executes a script in the context of the chef-shell main context" do @node = Chef::Node.new @node.name("ohai-world") response = {"rows" => [@node],"start" => 0,"total" => 1} diff --git a/spec/functional/resource/execute_spec.rb b/spec/functional/resource/execute_spec.rb index 020814fcd6..aaa1c772b7 100644 --- a/spec/functional/resource/execute_spec.rb +++ b/spec/functional/resource/execute_spec.rb @@ -35,6 +35,32 @@ describe Chef::Resource::Execute do end end + describe "when why_run is enabled" do + before do + Chef::Config[:why_run] = true + end + + let(:guard) { "ruby -e 'exit 0'" } + let!(:guard_resource) { + interpreter = Chef::GuardInterpreter::ResourceGuardInterpreter.new(resource, guard, nil) + interpreter.send(:get_interpreter_resource, resource) + } + + it "executes the guard and not the regular resource" do + expect_any_instance_of(Chef::GuardInterpreter::ResourceGuardInterpreter).to receive(:get_interpreter_resource).and_return(guard_resource) + + # why_run mode doesn't disable the updated_by_last_action logic, so we really have to look at the provider action + # to see if why_run correctly disabled the resource. It should shell_out! for the guard but not the resource. + expect_any_instance_of(Chef::Provider::Execute).to receive(:shell_out!).once + + resource.only_if guard + resource.run_action(:run) + + expect(resource).to be_updated_by_last_action + expect(guard_resource).to be_updated_by_last_action + end + end + describe "when parent resource sets :cwd" do let(:guard) { %{ruby -e 'exit 1 unless File.exists?("./big_json_plus_one.json")'} } diff --git a/spec/functional/resource/powershell_spec.rb b/spec/functional/resource/powershell_spec.rb index 033f34e256..1b3ac844e0 100644 --- a/spec/functional/resource/powershell_spec.rb +++ b/spec/functional/resource/powershell_spec.rb @@ -56,6 +56,21 @@ 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" do + file = Tempfile.new(['foo', '.ps1']) + begin + file.write "exit -27" + file.close + resource.code(". \"#{file.path}\"") + resource.returns(-27) + resource.run_action(:run) + ensure + file.close + file.unlink + end + end + + it "returns the process exit code" do resource.code(arbitrary_nonzero_process_exit_code_content) resource.returns(arbitrary_nonzero_process_exit_code) diff --git a/spec/functional/resource/user/useradd_spec.rb b/spec/functional/resource/user/useradd_spec.rb index 6b962c19aa..9ac88d7b60 100644 --- a/spec/functional/resource/user/useradd_spec.rb +++ b/spec/functional/resource/user/useradd_spec.rb @@ -82,12 +82,25 @@ describe Chef::Provider::User::Useradd, metadata do end after do - begin - pw_entry # will raise if the user doesn't exist - shell_out!("userdel", "-r", username, :returns => [0,12]) - rescue UserNotFound - # nothing to remove + max_retries = 3 + while max_retries > 0 + begin + pw_entry # will raise if the user doesn't exist + status = shell_out!("userdel", "-r", username, :returns => [0,8,12]) + + # Error code 8 during userdel indicates that the user is logged in. + # This occurs randomly because the accounts daemon holds a lock due to which userdel fails. + # The work around is to retry userdel for 3 times. + break if status.exitstatus != 8 + + sleep 1 + max_retries = max_retries -1 + rescue UserNotFound + break + end end + + status.error! if max_retries == 0 end let(:node) do diff --git a/spec/integration/client/client_spec.rb b/spec/integration/client/client_spec.rb index 62660bb852..3475a569b6 100644 --- a/spec/integration/client/client_spec.rb +++ b/spec/integration/client/client_spec.rb @@ -1,5 +1,34 @@ require 'support/shared/integration/integration_helper' require 'chef/mixin/shell_out' +require 'tiny_server' +require 'tmpdir' + +def recipes_filename + File.join(CHEF_SPEC_DATA, 'recipes.tgz') +end + +def start_tiny_server(server_opts={}) + recipes_size = File::Stat.new(recipes_filename).size + @server = TinyServer::Manager.new(server_opts) + @server.start + @api = TinyServer::API.instance + @api.clear + # + # trivial endpoints + # + # just a normal file + # (expected_content should be uncompressed) + @api.get("/recipes.tgz", 200) { + File.open(recipes_filename, "rb") do |f| + f.read + end + } +end + +def stop_tiny_server + @server.stop + @server = @api = nil +end describe "chef-client" do include IntegrationSupport @@ -279,4 +308,28 @@ end end end + context "when using recipe-url" do + before(:all) do + start_tiny_server + end + + after(:all) do + stop_tiny_server + end + + let(:tmp_dir) { Dir.mktmpdir("recipe-url") } + + it "should complete with success when passed -z and --recipe-url" do + file 'config/client.rb', <<EOM +chef_repo_path "#{tmp_dir}" +EOM + result = shell_out("#{chef_client} -c \"#{path_to('config/client.rb')}\" --recipe-url=http://localhost:9000/recipes.tgz -o 'x::default' -z", :cwd => tmp_dir) + result.error! + end + + it 'should fail when passed --recipe-url and not passed -z' do + result = shell_out("#{chef_client} --recipe-url=http://localhost:9000/recipes.tgz", :cwd => tmp_dir) + expect(result.exitstatus).to eq(1) + end + end end diff --git a/spec/integration/knife/chef_fs_data_store_spec.rb b/spec/integration/knife/chef_fs_data_store_spec.rb index eb02db5384..c1f2c7134f 100644 --- a/spec/integration/knife/chef_fs_data_store_spec.rb +++ b/spec/integration/knife/chef_fs_data_store_spec.rb @@ -138,6 +138,7 @@ EOM context 'PUT /TYPE/NAME' do before do file 'empty.json', {} + file 'dummynode.json', { "name" => "x", "chef_environment" => "rspec" , "json_class" => "Chef::Node", "normal" => {"foo" => "bar"}} file 'rolestuff.json', '{"description":"hi there","name":"x"}' file 'cookbooks_to_upload/x/metadata.rb', cookbook_x_100_metadata_rb end @@ -165,9 +166,10 @@ EOM knife('list --local /environments').should_succeed "/environments/x.json\n" end - it 'knife raw -z -i empty.json -m PUT /nodes/x' do - knife("raw -z -i #{path_to('empty.json')} -m PUT /nodes/x").should_succeed( /"x"/ ) + it 'knife raw -z -i dummynode.json -m PUT /nodes/x' do + knife("raw -z -i #{path_to('dummynode.json')} -m PUT /nodes/x").should_succeed( /"x"/ ) knife('list --local /nodes').should_succeed "/nodes/x.json\n" + knife('show -z /nodes/x.json --verbose').should_succeed /"bar"/ end it 'knife raw -z -i empty.json -m PUT /roles/x' do @@ -196,6 +198,7 @@ EOM context 'POST /TYPE/NAME' do before do file 'empty.json', { 'name' => 'z' } + file 'dummynode.json', { "name" => "z", "chef_environment" => "rspec" , "json_class" => "Chef::Node", "normal" => {"foo" => "bar"}} file 'empty_x.json', { 'name' => 'x' } file 'empty_id.json', { 'id' => 'z' } file 'rolestuff.json', '{"description":"hi there","name":"x"}' @@ -231,9 +234,10 @@ EOM knife('list --local /environments').should_succeed "/environments/z.json\n" end - it 'knife raw -z -i empty.json -m POST /nodes' do - knife("raw -z -i #{path_to('empty.json')} -m POST /nodes").should_succeed( /uri/ ) + it 'knife raw -z -i dummynode.json -m POST /nodes' do + knife("raw -z -i #{path_to('dummynode.json')} -m POST /nodes").should_succeed( /uri/ ) knife('list --local /nodes').should_succeed "/nodes/z.json\n" + knife('show -z /nodes/z.json').should_succeed /"bar"/ end it 'knife raw -z -i empty.json -m POST /roles' do diff --git a/spec/integration/knife/download_spec.rb b/spec/integration/knife/download_spec.rb index cf1e4fcf0f..c87e6fe20a 100644 --- a/spec/integration/knife/download_spec.rb +++ b/spec/integration/knife/download_spec.rb @@ -1069,7 +1069,7 @@ EOM end when_the_repository 'is empty' do - it 'knife download /cookbooks/x signs all requests', :ruby_gte_19_only do + it 'knife download /cookbooks/x signs all requests' do # Check that BasicClient.request() always gets called with X-OPS-USERID original_new = Chef::HTTP::BasicClient.method(:new) diff --git a/spec/integration/solo/solo_spec.rb b/spec/integration/solo/solo_spec.rb index cc9ba1abb2..41f5f5506f 100644 --- a/spec/integration/solo/solo_spec.rb +++ b/spec/integration/solo/solo_spec.rb @@ -83,8 +83,7 @@ end EOM end - # Ruby 1.8.7 doesn't have Process.spawn :( - it "while running solo concurrently", :ruby_gte_19_only => true do + it "while running solo concurrently" do file 'config/solo.rb', <<EOM cookbook_path "#{path_to('cookbooks')}" file_cache_path "#{path_to('config/cache')}" diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 995be5060b..b87736efef 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -124,11 +124,7 @@ RSpec.configure do |config| config.filter_run_excluding :aix_only => true unless aix? config.filter_run_excluding :supports_cloexec => true unless supports_cloexec? config.filter_run_excluding :selinux_only => true unless selinux_enabled? - config.filter_run_excluding :ruby_18_only => true unless ruby_18? - config.filter_run_excluding :ruby_19_only => true unless ruby_19? - config.filter_run_excluding :ruby_gte_19_only => true unless ruby_gte_19? config.filter_run_excluding :ruby_20_only => true unless ruby_20? - config.filter_run_excluding :ruby_gte_20_only => true unless ruby_gte_20? # chef_gte_XX_only and chef_lt_XX_only pair up correctly with the same XX # number. please conserve this pattern & resist filling out all the operators config.filter_run_excluding :chef_gte_13_only => true unless chef_gte_13? @@ -137,9 +133,8 @@ RSpec.configure do |config| config.filter_run_excluding :requires_root_or_running_windows => true unless (root? || windows?) config.filter_run_excluding :requires_unprivileged_user => true if root? config.filter_run_excluding :uses_diff => true unless has_diff? - config.filter_run_excluding :ruby_gte_20_and_openssl_gte_101 => true unless (ruby_gte_20? && openssl_gte_101?) + config.filter_run_excluding :openssl_gte_101 => true unless openssl_gte_101? config.filter_run_excluding :openssl_lt_101 => true unless openssl_lt_101? - config.filter_run_excluding :ruby_lt_20 => true unless ruby_lt_20? config.filter_run_excluding :aes_256_gcm_only => true unless aes_256_gcm? config.filter_run_excluding :broken => true diff --git a/spec/stress/win32/file_spec.rb b/spec/stress/win32/file_spec.rb index dd1dcd305e..6c4b26b05c 100644 --- a/spec/stress/win32/file_spec.rb +++ b/spec/stress/win32/file_spec.rb @@ -24,12 +24,12 @@ describe 'Chef::ReservedNames::Win32::File', :windows_only do @path = File.expand_path(File.join(File.dirname(__FILE__), "..", "..", "data", "old_home_dir", "my-dot-emacs")) end - it "should not leak significant memory" do + it "should not leak significant memory", :volatile do test = lambda { Chef::ReservedNames::Win32::File.symlink?(@path) } expect(test).not_to leak_memory(:warmup => 50000, :iterations => 50000) end - it "should not leak handles" do + it "should not leak handles", :volatile do test = lambda { Chef::ReservedNames::Win32::File.symlink?(@path) } expect(test).not_to leak_handles(:warmup => 50, :iterations => 100) end diff --git a/spec/support/platform_helpers.rb b/spec/support/platform_helpers.rb index 959580c953..a412fe38e1 100644 --- a/spec/support/platform_helpers.rb +++ b/spec/support/platform_helpers.rb @@ -6,10 +6,6 @@ class ShellHelpers extend Chef::Mixin::ShellOut end -def ruby_gte_20? - RUBY_VERSION.to_f >= 2.0 -end - def ruby_lt_20? !ruby_gte_20? end @@ -30,14 +26,6 @@ def ruby_20? !!(RUBY_VERSION =~ /^2.0/) end -def ruby_19? - !!(RUBY_VERSION =~ /^1.9/) -end - -def ruby_18? - !!(RUBY_VERSION =~ /^1.8/) -end - def windows? !!(RUBY_PLATFORM =~ /mswin|mingw|windows/) end diff --git a/spec/unit/application/client_spec.rb b/spec/unit/application/client_spec.rb index 33af9bc5c1..cce3d11577 100644 --- a/spec/unit/application/client_spec.rb +++ b/spec/unit/application/client_spec.rb @@ -26,6 +26,7 @@ describe Chef::Application::Client, "reconfigure" do before do allow(Kernel).to receive(:trap).and_return(:ok) + allow(::File).to receive(:read).with("/etc/chef/client.rb").and_return("") @original_argv = ARGV.dup ARGV.clear @@ -215,8 +216,21 @@ Enable chef-client interval runs by setting `:client_fork = true` in your config end end end + + describe "when both the pidfile and lockfile opts are set to the same value" do + + before do + Chef::Config[:pid_file] = "/path/to/file" + Chef::Config[:lockfile] = "/path/to/file" + end + + it "should throw an exception" do + expect { @app.reconfigure }.to raise_error + end + end end + describe Chef::Application::Client, "setup_application" do before do @app = Chef::Application::Client.new @@ -236,11 +250,13 @@ describe Chef::Application::Client, "setup_application" do end describe Chef::Application::Client, "configure_chef" do + let(:app) { Chef::Application::Client.new } + before do @original_argv = ARGV.dup ARGV.clear - @app = Chef::Application::Client.new - @app.configure_chef + allow(::File).to receive(:read).with("/etc/chef/client.rb").and_return("") + app.configure_chef end after do diff --git a/spec/unit/chef_fs/config_spec.rb b/spec/unit/chef_fs/config_spec.rb index 031da6c4b5..c7c47ad8ab 100644 --- a/spec/unit/chef_fs/config_spec.rb +++ b/spec/unit/chef_fs/config_spec.rb @@ -55,4 +55,56 @@ describe Chef::ChefFS::Config do Chef::ChefFS::Config.new(base_config, Dir.pwd, {}, ui) end end + + describe "local FS configuration" do + + let(:chef_config) do + Mash.new({ + client_path: "/base_path/clients", + cookbook_path: "/base_path/cookbooks", + data_bag_path: "/base_path/data_bags", + environment_path: "/base_path/environments", + node_path: "/base_path/nodes", + role_path: "/base_path/roles", + user_path: "/base_path/users", + policy_path: "/base_path/policies" + }) + end + + let(:chef_fs_config) { Chef::ChefFS::Config.new(chef_config, Dir.pwd) } + + subject(:local_fs) { chef_fs_config.local_fs } + + def platform_path(*args) + File.expand_path(*args) + end + + it "sets the correct nodes path on the local FS object" do + expect(local_fs.child_paths["nodes"]).to eq([platform_path("/base_path/nodes")]) + end + + it "sets the correct cookbook path on the local FS object" do + expect(local_fs.child_paths["cookbooks"]).to eq([platform_path("/base_path/cookbooks")]) + end + + it "sets the correct data bag path on the local FS object" do + expect(local_fs.child_paths["data_bags"]).to eq([platform_path("/base_path/data_bags")]) + end + + it "sets the correct environment path on the local FS object" do + expect(local_fs.child_paths["environments"]).to eq([platform_path("/base_path/environments")]) + end + + it "sets the correct role path on the local FS object" do + expect(local_fs.child_paths["roles"]).to eq([platform_path("/base_path/roles")]) + end + + it "sets the correct user path on the local FS object" do + expect(local_fs.child_paths["users"]).to eq([platform_path("/base_path/users")]) + end + + it "sets the correct policy path on the local FS object" do + expect(local_fs.child_paths["policies"]).to eq([platform_path("/base_path/policies")]) + end + end end diff --git a/spec/unit/chef_fs/file_pattern_spec.rb b/spec/unit/chef_fs/file_pattern_spec.rb index cdf506225a..a9f06e8424 100644 --- a/spec/unit/chef_fs/file_pattern_spec.rb +++ b/spec/unit/chef_fs/file_pattern_spec.rb @@ -355,7 +355,7 @@ describe Chef::ChefFS::FilePattern do 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: - pattern.could_match_children?('/xyz').should be_falsey + 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) diff --git a/spec/unit/data_bag_item_spec.rb b/spec/unit/data_bag_item_spec.rb index 4cf6e59242..4348252388 100644 --- a/spec/unit/data_bag_item_spec.rb +++ b/spec/unit/data_bag_item_spec.rb @@ -20,86 +20,86 @@ require 'spec_helper' require 'chef/data_bag_item' describe Chef::DataBagItem do - before(:each) do - @data_bag_item = Chef::DataBagItem.new - end + let(:data_bag_item) { Chef::DataBagItem.new } describe "initialize" do it "should be a Chef::DataBagItem" do - expect(@data_bag_item).to be_a_kind_of(Chef::DataBagItem) + expect(data_bag_item).to be_a_kind_of(Chef::DataBagItem) end end describe "data_bag" do it "should let you set the data_bag to a string" do - expect(@data_bag_item.data_bag("clowns")).to eq("clowns") + expect(data_bag_item.data_bag("clowns")).to eq("clowns") end it "should return the current data_bag type" do - @data_bag_item.data_bag "clowns" - expect(@data_bag_item.data_bag).to eq("clowns") + data_bag_item.data_bag "clowns" + expect(data_bag_item.data_bag).to eq("clowns") end it "should not accept spaces" do - expect { @data_bag_item.data_bag "clown masters" }.to raise_error(ArgumentError) + expect { data_bag_item.data_bag "clown masters" }.to raise_error(ArgumentError) end it "should throw an ArgumentError if you feed it anything but a string" do - expect { @data_bag_item.data_bag Hash.new }.to raise_error(ArgumentError) + expect { data_bag_item.data_bag Hash.new }.to raise_error(ArgumentError) end end describe "raw_data" do it "should let you set the raw_data with a hash" do - expect { @data_bag_item.raw_data = { "id" => "octahedron" } }.not_to raise_error + expect { data_bag_item.raw_data = { "id" => "octahedron" } }.not_to raise_error end it "should let you set the raw_data from a mash" do - expect { @data_bag_item.raw_data = Mash.new({ "id" => "octahedron" }) }.not_to raise_error + expect { data_bag_item.raw_data = Mash.new({ "id" => "octahedron" }) }.not_to raise_error end it "should raise an exception if you set the raw data without a key" do - expect { @data_bag_item.raw_data = { "monkey" => "pants" } }.to raise_error(ArgumentError) + expect { data_bag_item.raw_data = { "monkey" => "pants" } }.to raise_error(ArgumentError) end it "should raise an exception if you set the raw data to something other than a hash" do - expect { @data_bag_item.raw_data = "katie rules" }.to raise_error(ArgumentError) + expect { data_bag_item.raw_data = "katie rules" }.to raise_error(ArgumentError) end it "should accept alphanum/-/_ for the id" do - expect { @data_bag_item.raw_data = { "id" => "h1-_" } }.not_to raise_error + expect { data_bag_item.raw_data = { "id" => "h1-_" } }.not_to raise_error end it "should accept alphanum.alphanum for the id" do - expect { @data_bag_item.raw_data = { "id" => "foo.bar" } }.not_to raise_error + expect { data_bag_item.raw_data = { "id" => "foo.bar" } }.not_to raise_error end it "should accept .alphanum for the id" do - expect { @data_bag_item.raw_data = { "id" => ".bozo" } }.not_to raise_error + expect { data_bag_item.raw_data = { "id" => ".bozo" } }.not_to raise_error end it "should raise an exception if the id contains anything but alphanum/-/_" do - expect { @data_bag_item.raw_data = { "id" => "!@#" } }.to raise_error(ArgumentError) + expect { data_bag_item.raw_data = { "id" => "!@#" } }.to raise_error(ArgumentError) end it "should return the raw data" do - @data_bag_item.raw_data = { "id" => "highway_of_emptiness" } - expect(@data_bag_item.raw_data).to eq({ "id" => "highway_of_emptiness" }) + data_bag_item.raw_data = { "id" => "highway_of_emptiness" } + expect(data_bag_item.raw_data).to eq({ "id" => "highway_of_emptiness" }) end it "should be a Mash by default" do - expect(@data_bag_item.raw_data).to be_a_kind_of(Mash) + expect(data_bag_item.raw_data).to be_a_kind_of(Mash) end end describe "object_name" do - before(:each) do - @data_bag_item.data_bag("dreams") - @data_bag_item.raw_data = { "id" => "the_beatdown" } - end + let(:data_bag_item) { + data_bag_item = Chef::DataBagItem.new + data_bag_item.data_bag("dreams") + data_bag_item.raw_data = { "id" => "the_beatdown" } + data_bag_item + } it "should return an object name based on the bag name and the raw_data id" do - expect(@data_bag_item.object_name).to eq("data_bag_item_dreams_the_beatdown") + expect(data_bag_item.object_name).to eq("data_bag_item_dreams_the_beatdown") end end @@ -110,17 +110,19 @@ describe Chef::DataBagItem do end describe "when used like a Hash" do - before(:each) do - @data_bag_item.raw_data = { "id" => "journey", "trials" => "been through" } - end + let(:data_bag_item) { + data_bag_item = Chef::DataBagItem.new + data_bag_item.raw_data = { "id" => "journey", "trials" => "been through" } + data_bag_item + } it "responds to keys" do - expect(@data_bag_item.keys).to include("id") - expect(@data_bag_item.keys).to include("trials") + expect(data_bag_item.keys).to include("id") + expect(data_bag_item.keys).to include("trials") end it "supports element reference with []" do - expect(@data_bag_item["id"]).to eq("journey") + expect(data_bag_item["id"]).to eq("journey") end it "implements all the methods of Hash" do @@ -131,100 +133,113 @@ describe Chef::DataBagItem do :invert, :update, :replace, :merge!, :merge, :has_key?, :has_value?, :key?, :value?] methods.each do |m| - expect(@data_bag_item).to respond_to(m) + expect(data_bag_item).to respond_to(m) end end - end describe "to_hash" do - before(:each) do - @data_bag_item.data_bag("still_lost") - @data_bag_item.raw_data = { "id" => "whoa", "i_know" => "kung_fu" } - @to_hash = @data_bag_item.to_hash - end + let(:data_bag_item) { + data_bag_item = Chef::DataBagItem.new + data_bag_item.data_bag("still_lost") + data_bag_item.raw_data = { "id" => "whoa", "i_know" => "kung_fu" } + data_bag_item + } + + let(:to_hash) { data_bag_item.to_hash } it "should return a hash" do - expect(@to_hash).to be_a_kind_of(Hash) + expect(to_hash).to be_a_kind_of(Hash) end it "should have the raw_data keys as top level keys" do - expect(@to_hash["id"]).to eq("whoa") - expect(@to_hash["i_know"]).to eq("kung_fu") + expect(to_hash["id"]).to eq("whoa") + expect(to_hash["i_know"]).to eq("kung_fu") end it "should have the chef_type of data_bag_item" do - expect(@to_hash["chef_type"]).to eq("data_bag_item") + expect(to_hash["chef_type"]).to eq("data_bag_item") end it "should have the data_bag set" do - expect(@to_hash["data_bag"]).to eq("still_lost") + expect(to_hash["data_bag"]).to eq("still_lost") end end describe "when deserializing from JSON" do - before(:each) do - @data_bag_item.data_bag('mars_volta') - @data_bag_item.raw_data = { "id" => "octahedron", "snooze" => { "finally" => :world_will }} - @deserial = Chef::JSONCompat.from_json(Chef::JSONCompat.to_json(@data_bag_item)) - end + let(:data_bag_item) { + data_bag_item = Chef::DataBagItem.new + data_bag_item.data_bag('mars_volta') + data_bag_item.raw_data = { "id" => "octahedron", "snooze" => { "finally" => :world_will } } + data_bag_item + } + + let(:deserial) { Chef::JSONCompat.from_json(Chef::JSONCompat.to_json(data_bag_item)) } + it "should deserialize to a Chef::DataBagItem object" do - expect(@deserial).to be_a_kind_of(Chef::DataBagItem) + expect(deserial).to be_a_kind_of(Chef::DataBagItem) end it "should have a matching 'data_bag' value" do - expect(@deserial.data_bag).to eq(@data_bag_item.data_bag) + expect(deserial.data_bag).to eq(data_bag_item.data_bag) end it "should have a matching 'id' key" do - expect(@deserial["id"]).to eq("octahedron") + expect(deserial["id"]).to eq("octahedron") end it "should have a matching 'snooze' key" do - expect(@deserial["snooze"]).to eq({ "finally" => "world_will" }) + expect(deserial["snooze"]).to eq({ "finally" => "world_will" }) end include_examples "to_json equalivent to Chef::JSONCompat.to_json" do - let(:jsonable) { @data_bag_item } + let(:jsonable) { data_bag_item } end end describe "when converting to a string" do it "converts to a string in the form data_bag_item[ID]" do - @data_bag_item['id'] = "heart of darkness" - expect(@data_bag_item.to_s).to eq('data_bag_item[heart of darkness]') + data_bag_item['id'] = "heart of darkness" + expect(data_bag_item.to_s).to eq('data_bag_item[heart of darkness]') end it "inspects as data_bag_item[BAG, ID, RAW_DATA]" do raw_data = {"id" => "heart_of_darkness", "author" => "Conrad"} - @data_bag_item.raw_data = raw_data - @data_bag_item.data_bag("books") + data_bag_item.raw_data = raw_data + data_bag_item.data_bag("books") - expect(@data_bag_item.inspect).to eq("data_bag_item[\"books\", \"heart_of_darkness\", #{raw_data.inspect}]") + expect(data_bag_item.inspect).to eq("data_bag_item[\"books\", \"heart_of_darkness\", #{raw_data.inspect}]") end end describe "save" do + let(:server) { instance_double(Chef::REST) } + + let(:data_bag_item) { + data_bag_item = Chef::DataBagItem.new + data_bag_item['id'] = "heart of darkness" + data_bag_item.raw_data = {"id" => "heart_of_darkness", "author" => "Conrad"} + data_bag_item.data_bag("books") + data_bag_item + } + before do - @rest = double("Chef::REST") - allow(Chef::REST).to receive(:new).and_return(@rest) - @data_bag_item['id'] = "heart of darkness" - raw_data = {"id" => "heart_of_darkness", "author" => "Conrad"} - @data_bag_item.raw_data = raw_data - @data_bag_item.data_bag("books") + expect(Chef::REST).to receive(:new).and_return(server) end + it "should update the item when it already exists" do - expect(@rest).to receive(:put_rest).with("data/books/heart_of_darkness", @data_bag_item) - @data_bag_item.save + expect(server).to receive(:put_rest).with("data/books/heart_of_darkness", data_bag_item) + data_bag_item.save end it "should create if the item is not found" do exception = double("404 error", :code => "404") - expect(@rest).to receive(:put_rest).and_raise(Net::HTTPServerException.new("foo", exception)) - expect(@rest).to receive(:post_rest).with("data/books", @data_bag_item) - @data_bag_item.save + expect(server).to receive(:put_rest).and_raise(Net::HTTPServerException.new("foo", exception)) + expect(server).to receive(:post_rest).with("data/books", data_bag_item) + data_bag_item.save end + describe "when whyrun mode is enabled" do before do Chef::Config[:why_run] = true @@ -232,41 +247,60 @@ describe Chef::DataBagItem do after do Chef::Config[:why_run] = false end + it "should not save" do - expect(@rest).not_to receive(:put_rest) - expect(@rest).not_to receive(:post_rest) - @data_bag_item.data_bag("books") - @data_bag_item.save + expect(server).not_to receive(:put_rest) + expect(server).not_to receive(:post_rest) + data_bag_item.data_bag("books") + data_bag_item.save end end + end + + describe "destroy" do + let(:server) { instance_double(Chef::REST) } + + let(:data_bag_item) { + data_bag_item = Chef::DataBagItem.new + data_bag_item.data_bag('a_baggy_bag') + data_bag_item.raw_data = { "id" => "some_id" } + data_bag_item + } + it "should set default parameters" do + expect(Chef::REST).to receive(:new).and_return(server) + expect(server).to receive(:delete_rest).with("data/a_baggy_bag/data_bag_item_a_baggy_bag_some_id") + + data_bag_item.destroy + end end describe "when loading" do before do - @data_bag_item.raw_data = {"id" => "charlie", "shell" => "zsh", "ssh_keys" => %w{key1 key2}} - @data_bag_item.data_bag("users") + data_bag_item.raw_data = {"id" => "charlie", "shell" => "zsh", "ssh_keys" => %w{key1 key2}} + data_bag_item.data_bag("users") end describe "from an API call" do + let(:http_client) { double("Chef::REST") } + before do - @http_client = double("Chef::REST") - allow(Chef::REST).to receive(:new).and_return(@http_client) + allow(Chef::REST).to receive(:new).and_return(http_client) end it "converts raw data to a data bag item" do - expect(@http_client).to receive(:get_rest).with("data/users/charlie").and_return(@data_bag_item.to_hash) + expect(http_client).to receive(:get_rest).with("data/users/charlie").and_return(data_bag_item.to_hash) item = Chef::DataBagItem.load(:users, "charlie") expect(item).to be_a_kind_of(Chef::DataBagItem) - expect(item).to eq(@data_bag_item) + expect(item).to eq(data_bag_item) end it "does not convert when a DataBagItem is returned from the API call" do - expect(@http_client).to receive(:get_rest).with("data/users/charlie").and_return(@data_bag_item) + expect(http_client).to receive(:get_rest).with("data/users/charlie").and_return(data_bag_item) item = Chef::DataBagItem.load(:users, "charlie") expect(item).to be_a_kind_of(Chef::DataBagItem) - expect(item).to equal(@data_bag_item) + expect(item).to equal(data_bag_item) end end @@ -280,13 +314,11 @@ describe Chef::DataBagItem do end it "converts the raw data to a data bag item" do - expect(Chef::DataBag).to receive(:load).with('users').and_return({'charlie' => @data_bag_item.to_hash}) + expect(Chef::DataBag).to receive(:load).with('users').and_return({'charlie' => data_bag_item.to_hash}) item = Chef::DataBagItem.load('users', 'charlie') expect(item).to be_a_kind_of(Chef::DataBagItem) - expect(item).to eq(@data_bag_item) + expect(item).to eq(data_bag_item) end end - end - end diff --git a/spec/unit/encrypted_data_bag_item_spec.rb b/spec/unit/encrypted_data_bag_item_spec.rb index 14afea507c..0a4306727b 100644 --- a/spec/unit/encrypted_data_bag_item_spec.rb +++ b/spec/unit/encrypted_data_bag_item_spec.rb @@ -124,14 +124,6 @@ describe Chef::EncryptedDataBagItem::Encryptor do context "on unsupported platforms" do let(:aead_algorithm) { Chef::EncryptedDataBagItem::AEAD_ALGORITHM } - it "throws an error warning about the Ruby version if it has no GCM support" do - # Force OpenSSL with AEAD support - allow(OpenSSL::Cipher).to receive(:ciphers).and_return([ aead_algorithm ]) - # Ruby without AEAD support - expect(OpenSSL::Cipher).to receive(:method_defined?).with(:auth_data=).and_return(false) - expect { encryptor }.to raise_error(Chef::EncryptedDataBagItem::EncryptedDataBagRequirementsFailure, /requires Ruby/) - end - it "throws an error warning about the OpenSSL version if it has no GCM support" do # Force Ruby with AEAD support allow(OpenSSL::Cipher).to receive(:method_defined?).with(:auth_data=).and_return(true) @@ -140,14 +132,6 @@ describe Chef::EncryptedDataBagItem::Encryptor do expect { encryptor }.to raise_error(Chef::EncryptedDataBagItem::EncryptedDataBagRequirementsFailure, /requires an OpenSSL/) end - context "on platforms with old Ruby", :ruby_lt_20 do - - it "throws an error warning about the Ruby version" do - expect { encryptor }.to raise_error(Chef::EncryptedDataBagItem::EncryptedDataBagRequirementsFailure, /requires Ruby/) - end - - end # context on platforms with old Ruby - context "on platforms with old OpenSSL", :openssl_lt_101 do it "throws an error warning about the OpenSSL version" do @@ -214,14 +198,6 @@ describe Chef::EncryptedDataBagItem::Decryptor do } end - context "on platforms with old Ruby", :ruby_lt_20 do - - it "throws an error warning about the Ruby version" do - expect { decryptor }.to raise_error(Chef::EncryptedDataBagItem::EncryptedDataBagRequirementsFailure, /requires Ruby/) - end - - end # context on platforms with old Ruby - context "on platforms with old OpenSSL", :openssl_lt_101 do it "throws an error warning about the OpenSSL version" do diff --git a/spec/unit/knife/cookbook_site_share_spec.rb b/spec/unit/knife/cookbook_site_share_spec.rb index 0f97261ad4..515a1603ad 100644 --- a/spec/unit/knife/cookbook_site_share_spec.rb +++ b/spec/unit/knife/cookbook_site_share_spec.rb @@ -108,11 +108,20 @@ describe Chef::Knife::CookbookSiteShare do expect { @knife.run }.to raise_error(SystemExit) end - it 'should make a tarball of the cookbook' do - expect(@knife).to receive(:shell_out!) do |args| - expect(args.to_s).to match(/tar -czf/) + if File.exists?('/usr/bin/gnutar') || File.exists?('/bin/gnutar') + it 'should use gnutar to make a tarball of the cookbook' do + expect(@knife).to receive(:shell_out!) do |args| + expect(args.to_s).to match(/gnutar -czf/) + end + @knife.run + end + else + it 'should make a tarball of the cookbook' do + expect(@knife).to receive(:shell_out!) do |args| + expect(args.to_s).to match(/tar -czf/) + end + @knife.run end - @knife.run end it 'should exit and log to error when the tarball creation fails' do diff --git a/spec/unit/knife/cookbook_upload_spec.rb b/spec/unit/knife/cookbook_upload_spec.rb index 5dbd456ad8..fb94886cad 100644 --- a/spec/unit/knife/cookbook_upload_spec.rb +++ b/spec/unit/knife/cookbook_upload_spec.rb @@ -246,28 +246,62 @@ E describe 'with -a or --all' do before(:each) do knife.config[:all] = true - @test_cookbook1 = Chef::CookbookVersion.new('test_cookbook1', '/tmp/blah') - @test_cookbook2 = Chef::CookbookVersion.new('test_cookbook2', '/tmp/blah') - allow(cookbook_loader).to receive(:each).and_yield("test_cookbook1", @test_cookbook1).and_yield("test_cookbook2", @test_cookbook2) - allow(cookbook_loader).to receive(:cookbook_names).and_return(["test_cookbook1", "test_cookbook2"]) end - it 'should upload all cookbooks' do - expect(knife).to receive(:upload).once - knife.run - end + context 'when cookbooks exist in the cookbook path' do + before(:each) do + @test_cookbook1 = Chef::CookbookVersion.new('test_cookbook1', '/tmp/blah') + @test_cookbook2 = Chef::CookbookVersion.new('test_cookbook2', '/tmp/blah') + allow(cookbook_loader).to receive(:each).and_yield("test_cookbook1", @test_cookbook1).and_yield("test_cookbook2", @test_cookbook2) + allow(cookbook_loader).to receive(:cookbook_names).and_return(["test_cookbook1", "test_cookbook2"]) + end - it 'should report on success' do - expect(knife).to receive(:upload).once - expect(knife.ui).to receive(:info).with(/Uploaded all cookbooks/) - knife.run + it 'should upload all cookbooks' do + expect(knife).to receive(:upload).once + knife.run + end + + it 'should report on success' do + expect(knife).to receive(:upload).once + expect(knife.ui).to receive(:info).with(/Uploaded all cookbooks/) + knife.run + end + + it 'should update the version constraints for an environment' do + allow(knife).to receive(:assert_environment_valid!).and_return(true) + knife.config[:environment] = "production" + expect(knife).to receive(:update_version_constraints).once + knife.run + end end - it 'should update the version constraints for an environment' do - allow(knife).to receive(:assert_environment_valid!).and_return(true) - knife.config[:environment] = "production" - expect(knife).to receive(:update_version_constraints).once - knife.run + context 'when no cookbooks exist in the cookbook path' do + before(:each) do + allow(cookbook_loader).to receive(:each) + end + + it 'should not upload any cookbooks' do + expect(knife).to_not receive(:upload) + knife.run + end + + context 'when cookbook path is an array' do + it 'should warn users that no cookbooks exist' do + knife.config[:cookbook_path] = ['/chef-repo/cookbooks', '/home/user/cookbooks'] + expect(knife.ui).to receive(:warn).with( + /Could not find any cookbooks in your cookbook path: #{knife.config[:cookbook_path].join(', ')}\. Use --cookbook-path to specify the desired path\./) + knife.run + end + end + + context 'when cookbook path is a string' do + it 'should warn users that no cookbooks exist' do + knife.config[:cookbook_path] = '/chef-repo/cookbooks' + expect(knife.ui).to receive(:warn).with( + /Could not find any cookbooks in your cookbook path: #{knife.config[:cookbook_path]}\. Use --cookbook-path to specify the desired path\./) + knife.run + end + end end end diff --git a/spec/unit/knife/raw_spec.rb b/spec/unit/knife/raw_spec.rb new file mode 100644 index 0000000000..ab929abd39 --- /dev/null +++ b/spec/unit/knife/raw_spec.rb @@ -0,0 +1,43 @@ +# +# Author:: Steven Danna (<steve@getchef.com>) +# Copyright:: Copyright (c) 2014 Chef Software, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +require 'spec_helper' + +describe Chef::Knife::Raw do + let(:rest) do + r = double('Chef::Knife::Raw::RawInputServerAPI') + allow(Chef::Knife::Raw::RawInputServerAPI).to receive(:new).and_return(r) + r + end + + let(:knife) do + k = Chef::Knife::Raw.new + k.config[:method] = "GET" + k.name_args = [ "/nodes" ] + k + end + + describe "run" do + it "should set the x-ops-request-source header when --proxy-auth is set" do + knife.config[:proxy_auth] = true + expect(rest).to receive(:request).with(:GET, "/nodes", + { 'Content-Type' => 'application/json', + 'x-ops-request-source' => 'web'}, false) + knife.run + end + end +end diff --git a/spec/unit/org_spec.rb b/spec/unit/org_spec.rb new file mode 100644 index 0000000000..cd6cc94d91 --- /dev/null +++ b/spec/unit/org_spec.rb @@ -0,0 +1,196 @@ +# +# Author:: Steven Danna (steve@opscode.com) +# Copyright:: Copyright (c) 2014 Chef Software, Inc +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'spec_helper' + +require 'chef/org' +require 'tempfile' + +describe Chef::Org do + let(:org) { Chef::Org.new("an_org") } + + describe "initialize" do + it "is a Chef::Org" do + expect(org).to be_a_kind_of(Chef::Org) + end + end + + describe "name" do + it "lets you set the name to a string" do + org.name "sg1" + expect(org.name).to eq("sg1") + end + + # It is not feasible to check all invalid characters. Here are a few + # that we probably care about. + it "raises on invalid characters" do + # capital letters + expect { org.name "Bar" }.to raise_error(ArgumentError) + # slashes + expect { org.name "foo/bar" }.to raise_error(ArgumentError) + # ? + expect { org.name "foo?" }.to raise_error(ArgumentError) + # & + expect { org.name "foo&" }.to raise_error(ArgumentError) + # spaces + expect { org.name "foo " }.to raise_error(ArgumentError) + end + + it "raises an ArgumentError if you feed it anything but a string" do + expect { org.name Hash.new }.to raise_error(ArgumentError) + end + end + + describe "full_name" do + it "lets you set the full name" do + org.full_name "foo" + expect(org.full_name).to eq("foo") + end + + it "raises an ArgumentError if you feed it anything but a string" do + expect { org.name Hash.new }.to raise_error(ArgumentError) + end + end + + describe "private_key" do + it "returns the private key" do + org.private_key("super private") + expect(org.private_key).to eq("super private") + end + + it "raises an ArgumentError if you feed it something lame" do + expect { org.private_key Hash.new }.to raise_error(ArgumentError) + end + end + + describe "when serializing to JSON" do + let(:json) do + org.name("black") + org.full_name("black crowes") + org.to_json + end + + it "serializes as a JSON object" do + expect(json).to match(/^\{.+\}$/) + end + + it "includes the name value" do + expect(json).to include(%q{"name":"black"}) + end + + it "includes the full name value" do + expect(json).to include(%q{"full_name":"black crowes"}) + end + + it "includes the private key when present" do + org.private_key("monkeypants") + expect(org.to_json).to include(%q{"private_key":"monkeypants"}) + end + + it "does not include the private key if not present" do + expect(json).to_not include("private_key") + end + end + + describe "when deserializing from JSON" do + let(:org) do + o = { "name" => "turtle", + "full_name" => "turtle_club", + "private_key" => "pandas" } + Chef::Org.from_json(o.to_json) + end + + it "deserializes to a Chef::Org object" do + expect(org).to be_a_kind_of(Chef::Org) + end + + it "preserves the name" do + expect(org.name).to eq("turtle") + end + + it "preserves the full_name" do + expect(org.full_name).to eq("turtle_club") + end + + it "includes the private key if present" do + expect(org.private_key).to eq("pandas") + end + end + + describe "API Interactions" do + let(:rest) do + Chef::Config[:chef_server_root] = "http://www.example.com" + r = double('rest') + allow(Chef::REST).to receive(:new).and_return(r) + r + end + + let(:org) do + o = Chef::Org.new("foobar") + o.full_name "foo bar bat" + o + end + + describe "list" do + let(:response) { {"foobar" => "http://www.example.com/organizations/foobar"} } + let(:inflated_response) { {"foobar" => org } } + + it "lists all orgs" do + expect(rest).to receive(:get_rest).with("organizations").and_return(response) + expect(Chef::Org.list).to eq(response) + end + + it "inflate all orgs" do + allow(Chef::Org).to receive(:load).with("foobar").and_return(org) + expect(rest).to receive(:get_rest).with("organizations").and_return(response) + expect(Chef::Org.list(true)).to eq(inflated_response) + end + end + + describe "create" do + it "creates a new org via the API" do + expect(rest).to receive(:post_rest).with("organizations", {:name => "foobar", :full_name => "foo bar bat"}).and_return({}) + org.create + end + end + + describe "read" do + it "loads a named org from the API" do + expect(rest).to receive(:get_rest).with("organizations/foobar").and_return({"name" => "foobar", "full_name" => "foo bar bat", "private_key" => "private"}) + org = Chef::Org.load("foobar") + expect(org.name).to eq("foobar") + expect(org.full_name).to eq("foo bar bat") + expect(org.private_key).to eq("private") + end + end + + describe "update" do + it "updates an existing org on via the API" do + expect(rest).to receive(:put_rest).with("organizations/foobar", {:name => "foobar", :full_name => "foo bar bat"}).and_return({}) + org.update + end + end + + describe "destroy" do + it "deletes the specified org via the API" do + expect(rest).to receive(:delete_rest).with("organizations/foobar") + org.destroy + end + end + end +end diff --git a/spec/unit/policy_builder/policyfile_spec.rb b/spec/unit/policy_builder/policyfile_spec.rb index 5e2844201d..92cdd7f57e 100644 --- a/spec/unit/policy_builder/policyfile_spec.rb +++ b/spec/unit/policy_builder/policyfile_spec.rb @@ -211,9 +211,54 @@ describe Chef::PolicyBuilder::Policyfile do end end + context "and policy_document_native_api is configured" do + + before do + Chef::Config[:policy_document_native_api] = true + Chef::Config[:policy_group] = "policy-stage" + Chef::Config[:policy_name] = "example" + end + + context "and policy_name or policy_group are not configured" do + + it "raises a Configuration error for policy_group" do + Chef::Config[:policy_group] = nil + expect { policy_builder.policy }.to raise_error(err_namespace::ConfigurationError) + end + + it "raises a Configuration error for policy_name" do + Chef::Config[:policy_name] = nil + expect { policy_builder.policy }.to raise_error(err_namespace::ConfigurationError) + end + + end + + context "and policy_name and policy_group are configured" do + + let(:policy_relative_url) { "policies/policy-stage/example" } + + before do + expect(http_api).to receive(:get).with(policy_relative_url).and_return(parsed_policyfile_json) + end + + it "fetches the policy file from a data bag item" do + expect(policy_builder.policy).to eq(parsed_policyfile_json) + end + + it "extracts the run_list from the policyfile" do + expect(policy_builder.run_list).to eq(policyfile_run_list) + end + end + + end + + context "and a deployment_group is configured" do + + let(:policy_relative_url) { "data/policyfiles/example-policy-stage" } + before do - expect(http_api).to receive(:get).with("data/policyfiles/example-policy-stage").and_return(parsed_policyfile_json) + expect(http_api).to receive(:get).with(policy_relative_url).and_return(parsed_policyfile_json) end it "fetches the policy file from a data bag item" do diff --git a/spec/unit/provider/env_spec.rb b/spec/unit/provider/env_spec.rb index 19233dfba9..230603dcb3 100644 --- a/spec/unit/provider/env_spec.rb +++ b/spec/unit/provider/env_spec.rb @@ -252,7 +252,7 @@ describe Chef::Provider::Env do end context "when new_resource's value contains the delimiter" do - it "should return false if all the current values are contained" do + it "should return false if all the current values are contained in specified order" do @new_resource.value("C:/biz;C:/baz") @new_resource.delim(";") @current_resource.value("C:/biz;C:/foo/bin;C:/baz") @@ -265,6 +265,13 @@ describe Chef::Provider::Env do @current_resource.value("C:/biz;C:/foo/bin;C:/baz") expect(@provider.requires_modify_or_create?).to be_truthy end + + it "should return true if values are contained in different order" do + @new_resource.value("C:/biz;C:/baz") + @new_resource.delim(";") + @current_resource.value("C:/baz;C:/foo/bin;C:/biz") + expect(@provider.requires_modify_or_create?).to be_truthy + end end end @@ -286,12 +293,18 @@ describe Chef::Provider::Env do expect(passed_value).to eq(new_value) end - it "should only add values not already contained when a delimiter is provided" do + it "should only add values not already contained" do @new_resource.value("C:/foo;C:/bar;C:/baz") - @new_resource.delim(";") - @current_resource.value("C:/foo/bar;C:/bar;C:/baz") + @current_resource.value("C:/bar;C:/baz;C:/foo/bar") + @provider.modify_env + expect(@new_resource.value).to eq("C:/foo;C:/bar;C:/baz;C:/foo/bar") + end + + it "should reorder values to keep order which asked" do + @new_resource.value("C:/foo;C:/bar;C:/baz") + @current_resource.value("C:/foo/bar;C:/baz;C:/bar") @provider.modify_env - expect(@new_resource.value).to eq("C:/foo;C:/foo/bar;C:/bar;C:/baz") + expect(@new_resource.value).to eq("C:/foo;C:/bar;C:/baz;C:/foo/bar") end end end diff --git a/spec/unit/provider/package/rpm_spec.rb b/spec/unit/provider/package/rpm_spec.rb index 2aceee59a5..07c5dea8c6 100644 --- a/spec/unit/provider/package/rpm_spec.rb +++ b/spec/unit/provider/package/rpm_spec.rb @@ -19,138 +19,148 @@ require 'spec_helper' describe Chef::Provider::Package::Rpm do - before(:each) do - @node = Chef::Node.new - @events = Chef::EventDispatch::Dispatcher.new - @run_context = Chef::RunContext.new(@node, {}, @events) - - @new_resource = Chef::Resource::Package.new("ImageMagick-c++") - @new_resource.source "/tmp/ImageMagick-c++-6.5.4.7-7.el6_5.x86_64.rpm" - - @provider = Chef::Provider::Package::Rpm.new(@new_resource, @run_context) + let(:provider) { Chef::Provider::Package::Rpm.new(new_resource, run_context) } + let(:node) { Chef::Node.new } + let(:events) { Chef::EventDispatch::Dispatcher.new } + let(:run_context) { Chef::RunContext.new(node, {}, events) } + let(:new_resource) do + Chef::Resource::Package.new("ImageMagick-c++").tap do |resource| + resource.source "/tmp/ImageMagick-c++-6.5.4.7-7.el6_5.x86_64.rpm" + end + end + let(:exitstatus) { 0 } + let(:stdout) { String.new('') } + let(:status) { double('Process::Status', exitstatus: exitstatus, stdout: stdout) } - @status = double("Status", :exitstatus => 0) + before(:each) do allow(::File).to receive(:exists?).and_return(true) + allow(provider).to receive(:shell_out!).and_return(status) end describe "when determining the current state of the package" do - it "should create a current resource with the name of new_resource" do - allow(@provider).to receive(:popen4).and_return(@status) - @provider.load_current_resource - expect(@provider.current_resource.name).to eq("ImageMagick-c++") + provider.load_current_resource + expect(provider.current_resource.name).to eq("ImageMagick-c++") end it "should set the current reource package name to the new resource package name" do - allow(@provider).to receive(:popen4).and_return(@status) - @provider.load_current_resource - expect(@provider.current_resource.package_name).to eq('ImageMagick-c++') + provider.load_current_resource + expect(provider.current_resource.package_name).to eq('ImageMagick-c++') end it "should raise an exception if a source is supplied but not found" do allow(::File).to receive(:exists?).and_return(false) - expect { @provider.run_action(:any) }.to raise_error(Chef::Exceptions::Package) + expect { provider.run_action(:any) }.to raise_error(Chef::Exceptions::Package) end - it "should get the source package version from rpm if provided" do - @stdout = StringIO.new("ImageMagick-c++ 6.5.4.7-7.el6_5") - expect(@provider).to receive(:popen4).with("rpm -qp --queryformat '%{NAME} %{VERSION}-%{RELEASE}\n' /tmp/ImageMagick-c++-6.5.4.7-7.el6_5.x86_64.rpm").and_yield(@pid, @stdin, @stdout, @stderr).and_return(@status) - expect(@provider).to receive(:popen4).with("rpm -q --queryformat '%{NAME} %{VERSION}-%{RELEASE}\n' ImageMagick-c++").and_return(@status) - @provider.load_current_resource - expect(@provider.current_resource.package_name).to eq("ImageMagick-c++") - expect(@provider.new_resource.version).to eq("6.5.4.7-7.el6_5") - end + context "installation exists" do + let(:stdout) { "ImageMagick-c++ 6.5.4.7-7.el6_5" } - it "should return the current version installed if found by rpm" do - @stdout = StringIO.new("ImageMagick-c++ 6.5.4.7-7.el6_5") - expect(@provider).to receive(:popen4).with("rpm -qp --queryformat '%{NAME} %{VERSION}-%{RELEASE}\n' /tmp/ImageMagick-c++-6.5.4.7-7.el6_5.x86_64.rpm").and_return(@status) - expect(@provider).to receive(:popen4).with("rpm -q --queryformat '%{NAME} %{VERSION}-%{RELEASE}\n' ImageMagick-c++").and_yield(@pid, @stdin, @stdout, @stderr).and_return(@status) - @provider.load_current_resource - expect(@provider.current_resource.version).to eq("6.5.4.7-7.el6_5") - end + it "should get the source package version from rpm if provided" do + expect(provider).to receive(:shell_out!).with("rpm -qp --queryformat '%{NAME} %{VERSION}-%{RELEASE}\n' /tmp/ImageMagick-c++-6.5.4.7-7.el6_5.x86_64.rpm").and_return(status) + expect(provider).to receive(:shell_out!).with("rpm -q --queryformat '%{NAME} %{VERSION}-%{RELEASE}\n' ImageMagick-c++").and_return(status) + provider.load_current_resource + expect(provider.current_resource.package_name).to eq("ImageMagick-c++") + expect(provider.new_resource.version).to eq("6.5.4.7-7.el6_5") + end - it "should raise an exception if the source is not set but we are installing" do - new_resource = Chef::Resource::Package.new("ImageMagick-c++") - provider = Chef::Provider::Package::Rpm.new(new_resource, @run_context) - expect { provider.run_action(:any) }.to raise_error(Chef::Exceptions::Package) + it "should return the current version installed if found by rpm" do + expect(provider).to receive(:shell_out!).with("rpm -qp --queryformat '%{NAME} %{VERSION}-%{RELEASE}\n' /tmp/ImageMagick-c++-6.5.4.7-7.el6_5.x86_64.rpm").and_return(status) + expect(provider).to receive(:shell_out!).with("rpm -q --queryformat '%{NAME} %{VERSION}-%{RELEASE}\n' ImageMagick-c++").and_return(status) + provider.load_current_resource + expect(provider.current_resource.version).to eq("6.5.4.7-7.el6_5") + end end - it "should raise an exception if rpm fails to run" do - status = double("Status", :exitstatus => -1) - allow(@provider).to receive(:popen4).and_return(status) - expect { @provider.run_action(:any) }.to raise_error(Chef::Exceptions::Package) + context "source is not defiend" do + let(:new_resource) { Chef::Resource::Package.new("ImageMagick-c++") } + + it "should raise an exception if the source is not set but we are installing" do + expect { provider.run_action(:any) }.to raise_error(Chef::Exceptions::Package) + end end - it "should not detect the package name as version when not installed" do - @status = double("Status", :exitstatus => -1) - @stdout = StringIO.new("package openssh-askpass is not installed") - @new_resource = Chef::Resource::Package.new("openssh-askpass") - @new_resource.source 'openssh-askpass' - @provider = Chef::Provider::Package::Rpm.new(@new_resource, @run_context) - expect(@provider).to receive(:popen4).with("rpm -qp --queryformat '%{NAME} %{VERSION}-%{RELEASE}\n' openssh-askpass").and_yield(@pid, @stdin, @stdout, @stderr).and_return(@status) - expect(@provider).to receive(:popen4).with("rpm -q --queryformat '%{NAME} %{VERSION}-%{RELEASE}\n' openssh-askpass").and_return(@status) - @provider.load_current_resource - expect(@provider.current_resource.version).to be_nil + context "installation does not exist" do + let(:stdout) { String.new("package openssh-askpass is not installed") } + let(:exitstatus) { -1 } + let(:new_resource) do + Chef::Resource::Package.new("openssh-askpass").tap do |resource| + resource.source "openssh-askpass" + end + end + + it "should raise an exception if rpm fails to run" do + allow(provider).to receive(:shell_out!).and_return(status) + expect { provider.run_action(:any) }.to raise_error(Chef::Exceptions::Package) + end + + it "should not detect the package name as version when not installed" do + expect(provider).to receive(:shell_out!).with("rpm -qp --queryformat '%{NAME} %{VERSION}-%{RELEASE}\n' openssh-askpass").and_return(status) + expect(provider).to receive(:shell_out!).with("rpm -q --queryformat '%{NAME} %{VERSION}-%{RELEASE}\n' openssh-askpass").and_return(status) + provider.load_current_resource + expect(provider.current_resource.version).to be_nil + end end end describe "after the current resource is loaded" do - before do - @current_resource = Chef::Resource::Package.new("ImageMagick-c++") - @provider.current_resource = @current_resource + let(:current_resource) { Chef::Resource::Package.new("ImageMagick-c++") } + let(:provider) do + Chef::Provider::Package::Rpm.new(new_resource, run_context).tap do |provider| + provider.current_resource = current_resource + end end describe "when installing or upgrading" do it "should run rpm -i with the package source to install" do - expect(@provider).to receive(:shell_out!).with("rpm -i /tmp/ImageMagick-c++-6.5.4.7-7.el6_5.x86_64.rpm") - @provider.install_package("ImageMagick-c++", "6.5.4.7-7.el6_5") + expect(provider).to receive(:shell_out!).with("rpm -i /tmp/ImageMagick-c++-6.5.4.7-7.el6_5.x86_64.rpm") + provider.install_package("ImageMagick-c++", "6.5.4.7-7.el6_5") end it "should run rpm -U with the package source to upgrade" do - @current_resource.version("21.4-19.el5") - expect(@provider).to receive(:shell_out!).with("rpm -U /tmp/ImageMagick-c++-6.5.4.7-7.el6_5.x86_64.rpm") - @provider.upgrade_package("ImageMagick-c++", "6.5.4.7-7.el6_5") + current_resource.version("21.4-19.el5") + expect(provider).to receive(:shell_out!).with("rpm -U /tmp/ImageMagick-c++-6.5.4.7-7.el6_5.x86_64.rpm") + provider.upgrade_package("ImageMagick-c++", "6.5.4.7-7.el6_5") end it "should install package if missing and set to upgrade" do - @current_resource.version("ImageMagick-c++") - expect(@provider).to receive(:shell_out!).with("rpm -U /tmp/ImageMagick-c++-6.5.4.7-7.el6_5.x86_64.rpm") - @provider.upgrade_package("ImageMagick-c++", "6.5.4.7-7.el6_5") - end - - it "should install from a path when the package is a path and the source is nil" do - @new_resource = Chef::Resource::Package.new("/tmp/ImageMagick-c++-6.5.4.7-7.el6_5.x86_64.rpm") - @provider = Chef::Provider::Package::Rpm.new(@new_resource, @run_context) - expect(@new_resource.source).to eq("/tmp/ImageMagick-c++-6.5.4.7-7.el6_5.x86_64.rpm") - @current_resource = Chef::Resource::Package.new("ImageMagick-c++") - @provider.current_resource = @current_resource - expect(@provider).to receive(:shell_out!).with("rpm -i /tmp/ImageMagick-c++-6.5.4.7-7.el6_5.x86_64.rpm") - @provider.install_package("/tmp/ImageMagick-c++-6.5.4.7-7.el6_5.x86_64.rpm", "6.5.4.7-7.el6_5") + current_resource.version("ImageMagick-c++") + expect(provider).to receive(:shell_out!).with("rpm -U /tmp/ImageMagick-c++-6.5.4.7-7.el6_5.x86_64.rpm") + provider.upgrade_package("ImageMagick-c++", "6.5.4.7-7.el6_5") end - it "should uprgrade from a path when the package is a path and the source is nil" do - @new_resource = Chef::Resource::Package.new("/tmp/ImageMagick-c++-6.5.4.7-7.el6_5.x86_64.rpm") - @provider = Chef::Provider::Package::Rpm.new(@new_resource, @run_context) - expect(@new_resource.source).to eq("/tmp/ImageMagick-c++-6.5.4.7-7.el6_5.x86_64.rpm") - @current_resource = Chef::Resource::Package.new("ImageMagick-c++") - @current_resource.version("21.4-19.el5") - @provider.current_resource = @current_resource - expect(@provider).to receive(:shell_out!).with("rpm -U /tmp/ImageMagick-c++-6.5.4.7-7.el6_5.x86_64.rpm") - @provider.upgrade_package("/tmp/ImageMagick-c++-6.5.4.7-7.el6_5.x86_64.rpm", "6.5.4.7-7.el6_5") + context "installing when the name is a path" do + let(:new_resource) { Chef::Resource::Package.new("/tmp/ImageMagick-c++-6.5.4.7-7.el6_5.x86_64.rpm") } + let(:current_resource) { Chef::Resource::Package.new("ImageMagick-c++") } + + it "should install from a path when the package is a path and the source is nil" do + expect(new_resource.source).to eq("/tmp/ImageMagick-c++-6.5.4.7-7.el6_5.x86_64.rpm") + provider.current_resource = current_resource + expect(provider).to receive(:shell_out!).with("rpm -i /tmp/ImageMagick-c++-6.5.4.7-7.el6_5.x86_64.rpm") + provider.install_package("/tmp/ImageMagick-c++-6.5.4.7-7.el6_5.x86_64.rpm", "6.5.4.7-7.el6_5") + end + + it "should uprgrade from a path when the package is a path and the source is nil" do + expect(new_resource.source).to eq("/tmp/ImageMagick-c++-6.5.4.7-7.el6_5.x86_64.rpm") + current_resource.version("21.4-19.el5") + provider.current_resource = current_resource + expect(provider).to receive(:shell_out!).with("rpm -U /tmp/ImageMagick-c++-6.5.4.7-7.el6_5.x86_64.rpm") + provider.upgrade_package("/tmp/ImageMagick-c++-6.5.4.7-7.el6_5.x86_64.rpm", "6.5.4.7-7.el6_5") + end end it "installs with custom options specified in the resource" do - @provider.candidate_version = '11' - @new_resource.options("--dbpath /var/lib/rpm") - expect(@provider).to receive(:shell_out!).with("rpm --dbpath /var/lib/rpm -i /tmp/ImageMagick-c++-6.5.4.7-7.el6_5.x86_64.rpm") - @provider.install_package(@new_resource.name, @provider.candidate_version) + provider.candidate_version = '11' + new_resource.options("--dbpath /var/lib/rpm") + expect(provider).to receive(:shell_out!).with("rpm --dbpath /var/lib/rpm -i /tmp/ImageMagick-c++-6.5.4.7-7.el6_5.x86_64.rpm") + provider.install_package(new_resource.name, provider.candidate_version) end end describe "when removing the package" do it "should run rpm -e to remove the package" do - expect(@provider).to receive(:shell_out!).with("rpm -e ImageMagick-c++-6.5.4.7-7.el6_5") - @provider.remove_package("ImageMagick-c++", "6.5.4.7-7.el6_5") + expect(provider).to receive(:shell_out!).with("rpm -e ImageMagick-c++-6.5.4.7-7.el6_5") + provider.remove_package("ImageMagick-c++", "6.5.4.7-7.el6_5") end end end diff --git a/spec/unit/provider/service/openbsd_service_spec.rb b/spec/unit/provider/service/openbsd_service_spec.rb new file mode 100644 index 0000000000..1b5206470e --- /dev/null +++ b/spec/unit/provider/service/openbsd_service_spec.rb @@ -0,0 +1,543 @@ +# +# Author:: Bryan McLellan (btm@loftninjas.org) +# Author:: Scott Bonds (scott@ggr.com) +# Copyright:: Copyright (c) 2009 Bryan McLellan +# Copyright:: Copyright (c) 2014 Scott Bonds +# 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' + +class Chef::Provider::Service::Openbsd + public :builtin_service_enable_variable_name + public :determine_enabled_status! + public :determine_current_status! + public :is_enabled? + attr_accessor :rc_conf, :rc_conf_local +end + +describe Chef::Provider::Service::Openbsd do + let(:node) do + node = Chef::Node.new + node.automatic_attrs[:command] = {:ps => "ps -ax"} + node + end + + let(:new_resource) do + new_resource = Chef::Resource::Service.new("sndiod") + new_resource.pattern("sndiod") + new_resource.supports({:status => false}) + new_resource + end + + let(:current_resource) do + current_resource = Chef::Resource::Service.new("sndiod") + current_resource + end + + let(:provider) do + events = Chef::EventDispatch::Dispatcher.new + run_context = Chef::RunContext.new(node, {}, events) + allow(::File).to receive(:read).with('/etc/rc.conf').and_return('') + allow(::File).to receive(:read).with('/etc/rc.conf.local').and_return('') + provider = Chef::Provider::Service::Openbsd.new(new_resource,run_context) + provider.action = :start + provider + end + + before do + allow(Chef::Resource::Service).to receive(:new).and_return(current_resource) + end + + def stub_etc_rcd_script + allow(::File).to receive(:exist?).and_return(false) + expect(::File).to receive(:exist?).with("/etc/rc.d/#{new_resource.service_name}").and_return(true) + end + + def run_load_current_resource + stub_etc_rcd_script + provider.load_current_resource + end + + describe Chef::Provider::Service::Openbsd, "initialize" do + it "should find /etc/rc.d init scripts" do + stub_etc_rcd_script + expect(provider.init_command).to eql "/etc/rc.d/sndiod" + end + + it "should set init_command to nil if it can't find anything" do + expect(::File).to receive(:exist?).with('/etc/rc.d/sndiod').and_return(false) + expect(provider.init_command).to be nil + end + end + + describe Chef::Provider::Service::Openbsd, "determine_current_status!" do + before do + stub_etc_rcd_script + provider.current_resource = current_resource + current_resource.service_name(new_resource.service_name) + end + + context "when a status command has been specified" do + let(:status) { double(:stdout => "", :exitstatus => 0) } + + before do + new_resource.status_command("/bin/chefhasmonkeypants status") + end + + it "should run the services status command if one has been specified" do + expect(provider).to receive(:shell_out).with("/bin/chefhasmonkeypants status").and_return(status) + provider.determine_current_status! + end + end + + context "when the service supports status" do + let(:status) { double(:stdout => "", :exitstatus => 0) } + + before do + new_resource.supports({:status => true}) + end + + it "should run '/etc/rc.d/service_name status'" do + expect(provider).to receive(:shell_out).with("/etc/rc.d/#{new_resource.service_name} check").and_return(status) + provider.determine_current_status! + end + + it "should set running to true if the status command returns 0" do + expect(provider).to receive(:shell_out).with("/etc/rc.d/#{new_resource.service_name} check").and_return(status) + provider.determine_current_status! + expect(current_resource.running).to be true + end + + it "should set running to false if the status command returns anything except 0" do + expect(provider).to receive(:shell_out).with("/etc/rc.d/#{new_resource.service_name} check").and_raise(Mixlib::ShellOut::ShellCommandFailed) + provider.determine_current_status! + expect(current_resource.running).to be false + end + end + end + + describe Chef::Provider::Service::Openbsd, "determine_enabled_status!" do + before do + stub_etc_rcd_script + provider.current_resource = current_resource + current_resource.service_name(new_resource.service_name) + + allow(provider).to receive(:service_enable_variable_name).and_return("#{new_resource.service_name}_enable") + end + + context "when the service is builtin" do + before do + expect(::File).to receive(:open).with("/etc/rc.d/#{new_resource.service_name}") + provider.rc_conf = "#{provider.builtin_service_enable_variable_name}=NO" + provider.rc_conf_local = lines.join("\n") + end + + %w{YES Yes yes yEs YeS}.each do |setting| + context "when the enable variable is set to #{setting}" do + let(:lines) { [ %Q{#{provider.builtin_service_enable_variable_name}="#{setting}"} ] } + it "sets enabled to true" do + provider.determine_enabled_status! + expect(current_resource.enabled).to be true + end + end + end + + %w{No NO no nO None NONE none nOnE}.each do |setting| + context "when the enable variable is set to #{setting}" do + let(:lines) { [ %Q{#{provider.builtin_service_enable_variable_name}="#{setting}"} ] } + it "sets enabled to false" do + provider.determine_enabled_status! + expect(current_resource.enabled).to be false + end + end + end + + context "when the enable variable is garbage" do + let(:lines) { [ %Q{#{provider.builtin_service_enable_variable_name}_enable="alskdjflasdkjflakdfj"} ] } + it "sets enabled to false" do + provider.determine_enabled_status! + expect(current_resource.enabled).to be false + end + end + + context "when the enable variable partial matches (left) some other service and we are disabled" do + let(:lines) { [ + %Q{thing_#{provider.builtin_service_enable_variable_name}="YES"}, + %Q{#{provider.builtin_service_enable_variable_name}="NO"}, + ] } + it "sets enabled based on the exact match (false)" do + provider.determine_enabled_status! + expect(current_resource.enabled).to be false + end + end + + context "when the enable variable partial matches (right) some other service and we are disabled" do + let(:lines) { [ + %Q{#{provider.builtin_service_enable_variable_name}_thing="YES"}, + %Q{#{provider.builtin_service_enable_variable_name}}, + ] } + it "sets enabled based on the exact match (false)" do + provider.determine_enabled_status! + expect(current_resource.enabled).to be false + end + end + + context "when the enable variable partial matches (left) some other disabled service and we are enabled" do + let(:lines) { [ + %Q{thing_#{provider.builtin_service_enable_variable_name}="NO"}, + %Q{#{provider.builtin_service_enable_variable_name}="YES"}, + ] } + it "sets enabled based on the exact match (true)" do + provider.determine_enabled_status! + expect(current_resource.enabled).to be true + end + end + + context "when the enable variable partial matches (right) some other disabled service and we are enabled" do + let(:lines) { [ + %Q{#{provider.builtin_service_enable_variable_name}_thing="NO"}, + %Q{#{provider.builtin_service_enable_variable_name}="YES"}, + ] } + it "sets enabled based on the exact match (true)" do + provider.determine_enabled_status! + expect(current_resource.enabled).to be true + end + end + + context "when the enable variable only partial matches (left) some other enabled service" do + let(:lines) { [ %Q{thing_#{provider.builtin_service_enable_variable_name}_enable="YES"} ] } + it "sets enabled to false" do + provider.determine_enabled_status! + expect(current_resource.enabled).to be false + end + end + + context "when the enable variable only partial matches (right) some other enabled service" do + let(:lines) { [ %Q{#{provider.builtin_service_enable_variable_name}_thing_enable="YES"} ] } + it "sets enabled to false" do + provider.determine_enabled_status! + expect(current_resource.enabled).to be false + end + end + + context "when nothing matches" do + let(:lines) { [] } + it "sets enabled to true" do + provider.determine_enabled_status! + expect(current_resource.enabled).to be false + end + end + end + end + + describe Chef::Provider::Service::Openbsd, "load_current_resource" do + before(:each) do + stub_etc_rcd_script + expect(provider).to receive(:determine_current_status!) + current_resource.running(false) + allow(provider).to receive(:service_enable_variable_name).and_return "#{new_resource.service_name}_enable" + expect(::File).to receive(:open).with("/etc/rc.d/#{new_resource.service_name}") + end + + it "should create a current resource with the name of the new resource" do + expect(Chef::Resource::Service).to receive(:new).and_return(current_resource) + provider.load_current_resource + end + + it "should set the current resources service name to the new resources service name" do + provider.load_current_resource + expect(current_resource.service_name).to eq(new_resource.service_name) + end + + it "should return the current resource" do + expect(provider.load_current_resource).to eql(current_resource) + end + + end + + context "when testing actions" do + before(:each) do + stub_etc_rcd_script + expect(provider).to receive(:determine_current_status!) + current_resource.running(false) + expect(provider).to receive(:determine_enabled_status!) + current_resource.enabled(false) + provider.load_current_resource + end + + describe Chef::Provider::Service::Openbsd, "start_service" do + it "should call the start command if one is specified" do + new_resource.start_command("/etc/rc.d/chef startyousillysally") + expect(provider).to receive(:shell_out_with_systems_locale!).with("/etc/rc.d/chef startyousillysally") + provider.start_service() + end + + it "should call '/usr/local/etc/rc.d/service_name start' if no start command is specified" do + expect(provider).to receive(:shell_out_with_systems_locale!).with("/etc/rc.d/#{new_resource.service_name} start") + provider.start_service() + end + end + + describe Chef::Provider::Service::Openbsd, "stop_service" do + it "should call the stop command if one is specified" do + new_resource.stop_command("/etc/init.d/chef itoldyoutostop") + expect(provider).to receive(:shell_out_with_systems_locale!).with("/etc/init.d/chef itoldyoutostop") + provider.stop_service() + end + + it "should call '/usr/local/etc/rc.d/service_name stop' if no stop command is specified" do + expect(provider).to receive(:shell_out_with_systems_locale!).with("/etc/rc.d/#{new_resource.service_name} stop") + provider.stop_service() + end + end + + describe Chef::Provider::Service::Openbsd, "restart_service" do + it "should call 'restart' on the service_name if the resource supports it" do + new_resource.supports({:restart => true}) + expect(provider).to receive(:shell_out_with_systems_locale!).with("/etc/rc.d/#{new_resource.service_name} restart") + provider.restart_service() + end + + it "should call the restart_command if one has been specified" do + new_resource.restart_command("/etc/init.d/chef restartinafire") + expect(provider).to receive(:shell_out_with_systems_locale!).with("/etc/init.d/chef restartinafire") + provider.restart_service() + end + + it "otherwise it should call stop and start" do + expect(provider).to receive(:stop_service) + expect(provider).to receive(:start_service) + provider.restart_service() + end + end + end + + describe Chef::Provider::Service::Openbsd, "define_resource_requirements" do + before do + provider.current_resource = current_resource + end + + context "when the init script is not found" do + before do + provider.init_command = nil + allow(provider).to receive(:builtin_service_enable_variable_name).and_return("#{new_resource.service_name}_enable") + end + + [ "start", "reload", "restart", "enable" ].each do |action| + it "should raise an exception when the action is #{action}" do + provider.define_resource_requirements + provider.action = action + expect { provider.process_resource_requirements }.to raise_error(Chef::Exceptions::Service) + end + end + + [ "stop", "disable" ].each do |action| + it "should not raise an error when the action is #{action}" do + provider.define_resource_requirements + provider.action = action + expect { provider.process_resource_requirements }.not_to raise_error + end + end + end + + context "when the init script is found, but the service_enable_variable_name is nil" do + before do + allow(provider).to receive(:builtin_service_enable_variable_name).and_return(nil) + end + + [ "start", "reload", "restart", "enable" ].each do |action| + it "should raise an exception when the action is #{action}" do + provider.action = action + provider.define_resource_requirements + expect { provider.process_resource_requirements }.to raise_error(Chef::Exceptions::Service) + end + end + + [ "stop", "disable" ].each do |action| + it "should not raise an error when the action is #{action}" do + provider.action = action + provider.define_resource_requirements + expect { provider.process_resource_requirements }.not_to raise_error + end + end + end + end + + describe Chef::Provider::Service::Openbsd, "enable_service" do + before do + provider.current_resource = current_resource + allow(FileUtils).to receive(:touch).with('/etc/rc.conf.local') + end + context "is builtin and disabled by default" do + before do + provider.rc_conf = "#{provider.builtin_service_enable_variable_name}=NO" + end + context "is enabled" do + before do + provider.rc_conf_local = "#{provider.builtin_service_enable_variable_name}=\"\"" + end + it "should not change rc.conf.local since it is already enabled" do + expect(::File).not_to receive(:write) + provider.enable_service + end + end + context "is disabled" do + before do + provider.rc_conf_local = '' + end + it "should enable the service by adding a line to rc.conf.local" do + expect(::File).to receive(:write).with('/etc/rc.conf.local', include("#{provider.builtin_service_enable_variable_name}=\"\"")) + expect(provider.is_enabled?).to be false + provider.enable_service + expect(provider.is_enabled?).to be true + end + end + end + context "is builtin and enabled by default" do + before do + provider.rc_conf = "#{provider.builtin_service_enable_variable_name}=\"\"" + end + context "is enabled" do + before do + provider.rc_conf_local = '' + end + it "should not change rc.conf.local since it is already enabled" do + expect(::File).not_to receive(:write) + provider.enable_service + end + end + context "is disabled" do + before do + provider.rc_conf_local = "#{provider.builtin_service_enable_variable_name}=NO" + end + it "should enable the service by removing a line from rc.conf.local" do + expect(::File).to receive(:write).with('/etc/rc.conf.local', /^(?!#{provider.builtin_service_enable_variable_name})$/) + expect(provider.is_enabled?).to be false + provider.enable_service + expect(provider.is_enabled?).to be true + end + end + end + context "is not builtin" do + before do + provider.rc_conf = '' + end + context "is enabled" do + before do + provider.rc_conf_local = "pkg_scripts=\"#{new_resource.service_name}\"\n" + end + it "should not change rc.conf.local since it is already enabled" do + expect(::File).not_to receive(:write) + provider.enable_service + end + end + context "is disabled" do + before do + provider.rc_conf_local = '' + end + it "should enable the service by adding it to the pkg_scripts list" do + expect(::File).to receive(:write).with('/etc/rc.conf.local', "\npkg_scripts=\"#{new_resource.service_name}\"\n") + expect(provider.is_enabled?).to be false + provider.enable_service + expect(provider.is_enabled?).to be true + end + end + end + end + + describe Chef::Provider::Service::Openbsd, "disable_service" do + before do + provider.current_resource = current_resource + allow(FileUtils).to receive(:touch).with('/etc/rc.conf.local') + end + context "is builtin and disabled by default" do + before do + provider.rc_conf = "#{provider.builtin_service_enable_variable_name}=NO" + end + context "is enabled" do + before do + provider.rc_conf_local = "#{provider.builtin_service_enable_variable_name}=\"\"" + end + it "should disable the service by removing its line from rc.conf.local" do + expect(::File).to receive(:write).with('/etc/rc.conf.local', /^(?!#{provider.builtin_service_enable_variable_name})$/) + expect(provider.is_enabled?).to be true + provider.disable_service + expect(provider.is_enabled?).to be false + end + end + context "is disabled" do + before do + provider.rc_conf_local = '' + end + it "should not change rc.conf.local since it is already disabled" do + expect(::File).not_to receive(:write) + provider.disable_service + end + end + end + context "is builtin and enabled by default" do + before do + provider.rc_conf = "#{provider.builtin_service_enable_variable_name}=\"\"" + end + context "is enabled" do + before do + provider.rc_conf_local = '' + end + it "should disable the service by adding a line to rc.conf.local" do + expect(::File).to receive(:write).with('/etc/rc.conf.local', include("#{provider.builtin_service_enable_variable_name}=\"NO\"")) + expect(provider.is_enabled?).to be true + provider.disable_service + expect(provider.is_enabled?).to be false + end + end + context "is disabled" do + before do + provider.rc_conf_local = "#{provider.builtin_service_enable_variable_name}=NO" + end + it "should not change rc.conf.local since it is already disabled" do + expect(::File).not_to receive(:write) + provider.disable_service + end + end + end + context "is not builtin" do + before do + provider.rc_conf = '' + end + context "is enabled" do + before do + provider.rc_conf_local = "pkg_scripts=\"#{new_resource.service_name}\"\n" + end + it "should disable the service by removing it from the pkg_scripts list" do + expect(::File).to receive(:write).with('/etc/rc.conf.local', /^(?!#{new_resource.service_name})$/) + expect(provider.is_enabled?).to be true + provider.disable_service + expect(provider.is_enabled?).to be false + end + end + context "is disabled" do + before do + provider.rc_conf_local = '' + end + it "should not change rc.conf.local since it is already disabled" do + expect(::File).not_to receive(:write) + provider.disable_service + end + end + end + end + +end diff --git a/spec/unit/provider/user_spec.rb b/spec/unit/provider/user_spec.rb index 44434794e7..381168647b 100644 --- a/spec/unit/provider/user_spec.rb +++ b/spec/unit/provider/user_spec.rb @@ -91,7 +91,7 @@ describe Chef::Provider::User do expect(@current_resource.username).to eq(@new_resource.username) end - it "should change the encoding of gecos to the encoding of the new resource", :ruby_gte_19_only do + it "should change the encoding of gecos to the encoding of the new resource" do @pw_user.gecos.force_encoding('ASCII-8BIT') @provider.load_current_resource expect(@provider.current_resource.comment.encoding).to eq(@new_resource.comment.encoding) diff --git a/spec/unit/resource/conditional_spec.rb b/spec/unit/resource/conditional_spec.rb index 779c69425a..49240edfdf 100644 --- a/spec/unit/resource/conditional_spec.rb +++ b/spec/unit/resource/conditional_spec.rb @@ -47,7 +47,7 @@ describe Chef::Resource::Conditional do end describe "when created as an `only_if`" do - describe "after running a successful command" do + describe "after running a successful command given as a string" do before do @conditional = Chef::Resource::Conditional.only_if(@parent_resource, "true") end @@ -57,7 +57,7 @@ describe Chef::Resource::Conditional do end end - describe "after running a negative/false command" do + describe "after running a negative/false command given as a string" do before do @status.send("success?=", false) @conditional = Chef::Resource::Conditional.only_if(@parent_resource, "false") @@ -68,6 +68,27 @@ describe Chef::Resource::Conditional do end end + describe "after running a successful command given as an array" do + before do + @conditional = Chef::Resource::Conditional.only_if(@parent_resource, ["true"]) + end + + it "indicates that resource convergence should continue" do + expect(@conditional.continue?).to be true + end + end + + describe "after running a negative/false command given as an array" do + before do + @status.send("success?=", false) + @conditional = Chef::Resource::Conditional.only_if(@parent_resource, ["false"]) + end + + it "indicates that resource convergence should not continue" do + expect(@conditional.continue?).to be false + end + end + describe 'after running a command which timed out' do before do @conditional = Chef::Resource::Conditional.only_if(@parent_resource, "false") @@ -106,7 +127,7 @@ describe Chef::Resource::Conditional do end describe "when created as a `not_if`" do - describe "after running a successful/true command" do + describe "after running a successful/true command given as a string" do before do @conditional = Chef::Resource::Conditional.not_if(@parent_resource, "true") end @@ -116,7 +137,7 @@ describe Chef::Resource::Conditional do end end - describe "after running a failed/false command" do + describe "after running a failed/false command given as a string" do before do @status.send("success?=", false) @conditional = Chef::Resource::Conditional.not_if(@parent_resource, "false") @@ -127,6 +148,27 @@ describe Chef::Resource::Conditional do end end + describe "after running a successful/true command given as an array" do + before do + @conditional = Chef::Resource::Conditional.not_if(@parent_resource, ["true"]) + end + + it "indicates that resource convergence should not continue" do + expect(@conditional.continue?).to be false + end + end + + describe "after running a failed/false command given as an array" do + before do + @status.send("success?=", false) + @conditional = Chef::Resource::Conditional.not_if(@parent_resource, ["false"]) + end + + it "indicates that resource convergence should continue" do + expect(@conditional.continue?).to be true + end + end + describe 'after running a command which timed out' do before do @conditional = Chef::Resource::Conditional.not_if(@parent_resource, "false") diff --git a/spec/unit/resource/execute_spec.rb b/spec/unit/resource/execute_spec.rb index 70b9d87d4c..09160ddbd0 100644 --- a/spec/unit/resource/execute_spec.rb +++ b/spec/unit/resource/execute_spec.rb @@ -28,4 +28,8 @@ describe Chef::Resource::Execute do expect(execute_resource.guard_interpreter).to be(:execute) end + it "defaults to not being a guard interpreter" do + expect(execute_resource.is_guard_interpreter).to eq(false) + end + end diff --git a/spec/unit/util/diff_spec.rb b/spec/unit/util/diff_spec.rb index ea226f1c04..b0a57a32c0 100644 --- a/spec/unit/util/diff_spec.rb +++ b/spec/unit/util/diff_spec.rb @@ -105,7 +105,7 @@ shared_examples_for "a diff util" do end end - describe "when the default external encoding is UTF-8", :ruby_gte_19_only do + describe "when the default external encoding is UTF-8" do before do @saved_default_external = Encoding.default_external @@ -170,7 +170,7 @@ shared_examples_for "a diff util" do end - describe "when the default external encoding is Latin-1", :ruby_gte_19_only do + describe "when the default external encoding is Latin-1" do before do @saved_default_external = Encoding.default_external @@ -234,7 +234,7 @@ shared_examples_for "a diff util" do end end - describe "when the default external encoding is Shift_JIS", :ruby_gte_19_only do + describe "when the default external encoding is Shift_JIS" do before do @saved_default_external = Encoding.default_external @@ -411,7 +411,7 @@ shared_examples_for "a diff util" do end end - describe "when the default external encoding is UTF-8", :ruby_gte_19_only do + describe "when the default external encoding is UTF-8" do before do @saved_default_external = Encoding.default_external @@ -456,7 +456,7 @@ shared_examples_for "a diff util" do end - describe "when the default external encoding is Latin-1", :ruby_gte_19_only do + describe "when the default external encoding is Latin-1" do before do @saved_default_external = Encoding.default_external @@ -500,7 +500,7 @@ shared_examples_for "a diff util" do end end - describe "when the default external encoding is Shift-JIS", :ruby_gte_19_only do + describe "when the default external encoding is Shift-JIS" do before do @saved_default_external = Encoding.default_external diff --git a/spec/unit/util/path_helper_spec.rb b/spec/unit/util/path_helper_spec.rb index 4df4b9b1ff..5756c29b90 100644 --- a/spec/unit/util/path_helper_spec.rb +++ b/spec/unit/util/path_helper_spec.rb @@ -189,16 +189,8 @@ describe Chef::Util::PathHelper do end context "not on windows", :unix_only do - context "ruby is at least 1.9", :ruby_gte_19_only do - it "returns a canonical path" do - expect(PathHelper.canonical_path("/etc//apache.d/sites-enabled/../sites-available/default")).to eq("/etc/apache.d/sites-available/default") - end - end - - context "ruby is less than 1.9", :ruby_18_only do - it "returns a canonical path" do - expect { PathHelper.canonical_path("/etc//apache.d/sites-enabled/../sites-available/default") }.to raise_error(NotImplementedError) - end + it "returns a canonical path" do + expect(PathHelper.canonical_path("/etc//apache.d/sites-enabled/../sites-available/default")).to eq("/etc/apache.d/sites-available/default") end end end |