choria-colt 0.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.
- checksums.yaml +7 -0
- data/.rubocop.yml +38 -0
- data/Gemfile +6 -0
- data/README.md +54 -0
- data/Rakefile +8 -0
- data/choria-colt.gemspec +44 -0
- data/exe/colt +5 -0
- data/lib/choria/colt/cache.rb +33 -0
- data/lib/choria/colt/cli/thor.rb +42 -0
- data/lib/choria/colt/cli.rb +109 -0
- data/lib/choria/colt/version.rb +7 -0
- data/lib/choria/colt.rb +58 -0
- data/lib/choria/orchestrator/task.rb +59 -0
- data/lib/choria/orchestrator.rb +99 -0
- data/sig/choria/colt.rbs +6 -0
- metadata +159 -0
    
        checksums.yaml
    ADDED
    
    | @@ -0,0 +1,7 @@ | |
| 1 | 
            +
            ---
         | 
| 2 | 
            +
            SHA256:
         | 
| 3 | 
            +
              metadata.gz: fa21249d732edfc1a2391bd20dcfc33cd8eee373329f90ffbf95ad6e9feb0a5c
         | 
| 4 | 
            +
              data.tar.gz: 1ee9fbda91dd047ca932e8d117b65c2aab0252fdc96c30bf4b3be7a841ef3af5
         | 
| 5 | 
            +
            SHA512:
         | 
| 6 | 
            +
              metadata.gz: d6b9e92533f0911ba97d3032f46a6d6b7c8f779317cf471dae1ee70a5457cca53ee62b1410a8ecba691cba504dadc3df54f92f9dfb4a63238e11cbc16c04d261
         | 
| 7 | 
            +
              data.tar.gz: cc601e0c25747af5ff818a32eff04138138a66277a2daf430016ac5755a2906a3016f918fe43c23cffd3a5a3a66cc40267267a3cfe65bef211b178c1cc5ca8ef
         | 
    
        data/.rubocop.yml
    ADDED
    
    | @@ -0,0 +1,38 @@ | |
| 1 | 
            +
            AllCops:
         | 
| 2 | 
            +
              TargetRubyVersion: 2.5
         | 
| 3 | 
            +
              Exclude:
         | 
| 4 | 
            +
                - 'bin/colt'
         | 
| 5 | 
            +
                - 'vendor/**/*'
         | 
| 6 | 
            +
              NewCops: enable
         | 
| 7 | 
            +
             | 
| 8 | 
            +
            Metrics/AbcSize:
         | 
| 9 | 
            +
              Max: 20
         | 
| 10 | 
            +
             | 
| 11 | 
            +
            Metrics/MethodLength:
         | 
| 12 | 
            +
              Max: 17
         | 
| 13 | 
            +
             | 
| 14 | 
            +
            Layout/LineLength:
         | 
| 15 | 
            +
              Enabled: false
         | 
| 16 | 
            +
             | 
| 17 | 
            +
            Style/Documentation:
         | 
| 18 | 
            +
              Enabled: false
         | 
| 19 | 
            +
             | 
| 20 | 
            +
            Style/FrozenStringLiteralComment:
         | 
| 21 | 
            +
              Enabled: false
         | 
| 22 | 
            +
             | 
| 23 | 
            +
            Style/NumericLiterals:
         | 
| 24 | 
            +
              Enabled: false
         | 
| 25 | 
            +
             | 
| 26 | 
            +
            Style/TrailingCommaInArrayLiteral:
         | 
| 27 | 
            +
              EnforcedStyleForMultiline: comma
         | 
| 28 | 
            +
             | 
| 29 | 
            +
            Style/TrailingCommaInHashLiteral:
         | 
| 30 | 
            +
              EnforcedStyleForMultiline: comma
         | 
| 31 | 
            +
             | 
| 32 | 
            +
            Layout/HashAlignment:
         | 
| 33 | 
            +
              EnforcedHashRocketStyle: table
         | 
| 34 | 
            +
             | 
| 35 | 
            +
            Metrics/BlockLength:
         | 
| 36 | 
            +
              Exclude:
         | 
| 37 | 
            +
                - '*.gemspec'
         | 
| 38 | 
            +
                - 'spec/**/*_spec.rb'
         | 
    
        data/Gemfile
    ADDED
    
    
    
        data/README.md
    ADDED
    
    | @@ -0,0 +1,54 @@ | |
| 1 | 
            +
            # Choria::Colt
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            This _gem_ provides:
         | 
| 4 | 
            +
             * `colt` executable, a [Bolt](https://puppet.com/docs/bolt/latest/bolt.html)-like command line interface to run _Bolt_ task through [Choria](https://puppet.com/docs/bolt/latest/bolt.html)
         | 
| 5 | 
            +
             * helpers functions and classes to run _Bolt_ tasks through _Choria_ infrastructure from any _ruby_ code
         | 
| 6 | 
            +
             | 
| 7 | 
            +
            ## Usage
         | 
| 8 | 
            +
             | 
| 9 | 
            +
            ### CLI
         | 
| 10 | 
            +
             | 
| 11 | 
            +
            ```shell-session
         | 
| 12 | 
            +
            $ colt tasks run exec=hostname -t vm012345.example.net,vm543210.example.net
         | 
| 13 | 
            +
            ```
         | 
| 14 | 
            +
             | 
| 15 | 
            +
            ### Code
         | 
| 16 | 
            +
             | 
| 17 | 
            +
            ```ruby
         | 
| 18 | 
            +
            require 'choria/colt'
         | 
| 19 | 
            +
             | 
| 20 | 
            +
            targets = [ 'vm012345.example.net' ]
         | 
| 21 | 
            +
            task_name = 'exec'
         | 
| 22 | 
            +
            task_input = { 'command' => 'hostname' }
         | 
| 23 | 
            +
             | 
| 24 | 
            +
            colt = Choria::Colt.new(logger: Logger.new($stdout))
         | 
| 25 | 
            +
            results = colt.run_bolt_task task_name, input: task_input, targets: targets
         | 
| 26 | 
            +
             | 
| 27 | 
            +
            $stdout.puts JSON.pretty_generate(results)
         | 
| 28 | 
            +
            ```
         | 
| 29 | 
            +
             | 
| 30 | 
            +
            ## Installation
         | 
| 31 | 
            +
             | 
| 32 | 
            +
            Add this line to your application's Gemfile:
         | 
| 33 | 
            +
             | 
| 34 | 
            +
            ```ruby
         | 
| 35 | 
            +
            gem 'choria-colt'
         | 
| 36 | 
            +
            ```
         | 
| 37 | 
            +
             | 
| 38 | 
            +
            And then execute:
         | 
| 39 | 
            +
             | 
| 40 | 
            +
                $ bundle install
         | 
| 41 | 
            +
             | 
| 42 | 
            +
            Or install it yourself as:
         | 
| 43 | 
            +
             | 
| 44 | 
            +
                $ gem install choria-colt
         | 
| 45 | 
            +
             | 
| 46 | 
            +
            ## Development
         | 
| 47 | 
            +
             | 
| 48 | 
            +
            After checking out the repo, run `bin/setup` to install dependencies. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
         | 
| 49 | 
            +
             | 
| 50 | 
            +
            To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
         | 
| 51 | 
            +
             | 
| 52 | 
            +
            ## Contributing
         | 
| 53 | 
            +
             | 
| 54 | 
            +
            Bug reports and pull requests are welcome on GitHub at https://github.com/opus-codium/choria-colt.
         | 
    
        data/Rakefile
    ADDED
    
    
    
        data/choria-colt.gemspec
    ADDED
    
    | @@ -0,0 +1,44 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            require_relative 'lib/choria/colt/version'
         | 
| 4 | 
            +
             | 
| 5 | 
            +
            Gem::Specification.new do |spec|
         | 
| 6 | 
            +
              spec.name = 'choria-colt'
         | 
| 7 | 
            +
              spec.version = Choria::Colt::VERSION
         | 
| 8 | 
            +
              spec.authors = ['Romuald Conty']
         | 
| 9 | 
            +
              spec.email = ['romuald@opus-codium.fr']
         | 
| 10 | 
            +
             | 
| 11 | 
            +
              spec.summary = 'Bolt-like CLI to run Bolt tasks, through Choria'
         | 
| 12 | 
            +
              spec.description = 'Colt eases the Bolt tasks run through Choria'
         | 
| 13 | 
            +
              spec.homepage = 'https://github.com/opus-codium/choria-colt'
         | 
| 14 | 
            +
              spec.required_ruby_version = '>= 2.5.0'
         | 
| 15 | 
            +
             | 
| 16 | 
            +
              spec.metadata['homepage_uri'] = spec.homepage
         | 
| 17 | 
            +
              spec.metadata['source_code_uri'] = 'https://github.com/opus-codium/choria-colt'
         | 
| 18 | 
            +
              spec.metadata['changelog_uri'] = 'https://github.com/opus-codium/choria-colt/CHANGELOG.md'
         | 
| 19 | 
            +
             | 
| 20 | 
            +
              # Specify which files should be added to the gem when it is released.
         | 
| 21 | 
            +
              # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
         | 
| 22 | 
            +
              spec.files = Dir.chdir(File.expand_path(__dir__)) do
         | 
| 23 | 
            +
                `git ls-files -z`.split("\x0").reject do |f|
         | 
| 24 | 
            +
                  (f == __FILE__) || f.match(%r{\A(?:(?:bin|test|spec|features)/|\.(?:git|travis|circleci)|appveyor)})
         | 
| 25 | 
            +
                end
         | 
| 26 | 
            +
              end
         | 
| 27 | 
            +
              spec.bindir = 'exe'
         | 
| 28 | 
            +
              spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
         | 
| 29 | 
            +
              spec.require_paths = ['lib']
         | 
| 30 | 
            +
             | 
| 31 | 
            +
              spec.add_dependency 'choria-mcorpc-support'
         | 
| 32 | 
            +
              spec.add_dependency 'deep_merge'
         | 
| 33 | 
            +
              spec.add_dependency 'puppet'
         | 
| 34 | 
            +
              spec.add_dependency 'thor'
         | 
| 35 | 
            +
             | 
| 36 | 
            +
              spec.add_development_dependency 'rake'
         | 
| 37 | 
            +
              spec.add_development_dependency 'rspec'
         | 
| 38 | 
            +
              spec.add_development_dependency 'rubocop'
         | 
| 39 | 
            +
              #spec.add_development_dependency 'byebug'
         | 
| 40 | 
            +
             | 
| 41 | 
            +
              # For more information and examples about making a new gem, check out our
         | 
| 42 | 
            +
              # guide at: https://bundler.io/guides/creating_gem.html
         | 
| 43 | 
            +
              spec.metadata['rubygems_mfa_required'] = 'true'
         | 
| 44 | 
            +
            end
         | 
    
        data/exe/colt
    ADDED
    
    
| @@ -0,0 +1,33 @@ | |
| 1 | 
            +
            require 'yaml'
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            module Choria
         | 
| 4 | 
            +
              class Colt
         | 
| 5 | 
            +
                class Cache
         | 
| 6 | 
            +
                  def initialize(path:, force_refresh: false)
         | 
| 7 | 
            +
                    @path = path
         | 
| 8 | 
            +
                    @data = YAML.safe_load File.read(@path)
         | 
| 9 | 
            +
                    @clean = true unless force_refresh
         | 
| 10 | 
            +
                  rescue Errno::ENOENT
         | 
| 11 | 
            +
                    @clean = false
         | 
| 12 | 
            +
                  end
         | 
| 13 | 
            +
             | 
| 14 | 
            +
                  def dirty?
         | 
| 15 | 
            +
                    !clean?
         | 
| 16 | 
            +
                  end
         | 
| 17 | 
            +
             | 
| 18 | 
            +
                  def clean?
         | 
| 19 | 
            +
                    @clean
         | 
| 20 | 
            +
                  end
         | 
| 21 | 
            +
             | 
| 22 | 
            +
                  def load
         | 
| 23 | 
            +
                    @data
         | 
| 24 | 
            +
                  end
         | 
| 25 | 
            +
             | 
| 26 | 
            +
                  def save(data)
         | 
| 27 | 
            +
                    @data = data
         | 
| 28 | 
            +
                    File.write @path, data.to_yaml
         | 
| 29 | 
            +
                    @clean = true
         | 
| 30 | 
            +
                  end
         | 
| 31 | 
            +
                end
         | 
| 32 | 
            +
              end
         | 
| 33 | 
            +
            end
         | 
| @@ -0,0 +1,42 @@ | |
| 1 | 
            +
            require 'thor'
         | 
| 2 | 
            +
            require 'choria/colt/cli'
         | 
| 3 | 
            +
             | 
| 4 | 
            +
            module Choria
         | 
| 5 | 
            +
              class Colt
         | 
| 6 | 
            +
                # Workaround some, still unfixed, Thor behaviors
         | 
| 7 | 
            +
                #
         | 
| 8 | 
            +
                # This class extends ::Thor class to
         | 
| 9 | 
            +
                # - exit with status code sets to `1` on Thor failure (e.g. missing required option)
         | 
| 10 | 
            +
                # - exit with status code sets to `1` when user calls `msync` (or a subcommand) without required arguments
         | 
| 11 | 
            +
                # - show subcommands help using `msync subcommand --help`
         | 
| 12 | 
            +
                class Thor < ::Thor
         | 
| 13 | 
            +
                  def self.start(*args)
         | 
| 14 | 
            +
                    if (Thor::HELP_MAPPINGS & ARGV).any? && subcommands.none? { |command| command.start_with?(ARGV[0]) }
         | 
| 15 | 
            +
                      Thor::HELP_MAPPINGS.each do |cmd|
         | 
| 16 | 
            +
                        if (match = ARGV.delete(cmd))
         | 
| 17 | 
            +
                          ARGV.unshift match
         | 
| 18 | 
            +
                        end
         | 
| 19 | 
            +
                      end
         | 
| 20 | 
            +
                    end
         | 
| 21 | 
            +
                    super
         | 
| 22 | 
            +
                  end
         | 
| 23 | 
            +
             | 
| 24 | 
            +
                  desc '_invalid_command_call', 'Invalid command', hide: true
         | 
| 25 | 
            +
                  def _invalid_command_call
         | 
| 26 | 
            +
                    self.class.new.help
         | 
| 27 | 
            +
                    exit 1
         | 
| 28 | 
            +
                  end
         | 
| 29 | 
            +
                  default_task :_invalid_command_call
         | 
| 30 | 
            +
             | 
| 31 | 
            +
                  def self.exit_on_failure?
         | 
| 32 | 
            +
                    true
         | 
| 33 | 
            +
                  end
         | 
| 34 | 
            +
             | 
| 35 | 
            +
                  def self.is_thor_reserved_word?(word, type) # rubocop:disable Naming/PredicateName
         | 
| 36 | 
            +
                    return false if word == 'run'
         | 
| 37 | 
            +
             | 
| 38 | 
            +
                    super
         | 
| 39 | 
            +
                  end
         | 
| 40 | 
            +
                end
         | 
| 41 | 
            +
              end
         | 
| 42 | 
            +
            end
         | 
| @@ -0,0 +1,109 @@ | |
| 1 | 
            +
            require 'choria/colt'
         | 
| 2 | 
            +
            require 'choria/colt/cli/thor'
         | 
| 3 | 
            +
             | 
| 4 | 
            +
            require 'json'
         | 
| 5 | 
            +
            require 'logger'
         | 
| 6 | 
            +
             | 
| 7 | 
            +
            module Choria
         | 
| 8 | 
            +
              class Colt
         | 
| 9 | 
            +
                class CLI < Thor
         | 
| 10 | 
            +
                  class Tasks < Thor
         | 
| 11 | 
            +
                    # BOLT: desc 'run <task name> [parameters] {--targets TARGETS | --query QUERY | --rerun FILTER} [options]', 'Run a Bolt task'
         | 
| 12 | 
            +
                    desc 'run <task name> [parameters] --targets TARGETS [options]', 'Run a Bolt task'
         | 
| 13 | 
            +
                    long_desc <<~DESC
         | 
| 14 | 
            +
                      Run a task on the specified targets.
         | 
| 15 | 
            +
             | 
| 16 | 
            +
                      Parameters take the form parameter=value.
         | 
| 17 | 
            +
                    DESC
         | 
| 18 | 
            +
                    option :targets,
         | 
| 19 | 
            +
                           aliases: ['--target', '-t'],
         | 
| 20 | 
            +
                           desc: 'Identifies the targets of the command.',
         | 
| 21 | 
            +
                           required: true
         | 
| 22 | 
            +
                    def run(*args)
         | 
| 23 | 
            +
                      input = extract_task_parameters_from_args(args)
         | 
| 24 | 
            +
             | 
| 25 | 
            +
                      raise Thor::Error, 'Task name is required' if args.empty?
         | 
| 26 | 
            +
                      raise Thor::Error, "Too many arguments: #{args}" unless args.count == 1
         | 
| 27 | 
            +
             | 
| 28 | 
            +
                      task_name = args.shift
         | 
| 29 | 
            +
             | 
| 30 | 
            +
                      targets = options['targets'].split ','
         | 
| 31 | 
            +
                      targets = nil if options['targets'] == 'all'
         | 
| 32 | 
            +
             | 
| 33 | 
            +
                      results = colt.run_bolt_task task_name, input: input, targets: targets
         | 
| 34 | 
            +
                      $stdout.puts JSON.pretty_generate(results)
         | 
| 35 | 
            +
                    rescue Choria::Orchestrator::Error => e
         | 
| 36 | 
            +
                      raise Thor::Error, "#{e.class}: #{e}"
         | 
| 37 | 
            +
                    end
         | 
| 38 | 
            +
             | 
| 39 | 
            +
                    desc 'show [task name] [options]', 'Show available tasks and task documentation'
         | 
| 40 | 
            +
                    long_desc <<~DESC
         | 
| 41 | 
            +
                      Show available tasks and task documentation.
         | 
| 42 | 
            +
             | 
| 43 | 
            +
                      Omitting the name of a task will display a list of tasks available
         | 
| 44 | 
            +
                      in the Bolt project.
         | 
| 45 | 
            +
             | 
| 46 | 
            +
                      Providing the name of a task will display detailed documentation for
         | 
| 47 | 
            +
                      the task, including a list of available parameters.
         | 
| 48 | 
            +
                    DESC
         | 
| 49 | 
            +
                    option :environment,
         | 
| 50 | 
            +
                           aliases: ['-E'],
         | 
| 51 | 
            +
                           desc: 'Puppet environment to grab tasks from',
         | 
| 52 | 
            +
                           default: 'production'
         | 
| 53 | 
            +
                    def show(*tasks_names)
         | 
| 54 | 
            +
                      environment = options['environment']
         | 
| 55 | 
            +
                      cache_directory = File.expand_path('.cache/colt/tasks')
         | 
| 56 | 
            +
                      FileUtils.mkdir_p cache_directory
         | 
| 57 | 
            +
                      # TODO: Support multiple infrastructure
         | 
| 58 | 
            +
                      cache = Cache.new(path: File.join(cache_directory, "#{environment}.yaml"))
         | 
| 59 | 
            +
             | 
| 60 | 
            +
                      tasks = colt.tasks(environment: environment, cache: cache)
         | 
| 61 | 
            +
             | 
| 62 | 
            +
                      if tasks_names.nil?
         | 
| 63 | 
            +
                        show_tasks_summary
         | 
| 64 | 
            +
                      else
         | 
| 65 | 
            +
                        tasks_names.each { |task_name| show_task_details(task_name, tasks) }
         | 
| 66 | 
            +
                      end
         | 
| 67 | 
            +
                    end
         | 
| 68 | 
            +
             | 
| 69 | 
            +
                    no_commands do
         | 
| 70 | 
            +
                      def colt
         | 
| 71 | 
            +
                        @colt ||= Choria::Colt.new logger: Logger.new($stdout)
         | 
| 72 | 
            +
                      end
         | 
| 73 | 
            +
             | 
| 74 | 
            +
                      def extract_task_parameters_from_args(args)
         | 
| 75 | 
            +
                        parameters = args.grep(/^\w+=/)
         | 
| 76 | 
            +
                        args.reject! { |arg| arg =~ /^\w+=/ }
         | 
| 77 | 
            +
             | 
| 78 | 
            +
                        parameters.map do |parameter|
         | 
| 79 | 
            +
                          key, value = parameter.split('=')
         | 
| 80 | 
            +
                          [key, value]
         | 
| 81 | 
            +
                        end.to_h
         | 
| 82 | 
            +
                      end
         | 
| 83 | 
            +
             | 
| 84 | 
            +
                      def show_tasks_summary(tasks)
         | 
| 85 | 
            +
                        tasks.reject! { |_task, metadata| metadata['metadata']['private'] }
         | 
| 86 | 
            +
             | 
| 87 | 
            +
                        puts <<~OUTPUT
         | 
| 88 | 
            +
                          Tasks
         | 
| 89 | 
            +
                          #{tasks.map { |task, metadata| "#{task}#{' ' * (60 - task.size)}#{metadata['metadata']['description']}" }.join("\n").gsub(/^/, '  ')}
         | 
| 90 | 
            +
                        OUTPUT
         | 
| 91 | 
            +
                      end
         | 
| 92 | 
            +
             | 
| 93 | 
            +
                      def show_task_details(task_name, tasks)
         | 
| 94 | 
            +
                        metadata = tasks[task_name]
         | 
| 95 | 
            +
                        puts <<~OUTPUT
         | 
| 96 | 
            +
                          Task: '#{task_name}'
         | 
| 97 | 
            +
                            #{metadata['metadata']['description']}
         | 
| 98 | 
            +
             | 
| 99 | 
            +
                            #{metadata}
         | 
| 100 | 
            +
                        OUTPUT
         | 
| 101 | 
            +
                      end
         | 
| 102 | 
            +
                    end
         | 
| 103 | 
            +
                  end
         | 
| 104 | 
            +
             | 
| 105 | 
            +
                  desc 'tasks', 'Show and run Bolt tasks.'
         | 
| 106 | 
            +
                  subcommand 'tasks', CLI::Tasks
         | 
| 107 | 
            +
                end
         | 
| 108 | 
            +
              end
         | 
| 109 | 
            +
            end
         | 
    
        data/lib/choria/colt.rb
    ADDED
    
    | @@ -0,0 +1,58 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            require 'choria/colt/cache'
         | 
| 4 | 
            +
            require 'choria/colt/version'
         | 
| 5 | 
            +
            require 'choria/orchestrator'
         | 
| 6 | 
            +
            require 'choria/orchestrator/task'
         | 
| 7 | 
            +
             | 
| 8 | 
            +
            require 'logger'
         | 
| 9 | 
            +
             | 
| 10 | 
            +
            module Choria
         | 
| 11 | 
            +
              class Colt
         | 
| 12 | 
            +
                class Error < StandardError; end
         | 
| 13 | 
            +
             | 
| 14 | 
            +
                attr_reader :logger, :orchestrator
         | 
| 15 | 
            +
             | 
| 16 | 
            +
                def initialize(logger: nil)
         | 
| 17 | 
            +
                  @logger = logger
         | 
| 18 | 
            +
             | 
| 19 | 
            +
                  @orchestrator = Choria::Orchestrator.new logger: @logger
         | 
| 20 | 
            +
                end
         | 
| 21 | 
            +
             | 
| 22 | 
            +
                def run_bolt_task(task_name, input: {}, targets: nil)
         | 
| 23 | 
            +
                  logger.debug "Instantiate task '#{task_name}' and validate input"
         | 
| 24 | 
            +
                  task = Choria::Orchestrator::Task.new(task_name, input: input, orchestrator: orchestrator)
         | 
| 25 | 
            +
             | 
| 26 | 
            +
                  orchestrator.run(task, targets: targets)
         | 
| 27 | 
            +
                  task.wait
         | 
| 28 | 
            +
                  task.results
         | 
| 29 | 
            +
                rescue Choria::Orchestrator::Error => e
         | 
| 30 | 
            +
                  logger.error e.message
         | 
| 31 | 
            +
                  raise
         | 
| 32 | 
            +
                end
         | 
| 33 | 
            +
             | 
| 34 | 
            +
                def tasks(environment:, cache: nil)
         | 
| 35 | 
            +
                  tasks_names = orchestrator.tasks_support.tasks(environment).map do |task|
         | 
| 36 | 
            +
                    task['name']
         | 
| 37 | 
            +
                  end
         | 
| 38 | 
            +
             | 
| 39 | 
            +
                  def tasks_metadata(tasks, environment)
         | 
| 40 | 
            +
                    tasks.map do |task|
         | 
| 41 | 
            +
                      logger.debug "Fetching metadata for task '#{task}' (environment: '#{environment}')"
         | 
| 42 | 
            +
                      metadata = orchestrator.tasks_support.task_metadata(task, environment)
         | 
| 43 | 
            +
                      [task, metadata]
         | 
| 44 | 
            +
                    end.to_h
         | 
| 45 | 
            +
                  end
         | 
| 46 | 
            +
             | 
| 47 | 
            +
                  return tasks_metadata(tasks_names, environment) if cache.nil?
         | 
| 48 | 
            +
             | 
| 49 | 
            +
                  cached_tasks = cache.load
         | 
| 50 | 
            +
                  return cached_tasks if cache.clean? && cached_tasks.keys.sort == tasks_names.sort
         | 
| 51 | 
            +
             | 
| 52 | 
            +
                  updated_tasks = tasks_metadata(tasks_names, environment)
         | 
| 53 | 
            +
                  cache.save updated_tasks
         | 
| 54 | 
            +
             | 
| 55 | 
            +
                  updated_tasks
         | 
| 56 | 
            +
                end
         | 
| 57 | 
            +
              end
         | 
| 58 | 
            +
            end
         | 
| @@ -0,0 +1,59 @@ | |
| 1 | 
            +
            module Choria
         | 
| 2 | 
            +
              class Orchestrator
         | 
| 3 | 
            +
                class Task
         | 
| 4 | 
            +
                  class Error < Orchestrator::Error; end
         | 
| 5 | 
            +
             | 
| 6 | 
            +
                  attr_reader :name, :input, :environment, :rpc_results
         | 
| 7 | 
            +
                  attr_accessor :rpc_responses
         | 
| 8 | 
            +
             | 
| 9 | 
            +
                  def initialize(name, orchestrator:, input: {}, environment: 'production')
         | 
| 10 | 
            +
                    @name = name
         | 
| 11 | 
            +
                    @input = input
         | 
| 12 | 
            +
                    @environment = environment
         | 
| 13 | 
            +
                    @orchestrator = orchestrator
         | 
| 14 | 
            +
             | 
| 15 | 
            +
                    validate_inputs
         | 
| 16 | 
            +
                  end
         | 
| 17 | 
            +
             | 
| 18 | 
            +
                  def metadata
         | 
| 19 | 
            +
                    @metadata ||= _metadata
         | 
| 20 | 
            +
                  end
         | 
| 21 | 
            +
             | 
| 22 | 
            +
                  def files
         | 
| 23 | 
            +
                    metadata['files'].to_json
         | 
| 24 | 
            +
                  end
         | 
| 25 | 
            +
             | 
| 26 | 
            +
                  def wait
         | 
| 27 | 
            +
                    task_ids = rpc_responses.map { |res| res[:body][:data][:task_id] }.uniq
         | 
| 28 | 
            +
                    raise NotImplementedError, "Multiple task IDs: #{task_ids}" unless task_ids.count == 1
         | 
| 29 | 
            +
             | 
| 30 | 
            +
                    @rpc_results = @orchestrator.wait_results task_id: task_ids.first
         | 
| 31 | 
            +
                  end
         | 
| 32 | 
            +
             | 
| 33 | 
            +
                  def results
         | 
| 34 | 
            +
                    @rpc_results.map do |res|
         | 
| 35 | 
            +
                      raise NotImplementedError, 'What to do when res[:data][:stderr] contains something?' unless res[:data][:stderr].empty?
         | 
| 36 | 
            +
             | 
| 37 | 
            +
                      res[:result] = JSON.parse res[:data][:stdout]
         | 
| 38 | 
            +
                      res[:data].delete :stderr
         | 
| 39 | 
            +
                      res[:data].delete :stdout
         | 
| 40 | 
            +
                      res
         | 
| 41 | 
            +
                    end
         | 
| 42 | 
            +
                  end
         | 
| 43 | 
            +
             | 
| 44 | 
            +
                  private
         | 
| 45 | 
            +
             | 
| 46 | 
            +
                  def _metadata
         | 
| 47 | 
            +
                    # puts 'Retrieving task metadata for task %s from the Puppet Server' % task if verbose
         | 
| 48 | 
            +
                    @orchestrator.tasks_support.task_metadata(@name, @environment)
         | 
| 49 | 
            +
                  rescue RuntimeError => e
         | 
| 50 | 
            +
                    raise Error, e.message
         | 
| 51 | 
            +
                  end
         | 
| 52 | 
            +
             | 
| 53 | 
            +
                  def validate_inputs
         | 
| 54 | 
            +
                    ok, reason = @orchestrator.tasks_support.validate_task_inputs(@input, metadata)
         | 
| 55 | 
            +
                    raise Error, reason.sub(/^\n/, '') unless ok
         | 
| 56 | 
            +
                  end
         | 
| 57 | 
            +
                end
         | 
| 58 | 
            +
              end
         | 
| 59 | 
            +
            end
         | 
| @@ -0,0 +1,99 @@ | |
| 1 | 
            +
            require 'English'
         | 
| 2 | 
            +
            require 'mcollective'
         | 
| 3 | 
            +
             | 
| 4 | 
            +
            module Choria
         | 
| 5 | 
            +
              class Orchestrator
         | 
| 6 | 
            +
                class Error < StandardError; end
         | 
| 7 | 
            +
                class DiscoverError < Error; end
         | 
| 8 | 
            +
             | 
| 9 | 
            +
                include MCollective::RPC
         | 
| 10 | 
            +
             | 
| 11 | 
            +
                attr_reader :logger
         | 
| 12 | 
            +
             | 
| 13 | 
            +
                def initialize(logger:)
         | 
| 14 | 
            +
                  @logger = logger
         | 
| 15 | 
            +
             | 
| 16 | 
            +
                  configfile ||= MCollective::Util.config_file_for_user
         | 
| 17 | 
            +
                  MCollective::Config.instance.loadconfig(configfile)
         | 
| 18 | 
            +
                end
         | 
| 19 | 
            +
             | 
| 20 | 
            +
                def tasks_support
         | 
| 21 | 
            +
                  @tasks_support ||= MCollective::Util::Choria.new.tasks_support
         | 
| 22 | 
            +
                end
         | 
| 23 | 
            +
             | 
| 24 | 
            +
                def run(task, targets: nil, verbose: false)
         | 
| 25 | 
            +
                  logger.debug "Running task: '#{task.name}' (targets: #{targets.nil? ? 'all' : targets})"
         | 
| 26 | 
            +
                  rpc_client.progress = verbose
         | 
| 27 | 
            +
             | 
| 28 | 
            +
                  targets&.each { |target| rpc_client.identity_filter target }
         | 
| 29 | 
            +
             | 
| 30 | 
            +
                  raise DiscoverError, 'No request sent, no node discovered' if rpc_client.discover.size.zero?
         | 
| 31 | 
            +
             | 
| 32 | 
            +
                  logger.info "Attempting to download and run task '#{task.name}' on #{rpc_client.discover.size} nodes"
         | 
| 33 | 
            +
             | 
| 34 | 
            +
                  rpc_client.download(task: task.name, files: task.files, verbose: verbose)
         | 
| 35 | 
            +
             | 
| 36 | 
            +
                  # TODO: Extract error from 'rpc' (see MCollective::RPC#printrpc)
         | 
| 37 | 
            +
             | 
| 38 | 
            +
                  responses = []
         | 
| 39 | 
            +
                  rpc_client.run_no_wait(task: task.name, files: task.files, input: task.input.to_json, verbose: verbose) do |response|
         | 
| 40 | 
            +
                    logger.debug "  Response: '#{response}'"
         | 
| 41 | 
            +
                    responses << response
         | 
| 42 | 
            +
                  end
         | 
| 43 | 
            +
             | 
| 44 | 
            +
                  # TODO: Include stats in logs when logger will be available (see MCollective::RPC#printrpcstats)
         | 
| 45 | 
            +
             | 
| 46 | 
            +
                  task.rpc_responses = responses
         | 
| 47 | 
            +
                end
         | 
| 48 | 
            +
             | 
| 49 | 
            +
                def wait_results(task_id:)
         | 
| 50 | 
            +
                  raise 'Task ID is required!' if task_id.nil?
         | 
| 51 | 
            +
             | 
| 52 | 
            +
                  loop do
         | 
| 53 | 
            +
                    task_status_results = rpc_client.task_status(task_id: task_id).map(&:results)
         | 
| 54 | 
            +
                    logger.debug "Task ##{task_id} status: #{task_status_results}"
         | 
| 55 | 
            +
                    break if task_completed? task_status_results
         | 
| 56 | 
            +
                  end
         | 
| 57 | 
            +
             | 
| 58 | 
            +
                  task_status_results
         | 
| 59 | 
            +
                end
         | 
| 60 | 
            +
             | 
| 61 | 
            +
                def task_completed?(results)
         | 
| 62 | 
            +
                  results.each do |result|
         | 
| 63 | 
            +
                    return false unless result[:data][:completed]
         | 
| 64 | 
            +
                  end
         | 
| 65 | 
            +
             | 
| 66 | 
            +
                  true
         | 
| 67 | 
            +
                end
         | 
| 68 | 
            +
             | 
| 69 | 
            +
                def validate_rpc_result(result)
         | 
| 70 | 
            +
                  raise Error, "The RPC agent returned an error: #{result[:statusmsg]}" unless (result[:statuscode]).zero?
         | 
| 71 | 
            +
                end
         | 
| 72 | 
            +
             | 
| 73 | 
            +
                private
         | 
| 74 | 
            +
             | 
| 75 | 
            +
                def rpc_client
         | 
| 76 | 
            +
                  @rpc_client ||= rpcclient('bolt_tasks', options: rpc_options)
         | 
| 77 | 
            +
                end
         | 
| 78 | 
            +
             | 
| 79 | 
            +
                def rpc_options
         | 
| 80 | 
            +
                  {
         | 
| 81 | 
            +
                    verbose: false,
         | 
| 82 | 
            +
                    disctimeout: nil,
         | 
| 83 | 
            +
                    timeout: 5,
         | 
| 84 | 
            +
                    config: '/etc/choria/client.conf',
         | 
| 85 | 
            +
                    collective: 'mcollective',
         | 
| 86 | 
            +
                    discovery_method: nil,
         | 
| 87 | 
            +
                    discovery_options: [],
         | 
| 88 | 
            +
                    filter: {
         | 
| 89 | 
            +
                      'fact' => [], 'cf_class' => [], 'agent' => [], 'identity' => [], 'compound' => []
         | 
| 90 | 
            +
                    },
         | 
| 91 | 
            +
                    progress_bar: false,
         | 
| 92 | 
            +
                    mcollective_limit_targets: false,
         | 
| 93 | 
            +
                    batch_size: nil,
         | 
| 94 | 
            +
                    batch_sleep_time: 1,
         | 
| 95 | 
            +
                    output_format: :json,
         | 
| 96 | 
            +
                  }
         | 
| 97 | 
            +
                end
         | 
| 98 | 
            +
              end
         | 
| 99 | 
            +
            end
         | 
    
        data/sig/choria/colt.rbs
    ADDED
    
    
    
        metadata
    ADDED
    
    | @@ -0,0 +1,159 @@ | |
| 1 | 
            +
            --- !ruby/object:Gem::Specification
         | 
| 2 | 
            +
            name: choria-colt
         | 
| 3 | 
            +
            version: !ruby/object:Gem::Version
         | 
| 4 | 
            +
              version: 0.1.0
         | 
| 5 | 
            +
            platform: ruby
         | 
| 6 | 
            +
            authors:
         | 
| 7 | 
            +
            - Romuald Conty
         | 
| 8 | 
            +
            autorequire: 
         | 
| 9 | 
            +
            bindir: exe
         | 
| 10 | 
            +
            cert_chain: []
         | 
| 11 | 
            +
            date: 2022-03-02 00:00:00.000000000 Z
         | 
| 12 | 
            +
            dependencies:
         | 
| 13 | 
            +
            - !ruby/object:Gem::Dependency
         | 
| 14 | 
            +
              name: choria-mcorpc-support
         | 
| 15 | 
            +
              requirement: !ruby/object:Gem::Requirement
         | 
| 16 | 
            +
                requirements:
         | 
| 17 | 
            +
                - - ">="
         | 
| 18 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 19 | 
            +
                    version: '0'
         | 
| 20 | 
            +
              type: :runtime
         | 
| 21 | 
            +
              prerelease: false
         | 
| 22 | 
            +
              version_requirements: !ruby/object:Gem::Requirement
         | 
| 23 | 
            +
                requirements:
         | 
| 24 | 
            +
                - - ">="
         | 
| 25 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 26 | 
            +
                    version: '0'
         | 
| 27 | 
            +
            - !ruby/object:Gem::Dependency
         | 
| 28 | 
            +
              name: deep_merge
         | 
| 29 | 
            +
              requirement: !ruby/object:Gem::Requirement
         | 
| 30 | 
            +
                requirements:
         | 
| 31 | 
            +
                - - ">="
         | 
| 32 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 33 | 
            +
                    version: '0'
         | 
| 34 | 
            +
              type: :runtime
         | 
| 35 | 
            +
              prerelease: false
         | 
| 36 | 
            +
              version_requirements: !ruby/object:Gem::Requirement
         | 
| 37 | 
            +
                requirements:
         | 
| 38 | 
            +
                - - ">="
         | 
| 39 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 40 | 
            +
                    version: '0'
         | 
| 41 | 
            +
            - !ruby/object:Gem::Dependency
         | 
| 42 | 
            +
              name: puppet
         | 
| 43 | 
            +
              requirement: !ruby/object:Gem::Requirement
         | 
| 44 | 
            +
                requirements:
         | 
| 45 | 
            +
                - - ">="
         | 
| 46 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 47 | 
            +
                    version: '0'
         | 
| 48 | 
            +
              type: :runtime
         | 
| 49 | 
            +
              prerelease: false
         | 
| 50 | 
            +
              version_requirements: !ruby/object:Gem::Requirement
         | 
| 51 | 
            +
                requirements:
         | 
| 52 | 
            +
                - - ">="
         | 
| 53 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 54 | 
            +
                    version: '0'
         | 
| 55 | 
            +
            - !ruby/object:Gem::Dependency
         | 
| 56 | 
            +
              name: thor
         | 
| 57 | 
            +
              requirement: !ruby/object:Gem::Requirement
         | 
| 58 | 
            +
                requirements:
         | 
| 59 | 
            +
                - - ">="
         | 
| 60 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 61 | 
            +
                    version: '0'
         | 
| 62 | 
            +
              type: :runtime
         | 
| 63 | 
            +
              prerelease: false
         | 
| 64 | 
            +
              version_requirements: !ruby/object:Gem::Requirement
         | 
| 65 | 
            +
                requirements:
         | 
| 66 | 
            +
                - - ">="
         | 
| 67 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 68 | 
            +
                    version: '0'
         | 
| 69 | 
            +
            - !ruby/object:Gem::Dependency
         | 
| 70 | 
            +
              name: rake
         | 
| 71 | 
            +
              requirement: !ruby/object:Gem::Requirement
         | 
| 72 | 
            +
                requirements:
         | 
| 73 | 
            +
                - - ">="
         | 
| 74 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 75 | 
            +
                    version: '0'
         | 
| 76 | 
            +
              type: :development
         | 
| 77 | 
            +
              prerelease: false
         | 
| 78 | 
            +
              version_requirements: !ruby/object:Gem::Requirement
         | 
| 79 | 
            +
                requirements:
         | 
| 80 | 
            +
                - - ">="
         | 
| 81 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 82 | 
            +
                    version: '0'
         | 
| 83 | 
            +
            - !ruby/object:Gem::Dependency
         | 
| 84 | 
            +
              name: rspec
         | 
| 85 | 
            +
              requirement: !ruby/object:Gem::Requirement
         | 
| 86 | 
            +
                requirements:
         | 
| 87 | 
            +
                - - ">="
         | 
| 88 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 89 | 
            +
                    version: '0'
         | 
| 90 | 
            +
              type: :development
         | 
| 91 | 
            +
              prerelease: false
         | 
| 92 | 
            +
              version_requirements: !ruby/object:Gem::Requirement
         | 
| 93 | 
            +
                requirements:
         | 
| 94 | 
            +
                - - ">="
         | 
| 95 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 96 | 
            +
                    version: '0'
         | 
| 97 | 
            +
            - !ruby/object:Gem::Dependency
         | 
| 98 | 
            +
              name: rubocop
         | 
| 99 | 
            +
              requirement: !ruby/object:Gem::Requirement
         | 
| 100 | 
            +
                requirements:
         | 
| 101 | 
            +
                - - ">="
         | 
| 102 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 103 | 
            +
                    version: '0'
         | 
| 104 | 
            +
              type: :development
         | 
| 105 | 
            +
              prerelease: false
         | 
| 106 | 
            +
              version_requirements: !ruby/object:Gem::Requirement
         | 
| 107 | 
            +
                requirements:
         | 
| 108 | 
            +
                - - ">="
         | 
| 109 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 110 | 
            +
                    version: '0'
         | 
| 111 | 
            +
            description: Colt eases the Bolt tasks run through Choria
         | 
| 112 | 
            +
            email:
         | 
| 113 | 
            +
            - romuald@opus-codium.fr
         | 
| 114 | 
            +
            executables:
         | 
| 115 | 
            +
            - colt
         | 
| 116 | 
            +
            extensions: []
         | 
| 117 | 
            +
            extra_rdoc_files: []
         | 
| 118 | 
            +
            files:
         | 
| 119 | 
            +
            - ".rubocop.yml"
         | 
| 120 | 
            +
            - Gemfile
         | 
| 121 | 
            +
            - README.md
         | 
| 122 | 
            +
            - Rakefile
         | 
| 123 | 
            +
            - choria-colt.gemspec
         | 
| 124 | 
            +
            - exe/colt
         | 
| 125 | 
            +
            - lib/choria/colt.rb
         | 
| 126 | 
            +
            - lib/choria/colt/cache.rb
         | 
| 127 | 
            +
            - lib/choria/colt/cli.rb
         | 
| 128 | 
            +
            - lib/choria/colt/cli/thor.rb
         | 
| 129 | 
            +
            - lib/choria/colt/version.rb
         | 
| 130 | 
            +
            - lib/choria/orchestrator.rb
         | 
| 131 | 
            +
            - lib/choria/orchestrator/task.rb
         | 
| 132 | 
            +
            - sig/choria/colt.rbs
         | 
| 133 | 
            +
            homepage: https://github.com/opus-codium/choria-colt
         | 
| 134 | 
            +
            licenses: []
         | 
| 135 | 
            +
            metadata:
         | 
| 136 | 
            +
              homepage_uri: https://github.com/opus-codium/choria-colt
         | 
| 137 | 
            +
              source_code_uri: https://github.com/opus-codium/choria-colt
         | 
| 138 | 
            +
              changelog_uri: https://github.com/opus-codium/choria-colt/CHANGELOG.md
         | 
| 139 | 
            +
              rubygems_mfa_required: 'true'
         | 
| 140 | 
            +
            post_install_message: 
         | 
| 141 | 
            +
            rdoc_options: []
         | 
| 142 | 
            +
            require_paths:
         | 
| 143 | 
            +
            - lib
         | 
| 144 | 
            +
            required_ruby_version: !ruby/object:Gem::Requirement
         | 
| 145 | 
            +
              requirements:
         | 
| 146 | 
            +
              - - ">="
         | 
| 147 | 
            +
                - !ruby/object:Gem::Version
         | 
| 148 | 
            +
                  version: 2.5.0
         | 
| 149 | 
            +
            required_rubygems_version: !ruby/object:Gem::Requirement
         | 
| 150 | 
            +
              requirements:
         | 
| 151 | 
            +
              - - ">="
         | 
| 152 | 
            +
                - !ruby/object:Gem::Version
         | 
| 153 | 
            +
                  version: '0'
         | 
| 154 | 
            +
            requirements: []
         | 
| 155 | 
            +
            rubygems_version: 3.1.2
         | 
| 156 | 
            +
            signing_key: 
         | 
| 157 | 
            +
            specification_version: 4
         | 
| 158 | 
            +
            summary: Bolt-like CLI to run Bolt tasks, through Choria
         | 
| 159 | 
            +
            test_files: []
         |