summaryrefslogtreecommitdiff
path: root/lib/chef/util/dsc/lcm_output_parser.rb
blob: 420901bcfa02a275b38f569fcc68d2133f6b29cc (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
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