mtncloud-cli 1.0.0 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,2281 +1,2281 @@
1
- require 'mtncloud/logging'
2
- require 'mtncloud/benchmarking'
3
- require 'mtncloud/terminal'
4
- require 'mtncloud/cli/cli_registry'
5
- require 'mtncloud/cli/option_parser'
6
- require 'mtncloud/cli/option_types'
7
- require 'mtncloud/cli/mixins/print_helper'
8
- require 'mtncloud/cli/credentials'
9
- #require 'mtncloud/cli/commands/shell'
10
- #require 'mtncloud/cli/commands/remote'
11
- require 'mtncloud/api/api_client'
12
-
13
- module MtnCloud
14
- module Cli
15
- # Module to be included by every CLI command so that commands get registered
16
- # This mixin defines a print and puts method, and delegates
17
- # todo: use delegate
18
- module CliCommand
19
-
20
- def self.included(base)
21
- base.send :include, MtnCloud::Cli::PrintHelper
22
- base.send :include, MtnCloud::Cli::PromptHelper
23
- base.send :include, MtnCloud::Benchmarking::HasBenchmarking
24
- base.extend ClassMethods
25
- MtnCloud::Cli::CliRegistry.add(base, base.command_name)
26
- end
27
-
28
- # the beginning of instance variables from optparse !
29
-
30
- # this setting makes it easy for the called to disable prompting
31
- attr_reader :no_prompt
32
-
33
- # @return [MtnCloud::Terminal] the terminal this command is being executed inside of
34
- def my_terminal
35
- @my_terminal ||= MtnCloud::Terminal.instance
36
- end
37
-
38
- # set the terminal running this command.
39
- # @param term [MtnCloudTerminal] the terminal this command is assigned to
40
- # @return the Terminal this command is being executed inside of
41
- def my_terminal=(term)
42
- if !term.is_a?(MtnCloud::Terminal)
43
- raise "CliCommand (#{self.class}) my_terminal= expects object of type Terminal and instead got a #{term.class}"
44
- end
45
- @my_terminal = term
46
- end
47
-
48
-
49
- # delegate :print, to: :my_terminal
50
- # delegate :puts, to: :my_terminal
51
- # or . . . bum bum bummm
52
- # a paradigm shift away from include and use module functions instead.
53
- # module_function :print, puts
54
- # delegate :puts, to: :my_terminal
55
-
56
- def print(*msgs)
57
- my_terminal.stdout.print(*msgs)
58
- end
59
-
60
- def println(*msgs)
61
- print(*msgs)
62
- print "\n"
63
- end
64
-
65
- def puts(*msgs)
66
- my_terminal.stdout.puts(*msgs)
67
- end
68
-
69
- def print_error(*msgs)
70
- my_terminal.stderr.print(*msgs)
71
- end
72
-
73
- def puts_error(*msgs)
74
- my_terminal.stderr.puts(*msgs)
75
- end
76
-
77
- # todo: customizable output color, other than cyan.
78
- # def terminal_fg
79
- # end
80
- # def cyan
81
- # Term::ANSIColor.black
82
- # end
83
-
84
- # todo: use terminal.stdin
85
- # def readline(*msgs)
86
- # @my_terminal.stdin.readline(*msgs)
87
- # end
88
-
89
- # todo: maybe...
90
- # disabled prompting for this command
91
- # def noninteractive()
92
- # @no_prompt = true
93
- # self
94
- # end
95
-
96
- # whether to prompt or not, this is true by default.
97
- def interactive?
98
- @no_prompt != true
99
- end
100
-
101
- def raise_command_error(msg, args=[], optparse=nil, exit_code=nil)
102
- raise MtnCloud::Cli::CommandError.new(msg, args, optparse, exit_code)
103
- end
104
-
105
- def raise_args_error(msg, args=[], optparse=nil, exit_code=nil)
106
- raise MtnCloud::Cli::CommandArgumentsError.new(msg, args, optparse, exit_code)
107
- end
108
-
109
- # parse_id_list splits returns the given id_list with its values split on a comma
110
- # your id values cannot contain a comma, atm...
111
- # @param id_list [String or Array of Strings]
112
- # @param delim [String] Default is a comma and any surrounding white space.
113
- # @return array of values
114
- def parse_id_list(id_list, delim=/\s*\,\s*/)
115
- [id_list].flatten.collect {|it| it ? it.to_s.split(delim) : nil }.flatten.compact
116
- end
117
-
118
- def parse_bytes_param(bytes_param, option, assumed_unit = nil, allow_zero = false)
119
- if bytes_param && ( bytes_param.to_f > 0 || ( allow_zero && bytes_param.to_i == 0 ))
120
- bytes_param.upcase!
121
- multiplier = 1
122
- unit = nil
123
- number = (bytes_param.to_f == bytes_param.to_i ? bytes_param.to_i : bytes_param.to_f)
124
- if (bytes_param.end_with? 'GB') || ((!bytes_param.end_with? 'MB') && assumed_unit == 'GB')
125
- unit = 'GB'
126
- multiplier = 1024 * 1024 * 1024
127
- elsif (bytes_param.end_with? 'MB') || assumed_unit == 'MB'
128
- unit = 'MB'
129
- multiplier = 1024 * 1024
130
- end
131
- return {:bytes_param => bytes_param, :bytes => number * multiplier, :number => number, :multiplier => multiplier, :unit => unit}
132
- end
133
- raise_command_error "Invalid value for #{option} option"
134
- end
135
-
136
- # this returns all the options passed in by -O, parsed all nicely into objects.
137
- def parse_passed_options(options, parse_opts={})
138
- excludes = [parse_opts[:exclude], parse_opts[:excludes]].flatten.compact
139
- passed_options = options[:options] ? options[:options].reject {|k,v| k.is_a?(Symbol) || excludes.include?(k) } : {}
140
- return passed_options
141
- end
142
-
143
- # Appends Array of OptionType definitions to an OptionParser instance
144
- # This adds an option like --fieldContext.fieldName="VALUE"
145
- # @param opts [OptionParser]
146
- # @param options [Hash] output map that is being constructed
147
- # @param option_types [Array] list of OptionType definitions to add
148
- # @return void, this modifies the opts in place.
149
- def build_option_type_options(opts, options, option_types=[])
150
- #opts.separator ""
151
- #opts.separator "Options:"
152
- options[:options] ||= {} # this is where these go..for now
153
- options[:option_types] = (options[:option_types] || []) + option_types
154
- custom_options = options[:options]
155
-
156
- # add each one to the OptionParser
157
- option_types.each do |option_type|
158
- # skip hidden types
159
- if option_type['type'] == 'hidden'
160
- next
161
- end
162
- if option_type['fieldName'].empty?
163
- puts_error "Missing fieldName for option type: #{option_type}" if MtnCloud::Logging.debug?
164
- next
165
- end
166
- full_field_name = option_type['fieldContext'].to_s.empty? ? option_type['fieldName'] : "#{option_type['fieldContext']}.#{option_type['fieldName']}"
167
- field_namespace = full_field_name.split(".")
168
- field_name = field_namespace.pop
169
-
170
- description = "#{option_type['fieldLabel']}#{option_type['fieldAddOn'] ? ('(' + option_type['fieldAddOn'] + ') ') : '' }#{!option_type['required'] ? ' (optional)' : ''}"
171
- if option_type['description']
172
- # description << "\n #{option_type['description']}"
173
- description << " - #{option_type['description']}"
174
- end
175
- if option_type['defaultValue']
176
- description << ". Default: #{option_type['defaultValue']}"
177
- end
178
- if option_type['helpBlock']
179
- description << "\n #{option_type['helpBlock']}"
180
- end
181
-
182
- # description = option_type['description'].to_s
183
- # if option_type['defaultValue']
184
- # description = "#{description} Default: #{option_type['defaultValue']}"
185
- # end
186
- # if option_type['required'] == true
187
- # description = "(Required) #{description}"
188
- # end
189
-
190
- value_label = "VALUE"
191
- if option_type['placeHolder']
192
- value_label = option_type['placeHolder']
193
- elsif option_type['type'] == 'checkbox'
194
- value_label = '[on|off]' # or.. true|false
195
- elsif option_type['type'] == 'number'
196
- value_label = 'NUMBER'
197
- elsif option_type['type'] == 'multiSelect'
198
- value_label = 'LIST'
199
- # elsif option_type['type'] == 'select'
200
- # value_label = 'SELECT'
201
- # elsif option['type'] == 'select'
202
- end
203
- if option_type['optionalValue']
204
- value_label = "[#{value_label}]"
205
- end
206
- full_option = "--#{full_field_name} #{value_label}"
207
- # switch is an alias for the full option name, fieldName is the default
208
- if option_type['switch']
209
- full_option = "--#{option_type['switch']} #{value_label}"
210
- end
211
- arg1, arg2 = full_option, String
212
- if option_type['shorthand']
213
- arg1, arg2 = full_option, option_type['shorthand']
214
- end
215
- opts.on(arg1, arg2, description) do |val|
216
- if option_type['type'] == 'checkbox'
217
- val = (val.to_s != 'false' && val.to_s != 'off')
218
- elsif option_type['dataType'] != 'string'
219
- # 'dataType': 'string' added to cli to avoid auto conversion to JSON
220
- # attempt to parse JSON, this allows blank arrays for multiSelect like --tenants []
221
- if (val.to_s[0] == '{' && val.to_s[-1] == '}') || (val.to_s[0] == '[' && val.to_s[-1] == ']')
222
- begin
223
- val = JSON.parse(val)
224
- rescue
225
- MtnCloud::Logging::DarkPrinter.puts "Failed to parse option value '#{val}' as JSON" if MtnCloud::Logging.debug?
226
- end
227
- end
228
- end
229
- cur_namespace = custom_options
230
- field_namespace.each do |ns|
231
- next if ns.empty?
232
- cur_namespace[ns.to_s] ||= {}
233
- cur_namespace = cur_namespace[ns.to_s]
234
- end
235
- cur_namespace[field_name] = val
236
- end
237
-
238
- # todo: all the various types
239
- # number
240
- # checkbox [on|off]
241
- # select for optionSource and selectOptions
242
-
243
- end
244
- opts
245
- end
246
-
247
- ## the standard options for a command that makes api requests (most of them)
248
-
249
- def build_standard_get_options(opts, options, includes=[], excludes=[])
250
- build_common_options(opts, options, includes + [:query, :json, :yaml, :csv, :fields, :select, :delim, :quiet, :dry_run, :remote], excludes)
251
- end
252
-
253
- def build_standard_post_options(opts, options, includes=[], excludes=[])
254
- build_common_options(opts, options, includes + [:options, :payload, :json, :quiet, :dry_run, :remote], excludes)
255
- end
256
-
257
- def build_standard_put_options(opts, options, includes=[], excludes=[])
258
- build_standard_post_options(opts, options, includes, excludes)
259
- end
260
-
261
- def build_standard_delete_options(opts, options, includes=[], excludes=[])
262
- build_common_options(opts, options, includes + [:auto_confirm, :query, :json, :quiet, :dry_run, :remote], excludes)
263
- end
264
-
265
- # list is GET that supports phrase,max,offset,sort,direction
266
- def build_standard_list_options(opts, options, includes=[], excludes=[])
267
- build_standard_get_options(opts, options, [:list] + includes, excludes)
268
- end
269
-
270
- def build_standard_add_options(opts, options, includes=[], excludes=[])
271
- build_standard_post_options(opts, options, includes, excludes)
272
- end
273
-
274
- def build_standard_add_many_options(opts, options, includes=[], excludes=[])
275
- build_standard_post_options(opts, options, includes + [:payloads], excludes)
276
- end
277
-
278
- def build_standard_update_options(opts, options, includes=[], excludes=[])
279
- build_standard_put_options(opts, options, includes, excludes)
280
- end
281
-
282
- def build_standard_remove_options(opts, options, includes=[], excludes=[])
283
- build_standard_delete_options(opts, options, includes, excludes)
284
- end
285
-
286
- def build_standard_api_options(opts, options, includes=[], excludes=[])
287
- build_common_options(opts, options, includes + [:query, :options, :payload, :json, :yaml, :csv, :fields, :select, :delim, :quiet, :dry_run, :remote], excludes)
288
- end
289
-
290
- # number of decimal places to show with curreny
291
- def default_sigdig
292
- 2
293
- end
294
-
295
- # appends to the passed OptionParser all the generic options
296
- # @param opts [OptionParser] the option parser object being constructed
297
- # @param options [Hash] the output Hash that is to being modified
298
- # @param includes [Array] which options to include eg. :options, :json, :remote
299
- # @return opts
300
- def build_common_options(opts, options, includes=[], excludes=[])
301
- #opts.separator ""
302
- # opts.separator "Common options:"
303
- option_keys = includes.clone
304
- all_option_keys = option_keys.dup
305
- # todo: support --quiet everywhere
306
- # turn on some options all the time..
307
- # unless command_name == "shell"
308
- # option_keys << :quiet unless option_keys.include?(:quiet)
309
- # end
310
-
311
- # ensure commands can always access options[:options], until we can deprecate it...
312
- options[:options] ||= {}
313
-
314
- while (option_key = option_keys.shift) do
315
- case option_key.to_sym
316
-
317
- when :tenant, :account
318
- # todo: let's deprecate this in favor of :tenant --tenant to keep -a reserved for --all perhaps?
319
- opts.on('--tenant TENANT', String, "Tenant (Account) Name or ID") do |val|
320
- options[:account] = val
321
- end
322
- opts.on('--tenant-id ID', String, "Tenant (Account) ID") do |val|
323
- options[:account_id] = val
324
- end
325
- # todo: let's deprecate this in favor of :tenant --tenant to keep -a reserved for --all perhaps?
326
- opts.on('-a','--account ACCOUNT', "Alias for --tenant") do |val|
327
- options[:account] = val
328
- end
329
- opts.on('-A','--account-id ID', "Tenant (Account) ID") do |val|
330
- options[:account_id] = val
331
- end
332
- opts.add_hidden_option('--tenant-id') if opts.is_a?(MtnCloud::Cli::OptionParser)
333
- opts.add_hidden_option('-a, --account') if opts.is_a?(MtnCloud::Cli::OptionParser)
334
- opts.add_hidden_option('-A, --account-id') if opts.is_a?(MtnCloud::Cli::OptionParser)
335
-
336
- when :details
337
- opts.on('-a', '--all', "Show all details." ) do
338
- options[:details] = true
339
- end
340
- opts.on('--details', '--details', "Show more details" ) do
341
- options[:details] = true
342
- end
343
- opts.add_hidden_option('--details')
344
-
345
- when :sigdig
346
- opts.on('--sigdig DIGITS', "Significant digits to display for prices (currency). Default is #{default_sigdig}.") do |val|
347
- options[:sigdig] = val.to_i
348
- end
349
-
350
- when :options
351
- options[:options] ||= {}
352
- opts.on( '-O', '--option OPTION', "Option in the format -O field=\"value\"" ) do |option|
353
- # todo: look ahead and parse ALL the option=value args after -O switch
354
- #custom_option_args = option.split('=')
355
- custom_option_args = option.sub(/\s?\=\s?/, '__OPTION_DELIM__').split('__OPTION_DELIM__')
356
- custom_options = options[:options]
357
- option_name_args = custom_option_args[0].split('.')
358
- option_type = (options[:option_types] || []).find {|it| it['fieldName'] == custom_option_args[0]} || {}
359
- if option_name_args.count > 1
360
- nested_options = custom_options
361
- option_name_args.each_with_index do |name_element,index|
362
- if index < option_name_args.count - 1
363
- nested_options[name_element] = nested_options[name_element] || {}
364
- nested_options = nested_options[name_element]
365
- else
366
- val = custom_option_args[1]
367
- unless option_type['noParse']
368
- if (val.to_s[0] == '{' && val.to_s[-1] == '}') || (val.to_s[0] == '[' && val.to_s[-1] == ']')
369
- begin
370
- val = JSON.parse(val)
371
- rescue
372
- MtnCloud::Logging::DarkPrinter.puts "Failed to parse option value '#{val}' as JSON" if MtnCloud::Logging.debug?
373
- end
374
- end
375
- end
376
- nested_options[name_element] = val
377
- end
378
- end
379
- else
380
- val = custom_option_args[1]
381
- unless option_type['noParse']
382
- if (val.to_s[0] == '{' && val.to_s[-1] == '}') || (val.to_s[0] == '[' && val.to_s[-1] == ']')
383
- begin
384
- val = JSON.parse(val)
385
- rescue
386
- MtnCloud::Logging::DarkPrinter.puts "Failed to parse option value '#{val}' as JSON" if MtnCloud::Logging.debug?
387
- end
388
- end
389
- end
390
- custom_options[custom_option_args[0]] = val
391
- end
392
- # convert "true","on" and "false","off" to true and false
393
- unless options[:skip_booleanize]
394
- custom_options.booleanize!
395
- end
396
- options[:options] = custom_options
397
- end
398
- # --always-prompt can be used with for update commands where it normally defaults to --no-prompt
399
- opts.on('--prompt', "Always prompt for input on every option, even those not prompted for by default.") do
400
- options[:always_prompt] = true
401
- options[:options][:always_prompt] = true
402
- end
403
- opts.on('-N','--no-prompt', "No prompt, skips all input prompting.") do |val|
404
- options[:no_prompt] = true
405
- options[:options][:no_prompt] = true
406
- end
407
- # opts.on('--skip-prompt x,y,z', String, "Skip prompt, do not prompt for input of the specified options.") do |val|
408
- # options[:skip_prompt] ||= []
409
- # options[:skip_prompt] += parse_array(val)
410
- # options[:options][:skip_prompt] = options[:skip_prompt]
411
- # end
412
- # opts.on('--only-prompt x,y,z', String, "Only prompt for input on the specified options.") do |val|
413
- # options[:only_prompt] ||= []
414
- # options[:only_prompt] += parse_array(val)
415
- # options[:options][:only_prompt] = options[:only_prompt]
416
- # end
417
- opts.on('--no-options', String, "No options, skips all option parsing so no options are required and no default values are used.") do
418
- options[:no_options] = true
419
- options[:options][:no_options] = options[:no_options]
420
- end
421
- opts.on('--skip-options x,y,z', String, "Skip parsing of the specified options so that they are not required and their default value is not used.") do |val|
422
- options[:skip_options] ||= []
423
- options[:skip_options] += parse_array(val)
424
- options[:options][:skip_options] = options[:skip_options]
425
- end
426
- # opts.on('--only-options x,y,z', String, "Only parse the specified options and skip all others.") do |val|
427
- # options[:only_options] ||= []
428
- # options[:only_options] += parse_array(val)
429
- # options[:options][:only_options] = options[:only_options]
430
- # end
431
-
432
- # hide these while incubating
433
- opts.add_hidden_option('--skip-prompt')
434
- opts.add_hidden_option('--only-prompt')
435
- opts.add_hidden_option('--no-options')
436
- opts.add_hidden_option('--skip-options')
437
- opts.add_hidden_option('--only-options')
438
-
439
- when :payload
440
- opts.on('--payload FILE', String, "Payload from a local JSON or YAML file, skip all prompting") do |val|
441
- options[:payload_file] = val.to_s
442
- begin
443
- payload_file = File.expand_path(options[:payload_file])
444
- if !File.exist?(payload_file) || !File.file?(payload_file)
445
- raise ::OptionParser::InvalidOption.new("File not found: #{payload_file}")
446
- #return false
447
- end
448
- if payload_file =~ /\.ya?ml\Z/
449
- options[:payload] = YAML.load_file(payload_file)
450
- else
451
- options[:payload] = JSON.parse(File.read(payload_file))
452
- end
453
- rescue => ex
454
- raise ::OptionParser::InvalidOption.new("Failed to parse payload file: #{payload_file} Error: #{ex.message}")
455
- end
456
- end
457
- opts.on('--payload-dir DIRECTORY', String, "Payload from a local directory containing 1-N JSON or YAML files, skip all prompting. This makes one request, merging all the files into a single payload.") do |val|
458
- print_error yellow,"[DEPRECATED] The option `--payload-dir` is deprecated and will be removed. Use `--payloads` to make requests for each file in a directory.",reset,"\n"
459
- options[:payload_dir] = val.to_s
460
- payload_dir = File.expand_path(options[:payload_dir])
461
- if !Dir.exist?(payload_dir) || !File.directory?(payload_dir)
462
- raise ::OptionParser::InvalidOption.new("Directory not found: #{payload_dir}")
463
- end
464
- payload = {}
465
- begin
466
- merged_payload = {}
467
- payload_files = []
468
- payload_files += Dir["#{payload_dir}/*.json"]
469
- payload_files += Dir["#{payload_dir}/*.yml"]
470
- payload_files += Dir["#{payload_dir}/*.yaml"]
471
- if payload_files.empty?
472
- raise ::OptionParser::InvalidOption.new("No .json/yaml files found in config directory: #{payload_dir}")
473
- end
474
- payload_files.each do |payload_file|
475
- MtnCloud::Logging::DarkPrinter.puts "parsing payload file: #{payload_file}" if MtnCloud::Logging.debug?
476
- config_payload = {}
477
- if payload_file =~ /\.ya?ml\Z/
478
- config_payload = YAML.load_file(payload_file)
479
- else
480
- config_payload = JSON.parse(File.read(payload_file))
481
- end
482
- merged_payload.deep_merge!(config_payload)
483
- end
484
- options[:payload] = merged_payload
485
- rescue => ex
486
- raise ::OptionParser::InvalidOption.new("Failed to parse payload file: #{payload_file} Error: #{ex.message}")
487
- end
488
- end
489
- opts.add_hidden_option('--payload-dir')
490
- opts.on('--payload-json JSON', String, "Payload JSON, skip all prompting") do |val|
491
- begin
492
- options[:payload] = JSON.parse(val.to_s)
493
- rescue => ex
494
- raise ::OptionParser::InvalidOption.new("Failed to parse payload as JSON. Error: #{ex.message}")
495
- end
496
- end
497
- opts.on('--payload-yaml YAML', String, "Payload YAML, skip all prompting") do |val|
498
- begin
499
- options[:payload] = YAML.load(val.to_s)
500
- rescue => ex
501
- raise ::OptionParser::InvalidOption.new("Failed to parse payload as YAML. Error: #{ex.message}")
502
- end
503
- end
504
- # --payloads test-data/item*.json
505
- opts.on('--payloads PATH', String, "Payload(s) from one or more local JSON or YAML files, skip all prompting and execute the request 1-N times, once for each file. PATH can be a directory or a file pattern.") do |val|
506
- # maybe use parse_array(val) to support csv..
507
- # find files matching PATH
508
- # todo: probably support recursive... can be done with '**/*.json' now though.
509
- if val.to_s.strip.empty?
510
- raise ::OptionParser::InvalidOption.new("PATH must be provided as directory, file or pattern to find JSON or YAML files.")
511
- end
512
- filepath = File.expand_path(val.to_s.strip)
513
- files = []
514
- if File.directory?(filepath)
515
- # passed the name of a directory, include all the JSON and YAML files directly under it
516
- Dir.glob(File.join(filepath, "*")).each do |file|
517
- if File.file?(file) && ['.json','.yaml','.yml'].include?(File.extname(file))
518
- files << file
519
- end
520
- end
521
- if files.empty?
522
- raise ::OptionParser::InvalidOption.new("Failed to find any .json or .yaml files under the directory: #{filepath}")
523
- end
524
- elsif File.file?(filepath)
525
- # passed the name of a file
526
- files << filepath
527
- else
528
- # assume it is a pattern to find files with
529
- files = Dir.glob(filepath)
530
- if files.empty?
531
- raise ::OptionParser::InvalidOption.new("Failed to find any files matching path: #{filepath}")
532
- end
533
- end
534
- # parse files as JSON or YAML
535
- options[:payload_files] ||= []
536
- options[:payloads] ||= []
537
- files.each do |file|
538
- if options[:payload_files].include?(file)
539
- next
540
- else
541
- options[:payload_files] << file
542
- end
543
- payload = nil
544
- begin
545
- payload_file = File.expand_path(file)
546
- if !File.exist?(payload_file) || !File.file?(payload_file)
547
- raise ::OptionParser::InvalidOption.new("File not found: #{payload_file}")
548
- end
549
- # todo: could use parse_json_or_yaml()
550
- payload = nil
551
- if payload_file =~ /\.ya?ml\Z/
552
- payload = YAML.load_file(payload_file)
553
- else
554
- payload = JSON.parse(File.read(payload_file))
555
- end
556
- options[:payloads] << payload
557
- rescue => ex
558
- raise ::OptionParser::InvalidOption.new("Failed to parse payload file: #{payload_file} Error: #{ex.message}")
559
- end
560
- end
561
- end if all_option_keys.include?(:payloads)
562
- opts.on('--ignore-payload-errors', "Continue processing any remaining payloads if an error occurs. The default behavior is to stop processing when an error occurs.") do
563
- options[:ignore_payload_errors] = true
564
- end if all_option_keys.include?(:payloads)
565
- when :payloads
566
- # added with :payload too... just need it here to avoid unknown key error
567
- # todo: remove this when every command supporting :payload is updated to use parse_payload(options) and execute_api(options)
568
- when :list
569
- opts.on( '-m', '--max MAX', "Max Results" ) do |val|
570
- # api supports max=-1 for all at the moment..
571
- if val.to_s == "all" || val.to_s == "-1"
572
- options[:max] = "-1"
573
- else
574
- max = val.to_i
575
- if max <= 0
576
- raise ::OptionParser::InvalidArgument.new("must be a positive integer")
577
- end
578
- options[:max] = max
579
- end
580
- end
581
-
582
- opts.on( '-o', '--offset OFFSET', "Offset Results" ) do |val|
583
- offset = val.to_i
584
- if offset <= 0
585
- raise ::OptionParser::InvalidArgument.new("must be a positive integer")
586
- end
587
- options[:offset] = offset
588
- end
589
-
590
- if excludes.include?("search") == false
591
- opts.on( '-s', '--search PHRASE', "Search Phrase" ) do |phrase|
592
- options[:phrase] = phrase
593
- end
594
- end
595
-
596
- opts.on( '-S', '--sort ORDER', "Sort Order. DIRECTION may be included as \"ORDER [asc|desc]\"." ) do |v|
597
- if v.to_s.include?(",")
598
- # sorting on multiple properties, just pass it as is, newer api supports multiple fields
599
- options[:sort] = v
600
- else
601
- v_parts = v.to_s.split(" ")
602
- if v_parts.size > 1
603
- options[:sort] = v_parts[0]
604
- options[:direction] = (v_parts[1].strip == "desc") ? "desc" : "asc"
605
- else
606
- options[:sort] = v
607
- end
608
- end
609
- end
610
-
611
- opts.on( '-D', '--desc', "Descending Sort Direction." ) do |v|
612
- options[:direction] = "desc"
613
- end
614
-
615
- opts.on( "--reverse", "Reverse order of results. This invert is done by the client, not necessarily the entire dataset." ) do
616
- options[:reverse] = true
617
- end
618
-
619
- # arbitrary query parameters in the format -Q "category=web&phrase=nginx"
620
- # opts.on( '-Q', '--query PARAMS', "Query parameters. PARAMS format is 'foo=bar&category=web'" ) do |val|
621
- # options[:query_filters_raw] = val
622
- # options[:query_filters] = {}
623
- # # todo: smarter parsing
624
- # val.split('&').each do |filter|
625
- # k, v = filter.split('=')
626
- # # allow "woot:true instead of woot=true"
627
- # if (k.include?(":") && v == nil)
628
- # k, v = k.split(":")
629
- # end
630
- # if (!k.to_s.empty?)
631
- # options[:query_filters][k.to_s.strip] = v.to_s.strip
632
- # end
633
- # end
634
- # end
635
-
636
- when :query, :query_filters
637
- # arbitrary query parameters in the format -Q "category=web&phrase=nginx"
638
- # or pass it many times like -Q foo=bar -Q hello=world
639
- opts.on( '-Q', '--query PARAMS', "Query parameters. PARAMS format is 'foo=bar&category=web'" ) do |val|
640
- if options[:query_filters_raw] && !options[:query_filters_raw].empty?
641
- options[:query_filters_raw] += ("&" + val)
642
- else
643
- options[:query_filters_raw] = val
644
- end
645
- options[:query_filters] ||= {}
646
- val.split('&').each do |filter|
647
- k, v = filter.split('=')
648
- # allow woot:true instead of woot=true
649
- if (k.include?(":") && v == nil)
650
- k, v = k.split(":")
651
- end
652
- if (!k.to_s.empty?)
653
- if options[:query_filters].key?(k.to_s.strip)
654
- cur_val = options[:query_filters][k.to_s.strip]
655
- if cur_val.instance_of?(Array)
656
- options[:query_filters][k.to_s.strip] << v.to_s.strip
657
- else
658
- options[:query_filters][k.to_s.strip] = [cur_val, v.to_s.strip]
659
- end
660
- else
661
- options[:query_filters][k.to_s.strip] = v.to_s.strip
662
- end
663
- end
664
- end
665
- end
666
-
667
- when :last_updated
668
- # opts.on("--last-updated TIME", Time, "Filter by gte last updated") do |time|
669
- opts.on("--last-updated TIME", String, "Filter by Last Updated (gte)") do |time|
670
- begin
671
- options[:lastUpdated] = parse_time(time)
672
- rescue => e
673
- raise OptionParser::InvalidArgument.new "Failed to parse time '#{time}'. Error: #{e}"
674
- end
675
- end
676
-
677
- when :find_by_name
678
- opts.on('--find-by-name', "Always treat the identifier argument as a name, never an ID. Useful for specifying names that look like numbers. eg. '1234'" ) do
679
- options[:find_by_name] = true
680
- end
681
- # opts.add_hidden_option('--find-by-name') if opts.is_a?(MtnCloud::Cli::OptionParser)
682
-
683
- when :remote
684
- opts.on( '-r', '--remote REMOTE', "Remote name. The current remote is used by default." ) do |val|
685
- options[:remote] = val
686
- end
687
- opts.on( '--remote-url URL', '--remote-url URL', "Remote url. This allows adhoc requests instead of using a configured remote." ) do |val|
688
- options[:remote_url] = val
689
- end
690
- opts.on( '-T', '--token TOKEN', "Access token for authentication with --remote. Saved credentials are used by default." ) do |val|
691
- options[:remote_token] = val
692
- end unless excludes.include?(:remote_token)
693
- opts.on( '--token-file FILE', String, "Token File, read a file containing the access token." ) do |val|
694
- token_file = File.expand_path(val)
695
- if !File.exist?(token_file) || !File.file?(token_file)
696
- raise ::OptionParser::InvalidOption.new("File not found: #{token_file}")
697
- end
698
- options[:remote_token] = File.read(token_file).to_s.split("\n").first.strip
699
- end
700
- opts.add_hidden_option('--token-file') if opts.is_a?(MtnCloud::Cli::OptionParser)
701
- opts.on( '-U', '--username USERNAME', "Username for authentication." ) do |val|
702
- options[:remote_username] = val
703
- end unless excludes.include?(:remote_username)
704
-
705
-
706
- unless excludes.include?(:remote_password)
707
- opts.on( '-P', '--password PASSWORD', "Password for authentication." ) do |val|
708
- options[:remote_password] = val
709
- end
710
- opts.on( '--password-file FILE', String, "Password File, read a file containing the password for authentication." ) do |val|
711
- password_file = File.expand_path(val)
712
- if !File.exist?(password_file) || !File.file?(password_file)
713
- raise ::OptionParser::InvalidOption.new("File not found: #{password_file}")
714
- end
715
- file_content = File.read(password_file) #.strip
716
- options[:remote_password] = File.read(password_file).to_s.split("\n").first
717
- end
718
- opts.add_hidden_option('--password-file') if opts.is_a?(MtnCloud::Cli::OptionParser)
719
- end
720
-
721
- # todo: also require this for talking to plain old HTTP
722
- opts.on('-I','--insecure', "Allow insecure HTTPS communication. i.e. bad SSL certificate.") do |val|
723
- options[:insecure] = true
724
- MtnCloud::RestClient.enable_ssl_verification = false
725
- end
726
-
727
- #when :header, :headers
728
- opts.on( '-H', '--header HEADER', "Additional HTTP header to include with requests." ) do |val|
729
- options[:headers] ||= {}
730
- # header_list = val.to_s.split(',')
731
- header_list = [val.to_s]
732
- header_list.each do |h|
733
- header_parts = val.to_s.split(":")
734
- header_key, header_value = header_parts[0], header_parts[1..-1].join(":").strip
735
- if header_parts.size() < 2
736
- header_parts = val.to_s.split("=")
737
- header_key, header_value = header_parts[0], header_parts[1..-1].join("=").strip
738
- end
739
- if header_parts.size() < 2
740
- raise_command_error "Invalid HEADER value '#{val}'. HEADER should contain a key and a value. eg. -H 'X-Morpheus-Lease: $MORPHEUS_LEASE_TOKEN'"
741
- end
742
- options[:headers][header_key] = header_value
743
- end
744
- end
745
- # opts.add_hidden_option('-H') if opts.is_a?(MtnCloud::Cli::OptionParser)
746
- # opts.add_hidden_option('--header') if opts.is_a?(MtnCloud::Cli::OptionParser)
747
- opts.add_hidden_option('-H, --header') if opts.is_a?(MtnCloud::Cli::OptionParser)
748
-
749
- #when :timeout
750
- opts.on( '--timeout SECONDS', "Timeout for api requests. Default is typically 30 seconds." ) do |val|
751
- options[:timeout] = val ? val.to_f : nil
752
- end
753
- opts.add_hidden_option('--timeout') if opts.is_a?(MtnCloud::Cli::OptionParser)
754
-
755
- when :auto_confirm
756
- opts.on( '-y', '--yes', "Auto Confirm" ) do
757
- options[:yes] = true
758
- end
759
-
760
- when :json
761
- opts.on('-j','--json', "JSON Output") do
762
- options[:json] = true
763
- options[:format] = :json
764
- end
765
-
766
- opts.on('--json-raw', String, "JSON Output that is not so pretty.") do |val|
767
- options[:json] = true
768
- options[:format] = :json
769
- options[:pretty_json] = false
770
- end
771
- opts.add_hidden_option('json-raw') if opts.is_a?(MtnCloud::Cli::OptionParser)
772
-
773
- when :yaml
774
- # -y for --yes and for --yaml
775
- if includes.include?(:auto_confirm)
776
- opts.on(nil, '--yaml', "YAML Output") do
777
- options[:yaml] = true
778
- options[:format] = :yaml
779
- end
780
- else
781
- opts.on('-y', '--yaml', "YAML Output") do
782
- options[:yaml] = true
783
- options[:format] = :yaml
784
- end
785
- end
786
- opts.on(nil, '--yml', "alias for --yaml") do
787
- options[:yaml] = true
788
- options[:format] = :yaml
789
- end
790
- opts.add_hidden_option('yml') if opts.is_a?(MtnCloud::Cli::OptionParser)
791
-
792
- when :csv
793
- opts.on(nil, '--csv', "CSV Output") do
794
- options[:csv] = true
795
- options[:format] = :csv
796
- #options[:csv_delim] = options[:csv_delim] || ","
797
- end
798
- # deprecated --csv-delim, use --delimiter instead
799
- opts.on('--csv-delim CHAR', String, "Delimiter for CSV Output values. Default: ','") do |val|
800
- options[:csv] = true
801
- options[:format] = :csv
802
- val = val.gsub("\\n", "\n").gsub("\\r", "\r").gsub("\\t", "\t") if val.include?("\\")
803
- options[:csv_delim] = val
804
- end
805
- opts.add_hidden_option('--csv-delim') if opts.is_a?(MtnCloud::Cli::OptionParser)
806
-
807
- # deprecated --csv-newline, use --newline instead
808
- opts.on('--csv-newline [CHAR]', String, "Delimiter for CSV Output rows. Default: '\\n'") do |val|
809
- options[:csv] = true
810
- options[:format] = :csv
811
- if val == "no" || val == "none"
812
- options[:csv_newline] = ""
813
- else
814
- val = val.to_s.gsub("\\n", "\n").gsub("\\r", "\r").gsub("\\t", "\t") if val.include?("\\")
815
- options[:csv_newline] = val
816
- end
817
- end
818
- opts.add_hidden_option('--csv-newline') if opts.is_a?(MtnCloud::Cli::OptionParser)
819
-
820
- opts.on(nil, '--csv-quotes', "Wrap CSV values with \". Default: false") do
821
- options[:csv] = true
822
- options[:format] = :csv
823
- options[:csv_quotes] = true
824
- end
825
- opts.add_hidden_option('--csv-quotes') if opts.is_a?(MtnCloud::Cli::OptionParser)
826
-
827
- opts.on(nil, '--csv-no-header', "Exclude header for CSV Output.") do
828
- options[:csv] = true
829
- options[:format] = :csv
830
- options[:csv_no_header] = true
831
- end
832
- opts.add_hidden_option('--csv-no-header') if opts.is_a?(MtnCloud::Cli::OptionParser)
833
-
834
- opts.on(nil, '--quotes', "Wrap CSV values with \". Default: false") do
835
- options[:csv_quotes] = true
836
- end
837
- opts.add_hidden_option('--csv-quotes') if opts.is_a?(MtnCloud::Cli::OptionParser)
838
-
839
- opts.on(nil, '--no-header', "Exclude header for CSV Output.") do
840
- options[:csv_no_header] = true
841
- end
842
-
843
- when :fields
844
- opts.on('-f', '--fields x,y,z', Array, "Filter Output to a limited set of fields. Default is all fields for json,csv,yaml.") do |val|
845
- if val.size == 1 && val[0].downcase == 'all'
846
- options[:all_fields] = true
847
- else
848
- options[:include_fields] = val
849
- end
850
- end
851
- opts.on('-F', '--old-fields x,y,z', Array, "alias for -f, --fields") do |val|
852
- if val.size == 1 && val[0].downcase == 'all'
853
- options[:all_fields] = true
854
- else
855
- options[:include_fields] = val
856
- end
857
- end
858
- opts.add_hidden_option('-F, --old-fields') if opts.is_a?(MtnCloud::Cli::OptionParser)
859
- opts.on('--raw-fields [x,y,z]', String, "Raw fields filters output like --fields except the properties [x,y,z] must be specified from the root of the response instead of relative to the the list or object context for this particular resource.") do |val|
860
- if val.size == 1 && val[0].downcase == 'all'
861
- options[:all_fields] = true
862
- else
863
- options[:include_fields] = val.split(',').collect {|r| r.strip}.compact
864
- end
865
- options[:raw_fields] = true
866
- end
867
- opts.add_hidden_option('--raw-fields') if opts.is_a?(MtnCloud::Cli::OptionParser)
868
- opts.on(nil, '--all-fields', "Show all fields present in the data.") do
869
- options[:all_fields] = true
870
- end
871
- opts.on(nil, '--wrap', "Wrap table columns instead hiding them when terminal is not wide enough.") do
872
- options[:wrap] = true
873
- end
874
- when :select
875
- #opts.add_hidden_option('--all-fields') if opts.is_a?(MtnCloud::Cli::OptionParser)
876
- opts.on('--select x,y,z', String, "Filter Output to just print the value(s) of specific fields.") do |val|
877
- options[:select_fields] = val.split(',').collect {|r| r.strip}
878
- end
879
- opts.on('--raw-select x,y,z', String, "Raw select works like --select except the properties [x,y,z] must be specified from the root of the response instead of relative to the the list or object context for this particular resource.") do |val|
880
- options[:select_fields] = val.split(',').collect {|r| r.strip}
881
- options[:raw_fields] = true
882
- end
883
- opts.add_hidden_option('--raw-select') if opts.is_a?(MtnCloud::Cli::OptionParser)
884
-
885
- when :delim
886
- opts.on('--delimiter [CHAR]', String, "Delimiter for output values. Default: ',', use with --select and --csv") do |val|
887
- options[:csv] = true
888
- options[:format] = :csv
889
- val = val.to_s
890
- val = val.gsub("\\n", "\n").gsub("\\r", "\r").gsub("\\t", "\t") if val.include?("\\")
891
- options[:delim] = val
892
- end
893
-
894
- opts.on('--newline [CHAR]', String, "Delimiter for output rows. Default: '\\n', use with --select and --csv") do |val|
895
- options[:csv] = true
896
- options[:format] = :csv
897
- val = val.to_s
898
- if val == "no" || val == "none"
899
- options[:newline] = ""
900
- else
901
- val = val.to_s.gsub("\\n", "\n").gsub("\\r", "\r").gsub("\\t", "\t") if val.include?("\\")
902
- options[:newline] = val
903
- end
904
- end
905
- when :thin
906
- opts.on( '--thin', '--thin', "Format headers and columns with thin borders." ) do |val|
907
- options[:border_style] = :thin
908
- end
909
-
910
-
911
-
912
- when :dry_run
913
- opts.on('-d','--dry-run', "Dry Run, print the API request instead of executing it.") do
914
- # todo: this should print after parsing obv..
915
- # need a hook after parse! or a standard_handle(options) { ... } paradigm
916
- # either that or hook it up in every command somehow, maybe a hook on connect()
917
- #puts "#{cyan}#{dark} #=> DRY RUN#{reset}"
918
- # don't print this for --json combined with -d
919
- # print once and dont munge json
920
- if !options[:curl] && !options[:json]
921
- puts "#{cyan}#{bold}#{dark}DRY RUN#{reset}"
922
- end
923
- options[:dry_run] = true
924
- end
925
- opts.on(nil,'--curl', "Curl, print the API request as a curl command instead of executing it.") do
926
- # print once and dont munge json
927
- if !options[:dry_run] && !options[:json]
928
- puts "#{cyan}#{bold}#{dark}DRY RUN#{reset}"
929
- end
930
- options[:dry_run] = true
931
- options[:curl] = true
932
- end
933
- opts.on(nil,'--scrub', "Mask secrets in output, such as the Authorization header. For use with --curl and --dry-run.") do
934
- options[:scrub] = true
935
- end
936
- # dry run comes with hidden outfile options
937
- #opts.add_hidden_option('--scrub') if opts.is_a?(MtnCloud::Cli::OptionParser)
938
- opts.on('--out FILE', String, "Write standard output to a file instead of the terminal.") do |val|
939
- # could validate directory is writable..
940
- options[:outfile] = val
941
- end
942
- opts.add_hidden_option('--out') if opts.is_a?(MtnCloud::Cli::OptionParser)
943
- opts.on('--overwrite', '--overwrite', "Overwrite output file if it already exists.") do
944
- options[:overwrite] = true
945
- end
946
- opts.add_hidden_option('--overwrite') if opts.is_a?(MtnCloud::Cli::OptionParser)
947
-
948
- when :outfile
949
- opts.on('--out FILE', String, "Write standard output to a file instead of the terminal.") do |val|
950
- options[:outfile] = val
951
- end
952
- opts.on('--overwrite', '--overwrite', "Overwrite output file if it already exists.") do |val|
953
- options[:overwrite] = true
954
- end
955
- when :quiet
956
- opts.on('-q','--quiet', "No Output, do not print to stdout") do
957
- options[:quiet] = true
958
- end
959
-
960
- else
961
- raise "Unknown common_option key: #{option_key}"
962
- end
963
- end
964
-
965
- # options that are always included
966
-
967
- # always support thin, but hidden because mostly not hooked up at the moment...
968
- unless includes.include?(:thin)
969
- opts.on( '--thin', '--thin', "Format headers and columns with thin borders." ) do |val|
970
- options[:border_style] = :thin
971
- end
972
- opts.add_hidden_option('--thin') if opts.is_a?(MtnCloud::Cli::OptionParser)
973
- end
974
-
975
- # disable ANSI coloring
976
- opts.on('-C','--nocolor', "Disable ANSI coloring") do
977
- Term::ANSIColor::coloring = false
978
- end
979
-
980
-
981
- # Benchmark this command?
982
- # Also useful for seeing exit status for every command.
983
- opts.on('-B','--benchmark', "Print benchmark time and exit/error after the command is finished.") do
984
- options[:benchmark] = true
985
- # this is hacky, but working!
986
- # shell handles returning to false
987
- #MtnCloud::Benchmarking.enabled = true
988
- #my_terminal.benchmarking = true
989
- #start_benchmark(args.join(' '))
990
- # ok it happens outside of handle() alltogether..
991
- # wow, simplify me plz
992
- end
993
-
994
- opts.on('-V','--debug', "Print extra output for debugging.") do
995
- options[:debug] = true
996
- MtnCloud::Logging.set_log_level(MtnCloud::Logging::Logger::DEBUG)
997
- ::RestClient.log = MtnCloud::Logging.debug? ? MtnCloud::Logging::DarkPrinter.instance : nil
998
- # perhaps...
999
- # create a new logger instance just for this command instance
1000
- # this way we don't elevate the global level for subsequent commands in a shell
1001
- # @logger = MtnCloud::Logging::Logger.new(STDOUT)
1002
- # if !@logger.debug?
1003
- # @logger.log_level = MtnCloud::Logging::Logger::DEBUG
1004
- # end
1005
- end
1006
-
1007
- # A way to ensure debugging is off, it should go back on after the command is complete.
1008
- opts.on('--no-debug','--no-debug', "Disable debugging.") do
1009
- options[:debug] = false
1010
- MtnCloud::Logging.set_log_level(MtnCloud::Logging::Logger::INFO)
1011
- ::RestClient.log = MtnCloud::Logging.debug? ? MtnCloud::Logging::DarkPrinter.instance : nil
1012
- end
1013
- opts.add_hidden_option('--no-debug') if opts.is_a?(MtnCloud::Cli::OptionParser)
1014
-
1015
- opts.on('--hidden-help', "Print help that includes all the hidden options, like this one." ) do
1016
- puts opts.full_help_message({show_hidden_options:true})
1017
- exit 0 # return 0 maybe?
1018
- end
1019
- opts.add_hidden_option('--hidden-help') if opts.is_a?(MtnCloud::Cli::OptionParser)
1020
- opts.on('-h', '--help', "Print this help" ) do
1021
- puts opts
1022
- exit 0 # return 0 maybe?
1023
- end
1024
-
1025
- opts
1026
- end
1027
-
1028
- def prog_name
1029
- self.class.prog_name
1030
- end
1031
-
1032
- def command_name
1033
- self.class.command_name
1034
- end
1035
-
1036
- def command_description
1037
- self.class.command_description
1038
- end
1039
-
1040
- def subcommands
1041
- self.class.subcommands
1042
- end
1043
-
1044
- def visible_subcommands
1045
- self.class.visible_subcommands
1046
- end
1047
-
1048
- def subcommand_aliases
1049
- self.class.subcommand_aliases
1050
- end
1051
-
1052
- # def subcommand_descriptions
1053
- # self.class.subcommand_descriptions
1054
- # end
1055
-
1056
- def get_subcommand_description(subcmd)
1057
- self.class.get_subcommand_description(subcmd)
1058
- end
1059
-
1060
- # The name of the command method that called subcommand_usage or
1061
- # subcommand_description, for example "get" when called from Clusters#get.
1062
- # Before Ruby 3.4 the label is "get" or "block in get". From Ruby 3.4 it
1063
- # includes the class, as in "MtnCloud::Cli::Clusters#get".
1064
- def calling_method_name
1065
- label = caller_locations(2, 1)[0].label.to_s
1066
- label = label.sub(/\Ablock (\(\d+ levels\) )?in /, '')
1067
- label.split(/[#.]/).last.to_s
1068
- end
1069
-
1070
- def subcommand_description()
1071
- calling_method = calling_method_name
1072
- subcommand_name = subcommands.key(calling_method)
1073
- subcommand_name ? get_subcommand_description(subcommand_name) : nil
1074
- end
1075
-
1076
- def default_subcommand
1077
- self.class.default_subcommand
1078
- end
1079
-
1080
- def default_refresh_interval
1081
- self.class.default_refresh_interval
1082
- end
1083
-
1084
- def usage
1085
- if !subcommands.empty?
1086
- "Usage: #{prog_name} #{command_name} [command] [options]"
1087
- else
1088
- "Usage: #{prog_name} #{command_name} [options]"
1089
- end
1090
- end
1091
-
1092
- def my_help_command
1093
- "#{prog_name} #{command_name} --help"
1094
- end
1095
-
1096
- def subcommand_usage(*extra)
1097
- calling_method = calling_method_name
1098
- subcommand_name = subcommands.key(calling_method)
1099
- extra = extra.flatten
1100
- if !subcommand_name.empty? && extra.first == subcommand_name
1101
- extra.shift
1102
- end
1103
- #extra = ["[options]"] if extra.empty?
1104
- "Usage: #{prog_name} #{command_name} #{subcommand_name} #{extra.join(' ')}".squeeze(' ').strip
1105
- end
1106
-
1107
- # a string to describe the usage of your command
1108
- # this is what the --help option
1109
- # feel free to override this in your commands
1110
- def full_command_usage
1111
- out = ""
1112
- out << usage.to_s.strip if usage
1113
- out << "\n"
1114
- my_subcommands = visible_subcommands
1115
- if !my_subcommands.empty?
1116
- out << "Commands:"
1117
- out << "\n"
1118
- my_subcommands.sort.each {|subcmd, method|
1119
- desc = get_subcommand_description(subcmd)
1120
- out << "\t#{subcmd.to_s}"
1121
- out << "\t#{desc}" if desc
1122
- out << "\n"
1123
- }
1124
- end
1125
- if command_description && !command_description.to_s.strip.empty?
1126
- out << "\n"
1127
- out << "#{command_description.strip}\n"
1128
- end
1129
- # out << "\n"
1130
- out
1131
- end
1132
-
1133
- # a default handler
1134
- def handle_subcommand(args)
1135
- commands = subcommands
1136
- if subcommands.empty?
1137
- raise "#{self.class} has no available subcommands"
1138
- end
1139
- # meh, could deprecate and make subcommand define handle() itself
1140
- # if args.count == 0 && default_subcommand
1141
- # # p "using default subcommand #{default_subcommand}"
1142
- # return send(default_subcommand, args || [])
1143
- # end
1144
- subcommand_name = args[0]
1145
- if args.empty?
1146
- print_error MtnCloud::Terminal.angry_prompt
1147
- puts_error "[command] argument is required"
1148
- puts full_command_usage
1149
- exit 127
1150
- end
1151
- if args[0] == "-h" || args[0] == "--help" || args[0] == "help"
1152
- puts full_command_usage
1153
- return 0
1154
- end
1155
- if subcommand_aliases[subcommand_name]
1156
- subcommand_name = subcommand_aliases[subcommand_name]
1157
- end
1158
- cmd_method = subcommands[subcommand_name]
1159
- if !cmd_method
1160
- error_msg = "'#{command_name} #{subcommand_name}' is not a known command.\n#{full_command_usage}"
1161
- raise CommandNotFoundError.new(error_msg)
1162
- end
1163
- send(cmd_method, args[1..-1])
1164
- end
1165
-
1166
- def handle(args)
1167
- raise "#{self} has not defined handle()!"
1168
- end
1169
-
1170
- # executes block with each argument in the list
1171
- # @return [0|1] 0 if they were all successful, else 1
1172
- def run_command_for_each_arg(args, &block)
1173
- cmd_results = []
1174
- args.each do |arg|
1175
- begin
1176
- cur_result = yield arg
1177
- rescue SystemExit => err
1178
- cur_result = err.success? ? 0 : 1
1179
- end
1180
- cmd_results << cur_result
1181
- end
1182
- # find a bad result and return it
1183
- cmd_results = cmd_results.collect do |cmd_result|
1184
- if cmd_result.is_a?(Array)
1185
- cmd_result
1186
- else
1187
- [cmd_result, nil]
1188
- end
1189
- end
1190
- failed_result = cmd_results.find {|cmd_result| cmd_result[0] == false || (cmd_result[0].is_a?(Integer) && cmd_result[0] != 0) }
1191
- return failed_result ? failed_result : cmd_results.last
1192
- end
1193
-
1194
- # def connect(options={})
1195
- # MtnCloud::Logging::DarkPrinter.puts "#{command_name} has not defined connect()" if MtnCloud::Logging.debug?
1196
- # end
1197
-
1198
- # This supports the simple remote option eg. `instances add --remote "qa"`
1199
- # It will establish a connection to the pre-configured appliance named "qa"
1200
- # By default it will connect to the active (current) remote appliance
1201
- # This returns a new instance of MtnCloud::APIClient (and sets @access_token, and @appliance)
1202
- # Your command should be ready to make api requests after this.
1203
- # This will prompt for credentials if none are found, use :skip_login
1204
- # Credentials will be saved unless --remote-url or --token is being used.
1205
- def establish_remote_appliance_connection(options)
1206
- # todo: probably refactor and don't rely on this method to set these instance vars
1207
- @remote_appliance = nil
1208
- @appliance_name, @appliance_url, @access_token = nil, nil, nil
1209
- @api_client = nil
1210
- @do_save_credentials = true
1211
- # skip saving if --remote-url or --username or --password are passed in
1212
- if options[:remote_url] || options[:remote_token] || options[:remote_username] || options[:remote_password]
1213
- @do_save_credentials = false
1214
- end
1215
- appliance = nil
1216
- if options[:remote_url]
1217
- # --remote-url means use an arbitrary url, do not save any appliance config
1218
- # appliance = {name:'remote-url', url:options[:remote_url]}
1219
- appliance = {url:options[:remote_url]}
1220
- appliance[:temporary] = true
1221
- #appliance[:status] = "ready" # or "unknown"
1222
- # appliance[:last_check] = nil
1223
- elsif options[:remote]
1224
- # --remote means use the specified remote
1225
- appliance = ::MtnCloud::Cli::Remote.load_remote(options[:remote])
1226
- if appliance.nil?
1227
- if ::MtnCloud::Cli::Remote.appliances.empty?
1228
- raise_command_error "No remote appliances exist, see the command `remote add`."
1229
- else
1230
- raise_command_error "Remote appliance not found by the name '#{options[:remote]}', see `remote list`"
1231
- end
1232
- end
1233
- else
1234
- # use active remote
1235
- appliance = ::MtnCloud::Cli::Remote.load_active_remote()
1236
- if !appliance
1237
- if ::MtnCloud::Cli::Remote.appliances.empty?
1238
- raise_command_error "No remote appliances exist, see the command `remote add`"
1239
- else
1240
- raise_command_error "#{command_name} requires a remote to be specified, try the option -r [remote] or see the command `remote use`"
1241
- end
1242
- end
1243
- end
1244
- @remote_appliance = appliance
1245
- @appliance_name = appliance[:name]
1246
- @appliance_url = appliance[:url] || appliance[:host] # it used to store :host in the YAML
1247
- # set enable_ssl_verification
1248
- # instead of toggling this global value
1249
- # this should just be an attribute of the api client
1250
- # for now, this fixes the issue where passing --insecure or --remote
1251
- # would then apply to all subsequent commands...
1252
- allow_insecure = false
1253
- if options[:insecure] || appliance[:insecure] || MtnCloud::Cli::Shell.insecure
1254
- allow_insecure = true
1255
- end
1256
- @verify_ssl = !allow_insecure
1257
- # MtnCloud::RestClient.enable_ssl_verification = allow_insecure != true
1258
- if allow_insecure && MtnCloud::RestClient.ssl_verification_enabled?
1259
- MtnCloud::RestClient.enable_ssl_verification = false
1260
- elsif !allow_insecure && !MtnCloud::RestClient.ssl_verification_enabled?
1261
- MtnCloud::RestClient.enable_ssl_verification = true
1262
- end
1263
-
1264
- # always support accepting --username and --password on the command line
1265
- # it's probably better not to do that tho, just so it stays out of history files
1266
-
1267
- # if !@appliance_name && !@appliance_url
1268
- # raise_command_error "Please specify a remote appliance with -r or see the command `remote use`"
1269
- # end
1270
-
1271
- MtnCloud::Logging::DarkPrinter.puts "establishing connection to remote #{display_appliance(@appliance_name, @appliance_url)}" if MtnCloud::Logging.debug? # && !options[:quiet]
1272
-
1273
- if options[:no_authorization]
1274
- # maybe handle this here..
1275
- options[:skip_login] = true
1276
- options[:skip_verify_access_token] = true
1277
- end
1278
-
1279
- # ok, get some credentials.
1280
- # use saved credentials by default or prompts for username, password.
1281
- # passing --remote-url will skip loading saved credentials and prompt for login to use with the url
1282
- # passing --token skips login prompting and uses the provided token.
1283
- # passing --token or --username will skip saving credentials to appliance config, they are just used for one command
1284
- # ideally this should not prompt now and wait until the client is used on a protected endpoint.
1285
- # @wallet = nil
1286
- if options[:remote_token]
1287
- @wallet = {'access_token' => options[:remote_token]} #'username' => 'anonymous'
1288
- elsif options[:remote_url]
1289
- credentials = MtnCloud::Cli::Credentials.new(@appliance_name, @appliance_url)
1290
- unless options[:skip_login]
1291
- @wallet = credentials.request_credentials(options.merge({dry_run:false}), @do_save_credentials)
1292
- end
1293
- else
1294
- credentials = MtnCloud::Cli::Credentials.new(@appliance_name, @appliance_url)
1295
- # use saved credentials unless --username or passed
1296
- unless options[:remote_username]
1297
- @wallet = credentials.load_saved_credentials()
1298
- end
1299
- # using active remote OR --remote flag
1300
- # used saved credentials or login
1301
- # ideally this sould not prompt now and wait until the client is used on a protected endpoint.
1302
-
1303
-
1304
- if @wallet.nil? || @wallet['access_token'].nil?
1305
- unless options[:skip_login]
1306
- @wallet = credentials.request_credentials(options.merge({dry_run:false}), @do_save_credentials)
1307
- end
1308
- end
1309
-
1310
- end
1311
- @access_token = @wallet ? @wallet['access_token'] : nil
1312
-
1313
- # validate we have a token
1314
- # hrm...
1315
- unless options[:skip_verify_access_token]
1316
- if @access_token.empty?
1317
- raise AuthorizationRequiredError.new("Failed to acquire access token for #{display_appliance(@appliance_name, @appliance_url)}. Verify your credentials are correct.")
1318
- end
1319
- end
1320
-
1321
- # ok, connect to the appliance.. actually this just instantiates an ApiClient
1322
- api_client = MtnCloud::APIClient.new(@access_token,nil,nil, @appliance_url, @verify_ssl)
1323
- @api_client = api_client # meh, just return w/o setting instance attrs
1324
- return api_client
1325
- end
1326
-
1327
- # verify_args! verifies that the right number of commands were passed
1328
- # and raises a command error if not.
1329
- # Example: verify_args!(args:args, count:1, optparse:optparse)
1330
- # this could go be done in optparse.parse instead perhaps
1331
- def verify_args!(opts={})
1332
- args = opts[:args] || []
1333
- count = opts[:count]
1334
- # simplify output for verify_args!(min:2, max:2) or verify_args!(max:0)
1335
- if opts[:min] && opts[:max] && opts[:min] == opts[:max]
1336
- count = opts[:min]
1337
- elsif opts[:max] == 0
1338
- count = 0
1339
- end
1340
- if count
1341
- if args.count < count
1342
- raise_args_error("not enough arguments, expected #{count} and got #{args.count == 0 ? '0' : args.count.to_s + ': '}#{args.join(', ')}", args, opts[:optparse])
1343
- elsif args.count > count
1344
- raise_args_error("too many arguments, expected #{count} and got #{args.count == 0 ? '0' : args.count.to_s + ': '}#{args.join(', ')}", args, opts[:optparse])
1345
- end
1346
- else
1347
- if opts[:min]
1348
- if args.count < opts[:min]
1349
- raise_args_error("not enough arguments, expected #{opts[:min] || '0'}-#{opts[:max] || 'N'} and got #{args.count == 0 ? '0' : args.count.to_s + ': '}#{args.join(', ')}", args, opts[:optparse])
1350
- end
1351
- end
1352
- if opts[:max]
1353
- if args.count > opts[:max]
1354
- raise_args_error("too many arguments, expected #{opts[:min] || '0'}-#{opts[:max] || 'N'} and got #{args.count == 0 ? '0' : args.count.to_s + ': '}#{args.join(', ')}", args, opts[:optparse])
1355
- end
1356
- end
1357
- end
1358
- true
1359
- end
1360
-
1361
- def confirm(msg, options)
1362
- options[:yes] or ::MtnCloud::Cli::OptionTypes::confirm(msg, options)
1363
- end
1364
-
1365
- def confirm!(msg, options)
1366
- confirm(msg, options) or raise CommandAborted.new("confirmation declined: #{msg}")
1367
- end
1368
-
1369
- # The default way to build options for the list command
1370
- # @param [OptionParser] opts
1371
- # @param [Hash] options
1372
- # @param [Hash] params
1373
- def build_list_options(opts, options, params)
1374
- build_standard_list_options(opts, options)
1375
- end
1376
-
1377
- # The default way to parse options for the list command
1378
- # @param [Array] args
1379
- # @param [Hash] options
1380
- # @param [Hash] params
1381
- def parse_list_options!(args, options, params)
1382
- if args.count > 0
1383
- options[:phrase] = args.join(" ")
1384
- # params['phrase'] = = args.join(" ")
1385
- end
1386
- params.merge!(parse_list_options(options))
1387
- # query parameters are stored in :params
1388
- # preserve anything already set in :params by the OptionParser or command specific logic..
1389
- options[:params] ||= {}
1390
- options[:params].deep_merge!(params)
1391
- return options
1392
- end
1393
-
1394
- # The default way to build options for the list command
1395
- # @param [OptionParser] opts
1396
- # @param [Hash] options
1397
- # @param [Hash] params
1398
- def build_get_options(opts, options, params)
1399
- build_standard_get_options(opts, options)
1400
- end
1401
-
1402
- # The default way to parse options for the get command
1403
- # @param [OptionParser] opts
1404
- # @param [Hash] options
1405
- # @param [Hash] params
1406
- def parse_get_options!(args, options, params)
1407
- params.merge!(parse_query_options(options))
1408
- end
1409
-
1410
- # Add a name=value query parameter, building an array if there is more than one
1411
- # @param [Hash] params the query parameter map to append to
1412
- # @param [String] key the parameter name
1413
- # @param [Object] value the parameter value
1414
- def add_query_parameter(params, key, value)
1415
- # key = key.to_s
1416
- if params.key?(key) && !params[key].nil?
1417
- if !params[key].is_a?(Array)
1418
- params[key] = [params[key]]
1419
- end
1420
- params[key] << value
1421
- else
1422
- params[key] = value
1423
- end
1424
- params
1425
- end
1426
-
1427
- # The default way to parse options for the get command
1428
- # @param type [string]
1429
- # @param options [Hash] The command options
1430
- # @param params [Hash] The query parameters the output is being appended to
1431
- # @param param_name [String]
1432
- # @param lookup_ids [Boolean] Also lookup ids to make sure they exist or else error
1433
- # @return
1434
- def parse_parameter_as_resource_id!(type, options, params, param_name=nil, lookup_ids=false)
1435
- # type = type.to_s.singularize
1436
- if options.key?(type)
1437
- val = options[type].to_s
1438
- param_name ||= "#{type.to_s.camelcase}Id"
1439
- if val
1440
- if val.to_s !~ /\A\d{1,}\Z/ || lookup_ids
1441
- record = find_by_name(type, val)
1442
- if record.nil?
1443
- # avoid double error render by exiting here, ew
1444
- exit 1
1445
- raise_command_error "#{type.titleize} not found for '#{val}'"
1446
- end
1447
- params[param_name] = record['id']
1448
- else
1449
- params[param_name] = val
1450
- end
1451
- return params[param_name]
1452
- end
1453
- end
1454
- return nil
1455
- end
1456
-
1457
- # parse the parameters provided by the common :list options
1458
- # this includes the :query options too via parse_query_options().
1459
- # returns Hash of params the format {"phrase": => "foobar", "max": 100}
1460
- def parse_list_options(options={})
1461
- list_params = {}
1462
- [:phrase, :offset, :max, :sort, :direction, :lastUpdated].each do |k|
1463
- if options.key?(k)
1464
- list_params[k.to_s] = options[k]
1465
- elsif options.key?(k.to_s)
1466
- list_params[k.to_s] = options[k.to_s]
1467
- end
1468
- end
1469
- # arbitrary filters
1470
- list_params.merge!(parse_query_options(options))
1471
- # ok, any string keys in options can become query parameters, eg. options['name'] = 'foobar'
1472
- # do it!
1473
- # options.each do |k, v|
1474
- # list_params[k] = v
1475
- # end
1476
- return list_params
1477
- end
1478
-
1479
- # parse the parameters provided by the common :query (:query_filters) options
1480
- # returns Hash of params the format {"phrase": => "foobar", "max": 100}
1481
- def parse_query_options(options={})
1482
- list_params = {}
1483
- # arbitrary filters
1484
- if options[:query_filters]
1485
- options[:query_filters].each do |k, v|
1486
- if k
1487
- list_params[k] = v
1488
- end
1489
- end
1490
- end
1491
- return list_params
1492
- end
1493
-
1494
- # parse the subtitles provided by the common :list options
1495
- # returns Array of subtitles as strings in the format ["Phrase: blah", "Max: 100"]
1496
- def parse_list_subtitles(options={})
1497
- subtitles = []
1498
- if options[:tenant]
1499
- subtitles << "Tenant: #{options[:tenant]}".strip
1500
- end
1501
- if options[:include_tenants]
1502
- subtitles << "Include Tenants: true".strip
1503
- end
1504
- [:phrase, :offset, :max, :sort, :direction, :lastUpdated].each do |k|
1505
- if options.key?(k)
1506
- subtitles << "#{k.to_s}: #{options[k]}"
1507
- elsif options.key?(k.to_s)
1508
- subtitles << "#{k.to_s}: #{options[k.to_s]}"
1509
- end
1510
- end
1511
- # arbitrary filters
1512
- if options[:query_filters]
1513
- formatted_filters = options[:query_filters].collect {|k,v| "#{k.to_s}=#{v}" }.join('&')
1514
- subtitles << "Query: #{formatted_filters}"
1515
- # options[:query_filters].each do |k, v|
1516
- # subtitles << "#{k.to_s}: #{v}"
1517
- # end
1518
- end
1519
- return subtitles
1520
- end
1521
-
1522
- # Construct the request query parameters from the standard command options
1523
- # This populates :params with a map of parameters.
1524
- # This should replace the use of parse_query_options, parse_list_options and parse_list_options! and parse_get_options
1525
- # which are used everywhere eg. params.merge!(parse_query_options(options))
1526
- def parse_options(options, params={})
1527
- params = params ? params.dup : {}
1528
- # parse_list_options!(args, options, params)
1529
- # merge in options set with -Q max=3, --query max=3&sort=id
1530
- params.deep_merge!(options[:query_filters]) if options[:query_filters]
1531
- # query parameters are stored in :params
1532
- # preserve anything already set in :params by the OptionParser or command specific logic..
1533
- options[:params] ||= {}
1534
- options[:params].deep_merge!(params)
1535
- # (JSON) body parameters (JSON) are stored in :payload
1536
- # if options[:payload]
1537
- # end
1538
- # ok now call execute_request(@api_client.whoami, :get, nil, options)
1539
- return options
1540
- end
1541
-
1542
- # Parse payload(s) from the standard command options or else invoke the given block.
1543
- # First looks for --payload or --payload options and if they are nil then the block is executed to establish the payload
1544
- # By default this also merges all the values passed with -O, --options foo="bar" into payload under the object_key context.
1545
- # and they are merged under the object_key context (if passed). This can be disabled with apply_options: false
1546
- #
1547
- # @param options [Hash] standard command options
1548
- # @option options [Hash] :payload is a Hash of objects to serialize as the payload
1549
- # @option options [Hash] :payloads is an array of payload objects@yield [street_name] Invokes the block with a street name for eac
1550
- # This is silly and should go away, instead you should iterate in the terminal environment
1551
- # @option options [Boolean] :apply_options can be set to false to skip -O options merge
1552
- # @param object_key [String] The name of the object being constructed, -O --options will be merged under this context.
1553
- # @return array of payloads
1554
- # @yield [payload] Invokes the block to establish :payload (only when --payload(s) is not used)
1555
- def parse_payload(options, object_key=nil, &block)
1556
- # populate options[:params] here too
1557
- parse_options(options)
1558
- payloads = []
1559
- # todo: only need to support a a single payload here, :payloads is silly and is going away
1560
- if options[:payload]
1561
- # --payload option was used
1562
- payload = options[:payload]
1563
- # support -O OPTION switch on top of --payload
1564
- apply_options(payload, options, object_key) unless options[:apply_options] == false
1565
- payloads << payload
1566
- elsif options[:payloads]
1567
- # --payloads option was used
1568
- payloads = options[:payloads]
1569
- # support -O OPTION switch on top of --payloads
1570
- payloads.each do |payload|
1571
- apply_options(payload, options, object_key) unless options[:apply_options] == false
1572
- end
1573
- #else
1574
- # should always do this, but a lot of methods rely on this returning nil right now, not {}
1575
- # so for now only do it if block is given
1576
- elsif block_given?
1577
- # yield to block to construct the payload,
1578
- # this is typically where prompting for inputs with optionTypes happens
1579
- payload = {}
1580
- apply_options(payload, options, object_key) unless options[:apply_options] == false
1581
- if block_given?
1582
- yield payload
1583
- end
1584
- payloads << payload
1585
- options[:payload] = payload
1586
- end
1587
- return payloads.first
1588
- end
1589
-
1590
- # support -O OPTION switch
1591
- def apply_options(payload, options, object_key=nil)
1592
- payload ||= {}
1593
- if options[:options]
1594
- # allow options[:object_key] to be used
1595
- object_key = object_key ? object_key : options[:object_key]
1596
- # could use parse_passed_options() here to support exclusion of certain options
1597
- #passed_options = parse_passed_options(options, options[:apply_options] || {})
1598
- passed_options = options[:options].reject {|k,v| k.is_a?(Symbol)}
1599
- if options[:apply_options_exclude]
1600
- passed_options = options[:options].reject {|k,v| options[:skip_apply_options].include?(k.to_s) || options[:skip_apply_options].include?(k.to_sym) }
1601
- end
1602
- if object_key
1603
- payload.deep_merge!({object_key => passed_options})
1604
- else
1605
- payload.deep_merge!(passed_options)
1606
- end
1607
- end
1608
- payload
1609
- end
1610
-
1611
- # Executes the block with each payload (:payload or :payloads as parsed from --payload FILE and --payloads --PATH)
1612
- # This is a wrapper to support execution on 1-N payloads
1613
- # It also looks for --ignore-payload-errors behavior to continue processing
1614
- # It is up to the block to actually make the api request
1615
- # @param options [Hash] standard command options
1616
- # @raise [Error] if there is no :payload or :payloads defined.
1617
- # @yield [payload] Yields each payload to the block
1618
- # @return parsed command result of the last return value of the block ie. [0, nil]
1619
- def handle_each_payload(options, &block)
1620
- payloads = []
1621
- if options[:payloads]
1622
- payloads = options[:payloads]
1623
- elsif options[:payload]
1624
- payloads << options[:payload]
1625
- else
1626
- raise "handle_each_payload() requires :payload or :payloads"
1627
- end
1628
- if !payloads.is_a?(Array) || payloads.compact.empty?
1629
- raise "handle_each_payload() requires a payload"
1630
- end
1631
- if !block_given?
1632
- raise "handle_each_payload() requires a block to process the payload(s) with"
1633
- end
1634
- results = []
1635
- payloads.each do |payload|
1636
- begin
1637
- result = yield payload
1638
- results << MtnCloud::Cli::CliRegistry.parse_command_result(result)
1639
- #results << [0, nil]
1640
- rescue => e
1641
- if options[:payloads_ignore_error]
1642
- # results << [1, e.message]
1643
- result = MtnCloud::Cli::ErrorHandler.new(my_terminal.stderr).handle_error(e) # lol
1644
- results << result
1645
- # continue
1646
- else
1647
- raise e
1648
- end
1649
- end
1650
- end
1651
- return results.last
1652
- end
1653
-
1654
- # Standard handler for all commands that execute an api request.
1655
- # This looks for a payload that can be set with --payload or --payloads or the default prompting
1656
- # It is up to the block to handle the rendering behavior
1657
- # @param api_interface [APIClient] An APIClient instance
1658
- # @param api_method [String or Symbol] api method to invoke eg. :get, :create, :update, :destroy
1659
- # @param args [Array] Array of arguments to be passed to the api method, usually just the [payload] or [payload, query_params]
1660
- # @param options [Hash] options
1661
- # @param object_key [String or Symbol] name of object being constructed, used by default rendering eg. --fields id,name
1662
- # @yield [json_response] Invokes the block with the json response to handle rendering.
1663
- # @return parsed command result of the last block.call(json_response)
1664
- #
1665
- # @example Fetch first 100 backups
1666
- # execute_api(@api_client.backups, :list, [{"max" => 100}], options) do |json_response|
1667
- # print_green_success "Fetched first #{json_response['backups'].size} of #{json_response['meta']['total']} backups"
1668
- # end
1669
- #
1670
- # @example Create a backup, uses POST with options[:payload] as the body
1671
- # @options[:payload] = {"backup" => { }}
1672
- # execute_api(@api_client.backups, :create, nil, options, 'backup') do |json_response|
1673
- # print_green_success "Added backup #{json_response['backup']['name']}"
1674
- # end
1675
- #
1676
- def execute_api(api_interface, api_method, args, options, object_key=nil, &block)
1677
- args = args.is_a?(Array) ? args : [args].compact
1678
- if options[:payload] || options[:payloads]
1679
- execute_api_payload(api_interface, api_method, args, options, object_key, &block)
1680
- else
1681
- execute_api_request(api_interface, api_method, args, options, object_key, &block)
1682
- end
1683
- end
1684
-
1685
- # Standard handler for all POST commands that send a request for a payload
1686
- # This supports the --payloads option for 1-N payloads, that is silly and will probably go away
1687
- def execute_api_payload(api_interface, api_method, args, options, object_key=nil, &block)
1688
- handle_each_payload(options) do |payload|
1689
- execute_api_request(api_interface, api_method, (args || []) + [payload], options, object_key, &block)
1690
- end
1691
- end
1692
-
1693
- # Standard handler for executing any API request
1694
- # Supports the --dry-run option and standard rendering options --json, --yaml, --fields, --select, etc.
1695
- def execute_api_request(api_interface, api_method, args, options, object_key=nil, &block)
1696
- args = args.is_a?(Array) ? args : [args].compact # allow caller to pass [payload] or payload
1697
- api_interface.setopts(options) # this is needed to support --timeout and --headers
1698
- # this assumes the interface parameter order is: [payload, params] and not vice versa
1699
- if options[:params] && !options[:params].empty?
1700
- args << options[:params]
1701
- end
1702
- if options[:dry_run]
1703
- # this is a dry run
1704
- dry_response = api_interface.dry.send(api_method, *args)
1705
- print_dry_run(dry_response, options)
1706
- return 0, nil
1707
- else
1708
- # execute the request and render the result
1709
- json_response = api_interface.send(api_method, *args)
1710
- return render_response(json_response, options, object_key, &block)
1711
- end
1712
- end
1713
-
1714
- # Parse an array from a string (csv)
1715
- def parse_array(val, opts={})
1716
- opts = {strip:true, allow_blank:false}.merge(opts)
1717
- values = []
1718
- if val.is_a?(Array)
1719
- values = val
1720
- else
1721
- # parse csv and strip white space by default
1722
- values = val.to_s.split(",")#.collect {|it| it.to_s.strip }
1723
- if opts[:strip]
1724
- values = values.collect {|it| it.to_s.strip }
1725
- end
1726
- if !opts[:allow_blank]
1727
- values.reject! {|it| it.to_s.strip.empty? }
1728
- end
1729
- end
1730
- values
1731
- end
1732
-
1733
- def parse_labels(val)
1734
- parse_array(val, {strip:true, allow_blank:false})
1735
- end
1736
-
1737
- def validate_outfile(outfile, options)
1738
- full_filename = File.expand_path(outfile)
1739
- outdir = File.dirname(full_filename)
1740
- if Dir.exist?(full_filename)
1741
- print_red_alert "[local-file] is invalid. It is the name of an existing directory: #{outfile}"
1742
- return false
1743
- end
1744
- if !Dir.exist?(outdir)
1745
- if options[:mkdir]
1746
- print cyan,"Creating local directory #{outdir}",reset,"\n"
1747
- FileUtils.mkdir_p(outdir)
1748
- else
1749
- print_red_alert "[local-file] is invalid. Directory not found: #{outdir}"
1750
- return false
1751
- end
1752
- end
1753
- if File.exist?(full_filename) && !options[:overwrite]
1754
- print_red_alert "[local-file] is invalid. File already exists: #{outfile}\nUse -f to overwrite the existing file."
1755
- return false
1756
- end
1757
- return true
1758
- end
1759
-
1760
- # basic rendering for options :json, :yml, :csv, :quiet, and :outfile
1761
- # returns the string rendered, or nil if nothing was rendered.
1762
- def render_response(json_response, options, object_key=nil, &block)
1763
- # allow options[:object_key] to be used
1764
- object_key = object_key ? object_key : options[:object_key]
1765
- output = nil
1766
- if options[:select_fields]
1767
- # support foos get --raw-select foo.x,foo.y,foo.z
1768
- # and foos list --raw-select foos.x,foos.y,foos.z
1769
- row = (object_key && !options[:raw_fields]) ? json_response[object_key] : json_response
1770
- records = [row].flatten()
1771
- # look for an array in the first field only now...
1772
- field_parts = options[:select_fields][0].to_s.split(".")
1773
- field_context = field_parts[0]
1774
- context_data = json_response[field_context]
1775
- if field_parts.size > 1 && context_data.is_a?(Array)
1776
- # inject all the root level properties to be selectable too..
1777
- context_data = json_response.delete(field_context)
1778
- # records = context_data
1779
- records = context_data.collect {|it| it.is_a?(Hash) ? json_response.merge(it) : json_response }
1780
- options[:select_fields] = options[:select_fields].collect {|it| it.sub(field_context+'.', '')}
1781
- end
1782
- output = records.collect { |record|
1783
- options[:select_fields].collect { |field|
1784
- value = get_object_value(record, field)
1785
- if value.is_a?(String)
1786
- value
1787
- else
1788
- if options[:json]
1789
- as_json(value, options)
1790
- elsif options[:yaml]
1791
- output = as_yaml(value, options)
1792
- elsif options[:csv]
1793
- as_csv(value, nil, options)
1794
- else
1795
- # default behavior
1796
- # value.is_a?(String) ? value : as_json(value, options)
1797
- do_pretty = options.key?(:pretty_json) ? options[:pretty_json] : false # or true?
1798
- do_pretty ? JSON.pretty_generate(value) : JSON.fast_generate(value)
1799
- end
1800
- end
1801
- }.join(options[:delim] || ",")
1802
- }.join(options[:newline] || "\n")
1803
- elsif options[:json]
1804
- output = as_json(json_response, options, object_key)
1805
- elsif options[:yaml]
1806
- output = as_yaml(json_response, options, object_key)
1807
- elsif options[:csv]
1808
- output = as_csv(json_response, nil, options, object_key)
1809
- end
1810
- if options[:outfile]
1811
- full_outfile = File.expand_path(options[:outfile])
1812
- if output
1813
- print_to_file(output, options[:outfile], options[:overwrite])
1814
- print "#{cyan}Wrote output to file #{options[:outfile]} (#{format_bytes(File.size(full_outfile))})\n" unless options[:quiet]
1815
- else
1816
- # uhhh ok lets try this
1817
- MtnCloud::Logging::DarkPrinter.puts "using experimental feature: --out without a common format like json, yml or csv" if MtnCloud::Logging.debug?
1818
- result = with_stdout_to_file(options[:outfile], options[:overwrite], 'w+', &block)
1819
- if result && result != 0
1820
- return result
1821
- end
1822
- print "#{cyan}Wrote output to file #{options[:outfile]} (#{format_bytes(File.size(full_outfile))})\n" unless options[:quiet]
1823
- return 0, nil
1824
- end
1825
- else
1826
- # --quiet means do not render, still want to print to outfile though
1827
- if options[:quiet]
1828
- return 0, nil
1829
- end
1830
- # render ouput generated above
1831
- if output
1832
- puts output
1833
- return 0, nil
1834
- else
1835
- # no render happened, so calling the block if given
1836
- if block_given?
1837
- result = yield json_response
1838
- if result
1839
- return result
1840
- else
1841
- return 0, nil
1842
- end
1843
- else
1844
- # nil means nothing was rendered, some methods still using render_with_format() are relying on this
1845
- return nil
1846
- end
1847
- end
1848
- end
1849
- end
1850
-
1851
- alias :render_with_format :render_response
1852
-
1853
- # Dynamic find methods to load a record of any type
1854
- # def find_by_name_or_id(type, val)
1855
- # interface = instance_variable_get "@#{type}s_interface"
1856
- # typeCamelCase = type.gsub(/(?:^|_)([a-z])/) do $1.upcase end
1857
- # typeCamelCase = typeCamelCase[0, 1].downcase + typeCamelCase[1..-1]
1858
- # (val.to_s =~ /\A\d{1,}\Z/) ? interface.get(val.to_i)[typeCamelCase] : interface.list({'name' => val})["#{typeCamelCase}s"].first
1859
- # end
1860
-
1861
- # Find a resource by type and name or id
1862
- # @param type [String of Symbol] Type of resource formatted as singular, lowerscore with underscores.
1863
- # @param *id [String or Numeric] ID of resource, multiple arguments may be passed when using a secondary interface where parent_id, id is required.
1864
- # Example: find_by_name_or_id("instance", "K2")
1865
- # find_by_name_or_id("storage_volume", 42)
1866
- # find_by_name_or_id("instance", "My Instance")
1867
- # find_by_name_or_id("load_balancer_pool", load_balancer_id, id)
1868
- def find_by_name_or_id(*args)
1869
- val = args.last
1870
- if val.to_s =~ /\A\d{1,}\Z/
1871
- return find_by_id(*args)
1872
- else
1873
- return find_by_name(*args)
1874
- end
1875
- end
1876
-
1877
-
1878
- # Find a resource by type and id
1879
- # Usage: find_by_name_or_id("app", 3)
1880
- def find_by_id(*args)
1881
- #MtnCloud::Logging::DarkPrinter.puts "find_by_id(#{args.join(', ')})" if MtnCloud::Logging.debug?
1882
- # type, ids = args.first, args[1..-1]
1883
- type, *ids = args
1884
- type = type.to_s.singularize.underscore
1885
- # still relying on the command or helper to define these _label and _key methods
1886
- # label = send("#{type}_label")
1887
- # object_key = send("#{type}_object_key")
1888
- # ^ nope, not for long!
1889
- object_key = respond_to?("#{type}_object_key", true) ? send("#{type}_object_key") : type.camelcase.singularize
1890
- label = respond_to?("#{type}_label", true) ? send("#{type}_label") : type.titleize
1891
- interface_name = "@#{type.pluralize}_interface"
1892
- interface = instance_variable_get(interface_name)
1893
- if interface.nil?
1894
- api_client = instance_variable_get("@api_client")
1895
- if api_client
1896
- if api_client.respond_to?(type.pluralize)
1897
- interface = api_client.send(type.pluralize)
1898
- else
1899
- raise "@api_client.#{type.pluralize} is not a recognized interface"
1900
- end
1901
- else
1902
- raise "#{self.class} has not defined interface #{interface_name} or @api_client"
1903
- end
1904
- end
1905
- begin
1906
- json_response = interface.get(*ids)
1907
- if !json_response.key?(object_key)
1908
- # maybe just use the first key like this:
1909
- # object_key = json_response.keys.find { |k| json_response[k].is_a?(Hash) }
1910
- #puts_error(json_response) if MtnCloud::Logging.debug?
1911
- raise "API response is missing object property '#{object_key}'"
1912
- end
1913
- record = json_response[object_key]
1914
- if record.nil?
1915
- print_red_alert "#{label} not found in API response (#{object_key})"
1916
- return nil
1917
- else
1918
- return json_response[object_key]
1919
- end
1920
- rescue ::RestClient::Exception => e
1921
- if e.response && e.response.code == 404
1922
- print_red_alert "#{label} not found by id #{ids.last}"
1923
- return nil
1924
- else
1925
- raise e
1926
- end
1927
- end
1928
- end
1929
-
1930
- # Find a record by type and name
1931
- # Usage: find_by_name_or_id("network", "Skynet")
1932
- def find_by_name(*args)
1933
- #MtnCloud::Logging::DarkPrinter.puts "find_by_name(#{args.join(', ')})" if MtnCloud::Logging.debug?
1934
- # type, ids = args.first, args[1..-1]
1935
- type, *ids = args
1936
- type = type.to_s.singularize.underscore
1937
- val = ids.pop
1938
- params = {}
1939
- name_property = 'name'
1940
- if type == 'user'
1941
- name_property = 'username'
1942
- params['global'] = 'true'
1943
- end
1944
- params[name_property] = val.to_s
1945
- request_args = ids + [params]
1946
- request_args.unshift(type)
1947
- records = find_all(*request_args)
1948
- # still relying on the command or helper to define these _label and _key methods
1949
- label = respond_to?("#{type}_label", true) ? send("#{type}_label") : type.titleize
1950
- if records.empty?
1951
- print_red_alert "#{label} not found by name '#{val}'"
1952
- return nil
1953
- elsif records.size > 1
1954
- print_red_alert "More than one #{label.downcase} found by #{name_property} '#{val}'"
1955
- print_error "\n"
1956
- if type == "user"
1957
- puts_error as_pretty_table(records, [:id, :username, {"FIRST NAME" => "firstName"}, {"LAST NAME" => "lastName"}, {"TENANT" => lambda {|it| it['account']['name'] rescue ''}}], {color:red})
1958
- else
1959
- puts_error as_pretty_table(records, [:id, :name], {color:red})
1960
- end
1961
- print_red_alert "Try using ID instead"
1962
- print_error reset,"\n"
1963
- return nil
1964
- else
1965
- return records[0]
1966
- end
1967
- end
1968
-
1969
- # Load a list of records by type
1970
- # @example Find an app by id
1971
- # find_record(:app, 1)
1972
- # report_types = find_all("reportTypes", {phrase:"amazon"})
1973
- # pools = find_all("loadBalancerPool", load_balancer_id)
1974
- # @return Array of records
1975
- def find_all(*args)
1976
- #MtnCloud::Logging::DarkPrinter.puts "find_all(#{args.join(', ')})" if MtnCloud::Logging.debug?
1977
- type, *request_args = args
1978
- type = type.to_s.singularize.underscore
1979
- list_key = respond_to?("#{type}_list_key", true) ? send("#{type}_list_key") : get_list_key(type)
1980
- json_response = find_all_json(*args)
1981
- if !json_response.key?(list_key)
1982
- # maybe just use the first key like this:
1983
- # list_key = json_response.keys.find { |k| json_response[k].is_a?(Array) }
1984
- # puts_error(json_response) if MtnCloud::Logging.debug?
1985
- raise "API response is missing list property '#{list_key}'"
1986
- end
1987
- return json_response[list_key]
1988
- end
1989
-
1990
- # Load json response for a list of records by type
1991
- # @return Hash of JSON data
1992
- def find_all_json(*args)
1993
- type, *request_args = args
1994
- get_interface(type).list(*request_args)
1995
- end
1996
-
1997
- alias :find_all_records :find_all
1998
- alias :find_all_records_json :find_all_json
1999
-
2000
- # Load a single record (Hash) by type and id and optional parameters
2001
- # Examples:
2002
- # apps = find_record(:app)
2003
- # report_types = find_record("reportTypes", {phrase:"amazon"})
2004
- # pools = find_record("loadBalancerPool", balancer_id)
2005
- # @return [Hash] of the object that was found or raises an exception if 404 not found encountered.
2006
- def find_record(*args)
2007
- #MtnCloud::Logging::DarkPrinter.puts "find_record(#{args.join(', ')})" if MtnCloud::Logging.debug?
2008
- type, *request_args = args
2009
- type = type.to_s.singularize.underscore
2010
- object_key = respond_to?("#{type}_object_key", true) ? send("#{type}_object_key") : get_object_key(type)
2011
- json_response = find_record_json(*args)
2012
- if !json_response.key?(object_key)
2013
- # maybe just use the first key like this:
2014
- # object_key = json_response.keys.find { |k| json_response[k].is_a?(Hash) }
2015
- # puts_error(json_response) if MtnCloud::Logging.debug?
2016
- raise "API response is missing object property '#{object_key}'"
2017
- end
2018
- return json_response[object_key]
2019
- end
2020
-
2021
- # Load list of records by type and (optional) parameters
2022
- # Examples:
2023
- # apps = find_record(:app, 1)
2024
- # report_types = find_record("reportType", 1)
2025
- # pools = find_all("loadBalancerPool", load_balancer_id)
2026
- def find_record_json(*args)
2027
- #MtnCloud::Logging::DarkPrinter.puts "find_record_json(#{args.join(', ')})" if MtnCloud::Logging.debug?
2028
- type, *request_args = args
2029
- get_interface(type).get(*request_args)
2030
- end
2031
-
2032
- # Load json response for a list of records by type
2033
- def get_interface(type)
2034
- #MtnCloud::Logging::DarkPrinter.puts "get_interface(#{type})" if MtnCloud::Logging.debug?
2035
- # todo: can probably just do @api_client ? @api_client.interface(interface_name) : nil
2036
- type = type.to_s.singularize.underscore
2037
- interface_name = "@#{type.pluralize}_interface"
2038
- interface = nil
2039
- if instance_variable_defined?(interface_name)
2040
- interface = instance_variable_get(interface_name)
2041
- if interface.nil?
2042
- # Fix is to update connect() to do @apps_interface = @api_client.apps
2043
- raise "API Interface #{interface_name} is nil"
2044
- end
2045
- else
2046
- if @api_client.is_a?(MtnCloud::APIClient)
2047
- interface = @api_client.interface(type.pluralize)
2048
- else
2049
- raise "#{self.class} has not initalized @api_client"
2050
- end
2051
- end
2052
- return interface
2053
- end
2054
-
2055
- def get_list_key(type)
2056
- return get_object_key(type).pluralize
2057
- end
2058
-
2059
- def get_object_key(type)
2060
- key = type.camelcase.singularize
2061
- # add aliases here as needed
2062
- if key == "cloud"
2063
- key = "zone"
2064
- elsif key == "site"
2065
- key = "group"
2066
- elsif key == "backupJob"
2067
- key = "job"
2068
- elsif key == "backupResult"
2069
- key = "result"
2070
- elsif key == "backupRestore"
2071
- key = "restore"
2072
- end
2073
- return key
2074
- end
2075
-
2076
- module ClassMethods
2077
-
2078
- # attr_writer :command_name, :command_description, :hidden_command, :default_refresh_interval,
2079
- # :subcommands, :hidden_subcommands, :default_subcommand, :subcommand_aliases, :subcommand_descriptions
2080
-
2081
- def prog_name
2082
- MtnCloud::Cli.prog_name
2083
- end
2084
-
2085
- def set_command_name(cmd_name)
2086
- @command_name = cmd_name.to_sym
2087
- MtnCloud::Cli::CliRegistry.add(self, self.command_name)
2088
- end
2089
-
2090
- def default_command_name
2091
- class_name = self.name.split('::')[-1]
2092
- #class_name.sub!(/Command$/, '')
2093
- MtnCloud::Cli::CliRegistry.cli_ize(class_name).to_sym
2094
- end
2095
-
2096
- def command_name
2097
- @command_name ||= default_command_name
2098
- @command_name
2099
- end
2100
-
2101
- def set_command_hidden(val=true)
2102
- @hidden_command = val
2103
- end
2104
- # alias :command_name= :set_command_name
2105
-
2106
- def hidden_command
2107
- defined?(@hidden_command) && @hidden_command == true
2108
- end
2109
-
2110
- def hidden_subcommands
2111
- @hidden_subcommands ||= []
2112
- end
2113
-
2114
- def set_subcommands_hidden(*cmds)
2115
- @hidden_subcommands ||= []
2116
- cmds.flatten.each do |cmd|
2117
- @hidden_subcommands << cmd.to_sym
2118
- end
2119
- @hidden_subcommands
2120
- end
2121
-
2122
- def command_description
2123
- @command_description ||= ""
2124
- end
2125
-
2126
- def set_command_description(val)
2127
- @command_description = val
2128
- end
2129
- # alias :command_description= :set_command_description
2130
-
2131
- def default_refresh_interval
2132
- @default_refresh_interval ||= 30
2133
- end
2134
-
2135
- def set_default_refresh_interval(seconds)
2136
- @default_refresh_interval = seconds
2137
- end
2138
- #alias :default_refresh_interval= :set_default_refresh_interval
2139
-
2140
-
2141
- # construct map of command name => instance method
2142
- def register_subcommands(*cmds)
2143
- @subcommands ||= {}
2144
- cmds.flatten.each {|cmd|
2145
- if cmd.is_a?(Hash)
2146
- cmd.each {|k,v|
2147
- # @subcommands[k] = v
2148
- add_subcommand(k.to_s, v.to_s)
2149
- }
2150
- elsif cmd.is_a?(Array)
2151
- cmd.each {|it| register_subcommands(it) }
2152
- elsif cmd.is_a?(String) || cmd.is_a?(Symbol)
2153
- #k = MtnCloud::Cli::CliRegistry.cli_ize(cmd)
2154
- k = cmd.to_s.gsub('_', '-')
2155
- v = cmd.to_s.gsub('-', '_')
2156
- register_subcommands({(k) => v})
2157
- else
2158
- raise MtnCloud::Cli::CliRegistry::BadCommandDefinition.new("Unable to register command of type: #{cmd.class} #{cmd}")
2159
- end
2160
- }
2161
- return
2162
- end
2163
-
2164
- # this might be the new hotness
2165
- # register_subcommand(:show) # do not do this, always define a description!
2166
- # register_subcommand(:list, "List things")
2167
- # register_subcommand("update-all", "update_all", "Update all things")
2168
- # If the command name =~ method, no need to pass both
2169
- # command names will have "-" swapped in for "_" and vice versa for method names.
2170
- def register_subcommand(*args)
2171
- args = args.flatten
2172
- cmd_name = args[0]
2173
- cmd_method = nil
2174
- cmd_desc = nil
2175
- if args.count == 1
2176
- cmd_method = cmd_name
2177
- elsif args.count == 2
2178
- if args[1].is_a?(Symbol)
2179
- cmd_method = args[1]
2180
- else
2181
- cmd_method = cmd_name
2182
- cmd_desc = args[1]
2183
- end
2184
- elsif args.count == 3
2185
- cmd_method = args[1]
2186
- cmd_desc = args[2]
2187
- else
2188
- raise MtnCloud::Cli::CliRegistry::BadCommandDefinition.new("register_subcommand expects 1-3 arguments, got #{args.size} #{args.inspect}")
2189
- end
2190
- cmd_name = cmd_name.to_s.gsub("_", "-").to_sym
2191
- cmd_method = (cmd_method || cmd_name).to_s.gsub("-", "_").to_sym
2192
- cmd_definition = {(cmd_name) => cmd_method}
2193
- register_subcommands(cmd_definition)
2194
- add_subcommand_description(cmd_name, cmd_desc)
2195
- return
2196
- end
2197
-
2198
- def set_default_subcommand(cmd)
2199
- @default_subcommand = cmd
2200
- end
2201
-
2202
- def default_subcommand
2203
- @default_subcommand
2204
- end
2205
-
2206
- def subcommands
2207
- @subcommands ||= {}
2208
- end
2209
-
2210
- def visible_subcommands
2211
- cmds = subcommands.clone
2212
- hidden_subcommands.each do |hidden_cmd|
2213
- cmds.delete(hidden_cmd.to_s.gsub('_', '-'))
2214
- end
2215
- cmds
2216
- end
2217
-
2218
- def has_subcommand?(cmd_name)
2219
- return false if cmd_name.empty?
2220
- @subcommands && @subcommands[cmd_name.to_s]
2221
- end
2222
-
2223
- def add_subcommand(cmd_name, method)
2224
- @subcommands ||= {}
2225
- @subcommands[cmd_name.to_s] = method
2226
- end
2227
-
2228
- def remove_subcommand(cmd_name)
2229
- @subcommands ||= {}
2230
- @subcommands.delete(cmd_name.to_s)
2231
- end
2232
-
2233
- # register an alias for a command
2234
- def alias_subcommand(alias_cmd_name, cmd_name)
2235
- add_subcommand_alias(alias_cmd_name.to_s, cmd_name.to_s.gsub('_', '-'))
2236
- return
2237
- end
2238
-
2239
- def subcommand_aliases
2240
- @subcommand_aliases ||= {}
2241
- end
2242
-
2243
- def add_subcommand_alias(alias_cmd_name, cmd_name)
2244
- @subcommand_aliases ||= {}
2245
- @subcommand_aliases[alias_cmd_name.to_s] = cmd_name
2246
- end
2247
-
2248
- def remove_subcommand_alias(alias_cmd_name)
2249
- @subcommand_aliases ||= {}
2250
- @subcommand_aliases.delete(alias_cmd_name.to_s)
2251
- end
2252
-
2253
- def subcommand_descriptions
2254
- @subcommand_descriptions ||= {}
2255
- end
2256
-
2257
- def add_subcommand_description(cmd_name, description)
2258
- @subcommand_descriptions ||= {}
2259
- @subcommand_descriptions[cmd_name.to_s.gsub('_', '-')] = description
2260
- end
2261
-
2262
- def get_subcommand_description(cmd_name)
2263
- desc = subcommand_descriptions[cmd_name.to_s.gsub('_', '-')]
2264
- if desc
2265
- return desc
2266
- else
2267
- cmd_method = subcommands.key(cmd_name)
2268
- return cmd_method ? subcommand_descriptions[cmd_method.to_s.gsub('_', '-')] : nil
2269
- end
2270
- end
2271
-
2272
- def set_subcommand_descriptions(cmd_map)
2273
- cmd_map.each do |cmd_name, description|
2274
- add_subcommand_description(cmd_name, description)
2275
- end
2276
- end
2277
-
2278
- end
2279
- end
2280
- end
2281
- end
1
+ require 'mtncloud/logging'
2
+ require 'mtncloud/benchmarking'
3
+ require 'mtncloud/terminal'
4
+ require 'mtncloud/cli/cli_registry'
5
+ require 'mtncloud/cli/option_parser'
6
+ require 'mtncloud/cli/option_types'
7
+ require 'mtncloud/cli/mixins/print_helper'
8
+ require 'mtncloud/cli/credentials'
9
+ #require 'mtncloud/cli/commands/shell'
10
+ #require 'mtncloud/cli/commands/remote'
11
+ require 'mtncloud/api/api_client'
12
+
13
+ module MtnCloud
14
+ module Cli
15
+ # Module to be included by every CLI command so that commands get registered
16
+ # This mixin defines a print and puts method, and delegates
17
+ # todo: use delegate
18
+ module CliCommand
19
+
20
+ def self.included(base)
21
+ base.send :include, MtnCloud::Cli::PrintHelper
22
+ base.send :include, MtnCloud::Cli::PromptHelper
23
+ base.send :include, MtnCloud::Benchmarking::HasBenchmarking
24
+ base.extend ClassMethods
25
+ MtnCloud::Cli::CliRegistry.add(base, base.command_name)
26
+ end
27
+
28
+ # the beginning of instance variables from optparse !
29
+
30
+ # this setting makes it easy for the called to disable prompting
31
+ attr_reader :no_prompt
32
+
33
+ # @return [MtnCloud::Terminal] the terminal this command is being executed inside of
34
+ def my_terminal
35
+ @my_terminal ||= MtnCloud::Terminal.instance
36
+ end
37
+
38
+ # set the terminal running this command.
39
+ # @param term [MtnCloudTerminal] the terminal this command is assigned to
40
+ # @return the Terminal this command is being executed inside of
41
+ def my_terminal=(term)
42
+ if !term.is_a?(MtnCloud::Terminal)
43
+ raise "CliCommand (#{self.class}) my_terminal= expects object of type Terminal and instead got a #{term.class}"
44
+ end
45
+ @my_terminal = term
46
+ end
47
+
48
+
49
+ # delegate :print, to: :my_terminal
50
+ # delegate :puts, to: :my_terminal
51
+ # or . . . bum bum bummm
52
+ # a paradigm shift away from include and use module functions instead.
53
+ # module_function :print, puts
54
+ # delegate :puts, to: :my_terminal
55
+
56
+ def print(*msgs)
57
+ my_terminal.stdout.print(*msgs)
58
+ end
59
+
60
+ def println(*msgs)
61
+ print(*msgs)
62
+ print "\n"
63
+ end
64
+
65
+ def puts(*msgs)
66
+ my_terminal.stdout.puts(*msgs)
67
+ end
68
+
69
+ def print_error(*msgs)
70
+ my_terminal.stderr.print(*msgs)
71
+ end
72
+
73
+ def puts_error(*msgs)
74
+ my_terminal.stderr.puts(*msgs)
75
+ end
76
+
77
+ # todo: customizable output color, other than cyan.
78
+ # def terminal_fg
79
+ # end
80
+ # def cyan
81
+ # Term::ANSIColor.black
82
+ # end
83
+
84
+ # todo: use terminal.stdin
85
+ # def readline(*msgs)
86
+ # @my_terminal.stdin.readline(*msgs)
87
+ # end
88
+
89
+ # todo: maybe...
90
+ # disabled prompting for this command
91
+ # def noninteractive()
92
+ # @no_prompt = true
93
+ # self
94
+ # end
95
+
96
+ # whether to prompt or not, this is true by default.
97
+ def interactive?
98
+ @no_prompt != true
99
+ end
100
+
101
+ def raise_command_error(msg, args=[], optparse=nil, exit_code=nil)
102
+ raise MtnCloud::Cli::CommandError.new(msg, args, optparse, exit_code)
103
+ end
104
+
105
+ def raise_args_error(msg, args=[], optparse=nil, exit_code=nil)
106
+ raise MtnCloud::Cli::CommandArgumentsError.new(msg, args, optparse, exit_code)
107
+ end
108
+
109
+ # parse_id_list splits returns the given id_list with its values split on a comma
110
+ # your id values cannot contain a comma, atm...
111
+ # @param id_list [String or Array of Strings]
112
+ # @param delim [String] Default is a comma and any surrounding white space.
113
+ # @return array of values
114
+ def parse_id_list(id_list, delim=/\s*\,\s*/)
115
+ [id_list].flatten.collect {|it| it ? it.to_s.split(delim) : nil }.flatten.compact
116
+ end
117
+
118
+ def parse_bytes_param(bytes_param, option, assumed_unit = nil, allow_zero = false)
119
+ if bytes_param && ( bytes_param.to_f > 0 || ( allow_zero && bytes_param.to_i == 0 ))
120
+ bytes_param.upcase!
121
+ multiplier = 1
122
+ unit = nil
123
+ number = (bytes_param.to_f == bytes_param.to_i ? bytes_param.to_i : bytes_param.to_f)
124
+ if (bytes_param.end_with? 'GB') || ((!bytes_param.end_with? 'MB') && assumed_unit == 'GB')
125
+ unit = 'GB'
126
+ multiplier = 1024 * 1024 * 1024
127
+ elsif (bytes_param.end_with? 'MB') || assumed_unit == 'MB'
128
+ unit = 'MB'
129
+ multiplier = 1024 * 1024
130
+ end
131
+ return {:bytes_param => bytes_param, :bytes => number * multiplier, :number => number, :multiplier => multiplier, :unit => unit}
132
+ end
133
+ raise_command_error "Invalid value for #{option} option"
134
+ end
135
+
136
+ # this returns all the options passed in by -O, parsed all nicely into objects.
137
+ def parse_passed_options(options, parse_opts={})
138
+ excludes = [parse_opts[:exclude], parse_opts[:excludes]].flatten.compact
139
+ passed_options = options[:options] ? options[:options].reject {|k,v| k.is_a?(Symbol) || excludes.include?(k) } : {}
140
+ return passed_options
141
+ end
142
+
143
+ # Appends Array of OptionType definitions to an OptionParser instance
144
+ # This adds an option like --fieldContext.fieldName="VALUE"
145
+ # @param opts [OptionParser]
146
+ # @param options [Hash] output map that is being constructed
147
+ # @param option_types [Array] list of OptionType definitions to add
148
+ # @return void, this modifies the opts in place.
149
+ def build_option_type_options(opts, options, option_types=[])
150
+ #opts.separator ""
151
+ #opts.separator "Options:"
152
+ options[:options] ||= {} # this is where these go..for now
153
+ options[:option_types] = (options[:option_types] || []) + option_types
154
+ custom_options = options[:options]
155
+
156
+ # add each one to the OptionParser
157
+ option_types.each do |option_type|
158
+ # skip hidden types
159
+ if option_type['type'] == 'hidden'
160
+ next
161
+ end
162
+ if option_type['fieldName'].empty?
163
+ puts_error "Missing fieldName for option type: #{option_type}" if MtnCloud::Logging.debug?
164
+ next
165
+ end
166
+ full_field_name = option_type['fieldContext'].to_s.empty? ? option_type['fieldName'] : "#{option_type['fieldContext']}.#{option_type['fieldName']}"
167
+ field_namespace = full_field_name.split(".")
168
+ field_name = field_namespace.pop
169
+
170
+ description = "#{option_type['fieldLabel']}#{option_type['fieldAddOn'] ? ('(' + option_type['fieldAddOn'] + ') ') : '' }#{!option_type['required'] ? ' (optional)' : ''}"
171
+ if option_type['description']
172
+ # description << "\n #{option_type['description']}"
173
+ description << " - #{option_type['description']}"
174
+ end
175
+ if option_type['defaultValue']
176
+ description << ". Default: #{option_type['defaultValue']}"
177
+ end
178
+ if option_type['helpBlock']
179
+ description << "\n #{option_type['helpBlock']}"
180
+ end
181
+
182
+ # description = option_type['description'].to_s
183
+ # if option_type['defaultValue']
184
+ # description = "#{description} Default: #{option_type['defaultValue']}"
185
+ # end
186
+ # if option_type['required'] == true
187
+ # description = "(Required) #{description}"
188
+ # end
189
+
190
+ value_label = "VALUE"
191
+ if option_type['placeHolder']
192
+ value_label = option_type['placeHolder']
193
+ elsif option_type['type'] == 'checkbox'
194
+ value_label = '[on|off]' # or.. true|false
195
+ elsif option_type['type'] == 'number'
196
+ value_label = 'NUMBER'
197
+ elsif option_type['type'] == 'multiSelect'
198
+ value_label = 'LIST'
199
+ # elsif option_type['type'] == 'select'
200
+ # value_label = 'SELECT'
201
+ # elsif option['type'] == 'select'
202
+ end
203
+ if option_type['optionalValue']
204
+ value_label = "[#{value_label}]"
205
+ end
206
+ full_option = "--#{full_field_name} #{value_label}"
207
+ # switch is an alias for the full option name, fieldName is the default
208
+ if option_type['switch']
209
+ full_option = "--#{option_type['switch']} #{value_label}"
210
+ end
211
+ arg1, arg2 = full_option, String
212
+ if option_type['shorthand']
213
+ arg1, arg2 = full_option, option_type['shorthand']
214
+ end
215
+ opts.on(arg1, arg2, description) do |val|
216
+ if option_type['type'] == 'checkbox'
217
+ val = (val.to_s != 'false' && val.to_s != 'off')
218
+ elsif option_type['dataType'] != 'string'
219
+ # 'dataType': 'string' added to cli to avoid auto conversion to JSON
220
+ # attempt to parse JSON, this allows blank arrays for multiSelect like --tenants []
221
+ if (val.to_s[0] == '{' && val.to_s[-1] == '}') || (val.to_s[0] == '[' && val.to_s[-1] == ']')
222
+ begin
223
+ val = JSON.parse(val)
224
+ rescue
225
+ MtnCloud::Logging::DarkPrinter.puts "Failed to parse option value '#{val}' as JSON" if MtnCloud::Logging.debug?
226
+ end
227
+ end
228
+ end
229
+ cur_namespace = custom_options
230
+ field_namespace.each do |ns|
231
+ next if ns.empty?
232
+ cur_namespace[ns.to_s] ||= {}
233
+ cur_namespace = cur_namespace[ns.to_s]
234
+ end
235
+ cur_namespace[field_name] = val
236
+ end
237
+
238
+ # todo: all the various types
239
+ # number
240
+ # checkbox [on|off]
241
+ # select for optionSource and selectOptions
242
+
243
+ end
244
+ opts
245
+ end
246
+
247
+ ## the standard options for a command that makes api requests (most of them)
248
+
249
+ def build_standard_get_options(opts, options, includes=[], excludes=[])
250
+ build_common_options(opts, options, includes + [:query, :json, :yaml, :csv, :fields, :select, :delim, :quiet, :dry_run, :remote], excludes)
251
+ end
252
+
253
+ def build_standard_post_options(opts, options, includes=[], excludes=[])
254
+ build_common_options(opts, options, includes + [:options, :payload, :json, :quiet, :dry_run, :remote], excludes)
255
+ end
256
+
257
+ def build_standard_put_options(opts, options, includes=[], excludes=[])
258
+ build_standard_post_options(opts, options, includes, excludes)
259
+ end
260
+
261
+ def build_standard_delete_options(opts, options, includes=[], excludes=[])
262
+ build_common_options(opts, options, includes + [:auto_confirm, :query, :json, :quiet, :dry_run, :remote], excludes)
263
+ end
264
+
265
+ # list is GET that supports phrase,max,offset,sort,direction
266
+ def build_standard_list_options(opts, options, includes=[], excludes=[])
267
+ build_standard_get_options(opts, options, [:list] + includes, excludes)
268
+ end
269
+
270
+ def build_standard_add_options(opts, options, includes=[], excludes=[])
271
+ build_standard_post_options(opts, options, includes, excludes)
272
+ end
273
+
274
+ def build_standard_add_many_options(opts, options, includes=[], excludes=[])
275
+ build_standard_post_options(opts, options, includes + [:payloads], excludes)
276
+ end
277
+
278
+ def build_standard_update_options(opts, options, includes=[], excludes=[])
279
+ build_standard_put_options(opts, options, includes, excludes)
280
+ end
281
+
282
+ def build_standard_remove_options(opts, options, includes=[], excludes=[])
283
+ build_standard_delete_options(opts, options, includes, excludes)
284
+ end
285
+
286
+ def build_standard_api_options(opts, options, includes=[], excludes=[])
287
+ build_common_options(opts, options, includes + [:query, :options, :payload, :json, :yaml, :csv, :fields, :select, :delim, :quiet, :dry_run, :remote], excludes)
288
+ end
289
+
290
+ # number of decimal places to show with curreny
291
+ def default_sigdig
292
+ 2
293
+ end
294
+
295
+ # appends to the passed OptionParser all the generic options
296
+ # @param opts [OptionParser] the option parser object being constructed
297
+ # @param options [Hash] the output Hash that is to being modified
298
+ # @param includes [Array] which options to include eg. :options, :json, :remote
299
+ # @return opts
300
+ def build_common_options(opts, options, includes=[], excludes=[])
301
+ #opts.separator ""
302
+ # opts.separator "Common options:"
303
+ option_keys = includes.clone
304
+ all_option_keys = option_keys.dup
305
+ # todo: support --quiet everywhere
306
+ # turn on some options all the time..
307
+ # unless command_name == "shell"
308
+ # option_keys << :quiet unless option_keys.include?(:quiet)
309
+ # end
310
+
311
+ # ensure commands can always access options[:options], until we can deprecate it...
312
+ options[:options] ||= {}
313
+
314
+ while (option_key = option_keys.shift) do
315
+ case option_key.to_sym
316
+
317
+ when :tenant, :account
318
+ # todo: let's deprecate this in favor of :tenant --tenant to keep -a reserved for --all perhaps?
319
+ opts.on('--tenant TENANT', String, "Tenant (Account) Name or ID") do |val|
320
+ options[:account] = val
321
+ end
322
+ opts.on('--tenant-id ID', String, "Tenant (Account) ID") do |val|
323
+ options[:account_id] = val
324
+ end
325
+ # todo: let's deprecate this in favor of :tenant --tenant to keep -a reserved for --all perhaps?
326
+ opts.on('-a','--account ACCOUNT', "Alias for --tenant") do |val|
327
+ options[:account] = val
328
+ end
329
+ opts.on('-A','--account-id ID', "Tenant (Account) ID") do |val|
330
+ options[:account_id] = val
331
+ end
332
+ opts.add_hidden_option('--tenant-id') if opts.is_a?(MtnCloud::Cli::OptionParser)
333
+ opts.add_hidden_option('-a, --account') if opts.is_a?(MtnCloud::Cli::OptionParser)
334
+ opts.add_hidden_option('-A, --account-id') if opts.is_a?(MtnCloud::Cli::OptionParser)
335
+
336
+ when :details
337
+ opts.on('-a', '--all', "Show all details." ) do
338
+ options[:details] = true
339
+ end
340
+ opts.on('--details', '--details', "Show more details" ) do
341
+ options[:details] = true
342
+ end
343
+ opts.add_hidden_option('--details')
344
+
345
+ when :sigdig
346
+ opts.on('--sigdig DIGITS', "Significant digits to display for prices (currency). Default is #{default_sigdig}.") do |val|
347
+ options[:sigdig] = val.to_i
348
+ end
349
+
350
+ when :options
351
+ options[:options] ||= {}
352
+ opts.on( '-O', '--option OPTION', "Option in the format -O field=\"value\"" ) do |option|
353
+ # todo: look ahead and parse ALL the option=value args after -O switch
354
+ #custom_option_args = option.split('=')
355
+ custom_option_args = option.sub(/\s?\=\s?/, '__OPTION_DELIM__').split('__OPTION_DELIM__')
356
+ custom_options = options[:options]
357
+ option_name_args = custom_option_args[0].split('.')
358
+ option_type = (options[:option_types] || []).find {|it| it['fieldName'] == custom_option_args[0]} || {}
359
+ if option_name_args.count > 1
360
+ nested_options = custom_options
361
+ option_name_args.each_with_index do |name_element,index|
362
+ if index < option_name_args.count - 1
363
+ nested_options[name_element] = nested_options[name_element] || {}
364
+ nested_options = nested_options[name_element]
365
+ else
366
+ val = custom_option_args[1]
367
+ unless option_type['noParse']
368
+ if (val.to_s[0] == '{' && val.to_s[-1] == '}') || (val.to_s[0] == '[' && val.to_s[-1] == ']')
369
+ begin
370
+ val = JSON.parse(val)
371
+ rescue
372
+ MtnCloud::Logging::DarkPrinter.puts "Failed to parse option value '#{val}' as JSON" if MtnCloud::Logging.debug?
373
+ end
374
+ end
375
+ end
376
+ nested_options[name_element] = val
377
+ end
378
+ end
379
+ else
380
+ val = custom_option_args[1]
381
+ unless option_type['noParse']
382
+ if (val.to_s[0] == '{' && val.to_s[-1] == '}') || (val.to_s[0] == '[' && val.to_s[-1] == ']')
383
+ begin
384
+ val = JSON.parse(val)
385
+ rescue
386
+ MtnCloud::Logging::DarkPrinter.puts "Failed to parse option value '#{val}' as JSON" if MtnCloud::Logging.debug?
387
+ end
388
+ end
389
+ end
390
+ custom_options[custom_option_args[0]] = val
391
+ end
392
+ # convert "true","on" and "false","off" to true and false
393
+ unless options[:skip_booleanize]
394
+ custom_options.booleanize!
395
+ end
396
+ options[:options] = custom_options
397
+ end
398
+ # --always-prompt can be used with for update commands where it normally defaults to --no-prompt
399
+ opts.on('--prompt', "Always prompt for input on every option, even those not prompted for by default.") do
400
+ options[:always_prompt] = true
401
+ options[:options][:always_prompt] = true
402
+ end
403
+ opts.on('-N','--no-prompt', "No prompt, skips all input prompting.") do |val|
404
+ options[:no_prompt] = true
405
+ options[:options][:no_prompt] = true
406
+ end
407
+ # opts.on('--skip-prompt x,y,z', String, "Skip prompt, do not prompt for input of the specified options.") do |val|
408
+ # options[:skip_prompt] ||= []
409
+ # options[:skip_prompt] += parse_array(val)
410
+ # options[:options][:skip_prompt] = options[:skip_prompt]
411
+ # end
412
+ # opts.on('--only-prompt x,y,z', String, "Only prompt for input on the specified options.") do |val|
413
+ # options[:only_prompt] ||= []
414
+ # options[:only_prompt] += parse_array(val)
415
+ # options[:options][:only_prompt] = options[:only_prompt]
416
+ # end
417
+ opts.on('--no-options', String, "No options, skips all option parsing so no options are required and no default values are used.") do
418
+ options[:no_options] = true
419
+ options[:options][:no_options] = options[:no_options]
420
+ end
421
+ opts.on('--skip-options x,y,z', String, "Skip parsing of the specified options so that they are not required and their default value is not used.") do |val|
422
+ options[:skip_options] ||= []
423
+ options[:skip_options] += parse_array(val)
424
+ options[:options][:skip_options] = options[:skip_options]
425
+ end
426
+ # opts.on('--only-options x,y,z', String, "Only parse the specified options and skip all others.") do |val|
427
+ # options[:only_options] ||= []
428
+ # options[:only_options] += parse_array(val)
429
+ # options[:options][:only_options] = options[:only_options]
430
+ # end
431
+
432
+ # hide these while incubating
433
+ opts.add_hidden_option('--skip-prompt')
434
+ opts.add_hidden_option('--only-prompt')
435
+ opts.add_hidden_option('--no-options')
436
+ opts.add_hidden_option('--skip-options')
437
+ opts.add_hidden_option('--only-options')
438
+
439
+ when :payload
440
+ opts.on('--payload FILE', String, "Payload from a local JSON or YAML file, skip all prompting") do |val|
441
+ options[:payload_file] = val.to_s
442
+ begin
443
+ payload_file = File.expand_path(options[:payload_file])
444
+ if !File.exist?(payload_file) || !File.file?(payload_file)
445
+ raise ::OptionParser::InvalidOption.new("File not found: #{payload_file}")
446
+ #return false
447
+ end
448
+ if payload_file =~ /\.ya?ml\Z/
449
+ options[:payload] = YAML.load_file(payload_file)
450
+ else
451
+ options[:payload] = JSON.parse(File.read(payload_file))
452
+ end
453
+ rescue => ex
454
+ raise ::OptionParser::InvalidOption.new("Failed to parse payload file: #{payload_file} Error: #{ex.message}")
455
+ end
456
+ end
457
+ opts.on('--payload-dir DIRECTORY', String, "Payload from a local directory containing 1-N JSON or YAML files, skip all prompting. This makes one request, merging all the files into a single payload.") do |val|
458
+ print_error yellow,"[DEPRECATED] The option `--payload-dir` is deprecated and will be removed. Use `--payloads` to make requests for each file in a directory.",reset,"\n"
459
+ options[:payload_dir] = val.to_s
460
+ payload_dir = File.expand_path(options[:payload_dir])
461
+ if !Dir.exist?(payload_dir) || !File.directory?(payload_dir)
462
+ raise ::OptionParser::InvalidOption.new("Directory not found: #{payload_dir}")
463
+ end
464
+ payload = {}
465
+ begin
466
+ merged_payload = {}
467
+ payload_files = []
468
+ payload_files += Dir["#{payload_dir}/*.json"]
469
+ payload_files += Dir["#{payload_dir}/*.yml"]
470
+ payload_files += Dir["#{payload_dir}/*.yaml"]
471
+ if payload_files.empty?
472
+ raise ::OptionParser::InvalidOption.new("No .json/yaml files found in config directory: #{payload_dir}")
473
+ end
474
+ payload_files.each do |payload_file|
475
+ MtnCloud::Logging::DarkPrinter.puts "parsing payload file: #{payload_file}" if MtnCloud::Logging.debug?
476
+ config_payload = {}
477
+ if payload_file =~ /\.ya?ml\Z/
478
+ config_payload = YAML.load_file(payload_file)
479
+ else
480
+ config_payload = JSON.parse(File.read(payload_file))
481
+ end
482
+ merged_payload.deep_merge!(config_payload)
483
+ end
484
+ options[:payload] = merged_payload
485
+ rescue => ex
486
+ raise ::OptionParser::InvalidOption.new("Failed to parse payload file: #{payload_file} Error: #{ex.message}")
487
+ end
488
+ end
489
+ opts.add_hidden_option('--payload-dir')
490
+ opts.on('--payload-json JSON', String, "Payload JSON, skip all prompting") do |val|
491
+ begin
492
+ options[:payload] = JSON.parse(val.to_s)
493
+ rescue => ex
494
+ raise ::OptionParser::InvalidOption.new("Failed to parse payload as JSON. Error: #{ex.message}")
495
+ end
496
+ end
497
+ opts.on('--payload-yaml YAML', String, "Payload YAML, skip all prompting") do |val|
498
+ begin
499
+ options[:payload] = YAML.load(val.to_s)
500
+ rescue => ex
501
+ raise ::OptionParser::InvalidOption.new("Failed to parse payload as YAML. Error: #{ex.message}")
502
+ end
503
+ end
504
+ # --payloads test-data/item*.json
505
+ opts.on('--payloads PATH', String, "Payload(s) from one or more local JSON or YAML files, skip all prompting and execute the request 1-N times, once for each file. PATH can be a directory or a file pattern.") do |val|
506
+ # maybe use parse_array(val) to support csv..
507
+ # find files matching PATH
508
+ # todo: probably support recursive... can be done with '**/*.json' now though.
509
+ if val.to_s.strip.empty?
510
+ raise ::OptionParser::InvalidOption.new("PATH must be provided as directory, file or pattern to find JSON or YAML files.")
511
+ end
512
+ filepath = File.expand_path(val.to_s.strip)
513
+ files = []
514
+ if File.directory?(filepath)
515
+ # passed the name of a directory, include all the JSON and YAML files directly under it
516
+ Dir.glob(File.join(filepath, "*")).each do |file|
517
+ if File.file?(file) && ['.json','.yaml','.yml'].include?(File.extname(file))
518
+ files << file
519
+ end
520
+ end
521
+ if files.empty?
522
+ raise ::OptionParser::InvalidOption.new("Failed to find any .json or .yaml files under the directory: #{filepath}")
523
+ end
524
+ elsif File.file?(filepath)
525
+ # passed the name of a file
526
+ files << filepath
527
+ else
528
+ # assume it is a pattern to find files with
529
+ files = Dir.glob(filepath)
530
+ if files.empty?
531
+ raise ::OptionParser::InvalidOption.new("Failed to find any files matching path: #{filepath}")
532
+ end
533
+ end
534
+ # parse files as JSON or YAML
535
+ options[:payload_files] ||= []
536
+ options[:payloads] ||= []
537
+ files.each do |file|
538
+ if options[:payload_files].include?(file)
539
+ next
540
+ else
541
+ options[:payload_files] << file
542
+ end
543
+ payload = nil
544
+ begin
545
+ payload_file = File.expand_path(file)
546
+ if !File.exist?(payload_file) || !File.file?(payload_file)
547
+ raise ::OptionParser::InvalidOption.new("File not found: #{payload_file}")
548
+ end
549
+ # todo: could use parse_json_or_yaml()
550
+ payload = nil
551
+ if payload_file =~ /\.ya?ml\Z/
552
+ payload = YAML.load_file(payload_file)
553
+ else
554
+ payload = JSON.parse(File.read(payload_file))
555
+ end
556
+ options[:payloads] << payload
557
+ rescue => ex
558
+ raise ::OptionParser::InvalidOption.new("Failed to parse payload file: #{payload_file} Error: #{ex.message}")
559
+ end
560
+ end
561
+ end if all_option_keys.include?(:payloads)
562
+ opts.on('--ignore-payload-errors', "Continue processing any remaining payloads if an error occurs. The default behavior is to stop processing when an error occurs.") do
563
+ options[:ignore_payload_errors] = true
564
+ end if all_option_keys.include?(:payloads)
565
+ when :payloads
566
+ # added with :payload too... just need it here to avoid unknown key error
567
+ # todo: remove this when every command supporting :payload is updated to use parse_payload(options) and execute_api(options)
568
+ when :list
569
+ opts.on( '-m', '--max MAX', "Max Results" ) do |val|
570
+ # api supports max=-1 for all at the moment..
571
+ if val.to_s == "all" || val.to_s == "-1"
572
+ options[:max] = "-1"
573
+ else
574
+ max = val.to_i
575
+ if max <= 0
576
+ raise ::OptionParser::InvalidArgument.new("must be a positive integer")
577
+ end
578
+ options[:max] = max
579
+ end
580
+ end
581
+
582
+ opts.on( '-o', '--offset OFFSET', "Offset Results" ) do |val|
583
+ offset = val.to_i
584
+ if offset <= 0
585
+ raise ::OptionParser::InvalidArgument.new("must be a positive integer")
586
+ end
587
+ options[:offset] = offset
588
+ end
589
+
590
+ if excludes.include?("search") == false
591
+ opts.on( '-s', '--search PHRASE', "Search Phrase" ) do |phrase|
592
+ options[:phrase] = phrase
593
+ end
594
+ end
595
+
596
+ opts.on( '-S', '--sort ORDER', "Sort Order. DIRECTION may be included as \"ORDER [asc|desc]\"." ) do |v|
597
+ if v.to_s.include?(",")
598
+ # sorting on multiple properties, just pass it as is, newer api supports multiple fields
599
+ options[:sort] = v
600
+ else
601
+ v_parts = v.to_s.split(" ")
602
+ if v_parts.size > 1
603
+ options[:sort] = v_parts[0]
604
+ options[:direction] = (v_parts[1].strip == "desc") ? "desc" : "asc"
605
+ else
606
+ options[:sort] = v
607
+ end
608
+ end
609
+ end
610
+
611
+ opts.on( '-D', '--desc', "Descending Sort Direction." ) do |v|
612
+ options[:direction] = "desc"
613
+ end
614
+
615
+ opts.on( "--reverse", "Reverse order of results. This invert is done by the client, not necessarily the entire dataset." ) do
616
+ options[:reverse] = true
617
+ end
618
+
619
+ # arbitrary query parameters in the format -Q "category=web&phrase=nginx"
620
+ # opts.on( '-Q', '--query PARAMS', "Query parameters. PARAMS format is 'foo=bar&category=web'" ) do |val|
621
+ # options[:query_filters_raw] = val
622
+ # options[:query_filters] = {}
623
+ # # todo: smarter parsing
624
+ # val.split('&').each do |filter|
625
+ # k, v = filter.split('=')
626
+ # # allow "woot:true instead of woot=true"
627
+ # if (k.include?(":") && v == nil)
628
+ # k, v = k.split(":")
629
+ # end
630
+ # if (!k.to_s.empty?)
631
+ # options[:query_filters][k.to_s.strip] = v.to_s.strip
632
+ # end
633
+ # end
634
+ # end
635
+
636
+ when :query, :query_filters
637
+ # arbitrary query parameters in the format -Q "category=web&phrase=nginx"
638
+ # or pass it many times like -Q foo=bar -Q hello=world
639
+ opts.on( '-Q', '--query PARAMS', "Query parameters. PARAMS format is 'foo=bar&category=web'" ) do |val|
640
+ if options[:query_filters_raw] && !options[:query_filters_raw].empty?
641
+ options[:query_filters_raw] += ("&" + val)
642
+ else
643
+ options[:query_filters_raw] = val
644
+ end
645
+ options[:query_filters] ||= {}
646
+ val.split('&').each do |filter|
647
+ k, v = filter.split('=')
648
+ # allow woot:true instead of woot=true
649
+ if (k.include?(":") && v == nil)
650
+ k, v = k.split(":")
651
+ end
652
+ if (!k.to_s.empty?)
653
+ if options[:query_filters].key?(k.to_s.strip)
654
+ cur_val = options[:query_filters][k.to_s.strip]
655
+ if cur_val.instance_of?(Array)
656
+ options[:query_filters][k.to_s.strip] << v.to_s.strip
657
+ else
658
+ options[:query_filters][k.to_s.strip] = [cur_val, v.to_s.strip]
659
+ end
660
+ else
661
+ options[:query_filters][k.to_s.strip] = v.to_s.strip
662
+ end
663
+ end
664
+ end
665
+ end
666
+
667
+ when :last_updated
668
+ # opts.on("--last-updated TIME", Time, "Filter by gte last updated") do |time|
669
+ opts.on("--last-updated TIME", String, "Filter by Last Updated (gte)") do |time|
670
+ begin
671
+ options[:lastUpdated] = parse_time(time)
672
+ rescue => e
673
+ raise OptionParser::InvalidArgument.new "Failed to parse time '#{time}'. Error: #{e}"
674
+ end
675
+ end
676
+
677
+ when :find_by_name
678
+ opts.on('--find-by-name', "Always treat the identifier argument as a name, never an ID. Useful for specifying names that look like numbers. eg. '1234'" ) do
679
+ options[:find_by_name] = true
680
+ end
681
+ # opts.add_hidden_option('--find-by-name') if opts.is_a?(MtnCloud::Cli::OptionParser)
682
+
683
+ when :remote
684
+ opts.on( '-r', '--remote REMOTE', "Remote name. The current remote is used by default." ) do |val|
685
+ options[:remote] = val
686
+ end
687
+ opts.on( '--remote-url URL', '--remote-url URL', "Remote url. This allows adhoc requests instead of using a configured remote." ) do |val|
688
+ options[:remote_url] = val
689
+ end
690
+ opts.on( '-T', '--token TOKEN', "Access token for authentication with --remote. Saved credentials are used by default." ) do |val|
691
+ options[:remote_token] = val
692
+ end unless excludes.include?(:remote_token)
693
+ opts.on( '--token-file FILE', String, "Token File, read a file containing the access token." ) do |val|
694
+ token_file = File.expand_path(val)
695
+ if !File.exist?(token_file) || !File.file?(token_file)
696
+ raise ::OptionParser::InvalidOption.new("File not found: #{token_file}")
697
+ end
698
+ options[:remote_token] = File.read(token_file).to_s.split("\n").first.strip
699
+ end
700
+ opts.add_hidden_option('--token-file') if opts.is_a?(MtnCloud::Cli::OptionParser)
701
+ opts.on( '-U', '--username USERNAME', "Username for authentication." ) do |val|
702
+ options[:remote_username] = val
703
+ end unless excludes.include?(:remote_username)
704
+
705
+
706
+ unless excludes.include?(:remote_password)
707
+ opts.on( '-P', '--password PASSWORD', "Password for authentication." ) do |val|
708
+ options[:remote_password] = val
709
+ end
710
+ opts.on( '--password-file FILE', String, "Password File, read a file containing the password for authentication." ) do |val|
711
+ password_file = File.expand_path(val)
712
+ if !File.exist?(password_file) || !File.file?(password_file)
713
+ raise ::OptionParser::InvalidOption.new("File not found: #{password_file}")
714
+ end
715
+ file_content = File.read(password_file) #.strip
716
+ options[:remote_password] = File.read(password_file).to_s.split("\n").first
717
+ end
718
+ opts.add_hidden_option('--password-file') if opts.is_a?(MtnCloud::Cli::OptionParser)
719
+ end
720
+
721
+ # todo: also require this for talking to plain old HTTP
722
+ opts.on('-I','--insecure', "Allow insecure HTTPS communication. i.e. bad SSL certificate.") do |val|
723
+ options[:insecure] = true
724
+ MtnCloud::RestClient.enable_ssl_verification = false
725
+ end
726
+
727
+ #when :header, :headers
728
+ opts.on( '-H', '--header HEADER', "Additional HTTP header to include with requests." ) do |val|
729
+ options[:headers] ||= {}
730
+ # header_list = val.to_s.split(',')
731
+ header_list = [val.to_s]
732
+ header_list.each do |h|
733
+ header_parts = val.to_s.split(":")
734
+ header_key, header_value = header_parts[0], header_parts[1..-1].join(":").strip
735
+ if header_parts.size() < 2
736
+ header_parts = val.to_s.split("=")
737
+ header_key, header_value = header_parts[0], header_parts[1..-1].join("=").strip
738
+ end
739
+ if header_parts.size() < 2
740
+ raise_command_error "Invalid HEADER value '#{val}'. HEADER should contain a key and a value. eg. -H 'X-Morpheus-Lease: $MORPHEUS_LEASE_TOKEN'"
741
+ end
742
+ options[:headers][header_key] = header_value
743
+ end
744
+ end
745
+ # opts.add_hidden_option('-H') if opts.is_a?(MtnCloud::Cli::OptionParser)
746
+ # opts.add_hidden_option('--header') if opts.is_a?(MtnCloud::Cli::OptionParser)
747
+ opts.add_hidden_option('-H, --header') if opts.is_a?(MtnCloud::Cli::OptionParser)
748
+
749
+ #when :timeout
750
+ opts.on( '--timeout SECONDS', "Timeout for api requests. Default is typically 30 seconds." ) do |val|
751
+ options[:timeout] = val ? val.to_f : nil
752
+ end
753
+ opts.add_hidden_option('--timeout') if opts.is_a?(MtnCloud::Cli::OptionParser)
754
+
755
+ when :auto_confirm
756
+ opts.on( '-y', '--yes', "Auto Confirm" ) do
757
+ options[:yes] = true
758
+ end
759
+
760
+ when :json
761
+ opts.on('-j','--json', "JSON Output") do
762
+ options[:json] = true
763
+ options[:format] = :json
764
+ end
765
+
766
+ opts.on('--json-raw', String, "JSON Output that is not so pretty.") do |val|
767
+ options[:json] = true
768
+ options[:format] = :json
769
+ options[:pretty_json] = false
770
+ end
771
+ opts.add_hidden_option('json-raw') if opts.is_a?(MtnCloud::Cli::OptionParser)
772
+
773
+ when :yaml
774
+ # -y for --yes and for --yaml
775
+ if includes.include?(:auto_confirm)
776
+ opts.on(nil, '--yaml', "YAML Output") do
777
+ options[:yaml] = true
778
+ options[:format] = :yaml
779
+ end
780
+ else
781
+ opts.on('-y', '--yaml', "YAML Output") do
782
+ options[:yaml] = true
783
+ options[:format] = :yaml
784
+ end
785
+ end
786
+ opts.on(nil, '--yml', "alias for --yaml") do
787
+ options[:yaml] = true
788
+ options[:format] = :yaml
789
+ end
790
+ opts.add_hidden_option('yml') if opts.is_a?(MtnCloud::Cli::OptionParser)
791
+
792
+ when :csv
793
+ opts.on(nil, '--csv', "CSV Output") do
794
+ options[:csv] = true
795
+ options[:format] = :csv
796
+ #options[:csv_delim] = options[:csv_delim] || ","
797
+ end
798
+ # deprecated --csv-delim, use --delimiter instead
799
+ opts.on('--csv-delim CHAR', String, "Delimiter for CSV Output values. Default: ','") do |val|
800
+ options[:csv] = true
801
+ options[:format] = :csv
802
+ val = val.gsub("\\n", "\n").gsub("\\r", "\r").gsub("\\t", "\t") if val.include?("\\")
803
+ options[:csv_delim] = val
804
+ end
805
+ opts.add_hidden_option('--csv-delim') if opts.is_a?(MtnCloud::Cli::OptionParser)
806
+
807
+ # deprecated --csv-newline, use --newline instead
808
+ opts.on('--csv-newline [CHAR]', String, "Delimiter for CSV Output rows. Default: '\\n'") do |val|
809
+ options[:csv] = true
810
+ options[:format] = :csv
811
+ if val == "no" || val == "none"
812
+ options[:csv_newline] = ""
813
+ else
814
+ val = val.to_s.gsub("\\n", "\n").gsub("\\r", "\r").gsub("\\t", "\t") if val.include?("\\")
815
+ options[:csv_newline] = val
816
+ end
817
+ end
818
+ opts.add_hidden_option('--csv-newline') if opts.is_a?(MtnCloud::Cli::OptionParser)
819
+
820
+ opts.on(nil, '--csv-quotes', "Wrap CSV values with \". Default: false") do
821
+ options[:csv] = true
822
+ options[:format] = :csv
823
+ options[:csv_quotes] = true
824
+ end
825
+ opts.add_hidden_option('--csv-quotes') if opts.is_a?(MtnCloud::Cli::OptionParser)
826
+
827
+ opts.on(nil, '--csv-no-header', "Exclude header for CSV Output.") do
828
+ options[:csv] = true
829
+ options[:format] = :csv
830
+ options[:csv_no_header] = true
831
+ end
832
+ opts.add_hidden_option('--csv-no-header') if opts.is_a?(MtnCloud::Cli::OptionParser)
833
+
834
+ opts.on(nil, '--quotes', "Wrap CSV values with \". Default: false") do
835
+ options[:csv_quotes] = true
836
+ end
837
+ opts.add_hidden_option('--csv-quotes') if opts.is_a?(MtnCloud::Cli::OptionParser)
838
+
839
+ opts.on(nil, '--no-header', "Exclude header for CSV Output.") do
840
+ options[:csv_no_header] = true
841
+ end
842
+
843
+ when :fields
844
+ opts.on('-f', '--fields x,y,z', Array, "Filter Output to a limited set of fields. Default is all fields for json,csv,yaml.") do |val|
845
+ if val.size == 1 && val[0].downcase == 'all'
846
+ options[:all_fields] = true
847
+ else
848
+ options[:include_fields] = val
849
+ end
850
+ end
851
+ opts.on('-F', '--old-fields x,y,z', Array, "alias for -f, --fields") do |val|
852
+ if val.size == 1 && val[0].downcase == 'all'
853
+ options[:all_fields] = true
854
+ else
855
+ options[:include_fields] = val
856
+ end
857
+ end
858
+ opts.add_hidden_option('-F, --old-fields') if opts.is_a?(MtnCloud::Cli::OptionParser)
859
+ opts.on('--raw-fields [x,y,z]', String, "Raw fields filters output like --fields except the properties [x,y,z] must be specified from the root of the response instead of relative to the the list or object context for this particular resource.") do |val|
860
+ if val.size == 1 && val[0].downcase == 'all'
861
+ options[:all_fields] = true
862
+ else
863
+ options[:include_fields] = val.split(',').collect {|r| r.strip}.compact
864
+ end
865
+ options[:raw_fields] = true
866
+ end
867
+ opts.add_hidden_option('--raw-fields') if opts.is_a?(MtnCloud::Cli::OptionParser)
868
+ opts.on(nil, '--all-fields', "Show all fields present in the data.") do
869
+ options[:all_fields] = true
870
+ end
871
+ opts.on(nil, '--wrap', "Wrap table columns instead hiding them when terminal is not wide enough.") do
872
+ options[:wrap] = true
873
+ end
874
+ when :select
875
+ #opts.add_hidden_option('--all-fields') if opts.is_a?(MtnCloud::Cli::OptionParser)
876
+ opts.on('--select x,y,z', String, "Filter Output to just print the value(s) of specific fields.") do |val|
877
+ options[:select_fields] = val.split(',').collect {|r| r.strip}
878
+ end
879
+ opts.on('--raw-select x,y,z', String, "Raw select works like --select except the properties [x,y,z] must be specified from the root of the response instead of relative to the the list or object context for this particular resource.") do |val|
880
+ options[:select_fields] = val.split(',').collect {|r| r.strip}
881
+ options[:raw_fields] = true
882
+ end
883
+ opts.add_hidden_option('--raw-select') if opts.is_a?(MtnCloud::Cli::OptionParser)
884
+
885
+ when :delim
886
+ opts.on('--delimiter [CHAR]', String, "Delimiter for output values. Default: ',', use with --select and --csv") do |val|
887
+ options[:csv] = true
888
+ options[:format] = :csv
889
+ val = val.to_s
890
+ val = val.gsub("\\n", "\n").gsub("\\r", "\r").gsub("\\t", "\t") if val.include?("\\")
891
+ options[:delim] = val
892
+ end
893
+
894
+ opts.on('--newline [CHAR]', String, "Delimiter for output rows. Default: '\\n', use with --select and --csv") do |val|
895
+ options[:csv] = true
896
+ options[:format] = :csv
897
+ val = val.to_s
898
+ if val == "no" || val == "none"
899
+ options[:newline] = ""
900
+ else
901
+ val = val.to_s.gsub("\\n", "\n").gsub("\\r", "\r").gsub("\\t", "\t") if val.include?("\\")
902
+ options[:newline] = val
903
+ end
904
+ end
905
+ when :thin
906
+ opts.on( '--thin', '--thin', "Format headers and columns with thin borders." ) do |val|
907
+ options[:border_style] = :thin
908
+ end
909
+
910
+
911
+
912
+ when :dry_run
913
+ opts.on('-d','--dry-run', "Dry Run, print the API request instead of executing it.") do
914
+ # todo: this should print after parsing obv..
915
+ # need a hook after parse! or a standard_handle(options) { ... } paradigm
916
+ # either that or hook it up in every command somehow, maybe a hook on connect()
917
+ #puts "#{cyan}#{dark} #=> DRY RUN#{reset}"
918
+ # don't print this for --json combined with -d
919
+ # print once and dont munge json
920
+ if !options[:curl] && !options[:json]
921
+ puts "#{cyan}#{bold}#{dark}DRY RUN#{reset}"
922
+ end
923
+ options[:dry_run] = true
924
+ end
925
+ opts.on(nil,'--curl', "Curl, print the API request as a curl command instead of executing it.") do
926
+ # print once and dont munge json
927
+ if !options[:dry_run] && !options[:json]
928
+ puts "#{cyan}#{bold}#{dark}DRY RUN#{reset}"
929
+ end
930
+ options[:dry_run] = true
931
+ options[:curl] = true
932
+ end
933
+ opts.on(nil,'--scrub', "Mask secrets in output, such as the Authorization header. For use with --curl and --dry-run.") do
934
+ options[:scrub] = true
935
+ end
936
+ # dry run comes with hidden outfile options
937
+ #opts.add_hidden_option('--scrub') if opts.is_a?(MtnCloud::Cli::OptionParser)
938
+ opts.on('--out FILE', String, "Write standard output to a file instead of the terminal.") do |val|
939
+ # could validate directory is writable..
940
+ options[:outfile] = val
941
+ end
942
+ opts.add_hidden_option('--out') if opts.is_a?(MtnCloud::Cli::OptionParser)
943
+ opts.on('--overwrite', '--overwrite', "Overwrite output file if it already exists.") do
944
+ options[:overwrite] = true
945
+ end
946
+ opts.add_hidden_option('--overwrite') if opts.is_a?(MtnCloud::Cli::OptionParser)
947
+
948
+ when :outfile
949
+ opts.on('--out FILE', String, "Write standard output to a file instead of the terminal.") do |val|
950
+ options[:outfile] = val
951
+ end
952
+ opts.on('--overwrite', '--overwrite', "Overwrite output file if it already exists.") do |val|
953
+ options[:overwrite] = true
954
+ end
955
+ when :quiet
956
+ opts.on('-q','--quiet', "No Output, do not print to stdout") do
957
+ options[:quiet] = true
958
+ end
959
+
960
+ else
961
+ raise "Unknown common_option key: #{option_key}"
962
+ end
963
+ end
964
+
965
+ # options that are always included
966
+
967
+ # always support thin, but hidden because mostly not hooked up at the moment...
968
+ unless includes.include?(:thin)
969
+ opts.on( '--thin', '--thin', "Format headers and columns with thin borders." ) do |val|
970
+ options[:border_style] = :thin
971
+ end
972
+ opts.add_hidden_option('--thin') if opts.is_a?(MtnCloud::Cli::OptionParser)
973
+ end
974
+
975
+ # disable ANSI coloring
976
+ opts.on('-C','--nocolor', "Disable ANSI coloring") do
977
+ Term::ANSIColor::coloring = false
978
+ end
979
+
980
+
981
+ # Benchmark this command?
982
+ # Also useful for seeing exit status for every command.
983
+ opts.on('-B','--benchmark', "Print benchmark time and exit/error after the command is finished.") do
984
+ options[:benchmark] = true
985
+ # this is hacky, but working!
986
+ # shell handles returning to false
987
+ #MtnCloud::Benchmarking.enabled = true
988
+ #my_terminal.benchmarking = true
989
+ #start_benchmark(args.join(' '))
990
+ # ok it happens outside of handle() alltogether..
991
+ # wow, simplify me plz
992
+ end
993
+
994
+ opts.on('-V','--debug', "Print extra output for debugging.") do
995
+ options[:debug] = true
996
+ MtnCloud::Logging.set_log_level(MtnCloud::Logging::Logger::DEBUG)
997
+ ::RestClient.log = MtnCloud::Logging.debug? ? MtnCloud::Logging::DarkPrinter.instance : nil
998
+ # perhaps...
999
+ # create a new logger instance just for this command instance
1000
+ # this way we don't elevate the global level for subsequent commands in a shell
1001
+ # @logger = MtnCloud::Logging::Logger.new(STDOUT)
1002
+ # if !@logger.debug?
1003
+ # @logger.log_level = MtnCloud::Logging::Logger::DEBUG
1004
+ # end
1005
+ end
1006
+
1007
+ # A way to ensure debugging is off, it should go back on after the command is complete.
1008
+ opts.on('--no-debug','--no-debug', "Disable debugging.") do
1009
+ options[:debug] = false
1010
+ MtnCloud::Logging.set_log_level(MtnCloud::Logging::Logger::INFO)
1011
+ ::RestClient.log = MtnCloud::Logging.debug? ? MtnCloud::Logging::DarkPrinter.instance : nil
1012
+ end
1013
+ opts.add_hidden_option('--no-debug') if opts.is_a?(MtnCloud::Cli::OptionParser)
1014
+
1015
+ opts.on('--hidden-help', "Print help that includes all the hidden options, like this one." ) do
1016
+ puts opts.full_help_message({show_hidden_options:true})
1017
+ exit 0 # return 0 maybe?
1018
+ end
1019
+ opts.add_hidden_option('--hidden-help') if opts.is_a?(MtnCloud::Cli::OptionParser)
1020
+ opts.on('-h', '--help', "Print this help" ) do
1021
+ puts opts
1022
+ exit 0 # return 0 maybe?
1023
+ end
1024
+
1025
+ opts
1026
+ end
1027
+
1028
+ def prog_name
1029
+ self.class.prog_name
1030
+ end
1031
+
1032
+ def command_name
1033
+ self.class.command_name
1034
+ end
1035
+
1036
+ def command_description
1037
+ self.class.command_description
1038
+ end
1039
+
1040
+ def subcommands
1041
+ self.class.subcommands
1042
+ end
1043
+
1044
+ def visible_subcommands
1045
+ self.class.visible_subcommands
1046
+ end
1047
+
1048
+ def subcommand_aliases
1049
+ self.class.subcommand_aliases
1050
+ end
1051
+
1052
+ # def subcommand_descriptions
1053
+ # self.class.subcommand_descriptions
1054
+ # end
1055
+
1056
+ def get_subcommand_description(subcmd)
1057
+ self.class.get_subcommand_description(subcmd)
1058
+ end
1059
+
1060
+ # The name of the command method that called subcommand_usage or
1061
+ # subcommand_description, for example "get" when called from Clusters#get.
1062
+ # Before Ruby 3.4 the label is "get" or "block in get". From Ruby 3.4 it
1063
+ # includes the class, as in "MtnCloud::Cli::Clusters#get".
1064
+ def calling_method_name
1065
+ label = caller_locations(2, 1)[0].label.to_s
1066
+ label = label.sub(/\Ablock (\(\d+ levels\) )?in /, '')
1067
+ label.split(/[#.]/).last.to_s
1068
+ end
1069
+
1070
+ def subcommand_description()
1071
+ calling_method = calling_method_name
1072
+ subcommand_name = subcommands.key(calling_method)
1073
+ subcommand_name ? get_subcommand_description(subcommand_name) : nil
1074
+ end
1075
+
1076
+ def default_subcommand
1077
+ self.class.default_subcommand
1078
+ end
1079
+
1080
+ def default_refresh_interval
1081
+ self.class.default_refresh_interval
1082
+ end
1083
+
1084
+ def usage
1085
+ if !subcommands.empty?
1086
+ "Usage: #{prog_name} #{command_name} [command] [options]"
1087
+ else
1088
+ "Usage: #{prog_name} #{command_name} [options]"
1089
+ end
1090
+ end
1091
+
1092
+ def my_help_command
1093
+ "#{prog_name} #{command_name} --help"
1094
+ end
1095
+
1096
+ def subcommand_usage(*extra)
1097
+ calling_method = calling_method_name
1098
+ subcommand_name = subcommands.key(calling_method)
1099
+ extra = extra.flatten
1100
+ if !subcommand_name.empty? && extra.first == subcommand_name
1101
+ extra.shift
1102
+ end
1103
+ #extra = ["[options]"] if extra.empty?
1104
+ "Usage: #{prog_name} #{command_name} #{subcommand_name} #{extra.join(' ')}".squeeze(' ').strip
1105
+ end
1106
+
1107
+ # a string to describe the usage of your command
1108
+ # this is what the --help option
1109
+ # feel free to override this in your commands
1110
+ def full_command_usage
1111
+ out = ""
1112
+ out << usage.to_s.strip if usage
1113
+ out << "\n"
1114
+ my_subcommands = visible_subcommands
1115
+ if !my_subcommands.empty?
1116
+ out << "Commands:"
1117
+ out << "\n"
1118
+ my_subcommands.sort.each {|subcmd, method|
1119
+ desc = get_subcommand_description(subcmd)
1120
+ out << "\t#{subcmd.to_s}"
1121
+ out << "\t#{desc}" if desc
1122
+ out << "\n"
1123
+ }
1124
+ end
1125
+ if command_description && !command_description.to_s.strip.empty?
1126
+ out << "\n"
1127
+ out << "#{command_description.strip}\n"
1128
+ end
1129
+ # out << "\n"
1130
+ out
1131
+ end
1132
+
1133
+ # a default handler
1134
+ def handle_subcommand(args)
1135
+ commands = subcommands
1136
+ if subcommands.empty?
1137
+ raise "#{self.class} has no available subcommands"
1138
+ end
1139
+ # meh, could deprecate and make subcommand define handle() itself
1140
+ # if args.count == 0 && default_subcommand
1141
+ # # p "using default subcommand #{default_subcommand}"
1142
+ # return send(default_subcommand, args || [])
1143
+ # end
1144
+ subcommand_name = args[0]
1145
+ if args.empty?
1146
+ print_error MtnCloud::Terminal.angry_prompt
1147
+ puts_error "[command] argument is required"
1148
+ puts full_command_usage
1149
+ exit 127
1150
+ end
1151
+ if args[0] == "-h" || args[0] == "--help" || args[0] == "help"
1152
+ puts full_command_usage
1153
+ return 0
1154
+ end
1155
+ if subcommand_aliases[subcommand_name]
1156
+ subcommand_name = subcommand_aliases[subcommand_name]
1157
+ end
1158
+ cmd_method = subcommands[subcommand_name]
1159
+ if !cmd_method
1160
+ error_msg = "'#{command_name} #{subcommand_name}' is not a known command.\n#{full_command_usage}"
1161
+ raise CommandNotFoundError.new(error_msg)
1162
+ end
1163
+ send(cmd_method, args[1..-1])
1164
+ end
1165
+
1166
+ def handle(args)
1167
+ raise "#{self} has not defined handle()!"
1168
+ end
1169
+
1170
+ # executes block with each argument in the list
1171
+ # @return [0|1] 0 if they were all successful, else 1
1172
+ def run_command_for_each_arg(args, &block)
1173
+ cmd_results = []
1174
+ args.each do |arg|
1175
+ begin
1176
+ cur_result = yield arg
1177
+ rescue SystemExit => err
1178
+ cur_result = err.success? ? 0 : 1
1179
+ end
1180
+ cmd_results << cur_result
1181
+ end
1182
+ # find a bad result and return it
1183
+ cmd_results = cmd_results.collect do |cmd_result|
1184
+ if cmd_result.is_a?(Array)
1185
+ cmd_result
1186
+ else
1187
+ [cmd_result, nil]
1188
+ end
1189
+ end
1190
+ failed_result = cmd_results.find {|cmd_result| cmd_result[0] == false || (cmd_result[0].is_a?(Integer) && cmd_result[0] != 0) }
1191
+ return failed_result ? failed_result : cmd_results.last
1192
+ end
1193
+
1194
+ # def connect(options={})
1195
+ # MtnCloud::Logging::DarkPrinter.puts "#{command_name} has not defined connect()" if MtnCloud::Logging.debug?
1196
+ # end
1197
+
1198
+ # This supports the simple remote option eg. `instances add --remote "qa"`
1199
+ # It will establish a connection to the pre-configured appliance named "qa"
1200
+ # By default it will connect to the active (current) remote appliance
1201
+ # This returns a new instance of MtnCloud::APIClient (and sets @access_token, and @appliance)
1202
+ # Your command should be ready to make api requests after this.
1203
+ # This will prompt for credentials if none are found, use :skip_login
1204
+ # Credentials will be saved unless --remote-url or --token is being used.
1205
+ def establish_remote_appliance_connection(options)
1206
+ # todo: probably refactor and don't rely on this method to set these instance vars
1207
+ @remote_appliance = nil
1208
+ @appliance_name, @appliance_url, @access_token = nil, nil, nil
1209
+ @api_client = nil
1210
+ @do_save_credentials = true
1211
+ # skip saving if --remote-url or --username or --password are passed in
1212
+ if options[:remote_url] || options[:remote_token] || options[:remote_username] || options[:remote_password]
1213
+ @do_save_credentials = false
1214
+ end
1215
+ appliance = nil
1216
+ if options[:remote_url]
1217
+ # --remote-url means use an arbitrary url, do not save any appliance config
1218
+ # appliance = {name:'remote-url', url:options[:remote_url]}
1219
+ appliance = {url:options[:remote_url]}
1220
+ appliance[:temporary] = true
1221
+ #appliance[:status] = "ready" # or "unknown"
1222
+ # appliance[:last_check] = nil
1223
+ elsif options[:remote]
1224
+ # --remote means use the specified remote
1225
+ appliance = ::MtnCloud::Cli::Remote.load_remote(options[:remote])
1226
+ if appliance.nil?
1227
+ if ::MtnCloud::Cli::Remote.appliances.empty?
1228
+ raise_command_error "No remote appliances exist, see the command `remote add`."
1229
+ else
1230
+ raise_command_error "Remote appliance not found by the name '#{options[:remote]}', see `remote list`"
1231
+ end
1232
+ end
1233
+ else
1234
+ # use active remote
1235
+ appliance = ::MtnCloud::Cli::Remote.load_active_remote()
1236
+ if !appliance
1237
+ if ::MtnCloud::Cli::Remote.appliances.empty?
1238
+ raise_command_error "No remote appliances exist, see the command `remote add`"
1239
+ else
1240
+ raise_command_error "#{command_name} requires a remote to be specified, try the option -r [remote] or see the command `remote use`"
1241
+ end
1242
+ end
1243
+ end
1244
+ @remote_appliance = appliance
1245
+ @appliance_name = appliance[:name]
1246
+ @appliance_url = appliance[:url] || appliance[:host] # it used to store :host in the YAML
1247
+ # set enable_ssl_verification
1248
+ # instead of toggling this global value
1249
+ # this should just be an attribute of the api client
1250
+ # for now, this fixes the issue where passing --insecure or --remote
1251
+ # would then apply to all subsequent commands...
1252
+ allow_insecure = false
1253
+ if options[:insecure] || appliance[:insecure] || MtnCloud::Cli::Shell.insecure
1254
+ allow_insecure = true
1255
+ end
1256
+ @verify_ssl = !allow_insecure
1257
+ # MtnCloud::RestClient.enable_ssl_verification = allow_insecure != true
1258
+ if allow_insecure && MtnCloud::RestClient.ssl_verification_enabled?
1259
+ MtnCloud::RestClient.enable_ssl_verification = false
1260
+ elsif !allow_insecure && !MtnCloud::RestClient.ssl_verification_enabled?
1261
+ MtnCloud::RestClient.enable_ssl_verification = true
1262
+ end
1263
+
1264
+ # always support accepting --username and --password on the command line
1265
+ # it's probably better not to do that tho, just so it stays out of history files
1266
+
1267
+ # if !@appliance_name && !@appliance_url
1268
+ # raise_command_error "Please specify a remote appliance with -r or see the command `remote use`"
1269
+ # end
1270
+
1271
+ MtnCloud::Logging::DarkPrinter.puts "establishing connection to remote #{display_appliance(@appliance_name, @appliance_url)}" if MtnCloud::Logging.debug? # && !options[:quiet]
1272
+
1273
+ if options[:no_authorization]
1274
+ # maybe handle this here..
1275
+ options[:skip_login] = true
1276
+ options[:skip_verify_access_token] = true
1277
+ end
1278
+
1279
+ # ok, get some credentials.
1280
+ # use saved credentials by default or prompts for username, password.
1281
+ # passing --remote-url will skip loading saved credentials and prompt for login to use with the url
1282
+ # passing --token skips login prompting and uses the provided token.
1283
+ # passing --token or --username will skip saving credentials to appliance config, they are just used for one command
1284
+ # ideally this should not prompt now and wait until the client is used on a protected endpoint.
1285
+ # @wallet = nil
1286
+ if options[:remote_token]
1287
+ @wallet = {'access_token' => options[:remote_token]} #'username' => 'anonymous'
1288
+ elsif options[:remote_url]
1289
+ credentials = MtnCloud::Cli::Credentials.new(@appliance_name, @appliance_url)
1290
+ unless options[:skip_login]
1291
+ @wallet = credentials.request_credentials(options.merge({dry_run:false}), @do_save_credentials)
1292
+ end
1293
+ else
1294
+ credentials = MtnCloud::Cli::Credentials.new(@appliance_name, @appliance_url)
1295
+ # use saved credentials unless --username or passed
1296
+ unless options[:remote_username]
1297
+ @wallet = credentials.load_saved_credentials()
1298
+ end
1299
+ # using active remote OR --remote flag
1300
+ # used saved credentials or login
1301
+ # ideally this sould not prompt now and wait until the client is used on a protected endpoint.
1302
+
1303
+
1304
+ if @wallet.nil? || @wallet['access_token'].nil?
1305
+ unless options[:skip_login]
1306
+ @wallet = credentials.request_credentials(options.merge({dry_run:false}), @do_save_credentials)
1307
+ end
1308
+ end
1309
+
1310
+ end
1311
+ @access_token = @wallet ? @wallet['access_token'] : nil
1312
+
1313
+ # validate we have a token
1314
+ # hrm...
1315
+ unless options[:skip_verify_access_token]
1316
+ if @access_token.empty?
1317
+ raise AuthorizationRequiredError.new("Failed to acquire access token for #{display_appliance(@appliance_name, @appliance_url)}. Verify your credentials are correct.")
1318
+ end
1319
+ end
1320
+
1321
+ # ok, connect to the appliance.. actually this just instantiates an ApiClient
1322
+ api_client = MtnCloud::APIClient.new(@access_token,nil,nil, @appliance_url, @verify_ssl)
1323
+ @api_client = api_client # meh, just return w/o setting instance attrs
1324
+ return api_client
1325
+ end
1326
+
1327
+ # verify_args! verifies that the right number of commands were passed
1328
+ # and raises a command error if not.
1329
+ # Example: verify_args!(args:args, count:1, optparse:optparse)
1330
+ # this could go be done in optparse.parse instead perhaps
1331
+ def verify_args!(opts={})
1332
+ args = opts[:args] || []
1333
+ count = opts[:count]
1334
+ # simplify output for verify_args!(min:2, max:2) or verify_args!(max:0)
1335
+ if opts[:min] && opts[:max] && opts[:min] == opts[:max]
1336
+ count = opts[:min]
1337
+ elsif opts[:max] == 0
1338
+ count = 0
1339
+ end
1340
+ if count
1341
+ if args.count < count
1342
+ raise_args_error("not enough arguments, expected #{count} and got #{args.count == 0 ? '0' : args.count.to_s + ': '}#{args.join(', ')}", args, opts[:optparse])
1343
+ elsif args.count > count
1344
+ raise_args_error("too many arguments, expected #{count} and got #{args.count == 0 ? '0' : args.count.to_s + ': '}#{args.join(', ')}", args, opts[:optparse])
1345
+ end
1346
+ else
1347
+ if opts[:min]
1348
+ if args.count < opts[:min]
1349
+ raise_args_error("not enough arguments, expected #{opts[:min] || '0'}-#{opts[:max] || 'N'} and got #{args.count == 0 ? '0' : args.count.to_s + ': '}#{args.join(', ')}", args, opts[:optparse])
1350
+ end
1351
+ end
1352
+ if opts[:max]
1353
+ if args.count > opts[:max]
1354
+ raise_args_error("too many arguments, expected #{opts[:min] || '0'}-#{opts[:max] || 'N'} and got #{args.count == 0 ? '0' : args.count.to_s + ': '}#{args.join(', ')}", args, opts[:optparse])
1355
+ end
1356
+ end
1357
+ end
1358
+ true
1359
+ end
1360
+
1361
+ def confirm(msg, options)
1362
+ options[:yes] or ::MtnCloud::Cli::OptionTypes::confirm(msg, options)
1363
+ end
1364
+
1365
+ def confirm!(msg, options)
1366
+ confirm(msg, options) or raise CommandAborted.new("confirmation declined: #{msg}")
1367
+ end
1368
+
1369
+ # The default way to build options for the list command
1370
+ # @param [OptionParser] opts
1371
+ # @param [Hash] options
1372
+ # @param [Hash] params
1373
+ def build_list_options(opts, options, params)
1374
+ build_standard_list_options(opts, options)
1375
+ end
1376
+
1377
+ # The default way to parse options for the list command
1378
+ # @param [Array] args
1379
+ # @param [Hash] options
1380
+ # @param [Hash] params
1381
+ def parse_list_options!(args, options, params)
1382
+ if args.count > 0
1383
+ options[:phrase] = args.join(" ")
1384
+ # params['phrase'] = = args.join(" ")
1385
+ end
1386
+ params.merge!(parse_list_options(options))
1387
+ # query parameters are stored in :params
1388
+ # preserve anything already set in :params by the OptionParser or command specific logic..
1389
+ options[:params] ||= {}
1390
+ options[:params].deep_merge!(params)
1391
+ return options
1392
+ end
1393
+
1394
+ # The default way to build options for the list command
1395
+ # @param [OptionParser] opts
1396
+ # @param [Hash] options
1397
+ # @param [Hash] params
1398
+ def build_get_options(opts, options, params)
1399
+ build_standard_get_options(opts, options)
1400
+ end
1401
+
1402
+ # The default way to parse options for the get command
1403
+ # @param [OptionParser] opts
1404
+ # @param [Hash] options
1405
+ # @param [Hash] params
1406
+ def parse_get_options!(args, options, params)
1407
+ params.merge!(parse_query_options(options))
1408
+ end
1409
+
1410
+ # Add a name=value query parameter, building an array if there is more than one
1411
+ # @param [Hash] params the query parameter map to append to
1412
+ # @param [String] key the parameter name
1413
+ # @param [Object] value the parameter value
1414
+ def add_query_parameter(params, key, value)
1415
+ # key = key.to_s
1416
+ if params.key?(key) && !params[key].nil?
1417
+ if !params[key].is_a?(Array)
1418
+ params[key] = [params[key]]
1419
+ end
1420
+ params[key] << value
1421
+ else
1422
+ params[key] = value
1423
+ end
1424
+ params
1425
+ end
1426
+
1427
+ # The default way to parse options for the get command
1428
+ # @param type [string]
1429
+ # @param options [Hash] The command options
1430
+ # @param params [Hash] The query parameters the output is being appended to
1431
+ # @param param_name [String]
1432
+ # @param lookup_ids [Boolean] Also lookup ids to make sure they exist or else error
1433
+ # @return
1434
+ def parse_parameter_as_resource_id!(type, options, params, param_name=nil, lookup_ids=false)
1435
+ # type = type.to_s.singularize
1436
+ if options.key?(type)
1437
+ val = options[type].to_s
1438
+ param_name ||= "#{type.to_s.camelcase}Id"
1439
+ if val
1440
+ if val.to_s !~ /\A\d{1,}\Z/ || lookup_ids
1441
+ record = find_by_name(type, val)
1442
+ if record.nil?
1443
+ # avoid double error render by exiting here, ew
1444
+ exit 1
1445
+ raise_command_error "#{type.titleize} not found for '#{val}'"
1446
+ end
1447
+ params[param_name] = record['id']
1448
+ else
1449
+ params[param_name] = val
1450
+ end
1451
+ return params[param_name]
1452
+ end
1453
+ end
1454
+ return nil
1455
+ end
1456
+
1457
+ # parse the parameters provided by the common :list options
1458
+ # this includes the :query options too via parse_query_options().
1459
+ # returns Hash of params the format {"phrase": => "foobar", "max": 100}
1460
+ def parse_list_options(options={})
1461
+ list_params = {}
1462
+ [:phrase, :offset, :max, :sort, :direction, :lastUpdated].each do |k|
1463
+ if options.key?(k)
1464
+ list_params[k.to_s] = options[k]
1465
+ elsif options.key?(k.to_s)
1466
+ list_params[k.to_s] = options[k.to_s]
1467
+ end
1468
+ end
1469
+ # arbitrary filters
1470
+ list_params.merge!(parse_query_options(options))
1471
+ # ok, any string keys in options can become query parameters, eg. options['name'] = 'foobar'
1472
+ # do it!
1473
+ # options.each do |k, v|
1474
+ # list_params[k] = v
1475
+ # end
1476
+ return list_params
1477
+ end
1478
+
1479
+ # parse the parameters provided by the common :query (:query_filters) options
1480
+ # returns Hash of params the format {"phrase": => "foobar", "max": 100}
1481
+ def parse_query_options(options={})
1482
+ list_params = {}
1483
+ # arbitrary filters
1484
+ if options[:query_filters]
1485
+ options[:query_filters].each do |k, v|
1486
+ if k
1487
+ list_params[k] = v
1488
+ end
1489
+ end
1490
+ end
1491
+ return list_params
1492
+ end
1493
+
1494
+ # parse the subtitles provided by the common :list options
1495
+ # returns Array of subtitles as strings in the format ["Phrase: blah", "Max: 100"]
1496
+ def parse_list_subtitles(options={})
1497
+ subtitles = []
1498
+ if options[:tenant]
1499
+ subtitles << "Tenant: #{options[:tenant]}".strip
1500
+ end
1501
+ if options[:include_tenants]
1502
+ subtitles << "Include Tenants: true".strip
1503
+ end
1504
+ [:phrase, :offset, :max, :sort, :direction, :lastUpdated].each do |k|
1505
+ if options.key?(k)
1506
+ subtitles << "#{k.to_s}: #{options[k]}"
1507
+ elsif options.key?(k.to_s)
1508
+ subtitles << "#{k.to_s}: #{options[k.to_s]}"
1509
+ end
1510
+ end
1511
+ # arbitrary filters
1512
+ if options[:query_filters]
1513
+ formatted_filters = options[:query_filters].collect {|k,v| "#{k.to_s}=#{v}" }.join('&')
1514
+ subtitles << "Query: #{formatted_filters}"
1515
+ # options[:query_filters].each do |k, v|
1516
+ # subtitles << "#{k.to_s}: #{v}"
1517
+ # end
1518
+ end
1519
+ return subtitles
1520
+ end
1521
+
1522
+ # Construct the request query parameters from the standard command options
1523
+ # This populates :params with a map of parameters.
1524
+ # This should replace the use of parse_query_options, parse_list_options and parse_list_options! and parse_get_options
1525
+ # which are used everywhere eg. params.merge!(parse_query_options(options))
1526
+ def parse_options(options, params={})
1527
+ params = params ? params.dup : {}
1528
+ # parse_list_options!(args, options, params)
1529
+ # merge in options set with -Q max=3, --query max=3&sort=id
1530
+ params.deep_merge!(options[:query_filters]) if options[:query_filters]
1531
+ # query parameters are stored in :params
1532
+ # preserve anything already set in :params by the OptionParser or command specific logic..
1533
+ options[:params] ||= {}
1534
+ options[:params].deep_merge!(params)
1535
+ # (JSON) body parameters (JSON) are stored in :payload
1536
+ # if options[:payload]
1537
+ # end
1538
+ # ok now call execute_request(@api_client.whoami, :get, nil, options)
1539
+ return options
1540
+ end
1541
+
1542
+ # Parse payload(s) from the standard command options or else invoke the given block.
1543
+ # First looks for --payload or --payload options and if they are nil then the block is executed to establish the payload
1544
+ # By default this also merges all the values passed with -O, --options foo="bar" into payload under the object_key context.
1545
+ # and they are merged under the object_key context (if passed). This can be disabled with apply_options: false
1546
+ #
1547
+ # @param options [Hash] standard command options
1548
+ # @option options [Hash] :payload is a Hash of objects to serialize as the payload
1549
+ # @option options [Hash] :payloads is an array of payload objects@yield [street_name] Invokes the block with a street name for eac
1550
+ # This is silly and should go away, instead you should iterate in the terminal environment
1551
+ # @option options [Boolean] :apply_options can be set to false to skip -O options merge
1552
+ # @param object_key [String] The name of the object being constructed, -O --options will be merged under this context.
1553
+ # @return array of payloads
1554
+ # @yield [payload] Invokes the block to establish :payload (only when --payload(s) is not used)
1555
+ def parse_payload(options, object_key=nil, &block)
1556
+ # populate options[:params] here too
1557
+ parse_options(options)
1558
+ payloads = []
1559
+ # todo: only need to support a a single payload here, :payloads is silly and is going away
1560
+ if options[:payload]
1561
+ # --payload option was used
1562
+ payload = options[:payload]
1563
+ # support -O OPTION switch on top of --payload
1564
+ apply_options(payload, options, object_key) unless options[:apply_options] == false
1565
+ payloads << payload
1566
+ elsif options[:payloads]
1567
+ # --payloads option was used
1568
+ payloads = options[:payloads]
1569
+ # support -O OPTION switch on top of --payloads
1570
+ payloads.each do |payload|
1571
+ apply_options(payload, options, object_key) unless options[:apply_options] == false
1572
+ end
1573
+ #else
1574
+ # should always do this, but a lot of methods rely on this returning nil right now, not {}
1575
+ # so for now only do it if block is given
1576
+ elsif block_given?
1577
+ # yield to block to construct the payload,
1578
+ # this is typically where prompting for inputs with optionTypes happens
1579
+ payload = {}
1580
+ apply_options(payload, options, object_key) unless options[:apply_options] == false
1581
+ if block_given?
1582
+ yield payload
1583
+ end
1584
+ payloads << payload
1585
+ options[:payload] = payload
1586
+ end
1587
+ return payloads.first
1588
+ end
1589
+
1590
+ # support -O OPTION switch
1591
+ def apply_options(payload, options, object_key=nil)
1592
+ payload ||= {}
1593
+ if options[:options]
1594
+ # allow options[:object_key] to be used
1595
+ object_key = object_key ? object_key : options[:object_key]
1596
+ # could use parse_passed_options() here to support exclusion of certain options
1597
+ #passed_options = parse_passed_options(options, options[:apply_options] || {})
1598
+ passed_options = options[:options].reject {|k,v| k.is_a?(Symbol)}
1599
+ if options[:apply_options_exclude]
1600
+ passed_options = options[:options].reject {|k,v| options[:skip_apply_options].include?(k.to_s) || options[:skip_apply_options].include?(k.to_sym) }
1601
+ end
1602
+ if object_key
1603
+ payload.deep_merge!({object_key => passed_options})
1604
+ else
1605
+ payload.deep_merge!(passed_options)
1606
+ end
1607
+ end
1608
+ payload
1609
+ end
1610
+
1611
+ # Executes the block with each payload (:payload or :payloads as parsed from --payload FILE and --payloads --PATH)
1612
+ # This is a wrapper to support execution on 1-N payloads
1613
+ # It also looks for --ignore-payload-errors behavior to continue processing
1614
+ # It is up to the block to actually make the api request
1615
+ # @param options [Hash] standard command options
1616
+ # @raise [Error] if there is no :payload or :payloads defined.
1617
+ # @yield [payload] Yields each payload to the block
1618
+ # @return parsed command result of the last return value of the block ie. [0, nil]
1619
+ def handle_each_payload(options, &block)
1620
+ payloads = []
1621
+ if options[:payloads]
1622
+ payloads = options[:payloads]
1623
+ elsif options[:payload]
1624
+ payloads << options[:payload]
1625
+ else
1626
+ raise "handle_each_payload() requires :payload or :payloads"
1627
+ end
1628
+ if !payloads.is_a?(Array) || payloads.compact.empty?
1629
+ raise "handle_each_payload() requires a payload"
1630
+ end
1631
+ if !block_given?
1632
+ raise "handle_each_payload() requires a block to process the payload(s) with"
1633
+ end
1634
+ results = []
1635
+ payloads.each do |payload|
1636
+ begin
1637
+ result = yield payload
1638
+ results << MtnCloud::Cli::CliRegistry.parse_command_result(result)
1639
+ #results << [0, nil]
1640
+ rescue => e
1641
+ if options[:payloads_ignore_error]
1642
+ # results << [1, e.message]
1643
+ result = MtnCloud::Cli::ErrorHandler.new(my_terminal.stderr).handle_error(e) # lol
1644
+ results << result
1645
+ # continue
1646
+ else
1647
+ raise e
1648
+ end
1649
+ end
1650
+ end
1651
+ return results.last
1652
+ end
1653
+
1654
+ # Standard handler for all commands that execute an api request.
1655
+ # This looks for a payload that can be set with --payload or --payloads or the default prompting
1656
+ # It is up to the block to handle the rendering behavior
1657
+ # @param api_interface [APIClient] An APIClient instance
1658
+ # @param api_method [String or Symbol] api method to invoke eg. :get, :create, :update, :destroy
1659
+ # @param args [Array] Array of arguments to be passed to the api method, usually just the [payload] or [payload, query_params]
1660
+ # @param options [Hash] options
1661
+ # @param object_key [String or Symbol] name of object being constructed, used by default rendering eg. --fields id,name
1662
+ # @yield [json_response] Invokes the block with the json response to handle rendering.
1663
+ # @return parsed command result of the last block.call(json_response)
1664
+ #
1665
+ # @example Fetch first 100 backups
1666
+ # execute_api(@api_client.backups, :list, [{"max" => 100}], options) do |json_response|
1667
+ # print_green_success "Fetched first #{json_response['backups'].size} of #{json_response['meta']['total']} backups"
1668
+ # end
1669
+ #
1670
+ # @example Create a backup, uses POST with options[:payload] as the body
1671
+ # @options[:payload] = {"backup" => { }}
1672
+ # execute_api(@api_client.backups, :create, nil, options, 'backup') do |json_response|
1673
+ # print_green_success "Added backup #{json_response['backup']['name']}"
1674
+ # end
1675
+ #
1676
+ def execute_api(api_interface, api_method, args, options, object_key=nil, &block)
1677
+ args = args.is_a?(Array) ? args : [args].compact
1678
+ if options[:payload] || options[:payloads]
1679
+ execute_api_payload(api_interface, api_method, args, options, object_key, &block)
1680
+ else
1681
+ execute_api_request(api_interface, api_method, args, options, object_key, &block)
1682
+ end
1683
+ end
1684
+
1685
+ # Standard handler for all POST commands that send a request for a payload
1686
+ # This supports the --payloads option for 1-N payloads, that is silly and will probably go away
1687
+ def execute_api_payload(api_interface, api_method, args, options, object_key=nil, &block)
1688
+ handle_each_payload(options) do |payload|
1689
+ execute_api_request(api_interface, api_method, (args || []) + [payload], options, object_key, &block)
1690
+ end
1691
+ end
1692
+
1693
+ # Standard handler for executing any API request
1694
+ # Supports the --dry-run option and standard rendering options --json, --yaml, --fields, --select, etc.
1695
+ def execute_api_request(api_interface, api_method, args, options, object_key=nil, &block)
1696
+ args = args.is_a?(Array) ? args : [args].compact # allow caller to pass [payload] or payload
1697
+ api_interface.setopts(options) # this is needed to support --timeout and --headers
1698
+ # this assumes the interface parameter order is: [payload, params] and not vice versa
1699
+ if options[:params] && !options[:params].empty?
1700
+ args << options[:params]
1701
+ end
1702
+ if options[:dry_run]
1703
+ # this is a dry run
1704
+ dry_response = api_interface.dry.send(api_method, *args)
1705
+ print_dry_run(dry_response, options)
1706
+ return 0, nil
1707
+ else
1708
+ # execute the request and render the result
1709
+ json_response = api_interface.send(api_method, *args)
1710
+ return render_response(json_response, options, object_key, &block)
1711
+ end
1712
+ end
1713
+
1714
+ # Parse an array from a string (csv)
1715
+ def parse_array(val, opts={})
1716
+ opts = {strip:true, allow_blank:false}.merge(opts)
1717
+ values = []
1718
+ if val.is_a?(Array)
1719
+ values = val
1720
+ else
1721
+ # parse csv and strip white space by default
1722
+ values = val.to_s.split(",")#.collect {|it| it.to_s.strip }
1723
+ if opts[:strip]
1724
+ values = values.collect {|it| it.to_s.strip }
1725
+ end
1726
+ if !opts[:allow_blank]
1727
+ values.reject! {|it| it.to_s.strip.empty? }
1728
+ end
1729
+ end
1730
+ values
1731
+ end
1732
+
1733
+ def parse_labels(val)
1734
+ parse_array(val, {strip:true, allow_blank:false})
1735
+ end
1736
+
1737
+ def validate_outfile(outfile, options)
1738
+ full_filename = File.expand_path(outfile)
1739
+ outdir = File.dirname(full_filename)
1740
+ if Dir.exist?(full_filename)
1741
+ print_red_alert "[local-file] is invalid. It is the name of an existing directory: #{outfile}"
1742
+ return false
1743
+ end
1744
+ if !Dir.exist?(outdir)
1745
+ if options[:mkdir]
1746
+ print cyan,"Creating local directory #{outdir}",reset,"\n"
1747
+ FileUtils.mkdir_p(outdir)
1748
+ else
1749
+ print_red_alert "[local-file] is invalid. Directory not found: #{outdir}"
1750
+ return false
1751
+ end
1752
+ end
1753
+ if File.exist?(full_filename) && !options[:overwrite]
1754
+ print_red_alert "[local-file] is invalid. File already exists: #{outfile}\nUse -f to overwrite the existing file."
1755
+ return false
1756
+ end
1757
+ return true
1758
+ end
1759
+
1760
+ # basic rendering for options :json, :yml, :csv, :quiet, and :outfile
1761
+ # returns the string rendered, or nil if nothing was rendered.
1762
+ def render_response(json_response, options, object_key=nil, &block)
1763
+ # allow options[:object_key] to be used
1764
+ object_key = object_key ? object_key : options[:object_key]
1765
+ output = nil
1766
+ if options[:select_fields]
1767
+ # support foos get --raw-select foo.x,foo.y,foo.z
1768
+ # and foos list --raw-select foos.x,foos.y,foos.z
1769
+ row = (object_key && !options[:raw_fields]) ? json_response[object_key] : json_response
1770
+ records = [row].flatten()
1771
+ # look for an array in the first field only now...
1772
+ field_parts = options[:select_fields][0].to_s.split(".")
1773
+ field_context = field_parts[0]
1774
+ context_data = json_response[field_context]
1775
+ if field_parts.size > 1 && context_data.is_a?(Array)
1776
+ # inject all the root level properties to be selectable too..
1777
+ context_data = json_response.delete(field_context)
1778
+ # records = context_data
1779
+ records = context_data.collect {|it| it.is_a?(Hash) ? json_response.merge(it) : json_response }
1780
+ options[:select_fields] = options[:select_fields].collect {|it| it.sub(field_context+'.', '')}
1781
+ end
1782
+ output = records.collect { |record|
1783
+ options[:select_fields].collect { |field|
1784
+ value = get_object_value(record, field)
1785
+ if value.is_a?(String)
1786
+ value
1787
+ else
1788
+ if options[:json]
1789
+ as_json(value, options)
1790
+ elsif options[:yaml]
1791
+ output = as_yaml(value, options)
1792
+ elsif options[:csv]
1793
+ as_csv(value, nil, options)
1794
+ else
1795
+ # default behavior
1796
+ # value.is_a?(String) ? value : as_json(value, options)
1797
+ do_pretty = options.key?(:pretty_json) ? options[:pretty_json] : false # or true?
1798
+ do_pretty ? JSON.pretty_generate(value) : JSON.fast_generate(value)
1799
+ end
1800
+ end
1801
+ }.join(options[:delim] || ",")
1802
+ }.join(options[:newline] || "\n")
1803
+ elsif options[:json]
1804
+ output = as_json(json_response, options, object_key)
1805
+ elsif options[:yaml]
1806
+ output = as_yaml(json_response, options, object_key)
1807
+ elsif options[:csv]
1808
+ output = as_csv(json_response, nil, options, object_key)
1809
+ end
1810
+ if options[:outfile]
1811
+ full_outfile = File.expand_path(options[:outfile])
1812
+ if output
1813
+ print_to_file(output, options[:outfile], options[:overwrite])
1814
+ print "#{cyan}Wrote output to file #{options[:outfile]} (#{format_bytes(File.size(full_outfile))})\n" unless options[:quiet]
1815
+ else
1816
+ # uhhh ok lets try this
1817
+ MtnCloud::Logging::DarkPrinter.puts "using experimental feature: --out without a common format like json, yml or csv" if MtnCloud::Logging.debug?
1818
+ result = with_stdout_to_file(options[:outfile], options[:overwrite], 'w+', &block)
1819
+ if result && result != 0
1820
+ return result
1821
+ end
1822
+ print "#{cyan}Wrote output to file #{options[:outfile]} (#{format_bytes(File.size(full_outfile))})\n" unless options[:quiet]
1823
+ return 0, nil
1824
+ end
1825
+ else
1826
+ # --quiet means do not render, still want to print to outfile though
1827
+ if options[:quiet]
1828
+ return 0, nil
1829
+ end
1830
+ # render ouput generated above
1831
+ if output
1832
+ puts output
1833
+ return 0, nil
1834
+ else
1835
+ # no render happened, so calling the block if given
1836
+ if block_given?
1837
+ result = yield json_response
1838
+ if result
1839
+ return result
1840
+ else
1841
+ return 0, nil
1842
+ end
1843
+ else
1844
+ # nil means nothing was rendered, some methods still using render_with_format() are relying on this
1845
+ return nil
1846
+ end
1847
+ end
1848
+ end
1849
+ end
1850
+
1851
+ alias :render_with_format :render_response
1852
+
1853
+ # Dynamic find methods to load a record of any type
1854
+ # def find_by_name_or_id(type, val)
1855
+ # interface = instance_variable_get "@#{type}s_interface"
1856
+ # typeCamelCase = type.gsub(/(?:^|_)([a-z])/) do $1.upcase end
1857
+ # typeCamelCase = typeCamelCase[0, 1].downcase + typeCamelCase[1..-1]
1858
+ # (val.to_s =~ /\A\d{1,}\Z/) ? interface.get(val.to_i)[typeCamelCase] : interface.list({'name' => val})["#{typeCamelCase}s"].first
1859
+ # end
1860
+
1861
+ # Find a resource by type and name or id
1862
+ # @param type [String of Symbol] Type of resource formatted as singular, lowerscore with underscores.
1863
+ # @param *id [String or Numeric] ID of resource, multiple arguments may be passed when using a secondary interface where parent_id, id is required.
1864
+ # Example: find_by_name_or_id("instance", "K2")
1865
+ # find_by_name_or_id("storage_volume", 42)
1866
+ # find_by_name_or_id("instance", "My Instance")
1867
+ # find_by_name_or_id("load_balancer_pool", load_balancer_id, id)
1868
+ def find_by_name_or_id(*args)
1869
+ val = args.last
1870
+ if val.to_s =~ /\A\d{1,}\Z/
1871
+ return find_by_id(*args)
1872
+ else
1873
+ return find_by_name(*args)
1874
+ end
1875
+ end
1876
+
1877
+
1878
+ # Find a resource by type and id
1879
+ # Usage: find_by_name_or_id("app", 3)
1880
+ def find_by_id(*args)
1881
+ #MtnCloud::Logging::DarkPrinter.puts "find_by_id(#{args.join(', ')})" if MtnCloud::Logging.debug?
1882
+ # type, ids = args.first, args[1..-1]
1883
+ type, *ids = args
1884
+ type = type.to_s.singularize.underscore
1885
+ # still relying on the command or helper to define these _label and _key methods
1886
+ # label = send("#{type}_label")
1887
+ # object_key = send("#{type}_object_key")
1888
+ # ^ nope, not for long!
1889
+ object_key = respond_to?("#{type}_object_key", true) ? send("#{type}_object_key") : type.camelcase.singularize
1890
+ label = respond_to?("#{type}_label", true) ? send("#{type}_label") : type.titleize
1891
+ interface_name = "@#{type.pluralize}_interface"
1892
+ interface = instance_variable_get(interface_name)
1893
+ if interface.nil?
1894
+ api_client = instance_variable_get("@api_client")
1895
+ if api_client
1896
+ if api_client.respond_to?(type.pluralize)
1897
+ interface = api_client.send(type.pluralize)
1898
+ else
1899
+ raise "@api_client.#{type.pluralize} is not a recognized interface"
1900
+ end
1901
+ else
1902
+ raise "#{self.class} has not defined interface #{interface_name} or @api_client"
1903
+ end
1904
+ end
1905
+ begin
1906
+ json_response = interface.get(*ids)
1907
+ if !json_response.key?(object_key)
1908
+ # maybe just use the first key like this:
1909
+ # object_key = json_response.keys.find { |k| json_response[k].is_a?(Hash) }
1910
+ #puts_error(json_response) if MtnCloud::Logging.debug?
1911
+ raise "API response is missing object property '#{object_key}'"
1912
+ end
1913
+ record = json_response[object_key]
1914
+ if record.nil?
1915
+ print_red_alert "#{label} not found in API response (#{object_key})"
1916
+ return nil
1917
+ else
1918
+ return json_response[object_key]
1919
+ end
1920
+ rescue ::RestClient::Exception => e
1921
+ if e.response && e.response.code == 404
1922
+ print_red_alert "#{label} not found by id #{ids.last}"
1923
+ return nil
1924
+ else
1925
+ raise e
1926
+ end
1927
+ end
1928
+ end
1929
+
1930
+ # Find a record by type and name
1931
+ # Usage: find_by_name_or_id("network", "Skynet")
1932
+ def find_by_name(*args)
1933
+ #MtnCloud::Logging::DarkPrinter.puts "find_by_name(#{args.join(', ')})" if MtnCloud::Logging.debug?
1934
+ # type, ids = args.first, args[1..-1]
1935
+ type, *ids = args
1936
+ type = type.to_s.singularize.underscore
1937
+ val = ids.pop
1938
+ params = {}
1939
+ name_property = 'name'
1940
+ if type == 'user'
1941
+ name_property = 'username'
1942
+ params['global'] = 'true'
1943
+ end
1944
+ params[name_property] = val.to_s
1945
+ request_args = ids + [params]
1946
+ request_args.unshift(type)
1947
+ records = find_all(*request_args)
1948
+ # still relying on the command or helper to define these _label and _key methods
1949
+ label = respond_to?("#{type}_label", true) ? send("#{type}_label") : type.titleize
1950
+ if records.empty?
1951
+ print_red_alert "#{label} not found by name '#{val}'"
1952
+ return nil
1953
+ elsif records.size > 1
1954
+ print_red_alert "More than one #{label.downcase} found by #{name_property} '#{val}'"
1955
+ print_error "\n"
1956
+ if type == "user"
1957
+ puts_error as_pretty_table(records, [:id, :username, {"FIRST NAME" => "firstName"}, {"LAST NAME" => "lastName"}, {"TENANT" => lambda {|it| it['account']['name'] rescue ''}}], {color:red})
1958
+ else
1959
+ puts_error as_pretty_table(records, [:id, :name], {color:red})
1960
+ end
1961
+ print_red_alert "Try using ID instead"
1962
+ print_error reset,"\n"
1963
+ return nil
1964
+ else
1965
+ return records[0]
1966
+ end
1967
+ end
1968
+
1969
+ # Load a list of records by type
1970
+ # @example Find an app by id
1971
+ # find_record(:app, 1)
1972
+ # report_types = find_all("reportTypes", {phrase:"amazon"})
1973
+ # pools = find_all("loadBalancerPool", load_balancer_id)
1974
+ # @return Array of records
1975
+ def find_all(*args)
1976
+ #MtnCloud::Logging::DarkPrinter.puts "find_all(#{args.join(', ')})" if MtnCloud::Logging.debug?
1977
+ type, *request_args = args
1978
+ type = type.to_s.singularize.underscore
1979
+ list_key = respond_to?("#{type}_list_key", true) ? send("#{type}_list_key") : get_list_key(type)
1980
+ json_response = find_all_json(*args)
1981
+ if !json_response.key?(list_key)
1982
+ # maybe just use the first key like this:
1983
+ # list_key = json_response.keys.find { |k| json_response[k].is_a?(Array) }
1984
+ # puts_error(json_response) if MtnCloud::Logging.debug?
1985
+ raise "API response is missing list property '#{list_key}'"
1986
+ end
1987
+ return json_response[list_key]
1988
+ end
1989
+
1990
+ # Load json response for a list of records by type
1991
+ # @return Hash of JSON data
1992
+ def find_all_json(*args)
1993
+ type, *request_args = args
1994
+ get_interface(type).list(*request_args)
1995
+ end
1996
+
1997
+ alias :find_all_records :find_all
1998
+ alias :find_all_records_json :find_all_json
1999
+
2000
+ # Load a single record (Hash) by type and id and optional parameters
2001
+ # Examples:
2002
+ # apps = find_record(:app)
2003
+ # report_types = find_record("reportTypes", {phrase:"amazon"})
2004
+ # pools = find_record("loadBalancerPool", balancer_id)
2005
+ # @return [Hash] of the object that was found or raises an exception if 404 not found encountered.
2006
+ def find_record(*args)
2007
+ #MtnCloud::Logging::DarkPrinter.puts "find_record(#{args.join(', ')})" if MtnCloud::Logging.debug?
2008
+ type, *request_args = args
2009
+ type = type.to_s.singularize.underscore
2010
+ object_key = respond_to?("#{type}_object_key", true) ? send("#{type}_object_key") : get_object_key(type)
2011
+ json_response = find_record_json(*args)
2012
+ if !json_response.key?(object_key)
2013
+ # maybe just use the first key like this:
2014
+ # object_key = json_response.keys.find { |k| json_response[k].is_a?(Hash) }
2015
+ # puts_error(json_response) if MtnCloud::Logging.debug?
2016
+ raise "API response is missing object property '#{object_key}'"
2017
+ end
2018
+ return json_response[object_key]
2019
+ end
2020
+
2021
+ # Load list of records by type and (optional) parameters
2022
+ # Examples:
2023
+ # apps = find_record(:app, 1)
2024
+ # report_types = find_record("reportType", 1)
2025
+ # pools = find_all("loadBalancerPool", load_balancer_id)
2026
+ def find_record_json(*args)
2027
+ #MtnCloud::Logging::DarkPrinter.puts "find_record_json(#{args.join(', ')})" if MtnCloud::Logging.debug?
2028
+ type, *request_args = args
2029
+ get_interface(type).get(*request_args)
2030
+ end
2031
+
2032
+ # Load json response for a list of records by type
2033
+ def get_interface(type)
2034
+ #MtnCloud::Logging::DarkPrinter.puts "get_interface(#{type})" if MtnCloud::Logging.debug?
2035
+ # todo: can probably just do @api_client ? @api_client.interface(interface_name) : nil
2036
+ type = type.to_s.singularize.underscore
2037
+ interface_name = "@#{type.pluralize}_interface"
2038
+ interface = nil
2039
+ if instance_variable_defined?(interface_name)
2040
+ interface = instance_variable_get(interface_name)
2041
+ if interface.nil?
2042
+ # Fix is to update connect() to do @apps_interface = @api_client.apps
2043
+ raise "API Interface #{interface_name} is nil"
2044
+ end
2045
+ else
2046
+ if @api_client.is_a?(MtnCloud::APIClient)
2047
+ interface = @api_client.interface(type.pluralize)
2048
+ else
2049
+ raise "#{self.class} has not initalized @api_client"
2050
+ end
2051
+ end
2052
+ return interface
2053
+ end
2054
+
2055
+ def get_list_key(type)
2056
+ return get_object_key(type).pluralize
2057
+ end
2058
+
2059
+ def get_object_key(type)
2060
+ key = type.camelcase.singularize
2061
+ # add aliases here as needed
2062
+ if key == "cloud"
2063
+ key = "zone"
2064
+ elsif key == "site"
2065
+ key = "group"
2066
+ elsif key == "backupJob"
2067
+ key = "job"
2068
+ elsif key == "backupResult"
2069
+ key = "result"
2070
+ elsif key == "backupRestore"
2071
+ key = "restore"
2072
+ end
2073
+ return key
2074
+ end
2075
+
2076
+ module ClassMethods
2077
+
2078
+ # attr_writer :command_name, :command_description, :hidden_command, :default_refresh_interval,
2079
+ # :subcommands, :hidden_subcommands, :default_subcommand, :subcommand_aliases, :subcommand_descriptions
2080
+
2081
+ def prog_name
2082
+ MtnCloud::Cli.prog_name
2083
+ end
2084
+
2085
+ def set_command_name(cmd_name)
2086
+ @command_name = cmd_name.to_sym
2087
+ MtnCloud::Cli::CliRegistry.add(self, self.command_name)
2088
+ end
2089
+
2090
+ def default_command_name
2091
+ class_name = self.name.split('::')[-1]
2092
+ #class_name.sub!(/Command$/, '')
2093
+ MtnCloud::Cli::CliRegistry.cli_ize(class_name).to_sym
2094
+ end
2095
+
2096
+ def command_name
2097
+ @command_name ||= default_command_name
2098
+ @command_name
2099
+ end
2100
+
2101
+ def set_command_hidden(val=true)
2102
+ @hidden_command = val
2103
+ end
2104
+ # alias :command_name= :set_command_name
2105
+
2106
+ def hidden_command
2107
+ defined?(@hidden_command) && @hidden_command == true
2108
+ end
2109
+
2110
+ def hidden_subcommands
2111
+ @hidden_subcommands ||= []
2112
+ end
2113
+
2114
+ def set_subcommands_hidden(*cmds)
2115
+ @hidden_subcommands ||= []
2116
+ cmds.flatten.each do |cmd|
2117
+ @hidden_subcommands << cmd.to_sym
2118
+ end
2119
+ @hidden_subcommands
2120
+ end
2121
+
2122
+ def command_description
2123
+ @command_description ||= ""
2124
+ end
2125
+
2126
+ def set_command_description(val)
2127
+ @command_description = val
2128
+ end
2129
+ # alias :command_description= :set_command_description
2130
+
2131
+ def default_refresh_interval
2132
+ @default_refresh_interval ||= 30
2133
+ end
2134
+
2135
+ def set_default_refresh_interval(seconds)
2136
+ @default_refresh_interval = seconds
2137
+ end
2138
+ #alias :default_refresh_interval= :set_default_refresh_interval
2139
+
2140
+
2141
+ # construct map of command name => instance method
2142
+ def register_subcommands(*cmds)
2143
+ @subcommands ||= {}
2144
+ cmds.flatten.each {|cmd|
2145
+ if cmd.is_a?(Hash)
2146
+ cmd.each {|k,v|
2147
+ # @subcommands[k] = v
2148
+ add_subcommand(k.to_s, v.to_s)
2149
+ }
2150
+ elsif cmd.is_a?(Array)
2151
+ cmd.each {|it| register_subcommands(it) }
2152
+ elsif cmd.is_a?(String) || cmd.is_a?(Symbol)
2153
+ #k = MtnCloud::Cli::CliRegistry.cli_ize(cmd)
2154
+ k = cmd.to_s.gsub('_', '-')
2155
+ v = cmd.to_s.gsub('-', '_')
2156
+ register_subcommands({(k) => v})
2157
+ else
2158
+ raise MtnCloud::Cli::CliRegistry::BadCommandDefinition.new("Unable to register command of type: #{cmd.class} #{cmd}")
2159
+ end
2160
+ }
2161
+ return
2162
+ end
2163
+
2164
+ # this might be the new hotness
2165
+ # register_subcommand(:show) # do not do this, always define a description!
2166
+ # register_subcommand(:list, "List things")
2167
+ # register_subcommand("update-all", "update_all", "Update all things")
2168
+ # If the command name =~ method, no need to pass both
2169
+ # command names will have "-" swapped in for "_" and vice versa for method names.
2170
+ def register_subcommand(*args)
2171
+ args = args.flatten
2172
+ cmd_name = args[0]
2173
+ cmd_method = nil
2174
+ cmd_desc = nil
2175
+ if args.count == 1
2176
+ cmd_method = cmd_name
2177
+ elsif args.count == 2
2178
+ if args[1].is_a?(Symbol)
2179
+ cmd_method = args[1]
2180
+ else
2181
+ cmd_method = cmd_name
2182
+ cmd_desc = args[1]
2183
+ end
2184
+ elsif args.count == 3
2185
+ cmd_method = args[1]
2186
+ cmd_desc = args[2]
2187
+ else
2188
+ raise MtnCloud::Cli::CliRegistry::BadCommandDefinition.new("register_subcommand expects 1-3 arguments, got #{args.size} #{args.inspect}")
2189
+ end
2190
+ cmd_name = cmd_name.to_s.gsub("_", "-").to_sym
2191
+ cmd_method = (cmd_method || cmd_name).to_s.gsub("-", "_").to_sym
2192
+ cmd_definition = {(cmd_name) => cmd_method}
2193
+ register_subcommands(cmd_definition)
2194
+ add_subcommand_description(cmd_name, cmd_desc)
2195
+ return
2196
+ end
2197
+
2198
+ def set_default_subcommand(cmd)
2199
+ @default_subcommand = cmd
2200
+ end
2201
+
2202
+ def default_subcommand
2203
+ @default_subcommand
2204
+ end
2205
+
2206
+ def subcommands
2207
+ @subcommands ||= {}
2208
+ end
2209
+
2210
+ def visible_subcommands
2211
+ cmds = subcommands.clone
2212
+ hidden_subcommands.each do |hidden_cmd|
2213
+ cmds.delete(hidden_cmd.to_s.gsub('_', '-'))
2214
+ end
2215
+ cmds
2216
+ end
2217
+
2218
+ def has_subcommand?(cmd_name)
2219
+ return false if cmd_name.empty?
2220
+ @subcommands && @subcommands[cmd_name.to_s]
2221
+ end
2222
+
2223
+ def add_subcommand(cmd_name, method)
2224
+ @subcommands ||= {}
2225
+ @subcommands[cmd_name.to_s] = method
2226
+ end
2227
+
2228
+ def remove_subcommand(cmd_name)
2229
+ @subcommands ||= {}
2230
+ @subcommands.delete(cmd_name.to_s)
2231
+ end
2232
+
2233
+ # register an alias for a command
2234
+ def alias_subcommand(alias_cmd_name, cmd_name)
2235
+ add_subcommand_alias(alias_cmd_name.to_s, cmd_name.to_s.gsub('_', '-'))
2236
+ return
2237
+ end
2238
+
2239
+ def subcommand_aliases
2240
+ @subcommand_aliases ||= {}
2241
+ end
2242
+
2243
+ def add_subcommand_alias(alias_cmd_name, cmd_name)
2244
+ @subcommand_aliases ||= {}
2245
+ @subcommand_aliases[alias_cmd_name.to_s] = cmd_name
2246
+ end
2247
+
2248
+ def remove_subcommand_alias(alias_cmd_name)
2249
+ @subcommand_aliases ||= {}
2250
+ @subcommand_aliases.delete(alias_cmd_name.to_s)
2251
+ end
2252
+
2253
+ def subcommand_descriptions
2254
+ @subcommand_descriptions ||= {}
2255
+ end
2256
+
2257
+ def add_subcommand_description(cmd_name, description)
2258
+ @subcommand_descriptions ||= {}
2259
+ @subcommand_descriptions[cmd_name.to_s.gsub('_', '-')] = description
2260
+ end
2261
+
2262
+ def get_subcommand_description(cmd_name)
2263
+ desc = subcommand_descriptions[cmd_name.to_s.gsub('_', '-')]
2264
+ if desc
2265
+ return desc
2266
+ else
2267
+ cmd_method = subcommands.key(cmd_name)
2268
+ return cmd_method ? subcommand_descriptions[cmd_method.to_s.gsub('_', '-')] : nil
2269
+ end
2270
+ end
2271
+
2272
+ def set_subcommand_descriptions(cmd_map)
2273
+ cmd_map.each do |cmd_name, description|
2274
+ add_subcommand_description(cmd_name, description)
2275
+ end
2276
+ end
2277
+
2278
+ end
2279
+ end
2280
+ end
2281
+ end