morpheus-cli 4.1.8 → 4.1.9

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 (72) hide show
  1. checksums.yaml +4 -4
  2. data/Dockerfile +1 -1
  3. data/lib/morpheus/api/api_client.rb +24 -0
  4. data/lib/morpheus/api/{old_cypher_interface.rb → budgets_interface.rb} +10 -11
  5. data/lib/morpheus/api/cloud_datastores_interface.rb +7 -0
  6. data/lib/morpheus/api/cloud_resource_pools_interface.rb +2 -2
  7. data/lib/morpheus/api/cypher_interface.rb +18 -12
  8. data/lib/morpheus/api/health_interface.rb +72 -0
  9. data/lib/morpheus/api/instances_interface.rb +1 -1
  10. data/lib/morpheus/api/library_instance_types_interface.rb +7 -0
  11. data/lib/morpheus/api/log_settings_interface.rb +6 -0
  12. data/lib/morpheus/api/network_security_servers_interface.rb +30 -0
  13. data/lib/morpheus/api/price_sets_interface.rb +42 -0
  14. data/lib/morpheus/api/prices_interface.rb +68 -0
  15. data/lib/morpheus/api/provisioning_settings_interface.rb +29 -0
  16. data/lib/morpheus/api/servers_interface.rb +1 -1
  17. data/lib/morpheus/api/service_plans_interface.rb +34 -11
  18. data/lib/morpheus/api/task_sets_interface.rb +8 -0
  19. data/lib/morpheus/api/tasks_interface.rb +8 -0
  20. data/lib/morpheus/cli.rb +6 -3
  21. data/lib/morpheus/cli/appliance_settings_command.rb +13 -5
  22. data/lib/morpheus/cli/approvals_command.rb +1 -1
  23. data/lib/morpheus/cli/apps.rb +88 -28
  24. data/lib/morpheus/cli/backup_settings_command.rb +1 -1
  25. data/lib/morpheus/cli/blueprints_command.rb +2 -0
  26. data/lib/morpheus/cli/budgets_command.rb +672 -0
  27. data/lib/morpheus/cli/cli_command.rb +13 -2
  28. data/lib/morpheus/cli/cli_registry.rb +1 -0
  29. data/lib/morpheus/cli/clusters.rb +40 -274
  30. data/lib/morpheus/cli/commands/standard/benchmark_command.rb +114 -66
  31. data/lib/morpheus/cli/commands/standard/coloring_command.rb +12 -0
  32. data/lib/morpheus/cli/commands/standard/curl_command.rb +31 -6
  33. data/lib/morpheus/cli/commands/standard/echo_command.rb +8 -3
  34. data/lib/morpheus/cli/commands/standard/set_prompt_command.rb +1 -1
  35. data/lib/morpheus/cli/containers_command.rb +37 -24
  36. data/lib/morpheus/cli/cypher_command.rb +191 -150
  37. data/lib/morpheus/cli/health_command.rb +903 -0
  38. data/lib/morpheus/cli/hosts.rb +43 -32
  39. data/lib/morpheus/cli/instances.rb +119 -68
  40. data/lib/morpheus/cli/jobs_command.rb +1 -1
  41. data/lib/morpheus/cli/library_instance_types_command.rb +61 -11
  42. data/lib/morpheus/cli/library_option_types_command.rb +2 -2
  43. data/lib/morpheus/cli/log_settings_command.rb +46 -3
  44. data/lib/morpheus/cli/logs_command.rb +24 -17
  45. data/lib/morpheus/cli/mixins/accounts_helper.rb +2 -0
  46. data/lib/morpheus/cli/mixins/logs_helper.rb +73 -19
  47. data/lib/morpheus/cli/mixins/print_helper.rb +29 -1
  48. data/lib/morpheus/cli/mixins/provisioning_helper.rb +554 -96
  49. data/lib/morpheus/cli/mixins/whoami_helper.rb +13 -1
  50. data/lib/morpheus/cli/networks_command.rb +3 -0
  51. data/lib/morpheus/cli/option_types.rb +83 -53
  52. data/lib/morpheus/cli/price_sets_command.rb +543 -0
  53. data/lib/morpheus/cli/prices_command.rb +669 -0
  54. data/lib/morpheus/cli/processes_command.rb +0 -2
  55. data/lib/morpheus/cli/provisioning_settings_command.rb +237 -0
  56. data/lib/morpheus/cli/remote.rb +9 -4
  57. data/lib/morpheus/cli/reports_command.rb +10 -4
  58. data/lib/morpheus/cli/roles.rb +93 -38
  59. data/lib/morpheus/cli/security_groups.rb +10 -0
  60. data/lib/morpheus/cli/service_plans_command.rb +736 -0
  61. data/lib/morpheus/cli/tasks.rb +220 -8
  62. data/lib/morpheus/cli/tenants_command.rb +3 -16
  63. data/lib/morpheus/cli/users.rb +2 -25
  64. data/lib/morpheus/cli/version.rb +1 -1
  65. data/lib/morpheus/cli/whitelabel_settings_command.rb +18 -18
  66. data/lib/morpheus/cli/whoami.rb +28 -10
  67. data/lib/morpheus/cli/workflows.rb +488 -36
  68. data/lib/morpheus/formatters.rb +22 -0
  69. data/morpheus-cli.gemspec +1 -0
  70. metadata +28 -5
  71. data/lib/morpheus/cli/accounts.rb +0 -335
  72. data/lib/morpheus/cli/old_cypher_command.rb +0 -412
@@ -19,7 +19,6 @@ module Morpheus::Cli::WhoamiHelper
19
19
  exit 1
20
20
  end
21
21
  @is_master_account = whoami_response["isMasterAccount"]
22
- @user_permissions = whoami_response["permissions"]
23
22
 
24
23
  if whoami_response["appliance"]
25
24
  @appliance_build_verison = whoami_response["appliance"]["buildVersion"]
@@ -30,4 +29,17 @@ module Morpheus::Cli::WhoamiHelper
30
29
  return whoami_response
31
30
  end
32
31
 
32
+ def current_account
33
+ if @current_user.nil?
34
+ load_whoami
35
+ end
36
+ @current_user ? @current_user['account'] : nil
37
+ end
38
+
39
+ def is_master_account
40
+ if @current_user.nil?
41
+ load_whoami
42
+ end
43
+ @is_master_account
44
+ end
33
45
  end
@@ -457,6 +457,9 @@ class Morpheus::Cli::NetworksCommand
457
457
  #option_type['fieldContext'] = nil
458
458
  end
459
459
  network_type_params = Morpheus::Cli::OptionTypes.prompt(network_type_option_types,options[:options],@api_client, {zoneId: cloud['id']})
460
+ # network context options belong at network level and not network.network
461
+ network_context_params = network_type_params.delete('network')
462
+ payload['network'].deep_merge!(network_context_params) if network_context_params
460
463
  payload['network'].deep_merge!(network_type_params)
461
464
 
462
465
  #todo: special handling of type: 'aciVxlan'
@@ -36,6 +36,7 @@ module Morpheus
36
36
  end
37
37
 
38
38
  def self.prompt(option_types, options={}, api_client=nil, api_params={}, no_prompt=false, paging_enabled=false)
39
+ paging_enabled = false if Morpheus::Cli.windows?
39
40
  results = {}
40
41
  options = options || {}
41
42
  # puts "Options Prompt #{options}"
@@ -59,40 +60,45 @@ module Morpheus
59
60
  namespaces = field_key.split(".")
60
61
  field_name = namespaces.pop
61
62
 
62
- if field_key.include?(".")
63
- cur_namespace = options
64
-
65
- namespaces.each do |ns|
66
- next if ns.empty?
67
- cur_namespace[ns.to_s] ||= {}
68
- cur_namespace = cur_namespace[ns.to_s]
69
- context_map[ns.to_s] ||= {}
70
- context_map = context_map[ns.to_s]
71
- end
72
- # use the value passed in the options map
73
- if cur_namespace.key?(field_name)
74
- value = cur_namespace[field_name]
75
- if option_type['type'] == 'number'
76
- value = value.to_s.include?('.') ? value.to_f : value.to_i
77
- elsif option_type['type'] == 'select'
78
- # this should just fall down through below, with the extra params no_prompt, use_value
79
- value = select_prompt(option_type, api_client, (api_params || {}).merge(results), true, value)
80
- end
81
- if options[:always_prompt] != true
82
- value_found = true
63
+ # respect optionType.dependsOnCode
64
+ if option_type['dependsOnCode'] && option_type['dependsOnCode'] != ""
65
+ # optionTypes can have this setting in the format code=value or code:value
66
+ parts = option_type['dependsOnCode'].include?("=") ? option_type['dependsOnCode'].split("=") : option_type['dependsOnCode'].split(":")
67
+ depends_on_code = parts[0]
68
+ depends_on_value = parts[1]
69
+ depends_on_option_type = option_types.find {|it| it["code"] == depends_on_code }
70
+ # could not find the dependent option type, proceed and prompt
71
+ if !depends_on_option_type.nil?
72
+ # dependent option type has a different value
73
+ depends_on_field_key = depends_on_option_type['fieldContext'] ? "#{depends_on_option_type['fieldContext']}.#{depends_on_option_type['fieldName']}" : "#{depends_on_option_type['fieldName']}"
74
+ found_dep_value = get_object_value(results, depends_on_field_key) || get_object_value(options, depends_on_field_key)
75
+ if depends_on_value && depends_on_value != found_dep_value
76
+ next
83
77
  end
84
78
  end
85
- else
86
- # no fieldContext
87
- if value_found == false && options.key?(field_key)
88
- value = options[field_key]
89
- if option_type['type'] == 'number'
90
- value = value.to_s.include?('.') ? value.to_f : value.to_i
91
- end
92
- # still prompt
93
- if options[:always_prompt] != true
94
- value_found = true
95
- end
79
+ end
80
+
81
+
82
+ cur_namespace = options
83
+
84
+ namespaces.each do |ns|
85
+ next if ns.empty?
86
+ cur_namespace[ns.to_s] ||= {}
87
+ cur_namespace = cur_namespace[ns.to_s]
88
+ context_map[ns.to_s] ||= {}
89
+ context_map = context_map[ns.to_s]
90
+ end
91
+ # use the value passed in the options map
92
+ if cur_namespace.key?(field_name)
93
+ value = cur_namespace[field_name]
94
+ if option_type['type'] == 'number'
95
+ value = value.to_s.include?('.') ? value.to_f : value.to_i
96
+ elsif option_type['type'] == 'select'
97
+ # this should just fall down through below, with the extra params no_prompt, use_value
98
+ value = select_prompt(option_type.merge({'defaultValue' => value}), api_client, (api_params || {}).merge(results), true)
99
+ end
100
+ if options[:always_prompt] != true
101
+ value_found = true
96
102
  end
97
103
  end
98
104
 
@@ -101,14 +107,12 @@ module Morpheus
101
107
  option_type = option_type.clone
102
108
  option_type['defaultValue'] = value
103
109
  end
104
-
105
-
106
110
  # no_prompt means skip prompting and instead
107
111
  # use default value or error if a required option is not present
108
112
  no_prompt = no_prompt || options[:no_prompt]
109
113
  if no_prompt
110
114
  if !value_found
111
- if option_type['defaultValue'] != nil
115
+ if option_type['defaultValue'] != nil && option_type['type'] != 'select'
112
116
  value = option_type['defaultValue']
113
117
  value_found = true
114
118
  end
@@ -116,7 +120,7 @@ module Morpheus
116
120
  # select type is special because it supports skipSingleOption
117
121
  # and prints the available options on error
118
122
  if option_type['type'] == 'select'
119
- value = select_prompt(option_type, api_client, (api_params || {}).merge(results), true)
123
+ value = select_prompt(option_type.merge({'defaultValue' => value}), api_client, (api_params || {}).merge(results), true)
120
124
  value_found = !!value
121
125
  end
122
126
  if !value_found
@@ -246,28 +250,40 @@ module Morpheus
246
250
  end
247
251
 
248
252
  def self.select_prompt(option_type,api_client, api_params={}, no_prompt=false, use_value=nil, paging_enabled=false)
253
+ paging_enabled = false if Morpheus::Cli.windows?
249
254
  value_found = false
250
255
  value = nil
256
+ value_field = (option_type['config'] ? option_type['config']['valueField'] : nil) || 'value'
251
257
  default_value = option_type['defaultValue']
252
258
  # local array of options
253
259
  if option_type['selectOptions']
254
- select_options = option_type['selectOptions']
255
- # remote optionSource aka /api/options/$optionSource?
260
+ # calculate from inline lambda
261
+ if option_type['selectOptions'].is_a?(Proc)
262
+ select_options = option_type['selectOptions'].call()
263
+ else
264
+ # todo: better type validation
265
+ select_options = option_type['selectOptions']
266
+ end
256
267
  elsif option_type['optionSource']
257
- # /api/options/list is a special action for custom OptionTypeLists, just need to pass the optionTypeId parameter
258
- if option_type['optionSource'] == 'list'
268
+ # calculate from inline lambda
269
+ if option_type['optionSource'].is_a?(Proc)
270
+ select_options = option_type['optionSource'].call()
271
+ elsif option_type['optionSource'] == 'list'
272
+ # /api/options/list is a special action for custom OptionTypeLists, just need to pass the optionTypeId parameter
259
273
  select_options = load_source_options(option_type['optionSource'], api_client, {'optionTypeId' => option_type['id']})
260
274
  else
275
+ # remote optionSource aka /api/options/$optionSource?
261
276
  select_options = load_source_options(option_type['optionSource'], api_client, grails_params(api_params || {}))
262
- end
277
+ end
263
278
  else
264
279
  raise "select_prompt() requires selectOptions or optionSource!"
265
280
  end
281
+
266
282
  # ensure the preselected value (passed as an option) is in the dropdown
267
283
  if !use_value.nil?
268
- matched_value = select_options.find {|opt| opt['value'].to_s == use_value.to_s }
269
- if !matched_value.nil?
270
- value = use_value
284
+ matched_option = select_options.find {|opt| opt[value_field].to_s == use_value.to_s || opt['name'].to_s == use_value.to_s }
285
+ if !matched_option.nil?
286
+ value = matched_option[value_field]
271
287
  value_found = true
272
288
  else
273
289
  print Term::ANSIColor.red, "\nInvalid Option #{option_type['fieldLabel']}: [#{use_value}]\n\n", Term::ANSIColor.reset
@@ -296,14 +312,22 @@ module Morpheus
296
312
  if found_default_option
297
313
  default_value = found_default_option['name'] # name is prettier than value
298
314
  end
315
+ else
316
+ found_default_option = select_options.find {|opt| opt[value_field].to_s == default_value.to_s}
317
+ if found_default_option
318
+ default_value = found_default_option['name'] # name is prettier than value
319
+ end
299
320
  end
300
321
  end
301
322
 
302
323
  if no_prompt
303
324
  if !value_found
304
- if !select_options.nil? && select_options.count > 1 && option_type['autoPickOption'] == true
325
+ if !default_value.nil? && !select_options.nil? && select_options.find {|it| it['name'] == default_value || it[value_field].to_s == default_value.to_s}
305
326
  value_found = true
306
- value = select_options[0]['value']
327
+ value = select_options.find {|it| it['name'] == default_value || it[value_field].to_s == default_value.to_s}[value_field]
328
+ elsif !select_options.nil? && select_options.count > 1 && option_type['autoPickOption'] == true
329
+ value_found = true
330
+ value = select_options[0][value_field]
307
331
  elsif option_type['required']
308
332
  print Term::ANSIColor.red, "\nMissing Required Option\n\n", Term::ANSIColor.reset
309
333
  print Term::ANSIColor.red, " * #{option_type['fieldLabel']} [-O #{option_type['fieldContext'] ? (option_type['fieldContext']+'.') : ''}#{option_type['fieldName']}=] - #{option_type['description']}\n", Term::ANSIColor.reset
@@ -321,9 +345,13 @@ module Morpheus
321
345
  end
322
346
  end
323
347
 
324
- page_size = Readline.get_screen_size[0] - 6
325
- if paging_enabled && page_size < select_options.count
326
- paging = {:cur_page => 0, :page_size => page_size, :total => select_options.count}
348
+ paging = nil
349
+ if paging_enabled
350
+ option_count = select_options ? select_options.count : 0
351
+ page_size = Readline.get_screen_size[0] - 6
352
+ if page_size < option_count
353
+ paging = {:cur_page => 0, :page_size => page_size, :total => option_count}
354
+ end
327
355
  end
328
356
 
329
357
  while !value_found do
@@ -336,7 +364,7 @@ module Morpheus
336
364
  if option['name'] && option['name'] =~ /^#{Regexp.escape(s)}/
337
365
  matches << option['name']
338
366
  # elsif option['id'] && option['id'].to_s =~ /^#{Regexp.escape(s)}/
339
- elsif option['value'] && option['value'].to_s == s
367
+ elsif option[value_field] && option[value_field].to_s == s
340
368
  matches << option['name']
341
369
  end
342
370
  }
@@ -349,9 +377,9 @@ module Morpheus
349
377
  if input.empty? && default_value
350
378
  input = default_value.to_s
351
379
  end
352
- select_option = select_options.find{|b| b['name'] == input || (!b['value'].nil? && b['value'].to_s == input) || (b['value'].nil? && input.empty?)}
380
+ select_option = select_options.find{|b| b['name'] == input || (!b['value'].nil? && b['value'].to_s == input) || (!b[value_field].nil? && b[value_field].to_s == input) || (b[value_field].nil? && input.empty?)}
353
381
  if select_option
354
- value = select_option['value']
382
+ value = select_option[value_field]
355
383
  set_last_select(select_option)
356
384
  elsif !input.nil? && !input.to_s.empty?
357
385
  input = '?'
@@ -360,7 +388,9 @@ module Morpheus
360
388
  if input == '?'
361
389
  help_prompt(option_type)
362
390
  display_select_options(option_type, select_options, paging)
363
- paging[:cur_page] = (paging[:cur_page] + 1) * paging[:page_size] < paging[:total] ? paging[:cur_page] + 1 : 0 if paging
391
+ if paging
392
+ paging[:cur_page] = (paging[:cur_page] + 1) * paging[:page_size] < paging[:total] ? paging[:cur_page] + 1 : 0
393
+ end
364
394
  elsif !value.nil? || option_type['required'] != true
365
395
  value_found = true
366
396
  end
@@ -0,0 +1,543 @@
1
+ require 'morpheus/cli/cli_command'
2
+ require 'money'
3
+
4
+ class Morpheus::Cli::PriceSetsCommand
5
+ include Morpheus::Cli::CliCommand
6
+ include Morpheus::Cli::AccountsHelper
7
+ include Morpheus::Cli::ProvisioningHelper
8
+ include Morpheus::Cli::WhoamiHelper
9
+
10
+ set_command_name :'price-sets'
11
+
12
+ register_subcommands :list, :get, :add, :update, :deactivate
13
+ set_default_subcommand :list
14
+
15
+ def connect(opts)
16
+ @api_client = establish_remote_appliance_connection(opts)
17
+ @options_interface = @api_client.options
18
+ @accounts_interface = @api_client.accounts
19
+ @price_sets_interface = @api_client.price_sets
20
+ @prices_interface = @api_client.prices
21
+ @clouds_interface = @api_client.clouds
22
+ @cloud_resource_pools_interface = @api_client.cloud_resource_pools
23
+ end
24
+
25
+ def handle(args)
26
+ handle_subcommand(args)
27
+ end
28
+
29
+ def list(args)
30
+ options = {}
31
+ params = {'includeZones': true}
32
+ optparse = Morpheus::Cli::OptionParser.new do |opts|
33
+ opts.banner = subcommand_usage()
34
+ opts.on('-i', '--include-inactive [on|off]', String, "Can be used to enable / disable inactive filter. Default is on") do |val|
35
+ params['includeInactive'] = val.to_s == 'on' || val.to_s == 'true' || val.to_s == '1' || val.to_s == ''
36
+ end
37
+ build_common_options(opts, options, [:list, :query, :json, :yaml, :csv, :fields, :dry_run, :remote])
38
+ opts.footer = "List price sets."
39
+ end
40
+ optparse.parse!(args)
41
+ connect(options)
42
+ if args.count != 0
43
+ raise_command_error "wrong number of arguments, expected 0 and got (#{args.count}) #{args}\n#{optparse}"
44
+ return 1
45
+ end
46
+
47
+ begin
48
+ params.merge!(parse_list_options(options))
49
+
50
+ @price_sets_interface.setopts(options)
51
+ if options[:dry_run]
52
+ print_dry_run @price_sets_interface.dry.list(params)
53
+ return
54
+ end
55
+ json_response = @price_sets_interface.list(params)
56
+
57
+ render_result = render_with_format(json_response, options, 'priceSets')
58
+ return 0 if render_result
59
+
60
+ title = "Morpheus Price Sets"
61
+ subtitles = []
62
+ subtitles += parse_list_subtitles(options)
63
+ print_h1 title, subtitles
64
+
65
+ price_sets = json_response['priceSets']
66
+ if price_sets.empty?
67
+ print yellow,"No price sets found.",reset,"\n"
68
+ else
69
+ rows = price_sets.collect do |it|
70
+ {
71
+ id: (it['active'] ? cyan : yellow) + it['id'].to_s,
72
+ name: it['name'],
73
+ active: format_boolean(it['active']),
74
+ price_unit: it['priceUnit'],
75
+ type: price_set_type_label(it['type']),
76
+ price_count: it['prices'].count.to_s + cyan
77
+ }
78
+ end
79
+ columns = [
80
+ :id, :name, :active, :price_unit, :type, '# of prices' => :price_count
81
+ ]
82
+ columns.delete(:active) if !params['includeInactive']
83
+
84
+ print as_pretty_table(rows, columns, options)
85
+ print_results_pagination(json_response)
86
+ print reset,"\n"
87
+ end
88
+ print reset,"\n"
89
+ return 0
90
+ rescue RestClient::Exception => e
91
+ print_rest_exception(e, options)
92
+ exit 1
93
+ end
94
+ end
95
+
96
+ def get(args)
97
+ options = {}
98
+ optparse = Morpheus::Cli::OptionParser.new do |opts|
99
+ opts.banner = subcommand_usage("[price-set]")
100
+ build_common_options(opts, options, [:json, :dry_run, :remote])
101
+ opts.footer = "Get details about a price set.\n" +
102
+ "[price-set] is required. Price set ID, name or code"
103
+ end
104
+ optparse.parse!(args)
105
+ if args.count != 1
106
+ raise_command_error "wrong number of arguments, expected 1 and got (#{args.count}) #{args}\n#{optparse}"
107
+ end
108
+ connect(options)
109
+ return _get(args[0], options)
110
+ end
111
+
112
+ def _get(price_set_id, options = {})
113
+ params = {}
114
+ begin
115
+ @price_sets_interface.setopts(options)
116
+
117
+ if !(price_set_id.to_s =~ /\A\d{1,}\Z/)
118
+ price_set = find_price_set(price_set_id)
119
+
120
+ if !price_set
121
+ print_red_alert "Price set #{price_set_id} not found"
122
+ exit 1
123
+ end
124
+ price_set_id = price_set['id']
125
+ end
126
+
127
+ if options[:dry_run]
128
+ print_dry_run @price_sets_interface.dry.get(price_set_id)
129
+ return
130
+ end
131
+ json_response = @price_sets_interface.get(price_set_id)
132
+
133
+ render_result = render_with_format(json_response, options, 'priceSet')
134
+ return 0 if render_result
135
+
136
+ title = "Morpheus Price Set"
137
+ subtitles = []
138
+ subtitles += parse_list_subtitles(options)
139
+ print_h1 title, subtitles
140
+
141
+ price_set = json_response['priceSet']
142
+ print cyan
143
+ description_cols = {
144
+ "ID" => lambda {|it| it['id']},
145
+ "Name" => lambda {|it| it['name']},
146
+ "Code" => lambda {|it| it['code']},
147
+ "Region Code" => lambda {|it| it['regionCode']},
148
+ "Price Unit" => lambda {|it| (it['priceUnit'] || 'month').capitalize},
149
+ "Type" => lambda {|it| price_set_type_label(it['type'])},
150
+ "Cloud" => lambda {|it| it['zone'].nil? ? 'All' : it['zone']['name']},
151
+ "Resource Pool" => lambda {|it| it['zonePool'].nil? ? nil : it['zonePool']['name']}
152
+ }
153
+
154
+ print_description_list(description_cols, price_set)
155
+
156
+ print_h2 "Prices"
157
+ prices = price_set['prices']
158
+
159
+ if prices && !prices.empty?
160
+ rows = prices.collect do |it|
161
+ {
162
+ id: it['id'],
163
+ name: it['name'],
164
+ pricing: (it['priceType'] == 'platform' ? '+' : '') + currency_sym(it['currency']) + (it['price'] || 0.0).to_s + (it['additionalPriceUnit'].nil? ? '' : '/' + it['additionalPriceUnit']) + '/' + (it['priceUnit'] || 'month').capitalize
165
+ }
166
+ end
167
+ print as_pretty_table(rows, [:id, :name, :pricing], options)
168
+ else
169
+ print yellow,"No prices.",reset,"\n"
170
+ end
171
+ print reset,"\n"
172
+ return 0
173
+ rescue RestClient::Exception => e
174
+ print_rest_exception(e, options)
175
+ exit 1
176
+ end
177
+ end
178
+
179
+ def add(args)
180
+ options = {}
181
+ params = {}
182
+ optparse = Morpheus::Cli::OptionParser.new do |opts|
183
+ opts.banner = subcommand_usage()
184
+ opts.on("--name NAME", String, "Price set name") do |val|
185
+ params['name'] = val.to_s
186
+ end
187
+ opts.on("--code CODE", String, "Price set code, unique identifier") do |val|
188
+ params['code'] = val.to_s
189
+ end
190
+ opts.on("--region-code CODE", String, "Price set region code") do |val|
191
+ params['regionCode'] = val.to_s
192
+ end
193
+ opts.on("--cloud [CLOUD]", String, "Cloud ID or name") do |val|
194
+ options[:cloud] = val
195
+ end
196
+ opts.on("--resource-pool [POOL]", String, "Resource pool ID or name") do |val|
197
+ options[:resourcePool] = val
198
+ end
199
+ opts.on("--price-unit [UNIT]", String, "Price unit") do |val|
200
+ if price_units.include?(val)
201
+ params['priceUnit'] = val
202
+ else
203
+ raise_command_error "Unrecognized price unit '#{val}'\n- Available price units -\n#{price_units.join("\n")}"
204
+ end
205
+ end
206
+ opts.on('-t', "--type [TYPE]", String, "Price set type") do |val|
207
+ if ['fixed', 'compute_plus_storage', 'component'].include?(val)
208
+ params['type'] = val
209
+ else
210
+ raise_command_error "Unrecognized price set type #{val}"
211
+ end
212
+ end
213
+ opts.on('--prices [LIST]', Array, 'Price(s), comma separated list of price IDs') do |list|
214
+ params['prices'] = list.collect {|it| it.to_s.strip.empty? || !it.to_i ? nil : it.to_s.strip}.compact.uniq.collect {|it| {'id' => it.to_i}}
215
+ end
216
+ build_common_options(opts, options, [:options, :payload, :json, :dry_run, :remote, :quiet])
217
+ opts.footer = "Create price set.\n" +
218
+ "Name, code, type and price unit are required."
219
+ end
220
+ optparse.parse!(args)
221
+ connect(options)
222
+ if args.count != 0
223
+ raise_command_error "wrong number of arguments, expected 0 and got (#{args.count}) #{args}\n#{optparse}"
224
+ return 1
225
+ end
226
+
227
+ begin
228
+ payload = parse_payload(options)
229
+
230
+ if !payload
231
+ # name
232
+ params['name'] ||= Morpheus::Cli::OptionTypes.prompt([{'fieldName' => 'name', 'type' => 'text', 'fieldLabel' => 'Price Set Name', 'required' => true, 'description' => 'Price Set Name.'}],options[:options],@api_client,{}, options[:no_prompt])['name']
233
+
234
+ # code
235
+ params['code'] ||= Morpheus::Cli::OptionTypes.prompt([{'fieldName' => 'code', 'type' => 'text', 'fieldLabel' => 'Price Set Code', 'required' => true, 'defaultValue' => params['name'].gsub(/[^0-9a-z ]/i, '').gsub(' ', '.').downcase, 'description' => 'Price Set Code.'}],options[:options],@api_client,{}, options[:no_prompt])['code']
236
+
237
+ # region code
238
+ params['regionCode'] ||= Morpheus::Cli::OptionTypes.prompt([{'fieldName' => 'code', 'type' => 'text', 'fieldLabel' => 'Price Set Region Code', 'required' => false, 'description' => 'Price Set Region Code.'}],options[:options],@api_client,{}, options[:no_prompt])['code']
239
+
240
+ # cloud
241
+ if options[:cloud]
242
+ cloud = find_cloud(options[:cloud])
243
+
244
+ if cloud.nil?
245
+ print_red_alert "Cloud #{options[:cloud]} not found"
246
+ exit 1
247
+ end
248
+ params['zone'] = {'id' => cloud['id']}
249
+ else
250
+ cloud_id = Morpheus::Cli::OptionTypes.prompt(['fieldName' => 'value', 'type' => 'select', 'fieldLabel' => 'Cloud', 'required' => false, 'description' => 'Select cloud for price set', 'selectOptions' => @clouds_interface.list['zones'].collect {|it| {'name' => it['name'], 'value' => it['id']}}], options[:options], @api_client, {}, options[:no_prompt], true)['value']
251
+
252
+ if cloud_id
253
+ params['zone'] = {'id' => cloud_id}
254
+ end
255
+ end
256
+
257
+ # resource pool
258
+ if options[:resourcePool]
259
+ resource_pool = find_resource_pool(params['zone'].nil? ? nil : params['zone']['id'], options[:resourcePool])
260
+
261
+ if resource_pool.nil?
262
+ print_red_alert "Resource pool #{options[:resourcePool]} not found"
263
+ exit 1
264
+ end
265
+ params['zonePool'] = {'id' => resource_pool['id']}
266
+ else
267
+ resource_pool_id = Morpheus::Cli::OptionTypes.prompt(['fieldName' => 'value', 'type' => 'select', 'fieldLabel' => 'Resource Pool', 'required' => false, 'description' => 'Select resource pool for price set', 'selectOptions' => @cloud_resource_pools_interface.list(params['zone'] ? params['zone']['id'] : nil)['resourcePools'].collect {|it| {'name' => it['name'], 'value' => it['id']}}], options[:options], @api_client, {}, options[:no_prompt], true)['value']
268
+
269
+ if resource_pool_id
270
+ params['zonePool'] = {'id' => resource_pool_id}
271
+ end
272
+ end
273
+
274
+ # price unit
275
+ params['priceUnit'] ||= Morpheus::Cli::OptionTypes.prompt([{'fieldName' => 'priceUnit', 'type' => 'select', 'fieldLabel' => 'Price Unit', 'selectOptions' => price_units.collect {|it| {'name' => it.split(' ').collect {|it| it.capitalize}.join(' '), 'value' => it}}, 'required' => true, 'description' => 'Price Unit.', 'defaultValue' => 'month'}],options[:options],@api_client,{}, options[:no_prompt])['priceUnit']
276
+ if params['priceUnit'].nil?
277
+ print_red_alert "Price unit is required"
278
+ exit 1
279
+ end
280
+
281
+ # type
282
+ params['type'] ||= Morpheus::Cli::OptionTypes.prompt([{'fieldName' => 'type', 'type' => 'select', 'fieldLabel' => 'Price Set Type', 'selectOptions' => [{'name' => 'Everything', 'value' => 'fixed'}, {'name' => 'Compute + Storage', 'value' => 'compute_plus_storage'}, {'name' => 'Component', 'value' => 'component'}], 'required' => true, 'description' => 'Price Set Type.'}],options[:options],@api_client,{}, options[:no_prompt])['type']
283
+ if params['type'].nil?
284
+ print_red_alert "Type is required"
285
+ exit 1
286
+ end
287
+
288
+ # required prices
289
+ price_set_type = price_set_types[params['type']]
290
+ prices = params['prices'] ? @prices_interface.list({'ids' => params['prices'].collect {|it| it['id']}})['prices'] : []
291
+ required = price_set_type[:requires].reject {|it| prices.find {|price| price['priceType'] == it}}
292
+
293
+ if !options[:no_prompt]
294
+ params['prices'] ||= []
295
+ while required.count > 0 do
296
+ price_type = required.pop
297
+ avail_prices = @prices_interface.list({'priceType' => price_type, 'priceUnit' => params['priceUnit'], 'max' => 10000})['prices'].reject {|it| params['prices'].find {|price| price['id'] == it['id']}}.collect {|it| {'name' => it['name'], 'value' => it['id']}}
298
+
299
+ if avail_prices.count > 0
300
+ price_id = Morpheus::Cli::OptionTypes.prompt([{'fieldName' => 'price', 'type' => 'select', 'fieldLabel' => "Add #{price_type_label(price_type)} Price", 'selectOptions' => avail_prices, 'required' => true, 'description' => "'#{price_set_type[:label]}' price sets require 1 or more '#{price_type_label(price_type)}' price types"}],options[:options],@api_client,{}, options[:no_prompt], true)['price']
301
+ params['prices'] << {'id' => price_id}
302
+ else
303
+ print_red_alert "'#{price_set_type[:label]}' price sets require 1 or more '#{price_type_label(price_type)}' price types, however there are none available for the #{params['priceUnit']} price unit."
304
+ exit 1
305
+ end
306
+ end
307
+
308
+ # additional prices
309
+ avail_price_types = (price_set_type[:requires] + price_set_type[:allows]).collect {|it| {'name' => price_type_label(it), 'value' => it}}
310
+ price_type = nil
311
+ while Morpheus::Cli::OptionTypes.confirm("Add additional prices?") do
312
+ price_type = Morpheus::Cli::OptionTypes.prompt([{'fieldName' => 'priceType', 'type' => 'select', 'fieldLabel' => "Price Type", 'selectOptions' => avail_price_types, 'required' => true, 'defaultValue' => price_type, 'description' => "Select Price Type"}],options[:options],@api_client,{}, options[:no_prompt], true)['priceType']
313
+ avail_prices = @prices_interface.list({'priceType' => price_type, 'priceUnit' => params['priceUnit'], 'max' => 10000})['prices'].reject {|it| params['prices'].find {|price| price['id'] == it['id']}}.collect {|it| {'name' => it['name'], 'value' => it['id']}}
314
+
315
+ if avail_prices.count > 0
316
+ price_id = Morpheus::Cli::OptionTypes.prompt([{'fieldName' => 'price', 'type' => 'select', 'fieldLabel' => "Add #{price_type_label(price_type)} Price", 'selectOptions' => avail_prices, 'required' => true, 'description' => "Add #{price_type_label(price_type)} Price"}],options[:options],@api_client,{}, options[:no_prompt], true)['price']
317
+ params['prices'] << {'id' => price_id}
318
+ else
319
+ print_red_alert "No available prices for '#{price_type}'"
320
+ end
321
+ end
322
+ end
323
+ payload = {'priceSet' => params}
324
+ end
325
+
326
+ @price_sets_interface.setopts(options)
327
+ if options[:dry_run]
328
+ print_dry_run @price_sets_interface.dry.create(payload)
329
+ return
330
+ end
331
+ json_response = @price_sets_interface.create(payload)
332
+
333
+ if options[:json]
334
+ puts as_json(json_response, options)
335
+ elsif !options[:quiet]
336
+ if json_response['success']
337
+ print_green_success "Price set created"
338
+ _get(json_response['id'], options)
339
+ else
340
+ print_red_alert "Error creating price set: #{json_response['msg'] || json_response['errors']}"
341
+ end
342
+ end
343
+ return 0
344
+ rescue RestClient::Exception => e
345
+ print_rest_exception(e, options)
346
+ exit 1
347
+ end
348
+ end
349
+
350
+ def update(args)
351
+ options = {}
352
+ params = {}
353
+ optparse = Morpheus::Cli::OptionParser.new do |opts|
354
+ opts.banner = subcommand_usage("[price-set]")
355
+ opts.on("--name NAME", String, "Price set name") do |val|
356
+ params['name'] = val.to_s
357
+ end
358
+ opts.on('--restart-usage [on|off]', String, "Apply price changes to usage. Default is on") do |val|
359
+ params['restartUsage'] = val.to_s == 'on' || val.to_s == 'true' || val.to_s == '1' || val.to_s == ''
360
+ end
361
+ opts.on('--prices [LIST]', Array, 'Price(s), comma separated list of price IDs') do |list|
362
+ params['prices'] = list.collect {|it| it.to_s.strip.empty? || !it.to_i ? nil : it.to_s.strip}.compact.uniq.collect {|it| {'id' => it.to_i}}
363
+ end
364
+ build_common_options(opts, options, [:options, :payload, :json, :dry_run, :remote, :quiet])
365
+ opts.footer = "Update price set.\n" +
366
+ "[price-set] is required. Price set ID, name or code"
367
+ end
368
+ optparse.parse!(args)
369
+ connect(options)
370
+ if args.count != 1
371
+ raise_command_error "wrong number of arguments, expected 1 and got (#{args.count}) #{args}\n#{optparse}"
372
+ return 1
373
+ end
374
+
375
+ begin
376
+ price_set = find_price_set(args[0])
377
+
378
+ if price_set.nil?
379
+ print_red_alert "Price set #{args[0]} not found"
380
+ exit 1
381
+ end
382
+
383
+ payload = parse_payload(options)
384
+
385
+ if payload.nil?
386
+ payload = {'priceSet' => params}
387
+ end
388
+
389
+ if payload['priceSet'].empty?
390
+ print_green_success "Nothing to update"
391
+ return
392
+ end
393
+
394
+ @price_sets_interface.setopts(options)
395
+ if options[:dry_run]
396
+ print_dry_run @price_sets_interface.dry.update(price_set['id'], payload)
397
+ return
398
+ end
399
+ json_response = @price_sets_interface.update(price_set['id'], payload)
400
+
401
+ if options[:json]
402
+ puts as_json(json_response, options)
403
+ elsif !options[:quiet]
404
+ if json_response['success']
405
+ print_green_success "Price set updated"
406
+ _get(price_set['id'], options)
407
+ else
408
+ print_red_alert "Error updating price set: #{json_response['msg'] || json_response['errors']}"
409
+ end
410
+ end
411
+ return 0
412
+ rescue RestClient::Exception => e
413
+ print_rest_exception(e, options)
414
+ exit 1
415
+ end
416
+ end
417
+
418
+ def deactivate(args)
419
+ options = {}
420
+ params = {}
421
+ optparse = Morpheus::Cli::OptionParser.new do |opts|
422
+ opts.banner = subcommand_usage( "[price-set]")
423
+ build_common_options(opts, options, [:json, :dry_run, :remote])
424
+ opts.footer = "Deactivate price set.\n" +
425
+ "[price-set] is required. Price set ID, name or code"
426
+ end
427
+ optparse.parse!(args)
428
+ connect(options)
429
+ if args.count != 1
430
+ raise_command_error "wrong number of arguments, expected 1 and got (#{args.count}) #{args}\n#{optparse}"
431
+ return 1
432
+ end
433
+
434
+ begin
435
+ price_set = find_price_set(args[0])
436
+
437
+ if !price_set
438
+ print_red_alert "Price set #{args[0]} not found"
439
+ exit 1
440
+ end
441
+
442
+ if price_set['active'] == false
443
+ print_green_success "Price set #{price_set['name']} already deactived."
444
+ return 0
445
+ end
446
+
447
+ unless options[:yes] || ::Morpheus::Cli::OptionTypes::confirm("Are you sure you would like to deactivate the price set '#{price_set['name']}'?", options)
448
+ return 9, "aborted command"
449
+ end
450
+
451
+ @price_sets_interface.setopts(options)
452
+ if options[:dry_run]
453
+ print_dry_run @price_sets_interface.dry.deactivate(price_set['id'], params)
454
+ return
455
+ end
456
+
457
+ json_response = @price_sets_interface.deactivate(price_set['id'], params)
458
+
459
+ if options[:json]
460
+ print JSON.pretty_generate(json_response)
461
+ print "\n"
462
+ elsif !options[:quiet]
463
+ print_green_success "Price set #{price_set['name']} deactivate"
464
+ end
465
+ return 0
466
+ rescue RestClient::Exception => e
467
+ print_rest_exception(e, options)
468
+ exit 1
469
+ end
470
+ end
471
+
472
+ private
473
+
474
+ def currency_sym(currency)
475
+ Money::Currency.new((currency || 'usd').to_sym).symbol
476
+ end
477
+
478
+ def price_prefix(price)
479
+ (['platform', 'software'].include?(price['priceType']) ? '+' : '') + currency_sym(price['currency'])
480
+ end
481
+
482
+ def price_markup(price)
483
+ if price['markupType'] == 'fixed'
484
+ currency_sym(price['currency']) + format_amount(price['markup'] || 0)
485
+ elsif price['markupType'] == 'percent'
486
+ (price['markupPercent'] || 0).to_s + '%'
487
+ else
488
+ 'N/A'
489
+ end
490
+ end
491
+
492
+ def find_price_set(val)
493
+ (val.to_s =~ /\A\d{1,}\Z/) ? @price_sets_interface.get(val.to_i)['priceSet'] : @price_sets_interface.list({'code' => val, 'name' => val})["priceSets"].first
494
+ end
495
+
496
+ def find_cloud(val)
497
+ (val.to_s =~ /\A\d{1,}\Z/) ? @clouds_interface.get(val.to_i)['zone'] : @clouds_interface.list({'name' => val})["zones"].first
498
+ end
499
+
500
+ def find_resource_pool(cloud_id, val)
501
+ (val.to_s =~ /\A\d{1,}\Z/) ? @cloud_resource_pools_interface.get(cloud_id, val.to_i)['resourcePool'] : @cloud_resource_pools_interface.list(cloud_id, {'name' => val})["resourcePools"].first
502
+ end
503
+
504
+ def price_set_type_label(type)
505
+ price_set_types[type][:label]
506
+ end
507
+
508
+ def price_type_label(type)
509
+ {
510
+ 'fixed' => 'Everything',
511
+ 'compute' => 'Memory + CPU',
512
+ 'memory' => 'Memory Only (per MB)',
513
+ 'cores' => 'Cores Only (per core)',
514
+ 'storage' => 'Disk Only (per GB)',
515
+ 'datastore' => 'Datastore (per GB)',
516
+ 'platform' => 'Platform',
517
+ 'software' => 'Software'
518
+ }[type] || type.capitalize
519
+ end
520
+
521
+ def price_units
522
+ ['minute', 'hour', 'day', 'month', 'year', 'two year', 'three year', 'four year', 'five year']
523
+ end
524
+
525
+ def price_set_types
526
+ {
527
+ 'fixed' => {:label => 'Everything', :requires => ['fixed'], :allows => ['platform', 'software']},
528
+ 'compute_plus_storage' => {:label => 'Compute + Storage', :requires => ['compute', 'storage'], :allows => ['platform', 'software']},
529
+ 'component' => {:label => 'Component', :requires => ['memory', 'cores', 'storage'], :allows => ['platform', 'software']},
530
+ }
531
+ end
532
+
533
+ def format_amount(amount)
534
+ rtn = amount.to_s
535
+ if rtn.index('.').nil?
536
+ rtn += '.00'
537
+ elsif rtn.split('.')[1].length < 2
538
+ print rtn.split('.')[1].length
539
+ rtn = rtn + (['0'] * (2 - rtn.split('.')[1].length) * '')
540
+ end
541
+ rtn
542
+ end
543
+ end