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.
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