diff options
-rw-r--r-- | DOC_CHANGES.md | 541 | ||||
-rw-r--r-- | lib/chef/audit/audit_reporter.rb | 2 | ||||
-rw-r--r-- | lib/chef/audit/runner.rb | 12 | ||||
-rw-r--r-- | lib/chef/client.rb | 10 | ||||
-rw-r--r-- | lib/chef/exceptions.rb | 5 | ||||
-rw-r--r-- | lib/chef/formatters/doc.rb | 29 | ||||
-rw-r--r-- | spec/unit/audit/control_group_data_spec.rb | 459 |
7 files changed, 518 insertions, 540 deletions
diff --git a/DOC_CHANGES.md b/DOC_CHANGES.md index 22294a3e39..15f88abdca 100644 --- a/DOC_CHANGES.md +++ b/DOC_CHANGES.md @@ -6,540 +6,17 @@ Example Doc Change: Description of the required change. --> -### Knife now prefers to use `config.rb` rather than `knife.rb` +### Chef Why Run Mode Ignores Audit Phase -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. +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. -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. +#### Editors note 1 -### value_for_platform Method +The `--audit-mode` flag should be a link to the documentation for that flag -- 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: +#### Editors node 2 -+ * 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" } -+ ) -+ ``` - -+ If each value only has a single platform, then the syntax is like the following: - -### environment attribute to git provider - -Similar to other environment options: - -``` -environment Hash of environment variables in the form of {"ENV_VARIABLE" => "VALUE"}. -``` - -Also the `user` attribute should mention the setting of the HOME env var: - -``` -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/lib/chef/audit/audit_reporter.rb b/lib/chef/audit/audit_reporter.rb index d022ac0c47..00af9984b2 100644 --- a/lib/chef/audit/audit_reporter.rb +++ b/lib/chef/audit/audit_reporter.rb @@ -55,6 +55,8 @@ class Chef # 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.") diff --git a/lib/chef/audit/runner.rb b/lib/chef/audit/runner.rb index 51c007d1d0..e15b3103f8 100644 --- a/lib/chef/audit/runner.rb +++ b/lib/chef/audit/runner.rb @@ -33,6 +33,18 @@ class Chef 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 diff --git a/lib/chef/client.rb b/lib/chef/client.rb index aa0d6722fe..634773cf80 100644 --- a/lib/chef/client.rb +++ b/lib/chef/client.rb @@ -358,9 +358,12 @@ class Chef 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}") + Chef::Log.error("Audit phase failed with error message: #{e.message}") @events.audit_phase_failed(e) audit_exception = e end @@ -445,7 +448,10 @@ class Chef converge_error = converge_and_save(run_context) end - if Chef::Config[:audit_mode] != :disabled + 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 diff --git a/lib/chef/exceptions.rb b/lib/chef/exceptions.rb index c270f709c9..ef957ec502 100644 --- a/lib/chef/exceptions.rb +++ b/lib/chef/exceptions.rb @@ -390,6 +390,11 @@ class Chef 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` diff --git a/lib/chef/formatters/doc.rb b/lib/chef/formatters/doc.rb index 99603965a9..398c61fdc8 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 @@ -156,11 +170,6 @@ class Chef converge_complete end - ############# - # TODO - # Make all these document printers neater - ############# - # Called before audit phase starts def audit_phase_start(run_status) puts_line "Starting audit phase" @@ -181,6 +190,14 @@ class Chef 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/spec/unit/audit/control_group_data_spec.rb b/spec/unit/audit/control_group_data_spec.rb new file mode 100644 index 0000000000..e5e76a6011 --- /dev/null +++ b/spec/unit/audit/control_group_data_spec.rb @@ -0,0 +1,459 @@ + +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 |