morpheus-cli 5.0.0 → 5.2.2

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 (61) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -0
  3. data/Dockerfile +1 -1
  4. data/lib/morpheus/api/api_client.rb +16 -0
  5. data/lib/morpheus/api/billing_interface.rb +1 -0
  6. data/lib/morpheus/api/deploy_interface.rb +1 -1
  7. data/lib/morpheus/api/deployments_interface.rb +20 -1
  8. data/lib/morpheus/api/forgot_password_interface.rb +17 -0
  9. data/lib/morpheus/api/instances_interface.rb +16 -2
  10. data/lib/morpheus/api/invoices_interface.rb +12 -3
  11. data/lib/morpheus/api/search_interface.rb +13 -0
  12. data/lib/morpheus/api/servers_interface.rb +14 -0
  13. data/lib/morpheus/api/service_catalog_interface.rb +89 -0
  14. data/lib/morpheus/api/usage_interface.rb +18 -0
  15. data/lib/morpheus/cli.rb +6 -2
  16. data/lib/morpheus/cli/apps.rb +3 -23
  17. data/lib/morpheus/cli/budgets_command.rb +389 -319
  18. data/lib/morpheus/cli/{catalog_command.rb → catalog_item_types_command.rb} +182 -67
  19. data/lib/morpheus/cli/cli_command.rb +51 -10
  20. data/lib/morpheus/cli/commands/standard/curl_command.rb +26 -13
  21. data/lib/morpheus/cli/commands/standard/history_command.rb +9 -3
  22. data/lib/morpheus/cli/commands/standard/man_command.rb +74 -40
  23. data/lib/morpheus/cli/containers_command.rb +0 -24
  24. data/lib/morpheus/cli/cypher_command.rb +6 -2
  25. data/lib/morpheus/cli/dashboard_command.rb +260 -20
  26. data/lib/morpheus/cli/deploy.rb +199 -90
  27. data/lib/morpheus/cli/deployments.rb +341 -28
  28. data/lib/morpheus/cli/deploys.rb +206 -41
  29. data/lib/morpheus/cli/error_handler.rb +7 -0
  30. data/lib/morpheus/cli/forgot_password.rb +133 -0
  31. data/lib/morpheus/cli/groups.rb +1 -1
  32. data/lib/morpheus/cli/health_command.rb +59 -2
  33. data/lib/morpheus/cli/hosts.rb +271 -39
  34. data/lib/morpheus/cli/instances.rb +228 -129
  35. data/lib/morpheus/cli/invoices_command.rb +100 -20
  36. data/lib/morpheus/cli/jobs_command.rb +94 -92
  37. data/lib/morpheus/cli/library_option_lists_command.rb +1 -1
  38. data/lib/morpheus/cli/library_option_types_command.rb +10 -5
  39. data/lib/morpheus/cli/logs_command.rb +9 -6
  40. data/lib/morpheus/cli/mixins/accounts_helper.rb +5 -1
  41. data/lib/morpheus/cli/mixins/deployments_helper.rb +31 -2
  42. data/lib/morpheus/cli/mixins/print_helper.rb +13 -27
  43. data/lib/morpheus/cli/mixins/provisioning_helper.rb +108 -5
  44. data/lib/morpheus/cli/option_types.rb +271 -22
  45. data/lib/morpheus/cli/remote.rb +35 -10
  46. data/lib/morpheus/cli/reports_command.rb +99 -30
  47. data/lib/morpheus/cli/roles.rb +193 -155
  48. data/lib/morpheus/cli/search_command.rb +182 -0
  49. data/lib/morpheus/cli/service_catalog_command.rb +1474 -0
  50. data/lib/morpheus/cli/setup.rb +1 -1
  51. data/lib/morpheus/cli/shell.rb +33 -11
  52. data/lib/morpheus/cli/tasks.rb +29 -32
  53. data/lib/morpheus/cli/usage_command.rb +64 -11
  54. data/lib/morpheus/cli/version.rb +1 -1
  55. data/lib/morpheus/cli/virtual_images.rb +429 -254
  56. data/lib/morpheus/cli/whoami.rb +6 -6
  57. data/lib/morpheus/cli/workflows.rb +33 -40
  58. data/lib/morpheus/formatters.rb +75 -18
  59. data/lib/morpheus/terminal.rb +6 -2
  60. metadata +10 -4
  61. data/lib/morpheus/cli/mixins/catalog_helper.rb +0 -66
@@ -270,7 +270,7 @@ class Morpheus::Cli::LibraryOptionListsCommand
270
270
  else
271
271
  payload = {}
272
272
  payload.deep_merge!({'optionTypeList' => parse_passed_options(options)})
273
- list_payload = Morpheus::Cli::OptionTypes.no_prompt(update_option_type_option_types(), options[:options], @api_client)
273
+ list_payload = Morpheus::Cli::OptionTypes.no_prompt(update_option_type_list_option_types(), options[:options], @api_client)
274
274
  if list_payload['type'] == 'rest'
275
275
  # parse Source Headers
276
276
  if !(payload['optionTypeList']['config'] && payload['optionTypeList']['config']['sourceHeaders'])
@@ -129,7 +129,7 @@ class Morpheus::Cli::LibraryOptionTypesCommand
129
129
 
130
130
  print_h1 "Option Type Details"
131
131
  print cyan
132
- print_description_list({
132
+ columns = {
133
133
  "ID" => 'id',
134
134
  "Name" => 'name',
135
135
  "Description" => 'description',
@@ -138,11 +138,15 @@ class Morpheus::Cli::LibraryOptionTypesCommand
138
138
  # "Field Name" => 'fieldName',
139
139
  "Full Field Name" => lambda {|it| [it['fieldContext'], it['fieldName']].select {|it| !it.to_s.empty? }.join('.') },
140
140
  "Type" => lambda {|it| it['type'].to_s.capitalize },
141
+ "Option List" => lambda {|it| it['optionList'] ? it['optionList']['name'] : nil },
141
142
  "Placeholder" => 'placeHolder',
143
+ "Help Block" => 'helpBlock',
142
144
  "Default Value" => 'defaultValue',
143
145
  "Required" => lambda {|it| format_boolean(it['required']) },
144
146
  "Export As Tag" => lambda {|it| it['exportMeta'].nil? ? '' : format_boolean(it['exportMeta']) },
145
- }, option_type)
147
+ }
148
+ columns.delete("Option List") if option_type['optionList'].nil?
149
+ print as_description_list(option_type, columns, options)
146
150
  print reset,"\n"
147
151
  return 0
148
152
  rescue RestClient::Exception => e
@@ -294,9 +298,10 @@ class Morpheus::Cli::LibraryOptionTypesCommand
294
298
  {'fieldName' => 'optionList', 'fieldLabel' => 'Option List', 'type' => 'select', 'optionSource' => 'optionTypeLists', 'required' => true, 'dependsOnCode' => 'optionType.type:select', 'description' => "The Option List to be the source of options when type is 'select'.", 'displayOrder' => 5},
295
299
  {'fieldName' => 'fieldLabel', 'fieldLabel' => 'Field Label', 'type' => 'text', 'required' => true, 'description' => 'This is the input label that shows typically to the left of a custom option.', 'displayOrder' => 6},
296
300
  {'fieldName' => 'placeHolder', 'fieldLabel' => 'Placeholder', 'type' => 'text', 'displayOrder' => 7},
297
- {'fieldName' => 'defaultValue', 'fieldLabel' => 'Default Value', 'type' => 'text', 'displayOrder' => 8},
298
- {'fieldName' => 'required', 'fieldLabel' => 'Required', 'type' => 'checkbox', 'defaultValue' => false, 'displayOrder' => 9},
299
- {'fieldName' => 'exportMeta', 'fieldLabel' => 'Export As Tag', 'type' => 'checkbox', 'defaultValue' => false, 'description' => 'Export as Tag.', 'displayOrder' => 10},
301
+ {'fieldName' => 'helpBlock', 'fieldLabel' => 'Help Block', 'type' => 'text', 'description' => 'This is the explaination of the input that shows typically underneath the option.', 'displayOrder' => 8},
302
+ {'fieldName' => 'defaultValue', 'fieldLabel' => 'Default Value', 'type' => 'text', 'displayOrder' => 9},
303
+ {'fieldName' => 'required', 'fieldLabel' => 'Required', 'type' => 'checkbox', 'defaultValue' => false, 'displayOrder' => 10},
304
+ {'fieldName' => 'exportMeta', 'fieldLabel' => 'Export As Tag', 'type' => 'checkbox', 'defaultValue' => false, 'description' => 'Export as Tag.', 'displayOrder' => 11},
300
305
  ]
301
306
  end
302
307
 
@@ -34,7 +34,7 @@ class Morpheus::Cli::LogsCommand
34
34
  options = {}
35
35
  params = {}
36
36
  optparse = Morpheus::Cli::OptionParser.new do |opts|
37
- opts.banner = subcommand_usage("[id]")
37
+ opts.banner = subcommand_usage("[search]")
38
38
  opts.on('--hosts HOSTS', String, "Filter logs to specific Host ID(s)") do |val|
39
39
  params['servers'] = val.to_s.split(",").collect {|it| it.to_s.strip }.select {|it| it }.compact
40
40
  end
@@ -72,18 +72,21 @@ class Morpheus::Cli::LogsCommand
72
72
  options[:details] = true
73
73
  end
74
74
  build_common_options(opts, options, [:list, :query, :json, :yaml, :csv, :fields, :dry_run, :remote])
75
- opts.footer = "List logs for a container.\n" +
76
- "[id] is required. This is the id of a container."
75
+ opts.footer = "List logs for all hosts and containers."
77
76
  end
78
77
  optparse.parse!(args)
79
- if args.count != 0
80
- raise_command_error "wrong number of arguments, expected 0 and got (#{args.count}) #{args.join(' ')}\n#{optparse}"
78
+ if args.count > 0
79
+ options[:phrase] = args.join(" ")
81
80
  end
82
81
  connect(options)
83
82
  begin
84
83
  params['level'] = params['level'].collect {|it| it.to_s.upcase }.join('|') if params['level'] # api works with INFO|WARN
85
84
  params.merge!(parse_list_options(options))
86
- params['query'] = params.delete('phrase') if params['phrase']
85
+ if params['phrase']
86
+ options.delete(:phrase)
87
+ search_phrase = params.delete('phrase')
88
+ params['query'] = search_phrase
89
+ end
87
90
  params['order'] = params['direction'] unless params['direction'].nil? # old api version expects order instead of direction
88
91
  params['startMs'] = (options[:start].to_i * 1000) if options[:start]
89
92
  params['endMs'] = (options[:end].to_i * 1000) if options[:end]
@@ -401,7 +401,7 @@ module Morpheus::Cli::AccountsHelper
401
401
  end
402
402
 
403
403
  def get_access_string(access, return_color=cyan)
404
- get_access_color(access) + access + return_color
404
+ get_access_color(access) + access.to_s + return_color.to_s
405
405
  # access ||= 'none'
406
406
  # if access == 'none'
407
407
  # "#{white}#{access.to_s}#{return_color}"
@@ -426,10 +426,14 @@ module Morpheus::Cli::AccountsHelper
426
426
  # Examples: format_permission_access("read")
427
427
  # format_permission_access("custom", "full,custom,none")
428
428
  def format_access_string(access, access_levels=nil, return_color=cyan)
429
+ # nevermind all this, just colorized access level
430
+ return get_access_string(access, return_color)
431
+
429
432
  access = access.to_s.downcase.strip
430
433
  if access.empty?
431
434
  access = "none"
432
435
  end
436
+
433
437
  if access_levels.nil?
434
438
  access_levels = ["none","read","user","full"]
435
439
  elsif access_levels.is_a?(Array)
@@ -51,9 +51,10 @@ module Morpheus::Cli::DeploymentsHelper
51
51
  return nil
52
52
  elsif deployments.size > 1
53
53
  print_red_alert "#{deployments.size} deployments found by name '#{name}'"
54
+ print_error "\n"
54
55
  puts_error as_pretty_table(deployments, [:id, :name], {color:red})
55
56
  print_red_alert "Try using ID instead"
56
- print reset,"\n"
57
+ print_error reset,"\n"
57
58
  return nil
58
59
  else
59
60
  return deployments[0]
@@ -122,13 +123,41 @@ module Morpheus::Cli::DeploymentsHelper
122
123
  return nil
123
124
  elsif deployment_versions.size > 1
124
125
  print_red_alert "#{deployment_versions.size} deployment versions found by version '#{name}'"
126
+ print_error "\n"
125
127
  puts_error as_pretty_table(deployment_versions, {"ID" => 'id', "VERSION" => 'userVersion'}, {color:red})
126
128
  print_red_alert "Try using ID instead"
127
- print reset,"\n"
129
+ print_error reset,"\n"
128
130
  return nil
129
131
  else
130
132
  return deployment_versions[0]
131
133
  end
132
134
  end
133
135
 
136
+ def format_deployment_version_number(deployment_version)
137
+ if deployment_version
138
+ deployment_version['userVersion'] || deployment_version['version'] || ''
139
+ else
140
+ ''
141
+ end
142
+ end
143
+
144
+ def format_app_deploy_status(status, return_color=cyan)
145
+ out = ""
146
+ s = status.to_s.downcase
147
+ if s == 'deployed' || s == 'committed'
148
+ out << "#{green}#{s.upcase}#{return_color}"
149
+ elsif s == 'open' || s == 'archived'
150
+ out << "#{cyan}#{s.upcase}#{return_color}"
151
+ elsif s == 'failed'
152
+ out << "#{red}#{s.upcase}#{return_color}"
153
+ else
154
+ out << "#{yellow}#{s.upcase}#{return_color}"
155
+ end
156
+ out
157
+ end
158
+
159
+ def format_deploy_type(val)
160
+ return val
161
+ end
162
+
134
163
  end
@@ -448,7 +448,8 @@ module Morpheus::Cli::PrintHelper
448
448
 
449
449
  if opts[:bar_color] == :rainbow
450
450
  rainbow_bar = ""
451
- cur_rainbow_color = white
451
+ cur_rainbow_color = reset # default terminal color
452
+ rainbow_bar << cur_rainbow_color
452
453
  bars.each_with_index {|bar, i|
453
454
  reached_percent = (i / max_bars.to_f) * 100
454
455
  new_bar_color = cur_rainbow_color
@@ -458,6 +459,8 @@ module Morpheus::Cli::PrintHelper
458
459
  new_bar_color = yellow
459
460
  elsif reached_percent > 10
460
461
  new_bar_color = cyan
462
+ else
463
+ new_bar_color = reset
461
464
  end
462
465
  if cur_rainbow_color != new_bar_color
463
466
  cur_rainbow_color = new_bar_color
@@ -471,7 +474,7 @@ module Morpheus::Cli::PrintHelper
471
474
  #rainbow_bar << " " * padding
472
475
  end
473
476
  rainbow_bar << reset
474
- bar_display = white + "[" + rainbow_bar + white + "]" + " #{cur_rainbow_color}#{percent_label}#{reset}"
477
+ bar_display = cyan + "[" + rainbow_bar + cyan + "]" + " #{cur_rainbow_color}#{percent_label}#{reset}"
475
478
  out << bar_display
476
479
  elsif opts[:bar_color] == :solid
477
480
  bar_color = cyan
@@ -479,12 +482,16 @@ module Morpheus::Cli::PrintHelper
479
482
  bar_color = red
480
483
  elsif percent > 50
481
484
  bar_color = yellow
485
+ elsif percent > 10
486
+ bar_color = cyan
487
+ else
488
+ bar_color = reset
482
489
  end
483
- bar_display = white + "[" + bar_color + bars.join.ljust(max_bars, ' ') + white + "]" + " #{percent_label}" + reset
490
+ bar_display = cyan + "[" + bar_color + bars.join.ljust(max_bars, ' ') + cyan + "]" + " #{percent_label}" + reset
484
491
  out << bar_display
485
492
  else
486
- bar_color = opts[:bar_color] || cyan
487
- bar_display = white + "[" + bar_color + bars.join.ljust(max_bars, ' ') + white + "]" + " #{percent_label}" + reset
493
+ bar_color = opts[:bar_color] || reset
494
+ bar_display = cyan + "[" + bar_color + bars.join.ljust(max_bars, ' ') + cyan + "]" + " #{percent_label}" + reset
488
495
  out << bar_display
489
496
  end
490
497
  return out
@@ -504,7 +511,7 @@ module Morpheus::Cli::PrintHelper
504
511
  out << cyan + "Max CPU".rjust(label_width, ' ') + ": " + generate_usage_bar(cpu_usage.to_f, 100) + "\n"
505
512
  end
506
513
  if opts[:include].include?(:avg_cpu)
507
- cpu_usage = stats['cpuUsageAvg']
514
+ cpu_usage = stats['cpuUsageAvg'] || stats['cpuUsageAverage']
508
515
  out << cyan + "Avg. CPU".rjust(label_width, ' ') + ": " + generate_usage_bar(cpu_usage.to_f, 100) + "\n"
509
516
  end
510
517
  if opts[:include].include?(:cpu)
@@ -1151,27 +1158,6 @@ module Morpheus::Cli::PrintHelper
1151
1158
  out
1152
1159
  end
1153
1160
 
1154
- def format_list(items, conjunction="and", limit=nil)
1155
- items = items ? items.clone : []
1156
- if limit
1157
- items = items.first(limit)
1158
- end
1159
- last_item = items.pop
1160
- if items.empty?
1161
- return "#{last_item}"
1162
- else
1163
- return items.join(", ") + (conjunction.to_s.empty? ? ", " : " #{conjunction} ") + "#{last_item}" + ((limit && limit < (items.size+1)) ? " ..." : "")
1164
- end
1165
- end
1166
-
1167
- def anded_list(items, limit=nil)
1168
- format_list(items, "and", limit)
1169
- end
1170
-
1171
- def ored_list(items, limit=nil)
1172
- format_list(items, "or", limit)
1173
- end
1174
-
1175
1161
  def sleep_with_dots(sleep_seconds, dots=3, dot_chr=".")
1176
1162
  dot_interval = (sleep_seconds.to_f / dots.to_i)
1177
1163
  dots.to_i.times do |dot_index|
@@ -596,12 +596,12 @@ module Morpheus::Cli::ProvisioningHelper
596
596
  v_prompt = Morpheus::Cli::OptionTypes.prompt([{'fieldName' => 'environment', 'fieldLabel' => 'Environment', 'type' => 'select', 'required' => false, 'selectOptions' => get_available_environments()}], options[:options])
597
597
  payload['instance']['instanceContext'] = v_prompt['environment'] if !v_prompt['environment'].empty?
598
598
 
599
- # Labels (tags)
600
- if options[:tags]
601
- payload['instance']['tags'] = options[:tags].is_a?(Array) ? options[:tags] : options[:tags].to_s.split(',').collect {|it| it.to_s.strip }.compact.uniq
599
+ # Labels (used to be called tags)
600
+ if options[:labels]
601
+ payload['instance']['labels'] = options[:labels].is_a?(Array) ? options[:labels] : options[:labels].to_s.split(',').collect {|it| it.to_s.strip }.compact.uniq
602
602
  else
603
- v_prompt = Morpheus::Cli::OptionTypes.prompt([{'fieldName' => 'tags', 'fieldLabel' => 'Labels', 'type' => 'text', 'required' => false}], options[:options])
604
- payload['instance']['tags'] = v_prompt['tags'].split(',').collect {|it| it.to_s.strip }.compact.uniq if !v_prompt['tags'].empty?
603
+ v_prompt = Morpheus::Cli::OptionTypes.prompt([{'fieldName' => 'labels', 'fieldLabel' => 'Labels', 'type' => 'text', 'required' => false}], options[:options])
604
+ payload['instance']['labels'] = v_prompt['labels'].split(',').collect {|it| it.to_s.strip }.compact.uniq if !v_prompt['labels'].empty?
605
605
  end
606
606
 
607
607
  # Version and Layout
@@ -976,6 +976,9 @@ module Morpheus::Cli::ProvisioningHelper
976
976
  metadata_list = options[:metadata].split(",").select {|it| !it.to_s.empty? }
977
977
  metadata_list = metadata_list.collect do |it|
978
978
  metadata_pair = it.split(":")
979
+ if metadata_pair.size < 2 && it.include?("=")
980
+ metadata_pair = it.split("=")
981
+ end
979
982
  row = {}
980
983
  row['name'] = metadata_pair[0].to_s.strip
981
984
  row['value'] = metadata_pair[1].to_s.strip
@@ -1688,6 +1691,15 @@ module Morpheus::Cli::ProvisioningHelper
1688
1691
  row = {}
1689
1692
  row['name'] = metadata_pair[0].to_s.strip
1690
1693
  row['value'] = metadata_pair[1].to_s.strip
1694
+ # hacky way to set masked flag to true of false to (masked) in the value itself
1695
+ if(row['value'].include?("(masked)"))
1696
+ row['value'] = row['value'].gsub("(masked)", "").strip
1697
+ row['masked'] = true
1698
+ end
1699
+ if(row['value'].include?("(unmasked)"))
1700
+ row['value'] = row['value'].gsub("(unmasked)", "").strip
1701
+ row['masked'] = false
1702
+ end
1691
1703
  row
1692
1704
  end
1693
1705
  metadata = metadata_list
@@ -2175,6 +2187,82 @@ module Morpheus::Cli::ProvisioningHelper
2175
2187
  return ports
2176
2188
  end
2177
2189
 
2190
+ def format_instance_status(instance, return_color=cyan)
2191
+ out = ""
2192
+ status_string = instance['status'].to_s
2193
+ if status_string == 'running'
2194
+ out << "#{green}#{status_string.upcase}#{return_color}"
2195
+ elsif status_string == 'provisioning'
2196
+ out << "#{cyan}#{status_string.upcase}#{return_color}"
2197
+ elsif status_string == 'stopped' or status_string == 'failed'
2198
+ out << "#{red}#{status_string.upcase}#{return_color}"
2199
+ else
2200
+ out << "#{yellow}#{status_string.upcase}#{return_color}"
2201
+ end
2202
+ out
2203
+ end
2204
+
2205
+ def format_instance_connection_string(instance)
2206
+ if !instance['connectionInfo'].nil? && instance['connectionInfo'].empty? == false
2207
+ connection_string = "#{instance['connectionInfo'][0]['ip']}:#{instance['connectionInfo'][0]['port']}"
2208
+ end
2209
+ end
2210
+
2211
+ def format_app_status(app, return_color=cyan)
2212
+ out = ""
2213
+ status_string = app['status'] || app['appStatus'] || ''
2214
+ if status_string == 'running'
2215
+ out = "#{green}#{status_string.upcase}#{return_color}"
2216
+ elsif status_string == 'provisioning'
2217
+ out = "#{cyan}#{status_string.upcase}#{cyan}"
2218
+ elsif status_string == 'stopped' or status_string == 'failed'
2219
+ out = "#{red}#{status_string.upcase}#{return_color}"
2220
+ elsif status_string == 'unknown'
2221
+ out = "#{yellow}#{status_string.upcase}#{return_color}"
2222
+ elsif status_string == 'warning' && app['instanceCount'].to_i == 0
2223
+ # show this instead of WARNING
2224
+ out = "#{cyan}EMPTY#{return_color}"
2225
+ else
2226
+ out = "#{yellow}#{status_string.upcase}#{return_color}"
2227
+ end
2228
+ out
2229
+ end
2230
+
2231
+ def format_container_status(container, return_color=cyan)
2232
+ out = ""
2233
+ status_string = container['status'].to_s
2234
+ if status_string == 'running'
2235
+ out << "#{green}#{status_string.upcase}#{return_color}"
2236
+ elsif status_string == 'provisioning'
2237
+ out << "#{cyan}#{status_string.upcase}#{return_color}"
2238
+ elsif status_string == 'stopped' or status_string == 'failed'
2239
+ out << "#{red}#{status_string.upcase}#{return_color}"
2240
+ else
2241
+ out << "#{yellow}#{status_string.upcase}#{return_color}"
2242
+ end
2243
+ out
2244
+ end
2245
+
2246
+ def format_container_connection_string(container)
2247
+ if !container['ports'].nil? && container['ports'].empty? == false
2248
+ connection_string = "#{container['ip']}:#{container['ports'][0]['external']}"
2249
+ else
2250
+ # eh? more logic needed here i think, see taglib morph:containerLocationMenu
2251
+ connection_string = "#{container['ip']}"
2252
+ end
2253
+ end
2254
+
2255
+ def format_instance_container_display_name(instance, plural=false)
2256
+ #<span class="info-label">${[null,'docker'].contains(instance.layout?.provisionType?.code) ? 'Containers' : 'Virtual Machines'}:</span> <span class="info-value">${instance.containers?.size()}</span>
2257
+ v = plural ? "Containers" : "Container"
2258
+ if instance && instance['layout'] && instance['layout'].key?("provisionTypeCode")
2259
+ if [nil, 'docker'].include?(instance['layout']["provisionTypeCode"])
2260
+ v = plural ? "Virtual Machines" : "Virtual Machine"
2261
+ end
2262
+ end
2263
+ return v
2264
+ end
2265
+
2178
2266
  def format_blueprint_type(type_code)
2179
2267
  return type_code.to_s # just show it as is
2180
2268
  if type_code.to_s.empty?
@@ -2200,4 +2288,19 @@ module Morpheus::Cli::ProvisioningHelper
2200
2288
  return type_code.to_s.downcase
2201
2289
  end
2202
2290
  end
2291
+
2292
+ def format_snapshot_status(snapshot, return_color=cyan)
2293
+ out = ""
2294
+ status_string = snapshot['status'].to_s
2295
+ if status_string == 'complete'
2296
+ out << "#{green}#{status_string.upcase}#{return_color}"
2297
+ elsif status_string == 'creating'
2298
+ out << "#{cyan}#{status_string.upcase}#{return_color}"
2299
+ elsif status_string == 'failed'
2300
+ out << "#{red}#{status_string.upcase}#{return_color}"
2301
+ else
2302
+ out << "#{yellow}#{status_string.upcase}#{return_color}"
2303
+ end
2304
+ out
2305
+ end
2203
2306
  end
@@ -5,6 +5,7 @@ module Morpheus
5
5
  module Cli
6
6
  module OptionTypes
7
7
  include Term::ANSIColor
8
+ # include Morpheus::Cli::PrintHelper
8
9
 
9
10
  def self.confirm(message,options={})
10
11
  if options[:yes] == true
@@ -72,9 +73,15 @@ module Morpheus
72
73
  field_name = namespaces.pop
73
74
 
74
75
  # respect optionType.dependsOnCode
75
- if option_type['dependsOnCode'] && option_type['dependsOnCode'] != ""
76
+ # i guess this switched to visibleOnCode, respect one or the other
77
+ visible_option_check_value = option_type['dependsOnCode']
78
+ if !option_type['visibleOnCode'].to_s.empty?
79
+ visible_option_check_value = option_type['visibleOnCode']
80
+ end
81
+ if !visible_option_check_value.to_s.empty?
76
82
  # support formats code=value or code:value OR code:(value|value2|value3)
77
- parts = option_type['dependsOnCode'].include?("=") ? option_type['dependsOnCode'].split("=") : option_type['dependsOnCode'].split(":")
83
+ # OR fieldContext.fieldName=value
84
+ parts = visible_option_check_value.include?("=") ? visible_option_check_value.split("=") : visible_option_check_value.split(":")
78
85
  depends_on_code = parts[0]
79
86
  depends_on_value = parts[1].to_s.strip
80
87
  depends_on_values = []
@@ -87,14 +94,29 @@ module Morpheus
87
94
  depends_on_values = depends_on_value.split("|").collect { |it| it.strip }
88
95
  end
89
96
  depends_on_option_type = option_types.find {|it| it["code"] == depends_on_code }
90
- # could not find the dependent option type, proceed and prompt
91
- if !depends_on_option_type.nil?
97
+ if !depends_on_option_type
98
+ depends_on_option_type = option_types.find {|it|
99
+ (it['fieldContext'] ? "#{it['fieldContext']}.#{it['fieldName']}" : it['fieldName']) == depends_on_code
100
+ }
101
+ end
102
+ if depends_on_option_type
92
103
  # dependent option type has a different value
93
104
  depends_on_field_key = depends_on_option_type['fieldContext'] ? "#{depends_on_option_type['fieldContext']}.#{depends_on_option_type['fieldName']}" : "#{depends_on_option_type['fieldName']}"
94
105
  found_dep_value = get_object_value(results, depends_on_field_key) || get_object_value(options, depends_on_field_key)
95
- if depends_on_values.size > 0 && !depends_on_values.include?(found_dep_value)
96
- next
106
+ if depends_on_values.size > 0
107
+ # must be in the specified values
108
+ # todo: uhh this actually needs to change to parse regex
109
+ if !depends_on_values.include?(found_dep_value)
110
+ next
111
+ end
112
+ else
113
+ # no value found
114
+ if found_dep_value.to_s.empty?
115
+ next
116
+ end
97
117
  end
118
+ else
119
+ # could not find the dependent option type, proceed and prompt
98
120
  end
99
121
  end
100
122
 
@@ -115,22 +137,32 @@ module Morpheus
115
137
  # use the value passed in the options map
116
138
  if cur_namespace.respond_to?('key?') && cur_namespace.key?(field_name)
117
139
  value = cur_namespace[field_name]
118
- input_value = ['select', 'multiSelect'].include?(option_type['type']) && option_type['fieldInput'] ? cur_namespace[option_type['fieldInput']] : nil
140
+ input_value = ['select', 'multiSelect','typeahead', 'multiTypeahead'].include?(option_type['type']) && option_type['fieldInput'] ? cur_namespace[option_type['fieldInput']] : nil
119
141
  if option_type['type'] == 'number'
120
142
  value = value.to_s.include?('.') ? value.to_f : value.to_i
121
143
  # these select prompts should just fall down through below, with the extra params no_prompt, use_value
122
144
  elsif option_type['type'] == 'select'
123
- value = select_prompt(option_type.merge({'defaultValue' => value, 'defaultInputValue' => input_value}), api_client, (api_params || {}).merge(results), true)
145
+ value = select_prompt(option_type.merge({'defaultValue' => value, 'defaultInputValue' => input_value}), api_client, (option_type['noParams'] ? {} : (api_params || {}).merge(results)), true)
124
146
  elsif option_type['type'] == 'multiSelect'
125
147
  # support value as csv like "thing1, thing2"
126
148
  value_list = value.is_a?(String) ? value.parse_csv.collect {|v| v ? v.to_s.strip : v } : [value].flatten
127
149
  input_value_list = input_value.is_a?(String) ? input_value.parse_csv.collect {|v| v ? v.to_s.strip : v } : [input_value].flatten
128
150
  select_value_list = []
129
151
  value_list.each_with_index do |v, i|
130
- select_value_list << select_prompt(option_type.merge({'defaultValue' => v, 'defaultInputValue' => input_value_list[i]}), api_client, (api_params || {}).merge(results), true)
152
+ select_value_list << select_prompt(option_type.merge({'defaultValue' => v, 'defaultInputValue' => input_value_list[i]}), api_client, (option_type['noParams'] ? {} : (api_params || {}).merge(results)), true)
153
+ end
154
+ value = select_value_list
155
+ elsif option_type['type'] == 'typeahead'
156
+ value = typeahead_prompt(option_type.merge({'defaultValue' => value, 'defaultInputValue' => input_value}), api_client, (option_type['noParams'] ? {} : (api_params || {}).merge(results)), true)
157
+ elsif option_type['type'] == 'multiTypeahead'
158
+ # support value as csv like "thing1, thing2"
159
+ value_list = value.is_a?(String) ? value.parse_csv.collect {|v| v ? v.to_s.strip : v } : [value].flatten
160
+ input_value_list = input_value.is_a?(String) ? input_value.parse_csv.collect {|v| v ? v.to_s.strip : v } : [input_value].flatten
161
+ select_value_list = []
162
+ value_list.each_with_index do |v, i|
163
+ select_value_list << typeahead_prompt(option_type.merge({'defaultValue' => v, 'defaultInputValue' => input_value_list[i]}), api_client, (option_type['noParams'] ? {} : (api_params || {}).merge(results)), true)
131
164
  end
132
165
  value = select_value_list
133
-
134
166
  end
135
167
  if options[:always_prompt] != true
136
168
  value_found = true
@@ -147,7 +179,7 @@ module Morpheus
147
179
  no_prompt = no_prompt || options[:no_prompt]
148
180
  if no_prompt
149
181
  if !value_found
150
- if option_type['defaultValue'] != nil && !['select', 'multiSelect'].include?(option_type['type'])
182
+ if option_type['defaultValue'] != nil && !['select', 'multiSelect','typeahead','multiTypeahead'].include?(option_type['type'])
151
183
  value = option_type['defaultValue']
152
184
  value_found = true
153
185
  end
@@ -155,7 +187,11 @@ module Morpheus
155
187
  # select type is special because it supports skipSingleOption
156
188
  # and prints the available options on error
157
189
  if ['select', 'multiSelect'].include?(option_type['type'])
158
- value = select_prompt(option_type, api_client, (api_params || {}).merge(results), true)
190
+ value = select_prompt(option_type, api_client, (option_type['noParams'] ? {} : (api_params || {}).merge(results)), true)
191
+ value_found = !!value
192
+ end
193
+ if ['typeahead', 'multiTypeahead'].include?(option_type['type'])
194
+ value = typeahead_prompt(option_type, api_client, (option_type['noParams'] ? {} : (api_params || {}).merge(results)), true)
159
195
  value_found = !!value
160
196
  end
161
197
  if !value_found
@@ -192,11 +228,23 @@ module Morpheus
192
228
  # I suppose the entered value should take precedence
193
229
  # api_params = api_params.merge(options) # this might be good enough
194
230
  # dup it
195
- value = select_prompt(option_type, api_client, (api_params || {}).merge(results), options[:no_prompt], nil, paging_enabled)
231
+ value = select_prompt(option_type, api_client, (option_type['noParams'] ? {} : (api_params || {}).merge(results)), options[:no_prompt], nil, paging_enabled)
196
232
  if value && option_type['type'] == 'multiSelect'
197
233
  value = [value]
198
234
  while self.confirm("Add another #{option_type['fieldLabel']}?", {:default => false}) do
199
- if addn_value = select_prompt(option_type, api_client, (api_params || {}).merge(results), options[:no_prompt], nil, paging_enabled)
235
+ if addn_value = select_prompt(option_type, api_client, (option_type['noParams'] ? {} : (api_params || {}).merge(results)), options[:no_prompt], nil, paging_enabled)
236
+ value << addn_value
237
+ else
238
+ break
239
+ end
240
+ end
241
+ end
242
+ elsif ['typeahead', 'multiTypeahead'].include?(option_type['type'])
243
+ value = typeahead_prompt(option_type, api_client, (option_type['noParams'] ? {} : (api_params || {}).merge(results)), options[:no_prompt], nil, paging_enabled)
244
+ if value && option_type['type'] == 'multiTypeahead'
245
+ value = [value]
246
+ while self.confirm("Add another #{option_type['fieldLabel']}?", {:default => false}) do
247
+ if addn_value = typeahead_prompt(option_type, api_client, (option_type['noParams'] ? {} : (api_params || {}).merge(results)), options[:no_prompt], nil, paging_enabled)
200
248
  value << addn_value
201
249
  else
202
250
  break
@@ -310,11 +358,12 @@ module Morpheus
310
358
  value_field = (option_type['config'] ? option_type['config']['valueField'] : nil) || 'value'
311
359
  default_value = option_type['defaultValue']
312
360
  default_value = default_value['id'] if default_value && default_value.is_a?(Hash) && !default_value['id'].nil?
361
+ api_params ||= {}
313
362
  # local array of options
314
363
  if option_type['selectOptions']
315
364
  # calculate from inline lambda
316
365
  if option_type['selectOptions'].is_a?(Proc)
317
- select_options = option_type['selectOptions'].call()
366
+ select_options = option_type['selectOptions'].call(api_client, grails_params(api_params || {}))
318
367
  else
319
368
  # todo: better type validation
320
369
  select_options = option_type['selectOptions']
@@ -325,7 +374,7 @@ module Morpheus
325
374
  select_options = option_type['optionSource'].call(api_client, grails_params(api_params || {}))
326
375
  elsif option_type['optionSource'] == 'list'
327
376
  # /api/options/list is a special action for custom OptionTypeLists, just need to pass the optionTypeId parameter
328
- select_options = load_source_options(option_type['optionSource'], api_client, {'optionTypeId' => option_type['id']})
377
+ select_options = load_source_options(option_type['optionSource'], api_client, grails_params(api_params || {}).merge({'optionTypeId' => option_type['id']}))
329
378
  else
330
379
  # remote optionSource aka /api/options/$optionSource?
331
380
  select_options = load_source_options(option_type['optionSource'], api_client, grails_params(api_params || {}))
@@ -345,7 +394,7 @@ module Morpheus
345
394
  print Term::ANSIColor.red, " * #{option_type['fieldLabel']} [-O #{option_type['fieldContext'] ? (option_type['fieldContext']+'.') : ''}#{option_type['fieldName']}=] - #{option_type['description']}\n", Term::ANSIColor.reset
346
395
  if select_options && select_options.size > 10
347
396
  display_select_options(option_type, select_options.first(10))
348
- puts " (#{select_options.size-1} more)"
397
+ puts " (#{select_options.size-10} more)"
349
398
  else
350
399
  display_select_options(option_type, select_options)
351
400
  end
@@ -388,7 +437,7 @@ module Morpheus
388
437
  print Term::ANSIColor.red, " * #{option_type['fieldLabel']} [-O #{help_field_key}=] - #{option_type['description']}\n", Term::ANSIColor.reset
389
438
  if select_options && select_options.size > 10
390
439
  display_select_options(option_type, select_options.first(10))
391
- puts " (#{select_options.size-1} more)"
440
+ puts " (#{select_options.size-10} more)"
392
441
  else
393
442
  display_select_options(option_type, select_options)
394
443
  end
@@ -458,6 +507,154 @@ module Morpheus
458
507
  value
459
508
  end
460
509
 
510
+ # this works like select_prompt, but refreshes options with ?query=value between inputs
511
+ # paging_enabled is ignored right now
512
+ def self.typeahead_prompt(option_type,api_client, api_params={}, no_prompt=false, use_value=nil, paging_enabled=false)
513
+ select_options = []
514
+ field_key = [option_type['fieldContext'], option_type['fieldName']].select {|it| it && it != '' }.join('.')
515
+ help_field_key = option_type[:help_field_prefix] ? "#{option_type[:help_field_prefix]}.#{field_key}" : field_key
516
+ input = ""
517
+ value_found = false
518
+ value = nil
519
+ value_field = (option_type['config'] ? option_type['config']['valueField'] : nil) || 'value'
520
+ default_value = option_type['defaultValue']
521
+ default_value = default_value['id'] if default_value && default_value.is_a?(Hash) && !default_value['id'].nil?
522
+
523
+ while !value_found do
524
+ # ok get input, refresh options and see if it matches
525
+ # if matches one, cool otherwise print matches and reprompt or error
526
+ if use_value
527
+ input = use_value
528
+ elsif no_prompt
529
+ input = default_value
530
+ else
531
+ Readline.completion_append_character = ""
532
+ Readline.basic_word_break_characters = ''
533
+ Readline.completion_proc = proc {|s|
534
+ matches = []
535
+ available_options = (select_options || [])
536
+ available_options.each{|option|
537
+ if option['name'] && option['name'] =~ /^#{Regexp.escape(s)}/
538
+ matches << option['name']
539
+ # elsif option['id'] && option['id'].to_s =~ /^#{Regexp.escape(s)}/
540
+ elsif option[value_field] && option[value_field].to_s == s
541
+ matches << option['name']
542
+ end
543
+ }
544
+ matches
545
+ }
546
+ # prompt for typeahead input value
547
+ input = Readline.readline("#{option_type['fieldLabel']}#{option_type['fieldAddOn'] ? ('(' + option_type['fieldAddOn'] + ') ') : '' }#{!option_type['required'] ? ' (optional)' : ''}#{!default_value.to_s.empty? ? ' ['+default_value.to_s+']' : ''} ['?' for options]: ", false).to_s
548
+ input = input.chomp.strip
549
+ end
550
+
551
+ # just hit enter, use [default] if set
552
+ if input.empty? && default_value
553
+ input = default_value.to_s
554
+ end
555
+
556
+ # not required and no value? ok proceed
557
+ if input.to_s == "" && option_type['required'] != true
558
+ value_found = true
559
+ value = nil # or "" # hmm
560
+ #next
561
+ break
562
+ end
563
+
564
+ # required and no value? you need help
565
+ # if input.to_s == "" && option_type['required'] == true
566
+ # help_prompt(option_type)
567
+ # display_select_options(option_type, select_options) unless select_options.empty?
568
+ # next
569
+ # end
570
+
571
+ # looking for help with this input
572
+ if input == '?'
573
+ help_prompt(option_type)
574
+ display_select_options(option_type, select_options) unless select_options.empty?
575
+ next
576
+ end
577
+
578
+ # just hit enter? scram
579
+ # looking for help with this input
580
+ # if input == ""
581
+ # help_prompt(option_type)
582
+ # display_select_options(option_type, select_options)
583
+ # next
584
+ # end
585
+
586
+ # this is how typeahead works, it keeps refreshing the options with a new ?query={value}
587
+ # query_value = (value || use_value || default_value || '')
588
+ query_value = (input || '')
589
+ api_params ||= {}
590
+ api_params['query'] = query_value
591
+ # skip refresh if you just hit enter
592
+ if !query_value.empty?
593
+ select_options = load_options(option_type, api_client, api_params, query_value)
594
+ end
595
+
596
+ # match input to option name or value
597
+ # actually that is redundant, it should already be filtered to matches
598
+ # and can just do this:
599
+ # select_option = select_options.size == 1 ? select_options[0] : nil
600
+ select_option = select_options.find{|b| (b[value_field] && (b[value_field].to_s == input.to_s)) || ((b[value_field].nil? || b[value_field].empty?) && (input == "")) }
601
+ if select_option.nil?
602
+ select_option = select_options.find{|b| b['name'] && b['name'] == input }
603
+ end
604
+
605
+ # found matching value, else did not find a value, show matching options and prompt again or error
606
+ if select_option
607
+ value = select_option[value_field]
608
+ set_last_select(select_option)
609
+ value_found = true
610
+ else
611
+ if use_value || no_prompt
612
+ # todo: make this nicer
613
+ # help_prompt(option_type)
614
+ print Term::ANSIColor.red, "\nMissing Required Option\n\n", Term::ANSIColor.reset
615
+ print Term::ANSIColor.red, " * #{option_type['fieldLabel']} [-O #{help_field_key}=] - #{option_type['description']}\n", Term::ANSIColor.reset
616
+ if select_options && select_options.size > 10
617
+ display_select_options(option_type, select_options.first(10))
618
+ puts " (#{select_options.size-10} more)"
619
+ else
620
+ display_select_options(option_type, select_options)
621
+ end
622
+ print "\n"
623
+ if select_options.empty?
624
+ print "The value '#{input}' matched 0 options.\n"
625
+ # print "Please try again.\n"
626
+ else
627
+ print "The value '#{input}' matched #{select_options.size()} options.\n"
628
+ print "Perhaps you meant one of these? #{ored_list(select_options.collect {|i|i['name']}, 3)}\n"
629
+ # print "Please try again.\n"
630
+ end
631
+ print "\n"
632
+ exit 1
633
+ else
634
+ #help_prompt(option_type)
635
+ display_select_options(option_type, select_options)
636
+ print "\n"
637
+ if select_options.empty?
638
+ print "The value '#{input}' matched 0 options.\n"
639
+ print "Please try again.\n"
640
+ else
641
+ print "The value '#{input}' matched #{select_options.size()} options.\n"
642
+ print "Perhaps you meant one of these? #{ored_list(select_options.collect {|i|i['name']}, 3)}\n"
643
+ print "Please try again.\n"
644
+ end
645
+ print "\n"
646
+ # reprompting now...
647
+ end
648
+ end
649
+ end # end while !value_found
650
+
651
+ # wrap in object when using fieldInput
652
+ if value && !option_type['fieldInput'].nil?
653
+ value = {option_type['fieldName'].split('.').last => value, option_type['fieldInput'] => (no_prompt ? option_type['defaultInputValue'] : field_input_prompt(option_type))}
654
+ end
655
+ value
656
+ end
657
+
461
658
  # this is a funky one, the user is prompted for yes/no
462
659
  # but the return value is 'on','off',nil
463
660
  # todo: maybe make this easier to use, and have the api's be flexible too..
@@ -544,6 +741,9 @@ module Morpheus
544
741
  # value = input.empty? ? option_type['defaultValue'] : input
545
742
  if input == '?' && value.nil?
546
743
  help_prompt(option_type)
744
+ elsif input.chomp == '' && value.nil?
745
+ # just hit enter right away to skip this
746
+ value_found = true
547
747
  elsif input.chomp == 'EOF'
548
748
  value_found = true
549
749
  else
@@ -682,6 +882,42 @@ module Morpheus
682
882
  return file_params
683
883
  end
684
884
 
885
+ def self.load_options(option_type, api_client, api_params, query_value=nil)
886
+ select_options = []
887
+ # local array of options
888
+ if option_type['selectOptions']
889
+ # calculate from inline lambda
890
+ if option_type['selectOptions'].is_a?(Proc)
891
+ select_options = option_type['selectOptions'].call(api_client, grails_params(api_params || {}))
892
+ else
893
+ select_options = option_type['selectOptions']
894
+ end
895
+ # filter options ourselves
896
+ if query_value.to_s != ""
897
+ filtered_options = select_options.select { |it| it['value'].to_s == query_value.to_s }
898
+ if filtered_options.empty?
899
+ filtered_options = select_options.select { |it| it['name'].to_s == query_value.to_s }
900
+ end
901
+ select_options = filtered_options
902
+ end
903
+ elsif option_type['optionSource']
904
+ # calculate from inline lambda
905
+ if option_type['optionSource'].is_a?(Proc)
906
+ select_options = option_type['optionSource'].call(api_client, grails_params(api_params || {}))
907
+ elsif option_type['optionSource'] == 'list'
908
+ # /api/options/list is a special action for custom OptionTypeLists, just need to pass the optionTypeId parameter
909
+ select_options = load_source_options(option_type['optionSource'], api_client, grails_params(api_params || {}).merge({'optionTypeId' => option_type['id']}))
910
+ else
911
+ # remote optionSource aka /api/options/$optionSource?
912
+ select_options = load_source_options(option_type['optionSource'], api_client, grails_params(api_params || {}))
913
+ end
914
+ else
915
+ raise "option '#{field_key}' is type: 'typeahead' and missing selectOptions or optionSource!"
916
+ end
917
+
918
+ return select_options
919
+ end
920
+
685
921
  def self.help_prompt(option_type)
686
922
  field_key = [option_type['fieldContext'], option_type['fieldName']].select {|it| it && it != '' }.join('.')
687
923
  help_field_key = option_type[:help_field_prefix] ? "#{option_type[:help_field_prefix]}.#{field_key}" : field_key
@@ -691,6 +927,11 @@ module Morpheus
691
927
  else
692
928
  print Term::ANSIColor.green," * #{option_type['fieldLabel']} [-O #{help_field_key}=] - ", Term::ANSIColor.reset , "#{option_type['description']}\n"
693
929
  end
930
+ if option_type['type'].to_s == 'typeahead'
931
+ print "This is a typeahead input. Enter the name or value of an option.\n"
932
+ print "If the specified input matches more than one option, they will be printed and you will be prompted again.\n"
933
+ print "the matching options will be shown and you can try again.\n"
934
+ end
694
935
  end
695
936
 
696
937
 
@@ -698,7 +939,8 @@ module Morpheus
698
939
  api_client.options.options_for_source(source,params)['data']
699
940
  end
700
941
 
701
- def self.display_select_options(opt, select_options = [], paging = nil)
942
+ def self.format_select_options_help(opt, select_options = [], paging = nil)
943
+ out = ""
702
944
  header = opt['fieldLabel'] ? "#{opt['fieldLabel']} Options" : "Options"
703
945
  if paging
704
946
  offset = paging[:cur_page] * paging[:page_size]
@@ -706,11 +948,18 @@ module Morpheus
706
948
  header = "#{header} (#{offset+1}-#{limit+1} of #{paging[:total]})"
707
949
  select_options = select_options[(offset)..(limit)]
708
950
  end
709
- puts "\n#{header}"
710
- puts "==============="
951
+ out = ""
952
+ out << "\n"
953
+ out << "#{header}\n"
954
+ out << "===============\n"
711
955
  select_options.each do |option|
712
- puts " * #{option['name']} [#{option['value']}]"
956
+ out << " * #{option['name']} [#{option['value']}]\n"
713
957
  end
958
+ return out
959
+ end
960
+
961
+ def self.display_select_options(opt, select_options = [], paging = nil)
962
+ puts format_select_options_help(opt, select_options, paging)
714
963
  end
715
964
 
716
965
  def self.format_option_types_help(option_types, opts={})