morpheus-cli 4.1.8 → 4.1.9

Sign up to get free protection for your applications and to get access to all the features.
Files changed (72) hide show
  1. checksums.yaml +4 -4
  2. data/Dockerfile +1 -1
  3. data/lib/morpheus/api/api_client.rb +24 -0
  4. data/lib/morpheus/api/{old_cypher_interface.rb → budgets_interface.rb} +10 -11
  5. data/lib/morpheus/api/cloud_datastores_interface.rb +7 -0
  6. data/lib/morpheus/api/cloud_resource_pools_interface.rb +2 -2
  7. data/lib/morpheus/api/cypher_interface.rb +18 -12
  8. data/lib/morpheus/api/health_interface.rb +72 -0
  9. data/lib/morpheus/api/instances_interface.rb +1 -1
  10. data/lib/morpheus/api/library_instance_types_interface.rb +7 -0
  11. data/lib/morpheus/api/log_settings_interface.rb +6 -0
  12. data/lib/morpheus/api/network_security_servers_interface.rb +30 -0
  13. data/lib/morpheus/api/price_sets_interface.rb +42 -0
  14. data/lib/morpheus/api/prices_interface.rb +68 -0
  15. data/lib/morpheus/api/provisioning_settings_interface.rb +29 -0
  16. data/lib/morpheus/api/servers_interface.rb +1 -1
  17. data/lib/morpheus/api/service_plans_interface.rb +34 -11
  18. data/lib/morpheus/api/task_sets_interface.rb +8 -0
  19. data/lib/morpheus/api/tasks_interface.rb +8 -0
  20. data/lib/morpheus/cli.rb +6 -3
  21. data/lib/morpheus/cli/appliance_settings_command.rb +13 -5
  22. data/lib/morpheus/cli/approvals_command.rb +1 -1
  23. data/lib/morpheus/cli/apps.rb +88 -28
  24. data/lib/morpheus/cli/backup_settings_command.rb +1 -1
  25. data/lib/morpheus/cli/blueprints_command.rb +2 -0
  26. data/lib/morpheus/cli/budgets_command.rb +672 -0
  27. data/lib/morpheus/cli/cli_command.rb +13 -2
  28. data/lib/morpheus/cli/cli_registry.rb +1 -0
  29. data/lib/morpheus/cli/clusters.rb +40 -274
  30. data/lib/morpheus/cli/commands/standard/benchmark_command.rb +114 -66
  31. data/lib/morpheus/cli/commands/standard/coloring_command.rb +12 -0
  32. data/lib/morpheus/cli/commands/standard/curl_command.rb +31 -6
  33. data/lib/morpheus/cli/commands/standard/echo_command.rb +8 -3
  34. data/lib/morpheus/cli/commands/standard/set_prompt_command.rb +1 -1
  35. data/lib/morpheus/cli/containers_command.rb +37 -24
  36. data/lib/morpheus/cli/cypher_command.rb +191 -150
  37. data/lib/morpheus/cli/health_command.rb +903 -0
  38. data/lib/morpheus/cli/hosts.rb +43 -32
  39. data/lib/morpheus/cli/instances.rb +119 -68
  40. data/lib/morpheus/cli/jobs_command.rb +1 -1
  41. data/lib/morpheus/cli/library_instance_types_command.rb +61 -11
  42. data/lib/morpheus/cli/library_option_types_command.rb +2 -2
  43. data/lib/morpheus/cli/log_settings_command.rb +46 -3
  44. data/lib/morpheus/cli/logs_command.rb +24 -17
  45. data/lib/morpheus/cli/mixins/accounts_helper.rb +2 -0
  46. data/lib/morpheus/cli/mixins/logs_helper.rb +73 -19
  47. data/lib/morpheus/cli/mixins/print_helper.rb +29 -1
  48. data/lib/morpheus/cli/mixins/provisioning_helper.rb +554 -96
  49. data/lib/morpheus/cli/mixins/whoami_helper.rb +13 -1
  50. data/lib/morpheus/cli/networks_command.rb +3 -0
  51. data/lib/morpheus/cli/option_types.rb +83 -53
  52. data/lib/morpheus/cli/price_sets_command.rb +543 -0
  53. data/lib/morpheus/cli/prices_command.rb +669 -0
  54. data/lib/morpheus/cli/processes_command.rb +0 -2
  55. data/lib/morpheus/cli/provisioning_settings_command.rb +237 -0
  56. data/lib/morpheus/cli/remote.rb +9 -4
  57. data/lib/morpheus/cli/reports_command.rb +10 -4
  58. data/lib/morpheus/cli/roles.rb +93 -38
  59. data/lib/morpheus/cli/security_groups.rb +10 -0
  60. data/lib/morpheus/cli/service_plans_command.rb +736 -0
  61. data/lib/morpheus/cli/tasks.rb +220 -8
  62. data/lib/morpheus/cli/tenants_command.rb +3 -16
  63. data/lib/morpheus/cli/users.rb +2 -25
  64. data/lib/morpheus/cli/version.rb +1 -1
  65. data/lib/morpheus/cli/whitelabel_settings_command.rb +18 -18
  66. data/lib/morpheus/cli/whoami.rb +28 -10
  67. data/lib/morpheus/cli/workflows.rb +488 -36
  68. data/lib/morpheus/formatters.rb +22 -0
  69. data/morpheus-cli.gemspec +1 -0
  70. metadata +28 -5
  71. data/lib/morpheus/cli/accounts.rb +0 -335
  72. data/lib/morpheus/cli/old_cypher_command.rb +0 -412
@@ -321,89 +321,137 @@ EOT
321
321
  return 1
322
322
  end
323
323
 
324
- cmd = args.join(' ')
325
- benchmark_name ||= cmd
326
- if n == 1
327
- start_benchmark(benchmark_name)
328
- # exit_code, err = my_terminal.execute(cmd)
329
- cmd_result = Morpheus::Cli::CliRegistry.exec_expression(cmd)
330
- exit_code, err = Morpheus::Cli::CliRegistry.parse_command_result(cmd_result)
331
- benchmark_record = stop_benchmark(exit_code, err)
332
- Morpheus::Logging::DarkPrinter.puts(cyan + dark + benchmark_record.msg) if benchmark_record
333
- else
334
- benchmark_records = []
335
- n.times do |iteration_index|
324
+ exit_code = 0
325
+ out = ""
326
+
327
+ original_stdout = nil
328
+ begin
329
+ # --quiet actually unhooks stdout for this command
330
+ if options[:quiet]
331
+ original_stdout = my_terminal.stdout
332
+ my_terminal.set_stdout(Morpheus::Terminal::Blackhole.new)
333
+ end
334
+
335
+ cmd = args.join(' ')
336
+ benchmark_name ||= cmd
337
+
338
+
339
+ if n == 1
336
340
  start_benchmark(benchmark_name)
337
341
  # exit_code, err = my_terminal.execute(cmd)
338
342
  cmd_result = Morpheus::Cli::CliRegistry.exec_expression(cmd)
339
343
  exit_code, err = Morpheus::Cli::CliRegistry.parse_command_result(cmd_result)
340
344
  benchmark_record = stop_benchmark(exit_code, err)
341
- Morpheus::Logging::DarkPrinter.puts(cyan + dark + benchmark_record.msg) if Morpheus::Logging.debug?
342
- benchmark_records << benchmark_record
343
- end
344
- # calc total and mean and print it
345
- # all_durations = benchmark_records.collect {|benchmark_record| benchmark_record.duration }
346
- # total_duration = all_durations.inject(0.0) {|acc, i| acc + i }
347
- # avg_duration = total_duration / all_durations.size
348
- # total_time_str = "#{total_duration.round((total_duration > 0.002) ? 3 : 6)}s"
349
- # avg_time_str = "#{avg_duration.round((total_duration > 0.002) ? 3 : 6)}s"
345
+ # Morpheus::Logging::DarkPrinter.puts(cyan + dark + benchmark_record.msg) if benchmark_record
346
+ # return 0
347
+ if original_stdout
348
+ my_terminal.set_stdout(original_stdout)
349
+ original_stdout = nil
350
+ end
351
+ out = ""
352
+ # <benchmark name or command>
353
+ out << "#{benchmark_name.ljust(30, ' ')}"
354
+ # exit: 0
355
+ exit_code = benchmark_record.exit_code
356
+ bad_benchmark = benchmark_record.exit_code && benchmark_record.exit_code != 0
357
+ if bad_benchmark
358
+ out << "\texit: #{bad_benchmark.exit_code.to_s.ljust(2, ' ')}"
359
+ out << "\terror: #{bad_benchmark.error.to_s.ljust(12, ' ')}"
360
+ else
361
+ out << "\texit: 0 "
362
+ end
363
+ else
364
+ benchmark_records = []
365
+ n.times do |iteration_index|
366
+ start_benchmark(benchmark_name)
367
+ # exit_code, err = my_terminal.execute(cmd)
368
+ cmd_result = Morpheus::Cli::CliRegistry.exec_expression(cmd)
369
+ exit_code, err = Morpheus::Cli::CliRegistry.parse_command_result(cmd_result)
370
+ benchmark_record = stop_benchmark(exit_code, err)
371
+ Morpheus::Logging::DarkPrinter.puts(cyan + dark + benchmark_record.msg) if Morpheus::Logging.debug?
372
+ benchmark_records << benchmark_record
373
+ end
374
+ if original_stdout
375
+ my_terminal.set_stdout(original_stdout)
376
+ original_stdout = nil
377
+ end
378
+ # calc total and mean and print it
379
+ # all_durations = benchmark_records.collect {|benchmark_record| benchmark_record.duration }
380
+ # total_duration = all_durations.inject(0.0) {|acc, i| acc + i }
381
+ # avg_duration = total_duration / all_durations.size
382
+ # total_time_str = "#{total_duration.round((total_duration > 0.002) ? 3 : 6)}s"
383
+ # avg_time_str = "#{avg_duration.round((total_duration > 0.002) ? 3 : 6)}s"
350
384
 
351
- all_durations = []
352
- stats = {total: 0, avg: nil, min: nil, max: nil}
353
- benchmark_records.each do |benchmark_record|
354
- duration = benchmark_record.duration
355
- if duration
356
- all_durations << duration
357
- stats[:total] += duration
358
- if stats[:min].nil? || stats[:min] > duration
359
- stats[:min] = duration
360
- end
361
- if stats[:max].nil? || stats[:max] < duration
362
- stats[:max] = duration
385
+ all_durations = []
386
+ stats = {total: 0, avg: nil, min: nil, max: nil}
387
+ benchmark_records.each do |benchmark_record|
388
+ duration = benchmark_record.duration
389
+ if duration
390
+ all_durations << duration
391
+ stats[:total] += duration
392
+ if stats[:min].nil? || stats[:min] > duration
393
+ stats[:min] = duration
394
+ end
395
+ if stats[:max].nil? || stats[:max] < duration
396
+ stats[:max] = duration
397
+ end
363
398
  end
364
399
  end
365
- end
366
- if all_durations.size > 0
367
- stats[:avg] = stats[:total].to_f / all_durations.size
368
- end
400
+ if all_durations.size > 0
401
+ stats[:avg] = stats[:total].to_f / all_durations.size
402
+ end
369
403
 
370
- total_time_str = "#{stats[:total].round((stats[:total] > 0.002) ? 3 : 6)}s"
371
- min_time_str = stats[:min] ? "#{stats[:min].round((stats[:min] > 0.002) ? 3 : 6)}s" : ""
372
- max_time_str = stats[:max] ? "#{stats[:max].round((stats[:max] > 0.002) ? 3 : 6)}s" : ""
373
- avg_time_str = stats[:avg] ? "#{stats[:avg].round((stats[:avg] > 0.002) ? 3 : 6)}s" : ""
404
+ total_time_str = "#{stats[:total].round((stats[:total] > 0.002) ? 3 : 6)}s"
405
+ min_time_str = stats[:min] ? "#{stats[:min].round((stats[:min] > 0.002) ? 3 : 6)}s" : ""
406
+ max_time_str = stats[:max] ? "#{stats[:max].round((stats[:max] > 0.002) ? 3 : 6)}s" : ""
407
+ avg_time_str = stats[:avg] ? "#{stats[:avg].round((stats[:avg] > 0.002) ? 3 : 6)}s" : ""
374
408
 
375
- out = ""
376
- # <benchmark name or command>
377
- out << "#{benchmark_name.ljust(30, ' ')}"
378
- # exit: 0
379
- exit_str = "0"
380
- bad_benchmark = benchmark_records.find {|benchmark_record| benchmark_record.exit_code && benchmark_record.exit_code != 0 }
381
- if bad_benchmark
382
- bad_benchmark.exit_code.to_s
383
- out << "\texit: #{bad_benchmark.exit_code.to_s.ljust(2, ' ')}"
384
- out << "\terror: #{bad_benchmark.error.to_s.ljust(12, ' ')}"
385
- else
386
- out << "\texit: 0 "
387
- end
409
+ out = ""
410
+ # <benchmark name or command>
411
+ out << "#{benchmark_name.ljust(30, ' ')}"
412
+ # exit: 0
413
+ bad_benchmark = benchmark_records.find {|benchmark_record| benchmark_record.exit_code && benchmark_record.exit_code != 0 }
414
+ if bad_benchmark
415
+ exit_code = bad_benchmark.exit_code.to_i
416
+ out << "\texit: #{bad_benchmark.exit_code.to_s.ljust(2, ' ')}"
417
+ out << "\terror: #{bad_benchmark.error.to_s.ljust(12, ' ')}"
418
+ else
419
+ out << "\texit: 0 "
420
+ end
388
421
 
389
- out << "\tn: #{n.to_s.ljust(4, ' ')}"
390
- out << "\ttotal: #{total_time_str.ljust(9, ' ')}"
391
- out << "\tmin: #{min_time_str.ljust(9, ' ')}"
392
- out << "\tmax: #{max_time_str.ljust(9, ' ')}"
393
- out << "\tavg: #{avg_time_str.ljust(9, ' ')}"
422
+ out << "\tn: #{n.to_s.ljust(4, ' ')}"
423
+ out << "\ttotal: #{total_time_str.ljust(9, ' ')}"
424
+ out << "\tmin: #{min_time_str.ljust(9, ' ')}"
425
+ out << "\tmax: #{max_time_str.ljust(9, ' ')}"
426
+ out << "\tavg: #{avg_time_str.ljust(9, ' ')}"
394
427
 
395
428
 
396
- if bad_benchmark
397
- print red,out,reset,"\n"
398
- return 1
399
- else
429
+ # if bad_benchmark
430
+ # print_error red,out,reset,"\n"
431
+ # return 1
432
+ # else
433
+ # print cyan,out,reset,"\n"
434
+ # return 0
435
+ # end
436
+ end
437
+ if exit_code == 0
400
438
  print cyan,out,reset,"\n"
401
439
  return 0
440
+ else
441
+ print_error red,out,reset,"\n"
442
+ return exit_code
443
+ end
444
+ rescue => ex
445
+ raise ex
446
+ #raise_command_error "benchmark exec failed with error: #{ex}"
447
+ #puts_error "benchmark exec failed with error: #{ex}"
448
+ #return 1
449
+ ensure
450
+ if original_stdout
451
+ my_terminal.set_stdout(original_stdout)
452
+ original_stdout = nil
402
453
  end
403
-
404
454
  end
405
-
406
- return 0
407
455
  end
408
456
 
409
457
  end
@@ -57,11 +57,13 @@ class Morpheus::Cli::ColoringCommand
57
57
  if Term::ANSIColor::coloring?
58
58
  if coloring_was_enabled == false
59
59
  Morpheus::Logging::DarkPrinter.puts "coloring enabled" if Morpheus::Logging.debug?
60
+ recalculate_after_color_change()
60
61
  end
61
62
  puts "#{cyan}coloring: #{bold}#{green}on#{reset}"
62
63
  else
63
64
  if coloring_was_enabled == true
64
65
  Morpheus::Logging::DarkPrinter.puts "coloring disabled" if Morpheus::Logging.debug?
66
+ recalculate_after_color_change()
65
67
  end
66
68
  puts "coloring: off"
67
69
  end
@@ -69,4 +71,14 @@ class Morpheus::Cli::ColoringCommand
69
71
  return exit_code
70
72
  end
71
73
 
74
+ protected
75
+
76
+ def recalculate_after_color_change()
77
+ # recalculate shell prompt after this change
78
+ Morpheus::Cli::Echo.recalculate_variable_map()
79
+ if Morpheus::Cli::Shell.has_instance?
80
+ Morpheus::Cli::Shell.instance.recalculate_prompt()
81
+ end
82
+ end
83
+
72
84
  end
@@ -12,6 +12,9 @@ class Morpheus::Cli::CurlCommand
12
12
  split_args = args.join(" ").split(" -- ")
13
13
  args = split_args[0].split(" ")
14
14
  curl_args = split_args[1] ? split_args[1].split(" ") : []
15
+ curl_method = nil
16
+ curl_data = nil
17
+ show_progress = false
15
18
  # puts "args is : #{args}"
16
19
  # puts "curl_args is : #{curl_args}"
17
20
  options = {}
@@ -20,11 +23,20 @@ class Morpheus::Cli::CurlCommand
20
23
  opts.on( '-p', '--pretty', "Print result as parsed JSON." ) do
21
24
  options[:pretty] = true
22
25
  end
26
+ opts.on( '-X', '--request METHOD', "HTTP request method. Default is GET" ) do |val|
27
+ curl_method = val
28
+ end
29
+ opts.on( '--data DATA', String, "HTTP request body for use with POST and PUT, typically JSON." ) do |val|
30
+ curl_data = val
31
+ end
32
+ opts.on( '--progress', '--progress', "Display progress output by excluding the -s option." ) do
33
+ show_progress = true
34
+ end
23
35
  build_common_options(opts, options, [:dry_run, :remote])
24
36
  opts.add_hidden_option('--curl')
25
37
  #opts.add_hidden_option('--scrub')
26
38
  opts.footer = <<-EOT
27
- This invokes the `curl` command with url "appliance_url/api/$0
39
+ This invokes the `curl` command with url "appliance_url/$0
28
40
  and includes the authorization header -H "Authorization: Bearer access_token"
29
41
  Arguments for the curl command should be passed after ' -- '
30
42
  Example: morpheus curl "/api/servers/1" -- -XGET -sv
@@ -71,10 +83,21 @@ EOT
71
83
  api_path = api_path.sub(/^\//, "") # strip leading slash
72
84
  url = "#{@appliance_url.chomp('/')}/#{api_path}"
73
85
  end
74
- curl_cmd = "curl \"#{url}\""
86
+ curl_cmd = "curl"
87
+ if show_progress == false
88
+ curl_cmd << " -s"
89
+ end
90
+ if curl_method
91
+ curl_cmd << " -X#{curl_method}"
92
+ end
93
+ curl_cmd << " \"#{url}\""
75
94
  if @access_token
76
95
  curl_cmd << " -H \"Authorization: Bearer #{@access_token}\""
77
96
  end
97
+ if curl_data
98
+ #todo: curl_data.gsub("'","\\'")
99
+ curl_cmd << " --data '#{curl_data}'"
100
+ end
78
101
  if !curl_args.empty?
79
102
  curl_cmd << " " + curl_args.join(' ')
80
103
  end
@@ -89,16 +112,18 @@ EOT
89
112
  print reset
90
113
  return 0
91
114
  end
92
- print cyan
93
- print "#{cyan}#{curl_cmd_str}#{reset}"
94
- print "\n\n"
115
+ # print cyan
116
+ # print "#{cyan}#{curl_cmd_str}#{reset}"
117
+ # print "\n\n"
95
118
  print reset
96
119
  # print result
97
120
  curl_output = `#{curl_cmd}`
98
121
  if options[:pretty]
99
122
  output_lines = curl_output.split("\n")
100
123
  last_line = output_lines.pop
101
- puts output_lines.join("\n")
124
+ if output_lines.size > 0
125
+ puts output_lines.join("\n")
126
+ end
102
127
  begin
103
128
  json_data = JSON.parse(last_line)
104
129
  json_string = JSON.pretty_generate(json_data)
@@ -10,8 +10,8 @@ class Morpheus::Cli::Echo
10
10
  set_command_name :echo
11
11
  set_command_hidden
12
12
 
13
- unless defined?(DEFAULT_VARIABLE_MAP)
14
- DEFAULT_VARIABLE_MAP = {'%cyan' => Term::ANSIColor.cyan, '%magenta' => Term::ANSIColor.magenta, '%red' => Term::ANSIColor.red, '%green' => Term::ANSIColor.green, '%yellow' => Term::ANSIColor.yellow, '%white' => Term::ANSIColor.white, '%dark' => Term::ANSIColor.dark, '%reset' => Term::ANSIColor.reset}
13
+ unless defined?(COLOR_VARIABLE_MAP)
14
+ COLOR_VARIABLE_MAP = {'%cyan' => Term::ANSIColor.cyan, '%magenta' => Term::ANSIColor.magenta, '%red' => Term::ANSIColor.red, '%green' => Term::ANSIColor.green, '%yellow' => Term::ANSIColor.yellow, '%white' => Term::ANSIColor.white, '%dark' => Term::ANSIColor.dark, '%reset' => Term::ANSIColor.reset}
15
15
  end
16
16
 
17
17
  def self.variable_map
@@ -20,7 +20,12 @@ class Morpheus::Cli::Echo
20
20
 
21
21
  def self.recalculate_variable_map()
22
22
  var_map = {}
23
- var_map.merge!(DEFAULT_VARIABLE_MAP)
23
+ if Term::ANSIColor.coloring?
24
+ var_map.merge!(COLOR_VARIABLE_MAP)
25
+ else
26
+ COLOR_VARIABLE_MAP.each {|k,v| var_map[k] = "" }
27
+ end
28
+
24
29
  appliance = ::Morpheus::Cli::Remote.load_active_remote()
25
30
  if appliance
26
31
  var_map.merge!({'%remote' => appliance[:name], '%remote_url' => (appliance[:host].to_s || appliance[:url].to_s), '%username' => appliance[:username].to_s})
@@ -27,7 +27,7 @@ Examples:
27
27
  set-prompt "morpheus $ "
28
28
  set-prompt "%cyanmorpheus> "
29
29
  set-prompt "[%magenta%remote%reset] %cyan%username> "
30
- set-prompt "%green%username%reset@%remote morph %magenta> %reset"
30
+ set-prompt "%green%username%reset@%remote %magenta> %reset"
31
31
  set-prompt "%cyan%username%reset@%magenta%remote %cyanmorpheus> %reset"
32
32
 
33
33
  The available variables are:
@@ -4,11 +4,13 @@ require 'optparse'
4
4
  require 'filesize'
5
5
  require 'morpheus/cli/cli_command'
6
6
  require 'morpheus/cli/mixins/provisioning_helper'
7
+ require 'morpheus/cli/mixins/logs_helper'
7
8
  require 'morpheus/cli/option_types'
8
9
 
9
10
  class Morpheus::Cli::ContainersCommand
10
11
  include Morpheus::Cli::CliCommand
11
12
  include Morpheus::Cli::ProvisioningHelper
13
+ include Morpheus::Cli::LogsHelper
12
14
 
13
15
  set_command_name :containers
14
16
 
@@ -507,8 +509,24 @@ class Morpheus::Cli::ContainersCommand
507
509
 
508
510
  def logs(args)
509
511
  options = {}
512
+ params = {}
510
513
  optparse = Morpheus::Cli::OptionParser.new do |opts|
511
514
  opts.banner = subcommand_usage("[id]")
515
+ opts.on('--start TIMESTAMP','--start TIMESTAMP', "Start timestamp. Default is 30 days ago.") do |val|
516
+ options[:start] = parse_time(val) #.utc.iso8601
517
+ end
518
+ opts.on('--end TIMESTAMP','--end TIMESTAMP', "End timestamp. Default is now.") do |val|
519
+ options[:end] = parse_time(val) #.utc.iso8601
520
+ end
521
+ opts.on('--level VALUE', String, "Log Level. DEBUG,INFO,WARN,ERROR") do |val|
522
+ params['level'] = params['level'] ? [params['level'], val].flatten : val
523
+ end
524
+ opts.on('--table', '--table', "Format ouput as a table.") do
525
+ options[:table] = true
526
+ end
527
+ opts.on('-a', '--all', "Display all details: entire message." ) do
528
+ options[:details] = true
529
+ end
512
530
  build_common_options(opts, options, [:list, :query, :json, :yaml, :csv, :fields, :dry_run, :remote])
513
531
  opts.footer = "List logs for a container.\n" +
514
532
  "[id] is required. This is the id of a container."
@@ -523,47 +541,42 @@ class Morpheus::Cli::ContainersCommand
523
541
  id_list = parse_id_list(args)
524
542
  begin
525
543
  containers = id_list # heh
526
- params = {}
527
544
  params.merge!(parse_list_options(options))
528
- params[:query] = params.delete(:phrase) unless params[:phrase].nil?
529
- params['order'] = params['direction'] unless params['direction'].nil? # old api version expects order instead of direction
545
+ params['query'] = params.delete('phrase') if params['phrase']
546
+ params[:order] = params[:direction] unless params[:direction].nil? # old api version expects order instead of direction
547
+ params['startMs'] = (options[:start].to_i * 1000) if options[:start]
548
+ params['endMs'] = (options[:end].to_i * 1000) if options[:end]
530
549
  @logs_interface.setopts(options)
531
550
  if options[:dry_run]
532
551
  print_dry_run @logs_interface.dry.container_logs(containers, params)
533
552
  return
534
553
  end
535
554
  json_response = @logs_interface.container_logs(containers, params)
536
- render_result = render_with_format(json_response, options, 'data')
555
+ render_result = json_response['logs'] ? render_with_format(json_response, options, 'logs') : render_with_format(json_response, options, 'data')
537
556
  return 0 if render_result
538
557
 
539
558
  logs = json_response
540
559
  title = "Container Logs: #{containers.join(', ')}"
541
560
  subtitles = parse_list_subtitles(options)
561
+ if options[:start]
562
+ subtitles << "Start: #{options[:start]}".strip
563
+ end
564
+ if options[:end]
565
+ subtitles << "End: #{options[:end]}".strip
566
+ end
542
567
  if params[:query]
543
568
  subtitles << "Search: #{params[:query]}".strip
544
569
  end
545
- # todo: startMs, endMs, sorts insteaad of sort..etc
570
+ if params['level']
571
+ subtitles << "Level: #{params['level']}"
572
+ end
573
+ logs = json_response['data'] || json_response['logs']
546
574
  print_h1 title, subtitles, options
547
- if logs['data'].empty?
548
- puts "#{cyan}No logs found.#{reset}"
575
+ if logs.empty?
576
+ print "#{cyan}No logs found.#{reset}\n"
549
577
  else
550
- logs['data'].each do |log_entry|
551
- log_level = ''
552
- case log_entry['level']
553
- when 'INFO'
554
- log_level = "#{blue}#{bold}INFO#{reset}"
555
- when 'DEBUG'
556
- log_level = "#{white}#{bold}DEBUG#{reset}"
557
- when 'WARN'
558
- log_level = "#{yellow}#{bold}WARN#{reset}"
559
- when 'ERROR'
560
- log_level = "#{red}#{bold}ERROR#{reset}"
561
- when 'FATAL'
562
- log_level = "#{red}#{bold}FATAL#{reset}"
563
- end
564
- puts "[#{log_entry['ts']}] #{log_level} - #{log_entry['message'].to_s.strip}"
565
- end
566
- print_results_pagination({'meta'=>{'total'=>json_response['total'],'size'=>json_response['data'].size,'max'=>(json_response['max'] || options[:max]),'offset'=>(json_response['offset'] || options[:offset] || 0)}})
578
+ print format_log_records(logs, options)
579
+ print_results_pagination({'meta'=>{'total'=>(json_response['total']['value'] rescue json_response['total']),'size'=>logs.size,'max'=>(json_response['max'] || options[:max]),'offset'=>(json_response['offset'] || options[:offset] || 0)}})
567
580
  end
568
581
  print reset,"\n"
569
582
  return 0
@@ -66,6 +66,7 @@ class Morpheus::Cli::CypherCommand
66
66
  puts records_as_csv([json_response], options)
67
67
  return 0
68
68
  end
69
+ cypher_items = json_response["cypherItems"] || json_response["cyphers"]
69
70
  cypher_data = json_response["data"]
70
71
  title = "Morpheus Cypher Key List"
71
72
  subtitles = []
@@ -93,13 +94,15 @@ class Morpheus::Cli::CypherCommand
93
94
  "EXPIRATION" => lambda {|it|
94
95
  format_expiration_date(it["expireDate"])
95
96
  },
96
- "DATE CREATED" => lambda {|it| format_local_dt(it["dateCreated"]) },
97
- "LAST ACCESS" => lambda {|it| format_local_dt(it["lastAccessed"]) }
97
+ # "DATE CREATED" => lambda {|it| format_local_dt(it["dateCreated"]) },
98
+ "LAST UPDATED" => lambda {|it| format_local_dt(it["lastUpdated"]) },
99
+ "LAST ACCESSED" => lambda {|it| format_local_dt(it["lastAccessed"]) }
98
100
  }
99
101
  print cyan
100
- print as_pretty_table(json_response["cypherItems"], cypher_columns, options)
102
+ print as_pretty_table(cypher_items, cypher_columns, options)
101
103
  print reset
102
- print_results_pagination({size:cypher_keys.size,total:cypher_keys.size.to_i})
104
+ # print_results_pagination({size:cypher_keys.size,total:cypher_keys.size.to_i})
105
+ print_results_pagination(json_response)
103
106
  end
104
107
  print reset,"\n"
105
108
  return 0
@@ -114,7 +117,7 @@ class Morpheus::Cli::CypherCommand
114
117
  params = {}
115
118
  value_only = false
116
119
  do_decrypt = false
117
- item_ttl = nil
120
+ ttl = nil
118
121
  optparse = Morpheus::Cli::OptionParser.new do |opts|
119
122
  opts.banner = subcommand_usage("[key]")
120
123
  # opts.on(nil, '--decrypt', 'Display the decrypted value') do
@@ -127,14 +130,14 @@ class Morpheus::Cli::CypherCommand
127
130
  value_only = true
128
131
  end
129
132
  opts.on( '-t', '--ttl SECONDS', "Time to live, the lease duration before this key expires. Use if creating new key." ) do |val|
130
- item_ttl = val
133
+ ttl = val
131
134
  if val.to_s.empty? || val.to_s == '0'
132
- item_ttl = 0
135
+ ttl = 0
133
136
  else
134
- item_ttl = val
137
+ ttl = val
135
138
  end
136
139
  end
137
- build_common_options(opts, options, [:json, :yaml, :csv, :fields, :dry_run, :quiet, :remote])
140
+ build_common_options(opts, options, [:query, :json, :yaml, :csv, :fields, :outfile, :dry_run, :quiet, :remote])
138
141
  opts.footer = "Read a cypher item and display the decrypted value." + "\n" +
139
142
  "[key] is required. This is the cypher key to read." + "\n" +
140
143
  "Use --ttl to specify a ttl if expecting cypher engine to automatically create the key."
@@ -148,33 +151,24 @@ class Morpheus::Cli::CypherCommand
148
151
  connect(options)
149
152
  begin
150
153
  item_key = args[0]
151
- if item_ttl
152
- params["ttl"] = item_ttl
154
+ if ttl
155
+ params["ttl"] = ttl
153
156
  end
154
157
  @cypher_interface.setopts(options)
155
158
  if options[:dry_run]
156
159
  print_dry_run @cypher_interface.dry.get(item_key, params)
157
160
  return 0
158
161
  end
162
+ params.merge!(parse_list_options(options))
159
163
  json_response = @cypher_interface.get(item_key, params)
160
-
161
- if options[:quiet]
164
+ render_result = render_with_format(json_response, options)
165
+ if render_result
162
166
  return 0
163
167
  end
164
-
165
- if options[:json]
166
- puts as_json(json_response, options)
167
- return 0
168
- elsif options[:yaml]
169
- puts as_yaml(json_response, options)
170
- return 0
171
- elsif options[:csv]
172
- puts records_as_csv([json_response], options)
173
- return 0
174
- end
175
-
168
+
176
169
  cypher_item = json_response['cypher']
177
170
  decrypted_value = json_response["data"]
171
+ data_type = decrypted_value.is_a?(String) ? 'string' : 'object'
178
172
 
179
173
  if value_only
180
174
  print cyan
@@ -201,19 +195,24 @@ class Morpheus::Cli::CypherCommand
201
195
  "TTL" => lambda {|it|
202
196
  format_expiration_ttl(it["expireDate"])
203
197
  },
198
+ # "Type" => lambda {|it|
199
+ # data_type
200
+ # },
204
201
  "Expiration" => lambda {|it|
205
202
  format_expiration_date(it["expireDate"])
206
203
  },
207
- "Date Created" => lambda {|it| format_local_dt(it["dateCreated"]) },
208
- "Last Access" => lambda {|it| format_local_dt(it["lastAccessed"]) }
204
+ # "Date Created" => lambda {|it| format_local_dt(it["dateCreated"]) },
205
+ "Last Updated" => lambda {|it| format_local_dt(it["lastUpdated"]) },
206
+ "Last Accessed" => lambda {|it| format_local_dt(it["lastAccessed"]) }
209
207
  }
210
208
  if cypher_item["expireDate"].nil?
211
209
  description_cols.delete("Expires")
212
210
  end
213
211
  print_description_list(description_cols, cypher_item)
214
212
 
215
- print_h2 "Value", options
216
- # print_h2 "Decrypted Value"
213
+ # print_h2 "Value", options
214
+ # print_h2 "Data", options
215
+ print_h2 "Data (#{data_type})", options
217
216
 
218
217
  if decrypted_value
219
218
  print cyan
@@ -247,36 +246,44 @@ class Morpheus::Cli::CypherCommand
247
246
  end
248
247
 
249
248
  def put(args)
249
+ usage = <<-EOT
250
+ Usage: morpheus #{command_name} put [key] [value] [options] to store a string.
251
+ morpheus #{command_name} put [key] [k=v] [k=v] [options] to store an object.
252
+ EOT
250
253
  options = {}
251
254
  params = {}
252
255
  item_key = nil
253
256
  item_value = nil
254
- item_ttl = nil
257
+ ttl = nil
255
258
  no_overwrite = nil
256
259
  optparse = Morpheus::Cli::OptionParser.new do |opts|
257
- opts.banner = subcommand_usage("[key] [value]")
258
- # opts.on( '--key VALUE', String, "Key" ) do |val|
259
- # item_key = val
260
- # end
261
- opts.on( '-v', '--value VALUE', "Secret value" ) do |val|
260
+ # opts.banner = subcommand_usage("[key] [value]\n\t[key] [k=v] [k=v] [k=v]")
261
+ opts.banner = usage
262
+ opts.on( '--key KEY', String, "Key. This can also be passed as the first argument." ) do |val|
263
+ item_key = val
264
+ end
265
+ opts.on( '-v', '--value VALUE', "Secret value. This can be used to store a string instead of an object, and can also be passed as the second argument." ) do |val|
262
266
  item_value = val
263
267
  end
264
268
  opts.on( '-t', '--ttl SECONDS', "Time to live, the lease duration before this key expires." ) do |val|
265
- item_ttl = val
269
+ ttl = val
266
270
  if val.to_s.empty? || val.to_s == '0'
267
- item_ttl = 0
271
+ ttl = 0
268
272
  else
269
- item_ttl = val
273
+ ttl = val
270
274
  end
271
275
  end
272
276
  # opts.on( '--no-overwrite', '--no-overwrite', "Do not overwrite existing keys. Existing keys are overwritten by default." ) do
273
277
  # params['overwrite'] = false
274
278
  # end
275
- build_common_options(opts, options, [:auto_confirm, :options, :payload, :json, :dry_run, :quiet, :remote])
279
+ build_common_options(opts, options, [:auto_confirm, :options, :payload, :json, :yaml, :csv, :fields, :outfile, :dry_run, :quiet, :remote])
276
280
  opts.footer = "Create or update a cypher key." + "\n" +
277
- "[key] is required. This is the key of the cypher being created or updated." + "\n" +
278
- "[value] is required. This is the new value or value pairs being stored. Supports format foo=bar, 1-N arguments." + "\n" +
279
- "The --payload option can be used instead of passing [value] argument."
281
+ "[key] is required. This is the key of the cypher being created or updated. The key includes the mount prefix eg. secret/hello" + "\n" +
282
+ "[value] is required for some cypher engines, such as secret. This is the secret value or k=v pairs being stored. Supports 1-N arguments." + "\n" +
283
+ "If a single [value] is passed, it is stored as type string." + "\n" +
284
+ "If more than one [value] is passed, the format is expected to be k=v and the value will be stored as an object." + "\n" +
285
+ "The --value option can be used to store a string value." + "\n" +
286
+ "The --payload option can be used to store an object."
280
287
  end
281
288
  optparse.parse!(args)
282
289
  # if args.count < 1
@@ -285,126 +292,160 @@ class Morpheus::Cli::CypherCommand
285
292
  # return 1
286
293
  # end
287
294
  connect(options)
288
- begin
289
- if args[0]
290
- item_key = args[0]
295
+
296
+ # parse arguments like [value] or [k=v]
297
+ item_key = args[0]
298
+ item_value = args[1]
299
+ if args.count == 0
300
+ # prompt for key and value
301
+ elsif args.count == 1
302
+ # prompt for value
303
+ elsif args.count == 2
304
+ # expecting [value] or [k=v]
305
+ item_value_object = {}
306
+ item_value_pair = item_value.split("=")
307
+ if item_value_pair.size == 2
308
+ item_value_object[item_value_pair[0].to_s] = item_value_pair[1]
309
+ item_value = item_value_object
310
+ else
311
+ # item_value = item_value
312
+ end
313
+ elsif args.count > 2
314
+ # expecting [k=v] [k=v]
315
+ item_value_object = {}
316
+ args[1..(args.size-1)].each do |arg|
317
+ item_value_pair = arg.split("=")
318
+ item_value_object[item_value_pair[0].to_s] = item_value_pair[1]
291
319
  end
292
- options[:options] ||= {}
293
- options[:options]['key'] = item_key if item_key
294
- # Key prompt
320
+ item_value = item_value_object
321
+ end
322
+
323
+ # this is redunant and silly, refactor soon
324
+
325
+ # Key prompt
326
+ if item_key
327
+ options[:options]['key'] = item_key
328
+ end
329
+ if item_key.nil?
295
330
  v_prompt = Morpheus::Cli::OptionTypes.prompt([{'fieldName' => 'key', 'fieldLabel' => 'Key', 'type' => 'text', 'required' => true, 'description' => cypher_key_help}], options[:options])
296
331
  item_key = v_prompt['key']
332
+ end
297
333
 
298
- payload = nil
299
- if options[:payload]
300
- payload = options[:payload]
301
- payload.deep_merge!(options[:options].reject {|k,v| k.is_a?(Symbol) || ['key','value'].include?(k)}) if options[:options] && options[:options].keys.size > 0
302
- else
303
- # merge -O options into normally parsed options
304
- params.deep_merge!(options[:options].reject {|k,v| k.is_a?(Symbol) || ['key','value'].include?(k)}) if options[:options] && options[:options].keys.size > 0
305
-
306
- # Value prompt
307
- value_is_required = false
308
- cypher_mount_type = item_key.split("/").first
309
- if ["secret"].include?(cypher_mount_type)
310
- value_is_required = true
311
- end
334
+ payload = nil
335
+ if options[:payload]
336
+ payload = options[:payload]
337
+ payload.deep_merge!(options[:options].reject {|k,v| k.is_a?(Symbol) || ['key','value'].include?(k)}) if options[:options] && options[:options].keys.size > 0
338
+ else
339
+ # merge -O options into normally parsed options
340
+ params.deep_merge!(options[:options].reject {|k,v| k.is_a?(Symbol) || ['key','value'].include?(k)}) if options[:options] && options[:options].keys.size > 0
341
+
342
+ # Value prompt
343
+ value_is_required = false
344
+ cypher_mount_type = item_key.split("/").first
345
+ if ["secret","tfvars"].include?(cypher_mount_type)
346
+ value_is_required = true
347
+ end
312
348
 
313
- # todo: read value from STDIN shall we?
314
-
315
- # cool, we got value as arguments like foo=bar
316
- if args.count > 1
317
- # parse one and only arg as the value like password/mine mypassword123
318
- if args.count == 2 && args[1].split("=").size() == 1
319
- item_value = args[1]
320
- elsif args.count > 1
321
- # parse args as key value pairs like secret/config foo=bar thing=myvalue
322
- value_arguments = args[1..-1]
323
- value_arguments_map = {}
324
- value_arguments.each do |value_argument|
325
- value_pair = value_argument.split("=")
326
- value_arguments_map[value_pair[0]] = value_pair[1] ? value_pair[1..-1].join("=") : nil
327
- end
328
- item_value = value_arguments_map
329
- end
330
- else
331
- # Prompt for a single text value to be sent as {"value":"my secret"}
332
- if value_is_required
333
- options[:options]['value'] = item_value if item_value
334
- v_prompt = Morpheus::Cli::OptionTypes.prompt([{'fieldName' => 'value', 'fieldLabel' => 'Value', 'type' => 'text', 'required' => value_is_required, 'description' => "Secret value for this cypher"}], options[:options])
335
- item_value = v_prompt['value']
349
+ # todo: read value from STDIN shall we?
350
+
351
+ # cool, we got value as arguments like foo=bar
352
+ if args.count > 1
353
+ # parse one and only arg as the value like password/mine mypassword123
354
+ if args.count == 2 && args[1].split("=").size() == 1
355
+ item_value = args[1]
356
+ elsif args.count > 1
357
+ # parse args as key value pairs like secret/config foo=bar thing=myvalue
358
+ value_arguments = args[1..-1]
359
+ value_arguments_map = {}
360
+ value_arguments.each do |value_argument|
361
+ value_pair = value_argument.split("=")
362
+ value_arguments_map[value_pair[0]] = value_pair[1] ? value_pair[1..-1].join("=") : nil
336
363
  end
364
+ item_value = value_arguments_map
337
365
  end
338
-
339
- # construct payload
340
- # payload = {
341
- # 'cypher' => params
342
- # }
343
-
344
- # if value is valid json, then the payload IS the value
345
- if item_value.is_a?(String) && item_value.to_s[0] == '{' && item_value.to_s[-1] == '}'
346
- begin
347
- json_object = JSON.parse(item_value)
348
- item_value = json_object
349
- rescue => ex
350
- Morpheus::Logging::DarkPrinter.puts "Failed to parse cypher value '#{item_value}' as JSON. Error: #{ex}" if Morpheus::Logging.debug?
351
- raise_command_error "Failed to parse cypher value as JSON: #{item_value}"
352
- # return 1
353
- end
354
- else
355
- # it is just a string
356
- if item_value.is_a?(String)
357
- payload = {"value" => item_value}
358
- elsif item_value.nil?
359
- payload = {}
360
- else item_value
361
- # great, a Hash I hope
362
- payload = item_value
363
- end
366
+ else
367
+ # Prompt for a single text value to be sent as {"value":"my secret"}
368
+ if value_is_required
369
+ options[:options]['value'] = item_value if item_value
370
+ v_prompt = Morpheus::Cli::OptionTypes.prompt([{'fieldName' => 'value', 'fieldLabel' => 'Value', 'type' => 'text', 'required' => value_is_required, 'description' => "Secret value for this cypher"}], options[:options])
371
+ item_value = v_prompt['value']
364
372
  end
365
373
  end
366
374
 
367
- # prompt for Lease
368
- options[:options]['ttl'] = item_ttl if item_ttl
369
- v_prompt = Morpheus::Cli::OptionTypes.prompt([{'fieldName' => 'ttl', 'fieldLabel' => 'Lease (TTL in seconds)', 'type' => 'text', 'required' => false, 'description' => cypher_ttl_help}], options[:options])
370
- item_ttl = v_prompt['ttl']
371
-
375
+ # make sure not to pass a value for these or it will not save them.
376
+ # if ['uuid','key','password'].include?(cypher_mount_type)
377
+ # item_value = nil
378
+ # end
372
379
 
373
- if item_ttl
374
- # I would like this better as params...
375
- payload["ttl"] = item_ttl
376
- end
377
- @cypher_interface.setopts(options)
378
- if options[:dry_run]
379
- print_dry_run @cypher_interface.dry.create(item_key, payload)
380
- return
381
- end
382
- existing_cypher = nil
383
- json_response = @cypher_interface.list(item_key)
384
- if json_response["data"] && json_response["data"]["keys"]
385
- existing_cypher = json_response["data"]["keys"].find {|k| k == item_key }
386
- end
387
- if existing_cypher
388
- unless options[:yes] || Morpheus::Cli::OptionTypes.confirm("Are you sure you want to overwrite the cypher key #{item_key}?")
389
- return 9, "aborted command"
380
+ # construct payload
381
+ # payload = {
382
+ # 'cypher' => params
383
+ # }
384
+ payload = {}
385
+ # if value is valid json, then the payload IS the value
386
+ if item_value.is_a?(String) && item_value.to_s[0] == '{' && item_value.to_s[-1] == '}'
387
+ begin
388
+ json_object = JSON.parse(item_value)
389
+ item_value = json_object
390
+ rescue => ex
391
+ Morpheus::Logging::DarkPrinter.puts "Failed to parse cypher value '#{item_value}' as JSON. Error: #{ex}" if Morpheus::Logging.debug?
392
+ raise_command_error "Failed to parse cypher value as JSON: #{item_value}"
393
+ # return 1
394
+ end
395
+ else
396
+ # it is just a string
397
+ if item_value.is_a?(String)
398
+ params['type'] = 'string'
399
+ #params["value"] = item_value
400
+ payload["value"] = item_value
401
+ elsif item_value.nil?
402
+ payload = {}
403
+ else item_value
404
+ # great, a Hash I hope
405
+ payload = item_value
390
406
  end
391
407
  end
392
- json_response = @cypher_interface.create(item_key, payload)
393
- if options[:json]
394
- puts as_json(json_response, options)
395
- elsif !options[:quiet]
396
- print_green_success "Wrote cypher #{item_key}"
397
- # should print without doing get, because that can use a token.
398
- cypher_item = json_response['cypher']
399
- get([item_key])
408
+ end
409
+
410
+ # prompt for Lease
411
+ options[:options]['ttl'] = ttl if ttl
412
+ v_prompt = Morpheus::Cli::OptionTypes.prompt([{'fieldName' => 'ttl', 'fieldLabel' => 'Lease (TTL in seconds)', 'type' => 'text', 'required' => false, 'description' => cypher_ttl_help, 'defaultValue' => '0'}], options[:options])
413
+ ttl = v_prompt['ttl']
414
+
415
+ if ttl
416
+ params['ttl'] = ttl
417
+ #payload["ttl"] = ttl
418
+ end
419
+ @cypher_interface.setopts(options)
420
+ if options[:dry_run]
421
+ print_dry_run @cypher_interface.dry.create(item_key, params, payload)
422
+ return
423
+ end
424
+ existing_cypher = nil
425
+ json_response = @cypher_interface.list(item_key)
426
+ if json_response["data"] && json_response["data"]["keys"]
427
+ existing_cypher = json_response["data"]["keys"].find {|k| k == item_key }
428
+ end
429
+ if existing_cypher
430
+ unless options[:yes] || Morpheus::Cli::OptionTypes.confirm("Are you sure you want to overwrite the cypher key #{item_key}?")
431
+ return 9, "aborted command"
400
432
  end
433
+ end
434
+ json_response = @cypher_interface.create(item_key, params, payload)
435
+ render_result = render_with_format(json_response, options)
436
+ if render_result
401
437
  return 0
402
- rescue RestClient::Exception => e
403
- print_rest_exception(e, options)
404
- exit 1
405
438
  end
439
+ #print_green_success "Cypher #{item_key} updated"
440
+ # print_green_success "Wrote cypher #{item_key}"
441
+ print_green_success "Success! Data written to: #{item_key}"
442
+ # should print without doing get, because that can use a token.
443
+ cypher_item = json_response['cypher']
444
+ get_args = [item_key] + (options[:remote] ? ["-r",options[:remote]] : [])
445
+ get(get_args)
446
+ return 0
406
447
  end
407
-
448
+
408
449
  def remove(args)
409
450
  options = {}
410
451
  params = {}
@@ -412,7 +453,7 @@ class Morpheus::Cli::CypherCommand
412
453
  opts.banner = subcommand_usage("[key]")
413
454
  build_common_options(opts, options, [:auto_confirm, :json, :dry_run, :quiet, :remote])
414
455
  opts.footer = "Delete a cypher." + "\n" +
415
- "[key] is required. This is the key of a cypher."
456
+ "[key] is required. This is the cypher key to be deleted."
416
457
  end
417
458
  optparse.parse!(args)
418
459
 
@@ -478,16 +519,16 @@ key - Generates a Base 64 encoded AES Key of specified bit length in the key pat
478
519
 
479
520
  def cypher_ttl_help
480
521
  """
481
- Lease time in seconds
522
+ TTL in seconds
482
523
  Quick Second Time Reference:
483
524
  Hour: 3600
484
525
  Day: 86400
485
526
  Week: 604800
486
527
  Month (30 days): 2592000
487
528
  Year: 31536000
488
- This can also be passed in abbreviated format with the unit as the suffix. eg. 32d, 90s, 5y
489
- This can be passed as 0 to disable expiration and never expire.
490
- The default is 32 days (2764800).
529
+ Unlimited: 0
530
+ This can be passed in abbreviated duration format. eg. 32d, 90s, 5y
531
+ The default is 0, meaning Unlimited.
491
532
  """
492
533
  end
493
534