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.
- checksums.yaml +4 -4
- data/Dockerfile +1 -1
- data/lib/morpheus/api/api_client.rb +24 -0
- data/lib/morpheus/api/{old_cypher_interface.rb → budgets_interface.rb} +10 -11
- data/lib/morpheus/api/cloud_datastores_interface.rb +7 -0
- data/lib/morpheus/api/cloud_resource_pools_interface.rb +2 -2
- data/lib/morpheus/api/cypher_interface.rb +18 -12
- data/lib/morpheus/api/health_interface.rb +72 -0
- data/lib/morpheus/api/instances_interface.rb +1 -1
- data/lib/morpheus/api/library_instance_types_interface.rb +7 -0
- data/lib/morpheus/api/log_settings_interface.rb +6 -0
- data/lib/morpheus/api/network_security_servers_interface.rb +30 -0
- data/lib/morpheus/api/price_sets_interface.rb +42 -0
- data/lib/morpheus/api/prices_interface.rb +68 -0
- data/lib/morpheus/api/provisioning_settings_interface.rb +29 -0
- data/lib/morpheus/api/servers_interface.rb +1 -1
- data/lib/morpheus/api/service_plans_interface.rb +34 -11
- data/lib/morpheus/api/task_sets_interface.rb +8 -0
- data/lib/morpheus/api/tasks_interface.rb +8 -0
- data/lib/morpheus/cli.rb +6 -3
- data/lib/morpheus/cli/appliance_settings_command.rb +13 -5
- data/lib/morpheus/cli/approvals_command.rb +1 -1
- data/lib/morpheus/cli/apps.rb +88 -28
- data/lib/morpheus/cli/backup_settings_command.rb +1 -1
- data/lib/morpheus/cli/blueprints_command.rb +2 -0
- data/lib/morpheus/cli/budgets_command.rb +672 -0
- data/lib/morpheus/cli/cli_command.rb +13 -2
- data/lib/morpheus/cli/cli_registry.rb +1 -0
- data/lib/morpheus/cli/clusters.rb +40 -274
- data/lib/morpheus/cli/commands/standard/benchmark_command.rb +114 -66
- data/lib/morpheus/cli/commands/standard/coloring_command.rb +12 -0
- data/lib/morpheus/cli/commands/standard/curl_command.rb +31 -6
- data/lib/morpheus/cli/commands/standard/echo_command.rb +8 -3
- data/lib/morpheus/cli/commands/standard/set_prompt_command.rb +1 -1
- data/lib/morpheus/cli/containers_command.rb +37 -24
- data/lib/morpheus/cli/cypher_command.rb +191 -150
- data/lib/morpheus/cli/health_command.rb +903 -0
- data/lib/morpheus/cli/hosts.rb +43 -32
- data/lib/morpheus/cli/instances.rb +119 -68
- data/lib/morpheus/cli/jobs_command.rb +1 -1
- data/lib/morpheus/cli/library_instance_types_command.rb +61 -11
- data/lib/morpheus/cli/library_option_types_command.rb +2 -2
- data/lib/morpheus/cli/log_settings_command.rb +46 -3
- data/lib/morpheus/cli/logs_command.rb +24 -17
- data/lib/morpheus/cli/mixins/accounts_helper.rb +2 -0
- data/lib/morpheus/cli/mixins/logs_helper.rb +73 -19
- data/lib/morpheus/cli/mixins/print_helper.rb +29 -1
- data/lib/morpheus/cli/mixins/provisioning_helper.rb +554 -96
- data/lib/morpheus/cli/mixins/whoami_helper.rb +13 -1
- data/lib/morpheus/cli/networks_command.rb +3 -0
- data/lib/morpheus/cli/option_types.rb +83 -53
- data/lib/morpheus/cli/price_sets_command.rb +543 -0
- data/lib/morpheus/cli/prices_command.rb +669 -0
- data/lib/morpheus/cli/processes_command.rb +0 -2
- data/lib/morpheus/cli/provisioning_settings_command.rb +237 -0
- data/lib/morpheus/cli/remote.rb +9 -4
- data/lib/morpheus/cli/reports_command.rb +10 -4
- data/lib/morpheus/cli/roles.rb +93 -38
- data/lib/morpheus/cli/security_groups.rb +10 -0
- data/lib/morpheus/cli/service_plans_command.rb +736 -0
- data/lib/morpheus/cli/tasks.rb +220 -8
- data/lib/morpheus/cli/tenants_command.rb +3 -16
- data/lib/morpheus/cli/users.rb +2 -25
- data/lib/morpheus/cli/version.rb +1 -1
- data/lib/morpheus/cli/whitelabel_settings_command.rb +18 -18
- data/lib/morpheus/cli/whoami.rb +28 -10
- data/lib/morpheus/cli/workflows.rb +488 -36
- data/lib/morpheus/formatters.rb +22 -0
- data/morpheus-cli.gemspec +1 -0
- metadata +28 -5
- data/lib/morpheus/cli/accounts.rb +0 -335
- 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
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
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
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
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
|
-
|
255
|
-
|
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
|
-
#
|
258
|
-
if option_type['optionSource']
|
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
|
-
|
269
|
-
if !
|
270
|
-
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 !
|
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[
|
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
|
-
|
325
|
-
if paging_enabled
|
326
|
-
|
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[
|
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[
|
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[
|
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
|
-
|
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
|