summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJay Mundrawala <jdmundrawala@gmail.com>2014-08-13 12:39:01 -0700
committerAdam Edwards <adamed@opscode.com>2014-08-29 14:42:44 -0700
commit089b4850cc36db5963b3406e46616cda64c4ea2d (patch)
tree2d21ede133f32d2f92a5ca0789f8f4d0d4e9fa1b
parente64a2f512bd5501acf2953493e92336de490bf80 (diff)
downloadchef-089b4850cc36db5963b3406e46616cda64c4ea2d.tar.gz
Parse WhatIf from LCM
-rw-r--r--lib/chef/provider/dsc_script.rb18
-rw-r--r--lib/chef/util/dsc/lcm_output_parser.rb172
-rw-r--r--lib/chef/util/dsc/local_configuration_manager.rb26
-rw-r--r--lib/chef/util/dsc/resource_info.rb26
-rw-r--r--spec/unit/util/dsc/lcm_output_parser_spec.rb166
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