octocatalog-diff 1.5.2 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. checksums.yaml +5 -5
  2. data/.version +1 -1
  3. data/README.md +4 -4
  4. data/doc/CHANGELOG.md +50 -0
  5. data/doc/advanced-compare-file-text.md +79 -0
  6. data/doc/advanced-environments.md +5 -5
  7. data/doc/advanced-ignores.md +10 -0
  8. data/doc/advanced-puppet-master.md +25 -7
  9. data/doc/configuration-puppet.md +19 -19
  10. data/doc/configuration-puppetdb.md +8 -0
  11. data/doc/configuration.md +31 -31
  12. data/doc/dev/api/v1/calls/catalog-diff.md +6 -2
  13. data/doc/dev/api/v1/objects/diff.md +3 -3
  14. data/doc/dev/integration-tests.md +2 -2
  15. data/doc/dev/releasing.md +41 -41
  16. data/doc/dev/run-from-branch.md +23 -23
  17. data/doc/installation.md +14 -14
  18. data/doc/limitations.md +9 -9
  19. data/doc/optionsref.md +166 -11
  20. data/doc/requirements.md +6 -2
  21. data/lib/octocatalog-diff/catalog-diff/differ.rb +32 -5
  22. data/lib/octocatalog-diff/catalog-diff/filter/compilation_dir.rb +29 -25
  23. data/lib/octocatalog-diff/catalog-util/command.rb +25 -3
  24. data/lib/octocatalog-diff/catalog-util/fileresources.rb +39 -16
  25. data/lib/octocatalog-diff/catalog.rb +22 -4
  26. data/lib/octocatalog-diff/catalog/computed.rb +2 -1
  27. data/lib/octocatalog-diff/catalog/puppetmaster.rb +43 -5
  28. data/lib/octocatalog-diff/cli.rb +38 -6
  29. data/lib/octocatalog-diff/cli/options.rb +36 -1
  30. data/lib/octocatalog-diff/cli/options/compare_file_text.rb +18 -0
  31. data/lib/octocatalog-diff/cli/options/hostname.rb +13 -2
  32. data/lib/octocatalog-diff/cli/options/puppet_master_api_version.rb +2 -2
  33. data/lib/octocatalog-diff/cli/options/puppet_master_token.rb +20 -0
  34. data/lib/octocatalog-diff/cli/options/puppet_master_token_file.rb +35 -0
  35. data/lib/octocatalog-diff/cli/options/puppet_master_update_catalog.rb +20 -0
  36. data/lib/octocatalog-diff/cli/options/puppet_master_update_facts.rb +20 -0
  37. data/lib/octocatalog-diff/cli/options/puppetdb_package_inventory.rb +18 -0
  38. data/lib/octocatalog-diff/cli/options/use_lcs.rb +14 -0
  39. data/lib/octocatalog-diff/facts/json.rb +13 -2
  40. data/lib/octocatalog-diff/facts/puppetdb.rb +43 -2
  41. data/lib/octocatalog-diff/util/parallel.rb +20 -16
  42. data/lib/octocatalog-diff/util/util.rb +2 -0
  43. data/scripts/env/env.sh +1 -1
  44. data/scripts/git-extract/git-extract.sh +1 -1
  45. data/scripts/puppet/puppet.sh +1 -1
  46. metadata +36 -30
@@ -11,6 +11,7 @@ require_relative 'util/util'
11
11
  require_relative 'version'
12
12
 
13
13
  require 'logger'
14
+ require 'parallel'
14
15
  require 'socket'
15
16
 
16
17
  module OctocatalogDiff
@@ -43,7 +44,8 @@ module OctocatalogDiff
43
44
  display_datatype_changes: true,
44
45
  parallel: true,
45
46
  suppress_absent_file_details: true,
46
- hiera_path: 'hieradata'
47
+ hiera_path: 'hieradata',
48
+ use_lcs: true
47
49
  }.freeze
48
50
 
49
51
  # This method is the one to call externally. It is possible to specify alternate
@@ -116,16 +118,46 @@ module OctocatalogDiff
116
118
  end
117
119
 
118
120
  # Compile catalogs and do catalog-diff
119
- catalog_diff = OctocatalogDiff::API::V1.catalog_diff(options.merge(logger: logger))
121
+ node_set = options.delete(:node)
122
+ node_set = [node_set] unless node_set.is_a?(Array)
123
+
124
+ # run multiple node diffs in parallel
125
+ catalog_diffs = if node_set.size == 1
126
+ [run_octocatalog_diff(node_set.first, options, logger)]
127
+ else
128
+ ::Parallel.map(node_set, in_threads: 4) { |node| run_octocatalog_diff(node, options, logger) }
129
+ end
130
+
131
+ # Return the resulting diff object if requested (generally for testing)
132
+ # or otherwise return exit code
133
+ return catalog_diffs.first if opts[:INTEGRATION]
134
+
135
+ all_diffs = catalog_diffs.map(&:diffs)
136
+
137
+ all_diffs.each do |diff|
138
+ next unless diff.any?
139
+ return EXITCODE_SUCCESS_WITH_DIFFS
140
+ end
141
+
142
+ EXITCODE_SUCCESS_NO_DIFFS
143
+ end
144
+
145
+ # Run the octocatalog-diff process for a given node. Return the diffs for a contribution to
146
+ # the final exit status.
147
+ # node - String with the node
148
+ # options - All of the currently defined options
149
+ # logger - Logger object
150
+ def self.run_octocatalog_diff(node, options, logger)
151
+ options_copy = options.merge(node: node)
152
+ catalog_diff = OctocatalogDiff::API::V1.catalog_diff(options_copy.merge(logger: logger))
120
153
  diffs = catalog_diff.diffs
121
154
 
122
155
  # Display diffs
123
- printer_obj = OctocatalogDiff::Cli::Printer.new(options, logger)
156
+ printer_obj = OctocatalogDiff::Cli::Printer.new(options_copy, logger)
124
157
  printer_obj.printer(diffs, catalog_diff.from.compilation_dir, catalog_diff.to.compilation_dir)
125
158
 
126
- # Return the resulting diff object if requested (generally for testing) or otherwise return exit code
127
- return catalog_diff if opts[:INTEGRATION]
128
- diffs.any? ? EXITCODE_SUCCESS_WITH_DIFFS : EXITCODE_SUCCESS_NO_DIFFS
159
+ # Return catalog-diff object.
160
+ catalog_diff
129
161
  end
130
162
 
131
163
  # Parse command line options with 'optparse'. Returns a hash with the parsed arguments.
@@ -11,7 +11,7 @@ module OctocatalogDiff
11
11
  # This class contains the option parser. 'parse_options' is the external entry point.
12
12
  class Options
13
13
  # The usage banner.
14
- BANNER = 'Usage: catalog-diff -n <hostname> [-f <from environment>] [-t <to environment>]'.freeze
14
+ BANNER = 'Usage: catalog-diff -n <hostname>[,<hostname>...] [-f <from environment>] [-t <to environment>]'.freeze
15
15
 
16
16
  # An error class specifically for passing information to the document build task.
17
17
  class DocBuildError < RuntimeError; end
@@ -103,6 +103,7 @@ module OctocatalogDiff
103
103
  datatype = opts.fetch(:datatype, '')
104
104
  return option_globally_or_per_branch_string(opts) if datatype.is_a?(String)
105
105
  return option_globally_or_per_branch_array(opts) if datatype.is_a?(Array)
106
+ return option_globally_or_per_branch_boolean(opts) if datatype.is_a?(TrueClass) || datatype.is_a?(FalseClass)
106
107
  raise ArgumentError, "option_globally_or_per_branch not equipped to handle #{datatype.class}"
107
108
  end
108
109
 
@@ -177,6 +178,40 @@ module OctocatalogDiff
177
178
  end
178
179
  end
179
180
 
181
+ # See description of `option_globally_or_per_branch`. This implements the logic for a boolean value.
182
+ # @param :parser [OptionParser object] The OptionParser argument
183
+ # @param :options [Hash] Options hash being constructed; this is modified in this method.
184
+ # @param :cli_name [String] Name of option on command line (e.g. puppet-binary)
185
+ # @param :option_name [Symbol] Name of option in the options hash (e.g. :puppet_binary)
186
+ # @param :desc [String] Description of option on the command line; will have "for the XX branch" appended
187
+ def self.option_globally_or_per_branch_boolean(opts)
188
+ parser = opts.fetch(:parser)
189
+ options = opts.fetch(:options)
190
+ cli_name = opts.fetch(:cli_name)
191
+ option_name = opts.fetch(:option_name)
192
+ desc = opts.fetch(:desc)
193
+
194
+ flag = cli_name
195
+ from_option = "from_#{option_name}".to_sym
196
+ to_option = "to_#{option_name}".to_sym
197
+ parser.on("--[no-]#{flag}", "#{desc} globally") do |x|
198
+ translated = translate_option(opts[:translator], x)
199
+ options[to_option] = translated
200
+ options[from_option] = translated
201
+ post_process(opts[:post_process], options)
202
+ end
203
+ parser.on("--[no-]to-#{flag}", "#{desc} for the to branch") do |x|
204
+ translated = translate_option(opts[:translator], x)
205
+ options[to_option] = translated
206
+ post_process(opts[:post_process], options)
207
+ end
208
+ parser.on("--[no-]from-#{flag}", "#{desc} for the from branch") do |x|
209
+ translated = translate_option(opts[:translator], x)
210
+ options[from_option] = translated
211
+ post_process(opts[:post_process], options)
212
+ end
213
+ end
214
+
180
215
  # If a validator was provided, run the validator on the supplied value. The validator is expected to
181
216
  # throw an error if there is a problem. Note that the validator runs *before* the translator if both
182
217
  # a validator and translator are supplied.
@@ -15,3 +15,21 @@ OctocatalogDiff::Cli::Options::Option.newoption(:compare_file_text) do
15
15
  end
16
16
  end
17
17
  end
18
+
19
+ # Sometimes there is a particular file resource for which the file text
20
+ # comparison is not desired, while not disabling that option globally. Similar
21
+ # to --ignore_tags, it's possible to tag the file resource and exempt it from
22
+ # the --compare_file_text checks.
23
+ # @param parser [OptionParser object] The OptionParser argument
24
+ # @param options [Hash] Options hash being constructed; this is modified in this method.
25
+ OctocatalogDiff::Cli::Options::Option.newoption(:compare_file_text_ignore_tags) do
26
+ has_weight 415
27
+
28
+ def parse(parser, options)
29
+ description = 'Tags that exclude a file resource from text comparison'
30
+ parser.on('--compare-file-text-ignore-tags STRING1[,STRING2[,...]]', Array, description) do |x|
31
+ options[:compare_file_text_ignore_tags] ||= []
32
+ options[:compare_file_text_ignore_tags].concat x
33
+ end
34
+ end
35
+ end
@@ -1,6 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Set hostname, which is used to look up facts in PuppetDB, and in the header of diff display.
4
+ # This option can recieve a single hostname, or a comma separated list of
5
+ # multiple hostnames, which are split into an Array. Multiple hostnames do not
6
+ # work with the `catalog-only` or `bootstrap-then-exit` options.
4
7
  # @param parser [OptionParser object] The OptionParser argument
5
8
  # @param options [Hash] Options hash being constructed; this is modified in this method.
6
9
 
@@ -8,8 +11,16 @@ OctocatalogDiff::Cli::Options::Option.newoption(:hostname) do
8
11
  has_weight 1
9
12
 
10
13
  def parse(parser, options)
11
- parser.on('--hostname HOSTNAME', '-n', 'Use PuppetDB facts from last run of hostname') do |hostname|
12
- options[:node] = hostname
14
+ parser.on(
15
+ '--hostname HOSTNAME1[,HOSTNAME2[,...]]',
16
+ '-n',
17
+ 'Use PuppetDB facts from last run of a hostname or a comma separated list of multiple hostnames'
18
+ ) do |hostname|
19
+ options[:node] = if hostname.include?(',')
20
+ hostname.split(',')
21
+ else
22
+ hostname
23
+ end
13
24
  end
14
25
  end
15
26
  end
@@ -14,8 +14,8 @@ OctocatalogDiff::Cli::Options::Option.newoption(:puppet_master_api_version) do
14
14
  options: options,
15
15
  cli_name: 'puppet-master-api-version',
16
16
  option_name: 'puppet_master_api_version',
17
- desc: 'Puppet Master API version (2 for Puppet 3.x, 3 for Puppet 4.x)',
18
- validator: ->(x) { x =~ /^[23]$/ || raise(ArgumentError, 'Only API versions 2 and 3 are supported') },
17
+ desc: 'Puppet Master API version (2 for Puppet 3.x, 3 for Puppet 4.x, 4 for Puppet Server >= 6.3.0)',
18
+ validator: ->(x) { x =~ /^[234]$/ || raise(ArgumentError, 'Only API versions 2, 3, and 4 are supported') },
19
19
  translator: ->(x) { x.to_i }
20
20
  )
21
21
  end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Specify a PE RBAC token used to authenticate to Puppetserver for v4
4
+ # catalog API calls.
5
+ # @param parser [OptionParser object] The OptionParser argument
6
+ # @param options [Hash] Options hash being constructed; this is modified in this method.
7
+ OctocatalogDiff::Cli::Options::Option.newoption(:puppet_master_token) do
8
+ has_weight 310
9
+
10
+ def parse(parser, options)
11
+ OctocatalogDiff::Cli::Options.option_globally_or_per_branch(
12
+ parser: parser,
13
+ options: options,
14
+ datatype: '',
15
+ cli_name: 'puppet-master-token',
16
+ option_name: 'puppet_master_token',
17
+ desc: 'PE RBAC token to authenticate to the Puppetserver API v4'
18
+ )
19
+ end
20
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Specify a path to a file containing a PE RBAC token used to authenticate to the
4
+ # Puppetserver for a v4 catalog API call.
5
+ # @param parser [OptionParser object] The OptionParser argument
6
+ # @param options [Hash] Options hash being constructed; this is modified in this method.
7
+ OctocatalogDiff::Cli::Options::Option.newoption(:puppet_master_token_file) do
8
+ has_weight 300
9
+
10
+ def parse(parser, options)
11
+ OctocatalogDiff::Cli::Options.option_globally_or_per_branch(
12
+ parser: parser,
13
+ options: options,
14
+ datatype: '',
15
+ cli_name: 'puppet-master-token-file',
16
+ option_name: 'puppet_master_token_file',
17
+ desc: 'File containing PE RBAC token to authenticate to the Puppetserver API v4',
18
+ translator: ->(x) { x.start_with?('/', '~') ? x : File.join(options[:basedir], x) },
19
+ post_process: lambda do |opts|
20
+ %w(to from).each do |prefix|
21
+ fileopt = "#{prefix}_puppet_master_token_file".to_sym
22
+ tokenopt = "#{prefix}_puppet_master_token".to_sym
23
+
24
+ tokenfile = opts[fileopt]
25
+ next if tokenfile.nil?
26
+
27
+ raise(Errno::ENOENT, "Token file #{tokenfile} is not readable") unless File.readable?(tokenfile)
28
+
29
+ token = File.read(tokenfile).strip
30
+ opts[tokenopt] ||= token
31
+ end
32
+ end
33
+ )
34
+ end
35
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Specify if, when using the Puppetserver v4 catalog API, the Puppetserver should
4
+ # update the catalog in PuppetDB.
5
+ # @param parser [OptionParser object] The OptionParser argument
6
+ # @param options [Hash] Options hash being constructed; this is modified in this method.
7
+ OctocatalogDiff::Cli::Options::Option.newoption(:puppet_master_update_catalog) do
8
+ has_weight 320
9
+
10
+ def parse(parser, options)
11
+ OctocatalogDiff::Cli::Options.option_globally_or_per_branch(
12
+ parser: parser,
13
+ options: options,
14
+ datatype: false,
15
+ cli_name: 'puppet-master-update-catalog',
16
+ option_name: 'puppet_master_update_catalog',
17
+ desc: 'Update catalog in PuppetDB when using Puppetmaster API version 4'
18
+ )
19
+ end
20
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Specify if, when using the Puppetserver v4 catalog API, the Puppetserver should
4
+ # update the facts in PuppetDB.
5
+ # @param parser [OptionParser object] The OptionParser argument
6
+ # @param options [Hash] Options hash being constructed; this is modified in this method.
7
+ OctocatalogDiff::Cli::Options::Option.newoption(:puppet_master_update_facts) do
8
+ has_weight 320
9
+
10
+ def parse(parser, options)
11
+ OctocatalogDiff::Cli::Options.option_globally_or_per_branch(
12
+ parser: parser,
13
+ options: options,
14
+ datatype: false,
15
+ cli_name: 'puppet-master-update-facts',
16
+ option_name: 'puppet_master_update_facts',
17
+ desc: 'Update facts in PuppetDB when using Puppetmaster API version 4'
18
+ )
19
+ end
20
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ # When pulling facts from PuppetDB in a Puppet Enterprise environment, also include
4
+ # the Puppet Enterprise Package Inventory data in the fact results, if available.
5
+ # Generally you should not need to specify this, but including the package inventory
6
+ # data will produce a more accurate set of input facts for environments using
7
+ # package inventory.
8
+ # @param parser [OptionParser object] The OptionParser argument
9
+ # @param options [Hash] Options hash being constructed; this is modified in this method.
10
+ OctocatalogDiff::Cli::Options::Option.newoption(:puppetdb_package_inventory) do
11
+ has_weight 150
12
+
13
+ def parse(parser, options)
14
+ parser.on('--[no-]puppetdb-package-inventory', 'Include Puppet Enterprise package inventory data, if found') do |x|
15
+ options[:puppetdb_package_inventory] = x
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Configures using the Longest common subsequence (LCS) algorithm to determine differences in arrays
4
+ # @param parser [OptionParser object] The OptionParser argument
5
+ # @param options [Hash] Options hash being constructed; this is modified in this method.
6
+ OctocatalogDiff::Cli::Options::Option.newoption(:use_lcs) do
7
+ has_weight 250
8
+
9
+ def parse(parser, options)
10
+ parser.on('--[no-]use-lcs', 'Use the LCS algorithm to determine differences in arrays') do |x|
11
+ options[:use_lcs] = x
12
+ end
13
+ end
14
+ end
@@ -14,8 +14,19 @@ module OctocatalogDiff
14
14
  # @return [Hash] Facts
15
15
  def self.fact_retriever(options = {}, node = '')
16
16
  facts = ::JSON.parse(options.fetch(:fact_file_string))
17
- node = facts.fetch('fqdn', 'unknown.node') if node.empty?
18
- { 'name' => node, 'values' => facts }
17
+
18
+ if facts.keys.include?('name') && facts.keys.include?('values') && facts['values'].is_a?(Hash)
19
+ # If you saved the output of something like
20
+ # `puppet facts find $(hostname)` the structure will already be a
21
+ # {'name' => <fqdn>, 'values' => <hash of facts>}. We do nothing
22
+ # here because we don't want to double-encode.
23
+ else
24
+ facts = { 'name' => node, 'values' => facts }
25
+ end
26
+
27
+ facts['name'] = node unless node.empty?
28
+ facts['name'] = facts['values'].fetch('fqdn', 'unknown.node') if facts['name'].empty?
29
+ facts
19
30
  end
20
31
  end
21
32
  end
@@ -36,6 +36,7 @@ module OctocatalogDiff
36
36
  exception_class = nil
37
37
  exception_message = nil
38
38
  obj_to_return = nil
39
+ packages = nil
39
40
  (retries + 1).times do
40
41
  begin
41
42
  result = puppetdb.get(uri)
@@ -61,8 +62,48 @@ module OctocatalogDiff
61
62
  exception_message = "Fact retrieval failed for node #{node} from PuppetDB (#{exc.message})"
62
63
  end
63
64
  end
64
- return obj_to_return unless obj_to_return.nil?
65
- raise exception_class, exception_message
65
+
66
+ raise exception_class, exception_message if obj_to_return.nil?
67
+
68
+ return obj_to_return if puppetdb_api_version < 4 || (!options[:puppetdb_package_inventory])
69
+
70
+ (retries + 1).times do
71
+ begin
72
+ result = puppetdb.get("/pdb/query/v4/package-inventory/#{node}")
73
+ packages = {}
74
+ result.each do |pkg|
75
+ key = "#{pkg['package_name']}+#{pkg['provider']}"
76
+ # Need to handle the situation where a package has multiple versions installed.
77
+ # The _puppet_inventory_1 hash lists them separated by "; ".
78
+ if packages.key?(key)
79
+ packages[key]['version'] += "; #{pkg['version']}"
80
+ else
81
+ packages[key] = pkg
82
+ end
83
+ end
84
+ break
85
+ rescue OctocatalogDiff::Errors::PuppetDBConnectionError => exc
86
+ exception_class = OctocatalogDiff::Errors::FactSourceError
87
+ exception_message = "Package inventory retrieval failed (#{exc.class}) (#{exc.message})"
88
+ # This is not expected to occur, but we'll leave it just in case. A query to package-inventory
89
+ # for a non-existant node returns a 200 OK with an empty list of packages:
90
+ rescue OctocatalogDiff::Errors::PuppetDBNodeNotFoundError
91
+ packages = {}
92
+ rescue OctocatalogDiff::Errors::PuppetDBGenericError => exc
93
+ exception_class = OctocatalogDiff::Errors::FactRetrievalError
94
+ exception_message = "Package inventory retrieval failed for node #{node} from PuppetDB (#{exc.message})"
95
+ end
96
+ end
97
+
98
+ raise exception_class, exception_message if packages.nil?
99
+
100
+ unless packages.empty?
101
+ obj_to_return['values']['_puppet_inventory_1'] = {
102
+ 'packages' => packages.values.map { |pkg| [pkg['package_name'], pkg['version'], pkg['provider']] }
103
+ }
104
+ end
105
+
106
+ obj_to_return
66
107
  end
67
108
  end
68
109
  end
@@ -129,22 +129,26 @@ module OctocatalogDiff
129
129
 
130
130
  # Waiting for children and handling results
131
131
  while pidmap.any?
132
- this_pid, exit_obj = Process.wait2(0)
133
- next unless this_pid && pidmap.key?(this_pid)
134
- index = pidmap[this_pid][:index]
135
- exitstatus = exit_obj.exitstatus
136
- raise "PID=#{this_pid} exited abnormally: #{exit_obj.inspect}" if exitstatus.nil?
137
- raise "PID=#{this_pid} exited with status #{exitstatus}" unless exitstatus.zero?
138
-
139
- input = File.read(File.join(ipc_tempdir, "#{this_pid}.dat"))
140
- result[index] = Marshal.load(input) # rubocop:disable Security/MarshalLoad
141
- time_delta = Time.now - pidmap[this_pid][:start_time]
142
- pidmap.delete(this_pid)
143
-
144
- logger.debug "PID=#{this_pid} completed in #{time_delta} seconds, #{input.length} bytes"
145
-
146
- next if result[index].status
147
- return result[index].exception
132
+ pidmap.each do |pid|
133
+ status = Process.waitpid2(pid[0], Process::WNOHANG)
134
+ next if status.nil?
135
+ this_pid, exit_obj = status
136
+ next unless this_pid && pidmap.key?(this_pid)
137
+ index = pidmap[this_pid][:index]
138
+ exitstatus = exit_obj.exitstatus
139
+ raise "PID=#{this_pid} exited abnormally: #{exit_obj.inspect}" if exitstatus.nil?
140
+ raise "PID=#{this_pid} exited with status #{exitstatus}" unless exitstatus.zero?
141
+
142
+ input = File.read(File.join(ipc_tempdir, "#{this_pid}.dat"))
143
+ result[index] = Marshal.load(input) # rubocop:disable Security/MarshalLoad
144
+ time_delta = Time.now - pidmap[this_pid][:start_time]
145
+ pidmap.delete(this_pid)
146
+
147
+ logger.debug "PID=#{this_pid} completed in #{time_delta} seconds, #{input.length} bytes"
148
+
149
+ next if result[index].status
150
+ return result[index].exception
151
+ end
148
152
  end
149
153
 
150
154
  logger.debug 'All child processes completed with no exceptions raised'