summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore3
-rw-r--r--.kitchen.yml84
-rw-r--r--.travis.yml1
-rw-r--r--chef.gemspec10
-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--lib/chef/application/client.rb10
-rw-r--r--lib/chef/application/solo.rb3
-rw-r--r--lib/chef/audit/audit_event_proxy.rb76
-rw-r--r--lib/chef/audit/audit_reporter.rb166
-rw-r--r--lib/chef/audit/control_group_data.rb128
-rw-r--r--lib/chef/audit/rspec_formatter.rb19
-rw-r--r--lib/chef/audit/runner.rb174
-rw-r--r--lib/chef/client.rb92
-rw-r--r--lib/chef/config.rb3
-rw-r--r--lib/chef/dsl/audit.rb42
-rw-r--r--lib/chef/event_dispatch/base.rb30
-rw-r--r--lib/chef/exceptions.rb35
-rw-r--r--lib/chef/formatters/doc.rb30
-rw-r--r--lib/chef/monologger.rb2
-rw-r--r--lib/chef/recipe.rb2
-rw-r--r--lib/chef/run_context.rb4
-rw-r--r--lib/chef/version.rb2
-rw-r--r--spec/functional/resource/deploy_revision_spec.rb3
-rw-r--r--spec/functional/resource/git_spec.rb2
-rw-r--r--spec/integration/solo/solo_spec.rb2
-rw-r--r--spec/spec_helper.rb2
-rw-r--r--spec/unit/client_spec.rb9
-rw-r--r--spec/unit/exceptions_spec.rb46
43 files changed, 1264 insertions, 37 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..c9be1b56e7
--- /dev/null
+++ b/.kitchen.yml
@@ -0,0 +1,84 @@
+driver:
+ name: vagrant
+ forward_agent: yes
+ customize:
+ cpus: 4
+ memory: 4096
+ synced_folders:
+ - ['.', '/home/vagrant/chef']
+ - ['../ohai', '/home/vagrant/ohai']
+ - ['../triager', '/home/vagrant/triager']
+
+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/.travis.yml b/.travis.yml
index 37418ab621..e9e7c2cdc2 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -8,6 +8,7 @@ branches:
- 10-stable
- 11-stable
- 12-stable
+ - audit-mode
# do not run expensive spec tests on PRs, only on branches
script: "
diff --git a/chef.gemspec b/chef.gemspec
index f623f8bb82..eb7528e630 100644
--- a/chef.gemspec
+++ b/chef.gemspec
@@ -35,16 +35,16 @@ Gem::Specification.new do |s|
s.add_dependency 'plist', '~> 3.1.0'
+ %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.3"
+ s.add_dependency "specinfra", "~> 2.4"
+
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/lib/chef/application/client.rb b/lib/chef/application/client.rb
index 5463f504bc..6ca674d287 100644
--- a/lib/chef/application/client.rb
+++ b/lib/chef/application/client.rb
@@ -241,7 +241,15 @@ class Chef::Application::Client < Chef::Application
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
+ :boolean => true,
+ :proc => lambda { |set|
+ # Convert boolean to config options of :audit_only or :disabled
+ if set
+ :audit_only
+ else
+ :disabled
+ end
+ }
IMMEDIATE_RUN_SIGNAL = "1".freeze
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/audit/audit_event_proxy.rb b/lib/chef/audit/audit_event_proxy.rb
new file mode 100644
index 0000000000..6d5591d943
--- /dev/null
+++ b/lib/chef/audit/audit_event_proxy.rb
@@ -0,0 +1,76 @@
+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]
+ # TODO submit github PR to expose this
+ resource_name = described_class.instance_variable_get(:@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..21ffb62829
--- /dev/null
+++ b/lib/chef/audit/audit_reporter.rb
@@ -0,0 +1,166 @@
+#
+# Auther:: 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/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.0'
+
+ 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 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.
+ 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
+ ordered_control_groups.store(name, ControlGroupData.new(name))
+ 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
+ # TODO: Rather than a single string we might want to format the exception here similar to
+ # lib/chef/resource_reporter.rb#83
+ 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
+ code = e.response.code.nil? ? "Exception Code Empty" : e.response.code
+
+ # 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 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..e221ae94cc
--- /dev/null
+++ b/lib/chef/audit/control_group_data.rb
@@ -0,0 +1,128 @@
+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_success, :number_failed, :controls
+
+ def initialize(name)
+ @status = "success"
+ @controls = []
+ @number_success = 0
+ @number_failed = 0
+ @name = name
+ end
+
+
+ def example_success(control_data)
+ @number_success += 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_success => number_success,
+ :number_failed => number_failed,
+ :controls => controls.collect { |c| c.to_hash }
+ }
+ add_display_only_data(h)
+ end
+
+ private
+
+ def create_control(control_data)
+ name = control_data[:name]
+ resource_type = control_data[:resource_type]
+ resource_name = control_data[:resource_name]
+ context = control_data[:context]
+ line_number = control_data[:line_number]
+ # TODO make this smarter with splat arguments so if we start passing in more control_data
+ # I don't have to modify code in multiple places
+ ControlData.new(name, resource_type, resource_name, context, line_number)
+ 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(name, resource_type, resource_name, context, line_number)
+ @context = context
+ @name = name
+ @resource_type = resource_type
+ @resource_name = resource_name
+ @line_number = line_number
+ 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..990c1cd780
--- /dev/null
+++ b/lib/chef/audit/rspec_formatter.rb
@@ -0,0 +1,19 @@
+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..306212989a
--- /dev/null
+++ b/lib/chef/audit/runner.rb
@@ -0,0 +1,174 @@
+#
+# 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
+
+ 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
+ # TODO: We need to figure out a way to give audits its own configuration
+ # object. This involves finding a way to load these files w/o them adding
+ # to the configuration object used by our spec tests.
+ 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
+ # TODO: Do some testing to ensure these will output/output properly to
+ # a file.
+ 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.
+ def configure_specinfra
+ # TODO: We may need to be clever and adjust this based on operating
+ # system, or make it configurable. E.g., there is a PowerShell backend,
+ # as well as an SSH backend.
+ 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/client.rb b/lib/chef/client.rb
index 4f37bd0ee3..aa0d6722fe 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,52 @@ 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
+
+ # TODO don't want to change old API
+ 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
+ @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 +379,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 +425,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 +441,19 @@ 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[: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 +464,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..957db845b0 100644
--- a/lib/chef/config.rb
+++ b/lib/chef/config.rb
@@ -320,6 +320,9 @@ class Chef
default :ez, false
default :enable_reporting, true
default :enable_reporting_url_fatals, false
+ # Possible values for :audit_mode
+ # :enabled, :disabled, :audit_only,
+ default :audit_mode, :enabled
# 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/dsl/audit.rb b/lib/chef/dsl/audit.rb
new file mode 100644
index 0000000000..a11d9039ef
--- /dev/null
+++ b/lib/chef/dsl/audit.rb
@@ -0,0 +1,42 @@
+#
+# 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.controls.has_key?(name)
+ raise Chef::Exceptions::AuditControlGroupDuplicate.new(name)
+ end
+
+ run_context.audits[name] = { :args => args, :block => block }
+ end
+
+ end
+ end
+end
diff --git a/lib/chef/event_dispatch/base.rb b/lib/chef/event_dispatch/base.rb
index 50d261cecd..695e31cf2e 100644
--- a/lib/chef/event_dispatch/base.rb
+++ b/lib/chef/event_dispatch/base.rb
@@ -225,6 +225,36 @@ class Chef
def converge_complete
end
+ # Called if the converge phase fails
+ def converge_failed(exception)
+ end
+
+ # 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 c8d26dbed2..c270f709c9 100644
--- a/lib/chef/exceptions.rb
+++ b/lib/chef/exceptions.rb
@@ -378,5 +378,40 @@ 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
+
+ # 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\n"
+ @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..99603965a9 100644
--- a/lib/chef/formatters/doc.rb
+++ b/lib/chef/formatters/doc.rb
@@ -151,6 +151,36 @@ class Chef
unindent if @current_recipe
end
+ def converge_failed(e)
+ # TODO do we want to do anything else in here?
+ 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"
+ end
+
+ def audit_phase_complete
+ puts_line "Auditing complete"
+ end
+
+ def audit_phase_failed(error)
+ puts_line ""
+ puts_line "Audit phase exception:"
+ indent
+ # TODO error_mapper ?
+ puts_line "#{error.message}"
+ error.backtrace.each do |l|
+ puts_line l
+ end
+ 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/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/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/run_context.rb b/lib/chef/run_context.rb
index 1a2d7ba3a3..d14035da2f 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
diff --git a/lib/chef/version.rb b/lib/chef/version.rb
index a8fc002399..30c3394c2c 100644
--- a/lib/chef/version.rb
+++ b/lib/chef/version.rb
@@ -17,7 +17,7 @@
class Chef
CHEF_ROOT = File.dirname(File.expand_path(File.dirname(__FILE__)))
- VERSION = '12.1.0.dev.0'
+ VERSION = '12.2.0.alpha.0'
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/integration/solo/solo_spec.rb b/spec/integration/solo/solo_spec.rb
index cc9ba1abb2..9500e7a1ca 100644
--- a/spec/integration/solo/solo_spec.rb
+++ b/spec/integration/solo/solo_spec.rb
@@ -92,7 +92,7 @@ EOM
# We have a timeout protection here so that if due to some bug
# run_lock gets stuck we can discover it.
expect {
- Timeout.timeout(120) do
+ Timeout.timeout(1200) do
chef_dir = File.join(File.dirname(__FILE__), "..", "..", "..")
# Instantiate the first chef-solo run
diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb
index e3de80f3f1..2b880dc200 100644
--- a/spec/spec_helper.rb
+++ b/spec/spec_helper.rb
@@ -164,6 +164,8 @@ RSpec.configure do |config|
config.before(:each) do
Chef::Config.reset
+
+ allow_any_instance_of(Chef::Audit::Runner).to receive(:run).and_return(true)
end
config.before(:suite) do
diff --git a/spec/unit/client_spec.rb b/spec/unit/client_spec.rb
index 10958d628c..f38dee634d 100644
--- a/spec/unit/client_spec.rb
+++ b/spec/unit/client_spec.rb
@@ -192,6 +192,7 @@ describe Chef::Client do
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) { double("Chef::Audit::Runner") }
let(:api_client_exists?) { false }
@@ -253,6 +254,13 @@ describe Chef::Client do
expect_any_instance_of(Chef::ResourceReporter).to receive(:run_completed)
end
+ def stub_for_audit
+ # --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
+
def stub_for_node_save
allow(node).to receive(:data_for_save).and_return(node.for_json)
@@ -282,6 +290,7 @@ 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
diff --git a/spec/unit/exceptions_spec.rb b/spec/unit/exceptions_spec.rb
index 6318ec9227..165c11446b 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\n")
+ 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