cem_acpt 0.8.8 → 0.9.1
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.
- checksums.yaml +4 -4
- data/.github/workflows/spec.yml +0 -3
- data/Gemfile.lock +9 -1
- data/README.md +95 -13
- data/cem_acpt.gemspec +2 -1
- data/lib/cem_acpt/action_result.rb +8 -2
- data/lib/cem_acpt/actions.rb +153 -0
- data/lib/cem_acpt/bolt/cmd/base.rb +174 -0
- data/lib/cem_acpt/bolt/cmd/output.rb +315 -0
- data/lib/cem_acpt/bolt/cmd/task.rb +59 -0
- data/lib/cem_acpt/bolt/cmd.rb +22 -0
- data/lib/cem_acpt/bolt/errors.rb +49 -0
- data/lib/cem_acpt/bolt/helpers.rb +52 -0
- data/lib/cem_acpt/bolt/inventory.rb +62 -0
- data/lib/cem_acpt/bolt/project.rb +38 -0
- data/lib/cem_acpt/bolt/summary_results.rb +96 -0
- data/lib/cem_acpt/bolt/tasks.rb +181 -0
- data/lib/cem_acpt/bolt/tests.rb +415 -0
- data/lib/cem_acpt/bolt/yaml_file.rb +74 -0
- data/lib/cem_acpt/bolt.rb +142 -0
- data/lib/cem_acpt/cli.rb +6 -0
- data/lib/cem_acpt/config/base.rb +4 -0
- data/lib/cem_acpt/config/cem_acpt.rb +7 -1
- data/lib/cem_acpt/core_ext.rb +25 -0
- data/lib/cem_acpt/goss/api/action_response.rb +4 -0
- data/lib/cem_acpt/goss/api.rb +23 -25
- data/lib/cem_acpt/image_builder/provision_commands.rb +43 -0
- data/lib/cem_acpt/logging/formatter.rb +3 -3
- data/lib/cem_acpt/logging.rb +17 -1
- data/lib/cem_acpt/provision/terraform/linux.rb +2 -2
- data/lib/cem_acpt/test_data.rb +2 -0
- data/lib/cem_acpt/test_runner/log_formatter/base.rb +73 -0
- data/lib/cem_acpt/test_runner/log_formatter/bolt_error_formatter.rb +65 -0
- data/lib/cem_acpt/test_runner/log_formatter/bolt_output_formatter.rb +54 -0
- data/lib/cem_acpt/test_runner/log_formatter/bolt_summary_results_formatter.rb +64 -0
- data/lib/cem_acpt/test_runner/log_formatter/goss_action_response.rb +17 -30
- data/lib/cem_acpt/test_runner/log_formatter/goss_error_formatter.rb +31 -0
- data/lib/cem_acpt/test_runner/log_formatter/standard_error_formatter.rb +35 -0
- data/lib/cem_acpt/test_runner/log_formatter.rb +17 -5
- data/lib/cem_acpt/test_runner/test_results.rb +150 -0
- data/lib/cem_acpt/test_runner.rb +153 -53
- data/lib/cem_acpt/utils/files.rb +189 -0
- data/lib/cem_acpt/utils/finalizer_queue.rb +73 -0
- data/lib/cem_acpt/utils/shell.rb +13 -4
- data/lib/cem_acpt/version.rb +1 -1
- data/sample_config.yaml +13 -0
- metadata +41 -5
- data/lib/cem_acpt/test_runner/log_formatter/error_formatter.rb +0 -33
| @@ -0,0 +1,315 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            require 'json'
         | 
| 4 | 
            +
             | 
| 5 | 
            +
            module CemAcpt
         | 
| 6 | 
            +
              module Bolt
         | 
| 7 | 
            +
                module Cmd
         | 
| 8 | 
            +
                  # Wraps the output of a Bolt command
         | 
| 9 | 
            +
                  class Output
         | 
| 10 | 
            +
                    attr_reader :cmd_output, :items, :target_count, :elapsed_time, :error_obj
         | 
| 11 | 
            +
             | 
| 12 | 
            +
                    def initialize(cmd_output, strict: true, **item_defaults)
         | 
| 13 | 
            +
                      @original_cmd_output = cmd_output
         | 
| 14 | 
            +
                      @strict = strict
         | 
| 15 | 
            +
                      @item_defaults = item_defaults.transform_keys(&:to_s)
         | 
| 16 | 
            +
                      init_cmd_output_and_error_obj(cmd_output)
         | 
| 17 | 
            +
                      @target_count = @cmd_output['target_count'] || 0
         | 
| 18 | 
            +
                      @elapsed_time = @cmd_output['elapsed_time'] || 0
         | 
| 19 | 
            +
                    end
         | 
| 20 | 
            +
             | 
| 21 | 
            +
                    def action
         | 
| 22 | 
            +
                      items&.map(&:action)&.uniq || [@item_defaults['action']] || ['unknown']
         | 
| 23 | 
            +
                    end
         | 
| 24 | 
            +
             | 
| 25 | 
            +
                    def error
         | 
| 26 | 
            +
                      return nil unless error?
         | 
| 27 | 
            +
             | 
| 28 | 
            +
                      items.find(&:error?)&.error || error_obj
         | 
| 29 | 
            +
                    end
         | 
| 30 | 
            +
             | 
| 31 | 
            +
                    def error?
         | 
| 32 | 
            +
                      !error_obj.nil? || items.any?(&:error?)
         | 
| 33 | 
            +
                    end
         | 
| 34 | 
            +
             | 
| 35 | 
            +
                    def success?
         | 
| 36 | 
            +
                      !error?
         | 
| 37 | 
            +
                    end
         | 
| 38 | 
            +
             | 
| 39 | 
            +
                    def status
         | 
| 40 | 
            +
                      return 0 if success?
         | 
| 41 | 
            +
             | 
| 42 | 
            +
                      1
         | 
| 43 | 
            +
                    end
         | 
| 44 | 
            +
             | 
| 45 | 
            +
                    def summary
         | 
| 46 | 
            +
                      @summary ||= [
         | 
| 47 | 
            +
                        "status: #{success? ? 'passed' : 'failed'}",
         | 
| 48 | 
            +
                        "items total: #{items.length}",
         | 
| 49 | 
            +
                        "items succeeded: #{items.count(&:success?)}",
         | 
| 50 | 
            +
                        "items failed: #{items.count(&:error?)}",
         | 
| 51 | 
            +
                        "target count: #{target_count}",
         | 
| 52 | 
            +
                        "elapsed time: #{elapsed_time}",
         | 
| 53 | 
            +
                      ].join(', ')
         | 
| 54 | 
            +
                    end
         | 
| 55 | 
            +
             | 
| 56 | 
            +
                    def summary?
         | 
| 57 | 
            +
                      true
         | 
| 58 | 
            +
                    end
         | 
| 59 | 
            +
             | 
| 60 | 
            +
                    def inspect
         | 
| 61 | 
            +
                      "#<#{self.class}:#{object_id} #{self}>"
         | 
| 62 | 
            +
                    end
         | 
| 63 | 
            +
             | 
| 64 | 
            +
                    def to_h
         | 
| 65 | 
            +
                      @cmd_output
         | 
| 66 | 
            +
                    end
         | 
| 67 | 
            +
             | 
| 68 | 
            +
                    def results
         | 
| 69 | 
            +
                      @results ||= new_results
         | 
| 70 | 
            +
                    end
         | 
| 71 | 
            +
             | 
| 72 | 
            +
                    def results?
         | 
| 73 | 
            +
                      !results.empty?
         | 
| 74 | 
            +
                    end
         | 
| 75 | 
            +
             | 
| 76 | 
            +
                    def to_s
         | 
| 77 | 
            +
                      return error.to_s if error?
         | 
| 78 | 
            +
             | 
| 79 | 
            +
                      JSON.pretty_generate(@cmd_output)
         | 
| 80 | 
            +
                    end
         | 
| 81 | 
            +
             | 
| 82 | 
            +
                    def ==(other)
         | 
| 83 | 
            +
                      return false unless other.is_a?(self.class)
         | 
| 84 | 
            +
                      return false unless to_h == other.to_h
         | 
| 85 | 
            +
             | 
| 86 | 
            +
                      items.zip(other.items).all? { |a, b| a == b }
         | 
| 87 | 
            +
                    end
         | 
| 88 | 
            +
                    alias eql? ==
         | 
| 89 | 
            +
             | 
| 90 | 
            +
                    # Exists solely for Bolt tests
         | 
| 91 | 
            +
                    def copy_with_new_items(new_items = [])
         | 
| 92 | 
            +
                      new_self = self.class.new(original_cmd_output, strict: @strict, **@item_defaults)
         | 
| 93 | 
            +
                      new_self.items = new_items
         | 
| 94 | 
            +
                      new_self
         | 
| 95 | 
            +
                    end
         | 
| 96 | 
            +
             | 
| 97 | 
            +
                    def method_missing(method, *args, **kwargs, &block)
         | 
| 98 | 
            +
                      if @cmd_output.respond_to?(method)
         | 
| 99 | 
            +
                        @cmd_output.send(method, *args, **kwargs, &block)
         | 
| 100 | 
            +
                      elsif error.respond_to?(method)
         | 
| 101 | 
            +
                        error.send(method, *args, **kwargs, &block)
         | 
| 102 | 
            +
                      else
         | 
| 103 | 
            +
                        super
         | 
| 104 | 
            +
                      end
         | 
| 105 | 
            +
                    end
         | 
| 106 | 
            +
             | 
| 107 | 
            +
                    def respond_to_missing?(method, include_private = false)
         | 
| 108 | 
            +
                      @cmd_output.respond_to?(method, include_private) || error.respond_to?(method, include_private) || super
         | 
| 109 | 
            +
                    end
         | 
| 110 | 
            +
             | 
| 111 | 
            +
                    protected
         | 
| 112 | 
            +
             | 
| 113 | 
            +
                    attr_reader :original_cmd_output
         | 
| 114 | 
            +
                    attr_writer :cmd_output, :items, :target_count, :elapsed_time, :error_obj
         | 
| 115 | 
            +
             | 
| 116 | 
            +
                    private
         | 
| 117 | 
            +
             | 
| 118 | 
            +
                    def init_cmd_output_and_error_obj(cmd_out)
         | 
| 119 | 
            +
                      case cmd_out
         | 
| 120 | 
            +
                      when String
         | 
| 121 | 
            +
                        init_cmd_output_and_error_obj_from_str(cmd_out)
         | 
| 122 | 
            +
                      when StandardError
         | 
| 123 | 
            +
                        @error_obj = cmd_out
         | 
| 124 | 
            +
                        @cmd_output = ruby_error_to_cmd_output_hash(cmd_out)
         | 
| 125 | 
            +
                      else
         | 
| 126 | 
            +
                        raise ArgumentError, "cmd_out must be a String or StandardError, got #{cmd_out.class}"
         | 
| 127 | 
            +
                      end
         | 
| 128 | 
            +
                    ensure
         | 
| 129 | 
            +
                      @items = (@cmd_output['items'] || []).map { |item| OutputItem.new(item) }
         | 
| 130 | 
            +
                      if @items.empty? && @error_obj.nil? && @strict
         | 
| 131 | 
            +
                        err = RuntimeError.new("Cannot set results, no error or items found for cmd_output:\n#{cmd_output}")
         | 
| 132 | 
            +
                        @error_obj = err
         | 
| 133 | 
            +
                        @items = ruby_error_to_cmd_output_hash(err)['items'].map { |item| OutputItem.new(item) }
         | 
| 134 | 
            +
                      end
         | 
| 135 | 
            +
                    end
         | 
| 136 | 
            +
             | 
| 137 | 
            +
                    def init_cmd_output_and_error_obj_from_str(cmd_out)
         | 
| 138 | 
            +
                      @cmd_output = JSON.parse(cmd_out)
         | 
| 139 | 
            +
                      if @cmd_output.key?('_error') || @cmd_output.key?('error')
         | 
| 140 | 
            +
                        err_hash = (@cmd_output['_error'] || @cmd_output['error'])
         | 
| 141 | 
            +
                        @cmd_output['items'] ||= []
         | 
| 142 | 
            +
                        @cmd_output['items'] << {
         | 
| 143 | 
            +
                          'value' => {
         | 
| 144 | 
            +
                            '_error' => err_hash,
         | 
| 145 | 
            +
                          },
         | 
| 146 | 
            +
                        }
         | 
| 147 | 
            +
                      end
         | 
| 148 | 
            +
                    rescue JSON::ParserError => e
         | 
| 149 | 
            +
                      @error_obj = e
         | 
| 150 | 
            +
                      @cmd_output = ruby_error_to_cmd_output_hash(e)
         | 
| 151 | 
            +
                    end
         | 
| 152 | 
            +
             | 
| 153 | 
            +
                    def new_results
         | 
| 154 | 
            +
                      return new_error_results if items.empty?
         | 
| 155 | 
            +
             | 
| 156 | 
            +
                      items
         | 
| 157 | 
            +
                    end
         | 
| 158 | 
            +
             | 
| 159 | 
            +
                    def new_error_results
         | 
| 160 | 
            +
                      [error]
         | 
| 161 | 
            +
                    end
         | 
| 162 | 
            +
             | 
| 163 | 
            +
                    def ruby_error_to_cmd_output_hash(error)
         | 
| 164 | 
            +
                      error_kind = [
         | 
| 165 | 
            +
                        'cem_acpt',
         | 
| 166 | 
            +
                        error.class.name.split('::').join('.'),
         | 
| 167 | 
            +
                      ].join('.')
         | 
| 168 | 
            +
                      details = { 'exit_code' => 1 }
         | 
| 169 | 
            +
                      details['backtrace'] = error.backtrace if error.backtrace
         | 
| 170 | 
            +
                      {
         | 
| 171 | 
            +
                        'items' => [
         | 
| 172 | 
            +
                          {
         | 
| 173 | 
            +
                            'value' => {
         | 
| 174 | 
            +
                              '_error' => {
         | 
| 175 | 
            +
                                'kind' => error_kind,
         | 
| 176 | 
            +
                                'msg' => error.to_s,
         | 
| 177 | 
            +
                                'issue_code' => 'CEM_ACPT_ERROR',
         | 
| 178 | 
            +
                                'details' => {
         | 
| 179 | 
            +
                                  'exit_code' => 1,
         | 
| 180 | 
            +
                                  'backtrace' => error.backtrace,
         | 
| 181 | 
            +
                                },
         | 
| 182 | 
            +
                              },
         | 
| 183 | 
            +
                            },
         | 
| 184 | 
            +
                          },
         | 
| 185 | 
            +
                        ],
         | 
| 186 | 
            +
                      }
         | 
| 187 | 
            +
                    end
         | 
| 188 | 
            +
                  end
         | 
| 189 | 
            +
             | 
| 190 | 
            +
                  # Represents a single item in the output of a Bolt command
         | 
| 191 | 
            +
                  class OutputItem
         | 
| 192 | 
            +
                    ATTR_DEFVAL = 'unknown'
         | 
| 193 | 
            +
             | 
| 194 | 
            +
                    attr_reader :target, :action, :object, :status, :value
         | 
| 195 | 
            +
             | 
| 196 | 
            +
                    def initialize(item_hash, **item_defaults)
         | 
| 197 | 
            +
                      @item_hash = item_hash
         | 
| 198 | 
            +
                      @item_defaults = item_defaults.transform_keys(&:to_s)
         | 
| 199 | 
            +
                      @target = item_hash['target'] || @item_defaults['target'] || ATTR_DEFVAL
         | 
| 200 | 
            +
                      @action = item_hash['action'] || @item_defaults['action'] || ATTR_DEFVAL
         | 
| 201 | 
            +
                      @object = item_hash['object'] || @item_defaults['object'] || ATTR_DEFVAL
         | 
| 202 | 
            +
                      @status = item_hash['status'] || 'failure'
         | 
| 203 | 
            +
                      @value = item_hash['value'] || {}
         | 
| 204 | 
            +
                    end
         | 
| 205 | 
            +
             | 
| 206 | 
            +
                    def error?
         | 
| 207 | 
            +
                      !success?
         | 
| 208 | 
            +
                    end
         | 
| 209 | 
            +
             | 
| 210 | 
            +
                    def error
         | 
| 211 | 
            +
                      return unless error?
         | 
| 212 | 
            +
             | 
| 213 | 
            +
                      @error ||= new_error
         | 
| 214 | 
            +
                    end
         | 
| 215 | 
            +
             | 
| 216 | 
            +
                    def success?
         | 
| 217 | 
            +
                      status == 'success'
         | 
| 218 | 
            +
                    end
         | 
| 219 | 
            +
             | 
| 220 | 
            +
                    def output
         | 
| 221 | 
            +
                      return nil if error?
         | 
| 222 | 
            +
             | 
| 223 | 
            +
                      value['_output'] || value['output'] || value
         | 
| 224 | 
            +
                    end
         | 
| 225 | 
            +
             | 
| 226 | 
            +
                    def to_h
         | 
| 227 | 
            +
                      @item_hash
         | 
| 228 | 
            +
                    end
         | 
| 229 | 
            +
             | 
| 230 | 
            +
                    def to_s
         | 
| 231 | 
            +
                      "#<#{self.class}:#{object_id.to_s(16)} #{target},#{action},#{object},#{status}>"
         | 
| 232 | 
            +
                    end
         | 
| 233 | 
            +
             | 
| 234 | 
            +
                    def inspect
         | 
| 235 | 
            +
                      to_s
         | 
| 236 | 
            +
                    end
         | 
| 237 | 
            +
             | 
| 238 | 
            +
                    def ==(other)
         | 
| 239 | 
            +
                      return false unless other.is_a?(self.class)
         | 
| 240 | 
            +
             | 
| 241 | 
            +
                      to_h == other.to_h
         | 
| 242 | 
            +
                    end
         | 
| 243 | 
            +
                    alias eql? ==
         | 
| 244 | 
            +
             | 
| 245 | 
            +
                    private
         | 
| 246 | 
            +
             | 
| 247 | 
            +
                    def new_error
         | 
| 248 | 
            +
                      if value.is_a?(Hash) && (value.key?('_error') || value.key?('error'))
         | 
| 249 | 
            +
                        OutputError.new(value['_error'] || value['error'])
         | 
| 250 | 
            +
                      else
         | 
| 251 | 
            +
                        OutputError.new(
         | 
| 252 | 
            +
                          {
         | 
| 253 | 
            +
                            'kind' => 'cem_acpt.unknown',
         | 
| 254 | 
            +
                            'msg' => value,
         | 
| 255 | 
            +
                            'details' => { 'exit_code' => 1 },
         | 
| 256 | 
            +
                          },
         | 
| 257 | 
            +
                        )
         | 
| 258 | 
            +
                      end
         | 
| 259 | 
            +
                    end
         | 
| 260 | 
            +
                  end
         | 
| 261 | 
            +
             | 
| 262 | 
            +
                  # Represents a Bolt error value
         | 
| 263 | 
            +
                  class OutputError
         | 
| 264 | 
            +
                    attr_accessor :kind, :issue_code, :msg, :details
         | 
| 265 | 
            +
             | 
| 266 | 
            +
                    def initialize(error_hash)
         | 
| 267 | 
            +
                      @error_hash = error_hash
         | 
| 268 | 
            +
                      @kind = error_hash['kind']
         | 
| 269 | 
            +
                      @issue_code = error_hash['issue_code'] || 'OTHER_ERROR'
         | 
| 270 | 
            +
                      @msg = error_hash['msg']
         | 
| 271 | 
            +
                      @details = error_hash['details']
         | 
| 272 | 
            +
                    end
         | 
| 273 | 
            +
             | 
| 274 | 
            +
                    def error?
         | 
| 275 | 
            +
                      true
         | 
| 276 | 
            +
                    end
         | 
| 277 | 
            +
             | 
| 278 | 
            +
                    def success?
         | 
| 279 | 
            +
                      false
         | 
| 280 | 
            +
                    end
         | 
| 281 | 
            +
             | 
| 282 | 
            +
                    def status
         | 
| 283 | 
            +
                      'failure'
         | 
| 284 | 
            +
                    end
         | 
| 285 | 
            +
             | 
| 286 | 
            +
                    def exit_code
         | 
| 287 | 
            +
                      details['exit_code'] || 1
         | 
| 288 | 
            +
                    end
         | 
| 289 | 
            +
             | 
| 290 | 
            +
                    def backtrace
         | 
| 291 | 
            +
                      details['backtrace'] || []
         | 
| 292 | 
            +
                    end
         | 
| 293 | 
            +
             | 
| 294 | 
            +
                    def to_s
         | 
| 295 | 
            +
                      "issue code: #{issue_code}, kind: #{kind}, message: #{msg}"
         | 
| 296 | 
            +
                    end
         | 
| 297 | 
            +
             | 
| 298 | 
            +
                    def inspect
         | 
| 299 | 
            +
                      "#<#{self.class.name}(#{self.class.object_id})#{self}>"
         | 
| 300 | 
            +
                    end
         | 
| 301 | 
            +
             | 
| 302 | 
            +
                    def to_h
         | 
| 303 | 
            +
                      @error_hash
         | 
| 304 | 
            +
                    end
         | 
| 305 | 
            +
             | 
| 306 | 
            +
                    def ==(other)
         | 
| 307 | 
            +
                      return false unless other.is_a?(self.class)
         | 
| 308 | 
            +
             | 
| 309 | 
            +
                      to_h == other.to_h
         | 
| 310 | 
            +
                    end
         | 
| 311 | 
            +
                    alias eql? ==
         | 
| 312 | 
            +
                  end
         | 
| 313 | 
            +
                end
         | 
| 314 | 
            +
              end
         | 
| 315 | 
            +
            end
         | 
| @@ -0,0 +1,59 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            require_relative 'base'
         | 
| 4 | 
            +
             | 
| 5 | 
            +
            module CemAcpt
         | 
| 6 | 
            +
              module Bolt
         | 
| 7 | 
            +
                module Cmd
         | 
| 8 | 
            +
                  # Base class for task commands
         | 
| 9 | 
            +
                  class TaskBase < Base
         | 
| 10 | 
            +
                    attr_accessor :task_name
         | 
| 11 | 
            +
                    attr_reader :sub_command, :item_defaults
         | 
| 12 | 
            +
                    option :module_path, '--modulepath', config_path: 'bolt.module_path', default: Dir.pwd
         | 
| 13 | 
            +
                    option :log_level, '--log-level', config_path: 'bolt.log_level', default: 'warn'
         | 
| 14 | 
            +
                    option :clear_cache, '--clear-cache', config_path: 'bolt.clear_cache', default: false, bool_flag: true
         | 
| 15 | 
            +
                    option :project, '--project', config_path: 'bolt.project.path'
         | 
| 16 | 
            +
             | 
| 17 | 
            +
                    def initialize(config, sub_command = nil, task_name = nil)
         | 
| 18 | 
            +
                      @config = config
         | 
| 19 | 
            +
                      @sub_command = sub_command
         | 
| 20 | 
            +
                      @task_name = task_name
         | 
| 21 | 
            +
                      @item_defaults = {
         | 
| 22 | 
            +
                        'action' => 'task',
         | 
| 23 | 
            +
                        'object' => @task_name,
         | 
| 24 | 
            +
                      }
         | 
| 25 | 
            +
                      super()
         | 
| 26 | 
            +
                    end
         | 
| 27 | 
            +
             | 
| 28 | 
            +
                    def command_family
         | 
| 29 | 
            +
                      'task'
         | 
| 30 | 
            +
                    end
         | 
| 31 | 
            +
             | 
| 32 | 
            +
                    def cmd
         | 
| 33 | 
            +
                      join_array([bolt_bin, 'task', sub_command, task_name, options])
         | 
| 34 | 
            +
                    end
         | 
| 35 | 
            +
                  end
         | 
| 36 | 
            +
             | 
| 37 | 
            +
                  # Runs the Bolt task show command
         | 
| 38 | 
            +
                  class TaskShow < TaskBase
         | 
| 39 | 
            +
                    def initialize(config, task_name = nil, project: nil)
         | 
| 40 | 
            +
                      super(config, 'show', task_name)
         | 
| 41 | 
            +
                      @project = project.is_a?(String) ? project : project&.path
         | 
| 42 | 
            +
                    end
         | 
| 43 | 
            +
                  end
         | 
| 44 | 
            +
             | 
| 45 | 
            +
                  # Runs the Bolt task run command
         | 
| 46 | 
            +
                  class TaskRun < TaskBase
         | 
| 47 | 
            +
                    option :inventory, '--inventoryfile', config_path: 'bolt.inventory_path'
         | 
| 48 | 
            +
                    option :targets, '--targets', config_path: 'bolt.targets', default: 'nix'
         | 
| 49 | 
            +
                    supports_params
         | 
| 50 | 
            +
             | 
| 51 | 
            +
                    def initialize(config, task_name = nil, inventory: nil, project: nil)
         | 
| 52 | 
            +
                      super(config, 'run', task_name)
         | 
| 53 | 
            +
                      @inventory = inventory.is_a?(String) ? inventory : inventory&.path
         | 
| 54 | 
            +
                      @project = project.is_a?(String) ? project : project&.path
         | 
| 55 | 
            +
                    end
         | 
| 56 | 
            +
                  end
         | 
| 57 | 
            +
                end
         | 
| 58 | 
            +
              end
         | 
| 59 | 
            +
            end
         | 
| @@ -0,0 +1,22 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            require_relative 'cmd/task'
         | 
| 4 | 
            +
             | 
| 5 | 
            +
            module CemAcpt
         | 
| 6 | 
            +
              module Bolt
         | 
| 7 | 
            +
                # Namespace for all Bolt command classes
         | 
| 8 | 
            +
                module Cmd
         | 
| 9 | 
            +
                  # Represents the output of a Bolt command
         | 
| 10 | 
            +
                  # @param cmd_output [String, StandardError] the output of a Bolt command or an error
         | 
| 11 | 
            +
                  # @param strict [Boolean] whether to raise an error if the output does not match the expected format
         | 
| 12 | 
            +
                  # @param item_defaults [Hash] default values for the item.
         | 
| 13 | 
            +
                  # @option item_defaults [String] :action The action that was performed (ex: 'task')
         | 
| 14 | 
            +
                  # @option item_defaults [String] :object The object that the action was performed on (ex. The Bolt task name)
         | 
| 15 | 
            +
                  # @options item_defaults [String] :target The IP address of the target that the action was performed on
         | 
| 16 | 
            +
                  # @return [Output] a new Output object for the given command output
         | 
| 17 | 
            +
                  def self.new_output(cmd_output, strict: true, **item_defaults)
         | 
| 18 | 
            +
                    Output.new(cmd_output, strict: strict, **item_defaults)
         | 
| 19 | 
            +
                  end
         | 
| 20 | 
            +
                end
         | 
| 21 | 
            +
              end
         | 
| 22 | 
            +
            end
         | 
| @@ -0,0 +1,49 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            module CemAcpt
         | 
| 4 | 
            +
              module Bolt
         | 
| 5 | 
            +
                # Class for general Bolt errors. Can also wrap other errors.
         | 
| 6 | 
            +
                class BoltActionError < StandardError
         | 
| 7 | 
            +
                  attr_reader :original_error, :bolt_action, :bolt_object
         | 
| 8 | 
            +
             | 
| 9 | 
            +
                  def initialize(msg = 'Bolt error occured', original_error = nil, bolt_action = nil, bolt_object = nil)
         | 
| 10 | 
            +
                    @original_error = original_error
         | 
| 11 | 
            +
                    @bolt_action = bolt_action
         | 
| 12 | 
            +
                    @bolt_object = bolt_object
         | 
| 13 | 
            +
                    unless @original_error.nil?
         | 
| 14 | 
            +
                      set_backtrace(@original_error.backtrace)
         | 
| 15 | 
            +
                      msg = "#{msg}: #{@original_error}"
         | 
| 16 | 
            +
                    end
         | 
| 17 | 
            +
                    super(msg)
         | 
| 18 | 
            +
                  end
         | 
| 19 | 
            +
             | 
| 20 | 
            +
                  def bolt_target
         | 
| 21 | 
            +
                    @bolt_target ||= @bolt_object&.target
         | 
| 22 | 
            +
                  end
         | 
| 23 | 
            +
             | 
| 24 | 
            +
                  def to_h
         | 
| 25 | 
            +
                    {
         | 
| 26 | 
            +
                      bolt_action: bolt_action,
         | 
| 27 | 
            +
                      bolt_object: bolt_object,
         | 
| 28 | 
            +
                      original_error: original_error,
         | 
| 29 | 
            +
                      message: message,
         | 
| 30 | 
            +
                      backtrace: backtrace,
         | 
| 31 | 
            +
                    }
         | 
| 32 | 
            +
                  end
         | 
| 33 | 
            +
                end
         | 
| 34 | 
            +
             | 
| 35 | 
            +
                # Class for Bolt project errors. Can also wrap other errors.
         | 
| 36 | 
            +
                class BoltProjectError < BoltActionError
         | 
| 37 | 
            +
                  def initialize(msg = 'Bolt project error occured', original_error = nil, *_args)
         | 
| 38 | 
            +
                    super(msg, original_error, 'project', 'bolt-project.yaml')
         | 
| 39 | 
            +
                  end
         | 
| 40 | 
            +
                end
         | 
| 41 | 
            +
             | 
| 42 | 
            +
                # Class for Bolt inventory errors. Can also wrap other errors.
         | 
| 43 | 
            +
                class BoltInventoryError < BoltActionError
         | 
| 44 | 
            +
                  def initialize(msg = 'Bolt inventory error occured', original_error = nil, *_args)
         | 
| 45 | 
            +
                    super(msg, original_error, 'inventory', 'inventory.yaml')
         | 
| 46 | 
            +
                  end
         | 
| 47 | 
            +
                end
         | 
| 48 | 
            +
              end
         | 
| 49 | 
            +
            end
         | 
| @@ -0,0 +1,52 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            require_relative '../utils/shell'
         | 
| 4 | 
            +
             | 
| 5 | 
            +
            module CemAcpt
         | 
| 6 | 
            +
              module Bolt
         | 
| 7 | 
            +
                # Module containing helper methods for Bolt
         | 
| 8 | 
            +
                module Helpers
         | 
| 9 | 
            +
                  BOLT_PROJECT_FILE = 'bolt-project.yaml'
         | 
| 10 | 
            +
                  INVENTORY_FILE = 'inventory.yaml'
         | 
| 11 | 
            +
             | 
| 12 | 
            +
                  def load_object_test(bolt_test_data, bolt_object)
         | 
| 13 | 
            +
                    return { params: {} } unless bolt_test_data
         | 
| 14 | 
            +
             | 
| 15 | 
            +
                    bolt_test_data[bolt_object].transform || { params: {} }
         | 
| 16 | 
            +
                  end
         | 
| 17 | 
            +
             | 
| 18 | 
            +
                  def new_bolt_project_hash(module_name, config)
         | 
| 19 | 
            +
                    {
         | 
| 20 | 
            +
                      'name' => module_name,
         | 
| 21 | 
            +
                      'analytics' => false,
         | 
| 22 | 
            +
                    }.merge(config.get('bolt.project')&.transform_keys(&:to_s) || {})
         | 
| 23 | 
            +
                  end
         | 
| 24 | 
            +
             | 
| 25 | 
            +
                  def new_inventory_hash(hosts, private_key, config)
         | 
| 26 | 
            +
                    {
         | 
| 27 | 
            +
                      'groups' => [
         | 
| 28 | 
            +
                        {
         | 
| 29 | 
            +
                          'name' => 'nix',
         | 
| 30 | 
            +
                          'targets' => hosts,
         | 
| 31 | 
            +
                          'config' => {
         | 
| 32 | 
            +
                            'transport' => 'ssh',
         | 
| 33 | 
            +
                            'ssh' => {
         | 
| 34 | 
            +
                              'connect-timeout' => 60,
         | 
| 35 | 
            +
                              'disconnect-timeout' => 60,
         | 
| 36 | 
            +
                              'host-key-check' => false,
         | 
| 37 | 
            +
                              'private-key' => private_key || '~/.ssh/id_rsa',
         | 
| 38 | 
            +
                              'run-as' => 'root',
         | 
| 39 | 
            +
                              'tmpdir' => '/var/tmp', # /tmp is usually noexec
         | 
| 40 | 
            +
                            }.merge(config.get('bolt.transport.ssh')&.transform_keys(&:to_s) || {}),
         | 
| 41 | 
            +
                          },
         | 
| 42 | 
            +
                        },
         | 
| 43 | 
            +
                      ],
         | 
| 44 | 
            +
                    }
         | 
| 45 | 
            +
                  end
         | 
| 46 | 
            +
             | 
| 47 | 
            +
                  def bolt_bin
         | 
| 48 | 
            +
                    @bolt_bin ||= CemAcpt::Utils::Shell.which('bolt', raise_if_not_found: true)
         | 
| 49 | 
            +
                  end
         | 
| 50 | 
            +
                end
         | 
| 51 | 
            +
              end
         | 
| 52 | 
            +
            end
         | 
| @@ -0,0 +1,62 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            require_relative 'errors'
         | 
| 4 | 
            +
            require_relative 'yaml_file'
         | 
| 5 | 
            +
             | 
| 6 | 
            +
            module CemAcpt
         | 
| 7 | 
            +
              module Bolt
         | 
| 8 | 
            +
                # Provides an abstraction for the Bolt inventory file
         | 
| 9 | 
            +
                class Inventory < YamlFile
         | 
| 10 | 
            +
                  attr_reader :config, :hosts, :private_key
         | 
| 11 | 
            +
             | 
| 12 | 
            +
                  def initialize(config, hosts = [], private_key = nil)
         | 
| 13 | 
            +
                    path = config.get('bolt.inventory_path') || 'inventory.yaml'
         | 
| 14 | 
            +
                    super(path)
         | 
| 15 | 
            +
                    @config = config
         | 
| 16 | 
            +
                    @hosts = hosts
         | 
| 17 | 
            +
                    @private_key = private_key
         | 
| 18 | 
            +
                    @hash = new_inventory_hash(hosts, private_key, config)
         | 
| 19 | 
            +
                  end
         | 
| 20 | 
            +
             | 
| 21 | 
            +
                  def hosts=(hosts)
         | 
| 22 | 
            +
                    return if @hosts == hosts
         | 
| 23 | 
            +
             | 
| 24 | 
            +
                    @hosts = hosts
         | 
| 25 | 
            +
                    @hash = new_inventory_hash(hosts, @private_key, @config)
         | 
| 26 | 
            +
                  end
         | 
| 27 | 
            +
             | 
| 28 | 
            +
                  def private_key=(private_key)
         | 
| 29 | 
            +
                    return if @private_key == private_key
         | 
| 30 | 
            +
             | 
| 31 | 
            +
                    @private_key = private_key
         | 
| 32 | 
            +
                    @hash = new_inventory_hash(@hosts, private_key, @config)
         | 
| 33 | 
            +
                  end
         | 
| 34 | 
            +
             | 
| 35 | 
            +
                  private
         | 
| 36 | 
            +
             | 
| 37 | 
            +
                  def new_inventory_hash(hosts, private_key, config)
         | 
| 38 | 
            +
                    {
         | 
| 39 | 
            +
                      'groups' => [
         | 
| 40 | 
            +
                        {
         | 
| 41 | 
            +
                          'name' => 'nix',
         | 
| 42 | 
            +
                          'targets' => hosts,
         | 
| 43 | 
            +
                          'config' => {
         | 
| 44 | 
            +
                            'transport' => 'ssh',
         | 
| 45 | 
            +
                            'ssh' => {
         | 
| 46 | 
            +
                              'connect-timeout' => 60,
         | 
| 47 | 
            +
                              'disconnect-timeout' => 60,
         | 
| 48 | 
            +
                              'host-key-check' => false,
         | 
| 49 | 
            +
                              'private-key' => private_key || '~/.ssh/id_rsa',
         | 
| 50 | 
            +
                              'run-as' => 'root',
         | 
| 51 | 
            +
                              'tmpdir' => '/var/tmp', # /tmp is usually noexec
         | 
| 52 | 
            +
                            }.merge(config.get('bolt.transport.ssh')&.transform_keys(&:to_s) || {}),
         | 
| 53 | 
            +
                          },
         | 
| 54 | 
            +
                        },
         | 
| 55 | 
            +
                      ],
         | 
| 56 | 
            +
                    }
         | 
| 57 | 
            +
                  rescue StandardError => e
         | 
| 58 | 
            +
                    raise CemAcpt::Bolt::InventoryError.new('Error creating Bolt inventory hash', e)
         | 
| 59 | 
            +
                  end
         | 
| 60 | 
            +
                end
         | 
| 61 | 
            +
              end
         | 
| 62 | 
            +
            end
         | 
| @@ -0,0 +1,38 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            require_relative 'errors'
         | 
| 4 | 
            +
            require_relative 'yaml_file'
         | 
| 5 | 
            +
             | 
| 6 | 
            +
            module CemAcpt
         | 
| 7 | 
            +
              module Bolt
         | 
| 8 | 
            +
                # Provides an abstraction for the Bolt project file / config
         | 
| 9 | 
            +
                class Project < YamlFile
         | 
| 10 | 
            +
                  attr_reader :config, :module_name
         | 
| 11 | 
            +
             | 
| 12 | 
            +
                  def initialize(config, module_name)
         | 
| 13 | 
            +
                    path = config.get('bolt.project.path') || 'bolt-project.yaml'
         | 
| 14 | 
            +
                    super(path)
         | 
| 15 | 
            +
                    @config = config
         | 
| 16 | 
            +
                    @module_name = module_name
         | 
| 17 | 
            +
                    @hash = new_bolt_project_hash(module_name, config)
         | 
| 18 | 
            +
                  end
         | 
| 19 | 
            +
             | 
| 20 | 
            +
                  def latest_saved?
         | 
| 21 | 
            +
                    # We consider the project file to be up to date if it is subset of the contents on disk
         | 
| 22 | 
            +
                    # or equal to the contents on disk
         | 
| 23 | 
            +
                    (@hash == @saved_hash) || lte_to_disk?
         | 
| 24 | 
            +
                  end
         | 
| 25 | 
            +
             | 
| 26 | 
            +
                  private
         | 
| 27 | 
            +
             | 
| 28 | 
            +
                  def new_bolt_project_hash(module_name, config)
         | 
| 29 | 
            +
                    {
         | 
| 30 | 
            +
                      'name' => module_name,
         | 
| 31 | 
            +
                      'analytics' => false,
         | 
| 32 | 
            +
                    }.merge(config.get('bolt.project')&.transform_keys(&:to_s) || {})
         | 
| 33 | 
            +
                  rescue StandardError => e
         | 
| 34 | 
            +
                    raise CemAcpt::Bolt::BoltProjectError.new('Error creating Bolt project hash', e)
         | 
| 35 | 
            +
                  end
         | 
| 36 | 
            +
                end
         | 
| 37 | 
            +
              end
         | 
| 38 | 
            +
            end
         | 
| @@ -0,0 +1,96 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            require 'json'
         | 
| 4 | 
            +
            require_relative '../utils/finalizer_queue'
         | 
| 5 | 
            +
             | 
| 6 | 
            +
            module CemAcpt
         | 
| 7 | 
            +
              module Bolt
         | 
| 8 | 
            +
                # Class that holds the results of the entire Bolt test suite.
         | 
| 9 | 
            +
                class SummaryResults < CemAcpt::Utils::FinalizerQueue
         | 
| 10 | 
            +
                  alias all to_a
         | 
| 11 | 
            +
             | 
| 12 | 
            +
                  def error
         | 
| 13 | 
            +
                    require_finalized(binding)
         | 
| 14 | 
            +
                    return unless error?
         | 
| 15 | 
            +
             | 
| 16 | 
            +
                    @error ||= find { |r| !r.success? }
         | 
| 17 | 
            +
                  end
         | 
| 18 | 
            +
             | 
| 19 | 
            +
                  def error?
         | 
| 20 | 
            +
                    require_finalized(binding)
         | 
| 21 | 
            +
                    !success?
         | 
| 22 | 
            +
                  end
         | 
| 23 | 
            +
             | 
| 24 | 
            +
                  def success?
         | 
| 25 | 
            +
                    require_finalized(binding)
         | 
| 26 | 
            +
                    @success ||= all?(&:success?)
         | 
| 27 | 
            +
                  end
         | 
| 28 | 
            +
             | 
| 29 | 
            +
                  def success_count
         | 
| 30 | 
            +
                    require_finalized(binding)
         | 
| 31 | 
            +
                    @success_count ||= count(&:success?)
         | 
| 32 | 
            +
                  end
         | 
| 33 | 
            +
             | 
| 34 | 
            +
                  def failure_count
         | 
| 35 | 
            +
                    require_finalized(binding)
         | 
| 36 | 
            +
                    @failure_count ||= length - success_count
         | 
| 37 | 
            +
                  end
         | 
| 38 | 
            +
             | 
| 39 | 
            +
                  def status
         | 
| 40 | 
            +
                    require_finalized(binding)
         | 
| 41 | 
            +
                    success? ? 0 : 1
         | 
| 42 | 
            +
                  end
         | 
| 43 | 
            +
             | 
| 44 | 
            +
                  def status_str
         | 
| 45 | 
            +
                    require_finalized(binding)
         | 
| 46 | 
            +
                    success? ? 'passed' : 'failed'
         | 
| 47 | 
            +
                  end
         | 
| 48 | 
            +
             | 
| 49 | 
            +
                  def summary
         | 
| 50 | 
            +
                    require_finalized(binding)
         | 
| 51 | 
            +
                    @summary ||= [
         | 
| 52 | 
            +
                      "status: #{status_str}",
         | 
| 53 | 
            +
                      "tests total: #{length}",
         | 
| 54 | 
            +
                      "tests succeeded: #{success_count}",
         | 
| 55 | 
            +
                      "tests failed: #{failure_count}",
         | 
| 56 | 
            +
                    ].join(', ')
         | 
| 57 | 
            +
                  end
         | 
| 58 | 
            +
             | 
| 59 | 
            +
                  def summary?
         | 
| 60 | 
            +
                    require_finalized(binding)
         | 
| 61 | 
            +
                    true
         | 
| 62 | 
            +
                  end
         | 
| 63 | 
            +
             | 
| 64 | 
            +
                  def inspect
         | 
| 65 | 
            +
                    return "#<#{self.class}:#{object_id} unfinalized>" unless finalized?
         | 
| 66 | 
            +
             | 
| 67 | 
            +
                    "#<#{self.class}:#{object_id} #{self}>"
         | 
| 68 | 
            +
                  end
         | 
| 69 | 
            +
             | 
| 70 | 
            +
                  def to_h
         | 
| 71 | 
            +
                    require_finalized(binding)
         | 
| 72 | 
            +
                    { 'summary' => summary, 'status' => status, 'results' => map(&:to_h) }
         | 
| 73 | 
            +
                  end
         | 
| 74 | 
            +
             | 
| 75 | 
            +
                  def action
         | 
| 76 | 
            +
                    require_finalized(binding)
         | 
| 77 | 
            +
                    map { |rs| rs.results.map(&:action).uniq }.flatten.uniq
         | 
| 78 | 
            +
                  end
         | 
| 79 | 
            +
             | 
| 80 | 
            +
                  def results
         | 
| 81 | 
            +
                    require_finalized(binding)
         | 
| 82 | 
            +
                    @results ||= map(&:results).flatten
         | 
| 83 | 
            +
                  end
         | 
| 84 | 
            +
             | 
| 85 | 
            +
                  def results?
         | 
| 86 | 
            +
                    require_finalized(binding)
         | 
| 87 | 
            +
                    !results.empty?
         | 
| 88 | 
            +
                  end
         | 
| 89 | 
            +
             | 
| 90 | 
            +
                  def to_s
         | 
| 91 | 
            +
                    require_finalized(binding)
         | 
| 92 | 
            +
                    JSON.pretty_generate(to_h)
         | 
| 93 | 
            +
                  end
         | 
| 94 | 
            +
                end
         | 
| 95 | 
            +
              end
         | 
| 96 | 
            +
            end
         |