Module: PuppetLitmus::PuppetHelpers

Included in:
PuppetLitmus
Defined in:
lib/puppet_litmus/puppet_helpers.rb

Overview

helper functions for running puppet commands. They execute a target system specified by ENV heavily uses functions from here github.com/puppetlabs/bolt/blob/main/developer-docs/bolt_spec-run.md

Instance Method Summary collapse

Instance Method Details

#apply_manifest(manifest, opts = {}) ⇒ Object

Applies a manifest. returning the result of that apply. Mimics the apply_manifest from beaker

When you set the environment variable RSPEC_DEBUG, the output of your puppet run will be displayed. If you have set the :debug flag, you will see the full debug log. If you have not set the :debug flag, it will display the regular output.

Parameters:

  • manifest (String)

    puppet manifest code to be applied.

  • opts (Hash) (defaults to: {})

    Alters the behaviour of the command. Valid options are: :catch_changes [Boolean] (false) We’re after idempotency so allow exit code 0 only. :expect_changes [Boolean] (false) We’re after changes specifically so allow exit code 2 only. :catch_failures [Boolean] (false) We’re after only complete success so allow exit codes 0 and 2 only. :expect_failures [Boolean] (false) We’re after failures specifically so allow exit codes 1, 4, and 6 only. :manifest_file_location [Path] The place on the target system. :hiera_config [Path] The path to the hiera.yaml configuration on the target. :prefix_command [String] prefixes the puppet apply command; eg “export LANGUAGE=‘ja’”. :trace [Boolean] run puppet apply with the trace flag (defaults to ‘true`). :debug [Boolean] run puppet apply with the debug flag. :noop [Boolean] run puppet apply with the noop flag.

Yield Returns:

  • (Block)

    this method will yield to a block of code passed by the caller; this can be used for additional validation, etc.

Returns:

  • (Object)

    A result object from the apply.



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
# File 'lib/puppet_litmus/puppet_helpers.rb', line 40

def apply_manifest(manifest, opts = {})
  Honeycomb.start_span(name: 'litmus.apply_manifest') do |span|
    ENV['HONEYCOMB_TRACE'] = span.to_trace_header
    span.add_field('litmus.manifest', manifest)
    span.add_field('litmus.opts', opts)

    target_node_name = targeting_localhost? ? 'litmus_localhost' : ENV['TARGET_HOST']
    raise 'manifest and manifest_file_location in the opts hash are mutually exclusive arguments, pick one' if !manifest.nil? && !opts[:manifest_file_location].nil?
    raise 'please pass a manifest or the manifest_file_location in the opts hash' if (manifest.nil? || manifest == '') && opts[:manifest_file_location].nil?
    raise 'please specify only one of `catch_changes`, `expect_changes`, `catch_failures` or `expect_failures`' if
      [opts[:catch_changes], opts[:expect_changes], opts[:catch_failures], opts[:expect_failures]].compact.length > 1

    opts = { trace: true }.merge(opts)

    if opts[:catch_changes]
      use_detailed_exit_codes = true
      acceptable_exit_codes = [0]
    elsif opts[:catch_failures]
      use_detailed_exit_codes = true
      acceptable_exit_codes = [0, 2]
    elsif opts[:expect_failures]
      use_detailed_exit_codes = true
      acceptable_exit_codes = [1, 4, 6]
    elsif opts[:expect_changes]
      use_detailed_exit_codes = true
      acceptable_exit_codes = [2]
    else
      use_detailed_exit_codes = false
      acceptable_exit_codes = [0]
    end

    manifest_file_location = opts[:manifest_file_location] || create_manifest_file(manifest)
    inventory_hash = File.exist?('inventory.yaml') ? inventory_hash_from_inventory_file : localhost_inventory_hash
    raise "Target '#{target_node_name}' not found in inventory.yaml" unless target_in_inventory?(inventory_hash, target_node_name)

    span.add_field('litmus.node_name', target_node_name)
    add_platform_field(inventory_hash, target_node_name)

    # Forcibly set the locale of the command
    locale = if os[:family] != 'windows'
               'LC_ALL=en_US.UTF-8 '
             else
               ''
             end
    command_to_run = "#{locale}#{opts[:prefix_command]} puppet apply #{manifest_file_location}"
    command_to_run += ' --trace' if !opts[:trace].nil? && (opts[:trace] == true)
    command_to_run += " --modulepath #{Dir.pwd}/spec/fixtures/modules" if target_node_name == 'litmus_localhost'
    command_to_run += " --hiera_config='#{opts[:hiera_config]}'" unless opts[:hiera_config].nil?
    command_to_run += ' --debug' if !opts[:debug].nil? && (opts[:debug] == true)
    command_to_run += ' --noop' if !opts[:noop].nil? && (opts[:noop] == true)
    command_to_run += ' --detailed-exitcodes' if use_detailed_exit_codes == true

    span.add_field('litmus.target_node_name', target_node_name)

    if os[:family] == 'windows'
      # IAC-1365 - Workaround for BOLT-1535 and bolt issue #1650
      command_to_run = "try { #{command_to_run}; exit $LASTEXITCODE } catch { write-error $_ ; exit 1 }"
      span.add_field('litmus.command_to_run', command_to_run)
      bolt_result = Tempfile.open(['temp', '.ps1']) do |script|
        script.write(command_to_run)
        script.close
        run_script(script.path, target_node_name, [], options: {}, config: nil, inventory: inventory_hash)
      end
    else
      span.add_field('litmus.command_to_run', command_to_run)
      bolt_result = run_command(command_to_run, target_node_name, config: nil, inventory: inventory_hash)
    end
    span.add_field('litmus.bolt_result', bolt_result)
    result = OpenStruct.new(exit_code: bolt_result.first['value']['exit_code'],
                            stdout: bolt_result.first['value']['stdout'],
                            stderr: bolt_result.first['value']['stderr'])
    span.add_field('litmus.result', result.to_h)

    status = result.exit_code
    if opts[:catch_changes] && !acceptable_exit_codes.include?(status)
      report_puppet_apply_change(command_to_run, bolt_result)
    elsif !acceptable_exit_codes.include?(status)
      report_puppet_apply_error(command_to_run, bolt_result, acceptable_exit_codes)
    end

    yield result if block_given?

    if ENV['RSPEC_DEBUG']
      puts "apply manifest succeded\n #{command_to_run}\n======\nwith status #{result.exit_code}"
      puts result.stderr
      puts result.stdout
    end
    result
  end
end

#bolt_run_script(script, opts = {}, arguments: []) ⇒ Object

Runs a script against the target system.

Parameters:

  • script (String)

    The path to the script on the source machine

  • opts (Hash) (defaults to: {})

    Alters the behaviour of the command. Valid options are :expect_failures [Boolean] doesnt return an exit code of non-zero if the command failed.

  • arguments (Array) (defaults to: [])

    Array of arguments to pass to script on runtime

Yield Returns:

  • (Block)

    this method will yield to a block of code passed by the caller; this can be used for additional validation, etc.

Returns:

  • (Object)

    A result object from the script run.



366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
# File 'lib/puppet_litmus/puppet_helpers.rb', line 366

def bolt_run_script(script, opts = {}, arguments: [])
  Honeycomb.start_span(name: 'litmus.bolt_run_script') do |span|
    ENV['HONEYCOMB_TRACE'] = span.to_trace_header
    span.add_field('litmus.script', script)
    span.add_field('litmus.opts', opts)
    span.add_field('litmus.arguments', arguments)

    target_node_name = targeting_localhost? ? 'litmus_localhost' : ENV['TARGET_HOST']
    inventory_hash = File.exist?('inventory.yaml') ? inventory_hash_from_inventory_file : localhost_inventory_hash
    raise "Target '#{target_node_name}' not found in inventory.yaml" unless target_in_inventory?(inventory_hash, target_node_name)

    span.add_field('litmus.node_name', target_node_name)
    add_platform_field(inventory_hash, target_node_name)

    bolt_result = run_script(script, target_node_name, arguments, options: opts, config: nil, inventory: inventory_hash)

    if bolt_result.first['value']['exit_code'] != 0 && opts[:expect_failures] != true
      span.add_field('litmus_runscriptfailure', bolt_result)
      raise "script run failed\n`#{script}`\n======\n#{bolt_result}"
    end

    result = OpenStruct.new(exit_code: bolt_result.first['value']['exit_code'],
                            stdout: bolt_result.first['value']['stdout'],
                            stderr: bolt_result.first['value']['stderr'])
    yield result if block_given?
    span.add_field('litmus.result', result.to_h)
    result
  end
end

#bolt_upload_file(source, destination, opts = {}, options = {}) ⇒ Object

Copies file to the target, using its respective transport

Parameters:

  • source (String)

    place locally, to copy from.

  • destination (String)

    place on the target, to copy to.

  • opts (Hash) (defaults to: {})

    Alters the behaviour of the command. Valid options are :expect_failures [Boolean] doesnt return an exit code of non-zero if the command failed.

Yield Returns:

  • (Block)

    this method will yield to a block of code passed by the caller; this can be used for additional validation, etc.

Returns:

  • (Object)

    A result object from the command.



246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
# File 'lib/puppet_litmus/puppet_helpers.rb', line 246

def bolt_upload_file(source, destination, opts = {}, options = {})
  Honeycomb.start_span(name: 'litmus.bolt_upload_file') do |span|
    ENV['HONEYCOMB_TRACE'] = span.to_trace_header
    span.add_field('litmus.source', source)
    span.add_field('litmus.destination', destination)
    span.add_field('litmus.opts', opts)
    span.add_field('litmus.options', options)

    target_node_name = targeting_localhost? ? 'litmus_localhost' : ENV['TARGET_HOST']
    inventory_hash = File.exist?('inventory.yaml') ? inventory_hash_from_inventory_file : localhost_inventory_hash
    raise "Target '#{target_node_name}' not found in inventory.yaml" unless target_in_inventory?(inventory_hash, target_node_name)

    span.add_field('litmus.node_name', target_node_name)
    add_platform_field(inventory_hash, target_node_name)

    bolt_result = upload_file(source, destination, target_node_name, options: options, config: nil, inventory: inventory_hash)
    span.add_field('litmus.bolt_result', bolt_result)

    result_obj = {
      exit_code: 0,
      stdout: bolt_result.first['value']['_output'],
      stderr: nil,
      result: bolt_result.first['value'],
    }

    if bolt_result.first['status'] != 'success'
      if opts[:expect_failures] != true
        span.add_field('litmus_uploadfilefailure', bolt_result)
        raise "upload file failed\n======\n#{bolt_result}"
      end

      result_obj[:exit_code] = 255
      result_obj[:stderr]    = bolt_result.first['value']['_error']['msg']
    end

    result = OpenStruct.new(exit_code: result_obj[:exit_code],
                            stdout: result_obj[:stdout],
                            stderr: result_obj[:stderr])
    span.add_field('litmus.result', result.to_h)
    yield result if block_given?
    result
  end
end

#create_manifest_file(manifest) ⇒ String

Creates a manifest file locally in a temp location, if its a remote target copy it to there.

Parameters:

  • manifest (String)

    puppet manifest code.

Returns:

  • (String)

    The path to the location of the manifest.



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
# File 'lib/puppet_litmus/puppet_helpers.rb', line 135

def create_manifest_file(manifest)
  Honeycomb.start_span(name: 'litmus.create_manifest_file') do |span|
    ENV['HONEYCOMB_TRACE'] = span.to_trace_header
    span.add_field('litmus.manifest', manifest)

    require 'tmpdir'
    target_node_name = ENV['TARGET_HOST']
    tmp_filename = File.join(Dir.tmpdir, "manifest_#{Time.now.strftime('%Y%m%d')}_#{Process.pid}_#{rand(0x100000000).to_s(36)}.pp")
    manifest_file = File.open(tmp_filename, 'w')
    manifest_file.write(manifest)
    manifest_file.close
    if target_node_name.nil? || target_node_name == 'localhost'
      # no need to transfer
      manifest_file_location = manifest_file.path
    else
      # transfer to TARGET_HOST
      inventory_hash = inventory_hash_from_inventory_file
      span.add_field('litmus.node_name', target_node_name)
      add_platform_field(inventory_hash, target_node_name)

      manifest_file_location = File.basename(manifest_file)
      bolt_result = upload_file(manifest_file.path, manifest_file_location, target_node_name, options: {}, config: nil, inventory: inventory_hash)
      span.add_field('litmus.bolt_result', bolt_result)
      raise bolt_result.first['value'].to_s unless bolt_result.first['status'] == 'success'
    end

    span.add_field('litmus.manifest_file_location', manifest_file_location)

    manifest_file_location
  end
end

#idempotent_apply(manifest) ⇒ Boolean

Applies a manifest twice. First checking for errors. Secondly to make sure no changes occur.

Parameters:

  • manifest (String)

    puppet manifest code to be applied.

Returns:

  • (Boolean)

    The result of the 2 apply manifests.



10
11
12
13
14
15
16
17
# File 'lib/puppet_litmus/puppet_helpers.rb', line 10

def idempotent_apply(manifest)
  Honeycomb.start_span(name: 'litmus.idempotent_apply') do |span|
    ENV['HONEYCOMB_TRACE'] = span.to_trace_header
    manifest_file_location = create_manifest_file(manifest)
    apply_manifest(nil, catch_failures: true, manifest_file_location: manifest_file_location)
    apply_manifest(nil, catch_changes: true, manifest_file_location: manifest_file_location)
  end
end

#run_bolt_task(task_name, params = {}, opts = {}) ⇒ Object

Runs a task against the target system.

Parameters:

  • task_name (String)

    The name of the task to run.

  • params (Hash) (defaults to: {})

    key : value pairs to be passed to the task.

  • opts (Hash) (defaults to: {})

    Alters the behaviour of the command. Valid options are :expect_failures [Boolean] doesnt return an exit code of non-zero if the command failed. :inventory_file [String] path to the inventory file to use with the task.

Returns:

  • (Object)

    A result object from the task.The values available are stdout, stderr and result.



298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
# File 'lib/puppet_litmus/puppet_helpers.rb', line 298

def run_bolt_task(task_name, params = {}, opts = {})
  Honeycomb.start_span(name: 'litmus.run_task') do |span|
    ENV['HONEYCOMB_TRACE'] = span.to_trace_header
    span.add_field('litmus.task_name', task_name)
    span.add_field('litmus.params', params)
    span.add_field('litmus.opts', opts)

    config_data = { 'modulepath' => File.join(Dir.pwd, 'spec', 'fixtures', 'modules') }
    target_node_name = targeting_localhost? ? 'litmus_localhost' : ENV['TARGET_HOST']
    inventory_hash = if !opts[:inventory_file].nil? && File.exist?(opts[:inventory_file])
                       inventory_hash_from_inventory_file(opts[:inventory_file])
                     elsif File.exist?('inventory.yaml')
                       inventory_hash_from_inventory_file('inventory.yaml')
                     else
                       localhost_inventory_hash
                     end
    raise "Target '#{target_node_name}' not found in inventory.yaml" unless target_in_inventory?(inventory_hash, target_node_name)

    span.add_field('litmus.node_name', target_node_name)
    add_platform_field(inventory_hash, target_node_name)

    bolt_result = run_task(task_name, target_node_name, params, config: config_data, inventory: inventory_hash)
    result_obj = {
      exit_code: 0,
      stdout: nil,
      stderr: nil,
      result: bolt_result.first['value'],
    }

    if bolt_result.first['status'] == 'success'
      # stdout returns unstructured data if structured data is not available
      result_obj[:stdout] = if bolt_result.first['value']['_output'].nil?
                              bolt_result.first['value'].to_s
                            else
                              bolt_result.first['value']['_output']
                            end

    else
      if opts[:expect_failures] != true
        span.add_field('litmus_runtaskfailure', bolt_result)
        raise "task failed\n`#{task_name}`\n======\n#{bolt_result}"
      end

      result_obj[:exit_code] = if bolt_result.first['value']['_error']['details'].nil?
                                 255
                               else
                                 bolt_result.first['value']['_error']['details'].fetch('exitcode', 255)
                               end
      result_obj[:stderr]    = bolt_result.first['value']['_error']['msg']
    end

    result = OpenStruct.new(exit_code: result_obj[:exit_code],
                            stdout: result_obj[:stdout],
                            stderr: result_obj[:stderr],
                            result: result_obj[:result])
    yield result if block_given?
    span.add_field('litmus.result', result.to_h)
    result
  end
end

#run_shell(command_to_run, opts = {}) ⇒ Object

Runs a command against the target system

Parameters:

  • command_to_run (String)

    The command to execute.

  • opts (Hash) (defaults to: {})

    Alters the behaviour of the command. Valid options are :expect_failures [Boolean] doesnt return an exit code of non-zero if the command failed.

Yield Returns:

  • (Block)

    this method will yield to a block of code passed by the caller; this can be used for additional validation, etc.

Returns:

  • (Object)

    A result object from the command.



209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
# File 'lib/puppet_litmus/puppet_helpers.rb', line 209

def run_shell(command_to_run, opts = {})
  Honeycomb.start_span(name: 'litmus.run_shell') do |span|
    ENV['HONEYCOMB_TRACE'] = span.to_trace_header
    span.add_field('litmus.command_to_run', command_to_run)
    span.add_field('litmus.opts', opts)

    target_node_name = targeting_localhost? ? 'litmus_localhost' : ENV['TARGET_HOST']
    inventory_hash = File.exist?('inventory.yaml') ? inventory_hash_from_inventory_file : localhost_inventory_hash
    raise "Target '#{target_node_name}' not found in inventory.yaml" unless target_in_inventory?(inventory_hash, target_node_name)

    span.add_field('litmus.node_name', target_node_name)
    add_platform_field(inventory_hash, target_node_name)

    bolt_result = run_command(command_to_run, target_node_name, config: nil, inventory: inventory_hash)
    span.add_field('litmus.bolt_result', bolt_result)

    if bolt_result.first['value']['exit_code'] != 0 && opts[:expect_failures] != true
      raise "shell failed\n`#{command_to_run}`\n======\n#{bolt_result}"
    end

    result = OpenStruct.new(exit_code: bolt_result.first['value']['exit_code'],
                            exit_status: bolt_result.first['value']['exit_code'],
                            stdout: bolt_result.first['value']['stdout'],
                            stderr: bolt_result.first['value']['stderr'])
    span.add_field('litmus.result', result.to_h)
    yield result if block_given?
    result
  end
end

#targeting_localhost?Boolean

Determines if the current execution is targeting localhost or not

Returns:

  • (Boolean)

    true if targeting localhost in the tests



399
400
401
# File 'lib/puppet_litmus/puppet_helpers.rb', line 399

def targeting_localhost?
  ENV['TARGET_HOST'].nil? || ENV['TARGET_HOST'] == 'localhost'
end

#write_file(content, destination) ⇒ Bool

Writes a string variable to a file on a target node at a specified path.

Parameters:

  • content (String)

    String data to write to the file.

  • destination (String)

    The path on the target node to write the file.

Returns:

  • (Bool)

    Success. The file was succesfully writtne on the target.



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
# File 'lib/puppet_litmus/puppet_helpers.rb', line 172

def write_file(content, destination)
  Honeycomb.start_span(name: 'litmus.write_file') do |span|
    ENV['HONEYCOMB_TRACE'] = span.to_trace_header
    span.add_field('litmus.destination', destination)

    require 'tmpdir'
    target_node_name = ENV['TARGET_HOST']

    Tempfile.create('litmus') do |tmp_file|
      tmp_file.write(content)
      tmp_file.flush
      if target_node_name.nil? || target_node_name == 'localhost'
        require 'fileutils'
        # no need to transfer
        FileUtils.cp(tmp_file.path, destination)
      else
        # transfer to TARGET_HOST
        inventory_hash = inventory_hash_from_inventory_file
        span.add_field('litmus.node_name', target_node_name)
        add_platform_field(inventory_hash, target_node_name)

        bolt_result = upload_file(tmp_file.path, destination, target_node_name, options: {}, config: nil, inventory: inventory_hash)
        span.add_field('litmus.bolt_result.file_upload', bolt_result)
        raise bolt_result.first['value'].to_s unless bolt_result.first['status'] == 'success'
      end
    end

    true
  end
end