morpheus-cli 4.2.20 → 5.0.2

Sign up to get free protection for your applications and to get access to all the features.
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={})