morpheus-cli 5.0.0 → 5.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (44) hide show
  1. checksums.yaml +4 -4
  2. data/Dockerfile +1 -1
  3. data/lib/morpheus/api/api_client.rb +12 -0
  4. data/lib/morpheus/api/billing_interface.rb +1 -0
  5. data/lib/morpheus/api/deploy_interface.rb +1 -1
  6. data/lib/morpheus/api/deployments_interface.rb +20 -1
  7. data/lib/morpheus/api/forgot_password_interface.rb +17 -0
  8. data/lib/morpheus/api/instances_interface.rb +7 -0
  9. data/lib/morpheus/api/search_interface.rb +13 -0
  10. data/lib/morpheus/api/servers_interface.rb +7 -0
  11. data/lib/morpheus/api/usage_interface.rb +18 -0
  12. data/lib/morpheus/cli.rb +4 -1
  13. data/lib/morpheus/cli/cli_command.rb +26 -9
  14. data/lib/morpheus/cli/commands/standard/curl_command.rb +3 -5
  15. data/lib/morpheus/cli/commands/standard/history_command.rb +3 -1
  16. data/lib/morpheus/cli/commands/standard/man_command.rb +74 -40
  17. data/lib/morpheus/cli/deploy.rb +199 -90
  18. data/lib/morpheus/cli/deployments.rb +341 -28
  19. data/lib/morpheus/cli/deploys.rb +206 -41
  20. data/lib/morpheus/cli/error_handler.rb +7 -0
  21. data/lib/morpheus/cli/forgot_password.rb +133 -0
  22. data/lib/morpheus/cli/health_command.rb +2 -2
  23. data/lib/morpheus/cli/hosts.rb +169 -32
  24. data/lib/morpheus/cli/instances.rb +83 -32
  25. data/lib/morpheus/cli/invoices_command.rb +33 -16
  26. data/lib/morpheus/cli/logs_command.rb +9 -6
  27. data/lib/morpheus/cli/mixins/deployments_helper.rb +31 -2
  28. data/lib/morpheus/cli/mixins/print_helper.rb +0 -21
  29. data/lib/morpheus/cli/mixins/provisioning_helper.rb +24 -4
  30. data/lib/morpheus/cli/option_types.rb +266 -17
  31. data/lib/morpheus/cli/remote.rb +35 -10
  32. data/lib/morpheus/cli/reports_command.rb +99 -30
  33. data/lib/morpheus/cli/search_command.rb +182 -0
  34. data/lib/morpheus/cli/setup.rb +1 -1
  35. data/lib/morpheus/cli/shell.rb +33 -11
  36. data/lib/morpheus/cli/tasks.rb +20 -21
  37. data/lib/morpheus/cli/usage_command.rb +64 -11
  38. data/lib/morpheus/cli/version.rb +1 -1
  39. data/lib/morpheus/cli/virtual_images.rb +280 -199
  40. data/lib/morpheus/cli/whoami.rb +6 -6
  41. data/lib/morpheus/cli/workflows.rb +33 -40
  42. data/lib/morpheus/formatters.rb +22 -0
  43. data/lib/morpheus/terminal.rb +6 -2
  44. metadata +7 -2
@@ -25,16 +25,19 @@ class Morpheus::Cli::InvoicesCommand
25
25
  options = {}
26
26
  params = {}
27
27
  ref_ids = []
28
- query_tags = {}
29
28
  optparse = Morpheus::Cli::OptionParser.new do |opts|
30
29
  opts.banner = subcommand_usage()
31
30
  opts.on('-a', '--all', "Display all details, costs and prices." ) do
32
31
  options[:show_all] = true
32
+ options[:show_dates] = true
33
33
  options[:show_estimates] = true
34
34
  # options[:show_costs] = true
35
35
  options[:show_prices] = true
36
36
  # options[:show_raw_data] = true
37
37
  end
38
+ opts.on('--dates', "Display Ref Start, Ref End, etc.") do |val|
39
+ options[:show_dates] = true
40
+ end
38
41
  opts.on('--estimates', '--estimates', "Display all estimated costs, from usage info: Compute, Storage, Network, Extra" ) do
39
42
  options[:show_estimates] = true
40
43
  end
@@ -107,9 +110,12 @@ class Morpheus::Cli::InvoicesCommand
107
110
  params['accountId'] = val
108
111
  end
109
112
  opts.on('--tags Name=Value',String, "Filter by tags.") do |val|
110
- k,v = val.split("=")
111
- query_tags[k] ||= []
112
- query_tags[k] << v
113
+ val.split(",").each do |value_pair|
114
+ k,v = value_pair.strip.split("=")
115
+ options[:tags] ||= {}
116
+ options[:tags][k] ||= []
117
+ options[:tags][k] << (v || '')
118
+ end
113
119
  end
114
120
  opts.on('--raw-data', '--raw-data', "Display Raw Data, the cost data from the cloud provider's API.") do |val|
115
121
  options[:show_raw_data] = true
@@ -169,8 +175,8 @@ class Morpheus::Cli::InvoicesCommand
169
175
  end
170
176
  params['rawData'] = true if options[:show_raw_data]
171
177
  params['refId'] = ref_ids unless ref_ids.empty?
172
- if query_tags && !query_tags.empty?
173
- query_tags.each do |k,v|
178
+ if options[:tags] && !options[:tags].empty?
179
+ options[:tags].each do |k,v|
174
180
  params['tags.' + k] = v
175
181
  end
176
182
  end
@@ -203,6 +209,7 @@ class Morpheus::Cli::InvoicesCommand
203
209
  {"INVOICE ID" => lambda {|it| it['id'] } },
204
210
  {"TYPE" => lambda {|it| format_invoice_ref_type(it) } },
205
211
  {"REF ID" => lambda {|it| it['refId'] } },
212
+ {"REF UUID" => lambda {|it| it['refUuid'] } },
206
213
  {"REF NAME" => lambda {|it|
207
214
  if options[:show_all]
208
215
  it['refName']
@@ -218,9 +225,11 @@ class Morpheus::Cli::InvoicesCommand
218
225
  {"PERIOD" => lambda {|it| format_invoice_period(it) } },
219
226
  {"START" => lambda {|it| format_date(it['startDate']) } },
220
227
  {"END" => lambda {|it| format_date(it['endDate']) } },
221
- ] + (options[:show_all] ? [
228
+ ] + ((options[:show_dates] || options[:show_all]) ? [
222
229
  {"REF START" => lambda {|it| format_dt(it['refStart']) } },
223
230
  {"REF END" => lambda {|it| format_dt(it['refEnd']) } },
231
+ # {"LAST COST DATE" => lambda {|it| format_local_dt(it['lastCostDate']) } },
232
+ # {"LAST ACTUAL DATE" => lambda {|it| format_local_dt(it['lastActualDate']) } },
224
233
  ] : []) + [
225
234
  {"COMPUTE" => lambda {|it| format_money(it['computeCost'], 'usd', {sigdig:options[:sigdig]}) } },
226
235
  # {"MEMORY" => lambda {|it| format_money(it['memoryCost']) } },
@@ -272,9 +281,15 @@ class Morpheus::Cli::InvoicesCommand
272
281
  {"PROJECT TAGS" => lambda {|it| it['project'] ? truncate_string(format_metadata(it['project']['tags']), 50) : '' } },
273
282
  ]
274
283
  end
284
+ if options[:show_dates]
285
+ columns += [
286
+ {"LAST COST DATE" => lambda {|it| format_local_dt(it['lastCostDate']) } },
287
+ {"LAST ACTUAL DATE" => lambda {|it| format_local_dt(it['lastActualDate']) } },
288
+ ]
289
+ end
275
290
  columns += [
276
291
  {"CREATED" => lambda {|it| format_local_dt(it['dateCreated']) } },
277
- {"UPDATED" => lambda {|it| format_local_dt(it['lastUpdated']) } }
292
+ {"UPDATED" => lambda {|it| format_local_dt(it['lastUpdated']) } },
278
293
  ]
279
294
  if options[:show_raw_data]
280
295
  columns += [{"RAW DATA" => lambda {|it| truncate_string(it['rawData'].to_s, 10) } }]
@@ -656,7 +671,6 @@ EOT
656
671
  options = {}
657
672
  params = {}
658
673
  ref_ids = []
659
- query_tags = {}
660
674
  optparse = Morpheus::Cli::OptionParser.new do |opts|
661
675
  opts.banner = subcommand_usage()
662
676
  opts.on('-a', '--all', "Display all details, costs and prices." ) do
@@ -744,11 +758,14 @@ EOT
744
758
  opts.on('--tenant ID', String, "View invoice line items for a tenant. Default is your own account.") do |val|
745
759
  params['accountId'] = val
746
760
  end
747
- # opts.on('--tags Name=Value',String, "Filter by tags.") do |val|
748
- # k,v = val.split("=")
749
- # query_tags[k] ||= []
750
- # query_tags[k] << v
751
- # end
761
+ opts.on('--tags Name=Value',String, "Filter by tags.") do |val|
762
+ val.split(",").each do |value_pair|
763
+ k,v = value_pair.strip.split("=")
764
+ options[:tags] ||= {}
765
+ options[:tags][k] ||= []
766
+ options[:tags][k] << (v || '')
767
+ end
768
+ end
752
769
  opts.on('--raw-data', '--raw-data', "Display Raw Data, the cost data from the cloud provider's API.") do |val|
753
770
  options[:show_raw_data] = true
754
771
  end
@@ -808,8 +825,8 @@ EOT
808
825
  end
809
826
  params['rawData'] = true if options[:show_raw_data]
810
827
  params['refId'] = ref_ids unless ref_ids.empty?
811
- if query_tags && !query_tags.empty?
812
- query_tags.each do |k,v|
828
+ if options[:tags] && !options[:tags].empty?
829
+ options[:tags].each do |k,v|
813
830
  params['tags.' + k] = v
814
831
  end
815
832
  end
@@ -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]
@@ -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
@@ -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|
@@ -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
@@ -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={})