morpheus-cli 4.2.6 → 4.2.7

Sign up to get free protection for your applications and to get access to all the features.
Files changed (51) hide show
  1. checksums.yaml +4 -4
  2. data/Dockerfile +1 -1
  3. data/lib/morpheus/api/api_client.rb +4 -0
  4. data/lib/morpheus/api/clouds_interface.rb +14 -0
  5. data/lib/morpheus/api/guidance_interface.rb +47 -0
  6. data/lib/morpheus/api/users_interface.rb +7 -0
  7. data/lib/morpheus/cli.rb +1 -0
  8. data/lib/morpheus/cli/account_groups_command.rb +1 -1
  9. data/lib/morpheus/cli/approvals_command.rb +2 -2
  10. data/lib/morpheus/cli/apps.rb +26 -30
  11. data/lib/morpheus/cli/blueprints_command.rb +1 -1
  12. data/lib/morpheus/cli/budgets_command.rb +2 -2
  13. data/lib/morpheus/cli/change_password_command.rb +0 -1
  14. data/lib/morpheus/cli/cli_command.rb +19 -9
  15. data/lib/morpheus/cli/clouds.rb +107 -10
  16. data/lib/morpheus/cli/clusters.rb +12 -12
  17. data/lib/morpheus/cli/commands/standard/curl_command.rb +7 -0
  18. data/lib/morpheus/cli/deployments.rb +2 -2
  19. data/lib/morpheus/cli/environments_command.rb +1 -1
  20. data/lib/morpheus/cli/execution_request_command.rb +1 -1
  21. data/lib/morpheus/cli/groups.rb +1 -1
  22. data/lib/morpheus/cli/guidance_command.rb +529 -0
  23. data/lib/morpheus/cli/hosts.rb +2 -10
  24. data/lib/morpheus/cli/instances.rb +31 -13
  25. data/lib/morpheus/cli/integrations_command.rb +1 -1
  26. data/lib/morpheus/cli/jobs_command.rb +2 -2
  27. data/lib/morpheus/cli/library_container_types_command.rb +4 -4
  28. data/lib/morpheus/cli/library_instance_types_command.rb +3 -3
  29. data/lib/morpheus/cli/library_spec_templates_command.rb +1 -1
  30. data/lib/morpheus/cli/load_balancers.rb +2 -2
  31. data/lib/morpheus/cli/mixins/print_helper.rb +43 -3
  32. data/lib/morpheus/cli/mixins/provisioning_helper.rb +251 -165
  33. data/lib/morpheus/cli/network_routers_command.rb +1 -1
  34. data/lib/morpheus/cli/price_sets_command.rb +2 -2
  35. data/lib/morpheus/cli/provisioning_licenses_command.rb +1 -1
  36. data/lib/morpheus/cli/remote.rb +6 -1
  37. data/lib/morpheus/cli/reports_command.rb +1 -1
  38. data/lib/morpheus/cli/security_group_rules.rb +1 -1
  39. data/lib/morpheus/cli/security_groups.rb +13 -5
  40. data/lib/morpheus/cli/service_plans_command.rb +2 -2
  41. data/lib/morpheus/cli/user_groups_command.rb +2 -6
  42. data/lib/morpheus/cli/user_settings_command.rb +31 -5
  43. data/lib/morpheus/cli/user_sources_command.rb +3 -3
  44. data/lib/morpheus/cli/users.rb +117 -90
  45. data/lib/morpheus/cli/version.rb +1 -1
  46. data/lib/morpheus/cli/virtual_images.rb +2 -2
  47. data/lib/morpheus/cli/whitelabel_settings_command.rb +95 -15
  48. data/lib/morpheus/cli/wiki_command.rb +2 -2
  49. data/lib/morpheus/cli/workflows.rb +2 -3
  50. data/lib/morpheus/formatters.rb +14 -5
  51. 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 yellow,"No clusters found.",reset,"\n"
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 yellow,"No masters found.",reset,"\n"
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 yellow,"No workers found.",reset,"\n"
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 yellow,"No workers found.",reset,"\n"
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 yellow,"No masters found.",reset,"\n"
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 yellow,"No volumes found.",reset,"\n"
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 yellow,"No services found.",reset,"\n"
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 yellow,"No jobs found.",reset,"\n"
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 yellow,"No containers found.",reset,"\n"
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 yellow,"No #{resource_type}s found.",reset,"\n"
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 yellow,"No namespaces found.",reset,"\n"
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 yellow,"No datastores found.",reset,"\n"
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 yellow,"No deployments currently configured.",reset,"\n"
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 yellow,"No deployment versions currently exist.",reset,"\n"
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 yellow,"No Environments found.",reset
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
@@ -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