morpheus-cli 4.2.6 → 4.2.7
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 +4 -0
- data/lib/morpheus/api/clouds_interface.rb +14 -0
- data/lib/morpheus/api/guidance_interface.rb +47 -0
- data/lib/morpheus/api/users_interface.rb +7 -0
- data/lib/morpheus/cli.rb +1 -0
- data/lib/morpheus/cli/account_groups_command.rb +1 -1
- data/lib/morpheus/cli/approvals_command.rb +2 -2
- data/lib/morpheus/cli/apps.rb +26 -30
- data/lib/morpheus/cli/blueprints_command.rb +1 -1
- data/lib/morpheus/cli/budgets_command.rb +2 -2
- data/lib/morpheus/cli/change_password_command.rb +0 -1
- data/lib/morpheus/cli/cli_command.rb +19 -9
- data/lib/morpheus/cli/clouds.rb +107 -10
- data/lib/morpheus/cli/clusters.rb +12 -12
- data/lib/morpheus/cli/commands/standard/curl_command.rb +7 -0
- data/lib/morpheus/cli/deployments.rb +2 -2
- data/lib/morpheus/cli/environments_command.rb +1 -1
- data/lib/morpheus/cli/execution_request_command.rb +1 -1
- data/lib/morpheus/cli/groups.rb +1 -1
- data/lib/morpheus/cli/guidance_command.rb +529 -0
- data/lib/morpheus/cli/hosts.rb +2 -10
- data/lib/morpheus/cli/instances.rb +31 -13
- data/lib/morpheus/cli/integrations_command.rb +1 -1
- data/lib/morpheus/cli/jobs_command.rb +2 -2
- data/lib/morpheus/cli/library_container_types_command.rb +4 -4
- data/lib/morpheus/cli/library_instance_types_command.rb +3 -3
- data/lib/morpheus/cli/library_spec_templates_command.rb +1 -1
- data/lib/morpheus/cli/load_balancers.rb +2 -2
- data/lib/morpheus/cli/mixins/print_helper.rb +43 -3
- data/lib/morpheus/cli/mixins/provisioning_helper.rb +251 -165
- data/lib/morpheus/cli/network_routers_command.rb +1 -1
- data/lib/morpheus/cli/price_sets_command.rb +2 -2
- data/lib/morpheus/cli/provisioning_licenses_command.rb +1 -1
- data/lib/morpheus/cli/remote.rb +6 -1
- data/lib/morpheus/cli/reports_command.rb +1 -1
- data/lib/morpheus/cli/security_group_rules.rb +1 -1
- data/lib/morpheus/cli/security_groups.rb +13 -5
- data/lib/morpheus/cli/service_plans_command.rb +2 -2
- data/lib/morpheus/cli/user_groups_command.rb +2 -6
- data/lib/morpheus/cli/user_settings_command.rb +31 -5
- data/lib/morpheus/cli/user_sources_command.rb +3 -3
- data/lib/morpheus/cli/users.rb +117 -90
- data/lib/morpheus/cli/version.rb +1 -1
- data/lib/morpheus/cli/virtual_images.rb +2 -2
- data/lib/morpheus/cli/whitelabel_settings_command.rb +95 -15
- data/lib/morpheus/cli/wiki_command.rb +2 -2
- data/lib/morpheus/cli/workflows.rb +2 -3
- data/lib/morpheus/formatters.rb +14 -5
- metadata +4 -2
@@ -87,7 +87,7 @@ class Morpheus::Cli::Clusters
|
|
87
87
|
clusters = json_response['clusters']
|
88
88
|
|
89
89
|
if clusters.empty?
|
90
|
-
print
|
90
|
+
print cyan,"No clusters found.",reset,"\n"
|
91
91
|
else
|
92
92
|
print_clusters_table(clusters, options)
|
93
93
|
end
|
@@ -244,7 +244,7 @@ class Morpheus::Cli::Clusters
|
|
244
244
|
masters_json = @clusters_interface.list_masters(cluster['id'], options)
|
245
245
|
if masters_json.nil? || masters_json['masters'].empty?
|
246
246
|
print_h2 "Masters"
|
247
|
-
print
|
247
|
+
print cyan,"No masters found.",reset,"\n"
|
248
248
|
else
|
249
249
|
masters = masters_json['masters']
|
250
250
|
print_h2 "Masters"
|
@@ -265,7 +265,7 @@ class Morpheus::Cli::Clusters
|
|
265
265
|
workers_json = @clusters_interface.list_workers(cluster['id'], options)
|
266
266
|
if workers_json.nil? || workers_json['workers'].empty?
|
267
267
|
print_h2 "Workers"
|
268
|
-
print
|
268
|
+
print cyan,"No workers found.",reset,"\n"
|
269
269
|
else
|
270
270
|
workers = workers_json['workers']
|
271
271
|
print_h2 "Workers"
|
@@ -1010,7 +1010,7 @@ class Morpheus::Cli::Clusters
|
|
1010
1010
|
print_h1 title, subtitles
|
1011
1011
|
workers = json_response['workers']
|
1012
1012
|
if workers.empty?
|
1013
|
-
print
|
1013
|
+
print cyan,"No workers found.",reset,"\n"
|
1014
1014
|
else
|
1015
1015
|
# more stuff to show here
|
1016
1016
|
|
@@ -1310,7 +1310,7 @@ class Morpheus::Cli::Clusters
|
|
1310
1310
|
print_h1 title, subtitles
|
1311
1311
|
masters = json_response['masters']
|
1312
1312
|
if masters.empty?
|
1313
|
-
print
|
1313
|
+
print cyan,"No masters found.",reset,"\n"
|
1314
1314
|
else
|
1315
1315
|
# more stuff to show here
|
1316
1316
|
|
@@ -1409,7 +1409,7 @@ class Morpheus::Cli::Clusters
|
|
1409
1409
|
print_h1 title, subtitles
|
1410
1410
|
volumes = json_response['volumes']
|
1411
1411
|
if volumes.empty?
|
1412
|
-
print
|
1412
|
+
print cyan,"No volumes found.",reset,"\n"
|
1413
1413
|
else
|
1414
1414
|
# more stuff to show here
|
1415
1415
|
rows = volumes.collect do |ns|
|
@@ -1524,7 +1524,7 @@ class Morpheus::Cli::Clusters
|
|
1524
1524
|
print_h1 title, subtitles
|
1525
1525
|
services = json_response['services']
|
1526
1526
|
if services.empty?
|
1527
|
-
print
|
1527
|
+
print cyan,"No services found.",reset,"\n"
|
1528
1528
|
else
|
1529
1529
|
# more stuff to show here
|
1530
1530
|
rows = services.collect do |service|
|
@@ -1641,7 +1641,7 @@ class Morpheus::Cli::Clusters
|
|
1641
1641
|
print_h1 title, subtitles
|
1642
1642
|
jobs = json_response['jobs']
|
1643
1643
|
if jobs.empty?
|
1644
|
-
print
|
1644
|
+
print cyan,"No jobs found.",reset,"\n"
|
1645
1645
|
else
|
1646
1646
|
# more stuff to show here
|
1647
1647
|
rows = jobs.collect do |job|
|
@@ -1767,7 +1767,7 @@ class Morpheus::Cli::Clusters
|
|
1767
1767
|
print_h1 title, subtitles
|
1768
1768
|
containers = json_response['containers']
|
1769
1769
|
if containers.empty?
|
1770
|
-
print
|
1770
|
+
print cyan,"No containers found.",reset,"\n"
|
1771
1771
|
else
|
1772
1772
|
# more stuff to show here
|
1773
1773
|
rows = containers.collect do |it|
|
@@ -1928,7 +1928,7 @@ class Morpheus::Cli::Clusters
|
|
1928
1928
|
print_h1 title, subtitles
|
1929
1929
|
container_groups = json_response["#{resource_type}s"]
|
1930
1930
|
if container_groups.empty?
|
1931
|
-
print
|
1931
|
+
print cyan,"No #{resource_type}s found.",reset,"\n"
|
1932
1932
|
else
|
1933
1933
|
# more stuff to show here
|
1934
1934
|
rows = container_groups.collect do |it|
|
@@ -2322,7 +2322,7 @@ class Morpheus::Cli::Clusters
|
|
2322
2322
|
print_h1 title, subtitles
|
2323
2323
|
namespaces = json_response['namespaces']
|
2324
2324
|
if namespaces.empty?
|
2325
|
-
print
|
2325
|
+
print cyan,"No namespaces found.",reset,"\n"
|
2326
2326
|
else
|
2327
2327
|
# more stuff to show here
|
2328
2328
|
rows = namespaces.collect do |ns|
|
@@ -2567,7 +2567,7 @@ class Morpheus::Cli::Clusters
|
|
2567
2567
|
print_h1 title, subtitles
|
2568
2568
|
datastores = json_response['datastores']
|
2569
2569
|
if datastores.empty?
|
2570
|
-
print
|
2570
|
+
print cyan,"No datastores found.",reset,"\n"
|
2571
2571
|
else
|
2572
2572
|
# more stuff to show here
|
2573
2573
|
rows = datastores.collect do |ds|
|
@@ -14,6 +14,7 @@ class Morpheus::Cli::CurlCommand
|
|
14
14
|
curl_args = split_args[1] ? split_args[1].split(" ") : []
|
15
15
|
curl_method = nil
|
16
16
|
curl_data = nil
|
17
|
+
curl_verbsose = false
|
17
18
|
show_progress = false
|
18
19
|
# puts "args is : #{args}"
|
19
20
|
# puts "curl_args is : #{curl_args}"
|
@@ -26,6 +27,9 @@ class Morpheus::Cli::CurlCommand
|
|
26
27
|
opts.on( '-X', '--request METHOD', "HTTP request method. Default is GET" ) do |val|
|
27
28
|
curl_method = val
|
28
29
|
end
|
30
|
+
opts.on( '-v', '--verbose', "Print verbose output." ) do
|
31
|
+
curl_verbsose = true
|
32
|
+
end
|
29
33
|
opts.on( '--data DATA', String, "HTTP request body for use with POST and PUT, typically JSON." ) do |val|
|
30
34
|
curl_data = val
|
31
35
|
end
|
@@ -87,6 +91,9 @@ EOT
|
|
87
91
|
if show_progress == false
|
88
92
|
curl_cmd << " -s"
|
89
93
|
end
|
94
|
+
if curl_verbsose
|
95
|
+
curl_cmd << " -v"
|
96
|
+
end
|
90
97
|
if curl_method
|
91
98
|
curl_cmd << " -X#{curl_method}"
|
92
99
|
end
|
@@ -46,7 +46,7 @@ class Morpheus::Cli::Deployments
|
|
46
46
|
deployments = json_response['deployments']
|
47
47
|
print_h1 "Morpheus Deployments"
|
48
48
|
if deployments.empty?
|
49
|
-
print
|
49
|
+
print cyan,"No deployments found.",reset,"\n"
|
50
50
|
else
|
51
51
|
print cyan
|
52
52
|
rows = deployments.collect do |deployment|
|
@@ -95,7 +95,7 @@ class Morpheus::Cli::Deployments
|
|
95
95
|
versions = json_response['versions']
|
96
96
|
print_h1 "Deployment Versions: #{deployment['name']}"
|
97
97
|
if versions.empty?
|
98
|
-
print
|
98
|
+
print cyan,"No deployment versions found.",reset,"\n"
|
99
99
|
else
|
100
100
|
print cyan
|
101
101
|
rows = versions.collect do |version|
|
@@ -52,7 +52,7 @@ class Morpheus::Cli::EnvironmentsCommand
|
|
52
52
|
subtitles += parse_list_subtitles(options)
|
53
53
|
print_h1 title, subtitles
|
54
54
|
if environments.empty?
|
55
|
-
print
|
55
|
+
print cyan,"No Environments found.",reset
|
56
56
|
else
|
57
57
|
print_environments_table(environments)
|
58
58
|
print_results_pagination(json_response)
|
@@ -114,7 +114,7 @@ class Morpheus::Cli::ExecutionRequestCommand
|
|
114
114
|
}
|
115
115
|
print_description_list(description_cols, execution_request)
|
116
116
|
|
117
|
-
if execution_request['stdErr']
|
117
|
+
if execution_request['stdErr'] && execution_request['stdErr'] != "stdin: is not a tty\n"
|
118
118
|
print_h2 "Error"
|
119
119
|
puts execution_request['stdErr'].to_s.strip
|
120
120
|
end
|
data/lib/morpheus/cli/groups.rb
CHANGED
@@ -467,7 +467,7 @@ class Morpheus::Cli::Groups
|
|
467
467
|
options = {}
|
468
468
|
optparse = Morpheus::Cli::OptionParser.new do|opts|
|
469
469
|
opts.banner = subcommand_usage()
|
470
|
-
build_common_options(opts, options, [])
|
470
|
+
build_common_options(opts, options, [:remote])
|
471
471
|
opts.footer = "Print the name of the current active group"
|
472
472
|
end
|
473
473
|
optparse.parse!(args)
|
@@ -0,0 +1,529 @@
|
|
1
|
+
require 'morpheus/cli/cli_command'
|
2
|
+
require 'date'
|
3
|
+
|
4
|
+
class Morpheus::Cli::GuidanceCommand
|
5
|
+
include Morpheus::Cli::CliCommand
|
6
|
+
|
7
|
+
set_command_name :'guidance'
|
8
|
+
|
9
|
+
register_subcommands :list, :get, :stats, :execute, :ignore, :types
|
10
|
+
|
11
|
+
def connect(opts)
|
12
|
+
@api_client = establish_remote_appliance_connection(opts)
|
13
|
+
@guidance_interface = @api_client.guidance
|
14
|
+
end
|
15
|
+
|
16
|
+
def handle(args)
|
17
|
+
handle_subcommand(args)
|
18
|
+
end
|
19
|
+
|
20
|
+
def stats(args)
|
21
|
+
options = {}
|
22
|
+
optparse = Morpheus::Cli::OptionParser.new do |opts|
|
23
|
+
opts.banner = subcommand_usage()
|
24
|
+
build_standard_get_options(opts, options)
|
25
|
+
opts.footer = "Get guidance stats."
|
26
|
+
end
|
27
|
+
optparse.parse!(args)
|
28
|
+
connect(options)
|
29
|
+
if args.count != 0
|
30
|
+
raise_command_error "wrong number of arguments, expected 0 and got (#{args.count}) #{args}\n#{optparse}"
|
31
|
+
return 1
|
32
|
+
end
|
33
|
+
|
34
|
+
begin
|
35
|
+
@guidance_interface.setopts(options)
|
36
|
+
|
37
|
+
if options[:dry_run]
|
38
|
+
print_dry_run @guidance_interface.dry.stats()
|
39
|
+
return
|
40
|
+
end
|
41
|
+
json_response = @guidance_interface.stats()
|
42
|
+
if options[:json]
|
43
|
+
puts as_json(json_response, options, "stats")
|
44
|
+
return 0
|
45
|
+
elsif options[:yaml]
|
46
|
+
puts as_yaml(json_response, options, "stats")
|
47
|
+
return 0
|
48
|
+
elsif options[:csv]
|
49
|
+
puts records_as_csv([json_response['stats']], options)
|
50
|
+
return 0
|
51
|
+
end
|
52
|
+
|
53
|
+
stats = json_response['stats']
|
54
|
+
|
55
|
+
print_h1 "Guidance Stats"
|
56
|
+
print cyan
|
57
|
+
description_cols = {
|
58
|
+
"Total Actions" => lambda {|it| it['total'] },
|
59
|
+
"Savings Available" => lambda {|it| format_money(it['savings']['amount'], it['savings']['currency'], {:minus_color => red}) }
|
60
|
+
}
|
61
|
+
print_description_list(description_cols, stats)
|
62
|
+
|
63
|
+
print_h2 "Severity Totals"
|
64
|
+
{'info'=>white, 'low'=>yellow, 'warning'=>bright_yellow, 'critical'=>red}.each do |level, color|
|
65
|
+
print "#{cyan}#{level.capitalize}".rjust(14, ' ') + ": " + stats['severity'][level].to_s.ljust(10, ' ')
|
66
|
+
# print "#{cyan} #{guidance['stats']['severity'][level]} of #{guidance['stats']['severity'].collect{|k, v| v}.reduce(:+)}".ljust(20, ' ')
|
67
|
+
println generate_usage_bar(stats['severity'][level], stats['severity'].collect{|k, v| v}.reduce(:+), {:max_bars => 20, :bar_color => color})
|
68
|
+
end
|
69
|
+
|
70
|
+
# "size": 13, "shutdown": 15, "move": 0, "schedule"
|
71
|
+
print_h2 "Action Totals"
|
72
|
+
{'size'=>green, 'move'=>magenta, 'shutdown'=>red, 'schedule'=>bright_yellow}.each do |level, color|
|
73
|
+
print "#{cyan}#{level.capitalize}".rjust(14, ' ') + ": " + stats['type'][level].to_s.ljust(10, ' ')
|
74
|
+
println generate_usage_bar(stats['type'][level], stats['type'].collect{|k, v| v}.reduce(:+), {:max_bars => 20, :bar_color => color})
|
75
|
+
end
|
76
|
+
print reset "\n"
|
77
|
+
return 0
|
78
|
+
rescue RestClient::Exception => e
|
79
|
+
print_rest_exception(e, options)
|
80
|
+
return 1
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
def types(args)
|
85
|
+
options = {}
|
86
|
+
optparse = Morpheus::Cli::OptionParser.new do |opts|
|
87
|
+
opts.banner = subcommand_usage()
|
88
|
+
build_standard_get_options(opts, options)
|
89
|
+
opts.footer = "List discovery types."
|
90
|
+
end
|
91
|
+
optparse.parse!(args)
|
92
|
+
connect(options)
|
93
|
+
if args.count != 0
|
94
|
+
raise_command_error "wrong number of arguments, expected 0 and got (#{args.count}) #{args}\n#{optparse}"
|
95
|
+
return 1
|
96
|
+
end
|
97
|
+
|
98
|
+
begin
|
99
|
+
@guidance_interface.setopts(options)
|
100
|
+
|
101
|
+
if options[:dry_run]
|
102
|
+
print_dry_run @guidance_interface.dry.types()
|
103
|
+
return
|
104
|
+
end
|
105
|
+
json_response = @guidance_interface.types()
|
106
|
+
if options[:json]
|
107
|
+
puts as_json(json_response, options, "types")
|
108
|
+
return 0
|
109
|
+
elsif options[:yaml]
|
110
|
+
puts as_yaml(json_response, options, "types")
|
111
|
+
return 0
|
112
|
+
elsif options[:csv]
|
113
|
+
puts records_as_csv([json_response['types']], options)
|
114
|
+
return 0
|
115
|
+
end
|
116
|
+
|
117
|
+
types = json_response['types']
|
118
|
+
|
119
|
+
print_h1 "Discovery Types"
|
120
|
+
print cyan
|
121
|
+
|
122
|
+
cols = [
|
123
|
+
{"ID" => lambda {|it| it['id']}},
|
124
|
+
{"NAME" => lambda {|it| it['name']}},
|
125
|
+
{"TITLE" => lambda {|it| it['title']}},
|
126
|
+
{"CODE" => lambda {|it| it['code']}}
|
127
|
+
];
|
128
|
+
print as_pretty_table(types, cols, options)
|
129
|
+
print reset "\n"
|
130
|
+
return 0
|
131
|
+
rescue RestClient::Exception => e
|
132
|
+
print_rest_exception(e, options)
|
133
|
+
return 1
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
def list(args)
|
138
|
+
options = {}
|
139
|
+
params = {}
|
140
|
+
ref_ids = []
|
141
|
+
optparse = Morpheus::Cli::OptionParser.new do |opts|
|
142
|
+
opts.banner = subcommand_usage()
|
143
|
+
opts.on('-l', '--severity LEVEL', String, "Filter by Severity Level: info, low, warning, critical" ) do |val|
|
144
|
+
params['severity'] = val
|
145
|
+
end
|
146
|
+
opts.on('-t', '--type TYPE', String, "Filter by Type") do |val|
|
147
|
+
options[:type] = val
|
148
|
+
end
|
149
|
+
opts.on('-i', '--ignored', String, "Include Ignored Discoveries") do |val|
|
150
|
+
params['state'] = 'ignored'
|
151
|
+
end
|
152
|
+
opts.on('-p', '--processed', String, "Include Processed Discoveries") do |val|
|
153
|
+
params['state'] = 'processed'
|
154
|
+
end
|
155
|
+
opts.on('-a', '--any', String, "Include Processed and Ignored Discoveries") do |val|
|
156
|
+
params['state'] = 'any'
|
157
|
+
end
|
158
|
+
build_standard_list_options(opts, options)
|
159
|
+
opts.footer = "List discoveries"
|
160
|
+
end
|
161
|
+
optparse.parse!(args)
|
162
|
+
connect(options)
|
163
|
+
if args.count > 0
|
164
|
+
print_error Morpheus::Terminal.angry_prompt
|
165
|
+
puts_error "wrong number of arguments, expected 0 and got (#{args.count}) #{args.join(', ')}\n#{optparse}"
|
166
|
+
return 1
|
167
|
+
end
|
168
|
+
begin
|
169
|
+
if options[:type]
|
170
|
+
type = find_discovery_type(options[:type])
|
171
|
+
|
172
|
+
if !type
|
173
|
+
print_red_alert "Type #{options[:type]} not found"
|
174
|
+
exit 1
|
175
|
+
end
|
176
|
+
params['code'] = type['code']
|
177
|
+
end
|
178
|
+
|
179
|
+
params.merge!(parse_list_options(options))
|
180
|
+
@guidance_interface.setopts(options)
|
181
|
+
if options[:dry_run]
|
182
|
+
print_dry_run @guidance_interface.dry.list(params)
|
183
|
+
return
|
184
|
+
end
|
185
|
+
json_response = @guidance_interface.list(params)
|
186
|
+
render_result = render_with_format(json_response, options, 'discoveries')
|
187
|
+
return 0 if render_result
|
188
|
+
|
189
|
+
discoveries = json_response['discoveries']
|
190
|
+
|
191
|
+
subtitles = params['state'] ? ["state: #{params['state']}"] : []
|
192
|
+
subtitles += parse_list_subtitles(options)
|
193
|
+
print_h1 "Morpheus Discoveries", subtitles
|
194
|
+
|
195
|
+
if discoveries.empty?
|
196
|
+
print cyan,"No discoveries found.",reset,"\n"
|
197
|
+
else
|
198
|
+
cols = [
|
199
|
+
{"ID" => lambda {|it| it['id']}},
|
200
|
+
{"SEVERITY" => lambda {|it| (it['severity'] || '').capitalize}},
|
201
|
+
{"TYPE/METRIC" => lambda {|it| it['type'] ? "#{it['type']['name']}: #{it['actionCategory'].capitalize || ''}" : ''}},
|
202
|
+
{"ACTION" => lambda {|it| it['actionType'].capitalize}},
|
203
|
+
{"CLOUD" => lambda {|it| it['zone'] ? it['zone']['name'] : ''}},
|
204
|
+
{"RESOURCE" => lambda {|it| it['refName']}},
|
205
|
+
{"SAVINGS" => lambda {|it| format_money(it['savings']['amount'], it['savings']['currency'], {:minus_color => red})}},
|
206
|
+
{"DATE" => lambda {|it| format_local_date(it['dateCreated'], {:format => DEFAULT_TIME_FORMAT})}}
|
207
|
+
];
|
208
|
+
if params['state'] == 'any'
|
209
|
+
cols << {"STATE" => lambda {|it| (it['state'] == 'processed' ? green : (it['state'] == 'ignored' ? white : '')) + it['state'].capitalize + cyan}}
|
210
|
+
end
|
211
|
+
print as_pretty_table(discoveries, cols, options)
|
212
|
+
print_results_pagination(json_response, {:label => "discovery", :n_label => "discoveries"})
|
213
|
+
end
|
214
|
+
print reset,"\n"
|
215
|
+
rescue RestClient::Exception => e
|
216
|
+
print_rest_exception(e, options)
|
217
|
+
return 1
|
218
|
+
end
|
219
|
+
end
|
220
|
+
|
221
|
+
def get(args)
|
222
|
+
options = {}
|
223
|
+
optparse = Morpheus::Cli::OptionParser.new do |opts|
|
224
|
+
opts.banner = subcommand_usage("[id]")
|
225
|
+
build_standard_get_options(opts, options)
|
226
|
+
opts.footer = "Get details about a specific discovery."
|
227
|
+
end
|
228
|
+
optparse.parse!(args)
|
229
|
+
if args.count < 1
|
230
|
+
puts optparse
|
231
|
+
return 1
|
232
|
+
end
|
233
|
+
connect(options)
|
234
|
+
id_list = parse_id_list(args)
|
235
|
+
return run_command_for_each_arg(id_list) do |arg|
|
236
|
+
_get(arg, options)
|
237
|
+
end
|
238
|
+
end
|
239
|
+
|
240
|
+
def _get(id, options)
|
241
|
+
params = {}
|
242
|
+
begin
|
243
|
+
@guidance_interface.setopts(options)
|
244
|
+
if options[:dry_run]
|
245
|
+
print_dry_run @guidance_interface.dry.get(id, params)
|
246
|
+
return
|
247
|
+
end
|
248
|
+
json_response = @guidance_interface.get(id, params)
|
249
|
+
discovery = json_response['discovery']
|
250
|
+
render_result = render_with_format(json_response, options, 'discovery')
|
251
|
+
return 0 if render_result
|
252
|
+
|
253
|
+
print_h1 "Discovery Info"
|
254
|
+
print cyan
|
255
|
+
|
256
|
+
description_cols = {
|
257
|
+
"ID" => lambda {|it| it['id']},
|
258
|
+
#"Ref ID" => lambda {|it| it['refId'] },
|
259
|
+
"Resource" => lambda {|it| it['refName'] },
|
260
|
+
"Action" => lambda {|it| action_title(it['type'])},
|
261
|
+
"Date" => lambda {|it| format_local_date(it['dateCreated'], {:format => DEFAULT_TIME_FORMAT})},
|
262
|
+
"State" => lambda {|it| (it['state'] == 'processed' ? green : (it['state'] == 'ignored' ? white : cyan)) + it['state'].capitalize + cyan}
|
263
|
+
}
|
264
|
+
description_cols['Cloud'] = lambda {|it| it['zone']['name']} if discovery['refType'] == 'computeServer'
|
265
|
+
description_cols.merge!({
|
266
|
+
"Action Category" => lambda {|it| it['actionCategory'].capitalize},
|
267
|
+
"Action Type" => lambda {|it| it['actionType'].capitalize},
|
268
|
+
"Savings" => lambda {|it| format_money(it['savings']['amount'], it['savings']['currency']) + '/month'}
|
269
|
+
})
|
270
|
+
print_description_list(description_cols, discovery)
|
271
|
+
|
272
|
+
if discovery['resource']
|
273
|
+
print_h2 "Resource Info"
|
274
|
+
|
275
|
+
if discovery['refType'] == 'computeServer'
|
276
|
+
cols = [
|
277
|
+
{"Power State" => lambda {|it| format_status(it['resource']['powerState'])}},
|
278
|
+
{"Status" => lambda {|it| (format_status(it['resource']['status']))}},
|
279
|
+
{"Type" => lambda {|it| it['resource']['computeServerType']['name']}},
|
280
|
+
{"Platform" => lambda {|it| ((it['resource']['serverOs'] ? it['resource']['serverOs']['platform'] : it['resource']['osType']) || 'unknown').capitalize}},
|
281
|
+
{"Cloud Type" => lambda {|it| it['zone']['zoneType']['name']}}
|
282
|
+
]
|
283
|
+
elsif discovery['refType'] == 'computeZone'
|
284
|
+
cols = [
|
285
|
+
{"Status" => lambda {|it| format_status(it['resource']['status'])}},
|
286
|
+
{"Cloud Type" => lambda {|it| it['zone']['zoneType']['name']}}
|
287
|
+
]
|
288
|
+
end
|
289
|
+
print as_pretty_table(discovery, cols, options)
|
290
|
+
end
|
291
|
+
|
292
|
+
max_bars = 20
|
293
|
+
|
294
|
+
if discovery['type']['code'] == 'size'
|
295
|
+
print_h2 "Usage"
|
296
|
+
cols = [
|
297
|
+
{"Plan" => lambda {|it| it['planBeforeAction'] ? it['planBeforeAction']['name'] : '--'}},
|
298
|
+
{"Compute Usage" => lambda {|it|
|
299
|
+
usage = (it['config'] ? it['config']['cpuUsageAvg'] || 0 : 0)
|
300
|
+
"#{format_percent(usage)} of 100%".ljust(25, ' ') + generate_usage_bar(usage.round(2), 100, {:max_bars => max_bars}) + cyan
|
301
|
+
}},
|
302
|
+
{"Memory Usage" => lambda {|it|
|
303
|
+
max = (it['resource'] || {})['maxMemory'] || (it['planBeforeAction'] || {})['maxMemory']
|
304
|
+
usage = max > 0 ? (it['config']['usedMemoryAvg'] || 0).to_f / max * 100.0 : 0
|
305
|
+
usage = 200.0 if usage > 200
|
306
|
+
"#{format_bytes((it['config'] || {})['usedMemoryAvg'] || 0, 'auto', 1)} of #{format_bytes(max, 'auto', 1)}".ljust(25, ' ') + generate_usage_bar(usage, 100, {:max_bars => max_bars}) + cyan
|
307
|
+
}}
|
308
|
+
]
|
309
|
+
print_description_list(cols, discovery, options.merge({:wrap => false}))
|
310
|
+
print_h2 "After Resize"
|
311
|
+
|
312
|
+
if discovery['planAfterAction'] && discovery['planAfterAction']['id'] != (discovery['planBeforeAction'] || {})['id']
|
313
|
+
cols = [
|
314
|
+
{"Plan" => lambda {|it| it['planAfterAction'] ? it['planAfterAction']['name'] : '--'}},
|
315
|
+
{"Compute Usage" => lambda {|it|
|
316
|
+
usage = ((it['planAfterAction'] || {})['maxCores'] || 0) > 0 ? (((it['resource'] || {})['maxCores'] || (it['planBeforeAction'] || {})['maxCores']) || 0).to_f / it['planAfterAction']['maxCores'] * (it['config']['cpuUsageAvg'] || 0) : 0
|
317
|
+
"#{format_percent(usage)} of 100%".ljust(25, ' ') + generate_usage_bar(usage.round(2), 100, {:max_bars => max_bars}) + cyan
|
318
|
+
}},
|
319
|
+
{"Memory Usage" => lambda {|it|
|
320
|
+
max = (it['planAfterAction'] || {})['maxMemory'] || 0
|
321
|
+
usage = max > 0 ? ((it['config'] || {})['usedMemoryAvg'] || 0).to_f / it['planAfterAction']['maxMemory'] * 100.0 : 0
|
322
|
+
usage = 200.0 if usage > 200
|
323
|
+
"#{format_bytes((it['config'] || {})['usedMemoryAvg'] || 0, 'auto', 1)} of #{format_bytes(max, 'auto', 1)}".ljust(25, ' ') + generate_usage_bar(usage, 100, {:max_bars => max_bars}) + cyan
|
324
|
+
}}
|
325
|
+
]
|
326
|
+
else
|
327
|
+
if discovery['actionValueType'] == 'memory'
|
328
|
+
cols = [
|
329
|
+
{"Memory" => lambda {|it| format_bytes(it['actionValue'].to_i, 'auto')}},
|
330
|
+
{"Compute Usage" => lambda {|it|
|
331
|
+
usage = (it['config'] ? it['config']['cpuUsageAvg'] || 0 : 0).round(2)
|
332
|
+
"#{format_percent(usage)} of 100%".ljust(25, ' ') + generate_usage_bar(usage, 100, {:max_bars => max_bars}) + cyan
|
333
|
+
}},
|
334
|
+
{"Memory Usage" => lambda {|it|
|
335
|
+
max = (it['actionValue'] || 0).to_f
|
336
|
+
usage = max > 0 ? ((it['config'] || {})['usedMemoryAvg'] || 0) / max * 100.0 : 0
|
337
|
+
usage = 200.0 if usage > 200
|
338
|
+
"#{format_bytes((it['config'] || {})['usedMemoryAvg'] || 0, 'auto', 1)} of #{format_bytes(max, 'auto', 1)}".ljust(25, ' ') + generate_usage_bar(usage, 100, {:max_bars => max_bars}) + cyan
|
339
|
+
}}
|
340
|
+
]
|
341
|
+
elsif discovery['actionValueType'] == 'cpu'
|
342
|
+
cols = [
|
343
|
+
{"Cores" => lambda {|it| it['actionValue']}},
|
344
|
+
{"Compute Usage" => lambda {|it|
|
345
|
+
cores_before = it['beforeValue'] || (it['resource'] || {})['maxCores'] || (it['planBeforeAction'] || {})['maxCores'] || 0
|
346
|
+
usage = (it['actionValue'] || 0).to_f > 0 ? cores_before / it['actionValue'].to_f * ((it['config'] || {})['cpuUsageAvg'] || 0) : 0
|
347
|
+
"#{format_percent(usage)} of 100%".ljust(25, ' ') + generate_usage_bar(usage.round(2), 100, {:max_bars => max_bars}) + cyan
|
348
|
+
}},
|
349
|
+
{"Memory Usage" => lambda {|it|
|
350
|
+
max = (it['resource'] || {})['maxMemory'] || (it['planAfterAction'] || {})['maxMemory']
|
351
|
+
usage = max > 0 ? ((it['config'] || {})['usedMemoryAvg'] || 0) / max.to_f * 100.0 : 0
|
352
|
+
usage = 200.0 if usage > 200
|
353
|
+
"#{format_bytes((it['config'] || {})['usedMemoryAvg'] || 0, 'auto', 1)} of #{format_bytes(max, 'auto', 1)}".ljust(25, ' ') + generate_usage_bar(usage, 100, {:max_bars => max_bars}) + cyan
|
354
|
+
}}
|
355
|
+
]
|
356
|
+
end
|
357
|
+
end
|
358
|
+
print_description_list(cols, discovery, options.merge({:wrap => false}))
|
359
|
+
elsif discovery['type']['code'] == 'shutdown'
|
360
|
+
print_h2 "Usage"
|
361
|
+
cols = [
|
362
|
+
{"Plan" => lambda {|it| it['planBeforeAction'] ? it['planBeforeAction']['name'] : '--'}},
|
363
|
+
{"Compute Usage" => lambda {|it|
|
364
|
+
usage = (it['config'] ? it['config']['cpuUsageAvg'] || 0 : 0).round(2).to_s
|
365
|
+
"#{format_percent(usage)} of 100%".ljust(25, ' ') + generate_usage_bar(usage, 100, {:max_bars => max_bars}) + cyan
|
366
|
+
}},
|
367
|
+
{"Network Usage" => lambda {|it|
|
368
|
+
max = 1024 * 1024 * 1024
|
369
|
+
usage = (it['config']['networkBandwidthAvg'] || 0) > 0 ? ((it['config']['networkBandwidthAvg'] || 0) / max.to_f) * 100 : 0
|
370
|
+
"#{format_bytes((it['config']['networkBandwidthAvg'] || 0), 'auto', 1)} of #{format_bytes(max, 'auto', 1)}".ljust(25, ' ') + generate_usage_bar( usage, 100, {:max_bars => max_bars}) + cyan
|
371
|
+
}}
|
372
|
+
]
|
373
|
+
|
374
|
+
print_description_list(cols, discovery, options.merge({:wrap => false}))
|
375
|
+
|
376
|
+
print_h2 "After Shutdown"
|
377
|
+
cols = [
|
378
|
+
{"Plan" => lambda {|it| it['planAfterAction'] ? it['planAfterAction']['name'] : '--'}},
|
379
|
+
{"Monthly Savings" => lambda {|it| format_money(it['savings']['amount'], it['savings']['currency'], {:minus_color => red})}}
|
380
|
+
]
|
381
|
+
print_description_list(cols, discovery, options)
|
382
|
+
elsif discovery['type']['code'] == 'reservations'
|
383
|
+
print_h2 "Current Cost"
|
384
|
+
cols = [
|
385
|
+
{"Current Cost" => lambda {|it| format_money((it['onDemandCost'] || 0) + (it['reservedCost'] || 0), discovery['savings']['currency'], {:minus_color => red})}},
|
386
|
+
{"On-Demand Cost" => lambda {|it| format_money((it['onDemandCost'] || 0), discovery['savings']['currency'], {:minus_color => red})}},
|
387
|
+
#{"Proposed Cost" => lambda {|it| format_money((it['recommendedCost'] || 0), discovery['savings']['currency'], {:minus_color => red})}}
|
388
|
+
]
|
389
|
+
|
390
|
+
print_description_list(cols, discovery['config']['summary'], options)
|
391
|
+
|
392
|
+
print_h2 "After Reservations"
|
393
|
+
cols = [
|
394
|
+
{"Proposed Cost" => lambda {|it| format_money((it['recommendedCost'] || 0), discovery['savings']['currency'], {:minus_color => red})}},
|
395
|
+
{"Monthly Savings" => lambda {|it| format_money(discovery['savings']['amount'], discovery['savings']['currency'], {:minus_color => red})}},
|
396
|
+
{"Savings Percent" => lambda {|it| format_percent(it['totalSavingsPercent'] * 100.0)}}
|
397
|
+
]
|
398
|
+
print_description_list(cols, discovery['config']['summary'], options)
|
399
|
+
|
400
|
+
cols = [
|
401
|
+
{"Name" => lambda {|it| it['name']}},
|
402
|
+
{"Region" => lambda {|it| it['region']}},
|
403
|
+
{"Term" => lambda {|it| it['term']}},
|
404
|
+
{"Current Cost" => lambda {|it| format_money(it['onDemandCost'], discovery['savings']['currency'], {:minus_color => red})}},
|
405
|
+
{"Quantity" => lambda {|it| it['recommendedCount']}},
|
406
|
+
{"Proposed Cost" => lambda {|it| format_money(it['recommendedCost'], discovery['savings']['currency'], {:minus_color => red})}},
|
407
|
+
{"Savings" => lambda {|it| format_money(it['totalSavings'], discovery['savings']['currency'], {:minus_color => red})}}
|
408
|
+
]
|
409
|
+
print_h2 "Details"
|
410
|
+
print as_pretty_table(discovery['config']['detailList'], cols, options)
|
411
|
+
end
|
412
|
+
|
413
|
+
print reset,"\n"
|
414
|
+
return 0
|
415
|
+
rescue RestClient::Exception => e
|
416
|
+
print_rest_exception(e, options)
|
417
|
+
return 1
|
418
|
+
end
|
419
|
+
end
|
420
|
+
|
421
|
+
def execute(args)
|
422
|
+
options = {}
|
423
|
+
optparse = Morpheus::Cli::OptionParser.new do |opts|
|
424
|
+
opts.banner = subcommand_usage("[id]")
|
425
|
+
build_standard_remove_options(opts, options)
|
426
|
+
opts.footer = "Get details about a specific discovery."
|
427
|
+
end
|
428
|
+
optparse.parse!(args)
|
429
|
+
if args.count < 1
|
430
|
+
puts optparse
|
431
|
+
return 1
|
432
|
+
end
|
433
|
+
connect(options)
|
434
|
+
_resolve_action(args[0], options)
|
435
|
+
end
|
436
|
+
|
437
|
+
def ignore(args)
|
438
|
+
options = {}
|
439
|
+
optparse = Morpheus::Cli::OptionParser.new do |opts|
|
440
|
+
opts.banner = subcommand_usage("[id]")
|
441
|
+
build_standard_remove_options(opts, options)
|
442
|
+
opts.footer = "Ignore discovery."
|
443
|
+
end
|
444
|
+
optparse.parse!(args)
|
445
|
+
if args.count < 1
|
446
|
+
puts optparse
|
447
|
+
return 1
|
448
|
+
end
|
449
|
+
connect(options)
|
450
|
+
_resolve_action(args[0], options, false)
|
451
|
+
end
|
452
|
+
|
453
|
+
private
|
454
|
+
|
455
|
+
def _resolve_action(id, options, is_exec=true)
|
456
|
+
begin
|
457
|
+
discovery = find_discovery(id)
|
458
|
+
|
459
|
+
if !discovery
|
460
|
+
print_red_alert "Discovery #{id} not found"
|
461
|
+
exit 1
|
462
|
+
end
|
463
|
+
|
464
|
+
if discovery['state'] != 'open'
|
465
|
+
print_green_success "#{discovery['actionTitle'].capitalize} action for #{discovery['refName']} already #{discovery['state']}."
|
466
|
+
return 0
|
467
|
+
end
|
468
|
+
|
469
|
+
unless options[:yes] || ::Morpheus::Cli::OptionTypes::confirm("Are you sure you would like to #{is_exec ? 'execute' : 'ignore'} the #{discovery['actionTitle'].capitalize} action for #{discovery['refName']}?", options)
|
470
|
+
return 9, "aborted command"
|
471
|
+
end
|
472
|
+
|
473
|
+
@guidance_interface.setopts(options)
|
474
|
+
if options[:dry_run]
|
475
|
+
print_dry_run (is_exec ? @guidance_interface.dry.exec(discovery['id']) : @guidance_interface.dry.ignore(discovery['id']))
|
476
|
+
return
|
477
|
+
end
|
478
|
+
|
479
|
+
json_response = (is_exec ? @guidance_interface.exec(discovery['id']) : @guidance_interface.ignore(discovery['id']))
|
480
|
+
|
481
|
+
if options[:json]
|
482
|
+
puts as_json(json_response, options)
|
483
|
+
elsif !options[:quiet]
|
484
|
+
if json_response['success']
|
485
|
+
print_green_success "Discovery successfully #{is_exec ? 'queued' : 'ignored'}"
|
486
|
+
else
|
487
|
+
print_red_alert "Error #{is_exec ? 'executing' : 'ignoring'} the #{discovery['actionTitle'].capitalize} action: #{json_response['msg'] || json_response['errors']}"
|
488
|
+
end
|
489
|
+
end
|
490
|
+
return 0
|
491
|
+
rescue RestClient::Exception => e
|
492
|
+
print_rest_exception(e, options)
|
493
|
+
return 1
|
494
|
+
end
|
495
|
+
end
|
496
|
+
|
497
|
+
def find_discovery(id)
|
498
|
+
@guidance_interface.get(id)['discovery']
|
499
|
+
end
|
500
|
+
|
501
|
+
def find_discovery_type(id)
|
502
|
+
@guidance_interface.types()['types'].find {|it| it['id'].to_s == id.to_s || it['code'] == id || it['name'] == id}
|
503
|
+
end
|
504
|
+
|
505
|
+
def format_status(status)
|
506
|
+
color = white
|
507
|
+
if ['on', 'ok', 'provisioned', 'success', 'complete'].include? status
|
508
|
+
color = green
|
509
|
+
elsif ['off', 'failed', 'denied', 'cancelled', 'error'].include? status
|
510
|
+
color = red
|
511
|
+
elsif ['suspended', 'warning', 'deprovisioning', 'expired'].include? status
|
512
|
+
color = yellow
|
513
|
+
elsif ['available'].include? status
|
514
|
+
color = blue
|
515
|
+
end
|
516
|
+
"#{color}#{status.capitalize}#{cyan}"
|
517
|
+
end
|
518
|
+
|
519
|
+
def action_title(type)
|
520
|
+
{
|
521
|
+
'shutdown' => 'Shutdown Resource',
|
522
|
+
'size' => 'Resize Resource',
|
523
|
+
'hostCapacity' => 'Add Capacity',
|
524
|
+
'hostBalancing' => 'Balance Host',
|
525
|
+
'datastoreCapacity' => 'Add Capacity',
|
526
|
+
'reservations' => 'Reserve Compute'
|
527
|
+
}[type['code']] || type['title']
|
528
|
+
end
|
529
|
+
end
|