# frozen_string_literal: true # Note this file includes very few 'requires' because it expects to be used from the CLI. require 'optparse' module Bolt class BoltOptionParser < OptionParser OPTIONS = { inventory: %w[nodes targets query rerun description], authentication: %w[user password private-key host-key-check ssl ssl-verify], escalation: %w[run-as sudo-password], run_context: %w[concurrency inventoryfile save-rerun], global_config_setters: %w[modulepath boltdir configfile], transports: %w[transport connect-timeout tty], display: %w[format color verbose trace], global: %w[help version debug] }.freeze ACTION_OPTS = OPTIONS.values.flatten.freeze def get_help_text(subcommand, action = nil) case subcommand when 'apply' { flags: ACTION_OPTS + %w[noop execute compile-concurrency], banner: APPLY_HELP } when 'command' { flags: ACTION_OPTS, banner: COMMAND_HELP } when 'file' { flags: ACTION_OPTS + %w[tmpdir], banner: FILE_HELP } when 'inventory' { flags: OPTIONS[:inventory] + OPTIONS[:global] + %w[format inventoryfile boltdir configfile], banner: INVENTORY_HELP } when 'group' { flags: OPTIONS[:global] + %w[format inventoryfile boltdir configfile], banner: GROUP_HELP } when 'plan' case action when 'convert' { flags: OPTIONS[:global] + OPTIONS[:global_config_setters], banner: PLAN_CONVERT_HELP } when 'show' { flags: OPTIONS[:global] + OPTIONS[:global_config_setters], banner: PLAN_SHOW_HELP } when 'run' { flags: ACTION_OPTS + %w[params compile-concurrency tmpdir], banner: PLAN_RUN_HELP } else { flags: ACTION_OPTS + %w[params compile-concurrency tmpdir], banner: PLAN_HELP } end when 'puppetfile' case action when 'install' { flags: OPTIONS[:global] + OPTIONS[:global_config_setters], banner: PUPPETFILE_INSTALL_HELP } when 'show-modules' { flags: OPTIONS[:global] + OPTIONS[:global_config_setters], banner: PUPPETFILE_SHOWMODULES_HELP } when 'generate-types' { flags: OPTIONS[:global] + OPTIONS[:global_config_setters], banner: PUPPETFILE_GENERATETYPES_HELP } else { flags: OPTIONS[:global] + OPTIONS[:global_config_setters], banner: PUPPETFILE_HELP } end when 'script' { flags: ACTION_OPTS + %w[tmpdir], banner: SCRIPT_HELP } when 'secret' { flags: OPTIONS[:global] + OPTIONS[:global_config_setters] + %w[plugin], banner: SECRET_HELP } when 'task' case action when 'show' { flags: OPTIONS[:global] + OPTIONS[:global_config_setters], banner: TASK_SHOW_HELP } when 'run' { flags: ACTION_OPTS + %w[params tmpdir], banner: TASK_RUN_HELP } else { flags: ACTION_OPTS + %w[params tmpdir], banner: TASK_HELP } end else { flags: OPTIONS[:global], banner: BANNER } end end def self.examples(cmd, desc) <<~EXAMP #{desc} a Windows host via WinRM, providing for the password bolt #{cmd} -n winrm://winhost -u Administrator -p #{desc} the local machine, a Linux host via SSH, and hosts from a group specified in an inventory file bolt #{cmd} -n localhost,nixhost,node_group #{desc} Windows hosts queried from PuppetDB via WinRM as a domain user, prompting for the password bolt #{cmd} -q 'inventory[certname] { facts.os.family = "windows" }' --transport winrm -u 'domain\\Administrator' -p EXAMP end BANNER = <<~HELP Usage: bolt <subcommand> <action> Available subcommands: bolt command run <command> Run a command remotely bolt file upload <src> <dest> Upload a local file or directory bolt script run <script> Upload a local script and run it remotely bolt task show Show list of available tasks bolt task show <task> Show documentation for task bolt task run <task> [params] Run a Puppet task bolt plan convert <plan_path> Convert a YAML plan to a Puppet plan bolt plan show Show list of available plans bolt plan show <plan> Show details for plan bolt plan run <plan> [params] Run a Puppet task plan bolt apply <manifest> Apply Puppet manifest code bolt puppetfile install Install modules from a Puppetfile into a Boltdir bolt puppetfile show-modules List modules available to Bolt bolt secret createkeys Create new encryption keys bolt secret encrypt <plaintext> Encrypt a value bolt secret decrypt <encrypted> Decrypt a value bolt inventory show Show the list of targets an action would run on bolt group show Show the list of groups in the inventory Run `bolt <subcommand> --help` to view specific examples. Available options are: HELP TASK_HELP = <<~HELP Usage: bolt task <action> <task> [parameters] Available actions are: show Show list of available tasks show <task> Show documentation for task run <task> Run a Puppet task Parameters are of the form <parameter>=<value>. #{examples('task run facts', 'run facter on')} Available options are: HELP TASK_SHOW_HELP = <<~HELP Usage: bolt task show <task> Available actions are: show Show list of available tasks show <task> Show documentation for task Available options are: HELP TASK_RUN_HELP = <<~HELP Usage: bolt task run <task> [parameters] Parameters are of the form <parameter>=<value>. #{examples('task run facts', 'run facter on')} Available options are: HELP COMMAND_HELP = <<~HELP Usage: bolt command <action> <command> Available actions are: run Run a command remotely #{examples('command run hostname', 'run hostname on')} Available options are: HELP SCRIPT_HELP = <<~HELP Usage: bolt script <action> <script> [[arg1] ... [argN]] Available actions are: run Upload a local script and run it remotely #{examples('script run my_script.ps1 some args', 'run a script on')} Available options are: HELP PLAN_HELP = <<~HELP Usage: bolt plan <action> <plan> [parameters] Available actions are: convert <plan_path> Convert a YAML plan to a Puppet plan show Show list of available plans show <plan> Show details for plan run Run a Puppet task plan Parameters are of the form <parameter>=<value>. #{examples('plan run canary command=hostname', 'run the canary plan on')} Available options are: HELP PLAN_CONVERT_HELP = <<~HELP Usage: bolt plan convert <plan_path> Available options are: HELP PLAN_SHOW_HELP = <<~HELP Usage: bolt plan show <plan> Available actions are: show Show list of available plans show <plan> Show details for plan Available options are: HELP PLAN_RUN_HELP = <<~HELP Usage: bolt plan run <plan> [parameters] Parameters are of the form <parameter>=<value>. #{examples('plan run canary command=hostname', 'run the canary plan on')} Available options are: HELP FILE_HELP = <<~HELP Usage: bolt file <action> Available actions are: upload <src> <dest> Upload local file or directory <src> to <dest> on each node #{examples('file upload /tmp/source /etc/profile.d/login.sh', 'upload a file to')} Available options are: HELP PUPPETFILE_HELP = <<~HELP Usage: bolt puppetfile <action> Available actions are: install Install modules from a Puppetfile into a Boltdir show-modules List modules available to Bolt generate-types Generate type references to register in Plans Install modules into the local Boltdir bolt puppetfile install Available options are: HELP PUPPETFILE_INSTALL_HELP = <<~HELP Usage: bolt puppetfile install Install modules into the local Boltdir bolt puppetfile install Available options are: HELP PUPPETFILE_SHOWMODULES_HELP = <<~HELP Usage: bolt puppetfile show-modules List modules available to Bolt bolt puppetfile show-modules Available options are: HELP PUPPETFILE_GENERATETYPES_HELP = <<~HELP Usage: bolt puppetfile generate-types Generate type references to register in Plans bolt puppetfile generate-types Available options are: HELP APPLY_HELP = <<~HELP Usage: bolt apply <manifest.pp> #{examples('apply site.pp', 'apply a manifest on')} bolt apply site.pp --nodes foo.example.com,bar.example.com Available options are: HELP SECRET_HELP = <<~SECRET_HELP Usage: bolt secret <action> <value> Manage secrets for inventory and hiera data. Available actions are: createkeys Create new encryption keys encrypt Encrypt a value decrypt Decrypt a value Available options are: SECRET_HELP INVENTORY_HELP = <<~INVENTORY_HELP Usage: bolt inventory <action> Available actions are: show Show the list of targets an action would run on Available options are: INVENTORY_HELP GROUP_HELP = <<~GROUP_HELP Usage: bolt group <action> Available actions are: show Show the list of groups in the inventory Available options are: GROUP_HELP def initialize(options) super() @options = options define('-n', '--nodes NODES', 'Alias for --targets') do |nodes| @options [:nodes] ||= [] @options[:nodes] << get_arg_input(nodes) end define('-t', '--targets TARGETS', 'Identifies the targets of command.', 'Enter a comma-separated list of target URIs or group names.', "Or read a target list from an input file '@<file>' or stdin '-'.", 'Example: --targets localhost,node_group,ssh://nix.com:23,winrm://windows.puppet.com', 'URI format is [protocol://]host[:port]', "SSH is the default protocol; may be #{TRANSPORTS.keys.join(', ')}", 'For Windows targets, specify the winrm:// protocol if it has not be configured', 'For SSH, port defaults to `22`', 'For WinRM, port defaults to `5985` or `5986` based on the --[no-]ssl setting') do |targets| @options[:targets] ||= [] @options[:targets] << get_arg_input(targets) end define('-q', '--query QUERY', 'Query PuppetDB to determine the targets') do |query| @options[:query] = query end define('--rerun FILTER', 'Retry on nodes from the last run', "'all' all nodes that were part of the last run.", "'failure' nodes that failed in the last run.", "'success' nodes that succeeded in the last run.") do |rerun| @options[:rerun] = rerun end define('--noop', 'Execute a task that supports it in noop mode') do |_| @options[:noop] = true end define('--description DESCRIPTION', 'Description to use for the job') do |description| @options[:description] = description end define('--params PARAMETERS', "Parameters to a task or plan as json, a json file '@<file>', or on stdin '-'") do |params| @options[:task_options] = parse_params(params) end define('-e', '--execute CODE', "Puppet manifest code to apply to the targets") do |code| @options[:code] = code end separator "\nAuthentication:" define('-u', '--user USER', 'User to authenticate as') do |user| @options[:user] = user end define('-p', '--password [PASSWORD]', 'Password to authenticate with. Omit the value to prompt for the password.') do |password| if password.nil? STDOUT.print "Please enter your password: " @options[:password] = STDIN.noecho(&:gets).chomp STDOUT.puts else @options[:password] = password end end define('--private-key KEY', 'Private ssh key to authenticate with') do |key| @options[:'private-key'] = key end define('--[no-]host-key-check', 'Check host keys with SSH') do |host_key_check| @options[:'host-key-check'] = host_key_check end define('--[no-]ssl', 'Use SSL with WinRM') do |ssl| @options[:ssl] = ssl end define('--[no-]ssl-verify', 'Verify remote host SSL certificate with WinRM') do |ssl_verify| @options[:'ssl-verify'] = ssl_verify end separator "\nEscalation:" define('--run-as USER', 'User to run as using privilege escalation') do |user| @options[:'run-as'] = user end define('--sudo-password [PASSWORD]', 'Password for privilege escalation. Omit the value to prompt for the password.') do |password| if password.nil? STDOUT.print "Please enter your privilege escalation password: " @options[:'sudo-password'] = STDIN.noecho(&:gets).chomp STDOUT.puts else @options[:'sudo-password'] = password end end separator "\nRun context:" define('-c', '--concurrency CONCURRENCY', Integer, 'Maximum number of simultaneous connections (default: 100)') do |concurrency| @options[:concurrency] = concurrency end define('--compile-concurrency CONCURRENCY', Integer, 'Maximum number of simultaneous manifest block compiles (default: number of cores)') do |concurrency| @options[:'compile-concurrency'] = concurrency end define('-m', '--modulepath MODULES', "List of directories containing modules, separated by '#{File::PATH_SEPARATOR}'", 'Directories are case-sensitive') do |modulepath| # When specified from the CLI, modulepath entries are relative to pwd @options[:modulepath] = modulepath.split(File::PATH_SEPARATOR).map do |moduledir| File.expand_path(moduledir) end end define('--boltdir FILEPATH', 'Specify what Boltdir to load config from (default: autodiscovered from current working dir)') do |path| @options[:boltdir] = path end define('--configfile FILEPATH', 'Specify where to load config from (default: ~/.puppetlabs/bolt/bolt.yaml)') do |path| @options[:configfile] = path end define('-i', '--inventoryfile FILEPATH', 'Specify where to load inventory from (default: ~/.puppetlabs/bolt/inventory.yaml)') do |path| if ENV.include?(Bolt::Inventory::ENVIRONMENT_VAR) raise Bolt::CLIError, "Cannot pass inventory file when #{Bolt::Inventory::ENVIRONMENT_VAR} is set" end @options[:inventoryfile] = File.expand_path(path) end define('--[no-]save-rerun', 'Whether to update the rerun file after this command.') do |save| @options[:'save-rerun'] = save end separator "\nTransports:" define('--transport TRANSPORT', TRANSPORTS.keys.map(&:to_s), "Specify a default transport: #{TRANSPORTS.keys.join(', ')}") do |t| @options[:transport] = t end define('--connect-timeout TIMEOUT', Integer, 'Connection timeout (defaults vary)') do |timeout| @options[:'connect-timeout'] = timeout end define('--[no-]tty', 'Request a pseudo TTY on nodes that support it') do |tty| @options[:tty] = tty end define('--tmpdir DIR', 'The directory to upload and execute temporary files on the target') do |tmpdir| @options[:tmpdir] = tmpdir end separator "\nDisplay:" define('--format FORMAT', 'Output format to use: human or json') do |format| @options[:format] = format end define('--[no-]color', 'Whether to show output in color') do |color| @options[:color] = color end define('-v', '--[no-]verbose', 'Display verbose logging') do |value| @options[:verbose] = value end define('--trace', 'Display error stack traces') do |_| @options[:trace] = true end separator "\nGlobal:" define('-h', '--help', 'Display help') do |_| @options[:help] = true end define('--version', 'Display the version') do |_| puts Bolt::VERSION raise Bolt::CLIExit end define('--debug', 'Display debug logging') do |_| @options[:debug] = true end define('--plugin PLUGIN', 'Select the plugin to use') do |plug| @options[:plugin] = plug end end def remove_excluded_opts(option_list) # Remove any options that are not available for the specified subcommand top.list.delete_if do |opt| opt.respond_to?(:switch_name) && !option_list.include?(opt.switch_name) end # Remove any separators if all options of that type have been removed top.list.delete_if do |opt| i = top.list.index(opt) opt.is_a?(String) && top.list[i + 1].is_a?(String) end end def update help_text = get_help_text(@options[:subcommand], @options[:action]) # Update the banner according to the subcommand self.banner = help_text[:banner] # Builds the option list for the specified subcommand and removes all excluded # options from the help text remove_excluded_opts(help_text[:flags]) end def parse_params(params) json = get_arg_input(params) JSON.parse(json) rescue JSON::ParserError => e raise Bolt::CLIError, "Unable to parse --params value as JSON: #{e}" end def get_arg_input(value) if value.start_with?('@') file = value.sub(/^@/, '') read_arg_file(file) elsif value == '-' STDIN.read else value end end def read_arg_file(file) File.read(File.expand_path(file)) rescue StandardError => e raise Bolt::FileError.new("Error attempting to read #{file}: #{e}", file) end end end