morpheus-cli 4.1.8 → 4.1.9

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