morpheus-cli 4.1.8 → 4.1.9

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.
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