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 = {}) {|result| ... } ⇒ 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.

Yields:

  • (result)

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.



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

def apply_manifest(manifest, opts = {})
  target_node_name = targeting_localhost? ? 'litmus_localhost' : ENV.fetch('TARGET_HOST', nil)
  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?('spec/fixtures/litmus_inventory.yaml') ? inventory_hash_from_inventory_file : localhost_inventory_hash

  target_option = opts['targets'] || opts[:targets]
  if target_option.nil?
    raise "Target '#{target_node_name}' not found in spec/fixtures/litmus_inventory.yaml" unless target_in_inventory?(inventory_hash, target_node_name)
  else
    target_node_name = search_for_target(target_option, inventory_hash)
  end

  # Forcibly set the locale of the command
  locale = if os[:family] == 'windows'
             ''
           else
             'LC_ALL=en_US.UTF-8 '
           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

  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 }"
    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
    bolt_result = run_command(command_to_run, target_node_name, config: nil, inventory: inventory_hash)
  end
  result = OpenStruct.new(exit_code: bolt_result.first['value']['exit_code'],
                          stdout: bolt_result.first['value']['stdout'],
                          stderr: bolt_result.first['value']['stderr'])

  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

#bolt_run_script(script, opts = {}, arguments: []) {|result| ... } ⇒ 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

Yields:

  • (result)

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.



324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
# File 'lib/puppet_litmus/puppet_helpers.rb', line 324

def bolt_run_script(script, opts = {}, arguments: [])
  target_node_name = targeting_localhost? ? 'litmus_localhost' : ENV.fetch('TARGET_HOST', nil)
  inventory_hash = File.exist?('spec/fixtures/litmus_inventory.yaml') ? inventory_hash_from_inventory_file : localhost_inventory_hash
  target_option = opts['targets'] || opts[:targets]
  if target_option.nil?
    raise "Target '#{target_node_name}' not found in spec/fixtures/litmus_inventory.yaml" unless target_in_inventory?(inventory_hash, target_node_name)
  else
    target_node_name = search_for_target(target_option, inventory_hash)
  end

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

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

  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?
  result
end

#bolt_upload_file(source, destination, opts = {}, options = {}) {|result| ... } ⇒ 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.

Yields:

  • (result)

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.



223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
# File 'lib/puppet_litmus/puppet_helpers.rb', line 223

def bolt_upload_file(source, destination, opts = {}, options = {})
  inventory_hash = File.exist?('spec/fixtures/litmus_inventory.yaml') ? inventory_hash_from_inventory_file : localhost_inventory_hash
  target_option = opts['targets'] || opts[:targets]
  if target_option.nil?
    target_node_name = targeting_localhost? ? 'litmus_localhost' : ENV.fetch('TARGET_HOST', nil)
    raise "Target '#{target_node_name}' not found in spec/fixtures/litmus_inventory.yaml" unless target_in_inventory?(inventory_hash, target_node_name)
  else
    target_node_name = search_for_target(target_option, inventory_hash)
  end

  bolt_result = upload_file(source, destination, target_node_name, options: options, config: nil, inventory: inventory_hash)

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

  if bolt_result.first['status'] != 'success'
    raise "upload file failed\n======\n#{bolt_result}" if opts[:expect_failures] != true

    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])
  yield result if block_given?
  result
end

#create_manifest_file(manifest, opts = {}) ⇒ 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.



134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
# File 'lib/puppet_litmus/puppet_helpers.rb', line 134

def create_manifest_file(manifest, opts = {})
  require 'tmpdir'
  target_node_name = ENV.fetch('TARGET_HOST', nil)
  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
    target_option = opts['targets'] || opts[:targets]
    target_node_name = search_for_target(target_option, inventory_hash) unless target_option.nil?

    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)
    raise bolt_result.first['value'].to_s unless bolt_result.first['status'] == 'success'
  end

  manifest_file_location
end

#idempotent_apply(manifest, opts = {}) ⇒ 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.

  • 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.

Returns:

  • (Boolean)

    The result of the 2 apply manifests.



21
22
23
24
25
# File 'lib/puppet_litmus/puppet_helpers.rb', line 21

def idempotent_apply(manifest, opts = {})
  manifest_file_location = create_manifest_file(manifest)
  apply_manifest(nil, **opts, catch_failures: true, manifest_file_location: manifest_file_location)
  apply_manifest(nil, **opts, catch_changes: true, manifest_file_location: manifest_file_location)
end

#run_bolt_task(task_name, params = {}, opts = {}) {|result| ... } ⇒ 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.

Yields:

  • (result)

Returns:

  • (Object)

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



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
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
# File 'lib/puppet_litmus/puppet_helpers.rb', line 264

def run_bolt_task(task_name, params = {}, opts = {})
  config_data = { 'modulepath' => File.join(Dir.pwd, 'spec', 'fixtures', 'modules') }
  target_node_name = targeting_localhost? ? 'litmus_localhost' : ENV.fetch('TARGET_HOST', nil)
  inventory_hash = if !opts[:inventory_file].nil? && File.exist?(opts[:inventory_file])
                     inventory_hash_from_inventory_file(opts[:inventory_file])
                   elsif File.exist?('spec/fixtures/litmus_inventory.yaml')
                     inventory_hash_from_inventory_file('spec/fixtures/litmus_inventory.yaml')
                   else
                     localhost_inventory_hash
                   end

  target_option = opts['targets'] || opts[:targets]
  if target_option.nil?
    raise "Target '#{target_node_name}' not found in spec/fixtures/litmus_inventory.yaml" unless target_in_inventory?(inventory_hash, target_node_name)
  else
    target_node_name = search_for_target(target_option, inventory_hash)
  end

  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
    raise "task failed\n`#{task_name}`\n======\n#{bolt_result}" if opts[:expect_failures] != true

    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?
  result
end

#run_shell(command_to_run, opts = {}) {|result| ... } ⇒ 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.

Yields:

  • (result)

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.



193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
# File 'lib/puppet_litmus/puppet_helpers.rb', line 193

def run_shell(command_to_run, opts = {})
  inventory_hash = File.exist?('spec/fixtures/litmus_inventory.yaml') ? inventory_hash_from_inventory_file : localhost_inventory_hash

  target_option = opts['targets'] || opts[:targets]
  if target_option.nil?
    target_node_name = targeting_localhost? ? 'litmus_localhost' : ENV.fetch('TARGET_HOST', nil)
    raise "Target '#{target_node_name}' not found in spec/fixtures/litmus_inventory.yaml" unless target_in_inventory?(inventory_hash, target_node_name)
  else
    target_node_name = search_for_target(target_option, inventory_hash)
  end

  bolt_result = run_command(command_to_run, target_node_name, config: nil, inventory: inventory_hash)

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

  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'])
  yield result if block_given?
  result
end

#targeting_localhost?Boolean

Determines if the current execution is targeting localhost or not

Returns:

  • (Boolean)

    true if targeting localhost in the tests



348
349
350
# File 'lib/puppet_litmus/puppet_helpers.rb', line 348

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

#write_file(content, destination, opts = {}) ⇒ 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.



163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
# File 'lib/puppet_litmus/puppet_helpers.rb', line 163

def write_file(content, destination, opts = {})
  require 'tmpdir'
  inventory_hash = inventory_hash_from_inventory_file
  target_node_name = ENV.fetch('TARGET_HOST', nil)
  target_option = opts['targets'] || opts[:targets]
  target_node_name = search_for_target(target_option, inventory_hash) unless target_option.nil?

  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
      bolt_result = upload_file(tmp_file.path, destination, target_node_name, options: {}, config: nil, inventory: inventory_hash)
      raise bolt_result.first['value'].to_s unless bolt_result.first['status'] == 'success'
    end
  end

  true
end