morpheus-cli 4.2.20 → 5.0.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 (71) hide show
  1. checksums.yaml +4 -4
  2. data/Dockerfile +1 -1
  3. data/lib/morpheus/api/api_client.rb +26 -0
  4. data/lib/morpheus/api/billing_interface.rb +34 -0
  5. data/lib/morpheus/api/catalog_item_types_interface.rb +9 -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 +7 -0
  10. data/lib/morpheus/api/rest_interface.rb +0 -6
  11. data/lib/morpheus/api/roles_interface.rb +14 -0
  12. data/lib/morpheus/api/search_interface.rb +13 -0
  13. data/lib/morpheus/api/servers_interface.rb +7 -0
  14. data/lib/morpheus/api/usage_interface.rb +18 -0
  15. data/lib/morpheus/cli.rb +6 -3
  16. data/lib/morpheus/cli/apps.rb +3 -4
  17. data/lib/morpheus/cli/backup_jobs_command.rb +3 -0
  18. data/lib/morpheus/cli/backups_command.rb +3 -0
  19. data/lib/morpheus/cli/budgets_command.rb +4 -4
  20. data/lib/morpheus/cli/catalog_command.rb +507 -0
  21. data/lib/morpheus/cli/cli_command.rb +45 -20
  22. data/lib/morpheus/cli/commands/standard/curl_command.rb +26 -12
  23. data/lib/morpheus/cli/commands/standard/history_command.rb +3 -1
  24. data/lib/morpheus/cli/commands/standard/man_command.rb +74 -40
  25. data/lib/morpheus/cli/commands/standard/source_command.rb +1 -1
  26. data/lib/morpheus/cli/commands/standard/update_command.rb +76 -0
  27. data/lib/morpheus/cli/containers_command.rb +14 -0
  28. data/lib/morpheus/cli/deploy.rb +199 -90
  29. data/lib/morpheus/cli/deployments.rb +342 -29
  30. data/lib/morpheus/cli/deploys.rb +206 -41
  31. data/lib/morpheus/cli/error_handler.rb +7 -0
  32. data/lib/morpheus/cli/forgot_password.rb +133 -0
  33. data/lib/morpheus/cli/groups.rb +1 -1
  34. data/lib/morpheus/cli/health_command.rb +2 -2
  35. data/lib/morpheus/cli/hosts.rb +181 -26
  36. data/lib/morpheus/cli/instances.rb +102 -33
  37. data/lib/morpheus/cli/invoices_command.rb +33 -16
  38. data/lib/morpheus/cli/jobs_command.rb +28 -6
  39. data/lib/morpheus/cli/library_option_lists_command.rb +14 -6
  40. data/lib/morpheus/cli/logs_command.rb +9 -6
  41. data/lib/morpheus/cli/mixins/accounts_helper.rb +7 -6
  42. data/lib/morpheus/cli/mixins/backups_helper.rb +2 -4
  43. data/lib/morpheus/cli/mixins/catalog_helper.rb +66 -0
  44. data/lib/morpheus/cli/mixins/deployments_helper.rb +31 -3
  45. data/lib/morpheus/cli/mixins/option_source_helper.rb +1 -1
  46. data/lib/morpheus/cli/mixins/print_helper.rb +46 -21
  47. data/lib/morpheus/cli/mixins/provisioning_helper.rb +24 -4
  48. data/lib/morpheus/cli/network_pools_command.rb +14 -6
  49. data/lib/morpheus/cli/option_types.rb +266 -17
  50. data/lib/morpheus/cli/ping.rb +0 -1
  51. data/lib/morpheus/cli/provisioning_licenses_command.rb +2 -2
  52. data/lib/morpheus/cli/remote.rb +35 -12
  53. data/lib/morpheus/cli/reports_command.rb +99 -30
  54. data/lib/morpheus/cli/roles.rb +305 -3
  55. data/lib/morpheus/cli/search_command.rb +182 -0
  56. data/lib/morpheus/cli/service_plans_command.rb +2 -2
  57. data/lib/morpheus/cli/setup.rb +1 -1
  58. data/lib/morpheus/cli/shell.rb +33 -11
  59. data/lib/morpheus/cli/storage_providers_command.rb +40 -56
  60. data/lib/morpheus/cli/tasks.rb +20 -21
  61. data/lib/morpheus/cli/tenants_command.rb +1 -1
  62. data/lib/morpheus/cli/usage_command.rb +203 -0
  63. data/lib/morpheus/cli/user_settings_command.rb +1 -0
  64. data/lib/morpheus/cli/users.rb +12 -1
  65. data/lib/morpheus/cli/version.rb +1 -1
  66. data/lib/morpheus/cli/virtual_images.rb +280 -199
  67. data/lib/morpheus/cli/whoami.rb +6 -6
  68. data/lib/morpheus/cli/workflows.rb +34 -41
  69. data/lib/morpheus/formatters.rb +48 -5
  70. data/lib/morpheus/terminal.rb +6 -2
  71. metadata +13 -2
@@ -75,7 +75,7 @@ module Morpheus::Cli::OptionSourceHelper
75
75
 
76
76
  def get_cloud_options(refresh=false, api_params={})
77
77
  if !@available_cloud_options || refresh
78
- option_results = options_interface.options_for_source('clouds', api_params.mege({'default' => 'false'}))
78
+ option_results = options_interface.options_for_source('clouds', api_params.merge({'default' => 'false'}))
79
79
  @available_cloud_options = option_results['data'].collect {|it|
80
80
  {"name" => it["name"], "value" => it["value"], "id" => it["value"]}
81
81
  }
@@ -1151,27 +1151,6 @@ module Morpheus::Cli::PrintHelper
1151
1151
  out
1152
1152
  end
1153
1153
 
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
1154
  def sleep_with_dots(sleep_seconds, dots=3, dot_chr=".")
1176
1155
  dot_interval = (sleep_seconds.to_f / dots.to_i)
1177
1156
  dots.to_i.times do |dot_index|
@@ -1285,4 +1264,50 @@ module Morpheus::Cli::PrintHelper
1285
1264
  end
1286
1265
  end
1287
1266
 
1267
+ # convert JSON or YAML string to a map
1268
+ def parse_json_or_yaml(config, parsers = [:json, :yaml])
1269
+ rtn = {success: false, data: nil, err: nil}
1270
+ err = nil
1271
+ config = config.strip
1272
+ if config[0..2] == "---"
1273
+ parsers = [:yaml]
1274
+ end
1275
+ # ok only parse json for strings that start with {, consolidated yaml can look like json and cause issues}
1276
+ if config[0] && config[0].chr == "{" && config[-1] && config[-1].chr == "}"
1277
+ parsers = [:json]
1278
+ end
1279
+ parsers.each do |parser|
1280
+ if parser == :yaml
1281
+ begin
1282
+ # todo: one method to parse and return Hash
1283
+ # load does not raise an exception, it just returns the bad string
1284
+ #YAML.parse(config)
1285
+ config_map = YAML.load(config)
1286
+ if !config_map.is_a?(Hash)
1287
+ raise "Failed to parse config as YAML"
1288
+ end
1289
+ rtn[:data] = config_map
1290
+ rtn[:success] = true
1291
+ break
1292
+ rescue => ex
1293
+ rtn[:err] = ex if rtn[:err].nil?
1294
+ end
1295
+ elsif parser == :json
1296
+ begin
1297
+ config_map = JSON.parse(config)
1298
+ rtn[:data] = config_map
1299
+ rtn[:success] = true
1300
+ break
1301
+ rescue => ex
1302
+ rtn[:err] = ex if rtn[:err].nil?
1303
+ end
1304
+ end
1305
+ end
1306
+ return rtn
1307
+ end
1308
+
1309
+ def parse_yaml_or_json(config, parsers = [:yaml, :json])
1310
+ parse_json_or_yaml(config, parsers)
1311
+ end
1312
+
1288
1313
  end
@@ -596,11 +596,13 @@ 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 (Provisioning API still refers to these as tags)
600
+ # and tags (metadata tags) is called metadata.
601
+ # todo: switch this from 'tags' to labels' when the api changes
602
+ if options[:labels]
603
+ payload['instance']['tags'] = options[:labels].is_a?(Array) ? options[:labels] : options[:labels].to_s.split(',').collect {|it| it.to_s.strip }.compact.uniq
602
604
  else
603
- v_prompt = Morpheus::Cli::OptionTypes.prompt([{'fieldName' => 'tags', 'fieldLabel' => 'Labels', 'type' => 'text', 'required' => false}], options[:options])
605
+ v_prompt = Morpheus::Cli::OptionTypes.prompt([{'fieldName' => 'labels', 'fieldLabel' => 'Labels', 'type' => 'text', 'required' => false}], options[:options])
604
606
  payload['instance']['tags'] = v_prompt['tags'].split(',').collect {|it| it.to_s.strip }.compact.uniq if !v_prompt['tags'].empty?
605
607
  end
606
608
 
@@ -976,6 +978,9 @@ module Morpheus::Cli::ProvisioningHelper
976
978
  metadata_list = options[:metadata].split(",").select {|it| !it.to_s.empty? }
977
979
  metadata_list = metadata_list.collect do |it|
978
980
  metadata_pair = it.split(":")
981
+ if metadata_pair.size < 2 && it.include?("=")
982
+ metadata_pair = it.split("=")
983
+ end
979
984
  row = {}
980
985
  row['name'] = metadata_pair[0].to_s.strip
981
986
  row['value'] = metadata_pair[1].to_s.strip
@@ -2200,4 +2205,19 @@ module Morpheus::Cli::ProvisioningHelper
2200
2205
  return type_code.to_s.downcase
2201
2206
  end
2202
2207
  end
2208
+
2209
+ def format_snapshot_status(snapshot, return_color=cyan)
2210
+ out = ""
2211
+ status_string = snapshot['status'].to_s
2212
+ if status_string == 'complete'
2213
+ out << "#{green}#{status_string.upcase}#{return_color}"
2214
+ elsif status_string == 'creating'
2215
+ out << "#{cyan}#{status_string.upcase}#{return_color}"
2216
+ elsif status_string == 'failed'
2217
+ out << "#{red}#{status_string.upcase}#{return_color}"
2218
+ else
2219
+ out << "#{yellow}#{status_string.upcase}#{return_color}"
2220
+ end
2221
+ out
2222
+ end
2203
2223
  end
@@ -596,22 +596,28 @@ class Morpheus::Cli::NetworkPoolsCommand
596
596
  def add_ip(args)
597
597
  options = {}
598
598
  params = {}
599
+ next_free_ip = false
599
600
  optparse = Morpheus::Cli::OptionParser.new do |opts|
600
- opts.banner = subcommand_usage("[network-pool] [ip]")
601
+ opts.banner = subcommand_usage("[network-pool] [ip] [--next]")
601
602
  opts.on('--ip-address VALUE', String, "IP Address for this network pool IP") do |val|
602
603
  options[:options]['ipAddress'] = val
603
604
  end
605
+ opts.on('--next-free-ip', '--next-free-ip', "Use the next available ip address. This can be used instead of specifying an ip address") do
606
+ next_free_ip = true
607
+ end
604
608
  opts.on('--hostname VALUE', String, "Hostname for this network pool IP") do |val|
605
609
  options[:options]['hostname'] = val
606
610
  end
607
611
  build_common_options(opts, options, [:options, :payload, :json, :dry_run, :quiet, :remote])
608
612
  opts.footer = "Create a new network pool IP." + "\n" +
609
613
  "[network-pool] is required. This is the name or id of a network pool.\n" +
610
- "[ip] is required and can be passed as --ip-address instead."
614
+ "[ip] is required or --next-free-ip to use the next available address instead."
611
615
  end
612
616
  optparse.parse!(args)
613
- if args.count < 1 || args.count > 2
614
- raise_command_error "wrong number of arguments, expected 1-2 and got (#{args.count}) #{args}\n#{optparse}"
617
+ if next_free_ip
618
+ verify_args!(args:args, count:1, optparse:optparse)
619
+ else
620
+ verify_args!(args:args, min:1, max:2, optparse:optparse)
615
621
  end
616
622
  connect(options)
617
623
  begin
@@ -639,8 +645,10 @@ class Morpheus::Cli::NetworkPoolsCommand
639
645
  payload['networkPoolIp'].deep_merge!(options[:options].reject {|k,v| k.is_a?(Symbol) }) if options[:options]
640
646
 
641
647
  # IP Address
642
- v_prompt = Morpheus::Cli::OptionTypes.prompt([{'fieldName' => 'ipAddress', 'fieldLabel' => 'IP Address', 'type' => 'text', 'required' => true, 'description' => 'IP Address for this network pool IP.'}], options[:options])
643
- payload['networkPoolIp']['ipAddress'] = v_prompt['ipAddress'] unless v_prompt['ipAddress'].to_s.empty?
648
+ unless next_free_ip
649
+ v_prompt = Morpheus::Cli::OptionTypes.prompt([{'fieldName' => 'ipAddress', 'fieldLabel' => 'IP Address', 'type' => 'text', 'required' => true, 'description' => 'IP Address for this network pool IP.'}], options[:options])
650
+ payload['networkPoolIp']['ipAddress'] = v_prompt['ipAddress'] unless v_prompt['ipAddress'].to_s.empty?
651
+ end
644
652
 
645
653
  # Hostname
646
654
  v_prompt = Morpheus::Cli::OptionTypes.prompt([{'fieldName' => 'hostname', 'fieldLabel' => 'Hostname', 'type' => 'text', 'required' => true, 'description' => 'Hostname for this network pool IP.'}], options[:options])
@@ -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,7 +137,7 @@ 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
@@ -130,7 +152,17 @@ module Morpheus
130
152
  select_value_list << select_prompt(option_type.merge({'defaultValue' => v, 'defaultInputValue' => input_value_list[i]}), api_client, (api_params || {}).merge(results), true)
131
153
  end
132
154
  value = select_value_list
133
-
155
+ elsif option_type['type'] == 'typeahead'
156
+ value = typeahead_prompt(option_type.merge({'defaultValue' => value, 'defaultInputValue' => input_value}), api_client, (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, (api_params || {}).merge(results), true)
164
+ end
165
+ value = select_value_list
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
@@ -158,6 +190,10 @@ module Morpheus
158
190
  value = select_prompt(option_type, api_client, (api_params || {}).merge(results), true)
159
191
  value_found = !!value
160
192
  end
193
+ if ['typeahead', 'multiTypeahead'].include?(option_type['type'])
194
+ value = typeahead_prompt(option_type, api_client, (api_params || {}).merge(results), true)
195
+ value_found = !!value
196
+ end
161
197
  if !value_found
162
198
  if option_type['required']
163
199
  print Term::ANSIColor.red, "\nMissing Required Option\n\n", Term::ANSIColor.reset
@@ -203,6 +239,18 @@ module Morpheus
203
239
  end
204
240
  end
205
241
  end
242
+ elsif ['typeahead', 'multiTypeahead'].include?(option_type['type'])
243
+ value = typeahead_prompt(option_type, api_client, (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, (api_params || {}).merge(results), options[:no_prompt], nil, paging_enabled)
248
+ value << addn_value
249
+ else
250
+ break
251
+ end
252
+ end
253
+ end
206
254
  elsif option_type['type'] == 'hidden'
207
255
  value = option_type['defaultValue']
208
256
  input = value
@@ -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={})