summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore3
-rw-r--r--.kitchen.yml82
-rw-r--r--CHANGELOG.md38
-rw-r--r--DOC_CHANGES.md544
-rw-r--r--README.md10
-rw-r--r--RELEASE_NOTES.md58
-rw-r--r--appveyor.yml32
-rw-r--r--chef.gemspec11
-rw-r--r--kitchen-tests/.chef/client.rb9
-rw-r--r--kitchen-tests/cookbooks/audit_test/.gitignore16
-rw-r--r--kitchen-tests/cookbooks/audit_test/.kitchen.yml16
-rw-r--r--kitchen-tests/cookbooks/audit_test/Berksfile3
-rw-r--r--kitchen-tests/cookbooks/audit_test/README.md12
-rw-r--r--kitchen-tests/cookbooks/audit_test/chefignore95
-rw-r--r--kitchen-tests/cookbooks/audit_test/metadata.rb8
-rw-r--r--kitchen-tests/cookbooks/audit_test/recipes/default.rb26
-rw-r--r--kitchen-tests/cookbooks/audit_test/recipes/error_duplicate_control_groups.rb17
-rw-r--r--kitchen-tests/cookbooks/audit_test/recipes/error_no_block.rb7
-rw-r--r--kitchen-tests/cookbooks/audit_test/recipes/error_orphan_control.rb13
-rw-r--r--kitchen-tests/cookbooks/audit_test/recipes/failed_specs.rb14
-rw-r--r--kitchen-tests/cookbooks/audit_test/recipes/serverspec_collision.rb31
-rw-r--r--kitchen-tests/cookbooks/audit_test/recipes/serverspec_support.rb37
-rw-r--r--kitchen-tests/cookbooks/audit_test/recipes/with_include_recipe.rb16
-rw-r--r--kitchen-tests/cookbooks/webapp/README.md1
-rw-r--r--kitchen-tests/cookbooks/webapp/metadata.rb2
-rw-r--r--lib/chef/application/client.rb41
-rw-r--r--lib/chef/application/solo.rb3
-rw-r--r--lib/chef/application/windows_service_manager.rb10
-rw-r--r--lib/chef/audit/audit_event_proxy.rb93
-rw-r--r--lib/chef/audit/audit_reporter.rb169
-rw-r--r--lib/chef/audit/control_group_data.rb140
-rw-r--r--lib/chef/audit/rspec_formatter.rb37
-rw-r--r--lib/chef/audit/runner.rb178
-rw-r--r--lib/chef/chef_fs/file_system/chef_repository_file_system_entry.rb2
-rw-r--r--lib/chef/client.rb99
-rw-r--r--lib/chef/config.rb8
-rw-r--r--lib/chef/digester.rb1
-rw-r--r--lib/chef/dsl/audit.rb51
-rw-r--r--lib/chef/dsl/recipe.rb3
-rw-r--r--lib/chef/event_dispatch/base.rb35
-rw-r--r--lib/chef/exceptions.rb46
-rw-r--r--lib/chef/formatters/doc.rb48
-rw-r--r--lib/chef/http.rb2
-rw-r--r--lib/chef/knife.rb7
-rw-r--r--lib/chef/knife/cookbook_site_install.rb44
-rw-r--r--lib/chef/knife/search.rb8
-rw-r--r--lib/chef/knife/ssl_fetch.rb13
-rw-r--r--lib/chef/monologger.rb2
-rw-r--r--lib/chef/provider/link.rb2
-rw-r--r--lib/chef/provider/package/apt.rb4
-rw-r--r--lib/chef/provider/package/homebrew.rb3
-rw-r--r--lib/chef/provider/package/windows/msi.rb2
-rw-r--r--lib/chef/provider/powershell_script.rb4
-rw-r--r--lib/chef/provider/script.rb5
-rw-r--r--lib/chef/provider/service.rb2
-rw-r--r--lib/chef/provider/service/windows.rb100
-rw-r--r--lib/chef/provider/subversion.rb6
-rw-r--r--lib/chef/recipe.rb2
-rw-r--r--lib/chef/resource.rb114
-rw-r--r--lib/chef/resource/homebrew_package.rb3
-rw-r--r--lib/chef/resource/resource_notification.rb109
-rw-r--r--lib/chef/resource/windows_service.rb18
-rw-r--r--lib/chef/resource_collection/resource_set.rb16
-rw-r--r--lib/chef/run_context.rb12
-rw-r--r--lib/chef/search/query.rb153
-rw-r--r--lib/chef/util/dsc/lcm_output_parser.rb10
-rw-r--r--lib/chef/util/dsc/local_configuration_manager.rb2
-rw-r--r--lib/chef/whitelist.rb4
-rw-r--r--lib/chef/win32/api/file.rb20
-rw-r--r--spec/functional/audit/rspec_formatter_spec.rb53
-rw-r--r--spec/functional/audit/runner_spec.rb136
-rw-r--r--spec/functional/knife/ssh_spec.rb2
-rw-r--r--spec/functional/notifications_spec.rb169
-rw-r--r--spec/functional/resource/deploy_revision_spec.rb3
-rw-r--r--spec/functional/resource/git_spec.rb2
-rw-r--r--spec/functional/resource/link_spec.rb32
-rw-r--r--spec/functional/resource/registry_spec.rb2
-rw-r--r--spec/functional/resource/windows_service_spec.rb98
-rw-r--r--spec/functional/win32/service_manager_spec.rb60
-rw-r--r--spec/integration/client/client_spec.rb40
-rw-r--r--spec/spec_helper.rb1
-rw-r--r--spec/support/audit_helper.rb65
-rw-r--r--spec/support/chef_helpers.rb6
-rw-r--r--spec/support/platform_helpers.rb7
-rw-r--r--spec/support/shared/functional/win32_service.rb60
-rw-r--r--spec/unit/application/client_spec.rb105
-rw-r--r--spec/unit/application/solo_spec.rb67
-rw-r--r--spec/unit/audit/audit_event_proxy_spec.rb311
-rw-r--r--spec/unit/audit/audit_reporter_spec.rb393
-rw-r--r--spec/unit/audit/control_group_data_spec.rb478
-rw-r--r--spec/unit/audit/rspec_formatter_spec.rb29
-rw-r--r--spec/unit/audit/runner_spec.rb133
-rw-r--r--spec/unit/client_spec.rb192
-rw-r--r--spec/unit/dsl/audit_spec.rb43
-rw-r--r--spec/unit/exceptions_spec.rb46
-rw-r--r--spec/unit/http_spec.rb7
-rw-r--r--spec/unit/knife/cookbook_site_install_spec.rb269
-rw-r--r--spec/unit/knife/ssl_fetch_spec.rb53
-rw-r--r--spec/unit/knife_spec.rb158
-rw-r--r--spec/unit/mixin/shell_out_spec.rb79
-rw-r--r--spec/unit/node_spec.rb34
-rw-r--r--spec/unit/provider/execute_spec.rb1
-rw-r--r--spec/unit/provider/link_spec.rb10
-rw-r--r--spec/unit/provider/package/apt_spec.rb521
-rw-r--r--spec/unit/provider/package/windows/msi_spec.rb21
-rw-r--r--spec/unit/provider/service/windows_spec.rb94
-rw-r--r--spec/unit/provider/subversion_spec.rb10
-rw-r--r--spec/unit/provider_resolver_spec.rb4
-rw-r--r--spec/unit/recipe_spec.rb8
-rw-r--r--spec/unit/resource/apt_package_spec.rb8
-rw-r--r--spec/unit/resource/resource_notification_spec.rb170
-rw-r--r--spec/unit/resource_spec.rb151
-rw-r--r--spec/unit/run_context_spec.rb110
-rw-r--r--spec/unit/search/query_spec.rb90
-rw-r--r--spec/unit/util/dsc/lcm_output_parser_spec.rb19
-rw-r--r--spec/unit/util/dsc/local_configuration_manager_spec.rb8
116 files changed, 5373 insertions, 1717 deletions
diff --git a/.gitignore b/.gitignore
index a9e4338e2a..ecba9f4030 100644
--- a/.gitignore
+++ b/.gitignore
@@ -38,3 +38,6 @@ Berksfile.lock
# Vagrant
Vagrantfile
.vagrant/
+
+# Kitchen Tests Local Mode Data
+kitchen-tests/nodes/*
diff --git a/.kitchen.yml b/.kitchen.yml
new file mode 100644
index 0000000000..ed49eb3e57
--- /dev/null
+++ b/.kitchen.yml
@@ -0,0 +1,82 @@
+driver:
+ name: vagrant
+ forward_agent: yes
+ customize:
+ cpus: 4
+ memory: 4096
+ synced_folders:
+ - ['.', '/home/vagrant/chef']
+
+provisioner:
+ name: chef_zero
+ require_chef_omnibus: 12.0.0.rc.1
+
+platforms:
+ - name: centos-5.10
+ run_list:
+ - name: centos-6.5
+ run_list:
+ - name: debian-7.2.0
+ run_list:
+ - name: debian-7.4
+ run_list:
+ - name: debian-6.0.8
+ run_list:
+ - name: freebsd-9.2
+ run_list:
+ - name: freebsd-10.0
+ run_list:
+ - name: ubuntu-10.04
+ run_list:
+ - name: ubuntu-12.04
+ run_list:
+ - name: ubuntu-12.10
+ run_list:
+ - name: ubuntu-13.04
+ run_list:
+ - name: ubuntu-13.10
+ run_list:
+ - name: ubuntu-14.04
+ run_list:
+ # The following boxes are shared via VagrantCloud. Until kitchen-vagrant
+ # is updated you'll need to add the box manually:
+ #
+ # vagrant box add chef/windows-8.1-professional
+ #
+ # Please note this may require a `vagrant login` if the box is private.
+ #
+ # The following boxes are VMware only also. You can enable VMware Fusion
+ # as the default provider by copying `.kitchen.local.yml.vmware.example`
+ # over to `.kitchen.local.yml`.
+ #
+ - name: macosx-10.8
+ driver:
+ box: chef/macosx-10.8 # private
+ - name: macosx-10.9
+ driver:
+ box: chef/macosx-10.9 # private
+ - name: macosx-10.10
+ driver:
+ box: chef/macosx-10.10 # private
+ # - name: windows-7-professional
+ # provisioner:
+ # name: windows_chef_zero
+ # require_chef_omnibus: 11.12.4
+ # driver:
+ # box: chef/windows-7-professional # private
+ # - name: windows-8.1-professional
+ # provisioner:
+ # name: windows_chef_zero
+ # require_chef_omnibus: 11.12.4
+ # driver:
+ # box: chef/windows-8.1-professional # private
+ # - name: windows-2008r2-standard
+ # provisioner:
+ # name: windows_chef_zero
+ # require_chef_omnibus: 11.12.4
+ # driver:
+ # box: chef/windows-server-2008r2-standard # private
+
+suites:
+ - name: chef
+ run_list:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4afea31c5b..0ea4b68264 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,4 +1,4 @@
-## Unreleased
+## 12.1.0 (Unreleased)
* [**Vasiliy Tolstov**](https://github.com/vtolstov):
cleanup cookbook path from stale files (when using chef-solo with a tarball url)
@@ -18,17 +18,43 @@
Typo fixes
* [**Tim Smith**](https://github.com/tas50)
Typo fixes
+* [Pull 2505](https://github.com/opscode/chef/pull/2505) Make Chef handle URIs in a case-insensitive manner
### Chef Contributions
* ruby 1.9.3 support is dropped
* Update Chef to use RSpec 3.
-* Create constant for LWRP before calling `provides`
* Cleaned up script and execute provider + specs
* Added deprecation warnings around the use of command attribute in script resources
-* `path` attribute of `execute` resource is restored to provide backwards compatibility with Chef 11.
-* Fix `Chef::Knife::Core::BootstrapContext` constructor for knife-windows compat.
-* Make sure Client doesn't raise error when interval is set on Windows.
-* Check that `installed` is not empty for `keg_only` formula in Homebrew provider
+* Audit mode feature added - see the RELEASE_NOTES for details
+
+## 12.0.3
+* [**Phil Dibowitz**](https://github.com/jaymzh):
+[Issue 2594](https://github.com/opscode/chef/issues/2594) Restore missing require in `digester`.
+
+## 12.0.2
+* [Issue 2578](https://github.com/opscode/chef/issues/2578) Check that `installed` is not empty for `keg_only` formula in Homebrew provider
+* [Issue 2609](https://github.com/opscode/chef/issues/2609) Resolve the circular dependency between ProviderResolver and Resource.
+* [Issue 2596](https://github.com/opscode/chef/issues/2596) Fix nodes not writing to disk
+* [Issue 2580](https://github.com/opscode/chef/issues/2580) Make sure the relative paths are preserved when using link resource.
+* [Pull 2630](https://github.com/opscode/chef/pull/2630) Improve knife's SSL error messaging
+* [Issue 2606](https://github.com/opscode/chef/issues/2606) chef 12 ignores default_release for apt_package
+* [Issue 2602](https://github.com/opscode/chef/issues/2602) Fix `subscribes` resource notifications.
+* [Issue 2578](https://github.com/opscode/chef/issues/2578) Check that `installed` is not empty for `keg_only` formula in Homebrew provider.
+* [**gh2k**](https://github.com/gh2k):
+ [Issue 2625](https://github.com/opscode/chef/issues/2625) Fix missing `shell_out!` for `windows_package` resource
+* [**BackSlasher**](https://github.com/BackSlasher):
+ [Issue 2634](https://github.com/opscode/chef/issues/2634) Fix `option ':command' is not a valid option` error in subversion provider.
+* [**Seth Vargo**](https://github.com/sethvargo):
+ [Issue 2345](https://github.com/opscode/chef/issues/2345) Allow knife to install cookbooks with metadata.json.
+
+## 12.0.1
+
+* [Issue 2552](https://github.com/opscode/chef/issues/2552) Create constant for LWRP before calling `provides`
+* [Issue 2545](https://github.com/opscode/chef/issues/2545) `path` attribute of `execute` resource is restored to provide backwards compatibility with Chef 11.
+* [Issue 2565](https://github.com/opscode/chef/issues/2565) Fix `Chef::Knife::Core::BootstrapContext` constructor for knife-windows compat.
+* [Issue 2566](https://github.com/opscode/chef/issues/2566) Make sure Client doesn't raise error when interval is set on Windows.
+* [Issue 2560](https://github.com/opscode/chef/issues/2560) Fix `uninitialized constant Windows::Constants` in `windows_eventlog`.
+* [Issue 2563](https://github.com/opscode/chef/issues/2563) Make sure the Chef Client rpm packages are signed with GPG keys correctly.
## 12.0.0
diff --git a/DOC_CHANGES.md b/DOC_CHANGES.md
index 22294a3e39..7429baca2a 100644
--- a/DOC_CHANGES.md
+++ b/DOC_CHANGES.md
@@ -6,540 +6,32 @@ Example Doc Change:
Description of the required change.
-->
-### Knife now prefers to use `config.rb` rather than `knife.rb`
+### Chef now handles URI Schemes in a case insensitive manner
-Knife will now look for `config.rb` in preference to `knife.rb` for its
-configuration file. The syntax and configuration options available in
-`config.rb` are identical to `knife.rb`. Also, the search path for
-configuration files is unchanged.
+Previously, when a URI scheme contained all uppercase letters, Chef would reject the URI as invalid. In compliance with RFC3986, Chef now treats URI schemes in a case insensitive manner. This applies to all resources which accept URIs such as remote_file etc.
-At this time, it is _recommended_ that users use `config.rb` instead of
-`knife.rb`, but `knife.rb` is not deprecated; no warning will be emitted
-when using `knife.rb`. Once third-party application developers have had
-sufficient time to adapt to the change, `knife.rb` will become
-deprecated and config.rb will be preferred.
+### Experimental Audit Mode Feature
-### value_for_platform Method
+There is a new command_line flag provided for `chef-client`: `--audit-mode`. This accepts 1 of 3 arguments:
-- where <code>"platform"</code> can be a comma-separated list, each specifying a platform, such as Red Hat, openSUSE, or Fedora, <code>version</code> specifies the version of that platform, and <code>value</code> specifies the value that will be used if the node's platform matches the <code>value_for_platform</code> method. If each value only has a single platform, then the syntax is like the following:
-+ where <code>platform</code> can be a comma-separated list, each specifying a platform, such as Red Hat, openSUSE, or Fedora, <code>version</code> specifies either the exact version of that platform, or a constraint to match the platform's version against. The following rules apply to constraint matches:
+* `disabled` (default) - Audits are disabled and the phase is skipped. This is the default while Audit mode is an
+experimental feature.
+* `enabled` - Audits are enabled and will be performed after the converge phase.
+* `audit-only` - Audits are enabled and convergence is disabled. Only audits will be performed.
-+ * Exact matches take precedence no matter what, and should never throw exceptions.
-+ * Matching multiple constraints raises a <code>RuntimeError</code>.
-+ * The following constraints are allowed: <code><,<=,>,>=,~></code>.
-+
-+ The following is an example of using the method with constraints:
-+
-+ ```ruby
-+ value_for_platform(
-+ "os1" => {
-+ "< 1.0" => "less than 1.0",
-+ "~> 2.0" => "version 2.x",
-+ ">= 3.0" => "version 3.0",
-+ "3.0.1" => "3.0.1 will always use this value" }
-+ )
-+ ```
+This can also be configured in your node's client.rb with the key `audit_mode` and a value of `:disabled`, `:enabled` or `:audit_only`.
-+ If each value only has a single platform, then the syntax is like the following:
+### Chef Why Run Mode Ignores Audit Phase
-### environment attribute to git provider
+Because most users enable `why_run` mode to determine what resources convergence will update on their system, the audit
+phase is not executed. There is no way to get both `why_run` output and audit output in 1 single command. To get
+audit output without performing convergence use the `--audit-mode` flag.
-Similar to other environment options:
+#### Editors note 1
-```
-environment Hash of environment variables in the form of {"ENV_VARIABLE" => "VALUE"}.
-```
+The `--audit-mode` flag should be a link to the documentation for that flag
-Also the `user` attribute should mention the setting of the HOME env var:
+#### Editors node 2
-```
-user The system user that is responsible for the checked-out code. The HOME environment variable will automatically be
-set to the home directory of this user when using this option.
-```
-
-### Metadata `name` Attribute is Required.
-
-Current documentation states:
-
-> The name of the cookbook. This field is inferred unless specified.
-
-This is no longer correct as of 12.0. The `name` field is required; if
-it is not specified, an error will be raised if it is not specified.
-
-### chef-zero port ranges
-
-- to avoid crashes, by default, Chef will now scan a port range and take the first available port from 8889-9999.
-- to change this behavior, you can pass --chef-zero-port=PORT_RANGE (for example, 10,20,30 or 10000-20000) or modify Chef::Config.chef_zero.port to be a po
-rt string, an enumerable of ports, or a single port number.
-
-### Encrypted Data Bags Version 3
-
-Encrypted Data Bag version 3 uses [GCM](http://en.wikipedia.org/wiki/Galois/Counter_Mode) internally. Ruby 2 and OpenSSL version 1.0.1 or higher are required to use it.
-
-### New windows_service resource
-
-The windows_service resource inherits from the service resource and has all the same options but adds an action and attribute.
-
-action :configure_startup - sets the startup type on the resource to the value of the `startup_type` attribute
-attribute startup_type - the value as a symbol that the startup type should be set to on the service, valid options :automatic, :manual, :disabled
-
-Note that the service resource will also continue to set the startup type to automatic or disabled, respectively, when the enabled or disabled actions are used.
-
-### Fetch encrypted data bag items with dsl method
-DSL method `data_bag_item` now takes an optional String parameter `secret`, which is used to interact with encrypted data bag items.
-If the data bag item being fetched is encrypted and no `secret` is provided, Chef looks for a secret at `Chef::Config[:encrypted_data_bag_secret]`.
-If `secret` is provided, but the data bag item is not encrypted, then a regular data bag item is returned (no decryption is attempted).
-
-### Encrypted data bag UX
-The user can now provide a secret for data bags in 4 ways. They are, in order of descending preference:
-1. Provide the secret on the command line of `knife data bag` and `knife bootstrap` commands with `--secret`
-1. Provide the location of a file containing the secret on the command line of `knife data bag` and `knife bootstrap` commands with `--secret-file`
-1. Add the secret to your workstation config with `knife[:secret] = ...`
-1. Add the location of a file containing the secret to your workstation config with `knife[:secret-file] = ...`
-
-When adding the secret information to your workstation config, it will not be used for writeable operations unless `--encrypt` is also passed on the command line.
-Data bag read-only operations (`knife data bag show` and `knife bootstrap`) do not require `--encrypt` to be passed, and will attempt to use an available secret for decryption.
-Unencrypted data bags will not attempt to be unencrypted, even if a secret is provided.
-Trying to view an encrypted data bag without providing a secret will issue a warning and show the encrypted contents.
-Trying to edit or create an encrypted data bag without providing a secret will fail.
-
-Here are some example scenarios:
-
-```
-# Providing `knife[:secret_file] = ...` in knife.rb will create and encrypt the data bag
-knife data bag create BAG_NAME ITEM_NAME --encrypt
-
-# The same command ran with --secret will use the command line secret instead of the knife.rb secret
-knife data bag create ANOTHER_BAG ITEM_NAME --encrypt --secret 'ANOTHER_SECRET'
-
-# The next two commands will fail, because they are using the wrong secret
-knife data bag edit BAG_NAME --secret 'ANOTHER_SECRET'
-knife data bag edit ANOTHER_BAG --encrypt
-
-# The next command will unencrypt the data and show it using the `knife[:secret_file]` without passing the --encrypt flag
-knife data bag show BAG_NAME
-
-# To create an unencrypted data bag, simply do not provide `--secret`, `--secret-file` or `--encrypt`
-knife data bag create UNENCRYPTED_BAG
-
-# If a secret is available from any of the 4 possible entries, it will be copied to a bootstrapped node, even if `--encrypt` is not present
-knife bootstrap FQDN
-```
-
-### Enhanced search functionality: result filtering
-#### Use in recipes
-`Chef::Search::Query#search` can take an optional `:filter_result` argument which returns search data in the form of the Hash specified. Suppose your data looks like
-```json
-{"languages": {
- "c": {
- "gcc": {
- "version": "4.6.3",
- "description": "gcc version 4.6.3 (Ubuntu/Linaro 4.6.3-1ubuntu5) "
- }
- },
- "ruby": {
- "platform": "x86_64-linux",
- "version": "1.9.3",
- "release_date": "2013-11-22"
- },
- "perl": {
- "version": "5.14.2",
- "archname": "x86_64-linux-gnu-thread-multi"
- },
- "python": {
- "version": "2.7.3",
- "builddate": "Feb 27 2014, 19:58:35"
- }
-}}
-```
-for a node running Ubuntu named `node01`, and you want to get back only information on which versions of c and ruby you have. In a recipe you would write
-```ruby
-search(:node, "platform:ubuntu", :filter_result => {"c_version" => ["languages", "c", "gcc", "version"],
- "ruby_version" => ["languages", "ruby", "version"]})
-```
-and receive
-```ruby
-[
- {"url" => "https://api.opscode.com/organization/YOUR_ORG/nodes/node01",
- "data" => {"c_version" => "4.6.3", "ruby_version" => "1.9.3"},
- # snip other Ubuntu nodes
-]
-```
-If instead you wanted all the languages data (remember, `"languages"` is only one tiny piece of information the Chef Server stores about your node), you would have `:filter_result => {"languages" => ["languages"]}` in your search query.
-
-For backwards compatibility, a `partial_search` method has been added to `Chef::Search::Query` which can be used in the same way as the `partial_search` method from the [partial_search cookbook](https://supermarket.getchef.com/cookbooks/partial_search). Note that this method has been deprecated and will be removed in future versions of Chef.
-
-#### Use in knife
-Search results can likewise be filtered by adding the `--filter-result` (or `-f`) option. Considering the node data above, you can use `knife search` with filtering to extract the c and ruby versions on your Ubuntu platforms:
-```bash
-$ knife search node "platform:ubuntu" --filter-result "c_version:languages.c.gcc.version, ruby_version:languages.ruby.version"
-1 items found
-
-:
- c_version: 4.6.3
- ruby_version: 1.9.3
-
-$
-```
-
-## Client and solo application changes
-
-### Unforked interval chef-client runs are disabled
-Unforked interval and daemonized chef-client runs are now explicitly prohibited. Runs configured with CLI options
-`--interval SEC` or `--daemonize` paired with `--no-fork`, or the equivalent config options paired with
-`client_fork false` will fail immediately with error.
-
-### Sleep happens before converge
-When configured to splay sleep or run at intervals, `chef-client` and `chef-solo` perform both splay and interval
-sleeps before converging. In previous releases, chef would splay sleep then converge then interval sleep.
-
-### Signal handling
-When sent `SIGTERM` the thread or process will:
-1. if chef is not converging, exit immediately with exitstatus 3 or
-1. allow chef to finish converging then exit immediately with the converge's exitstatus.
-
-To terminate immediately, send `SIGINT`.
-
-# `knife ssl check` will verify X509 properties of your trusted certificates
-
-When you run `knife ssl check URL (options)` knife will verify if the certificate files, with extensions `*.crt` and `*.pem`
-in your `:trusted_certs_dir` have valid X509 certificate properties. Knife will generate warnings for certificates that
-do not meet X509 standards. OpenSSL **will not** use these certificates in verifying SSL connections.
-
-## Troubleshooting
-For each certificate that does not meet X509 specifications, a message will be displayed indicating why the certificate
-failed to meet these specifications. You may see output similar to
-
-```
-There are invalid certificates in your trusted_certs_dir.
-OpenSSL will not use the following certificates when verifying SSL connections:
-
-/path/to/your/invalid/certificate.crt: a message to help you debug
-```
-
-The documentation for resolving common issues with certificates is a work in progress. A few suggestions
-are outlined in the following sections. If you would like to help expand this documentation, please
-submit a pull request to [chef-docs](https://github.com/opscode/chef-docs) with your contribution.
-
-### Fetch the certificate again
-If the certificate was generated by your chef server, you may want to try downloading the certificate again.
-By default, the certificate is stored in the following location on the host where your chef-server runs:
-`/var/opt/chef-server/nginx/ca/SERVER_HOSTNAME.crt`. Copy that file into your `:trusted_certs_dir` using SSH,
-SCP, or some other secure method and run `knife ssl check URL (options)` again.
-
-### Generate a new certificate
-If you control the trusted certificate and you suspect it is bad (e.g., you've fetched the certificate again,
-but you're still getting warnings about it from `knife ssl check`), you might try generating a new certificate.
-
-#### Generate a certificate signing request
-If you used a certificate authority (CA) to authenticate your certificate, you'll need to generate
-a certificate signing request (CSR) to fetch a new certificate.
-
-If you don't have one already, you'll need to create an openssl configuration file. This example
-configuration file is saved in our current working directory as openssl.cnf
-
-```
-#
-# OpenSSL configuration file
-# ./openssl.cnf
-#
-
-[ req ]
-default_bits = 1024 # Size of keys
-default_keyfile = key.pem # name of generated keys
-default_md = md5 # message digest algorithm
-string_mask = nombstr # permitted characters
-distinguished_name = req_distinguished_name
-req_extensions = v3_req
-
-[ req_distinguished_name ]
-# Variable name Prompt string
-#--------------------- ----------------------------------
-0.organizationName = Organization Name (company)
-organizationalUnitName = Organizational Unit Name (department, division)
-emailAddress = Email Address
-emailAddress_max = 40
-localityName = Locality Name (city, district)
-stateOrProvinceName = State or Province Name (full name)
-countryName = Country Name (2 letter code)
-countryName_min = 2
-countryName_max = 2
-commonName = Common Name (hostname, IP, or your name)
-commonName_max = 64
-
-# Default values for the above, for consistency and less typing.
-# Variable name Value
-#-------------------------- ------------------------------
-0.organizationName_default = My Company
-localityName_default = My Town
-stateOrProvinceName_default = State or Providence
-countryName_default = US
-
-[ v3_req ]
-basicConstraints = CA:FALSE # This is NOT a CA certificate
-subjectKeyIdentifier = hash
-```
-
-You can use `openssl` to create a certificate from an existing private key
-```
-$ openssl req -new -extensions v3_req -key KEYNAME.pem -out REQNAME.pem -config ./openssl.cnf
-```
-or `openssl` can create a new private key simultaneously
-```
-$ openssl req -new -extensions v3_req -keyout KEYNAME.pem -out REQNAME.pem -config ./openssl.cnf
-```
-where `KEYNAME` is the path to your private key and `REQNAME` is the path to your CSR.
-
-You can verify your CSR was generated correctly
-```
-$ openssl req -noout -text -in REQNAME.pem
-```
-
-The final step is to submit your CSR to your certificate authority (CA) for signing.
-
-### Generate a self-signed (root) certificate
-You'll need to modify your openssl configuration file, or create a separate file, for
-generating root certificates.
-
-```
-#
-# OpenSSL configuration file
-# ./openssl.cnf
-#
-
-dir = .
-
-[ ca ]
-default_ca = CA_default
-
-[ CA_default ]
-serial = $dir/serial
-database = $dir/certindex.txt
-new_certs_dir = $dir/certs
-certificate = $dir/cacert.pem
-private_key = $dir/private/cakey.pem
-default_days = 365
-default_md = md5
-preserve = no
-email_in_dn = no
-nameopt = default_ca
-certopt = default_ca
-policy = policy_match
-
-[ policy_match ]
-countryName = match
-stateOrProvinceName = match
-organizationName = match
-organizationalUnitName = optional
-commonName = supplied
-emailAddress = optional
-
-[ v3_ca ]
-basicConstraints = CA:TRUE # This is a CA certificate
-subjectKeyIdentifier = hash
-authorityKeyIdentifier = keyid:always,issuer:always
-```
-
-You can now create a root certificate. If you have a private key you would like
-to use
-```
-$ openssl req -new -x509 -extensions v3_ca -key KEYNAME.pem -out CERTNAME.pem -config ./openssl.cnf
-```
-or `openssl` can create a new private key simultaneously
-```
-$ openssl req -new -x509 -extensions v3_ca -keyout KEYNAME.pem -out CERTNAME.pem -config ./openssl.cnf
-```
-where `KEYNAME` is the path to your private key and `REQNAME` is the path to your CSR.
-
-At this point, you should add the generated certificate to your trusted certificates as well as
-replace the old server certificate. Furthermore, you should regenerate any certificates that
-were signed by the previous root certificate.
-
-For more information and an example on how to set up your server to generate certificates
-check out this post on [setting up OpenSSL to create certificates](http://www.flatmtn.com/article/setting-openssl-create-certificates).
-
-#### Signing certificates
-Use your root certificate to sign certificate requests sent to your server
-```
-$ openssl ca -out CERTNAME.pem -config ./openssl.cnf -infiles REQNAME.pem
-```
-This creates the certificate `CERTNAME.pem` generated from CSR `REQNAME.pem`. You
-should send `CERTNAME.pem` back to the client who generated the CSR.
-
-### Certificate attributes
-When creating certificates and certificate signing requests, you will be prompted for
-information via the command line. These are your certificate attributes.
-
-RDN | Name | Explanation | Examples
-:---: | :---: | --- | ---
-CN | Common Name | You server's FQDN, or YOUR_SERVER Certificate Authority if root certificate | mail.domain.com, *.domain.com, MyServer Certificate Authority
-OU | Organizational Unit | (Optional) Additional organization information. | mail server, R&D
-O | Organization | The exact name of your organization. Do not abbreviate. | DevOpsRUs Inc.
-L | Locality | The city where your organization is located | Seattle
-S | State or Province | The state or province where your organization is located. Do not abbreviate. | Washington
-C | Country Name | 2-letter ISO abbreviation for your country. | US
- | Email Address | How you or another maintainer can be reached. | maintainers@devopsr.us
-
-If you examine the `policy_match` section in the openssl configuration file example from the section on generating
-self signed certificates, you'll see specifications that CSRs need to match the countryName, stateOrProvinceName,
-and the organizationName. CSRs whose CN, S, and O values do not match those of the root certificate will not be
-signed by that root certificate. You can modify these requirements as desired.
-
-### Key usage
-A keyUsage field can be added to your `v3_req` and `v3_ca` sections of your configuration file.
-Key usage extensions define the purpose of the public key contained in a certificate, limiting what
-it can and cannot be used for.
-
-Extension | Description
---- | ---
-digitalSignature | Use when the public key is used with a digital signature mechanism to support security services other than non-repudiation, certificate signing, or CRL signing. A digital signature is often used for entity authentication and data origin authentication with integrity
-nonRepudiation | Use when the public key is used to verify digital signatures used to provide a non-repudiation service. Non-repudiation protects against the signing entity falsely denying some action (excluding certificate or CRL signing).
-keyEncipherment | Use when a certificate will be used with a protocol that encrypts keys.
-dataEncipherment | Use when the public key is used for encrypting user data, other than cryptographic keys.
-keyAgreement | Use when the sender and receiver of the public key need to derive the key without using encryption. This key can then can be used to encrypt messages between the sender and receiver. Key agreement is typically used with Diffie-Hellman ciphers.
-certificateSigning | Use when the subject public key is used to verify a signature on certificates. This extension can be used only in CA certificates.
-cRLSigning | Use when the subject public key is to verify a signature on revocation information, such as a CRL.
-encipherOnly | Use only when key agreement is also enabled. This enables the public key to be used only for enciphering data while performing key agreement.
-decipherOnly | Use only when key agreement is also enabled. This enables the public key to be used only for deciphering data while performing key agreement.
-[Source](http://www-01.ibm.com/support/knowledgecenter/SSKTMJ_8.0.1/com.ibm.help.domino.admin.doc/DOC/H_KEY_USAGE_EXTENSIONS_FOR_INTERNET_CERTIFICATES_1521_OVER.html)
-
-### Subject Alternative Names
-Subject alternative names (SANs) allow you to list host names to protect with a single certificate.
-To create a certificate using SANs, you'll need to add a `subjectAltName` field to your `v3_req` section
-in your openssl configuration file
-
-```
-[ v3_req ]
-basicConstraints = CA:FALSE # This is NOT a CA certificate
-subjectKeyIdentifier = hash
-subjectAltName = @alt_names
-
-[alt_names]
-DNS.1 = kb.example.com
-DNS.2 = helpdesk.example.org
-DNS.3 = systems.example.net
-IP.1 = 192.168.1.1
-IP.2 = 192.168.69.14
-```
-
-### Reboot resource in core
-The `reboot` resource will reboot the server, a necessary step in some installations, especially on Windows. If this resource is used with notifications, it must receive explicit `:immediate` notifications only: results of delayed notifications are undefined. Currently supported on Windows, Linux, and OS X; will work incidentally on some other Unixes.
-
-There are three actions:
-
-```ruby
-reboot "app_requires_reboot" do
- action :request_reboot
- reason "Need to reboot when the run completes successfully."
- delay_mins 5
-end
-
-reboot "cancel_reboot_request" do
- action :cancel
- reason "Cancel a previous end-of-run reboot request."
-end
-
-reboot "now" do
- action :reboot_now
- reason "Cannot continue Chef run without a reboot."
- delay_mins 2
-end
-
-# the `:immediate` is required for results to be defined.
-notifies :reboot_now, "reboot[now]", :immediate
-```
-
-### Escape sensitive characters before globbing
-Some paths contain characters reserved by glob and must be escaped so that
-glob operations perform as expected. One common example is Windows file paths
-separated by `"\\"`. To ensure that your globs work correctly, it is recommended
-that you apply `Chef::Util::PathHelper::escape_glob` before globbing file paths.
-
-```ruby
-path = "C:\\Users\\me\\chef-repo\\cookbooks"
-Dir.exist?(path) # true
-Dir.entries(path) # [".", "..", "apache2", "apt", ...]
-
-Dir.glob(File.join(path, "*")) # []
-Dir[File.join(path, "*")] # []
-
-PathHelper = Chef::Util::PathHelper
-Dir.glob(File.join(PathHelper.escape_glob(path), "*")) # ["#{path}\\apache2", "#{path}\\apt", ...]
-Dir[PathHelper.escape_glob(path) + "/*"] # ["#{path}\\apache2", "#{path}\\apt", ...]
-```
-## Mac OS X default package provider is now Homebrew
-
-Per [Chef RFC 016](https://github.com/opscode/chef-rfc/blob/master/rfc016-homebrew-osx-package-provider.md), the default provider for the `package` resource on Mac OS X is now [Homebrew](http://brew.sh). The [homebrew cookbook's](https://supermarket.getchef.com/cookbooks/homebrew) default recipe, or some other method is still required for getting homebrew installed on the system. The cookbook won't be strictly required just to install packages from homebrew on OS X, though. To use this, simply use the `package` resource, or the `homebrew_package` shortcut resource:
-
-```ruby
-package 'emacs'
-```
-
-Or,
-
-```ruby
-homebrew_package 'emacs'
-```
-
-The macports provider will still be available, and can be used with the shortcut resource, or by using the `provider` attribute:
-
-```ruby
-macports_package 'emacs'
-```
-
-Or,
-
-```ruby
-package 'emacs' do
- provider Chef::Provider::Package::Macports
-end
-```
-
-### Providing `homebrew_user`
-
-Homebrew recommends being ran as a non-root user, whereas Chef recommends being ran with root privileges. The
-`homebrew_package` provider has logic to try and determine which user to install Homebrew packages as.
-
-By default, the `homebrew_package` provider will try to execute the homebrew command as the owner of the `/usr/local/bin/brew`
-executable. If that executable does not exist, Chef will try to find it by executing `which brew`. If that cannot be
-found, Chef then errors. The Homebrew recommendation is the default install, which will place the executable at
-`/usr/local/bin/brew` owned by a non-root user.
-
-You can circumvent this by providing the `homebrew_package` a `homebrew_user` attribute, like:
-
-```ruby
-# provided as a uid
-homebrew_package 'emacs' do
- homebrew_user 1001
-end
-
-# provided as a string
-homebrew_package 'vim' do
- homebrew_user 'user1'
-end
-```
-
-Chef will then execute the Homebrew command as that user. The `homebrew_user` attribute can only be provided to the
-`homebrew_package` resource, not the `package` resource.
-
-### Default `guard_interpreter` attribute for `powershell_script` resource
-
-For the `powershell_script` resource, the `guard_interpreter` attribute is set to `:powershell_script` by default. This means
-that if a string is supplied to an `only_if` or `not_if` attribute of a `powersell_script` resource, the PowerShell command
-interpreter (the 64-bit version) will be used to evaluate the guard. It also means that other features available to the guard
-when `guard_interpreter` is set to something other than `:default`, such as inheritance of attributes and the specification of
-process architectur of the guard process (i.e. 32-bit or 64-bit process) are available by default.
-
-In versions of Chef prior to Chef 12, the value of the attribute was `:default` by default, which uses the 32-bit version of the
-`cmd.exe` (batch script language) shell to evaluate strings supplied to guards.
-
-### Default `guard_interpreter` attribute for `batch` resource
-
-For the`batch` resource, the `guard_interpreter` attribute it is set to `:batch` by default. This means
-that if a string is supplied to an `only_if` or `not_if` attribute of a `batch` resource, the 64-bit version of the Windows
-default command interpreter, `cmd.exe`, will be used to evaluate the guard. It also means that other features available to the guard
-when `guard_interpreter` is set to something other than `:default`, such as inheritance of attributes and the specification of
-process architecture of the guard process (i.e. 32-bit or 64-bit process) are available by default.
-
-In versions of Chef prior to Chef 12, the value of the attribute was `:default` by default, which means the 32-bit version of the
-`cmd.exe` (batch script language) shell would be used to evaluate strings supplied to guards.
+This probably only needs to be a bullet point added to http://docs.getchef.com/nodes.html#about-why-run-mode under the
+`certain assumptions` section
diff --git a/README.md b/README.md
index 93665ee991..8793cc190b 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,7 @@
-# Chef [![Code Climate](https://codeclimate.com/github/opscode/chef.png)](https://codeclimate.com/github/opscode/chef)
+# Chef
+[![Code Climate](https://codeclimate.com/github/opscode/chef.png)](https://codeclimate.com/github/opscode/chef)
+[![Build Status Master](https://travis-ci.org/opscode/chef.svg?branch=master)](https://travis-ci.org/opscode/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)
Want to try Chef? Get started with [learnchef](https://learnchef.opscode.com)
@@ -87,6 +90,11 @@ environment to run the specs--they are completely standalone.
# Run a Subset of Tests
bundle exec rspec spec/PATH/TO/DIR
+When you submit a pull request, we will automatically run the functional and unit
+tests in spec/functional/ and spec/unit/ respectively. These will be run on Ubuntu
+through Travis CI, and on Windows through AppVeyor. The status of these runs will
+be displayed with your pull request.
+
# License
Chef - A configuration management system
diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md
index 630aa737df..43c8f06d93 100644
--- a/RELEASE_NOTES.md
+++ b/RELEASE_NOTES.md
@@ -1,13 +1,69 @@
-# Chef Client Release Notes 12.2.0:
+# Chef Client Release Notes 12.1.0:
# Internal API Changes in this Release
+## Experimental Audit Mode Feature
+
+This is a new feature intended to provide _infrastructure audits_. Chef already allows you to configure your infrastructure
+with code, but there are some use cases that are not covered by resource convergence. What if you want to check that
+the application Chef just installed is functioning correctly? If it provides a status page an audit can check this
+and validate that the application has database connectivity.
+
+Audits are performed by leveraging [Serverspec](http://serverspec.org/) and [RSpec](https://relishapp.com/rspec) on the
+node. As such the syntax is very similar to a normal RSpec spec.
+
+### Syntax
+
+```ruby
+controls "Database Audit" do
+
+ control "postgres package" do
+ it "should not be installed" do
+ expect(package("postgresql")).to_not be_installed
+ end
+ end
+
+ let(:p) { port(111) }
+ control p do
+ it "has nothing listening" do
+ expect(p).to_not be_listening
+ end
+ end
+
+end
+```
+
+Using the example above I will break down the components of an Audit:
+
+* `controls` - This named block contains all the audits to be performed during the audit phase. During Chef convergence
+ the audits will be collected and ran in a separate phase at the end of the Chef run. Any `controls` block defined in
+ a recipe that is ran on the node will be performed.
+* `control` - This keyword describes a section of audits to perform. The name here should either be a string describing
+the system under test, or a [Serverspec resource](http://serverspec.org/resource_types.html).
+* `it` - Inside this block you can use [RSpec expectations](https://relishapp.com/rspec/rspec-expectations/docs) to
+write the audits. You can use the Serverspec resources here or regular ruby code. Any raised errors will fail the
+audit.
+
+### Output and error handling
+
+Output from the audit run will appear in your `Chef::Config[:log_location]`. If an audit fails then Chef will raise
+an error and exit with a non-zero status.
+
+### Further reading
+
+More information about the audit mode can be found in its
+[RFC](https://github.com/opscode/chef-rfc/blob/master/rfc035-audit-mode.md)
+
# End-User Changes
## OpenBSD Package provider was added
The package resource on OpenBSD is wired up to use the new OpenBSD package provider to install via pkg_add on OpenBSD systems.
+## Case Insensitive URI Handling
+
+Previously, when a URI scheme contained all uppercase letters, Chef would reject the URI as invalid. In compliance with RFC3986, Chef now treats URI schemes in a case insensitive manner.
+
# Chef Client Release Notes 12.0.0:
# Internal API Changes in this Release
diff --git a/appveyor.yml b/appveyor.yml
new file mode 100644
index 0000000000..b9748c220b
--- /dev/null
+++ b/appveyor.yml
@@ -0,0 +1,32 @@
+version: "master-{build}"
+
+os: Windows Server 2012
+platform:
+ - x64
+
+environment:
+ matrix:
+ - ruby_version: "200"
+
+clone_folder: c:\projects\chef
+clone_depth: 1
+skip_tags: true
+branches:
+ only:
+ - master
+ - 12-stable
+
+install:
+ - winrm quickconfig -q
+ - SET PATH=C:\Ruby%ruby_version%\bin;%PATH%
+ - echo %PATH%
+ - ruby --version
+ - gem --version
+ - gem install bundler --quiet --no-ri --no-rdoc
+ - bundler --version
+
+build_script:
+ - bundle install
+
+test_script:
+ - bundle exec rake spec
diff --git a/chef.gemspec b/chef.gemspec
index f623f8bb82..52babdc5a1 100644
--- a/chef.gemspec
+++ b/chef.gemspec
@@ -35,16 +35,17 @@ Gem::Specification.new do |s|
s.add_dependency 'plist', '~> 3.1.0'
+ # Audit mode requires these, so they are non-developmental dependencies now
+ %w(rspec-core rspec-expectations rspec-mocks).each { |gem| s.add_dependency gem, "~> 3.1" }
+ s.add_dependency "rspec_junit_formatter", "~> 0.2.0"
+ s.add_dependency "serverspec", "~> 2.7"
+ s.add_dependency "specinfra", "~> 2.10"
+
s.add_development_dependency "rack"
# Rake 10.2 drops Ruby 1.8 support
s.add_development_dependency "rake", "~> 10.1.0"
- # rspec_junit_formatter 0.2.0 drops ruby 1.8.7 support
- s.add_development_dependency "rspec_junit_formatter", "~> 0.2.0"
-
- %w(rspec-core rspec-expectations rspec-mocks).each { |gem| s.add_development_dependency gem, "~> 3.0" }
-
s.bindir = "bin"
s.executables = %w( chef-client chef-solo knife chef-shell chef-apply )
diff --git a/kitchen-tests/.chef/client.rb b/kitchen-tests/.chef/client.rb
index 5eb200a939..98f773d691 100644
--- a/kitchen-tests/.chef/client.rb
+++ b/kitchen-tests/.chef/client.rb
@@ -1,7 +1,8 @@
-chef_dir = File.expand_path(File.dirame(__FILE__))
-repo_dir = File.expand_path(Fild.join(chef_dir, '..'))
+chef_dir = File.expand_path(File.dirname(__FILE__))
+repo_dir = File.expand_path(File.join(chef_dir, '..'))
-log_level :info
+log_level :info
chef_repo_path repo_dir
-local_mode true
+local_mode true
+cache_path "#{ENV['HOME']}/.cache/chef"
diff --git a/kitchen-tests/cookbooks/audit_test/.gitignore b/kitchen-tests/cookbooks/audit_test/.gitignore
new file mode 100644
index 0000000000..ec2a890bd3
--- /dev/null
+++ b/kitchen-tests/cookbooks/audit_test/.gitignore
@@ -0,0 +1,16 @@
+.vagrant
+Berksfile.lock
+*~
+*#
+.#*
+\#*#
+.*.sw[a-z]
+*.un~
+
+# Bundler
+Gemfile.lock
+bin/*
+.bundle/*
+
+.kitchen/
+.kitchen.local.yml
diff --git a/kitchen-tests/cookbooks/audit_test/.kitchen.yml b/kitchen-tests/cookbooks/audit_test/.kitchen.yml
new file mode 100644
index 0000000000..be11e33081
--- /dev/null
+++ b/kitchen-tests/cookbooks/audit_test/.kitchen.yml
@@ -0,0 +1,16 @@
+---
+driver:
+ name: vagrant
+
+provisioner:
+ name: chef_zero
+
+platforms:
+ - name: ubuntu-12.04
+ - name: centos-6.5
+
+suites:
+ - name: default
+ run_list:
+ - recipe[audit_test::default]
+ attributes:
diff --git a/kitchen-tests/cookbooks/audit_test/Berksfile b/kitchen-tests/cookbooks/audit_test/Berksfile
new file mode 100644
index 0000000000..0ac9b78cf7
--- /dev/null
+++ b/kitchen-tests/cookbooks/audit_test/Berksfile
@@ -0,0 +1,3 @@
+source "https://supermarket.getchef.com"
+
+metadata
diff --git a/kitchen-tests/cookbooks/audit_test/README.md b/kitchen-tests/cookbooks/audit_test/README.md
new file mode 100644
index 0000000000..75e2f44808
--- /dev/null
+++ b/kitchen-tests/cookbooks/audit_test/README.md
@@ -0,0 +1,12 @@
+# audit_test
+
+This cookbook has some basic recipes to test audit mode.
+
+In order to run these tests on your dev box:
+
+```
+$ bundle install
+$ bundle exec chef-client -c kitchen-tests/.chef/client.rb -z -o audit_test::default -l debug
+```
+
+Expected JSON output for the tests will be printed to `debug` log.
diff --git a/kitchen-tests/cookbooks/audit_test/chefignore b/kitchen-tests/cookbooks/audit_test/chefignore
new file mode 100644
index 0000000000..80dc2d20ef
--- /dev/null
+++ b/kitchen-tests/cookbooks/audit_test/chefignore
@@ -0,0 +1,95 @@
+# Put files/directories that should be ignored in this file when uploading
+# or sharing to the community site.
+# Lines that start with '# ' are comments.
+
+# OS generated files #
+######################
+.DS_Store
+Icon?
+nohup.out
+ehthumbs.db
+Thumbs.db
+
+# SASS #
+########
+.sass-cache
+
+# EDITORS #
+###########
+\#*
+.#*
+*~
+*.sw[a-z]
+*.bak
+REVISION
+TAGS*
+tmtags
+*_flymake.*
+*_flymake
+*.tmproj
+.project
+.settings
+mkmf.log
+
+## COMPILED ##
+##############
+a.out
+*.o
+*.pyc
+*.so
+*.com
+*.class
+*.dll
+*.exe
+*/rdoc/
+
+# Testing #
+###########
+.watchr
+.rspec
+spec/*
+spec/fixtures/*
+test/*
+features/*
+Guardfile
+Procfile
+
+# SCM #
+#######
+.git
+*/.git
+.gitignore
+.gitmodules
+.gitconfig
+.gitattributes
+.svn
+*/.bzr/*
+*/.hg/*
+*/.svn/*
+
+# Berkshelf #
+#############
+Berksfile
+Berksfile.lock
+cookbooks/*
+tmp
+
+# Cookbooks #
+#############
+CONTRIBUTING
+
+# Strainer #
+############
+Colanderfile
+Strainerfile
+.colander
+.strainer
+
+# Vagrant #
+###########
+.vagrant
+Vagrantfile
+
+# Travis #
+##########
+.travis.yml
diff --git a/kitchen-tests/cookbooks/audit_test/metadata.rb b/kitchen-tests/cookbooks/audit_test/metadata.rb
new file mode 100644
index 0000000000..4a60104e92
--- /dev/null
+++ b/kitchen-tests/cookbooks/audit_test/metadata.rb
@@ -0,0 +1,8 @@
+name 'audit_test'
+maintainer 'The Authors'
+maintainer_email 'you@example.com'
+license 'all_rights'
+description 'Installs/Configures audit_test'
+long_description 'Installs/Configures audit_test'
+version '0.1.0'
+
diff --git a/kitchen-tests/cookbooks/audit_test/recipes/default.rb b/kitchen-tests/cookbooks/audit_test/recipes/default.rb
new file mode 100644
index 0000000000..4f634d73c1
--- /dev/null
+++ b/kitchen-tests/cookbooks/audit_test/recipes/default.rb
@@ -0,0 +1,26 @@
+#
+# Cookbook Name:: audit_test
+# Recipe:: default
+#
+# Copyright (c) 2014 The Authors, All Rights Reserved.
+
+controls "basic control group" do
+ control "basic math" do
+ it "should pass" do
+ expect(2 - 2).to eq(0)
+ end
+ end
+end
+
+controls "control group without top level control" do
+ it "should pass" do
+ expect(2 - 2).to eq(0)
+ end
+end
+
+controls "control group with empty control" do
+ control "empty"
+end
+
+controls "empty control group with block" do
+end
diff --git a/kitchen-tests/cookbooks/audit_test/recipes/error_duplicate_control_groups.rb b/kitchen-tests/cookbooks/audit_test/recipes/error_duplicate_control_groups.rb
new file mode 100644
index 0000000000..77a4592e9d
--- /dev/null
+++ b/kitchen-tests/cookbooks/audit_test/recipes/error_duplicate_control_groups.rb
@@ -0,0 +1,17 @@
+#
+# Cookbook Name:: audit_test
+# Recipe:: error_duplicate_control_groups
+#
+# Copyright (c) 2014 The Authors, All Rights Reserved.
+
+controls "basic control group" do
+ it "should pass" do
+ expect(2 - 2).to eq(0)
+ end
+end
+
+controls "basic control group" do
+ it "should pass" do
+ expect(2 - 2).to eq(0)
+ end
+end
diff --git a/kitchen-tests/cookbooks/audit_test/recipes/error_no_block.rb b/kitchen-tests/cookbooks/audit_test/recipes/error_no_block.rb
new file mode 100644
index 0000000000..76a8817b5d
--- /dev/null
+++ b/kitchen-tests/cookbooks/audit_test/recipes/error_no_block.rb
@@ -0,0 +1,7 @@
+#
+# Cookbook Name:: audit_test
+# Recipe:: error_no_block
+#
+# Copyright (c) 2014 The Authors, All Rights Reserved.
+
+controls "empty control group without block"
diff --git a/kitchen-tests/cookbooks/audit_test/recipes/error_orphan_control.rb b/kitchen-tests/cookbooks/audit_test/recipes/error_orphan_control.rb
new file mode 100644
index 0000000000..d74acd6c6b
--- /dev/null
+++ b/kitchen-tests/cookbooks/audit_test/recipes/error_orphan_control.rb
@@ -0,0 +1,13 @@
+#
+# Cookbook Name:: audit_test
+# Recipe:: error_orphan_control
+#
+# Copyright (c) 2014 The Authors, All Rights Reserved.
+
+controls "basic control group" do
+ it "should pass" do
+ expect(2 - 2).to eq(0)
+ end
+end
+
+control "orphan control"
diff --git a/kitchen-tests/cookbooks/audit_test/recipes/failed_specs.rb b/kitchen-tests/cookbooks/audit_test/recipes/failed_specs.rb
new file mode 100644
index 0000000000..3225d3983e
--- /dev/null
+++ b/kitchen-tests/cookbooks/audit_test/recipes/failed_specs.rb
@@ -0,0 +1,14 @@
+#
+# Cookbook Name:: audit_test
+# Recipe:: failed_specs
+#
+# Copyright (c) 2014 The Authors, All Rights Reserved.
+
+controls "basic control group" do
+ control "basic math" do
+ # Can not write a good control :(
+ it "should pass" do
+ expect(2 - 0).to eq(0)
+ end
+ end
+end
diff --git a/kitchen-tests/cookbooks/audit_test/recipes/serverspec_collision.rb b/kitchen-tests/cookbooks/audit_test/recipes/serverspec_collision.rb
new file mode 100644
index 0000000000..70109d84b8
--- /dev/null
+++ b/kitchen-tests/cookbooks/audit_test/recipes/serverspec_collision.rb
@@ -0,0 +1,31 @@
+#
+# Cookbook Name:: audit_test
+# Recipe:: serverspec_collision
+#
+# Copyright (c) 2014 The Authors, All Rights Reserved.
+
+file "/tmp/audit_test_file" do
+ action :create
+ content "Welcome to audit mode."
+end
+
+controls "file auditing" do
+ describe "test file" do
+ it "says welcome" do
+ expect(file("/tmp/audit_test_file")).to contain("Welcome")
+ end
+ end
+end
+
+file "/tmp/audit_test_file_2" do
+ action :create
+ content "Bye to audit mode."
+end
+
+controls "end file auditing" do
+ describe "end file" do
+ it "says bye" do
+ expect(file("/tmp/audit_test_file_2")).to contain("Bye")
+ end
+ end
+end
diff --git a/kitchen-tests/cookbooks/audit_test/recipes/serverspec_support.rb b/kitchen-tests/cookbooks/audit_test/recipes/serverspec_support.rb
new file mode 100644
index 0000000000..0396cc0de7
--- /dev/null
+++ b/kitchen-tests/cookbooks/audit_test/recipes/serverspec_support.rb
@@ -0,0 +1,37 @@
+#
+# Cookbook Name:: audit_test
+# Recipe:: serverspec_support
+#
+# Copyright (c) 2014 The Authors, All Rights Reserved.
+
+file "/tmp/audit_test_file" do
+ action :create
+ content "Welcome to audit mode."
+end
+
+# package "curl" do
+# action :install
+# end
+
+controls "serverspec helpers with types" do
+ control "file helper" do
+ it "says welcome" do
+ expect(file("/tmp/audit_test_file")).to contain("Welcome")
+ end
+ end
+
+ control service("com.apple.CoreRAID") do
+ it { is_expected.to be_enabled }
+ it { is_expected.not_to be_running }
+ end
+
+ # describe "package helper" do
+ # it "works" do
+ # expect(package("curl")).to be_installed
+ # end
+ # end
+
+ control package("postgresql") do
+ it { is_expected.to_not be_installed }
+ end
+end
diff --git a/kitchen-tests/cookbooks/audit_test/recipes/with_include_recipe.rb b/kitchen-tests/cookbooks/audit_test/recipes/with_include_recipe.rb
new file mode 100644
index 0000000000..ff39cde117
--- /dev/null
+++ b/kitchen-tests/cookbooks/audit_test/recipes/with_include_recipe.rb
@@ -0,0 +1,16 @@
+#
+# Cookbook Name:: audit_test
+# Recipe:: with_include_recipe
+#
+# Copyright (c) 2014 The Authors, All Rights Reserved.
+
+include_recipe "audit_test::serverspec_collision"
+
+controls "basic example" do
+ it "should pass" do
+ expect(2 - 2).to eq(0)
+ end
+end
+
+include_recipe "audit_test::serverspec_collision"
+include_recipe "audit_test::default"
diff --git a/kitchen-tests/cookbooks/webapp/README.md b/kitchen-tests/cookbooks/webapp/README.md
index e8de6ee467..f19ab46735 100644
--- a/kitchen-tests/cookbooks/webapp/README.md
+++ b/kitchen-tests/cookbooks/webapp/README.md
@@ -1,4 +1,3 @@
# webapp
TODO: Enter the cookbook description here.
-
diff --git a/kitchen-tests/cookbooks/webapp/metadata.rb b/kitchen-tests/cookbooks/webapp/metadata.rb
index ecfb419953..c26ad23979 100644
--- a/kitchen-tests/cookbooks/webapp/metadata.rb
+++ b/kitchen-tests/cookbooks/webapp/metadata.rb
@@ -7,6 +7,6 @@ long_description 'Installs/Configures webapp'
version '0.1.0'
depends 'apache2'
-depends 'database'
+depends 'database', '~> 2.3.1'
depends 'mysql'
depends 'php'
diff --git a/lib/chef/application/client.rb b/lib/chef/application/client.rb
index 295dc2470e..40772c0f8f 100644
--- a/lib/chef/application/client.rb
+++ b/lib/chef/application/client.rb
@@ -239,9 +239,9 @@ class Chef::Application::Client < Chef::Application
end
option :audit_mode,
- :long => "--[no-]audit-mode",
- :description => "If not specified, run converge and audit phase. If true, run only audit phase. If false, run only converge phase.",
- :boolean => true
+ :long => "--audit-mode MODE",
+ :description => "Enable audit-mode with `enabled`. Disable audit-mode with `disabled`. Skip converge and only perform audits with `audit-only`",
+ :proc => lambda { |mo| mo.gsub("-", "_").to_sym }
IMMEDIATE_RUN_SIGNAL = "1".freeze
@@ -280,6 +280,19 @@ class Chef::Application::Client < Chef::Application
config_fetcher = Chef::ConfigFetcher.new(Chef::Config[:json_attribs])
@chef_client_json = config_fetcher.fetch_json
end
+
+ if mode = config[:audit_mode] || Chef::Config[:audit_mode]
+ expected_modes = [:enabled, :disabled, :audit_only]
+ unless expected_modes.include?(mode)
+ Chef::Application.fatal!(unrecognized_audit_mode(mode))
+ end
+
+ unless mode == :disabled
+ # This should be removed when audit-mode is enabled by default/no longer
+ # an experimental feature.
+ Chef::Log.warn(audit_mode_experimental_message)
+ end
+ end
end
def load_config_file
@@ -400,4 +413,26 @@ class Chef::Application::Client < Chef::Application
"#{"\n interval = #{Chef::Config[:interval]} seconds" if Chef::Config[:interval]}" +
"\nEnable chef-client interval runs by setting `:client_fork = true` in your config file or adding `--fork` to your command line options."
end
+
+ def audit_mode_settings_explaination
+ "\n* To enable audit mode after converge, use command line option `--audit-mode enabled` or set `:audit_mode = :enabled` in your config file." +
+ "\n* To disable audit mode, use command line option `--audit-mode disabled` or set `:audit_mode = :disabled` in your config file." +
+ "\n* To only run audit mode, use command line option `--audit-mode audit-only` or set `:audit_mode = :audit_only` in your config file." +
+ "\nAudit mode is disabled by default."
+ end
+
+ def unrecognized_audit_mode(mode)
+ "Unrecognized setting #{mode} for audit mode." + audit_mode_settings_explaination
+ end
+
+ def audit_mode_experimental_message
+ msg = if Chef::Config[:audit_mode] == :audit_only
+ "Chef-client has been configured to skip converge and run only audits."
+ else
+ "Chef-client has been configured to run audits after it converges."
+ end
+ msg += " Audit mode is an experimental feature currently under development. API changes may occur. Use at your own risk."
+ msg += audit_mode_settings_explaination
+ return msg
+ end
end
diff --git a/lib/chef/application/solo.rb b/lib/chef/application/solo.rb
index 6e568ddbb1..c3f5444ef7 100644
--- a/lib/chef/application/solo.rb
+++ b/lib/chef/application/solo.rb
@@ -211,6 +211,9 @@ class Chef::Application::Solo < Chef::Application
config_fetcher = Chef::ConfigFetcher.new(Chef::Config[:json_attribs])
@chef_client_json = config_fetcher.fetch_json
end
+
+ # Disable auditing for solo
+ Chef::Config[:audit_mode] = :disabled
end
def setup_application
diff --git a/lib/chef/application/windows_service_manager.rb b/lib/chef/application/windows_service_manager.rb
index 30810c51f2..de8ed657c2 100644
--- a/lib/chef/application/windows_service_manager.rb
+++ b/lib/chef/application/windows_service_manager.rb
@@ -16,7 +16,9 @@
# limitations under the License.
#
-require 'win32/service'
+if RUBY_PLATFORM =~ /mswin|mingw32|windows/
+ require 'win32/service'
+end
require 'chef/config'
require 'mixlib/cli'
@@ -88,6 +90,8 @@ class Chef
@service_display_name = service_options[:service_display_name]
@service_description = service_options[:service_description]
@service_file_path = service_options[:service_file_path]
+ @service_start_name = service_options[:run_as_user]
+ @password = service_options[:run_as_password]
end
def run(params = ARGV)
@@ -116,7 +120,9 @@ class Chef
# and we don't want that, so we need to override the service type.
:service_type => ::Win32::Service::SERVICE_WIN32_OWN_PROCESS,
:start_type => ::Win32::Service::SERVICE_AUTO_START,
- :binary_path_name => cmd
+ :binary_path_name => cmd,
+ :service_start_name => @service_start_name,
+ :password => @password,
)
puts "Service '#{@service_name}' has successfully been installed."
end
diff --git a/lib/chef/audit/audit_event_proxy.rb b/lib/chef/audit/audit_event_proxy.rb
new file mode 100644
index 0000000000..2512b8bfe2
--- /dev/null
+++ b/lib/chef/audit/audit_event_proxy.rb
@@ -0,0 +1,93 @@
+#
+# Author:: Tyler Ball (<tball@chef.io>)
+# 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.
+#
+
+RSpec::Support.require_rspec_core "formatters/base_text_formatter"
+
+class Chef
+ class Audit
+ class AuditEventProxy < ::RSpec::Core::Formatters::BaseFormatter
+ ::RSpec::Core::Formatters.register self, :stop, :example_group_started
+
+ # TODO I don't like this, but I don't see another way to pass this in
+ # see rspec files configuration.rb#L671 and formatters.rb#L129
+ def self.events=(events)
+ @@events = events
+ end
+
+ def events
+ @@events
+ end
+
+ def example_group_started(notification)
+ if notification.group.parent_groups.size == 1
+ # top level `controls` block
+ desc = notification.group.description
+ Chef::Log.debug("Entered `controls` block named #{desc}")
+ events.control_group_started(desc)
+ end
+ end
+
+ def stop(notification)
+ Chef::Log.info("Successfully executed all `controls` blocks and contained examples")
+ notification.examples.each do |example|
+ control_group_name, control_data = build_control_from(example)
+ e = example.exception
+ if e
+ events.control_example_failure(control_group_name, control_data, e)
+ else
+ events.control_example_success(control_group_name, control_data)
+ end
+ end
+ end
+
+ private
+
+ def build_control_from(example)
+ described_class = example.metadata[:described_class]
+ if described_class
+ resource_type = described_class.class.name.split(':')[-1]
+ resource_name = described_class.name
+ end
+
+ # The following code builds up the context - the list of wrapping `describe` or `control` blocks
+ describe_groups = []
+ group = example.metadata[:example_group]
+ # If the innermost block has a resource instead of a string, don't include it in context
+ describe_groups.unshift(group[:description]) if described_class.nil?
+ group = group[:parent_example_group]
+ while !group.nil?
+ describe_groups.unshift(group[:description])
+ group = group[:parent_example_group]
+ end
+
+ # We know all of our examples each live in a top-level `controls` block - get this name now
+ outermost_group_desc = describe_groups.shift
+
+ return outermost_group_desc, {
+ :name => example.description,
+ :desc => example.full_description,
+ :resource_type => resource_type,
+ :resource_name => resource_name,
+ :context => describe_groups,
+ :line_number => example.metadata[:line_number]
+ }
+ end
+
+ end
+ end
+end
diff --git a/lib/chef/audit/audit_reporter.rb b/lib/chef/audit/audit_reporter.rb
new file mode 100644
index 0000000000..a5dd9a6c48
--- /dev/null
+++ b/lib/chef/audit/audit_reporter.rb
@@ -0,0 +1,169 @@
+#
+# Author:: Tyler Ball (<tball@chef.io>)
+#
+# 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/event_dispatch/base'
+require 'chef/audit/control_group_data'
+require 'time'
+
+class Chef
+ class Audit
+ class AuditReporter < EventDispatch::Base
+
+ attr_reader :rest_client, :audit_data, :ordered_control_groups, :run_status
+ private :rest_client, :audit_data, :ordered_control_groups, :run_status
+
+ PROTOCOL_VERSION = '0.1.1'
+
+ def initialize(rest_client)
+ @rest_client = rest_client
+ # Ruby 1.9.3 and above "enumerate their values in the order that the corresponding keys were inserted."
+ @ordered_control_groups = Hash.new
+ end
+
+ def run_context
+ run_status.run_context
+ end
+
+ def audit_phase_start(run_status)
+ Chef::Log.debug("Audit Reporter starting")
+ @audit_data = AuditData.new(run_status.node.name, run_status.run_id)
+ @run_status = run_status
+ end
+
+ def audit_phase_complete
+ Chef::Log.debug("Audit Reporter completed successfully without errors.")
+ ordered_control_groups.each do |name, control_group|
+ audit_data.add_control_group(control_group)
+ end
+ end
+
+ # If the audit phase failed, its because there was some kind of error in the framework
+ # that runs tests - normal errors are interpreted as EXAMPLE failures and captured.
+ # We still want to send available audit information to the server so we process the
+ # known control groups.
+ def audit_phase_failed(error)
+ # The stacktrace information has already been logged elsewhere
+ Chef::Log.debug("Audit Reporter failed.")
+ ordered_control_groups.each do |name, control_group|
+ audit_data.add_control_group(control_group)
+ end
+ end
+
+ def run_completed(node)
+ post_auditing_data
+ end
+
+ def run_failed(error)
+ post_auditing_data(error)
+ end
+
+ def control_group_started(name)
+ if ordered_control_groups.has_key?(name)
+ raise Chef::Exceptions::AuditControlGroupDuplicate.new(name)
+ end
+ metadata = run_context.audits[name].metadata
+ ordered_control_groups.store(name, ControlGroupData.new(name, metadata))
+ end
+
+ def control_example_success(control_group_name, example_data)
+ control_group = ordered_control_groups[control_group_name]
+ control_group.example_success(example_data)
+ end
+
+ def control_example_failure(control_group_name, example_data, error)
+ control_group = ordered_control_groups[control_group_name]
+ control_group.example_failure(example_data, error.message)
+ end
+
+ # If @audit_enabled is nil or true, we want to run audits
+ def auditing_enabled?
+ Chef::Config[:audit_mode] != :disabled
+ end
+
+ private
+
+ def post_auditing_data(error = nil)
+ unless auditing_enabled?
+ Chef::Log.debug("Audit Reports are disabled. Skipping sending reports.")
+ return
+ end
+
+ unless run_status
+ Chef::Log.debug("Run failed before audits were initialized, not sending audit report to server")
+ return
+ end
+
+ audit_data.start_time = iso8601ify(run_status.start_time)
+ audit_data.end_time = iso8601ify(run_status.end_time)
+
+ audit_history_url = "controls"
+ Chef::Log.debug("Sending audit report (run-id: #{audit_data.run_id})")
+ run_data = audit_data.to_hash
+
+ if error
+ run_data[:error] = "#{error.class.to_s}: #{error.message}\n#{error.backtrace.join("\n")}"
+ end
+
+ Chef::Log.debug "Audit Report:\n#{Chef::JSONCompat.to_json_pretty(run_data)}"
+ # Since we're posting compressed data we can not directly call post_rest which expects JSON
+ begin
+ audit_url = rest_client.create_url(audit_history_url)
+ rest_client.post(audit_url, run_data, headers)
+ rescue StandardError => e
+ if e.respond_to? :response
+ # 404 error code is OK. This means the version of server we're running against doesn't support
+ # audit reporting. Don't alarm failure in this case.
+ if e.response.code == "404"
+ Chef::Log.debug("Server doesn't support audit reporting. Skipping report.")
+ return
+ else
+ # Save the audit report to local disk
+ error_file = "failed-audit-data.json"
+ Chef::FileCache.store(error_file, Chef::JSONCompat.to_json_pretty(run_data), 0640)
+ Chef::Log.error("Failed to post audit report to server. Saving report to #{Chef::FileCache.load(error_file, false)}")
+ end
+ else
+ Chef::Log.error("Failed to post audit report to server (#{e})")
+ end
+
+ if Chef::Config[:enable_reporting_url_fatals]
+ Chef::Log.error("Reporting fatals enabled. Aborting run.")
+ raise
+ end
+ end
+ end
+
+ def headers(additional_headers = {})
+ options = {'X-Ops-Audit-Report-Protocol-Version' => PROTOCOL_VERSION}
+ options.merge(additional_headers)
+ end
+
+ def encode_gzip(data)
+ "".tap do |out|
+ Zlib::GzipWriter.wrap(StringIO.new(out)){|gz| gz << data }
+ end
+ end
+
+ def iso8601ify(time)
+ time.utc.iso8601.to_s
+ end
+
+ end
+ end
+end
diff --git a/lib/chef/audit/control_group_data.rb b/lib/chef/audit/control_group_data.rb
new file mode 100644
index 0000000000..204d7f8070
--- /dev/null
+++ b/lib/chef/audit/control_group_data.rb
@@ -0,0 +1,140 @@
+#
+# Author:: Tyler Ball (<tball@chef.io>)
+#
+# 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 'securerandom'
+
+class Chef
+ class Audit
+ class AuditData
+ attr_reader :node_name, :run_id, :control_groups
+ attr_accessor :start_time, :end_time
+
+ def initialize(node_name, run_id)
+ @node_name = node_name
+ @run_id = run_id
+ @control_groups = []
+ end
+
+ def add_control_group(control_group)
+ control_groups << control_group
+ end
+
+ def to_hash
+ {
+ :node_name => node_name,
+ :run_id => run_id,
+ :start_time => start_time,
+ :end_time => end_time,
+ :control_groups => control_groups.collect { |c| c.to_hash }
+ }
+ end
+ end
+
+ class ControlGroupData
+ attr_reader :name, :status, :number_succeeded, :number_failed, :controls, :metadata
+
+ def initialize(name, metadata={})
+ @status = "success"
+ @controls = []
+ @number_succeeded = 0
+ @number_failed = 0
+ @name = name
+ @metadata = metadata
+ end
+
+
+ def example_success(control_data)
+ @number_succeeded += 1
+ control = create_control(control_data)
+ control.status = "success"
+ controls << control
+ control
+ end
+
+ def example_failure(control_data, details)
+ @number_failed += 1
+ @status = "failure"
+ control = create_control(control_data)
+ control.details = details if details
+ control.status = "failure"
+ controls << control
+ control
+ end
+
+ def to_hash
+ # We sort it so the examples appear in the output in the same order
+ # they appeared in the recipe
+ controls.sort! {|x,y| x.line_number <=> y.line_number}
+ h = {
+ :name => name,
+ :status => status,
+ :number_succeeded => number_succeeded,
+ :number_failed => number_failed,
+ :controls => controls.collect { |c| c.to_hash }
+ }
+ # If there is a duplicate key, metadata will overwrite it
+ add_display_only_data(h).merge(metadata)
+ end
+
+ private
+
+ def create_control(control_data)
+ ControlData.new(control_data)
+ end
+
+ # The id and control sequence number are ephemeral data - they are not needed
+ # to be persisted and can be regenerated at will. They are only needed
+ # for display purposes.
+ def add_display_only_data(group)
+ group[:id] = SecureRandom.uuid
+ group[:controls].collect!.with_index do |c, i|
+ # i is zero-indexed, and we want the display one-indexed
+ c[:sequence_number] = i+1
+ c
+ end
+ group
+ end
+
+ end
+
+ class ControlData
+ attr_reader :name, :resource_type, :resource_name, :context, :line_number
+ attr_accessor :status, :details
+
+ def initialize(control_data={})
+ control_data.each do |k, v|
+ self.instance_variable_set("@#{k}", v)
+ end
+ end
+
+ def to_hash
+ h = {
+ :name => name,
+ :status => status,
+ :details => details,
+ :resource_type => resource_type,
+ :resource_name => resource_name
+ }
+ h[:context] = context || []
+ h
+ end
+ end
+
+ end
+end
diff --git a/lib/chef/audit/rspec_formatter.rb b/lib/chef/audit/rspec_formatter.rb
new file mode 100644
index 0000000000..074a11bed3
--- /dev/null
+++ b/lib/chef/audit/rspec_formatter.rb
@@ -0,0 +1,37 @@
+#
+# Author:: Serdar Sutay (<serdar@chef.io>)
+# 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 'rspec/core'
+
+class Chef
+ class Audit
+ class RspecFormatter < RSpec::Core::Formatters::DocumentationFormatter
+ RSpec::Core::Formatters.register self, :close
+
+ # @api public
+ #
+ # Invoked at the very end, `close` allows the formatter to clean
+ # up resources, e.g. open streams, etc.
+ #
+ # @param _notification [NullNotification] (Ignored)
+ def close(_notification)
+ # Normally Rspec closes the streams it's given. We don't want it for Chef.
+ end
+ end
+ end
+end
diff --git a/lib/chef/audit/runner.rb b/lib/chef/audit/runner.rb
new file mode 100644
index 0000000000..7ef17a4301
--- /dev/null
+++ b/lib/chef/audit/runner.rb
@@ -0,0 +1,178 @@
+#
+# Author:: Claire McQuin (<claire@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.
+#
+
+class Chef
+ class Audit
+ class Runner
+
+ attr_reader :run_context
+ private :run_context
+
+ def initialize(run_context)
+ @run_context = run_context
+ end
+
+ def run
+ setup
+ register_controls
+ do_run
+ end
+
+ def failed?
+ RSpec.world.reporter.failed_examples.size > 0
+ end
+
+ def num_failed
+ RSpec.world.reporter.failed_examples.size
+ end
+
+ def num_total
+ RSpec.world.reporter.examples.size
+ end
+
+ private
+ # Prepare to run audits:
+ # - Require files
+ # - Configure RSpec
+ # - Configure Specinfra/Serverspec
+ def setup
+ require_deps
+ configure_rspec
+ configure_specinfra
+ end
+
+ # RSpec uses a global configuration object, RSpec.configuration. We found
+ # there was interference between the configuration for audit-mode and
+ # the configuration for our own spec tests in these cases:
+ # 1. Specinfra and Serverspec modify RSpec.configuration when loading.
+ # 2. Setting output/error streams.
+ # 3. Adding formatters.
+ # 4. Defining example group aliases.
+ #
+ # Moreover, Serverspec loads its DSL methods into the global namespace,
+ # which causes conflicts with the Chef namespace for resources and packages.
+ #
+ # We wait until we're in the audit-phase of the chef-client run to load
+ # these files. This helps with the namespacing problems we saw, and
+ # prevents Specinfra and Serverspec from modifying the RSpec configuration
+ # used by our spec tests.
+ def require_deps
+ require 'rspec'
+ require 'rspec/its'
+ require 'specinfra'
+ require 'serverspec/helper'
+ require 'serverspec/matcher'
+ require 'serverspec/subject'
+ require 'chef/audit/audit_event_proxy'
+ require 'chef/audit/rspec_formatter'
+ end
+
+ # Configure RSpec just the way we like it:
+ # - Set location of error and output streams
+ # - Add custom audit-mode formatters
+ # - Explicitly disable :should syntax
+ # - Set :color option according to chef config
+ # - Disable exposure of global DSL
+ def configure_rspec
+ set_streams
+ add_formatters
+ disable_should_syntax
+
+ RSpec.configure do |c|
+ c.color = Chef::Config[:color]
+ c.expose_dsl_globally = false
+ end
+ end
+
+ # Set the error and output streams which audit-mode will use to report
+ # human-readable audit information.
+ #
+ # This should always be called before #add_formatters. RSpec won't allow
+ # the output stream to be changed for a formatter once the formatter has
+ # been added.
+ def set_streams
+ RSpec.configuration.output_stream = Chef::Config[:log_location]
+ RSpec.configuration.error_stream = Chef::Config[:log_location]
+ end
+
+ # Add formatters which we use to
+ # 1. Output human-readable data to the output stream,
+ # 2. Collect JSON data to send back to the analytics server.
+ def add_formatters
+ RSpec.configuration.add_formatter(Chef::Audit::AuditEventProxy)
+ RSpec.configuration.add_formatter(Chef::Audit::RspecFormatter)
+ Chef::Audit::AuditEventProxy.events = run_context.events
+ end
+
+ # Audit-mode uses RSpec 3. :should syntax is deprecated by default in
+ # RSpec 3, so we explicitly disable it here.
+ #
+ # This can be removed once :should is removed from RSpec.
+ def disable_should_syntax
+ RSpec.configure do |config|
+ config.expect_with :rspec do |c|
+ c.syntax = :expect
+ end
+ end
+ end
+
+ # Set up the backend for Specinfra/Serverspec. :exec is the local system.
+ def configure_specinfra
+ Specinfra.configuration.backend = :exec
+ end
+
+ # Iterates through the controls registered to this run_context, builds an
+ # example group (RSpec::Core::ExampleGroup) object per controls, and
+ # registers the group with the RSpec.world.
+ #
+ # We could just store an array of example groups and not use RSpec.world,
+ # but it may be useful later if we decide to apply our own ordering scheme
+ # or use example group filters.
+ def register_controls
+ add_example_group_methods
+ run_context.audits.each do |name, group|
+ ctl_grp = RSpec::Core::ExampleGroup.__controls__(*group.args, &group.block)
+ RSpec.world.register(ctl_grp)
+ end
+ end
+
+ # Add example group method aliases to RSpec.
+ #
+ # __controls__: Used internally to create example groups from the controls
+ # saved in the run_context.
+ # control: Used within the context of a controls block, like RSpec's
+ # describe or context.
+ def add_example_group_methods
+ RSpec::Core::ExampleGroup.define_example_group_method :__controls__
+ RSpec::Core::ExampleGroup.define_example_group_method :control
+ end
+
+ # Run the audits!
+ def do_run
+ # RSpec::Core::Runner wants to be initialized with an
+ # RSpec::Core::ConfigurationOptions object, which is used to process
+ # command line configuration arguments. We directly fiddle with the
+ # internal RSpec configuration object, so we give nil here and let
+ # RSpec pick up its own configuration and world.
+ runner = RSpec::Core::Runner.new(nil)
+ runner.run_specs(RSpec.world.ordered_example_groups)
+ end
+
+ end
+ end
+end
diff --git a/lib/chef/chef_fs/file_system/chef_repository_file_system_entry.rb b/lib/chef/chef_fs/file_system/chef_repository_file_system_entry.rb
index 9acfe4b936..0b14750744 100644
--- a/lib/chef/chef_fs/file_system/chef_repository_file_system_entry.rb
+++ b/lib/chef/chef_fs/file_system/chef_repository_file_system_entry.rb
@@ -64,7 +64,7 @@ class Chef
end
def minimize(file_contents, entry)
- object = Chef::JSONCompat.from_json(file_contents)
+ object = Chef::JSONCompat.parse(file_contents)
object = data_handler.normalize(object, entry)
object = data_handler.minimize(object, entry)
Chef::JSONCompat.to_json_pretty(object)
diff --git a/lib/chef/client.rb b/lib/chef/client.rb
index 4f37bd0ee3..77f63671d7 100644
--- a/lib/chef/client.rb
+++ b/lib/chef/client.rb
@@ -25,6 +25,7 @@ require 'chef/log'
require 'chef/rest'
require 'chef/api_client'
require 'chef/api_client/registration'
+require 'chef/audit/runner'
require 'chef/node'
require 'chef/role'
require 'chef/file_cache'
@@ -43,6 +44,7 @@ require 'chef/formatters/doc'
require 'chef/formatters/minimal'
require 'chef/version'
require 'chef/resource_reporter'
+require 'chef/audit/audit_reporter'
require 'chef/run_lock'
require 'chef/policy_builder'
require 'chef/request_id'
@@ -209,6 +211,17 @@ class Chef
end
end
+ # Resource repoters send event information back to the chef server for processing.
+ # Can only be called after we have a @rest object
+ def register_reporters
+ [
+ Chef::ResourceReporter.new(rest),
+ Chef::Audit::AuditReporter.new(rest)
+ ].each do |r|
+ events.register(r)
+ end
+ end
+
# Instantiates a Chef::Node object, possibly loading the node's prior state
# when using chef-client. Delegates to policy_builder
#
@@ -246,7 +259,6 @@ class Chef
@policy_builder ||= Chef::PolicyBuilder.strategy.new(node_name, ohai.data, json_attribs, @override_runlist, events)
end
-
def save_updated_node
if Chef::Config[:solo]
# nothing to do
@@ -260,6 +272,7 @@ class Chef
def run_ohai
ohai.all_plugins
+ @events.ohai_completed(node)
end
def node_name
@@ -295,8 +308,7 @@ class Chef
end
# We now have the client key, and should use it from now on.
@rest = Chef::REST.new(config[:chef_server_url], client_name, config[:client_key])
- @resource_reporter = Chef::ResourceReporter.new(@rest)
- @events.register(@resource_reporter)
+ register_reporters
rescue Exception => e
# TODO: munge exception so a semantic failure message can be given to the
# user
@@ -307,18 +319,56 @@ class Chef
# Converges the node.
#
# === Returns
- # true:: Always returns true
+ # The thrown exception, if there was one. If this returns nil the converge was successful.
def converge(run_context)
- @events.converge_start(run_context)
- Chef::Log.debug("Converging node #{node_name}")
- @runner = Chef::Runner.new(run_context)
- runner.converge
- @events.converge_complete
- true
- rescue Exception
- # TODO: should this be a separate #converge_failed(exception) method?
- @events.converge_complete
- raise
+ converge_exception = nil
+ catch(:end_client_run_early) do
+ begin
+ @events.converge_start(run_context)
+ Chef::Log.debug("Converging node #{node_name}")
+ @runner = Chef::Runner.new(run_context)
+ runner.converge
+ @events.converge_complete
+ rescue Exception => e
+ Chef::Log.error("Converge failed with error message #{e.message}")
+ @events.converge_failed(e)
+ converge_exception = e
+ end
+ end
+ converge_exception
+ end
+
+ # We don't want to change the old API on the `converge` method to have it perform
+ # saving. So we wrap it in this method.
+ def converge_and_save(run_context)
+ converge_exception = converge(run_context)
+ unless converge_exception
+ begin
+ save_updated_node
+ rescue Exception => e
+ converge_exception = e
+ end
+ end
+ converge_exception
+ end
+
+ def run_audits(run_context)
+ audit_exception = nil
+ begin
+ @events.audit_phase_start(run_status)
+ Chef::Log.info("Starting audit phase")
+ auditor = Chef::Audit::Runner.new(run_context)
+ auditor.run
+ if auditor.failed?
+ raise Chef::Exceptions::AuditsFailed.new(auditor.num_failed, auditor.num_total)
+ end
+ @events.audit_phase_complete
+ rescue Exception => e
+ Chef::Log.error("Audit phase failed with error message: #{e.message}")
+ @events.audit_phase_failed(e)
+ audit_exception = e
+ end
+ audit_exception
end
# Expands the run list. Delegates to the policy_builder.
@@ -333,7 +383,6 @@ class Chef
policy_builder.expand_run_list
end
-
def do_windows_admin_check
if Chef::Platform.windows?
Chef::Log.debug("Checking for administrator privileges....")
@@ -380,7 +429,7 @@ class Chef
Chef::Log.debug("Chef-client request_id: #{request_id}")
enforce_path_sanity
run_ohai
- @events.ohai_completed(node)
+
register unless Chef::Config[:solo]
load_node
@@ -396,11 +445,22 @@ class Chef
run_context = setup_run_context
- catch(:end_client_run_early) do
- converge(run_context)
+ if Chef::Config[:audit_mode] != :audit_only
+ converge_error = converge_and_save(run_context)
end
- save_updated_node
+ if Chef::Config[:why_run] == true
+ # why_run should probably be renamed to why_converge
+ Chef::Log.debug("Not running audits in 'why_run' mode - this mode is used to see potential converge changes")
+ elsif Chef::Config[:audit_mode] != :disabled
+ audit_error = run_audits(run_context)
+ end
+
+ if converge_error || audit_error
+ e = Chef::Exceptions::RunFailedWrappingError.new(converge_error, audit_error)
+ e.fill_backtrace
+ raise e
+ end
run_status.stop_clock
Chef::Log.info("Chef Run complete in #{run_status.elapsed_time} seconds")
@@ -411,6 +471,7 @@ class Chef
Chef::Platform::Rebooter.reboot_if_needed!(node)
true
+
rescue Exception => e
# CHEF-3336: Send the error first in case something goes wrong below and we don't know why
Chef::Log.debug("Re-raising exception: #{e.class} - #{e.message}\n#{e.backtrace.join("\n ")}")
diff --git a/lib/chef/config.rb b/lib/chef/config.rb
index 4b83a0eca3..453a8f83da 100644
--- a/lib/chef/config.rb
+++ b/lib/chef/config.rb
@@ -320,6 +320,14 @@ class Chef
default :ez, false
default :enable_reporting, true
default :enable_reporting_url_fatals, false
+ # Possible values for :audit_mode
+ # :enabled, :disabled, :audit_only,
+ #
+ # TODO: 11 Dec 2014: Currently audit-mode is an experimental feature
+ # and is disabled by default. When users choose to enable audit-mode,
+ # a warning is issued in application/client#reconfigure.
+ # This can be removed when audit-mode is enabled by default.
+ default :audit_mode, :disabled
# Policyfile is an experimental feature where a node gets its run list and
# cookbook version set from a single document on the server instead of
diff --git a/lib/chef/digester.rb b/lib/chef/digester.rb
index 0805bccee3..75c4e76859 100644
--- a/lib/chef/digester.rb
+++ b/lib/chef/digester.rb
@@ -19,6 +19,7 @@
#
require 'openssl'
+require 'singleton'
class Chef
class Digester
diff --git a/lib/chef/dsl/audit.rb b/lib/chef/dsl/audit.rb
new file mode 100644
index 0000000000..022bbcce01
--- /dev/null
+++ b/lib/chef/dsl/audit.rb
@@ -0,0 +1,51 @@
+#
+# Author:: Tyler Ball (<tball@getchef.com>)
+# Copyright:: Copyright (c) 2014 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'chef/exceptions'
+
+class Chef
+ module DSL
+ module Audit
+
+ # Can encompass tests in a `control` block or `describe` block
+ # Adds the controls group and block (containing controls to execute) to the runner's list of pending examples
+ def controls(*args, &block)
+ raise Chef::Exceptions::NoAuditsProvided unless block
+
+ name = args[0]
+ if name.nil? || name.empty?
+ raise Chef::Exceptions::AuditNameMissing
+ elsif run_context.audits.has_key?(name)
+ raise Chef::Exceptions::AuditControlGroupDuplicate.new(name)
+ end
+
+ # This DSL will only work in the Recipe class because that exposes the cookbook_name
+ cookbook_name = self.cookbook_name
+ metadata = {
+ cookbook_name: cookbook_name,
+ cookbook_version: self.run_context.cookbook_collection[cookbook_name].version,
+ recipe_name: self.recipe_name,
+ line_number: block.source_location[1]
+ }
+
+ run_context.audits[name] = Struct.new(:args, :block, :metadata).new(args, block, metadata)
+ end
+
+ end
+ end
+end
diff --git a/lib/chef/dsl/recipe.rb b/lib/chef/dsl/recipe.rb
index 120497d56e..d70266c14c 100644
--- a/lib/chef/dsl/recipe.rb
+++ b/lib/chef/dsl/recipe.rb
@@ -81,7 +81,7 @@ class Chef
resource = build_resource(type, name, created_at, &resource_attrs_block)
- run_context.resource_collection.insert(resource, resource_type:type, instance_name:name)
+ run_context.resource_collection.insert(resource, resource_type: type, instance_name: name)
resource
end
@@ -101,6 +101,7 @@ class Chef
resource = resource_class.new(name, run_context)
resource.source_line = created_at
+ resource.declared_type = type
# If we have a resource like this one, we want to steal its state
# This behavior is very counter-intuitive and should be removed.
# See CHEF-3694, https://tickets.opscode.com/browse/CHEF-3694
diff --git a/lib/chef/event_dispatch/base.rb b/lib/chef/event_dispatch/base.rb
index 50d261cecd..25dd9fd1b2 100644
--- a/lib/chef/event_dispatch/base.rb
+++ b/lib/chef/event_dispatch/base.rb
@@ -225,6 +225,41 @@ class Chef
def converge_complete
end
+ # Called if the converge phase fails
+ def converge_failed(exception)
+ end
+
+ ##################################
+ # Audit Mode Events
+ # This phase is currently experimental and these event APIs are subject to change
+ ##################################
+
+ # Called before audit phase starts
+ def audit_phase_start(run_status)
+ end
+
+ # Called when audit phase successfully finishes
+ def audit_phase_complete
+ end
+
+ # Called if there is an uncaught exception during the audit phase. The audit runner should
+ # be catching and handling errors from the examples, so this is only uncaught errors (like
+ # bugs in our handling code)
+ def audit_phase_failed(exception)
+ end
+
+ # Signifies the start of a `controls` block with a defined name
+ def control_group_started(name)
+ end
+
+ # An example in a `controls` block completed successfully
+ def control_example_success(control_group_name, example_data)
+ end
+
+ # An example in a `controls` block failed with the provided error
+ def control_example_failure(control_group_name, example_data, error)
+ end
+
# TODO: need events for notification resolve?
# def notifications_resolved
# end
diff --git a/lib/chef/exceptions.rb b/lib/chef/exceptions.rb
index bf33cb9b52..b949e7b975 100644
--- a/lib/chef/exceptions.rb
+++ b/lib/chef/exceptions.rb
@@ -124,6 +124,7 @@ class Chef
class DuplicateDataBagItem < RuntimeError; end
class PowershellCmdletException < RuntimeError; end
+ class LCMParser < RuntimeError; end
class CannotDetermineHomebrewOwner < Package; end
@@ -150,6 +151,11 @@ class Chef
class IllegalVersionConstraint < NotImplementedError; end
class MetadataNotValid < StandardError; end
+ class MetadataNotFound < StandardError
+ def initialize
+ super "No metadata.rb or metadata.json!"
+ end
+ end
# File operation attempted but no permissions to perform it
class InsufficientPermissions < RuntimeError; end
@@ -379,5 +385,45 @@ class Chef
super "Found more than one provider for #{resource.resource_name} resource: #{classes}"
end
end
+
+ class AuditControlGroupDuplicate < RuntimeError
+ def initialize(name)
+ super "Audit control group with name '#{name}' has already been defined"
+ end
+ end
+ class AuditNameMissing < RuntimeError; end
+ class NoAuditsProvided < RuntimeError
+ def initialize
+ super "You must provide a block with audits"
+ end
+ end
+ class AuditsFailed < RuntimeError
+ def initialize(num_failed, num_total)
+ super "Audit phase found failures - #{num_failed}/#{num_total} audits failed"
+ end
+ end
+
+ # If a converge or audit fails, we want to wrap the output from those errors into 1 error so we can
+ # see both issues in the output. It is possible that nil will be provided. You must call `fill_backtrace`
+ # to correctly populate the backtrace with the wrapped backtraces.
+ class RunFailedWrappingError < RuntimeError
+ attr_reader :wrapped_errors
+ def initialize(*errors)
+ errors = errors.select {|e| !e.nil?}
+ output = "Found #{errors.size} errors, they are stored in the backtrace"
+ @wrapped_errors = errors
+ super output
+ end
+
+ def fill_backtrace
+ backtrace = []
+ wrapped_errors.each_with_index do |e,i|
+ backtrace << "#{i+1}) #{e.class} - #{e.message}"
+ backtrace += e.backtrace if e.backtrace
+ backtrace << ""
+ end
+ set_backtrace(backtrace)
+ end
+ end
end
end
diff --git a/lib/chef/formatters/doc.rb b/lib/chef/formatters/doc.rb
index 4a08b9d095..489888db8f 100644
--- a/lib/chef/formatters/doc.rb
+++ b/lib/chef/formatters/doc.rb
@@ -8,7 +8,9 @@ class Chef
# "specdoc"
class Doc < Formatters::Base
- attr_reader :start_time, :end_time
+ attr_reader :start_time, :end_time, :successful_audits, :failed_audits
+ private :successful_audits, :failed_audits
+
cli_name(:doc)
def initialize(out, err)
@@ -16,6 +18,8 @@ class Chef
@updated_resources = 0
@up_to_date_resources = 0
+ @successful_audits = 0
+ @failed_audits = 0
@start_time = Time.now
@end_time = @start_time
end
@@ -32,12 +36,19 @@ class Chef
@up_to_date_resources + @updated_resources
end
+ def total_audits
+ successful_audits + failed_audits
+ end
+
def run_completed(node)
@end_time = Time.now
if Chef::Config[:why_run]
puts_line "Chef Client finished, #{@updated_resources}/#{total_resources} resources would have been updated"
else
puts_line "Chef Client finished, #{@updated_resources}/#{total_resources} resources updated in #{elapsed_time} seconds"
+ if total_audits > 0
+ puts_line " #{successful_audits}/#{total_audits} Audits succeeded"
+ end
end
end
@@ -47,6 +58,9 @@ class Chef
puts_line "Chef Client failed. #{@updated_resources} resources would have been updated"
else
puts_line "Chef Client failed. #{@updated_resources} resources updated in #{elapsed_time} seconds"
+ if total_audits > 0
+ puts_line " #{successful_audits} Audits succeeded"
+ end
end
end
@@ -151,6 +165,38 @@ class Chef
unindent if @current_recipe
end
+ def converge_failed(e)
+ # Currently a failed converge is handled the same way as a successful converge
+ converge_complete
+ end
+
+ # Called before audit phase starts
+ def audit_phase_start(run_status)
+ puts_line "Starting audit phase"
+ end
+
+ def audit_phase_complete
+ puts_line "Auditing complete"
+ end
+
+ def audit_phase_failed(error)
+ puts_line ""
+ puts_line "Audit phase exception:"
+ indent
+ puts_line "#{error.message}"
+ error.backtrace.each do |l|
+ puts_line l
+ end
+ end
+
+ def control_example_success(control_group_name, example_data)
+ @successful_audits += 1
+ end
+
+ def control_example_failure(control_group_name, example_data, error)
+ @failed_audits += 1
+ end
+
# Called before action is executed on a resource.
def resource_action_start(resource, action, notification_type=nil, notifier=nil)
if resource.cookbook_name && resource.recipe_name
diff --git a/lib/chef/http.rb b/lib/chef/http.rb
index 8d00a38dc1..5e52337aff 100644
--- a/lib/chef/http.rb
+++ b/lib/chef/http.rb
@@ -203,7 +203,7 @@ class Chef
def create_url(path)
return path if path.is_a?(URI)
- if path =~ /^(http|https):\/\//
+ if path =~ /^(http|https):\/\//i
URI.parse(path)
elsif path.nil? or path.empty?
URI.parse(@url)
diff --git a/lib/chef/knife.rb b/lib/chef/knife.rb
index 3f234d7ce3..51ccb99955 100644
--- a/lib/chef/knife.rb
+++ b/lib/chef/knife.rb
@@ -428,6 +428,13 @@ class Chef
raise # make sure exit passes through.
when Net::HTTPServerException, Net::HTTPFatalError
humanize_http_exception(e)
+ when OpenSSL::SSL::SSLError
+ ui.error "Could not establish a secure connection to the server."
+ ui.info "Use `knife ssl check` to troubleshoot your SSL configuration."
+ ui.info "If your Chef Server uses a self-signed certificate, you can use"
+ ui.info "`knife ssl fetch` to make knife trust the server's certificates."
+ ui.info ""
+ ui.info "Original Exception: #{e.class.name}: #{e.message}"
when Errno::ECONNREFUSED, Timeout::Error, Errno::ETIMEDOUT, SocketError
ui.error "Network Error: #{e.message}"
ui.info "Check your knife configuration and network settings"
diff --git a/lib/chef/knife/cookbook_site_install.rb b/lib/chef/knife/cookbook_site_install.rb
index 913d171b3c..edf8dd14f0 100644
--- a/lib/chef/knife/cookbook_site_install.rb
+++ b/lib/chef/knife/cookbook_site_install.rb
@@ -17,11 +17,11 @@
#
require 'chef/knife'
+require 'chef/exceptions'
require 'shellwords'
class Chef
class Knife
-
class CookbookSiteInstall < Knife
deps do
@@ -107,11 +107,8 @@ class Chef
end
end
-
unless config[:no_deps]
- md = Chef::Cookbook::Metadata.new
- md.from_file(File.join(@install_path, @cookbook_name, "metadata.rb"))
- md.dependencies.each do |cookbook, version_list|
+ preferred_metadata.dependencies.each do |cookbook, version_list|
# Doesn't do versions.. yet
nv = self.class.new
nv.config = config
@@ -144,6 +141,7 @@ class Chef
def extract_cookbook(upstream_file, version)
ui.info("Uncompressing #{@cookbook_name} version #{version}.")
+ # FIXME: Detect if we have the bad tar from git on Windows: https://github.com/opscode/chef/issues/1753
shell_out!("tar zxvf #{convert_path upstream_file}", :cwd => @install_path)
end
@@ -153,11 +151,37 @@ class Chef
end
def convert_path(upstream_file)
- if ENV['MSYSTEM'] == 'MINGW32'
- return upstream_file.sub(/^([[:alpha:]]):/, '/\1')
- else
- return Shellwords.escape upstream_file
- end
+ # converts a Windows path (C:\foo) to a mingw path (/c/foo)
+ if ENV['MSYSTEM'] == 'MINGW32'
+ return upstream_file.sub(/^([[:alpha:]]):/, '/\1')
+ else
+ return Shellwords.escape upstream_file
+ end
+ end
+
+ # Get the preferred metadata path on disk. Chef prefers the metadata.rb
+ # over the metadata.json.
+ #
+ # @raise if there is no metadata in the cookbook
+ #
+ # @return [Chef::Cookbook::Metadata]
+ def preferred_metadata
+ md = Chef::Cookbook::Metadata.new
+
+ rb = File.join(@install_path, @cookbook_name, "metadata.rb")
+ if File.exist?(rb)
+ md.from_file(rb)
+ return md
+ end
+
+ json = File.join(@install_path, @cookbook_name, "metadata.json")
+ if File.exist?(json)
+ json = IO.read(json)
+ md.from_json(json)
+ return md
+ end
+
+ raise Chef::Exceptions::MetadataNotFound
end
end
end
diff --git a/lib/chef/knife/search.rb b/lib/chef/knife/search.rb
index 34d12168b6..caca99b4d8 100644
--- a/lib/chef/knife/search.rb
+++ b/lib/chef/knife/search.rb
@@ -53,7 +53,7 @@ class Chef
:short => "-R INT",
:long => "--rows INT",
:description => "The number of rows to return",
- :default => 1000,
+ :default => nil,
:proc => lambda { |i| i.to_i }
option :run_list,
@@ -92,9 +92,9 @@ class Chef
result_count = 0
search_args = Hash.new
- search_args[:sort] = config[:sort]
- search_args[:start] = config[:start]
- search_args[:rows] = config[:rows]
+ search_args[:sort] = config[:sort] if config[:sort]
+ search_args[:start] = config[:start] if config[:start]
+ search_args[:rows] = config[:rows] if config[:rows]
if config[:filter_result]
search_args[:filter_result] = create_result_filter(config[:filter_result])
elsif (not ui.config[:attribute].nil?) && (not ui.config[:attribute].empty?)
diff --git a/lib/chef/knife/ssl_fetch.rb b/lib/chef/knife/ssl_fetch.rb
index 5626a5610d..745aca5786 100644
--- a/lib/chef/knife/ssl_fetch.rb
+++ b/lib/chef/knife/ssl_fetch.rb
@@ -136,6 +136,19 @@ TRUST_TRUST
remote_cert_chain.each do |cert|
write_cert(cert)
end
+ rescue OpenSSL::SSL::SSLError => e
+ # 'unknown protocol' usually means you tried to connect to a non-ssl
+ # service. We handle that specially here, any other error we let bubble
+ # up (probably a bug of some sort).
+ raise unless e.message.include?("unknown protocol")
+
+ ui.error("The service at the given URI (#{uri}) does not accept SSL connections")
+
+ if uri.scheme == "http"
+ https_uri = uri.to_s.sub(/^http/, 'https')
+ ui.error("Perhaps you meant to connect to '#{https_uri}'?")
+ end
+ exit 1
end
diff --git a/lib/chef/monologger.rb b/lib/chef/monologger.rb
index 464b21bdd3..f7d226f82e 100644
--- a/lib/chef/monologger.rb
+++ b/lib/chef/monologger.rb
@@ -1,5 +1,4 @@
require 'logger'
-
require 'pp'
#== MonoLogger
@@ -89,4 +88,3 @@ class MonoLogger < Logger
end
-
diff --git a/lib/chef/provider/link.rb b/lib/chef/provider/link.rb
index 417d6a21b0..c811c13cdf 100644
--- a/lib/chef/provider/link.rb
+++ b/lib/chef/provider/link.rb
@@ -86,7 +86,7 @@ class Chef
end
def canonicalize(path)
- Chef::Util::PathHelper.canonical_path(path)
+ Chef::Platform.windows? ? path.gsub('/', '\\') : path
end
def action_create
diff --git a/lib/chef/provider/package/apt.rb b/lib/chef/provider/package/apt.rb
index eb2c038eaa..fd132c817c 100644
--- a/lib/chef/provider/package/apt.rb
+++ b/lib/chef/provider/package/apt.rb
@@ -46,8 +46,8 @@ class Chef
end
def default_release_options
- # Use apt::Default-Release option only if provider was explicitly defined
- "-o APT::Default-Release=#{@new_resource.default_release}" if @new_resource.provider && @new_resource.default_release
+ # Use apt::Default-Release option only if provider supports it
+ "-o APT::Default-Release=#{@new_resource.default_release}" if @new_resource.respond_to?(:default_release) && @new_resource.default_release
end
def check_package_state(package)
diff --git a/lib/chef/provider/package/homebrew.rb b/lib/chef/provider/package/homebrew.rb
index 1b407a1901..e043c01f56 100644
--- a/lib/chef/provider/package/homebrew.rb
+++ b/lib/chef/provider/package/homebrew.rb
@@ -26,7 +26,8 @@ class Chef
class Package
class Homebrew < Chef::Provider::Package
- provides :homebrew_package, os: "mac_os_x"
+ provides :homebrew_package
+ provides :package, os: ["mac_os_x", "darwin"]
include Chef::Mixin::HomebrewUser
diff --git a/lib/chef/provider/package/windows/msi.rb b/lib/chef/provider/package/windows/msi.rb
index cc07909d8e..e43a307cca 100644
--- a/lib/chef/provider/package/windows/msi.rb
+++ b/lib/chef/provider/package/windows/msi.rb
@@ -19,6 +19,7 @@
# TODO: Allow @new_resource.source to be a Product Code as a GUID for uninstall / network install
require 'chef/win32/api/installer' if RUBY_PLATFORM =~ /mswin|mingw32|windows/
+require 'chef/mixin/shell_out'
class Chef
class Provider
@@ -26,6 +27,7 @@ class Chef
class Windows
class MSI
include Chef::ReservedNames::Win32::API::Installer if RUBY_PLATFORM =~ /mswin|mingw32|windows/
+ include Chef::Mixin::ShellOut
def initialize(resource)
@new_resource = resource
diff --git a/lib/chef/provider/powershell_script.rb b/lib/chef/provider/powershell_script.rb
index 0e76cd1656..96955f73b6 100644
--- a/lib/chef/provider/powershell_script.rb
+++ b/lib/chef/provider/powershell_script.rb
@@ -43,7 +43,7 @@ class Chef
code.to_s +
EXIT_STATUS_NORMALIZATION_SCRIPT )
convert_boolean_return = @new_resource.convert_boolean_return
- @code = <<EOH
+ self.code = <<EOH
new-variable -name interpolatedexitcode -visibility private -value $#{convert_boolean_return}
new-variable -name chefscriptresult -visibility private
$chefscriptresult = {
@@ -52,7 +52,7 @@ $chefscriptresult = {
if ($interpolatedexitcode -and $chefscriptresult.gettype().name -eq 'boolean') { exit [int32](!$chefscriptresult) } else { exit 0 }
EOH
Chef::Log.debug("powershell_script provider called with script code:\n\n#{code}\n")
- Chef::Log.debug("powershell_script provider will execute transformed code:\n\n#{@code}\n")
+ Chef::Log.debug("powershell_script provider will execute transformed code:\n\n#{self.code}\n")
end
public
diff --git a/lib/chef/provider/script.rb b/lib/chef/provider/script.rb
index ea286cb0e4..e8b5235b7a 100644
--- a/lib/chef/provider/script.rb
+++ b/lib/chef/provider/script.rb
@@ -32,10 +32,13 @@ class Chef
provides :ruby
provides :script
- def_delegators :@new_resource, :code, :interpreter, :flags
+ def_delegators :@new_resource, :interpreter, :flags
+
+ attr_accessor :code
def initialize(new_resource, run_context)
super
+ self.code = new_resource.code
end
def command
diff --git a/lib/chef/provider/service.rb b/lib/chef/provider/service.rb
index 968f9bff9c..75da2ddb31 100644
--- a/lib/chef/provider/service.rb
+++ b/lib/chef/provider/service.rb
@@ -150,7 +150,7 @@ class Chef
end
def reload_service
- raise Chef::Exceptions::UnsupportedAction, "#{self.to_s} does not support :restart"
+ raise Chef::Exceptions::UnsupportedAction, "#{self.to_s} does not support :reload"
end
protected
diff --git a/lib/chef/provider/service/windows.rb b/lib/chef/provider/service/windows.rb
index d4c272354e..ba53f0a3c3 100644
--- a/lib/chef/provider/service/windows.rb
+++ b/lib/chef/provider/service/windows.rb
@@ -20,6 +20,7 @@
require 'chef/provider/service/simple'
if RUBY_PLATFORM =~ /mswin|mingw32|windows/
+ require 'chef/win32/error'
require 'win32/service'
end
@@ -29,6 +30,7 @@ class Chef::Provider::Service::Windows < Chef::Provider::Service
provides :windows_service, os: "windows"
include Chef::Mixin::ShellOut
+ include Chef::ReservedNames::Win32::API::Error rescue LoadError
#Win32::Service.get_start_type
AUTO_START = 'auto start'
@@ -67,6 +69,22 @@ class Chef::Provider::Service::Windows < Chef::Provider::Service
def start_service
if Win32::Service.exists?(@new_resource.service_name)
+ # reconfiguration is idempotent, so just do it.
+ new_config = {
+ service_name: @new_resource.service_name,
+ service_start_name: @new_resource.run_as_user,
+ password: @new_resource.run_as_password,
+ }.reject { |k,v| v.nil? || v.length == 0 }
+
+ Win32::Service.configure(new_config)
+ Chef::Log.info "#{@new_resource} configured with #{new_config.inspect}"
+
+ # it would be nice to check if the user already has the logon privilege, but that turns out to be
+ # nontrivial.
+ if new_config.has_key?(:service_start_name)
+ grant_service_logon(new_config[:service_start_name])
+ end
+
state = current_state
if state == RUNNING
Chef::Log.debug "#{@new_resource} already started - nothing to do"
@@ -79,7 +97,17 @@ class Chef::Provider::Service::Windows < Chef::Provider::Service
shell_out!(@new_resource.start_command)
else
spawn_command_thread do
- Win32::Service.start(@new_resource.service_name)
+ begin
+ Win32::Service.start(@new_resource.service_name)
+ rescue SystemCallError => ex
+ if ex.errno == ERROR_SERVICE_LOGON_FAILED
+ Chef::Log.error ex.message
+ raise Chef::Exceptions::Service,
+ "Service #{@new_resource} did not start due to a logon failure (error #{ERROR_SERVICE_LOGON_FAILED}): possibly the specified user '#{@new_resource.run_as_user}' does not have the 'log on as a service' privilege, or the password is incorrect."
+ else
+ raise ex
+ end
+ end
end
wait_for_state(RUNNING)
end
@@ -209,6 +237,76 @@ class Chef::Provider::Service::Windows < Chef::Provider::Service
end
private
+ def make_policy_text(username)
+ text = <<-EOS
+[Unicode]
+Unicode=yes
+[Privilege Rights]
+SeServiceLogonRight = \\\\#{canonicalize_username(username)},*S-1-5-80-0
+[Version]
+signature="$CHICAGO$"
+Revision=1
+EOS
+ end
+
+ def grant_logfile_name(username)
+ Chef::Util::PathHelper.canonical_path("#{Dir.tmpdir}/logon_grant-#{clean_username_for_path(username)}-#{$$}.log", prefix=false)
+ end
+
+ def grant_policyfile_name(username)
+ Chef::Util::PathHelper.canonical_path("#{Dir.tmpdir}/service_logon_policy-#{clean_username_for_path(username)}-#{$$}.inf", prefix=false)
+ end
+
+ def grant_dbfile_name(username)
+ "#{ENV['TEMP']}\\secedit.sdb"
+ end
+
+ def grant_service_logon(username)
+ logfile = grant_logfile_name(username)
+ policy_file = ::File.new(grant_policyfile_name(username), 'w')
+ policy_text = make_policy_text(username)
+ dbfile = grant_dbfile_name(username) # this is just an audit file.
+
+ begin
+ Chef::Log.debug "Policy file text:\n#{policy_text}"
+ policy_file.puts(policy_text)
+ policy_file.close # need to flush the buffer.
+
+ # it would be nice to do this with APIs instead, but the LSA_* APIs are
+ # particularly onerous and life is short.
+ cmd = %Q{secedit.exe /configure /db "#{dbfile}" /cfg "#{policy_file.path}" /areas USER_RIGHTS SECURITYPOLICY SERVICES /log "#{logfile}"}
+ Chef::Log.debug "Granting logon-as-service privilege with: #{cmd}"
+ runner = shell_out(cmd)
+
+ if runner.exitstatus != 0
+ Chef::Log.fatal "Logon-as-service grant failed with output: #{runner.stdout}"
+ raise Chef::Exceptions::Service, <<-EOS
+Logon-as-service grant failed with policy file #{policy_file.path}.
+You can look at #{logfile} for details, or do `secedit /analyze #{dbfile}`.
+The failed command was `#{cmd}`.
+EOS
+ end
+
+ Chef::Log.info "Grant logon-as-service to user '#{username}' successful."
+
+ ::File.delete(dbfile) rescue nil
+ ::File.delete(policy_file)
+ ::File.delete(logfile) rescue nil # logfile is not always present at end.
+ end
+ true
+ end
+
+ # remove characters that make for broken or wonky filenames.
+ def clean_username_for_path(username)
+ username.gsub(/[\/\\. ]+/, '_')
+ end
+
+ # the security policy file only seems to accept \\username, so fix .\username or .\\username.
+ # TODO: this probably has to be fixed to handle various valid Windows names correctly.
+ def canonicalize_username(username)
+ username.sub(/^\.?\\+/, '')
+ end
+
def current_state
Win32::Service.status(@new_resource.service_name).current_state
end
diff --git a/lib/chef/provider/subversion.rb b/lib/chef/provider/subversion.rb
index f4a0e6fc13..5f36483c32 100644
--- a/lib/chef/provider/subversion.rb
+++ b/lib/chef/provider/subversion.rb
@@ -62,7 +62,7 @@ class Chef
def action_checkout
if target_dir_non_existent_or_empty?
converge_by("perform checkout of #{@new_resource.repository} into #{@new_resource.destination}") do
- shell_out!(run_options(command: checkout_command))
+ shell_out!(checkout_command, run_options)
end
else
Chef::Log.debug "#{@new_resource} checkout destination #{@new_resource.destination} already exists or is a non-empty directory - nothing to do"
@@ -79,7 +79,7 @@ class Chef
def action_force_export
converge_by("export #{@new_resource.repository} into #{@new_resource.destination}") do
- shell_out!(run_options(command: export_command))
+ shell_out!(export_command, run_options)
end
end
@@ -90,7 +90,7 @@ class Chef
Chef::Log.debug "#{@new_resource} current revision: #{current_rev} target revision: #{revision_int}"
unless current_revision_matches_target_revision?
converge_by("sync #{@new_resource.destination} from #{@new_resource.repository}") do
- shell_out!(run_options(command: sync_command))
+ shell_out!(sync_command, run_options)
Chef::Log.info "#{@new_resource} updated to revision: #{revision_int}"
end
end
diff --git a/lib/chef/recipe.rb b/lib/chef/recipe.rb
index e54a1d98e3..621d93099b 100644
--- a/lib/chef/recipe.rb
+++ b/lib/chef/recipe.rb
@@ -24,6 +24,7 @@ require 'chef/dsl/platform_introspection'
require 'chef/dsl/include_recipe'
require 'chef/dsl/registry_helper'
require 'chef/dsl/reboot_pending'
+require 'chef/dsl/audit'
require 'chef/mixin/from_file'
@@ -40,6 +41,7 @@ class Chef
include Chef::DSL::Recipe
include Chef::DSL::RegistryHelper
include Chef::DSL::RebootPending
+ include Chef::DSL::Audit
include Chef::Mixin::FromFile
include Chef::Mixin::Deprecation
diff --git a/lib/chef/resource.rb b/lib/chef/resource.rb
index 8d964da66d..17f109242f 100644
--- a/lib/chef/resource.rb
+++ b/lib/chef/resource.rb
@@ -29,99 +29,14 @@ require 'chef/resource/conditional_action_not_nothing'
require 'chef/resource_collection'
require 'chef/node_map'
require 'chef/node'
-require 'chef/provider_resolver'
require 'chef/platform'
+require 'chef/resource/resource_notification'
require 'chef/mixin/deprecation'
require 'chef/mixin/descendants_tracker'
class Chef
class Resource
- class Notification < Struct.new(:resource, :action, :notifying_resource)
-
- def duplicates?(other_notification)
- unless other_notification.respond_to?(:resource) && other_notification.respond_to?(:action)
- msg = "only duck-types of Chef::Resource::Notification can be checked for duplication "\
- "you gave #{other_notification.inspect}"
- raise ArgumentError, msg
- end
- other_notification.resource == resource && other_notification.action == action
- end
-
- # If resource and/or notifying_resource is not a resource object, this will look them up in the resource collection
- # and fix the references from strings to actual Resource objects.
- def resolve_resource_reference(resource_collection)
- return resource if resource.kind_of?(Chef::Resource) && notifying_resource.kind_of?(Chef::Resource)
-
- if not(resource.kind_of?(Chef::Resource))
- fix_resource_reference(resource_collection)
- end
-
- if not(notifying_resource.kind_of?(Chef::Resource))
- fix_notifier_reference(resource_collection)
- end
- end
-
- # This will look up the resource if it is not a Resource Object. It will complain if it finds multiple
- # resources, can't find a resource, or gets invalid syntax.
- def fix_resource_reference(resource_collection)
- matching_resource = resource_collection.find(resource)
- if Array(matching_resource).size > 1
- msg = "Notification #{self} from #{notifying_resource} was created with a reference to multiple resources, "\
- "but can only notify one resource. Notifying resource was defined on #{notifying_resource.source_line}"
- raise Chef::Exceptions::InvalidResourceReference, msg
- end
- self.resource = matching_resource
-
- rescue Chef::Exceptions::ResourceNotFound => e
- err = Chef::Exceptions::ResourceNotFound.new(<<-FAIL)
-resource #{notifying_resource} is configured to notify resource #{resource} with action #{action}, \
-but #{resource} cannot be found in the resource collection. #{notifying_resource} is defined in \
-#{notifying_resource.source_line}
-FAIL
- err.set_backtrace(e.backtrace)
- raise err
- rescue Chef::Exceptions::InvalidResourceSpecification => e
- err = Chef::Exceptions::InvalidResourceSpecification.new(<<-F)
-Resource #{notifying_resource} is configured to notify resource #{resource} with action #{action}, \
-but #{resource.inspect} is not valid syntax to look up a resource in the resource collection. Notification \
-is defined near #{notifying_resource.source_line}
-F
- err.set_backtrace(e.backtrace)
- raise err
- end
-
- # This will look up the notifying_resource if it is not a Resource Object. It will complain if it finds multiple
- # resources, can't find a resource, or gets invalid syntax.
- def fix_notifier_reference(resource_collection)
- matching_notifier = resource_collection.find(notifying_resource)
- if Array(matching_notifier).size > 1
- msg = "Notification #{self} from #{notifying_resource} was created with a reference to multiple notifying "\
- "resources, but can only originate from one resource. Destination resource was defined "\
- "on #{resource.source_line}"
- raise Chef::Exceptions::InvalidResourceReference, msg
- end
- self.notifying_resource = matching_notifier
-
- rescue Chef::Exceptions::ResourceNotFound => e
- err = Chef::Exceptions::ResourceNotFound.new(<<-FAIL)
-Resource #{resource} is configured to receive notifications from #{notifying_resource} with action #{action}, \
-but #{notifying_resource} cannot be found in the resource collection. #{resource} is defined in \
-#{resource.source_line}
-FAIL
- err.set_backtrace(e.backtrace)
- raise err
- rescue Chef::Exceptions::InvalidResourceSpecification => e
- err = Chef::Exceptions::InvalidResourceSpecification.new(<<-F)
-Resource #{resource} is configured to receive notifications from #{notifying_resource} with action #{action}, \
-but #{notifying_resource.inspect} is not valid syntax to look up a resource in the resource collection. Notification \
-is defined near #{resource.source_line}
-F
- err.set_backtrace(e.backtrace)
- raise err
- end
-
- end
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]
@@ -212,6 +127,7 @@ F
attr_accessor :source_line
attr_accessor :retries
attr_accessor :retry_delay
+ attr_accessor :declared_type
attr_reader :updated
@@ -299,7 +215,7 @@ F
def load_prior_resource(resource_type, instance_name)
begin
- key = ::Chef::ResourceCollection::ResourceSet.create_key(resource_type, instance_name)
+ key = "#{resource_type}[#{instance_name}]"
prior_resource = run_context.resource_collection.lookup(key)
# if we get here, there is a prior resource (otherwise we'd have jumped
# to the rescue clause).
@@ -426,11 +342,11 @@ F
def notifies(action, resource_spec, timing=:delayed)
# when using old-style resources(:template => "/foo.txt") style, you
# could end up with multiple resources.
+ validate_resource_spec!(resource_spec)
+
resources = [ resource_spec ].flatten
resources.each do |resource|
- validate_resource_spec!(resource_spec)
-
case timing.to_s
when 'delayed'
notifies_delayed(action, resource)
@@ -449,8 +365,12 @@ F
# 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) }
+ 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
def notifies_immediately(action, resource_spec)
@@ -499,6 +419,14 @@ F
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}]"
+ end
+
def to_s
"#{@resource_name}[#{@name}]"
end
@@ -833,3 +761,7 @@ F
end
end
end
+
+# We require this at the BOTTOM of this file to avoid circular requires (it is used
+# at runtime but not load time)
+require 'chef/provider_resolver'
diff --git a/lib/chef/resource/homebrew_package.rb b/lib/chef/resource/homebrew_package.rb
index 952552e3a8..3bd5dc16dc 100644
--- a/lib/chef/resource/homebrew_package.rb
+++ b/lib/chef/resource/homebrew_package.rb
@@ -25,7 +25,8 @@ class Chef
class Resource
class HomebrewPackage < Chef::Resource::Package
- provides :homebrew_package, os: "mac_os_x"
+ provides :homebrew_package
+ provides :package, os: ["mac_os_x", "darwin"]
def initialize(name, run_context=nil)
super
diff --git a/lib/chef/resource/resource_notification.rb b/lib/chef/resource/resource_notification.rb
new file mode 100644
index 0000000000..a27ed961c7
--- /dev/null
+++ b/lib/chef/resource/resource_notification.rb
@@ -0,0 +1,109 @@
+#
+# Author:: Tyler Ball (<tball@chef.io>)
+# 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/resource'
+
+class Chef
+ class Resource
+ class Notification < Struct.new(:resource, :action, :notifying_resource)
+
+ def duplicates?(other_notification)
+ unless other_notification.respond_to?(:resource) && other_notification.respond_to?(:action)
+ msg = "only duck-types of Chef::Resource::Notification can be checked for duplication "\
+ "you gave #{other_notification.inspect}"
+ raise ArgumentError, msg
+ end
+ other_notification.resource == resource && other_notification.action == action
+ end
+
+ # If resource and/or notifying_resource is not a resource object, this will look them up in the resource collection
+ # and fix the references from strings to actual Resource objects.
+ def resolve_resource_reference(resource_collection)
+ return resource if resource.kind_of?(Chef::Resource) && notifying_resource.kind_of?(Chef::Resource)
+
+ if not(resource.kind_of?(Chef::Resource))
+ fix_resource_reference(resource_collection)
+ end
+
+ if not(notifying_resource.kind_of?(Chef::Resource))
+ fix_notifier_reference(resource_collection)
+ end
+ end
+
+ # This will look up the resource if it is not a Resource Object. It will complain if it finds multiple
+ # resources, can't find a resource, or gets invalid syntax.
+ def fix_resource_reference(resource_collection)
+ matching_resource = resource_collection.find(resource)
+ if Array(matching_resource).size > 1
+ msg = "Notification #{self} from #{notifying_resource} was created with a reference to multiple resources, "\
+ "but can only notify one resource. Notifying resource was defined on #{notifying_resource.source_line}"
+ raise Chef::Exceptions::InvalidResourceReference, msg
+ end
+ self.resource = matching_resource
+
+ rescue Chef::Exceptions::ResourceNotFound => e
+ err = Chef::Exceptions::ResourceNotFound.new(<<-FAIL)
+resource #{notifying_resource} is configured to notify resource #{resource} with action #{action}, \
+but #{resource} cannot be found in the resource collection. #{notifying_resource} is defined in \
+#{notifying_resource.source_line}
+ FAIL
+ err.set_backtrace(e.backtrace)
+ raise err
+ rescue Chef::Exceptions::InvalidResourceSpecification => e
+ err = Chef::Exceptions::InvalidResourceSpecification.new(<<-F)
+Resource #{notifying_resource} is configured to notify resource #{resource} with action #{action}, \
+but #{resource.inspect} is not valid syntax to look up a resource in the resource collection. Notification \
+is defined near #{notifying_resource.source_line}
+ F
+ err.set_backtrace(e.backtrace)
+ raise err
+ end
+
+ # This will look up the notifying_resource if it is not a Resource Object. It will complain if it finds multiple
+ # resources, can't find a resource, or gets invalid syntax.
+ def fix_notifier_reference(resource_collection)
+ matching_notifier = resource_collection.find(notifying_resource)
+ if Array(matching_notifier).size > 1
+ msg = "Notification #{self} from #{notifying_resource} was created with a reference to multiple notifying "\
+ "resources, but can only originate from one resource. Destination resource was defined "\
+ "on #{resource.source_line}"
+ raise Chef::Exceptions::InvalidResourceReference, msg
+ end
+ self.notifying_resource = matching_notifier
+
+ rescue Chef::Exceptions::ResourceNotFound => e
+ err = Chef::Exceptions::ResourceNotFound.new(<<-FAIL)
+Resource #{resource} is configured to receive notifications from #{notifying_resource} with action #{action}, \
+but #{notifying_resource} cannot be found in the resource collection. #{resource} is defined in \
+#{resource.source_line}
+ FAIL
+ err.set_backtrace(e.backtrace)
+ raise err
+ rescue Chef::Exceptions::InvalidResourceSpecification => e
+ err = Chef::Exceptions::InvalidResourceSpecification.new(<<-F)
+Resource #{resource} is configured to receive notifications from #{notifying_resource} with action #{action}, \
+but #{notifying_resource.inspect} is not valid syntax to look up a resource in the resource collection. Notification \
+is defined near #{resource.source_line}
+ F
+ err.set_backtrace(e.backtrace)
+ raise err
+ end
+
+ end
+ end
+end
diff --git a/lib/chef/resource/windows_service.rb b/lib/chef/resource/windows_service.rb
index 2aec4d6304..8090adceb0 100644
--- a/lib/chef/resource/windows_service.rb
+++ b/lib/chef/resource/windows_service.rb
@@ -37,6 +37,8 @@ class Chef
@resource_name = :windows_service
@allowed_actions.push(:configure_startup)
@startup_type = :automatic
+ @run_as_user = ""
+ @run_as_password = ""
end
def startup_type(arg=nil)
@@ -48,6 +50,22 @@ class Chef
:equal_to => [ :automatic, :manual, :disabled ]
)
end
+
+ def run_as_user(arg=nil)
+ set_or_return(
+ :run_as_user,
+ arg,
+ :kind_of => [ String ]
+ )
+ end
+
+ def run_as_password(arg=nil)
+ set_or_return(
+ :run_as_password,
+ arg,
+ :kind_of => [ String ]
+ )
+ end
end
end
end
diff --git a/lib/chef/resource_collection/resource_set.rb b/lib/chef/resource_collection/resource_set.rb
index 6425c2ab08..1b39298cb4 100644
--- a/lib/chef/resource_collection/resource_set.rb
+++ b/lib/chef/resource_collection/resource_set.rb
@@ -44,7 +44,7 @@ class Chef
is_chef_resource!(resource)
resource_type ||= resource.resource_name
instance_name ||= resource.name
- key = ResourceSet.create_key(resource_type, instance_name)
+ key = create_key(resource_type, instance_name)
@resources_by_key[key] = resource
end
@@ -53,7 +53,7 @@ class Chef
when key.kind_of?(String)
lookup_by = key
when key.kind_of?(Chef::Resource)
- lookup_by = ResourceSet.create_key(key.resource_name, key.name)
+ lookup_by = create_key(key.resource_name, key.name)
else
raise ArgumentError, "Must pass a Chef::Resource or String to lookup"
end
@@ -128,18 +128,18 @@ class Chef
end
end
- def self.create_key(resource_type, instance_name)
+ private
+
+ def create_key(resource_type, instance_name)
"#{resource_type}[#{instance_name}]"
end
- private
-
def find_resource_by_hash(arg)
results = Array.new
arg.each do |resource_type, name_list|
instance_names = name_list.kind_of?(Array) ? name_list : [ name_list ]
instance_names.each do |instance_name|
- results << lookup(ResourceSet.create_key(resource_type, instance_name))
+ results << lookup(create_key(resource_type, instance_name))
end
end
return results
@@ -153,12 +153,12 @@ class Chef
arg =~ /^.+\[(.+)\]$/
resource_list = $1
resource_list.split(",").each do |instance_name|
- results << lookup(ResourceSet.create_key(resource_type, instance_name))
+ results << lookup(create_key(resource_type, instance_name))
end
when SINGLE_RESOURCE_MATCH
resource_type = $1
name = $2
- results << lookup(ResourceSet.create_key(resource_type, name))
+ results << lookup(create_key(resource_type, name))
else
raise ArgumentError, "Bad string format #{arg}, you must have a string like resource_type[name]!"
end
diff --git a/lib/chef/run_context.rb b/lib/chef/run_context.rb
index 1a2d7ba3a3..6803dc5796 100644
--- a/lib/chef/run_context.rb
+++ b/lib/chef/run_context.rb
@@ -50,6 +50,9 @@ class Chef
# recipes, which is triggered by #load. (See also: CookbookCompiler)
attr_accessor :resource_collection
+ # The list of audits (control groups) to execute during the audit phase
+ attr_accessor :audits
+
# A Hash containing the immediate notifications triggered by resources
# during the converge phase of the chef run.
attr_accessor :immediate_notification_collection
@@ -73,6 +76,7 @@ class Chef
@node = node
@cookbook_collection = cookbook_collection
@resource_collection = Chef::ResourceCollection.new
+ @audits = {}
@immediate_notification_collection = Hash.new {|h,k| h[k] = []}
@delayed_notification_collection = Hash.new {|h,k| h[k] = []}
@definitions = Hash.new
@@ -100,7 +104,7 @@ class Chef
if nr.instance_of?(Chef::Resource)
@immediate_notification_collection[nr.name] << notification
else
- @immediate_notification_collection[nr.to_s] << notification
+ @immediate_notification_collection[nr.declared_key] << notification
end
end
@@ -111,7 +115,7 @@ class Chef
if nr.instance_of?(Chef::Resource)
@delayed_notification_collection[nr.name] << notification
else
- @delayed_notification_collection[nr.to_s] << notification
+ @delayed_notification_collection[nr.declared_key] << notification
end
end
@@ -119,7 +123,7 @@ class Chef
if resource.instance_of?(Chef::Resource)
return @immediate_notification_collection[resource.name]
else
- return @immediate_notification_collection[resource.to_s]
+ return @immediate_notification_collection[resource.declared_key]
end
end
@@ -127,7 +131,7 @@ class Chef
if resource.instance_of?(Chef::Resource)
return @delayed_notification_collection[resource.name]
else
- return @delayed_notification_collection[resource.to_s]
+ return @delayed_notification_collection[resource.declared_key]
end
end
diff --git a/lib/chef/search/query.rb b/lib/chef/search/query.rb
index cc43efe1b1..8656e810db 100644
--- a/lib/chef/search/query.rb
+++ b/lib/chef/search/query.rb
@@ -17,52 +17,47 @@
#
require 'chef/config'
-require 'uri'
-require 'chef/rest'
-require 'chef/node'
-require 'chef/role'
-require 'chef/data_bag'
-require 'chef/data_bag_item'
require 'chef/exceptions'
+require 'chef/rest'
+
+require 'uri'
class Chef
class Search
class Query
attr_accessor :rest
+ attr_reader :config
- def initialize(url=nil)
- @rest = Chef::REST.new(url ||Chef::Config[:chef_server_url])
+ def initialize(url=nil, config:Chef::Config)
+ @config = config
+ @url = url
end
+ def rest
+ @rest ||= Chef::REST.new(@url || @config[:chef_server_url])
+ end
- # This search is only kept for backwards compatibility, since the results of the
- # new filtered search method will be in a slightly different format
+ # Backwards compatability for cookbooks.
+ # This can be removed in Chef > 12.
def partial_search(type, query='*:*', *args, &block)
- Chef::Log.warn("DEPRECATED: The 'partial_search' api is deprecated, please use the search api with 'filter_result'")
- # accept both types of args
- if args.length == 1 && args[0].is_a?(Hash)
- args_hash = args[0].dup
- # partial_search implemented in the partial search cookbook uses the
- # arg hash :keys instead of :filter_result to filter returned data
- args_hash[:filter_result] = args_hash[:keys]
+ Chef::Log.warn(<<-WARNDEP)
+DEPRECATED: The 'partial_search' API is deprecated and will be removed in
+future releases. Please use 'search' with a :filter_result argument to get
+partial search data.
+WARNDEP
+
+ if !args.empty? && args.first.is_a?(Hash)
+ # partial_search uses :keys instead of :filter_result for
+ # result filtering.
+ args_h = args.first.dup
+ args_h[:filter_result] = args_h[:keys]
+ args_h.delete(:keys)
+
+ search(type, query, args_h, &block)
else
- args_hash = {}
- args_hash[:sort] = args[0] if args.length >= 1
- args_hash[:start] = args[1] if args.length >= 2
- args_hash[:rows] = args[2] if args.length >= 3
+ search(type, query, *args, &block)
end
-
- unless block.nil?
- raw_results = search(type,query,args_hash)
- else
- raw_results = search(type,query,args_hash,&block)
- end
- results = Array.new
- raw_results[0].each do |r|
- results << r["data"]
- end
- return results
end
#
@@ -87,87 +82,71 @@ class Chef
#
def search(type, query='*:*', *args, &block)
validate_type(type)
- validate_args(args)
- scrubbed_args = Hash.new
+ args_h = hashify_args(*args)
+ response = call_rest_service(type, query: query, **args_h)
- # argify everything
- if args[0].kind_of?(Hash)
- scrubbed_args = args[0]
+ if block
+ response["rows"].each { |row| block.call(row) if row }
+ unless (response["start"] + response["rows"].length) >= response["total"]
+ args_h[:start] = response["start"] + (args_h[:rows] || 0)
+ search(type, query, args_h, &block)
+ end
+ true
else
- # This api will be deprecated in a future release
- scrubbed_args = { :sort => args[0], :start => args[1], :rows => args[2] }
+ [ response["rows"], response["start"], response["total"] ]
end
-
- # set defaults, if they haven't been set yet.
- scrubbed_args[:sort] ||= 'X_CHEF_id_CHEF_X asc'
- scrubbed_args[:start] ||= 0
- scrubbed_args[:rows] ||= 1000
-
- do_search(type, query, scrubbed_args, &block)
- end
-
- def list_indexes
- @rest.get_rest("search")
end
private
def validate_type(t)
unless t.kind_of?(String) || t.kind_of?(Symbol)
msg = "Invalid search object type #{t.inspect} (#{t.class}), must be a String or Symbol." +
- "Useage: search(:node, QUERY, [OPTIONAL_ARGS])" +
- " `knife search environment QUERY (options)`"
+ "Usage: search(:node, QUERY[, OPTIONAL_ARGS])" +
+ " `knife search environment QUERY (options)`"
raise Chef::Exceptions::InvalidSearchQuery, msg
end
end
- def validate_args(a)
- max_args = 3
- raise Chef::Exceptions::InvalidSearchQuery, "Too many arguments! (#{a.size} for <= #{max_args})" if a.size > max_args
+ def hashify_args(*args)
+ return Hash.new if args.empty?
+ return args.first if args.first.is_a?(Hash)
+
+ args_h = Hash.new
+ args_h[:sort] = args[0] if args[0]
+ args_h[:start] = args[1] if args[1]
+ args_h[:rows] = args[2]
+ args_h[:filter_result] = args[3]
+ args_h
end
def escape(s)
s && URI.escape(s.to_s)
end
- # new search api that allows for a cleaner implementation of things like return filters
- # (formerly known as 'partial search').
- # Also args should never be nil, but that is required for Ruby 1.8 compatibility
- def do_search(type, query="*:*", args=nil, &block)
- query_string = create_query_string(type, query, args)
- response = call_rest_service(query_string, args)
- unless block.nil?
- response["rows"].each { |rowset| block.call(rowset) unless rowset.nil?}
- unless (response["start"] + response["rows"].length) >= response["total"]
- args[:start] = response["start"] + args[:rows]
- do_search(type, query, args, &block)
- end
- true
- else
- [ response["rows"], response["start"], response["total"] ]
- end
+ def create_query_string(type, query, rows, start, sort)
+ qstr = "search/#{type}?q=#{escape(query)}"
+ qstr += "&sort=#{escape(sort)}" if sort
+ qstr += "&start=#{escape(start)}" if start
+ qstr += "&rows=#{escape(rows)}" if rows
+ qstr
end
- # create the full rest url string
- def create_query_string(type, query, args)
- # create some default variables just so we don't break backwards compatibility
- sort = args[:sort]
- start = args[:start]
- rows = args[:rows]
+ def call_rest_service(type, query:'*:*', rows:nil, start:0, sort:'X_CHEF_id_CHEF_X asc', filter_result:nil)
+ query_string = create_query_string(type, query, rows, start, sort)
- return "search/#{type}?q=#{escape(query)}&sort=#{escape(sort)}&start=#{escape(start)}&rows=#{escape(rows)}"
- end
-
- def call_rest_service(query_string, args)
- if args.key?(:filter_result)
- response = @rest.post_rest(query_string, args[:filter_result])
- response_rows = response['rows'].map { |row| row['data'] }
+ if filter_result
+ response = rest.post_rest(query_string, filter_result)
+ # response returns rows in the format of
+ # { "url" => url_to_node, "data" => filter_result_hash }
+ response['rows'].map! { |row| row['data'] }
else
- response = @rest.get_rest(query_string)
- response_rows = response['rows']
+ response = rest.get_rest(query_string)
end
- return response
+
+ response
end
+
end
end
end
diff --git a/lib/chef/util/dsc/lcm_output_parser.rb b/lib/chef/util/dsc/lcm_output_parser.rb
index f8f853a33a..754fde3e8b 100644
--- a/lib/chef/util/dsc/lcm_output_parser.rb
+++ b/lib/chef/util/dsc/lcm_output_parser.rb
@@ -18,6 +18,7 @@
require 'chef/log'
require 'chef/util/dsc/resource_info'
+require 'chef/exceptions'
class Chef
class Util
@@ -53,8 +54,7 @@ class Chef
# ]
#
def self.parse(lcm_output)
- return [] unless lcm_output
-
+ lcm_output ||= ""
current_resource = Hash.new
resources = []
@@ -96,7 +96,11 @@ class Chef
resources.push(current_resource)
end
- build_resource_info(resources)
+ if resources.length > 0
+ build_resource_info(resources)
+ else
+ raise Chef::Exceptions::LCMParser, "Could not parse:\n#{lcm_output}"
+ end
end
def self.parse_line(line)
diff --git a/lib/chef/util/dsc/local_configuration_manager.rb b/lib/chef/util/dsc/local_configuration_manager.rb
index f498a2bfea..c3149429f9 100644
--- a/lib/chef/util/dsc/local_configuration_manager.rb
+++ b/lib/chef/util/dsc/local_configuration_manager.rb
@@ -103,7 +103,7 @@ EOH
Chef::Log.debug("DSC: DSC returned the following '-whatif' output from test operation:\n#{what_if_output}")
begin
Parser::parse(what_if_output)
- rescue Chef::Util::DSC::LocalConfigurationManager::Parser => e
+ rescue Chef::Exceptions::LCMParser => e
Chef::Log::warn("Could not parse LCM output: #{e}")
[Chef::Util::DSC::ResourceInfo.new('Unknown DSC Resources', true, ['Unknown changes because LCM output was not parsable.'])]
end
diff --git a/lib/chef/whitelist.rb b/lib/chef/whitelist.rb
index ad52215f11..86c229d22c 100644
--- a/lib/chef/whitelist.rb
+++ b/lib/chef/whitelist.rb
@@ -57,7 +57,9 @@ class Chef
all_data = all_data[part]
end
- unless all_data[parts[-1]]
+ # Note: You can't do all_data[parts[-1]] here because the value
+ # may be false-y
+ unless all_data.key?(parts[-1])
Chef::Log.warn("Could not find whitelist attribute #{item}.")
return nil
end
diff --git a/lib/chef/win32/api/file.rb b/lib/chef/win32/api/file.rb
index da9713e119..86b2b942c2 100644
--- a/lib/chef/win32/api/file.rb
+++ b/lib/chef/win32/api/file.rb
@@ -457,11 +457,25 @@ BOOL WINAPI DeviceIoControl(
# takes the given path pre-pends "\\?\" and
# UTF-16LE encodes it. Used to prepare paths
# to be passed to the *W vesion of WinAPI File
- # functions
+ # functions.
+ # This function is used by the "Link" resources where we need
+ # preserve relative paths because symbolic links can actually
+ # point to a relative path (relative to the link itself).
def encode_path(path)
+ (path_prepender << path.gsub(::File::SEPARATOR, ::File::ALT_SEPARATOR)).to_wstring
+ end
+
+ # Expands the path, prepends "\\?\" and UTF-16LE encodes it.
+ # This function is used by the "File" resources where we need
+ # convert relative paths to fully qualified paths.
+ def canonical_encode_path(path)
Chef::Util::PathHelper.canonical_path(path).to_wstring
end
+ def path_prepender
+ "\\\\?\\"
+ end
+
# retrieves a file search handle and passes it
# to +&block+ along with the find_data. also
# ensures the handle is closed on exit of the block
@@ -474,7 +488,7 @@ BOOL WINAPI DeviceIoControl(
# broader fix to map all the paths starting with "/" to
# SYSTEM_DRIVE on windows.
path = ::File.expand_path(path) if path.start_with? "/"
- path = encode_path(path)
+ path = canonical_encode_path(path)
find_data = WIN32_FIND_DATA.new
handle = FindFirstFileW(path, find_data)
if handle == INVALID_HANDLE_VALUE
@@ -491,7 +505,7 @@ BOOL WINAPI DeviceIoControl(
# ensures the handle is closed on exit of the block
def file_handle(path, &block)
begin
- path = encode_path(path)
+ path = canonical_encode_path(path)
handle = CreateFileW(path, GENERIC_READ, FILE_SHARE_READ,
nil, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL | FILE_FLAG_BACKUP_SEMANTICS, nil)
diff --git a/spec/functional/audit/rspec_formatter_spec.rb b/spec/functional/audit/rspec_formatter_spec.rb
new file mode 100644
index 0000000000..43d3c2f6dd
--- /dev/null
+++ b/spec/functional/audit/rspec_formatter_spec.rb
@@ -0,0 +1,53 @@
+#
+# Author:: Tyler Ball (<tball@chef.io>)
+# Author:: Claire McQuin (<claire@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'
+require 'spec/support/audit_helper'
+require 'chef/audit/runner'
+require 'rspec/support/spec/in_sub_process'
+require 'chef/audit/rspec_formatter'
+
+describe Chef::Audit::RspecFormatter do
+ include RSpec::Support::InSubProcess
+
+ let(:events) { double("events").as_null_object }
+ let(:audits) { {} }
+ let(:run_context) { instance_double(Chef::RunContext, :events => events, :audits => audits) }
+ let(:runner) { Chef::Audit::Runner.new(run_context) }
+
+ let(:output) { double("output") }
+ # aggressively define this so we can mock out the new call later
+ let!(:formatter) { Chef::Audit::RspecFormatter.new(output) }
+
+ around(:each) do |ex|
+ Sandboxing.sandboxed { ex.run }
+ end
+
+ it "should not close the output using our formatter" do
+ in_sub_process do
+ expect_any_instance_of(Chef::Audit::RspecFormatter).to receive(:new).and_return(formatter)
+ expect(formatter).to receive(:close).and_call_original
+ expect(output).to_not receive(:close)
+
+ runner.run
+ end
+ end
+
+end
diff --git a/spec/functional/audit/runner_spec.rb b/spec/functional/audit/runner_spec.rb
new file mode 100644
index 0000000000..aa35548f2f
--- /dev/null
+++ b/spec/functional/audit/runner_spec.rb
@@ -0,0 +1,136 @@
+#
+# Author:: Tyler Ball (<tball@chef.io>)
+# 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 'spec/support/audit_helper'
+require 'chef/audit/runner'
+require 'rspec/support/spec/in_sub_process'
+require 'tempfile'
+
+##
+# This functional test ensures that our runner can be setup to not interfere with existing RSpec
+# configuration and world objects. When normally running Chef, there is only 1 RSpec instance
+# so this isn't needed. In unit testing the Runner should be mocked appropriately.
+
+describe Chef::Audit::Runner do
+
+ # The functional tests must be run in a sub_process. Including Serverspec includes the Serverspec DSL - this
+ # conflicts with our `package` DSL (among others) when we try to test `package` inside an RSpec example.
+ # Our DSL leverages `method_missing` while the Serverspec DSL defines a method on the RSpec::Core::ExampleGroup.
+ # The defined method wins our and returns before our `method_missing` DSL can be called.
+ #
+ # Running in a sub_process means the serverspec libraries will only be included in a forked process, not the main one.
+ include RSpec::Support::InSubProcess
+
+ let(:events) { double("events").as_null_object }
+ let(:runner) { Chef::Audit::Runner.new(run_context) }
+ let(:stdout) { StringIO.new }
+
+ around(:each) do |ex|
+ Sandboxing.sandboxed { ex.run }
+ end
+
+ before do
+ Chef::Config[:log_location] = stdout
+ end
+
+ describe "#run" do
+
+ let(:audits) { {} }
+ let(:run_context) { instance_double(Chef::RunContext, :events => events, :audits => audits) }
+ let(:controls_name) { "controls_name" }
+
+ it "Correctly runs an empty controls block" do
+ in_sub_process do
+ runner.run
+ end
+ end
+
+ shared_context "passing audit" do
+ let(:audits) do
+ should_pass = lambda do
+ it "should pass" do
+ expect(2 - 2).to eq(0)
+ end
+ end
+ { controls_name => Struct.new(:args, :block).new([controls_name], should_pass)}
+ end
+ end
+
+ shared_context "failing audit" do
+ let(:audits) do
+ should_fail = lambda do
+ it "should fail" do
+ expect(2 - 1).to eq(0)
+ end
+ end
+ { controls_name => Struct.new(:args, :block).new([controls_name], should_fail)}
+ end
+ end
+
+ context "there is a single successful control" do
+ include_context "passing audit"
+ it "correctly runs" do
+ in_sub_process do
+ runner.run
+
+ expect(stdout.string).to match(/1 example, 0 failures/)
+ end
+ end
+ end
+
+ context "there is a single failing control" do
+ include_context "failing audit"
+ it "correctly runs" do
+ in_sub_process do
+ runner.run
+
+ expect(stdout.string).to match(/Failure\/Error: expect\(2 - 1\)\.to eq\(0\)/)
+ expect(stdout.string).to match(/1 example, 1 failure/)
+ expect(stdout.string).to match(/# controls_name should fail/)
+ end
+ end
+ end
+
+ describe "log location is a file" do
+ let(:tmpfile) { Tempfile.new("audit") }
+ before do
+ Chef::Config[:log_location] = tmpfile.path
+ end
+
+ after do
+ tmpfile.close
+ tmpfile.unlink
+ end
+
+ include_context "failing audit"
+ it "correctly runs" do
+ in_sub_process do
+ runner.run
+
+ contents = tmpfile.read
+ expect(contents).to match(/Failure\/Error: expect\(2 - 1\)\.to eq\(0\)/)
+ expect(contents).to match(/1 example, 1 failure/)
+ expect(contents).to match(/# controls_name should fail/)
+ end
+ end
+ end
+
+ end
+
+end
diff --git a/spec/functional/knife/ssh_spec.rb b/spec/functional/knife/ssh_spec.rb
index cde702e8b2..5b8ad6f368 100644
--- a/spec/functional/knife/ssh_spec.rb
+++ b/spec/functional/knife/ssh_spec.rb
@@ -260,7 +260,7 @@ describe Chef::Knife::Ssh do
Chef::Config[:client_key] = nil
Chef::Config[:chef_server_url] = 'http://localhost:9000'
- @api.get("/search/node?q=*:*&sort=X_CHEF_id_CHEF_X%20asc&start=0&rows=1000", 200) {
+ @api.get("/search/node?q=*:*&sort=X_CHEF_id_CHEF_X%20asc&start=0", 200) {
%({"total":1, "start":0, "rows":[{"name":"i-xxxxxxxx", "json_class":"Chef::Node", "automatic":{"fqdn":"the.fqdn", "ec2":{"public_hostname":"the_public_hostname"}},"recipes":[]}]})
}
end
diff --git a/spec/functional/notifications_spec.rb b/spec/functional/notifications_spec.rb
new file mode 100644
index 0000000000..a02fdffe5e
--- /dev/null
+++ b/spec/functional/notifications_spec.rb
@@ -0,0 +1,169 @@
+require 'spec_helper'
+require 'chef/recipe'
+
+
+# The goal of these tests is to make sure that loading resources from a file creates the necessary notifications.
+# Then once converge has started, both immediate and delayed notifications are called as the resources are converged.
+# We want to do this WITHOUT actually converging any resources - we don't want to take time changing the system,
+# we just want to make sure the run_context, the notification DSL and the converge hooks are working together
+# to perform notifications.
+
+# This test is extremely fragile since it mocks MANY different systems at once - any of them changes, this test
+# breaks
+describe "Notifications" do
+
+ # We always pretend we are on OSx because that has a specific provider (HomebrewProvider) so it
+ # tests the translation from Provider => HomebrewProvider
+ let(:node) {
+ n = Chef::Node.new
+ n.override[:os] = "darwin"
+ n
+ }
+ let(:cookbook_collection) { double("Chef::CookbookCollection").as_null_object }
+ let(:events) { double("Chef::EventDispatch::Dispatcher").as_null_object }
+ let(:run_context) { Chef::RunContext.new(node, cookbook_collection, events) }
+ let(:recipe) { Chef::Recipe.new("notif", "test", run_context) }
+ let(:runner) { Chef::Runner.new(run_context) }
+
+ before do
+ # By default, every provider will do nothing
+ p = Chef::Provider.new(nil, run_context)
+ allow_any_instance_of(Chef::Resource).to receive(:provider_for_action).and_return(p)
+ allow(p).to receive(:run_action)
+ end
+
+ it "should subscribe from one resource to another" do
+ log_resource = recipe.declare_resource(:log, "subscribed-log") do
+ message "This is a log message"
+ action :nothing
+ subscribes :write, "package[vim]", :immediately
+ end
+
+ package_resource = recipe.declare_resource(:package, "vim") do
+ action :install
+ end
+
+ expect(log_resource).to receive(:run_action).with(:nothing, nil, nil).and_call_original
+
+ expect(package_resource).to receive(:run_action).with(:install, nil, nil).and_call_original
+ update_action(package_resource)
+
+ expect(log_resource).to receive(:run_action).with(:write, :immediate, package_resource).and_call_original
+
+ runner.converge
+ end
+
+ it "should notify from one resource to another immediately" do
+ log_resource = recipe.declare_resource(:log, "log") do
+ message "This is a log message"
+ action :write
+ notifies :install, "package[vim]", :immediately
+ end
+
+ package_resource = recipe.declare_resource(:package, "vim") do
+ action :nothing
+ end
+
+ expect(log_resource).to receive(:run_action).with(:write, nil, nil).and_call_original
+ update_action(log_resource)
+
+ expect(package_resource).to receive(:run_action).with(:install, :immediate, log_resource).ordered.and_call_original
+
+ expect(package_resource).to receive(:run_action).with(:nothing, nil, nil).ordered.and_call_original
+
+ runner.converge
+ end
+
+ it "should notify from one resource to another delayed" do
+ log_resource = recipe.declare_resource(:log, "log") do
+ message "This is a log message"
+ action :write
+ notifies :install, "package[vim]", :delayed
+ end
+
+ package_resource = recipe.declare_resource(:package, "vim") do
+ action :nothing
+ end
+
+ expect(log_resource).to receive(:run_action).with(:write, nil, nil).and_call_original
+ update_action(log_resource)
+
+ expect(package_resource).to receive(:run_action).with(:nothing, nil, nil).ordered.and_call_original
+
+ expect(package_resource).to receive(:run_action).with(:install, :delayed, nil).ordered.and_call_original
+
+ runner.converge
+ end
+
+ describe "when one resource is defined lazily" do
+
+ it "subscribes to a resource defined in a ruby block" do
+ r = recipe
+ t = self
+ ruby_block = recipe.declare_resource(:ruby_block, "rblock") do
+ block do
+ log_resource = r.declare_resource(:log, "log") do
+ message "This is a log message"
+ action :write
+ end
+ t.expect(log_resource).to t.receive(:run_action).with(:write, nil, nil).and_call_original
+ t.update_action(log_resource)
+ end
+ end
+
+ package_resource = recipe.declare_resource(:package, "vim") do
+ action :nothing
+ subscribes :install, "log[log]", :delayed
+ end
+
+ # RubyBlock needs to be able to run for our lazy examples to work - and it alone cannot affect the system
+ expect(ruby_block).to receive(:provider_for_action).and_call_original
+
+ expect(package_resource).to receive(:run_action).with(:nothing, nil, nil).ordered.and_call_original
+
+ expect(package_resource).to receive(:run_action).with(:install, :delayed, nil).ordered.and_call_original
+
+ runner.converge
+ end
+
+ it "notifies from inside a ruby_block to a resource defined outside" do
+ r = recipe
+ t = self
+ ruby_block = recipe.declare_resource(:ruby_block, "rblock") do
+ block do
+ log_resource = r.declare_resource(:log, "log") do
+ message "This is a log message"
+ action :write
+ notifies :install, "package[vim]", :immediately
+ end
+ t.expect(log_resource).to t.receive(:run_action).with(:write, nil, nil).and_call_original
+ t.update_action(log_resource)
+ end
+ end
+
+ package_resource = recipe.declare_resource(:package, "vim") do
+ action :nothing
+ end
+
+ # RubyBlock needs to be able to run for our lazy examples to work - and it alone cannot affect the system
+ expect(ruby_block).to receive(:provider_for_action).and_call_original
+
+ expect(package_resource).to receive(:run_action).with(:install, :immediate, instance_of(Chef::Resource::Log)).ordered.and_call_original
+
+ expect(package_resource).to receive(:run_action).with(:nothing, nil, nil).ordered.and_call_original
+
+ runner.converge
+ end
+
+ end
+
+ # Mocks having the provider run successfully and update the resource
+ def update_action(resource)
+ p = Chef::Provider.new(resource, run_context)
+ expect(resource).to receive(:provider_for_action).and_return(p)
+ expect(p).to receive(:run_action) {
+ resource.updated_by_last_action(true)
+ }
+ end
+
+end
diff --git a/spec/functional/resource/deploy_revision_spec.rb b/spec/functional/resource/deploy_revision_spec.rb
index 7bc3da9a05..e5f5341fcd 100644
--- a/spec/functional/resource/deploy_revision_spec.rb
+++ b/spec/functional/resource/deploy_revision_spec.rb
@@ -45,11 +45,10 @@ describe Chef::Resource::DeployRevision, :unix_only => true do
before(:all) do
@ohai = Ohai::System.new
- @ohai.all_plugins("os")
+ @ohai.all_plugins(["platform", "os"])
end
let(:node) do
-
Chef::Node.new.tap do |n|
n.name "rspec-test"
n.consume_external_attrs(@ohai.data, {})
diff --git a/spec/functional/resource/git_spec.rb b/spec/functional/resource/git_spec.rb
index 4f462b7cb6..9d3b82f19e 100644
--- a/spec/functional/resource/git_spec.rb
+++ b/spec/functional/resource/git_spec.rb
@@ -92,7 +92,7 @@ E
before(:all) do
@ohai = Ohai::System.new
- @ohai.all_plugins("os")
+ @ohai.all_plugins(["platform", "os"])
end
context "working with pathes with special characters" do
diff --git a/spec/functional/resource/link_spec.rb b/spec/functional/resource/link_spec.rb
index b4c6412e5d..d39a0c2ef6 100644
--- a/spec/functional/resource/link_spec.rb
+++ b/spec/functional/resource/link_spec.rb
@@ -72,8 +72,8 @@ describe Chef::Resource::Link do
end
end
- def paths_eql?(path1, path2)
- Chef::Util::PathHelper.paths_eql?(path1, path2)
+ def canonicalize(path)
+ windows? ? path.gsub('/', '\\') : path
end
def symlink(a, b)
@@ -180,7 +180,7 @@ describe Chef::Resource::Link do
it 'links to the target file' do
expect(symlink?(target_file)).to be_truthy
- expect(paths_eql?(readlink(target_file), to)).to be_truthy
+ expect(readlink(target_file)).to eq(canonicalize(to))
end
it 'marks the resource updated' do
expect(resource).to be_updated
@@ -201,7 +201,7 @@ describe Chef::Resource::Link do
it 'leaves the file linked' do
expect(symlink?(target_file)).to be_truthy
- expect(paths_eql?(readlink(target_file), to)).to be_truthy
+ expect(readlink(target_file)).to eq(canonicalize(to))
end
it 'does not mark the resource updated' do
expect(resource).not_to be_updated
@@ -279,7 +279,7 @@ describe Chef::Resource::Link do
before(:each) do
symlink(to, target_file)
expect(symlink?(target_file)).to be_truthy
- expect(paths_eql?(readlink(target_file), to)).to be_truthy
+ expect(readlink(target_file)).to eq(canonicalize(to))
end
include_context 'create symbolic link is noop'
include_context 'delete succeeds'
@@ -294,7 +294,7 @@ describe Chef::Resource::Link do
File.open(@other_target, 'w') { |file| file.write('eek') }
symlink(@other_target, target_file)
expect(symlink?(target_file)).to be_truthy
- expect(paths_eql?(readlink(target_file), @other_target)).to be_truthy
+ expect(readlink(target_file)).to eq(canonicalize(@other_target))
end
after(:each) do
File.delete(@other_target)
@@ -311,7 +311,7 @@ describe Chef::Resource::Link do
nonexistent = File.join(test_file_dir, make_tmpname('nonexistent_spec'))
symlink(nonexistent, target_file)
expect(symlink?(target_file)).to be_truthy
- expect(paths_eql?(readlink(target_file), nonexistent)).to be_truthy
+ expect(readlink(target_file)).to eq(canonicalize(nonexistent))
end
include_context 'create symbolic link succeeds'
include_context 'delete succeeds'
@@ -393,7 +393,7 @@ describe Chef::Resource::Link do
File.open(@other_target, "w") { |file| file.write("eek") }
symlink(@other_target, to)
expect(symlink?(to)).to be_truthy
- expect(paths_eql?(readlink(to), @other_target)).to be_truthy
+ expect(readlink(to)).to eq(canonicalize(@other_target))
end
after(:each) do
File.delete(@other_target)
@@ -408,7 +408,7 @@ describe Chef::Resource::Link do
@other_target = File.join(test_file_dir, make_tmpname("other_spec"))
symlink(@other_target, to)
expect(symlink?(to)).to be_truthy
- expect(paths_eql?(readlink(to), @other_target)).to be_truthy
+ expect(readlink(to)).to eq(canonicalize(@other_target))
end
context 'and the link does not yet exist' do
include_context 'create symbolic link succeeds'
@@ -441,7 +441,7 @@ describe Chef::Resource::Link do
before(:each) do
symlink(to, target_file)
expect(symlink?(target_file)).to be_truthy
- expect(paths_eql?(readlink(target_file), to)).to be_truthy
+ expect(readlink(target_file)).to eq(canonicalize(to))
end
include_context 'create symbolic link is noop'
include_context 'delete succeeds'
@@ -450,7 +450,7 @@ describe Chef::Resource::Link do
before(:each) do
symlink(absolute_to, target_file)
expect(symlink?(target_file)).to be_truthy
- expect(paths_eql?(readlink(target_file), absolute_to)).to be_truthy
+ expect(readlink(target_file)).to eq(canonicalize(absolute_to))
end
include_context 'create symbolic link succeeds'
include_context 'delete succeeds'
@@ -478,7 +478,7 @@ describe Chef::Resource::Link do
before(:each) do
symlink(to, target_file)
expect(symlink?(target_file)).to be_truthy
- expect(paths_eql?(readlink(target_file), to)).to be_truthy
+ expect(readlink(target_file)).to eq(canonicalize(to))
end
include_context 'create hard link succeeds'
it_behaves_like 'delete errors out'
@@ -552,7 +552,7 @@ describe Chef::Resource::Link do
File.open(@other_target, "w") { |file| file.write("eek") }
symlink(@other_target, to)
expect(symlink?(to)).to be_truthy
- expect(paths_eql?(readlink(to), @other_target)).to be_truthy
+ expect(readlink(to)).to eq(canonicalize(@other_target))
end
after(:each) do
File.delete(@other_target)
@@ -564,7 +564,7 @@ describe Chef::Resource::Link do
# OS X gets angry about this sort of link. Bug in OS X, IMO.
pending('OS X/FreeBSD/AIX symlink? and readlink working on hard links to symlinks') if (os_x? or freebsd? or aix?)
expect(symlink?(target_file)).to be_truthy
- expect(paths_eql?(readlink(target_file), @other_target)).to be_truthy
+ expect(readlink(target_file)).to eq(canonicalize(@other_target))
end
include_context 'delete is noop'
end
@@ -574,7 +574,7 @@ describe Chef::Resource::Link do
@other_target = File.join(test_file_dir, make_tmpname("other_spec"))
symlink(@other_target, to)
expect(symlink?(to)).to be_truthy
- expect(paths_eql?(readlink(to), @other_target)).to be_truthy
+ expect(readlink(to)).to eq(canonicalize(@other_target))
end
context 'and the link does not yet exist' do
it 'links to the target file' do
@@ -587,7 +587,7 @@ describe Chef::Resource::Link do
expect(File.exists?(target_file)).to be_falsey
end
expect(symlink?(target_file)).to be_truthy
- expect(paths_eql?(readlink(target_file), @other_target)).to be_truthy
+ expect(readlink(target_file)).to eq(canonicalize(@other_target))
end
include_context 'delete is noop'
end
diff --git a/spec/functional/resource/registry_spec.rb b/spec/functional/resource/registry_spec.rb
index d3120ac77c..f112ad9b00 100644
--- a/spec/functional/resource/registry_spec.rb
+++ b/spec/functional/resource/registry_spec.rb
@@ -40,7 +40,7 @@ describe Chef::Resource::RegistryKey, :unix_only do
end
end
-describe Chef::Resource::RegistryKey, :windows_only, :pending => "Refactor helper methods" do
+describe Chef::Resource::RegistryKey, :windows_only, :broken => true do
# parent and key must be single keys, not paths
let(:parent) { 'Opscode' }
diff --git a/spec/functional/resource/windows_service_spec.rb b/spec/functional/resource/windows_service_spec.rb
new file mode 100644
index 0000000000..29d1fc42c3
--- /dev/null
+++ b/spec/functional/resource/windows_service_spec.rb
@@ -0,0 +1,98 @@
+#
+# Author:: Chris Doherty (<cdoherty@chef.io>)
+# 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::Resource::WindowsService, :windows_only, :system_windows_service_gem_only do
+
+ include_context "using Win32::Service"
+
+ let(:username) { "service_spec_user"}
+ let(:qualified_username) { ".\\#{username}"}
+ let(:password) { "1a2b3c4X!&narf"}
+
+ let(:user_resource) {
+ r = Chef::Resource::User.new(username, run_context)
+ r.username(username)
+ r.password(password)
+ r.comment("temp spec user")
+ r
+ }
+
+ let(:global_service_file_path) {
+ "#{ENV['WINDIR']}\\temp\\#{File.basename(test_service[:service_file_path])}"
+ }
+
+ let(:service_params) {
+
+ id = "#{$$}_#{rand(1000)}"
+
+ test_service.merge( {
+ run_as_user: qualified_username,
+ run_as_password: password,
+ service_name: "spec_service_#{id}",
+ service_display_name: "windows_service spec #{id}}",
+ service_description: "Test service for running the windows_service functional spec.",
+ service_file_path: global_service_file_path,
+ } )
+ }
+
+ let(:manager) {
+ Chef::Application::WindowsServiceManager.new(service_params)
+ }
+
+ let(:service_resource) {
+ r = Chef::Resource::WindowsService.new(service_params[:service_name], run_context)
+ [:run_as_user, :run_as_password].each { |prop| r.send(prop, service_params[prop]) }
+ r
+ }
+
+ before {
+ user_resource.run_action(:create)
+
+ # the service executable has to be outside the current user's home
+ # directory in order for the logon user to execute it.
+ FileUtils::copy_file(test_service[:service_file_path], global_service_file_path)
+
+ # if you don't make the file executable by the service user, you'll get
+ # the not-very-helpful "service did not respond fast enough" error.
+
+ # #mode may break in a post-Windows 8.1 release, and have to be replaced
+ # with the rights stuff in the file resource.
+ file = Chef::Resource::File.new(global_service_file_path, run_context)
+ file.mode("0777")
+
+ file.run_action(:create)
+
+ manager.run(%w{--action install})
+ }
+
+ after {
+ user_resource.run_action(:remove)
+ manager.run(%w{--action uninstall})
+ File.delete(global_service_file_path)
+ }
+
+ describe "logon as a service" do
+ it "successfully runs a service as another user" do
+ service_resource.run_action(:start)
+ end
+
+ it "raises an exception when it can't grant the logon privilege"
+ end
+end
diff --git a/spec/functional/win32/service_manager_spec.rb b/spec/functional/win32/service_manager_spec.rb
index fd21e7d82e..d2474deace 100644
--- a/spec/functional/win32/service_manager_spec.rb
+++ b/spec/functional/win32/service_manager_spec.rb
@@ -24,7 +24,7 @@ end
#
# ATTENTION:
# This test creates a windows service for testing purposes and runs it
-# as Local System on windows boxes.
+# as Local System (or an otherwise specified user) on windows boxes.
# This test will fail if you run the tests inside a Windows VM by
# sharing the code from your host since Local System account by
# default can't see the mounted partitions.
@@ -35,61 +35,7 @@ end
describe "Chef::Application::WindowsServiceManager", :windows_only, :system_windows_service_gem_only do
- # Some helper methods.
-
- def test_service_exists?
- ::Win32::Service.exists?("spec-service")
- end
-
- def test_service_state
- ::Win32::Service.status("spec-service").current_state
- end
-
- def service_manager
- Chef::Application::WindowsServiceManager.new(test_service)
- end
-
- def cleanup
- # Uninstall if the test service is installed.
- if test_service_exists?
-
- # We can only uninstall when the service is stopped.
- if test_service_state != "stopped"
- ::Win32::Service.send("stop", "spec-service")
- while test_service_state != "stopped"
- sleep 1
- end
- end
-
- ::Win32::Service.delete("spec-service")
- end
-
- # Delete the test_service_file if it exists
- if File.exists?(test_service_file)
- File.delete(test_service_file)
- end
-
- end
-
-
- # Definition for the test-service
-
- let(:test_service) {
- {
- :service_name => "spec-service",
- :service_display_name => "Spec Test Service",
- :service_description => "Service for testing Chef::Application::WindowsServiceManager.",
- :service_file_path => File.expand_path(File.join(File.dirname(__FILE__), '../../support/platforms/win32/spec_service.rb'))
- }
- }
-
- # Test service creates a file for us to verify that it is running.
- # Since our test service is running as Local System we should look
- # for the file it creates under SYSTEM temp directory
-
- let(:test_service_file) {
- "#{ENV['SystemDrive']}\\windows\\temp\\spec_service_file"
- }
+ include_context "using Win32::Service"
context "with invalid service definition" do
it "throws an error when initialized with no service definition" do
@@ -190,7 +136,7 @@ describe "Chef::Application::WindowsServiceManager", :windows_only, :system_wind
["pause", "resume"].each do |action|
it "#{action} => should raise error" do
- expect {service_manager.run(["-a", action])}.to raise_error(::Win32::Service::Error)
+ expect { service_manager.run(["-a", action]) }.to raise_error(SystemCallError)
end
end
diff --git a/spec/integration/client/client_spec.rb b/spec/integration/client/client_spec.rb
index f4bb124781..62660bb852 100644
--- a/spec/integration/client/client_spec.rb
+++ b/spec/integration/client/client_spec.rb
@@ -239,4 +239,44 @@ EOM
end
end
+
+ when_the_repository "has a cookbook with only an audit recipe" do
+
+ before do
+ file 'config/client.rb', <<EOM
+local_mode true
+cookbook_path "#{path_to('cookbooks')}"
+audit_mode :enabled
+EOM
+ end
+
+ it "should exit with a zero code when there is not an audit failure" do
+ file 'cookbooks/audit_test/recipes/succeed.rb', <<-RECIPE
+controls "control group without top level control" do
+ it "should succeed" do
+ expect(2 - 2).to eq(0)
+ end
+end
+ RECIPE
+
+ result = shell_out("#{chef_client} -c \"#{path_to('config/client.rb')}\" -o 'audit_test::succeed'", :cwd => chef_dir)
+ expect(result.error?).to be_falsey
+ expect(result.stdout).to include("Successfully executed all `controls` blocks and contained examples")
+ end
+
+ it "should exit with a non-zero code when there is an audit failure" do
+ file 'cookbooks/audit_test/recipes/fail.rb', <<-RECIPE
+controls "control group without top level control" do
+ it "should fail" do
+ expect(2 - 2).to eq(1)
+ end
+end
+ RECIPE
+
+ result = shell_out("#{chef_client} -c \"#{path_to('config/client.rb')}\" -o 'audit_test::fail'", :cwd => chef_dir)
+ expect(result.error?).to be_truthy
+ expect(result.stdout).to include("Failure/Error: expect(2 - 2).to eq(1)")
+ end
+ end
+
end
diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb
index b155e6da24..995be5060b 100644
--- a/spec/spec_helper.rb
+++ b/spec/spec_helper.rb
@@ -141,6 +141,7 @@ RSpec.configure do |config|
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
running_platform_arch = `uname -m`.strip
diff --git a/spec/support/audit_helper.rb b/spec/support/audit_helper.rb
new file mode 100644
index 0000000000..8fd3f4d719
--- /dev/null
+++ b/spec/support/audit_helper.rb
@@ -0,0 +1,65 @@
+# This code comes from https://github.com/rspec/rspec-core/blob/master/spec/spec_helper.rb and
+# https://github.com/rspec/rspec-core/blob/master/spec/support/sandboxing.rb
+
+# To leverage the sandboxing use an `around` block:
+# around(:each) do |ex|
+# Sandboxing.sandboxed { ex.run }
+# end
+
+# rspec-core did not include a license on Github
+# TODO when this API is exposed publicly from rspec-core, get rid of this copy pasta
+
+# Adding these as writers is necessary, otherwise we cannot set the new configuration.
+# Only want to do this in the specs.
+class << RSpec
+ attr_writer :configuration, :world
+end
+
+class NullObject
+ private
+ def method_missing(method, *args, &block)
+ # ignore
+ end
+end
+
+# TODO remove this when RSPec exposes this functionality publically
+# https://github.com/rspec/rspec-core/pull/1808
+module Sandboxing
+ def self.sandboxed(&block)
+ orig_load_path = $LOAD_PATH.dup
+ orig_config = RSpec.configuration
+ orig_world = RSpec.world
+ orig_example = RSpec.current_example
+ new_config = RSpec::Core::Configuration.new
+ new_config.expose_dsl_globally = false
+ new_config.expecting_with_rspec = true
+ new_world = RSpec::Core::World.new(new_config)
+ RSpec.configuration = new_config
+ RSpec.world = new_world
+ object = Object.new
+ object.extend(RSpec::Core::SharedExampleGroup)
+
+ (class << RSpec::Core::ExampleGroup; self; end).class_exec do
+ alias_method :orig_run, :run
+ def run(reporter=nil)
+ RSpec.current_example = nil
+ orig_run(reporter || NullObject.new)
+ end
+ end
+
+ RSpec::Mocks.with_temporary_scope do
+ object.instance_exec(&block)
+ end
+ ensure
+ (class << RSpec::Core::ExampleGroup; self; end).class_exec do
+ remove_method :run
+ alias_method :run, :orig_run
+ remove_method :orig_run
+ end
+
+ RSpec.configuration = orig_config
+ RSpec.world = orig_world
+ RSpec.current_example = orig_example
+ $LOAD_PATH.replace(orig_load_path)
+ end
+end
diff --git a/spec/support/chef_helpers.rb b/spec/support/chef_helpers.rb
index 237543748c..851b1dce0a 100644
--- a/spec/support/chef_helpers.rb
+++ b/spec/support/chef_helpers.rb
@@ -67,15 +67,15 @@ end
# win32/service gem. windows_service_manager tests create a windows
# service that starts with the system ruby and requires this gem.
def system_windows_service_gem?
- windows_service_gem_check_command = "ruby -e 'require \"win32/daemon\"' > /dev/null 2>&1"
+ windows_service_gem_check_command = %q{ruby -r "win32/daemon" -e ":noop"}
if defined?(Bundler)
Bundler.with_clean_env do
# This returns true if the gem can be loaded
- system windows_service_gem_check_command
+ system(windows_service_gem_check_command)
end
else
# This returns true if the gem can be loaded
- system windows_service_gem_check_command
+ system(windows_service_gem_check_command)
end
end
diff --git a/spec/support/platform_helpers.rb b/spec/support/platform_helpers.rb
index db0f9ccee4..959580c953 100644
--- a/spec/support/platform_helpers.rb
+++ b/spec/support/platform_helpers.rb
@@ -1,7 +1,10 @@
require 'fcntl'
require 'chef/mixin/shell_out'
-include Chef::Mixin::ShellOut
+
+class ShellHelpers
+ extend Chef::Mixin::ShellOut
+end
def ruby_gte_20?
RUBY_VERSION.to_f >= 2.0
@@ -86,7 +89,7 @@ end
def mac_osx_106?
if File.exists? "/usr/bin/sw_vers"
- result = shell_out("/usr/bin/sw_vers")
+ result = ShellHelpers.shell_out("/usr/bin/sw_vers")
result.stdout.each_line do |line|
if line =~ /^ProductVersion:\s10.6.*$/
return true
diff --git a/spec/support/shared/functional/win32_service.rb b/spec/support/shared/functional/win32_service.rb
new file mode 100644
index 0000000000..7dd1920418
--- /dev/null
+++ b/spec/support/shared/functional/win32_service.rb
@@ -0,0 +1,60 @@
+
+require 'chef/application/windows_service_manager'
+
+shared_context "using Win32::Service" do
+ # Some helper methods.
+
+ def test_service_exists?
+ ::Win32::Service.exists?("spec-service")
+ end
+
+ def test_service_state
+ ::Win32::Service.status("spec-service").current_state
+ end
+
+ def service_manager
+ Chef::Application::WindowsServiceManager.new(test_service)
+ end
+
+ def cleanup
+ # Uninstall if the test service is installed.
+ if test_service_exists?
+
+ # We can only uninstall when the service is stopped.
+ if test_service_state != "stopped"
+ ::Win32::Service.send("stop", "spec-service")
+ while test_service_state != "stopped"
+ sleep 1
+ end
+ end
+
+ ::Win32::Service.delete("spec-service")
+ end
+
+ # Delete the test_service_file if it exists
+ if File.exists?(test_service_file)
+ File.delete(test_service_file)
+ end
+
+ end
+
+
+ # Definition for the test-service
+
+ let(:test_service) {
+ {
+ :service_name => "spec-service",
+ :service_display_name => "Spec Test Service",
+ :service_description => "Service for testing Chef::Application::WindowsServiceManager.",
+ :service_file_path => File.expand_path(File.join(File.dirname(__FILE__), '../../platforms/win32/spec_service.rb'))
+ }
+ }
+
+ # Test service creates a file for us to verify that it is running.
+ # Since our test service is running as Local System we should look
+ # for the file it creates under SYSTEM temp directory
+
+ let(:test_service_file) {
+ "#{ENV['SystemDrive']}\\windows\\temp\\spec_service_file"
+ }
+end
diff --git a/spec/unit/application/client_spec.rb b/spec/unit/application/client_spec.rb
index c2d3ec0507..33af9bc5c1 100644
--- a/spec/unit/application/client_spec.rb
+++ b/spec/unit/application/client_spec.rb
@@ -18,18 +18,20 @@
require 'spec_helper'
describe Chef::Application::Client, "reconfigure" do
+ let(:app) do
+ a = described_class.new
+ a.cli_arguments = []
+ a
+ end
+
before do
allow(Kernel).to receive(:trap).and_return(:ok)
@original_argv = ARGV.dup
ARGV.clear
- @app = Chef::Application::Client.new
- allow(@app).to receive(:trap)
- allow(@app).to receive(:configure_opt_parser).and_return(true)
- allow(@app).to receive(:configure_chef).and_return(true)
- allow(@app).to receive(:configure_logging).and_return(true)
- @app.cli_arguments = []
+ allow(app).to receive(:trap)
+ allow(app).to receive(:configure_logging).and_return(true)
Chef::Config[:interval] = 10
Chef::Config[:once] = false
@@ -60,7 +62,7 @@ Configuration settings:
interval = 600 seconds
Enable chef-client interval runs by setting `:client_fork = true` in your config file or adding `--fork` to your command line options."
)
- @app.reconfigure
+ app.reconfigure
end
end
@@ -72,7 +74,7 @@ Enable chef-client interval runs by setting `:client_fork = true` in your config
it "should not terminate" do
expect(Chef::Application).not_to receive(:fatal!)
- @app.reconfigure
+ app.reconfigure
end
end
@@ -83,7 +85,7 @@ Enable chef-client interval runs by setting `:client_fork = true` in your config
end
it "should reconfigure chef-client" do
- @app.reconfigure
+ app.reconfigure
expect(Chef::Config[:interval]).to be_nil
end
end
@@ -96,7 +98,7 @@ Enable chef-client interval runs by setting `:client_fork = true` in your config
end
it "should set the interval to 1800" do
- @app.reconfigure
+ app.reconfigure
expect(Chef::Config.interval).to eq(1800)
end
end
@@ -110,12 +112,12 @@ Enable chef-client interval runs by setting `:client_fork = true` in your config
end
it "ignores the splay" do
- @app.reconfigure
+ app.reconfigure
expect(Chef::Config.splay).to be_nil
end
it "forces the interval to nil" do
- @app.reconfigure
+ app.reconfigure
expect(Chef::Config.interval).to be_nil
end
@@ -128,14 +130,89 @@ Enable chef-client interval runs by setting `:client_fork = true` in your config
let(:json_source) { "https://foo.com/foo.json" }
before do
+ allow(app).to receive(:configure_chef).and_return(true)
Chef::Config[:json_attribs] = json_source
expect(Chef::ConfigFetcher).to receive(:new).with(json_source).
and_return(config_fetcher)
end
it "reads the JSON attributes from the specified source" do
- @app.reconfigure
- expect(@app.chef_client_json).to eq(json_attribs)
+ app.reconfigure
+ expect(app.chef_client_json).to eq(json_attribs)
+ end
+ end
+
+ describe "audit mode" do
+ shared_examples "experimental feature" do
+ before do
+ allow(Chef::Log).to receive(:warn)
+ end
+
+ it "emits a warning that audit mode is an experimental feature" do
+ expect(Chef::Log).to receive(:warn).with(/Audit mode is an experimental feature/)
+ app.reconfigure
+ end
+ end
+
+ shared_examples "unrecognized setting" do
+ it "fatals with a message including the incorrect setting" do
+ expect(Chef::Application).to receive(:fatal!).with(/Unrecognized setting #{mode} for audit mode/)
+ app.reconfigure
+ end
+ end
+
+ shared_context "set via config file" do
+ before do
+ Chef::Config[:audit_mode] = mode
+ end
+ end
+
+ shared_context "set via command line" do
+ before do
+ ARGV.replace(["--audit-mode", mode])
+ end
+ end
+
+ describe "enabled via config file" do
+ include_context "set via config file" do
+ let(:mode) { :enabled }
+ include_examples "experimental feature"
+ end
+ end
+
+ describe "enabled via command line" do
+ include_context "set via command line" do
+ let(:mode) { "enabled" }
+ include_examples "experimental feature"
+ end
+ end
+
+ describe "audit_only via config file" do
+ include_context "set via config file" do
+ let(:mode) { :audit_only }
+ include_examples "experimental feature"
+ end
+ end
+
+ describe "audit-only via command line" do
+ include_context "set via command line" do
+ let(:mode) { "audit-only" }
+ include_examples "experimental feature"
+ end
+ end
+
+ describe "unrecognized setting via config file" do
+ include_context "set via config file" do
+ let(:mode) { :derp }
+ include_examples "unrecognized setting"
+ end
+ end
+
+ describe "unrecognized setting via command line" do
+ include_context "set via command line" do
+ let(:mode) { "derp" }
+ include_examples "unrecognized setting"
+ end
end
end
end
diff --git a/spec/unit/application/solo_spec.rb b/spec/unit/application/solo_spec.rb
index 80f0bead8b..2a07ff38ad 100644
--- a/spec/unit/application/solo_spec.rb
+++ b/spec/unit/application/solo_spec.rb
@@ -18,13 +18,16 @@
require 'spec_helper'
describe Chef::Application::Solo do
+
+ let(:app) { Chef::Application::Solo.new }
+
before do
allow(Kernel).to receive(:trap).and_return(:ok)
- @app = Chef::Application::Solo.new
- allow(@app).to receive(:configure_opt_parser).and_return(true)
- allow(@app).to receive(:configure_chef).and_return(true)
- allow(@app).to receive(:configure_logging).and_return(true)
- allow(@app).to receive(:trap)
+ allow(app).to receive(:configure_opt_parser).and_return(true)
+ allow(app).to receive(:configure_chef).and_return(true)
+ allow(app).to receive(:configure_logging).and_return(true)
+ allow(app).to receive(:trap)
+
Chef::Config[:recipe_url] = false
Chef::Config[:json_attribs] = false
Chef::Config[:solo] = true
@@ -32,10 +35,15 @@ describe Chef::Application::Solo do
describe "configuring the application" do
it "should set solo mode to true" do
- @app.reconfigure
+ app.reconfigure
expect(Chef::Config[:solo]).to be_truthy
end
+ it "should set audit-mode to :disabled" do
+ app.reconfigure
+ expect(Chef::Config[:audit_mode]).to be :disabled
+ end
+
describe "when configured to not fork the client process" do
before do
Chef::Config[:client_fork] = false
@@ -56,7 +64,7 @@ Configuration settings:
interval = 600 seconds
Enable chef-client interval runs by setting `:client_fork = true` in your config file or adding `--fork` to your command line options."
)
- @app.reconfigure
+ app.reconfigure
end
end
end
@@ -68,7 +76,7 @@ Enable chef-client interval runs by setting `:client_fork = true` in your config
it "should set the interval to 1800" do
Chef::Config[:interval] = nil
- @app.reconfigure
+ app.reconfigure
expect(Chef::Config[:interval]).to eq(1800)
end
end
@@ -85,44 +93,46 @@ Enable chef-client interval runs by setting `:client_fork = true` in your config
end
it "reads the JSON attributes from the specified source" do
- @app.reconfigure
- expect(@app.chef_client_json).to eq(json_attribs)
+ app.reconfigure
+ expect(app.chef_client_json).to eq(json_attribs)
end
end
describe "when the recipe_url configuration option is specified" do
+ let(:tarfile) { StringIO.new("remote_tarball_content") }
+ let(:target_file) { StringIO.new }
+
before do
Chef::Config[:cookbook_path] = "#{Dir.tmpdir}/chef-solo/cookbooks"
Chef::Config[:recipe_url] = "http://junglist.gen.nz/recipes.tgz"
+
allow(FileUtils).to receive(:rm_rf).and_return(true)
allow(FileUtils).to receive(:mkdir_p).and_return(true)
- @tarfile = StringIO.new("remote_tarball_content")
- allow(@app).to receive(:open).with("http://junglist.gen.nz/recipes.tgz").and_yield(@tarfile)
- @target_file = StringIO.new
- allow(File).to receive(:open).with("#{Dir.tmpdir}/chef-solo/recipes.tgz", "wb").and_yield(@target_file)
+ allow(app).to receive(:open).with("http://junglist.gen.nz/recipes.tgz").and_yield(tarfile)
+ allow(File).to receive(:open).with("#{Dir.tmpdir}/chef-solo/recipes.tgz", "wb").and_yield(target_file)
allow(Chef::Mixin::Command).to receive(:run_command).and_return(true)
end
it "should create the recipes path based on the parent of the cookbook path" do
expect(FileUtils).to receive(:mkdir_p).with("#{Dir.tmpdir}/chef-solo").and_return(true)
- @app.reconfigure
+ app.reconfigure
end
it "should download the recipes" do
- expect(@app).to receive(:open).with("http://junglist.gen.nz/recipes.tgz").and_yield(@tarfile)
- @app.reconfigure
+ expect(app).to receive(:open).with("http://junglist.gen.nz/recipes.tgz").and_yield(tarfile)
+ app.reconfigure
end
it "should write the recipes to the target path" do
- @app.reconfigure
- expect(@target_file.string).to eq("remote_tarball_content")
+ app.reconfigure
+ expect(target_file.string).to eq("remote_tarball_content")
end
it "should untar the target file to the parent of the cookbook path" do
expect(Chef::Mixin::Command).to receive(:run_command).with({:command => "tar zxvf #{Dir.tmpdir}/chef-solo/recipes.tgz -C #{Dir.tmpdir}/chef-solo"}).and_return(true)
- @app.reconfigure
+ app.reconfigure
end
end
end
@@ -142,9 +152,9 @@ Enable chef-client interval runs by setting `:client_fork = true` in your config
end
it "should fetch the recipe_url first" do
- expect(@app).to receive(:fetch_recipe_tarball).ordered
+ expect(app).to receive(:fetch_recipe_tarball).ordered
expect(Chef::ConfigFetcher).to receive(:new).ordered.and_return(config_fetcher)
- @app.reconfigure
+ app.reconfigure
end
end
@@ -153,18 +163,17 @@ Enable chef-client interval runs by setting `:client_fork = true` in your config
Chef::Config[:solo] = true
allow(Chef::Daemon).to receive(:change_privilege)
- @chef_client = double("Chef::Client")
- allow(Chef::Client).to receive(:new).and_return(@chef_client)
- @app = Chef::Application::Solo.new
+ chef_client = double("Chef::Client")
+ allow(Chef::Client).to receive(:new).and_return(chef_client)
# this is all stuff the reconfigure method needs
- allow(@app).to receive(:configure_opt_parser).and_return(true)
- allow(@app).to receive(:configure_chef).and_return(true)
- allow(@app).to receive(:configure_logging).and_return(true)
+ allow(app).to receive(:configure_opt_parser).and_return(true)
+ allow(app).to receive(:configure_chef).and_return(true)
+ allow(app).to receive(:configure_logging).and_return(true)
end
it "should change privileges" do
expect(Chef::Daemon).to receive(:change_privilege).and_return(true)
- @app.setup_application
+ app.setup_application
end
end
diff --git a/spec/unit/audit/audit_event_proxy_spec.rb b/spec/unit/audit/audit_event_proxy_spec.rb
new file mode 100644
index 0000000000..899ba468b1
--- /dev/null
+++ b/spec/unit/audit/audit_event_proxy_spec.rb
@@ -0,0 +1,311 @@
+#
+# Author:: Tyler Ball (<tball@chef.io>)
+# Author:: Claire McQuin (<claire@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'
+require 'chef/audit/audit_event_proxy'
+
+describe Chef::Audit::AuditEventProxy do
+
+ let(:stdout) { StringIO.new }
+ let(:events) { double("Chef::Events") }
+ let(:audit_event_proxy) { Chef::Audit::AuditEventProxy.new(stdout) }
+
+ before do
+ Chef::Audit::AuditEventProxy.events = events
+ end
+
+ describe "#example_group_started" do
+
+ let(:description) { "poots" }
+ let(:group) { double("ExampleGroup", :parent_groups => parents,
+ :description => description) }
+ let(:notification) { double("Notification", :group => group) }
+
+ context "when notified from a top-level example group" do
+
+ let(:parents) { [double("ExampleGroup")] }
+
+ it "notifies control_group_started event" do
+ expect(Chef::Log).to receive(:debug).
+ with("Entered \`controls\` block named poots")
+ expect(events).to receive(:control_group_started).
+ with(description)
+ audit_event_proxy.example_group_started(notification)
+ end
+ end
+
+ context "when notified from an inner-level example group" do
+
+ let(:parents) { [double("ExampleGroup"), double("OuterExampleGroup")] }
+
+ it "does nothing" do
+ expect(events).to_not receive(:control_group_started)
+ audit_event_proxy.example_group_started(notification)
+ end
+ end
+ end
+
+ describe "#stop" do
+
+ let(:examples) { [] }
+ let(:notification) { double("Notification", :examples => examples) }
+ let(:exception) { nil }
+ let(:example) { double("Example", :exception => exception) }
+ let(:control_group_name) { "audit test" }
+ let(:control_data) { double("ControlData") }
+
+ before do
+ allow(Chef::Log).to receive(:info) # silence messages to output stream
+ end
+
+ it "sends a message that audits completed" do
+ expect(Chef::Log).to receive(:info).with("Successfully executed all \`controls\` blocks and contained examples")
+ audit_event_proxy.stop(notification)
+ end
+
+ context "when an example succeeded" do
+
+ let(:examples) { [example] }
+ let(:excpetion) { nil }
+
+ before do
+ allow(audit_event_proxy).to receive(:build_control_from).
+ with(example).
+ and_return([control_group_name, control_data])
+ end
+
+ it "notifies events" do
+ expect(events).to receive(:control_example_success).
+ with(control_group_name, control_data)
+ audit_event_proxy.stop(notification)
+ end
+ end
+
+ context "when an example failed" do
+
+ let(:examples) { [example] }
+ let(:exception) { double("ExpectationNotMet") }
+
+ before do
+ allow(audit_event_proxy).to receive(:build_control_from).
+ with(example).
+ and_return([control_group_name, control_data])
+ end
+
+ it "notifies events" do
+ expect(events).to receive(:control_example_failure).
+ with(control_group_name, control_data, exception)
+ audit_event_proxy.stop(notification)
+ end
+ end
+
+ describe "#build_control_from" do
+
+ let(:examples) { [example] }
+
+ let(:example) { double("Example", :metadata => metadata,
+ :description => example_description,
+ :full_description => full_description, :exception => nil) }
+
+ let(:metadata) {
+ {
+ :described_class => described_class,
+ :example_group => example_group,
+ :line_number => line
+ }
+ }
+
+ let(:example_group) {
+ {
+ :description => group_description,
+ :parent_example_group => parent_group
+ }
+ }
+
+ let(:parent_group) {
+ {
+ :description => control_group_name,
+ :parent_example_group => nil
+ }
+ }
+
+ let(:line) { 27 }
+
+ let(:control_data) {
+ {
+ :name => example_description,
+ :desc => full_description,
+ :resource_type => resource_type,
+ :resource_name => resource_name,
+ :context => context,
+ :line_number => line
+ }
+ }
+
+ shared_examples "built control" do
+
+ before do
+ if described_class
+ allow(described_class).to receive(:instance_variable_get).
+ with(:@name).
+ and_return(resource_name)
+ allow(described_class.class).to receive(:name).
+ and_return(described_class.class)
+ end
+ end
+
+ it "returns the controls block name and example metadata for reporting" do
+ expect(events).to receive(:control_example_success).
+ with(control_group_name, control_data)
+ audit_event_proxy.stop(notification)
+ end
+ end
+
+ describe "a top-level example" do
+ # controls "port 111" do
+ # it "has nobody listening" do
+ # expect(port("111")).to_not be_listening
+ # end
+ # end
+
+ # Description parts
+ let(:group_description) { "port 111" }
+ let(:example_description) { "has nobody listening" }
+ let(:full_description) { group_description + " " + example_description }
+
+ # Metadata fields
+ let(:described_class) { nil }
+
+ # Example group (metadata[:example_group]) fields
+ let(:parent_group) { nil }
+
+ # Expected returns
+ let(:control_group_name) { group_description }
+
+ # Control data fields
+ let(:resource_type) { nil }
+ let(:resource_name) { nil }
+ let(:context) { [] }
+
+ include_examples "built control"
+ end
+
+ describe "an example with an implicit subject" do
+ # controls "application ports" do
+ # control port(111) do
+ # it { is_expected.to_not be_listening }
+ # end
+ # end
+
+ # Description parts
+ let(:control_group_name) { "application ports" }
+ let(:group_description) { "#{resource_type} #{resource_name}" }
+ let(:example_description) { "should not be listening" }
+ let(:full_description) { [control_group_name, group_description,
+ example_description].join(" ") }
+
+ # Metadata fields
+ let(:described_class) { double("Serverspec::Type::Port",
+ :class => "Serverspec::Type::Port", :name => resource_name) }
+
+ # Control data fields
+ let(:resource_type) { "Port" }
+ let(:resource_name) { "111" }
+ let(:context) { [] }
+
+ include_examples "built control"
+ end
+
+ describe "an example in a nested context" do
+ # controls "application ports" do
+ # control "port 111" do
+ # it "is not listening" do
+ # expect(port(111)).to_not be_listening
+ # end
+ # end
+ # end
+
+ # Description parts
+ let(:control_group_name) { "application ports" }
+ let(:group_description) { "port 111" }
+ let(:example_description) { "is not listening" }
+ let(:full_description) { [control_group_name, group_description,
+ example_description].join(" ") }
+
+ # Metadata fields
+ let(:described_class) { nil }
+
+ # Control data fields
+ let(:resource_type) { nil }
+ let(:resource_name) { nil }
+ let(:context) { [group_description] }
+
+ include_examples "built control"
+ end
+
+ describe "an example in a nested context including Serverspec" do
+ # controls "application directory" do
+ # control file("/tmp/audit") do
+ # describe file("/tmp/audit/test_file") do
+ # it "is a file" do
+ # expect(subject).to be_file
+ # end
+ # end
+ # end
+ # end
+
+ # Description parts
+ let(:control_group_name) { "application directory" }
+ let(:outer_group_description) { "File \"tmp/audit\"" }
+ let(:group_description) { "#{resource_type} #{resource_name}" }
+ let(:example_description) { "is a file" }
+ let(:full_description) { [control_group_name, outer_group_description,
+ group_description, example_description].join(" ") }
+
+ # Metadata parts
+ let(:described_class) { double("Serverspec::Type::File",
+ :class => "Serverspec::Type::File", :name => resource_name) }
+
+ # Example group parts
+ let(:parent_group) {
+ {
+ :description => outer_group_description,
+ :parent_example_group => control_group
+ }
+ }
+
+ let(:control_group) {
+ {
+ :description => control_group_name,
+ :parent_example_group => nil
+ }
+ }
+
+ # Control data parts
+ let(:resource_type) { "File" }
+ let(:resource_name) { "/tmp/audit/test_file" }
+ let(:context) { [outer_group_description] }
+
+ include_examples "built control"
+ end
+ end
+ end
+
+end
diff --git a/spec/unit/audit/audit_reporter_spec.rb b/spec/unit/audit/audit_reporter_spec.rb
new file mode 100644
index 0000000000..84d7ea82f0
--- /dev/null
+++ b/spec/unit/audit/audit_reporter_spec.rb
@@ -0,0 +1,393 @@
+#
+# Author:: Tyler Ball (<tball@chef.io>)
+# Author:: Claire McQuin (<claire@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::Audit::AuditReporter do
+
+ let(:rest) { double("rest") }
+ let(:reporter) { described_class.new(rest) }
+ let(:node) { double("node", :name => "sofreshsoclean") }
+ let(:run_id) { 0 }
+ let(:start_time) { Time.new(2014, 12, 3, 9, 31, 05, "-08:00") }
+ let(:end_time) { Time.new(2014, 12, 3, 9, 36, 14, "-08:00") }
+ let(:run_status) { instance_double(Chef::RunStatus, :node => node, :run_id => run_id,
+ :start_time => start_time, :end_time => end_time) }
+
+ describe "#audit_phase_start" do
+
+ it "notifies audit phase start to debug log" do
+ expect(Chef::Log).to receive(:debug).with(/Audit Reporter starting/)
+ reporter.audit_phase_start(run_status)
+ end
+
+ it "initializes an AuditData object" do
+ expect(Chef::Audit::AuditData).to receive(:new).with(run_status.node.name, run_status.run_id)
+ reporter.audit_phase_start(run_status)
+ end
+
+ it "saves the run status" do
+ reporter.audit_phase_start(run_status)
+ expect(reporter.instance_variable_get(:@run_status)).to eq run_status
+ end
+ end
+
+ describe "#run_completed" do
+
+ let(:audit_data) { Chef::Audit::AuditData.new(node.name, run_id) }
+ let(:run_data) { audit_data.to_hash }
+
+ before do
+ allow(reporter).to receive(:auditing_enabled?).and_return(true)
+ allow(reporter).to receive(:run_status).and_return(run_status)
+ allow(rest).to receive(:create_url).and_return(true)
+ allow(rest).to receive(:post).and_return(true)
+ allow(reporter).to receive(:audit_data).and_return(audit_data)
+ allow(reporter).to receive(:run_status).and_return(run_status)
+ allow(audit_data).to receive(:to_hash).and_return(run_data)
+ end
+
+ describe "a successful run with auditing enabled" do
+ it "sets run start and end times" do
+ iso_start_time = "2014-12-03T17:31:05Z"
+ iso_end_time = "2014-12-03T17:36:14Z"
+
+ reporter.run_completed(node)
+ expect(audit_data.start_time).to eq iso_start_time
+ expect(audit_data.end_time).to eq iso_end_time
+ end
+
+ it "posts audit data to server endpoint" do
+ endpoint = "api.opscode.us/orgname/controls"
+ headers = {
+ 'X-Ops-Audit-Report-Protocol-Version' => Chef::Audit::AuditReporter::PROTOCOL_VERSION
+ }
+
+ expect(rest).to receive(:create_url).
+ with("controls").
+ and_return(endpoint)
+ expect(rest).to receive(:post).
+ with(endpoint, run_data, headers)
+ reporter.run_completed(node)
+ end
+
+ context "when unable to post to server" do
+
+ let(:error) do
+ e = StandardError.new
+ e.set_backtrace(caller)
+ e
+ end
+
+ before do
+ expect(rest).to receive(:post).and_raise(error)
+ allow(error).to receive(:respond_to?).and_call_original
+ end
+
+ context "the error is an http error" do
+
+ let(:response) { double("response", :code => code) }
+
+ before do
+ expect(Chef::Log).to receive(:debug).with(/Sending audit report/)
+ expect(Chef::Log).to receive(:debug).with(/Audit Report/)
+ allow(error).to receive(:response).and_return(response)
+ expect(error).to receive(:respond_to?).with(:response).and_return(true)
+ end
+
+ context "when the code is 404" do
+
+ let(:code) { "404" }
+
+ it "logs that the server doesn't support audit reporting" do
+ expect(Chef::Log).to receive(:debug).with(/Server doesn't support audit reporting/)
+ reporter.run_completed(node)
+ end
+ end
+
+ shared_examples "non-404 error code" do
+
+ it "saves the error report" do
+ expect(Chef::FileCache).to receive(:store).
+ with("failed-audit-data.json", an_instance_of(String), 0640).
+ and_return(true)
+ expect(Chef::FileCache).to receive(:load).
+ with("failed-audit-data.json", false).
+ and_return(true)
+ expect(Chef::Log).to receive(:error).with(/Failed to post audit report to server/)
+ reporter.run_completed(node)
+ end
+
+ end
+
+ context "when the code is not 404" do
+ include_examples "non-404 error code" do
+ let(:code) { "505" }
+ end
+ end
+
+ context "when there is no code" do
+ include_examples "non-404 error code" do
+ let(:code) { nil }
+ end
+ end
+
+ end
+
+ context "the error is not an http error" do
+
+ it "logs the error" do
+ expect(error).to receive(:respond_to?).with(:response).and_return(false)
+ expect(Chef::Log).to receive(:error).with(/Failed to post audit report to server/)
+ reporter.run_completed(node)
+ end
+
+ end
+
+ context "when reporting url fatals are enabled" do
+
+ before do
+ allow(Chef::Config).to receive(:[]).
+ with(:enable_reporting_url_fatals).
+ and_return(true)
+ end
+
+ it "raises the error" do
+ expect(error).to receive(:respond_to?).with(:response).and_return(false)
+ allow(Chef::Log).to receive(:error).and_return(true)
+ expect(Chef::Log).to receive(:error).with(/Reporting fatals enabled. Aborting run./)
+ expect{ reporter.run_completed(node) }.to raise_error(error)
+ end
+
+ end
+ end
+ end
+
+ context "when auditing is not enabled" do
+
+ before do
+ allow(Chef::Log).to receive(:debug)
+ end
+
+ it "doesn't send reports" do
+ expect(reporter).to receive(:auditing_enabled?).and_return(false)
+ expect(Chef::Log).to receive(:debug).with("Audit Reports are disabled. Skipping sending reports.")
+ reporter.run_completed(node)
+ end
+
+ end
+
+ context "when the run fails before audits" do
+
+ before do
+ allow(Chef::Log).to receive(:debug)
+ end
+
+ it "doesn't send reports" do
+ expect(reporter).to receive(:auditing_enabled?).and_return(true)
+ expect(reporter).to receive(:run_status).and_return(nil)
+ expect(Chef::Log).to receive(:debug).with("Run failed before audits were initialized, not sending audit report to server")
+ reporter.run_completed(node)
+ end
+
+ end
+ end
+
+ describe "#run_failed" do
+
+ let(:audit_data) { Chef::Audit::AuditData.new(node.name, run_id) }
+ let(:run_data) { audit_data.to_hash }
+
+ let(:error) { double("AuditError", :class => "Chef::Exception::AuditError",
+ :message => "Well that certainly didn't work",
+ :backtrace => ["line 0", "line 1", "line 2"]) }
+
+ before do
+ allow(reporter).to receive(:auditing_enabled?).and_return(true)
+ allow(reporter).to receive(:run_status).and_return(run_status)
+ allow(reporter).to receive(:audit_data).and_return(audit_data)
+ allow(audit_data).to receive(:to_hash).and_return(run_data)
+ end
+
+ it "adds the error information to the reported data" do
+ expect(rest).to receive(:create_url)
+ expect(rest).to receive(:post)
+ reporter.run_failed(error)
+ expect(run_data).to have_key(:error)
+ expect(run_data[:error]).to eq "Chef::Exception::AuditError: Well that certainly didn't work\n" +
+ "line 0\nline 1\nline 2"
+ end
+
+ end
+
+ shared_context "audit data" do
+
+ let(:control_group_foo) { instance_double(Chef::Audit::ControlGroupData,
+ :metadata => double("foo metadata")) }
+ let(:control_group_bar) { instance_double(Chef::Audit::ControlGroupData,
+ :metadata => double("bar metadata")) }
+
+ let(:ordered_control_groups) {
+ {
+ "foo" => control_group_foo,
+ "bar" => control_group_bar
+ }
+ }
+
+ let(:audit_data) { instance_double(Chef::Audit::AuditData,
+ :add_control_group => true) }
+
+ let(:run_context) { instance_double(Chef::RunContext,
+ :audits => ordered_control_groups) }
+
+ before do
+ allow(reporter).to receive(:ordered_control_groups).and_return(ordered_control_groups)
+ allow(reporter).to receive(:audit_data).and_return(audit_data)
+ allow(reporter).to receive(:run_status).and_return(run_status)
+ allow(run_status).to receive(:run_context).and_return(run_context)
+ end
+ end
+
+ describe "#audit_phase_complete" do
+ include_context "audit data"
+
+ it "notifies audit phase finished to debug log" do
+ expect(Chef::Log).to receive(:debug).with(/Audit Reporter completed/)
+ reporter.audit_phase_complete
+ end
+
+ it "collects audit data" do
+ ordered_control_groups.each do |_name, group|
+ expect(audit_data).to receive(:add_control_group).with(group)
+ end
+ reporter.audit_phase_complete
+ end
+ end
+
+ describe "#audit_phase_failed" do
+ include_context "audit data"
+
+ let(:error) { double("Exception") }
+
+ it "notifies audit phase failed to debug log" do
+ expect(Chef::Log).to receive(:debug).with(/Audit Reporter failed/)
+ reporter.audit_phase_failed(error)
+ end
+
+ it "collects audit data" do
+ ordered_control_groups.each do |_name, group|
+ expect(audit_data).to receive(:add_control_group).with(group)
+ end
+ reporter.audit_phase_failed(error)
+ end
+ end
+
+ describe "#control_group_started" do
+ include_context "audit data"
+
+ let(:name) { "bat" }
+ let(:control_group) { instance_double(Chef::Audit::ControlGroupData,
+ :metadata => double("metadata")) }
+
+ before do
+ allow(Chef::Audit::ControlGroupData).to receive(:new).
+ with(name, control_group.metadata).
+ and_return(control_group)
+ end
+
+ it "stores the control group" do
+ expect(ordered_control_groups).to receive(:has_key?).with(name).and_return(false)
+ allow(run_context.audits).to receive(:[]).with(name).and_return(control_group)
+ expect(ordered_control_groups).to receive(:store).
+ with(name, control_group).
+ and_call_original
+ reporter.control_group_started(name)
+ # stubbed :has_key? above, which is used by the have_key matcher,
+ # so instead we check the response to Hash's #key? because luckily
+ # #key? does not call #has_key?
+ expect(ordered_control_groups.key?(name)).to be true
+ expect(ordered_control_groups[name]).to eq control_group
+ end
+
+ context "when a control group with the same name has been seen" do
+ it "raises an exception" do
+ expect(ordered_control_groups).to receive(:has_key?).with(name).and_return(true)
+ expect{ reporter.control_group_started(name) }.to raise_error(Chef::Exceptions::AuditControlGroupDuplicate)
+ end
+ end
+ end
+
+ describe "#control_example_success" do
+ include_context "audit data"
+
+ let(:name) { "foo" }
+ let(:example_data) { double("example data") }
+
+ it "notifies the control group the example succeeded" do
+ expect(control_group_foo).to receive(:example_success).with(example_data)
+ reporter.control_example_success(name, example_data)
+ end
+ end
+
+ describe "#control_example_failure" do
+ include_context "audit data"
+
+ let(:name) { "bar" }
+ let(:example_data) { double("example data") }
+ let(:error) { double("Exception", :message => "oopsie") }
+
+ it "notifies the control group the example failed" do
+ expect(control_group_bar).to receive(:example_failure).
+ with(example_data, error.message)
+ reporter.control_example_failure(name, example_data, error)
+ end
+ end
+
+ describe "#auditing_enabled?" do
+ shared_examples "enabled?" do |true_or_false|
+
+ it "returns #{true_or_false}" do
+ expect(Chef::Config).to receive(:[]).
+ with(:audit_mode).
+ and_return(audit_setting)
+ expect(reporter.auditing_enabled?).to be true_or_false
+ end
+ end
+
+ context "when auditing is disabled" do
+ include_examples "enabled?", false do
+ let(:audit_setting) { :disabled }
+ end
+ end
+
+ context "when auditing in audit-only mode" do
+ include_examples "enabled?", true do
+ let(:audit_setting) { :audit_only }
+ end
+ end
+
+ context "when auditing is enabled" do
+ include_examples "enabled?", true do
+ let(:audit_setting) { :enabled }
+ end
+ end
+ end
+
+end
diff --git a/spec/unit/audit/control_group_data_spec.rb b/spec/unit/audit/control_group_data_spec.rb
new file mode 100644
index 0000000000..e21ab066fd
--- /dev/null
+++ b/spec/unit/audit/control_group_data_spec.rb
@@ -0,0 +1,478 @@
+#
+# Author:: Tyler Ball (<tball@chef.io>)
+# Author:: Claire McQuin (<claire@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'
+require 'securerandom'
+
+describe Chef::Audit::AuditData do
+
+ let(:node_name) { "noodles" }
+ let(:run_id) { SecureRandom.uuid }
+ let(:audit_data) { described_class.new(node_name, run_id) }
+
+ let(:control_group_1) { double("control group 1") }
+ let(:control_group_2) { double("control group 2") }
+
+ describe "#add_control_group" do
+ context "when no control groups have been added" do
+ it "stores the control group" do
+ audit_data.add_control_group(control_group_1)
+ expect(audit_data.control_groups).to include(control_group_1)
+ end
+
+ end
+
+ context "when adding additional control groups" do
+
+ before do
+ audit_data.add_control_group(control_group_1)
+ end
+
+ it "stores the control group" do
+ audit_data.add_control_group(control_group_2)
+ expect(audit_data.control_groups).to include(control_group_2)
+ end
+
+ it "stores all control groups" do
+ audit_data.add_control_group(control_group_2)
+ expect(audit_data.control_groups).to include(control_group_1)
+ end
+ end
+ end
+
+ describe "#to_hash" do
+
+ let(:audit_data_hash) { audit_data.to_hash }
+
+ it "returns a hash" do
+ expect(audit_data_hash).to be_a(Hash)
+ end
+
+ it "describes a Chef::Audit::AuditData object" do
+ keys = [:node_name, :run_id, :start_time, :end_time, :control_groups]
+ expect(audit_data_hash.keys).to match_array(keys)
+ end
+
+ describe ":control_groups" do
+
+ let(:control_hash_1) { {:name => "control group 1"} }
+ let(:control_hash_2) { {:name => "control group 2"} }
+
+ let(:control_groups) { audit_data_hash[:control_groups] }
+
+ context "with no control groups added" do
+ it "is an empty list" do
+ expect(control_groups).to eq []
+ end
+ end
+
+ context "with one control group added" do
+
+ before do
+ allow(audit_data).to receive(:control_groups).and_return([control_group_1])
+ end
+
+ it "is a one-element list containing the control group hash" do
+ expect(control_group_1).to receive(:to_hash).once.and_return(control_hash_1)
+ expect(control_groups.size).to eq 1
+ expect(control_groups).to include(control_hash_1)
+ end
+ end
+
+ context "with multiple control groups added" do
+
+ before do
+ allow(audit_data).to receive(:control_groups).and_return([control_group_1, control_group_2])
+ end
+
+ it "is a list of control group hashes" do
+ expect(control_group_1).to receive(:to_hash).and_return(control_hash_1)
+ expect(control_group_2).to receive(:to_hash).and_return(control_hash_2)
+ expect(control_groups.size).to eq 2
+ expect(control_groups).to include(control_hash_1)
+ expect(control_groups).to include(control_hash_2)
+ end
+ end
+ end
+ end
+end
+
+describe Chef::Audit::ControlData do
+
+ let(:name) { "ramen" }
+ let(:resource_type) { double("Service") }
+ let(:resource_name) { "mysql" }
+ let(:context) { nil }
+ let(:line_number) { 27 }
+
+ let(:control_data) { described_class.new(name: name,
+ resource_type: resource_type, resource_name: resource_name,
+ context: context, line_number: line_number) }
+
+
+ describe "#to_hash" do
+
+ let(:control_data_hash) { control_data.to_hash }
+
+ it "returns a hash" do
+ expect(control_data_hash).to be_a(Hash)
+ end
+
+ it "describes a Chef::Audit::ControlData object" do
+ keys = [:name, :resource_type, :resource_name, :context, :status, :details]
+ expect(control_data_hash.keys).to match_array(keys)
+ end
+
+ context "when context is nil" do
+
+ it "sets :context to an empty array" do
+ expect(control_data_hash[:context]).to eq []
+ end
+
+ end
+
+ context "when context is non-nil" do
+
+ let(:context) { ["outer"] }
+
+ it "sets :context to its value" do
+ expect(control_data_hash[:context]).to eq context
+ end
+ end
+ end
+end
+
+describe Chef::Audit::ControlGroupData do
+
+ let(:name) { "balloon" }
+ let(:control_group_data) { described_class.new(name) }
+
+ shared_context "control data" do
+
+ let(:name) { "" }
+ let(:resource_type) { nil }
+ let(:resource_name) { nil }
+ let(:context) { nil }
+ let(:line_number) { 0 }
+
+ let(:control_data) {
+ {
+ :name => name,
+ :resource_type => resource_type,
+ :resource_name => resource_name,
+ :context => context,
+ :line_number => line_number
+ }
+ }
+
+ end
+
+ shared_context "control" do
+ include_context "control data"
+
+ let(:control) { Chef::Audit::ControlData.new(name: name,
+ resource_type: resource_type, resource_name: resource_name,
+ context: context, line_number: line_number) }
+
+ before do
+ allow(Chef::Audit::ControlData).to receive(:new).
+ with(name: name, resource_type: resource_type,
+ resource_name: resource_name, context: context,
+ line_number: line_number).
+ and_return(control)
+ end
+ end
+
+ describe "#new" do
+ it "has status \"success\"" do
+ expect(control_group_data.status).to eq "success"
+ end
+ end
+
+ describe "#example_success" do
+ include_context "control"
+
+ def notify_success
+ control_group_data.example_success(control_data)
+ end
+
+ it "increments the number of successful audits" do
+ num_success = control_group_data.number_succeeded
+ notify_success
+ expect(control_group_data.number_succeeded).to eq (num_success + 1)
+ end
+
+ it "does not increment the number of failed audits" do
+ num_failed = control_group_data.number_failed
+ notify_success
+ expect(control_group_data.number_failed).to eq (num_failed)
+ end
+
+ it "marks the audit's status as success" do
+ notify_success
+ expect(control.status).to eq "success"
+ end
+
+ it "does not modify its own status" do
+ expect(control_group_data).to_not receive(:status=)
+ status = control_group_data.status
+ notify_success
+ expect(control_group_data.status).to eq status
+ end
+
+ it "saves the control" do
+ controls = control_group_data.controls
+ expect(controls).to_not include(control)
+ notify_success
+ expect(controls).to include(control)
+ end
+ end
+
+ describe "#example_failure" do
+ include_context "control"
+
+ let(:details) { "poop" }
+
+ def notify_failure
+ control_group_data.example_failure(control_data, details)
+ end
+
+ it "does not increment the number of successful audits" do
+ num_success = control_group_data.number_succeeded
+ notify_failure
+ expect(control_group_data.number_succeeded).to eq num_success
+ end
+
+ it "increments the number of failed audits" do
+ num_failed = control_group_data.number_failed
+ notify_failure
+ expect(control_group_data.number_failed).to eq (num_failed + 1)
+ end
+
+ it "marks the audit's status as failure" do
+ notify_failure
+ expect(control.status).to eq "failure"
+ end
+
+ it "marks its own status as failure" do
+ notify_failure
+ expect(control_group_data.status).to eq "failure"
+ end
+
+ it "saves the control" do
+ controls = control_group_data.controls
+ expect(controls).to_not include(control)
+ notify_failure
+ expect(controls).to include(control)
+ end
+
+ context "when details are not provided" do
+
+ let(:details) { nil }
+
+ it "does not save details to the control" do
+ default_details = control.details
+ expect(control).to_not receive(:details=)
+ notify_failure
+ expect(control.details).to eq default_details
+ end
+ end
+
+ context "when details are provided" do
+
+ let(:details) { "yep that didn't work" }
+
+ it "saves details to the control" do
+ notify_failure
+ expect(control.details).to eq details
+ end
+ end
+ end
+
+ shared_examples "multiple audits" do |success_or_failure|
+ include_context "control"
+
+ let(:num_success) { 0 }
+ let(:num_failure) { 0 }
+
+ before do
+ if num_failure == 0
+ num_success.times { control_group_data.example_success(control_data) }
+ elsif num_success == 0
+ num_failure.times { control_group_data.example_failure(control_data, nil) }
+ end
+ end
+
+ it "counts the number of successful audits" do
+ expect(control_group_data.number_succeeded).to eq num_success
+ end
+
+ it "counts the number of failed audits" do
+ expect(control_group_data.number_failed).to eq num_failure
+ end
+
+ it "marks its status as \"#{success_or_failure}\"" do
+ expect(control_group_data.status).to eq success_or_failure
+ end
+ end
+
+ context "when all audits pass" do
+ include_examples "multiple audits", "success" do
+ let(:num_success) { 3 }
+ end
+ end
+
+ context "when one audit fails" do
+ shared_examples "mixed audit results" do
+ include_examples "multiple audits", "failure" do
+
+ let(:audit_results) { [] }
+ let(:num_success) { audit_results.count("success") }
+ let(:num_failure) { 1 }
+
+ before do
+ audit_results.each do |result|
+ if result == "success"
+ control_group_data.example_success(control_data)
+ else
+ control_group_data.example_failure(control_data, nil)
+ end
+ end
+ end
+ end
+ end
+
+ context "and it's the first audit" do
+ include_examples "mixed audit results" do
+ let(:audit_results) { ["failure", "success", "success"] }
+ end
+ end
+
+ context "and it's an audit in the middle" do
+ include_examples "mixed audit results" do
+ let(:audit_results) { ["success", "failure", "success"] }
+ end
+ end
+
+ context "and it's the last audit" do
+ include_examples "mixed audit results" do
+ let(:audit_results) { ["success", "success", "failure"] }
+ end
+ end
+ end
+
+ context "when all audits fail" do
+ include_examples "multiple audits", "failure" do
+ let(:num_failure) { 3 }
+ end
+ end
+
+ describe "#to_hash" do
+
+ let(:control_group_data_hash) { control_group_data.to_hash }
+
+ it "returns a hash" do
+ expect(control_group_data_hash).to be_a(Hash)
+ end
+
+ it "describes a Chef::Audit::ControlGroupData object" do
+ keys = [:name, :status, :number_succeeded, :number_failed,
+ :controls, :id]
+ expect(control_group_data_hash.keys).to match_array(keys)
+ end
+
+ describe ":controls" do
+
+ let(:control_group_controls) { control_group_data_hash[:controls] }
+
+ context "with no controls added" do
+ it "is an empty list" do
+ expect(control_group_controls).to eq []
+ end
+ end
+
+ context "with one control added" do
+ include_context "control"
+
+ let(:control_list) { [control_data] }
+ let(:control_hash) { control.to_hash }
+
+ before do
+ expect(control_group_data).to receive(:controls).twice.and_return(control_list)
+ expect(control_data).to receive(:to_hash).and_return(control_hash)
+ end
+
+ it "is a one-element list containing the control hash" do
+ expect(control_group_controls.size).to eq 1
+ expect(control_group_controls).to include(control_hash)
+ end
+
+ it "adds a sequence number to the control" do
+ control_group_data.to_hash
+ expect(control_hash).to have_key(:sequence_number)
+ end
+
+ end
+
+ context "with multiple controls added" do
+
+ let(:control_hash_1) { {:line_number => 27} }
+ let(:control_hash_2) { {:line_number => 13} }
+ let(:control_hash_3) { {:line_number => 35} }
+
+ let(:control_1) { double("control 1",
+ :line_number => control_hash_1[:line_number],
+ :to_hash => control_hash_1) }
+ let(:control_2) { double("control 2",
+ :line_number => control_hash_2[:line_number],
+ :to_hash => control_hash_2) }
+ let(:control_3) { double("control 3",
+ :line_number => control_hash_3[:line_number],
+ :to_hash => control_hash_3) }
+
+ let(:control_list) { [control_1, control_2, control_3] }
+ let(:ordered_control_hashes) { [control_hash_2, control_hash_1, control_hash_3] }
+
+ before do
+ # Another way to do this would be to call #example_success
+ # or #example_failure per control hash, but we'd have to
+ # then stub #create_control and it's a lot of extra stubbing work.
+ # We can't stub the controls reader to return a list of
+ # controls because of the call to sort! and the following
+ # reading of controls.
+ control_group_data.instance_variable_set(:@controls, control_list)
+ end
+
+ it "is a list of control group hashes ordered by line number" do
+ expect(control_group_controls.size).to eq 3
+ expect(control_group_controls).to eq ordered_control_hashes
+ end
+
+ it "assigns sequence numbers in order" do
+ control_group_data.to_hash
+ ordered_control_hashes.each_with_index do |control_hash, idx|
+ expect(control_hash[:sequence_number]).to eq idx + 1
+ end
+ end
+ end
+ end
+ end
+
+end
diff --git a/spec/unit/audit/rspec_formatter_spec.rb b/spec/unit/audit/rspec_formatter_spec.rb
new file mode 100644
index 0000000000..471473e387
--- /dev/null
+++ b/spec/unit/audit/rspec_formatter_spec.rb
@@ -0,0 +1,29 @@
+#
+# Author:: Tyler Ball (<tball@chef.io>)
+# Author:: Claire McQuin (<claire@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'
+require 'chef/audit/rspec_formatter'
+
+describe Chef::Audit::RspecFormatter do
+ let(:formatter) { Chef::Audit::RspecFormatter.new(nil) }
+ it "should respond to close" do
+ expect(formatter).to respond_to(:close)
+ end
+end
diff --git a/spec/unit/audit/runner_spec.rb b/spec/unit/audit/runner_spec.rb
new file mode 100644
index 0000000000..67590fecf9
--- /dev/null
+++ b/spec/unit/audit/runner_spec.rb
@@ -0,0 +1,133 @@
+#
+# Author:: Tyler Ball (<tball@chef.io>)
+# 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 'spec/support/audit_helper'
+require 'chef/audit/runner'
+require 'chef/audit/audit_event_proxy'
+require 'chef/audit/rspec_formatter'
+require 'rspec/support/spec/in_sub_process'
+
+describe Chef::Audit::Runner do
+ include RSpec::Support::InSubProcess
+
+ let(:events) { double("events") }
+ let(:run_context) { instance_double(Chef::RunContext, :events => events) }
+ let(:runner) { Chef::Audit::Runner.new(run_context) }
+
+ around(:each) do |ex|
+ Sandboxing.sandboxed { ex.run }
+ end
+
+ describe "#initialize" do
+ it "correctly sets the run_context during initialization" do
+ expect(runner.instance_variable_get(:@run_context)).to eq(run_context)
+ end
+ end
+
+ context "during #run" do
+
+ describe "#setup" do
+ let(:log_location) { File.join(Dir.tmpdir, 'audit_log') }
+ let(:color) { false }
+
+ before do
+ Chef::Config[:log_location] = log_location
+ Chef::Config[:color] = color
+ end
+
+ it "sets all the config values" do
+ # This runs the Serverspec includes - we don't want these hanging around in all subsequent tests so
+ # we run this in a forked process. Keeps Serverspec files from getting loaded into main process.
+ in_sub_process do
+ runner.send(:setup)
+
+ expect(RSpec.configuration.output_stream).to eq(log_location)
+ expect(RSpec.configuration.error_stream).to eq(log_location)
+
+ expect(RSpec.configuration.formatters.size).to eq(2)
+ expect(RSpec.configuration.formatters).to include(instance_of(Chef::Audit::AuditEventProxy))
+ expect(RSpec.configuration.formatters).to include(instance_of(Chef::Audit::RspecFormatter))
+ expect(Chef::Audit::AuditEventProxy.class_variable_get(:@@events)).to eq(run_context.events)
+
+ expect(RSpec.configuration.expectation_frameworks).to eq([RSpec::Matchers])
+ expect(RSpec::Matchers.configuration.syntax).to eq([:expect])
+
+ expect(RSpec.configuration.color).to eq(color)
+ expect(RSpec.configuration.expose_dsl_globally?).to eq(false)
+
+ expect(Specinfra.configuration.backend).to eq(:exec)
+ end
+ end
+ end
+
+ describe "#register_controls" do
+ let(:audits) { [] }
+ let(:run_context) { instance_double(Chef::RunContext, :audits => audits) }
+
+ it "adds the control group aliases" do
+ runner.send(:register_controls)
+
+ expect(RSpec::Core::DSL.example_group_aliases).to include(:__controls__)
+ expect(RSpec::Core::DSL.example_group_aliases).to include(:control)
+ end
+
+ context "audits exist" do
+ let(:audits) { {"audit_name" => group} }
+ let(:group) {Struct.new(:args, :block).new(["group_name"], nil)}
+
+ it "sends the audits to the world" do
+ runner.send(:register_controls)
+
+ expect(RSpec.world.example_groups.size).to eq(1)
+ # For whatever reason, `kind_of` is not working
+ # expect(RSpec.world.example_groups).to include(kind_of(RSpec::Core::ExampleGroup)) => FAIL
+ g = RSpec.world.example_groups[0]
+ expect(g.ancestors).to include(RSpec::Core::ExampleGroup)
+ expect(g.description).to eq("group_name")
+ end
+ end
+ end
+
+ describe "#do_run" do
+ let(:rspec_runner) { instance_double(RSpec::Core::Runner) }
+
+ it "executes the runner" do
+ expect(RSpec::Core::Runner).to receive(:new).with(nil).and_return(rspec_runner)
+ expect(rspec_runner).to receive(:run_specs).with([])
+
+ runner.send(:do_run)
+ end
+ end
+ end
+
+ describe "counters" do
+ it "correctly calculates failed?" do
+ expect(runner.failed?).to eq(false)
+ end
+
+ it "correctly calculates num_failed" do
+ expect(runner.num_failed).to eq(0)
+ end
+
+ it "correctly calculates num_total" do
+ expect(runner.num_total).to eq(0)
+ end
+ end
+
+end
diff --git a/spec/unit/client_spec.rb b/spec/unit/client_spec.rb
index 10958d628c..2ec32b32ac 100644
--- a/spec/unit/client_spec.rb
+++ b/spec/unit/client_spec.rb
@@ -187,11 +187,12 @@ describe Chef::Client do
end
describe "a full client run" do
- shared_examples_for "a successful client run" do
+ shared_context "a client run" do
let(:http_node_load) { double("Chef::REST (node)") }
let(:http_cookbook_sync) { double("Chef::REST (cookbook sync)") }
let(:http_node_save) { double("Chef::REST (node save)") }
let(:runner) { double("Chef::Runner") }
+ let(:audit_runner) { instance_double("Chef::Audit::Runner", :failed? => false) }
let(:api_client_exists?) { false }
@@ -204,7 +205,11 @@ describe Chef::Client do
# --Client.register
# Make sure Client#register thinks the client key doesn't
# exist, so it tries to register and create one.
- expect(File).to receive(:exists?).with(Chef::Config[:client_key]).exactly(1).times.and_return(api_client_exists?)
+ allow(File).to receive(:exists?).and_call_original
+ expect(File).to receive(:exists?).
+ with(Chef::Config[:client_key]).
+ exactly(:once).
+ and_return(api_client_exists?)
unless api_client_exists?
# Client.register will register with the validation client name.
@@ -218,7 +223,7 @@ describe Chef::Client do
# previous step.
expect(Chef::REST).to receive(:new).
with(Chef::Config[:chef_server_url], fqdn, Chef::Config[:client_key]).
- exactly(1).
+ exactly(:once).
and_return(http_node_load)
# --Client#build_node
@@ -246,11 +251,12 @@ describe Chef::Client do
# --Client#converge
expect(Chef::Runner).to receive(:new).and_return(runner)
expect(runner).to receive(:converge).and_return(true)
+ end
- # --ResourceReporter#run_completed
- # updates the server with the resource history
- # (has its own tests, so stubbing it here.)
- expect_any_instance_of(Chef::ResourceReporter).to receive(:run_completed)
+ def stub_for_audit
+ # -- Client#run_audits
+ expect(Chef::Audit::Runner).to receive(:new).and_return(audit_runner)
+ expect(audit_runner).to receive(:run).and_return(true)
end
def stub_for_node_save
@@ -269,11 +275,22 @@ describe Chef::Client do
# Post conditions: check that node has been filled in correctly
expect(client).to receive(:run_started)
expect(client).to receive(:run_completed_successfully)
+
+ # --ResourceReporter#run_completed
+ # updates the server with the resource history
+ # (has its own tests, so stubbing it here.)
+ expect_any_instance_of(Chef::ResourceReporter).to receive(:run_completed)
+ # --AuditReporter#run_completed
+ # posts the audit data to server.
+ # (has its own tests, so stubbing it here.)
+ expect_any_instance_of(Chef::Audit::AuditReporter).to receive(:run_completed)
end
before do
Chef::Config[:client_fork] = enable_fork
Chef::Config[:cache_path] = windows? ? 'C:\chef' : '/var/chef'
+ Chef::Config[:why_run] = false
+ Chef::Config[:audit_mode] = :enabled
stub_const("Chef::Client::STDOUT_FD", stdout)
stub_const("Chef::Client::STDERR_FD", stderr)
@@ -282,11 +299,16 @@ describe Chef::Client do
stub_for_node_load
stub_for_sync_cookbooks
stub_for_converge
+ stub_for_audit
stub_for_node_save
stub_for_run
end
+ end
- it "runs ohai, sets up authentication, loads node state, synchronizes policy, and converges" do
+ shared_examples_for "a successful client run" do
+ include_context "a client run"
+
+ it "runs ohai, sets up authentication, loads node state, synchronizes policy, converges, and runs audits" do
# This is what we're testing.
client.run
@@ -296,16 +318,12 @@ describe Chef::Client do
end
end
-
describe "when running chef-client without fork" do
-
include_examples "a successful client run"
end
describe "when the client key already exists" do
-
let(:api_client_exists?) { true }
-
include_examples "a successful client run"
end
@@ -344,7 +362,6 @@ describe Chef::Client do
end
describe "when a permanent run list is passed as an option" do
-
include_examples "a successful client run" do
let(:new_runlist) { "recipe[new_run_list_recipe]" }
@@ -374,6 +391,155 @@ describe Chef::Client do
end
end
+ describe "when converge fails" do
+ include_context "a client run" do
+ let(:e) { Exception.new }
+ def stub_for_converge
+ expect(Chef::Runner).to receive(:new).and_return(runner)
+ expect(runner).to receive(:converge).and_raise(e)
+ expect(Chef::Application).to receive(:debug_stacktrace).with an_instance_of(Chef::Exceptions::RunFailedWrappingError)
+ end
+
+ def stub_for_node_save
+ expect(client).to_not receive(:save_updated_node)
+ end
+
+ def stub_for_run
+ expect_any_instance_of(Chef::RunLock).to receive(:acquire)
+ expect_any_instance_of(Chef::RunLock).to receive(:save_pid)
+ expect_any_instance_of(Chef::RunLock).to receive(:release)
+
+ # Post conditions: check that node has been filled in correctly
+ expect(client).to receive(:run_started)
+ expect(client).to receive(:run_failed)
+
+ expect_any_instance_of(Chef::ResourceReporter).to receive(:run_failed)
+ expect_any_instance_of(Chef::Audit::AuditReporter).to receive(:run_failed)
+ end
+ end
+
+ it "runs the audits and raises the error" do
+ expect{ client.run }.to raise_error(Chef::Exceptions::RunFailedWrappingError) do |error|
+ expect(error.wrapped_errors.size).to eq(1)
+ expect(error.wrapped_errors[0]).to eq(e)
+ end
+ end
+ end
+
+ describe "when the audit phase fails" do
+ context "with an exception" do
+ include_context "a client run" do
+ let(:e) { Exception.new }
+ def stub_for_audit
+ expect(Chef::Audit::Runner).to receive(:new).and_return(audit_runner)
+ expect(audit_runner).to receive(:run).and_raise(e)
+ expect(Chef::Application).to receive(:debug_stacktrace).with an_instance_of(Chef::Exceptions::RunFailedWrappingError)
+ end
+
+ def stub_for_run
+ expect_any_instance_of(Chef::RunLock).to receive(:acquire)
+ expect_any_instance_of(Chef::RunLock).to receive(:save_pid)
+ expect_any_instance_of(Chef::RunLock).to receive(:release)
+
+ # Post conditions: check that node has been filled in correctly
+ expect(client).to receive(:run_started)
+ expect(client).to receive(:run_failed)
+
+ expect_any_instance_of(Chef::ResourceReporter).to receive(:run_failed)
+ expect_any_instance_of(Chef::Audit::AuditReporter).to receive(:run_failed)
+ end
+ end
+
+ it "should save the node after converge and raise exception" do
+ expect{ client.run }.to raise_error(Chef::Exceptions::RunFailedWrappingError) do |error|
+ expect(error.wrapped_errors.size).to eq(1)
+ expect(error.wrapped_errors[0]).to eq(e)
+ end
+ end
+ end
+
+ context "with failed audits" do
+ include_context "a client run" do
+ let(:audit_runner) do
+ instance_double("Chef::Audit::Runner", :run => true, :failed? => true, :num_failed => 1, :num_total => 1)
+ end
+
+ def stub_for_audit
+ expect(Chef::Audit::Runner).to receive(:new).and_return(audit_runner)
+ expect(Chef::Application).to receive(:debug_stacktrace).with an_instance_of(Chef::Exceptions::RunFailedWrappingError)
+ end
+
+ def stub_for_run
+ expect_any_instance_of(Chef::RunLock).to receive(:acquire)
+ expect_any_instance_of(Chef::RunLock).to receive(:save_pid)
+ expect_any_instance_of(Chef::RunLock).to receive(:release)
+
+ # Post conditions: check that node has been filled in correctly
+ expect(client).to receive(:run_started)
+ expect(client).to receive(:run_failed)
+
+ expect_any_instance_of(Chef::ResourceReporter).to receive(:run_failed)
+ expect_any_instance_of(Chef::Audit::AuditReporter).to receive(:run_failed)
+ end
+ end
+
+ it "should save the node after converge and raise exception" do
+ expect{ client.run }.to raise_error(Chef::Exceptions::RunFailedWrappingError) do |error|
+ expect(error.wrapped_errors.size).to eq(1)
+ expect(error.wrapped_errors[0]).to be_instance_of(Chef::Exceptions::AuditsFailed)
+ end
+ end
+ end
+ end
+
+ describe "when why_run mode is enabled" do
+ include_context "a client run" do
+
+ before do
+ Chef::Config[:why_run] = true
+ end
+
+ def stub_for_audit
+ expect(Chef::Audit::Runner).to_not receive(:new)
+ end
+
+ def stub_for_node_save
+ # This is how we should be mocking external calls - not letting it fall all the way through to the
+ # REST call
+ expect(node).to receive(:save)
+ end
+
+ it "runs successfully without enabling the audit runner" do
+ client.run
+
+ # fork is stubbed, so we can see the outcome of the run
+ expect(node.automatic_attrs[:platform]).to eq("example-platform")
+ expect(node.automatic_attrs[:platform_version]).to eq("example-platform-1.0")
+ end
+ end
+ end
+
+ describe "when audits are disabled" do
+ include_context "a client run" do
+
+ before do
+ Chef::Config[:audit_mode] = :disabled
+ end
+
+ def stub_for_audit
+ expect(Chef::Audit::Runner).to_not receive(:new)
+ end
+
+ it "runs successfully without enabling the audit runner" do
+ client.run
+
+ # fork is stubbed, so we can see the outcome of the run
+ expect(node.automatic_attrs[:platform]).to eq("example-platform")
+ expect(node.automatic_attrs[:platform_version]).to eq("example-platform-1.0")
+ end
+ end
+ end
+
end
diff --git a/spec/unit/dsl/audit_spec.rb b/spec/unit/dsl/audit_spec.rb
new file mode 100644
index 0000000000..38707127f0
--- /dev/null
+++ b/spec/unit/dsl/audit_spec.rb
@@ -0,0 +1,43 @@
+
+require 'spec_helper'
+require 'chef/dsl/audit'
+
+class AuditDSLTester < Chef::Recipe
+ include Chef::DSL::Audit
+end
+
+class BadAuditDSLTester
+ include Chef::DSL::Audit
+end
+
+describe Chef::DSL::Audit do
+ let(:auditor) { AuditDSLTester.new("cookbook_name", "recipe_name", run_context) }
+ let(:run_context) { instance_double(Chef::RunContext, :audits => audits, :cookbook_collection => cookbook_collection) }
+ let(:audits) { {} }
+ let(:cookbook_collection) { {} }
+
+ it "raises an error when a block of audits is not provided" do
+ expect{ auditor.controls "name" }.to raise_error(Chef::Exceptions::NoAuditsProvided)
+ end
+
+ it "raises an error when no audit name is given" do
+ expect{ auditor.controls do end }.to raise_error(Chef::Exceptions::AuditNameMissing)
+ end
+
+ context "audits already populated" do
+ let(:audits) { {"unique" => {} } }
+
+ it "raises an error if the audit name is a duplicate" do
+ expect { auditor.controls "unique" do end }.to raise_error(Chef::Exceptions::AuditControlGroupDuplicate)
+ end
+ end
+
+ context "included in a class without recipe DSL" do
+ let(:auditor) { BadAuditDSLTester.new }
+
+ it "fails because it relies on the recipe DSL existing" do
+ expect { auditor.controls "unique" do end }.to raise_error(NoMethodError, /undefined method `cookbook_name'/)
+ end
+ end
+
+end
diff --git a/spec/unit/exceptions_spec.rb b/spec/unit/exceptions_spec.rb
index 6318ec9227..d35ecc8ec8 100644
--- a/spec/unit/exceptions_spec.rb
+++ b/spec/unit/exceptions_spec.rb
@@ -81,4 +81,50 @@ describe Chef::Exceptions do
end
end
end
+
+ describe Chef::Exceptions::RunFailedWrappingError do
+ shared_examples "RunFailedWrappingError expectations" do
+ it "should initialize with a default message" do
+ expect(e.message).to eq("Found #{num_errors} errors, they are stored in the backtrace")
+ end
+
+ it "should provide a modified backtrace when requested" do
+ e.fill_backtrace
+ expect(e.backtrace).to eq(backtrace)
+ end
+ end
+
+ context "initialized with nothing" do
+ let(:e) { Chef::Exceptions::RunFailedWrappingError.new }
+ let(:num_errors) { 0 }
+ let(:backtrace) { [] }
+
+ include_examples "RunFailedWrappingError expectations"
+ end
+
+ context "initialized with nil" do
+ let(:e) { Chef::Exceptions::RunFailedWrappingError.new(nil, nil) }
+ let(:num_errors) { 0 }
+ let(:backtrace) { [] }
+
+ include_examples "RunFailedWrappingError expectations"
+ end
+
+ context "initialized with 1 error and nil" do
+ let(:e) { Chef::Exceptions::RunFailedWrappingError.new(RuntimeError.new("foo"), nil) }
+ let(:num_errors) { 1 }
+ let(:backtrace) { ["1) RuntimeError - foo", ""] }
+
+ include_examples "RunFailedWrappingError expectations"
+ end
+
+ context "initialized with 2 errors" do
+ let(:e) { Chef::Exceptions::RunFailedWrappingError.new(RuntimeError.new("foo"), RuntimeError.new("bar")) }
+ let(:num_errors) { 2 }
+ let(:backtrace) { ["1) RuntimeError - foo", "", "2) RuntimeError - bar", ""] }
+
+ include_examples "RunFailedWrappingError expectations"
+ end
+
+ end
end
diff --git a/spec/unit/http_spec.rb b/spec/unit/http_spec.rb
index 60d36eb4a0..ddfc56583d 100644
--- a/spec/unit/http_spec.rb
+++ b/spec/unit/http_spec.rb
@@ -44,6 +44,13 @@ describe Chef::HTTP do
expect(http.create_url('///api/endpoint?url=http://foo.bar')).to eql(URI.parse('http://www.getchef.com/organization/org/api/endpoint?url=http://foo.bar'))
end
+ # As per: https://github.com/opscode/chef/issues/2500
+ it 'should treat scheme part of the URI in a case-insensitive manner' do
+ http = Chef::HTTP.allocate # Calling Chef::HTTP::new sets @url, don't want that.
+ expect { http.create_url('HTTP://www1.chef.io/') }.not_to raise_error
+ expect(http.create_url('HTTP://www2.chef.io/')).to eql(URI.parse('http://www2.chef.io/'))
+ end
+
end # create_url
describe "head" do
diff --git a/spec/unit/knife/cookbook_site_install_spec.rb b/spec/unit/knife/cookbook_site_install_spec.rb
index c4bd8f67d2..b3eef32b39 100644
--- a/spec/unit/knife/cookbook_site_install_spec.rb
+++ b/spec/unit/knife/cookbook_site_install_spec.rb
@@ -19,132 +19,173 @@
require File.expand_path(File.join(File.dirname(__FILE__), "..", "..", "spec_helper"))
describe Chef::Knife::CookbookSiteInstall do
+ let(:knife) { Chef::Knife::CookbookSiteInstall.new }
+ let(:stdout) { StringIO.new }
+ let(:stderr) { StringIO.new }
+ let(:downloader) { Hash.new }
+ let(:repo) { double(:sanity_check => true, :reset_to_default_state => true,
+ :prepare_to_import => true, :finalize_updates_to => true,
+ :merge_updates_from => true) }
+ let(:install_path) { if Chef::Platform.windows?
+ 'C:/tmp/chef'
+ else
+ '/var/tmp/chef'
+ end }
+
before(:each) do
require 'chef/knife/core/cookbook_scm_repo'
- @stdout = StringIO.new
- @knife = Chef::Knife::CookbookSiteInstall.new
- allow(@knife.ui).to receive(:stdout).and_return(@stdout)
- @knife.config = {}
- if Chef::Platform.windows?
- @install_path = 'C:/tmp/chef'
- else
- @install_path = '/var/tmp/chef'
- end
- @knife.config[:cookbook_path] = [ @install_path ]
-
- @stdout = StringIO.new
- @stderr = StringIO.new
- allow(@knife).to receive(:stderr).and_return(@stdout)
- allow(@knife).to receive(:stdout).and_return(@stdout)
-
- #Assume all external commands would have succeed. :(
+
+ allow(knife.ui).to receive(:stdout).and_return(stdout)
+ knife.config = {}
+ knife.config[:cookbook_path] = [ install_path ]
+
+ allow(knife).to receive(:stderr).and_return(stderr)
+ allow(knife).to receive(:stdout).and_return(stdout)
+
+ # Assume all external commands would have succeed. :(
allow(File).to receive(:unlink)
allow(File).to receive(:rmtree)
- allow(@knife).to receive(:shell_out!).and_return(true)
-
- #CookbookSiteDownload Stup
- @downloader = {}
- allow(@knife).to receive(:download_cookbook_to).and_return(@downloader)
- allow(@downloader).to receive(:version) do
- if @knife.name_args.size == 2
- @knife.name_args[1]
+ allow(knife).to receive(:shell_out!).and_return(true)
+
+ # CookbookSiteDownload Stup
+ allow(knife).to receive(:download_cookbook_to).and_return(downloader)
+ allow(downloader).to receive(:version) do
+ if knife.name_args.size == 2
+ knife.name_args[1]
else
"0.3.0"
end
end
- #Stubs for CookbookSCMRepo
- @repo = double(:sanity_check => true, :reset_to_default_state => true,
- :prepare_to_import => true, :finalize_updates_to => true,
- :merge_updates_from => true)
- allow(Chef::Knife::CookbookSCMRepo).to receive(:new).and_return(@repo)
+ # Stubs for CookbookSCMRepo
+ allow(Chef::Knife::CookbookSCMRepo).to receive(:new).and_return(repo)
end
-
describe "run" do
- it "should return an error if a cookbook name is not provided" do
- @knife.name_args = []
- expect(@knife.ui).to receive(:error).with("Please specify a cookbook to download and install.")
- expect { @knife.run }.to raise_error(SystemExit)
- end
-
- it "should return an error if more than two arguments are given" do
- @knife.name_args = ["foo", "bar", "baz"]
- expect(@knife.ui).to receive(:error).with("Installing multiple cookbooks at once is not supported.")
- expect { @knife.run }.to raise_error(SystemExit)
- end
-
- it "should return an error if the second argument is not a version" do
- @knife.name_args = ["getting-started", "1pass"]
- expect(@knife.ui).to receive(:error).with("Installing multiple cookbooks at once is not supported.")
- expect { @knife.run }.to raise_error(SystemExit)
- end
-
- it "should return an error if the second argument is a four-digit version" do
- @knife.name_args = ["getting-started", "0.0.0.1"]
- expect(@knife.ui).to receive(:error).with("Installing multiple cookbooks at once is not supported.")
- expect { @knife.run }.to raise_error(SystemExit)
- end
-
- it "should return an error if the second argument is a one-digit version" do
- @knife.name_args = ["getting-started", "1"]
- expect(@knife.ui).to receive(:error).with("Installing multiple cookbooks at once is not supported.")
- expect { @knife.run }.to raise_error(SystemExit)
- end
-
- it "should install the specified version if second argument is a three-digit version" do
- @knife.name_args = ["getting-started", "0.1.0"]
- @knife.config[:no_deps] = true
- upstream_file = File.join(@install_path, "getting-started.tar.gz")
- expect(@knife).to receive(:download_cookbook_to).with(upstream_file)
- expect(@knife).to receive(:extract_cookbook).with(upstream_file, "0.1.0")
- expect(@knife).to receive(:clear_existing_files).with(File.join(@install_path, "getting-started"))
- expect(@repo).to receive(:merge_updates_from).with("getting-started", "0.1.0")
- @knife.run
- end
-
- it "should install the specified version if second argument is a two-digit version" do
- @knife.name_args = ["getting-started", "0.1"]
- @knife.config[:no_deps] = true
- upstream_file = File.join(@install_path, "getting-started.tar.gz")
- expect(@knife).to receive(:download_cookbook_to).with(upstream_file)
- expect(@knife).to receive(:extract_cookbook).with(upstream_file, "0.1")
- expect(@knife).to receive(:clear_existing_files).with(File.join(@install_path, "getting-started"))
- expect(@repo).to receive(:merge_updates_from).with("getting-started", "0.1")
- @knife.run
- end
-
- it "should install the latest version if only a cookbook name is given" do
- @knife.name_args = ["getting-started"]
- @knife.config[:no_deps] = true
- upstream_file = File.join(@install_path, "getting-started.tar.gz")
- expect(@knife).to receive(:download_cookbook_to).with(upstream_file)
- expect(@knife).to receive(:extract_cookbook).with(upstream_file, "0.3.0")
- expect(@knife).to receive(:clear_existing_files).with(File.join(@install_path, "getting-started"))
- expect(@repo).to receive(:merge_updates_from).with("getting-started", "0.3.0")
- @knife.run
- end
-
- it "should not create/reset git branches if use_current_branch is set" do
- @knife.name_args = ["getting-started"]
- @knife.config[:use_current_branch] = true
- @knife.config[:no_deps] = true
- upstream_file = File.join(@install_path, "getting-started.tar.gz")
- expect(@repo).not_to receive(:prepare_to_import)
- expect(@repo).not_to receive(:reset_to_default_state)
- @knife.run
- end
-
- it "should not raise an error if cookbook_path is a string" do
- @knife.config[:cookbook_path] = @install_path
- @knife.config[:no_deps] = true
- @knife.name_args = ["getting-started"]
- upstream_file = File.join(@install_path, "getting-started.tar.gz")
- expect(@knife).to receive(:download_cookbook_to).with(upstream_file)
- expect(@knife).to receive(:extract_cookbook).with(upstream_file, "0.3.0")
- expect(@knife).to receive(:clear_existing_files).with(File.join(@install_path, "getting-started"))
- expect(@repo).to receive(:merge_updates_from).with("getting-started", "0.3.0")
- expect { @knife.run }.not_to raise_error
+ it "raises an error if a cookbook name is not provided" do
+ knife.name_args = []
+ expect(knife.ui).to receive(:error).with("Please specify a cookbook to download and install.")
+ expect { knife.run }.to raise_error(SystemExit)
+ end
+
+ it "raises an error if more than two arguments are given" do
+ knife.name_args = ["foo", "bar", "baz"]
+ expect(knife.ui).to receive(:error).with("Installing multiple cookbooks at once is not supported.")
+ expect { knife.run }.to raise_error(SystemExit)
+ end
+
+ it "raises an error if the second argument is not a version" do
+ knife.name_args = ["getting-started", "1pass"]
+ expect(knife.ui).to receive(:error).with("Installing multiple cookbooks at once is not supported.")
+ expect { knife.run }.to raise_error(SystemExit)
+ end
+
+ it "raises an error if the second argument is a four-digit version" do
+ knife.name_args = ["getting-started", "0.0.0.1"]
+ expect(knife.ui).to receive(:error).with("Installing multiple cookbooks at once is not supported.")
+ expect { knife.run }.to raise_error(SystemExit)
+ end
+
+ it "raises an error if the second argument is a one-digit version" do
+ knife.name_args = ["getting-started", "1"]
+ expect(knife.ui).to receive(:error).with("Installing multiple cookbooks at once is not supported.")
+ expect { knife.run }.to raise_error(SystemExit)
+ end
+
+ it "installs the specified version if second argument is a three-digit version" do
+ knife.name_args = ["getting-started", "0.1.0"]
+ knife.config[:no_deps] = true
+ upstream_file = File.join(install_path, "getting-started.tar.gz")
+ expect(knife).to receive(:download_cookbook_to).with(upstream_file)
+ expect(knife).to receive(:extract_cookbook).with(upstream_file, "0.1.0")
+ expect(knife).to receive(:clear_existing_files).with(File.join(install_path, "getting-started"))
+ expect(repo).to receive(:merge_updates_from).with("getting-started", "0.1.0")
+ knife.run
+ end
+
+ it "installs the specified version if second argument is a two-digit version" do
+ knife.name_args = ["getting-started", "0.1"]
+ knife.config[:no_deps] = true
+ upstream_file = File.join(install_path, "getting-started.tar.gz")
+ expect(knife).to receive(:download_cookbook_to).with(upstream_file)
+ expect(knife).to receive(:extract_cookbook).with(upstream_file, "0.1")
+ expect(knife).to receive(:clear_existing_files).with(File.join(install_path, "getting-started"))
+ expect(repo).to receive(:merge_updates_from).with("getting-started", "0.1")
+ knife.run
+ end
+
+ it "installs the latest version if only a cookbook name is given" do
+ knife.name_args = ["getting-started"]
+ knife.config[:no_deps] = true
+ upstream_file = File.join(install_path, "getting-started.tar.gz")
+ expect(knife).to receive(:download_cookbook_to).with(upstream_file)
+ expect(knife).to receive(:extract_cookbook).with(upstream_file, "0.3.0")
+ expect(knife).to receive(:clear_existing_files).with(File.join(install_path, "getting-started"))
+ expect(repo).to receive(:merge_updates_from).with("getting-started", "0.3.0")
+ knife.run
+ end
+
+ it "does not create/reset git branches if use_current_branch is set" do
+ knife.name_args = ["getting-started"]
+ knife.config[:use_current_branch] = true
+ knife.config[:no_deps] = true
+ upstream_file = File.join(install_path, "getting-started.tar.gz")
+ expect(repo).not_to receive(:prepare_to_import)
+ expect(repo).not_to receive(:reset_to_default_state)
+ knife.run
end
+
+ it "does not raise an error if cookbook_path is a string" do
+ knife.config[:cookbook_path] = install_path
+ knife.config[:no_deps] = true
+ knife.name_args = ["getting-started"]
+ upstream_file = File.join(install_path, "getting-started.tar.gz")
+ expect(knife).to receive(:download_cookbook_to).with(upstream_file)
+ expect(knife).to receive(:extract_cookbook).with(upstream_file, "0.3.0")
+ expect(knife).to receive(:clear_existing_files).with(File.join(install_path, "getting-started"))
+ expect(repo).to receive(:merge_updates_from).with("getting-started", "0.3.0")
+ expect { knife.run }.not_to raise_error
+ end
+ end # end of run
+
+ let(:metadata) { Chef::Cookbook::Metadata.new }
+ let(:rb_metadata_path) { File.join(install_path, "post-punk-kitchen", "metadata.rb") }
+ let(:json_metadata_path) { File.join(install_path, "post-punk-kitchen", "metadata.json") }
+
+ describe "preferred_metadata" do
+ before do
+ allow(Chef::Cookbook::Metadata).to receive(:new).and_return(metadata)
+ allow(File).to receive(:exist?).and_return(false)
+ knife.instance_variable_set(:@cookbook_name, "post-punk-kitchen")
+ knife.instance_variable_set(:@install_path, install_path)
+ end
+
+ it "returns a populated Metadata object if metadata.rb exists" do
+ allow(File).to receive(:exist?).with(rb_metadata_path).and_return(true)
+ expect(metadata).to receive(:from_file).with(rb_metadata_path)
+ knife.preferred_metadata
+ end
+
+ it "returns a populated Metadata object if metadata.json exists" do
+ allow(File).to receive(:exist?).with(json_metadata_path).and_return(true)
+ #expect(IO).to receive(:read).with(json_metadata_path)
+ allow(IO).to receive(:read)
+ expect(metadata).to receive(:from_json)
+ knife.preferred_metadata
+ end
+
+ it "prefers metadata.rb over metadata.json" do
+ allow(File).to receive(:exist?).with(rb_metadata_path).and_return(true)
+ allow(File).to receive(:exist?).with(json_metadata_path).and_return(true)
+ allow(IO).to receive(:read)
+ expect(metadata).to receive(:from_file).with(rb_metadata_path)
+ expect(metadata).not_to receive(:from_json)
+ knife.preferred_metadata
+ end
+
+ it "rasies an error if it finds no metadata file" do
+ expect { knife.preferred_metadata }.to raise_error(Chef::Exceptions::MetadataNotFound)
+ end
+
end
end
diff --git a/spec/unit/knife/ssl_fetch_spec.rb b/spec/unit/knife/ssl_fetch_spec.rb
index 24101dbe7a..cd0e423459 100644
--- a/spec/unit/knife/ssl_fetch_spec.rb
+++ b/spec/unit/knife/ssl_fetch_spec.rb
@@ -130,22 +130,55 @@ E
before do
Chef::Config.trusted_certs_dir = trusted_certs_dir
-
- expect(TCPSocket).to receive(:new).with("foo.example.com", 8443).and_return(tcp_socket)
- expect(OpenSSL::SSL::SSLSocket).to receive(:new).with(tcp_socket, ssl_fetch.noverify_peer_ssl_context).and_return(ssl_socket)
- expect(ssl_socket).to receive(:connect)
- expect(ssl_socket).to receive(:peer_cert_chain).and_return([self_signed_crt])
end
after do
FileUtils.rm_rf(trusted_certs_dir)
end
- it "fetches the cert chain and writes the certs to the trusted_certs_dir" do
- run
- stored_cert_path = File.join(trusted_certs_dir, "example_local.crt")
- expect(File).to exist(stored_cert_path)
- expect(File.read(stored_cert_path)).to eq(File.read(self_signed_crt_path))
+ context "when the TLS connection is successful" do
+
+ before do
+ expect(TCPSocket).to receive(:new).with("foo.example.com", 8443).and_return(tcp_socket)
+ expect(OpenSSL::SSL::SSLSocket).to receive(:new).with(tcp_socket, ssl_fetch.noverify_peer_ssl_context).and_return(ssl_socket)
+ expect(ssl_socket).to receive(:connect)
+ expect(ssl_socket).to receive(:peer_cert_chain).and_return([self_signed_crt])
+ end
+
+ it "fetches the cert chain and writes the certs to the trusted_certs_dir" do
+ run
+ stored_cert_path = File.join(trusted_certs_dir, "example_local.crt")
+ expect(File).to exist(stored_cert_path)
+ expect(File.read(stored_cert_path)).to eq(File.read(self_signed_crt_path))
+ end
+
+ end
+
+ context "when connecting to a non-SSL service (like HTTP)" do
+
+ let(:name_args) { %w{http://foo.example.com} }
+
+ let(:unknown_protocol_error) { OpenSSL::SSL::SSLError.new("SSL_connect returned=1 errno=0 state=SSLv2/v3 read server hello A: unknown protocol") }
+
+ before do
+ expect(TCPSocket).to receive(:new).with("foo.example.com", 80).and_return(tcp_socket)
+ expect(OpenSSL::SSL::SSLSocket).to receive(:new).with(tcp_socket, ssl_fetch.noverify_peer_ssl_context).and_return(ssl_socket)
+ expect(ssl_socket).to receive(:connect).and_raise(unknown_protocol_error)
+
+ expect(ssl_fetch).to receive(:exit).with(1)
+ end
+
+ it "tells the user their URL is for a non-ssl service" do
+ expected_error_text = <<-ERROR_TEXT
+ERROR: The service at the given URI (http://foo.example.com) does not accept SSL connections
+ERROR: Perhaps you meant to connect to 'https://foo.example.com'?
+ERROR_TEXT
+
+ run
+ expect(stderr).to include(expected_error_text)
+ end
+
end
+
end
end
diff --git a/spec/unit/knife_spec.rb b/spec/unit/knife_spec.rb
index 8f652e58c7..2ccf8493ad 100644
--- a/spec/unit/knife_spec.rb
+++ b/spec/unit/knife_spec.rb
@@ -25,6 +25,11 @@ require 'spec_helper'
require 'uri'
describe Chef::Knife do
+
+ let(:stderr) { StringIO.new }
+
+ let(:knife) { Chef::Knife.new }
+
before(:each) do
Chef::Log.logger = Logger.new(StringIO.new)
@@ -32,16 +37,14 @@ describe Chef::Knife do
# Prevent gratuitous code reloading:
allow(Chef::Knife).to receive(:load_commands)
- @knife = Chef::Knife.new
- allow(@knife.ui).to receive(:puts)
- allow(@knife.ui).to receive(:print)
+ allow(knife.ui).to receive(:puts)
+ allow(knife.ui).to receive(:print)
allow(Chef::Log).to receive(:init)
allow(Chef::Log).to receive(:level)
[:debug, :info, :warn, :error, :crit].each do |level_sym|
allow(Chef::Log).to receive(level_sym)
end
allow(Chef::Knife).to receive(:puts)
- @stderr = StringIO.new
end
after(:each) do
@@ -217,7 +220,10 @@ describe Chef::Knife do
end
it "exits if no subcommand matches the CLI args" do
- allow(Chef::Knife.ui).to receive(:stderr).and_return(@stderr)
+ stdout = StringIO.new
+
+ allow(Chef::Knife.ui).to receive(:stderr).and_return(stderr)
+ allow(Chef::Knife.ui).to receive(:stdout).and_return(stdout)
expect(Chef::Knife.ui).to receive(:fatal)
expect {Chef::Knife.run(%w{fuuu uuuu fuuuu})}.to raise_error(SystemExit) { |e| expect(e.status).not_to eq(0) }
end
@@ -269,157 +275,181 @@ describe Chef::Knife do
let(:fake_config) { "/does/not/exist/knife.rb" }
before do
- @knife.config[:verbosity] = 1
- @knife.config[:config_file] = fake_config
+ knife.config[:verbosity] = 1
+ knife.config[:config_file] = fake_config
config_loader = double("Chef::WorkstationConfigLoader", :load => true, :no_config_found? => false, :chef_config_dir => "/etc/chef", :config_location => fake_config)
allow(config_loader).to receive(:explicit_config_file=).with(fake_config).and_return(fake_config)
allow(Chef::WorkstationConfigLoader).to receive(:new).and_return(config_loader)
end
it "prints the path to the configuration file used" do
- @stdout, @stderr, @stdin = StringIO.new, StringIO.new, StringIO.new
- @knife.ui = Chef::Knife::UI.new(@stdout, @stderr, @stdin, {})
+ stdout, stderr, stdin = StringIO.new, StringIO.new, StringIO.new
+ knife.ui = Chef::Knife::UI.new(stdout, stderr, stdin, {})
expect(Chef::Log).to receive(:info).with("Using configuration from #{fake_config}")
- @knife.configure_chef
+ knife.configure_chef
end
end
end
end
describe "when first created" do
+
+ let(:knife) { KnifeSpecs::TestYourself.new(%w{with some args -s scrogramming}) }
+
before do
unless KnifeSpecs.const_defined?(:TestYourself)
Kernel.load(File.join(CHEF_SPEC_DATA, 'knife_subcommand', 'test_yourself.rb'))
end
- @knife = KnifeSpecs::TestYourself.new(%w{with some args -s scrogramming})
end
it "it parses the options passed to it" do
- expect(@knife.config[:scro]).to eq('scrogramming')
+ expect(knife.config[:scro]).to eq('scrogramming')
end
it "extracts its command specific args from the full arg list" do
- expect(@knife.name_args).to eq(%w{with some args})
+ expect(knife.name_args).to eq(%w{with some args})
end
it "does not have lazy dependencies loaded" do
- expect(@knife.class.test_deps_loaded).not_to be_truthy
+ expect(knife.class.test_deps_loaded).not_to be_truthy
end
end
describe "when formatting exceptions" do
+
+ let(:stdout) { StringIO.new }
+ let(:stderr) { StringIO.new }
+ let(:stdin) { StringIO.new }
+
+ let(:ui) { Chef::Knife::UI.new(stdout, stderr, stdin, {}) }
+
before do
- @stdout, @stderr, @stdin = StringIO.new, StringIO.new, StringIO.new
- @knife.ui = Chef::Knife::UI.new(@stdout, @stderr, @stdin, {})
- expect(@knife).to receive(:exit).with(100)
+ knife.ui = ui
+ expect(knife).to receive(:exit).with(100)
end
it "formats 401s nicely" do
response = Net::HTTPUnauthorized.new("1.1", "401", "Unauthorized")
response.instance_variable_set(:@read, true) # I hate you, net/http.
allow(response).to receive(:body).and_return(Chef::JSONCompat.to_json(:error => "y u no syncronize your clock?"))
- allow(@knife).to receive(:run).and_raise(Net::HTTPServerException.new("401 Unauthorized", response))
- @knife.run_with_pretty_exceptions
- expect(@stderr.string).to match(/ERROR: Failed to authenticate to/)
- expect(@stderr.string).to match(/Response: y u no syncronize your clock\?/)
+ allow(knife).to receive(:run).and_raise(Net::HTTPServerException.new("401 Unauthorized", response))
+ knife.run_with_pretty_exceptions
+ expect(stderr.string).to match(/ERROR: Failed to authenticate to/)
+ expect(stderr.string).to match(/Response: y u no syncronize your clock\?/)
end
it "formats 403s nicely" do
response = Net::HTTPForbidden.new("1.1", "403", "Forbidden")
response.instance_variable_set(:@read, true) # I hate you, net/http.
allow(response).to receive(:body).and_return(Chef::JSONCompat.to_json(:error => "y u no administrator"))
- allow(@knife).to receive(:run).and_raise(Net::HTTPServerException.new("403 Forbidden", response))
- allow(@knife).to receive(:username).and_return("sadpanda")
- @knife.run_with_pretty_exceptions
- expect(@stderr.string).to match(%r[ERROR: You authenticated successfully to http.+ as sadpanda but you are not authorized for this action])
- expect(@stderr.string).to match(%r[Response: y u no administrator])
+ allow(knife).to receive(:run).and_raise(Net::HTTPServerException.new("403 Forbidden", response))
+ allow(knife).to receive(:username).and_return("sadpanda")
+ knife.run_with_pretty_exceptions
+ expect(stderr.string).to match(%r[ERROR: You authenticated successfully to http.+ as sadpanda but you are not authorized for this action])
+ expect(stderr.string).to match(%r[Response: y u no administrator])
end
it "formats 400s nicely" do
response = Net::HTTPBadRequest.new("1.1", "400", "Bad Request")
response.instance_variable_set(:@read, true) # I hate you, net/http.
allow(response).to receive(:body).and_return(Chef::JSONCompat.to_json(:error => "y u search wrong"))
- allow(@knife).to receive(:run).and_raise(Net::HTTPServerException.new("400 Bad Request", response))
- @knife.run_with_pretty_exceptions
- expect(@stderr.string).to match(%r[ERROR: The data in your request was invalid])
- expect(@stderr.string).to match(%r[Response: y u search wrong])
+ allow(knife).to receive(:run).and_raise(Net::HTTPServerException.new("400 Bad Request", response))
+ knife.run_with_pretty_exceptions
+ expect(stderr.string).to match(%r[ERROR: The data in your request was invalid])
+ expect(stderr.string).to match(%r[Response: y u search wrong])
end
it "formats 404s nicely" do
response = Net::HTTPNotFound.new("1.1", "404", "Not Found")
response.instance_variable_set(:@read, true) # I hate you, net/http.
allow(response).to receive(:body).and_return(Chef::JSONCompat.to_json(:error => "nothing to see here"))
- allow(@knife).to receive(:run).and_raise(Net::HTTPServerException.new("404 Not Found", response))
- @knife.run_with_pretty_exceptions
- expect(@stderr.string).to match(%r[ERROR: The object you are looking for could not be found])
- expect(@stderr.string).to match(%r[Response: nothing to see here])
+ allow(knife).to receive(:run).and_raise(Net::HTTPServerException.new("404 Not Found", response))
+ knife.run_with_pretty_exceptions
+ expect(stderr.string).to match(%r[ERROR: The object you are looking for could not be found])
+ expect(stderr.string).to match(%r[Response: nothing to see here])
end
it "formats 500s nicely" do
response = Net::HTTPInternalServerError.new("1.1", "500", "Internal Server Error")
response.instance_variable_set(:@read, true) # I hate you, net/http.
allow(response).to receive(:body).and_return(Chef::JSONCompat.to_json(:error => "sad trombone"))
- allow(@knife).to receive(:run).and_raise(Net::HTTPFatalError.new("500 Internal Server Error", response))
- @knife.run_with_pretty_exceptions
- expect(@stderr.string).to match(%r[ERROR: internal server error])
- expect(@stderr.string).to match(%r[Response: sad trombone])
+ allow(knife).to receive(:run).and_raise(Net::HTTPFatalError.new("500 Internal Server Error", response))
+ knife.run_with_pretty_exceptions
+ expect(stderr.string).to match(%r[ERROR: internal server error])
+ expect(stderr.string).to match(%r[Response: sad trombone])
end
it "formats 502s nicely" do
response = Net::HTTPBadGateway.new("1.1", "502", "Bad Gateway")
response.instance_variable_set(:@read, true) # I hate you, net/http.
allow(response).to receive(:body).and_return(Chef::JSONCompat.to_json(:error => "sadder trombone"))
- allow(@knife).to receive(:run).and_raise(Net::HTTPFatalError.new("502 Bad Gateway", response))
- @knife.run_with_pretty_exceptions
- expect(@stderr.string).to match(%r[ERROR: bad gateway])
- expect(@stderr.string).to match(%r[Response: sadder trombone])
+ allow(knife).to receive(:run).and_raise(Net::HTTPFatalError.new("502 Bad Gateway", response))
+ knife.run_with_pretty_exceptions
+ expect(stderr.string).to match(%r[ERROR: bad gateway])
+ expect(stderr.string).to match(%r[Response: sadder trombone])
end
it "formats 503s nicely" do
response = Net::HTTPServiceUnavailable.new("1.1", "503", "Service Unavailable")
response.instance_variable_set(:@read, true) # I hate you, net/http.
allow(response).to receive(:body).and_return(Chef::JSONCompat.to_json(:error => "saddest trombone"))
- allow(@knife).to receive(:run).and_raise(Net::HTTPFatalError.new("503 Service Unavailable", response))
- @knife.run_with_pretty_exceptions
- expect(@stderr.string).to match(%r[ERROR: Service temporarily unavailable])
- expect(@stderr.string).to match(%r[Response: saddest trombone])
+ allow(knife).to receive(:run).and_raise(Net::HTTPFatalError.new("503 Service Unavailable", response))
+ knife.run_with_pretty_exceptions
+ expect(stderr.string).to match(%r[ERROR: Service temporarily unavailable])
+ expect(stderr.string).to match(%r[Response: saddest trombone])
end
it "formats other HTTP errors nicely" do
response = Net::HTTPPaymentRequired.new("1.1", "402", "Payment Required")
response.instance_variable_set(:@read, true) # I hate you, net/http.
allow(response).to receive(:body).and_return(Chef::JSONCompat.to_json(:error => "nobugfixtillyoubuy"))
- allow(@knife).to receive(:run).and_raise(Net::HTTPServerException.new("402 Payment Required", response))
- @knife.run_with_pretty_exceptions
- expect(@stderr.string).to match(%r[ERROR: Payment Required])
- expect(@stderr.string).to match(%r[Response: nobugfixtillyoubuy])
+ allow(knife).to receive(:run).and_raise(Net::HTTPServerException.new("402 Payment Required", response))
+ knife.run_with_pretty_exceptions
+ expect(stderr.string).to match(%r[ERROR: Payment Required])
+ expect(stderr.string).to match(%r[Response: nobugfixtillyoubuy])
end
it "formats NameError and NoMethodError nicely" do
- allow(@knife).to receive(:run).and_raise(NameError.new("Undefined constant FUUU"))
- @knife.run_with_pretty_exceptions
- expect(@stderr.string).to match(%r[ERROR: knife encountered an unexpected error])
- expect(@stderr.string).to match(%r[This may be a bug in the 'knife' knife command or plugin])
- expect(@stderr.string).to match(%r[Exception: NameError: Undefined constant FUUU])
+ allow(knife).to receive(:run).and_raise(NameError.new("Undefined constant FUUU"))
+ knife.run_with_pretty_exceptions
+ expect(stderr.string).to match(%r[ERROR: knife encountered an unexpected error])
+ expect(stderr.string).to match(%r[This may be a bug in the 'knife' knife command or plugin])
+ expect(stderr.string).to match(%r[Exception: NameError: Undefined constant FUUU])
end
it "formats missing private key errors nicely" do
- allow(@knife).to receive(:run).and_raise(Chef::Exceptions::PrivateKeyMissing.new('key not there'))
- allow(@knife).to receive(:api_key).and_return("/home/root/.chef/no-key-here.pem")
- @knife.run_with_pretty_exceptions
- expect(@stderr.string).to match(%r[ERROR: Your private key could not be loaded from /home/root/.chef/no-key-here.pem])
- expect(@stderr.string).to match(%r[Check your configuration file and ensure that your private key is readable])
+ allow(knife).to receive(:run).and_raise(Chef::Exceptions::PrivateKeyMissing.new('key not there'))
+ allow(knife).to receive(:api_key).and_return("/home/root/.chef/no-key-here.pem")
+ knife.run_with_pretty_exceptions
+ expect(stderr.string).to match(%r[ERROR: Your private key could not be loaded from /home/root/.chef/no-key-here.pem])
+ expect(stderr.string).to match(%r[Check your configuration file and ensure that your private key is readable])
end
it "formats connection refused errors nicely" do
- allow(@knife).to receive(:run).and_raise(Errno::ECONNREFUSED.new('y u no shut up'))
- @knife.run_with_pretty_exceptions
+ allow(knife).to receive(:run).and_raise(Errno::ECONNREFUSED.new('y u no shut up'))
+ knife.run_with_pretty_exceptions
# Errno::ECONNREFUSED message differs by platform
# *nix = Errno::ECONNREFUSED: Connection refused
# win32: Errno::ECONNREFUSED: No connection could be made because the target machine actively refused it.
- expect(@stderr.string).to match(%r[ERROR: Network Error: .* - y u no shut up])
- expect(@stderr.string).to match(%r[Check your knife configuration and network settings])
+ expect(stderr.string).to match(%r[ERROR: Network Error: .* - y u no shut up])
+ expect(stderr.string).to match(%r[Check your knife configuration and network settings])
end
+
+ it "formats SSL errors nicely and suggests to use `knife ssl check` and `knife ssl fetch`" do
+ error = OpenSSL::SSL::SSLError.new("SSL_connect returned=1 errno=0 state=SSLv3 read server certificate B: certificate verify failed")
+ allow(knife).to receive(:run).and_raise(error)
+
+ knife.run_with_pretty_exceptions
+
+ expected_message=<<-MSG
+ERROR: Could not establish a secure connection to the server.
+Use `knife ssl check` to troubleshoot your SSL configuration.
+If your Chef Server uses a self-signed certificate, you can use
+`knife ssl fetch` to make knife trust the server's certificates.
+MSG
+ expect(stderr.string).to include(expected_message)
+ end
+
end
end
diff --git a/spec/unit/mixin/shell_out_spec.rb b/spec/unit/mixin/shell_out_spec.rb
index afce4dc826..3dc9d42574 100644
--- a/spec/unit/mixin/shell_out_spec.rb
+++ b/spec/unit/mixin/shell_out_spec.rb
@@ -23,10 +23,10 @@
require 'spec_helper'
describe Chef::Mixin::ShellOut do
- include Chef::Mixin::ShellOut
-
+ let(:shell_out_class) { Class.new { include Chef::Mixin::ShellOut } }
+ subject(:shell_out_obj) { shell_out_class.new }
describe '#run_command_compatible_options' do
- subject { run_command_compatible_options(command_args) }
+ subject { shell_out_obj.run_command_compatible_options(command_args) }
let(:command_args) { [ cmd, options ] }
let(:cmd) { "echo '#{rand(1000)}'" }
@@ -117,7 +117,6 @@ describe Chef::Mixin::ShellOut do
ENV.update(@original_env)
end
- let(:shell_out) { Chef::Mixin::ShellOut }
let(:cmd) { "echo '#{rand(1000)}'" }
describe "#shell_out" do
@@ -126,30 +125,30 @@ describe Chef::Mixin::ShellOut do
describe "and environment is an option" do
it "should not change environment['LC_ALL'] when set to nil" do
options = { :environment => { 'LC_ALL' => nil } }
- expect(shell_out).to receive(:shell_out_command).with(cmd, options).and_return(true)
- shell_out.shell_out(cmd, options)
+ expect(shell_out_obj).to receive(:shell_out_command).with(cmd, options).and_return(true)
+ shell_out_obj.shell_out(cmd, options)
end
it "should not change environment['LC_ALL'] when set to non-nil" do
options = { :environment => { 'LC_ALL' => 'en_US.UTF-8' } }
- expect(shell_out).to receive(:shell_out_command).with(cmd, options).and_return(true)
- shell_out.shell_out(cmd, options)
+ expect(shell_out_obj).to receive(:shell_out_command).with(cmd, options).and_return(true)
+ shell_out_obj.shell_out(cmd, options)
end
it "should set environment['LC_ALL'] to 'en_US.UTF-8' when 'LC_ALL' not present" do
options = { :environment => { 'HOME' => '/Users/morty' } }
- expect(shell_out).to receive(:shell_out_command).with(cmd, {
+ expect(shell_out_obj).to receive(:shell_out_command).with(cmd, {
:environment => { 'HOME' => '/Users/morty', 'LC_ALL' => Chef::Config[:internal_locale] },
}).and_return(true)
- shell_out.shell_out(cmd, options)
+ shell_out_obj.shell_out(cmd, options)
end
it "should not mutate the options hash when it adds LC_ALL" do
options = { :environment => { 'HOME' => '/Users/morty' } }
- expect(shell_out).to receive(:shell_out_command).with(cmd, {
+ expect(shell_out_obj).to receive(:shell_out_command).with(cmd, {
:environment => { 'HOME' => '/Users/morty', 'LC_ALL' => Chef::Config[:internal_locale] },
}).and_return(true)
- shell_out.shell_out(cmd, options)
+ shell_out_obj.shell_out(cmd, options)
expect(options[:environment].has_key?('LC_ALL')).to be false
end
end
@@ -157,30 +156,30 @@ describe Chef::Mixin::ShellOut do
describe "and env is an option" do
it "should not change env when set to nil" do
options = { :env => { 'LC_ALL' => nil } }
- expect(shell_out).to receive(:shell_out_command).with(cmd, options).and_return(true)
- shell_out.shell_out(cmd, options)
+ expect(shell_out_obj).to receive(:shell_out_command).with(cmd, options).and_return(true)
+ shell_out_obj.shell_out(cmd, options)
end
it "should not change env when set to non-nil" do
options = { :env => { 'LC_ALL' => 'de_DE.UTF-8'}}
- expect(shell_out).to receive(:shell_out_command).with(cmd, options).and_return(true)
- shell_out.shell_out(cmd, options)
+ expect(shell_out_obj).to receive(:shell_out_command).with(cmd, options).and_return(true)
+ shell_out_obj.shell_out(cmd, options)
end
it "should set env['LC_ALL'] to 'en_US.UTF-8' when 'LC_ALL' not present" do
options = { :env => { 'HOME' => '/Users/morty' } }
- expect(shell_out).to receive(:shell_out_command).with(cmd, {
+ expect(shell_out_obj).to receive(:shell_out_command).with(cmd, {
:env => { 'HOME' => '/Users/morty', 'LC_ALL' => Chef::Config[:internal_locale] },
}).and_return(true)
- shell_out.shell_out(cmd, options)
+ shell_out_obj.shell_out(cmd, options)
end
it "should not mutate the options hash when it adds LC_ALL" do
options = { :env => { 'HOME' => '/Users/morty' } }
- expect(shell_out).to receive(:shell_out_command).with(cmd, {
+ expect(shell_out_obj).to receive(:shell_out_command).with(cmd, {
:env => { 'HOME' => '/Users/morty', 'LC_ALL' => Chef::Config[:internal_locale] },
}).and_return(true)
- shell_out.shell_out(cmd, options)
+ shell_out_obj.shell_out(cmd, options)
expect(options[:env].has_key?('LC_ALL')).to be false
end
end
@@ -188,20 +187,20 @@ describe Chef::Mixin::ShellOut do
describe "and no env/environment option is present" do
it "should add environment option and set environment['LC_ALL'] to 'en_US.UTF_8'" do
options = { :user => 'morty' }
- expect(shell_out).to receive(:shell_out_command).with(cmd, {
+ expect(shell_out_obj).to receive(:shell_out_command).with(cmd, {
:user => 'morty', :environment => { 'LC_ALL' => Chef::Config[:internal_locale] },
}).and_return(true)
- shell_out.shell_out(cmd, options)
+ shell_out_obj.shell_out(cmd, options)
end
end
end
describe "when the last argument is not a Hash" do
it "should add environment options and set environment['LC_ALL'] to 'en_US.UTF-8'" do
- expect(shell_out).to receive(:shell_out_command).with(cmd, {
+ expect(shell_out_obj).to receive(:shell_out_command).with(cmd, {
:environment => { 'LC_ALL' => Chef::Config[:internal_locale] },
}).and_return(true)
- shell_out.shell_out(cmd)
+ shell_out_obj.shell_out(cmd)
end
end
@@ -213,56 +212,56 @@ describe Chef::Mixin::ShellOut do
describe "and environment is an option" do
it "should not change environment['LC_ALL'] when set to nil" do
options = { :environment => { 'LC_ALL' => nil } }
- expect(shell_out).to receive(:shell_out_command).with(cmd, options).and_return(true)
- shell_out.shell_out_with_systems_locale(cmd, options)
+ expect(shell_out_obj).to receive(:shell_out_command).with(cmd, options).and_return(true)
+ shell_out_obj.shell_out_with_systems_locale(cmd, options)
end
it "should not change environment['LC_ALL'] when set to non-nil" do
options = { :environment => { 'LC_ALL' => 'en_US.UTF-8' } }
- expect(shell_out).to receive(:shell_out_command).with(cmd, options).and_return(true)
- shell_out.shell_out_with_systems_locale(cmd, options)
+ expect(shell_out_obj).to receive(:shell_out_command).with(cmd, options).and_return(true)
+ shell_out_obj.shell_out_with_systems_locale(cmd, options)
end
it "should no longer set environment['LC_ALL'] to nil when 'LC_ALL' not present" do
options = { :environment => { 'HOME' => '/Users/morty' } }
- expect(shell_out).to receive(:shell_out_command).with(cmd, options).and_return(true)
- shell_out.shell_out_with_systems_locale(cmd, options)
+ expect(shell_out_obj).to receive(:shell_out_command).with(cmd, options).and_return(true)
+ shell_out_obj.shell_out_with_systems_locale(cmd, options)
end
end
describe "and env is an option" do
it "should not change env when set to nil" do
options = { :env => { 'LC_ALL' => nil } }
- expect(shell_out).to receive(:shell_out_command).with(cmd, options).and_return(true)
- shell_out.shell_out_with_systems_locale(cmd, options)
+ expect(shell_out_obj).to receive(:shell_out_command).with(cmd, options).and_return(true)
+ shell_out_obj.shell_out_with_systems_locale(cmd, options)
end
it "should not change env when set to non-nil" do
options = { :env => { 'LC_ALL' => 'en_US.UTF-8'}}
- expect(shell_out).to receive(:shell_out_command).with(cmd, options).and_return(true)
- shell_out.shell_out_with_systems_locale(cmd, options)
+ expect(shell_out_obj).to receive(:shell_out_command).with(cmd, options).and_return(true)
+ shell_out_obj.shell_out_with_systems_locale(cmd, options)
end
it "should no longer set env['LC_ALL'] to nil when 'LC_ALL' not present" do
options = { :env => { 'HOME' => '/Users/morty' } }
- expect(shell_out).to receive(:shell_out_command).with(cmd, options).and_return(true)
- shell_out.shell_out_with_systems_locale(cmd, options)
+ expect(shell_out_obj).to receive(:shell_out_command).with(cmd, options).and_return(true)
+ shell_out_obj.shell_out_with_systems_locale(cmd, options)
end
end
describe "and no env/environment option is present" do
it "should no longer add environment option and set environment['LC_ALL'] to nil" do
options = { :user => 'morty' }
- expect(shell_out).to receive(:shell_out_command).with(cmd, options).and_return(true)
- shell_out.shell_out_with_systems_locale(cmd, options)
+ expect(shell_out_obj).to receive(:shell_out_command).with(cmd, options).and_return(true)
+ shell_out_obj.shell_out_with_systems_locale(cmd, options)
end
end
end
describe "when the last argument is not a Hash" do
it "should no longer add environment options and set environment['LC_ALL'] to nil" do
- expect(shell_out).to receive(:shell_out_command).with(cmd).and_return(true)
- shell_out.shell_out_with_systems_locale(cmd)
+ expect(shell_out_obj).to receive(:shell_out_command).with(cmd).and_return(true)
+ shell_out_obj.shell_out_with_systems_locale(cmd)
end
end
end
diff --git a/spec/unit/node_spec.rb b/spec/unit/node_spec.rb
index 1daaf9ec52..0bc76db272 100644
--- a/spec/unit/node_spec.rb
+++ b/spec/unit/node_spec.rb
@@ -1213,6 +1213,40 @@ describe Chef::Node do
node.save
end
+ it "should save false-y whitelisted attributes" do
+ Chef::Config[:default_attribute_whitelist] = [
+ "foo/bar/baz"
+ ]
+
+ data = {
+ "default" => {
+ "foo" => {
+ "bar" => {
+ "baz" => false,
+ },
+ "other" => {
+ "stuff" => true,
+ }
+ }
+ }
+ }
+
+ selected_data = {
+ "default" => {
+ "foo" => {
+ "bar" => {
+ "baz" => false,
+ }
+ }
+ }
+ }
+
+ node.name("falsey-monkey")
+ allow(node).to receive(:for_json).and_return(data)
+ expect(@rest).to receive(:put_rest).with("nodes/falsey-monkey", selected_data).and_return("foo")
+ node.save
+ end
+
it "should not save any attributes if the whitelist is empty" do
Chef::Config[:automatic_attribute_whitelist] = []
diff --git a/spec/unit/provider/execute_spec.rb b/spec/unit/provider/execute_spec.rb
index 794994533e..51305b6225 100644
--- a/spec/unit/provider/execute_spec.rb
+++ b/spec/unit/provider/execute_spec.rb
@@ -39,6 +39,7 @@ describe Chef::Provider::Execute do
let(:new_resource) { Chef::Resource::Execute.new("foo_resource", run_context) }
before do
+ allow(Chef::Platform).to receive(:windows?) { false }
@original_log_level = Chef::Log.level
Chef::Log.level = :info
allow(STDOUT).to receive(:tty?).and_return(true)
diff --git a/spec/unit/provider/link_spec.rb b/spec/unit/provider/link_spec.rb
index ab5b439eac..0f95ce997e 100644
--- a/spec/unit/provider/link_spec.rb
+++ b/spec/unit/provider/link_spec.rb
@@ -38,8 +38,8 @@ describe Chef::Resource::Link, :not_supported_on_win2k3 do
result
end
- def paths_eql?(path1, path2)
- Chef::Util::PathHelper.paths_eql?(path1, path2)
+ def canonicalize(path)
+ Chef::Platform.windows? ? path.gsub('/', '\\') : path
end
describe "when the target is a symlink" do
@@ -68,7 +68,7 @@ describe Chef::Resource::Link, :not_supported_on_win2k3 do
expect(provider.current_resource.link_type).to eq(:symbolic)
end
it "should update the source of the existing link with the links target" do
- expect(paths_eql?(provider.current_resource.to, "#{CHEF_SPEC_DATA}/fofile")).to be_truthy
+ expect(provider.current_resource.to).to eq(canonicalize("#{CHEF_SPEC_DATA}/fofile"))
end
it "should set the owner" do
expect(provider.current_resource.owner).to eq(501)
@@ -110,7 +110,7 @@ describe Chef::Resource::Link, :not_supported_on_win2k3 do
expect(provider.current_resource.link_type).to eq(:symbolic)
end
it "should update the source of the existing link to the link's target" do
- expect(paths_eql?(provider.current_resource.to, "#{CHEF_SPEC_DATA}/fofile")).to be_truthy
+ expect(provider.current_resource.to).to eq(canonicalize("#{CHEF_SPEC_DATA}/fofile"))
end
it "should not set the owner" do
expect(provider.current_resource.owner).to be_nil
@@ -221,7 +221,7 @@ describe Chef::Resource::Link, :not_supported_on_win2k3 do
expect(provider.current_resource.link_type).to eq(:hard)
end
it "should update the source of the existing link to the link's target" do
- expect(paths_eql?(provider.current_resource.to, "#{CHEF_SPEC_DATA}/fofile")).to be_truthy
+ expect(provider.current_resource.to).to eq(canonicalize("#{CHEF_SPEC_DATA}/fofile"))
end
it "should not set the owner" do
expect(provider.current_resource.owner).to eq(nil)
diff --git a/spec/unit/provider/package/apt_spec.rb b/spec/unit/provider/package/apt_spec.rb
index edca3e4c22..e53fdc3f27 100644
--- a/spec/unit/provider/package/apt_spec.rb
+++ b/spec/unit/provider/package/apt_spec.rb
@@ -20,46 +20,51 @@ require 'spec_helper'
require 'ostruct'
describe Chef::Provider::Package::Apt 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("irssi", @run_context)
-
- @status = double("Status", :exitstatus => 0)
- @provider = Chef::Provider::Package::Apt.new(@new_resource, @run_context)
- @stdin = StringIO.new
- @stdout =<<-PKG_STATUS
+ # XXX: sorry this is ugly and was done quickly to get 12.0.2 out, this file needs a rewrite to use
+ # let blocks and shared examples
+ [ Chef::Resource::Package, Chef::Resource::AptPackage ].each do |resource_klass|
+ describe "when the new_resource is a #{resource_klass}" do
+
+ before(:each) do
+ @node = Chef::Node.new
+ @events = Chef::EventDispatch::Dispatcher.new
+ @run_context = Chef::RunContext.new(@node, {}, @events)
+ @new_resource = resource_klass.new("irssi", @run_context)
+
+ @status = double("Status", :exitstatus => 0)
+ @provider = Chef::Provider::Package::Apt.new(@new_resource, @run_context)
+ @stdin = StringIO.new
+ @stdout =<<-PKG_STATUS
irssi:
Installed: (none)
Candidate: 0.8.14-1ubuntu4
Version table:
0.8.14-1ubuntu4 0
500 http://us.archive.ubuntu.com/ubuntu/ lucid/main Packages
-PKG_STATUS
- @stderr = ""
- @shell_out = OpenStruct.new(:stdout => @stdout,:stdin => @stdin,:stderr => @stderr,:status => @status,:exitstatus => 0)
- @timeout = 900
- end
-
- describe "when loading current resource" do
-
- it "should create a current resource with the name of the new_resource" do
- expect(@provider).to receive(:shell_out!).with(
- "apt-cache policy #{@new_resource.package_name}",
- :timeout => @timeout
- ).and_return(@shell_out)
- @provider.load_current_resource
+ PKG_STATUS
+ @stderr = ""
+ @shell_out = OpenStruct.new(:stdout => @stdout,:stdin => @stdin,:stderr => @stderr,:status => @status,:exitstatus => 0)
+ @timeout = 900
+ end
- current_resource = @provider.current_resource
- expect(current_resource).to be_a(Chef::Resource::Package)
- expect(current_resource.name).to eq("irssi")
- expect(current_resource.package_name).to eq("irssi")
- expect(current_resource.version).to be_nil
- end
+ describe "when loading current resource" do
- it "should set the installed version if package has one" do
- @stdout.replace(<<-INSTALLED)
+ it "should create a current resource with the name of the new_resource" do
+ expect(@provider).to receive(:shell_out!).with(
+ "apt-cache policy #{@new_resource.package_name}",
+ :timeout => @timeout
+ ).and_return(@shell_out)
+ @provider.load_current_resource
+
+ current_resource = @provider.current_resource
+ expect(current_resource).to be_a(Chef::Resource::Package)
+ expect(current_resource.name).to eq("irssi")
+ expect(current_resource.package_name).to eq("irssi")
+ expect(current_resource.version).to be_nil
+ end
+
+ it "should set the installed version if package has one" do
+ @stdout.replace(<<-INSTALLED)
sudo:
Installed: 1.7.2p1-1ubuntu5.3
Candidate: 1.7.2p1-1ubuntu5.3
@@ -70,29 +75,29 @@ sudo:
100 /var/lib/dpkg/status
1.7.2p1-1ubuntu5 0
500 http://us.archive.ubuntu.com/ubuntu/ lucid/main Packages
-INSTALLED
- expect(@provider).to receive(:shell_out!).and_return(@shell_out)
- @provider.load_current_resource
- expect(@provider.current_resource.version).to eq("1.7.2p1-1ubuntu5.3")
- expect(@provider.candidate_version).to eql("1.7.2p1-1ubuntu5.3")
- end
-
- # libmysqlclient-dev is a real package in newer versions of debian + ubuntu
- # list of virtual packages: http://www.debian.org/doc/packaging-manuals/virtual-package-names-list.txt
- it "should not install the virtual package there is a single provider package and it is installed" do
- @new_resource.package_name("libmysqlclient15-dev")
- virtual_package_out=<<-VPKG_STDOUT
+ INSTALLED
+ expect(@provider).to receive(:shell_out!).and_return(@shell_out)
+ @provider.load_current_resource
+ expect(@provider.current_resource.version).to eq("1.7.2p1-1ubuntu5.3")
+ expect(@provider.candidate_version).to eql("1.7.2p1-1ubuntu5.3")
+ end
+
+ # libmysqlclient-dev is a real package in newer versions of debian + ubuntu
+ # list of virtual packages: http://www.debian.org/doc/packaging-manuals/virtual-package-names-list.txt
+ it "should not install the virtual package there is a single provider package and it is installed" do
+ @new_resource.package_name("libmysqlclient15-dev")
+ virtual_package_out=<<-VPKG_STDOUT
libmysqlclient15-dev:
Installed: (none)
Candidate: (none)
Version table:
-VPKG_STDOUT
- virtual_package = double(:stdout => virtual_package_out,:exitstatus => 0)
- expect(@provider).to receive(:shell_out!).with(
- "apt-cache policy libmysqlclient15-dev",
- :timeout => @timeout
- ).and_return(virtual_package)
- showpkg_out =<<-SHOWPKG_STDOUT
+ VPKG_STDOUT
+ virtual_package = double(:stdout => virtual_package_out,:exitstatus => 0)
+ expect(@provider).to receive(:shell_out!).with(
+ "apt-cache policy libmysqlclient15-dev",
+ :timeout => @timeout
+ ).and_return(virtual_package)
+ showpkg_out =<<-SHOWPKG_STDOUT
Package: libmysqlclient15-dev
Versions:
@@ -109,13 +114,13 @@ Reverse Provides:
libmysqlclient-dev 5.1.41-3ubuntu12.7
libmysqlclient-dev 5.1.41-3ubuntu12.10
libmysqlclient-dev 5.1.41-3ubuntu12
-SHOWPKG_STDOUT
- showpkg = double(:stdout => showpkg_out,:exitstatus => 0)
- expect(@provider).to receive(:shell_out!).with(
- "apt-cache showpkg libmysqlclient15-dev",
- :timeout => @timeout
- ).and_return(showpkg)
- real_package_out=<<-RPKG_STDOUT
+ SHOWPKG_STDOUT
+ showpkg = double(:stdout => showpkg_out,:exitstatus => 0)
+ expect(@provider).to receive(:shell_out!).with(
+ "apt-cache showpkg libmysqlclient15-dev",
+ :timeout => @timeout
+ ).and_return(showpkg)
+ real_package_out=<<-RPKG_STDOUT
libmysqlclient-dev:
Installed: 5.1.41-3ubuntu12.10
Candidate: 5.1.41-3ubuntu12.10
@@ -127,29 +132,29 @@ libmysqlclient-dev:
500 http://security.ubuntu.com/ubuntu/ lucid-security/main Packages
5.1.41-3ubuntu12 0
500 http://us.archive.ubuntu.com/ubuntu/ lucid/main Packages
-RPKG_STDOUT
- real_package = double(:stdout => real_package_out,:exitstatus => 0)
- expect(@provider).to receive(:shell_out!).with(
- "apt-cache policy libmysqlclient-dev",
- :timeout => @timeout
- ).and_return(real_package)
- @provider.load_current_resource
- end
+ RPKG_STDOUT
+ real_package = double(:stdout => real_package_out,:exitstatus => 0)
+ expect(@provider).to receive(:shell_out!).with(
+ "apt-cache policy libmysqlclient-dev",
+ :timeout => @timeout
+ ).and_return(real_package)
+ @provider.load_current_resource
+ end
- it "should raise an exception if you specify a virtual package with multiple provider packages" do
- @new_resource.package_name("mp3-decoder")
- virtual_package_out=<<-VPKG_STDOUT
+ it "should raise an exception if you specify a virtual package with multiple provider packages" do
+ @new_resource.package_name("mp3-decoder")
+ virtual_package_out=<<-VPKG_STDOUT
mp3-decoder:
Installed: (none)
Candidate: (none)
Version table:
-VPKG_STDOUT
- virtual_package = double(:stdout => virtual_package_out,:exitstatus => 0)
- expect(@provider).to receive(:shell_out!).with(
- "apt-cache policy mp3-decoder",
- :timeout => @timeout
- ).and_return(virtual_package)
- showpkg_out=<<-SHOWPKG_STDOUT
+ VPKG_STDOUT
+ virtual_package = double(:stdout => virtual_package_out,:exitstatus => 0)
+ expect(@provider).to receive(:shell_out!).with(
+ "apt-cache policy mp3-decoder",
+ :timeout => @timeout
+ ).and_return(virtual_package)
+ showpkg_out=<<-SHOWPKG_STDOUT
Package: mp3-decoder
Versions:
@@ -169,191 +174,193 @@ vlc 1.0.6-1ubuntu1
opencubicplayer 1:0.1.17-2
mpg321 0.2.10.6
mpg123 1.12.1-0ubuntu1
-SHOWPKG_STDOUT
- showpkg = double(:stdout => showpkg_out,:exitstatus => 0)
- expect(@provider).to receive(:shell_out!).with(
- "apt-cache showpkg mp3-decoder",
- :timeout => @timeout
- ).and_return(showpkg)
- expect { @provider.load_current_resource }.to raise_error(Chef::Exceptions::Package)
- end
-
- it "should run apt-cache policy with the default_release option, if there is one and provider is explicitly defined" do
- @new_resource = Chef::Resource::AptPackage.new("irssi", @run_context)
- @provider = Chef::Provider::Package::Apt.new(@new_resource, @run_context)
-
- allow(@new_resource).to receive(:default_release).and_return("lenny-backports")
- allow(@new_resource).to receive(:provider).and_return("Chef::Provider::Package::Apt")
- expect(@provider).to receive(:shell_out!).with(
- "apt-cache -o APT::Default-Release=lenny-backports policy irssi",
- :timeout => @timeout
- ).and_return(@shell_out)
- @provider.load_current_resource
- end
-
- it "raises an exception if a source is specified (CHEF-5113)" do
- @new_resource.source "pluto"
- @provider.define_resource_requirements
- expect(@provider).to receive(:shell_out!).with("apt-cache policy irssi", {:timeout=>900}).and_return(@shell_out)
- expect { @provider.run_action(:install) }.to raise_error(Chef::Exceptions::Package)
- end
- end
-
- context "after loading the current resource" do
- before do
- @current_resource = Chef::Resource::Package.new("irssi", @run_context)
- @provider.current_resource = @current_resource
- end
-
- describe "install_package" do
- it "should run apt-get install with the package name and version" do
- expect(@provider).to receive(:shell_out!). with(
- "apt-get -q -y install irssi=0.8.12-7",
- :env => { "DEBIAN_FRONTEND" => "noninteractive", "LC_ALL" => nil},
- :timeout => @timeout
- )
- @provider.install_package("irssi", "0.8.12-7")
- end
-
- it "should run apt-get install with the package name and version and options if specified" do
- expect(@provider).to receive(:shell_out!).with(
- "apt-get -q -y --force-yes install irssi=0.8.12-7",
- :env => {"DEBIAN_FRONTEND" => "noninteractive", "LC_ALL" => nil },
- :timeout => @timeout
- )
- @new_resource.options("--force-yes")
- @provider.install_package("irssi", "0.8.12-7")
- end
-
- it "should run apt-get install with the package name and version and default_release if there is one and provider is explicitly defined" do
- @new_resource = nil
- @new_resource = Chef::Resource::AptPackage.new("irssi", @run_context)
- @new_resource.default_release("lenny-backports")
- @new_resource.provider = @provider
- @provider.new_resource = @new_resource
-
- expect(@provider).to receive(:shell_out!).with(
- "apt-get -q -y -o APT::Default-Release=lenny-backports install irssi=0.8.12-7",
- :env => {"DEBIAN_FRONTEND" => "noninteractive", "LC_ALL" => nil },
- :timeout => @timeout
- )
-
- @provider.install_package("irssi", "0.8.12-7")
- end
- end
-
- describe Chef::Provider::Package::Apt, "upgrade_package" do
-
- it "should run install_package with the name and version" do
- expect(@provider).to receive(:install_package).with("irssi", "0.8.12-7")
- @provider.upgrade_package("irssi", "0.8.12-7")
- end
- end
-
- describe Chef::Provider::Package::Apt, "remove_package" do
-
- it "should run apt-get remove with the package name" do
- expect(@provider).to receive(:shell_out!).with(
- "apt-get -q -y remove irssi",
- :env => {"DEBIAN_FRONTEND" => "noninteractive", "LC_ALL" => nil},
- :timeout => @timeout
- )
- @provider.remove_package("irssi", "0.8.12-7")
- end
-
- it "should run apt-get remove with the package name and options if specified" do
- expect(@provider).to receive(:shell_out!).with(
- "apt-get -q -y --force-yes remove irssi",
- :env => { "DEBIAN_FRONTEND" => "noninteractive", "LC_ALL" => nil },
- :timeout => @timeout
- )
- @new_resource.options("--force-yes")
-
- @provider.remove_package("irssi", "0.8.12-7")
- end
- end
-
- describe "when purging a package" do
-
- it "should run apt-get purge with the package name" do
- expect(@provider).to receive(:shell_out!).with(
- "apt-get -q -y purge irssi",
- :env => { "DEBIAN_FRONTEND" => "noninteractive", "LC_ALL" => nil },
- :timeout => @timeout
- )
- @provider.purge_package("irssi", "0.8.12-7")
- end
-
- it "should run apt-get purge with the package name and options if specified" do
- expect(@provider).to receive(:shell_out!).with(
- "apt-get -q -y --force-yes purge irssi",
- :env => { "DEBIAN_FRONTEND" => "noninteractive", "LC_ALL" => nil },
- :timeout => @timeout
- )
- @new_resource.options("--force-yes")
-
- @provider.purge_package("irssi", "0.8.12-7")
- end
- end
-
- describe "when preseeding a package" do
- before(:each) do
- allow(@provider).to receive(:get_preseed_file).and_return("/tmp/irssi-0.8.12-7.seed")
- end
-
- it "should get the full path to the preseed response file" do
- expect(@provider).to receive(:get_preseed_file).with("irssi", "0.8.12-7").and_return("/tmp/irssi-0.8.12-7.seed")
- file = @provider.get_preseed_file("irssi", "0.8.12-7")
-
- expect(@provider).to receive(:shell_out!).with(
- "debconf-set-selections /tmp/irssi-0.8.12-7.seed",
- :env => {"DEBIAN_FRONTEND" => "noninteractive", "LC_ALL" => nil},
- :timeout => @timeout
- )
-
- @provider.preseed_package(file)
- end
-
- it "should run debconf-set-selections on the preseed file if it has changed" do
- expect(@provider).to receive(:shell_out!).with(
- "debconf-set-selections /tmp/irssi-0.8.12-7.seed",
- :env => {"DEBIAN_FRONTEND" => "noninteractive", "LC_ALL" => nil},
- :timeout => @timeout
- )
- file = @provider.get_preseed_file("irssi", "0.8.12-7")
- @provider.preseed_package(file)
- end
+ SHOWPKG_STDOUT
+ showpkg = double(:stdout => showpkg_out,:exitstatus => 0)
+ expect(@provider).to receive(:shell_out!).with(
+ "apt-cache showpkg mp3-decoder",
+ :timeout => @timeout
+ ).and_return(showpkg)
+ expect { @provider.load_current_resource }.to raise_error(Chef::Exceptions::Package)
+ end
- it "should not run debconf-set-selections if the preseed file has not changed" do
- allow(@provider).to receive(:check_package_state)
- @current_resource.version "0.8.11"
- @new_resource.response_file "/tmp/file"
- allow(@provider).to receive(:get_preseed_file).and_return(false)
- expect(@provider).not_to receive(:shell_out!)
- @provider.run_action(:reconfig)
- end
- end
+ it "should run apt-cache policy with the default_release option, if there is one on the resource" do
+ @new_resource = Chef::Resource::AptPackage.new("irssi", @run_context)
+ @provider = Chef::Provider::Package::Apt.new(@new_resource, @run_context)
- describe "when reconfiguring a package" do
- it "should run dpkg-reconfigure package" do
- expect(@provider).to receive(:shell_out!).with(
- "dpkg-reconfigure irssi",
- :env => {"DEBIAN_FRONTEND" => "noninteractive", "LC_ALL" => nil },
- :timeout => @timeout
- )
- @provider.reconfig_package("irssi", "0.8.12-7")
- end
- end
-
- describe "when installing a virtual package" do
- it "should install the package without specifying a version" do
- @provider.is_virtual_package = true
+ allow(@new_resource).to receive(:default_release).and_return("lenny-backports")
+ allow(@new_resource).to receive(:provider).and_return(nil)
expect(@provider).to receive(:shell_out!).with(
- "apt-get -q -y install libmysqlclient-dev",
- :env => {"DEBIAN_FRONTEND" => "noninteractive", "LC_ALL" => nil },
+ "apt-cache -o APT::Default-Release=lenny-backports policy irssi",
:timeout => @timeout
- )
- @provider.install_package("libmysqlclient-dev", "not_a_real_version")
+ ).and_return(@shell_out)
+ @provider.load_current_resource
+ end
+
+ it "raises an exception if a source is specified (CHEF-5113)" do
+ @new_resource.source "pluto"
+ @provider.define_resource_requirements
+ expect(@provider).to receive(:shell_out!).with("apt-cache policy irssi", {:timeout=>900}).and_return(@shell_out)
+ expect { @provider.run_action(:install) }.to raise_error(Chef::Exceptions::Package)
+ end
+ end
+
+ context "after loading the current resource" do
+ before do
+ @current_resource = resource_klass.new("irssi", @run_context)
+ @provider.current_resource = @current_resource
+ end
+
+ describe "install_package" do
+ it "should run apt-get install with the package name and version" do
+ expect(@provider).to receive(:shell_out!). with(
+ "apt-get -q -y install irssi=0.8.12-7",
+ :env => { "DEBIAN_FRONTEND" => "noninteractive", "LC_ALL" => nil},
+ :timeout => @timeout
+ )
+ @provider.install_package("irssi", "0.8.12-7")
+ end
+
+ it "should run apt-get install with the package name and version and options if specified" do
+ expect(@provider).to receive(:shell_out!).with(
+ "apt-get -q -y --force-yes install irssi=0.8.12-7",
+ :env => {"DEBIAN_FRONTEND" => "noninteractive", "LC_ALL" => nil },
+ :timeout => @timeout
+ )
+ @new_resource.options("--force-yes")
+ @provider.install_package("irssi", "0.8.12-7")
+ end
+
+ it "should run apt-get install with the package name and version and default_release if there is one and provider is explicitly defined" do
+ @new_resource = nil
+ @new_resource = Chef::Resource::AptPackage.new("irssi", @run_context)
+ @new_resource.default_release("lenny-backports")
+ @new_resource.provider = nil
+ @provider.new_resource = @new_resource
+
+ expect(@provider).to receive(:shell_out!).with(
+ "apt-get -q -y -o APT::Default-Release=lenny-backports install irssi=0.8.12-7",
+ :env => {"DEBIAN_FRONTEND" => "noninteractive", "LC_ALL" => nil },
+ :timeout => @timeout
+ )
+
+ @provider.install_package("irssi", "0.8.12-7")
+ end
+ end
+
+ describe resource_klass, "upgrade_package" do
+
+ it "should run install_package with the name and version" do
+ expect(@provider).to receive(:install_package).with("irssi", "0.8.12-7")
+ @provider.upgrade_package("irssi", "0.8.12-7")
+ end
+ end
+
+ describe resource_klass, "remove_package" do
+
+ it "should run apt-get remove with the package name" do
+ expect(@provider).to receive(:shell_out!).with(
+ "apt-get -q -y remove irssi",
+ :env => {"DEBIAN_FRONTEND" => "noninteractive", "LC_ALL" => nil},
+ :timeout => @timeout
+ )
+ @provider.remove_package("irssi", "0.8.12-7")
+ end
+
+ it "should run apt-get remove with the package name and options if specified" do
+ expect(@provider).to receive(:shell_out!).with(
+ "apt-get -q -y --force-yes remove irssi",
+ :env => { "DEBIAN_FRONTEND" => "noninteractive", "LC_ALL" => nil },
+ :timeout => @timeout
+ )
+ @new_resource.options("--force-yes")
+
+ @provider.remove_package("irssi", "0.8.12-7")
+ end
+ end
+
+ describe "when purging a package" do
+
+ it "should run apt-get purge with the package name" do
+ expect(@provider).to receive(:shell_out!).with(
+ "apt-get -q -y purge irssi",
+ :env => { "DEBIAN_FRONTEND" => "noninteractive", "LC_ALL" => nil },
+ :timeout => @timeout
+ )
+ @provider.purge_package("irssi", "0.8.12-7")
+ end
+
+ it "should run apt-get purge with the package name and options if specified" do
+ expect(@provider).to receive(:shell_out!).with(
+ "apt-get -q -y --force-yes purge irssi",
+ :env => { "DEBIAN_FRONTEND" => "noninteractive", "LC_ALL" => nil },
+ :timeout => @timeout
+ )
+ @new_resource.options("--force-yes")
+
+ @provider.purge_package("irssi", "0.8.12-7")
+ end
+ end
+
+ describe "when preseeding a package" do
+ before(:each) do
+ allow(@provider).to receive(:get_preseed_file).and_return("/tmp/irssi-0.8.12-7.seed")
+ end
+
+ it "should get the full path to the preseed response file" do
+ expect(@provider).to receive(:get_preseed_file).with("irssi", "0.8.12-7").and_return("/tmp/irssi-0.8.12-7.seed")
+ file = @provider.get_preseed_file("irssi", "0.8.12-7")
+
+ expect(@provider).to receive(:shell_out!).with(
+ "debconf-set-selections /tmp/irssi-0.8.12-7.seed",
+ :env => {"DEBIAN_FRONTEND" => "noninteractive", "LC_ALL" => nil},
+ :timeout => @timeout
+ )
+
+ @provider.preseed_package(file)
+ end
+
+ it "should run debconf-set-selections on the preseed file if it has changed" do
+ expect(@provider).to receive(:shell_out!).with(
+ "debconf-set-selections /tmp/irssi-0.8.12-7.seed",
+ :env => {"DEBIAN_FRONTEND" => "noninteractive", "LC_ALL" => nil},
+ :timeout => @timeout
+ )
+ file = @provider.get_preseed_file("irssi", "0.8.12-7")
+ @provider.preseed_package(file)
+ end
+
+ it "should not run debconf-set-selections if the preseed file has not changed" do
+ allow(@provider).to receive(:check_package_state)
+ @current_resource.version "0.8.11"
+ @new_resource.response_file "/tmp/file"
+ allow(@provider).to receive(:get_preseed_file).and_return(false)
+ expect(@provider).not_to receive(:shell_out!)
+ @provider.run_action(:reconfig)
+ end
+ end
+
+ describe "when reconfiguring a package" do
+ it "should run dpkg-reconfigure package" do
+ expect(@provider).to receive(:shell_out!).with(
+ "dpkg-reconfigure irssi",
+ :env => {"DEBIAN_FRONTEND" => "noninteractive", "LC_ALL" => nil },
+ :timeout => @timeout
+ )
+ @provider.reconfig_package("irssi", "0.8.12-7")
+ end
+ end
+
+ describe "when installing a virtual package" do
+ it "should install the package without specifying a version" do
+ @provider.is_virtual_package = true
+ expect(@provider).to receive(:shell_out!).with(
+ "apt-get -q -y install libmysqlclient-dev",
+ :env => {"DEBIAN_FRONTEND" => "noninteractive", "LC_ALL" => nil },
+ :timeout => @timeout
+ )
+ @provider.install_package("libmysqlclient-dev", "not_a_real_version")
+ end
+ end
end
end
end
diff --git a/spec/unit/provider/package/windows/msi_spec.rb b/spec/unit/provider/package/windows/msi_spec.rb
index e539bbbb79..bef202847f 100644
--- a/spec/unit/provider/package/windows/msi_spec.rb
+++ b/spec/unit/provider/package/windows/msi_spec.rb
@@ -18,13 +18,22 @@
require 'spec_helper'
-describe Chef::Provider::Package::Windows::MSI, :windows_only do
+describe Chef::Provider::Package::Windows::MSI do
let(:node) { double('Chef::Node') }
let(:events) { double('Chef::Events').as_null_object } # mock all the methods
let(:run_context) { double('Chef::RunContext', :node => node, :events => events) }
let(:new_resource) { Chef::Resource::WindowsPackage.new("calculator.msi") }
let(:provider) { Chef::Provider::Package::Windows::MSI.new(new_resource) }
+ before(:each) do
+ stub_const("File::ALT_SEPARATOR", "\\")
+ allow(::File).to receive(:absolute_path).with("calculator.msi").and_return("calculator.msi")
+ end
+
+ it "responds to shell_out!" do
+ expect(provider).to respond_to(:shell_out!)
+ end
+
describe "expand_options" do
it "returns an empty string if passed no options" do
expect(provider.expand_options(nil)).to eql ""
@@ -51,10 +60,16 @@ describe Chef::Provider::Package::Windows::MSI, :windows_only do
end
describe "install_package" do
- # calls shell_out!
+ it "calls msiexec /qn /i" do
+ expect(provider).to receive(:shell_out!).with(/msiexec \/qn \/i \"calculator.msi\"/, kind_of(Hash))
+ provider.install_package("unused", "unused")
+ end
end
describe "remove_package" do
- # calls shell_out!
+ it "calls msiexec /qn /x" do
+ expect(provider).to receive(:shell_out!).with(/msiexec \/qn \/x \"calculator.msi\"/, kind_of(Hash))
+ provider.remove_package("unused", "unused")
+ end
end
end
diff --git a/spec/unit/provider/service/windows_spec.rb b/spec/unit/provider/service/windows_spec.rb
index e4b0714d22..784a2232b2 100644
--- a/spec/unit/provider/service/windows_spec.rb
+++ b/spec/unit/provider/service/windows_spec.rb
@@ -18,6 +18,7 @@
#
require 'spec_helper'
+require 'mixlib/shellout'
describe Chef::Provider::Service::Windows, "load_current_resource" do
before(:each) do
@@ -38,6 +39,7 @@ describe Chef::Provider::Service::Windows, "load_current_resource" do
allow(Win32::Service).to receive(:config_info).with(@new_resource.service_name).and_return(
double("ConfigStruct", :start_type => "auto start"))
allow(Win32::Service).to receive(:exists?).and_return(true)
+ allow(Win32::Service).to receive(:configure).and_return(Win32::Service)
end
it "should set the current resources service name to the new resources service name" do
@@ -131,6 +133,26 @@ describe Chef::Provider::Service::Windows, "load_current_resource" do
expect(@new_resource.updated_by_last_action?).to be_falsey
end
+ describe "running as a different account" do
+ let(:old_run_as_user) { @new_resource.run_as_user }
+ let(:old_run_as_password) { @new_resource.run_as_password }
+
+ before {
+ @new_resource.run_as_user(".\\wallace")
+ @new_resource.run_as_password("Wensleydale")
+ }
+
+ after {
+ @new_resource.run_as_user(old_run_as_user)
+ @new_resource.run_as_password(old_run_as_password)
+ }
+
+ it "should call #grant_service_logon if the :run_as_user and :run_as_password attributes are present" do
+ expect(Win32::Service).to receive(:start)
+ expect(@provider).to receive(:grant_service_logon).and_return(true)
+ @provider.start_service
+ end
+ end
end
@@ -364,4 +386,76 @@ describe Chef::Provider::Service::Windows, "load_current_resource" do
expect { @provider.send(:set_startup_type, :fire_truck) }.to raise_error(Chef::Exceptions::ConfigurationError)
end
end
+
+ shared_context "testing private methods" do
+
+ let(:private_methods) {
+ described_class.private_instance_methods
+ }
+
+ before {
+ described_class.send(:public, *private_methods)
+ }
+
+ after {
+ described_class.send(:private, *private_methods)
+ }
+ end
+
+ describe "grant_service_logon" do
+ include_context "testing private methods"
+
+ let(:username) { "unit_test_user" }
+ let(:success_string) { "The task has completed successfully.\r\nSee logfile etc." }
+ let(:failure_string) { "Look on my works, ye Mighty, and despair!" }
+ let(:command) {
+ dbfile = @provider.grant_dbfile_name(username)
+ policyfile = @provider.grant_policyfile_name(username)
+ logfile = @provider.grant_logfile_name(username)
+
+ %Q{secedit.exe /configure /db "#{dbfile}" /cfg "#{policyfile}" /areas USER_RIGHTS SECURITYPOLICY SERVICES /log "#{logfile}"}
+ }
+ let(:shellout_env) { {:environment=>{"LC_ALL"=>"en_US.UTF-8"}} }
+
+ before {
+ expect_any_instance_of(described_class).to receive(:shell_out).with(command).and_call_original
+ expect_any_instance_of(Mixlib::ShellOut).to receive(:run_command).and_return(nil)
+ }
+
+ after {
+ # only needed for the second test.
+ ::File.delete(@provider.grant_policyfile_name(username)) rescue nil
+ ::File.delete(@provider.grant_logfile_name(username)) rescue nil
+ ::File.delete(@provider.grant_dbfile_name(username)) rescue nil
+ }
+
+ it "calls Mixlib::Shellout with the correct command string" do
+ expect_any_instance_of(Mixlib::ShellOut).to receive(:exitstatus).and_return(0)
+ expect(@provider.grant_service_logon(username)).to equal true
+ end
+
+ it "raises an exception when the grant command fails" do
+ expect_any_instance_of(Mixlib::ShellOut).to receive(:exitstatus).and_return(1)
+ expect_any_instance_of(Mixlib::ShellOut).to receive(:stdout).and_return(failure_string)
+ expect { @provider.grant_service_logon(username) }.to raise_error(Chef::Exceptions::Service)
+ end
+ end
+
+ describe "cleaning usernames" do
+ include_context "testing private methods"
+
+ it "correctly reformats usernames to create valid filenames" do
+ expect(@provider.clean_username_for_path("\\\\problem username/oink.txt")).to eq("_problem_username_oink_txt")
+ expect(@provider.clean_username_for_path("boring_username")).to eq("boring_username")
+ end
+
+ it "correctly reformats usernames for the policy file" do
+ expect(@provider.canonicalize_username(".\\maryann")).to eq("maryann")
+ expect(@provider.canonicalize_username("maryann")).to eq("maryann")
+
+ expect(@provider.canonicalize_username("\\\\maryann")).to eq("maryann")
+ expect(@provider.canonicalize_username("mydomain\\\\maryann")).to eq("mydomain\\\\maryann")
+ expect(@provider.canonicalize_username("\\\\mydomain\\\\maryann")).to eq("mydomain\\\\maryann")
+ end
+ end
end
diff --git a/spec/unit/provider/subversion_spec.rb b/spec/unit/provider/subversion_spec.rb
index b372f0df7a..9ca11b8d82 100644
--- a/spec/unit/provider/subversion_spec.rb
+++ b/spec/unit/provider/subversion_spec.rb
@@ -198,7 +198,7 @@ describe Chef::Provider::Subversion do
it "runs an export with the --force option" do
allow(::File).to receive(:directory?).with("/my/deploy").and_return(true)
expected_cmd = "svn export --force -q -r12345 http://svn.example.org/trunk/ /my/deploy/dir"
- expect(@provider).to receive(:shell_out!).with(command: expected_cmd)
+ expect(@provider).to receive(:shell_out!).with(expected_cmd, {})
@provider.run_action(:force_export)
expect(@resource).to be_updated
end
@@ -206,7 +206,7 @@ describe Chef::Provider::Subversion do
it "runs the checkout command for action_checkout" do
allow(::File).to receive(:directory?).with("/my/deploy").and_return(true)
expected_cmd = "svn checkout -q -r12345 http://svn.example.org/trunk/ /my/deploy/dir"
- expect(@provider).to receive(:shell_out!).with(command: expected_cmd)
+ expect(@provider).to receive(:shell_out!).with(expected_cmd, {})
@provider.run_action(:checkout)
expect(@resource).to be_updated
end
@@ -230,7 +230,7 @@ describe Chef::Provider::Subversion do
@resource.user "whois"
@resource.group "thisis"
expected_cmd = "svn checkout -q -r12345 http://svn.example.org/trunk/ /my/deploy/dir"
- expect(@provider).to receive(:shell_out!).with(command: expected_cmd, user: "whois", group: "thisis")
+ expect(@provider).to receive(:shell_out!).with(expected_cmd, {user: "whois", group: "thisis"})
@provider.run_action(:checkout)
expect(@resource).to be_updated
end
@@ -255,7 +255,7 @@ describe Chef::Provider::Subversion do
allow(@provider).to receive(:find_current_revision).and_return("11410")
allow(@provider).to receive(:current_revision_matches_target_revision?).and_return(false)
expected_cmd = "svn update -q -r12345 /my/deploy/dir"
- expect(@provider).to receive(:shell_out!).with(command: expected_cmd)
+ expect(@provider).to receive(:shell_out!).with(expected_cmd, {})
@provider.run_action(:sync)
expect(@resource).to be_updated
end
@@ -272,7 +272,7 @@ describe Chef::Provider::Subversion do
it "runs the export_command on action_export" do
allow(::File).to receive(:directory?).with("/my/deploy").and_return(true)
expected_cmd = "svn export --force -q -r12345 http://svn.example.org/trunk/ /my/deploy/dir"
- expect(@provider).to receive(:shell_out!).with(command: expected_cmd)
+ expect(@provider).to receive(:shell_out!).with(expected_cmd, {})
@provider.run_action(:export)
expect(@resource).to be_updated
end
diff --git a/spec/unit/provider_resolver_spec.rb b/spec/unit/provider_resolver_spec.rb
index c56207c554..ab19ff4bee 100644
--- a/spec/unit/provider_resolver_spec.rb
+++ b/spec/unit/provider_resolver_spec.rb
@@ -514,7 +514,7 @@ describe Chef::ProviderResolver do
:deploy_revision, :directory, :dpkg_package, :easy_install_package,
:erl_call, :execute, :file, :gem_package, :git, :http_request, :link, :log, :pacman_package, :paludis_package,
:perl, :python, :remote_directory, :route, :rpm_package, :ruby, :ruby_block, :script,
- :subversion, :template, :timestamped_deploy, :whyrun_safe_ruby_block, :yum_package,
+ :subversion, :template, :timestamped_deploy, :whyrun_safe_ruby_block, :yum_package, :homebrew_package,
]
supported_providers.each do |static_resource|
@@ -530,7 +530,7 @@ describe Chef::ProviderResolver do
end
unsupported_providers = [
- :bff_package, :dsc_script, :homebrew_package, :ips_package, :macports_package,
+ :bff_package, :dsc_script, :ips_package, :macports_package,
:smartos_package, :solaris_package, :windows_package,
:windows_service,
]
diff --git a/spec/unit/recipe_spec.rb b/spec/unit/recipe_spec.rb
index 1b7506d965..e8c1358ba2 100644
--- a/spec/unit/recipe_spec.rb
+++ b/spec/unit/recipe_spec.rb
@@ -154,6 +154,7 @@ describe Chef::Recipe do
expect(zm_resource.recipe_name).to eq("test")
expect(zm_resource.cookbook_name).to eq("hjk")
expect(zm_resource.source_line).to include(__FILE__)
+ expect(zm_resource.declared_type).to eq(:zen_master)
end
it "does not add the resource to the resource collection" do
@@ -483,4 +484,11 @@ describe Chef::Recipe do
expect(node[:tags]).to eql([])
end
end
+
+ describe "included DSL" do
+ it "should include features from Chef::DSL::Audit" do
+ expect(recipe.singleton_class.included_modules).to include(Chef::DSL::Audit)
+ expect(recipe.respond_to?(:controls)).to be true
+ end
+ end
end
diff --git a/spec/unit/resource/apt_package_spec.rb b/spec/unit/resource/apt_package_spec.rb
index be8eb9c324..3c31f63dd7 100644
--- a/spec/unit/resource/apt_package_spec.rb
+++ b/spec/unit/resource/apt_package_spec.rb
@@ -29,12 +29,10 @@ describe Chef::Resource::AptPackage, "initialize" do
os: "linux",
)
- before(:each) do
- @resource = Chef::Resource::AptPackage.new("foo")
- end
+ let(:resource) { Chef::Resource::AptPackage.new("foo") }
it "should support default_release" do
- @resource.default_release("lenny-backports")
- expect(@resource.default_release).to eql("lenny-backports")
+ resource.default_release("lenny-backports")
+ expect(resource.default_release).to eql("lenny-backports")
end
end
diff --git a/spec/unit/resource/resource_notification_spec.rb b/spec/unit/resource/resource_notification_spec.rb
new file mode 100644
index 0000000000..7f6b124d4d
--- /dev/null
+++ b/spec/unit/resource/resource_notification_spec.rb
@@ -0,0 +1,170 @@
+#
+# Author:: Tyler Ball (<tball@chef.io>)
+# 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/resource/resource_notification'
+
+describe Chef::Resource::Notification do
+ before do
+ @notification = Chef::Resource::Notification.new(:service_apache, :restart, :template_httpd_conf)
+ end
+
+ it "has a resource to be notified" do
+ expect(@notification.resource).to eq(:service_apache)
+ end
+
+ it "has an action to take on the service" do
+ expect(@notification.action).to eq(:restart)
+ end
+
+ it "has a notifying resource" do
+ expect(@notification.notifying_resource).to eq(:template_httpd_conf)
+ end
+
+ it "is a duplicate of another notification with the same target resource and action" do
+ other = Chef::Resource::Notification.new(:service_apache, :restart, :sync_web_app_code)
+ expect(@notification.duplicates?(other)).to be_truthy
+ end
+
+ it "is not a duplicate of another notification if the actions differ" do
+ other = Chef::Resource::Notification.new(:service_apache, :enable, :install_apache)
+ expect(@notification.duplicates?(other)).to be_falsey
+ end
+
+ it "is not a duplicate of another notification if the target resources differ" do
+ other = Chef::Resource::Notification.new(:service_sshd, :restart, :template_httpd_conf)
+ expect(@notification.duplicates?(other)).to be_falsey
+ end
+
+ it "raises an ArgumentError if you try to check a non-ducktype object for duplication" do
+ expect {@notification.duplicates?(:not_a_notification)}.to raise_error(ArgumentError)
+ end
+
+ it "takes no action to resolve a resource reference that doesn't need to be resolved" do
+ @keyboard_cat = Chef::Resource::Cat.new("keyboard_cat")
+ @notification.resource = @keyboard_cat
+ @long_cat = Chef::Resource::Cat.new("long_cat")
+ @notification.notifying_resource = @long_cat
+ @resource_collection = Chef::ResourceCollection.new
+ # would raise an error since the resource is not in the collection
+ @notification.resolve_resource_reference(@resource_collection)
+ expect(@notification.resource).to eq(@keyboard_cat)
+ end
+
+ it "resolves a lazy reference to a resource" do
+ @notification.resource = {:cat => "keyboard_cat"}
+ @keyboard_cat = Chef::Resource::Cat.new("keyboard_cat")
+ @resource_collection = Chef::ResourceCollection.new
+ @resource_collection << @keyboard_cat
+ @long_cat = Chef::Resource::Cat.new("long_cat")
+ @notification.notifying_resource = @long_cat
+ @notification.resolve_resource_reference(@resource_collection)
+ expect(@notification.resource).to eq(@keyboard_cat)
+ end
+
+ it "resolves a lazy reference to its notifying resource" do
+ @keyboard_cat = Chef::Resource::Cat.new("keyboard_cat")
+ @notification.resource = @keyboard_cat
+ @notification.notifying_resource = {:cat => "long_cat"}
+ @long_cat = Chef::Resource::Cat.new("long_cat")
+ @resource_collection = Chef::ResourceCollection.new
+ @resource_collection << @long_cat
+ @notification.resolve_resource_reference(@resource_collection)
+ expect(@notification.notifying_resource).to eq(@long_cat)
+ end
+
+ it "resolves lazy references to both its resource and its notifying resource" do
+ @notification.resource = {:cat => "keyboard_cat"}
+ @keyboard_cat = Chef::Resource::Cat.new("keyboard_cat")
+ @resource_collection = Chef::ResourceCollection.new
+ @resource_collection << @keyboard_cat
+ @notification.notifying_resource = {:cat => "long_cat"}
+ @long_cat = Chef::Resource::Cat.new("long_cat")
+ @resource_collection << @long_cat
+ @notification.resolve_resource_reference(@resource_collection)
+ expect(@notification.resource).to eq(@keyboard_cat)
+ expect(@notification.notifying_resource).to eq(@long_cat)
+ end
+
+ it "raises a RuntimeError if you try to reference multiple resources" do
+ @notification.resource = {:cat => ["keyboard_cat", "cheez_cat"]}
+ @keyboard_cat = Chef::Resource::Cat.new("keyboard_cat")
+ @cheez_cat = Chef::Resource::Cat.new("cheez_cat")
+ @resource_collection = Chef::ResourceCollection.new
+ @resource_collection << @keyboard_cat
+ @resource_collection << @cheez_cat
+ @long_cat = Chef::Resource::Cat.new("long_cat")
+ @notification.notifying_resource = @long_cat
+ expect {@notification.resolve_resource_reference(@resource_collection)}.to raise_error(RuntimeError)
+ end
+
+ it "raises a RuntimeError if you try to reference multiple notifying resources" do
+ @notification.notifying_resource = {:cat => ["long_cat", "cheez_cat"]}
+ @long_cat = Chef::Resource::Cat.new("long_cat")
+ @cheez_cat = Chef::Resource::Cat.new("cheez_cat")
+ @resource_collection = Chef::ResourceCollection.new
+ @resource_collection << @long_cat
+ @resource_collection << @cheez_cat
+ @keyboard_cat = Chef::Resource::Cat.new("keyboard_cat")
+ @notification.resource = @keyboard_cat
+ expect {@notification.resolve_resource_reference(@resource_collection)}.to raise_error(RuntimeError)
+ end
+
+ it "raises a RuntimeError if it can't find a resource in the resource collection when resolving a lazy reference" do
+ @notification.resource = {:cat => "keyboard_cat"}
+ @cheez_cat = Chef::Resource::Cat.new("cheez_cat")
+ @resource_collection = Chef::ResourceCollection.new
+ @resource_collection << @cheez_cat
+ @long_cat = Chef::Resource::Cat.new("long_cat")
+ @notification.notifying_resource = @long_cat
+ expect {@notification.resolve_resource_reference(@resource_collection)}.to raise_error(RuntimeError)
+ end
+
+ it "raises a RuntimeError if it can't find a notifying resource in the resource collection when resolving a lazy reference" do
+ @notification.notifying_resource = {:cat => "long_cat"}
+ @cheez_cat = Chef::Resource::Cat.new("cheez_cat")
+ @resource_collection = Chef::ResourceCollection.new
+ @resource_collection << @cheez_cat
+ @keyboard_cat = Chef::Resource::Cat.new("keyboard_cat")
+ @notification.resource = @keyboard_cat
+ expect {@notification.resolve_resource_reference(@resource_collection)}.to raise_error(RuntimeError)
+ end
+
+ it "raises an ArgumentError if improper syntax is used in the lazy reference to its resource" do
+ @notification.resource = "cat => keyboard_cat"
+ @keyboard_cat = Chef::Resource::Cat.new("keyboard_cat")
+ @resource_collection = Chef::ResourceCollection.new
+ @resource_collection << @keyboard_cat
+ @long_cat = Chef::Resource::Cat.new("long_cat")
+ @notification.notifying_resource = @long_cat
+ expect {@notification.resolve_resource_reference(@resource_collection)}.to raise_error(ArgumentError)
+ end
+
+ it "raises an ArgumentError if improper syntax is used in the lazy reference to its notifying resource" do
+ @notification.notifying_resource = "cat => long_cat"
+ @long_cat = Chef::Resource::Cat.new("long_cat")
+ @resource_collection = Chef::ResourceCollection.new
+ @resource_collection << @long_cat
+ @keyboard_cat = Chef::Resource::Cat.new("keyboard_cat")
+ @notification.resource = @keyboard_cat
+ expect {@notification.resolve_resource_reference(@resource_collection)}.to raise_error(ArgumentError)
+ end
+
+ # Create test to resolve lazy references to both notifying resource and dest. resource
+ # Create tests to check proper error raising
+
+end
diff --git a/spec/unit/resource_spec.rb b/spec/unit/resource_spec.rb
index 335956009f..79d47ad4dc 100644
--- a/spec/unit/resource_spec.rb
+++ b/spec/unit/resource_spec.rb
@@ -855,154 +855,3 @@ describe Chef::Resource do
end
end
-
-describe Chef::Resource::Notification do
- before do
- @notification = Chef::Resource::Notification.new(:service_apache, :restart, :template_httpd_conf)
- end
-
- it "has a resource to be notified" do
- expect(@notification.resource).to eq(:service_apache)
- end
-
- it "has an action to take on the service" do
- expect(@notification.action).to eq(:restart)
- end
-
- it "has a notifying resource" do
- expect(@notification.notifying_resource).to eq(:template_httpd_conf)
- end
-
- it "is a duplicate of another notification with the same target resource and action" do
- other = Chef::Resource::Notification.new(:service_apache, :restart, :sync_web_app_code)
- expect(@notification.duplicates?(other)).to be_truthy
- end
-
- it "is not a duplicate of another notification if the actions differ" do
- other = Chef::Resource::Notification.new(:service_apache, :enable, :install_apache)
- expect(@notification.duplicates?(other)).to be_falsey
- end
-
- it "is not a duplicate of another notification if the target resources differ" do
- other = Chef::Resource::Notification.new(:service_sshd, :restart, :template_httpd_conf)
- expect(@notification.duplicates?(other)).to be_falsey
- end
-
- it "raises an ArgumentError if you try to check a non-ducktype object for duplication" do
- expect {@notification.duplicates?(:not_a_notification)}.to raise_error(ArgumentError)
- end
-
- it "takes no action to resolve a resource reference that doesn't need to be resolved" do
- @keyboard_cat = Chef::Resource::Cat.new("keyboard_cat")
- @notification.resource = @keyboard_cat
- @long_cat = Chef::Resource::Cat.new("long_cat")
- @notification.notifying_resource = @long_cat
- @resource_collection = Chef::ResourceCollection.new
- # would raise an error since the resource is not in the collection
- @notification.resolve_resource_reference(@resource_collection)
- expect(@notification.resource).to eq(@keyboard_cat)
- end
-
- it "resolves a lazy reference to a resource" do
- @notification.resource = {:cat => "keyboard_cat"}
- @keyboard_cat = Chef::Resource::Cat.new("keyboard_cat")
- @resource_collection = Chef::ResourceCollection.new
- @resource_collection << @keyboard_cat
- @long_cat = Chef::Resource::Cat.new("long_cat")
- @notification.notifying_resource = @long_cat
- @notification.resolve_resource_reference(@resource_collection)
- expect(@notification.resource).to eq(@keyboard_cat)
- end
-
- it "resolves a lazy reference to its notifying resource" do
- @keyboard_cat = Chef::Resource::Cat.new("keyboard_cat")
- @notification.resource = @keyboard_cat
- @notification.notifying_resource = {:cat => "long_cat"}
- @long_cat = Chef::Resource::Cat.new("long_cat")
- @resource_collection = Chef::ResourceCollection.new
- @resource_collection << @long_cat
- @notification.resolve_resource_reference(@resource_collection)
- expect(@notification.notifying_resource).to eq(@long_cat)
- end
-
- it "resolves lazy references to both its resource and its notifying resource" do
- @notification.resource = {:cat => "keyboard_cat"}
- @keyboard_cat = Chef::Resource::Cat.new("keyboard_cat")
- @resource_collection = Chef::ResourceCollection.new
- @resource_collection << @keyboard_cat
- @notification.notifying_resource = {:cat => "long_cat"}
- @long_cat = Chef::Resource::Cat.new("long_cat")
- @resource_collection << @long_cat
- @notification.resolve_resource_reference(@resource_collection)
- expect(@notification.resource).to eq(@keyboard_cat)
- expect(@notification.notifying_resource).to eq(@long_cat)
- end
-
- it "raises a RuntimeError if you try to reference multiple resources" do
- @notification.resource = {:cat => ["keyboard_cat", "cheez_cat"]}
- @keyboard_cat = Chef::Resource::Cat.new("keyboard_cat")
- @cheez_cat = Chef::Resource::Cat.new("cheez_cat")
- @resource_collection = Chef::ResourceCollection.new
- @resource_collection << @keyboard_cat
- @resource_collection << @cheez_cat
- @long_cat = Chef::Resource::Cat.new("long_cat")
- @notification.notifying_resource = @long_cat
- expect {@notification.resolve_resource_reference(@resource_collection)}.to raise_error(RuntimeError)
- end
-
- it "raises a RuntimeError if you try to reference multiple notifying resources" do
- @notification.notifying_resource = {:cat => ["long_cat", "cheez_cat"]}
- @long_cat = Chef::Resource::Cat.new("long_cat")
- @cheez_cat = Chef::Resource::Cat.new("cheez_cat")
- @resource_collection = Chef::ResourceCollection.new
- @resource_collection << @long_cat
- @resource_collection << @cheez_cat
- @keyboard_cat = Chef::Resource::Cat.new("keyboard_cat")
- @notification.resource = @keyboard_cat
- expect {@notification.resolve_resource_reference(@resource_collection)}.to raise_error(RuntimeError)
- end
-
- it "raises a RuntimeError if it can't find a resource in the resource collection when resolving a lazy reference" do
- @notification.resource = {:cat => "keyboard_cat"}
- @cheez_cat = Chef::Resource::Cat.new("cheez_cat")
- @resource_collection = Chef::ResourceCollection.new
- @resource_collection << @cheez_cat
- @long_cat = Chef::Resource::Cat.new("long_cat")
- @notification.notifying_resource = @long_cat
- expect {@notification.resolve_resource_reference(@resource_collection)}.to raise_error(RuntimeError)
- end
-
- it "raises a RuntimeError if it can't find a notifying resource in the resource collection when resolving a lazy reference" do
- @notification.notifying_resource = {:cat => "long_cat"}
- @cheez_cat = Chef::Resource::Cat.new("cheez_cat")
- @resource_collection = Chef::ResourceCollection.new
- @resource_collection << @cheez_cat
- @keyboard_cat = Chef::Resource::Cat.new("keyboard_cat")
- @notification.resource = @keyboard_cat
- expect {@notification.resolve_resource_reference(@resource_collection)}.to raise_error(RuntimeError)
- end
-
- it "raises an ArgumentError if improper syntax is used in the lazy reference to its resource" do
- @notification.resource = "cat => keyboard_cat"
- @keyboard_cat = Chef::Resource::Cat.new("keyboard_cat")
- @resource_collection = Chef::ResourceCollection.new
- @resource_collection << @keyboard_cat
- @long_cat = Chef::Resource::Cat.new("long_cat")
- @notification.notifying_resource = @long_cat
- expect {@notification.resolve_resource_reference(@resource_collection)}.to raise_error(ArgumentError)
- end
-
- it "raises an ArgumentError if improper syntax is used in the lazy reference to its notifying resource" do
- @notification.notifying_resource = "cat => long_cat"
- @long_cat = Chef::Resource::Cat.new("long_cat")
- @resource_collection = Chef::ResourceCollection.new
- @resource_collection << @long_cat
- @keyboard_cat = Chef::Resource::Cat.new("keyboard_cat")
- @notification.resource = @keyboard_cat
- expect {@notification.resolve_resource_reference(@resource_collection)}.to raise_error(ArgumentError)
- end
-
- # Create test to resolve lazy references to both notifying resource and dest. resource
- # Create tests to check proper error raising
-
-end
diff --git a/spec/unit/run_context_spec.rb b/spec/unit/run_context_spec.rb
index cc112f332a..ba4f2ede68 100644
--- a/spec/unit/run_context_spec.rb
+++ b/spec/unit/run_context_spec.rb
@@ -21,19 +21,24 @@
require 'spec_helper'
require 'support/lib/library_load_order'
-
describe Chef::RunContext do
+ let(:chef_repo_path) { File.expand_path(File.join(CHEF_SPEC_DATA, "run_context", "cookbooks")) }
+ let(:cookbook_collection) {
+ cl = Chef::CookbookLoader.new(chef_repo_path)
+ cl.load_cookbooks
+ Chef::CookbookCollection.new(cl)
+ }
+ let(:node) {
+ node = Chef::Node.new
+ node.run_list << "test" << "test::one" << "test::two"
+ node
+ }
+ let(:events) { Chef::EventDispatch::Dispatcher.new }
+ let(:run_context) { Chef::RunContext.new(node, cookbook_collection, events) }
+
before(:each) do
@original_log_level = Chef::Log.level
Chef::Log.level = :debug
- @chef_repo_path = File.expand_path(File.join(CHEF_SPEC_DATA, "run_context", "cookbooks"))
- cl = Chef::CookbookLoader.new(@chef_repo_path)
- cl.load_cookbooks
- @cookbook_collection = Chef::CookbookCollection.new(cl)
- @node = Chef::Node.new
- @node.run_list << "test" << "test::one" << "test::two"
- @events = Chef::EventDispatch::Dispatcher.new
- @run_context = Chef::RunContext.new(@node, @cookbook_collection, @events)
end
after(:each) do
@@ -41,11 +46,11 @@ describe Chef::RunContext do
end
it "has a cookbook collection" do
- expect(@run_context.cookbook_collection).to eq(@cookbook_collection)
+ expect(run_context.cookbook_collection).to eq(cookbook_collection)
end
it "has a node" do
- expect(@run_context.node).to eq(@node)
+ expect(run_context.node).to eq(node)
end
describe "loading cookbooks for a run list" do
@@ -57,44 +62,44 @@ describe Chef::RunContext do
Chef::Provider.send(:remove_const, :TestProvider)
end
- @node.run_list << "test" << "test::one" << "test::two"
- expect(@node).to receive(:loaded_recipe).with(:test, "default")
- expect(@node).to receive(:loaded_recipe).with(:test, "one")
- expect(@node).to receive(:loaded_recipe).with(:test, "two")
- @run_context.load(@node.run_list.expand('_default'))
+ node.run_list << "test" << "test::one" << "test::two"
+ expect(node).to receive(:loaded_recipe).with(:test, "default")
+ expect(node).to receive(:loaded_recipe).with(:test, "one")
+ expect(node).to receive(:loaded_recipe).with(:test, "two")
+ run_context.load(node.run_list.expand('_default'))
end
it "should load all the definitions in the cookbooks for this node" do
- expect(@run_context.definitions).to have_key(:new_cat)
- expect(@run_context.definitions).to have_key(:new_badger)
- expect(@run_context.definitions).to have_key(:new_dog)
+ expect(run_context.definitions).to have_key(:new_cat)
+ expect(run_context.definitions).to have_key(:new_badger)
+ expect(run_context.definitions).to have_key(:new_dog)
end
it "should load all the recipes specified for this node" do
- expect(@run_context.resource_collection[0].to_s).to eq("cat[einstein]")
- expect(@run_context.resource_collection[1].to_s).to eq("cat[loulou]")
- expect(@run_context.resource_collection[2].to_s).to eq("cat[birthday]")
- expect(@run_context.resource_collection[3].to_s).to eq("cat[peanut]")
- expect(@run_context.resource_collection[4].to_s).to eq("cat[fat peanut]")
+ expect(run_context.resource_collection[0].to_s).to eq("cat[einstein]")
+ expect(run_context.resource_collection[1].to_s).to eq("cat[loulou]")
+ expect(run_context.resource_collection[2].to_s).to eq("cat[birthday]")
+ expect(run_context.resource_collection[3].to_s).to eq("cat[peanut]")
+ expect(run_context.resource_collection[4].to_s).to eq("cat[fat peanut]")
end
it "loads all the attribute files in the cookbook collection" do
- expect(@run_context.loaded_fully_qualified_attribute?("test", "george")).to be_truthy
- expect(@node[:george]).to eq("washington")
+ expect(run_context.loaded_fully_qualified_attribute?("test", "george")).to be_truthy
+ expect(node[:george]).to eq("washington")
end
it "registers attributes files as loaded so they won't be reloaded" do
# This test unfortunately is pretty tightly intertwined with the
# implementation of how nodes load attribute files, but is the only
# convenient way to test this behavior.
- expect(@node).not_to receive(:from_file)
- @node.include_attribute("test::george")
+ expect(node).not_to receive(:from_file)
+ node.include_attribute("test::george")
end
it "raises an error when attempting to include_recipe from a cookbook not reachable by run list or dependencies" do
- expect(@node).to receive(:loaded_recipe).with(:ancient, "aliens")
+ expect(node).to receive(:loaded_recipe).with(:ancient, "aliens")
expect do
- @run_context.include_recipe("ancient::aliens")
+ run_context.include_recipe("ancient::aliens")
# In CHEF-5120, this becomes a Chef::Exceptions::MissingCookbookDependency error:
end.to raise_error(Chef::Exceptions::CookbookNotFound)
end
@@ -102,39 +107,34 @@ describe Chef::RunContext do
end
describe "querying the contents of cookbooks" do
- before do
- @chef_repo_path = File.expand_path(File.join(CHEF_SPEC_DATA, "cookbooks"))
- cl = Chef::CookbookLoader.new(@chef_repo_path)
- cl.load_cookbooks
- @cookbook_collection = Chef::CookbookCollection.new(cl)
- @node = Chef::Node.new
- @node.set[:platform] = "ubuntu"
- @node.set[:platform_version] = "13.04"
- @node.name("testing")
- @events = Chef::EventDispatch::Dispatcher.new
- @run_context = Chef::RunContext.new(@node, @cookbook_collection, @events)
- end
-
+ let(:chef_repo_path) { File.expand_path(File.join(CHEF_SPEC_DATA, "cookbooks")) }
+ let(:node) {
+ node = Chef::Node.new
+ node.set[:platform] = "ubuntu"
+ node.set[:platform_version] = "13.04"
+ node.name("testing")
+ node
+ }
it "queries whether a given cookbook has a specific template" do
- expect(@run_context).to have_template_in_cookbook("openldap", "test.erb")
- expect(@run_context).not_to have_template_in_cookbook("openldap", "missing.erb")
+ expect(run_context).to have_template_in_cookbook("openldap", "test.erb")
+ expect(run_context).not_to have_template_in_cookbook("openldap", "missing.erb")
end
it "errors when querying for a template in a not-available cookbook" do
expect do
- @run_context.has_template_in_cookbook?("no-such-cookbook", "foo.erb")
+ run_context.has_template_in_cookbook?("no-such-cookbook", "foo.erb")
end.to raise_error(Chef::Exceptions::CookbookNotFound)
end
it "queries whether a given cookbook has a specific cookbook_file" do
- expect(@run_context).to have_cookbook_file_in_cookbook("java", "java.response")
- expect(@run_context).not_to have_cookbook_file_in_cookbook("java", "missing.txt")
+ expect(run_context).to have_cookbook_file_in_cookbook("java", "java.response")
+ expect(run_context).not_to have_cookbook_file_in_cookbook("java", "missing.txt")
end
it "errors when querying for a cookbook_file in a not-available cookbook" do
expect do
- @run_context.has_cookbook_file_in_cookbook?("no-such-cookbook", "foo.txt")
+ run_context.has_cookbook_file_in_cookbook?("no-such-cookbook", "foo.txt")
end.to raise_error(Chef::Exceptions::CookbookNotFound)
end
end
@@ -145,13 +145,13 @@ describe Chef::RunContext do
end
it "stores and deletes the reboot request" do
- @run_context.request_reboot(expected)
- expect(@run_context.reboot_info).to eq(expected)
- expect(@run_context.reboot_requested?).to be_truthy
+ run_context.request_reboot(expected)
+ expect(run_context.reboot_info).to eq(expected)
+ expect(run_context.reboot_requested?).to be_truthy
- @run_context.cancel_reboot
- expect(@run_context.reboot_info).to eq({})
- expect(@run_context.reboot_requested?).to be_falsey
+ run_context.cancel_reboot
+ expect(run_context.reboot_info).to eq({})
+ expect(run_context.reboot_requested?).to be_falsey
end
end
end
diff --git a/spec/unit/search/query_spec.rb b/spec/unit/search/query_spec.rb
index d4ff9e4367..2fb197b183 100644
--- a/spec/unit/search/query_spec.rb
+++ b/spec/unit/search/query_spec.rb
@@ -24,7 +24,7 @@ describe Chef::Search::Query do
let(:query) { Chef::Search::Query.new }
shared_context "filtered search" do
- let(:query_string) { "search/node?q=platform:rhel&sort=X_CHEF_id_CHEF_X%20asc&start=0&rows=1000" }
+ let(:query_string) { "search/node?q=platform:rhel&sort=X_CHEF_id_CHEF_X%20asc&start=0" }
let(:server_url) { "https://api.opscode.com/organizations/opscode/nodes" }
let(:args) { { filter_key => filter_hash } }
let(:filter_hash) {
@@ -65,6 +65,14 @@ describe Chef::Search::Query do
"total" => 4
}
}
+ let(:response_rows) {
+ [
+ { "env" => "elysium", "ruby_plat" => "nudibranch" },
+ { "env" => "hades", "ruby_plat" => "i386-mingw32"},
+ { "env" => "elysium", "ruby_plat" => "centos"},
+ { "env" => "moon", "ruby_plat" => "solaris2"}
+ ]
+ }
end
before(:each) do
@@ -132,59 +140,59 @@ describe Chef::Search::Query do
"total" => 4
} }
- it "should accept a type as the first argument" do
+ it "accepts a type as the first argument" do
expect { query.search("node") }.not_to raise_error
expect { query.search(:node) }.not_to raise_error
expect { query.search(Hash.new) }.to raise_error(Chef::Exceptions::InvalidSearchQuery, /(Hash)/)
end
- it "should query for every object of a type by default" do
- expect(rest).to receive(:get_rest).with("search/node?q=*:*&sort=X_CHEF_id_CHEF_X%20asc&start=0&rows=1000").and_return(response)
+ it "queries for every object of a type by default" do
+ expect(rest).to receive(:get_rest).with("search/node?q=*:*&sort=X_CHEF_id_CHEF_X%20asc&start=0").and_return(response)
query.search(:node)
end
- it "should allow a custom query" do
- expect(rest).to receive(:get_rest).with("search/node?q=platform:rhel&sort=X_CHEF_id_CHEF_X%20asc&start=0&rows=1000").and_return(response)
+ it "allows a custom query" do
+ expect(rest).to receive(:get_rest).with("search/node?q=platform:rhel&sort=X_CHEF_id_CHEF_X%20asc&start=0").and_return(response)
query.search(:node, "platform:rhel")
end
- it "should let you set a sort order" do
- expect(rest).to receive(:get_rest).with("search/node?q=platform:rhel&sort=id%20desc&start=0&rows=1000").and_return(response)
- query.search(:node, "platform:rhel", "id desc")
+ it "lets you set a sort order" do
+ expect(rest).to receive(:get_rest).with("search/node?q=platform:rhel&sort=id%20desc&start=0").and_return(response)
+ query.search(:node, "platform:rhel", sort: "id desc")
end
- it "should let you set a starting object" do
- expect(rest).to receive(:get_rest).with("search/node?q=platform:rhel&sort=id%20desc&start=2&rows=1000").and_return(response)
- query.search(:node, "platform:rhel", "id desc", 2)
+ it "lets you set a starting object" do
+ expect(rest).to receive(:get_rest).with("search/node?q=platform:rhel&sort=X_CHEF_id_CHEF_X%20asc&start=2").and_return(response)
+ query.search(:node, "platform:rhel", start: 2)
end
- it "should let you set how many rows to return" do
- expect(rest).to receive(:get_rest).with("search/node?q=platform:rhel&sort=id%20desc&start=2&rows=40").and_return(response)
- query.search(:node, "platform:rhel", "id desc", 2, 40)
+ it "lets you set how many rows to return" do
+ expect(rest).to receive(:get_rest).with("search/node?q=platform:rhel&sort=X_CHEF_id_CHEF_X%20asc&start=0&rows=40").and_return(response)
+ query.search(:node, "platform:rhel", rows: 40)
end
- it "should throw an exception if you pass to many options" do
- expect { query.search(:node, "platform:rhel", "id desc", 2, 40, "wrong") }
- .to raise_error(Chef::Exceptions::InvalidSearchQuery, "Too many arguments! (4 for <= 3)")
+ it "throws an exception if you pass an incorrect option" do
+ expect { query.search(:node, "platform:rhel", total: 10) }
+ .to raise_error(ArgumentError, /unknown keyword: total/)
end
- it "should return the raw rows, start, and total if no block is passed" do
+ it "returns the raw rows, start, and total if no block is passed" do
rows, start, total = query.search(:node)
expect(rows).to equal(response["rows"])
expect(start).to equal(response["start"])
expect(total).to equal(response["total"])
end
- it "should call a block for each object in the response" do
+ it "calls a block for each object in the response" do
@call_me = double("blocky")
response["rows"].each { |r| expect(@call_me).to receive(:do).with(r) }
query.search(:node) { |r| @call_me.do(r) }
end
- it "should page through the responses" do
+ it "pages through the responses" do
@call_me = double("blocky")
response["rows"].each { |r| expect(@call_me).to receive(:do).with(r) }
- query.search(:node, "*:*", nil, 0, 1) { |r| @call_me.do(r) }
+ query.search(:node, "*:*", sort: nil, start: 0, rows: 1) { |r| @call_me.do(r) }
end
context "when :filter_result is provided as a result" do
@@ -195,31 +203,19 @@ describe Chef::Search::Query do
expect(rest).to receive(:post_rest).with(query_string, args[filter_key]).and_return(response)
end
- it "should return start" do
+ it "returns start" do
start = query.search(:node, "platform:rhel", args)[1]
expect(start).to eq(response['start'])
end
- it "should return total" do
+ it "returns total" do
total = query.search(:node, "platform:rhel", args)[2]
expect(total).to eq(response['total'])
end
- it "should return rows with the filter applied" do
- results = query.search(:node, "platform:rhel", args)[0]
-
- results.each_with_index do |result, idx|
- expected = response["rows"][idx]
-
- expect(result).to have_key("url")
- expect(result["url"]).to eq(expected["url"])
-
- expect(result).to have_key("data")
- filter_hash.keys.each do |filter_key|
- expect(result["data"]).to have_key(filter_key)
- expect(result["data"][filter_key]).to eq(expected["data"][filter_key])
- end
- end
+ it "returns rows with the filter applied" do
+ filtered_rows = query.search(:node, "platform:rhel", args)[0]
+ expect(filtered_rows).to match_array(response_rows)
end
end
@@ -230,25 +226,17 @@ describe Chef::Search::Query do
include_context "filtered search" do
let(:filter_key) { :keys }
- it "should emit a deprecation warning" do
+ it "emits a deprecation warning" do
# partial_search calls search, so we'll stub search to return empty
allow(query).to receive(:search).and_return( [ [], 0, 0 ] )
- expect(Chef::Log).to receive(:warn).with("DEPRECATED: The 'partial_search' api is deprecated, please use the search api with 'filter_result'")
+ expect(Chef::Log).to receive(:warn).with(/DEPRECATED: The 'partial_search' API is deprecated/)
query.partial_search(:node, "platform:rhel", args)
end
- it "should return an array of filtered hashes" do
+ it "returns an array of filtered hashes" do
expect(rest).to receive(:post_rest).with(query_string, args[filter_key]).and_return(response)
results = query.partial_search(:node, "platform:rhel", args)
-
- results.each_with_index do |result, idx|
- expected = response["rows"][idx]
-
- filter_hash.keys.each do |filter_key|
- expect(result).to have_key(filter_key)
- expect(result[filter_key]).to eq(expected["data"][filter_key])
- end
- end
+ expect(results[0]).to match_array(response_rows)
end
end
end
diff --git a/spec/unit/util/dsc/lcm_output_parser_spec.rb b/spec/unit/util/dsc/lcm_output_parser_spec.rb
index ba4f40c4f7..3d44e07885 100644
--- a/spec/unit/util/dsc/lcm_output_parser_spec.rb
+++ b/spec/unit/util/dsc/lcm_output_parser_spec.rb
@@ -20,24 +20,19 @@ require 'chef/util/dsc/lcm_output_parser'
describe Chef::Util::DSC::LocalConfigurationManager::Parser do
context 'empty input parameter' do
- it 'returns an empty array for a 0 length string' do
- expect(Chef::Util::DSC::LocalConfigurationManager::Parser::parse('')).to be_empty
+ it 'raises an exception when there are no valid lines' do
+ str = <<-EOF
+
+ EOF
+ expect {Chef::Util::DSC::LocalConfigurationManager::Parser::parse(str)}.to raise_error(Chef::Exceptions::LCMParser)
end
- it 'returns an empty array for a nil input' do
- expect(Chef::Util::DSC::LocalConfigurationManager::Parser::parse('')).to be_empty
+ it 'raises an exception for a nil input' do
+ expect {Chef::Util::DSC::LocalConfigurationManager::Parser::parse(nil)}.to raise_error(Chef::Exceptions::LCMParser)
end
end
context 'correctly formatted output from lcm' do
- it 'returns an empty array for a log with no resources' do
- str = <<EOF
-logtype: [machinename]: LCM: [ Start Set ]
-logtype: [machinename]: LCM: [ End Set ]
-EOF
- expect(Chef::Util::DSC::LocalConfigurationManager::Parser::parse(str)).to be_empty
- end
-
it 'returns a single resource when only 1 logged with the correct name' do
str = <<EOF
logtype: [machinename]: LCM: [ Start Set ]
diff --git a/spec/unit/util/dsc/local_configuration_manager_spec.rb b/spec/unit/util/dsc/local_configuration_manager_spec.rb
index 009c667c87..1281862e67 100644
--- a/spec/unit/util/dsc/local_configuration_manager_spec.rb
+++ b/spec/unit/util/dsc/local_configuration_manager_spec.rb
@@ -82,7 +82,7 @@ EOH
end
it 'should should return a (possibly empty) array of ResourceInfo instances' do
- expect(Chef::Log).to receive(:warn)
+ expect(Chef::Log).to receive(:warn).at_least(:once)
expect(lcm).to receive(:whatif_not_supported?).and_call_original
test_configuration_result = nil
expect {test_configuration_result = lcm.test_configuration('config')}.not_to raise_error
@@ -96,14 +96,14 @@ EOH
let(:lcm_cmdlet_success) { false }
it 'should log a warning if the message is formatted as expected when a resource import failure occurs' do
- expect(Chef::Log).to receive(:warn)
+ expect(Chef::Log).to receive(:warn).at_least(:once)
expect(lcm).to receive(:dsc_module_import_failure?).and_call_original
test_configuration_result = nil
expect {test_configuration_result = lcm.test_configuration('config')}.not_to raise_error
end
it 'should return a (possibly empty) array of ResourceInfo instances' do
- expect(Chef::Log).to receive(:warn)
+ expect(Chef::Log).to receive(:warn).at_least(:once)
test_configuration_result = nil
expect {test_configuration_result = lcm.test_configuration('config')}.not_to raise_error
expect(test_configuration_result.class).to be(Array)
@@ -116,7 +116,7 @@ EOH
let(:lcm_cmdlet_success) { false }
it 'should log a warning' do
- expect(Chef::Log).to receive(:warn)
+ expect(Chef::Log).to receive(:warn).at_least(:once)
expect(lcm).to receive(:dsc_module_import_failure?).and_call_original
expect {lcm.test_configuration('config')}.not_to raise_error
end