morpheus-cli 5.4.0 → 5.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (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