diff options
author | Jay Mundrawala <jdmundrawala@gmail.com> | 2014-08-13 12:39:01 -0700 |
---|---|---|
committer | Adam Edwards <adamed@opscode.com> | 2014-08-29 14:42:44 -0700 |
commit | 089b4850cc36db5963b3406e46616cda64c4ea2d (patch) | |
tree | 2d21ede133f32d2f92a5ca0789f8f4d0d4e9fa1b | |
parent | e64a2f512bd5501acf2953493e92336de490bf80 (diff) | |
download | chef-089b4850cc36db5963b3406e46616cda64c4ea2d.tar.gz |
Parse WhatIf from LCM
-rw-r--r-- | lib/chef/provider/dsc_script.rb | 18 | ||||
-rw-r--r-- | lib/chef/util/dsc/lcm_output_parser.rb | 172 | ||||
-rw-r--r-- | lib/chef/util/dsc/local_configuration_manager.rb | 26 | ||||
-rw-r--r-- | lib/chef/util/dsc/resource_info.rb | 26 | ||||
-rw-r--r-- | spec/unit/util/dsc/lcm_output_parser_spec.rb | 166 |
5 files changed, 383 insertions, 25 deletions
diff --git a/lib/chef/provider/dsc_script.rb b/lib/chef/provider/dsc_script.rb index 5cdc37a312..2adb32937f 100644 --- a/lib/chef/provider/dsc_script.rb +++ b/lib/chef/provider/dsc_script.rb @@ -38,7 +38,7 @@ class Chef def action_run if ! @resource_converged - converge_by("DSC resource script for configuration '#{configuration_friendly_name}'") do + converge_by(generate_description) do run_configuration(:set) Chef::Log.info("DSC resource configuration completed successfully") end @@ -46,7 +46,10 @@ class Chef end def load_current_resource - @resource_converged = ! run_configuration(:test) + @dsc_resources_info = run_configuration(:test) + @resource_converged = @dsc_resources_info.all? do |resource| + !resource.changes_state? + end end def whyrun_supported? @@ -98,6 +101,17 @@ class Chef configuration_name end end + + private + + def generate_description + ["DSC resource script for configuration '#{configuration_friendly_name}'"] + + @dsc_resources_info.map do |resource| + # We ignore the last log message because it only contains the time it took, which looks weird + cleaned_messages = resource.change_log[0..-2].map { |c| c.sub(/^#{Regexp.escape(resource.name)}/, '').strip } + cleaned_messages.find_all{ |c| c != ''}.join("\n") + end + end end end end diff --git a/lib/chef/util/dsc/lcm_output_parser.rb b/lib/chef/util/dsc/lcm_output_parser.rb new file mode 100644 index 0000000000..420901bcfa --- /dev/null +++ b/lib/chef/util/dsc/lcm_output_parser.rb @@ -0,0 +1,172 @@ +# +# Author:: Jay Mundrawala (<jdm@getchef.com>) +# +# Copyright:: 2014, Chef Software, Inc. +# +# 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/util/dsc/resource_info' + +class Chef + class Util + class DSC + class LocalConfigurationManager + module Parser + class ParseException < RuntimeError; end + + class Operation + attr_reader :op_type + attr_reader :resources + attr_reader :info + attr_reader :sets + attr_reader :tests + + def initialize(op_type, info) + @op_type = op_type + @info = [] + @sets = [] + @tests = [] + @resources = [] + add_info(info) + end + + def add_info(info) + @info << info + end + + def add_set(set) + raise ParseException, "add_set is not allowed in this context. Found #{@op_type}" unless [:resource, :set].include?(@op_type) + @sets << set + end + + def add_test(test) + raise ParseException, "add_test is not allowed in this context. Found #{@op_type}" unless [:resource, :set].include?(@op_type) + @tests << test + end + + def add_resource(resource) + raise ParseException, 'add_resource is only allowed to be added to the set op_type' unless @op_type == :set + @resources << resource + end + end + + # Parses the output from LCM and returns a list of Chef::Util::DSC::ResourceInfo objects + # that describe how the resources affected the system + # + # Example: + # parse <<-EOF + # What if: [Machine]: LCM: [Start Set ] + # What if: [Machine]: LCM: [Start Resource ] [[File]FileToNotBeThere] + # What if: [Machine]: LCM: [Start Set ] [[File]FileToNotBeThere] + # What if: [C:\ShouldNotExist.txt] removed + # What if: [Machine]: LCM: [End Set ] [[File]FileToNotBeThere] in 0.1 seconds + # What if: [Machine]: LCM: [End Resource ] [[File]FileToNotBeThere] + # What if: [Machine]: LCM: [End Set ] + # EOF + # + # would return + # + # [ + # Chef::Util::DSC::ResourceInfo.new( + # '[[File]FileToNotBeThere]', + # true, + # [ + # '[[File]FileToNotBeThere]', + # '[C:\Shouldnotexist.txt]', + # '[[File]FileToNotBeThere] in 0.1 seconds' + # ] + # ) + # ] + # + def self.parse(lcm_output) + return [] unless lcm_output + + stack = Array.new + popped_op = nil + lcm_output.lines.each do |line| + op_action, op_type, info = parse_line(line) + info.strip! # Because this was formatted for humans + + # The rules: + # - For each `start` action, there must be a matching `end` action + # - `skip` actions do not not do anything (They don't add to the stack) + case op_action + when :start + new_op = Operation.new(op_type, info) + case op_type + when :set + stack[-1].add_set(new_op) if stack[-1] + when :test + stack[-1].add_test(new_op) + when :resource + stack[-1].add_resource(new_op) + else + Chef::Log.warn("Unknown op_action #{op_action}: Read line #{line}") + end + stack.push(new_op) + when :end + popped_op = stack.pop + popped_op.add_info(info) + if popped_op.op_type != op_type + raise LCMOutputParseException, "Unmatching end for op_type. Expected op_type=#{op_type}, found op_type=#{popped_op.op_type}" + end + when :skip + # We don't really have anything to do here + when :info + stack[-1].add_info(info) if stack[-1] + else + stack[-1].add_info(line) if stack[-1] + end + end + + op_to_resource_infos(popped_op) + end + + def self.parse_line(line) + if match = line.match(/^.*?:.*?:\s*LCM:\s*\[(.*?)\](.*)/) + # If the line looks like + # x: [y]: LCM: [op_action op_type] message + # extract op_action, op_type, and message + operation, info = match.captures + op_action, op_type = operation.strip.split(' ').map {|m| m.downcase.to_sym} + else + # If the line looks like + # x: [y]: message + # extract message + match = line.match(/^.*?:.*?: \s+(.*)/) + op_action = op_type = :info + info = match.captures[0] + end + info.strip! # Because this was formatted for humans + return [op_action, op_type, info] + end + private_class_method :parse_line + + def self.op_to_resource_infos(op) + resources = op ? op.resources : [] + + resources.map do |r| + name = r.info[0] + sets = r.sets.length > 0 + change_log = r.sets[-1].info if sets + Chef::Util::DSC::ResourceInfo.new(name, sets, change_log) + end + end + private_class_method :op_to_resource_infos + + end + end + end + end +end diff --git a/lib/chef/util/dsc/local_configuration_manager.rb b/lib/chef/util/dsc/local_configuration_manager.rb index a9c5b867df..d7d532a686 100644 --- a/lib/chef/util/dsc/local_configuration_manager.rb +++ b/lib/chef/util/dsc/local_configuration_manager.rb @@ -17,6 +17,7 @@ # require 'chef/util/powershell/cmdlet' +require 'chef/util/dsc/lcm_output_parser' class Chef::Util::DSC class LocalConfigurationManager @@ -68,7 +69,8 @@ class Chef::Util::DSC def configuration_update_required?(what_if_output) Chef::Log.debug("DSC: DSC returned the following '-whatif' output from test operation:\n#{what_if_output}") - parse_what_if_output(what_if_output) + #parse_what_if_output(what_if_output) + Parser::parse(what_if_output) end def save_configuration_document(configuration_document) @@ -86,28 +88,6 @@ class Chef::Util::DSC File.join(@configuration_path,'..mof') end - def parse_what_if_output(what_if_output) - - # What-if output for start-dscconfiguration contains lines that look like one of the following: - # - # What if: [SEA-ADAMED1]: LCM: [ Start Set ] [[Group]chef_dsc] - # What if: [SEA-ADAMED1]: [[Group]chef_dsc] Performing the operation "Add" on target "Group: demo1" - # - # The second line lacking the 'LCM:' is what happens if there is a change required to make the system consistent with the resource. - # Such a line without LCM is only present if an update to the system is required. Therefore, we test each line below - # to see if it is missing the LCM, and declare that an update is needed if so. - has_change_line = false - - what_if_output.lines.each do |line| - if (line =~ /.+\:\s+\[\S*\]\:\s+LCM\:/).nil? - has_change_line = true - break - end - end - - has_change_line - end - def clear_execution_time @operation_start_time = nil @operation_end_time = nil diff --git a/lib/chef/util/dsc/resource_info.rb b/lib/chef/util/dsc/resource_info.rb new file mode 100644 index 0000000000..4a32451721 --- /dev/null +++ b/lib/chef/util/dsc/resource_info.rb @@ -0,0 +1,26 @@ + +class Chef + class Util + class DSC + class ResourceInfo + # The name is the text following [Start Set] + attr_reader :name + + # A list of all log messages between [Start Set] and [End Set]. + # Each line is an element in the list. + attr_reader :change_log + + def initialize(name, sets, change_log) + @name = name + @sets = sets + @change_log = change_log || [] + end + + # Does this resource change the state of the system? + def changes_state? + @sets + end + end + end + end +end diff --git a/spec/unit/util/dsc/lcm_output_parser_spec.rb b/spec/unit/util/dsc/lcm_output_parser_spec.rb new file mode 100644 index 0000000000..8ed305827c --- /dev/null +++ b/spec/unit/util/dsc/lcm_output_parser_spec.rb @@ -0,0 +1,166 @@ +# +# Author:: Bryan McLellan <btm@loftninjas.org> +# Copyright:: Copyright (c) 2014 Chef Software, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'chef/util/dsc/lcm_output_parser' + +describe Chef::Util::DSC::LocalConfigurationManager::Parser do + context 'empty input parameter' do + it 'returns an emtpy array for a 0 length string' do + Chef::Util::DSC::LocalConfigurationManager::Parser::parse('').should be_empty + end + + it 'returns an emtpy array for a nil input' do + Chef::Util::DSC::LocalConfigurationManager::Parser::parse('').should be_empty + end + end + + context 'correctly formatted output from lcm' do + it 'returns an empty array for a log with no resources' do + str = <<EOF +logtype: [machinename]: LCM: [ Start Set ] +logtype: [machinename]: LCM: [ End Set ] +EOF + Chef::Util::DSC::LocalConfigurationManager::Parser::parse(str).should be_empty + end + + it 'returns a single resource when only 1 logged with the correct name' do + str = <<EOF +logtype: [machinename]: LCM: [ Start Set ] +logtype: [machinename]: LCM: [ Start Resource ] [name] +logtype: [machinename]: LCM: [ End Resource ] [name] +logtype: [machinename]: LCM: [ End Set ] +EOF + resources = Chef::Util::DSC::LocalConfigurationManager::Parser::parse(str) + resources.length.should eq(1) + resources[0].name.should eq('[name]') + end + + it 'identifies when a resource changes the state of the system' do + str = <<EOF +logtype: [machinename]: LCM: [ Start Set ] +logtype: [machinename]: LCM: [ Start Resource ] [name] +logtype: [machinename]: LCM: [ Start Set ] [name] +logtype: [machinename]: LCM: [ End Set ] [name] +logtype: [machinename]: LCM: [ End Resource ] [name] +logtype: [machinename]: LCM: [ End Set ] +EOF + resources = Chef::Util::DSC::LocalConfigurationManager::Parser::parse(str) + resources[0].changes_state?.should be_true + end + + it 'preserves the log provided for how the system changed the state' do + str = <<EOF +logtype: [machinename]: LCM: [ Start Set ] +logtype: [machinename]: LCM: [ Start Resource ] [name] +logtype: [machinename]: LCM: [ Start Set ] [name] +logtype: [machinename]: [message] +logtype: [machinename]: LCM: [ End Set ] [name] +logtype: [machinename]: LCM: [ End Resource ] [name] +logtype: [machinename]: LCM: [ End Set ] +EOF + resources = Chef::Util::DSC::LocalConfigurationManager::Parser::parse(str) + resources[0].change_log.should match_array(["[name]","[message]","[name]"]) + end + + it 'should return false for changes_state?' do + str = <<EOF +logtype: [machinename]: LCM: [ Start Set ] +logtype: [machinename]: LCM: [ Start Resource ] [name] +logtype: [machinename]: LCM: [ Skip Set ] [name] +logtype: [machinename]: LCM: [ End Resource ] [name] +logtype: [machinename]: LCM: [ End Set ] +EOF + resources = Chef::Util::DSC::LocalConfigurationManager::Parser::parse(str) + resources[0].changes_state?.should be_false + end + + it 'should return an empty array for change_log if changes_state? is false' do + str = <<EOF +logtype: [machinename]: LCM: [ Start Set ] +logtype: [machinename]: LCM: [ Start Resource ] [name] +logtype: [machinename]: LCM: [ Skip Set ] [name] +logtype: [machinename]: LCM: [ End Resource ] [name] +logtype: [machinename]: LCM: [ End Set ] +EOF + resources = Chef::Util::DSC::LocalConfigurationManager::Parser::parse(str) + resources[0].change_log.should be_empty + end + end + + context 'Incorrectly formatted output from LCM' do + it 'should raise an exception if a set is found inside a test' do + str = <<EOF +logtype: [machinename]: LCM: [ Start Set ] +logtype: [machinename]: LCM: [ Start Resource ] [name] +logtype: [machinename]: LCM: [ Start Test ] +logtype: [machinename]: LCM: [ Start Set ] +logtype: [machinename]: LCM: [ End Set ] +logtype: [machinename]: LCM: [ End Test ] +logtype: [machinename]: LCM: [ End Resource ] [name] +logtype: [machinename]: LCM: [ End Set ] +EOF + expect { Chef::Util::DSC::LocalConfigurationManager::Parser::parse(str) }.to(raise_error( + Chef::Util::DSC::LocalConfigurationManager::Parser::ParseException)) + end + + it 'should raise an exception if a test is found inside a test' do + str = <<EOF +logtype: [machinename]: LCM: [ Start Set ] +logtype: [machinename]: LCM: [ Start Resource ] [name] +logtype: [machinename]: LCM: [ Start Test ] +logtype: [machinename]: LCM: [ Start Test ] +logtype: [machinename]: LCM: [ End Test ] +logtype: [machinename]: LCM: [ End Test ] +logtype: [machinename]: LCM: [ End Resource ] [name] +logtype: [machinename]: LCM: [ End Set ] +EOF + expect { Chef::Util::DSC::LocalConfigurationManager::Parser::parse(str) }.to(raise_error( + Chef::Util::DSC::LocalConfigurationManager::Parser::ParseException)) + end + + it 'should raise an exception if a resource is found inside a test' do + str = <<EOF +logtype: [machinename]: LCM: [ Start Set ] +logtype: [machinename]: LCM: [ Start Resource ] [name] +logtype: [machinename]: LCM: [ Start Test ] +logtype: [machinename]: LCM: [ Start Resource ] +logtype: [machinename]: LCM: [ End Resource ] +logtype: [machinename]: LCM: [ End Test ] +logtype: [machinename]: LCM: [ End Resource ] [name] +logtype: [machinename]: LCM: [ End Set ] +EOF + expect { Chef::Util::DSC::LocalConfigurationManager::Parser::parse(str) }.to(raise_error( + Chef::Util::DSC::LocalConfigurationManager::Parser::ParseException)) + end + + it 'should raise an exception if a resource is found inside a resource' do + str = <<EOF +logtype: [machinename]: LCM: [ Start Set ] +logtype: [machinename]: LCM: [ Start Resource ] [name] +logtype: [machinename]: LCM: [ Start Resource ] +logtype: [machinename]: LCM: [ End Resource ] +logtype: [machinename]: LCM: [ End Resource ] [name] +logtype: [machinename]: LCM: [ End Set ] +EOF + expect { Chef::Util::DSC::LocalConfigurationManager::Parser::parse(str) }.to(raise_error( + Chef::Util::DSC::LocalConfigurationManager::Parser::ParseException)) + end + + end + +end |