morpheus-cli 5.4.0 → 5.4.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (75) hide show
  1. checksums.yaml +4 -4
  2. data/Dockerfile +1 -1
  3. data/lib/morpheus/api/account_users_interface.rb +68 -0
  4. data/lib/morpheus/api/api_client.rb +51 -9
  5. data/lib/morpheus/api/audit_interface.rb +9 -0
  6. data/lib/morpheus/api/instances_interface.rb +21 -0
  7. data/lib/morpheus/api/load_balancer_monitors_interface.rb +9 -0
  8. data/lib/morpheus/api/load_balancer_pools_interface.rb +4 -4
  9. data/lib/morpheus/api/load_balancer_profiles_interface.rb +4 -5
  10. data/lib/morpheus/api/load_balancer_virtual_servers_interface.rb +13 -4
  11. data/lib/morpheus/api/load_balancers_interface.rb +5 -0
  12. data/lib/morpheus/api/network_routers_interface.rb +9 -0
  13. data/lib/morpheus/api/network_static_routes_interface.rb +36 -0
  14. data/lib/morpheus/api/read_interface.rb +4 -3
  15. data/lib/morpheus/api/rest_interface.rb +3 -3
  16. data/lib/morpheus/api/secondary_read_interface.rb +1 -1
  17. data/lib/morpheus/api/secondary_rest_interface.rb +19 -19
  18. data/lib/morpheus/api/storage_server_types_interface.rb +14 -0
  19. data/lib/morpheus/api/storage_servers_interface.rb +9 -0
  20. data/lib/morpheus/api/storage_volume_types_interface.rb +9 -0
  21. data/lib/morpheus/api/storage_volumes_interface.rb +9 -0
  22. data/lib/morpheus/api/users_interface.rb +16 -63
  23. data/lib/morpheus/cli/cli_command.rb +253 -5
  24. data/lib/morpheus/cli/cli_registry.rb +1 -1
  25. data/lib/morpheus/cli/commands/alias_command.rb +1 -1
  26. data/lib/morpheus/cli/commands/apps.rb +14 -78
  27. data/lib/morpheus/cli/commands/audit.rb +188 -0
  28. data/lib/morpheus/cli/commands/blueprints_command.rb +1 -1
  29. data/lib/morpheus/cli/commands/change_password_command.rb +4 -4
  30. data/lib/morpheus/cli/commands/clusters.rb +37 -12
  31. data/lib/morpheus/cli/commands/hosts.rb +15 -15
  32. data/lib/morpheus/cli/commands/instances.rb +109 -2
  33. data/lib/morpheus/cli/commands/load_balancer_monitors.rb +71 -0
  34. data/lib/morpheus/cli/commands/load_balancer_pools.rb +30 -50
  35. data/lib/morpheus/cli/commands/load_balancer_profiles.rb +65 -0
  36. data/lib/morpheus/cli/commands/load_balancer_types.rb +9 -4
  37. data/lib/morpheus/cli/commands/load_balancer_virtual_servers.rb +77 -57
  38. data/lib/morpheus/cli/commands/load_balancers.rb +93 -6
  39. data/lib/morpheus/cli/commands/network_firewalls_command.rb +22 -5
  40. data/lib/morpheus/cli/commands/network_routers_command.rb +96 -45
  41. data/lib/morpheus/cli/commands/network_static_routes_command.rb +446 -0
  42. data/lib/morpheus/cli/commands/network_transport_zones_command.rb +4 -4
  43. data/lib/morpheus/cli/commands/open_command.rb +30 -0
  44. data/lib/morpheus/cli/commands/options.rb +98 -0
  45. data/lib/morpheus/cli/commands/policies_command.rb +1 -1
  46. data/lib/morpheus/cli/commands/prices_command.rb +7 -7
  47. data/lib/morpheus/cli/commands/remote.rb +4 -2
  48. data/lib/morpheus/cli/commands/roles.rb +1 -1
  49. data/lib/morpheus/cli/commands/shell.rb +2 -2
  50. data/lib/morpheus/cli/commands/storage_server_types.rb +50 -0
  51. data/lib/morpheus/cli/commands/storage_servers.rb +122 -0
  52. data/lib/morpheus/cli/commands/storage_volume_types.rb +50 -0
  53. data/lib/morpheus/cli/commands/storage_volumes.rb +103 -0
  54. data/lib/morpheus/cli/commands/tenants_command.rb +1 -1
  55. data/lib/morpheus/cli/commands/user_groups_command.rb +1 -1
  56. data/lib/morpheus/cli/commands/user_settings_command.rb +2 -1
  57. data/lib/morpheus/cli/commands/user_sources_command.rb +1 -1
  58. data/lib/morpheus/cli/commands/users.rb +28 -28
  59. data/lib/morpheus/cli/commands/view.rb +102 -0
  60. data/lib/morpheus/cli/mixins/accounts_helper.rb +5 -5
  61. data/lib/morpheus/cli/mixins/load_balancers_helper.rb +24 -4
  62. data/lib/morpheus/cli/mixins/print_helper.rb +50 -18
  63. data/lib/morpheus/cli/mixins/processes_helper.rb +1 -2
  64. data/lib/morpheus/cli/mixins/provisioning_helper.rb +15 -5
  65. data/lib/morpheus/cli/mixins/rest_command.rb +145 -73
  66. data/lib/morpheus/cli/mixins/secondary_rest_command.rb +174 -81
  67. data/lib/morpheus/cli/mixins/storage_servers_helper.rb +156 -0
  68. data/lib/morpheus/cli/mixins/storage_volumes_helper.rb +119 -0
  69. data/lib/morpheus/cli/option_types.rb +45 -24
  70. data/lib/morpheus/cli/version.rb +1 -1
  71. data/lib/morpheus/cli.rb +1 -0
  72. data/lib/morpheus/ext/string.rb +29 -6
  73. data/lib/morpheus/routes.rb +238 -0
  74. data/lib/morpheus/util.rb +6 -1
  75. metadata +29 -8
@@ -0,0 +1,119 @@
1
+ require 'morpheus/cli/mixins/print_helper'
2
+ require 'morpheus/cli/option_types'
3
+ require 'morpheus/rest_client'
4
+ # Mixin for Morpheus::Cli command classes
5
+ # Provides common methods for storage volume management
6
+ # including storage volumes and storage volume types
7
+ module Morpheus::Cli::StorageVolumesHelper
8
+
9
+ def self.included(klass)
10
+ klass.send :include, Morpheus::Cli::PrintHelper
11
+ end
12
+
13
+ def storage_volumes_interface
14
+ # @api_client.storage_volumes
15
+ raise "#{self.class} has not defined @storage_volumes_interface" if @storage_volumes_interface.nil?
16
+ @storage_volumes_interface
17
+ end
18
+
19
+ def storage_volume_types_interface
20
+ # @api_client.storage_volume_types
21
+ raise "#{self.class} has not defined @storage_volume_types_interface" if @storage_volume_types_interface.nil?
22
+ @storage_volume_types_interface
23
+ end
24
+
25
+ def storage_volume_object_key
26
+ 'storageVolume'
27
+ end
28
+
29
+ def storage_volume_list_key
30
+ 'storageVolumes'
31
+ end
32
+
33
+ def storage_volume_label
34
+ 'Storage Volume'
35
+ end
36
+
37
+ def storage_volume_label_plural
38
+ 'Storage Volume'
39
+ end
40
+
41
+ def storage_volume_type_object_key
42
+ 'storageVolumeType'
43
+ end
44
+
45
+ def storage_volume_type_list_key
46
+ 'storageVolumeTypes'
47
+ end
48
+
49
+ def storage_volume_type_label
50
+ 'Storage Volume Type'
51
+ end
52
+
53
+ def storage_volume_type_label_plural
54
+ 'Storage Volume Types'
55
+ end
56
+
57
+ def get_available_storage_volume_types(refresh=false)
58
+ if !@available_storage_volume_types || refresh
59
+ @available_storage_volume_types = storage_volume_types_interface.list({max:1000})[storage_volume_type_list_key]
60
+ end
61
+ return @available_storage_volume_types
62
+ end
63
+
64
+ def storage_volume_type_for_name_or_id(val)
65
+ if val.to_s =~ /\A\d{1,}\Z/
66
+ return storage_volume_type_for_id(val)
67
+ else
68
+ return storage_volume_type_for_name(val)
69
+ end
70
+ end
71
+
72
+ def storage_volume_type_for_id(val)
73
+ record = get_available_storage_volume_types().find { |z| z['id'].to_i == val.to_i}
74
+ label = "Storage Volume Type"
75
+ if record.nil?
76
+ print_red_alert "#{label} not found by id #{val}"
77
+ return nil
78
+ end
79
+ return record
80
+ end
81
+
82
+ def storage_volume_type_for_name(val)
83
+ records = get_available_storage_volume_types().select { |z| z['name'].downcase == val.downcase || z['code'].downcase == val.downcase}
84
+ label = "Storage Volume Type"
85
+ if records.empty?
86
+ print_red_alert "#{label} not found by name '#{val}'"
87
+ return nil
88
+ elsif records.size > 1
89
+ print_red_alert "More than one #{label.downcase} found by name '#{val}'"
90
+ print_error "\n"
91
+ puts_error as_pretty_table(records, [:id, :name], {color:red})
92
+ print_red_alert "Try using ID instead"
93
+ print_error reset,"\n"
94
+ return nil
95
+ else
96
+ return records[0]
97
+ end
98
+ end
99
+
100
+ def format_storage_volume_status(record, return_color=cyan)
101
+ out = ""
102
+ status_string = record['status']
103
+ if status_string.nil? || status_string.empty? || status_string == "unknown"
104
+ out << "#{white}UNKNOWN#{return_color}"
105
+ elsif status_string == 'provisioned' || status_string == 'unattached'
106
+ out << "#{cyan}#{status_string.capitalize}#{return_color}"
107
+ elsif status_string == 'syncing'
108
+ out << "#{yellow}#{status_string.capitalize}#{return_color}"
109
+ else
110
+ out << "#{red}#{status_string ? status_string.capitalize : 'N/A'}#{record['statusMessage'] ? "#{return_color} - #{record['statusMessage']}" : ''}#{return_color}"
111
+ end
112
+ out
113
+ end
114
+
115
+ def format_storage_volume_source(storage_volume)
116
+ storage_volume['source']
117
+ end
118
+
119
+ end
@@ -128,7 +128,11 @@ module Morpheus
128
128
  get_object_value(options, depends_on_field_key) ||
129
129
  get_object_value(api_params, depends_on_field_key)
130
130
 
131
- if !field_value.nil? && (depends_on_value.nil? || depends_on_value.empty? || field_value.match?(depends_on_value))
131
+ if field_value.nil? && !options['_object_key'].nil?
132
+ field_value = get_object_value({options['_object_key'] => results}, depends_on_field_key)
133
+ end
134
+
135
+ if !field_value.nil? && (depends_on_value.nil? || depends_on_value.empty? || field_value.to_s.match?(depends_on_value))
132
136
  found_dep_value = true if match_type != 'all'
133
137
  else
134
138
  found_dep_value = false if match_type == 'all'
@@ -151,6 +155,10 @@ module Morpheus
151
155
  context_map = context_map[ns.to_s]
152
156
  end
153
157
 
158
+ # build parameters for option source api request
159
+ option_params = (option_type['noParams'] ? {} : (api_params || {}).merge(results))
160
+ option_params.merge!(option_type['optionParams']) if option_type['optionParams']
161
+
154
162
  # use the value passed in the options map
155
163
  if cur_namespace.respond_to?('key?') && cur_namespace.key?(field_name)
156
164
  value = cur_namespace[field_name]
@@ -161,25 +169,25 @@ module Morpheus
161
169
  end
162
170
  # these select prompts should just fall down through below, with the extra params no_prompt, use_value
163
171
  elsif option_type['type'] == 'select'
164
- value = select_prompt(option_type.merge({'defaultValue' => value, 'defaultInputValue' => input_value}), api_client, (option_type['noParams'] ? {} : (api_params || {}).merge(results)), true)
172
+ value = select_prompt(option_type.merge({'defaultValue' => value, 'defaultInputValue' => input_value}), api_client, option_params, true)
165
173
  elsif option_type['type'] == 'multiSelect'
166
174
  # support value as csv like "thing1, thing2"
167
175
  value_list = value.is_a?(String) ? value.parse_csv.collect {|v| v ? v.to_s.strip : v } : [value].flatten
168
176
  input_value_list = input_value.is_a?(String) ? input_value.parse_csv.collect {|v| v ? v.to_s.strip : v } : [input_value].flatten
169
177
  select_value_list = []
170
178
  value_list.each_with_index do |v, i|
171
- select_value_list << select_prompt(option_type.merge({'defaultValue' => v, 'defaultInputValue' => input_value_list[i]}), api_client, (option_type['noParams'] ? {} : (api_params || {}).merge(results)), true)
179
+ select_value_list << select_prompt(option_type.merge({'defaultValue' => v, 'defaultInputValue' => input_value_list[i]}), api_client, option_params, true)
172
180
  end
173
181
  value = select_value_list
174
182
  elsif option_type['type'] == 'typeahead'
175
- value = typeahead_prompt(option_type.merge({'defaultValue' => value, 'defaultInputValue' => input_value}), api_client, (option_type['noParams'] ? {} : (api_params || {}).merge(results)), true)
183
+ value = typeahead_prompt(option_type.merge({'defaultValue' => value, 'defaultInputValue' => input_value}), api_client, option_params, true)
176
184
  elsif option_type['type'] == 'multiTypeahead'
177
185
  # support value as csv like "thing1, thing2"
178
186
  value_list = value.is_a?(String) ? value.parse_csv.collect {|v| v ? v.to_s.strip : v } : [value].flatten
179
187
  input_value_list = input_value.is_a?(String) ? input_value.parse_csv.collect {|v| v ? v.to_s.strip : v } : [input_value].flatten
180
188
  select_value_list = []
181
189
  value_list.each_with_index do |v, i|
182
- select_value_list << typeahead_prompt(option_type.merge({'defaultValue' => v, 'defaultInputValue' => input_value_list[i]}), api_client, (option_type['noParams'] ? {} : (api_params || {}).merge(results)), true)
190
+ select_value_list << typeahead_prompt(option_type.merge({'defaultValue' => v, 'defaultInputValue' => input_value_list[i]}), api_client, option_params, true)
183
191
  end
184
192
  value = select_value_list
185
193
  end
@@ -205,11 +213,11 @@ module Morpheus
205
213
  # select type is special because it supports skipSingleOption
206
214
  # and prints the available options on error
207
215
  if ['select', 'multiSelect'].include?(option_type['type'])
208
- value = select_prompt(option_type, api_client, (option_type['noParams'] ? {} : (api_params || {}).merge(results)), true)
216
+ value = select_prompt(option_type, api_client, option_params, true)
209
217
  value_found = !!value
210
218
  end
211
219
  if ['typeahead', 'multiTypeahead'].include?(option_type['type'])
212
- value = typeahead_prompt(option_type, api_client, (option_type['noParams'] ? {} : (api_params || {}).merge(results)), true)
220
+ value = typeahead_prompt(option_type, api_client, option_params, true)
213
221
  value_found = !!value
214
222
  end
215
223
  if !value_found
@@ -246,11 +254,11 @@ module Morpheus
246
254
  # I suppose the entered value should take precedence
247
255
  # api_params = api_params.merge(options) # this might be good enough
248
256
  # dup it
249
- value = select_prompt(option_type, api_client, (option_type['noParams'] ? {} : (api_params || {}).deep_merge(results)), options[:no_prompt], nil, paging_enabled)
257
+ value = select_prompt(option_type, api_client, option_params, options[:no_prompt], nil, paging_enabled)
250
258
  if value && option_type['type'] == 'multiSelect'
251
259
  value = [value]
252
260
  while self.confirm("Add another #{option_type['fieldLabel']}?", {:default => false}) do
253
- if addn_value = select_prompt(option_type, api_client, (option_type['noParams'] ? {} : (api_params || {}).merge(results)), options[:no_prompt], nil, paging_enabled)
261
+ if addn_value = select_prompt(option_type, api_client, option_params, options[:no_prompt], nil, paging_enabled)
254
262
  value << addn_value
255
263
  else
256
264
  break
@@ -258,11 +266,11 @@ module Morpheus
258
266
  end
259
267
  end
260
268
  elsif ['typeahead', 'multiTypeahead'].include?(option_type['type'])
261
- value = typeahead_prompt(option_type, api_client, (option_type['noParams'] ? {} : (api_params || {}).merge(results)), options[:no_prompt], nil, paging_enabled)
269
+ value = typeahead_prompt(option_type, api_client, option_params, options[:no_prompt], nil, paging_enabled)
262
270
  if value && option_type['type'] == 'multiTypeahead'
263
271
  value = [value]
264
272
  while self.confirm("Add another #{option_type['fieldLabel']}?", {:default => false}) do
265
- if addn_value = typeahead_prompt(option_type, api_client, (option_type['noParams'] ? {} : (api_params || {}).merge(results)), options[:no_prompt], nil, paging_enabled)
273
+ if addn_value = typeahead_prompt(option_type, api_client, option_params, options[:no_prompt], nil, paging_enabled)
266
274
  value << addn_value
267
275
  else
268
276
  break
@@ -273,7 +281,7 @@ module Morpheus
273
281
  if option_type['optionSource'].nil?
274
282
  value = option_type['defaultValue']
275
283
  else
276
- value = load_source_options(option_type['optionSource'], option_type['optionSourceType'], api_client, api_params || {})
284
+ value = load_source_options(option_type['optionSource'], option_params, api_client, api_params || {})
277
285
  end
278
286
  elsif option_type['type'] == 'file'
279
287
  value = file_prompt(option_type)
@@ -287,6 +295,11 @@ module Morpheus
287
295
  if option_type['type'] == 'multiSelect'
288
296
  value = [value] if !value.nil? && !value.is_a?(Array)
289
297
  # parent_context_map[parent_ns] = value
298
+ elsif option_type['type'] == 'multiText'
299
+ # multiText expects csv value
300
+ if value && value.is_a?(String)
301
+ value = value.split(",").collect {|it| it.strip }
302
+ end
290
303
  end
291
304
  context_map[field_name] = value if !(value.nil? || (value.is_a?(Hash) && value.empty?))
292
305
  parent_context_map.reject! {|k,v| k == parent_ns && (v.nil? || (v.is_a?(Hash) && v.empty?))}
@@ -305,7 +318,7 @@ module Morpheus
305
318
  end
306
319
  optionString = options.collect{ |b| b[:checked] ? "(#{b[:key]})" : b[:key]}.join(', ')
307
320
  while !value_found do
308
- print "#{option_type['fieldLabel']}#{option_type['fieldAddOn'] ? ('(' + option_type['fieldAddOn'] + ') ') : '' }[#{optionString}]: "
321
+ print "#{option_type['fieldLabel']}#{option_type['fieldAddOn'] ? (' (' + option_type['fieldAddOn'] + ') ') : '' }[#{optionString}]: "
309
322
  input = $stdin.gets.chomp!
310
323
  if input == '?'
311
324
  help_prompt(option_type)
@@ -332,7 +345,7 @@ module Morpheus
332
345
  value_found = false
333
346
  value = nil
334
347
  while !value_found do
335
- print "#{option_type['fieldLabel']}#{option_type['fieldAddOn'] ? ('(' + option_type['fieldAddOn'] + ') ') : '' }#{!option_type['required'] ? ' (optional)' : ''}#{!option_type['defaultValue'].to_s.empty? ? ' ['+option_type['defaultValue'].to_s+']' : ''}: "
348
+ print "#{option_type['fieldLabel']}#{option_type['fieldAddOn'] ? (' (' + option_type['fieldAddOn'] + ') ') : '' }#{!option_type['required'] ? ' (optional)' : ''}#{!option_type['defaultValue'].to_s.empty? ? ' ['+option_type['defaultValue'].to_s+']' : ''}: "
336
349
  input = $stdin.gets.chomp!
337
350
  value = input.empty? ? option_type['defaultValue'] : input
338
351
  if !value.to_s.empty?
@@ -356,7 +369,7 @@ module Morpheus
356
369
  Thread.current[:_last_select]
357
370
  end
358
371
 
359
- def self.select_prompt(option_type,api_client, api_params={}, no_prompt=false, use_value=nil, paging_enabled=false)
372
+ def self.select_prompt(option_type, api_client, api_params={}, no_prompt=false, use_value=nil, paging_enabled=false)
360
373
  paging_enabled = false if Morpheus::Cli.windows?
361
374
  field_key = [option_type['fieldContext'], option_type['fieldName']].select {|it| it && it != '' }.join('.')
362
375
  help_field_key = option_type[:help_field_prefix] ? "#{option_type[:help_field_prefix]}.#{field_key}" : field_key
@@ -365,7 +378,12 @@ module Morpheus
365
378
  value_field = (option_type['config'] ? option_type['config']['valueField'] : nil) || 'value'
366
379
  default_value = option_type['defaultValue']
367
380
  default_value = default_value['id'] if default_value && default_value.is_a?(Hash) && !default_value['id'].nil?
368
- api_params ||= {}
381
+
382
+ if !option_type['params'].nil?
383
+ api_params = (api_params || {}).select {|k,v| option_type['params'].key?(k) || option_type['params'].key?(k.to_s)}
384
+ option_type['params'].select {|k,v| !v.empty?}.each {|k,v| api_params[k] = v}
385
+ end
386
+
369
387
  # local array of options
370
388
  if option_type['selectOptions']
371
389
  # calculate from inline lambda
@@ -424,7 +442,8 @@ module Morpheus
424
442
  default_value = found_default_option['name'] # name is prettier than value
425
443
  end
426
444
  else
427
- found_default_option = select_options.find {|opt| opt[value_field].to_s == default_value.to_s}
445
+ found_default_option = select_options.find {|opt| opt[value_field].to_s == default_value.to_s || opt['name'] == default_value.to_s}
446
+ found_default_option = select_options.find {|opt| opt[value_field].to_s.start_with?(default_value.to_s) || opt['name'].to_s.start_with?(default_value.to_s)} if !found_default_option
428
447
  if found_default_option
429
448
  default_value = found_default_option['name'] # name is prettier than value
430
449
  end
@@ -483,7 +502,7 @@ module Morpheus
483
502
  }
484
503
 
485
504
  has_more_pages = paging && (paging[:cur_page] * paging[:page_size]) < paging[:total]
486
- 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#{has_more_pages && paging[:cur_page] > 0 ? ' more ' : ' '}options]: ", false).to_s
505
+ 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#{has_more_pages && paging[:cur_page] > 0 ? ' more ' : ' '}options]: ", false).to_s
487
506
  input = input.chomp.strip
488
507
  if input.empty? && default_value
489
508
  input = default_value.to_s
@@ -554,7 +573,7 @@ module Morpheus
554
573
  }
555
574
  # prompt for typeahead input value
556
575
  has_more_pages = paging && ((paging[:cur_page] + 1) * paging[:page_size]) < paging[:total]
557
- 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#{has_more_pages ? ' more ' : ' '}options]: ", false).to_s
576
+ 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#{has_more_pages ? ' more ' : ' '}options]: ", false).to_s
558
577
  input = input.chomp.strip
559
578
  end
560
579
 
@@ -743,7 +762,7 @@ module Morpheus
743
762
  value_found = false
744
763
  value = nil
745
764
  while !value_found do
746
- print "#{option_type['fieldLabel']}#{option_type['fieldAddOn'] ? ('(' + option_type['fieldAddOn'] + ') ') : '' }#{!option_type['required'] ? ' (optional)' : ''}#{!option_type['defaultValue'].to_s.empty? ? ' ['+option_type['defaultValue'].to_s+']' : ''}: "
765
+ print "#{option_type['fieldLabel']}#{option_type['fieldAddOn'] ? (' (' + option_type['fieldAddOn'] + ') ') : '' }#{!option_type['required'] ? ' (optional)' : ''}#{!option_type['defaultValue'].to_s.empty? ? ' ['+option_type['defaultValue'].to_s+']' : ''}: "
747
766
  input = $stdin.gets.chomp!
748
767
  value = input.empty? ? option_type['defaultValue'] : input
749
768
  if input == '?'
@@ -760,7 +779,7 @@ module Morpheus
760
779
  value = nil
761
780
  while !value_found do
762
781
  if value.nil?
763
- print "#{option_type['fieldLabel']}#{option_type['fieldAddOn'] ? ('(' + option_type['fieldAddOn'] + ') ') : '' }#{optional_label(option_type)} [Type 'EOF' to stop input]: \n"
782
+ print "#{option_type['fieldLabel']}#{option_type['fieldAddOn'] ? (' (' + option_type['fieldAddOn'] + ') ') : '' }#{optional_label(option_type)} [Type 'EOF' to stop input]: \n"
764
783
  end
765
784
  input = $stdin.gets.chomp!
766
785
  # value = input.empty? ? option_type['defaultValue'] : input
@@ -784,7 +803,7 @@ module Morpheus
784
803
  def self.password_prompt(option_type)
785
804
  value_found = false
786
805
  while !value_found do
787
- print "#{option_type['fieldLabel']}#{option_type['fieldAddOn'] ? ('(' + option_type['fieldAddOn'] + ') ') : '' }#{optional_label(option_type)}#{option_type['defaultValue'] ? ' ['+option_type['defaultValue'].to_s+']' : ''}: "
806
+ print "#{option_type['fieldLabel']}#{option_type['fieldAddOn'] ? (' (' + option_type['fieldAddOn'] + ') ') : '' }#{optional_label(option_type)}#{option_type['defaultValue'] ? ' ['+option_type['defaultValue'].to_s+']' : ''}: "
788
807
  input = $stdin.noecho(&:gets).chomp!
789
808
  value = input
790
809
  print "\n"
@@ -804,11 +823,11 @@ module Morpheus
804
823
  value_found = false
805
824
  value = nil
806
825
  while !value_found do
807
- #print "#{option_type['fieldLabel']}#{option_type['fieldAddOn'] ? ('(' + option_type['fieldAddOn'] + ') ') : '' }#{optional_label(option_type)}#{option_type['defaultValue'] ? ' ['+option_type['defaultValue'].to_s+']' : ''}: "
826
+ #print "#{option_type['fieldLabel']}#{option_type['fieldAddOn'] ? (' (' + option_type['fieldAddOn'] + ') ') : '' }#{optional_label(option_type)}#{option_type['defaultValue'] ? ' ['+option_type['defaultValue'].to_s+']' : ''}: "
808
827
  Readline.completion_append_character = ""
809
828
  Readline.basic_word_break_characters = ''
810
829
  Readline.completion_proc = proc {|s| Readline::FILENAME_COMPLETION_PROC.call(s) }
811
- input = Readline.readline("#{option_type['fieldLabel']}#{option_type['fieldAddOn'] ? ('(' + option_type['fieldAddOn'] + ') ') : '' }#{optional_label(option_type)}#{option_type['defaultValue'] ? ' ['+option_type['defaultValue'].to_s+']' : ''}: ", false).to_s
830
+ input = Readline.readline("#{option_type['fieldLabel']}#{option_type['fieldAddOn'] ? (' (' + option_type['fieldAddOn'] + ') ') : '' }#{optional_label(option_type)}#{option_type['defaultValue'] ? ' ['+option_type['defaultValue'].to_s+']' : ''}: ", false).to_s
812
831
  input = input.chomp.strip
813
832
  #input = $stdin.gets.chomp!
814
833
  value = input.empty? ? option_type['defaultValue'] : input.to_s
@@ -926,6 +945,8 @@ module Morpheus
926
945
  select_options = filtered_options
927
946
  end
928
947
  elsif option_type['optionSource']
948
+ api_params = api_params.select {|k,v| option_type['params'].include(k)} if option_type['params'].nil? && api_params
949
+
929
950
  # calculate from inline lambda
930
951
  if option_type['optionSource'].is_a?(Proc)
931
952
  select_options = option_type['optionSource'].call(api_client, api_params || {})
@@ -1,6 +1,6 @@
1
1
 
2
2
  module Morpheus
3
3
  module Cli
4
- VERSION = "5.4.0"
4
+ VERSION = "5.4.1"
5
5
  end
6
6
  end
data/lib/morpheus/cli.rb CHANGED
@@ -4,6 +4,7 @@ require 'morpheus/rest_client'
4
4
  require 'morpheus/formatters'
5
5
  require 'morpheus/logging'
6
6
  require 'morpheus/util'
7
+ require 'morpheus/routes'
7
8
  require 'term/ansicolor'
8
9
 
9
10
  Dir[File.dirname(__FILE__) + "/ext/*.rb"].each {|file| require file }
@@ -1,7 +1,7 @@
1
1
  class String
2
2
 
3
3
  def pluralize
4
- value = self
4
+ value = self.dup
5
5
  if value == ""
6
6
  value
7
7
  elsif value[-1].chr == "y"
@@ -18,7 +18,7 @@ class String
18
18
  end
19
19
 
20
20
  def singularize
21
- value = self
21
+ value = self.dup
22
22
  if value == ""
23
23
  value
24
24
  elsif value.size > 3 && value[-3..-1] == "ies"
@@ -27,15 +27,38 @@ class String
27
27
  value[0..-3]
28
28
  elsif value[-1] == "s"
29
29
  value[0..-2]
30
+ else
31
+ value
30
32
  end
31
33
  end
32
34
 
33
- def dasherize
34
- self.gsub(" ", "-").gsub("_", "-")
35
+ def underscore
36
+ value = self.dup
37
+ value.gsub!(/::/, '/')
38
+ value.gsub!(/([A-Z]+)([A-Z][a-z])/,'\1_\2')
39
+ value.gsub!(/([a-z\d])([A-Z])/,'\1_\2')
40
+ value.tr!("-", "_")
41
+ value.tr!(" ", "_")
42
+ value.downcase!
43
+ value
44
+ end
45
+
46
+ def camelcase
47
+ value = self.underscore.gsub(/\_([a-z])/) do $1.upcase end
48
+ value = value[0, 1].downcase + value[1..-1]
49
+ value
35
50
  end
36
51
 
37
- def underscoreize
38
- self.gsub(" ", "_").gsub("-", "_")
52
+ def upcamelcase
53
+ self.camelcase.capitalize
54
+ end
55
+
56
+ def titleize
57
+ self.underscore.split("_").map(&:capitalize).join(" ")
58
+ end
59
+
60
+ def dasherize
61
+ self.underscore.gsub("_", "-")
39
62
  end
40
63
 
41
64
  end
@@ -0,0 +1,238 @@
1
+ # Routes is a module to provide a way to look up routes in the Morpheus UI.
2
+ # Examples:
3
+ # Routes.lookup("users") == "/admin/users"
4
+ # Routes.lookup("instances") == "/provisioning/instances"
5
+ #
6
+ # puts "All routes", Morpheus::Routes.routes
7
+ #
8
+ module Morpheus::Routes
9
+
10
+ # A static site map for the Morpheus UI.
11
+ # todo: move to YAML or load from api
12
+ SITE_MAP = {
13
+ operations: {
14
+ dashboard: {},
15
+ reports: {},
16
+ analytics: {},
17
+ guidance: {},
18
+ wiki: {},
19
+ costing: {
20
+ budgets: {},
21
+ invoices: {},
22
+ usage: {},
23
+ },
24
+ approvals: {},
25
+ activity: {},
26
+ alarms: {},
27
+ },
28
+ provisioning: {
29
+ instances: {},
30
+ apps: {},
31
+ catalog: {},
32
+ jobs: {},
33
+ executions: {},
34
+ code: {
35
+ respositories: {},
36
+ deployments: {},
37
+ # integrations: {}, # Integrations
38
+ },
39
+ },
40
+ library: {
41
+ automation: [
42
+ "#!tasks",
43
+ "#!workflows",
44
+ "#!thresholds",
45
+ "#!power-schedules",
46
+ "#!execute-schedules",
47
+ ],
48
+ blueprints: [
49
+ "#instance-types",
50
+ "#!instance-type-layouts",
51
+ "#!container-types",
52
+ "#!app-templates", # App Blueprints (blueprints)
53
+ "#!catalog-items",
54
+ "#!compute-type-layouts", # Cluster Layouts
55
+ ],
56
+ :'virtual-images' => {},
57
+ options: [
58
+ "#!option-type-lists", # Option Lists
59
+ ],
60
+ templates: [
61
+ "#!specs",
62
+ "#!files",
63
+ "#!scripts",
64
+ "#!security-specs",
65
+ ],
66
+ services: {}, # Integrations
67
+
68
+ },
69
+ infrastructure: {
70
+ groups: {},
71
+ clouds: {},
72
+ clusters: {},
73
+ servers: {}, # Hosts (still used for loading by id)
74
+ inventory: [ # Compute
75
+ "#!hosts",
76
+ "#!virtual-machines",
77
+ "#!containers",
78
+ "#!resources",
79
+ "#!bare-metal",
80
+ ],
81
+ networks: {},
82
+ :'load-balancers' => {},
83
+ storage: {
84
+ buckets: {},
85
+ shares: {}, # File Shares
86
+ volumes: {},
87
+ :'data-stores' => {}, # ugh, should be datastores
88
+ servers: {}, # Storage Servers
89
+ },
90
+ # :'keys-and-certs' => {},
91
+ :'key-pairs' => {},
92
+ certificates: {},
93
+ },
94
+ backups: {
95
+
96
+ },
97
+ monitoring: {
98
+ status: {},
99
+ logs: {},
100
+ apps: {},
101
+ checks: {},
102
+ groups: {},
103
+ incidents: {},
104
+ contacts: {},
105
+ :'alert-rules' => {},
106
+ },
107
+ tools: {
108
+ cypher: {},
109
+ archives: {
110
+ buckets: {},
111
+ },
112
+ :'image-builder' => {},
113
+ :vdi => {}
114
+ },
115
+ admin: {
116
+ accounts: {}, # Tenants
117
+ :'service-plans' => [
118
+ "#prices",
119
+ "#pricesets"
120
+ ],
121
+ roles: {},
122
+ users: {},
123
+ :'user-groups' => {},
124
+ integrations: {},
125
+ policies: {},
126
+ health: ["logs"],
127
+ settings: {},
128
+ },
129
+ :'user-settings' => {}, # User Settings (Profile)
130
+ } unless defined?(SITE_MAP)
131
+
132
+ # A list of routes generated from the site map and cached
133
+ @@routes = nil
134
+
135
+ # @return an array of well known Morpheus UI routes.
136
+ def self.routes
137
+ if !@@routes
138
+ @@routes = build_routes(SITE_MAP)
139
+ end
140
+ return @@routes
141
+ end
142
+
143
+ # lookup a route in the morpheus UI
144
+ # @param path [String] The input to lookup a route for eg. "dashboard"
145
+ # @return full path like "/operations/dashboard"
146
+ def self.lookup(input)
147
+ path = input.to_s
148
+ if path.start_with?("/")
149
+ # absolute path is being looked up
150
+ return path
151
+ else
152
+ # todo: make this smarter, regex, case insensitive, etc
153
+ # find the one with smallest match index
154
+
155
+ # map well known aliases
156
+ case(path.underscore.singularize)
157
+ when "server","host","vm","virtual-machine"
158
+ # actually should be "/infrastructure/inventory" unless id is passed, show route uses /servers though
159
+ path = "/infrastructure/servers"
160
+ when "compute"
161
+ path = "/infrastructure/inventory"
162
+ when "tenant"
163
+ path = "/admin/accounts"
164
+ end
165
+ # dasherize path and attempt to match the plural first
166
+ plural_path = path.pluralize
167
+ paths = [path.dasherize]
168
+ if plural_path != path
169
+ paths.unshift(plural_path.dasherize)
170
+ end
171
+
172
+ best_route = nil
173
+ best_index = nil
174
+ best_prefix_words = nil
175
+ paths.each do |p|
176
+ if best_route.nil?
177
+ self.routes.each do |it|
178
+ match_index = it.index(p)
179
+ if match_index
180
+ prefix_route = match_index == 0 ? "" : it[0..(match_index-1)]
181
+ prefix_words = prefix_route.split("/")
182
+ #if best_index.nil? || match_index < best_index
183
+ if best_prefix_words.nil? || prefix_words.size < best_prefix_words.size
184
+ best_route = it
185
+ best_index = match_index
186
+ best_prefix_words = prefix_words
187
+ end
188
+ end
189
+ end
190
+ end
191
+ end
192
+ if best_route
193
+ return best_route
194
+ else
195
+ # no match found
196
+ return nil
197
+ end
198
+ end
199
+
200
+ end
201
+
202
+ protected
203
+
204
+ # build_routes constructs an array of routes (paths)
205
+ # This traversing the route map recursively and appends paths to output
206
+ # @params route_map [Hash] map of routes
207
+ # @params context [String] the current content
208
+ # @params output [Array] the list of route paths being constructed for return
209
+ # @return array like ["/operations", "/operations/dashboard", "/admin", "/etc"]
210
+ def self.build_routes(route_map, context="", output = nil)
211
+ if output.nil?
212
+ output = []
213
+ end
214
+ route_map.each do |k,v|
215
+ leaf_path = "#{context}/#{k}"
216
+ output << leaf_path
217
+ if v.is_a?(Hash)
218
+ build_routes(v, leaf_path, output)
219
+ elsif v.is_a?(Array)
220
+ v.each do |obj|
221
+ if obj.is_a?(Hash)
222
+ build_routes(obj, leaf_path, output)
223
+ elsif obj.is_a?(Symbol) || obj.is_a?(String)
224
+ # route leaf type not handled
225
+ output << "#{leaf_path}/#{obj}"
226
+ else
227
+ # route leaf type not handled
228
+ end
229
+ end
230
+ else
231
+ # route leaf type not handled
232
+ end
233
+ end
234
+ return output
235
+ end
236
+
237
+
238
+ end