Class: Toaster::TestManager

Inherits:
Object
  • Object
show all
Defined in:
lib/toaster/test_manager.rb

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(config = {}) ⇒ TestManager

Returns a new instance of TestManager.



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
# File 'lib/toaster/test_manager.rb', line 26

def initialize(config = {})
  @states = {}
  @current_executions = {}
  @cookbook_paths = config["cookbook_paths"] || []
  @transfer_state_config = false
  host = "localhost"
  TestManager.init_db(config)
  user = User.find(config["user_id"])
  User.set_current_user(user)
  run = AutomationRun.new(
    :machine_id => Util.get_machine_id(),
    :user => user)
  AutomationRun.set_current(run)

  if config["task_execution_timeout"]
    ChefListener.task_execution_timeout = config["task_execution_timeout"]
  end
  if config["task_exec_timeout_repeated"]
    ChefListener.task_exec_timeout_repeated = config["task_exec_timeout_repeated"] 
  end
  if config["rest_timeout"]
    Chef::Config[:rest_timeout] = config["rest_timeout"] 
  end
  if !config["transfer_state_config"].nil?
    @transfer_state_config = config["transfer_state_config"]
  end

  # Leave this output line unchanged - it is later extracted and parsed by
  # test_runner.rb, which needs to be able to determine the automation run ID.
  # This is a bit hacky, but seemed to be the best/fastest way to solve this.
  puts "INFO: Current automation run ID: #{AutomationRun.get_current.id}"

  @automation_name = config["automation_name"] || ""
  @recipes = config["recipes"] || []
  @skip_tasks = config["skip_tasks"] || []
  @repeat_tasks = config["repeat_tasks"] || []
  @repeated_tasks = []
  @state_change_config = {}

  @state_observer_thread = nil
end

Class Method Details

.init_db(config) ⇒ Object



223
224
225
# File 'lib/toaster/test_manager.rb', line 223

def self.init_db(config)
  Config.init_db_connection(config)
end

.init_test(automation_name, recipes = [], test_id = nil, prototype = "default", destroy_container = true, print_output = false) ⇒ Object



227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
# File 'lib/toaster/test_manager.rb', line 227

def self.init_test(automation_name, recipes = [], test_id = nil, 
    prototype="default", destroy_container=true, print_output=false)
  test_id = test_id || Util.generate_short_uid()
  suite = TestSuite.find({"uuid" => test_id})
  return suite if suite && suite.size > 0
  suite = TestSuite.new(
    :recipes => recipes,
    :uuid => test_id,
    :lxc_prototype => prototype)
  suite.save
  automation = TestRunner.ensure_automation_exists_in_db(
    automation_name, recipes, suite, destroy_container, print_output)
  if !automation
    puts "WARN: Could not ensure that automation '#{automation_name}' exists in DB." 
    return nil
  end
  suite.automation = automation
  suite.save
  return suite
end

.run_tests(test_suite, blocking = true) ⇒ Object



248
249
250
251
252
253
254
# File 'lib/toaster/test_manager.rb', line 248

def self.run_tests(test_suite, blocking = true)
  if !test_suite.kind_of?(TestSuite)
    test_suite = TestSuite.find({"uuid" => test_suite})
  end
  tests_to_run = test_suite.test_cases.select { |tc| !tc.executed? }
  TestRunner.instance.execute_tests(tests_to_run, blocking)
end

Instance Method Details

#after_run_action(task, execution_uuid, error = nil, script_output = nil) ⇒ Object

called by chef_listener AFTER a chef resource (task) has been executed by Chef’s Runner::run_action method.



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
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
# File 'lib/toaster/test_manager.rb', line 147

def after_run_action(task, execution_uuid, error = nil, script_output = nil)
  s_before = nil
  s_after = nil
  begin
    if !execution_uuid || !@current_executions[execution_uuid]
      # "init_chef_listener" is part of a special resource name which 
      # performs the AOP based instrumentation of the Chef run
      if !task.resource.to_s.include?("init_chef_listener")
        puts "WARN: Unable to find previous state for task execution " +
          "UUID '#{execution_uuid}' in 'after_run_action' " +
          "(This may be NORMAL within the context of a Chef notification execution). " +
          "Currently active executions: #{@current_executions.inspect}"
      end
      return
    end


    # pause/stop monitoring
    add_prestate = @state_tracer.dump_execution_prestate
    add_state_change_config = SystemState.get_statechange_config_from_state(add_prestate)
    @state_tracer.stop

    # get additional_state_configs from automation
    add_automation_specific_state_config(@state_change_config)

    @state_change_config = ResourceInspector.get_config_for_potential_state_changes(
        task, @cookbook_paths, @state_change_config)

    # add additional state change configs from state tracer
    MarkupUtil.rmerge!(@state_change_config, add_state_change_config, true)

    if @state_change_config.empty?
      puts "WARN: Empty state change config for task UUID #{task.uuid}:\n#{task.sourcecode}\n------"
    end
    state = SystemState.get_system_state(@state_change_config)
    execution = @current_executions[execution_uuid]
    @current_executions.delete(execution_uuid)

    # add additional pre-states from state tracer
    MarkupUtil.rmerge!(execution.state_before, add_prestate, true)

    execution.end_time = TimeStamp.now().to_i
    execution.output = script_output
    execution.reduce_and_set_state_after(state)
    execution.success = error.nil?
    error = "#{error}\n" + "#{error.backtrace.join("\n")}" if error.respond_to?("backtrace")
    execution.error_details = error

    # clone the state hashes
    s_before = MarkupUtil.clone(execution.state_before)
    s_after = MarkupUtil.clone(execution.state_after)

    #puts "DEBUG: states before/after #{execution}: #{execution.state_before}\n/\n#{execution.state_after}"
    # compute state changes
    prop_changes = SystemState.get_state_diff(s_before, s_after)
    execution.state_changes = prop_changes
    puts "INFO: Property changes (#{prop_changes.size}): #{prop_changes.inspect}"

    # remove ignored properties (e.g., file modification time etc.)
    SystemState.remove_ignore_props!(execution.state_before)
    SystemState.remove_ignore_props!(execution.state_after)

    # mongodb does not allow special chars like "." in the JSON hash
    MarkupUtil.rectify_keys(execution.state_before)
    MarkupUtil.rectify_keys(execution.state_after)

    execution = execution.save
  rescue => ex
    Util.print_backtrace(ex)
    puts "INFO: pre-state (original): #{s_before}"
    puts "INFO: post-state (original): #{s_after}"
    puts "INFO: pre-state: #{execution.state_before}"
    puts "INFO: post-state: #{execution.state_after}"
  end
end

#before_run_action(task, execution_uuid) ⇒ Object

called by chef_listener BEFORE a chef resource (task) is executed by Chef’s Runner::run_action method.



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
# File 'lib/toaster/test_manager.rb', line 97

def before_run_action(task, execution_uuid)
  # indicate whether or not this task should be started/continued
  return false if @skip_tasks.include?(task.uuid)

  # clear the old state change config, if needed
  if !@transfer_state_config
    @state_change_config = {}
  end

  # get additional_state_configs from automation
  automation = AutomationRun.get_current().automation
  add_automation_specific_state_config(@state_change_config)
  task.automation = automation if !task.automation

  # determine which parameters the task code accesses:
  task.task_parameters.concat(ResourceInspector.get_accessed_parameters(task))
  task.save

  if execution_uuid
    @state_change_config = ResourceInspector.get_config_for_potential_state_changes(
        task, @cookbook_paths, @state_change_config)
    state = SystemState.get_system_state(@state_change_config)
    execution = TaskExecution.new(
      :task => task, 
      :state_before => state, 
      :uuid => execution_uuid)
    execution.automation_run = AutomationRun.get_current()
    sourcecode = ChefUtil.runtime_resource_sourcecode(task.resource_obj)
    sourcecode.strip! if sourcecode
    if sourcecode != task.sourcecode
      execution.sourcecode = sourcecode
    end
    @current_executions[execution_uuid] = execution

    # use the external ptrace-based program to monitor the Chef execution 
    # for changes in the file system (implemented via syscall hooks)
    if !@state_tracer
      @state_tracer = SyscallTracer.new()
    end
    @state_tracer.start

  end

  return true
end

#num_requested_task_executions(task) ⇒ Object

Return the number of times a given task should be executed. The result is usually 1 (default), or 0 (skip task) or 2 (repeat task once)



72
73
74
75
76
# File 'lib/toaster/test_manager.rb', line 72

def num_requested_task_executions(task)
  return 0 if @skip_tasks.include?(task.uuid)
  return 2 if @repeat_tasks.include?(task.uuid)
  return 1
end

#tasks_to_repeat_now(last_executed_task) ⇒ Object



78
79
80
81
82
83
84
85
86
87
88
89
90
91
# File 'lib/toaster/test_manager.rb', line 78

def tasks_to_repeat_now(last_executed_task)
  uuid = last_executed_task.kind_of?(String) ? last_executed_task : last_executed_task.uuid
  @repeat_tasks.each do |list|
    if list.kind_of?(Array)
      if !@repeated_tasks.include?(list)
        if list[-1] == uuid
          @repeated_tasks << list
          return list
        end
      end
    end
  end
  return nil
end