diff options
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 |