bolt 0.16.4 → 0.17.0

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of bolt might be problematic. Click here for more details.

Files changed (36) hide show
  1. checksums.yaml +4 -4
  2. data/bolt-modules/boltlib/lib/puppet/functions/fail_plan.rb +2 -3
  3. data/bolt-modules/boltlib/lib/puppet/functions/file_upload.rb +2 -2
  4. data/bolt-modules/boltlib/lib/puppet/functions/get_targets.rb +2 -3
  5. data/bolt-modules/boltlib/lib/puppet/functions/run_command.rb +2 -2
  6. data/bolt-modules/boltlib/lib/puppet/functions/run_plan.rb +2 -2
  7. data/bolt-modules/boltlib/lib/puppet/functions/run_task.rb +2 -2
  8. data/bolt-modules/boltlib/lib/puppet/functions/set_var.rb +29 -0
  9. data/bolt-modules/boltlib/lib/puppet/functions/vars.rb +27 -0
  10. data/lib/bolt/cli.rb +220 -184
  11. data/lib/bolt/config.rb +95 -13
  12. data/lib/bolt/executor.rb +12 -5
  13. data/lib/bolt/inventory.rb +48 -11
  14. data/lib/bolt/inventory/group.rb +22 -2
  15. data/lib/bolt/logger.rb +56 -8
  16. data/lib/bolt/outputter/human.rb +18 -1
  17. data/lib/bolt/pal.rb +6 -1
  18. data/lib/bolt/target.rb +1 -1
  19. data/lib/bolt/transport/base.rb +3 -0
  20. data/lib/bolt/transport/local.rb +90 -0
  21. data/lib/bolt/transport/local/shell.rb +29 -0
  22. data/lib/bolt/transport/orch.rb +2 -2
  23. data/lib/bolt/transport/ssh.rb +0 -3
  24. data/lib/bolt/transport/winrm.rb +0 -3
  25. data/lib/bolt/util.rb +31 -4
  26. data/lib/bolt/version.rb +1 -1
  27. data/lib/bolt_ext/puppetdb_inventory.rb +9 -2
  28. data/modules/aggregate/lib/puppet/functions/aggregate/count.rb +19 -0
  29. data/modules/aggregate/lib/puppet/functions/aggregate/nodes.rb +19 -0
  30. data/modules/aggregate/plans/count.pp +35 -0
  31. data/modules/aggregate/plans/nodes.pp +35 -0
  32. data/modules/canary/lib/puppet/functions/canary/merge.rb +11 -0
  33. data/modules/canary/lib/puppet/functions/canary/random_split.rb +20 -0
  34. data/modules/canary/lib/puppet/functions/canary/skip.rb +23 -0
  35. data/modules/canary/plans/init.pp +52 -0
  36. metadata +14 -16
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: dabc4d0d53c4dc91e19ca332cc074c70026d9934
4
- data.tar.gz: e4bd3c566ddb2cb37724de78063c62eafc4e2fef
3
+ metadata.gz: 91e33f8c654cb910a35ab5584efb6ac4fe2b3005
4
+ data.tar.gz: 5c2e652d4d607538aa57bf932d43822dbba32e3c
5
5
  SHA512:
6
- metadata.gz: 579ed3e93ab4dc82d5ded1dedac8f42793217a6832360c13b8594cab22e5b2a6f6222ecb2a280bb7d6024f93374e3caca9037350c6dc05e80856202be813f84c
7
- data.tar.gz: 25bff2cb74e014d109f3de109a1bf330a169367a528ffa40b29246020ca905578e90d0c4db08334455cb0d1b2a7ad98e1b8039fce5393546feb0de6947c85528
6
+ metadata.gz: d78fdf3b548aeaa383afb25d72a9e0df827e3929419e8d745efc1fc2d2fa98015e107c2fdb47ae5315002069a4da2bd7bdc7eaa3dd4adc9cfff5226b80e5e415
7
+ data.tar.gz: b1ea22fe91aabd4860cbdebb08dd50be7cdd43d18b312f044c5c00e64e0e2ca0de4c09835655e2b153217b756a570991195c6042d9c6e843e60932d89efdcc9f
@@ -1,10 +1,9 @@
1
+ require 'bolt/error'
2
+
1
3
  # Raises a Bolt::PlanFailure exception to signal to callers that the plan failed
2
4
  #
3
5
  # Plan authors should call this function when their plan is not successful. The
4
6
  # error may then be caught by another plans run_plan function or in bolt itself
5
-
6
- require 'bolt/error'
7
-
8
7
  Puppet::Functions.create_function(:fail_plan) do
9
8
  dispatch :from_args do
10
9
  param 'String[1]', :msg
@@ -1,3 +1,5 @@
1
+ require 'bolt/error'
2
+
1
3
  # Uploads the given file or directory to the given set of targets and returns the result from each upload.
2
4
  #
3
5
  # * This function does nothing if the list of targets is empty.
@@ -5,8 +7,6 @@
5
7
  # * A target is a String with a targets's hostname or a Target.
6
8
  # * The returned value contains information about the result per target.
7
9
  #
8
- require 'bolt/error'
9
-
10
10
  Puppet::Functions.create_function(:file_upload, Puppet::Functions::InternalFunction) do
11
11
  dispatch :file_upload do
12
12
  scope_param
@@ -1,3 +1,5 @@
1
+ require 'bolt/error'
2
+
1
3
  # Parses common ways of referring to targets and returns an array of Targets.
2
4
  #
3
5
  # Accepts input consisting of
@@ -12,9 +14,6 @@
12
14
  # - ['host1', 'group1', 'winrm://host2:54321']
13
15
  #
14
16
  # Returns an array of unique Targets resolved from any target URIs and groups.
15
-
16
- require 'bolt/error'
17
-
18
17
  Puppet::Functions.create_function(:get_targets) do
19
18
  dispatch :get_targets do
20
19
  param 'Boltlib::TargetSpec', :names
@@ -1,3 +1,5 @@
1
+ require 'bolt/error'
2
+
1
3
  # Runs a command on the given set of targets and returns the result from each command execution.
2
4
  #
3
5
  # * This function does nothing if the list of targets is empty.
@@ -5,8 +7,6 @@
5
7
  # * A target is a String with a targets's hostname or a Target.
6
8
  # * The returned value contains information about the result per target.
7
9
  #
8
- require 'bolt/error'
9
-
10
10
  Puppet::Functions.create_function(:run_command) do
11
11
  dispatch :run_command do
12
12
  param 'String[1]', :command
@@ -1,3 +1,5 @@
1
+ require 'bolt/error'
2
+
1
3
  # Runs the `plan` referenced by its name passing giving arguments to it given as a hash of name to value mappings.
2
4
  # A plan is autoloaded from under <root>/plans if not already defined.
3
5
  #
@@ -8,8 +10,6 @@
8
10
  # }
9
11
  # run_plan('myplan', { x => 'testing' })
10
12
  #
11
- require 'bolt/error'
12
-
13
13
  Puppet::Functions.create_function(:run_plan, Puppet::Functions::InternalFunction) do
14
14
  dispatch :run_plan do
15
15
  scope_param
@@ -1,3 +1,5 @@
1
+ require 'bolt/error'
2
+
1
3
  # Runs a given instance of a `Task` on the given set of targets and returns the result from each.
2
4
  #
3
5
  # * This function does nothing if the list of targets is empty.
@@ -5,8 +7,6 @@
5
7
  # * A target is a String with a targets's hostname or a Target.
6
8
  # * The returned value contains information about the result per target.
7
9
  #
8
- require 'bolt/error'
9
-
10
10
  Puppet::Functions.create_function(:run_task) do
11
11
  dispatch :run_task do
12
12
  param 'String[1]', :task_name
@@ -0,0 +1,29 @@
1
+ require 'bolt/error'
2
+
3
+ # Sets a variable { key => value } for a target.
4
+ #
5
+ # This function takes 3 parameters:
6
+ # * A Target object to set the variable for
7
+ # * The key for the variable (String)
8
+ # * The value of the variable (Data)
9
+ #
10
+ # Returns undef.
11
+ Puppet::Functions.create_function(:set_var) do
12
+ dispatch :set_var do
13
+ param 'Target', :target
14
+ param 'String', :key
15
+ param 'Data', :value
16
+ end
17
+
18
+ def set_var(target, key, value)
19
+ unless Puppet[:tasks]
20
+ raise Puppet::ParseErrorWithIssue.from_issue_and_stack(
21
+ Puppet::Pops::Issues::TASK_OPERATION_NOT_SUPPORTED_WHEN_COMPILING, operation: 'set_var'
22
+ )
23
+ end
24
+
25
+ inventory = Puppet.lookup(:bolt_inventory) { nil }
26
+
27
+ inventory.set_var(target, key, value)
28
+ end
29
+ end
@@ -0,0 +1,27 @@
1
+ require 'bolt/error'
2
+
3
+ # Returns a hash of the 'vars' (variables) assigned to a target through the
4
+ # inventory file or `set_var` function.
5
+ #
6
+ # Accepts no parameters.
7
+ #
8
+ # Plan authors can call this function on a target to get the variable hash
9
+ # for that target.
10
+ Puppet::Functions.create_function(:vars) do
11
+ dispatch :vars do
12
+ param 'Target', :target
13
+ return_type 'Hash[String, Data]'
14
+ end
15
+
16
+ def vars(target)
17
+ unless Puppet[:tasks]
18
+ raise Puppet::ParseErrorWithIssue.from_issue_and_stack(
19
+ Puppet::Pops::Issues::TASK_OPERATION_NOT_SUPPORTED_WHEN_COMPILING, operation: 'set_var'
20
+ )
21
+ end
22
+
23
+ inventory = Puppet.lookup(:bolt_inventory) { nil }
24
+
25
+ inventory.vars(target)
26
+ end
27
+ end
@@ -24,7 +24,8 @@ module Bolt
24
24
  class CLIExit < StandardError; end
25
25
 
26
26
  class CLI
27
- BANNER = <<-HELP.freeze
27
+ class BoltOptionParser < OptionParser
28
+ BANNER = <<-HELP.freeze
28
29
  Usage: bolt <subcommand> <action> [options]
29
30
 
30
31
  Available subcommands:
@@ -39,9 +40,9 @@ Available subcommands:
39
40
  bolt file upload <src> <dest> Upload a local file
40
41
 
41
42
  where [options] are:
42
- HELP
43
+ HELP
43
44
 
44
- TASK_HELP = <<-HELP.freeze
45
+ TASK_HELP = <<-HELP.freeze
45
46
  Usage: bolt task <action> <task> [options] [parameters]
46
47
 
47
48
  Available actions are:
@@ -52,27 +53,27 @@ Available actions are:
52
53
  Parameters are of the form <parameter>=<value>.
53
54
 
54
55
  Available options are:
55
- HELP
56
+ HELP
56
57
 
57
- COMMAND_HELP = <<-HELP.freeze
58
+ COMMAND_HELP = <<-HELP.freeze
58
59
  Usage: bolt command <action> <command> [options]
59
60
 
60
61
  Available actions are:
61
62
  run Run a command remotely
62
63
 
63
64
  Available options are:
64
- HELP
65
+ HELP
65
66
 
66
- SCRIPT_HELP = <<-HELP.freeze
67
+ SCRIPT_HELP = <<-HELP.freeze
67
68
  Usage: bolt script <action> <script> [[arg1] ... [argN]] [options]
68
69
 
69
70
  Available actions are:
70
71
  run Upload a local script and run it remotely
71
72
 
72
73
  Available options are:
73
- HELP
74
+ HELP
74
75
 
75
- PLAN_HELP = <<-HELP.freeze
76
+ PLAN_HELP = <<-HELP.freeze
76
77
  Usage: bolt plan <action> <plan> [options] [parameters]
77
78
 
78
79
  Available actions are:
@@ -83,214 +84,266 @@ Available actions are:
83
84
  Parameters are of the form <parameter>=<value>.
84
85
 
85
86
  Available options are:
86
- HELP
87
+ HELP
87
88
 
88
- FILE_HELP = <<-HELP.freeze
89
+ FILE_HELP = <<-HELP.freeze
89
90
  Usage: bolt file <action> [options]
90
91
 
91
92
  Available actions are:
92
93
  upload <src> <dest> Upload local file <src> to <dest> on each node
93
94
 
94
95
  Available options are:
95
- HELP
96
+ HELP
96
97
 
97
- COMMANDS = { 'command' => %w[run],
98
- 'script' => %w[run],
99
- 'task' => %w[show run],
100
- 'plan' => %w[show run],
101
- 'file' => %w[upload] }.freeze
102
- TRANSPORTS = %w[ssh winrm pcp].freeze
98
+ # A helper mixin for OptionParser::Switch instances which will allow
99
+ # us to show/hide particular switch in the help message produced by
100
+ # the OptionParser#help method on demand.
101
+ module SwitchHider
102
+ attr_accessor :hide
103
103
 
104
- attr_reader :parser, :config
105
- attr_accessor :options
106
-
107
- def initialize(argv)
108
- Bolt::Logger.initialize_logging
109
- @argv = argv
110
- @options = {
111
- nodes: []
112
- }
113
- @config = Bolt::Config.new
114
-
115
- # parse mode and object, use COMMANDS as a whitelist
116
- @options[:mode] = argv[0] if COMMANDS.keys.any? { |mode| argv[0] == mode }
117
- @options[:object] = argv[1] if COMMANDS.values.flatten.uniq.any? { |object| argv[1] == object }
118
- @parser = create_option_parser(@options)
119
- @logger = Logging.logger[self]
120
- end
121
-
122
- def create_option_parser(results)
123
- parser = OptionParser.new('') do |opts|
124
- unless results[:mode] == 'plan'
125
- opts.on('-n', '--nodes NODES',
126
- 'Node(s) to connect to in URI format [protocol://]host[:port] (Optional)',
127
- 'Eg. --nodes bolt.puppet.com',
128
- 'Eg. --nodes localhost,ssh://nix.com:2222,winrm://windows.puppet.com',
129
- "\n",
130
- '* NODES can either be comma-separated, \'@<file>\' to read',
131
- '* nodes from a file, or \'-\' to read from stdin',
132
- '* Windows nodes must specify protocol with winrm://',
133
- '* protocol is `ssh` by default, may be `ssh` or `winrm`',
134
- '* port defaults to `22` for SSH',
135
- '* port defaults to `5985` or `5986` for WinRM, based on the --[no-]ssl setting') do |nodes|
136
- results[:nodes] << get_arg_input(nodes)
137
- end
104
+ def summarize(*args)
105
+ return self if hide
106
+ super
138
107
  end
139
- opts.on('-u', '--user USER',
140
- "User to authenticate as (Optional)") do |user|
141
- results[:user] = user
108
+ end
109
+
110
+ def initialize(options)
111
+ super()
112
+
113
+ @options = options
114
+
115
+ @nodes = define('-n', '--nodes NODES',
116
+ 'Node(s) to connect to in URI format [protocol://]host[:port] (Optional)',
117
+ 'Eg. --nodes bolt.puppet.com',
118
+ 'Eg. --nodes localhost,ssh://nix.com:2222,winrm://windows.puppet.com',
119
+ # An empty line in a switch description causes the OptionParser#help
120
+ # method to raise an exception. Specifying a line containing only a
121
+ # newline is the closest one can get to the empty line without
122
+ # triggering that exception.
123
+ "\n",
124
+ '* NODES can either be comma-separated, \'@<file>\' to read',
125
+ '* nodes from a file, or \'-\' to read from stdin',
126
+ '* Windows nodes must specify protocol with winrm://',
127
+ '* protocol is `ssh` by default, may be `ssh` or `winrm`',
128
+ '* port defaults to `22` for SSH',
129
+ '* port defaults to `5985` or `5986` for WinRM, based on the --[no-]ssl setting') do |nodes|
130
+ @options[:nodes] << get_arg_input(nodes)
131
+ end.extend(SwitchHider)
132
+ define('-u', '--user USER',
133
+ 'User to authenticate as (Optional)') do |user|
134
+ @options[:user] = user
142
135
  end
143
- opts.on('-p', '--password [PASSWORD]',
144
- 'Password to authenticate with (Optional).',
145
- 'Omit the value to prompt for the password.') do |password|
136
+ define('-p', '--password [PASSWORD]',
137
+ 'Password to authenticate with (Optional).',
138
+ 'Omit the value to prompt for the password.') do |password|
146
139
  if password.nil?
147
140
  STDOUT.print "Please enter your password: "
148
- results[:password] = STDIN.noecho(&:gets).chomp
141
+ @options[:password] = STDIN.noecho(&:gets).chomp
149
142
  STDOUT.puts
150
143
  else
151
- results[:password] = password
144
+ @options[:password] = password
152
145
  end
153
146
  end
154
- opts.on('--private-key KEY',
155
- "Private ssh key to authenticate with (Optional)") do |key|
156
- results[:key] = key
147
+ define('--private-key KEY',
148
+ 'Private ssh key to authenticate with (Optional)') do |key|
149
+ @options[:key] = key
157
150
  end
158
- opts.on('--tmpdir DIR',
159
- "The directory to upload and execute temporary files on the target (Optional)") do |tmpdir|
160
- results[:tmpdir] = tmpdir
151
+ define('--tmpdir DIR',
152
+ 'The directory to upload and execute temporary files on the target (Optional)') do |tmpdir|
153
+ @options[:tmpdir] = tmpdir
161
154
  end
162
- opts.on('-c', '--concurrency CONCURRENCY', Integer,
163
- "Maximum number of simultaneous connections " \
164
- "(Optional, defaults to 100)") do |concurrency|
165
- results[:concurrency] = concurrency
155
+ define('-c', '--concurrency CONCURRENCY', Integer,
156
+ 'Maximum number of simultaneous connections ' \
157
+ '(Optional, defaults to 100)') do |concurrency|
158
+ @options[:concurrency] = concurrency
166
159
  end
167
- opts.on('--connect-timeout TIMEOUT', Integer,
168
- "Connection timeout (Optional)") do |timeout|
169
- results[:connect_timeout] = timeout
160
+ define('--connect-timeout TIMEOUT', Integer,
161
+ 'Connection timeout (Optional)') do |timeout|
162
+ @options[:connect_timeout] = timeout
170
163
  end
171
- opts.on('--modulepath MODULES',
172
- "List of directories containing modules, " \
173
- "separated by #{File::PATH_SEPARATOR}") do |modulepath|
174
- results[:modulepath] = modulepath.split(File::PATH_SEPARATOR)
164
+ define('--modulepath MODULES',
165
+ 'List of directories containing modules, ' \
166
+ 'separated by ' << File::PATH_SEPARATOR) do |modulepath|
167
+ @options[:modulepath] = modulepath.split(File::PATH_SEPARATOR)
175
168
  end
176
- opts.on('--params PARAMETERS',
177
- "Parameters to a task or plan") do |params|
178
- results[:task_options] = parse_params(params)
169
+ define('--params PARAMETERS',
170
+ 'Parameters to a task or plan') do |params|
171
+ @options[:task_options] = parse_params(params)
179
172
  end
180
173
 
181
- opts.on('--format FORMAT',
182
- "Output format to use: human or json") do |format|
183
- results[:format] = format
174
+ define('--format FORMAT',
175
+ 'Output format to use: human or json') do |format|
176
+ @options[:format] = format
184
177
  end
185
- opts.on('--[no-]host-key-check',
186
- "Check host keys with SSH") do |host_key_check|
187
- results[:host_key_check] = host_key_check
178
+ define('--[no-]host-key-check',
179
+ 'Check host keys with SSH') do |host_key_check|
180
+ @options[:host_key_check] = host_key_check
188
181
  end
189
- opts.on('--[no-]ssl',
190
- "Use SSL with WinRM") do |ssl|
191
- results[:ssl] = ssl
182
+ define('--[no-]ssl',
183
+ 'Use SSL with WinRM') do |ssl|
184
+ @options[:ssl] = ssl
192
185
  end
193
- opts.on('--transport TRANSPORT', TRANSPORTS,
194
- "Specify a default transport: #{TRANSPORTS.join(', ')}") do |t|
195
- results[:transport] = t
186
+ define('--transport TRANSPORT', TRANSPORTS.map(&:to_s),
187
+ 'Specify a default transport: ' << TRANSPORTS.join(', ')) do |t|
188
+ @options[:transport] = t
196
189
  end
197
- opts.on('--run-as USER',
198
- "User to run as using privilege escalation") do |user|
199
- results[:run_as] = user
190
+ define('--run-as USER',
191
+ 'User to run as using privilege escalation') do |user|
192
+ @options[:run_as] = user
200
193
  end
201
- opts.on('--sudo-password [PASSWORD]',
202
- 'Password for privilege escalation') do |password|
194
+ define('--sudo-password [PASSWORD]',
195
+ 'Password for privilege escalation') do |password|
203
196
  if password.nil?
204
197
  STDOUT.print "Please enter your privilege escalation password: "
205
- results[:sudo_password] = STDIN.noecho(&:gets).chomp
198
+ @options[:sudo_password] = STDIN.noecho(&:gets).chomp
206
199
  STDOUT.puts
207
200
  else
208
- results[:sudo_password] = password
201
+ @options[:sudo_password] = password
209
202
  end
210
203
  end
211
- opts.on('--configfile CONFIG_PATH',
212
- 'Specify where to load the config file from') do |path|
213
- results[:configfile] = path
204
+ define('--configfile CONFIG_PATH',
205
+ 'Specify where to load the config file from') do |path|
206
+ @options[:configfile] = path
214
207
  end
215
- opts.on('--inventoryfile INVENTORY_PATH',
216
- 'Specify where to load the invenotry file from') do |path|
217
- results[:inventoryfile] = path
208
+ define('--inventoryfile INVENTORY_PATH',
209
+ 'Specify where to load the inventory file from') do |path|
210
+ @options[:inventoryfile] = path
218
211
  end
219
- opts.on_tail('--[no-]tty',
220
- "Request a pseudo TTY on nodes that support it") do |tty|
221
- results[:tty] = tty
212
+ define_tail('--[no-]tty',
213
+ 'Request a pseudo TTY on nodes that support it') do |tty|
214
+ @options[:tty] = tty
222
215
  end
223
- opts.on_tail('--noop',
224
- "Execute a task that supports it in noop mode") do |_|
225
- results[:noop] = true
216
+ define_tail('--noop',
217
+ 'Execute a task that supports it in noop mode') do |_|
218
+ @options[:noop] = true
226
219
  end
227
- opts.on_tail('-h', '--help', 'Display help') do |_|
228
- results[:help] = true
220
+ define_tail('-h', '--help', 'Display help') do |_|
221
+ @options[:help] = true
229
222
  end
230
- opts.on_tail('--verbose', 'Display verbose logging') do |_|
231
- results[:verbose] = true
223
+ define_tail('--verbose', 'Display verbose logging') do |_|
224
+ @options[:verbose] = true
232
225
  end
233
- opts.on_tail('--debug', 'Display debug logging') do |_|
234
- results[:debug] = true
226
+ define_tail('--debug', 'Display debug logging') do |_|
227
+ @options[:debug] = true
235
228
  end
236
- opts.on_tail('--version', 'Display the version') do |_|
229
+ define_tail('--version', 'Display the version') do |_|
237
230
  puts Bolt::VERSION
238
231
  raise Bolt::CLIExit
239
232
  end
233
+
234
+ update
240
235
  end
241
236
 
242
- parser.banner = case results[:mode]
243
- when "plan"
237
+ def update
238
+ # Update the banner according to the mode
239
+ self.banner = case @options[:mode]
240
+ when 'plan'
244
241
  PLAN_HELP
245
- when "command"
242
+ when 'command'
246
243
  COMMAND_HELP
247
- when "script"
244
+ when 'script'
248
245
  SCRIPT_HELP
249
- when "task"
246
+ when 'task'
250
247
  TASK_HELP
251
- when "file"
248
+ when 'file'
252
249
  FILE_HELP
253
250
  else
254
251
  BANNER
255
252
  end
256
- parser
257
- end
258
253
 
259
- # Only call after @config has been initialized.
260
- def inventory
261
- Bolt::Inventory.from_config(@config)
262
- end
263
- private :inventory
254
+ # Only show the --nodes switch in the help message produced by
255
+ # the #help method when not dealing with plans
256
+ @nodes.hide = (@options[:mode] == 'plan')
257
+ end
264
258
 
265
- def parse
266
- if @argv.empty?
267
- options[:help] = true
259
+ def parse_params(params)
260
+ json = get_arg_input(params)
261
+ JSON.parse(json)
262
+ rescue JSON::ParserError => err
263
+ raise Bolt::CLIError, "Unable to parse --params value as JSON: #{err}"
264
+ end
265
+
266
+ def get_arg_input(value)
267
+ if value.start_with?('@')
268
+ file = value.sub(/^@/, '')
269
+ read_arg_file(file)
270
+ elsif value == '-'
271
+ STDIN.read
272
+ else
273
+ value
274
+ end
268
275
  end
269
276
 
270
- remaining = handle_parser_errors do
271
- parser.permute(@argv)
277
+ def read_arg_file(file)
278
+ File.read(file)
279
+ rescue StandardError => err
280
+ raise Bolt::CLIError, "Error attempting to read #{file}: #{err}"
272
281
  end
282
+ end
283
+
284
+ COMMANDS = { 'command' => %w[run],
285
+ 'script' => %w[run],
286
+ 'task' => %w[show run],
287
+ 'plan' => %w[show run],
288
+ 'file' => %w[upload] }.freeze
273
289
 
274
- # Shortcut to handle help before other errors may be generated
275
- options[:mode] = remaining.shift
290
+ attr_reader :config, :options
276
291
 
277
- if options[:mode] == 'help'
278
- options[:help] = true
292
+ def initialize(argv)
293
+ Bolt::Logger.initialize_logging
294
+ @logger = Logging.logger[self]
279
295
 
280
- # regenerate options parser with new mode
281
- options[:mode] = remaining.shift
282
- @parser = create_option_parser(options)
283
- end
296
+ @config = Bolt::Config.new
297
+
298
+ @argv = argv
299
+
300
+ @options = {
301
+ nodes: []
302
+ }
303
+ end
304
+
305
+ # Only call after @config has been initialized.
306
+ def inventory
307
+ @inventory ||= Bolt::Inventory.from_config(config)
308
+ end
309
+ private :inventory
284
310
 
285
- if options[:help]
311
+ def parse
312
+ parser = BoltOptionParser.new(options)
313
+
314
+ if @argv.empty? ||
315
+ begin
316
+ # RuboCop apparently doesn't realize this is a block and issues
317
+ # the Lint/AssignmentInCondition warning for every assignment in
318
+ # it, so we disable that warning here.
319
+ # rubocop:disable Lint/AssignmentInCondition
320
+ remaining = handle_parser_errors do
321
+ parser.permute(@argv)
322
+ end
323
+
324
+ # Set the mode
325
+ options[:mode] = remaining.shift
326
+
327
+ if options[:mode] == 'help'
328
+ options[:help] = true
329
+ options[:mode] = remaining.shift
330
+ end
331
+
332
+ # Update the parser for the new mode
333
+ parser.update
334
+
335
+ options[:help]
336
+ # rubocop:enable Lint/AssignmentInCondition
337
+ end
338
+ then # rubocop:disable Style/MultilineIfThen
286
339
  puts parser.help
287
340
  raise Bolt::CLIExit
288
341
  end
289
342
 
290
- @config.load_file(options[:configfile])
291
- @config.update_from_cli(options)
292
- @config.validate
293
- Logging.logger[:root].level = @config[:log_level] || :notice
343
+ config.load_file(options[:configfile])
344
+ config.update_from_cli(options)
345
+ config.validate
346
+ Bolt::Logger.configure(config)
294
347
 
295
348
  # This section handles parsing non-flag options which are
296
349
  # mode specific rather then part of the config
@@ -313,7 +366,9 @@ HELP
313
366
  validate(options)
314
367
 
315
368
  # After validation, initialize inventory and targets. Errors here are better to catch early.
316
- options[:targets] = inventory.get_targets(options[:nodes]) if options[:nodes]
369
+ unless options[:action] == 'show' || options[:mode] == 'plan'
370
+ options[:targets] = inventory.get_targets(options[:nodes]) if options[:nodes]
371
+ end
317
372
 
318
373
  options
319
374
  rescue Bolt::Error => e
@@ -321,30 +376,6 @@ HELP
321
376
  raise e
322
377
  end
323
378
 
324
- def parse_params(params)
325
- json = get_arg_input(params)
326
- JSON.parse(json)
327
- rescue JSON::ParserError => err
328
- raise Bolt::CLIError, "Unable to parse --params value as JSON: #{err}"
329
- end
330
-
331
- def get_arg_input(value)
332
- if value.start_with?('@')
333
- file = value.sub(/^@/, '')
334
- read_arg_file(file)
335
- elsif value == '-'
336
- STDIN.read
337
- else
338
- value
339
- end
340
- end
341
-
342
- def read_arg_file(file)
343
- File.read(file)
344
- rescue StandardError => err
345
- raise Bolt::CLIError, "Error attempting to read #{file}: #{err}"
346
- end
347
-
348
379
  def validate(options)
349
380
  unless COMMANDS.include?(options[:mode])
350
381
  raise Bolt::CLIError,
@@ -385,12 +416,6 @@ HELP
385
416
  raise Bolt::CLIError, "Option '--nodes' must be specified"
386
417
  end
387
418
 
388
- if %w[task plan].include?(options[:mode]) && @config[:modulepath].nil?
389
- raise Bolt::CLIError,
390
- "Option '--modulepath' must be specified when using" \
391
- " a task or plan"
392
- end
393
-
394
419
  if options[:noop] && (options[:mode] != 'task' || options[:action] != 'run')
395
420
  raise Bolt::CLIError,
396
421
  "Option '--noop' may only be specified when running a task"
@@ -401,6 +426,8 @@ HELP
401
426
  yield
402
427
  rescue OptionParser::MissingArgument => e
403
428
  raise Bolt::CLIError, "Option '#{e.args.first}' needs a parameter"
429
+ rescue OptionParser::InvalidArgument => e
430
+ raise Bolt::CLIError, "Invalid parameter specified for option '#{e.args.first}': #{e.args[1]}"
404
431
  rescue OptionParser::InvalidOption, OptionParser::AmbiguousOption => e
405
432
  raise Bolt::CLIError, "Unknown argument '#{e.args.first}'"
406
433
  end
@@ -416,7 +443,7 @@ HELP
416
443
  end
417
444
 
418
445
  if options[:mode] == 'plan' || options[:mode] == 'task'
419
- pal = Bolt::PAL.new(@config)
446
+ pal = Bolt::PAL.new(config)
420
447
  end
421
448
 
422
449
  if options[:action] == 'show'
@@ -443,13 +470,22 @@ HELP
443
470
  message = 'There may be processes left executing on some nodes.'
444
471
 
445
472
  if options[:mode] == 'plan'
446
- executor = Bolt::Executor.new(@config, options[:noop], true)
473
+ unless options[:nodes].empty?
474
+ if options[:task_options]['nodes']
475
+ raise Bolt::CLIError,
476
+ "A plan's 'nodes' parameter may be specified using the --nodes option, but in that " \
477
+ "case it must not be specified as a separate nodes=<value> parameter nor included " \
478
+ "in the JSON data passed in the --params option"
479
+ end
480
+ options[:task_options]['nodes'] = options[:nodes].join(',')
481
+ end
482
+ executor = Bolt::Executor.new(config, options[:noop], true)
447
483
  result = pal.run_plan(options[:object], options[:task_options], executor, inventory)
448
484
  outputter.print_plan_result(result)
449
485
  # An exception would have been raised if the plan failed
450
486
  code = 0
451
487
  else
452
- executor = Bolt::Executor.new(@config, options[:noop])
488
+ executor = Bolt::Executor.new(config, options[:noop])
453
489
  targets = options[:targets]
454
490
 
455
491
  results = nil
@@ -525,7 +561,7 @@ HELP
525
561
  end
526
562
 
527
563
  def outputter
528
- @outputter ||= Bolt::Outputter.for_format(@config[:format])
564
+ @outputter ||= Bolt::Outputter.for_format(config[:format])
529
565
  end
530
566
  end
531
567
  end