morpheus-cli 5.0.2 → 5.2.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (38) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -0
  3. data/Dockerfile +1 -1
  4. data/lib/morpheus/api/api_client.rb +4 -0
  5. data/lib/morpheus/api/instances_interface.rb +30 -2
  6. data/lib/morpheus/api/invoices_interface.rb +12 -3
  7. data/lib/morpheus/api/servers_interface.rb +7 -0
  8. data/lib/morpheus/api/service_catalog_interface.rb +89 -0
  9. data/lib/morpheus/cli.rb +2 -1
  10. data/lib/morpheus/cli/apps.rb +3 -23
  11. data/lib/morpheus/cli/budgets_command.rb +389 -319
  12. data/lib/morpheus/cli/{catalog_command.rb → catalog_item_types_command.rb} +182 -67
  13. data/lib/morpheus/cli/cli_command.rb +25 -1
  14. data/lib/morpheus/cli/commands/standard/curl_command.rb +25 -10
  15. data/lib/morpheus/cli/commands/standard/history_command.rb +6 -2
  16. data/lib/morpheus/cli/containers_command.rb +0 -24
  17. data/lib/morpheus/cli/cypher_command.rb +6 -2
  18. data/lib/morpheus/cli/dashboard_command.rb +260 -20
  19. data/lib/morpheus/cli/health_command.rb +57 -0
  20. data/lib/morpheus/cli/hosts.rb +128 -11
  21. data/lib/morpheus/cli/instances.rb +270 -108
  22. data/lib/morpheus/cli/invoices_command.rb +67 -4
  23. data/lib/morpheus/cli/jobs_command.rb +94 -92
  24. data/lib/morpheus/cli/library_option_lists_command.rb +1 -1
  25. data/lib/morpheus/cli/library_option_types_command.rb +10 -5
  26. data/lib/morpheus/cli/mixins/accounts_helper.rb +5 -1
  27. data/lib/morpheus/cli/mixins/print_helper.rb +13 -6
  28. data/lib/morpheus/cli/mixins/provisioning_helper.rb +88 -5
  29. data/lib/morpheus/cli/option_types.rb +10 -10
  30. data/lib/morpheus/cli/projects_command.rb +1 -1
  31. data/lib/morpheus/cli/roles.rb +193 -155
  32. data/lib/morpheus/cli/service_catalog_command.rb +1474 -0
  33. data/lib/morpheus/cli/tasks.rb +9 -11
  34. data/lib/morpheus/cli/version.rb +1 -1
  35. data/lib/morpheus/cli/virtual_images.rb +162 -68
  36. data/lib/morpheus/formatters.rb +55 -20
  37. metadata +5 -4
  38. data/lib/morpheus/cli/mixins/catalog_helper.rb +0 -66
@@ -26,8 +26,8 @@ class Morpheus::Cli::CurlCommand
26
26
  options = {}
27
27
  optparse = Morpheus::Cli::OptionParser.new do|opts|
28
28
  opts.banner = "Usage: morpheus curl [path] -- [*args]"
29
- opts.on( '-p', '--pretty', "Print result as parsed JSON." ) do
30
- options[:pretty] = true
29
+ opts.on( '-p', '--pretty', "Print result as parsed JSON. Alias for -j" ) do
30
+ options[:json] = true
31
31
  end
32
32
  opts.on( '-X', '--request METHOD', "HTTP request method. Default is GET" ) do |val|
33
33
  curl_method = val
@@ -134,29 +134,44 @@ EOT
134
134
  print reset
135
135
  return 0
136
136
  end
137
+ exit_code, err = 0, nil
137
138
  # print cyan
138
139
  # print "#{cyan}#{curl_cmd_str}#{reset}"
139
140
  # print "\n\n"
140
141
  print reset
141
142
  # print result
142
143
  curl_output = `#{curl_cmd}`
143
- if options[:pretty] || options[:json]
144
+
145
+ if $?.success? != true
146
+ exit_code = $?.exitstatus
147
+ err = "curl command exited non-zero"
148
+ end
149
+ json_response = {}
150
+ other_output = nil
151
+ if options[:json] || options[:yaml] || options[:csv]
144
152
  output_lines = curl_output.split("\n")
145
153
  last_line = output_lines.pop
146
154
  if output_lines.size > 0
147
- puts output_lines.join("\n")
155
+ other_output = output_lines.join("\n")
148
156
  end
149
157
  begin
150
- puts as_json(JSON.parse(last_line), options)
158
+ json_response = JSON.parse(last_line)
151
159
  rescue => ex
152
- Morpheus::Logging::DarkPrinter.puts "failed to parse curl result as JSON data Error: #{ex.message}" if Morpheus::Logging.debug?
153
- puts last_line
160
+ puts_error curl_output
161
+ print_red_alert "failed to parse curl result as JSON data Error: #{ex.message}"
162
+
163
+ exit_code = 2
164
+ err = "failed to parse curl result as JSON data Error: #{ex.message}"
165
+ return exit_code, err
154
166
  end
155
167
  else
156
- puts curl_output
168
+ other_output = curl_output
157
169
  end
158
- return $?.success?
159
-
170
+ curl_object_key = nil # json_response.keys.first
171
+ render_response(json_response, options, curl_object_key) do
172
+ puts other_output
173
+ end
174
+ return exit_code, err
160
175
  end
161
176
 
162
177
  def command_available?(cmd)
@@ -15,7 +15,7 @@ class Morpheus::Cli::HistoryCommand
15
15
  def handle(args)
16
16
  options = {show_pagination:false}
17
17
  optparse = Morpheus::Cli::OptionParser.new do|opts|
18
- opts.banner = "Usage: morpheus #{command_name}"
18
+ opts.banner = "Usage: morpheus #{command_name} [search]"
19
19
  # -n is a hidden alias for -m
20
20
  opts.on( '-n', '--max-commands MAX', "Alias for -m, --max option." ) do |val|
21
21
  options[:max] = val
@@ -35,6 +35,7 @@ The --flush option can be used to purge the history.
35
35
  Examples:
36
36
  history
37
37
  history -m 100
38
+ history "instances list"
38
39
  history --flush
39
40
 
40
41
  The most recently executed commands are seen by default. Use --reverse to see the oldest commands.
@@ -42,7 +43,10 @@ EOT
42
43
  end
43
44
  raw_cmd = "#{command_name} #{args.join(' ')}"
44
45
  optparse.parse!(args)
45
- verify_args!(args:args, count: 0, optparse:optparse)
46
+ # verify_args!(args:args, count: 0, optparse:optparse)
47
+ if args.count > 0
48
+ options[:phrase] = args.join(" ")
49
+ end
46
50
  if options[:do_flush]
47
51
  command_count = Morpheus::Cli::Shell.instance.history_commands_count
48
52
  unless options[:yes] || Morpheus::Cli::OptionTypes.confirm("Are you sure you want to flush your command history (#{format_number(command_count)} #{command_count == 1 ? 'command' : 'commands'})?")
@@ -699,28 +699,4 @@ private
699
699
  end
700
700
  end
701
701
 
702
- def format_container_status(container, return_color=cyan)
703
- out = ""
704
- status_string = container['status'].to_s
705
- if status_string == 'running'
706
- out << "#{green}#{status_string.upcase}#{return_color}"
707
- elsif status_string == 'stopped' or status_string == 'failed'
708
- out << "#{red}#{status_string.upcase}#{return_color}"
709
- elsif status_string == 'unknown'
710
- out << "#{white}#{status_string.upcase}#{return_color}"
711
- else
712
- out << "#{yellow}#{status_string.upcase}#{return_color}"
713
- end
714
- out
715
- end
716
-
717
- def format_container_connection_string(container)
718
- if !container['ports'].nil? && container['ports'].empty? == false
719
- connection_string = "#{container['ip']}:#{container['ports'][0]['external']}"
720
- else
721
- # eh? more logic needed here i think, see taglib morph:containerLocationMenu
722
- connection_string = "#{container['ip']}"
723
- end
724
- end
725
-
726
702
  end
@@ -265,6 +265,9 @@ EOT
265
265
  opts.on( '-v', '--value VALUE', "Secret value. This can be used to store a string instead of an object, and can also be passed as the second argument." ) do |val|
266
266
  item_value = val
267
267
  end
268
+ # opts.on( '--type VALUE', String, "Type, default is based on key engine, string or object" ) do |val|
269
+ # params['type'] = val
270
+ # end
268
271
  opts.on( '-t', '--ttl SECONDS', "Time to live, the lease duration before this key expires." ) do |val|
269
272
  ttl = val
270
273
  if val.to_s.empty? || val.to_s == '0'
@@ -276,7 +279,7 @@ EOT
276
279
  # opts.on( '--no-overwrite', '--no-overwrite', "Do not overwrite existing keys. Existing keys are overwritten by default." ) do
277
280
  # params['overwrite'] = false
278
281
  # end
279
- build_common_options(opts, options, [:auto_confirm, :options, :payload, :json, :yaml, :csv, :fields, :outfile, :dry_run, :quiet, :remote])
282
+ build_common_options(opts, options, [:auto_confirm, :options, :payload, :query, :json, :yaml, :csv, :fields, :outfile, :dry_run, :quiet, :remote])
280
283
  opts.footer = "Create or update a cypher key." + "\n" +
281
284
  "[key] is required. This is the key of the cypher being created or updated. The key includes the mount prefix eg. secret/hello" + "\n" +
282
285
  "[value] is required for some cypher engines, such as secret. This is the secret value or k=v pairs being stored. Supports 1-N arguments." + "\n" +
@@ -292,7 +295,8 @@ EOT
292
295
  # return 1
293
296
  # end
294
297
  connect(options)
295
-
298
+ params.merge!(parse_query_options(options))
299
+
296
300
  # parse arguments like [value] or [k=v]
297
301
  if args.count == 0
298
302
  # prompt for key and value
@@ -4,8 +4,9 @@ require 'json'
4
4
 
5
5
  class Morpheus::Cli::DashboardCommand
6
6
  include Morpheus::Cli::CliCommand
7
+ include Morpheus::Cli::ProvisioningHelper
7
8
  set_command_name :dashboard
8
- set_command_hidden # remove once this is done
9
+ set_command_description "View Morpheus Dashboard"
9
10
 
10
11
  def initialize()
11
12
  # @appliance_name, @appliance_url = Morpheus::Cli::Remote.active_appliance
@@ -28,34 +29,273 @@ class Morpheus::Cli::DashboardCommand
28
29
  options = {}
29
30
  optparse = Morpheus::Cli::OptionParser.new do |opts|
30
31
  opts.banner = usage
31
- build_common_options(opts, options, [:json, :dry_run]) # todo: support :account
32
+ opts.on('-a', '--details', "Display all details: more instance usage stats, etc" ) do
33
+ options[:details] = true
34
+ end
35
+ build_standard_list_options(opts, options)
36
+ opts.footer = <<-EOT
37
+ View Morpheus Dashboard.
38
+ This includes instance and backup counts, favorite instances, monitoring and recent activity.
39
+ EOT
32
40
  end
33
41
  optparse.parse!(args)
34
-
42
+ verify_args!(args:args, optparse:optparse, count:0)
35
43
  connect(options)
36
- begin
37
- params = {}
38
- @dashboard_interface.setopts(options)
39
- if options[:dry_run]
40
- print_dry_run @dashboard_interface.dry.get(params)
41
- return
44
+ params = {}
45
+ params.merge!(parse_list_options(options))
46
+ @dashboard_interface.setopts(options)
47
+ if options[:dry_run]
48
+ print_dry_run @dashboard_interface.dry.get(params)
49
+ return
50
+ end
51
+ json_response = @dashboard_interface.get(params)
52
+ render_response(json_response, options) do
53
+ print_h1 "Morpheus Dashboard", [], options
54
+
55
+ ## STATUS
56
+
57
+ status_column_definitions = {
58
+ "Instances" => lambda {|it|
59
+ format_number(it['instanceStats']['total']) rescue nil
60
+ },
61
+ "Running" => lambda {|it|
62
+ format_number(it['instanceStats']['running']) rescue nil
63
+ },
64
+ # "Used Storage" => lambda {|it|
65
+ # ((it['instanceStats']['maxStorage'].to_i > 0) ? ((it['instanceStats']['usedStorage'].to_f / it['instanceStats']['maxStorage'].to_f) * 100).round(1) : 0).to_s + '%' rescue nil
66
+ # },
67
+ }
68
+ print as_description_list(json_response, status_column_definitions, options)
69
+ # print reset,"\n"
70
+
71
+ stats = json_response['instanceStats']
72
+ if stats
73
+ print_h2 "Instance Usage", options
74
+ print_stats_usage(stats, {include: [:max_cpu, :avg_cpu, :memory, :storage]})
42
75
  end
43
- json_response = @dashboard_interface.get(params)
44
- if options[:json]
45
- print JSON.pretty_generate(json_response)
46
- print "\n"
76
+
77
+
78
+
79
+ open_incident_count = json_response['monitoring']['openIncidents'] rescue (json_response['appStatus']['openIncidents'] rescue nil)
80
+
81
+ avg_response_time = json_response['monitoring']['avgResponseTime'] rescue nil
82
+ warning_apps = json_response['monitoring']['warningApps'] rescue 0
83
+ warning_checks = json_response['monitoring']['warningChecks'] rescue 0
84
+ fail_checks = json_response['monitoring']['failChecks'] rescue 0
85
+ fail_apps = json_response['monitoring']['failApps'] rescue 0
86
+ success_checks = json_response['monitoring']['successChecks'] rescue 0
87
+ success_apps = json_response['monitoring']['successApps'] rescue 0
88
+ monitoring_status_color = cyan
89
+ if fail_checks > 0 || fail_apps > 0
90
+ monitoring_status_color = red
91
+ elsif warning_checks > 0 || warning_apps > 0
92
+ monitoring_status_color = yellow
93
+ end
94
+
95
+ print_h2 "Monitoring"
96
+
97
+ monitoring_column_definitions = {
98
+ "Status" => lambda {|it|
99
+ if fail_apps > 0 # || fail_checks > 0
100
+ # check_summary = [fail_apps > 0 ? "#{fail_apps} Apps" : nil,fail_checks > 0 ? "#{fail_checks} Checks" : nil].compact.join(", ")
101
+ # red + "ERROR" + " (" + check_summary + ")" + cyan
102
+ red + "ERROR" + cyan
103
+ elsif warning_apps > 0 || warning_checks > 0
104
+ # check_summary = [warning_apps > 0 ? "#{warning_apps} Apps" : nil,warning_checks > 0 ? "#{warning_checks} Checks" : nil].compact.join(", ")
105
+ # red + "WARNING" + " (" + check_summary + ")" + cyan
106
+ yellow + "WARNING" + cyan
107
+ else
108
+ cyan + "HEALTHY" + cyan
109
+ end
110
+ },
111
+ # "Availability" => lambda {|it|
112
+ # # todo
113
+ # },
114
+ "Response Time" => lambda {|it|
115
+ # format_number(avg_response_time).to_s + "ms"
116
+ (avg_response_time.round).to_s + "ms"
117
+ },
118
+ "Open Incidents" => lambda {|it|
119
+ monitoring_status_color = cyan
120
+ # if fail_checks > 0 || fail_apps > 0
121
+ # monitoring_status_color = red
122
+ # elsif warning_checks > 0 || warning_apps > 0
123
+ # monitoring_status_color = yellow
124
+ # end
125
+ if open_incident_count.nil?
126
+ yellow + "n/a" + cyan + "\n"
127
+ elsif open_incident_count == 0
128
+ monitoring_status_color + "0 Open Incidents" + cyan
129
+ elsif open_incident_count == 1
130
+ monitoring_status_color + "1 Open Incident" + cyan
131
+ else
132
+ monitoring_status_color + "#{open_incident_count} Open Incidents" + cyan
133
+ end
134
+ }
135
+ }
136
+ #print as_description_list(json_response, monitoring_column_definitions, options)
137
+ print as_pretty_table([json_response], monitoring_column_definitions.upcase_keys!, options)
138
+
139
+
140
+ if json_response['logStats']
141
+ # todo: should come from monitoring.startMs-endMs
142
+ log_period_display = "7 Days"
143
+ print_h2 "Logs (#{log_period_display})", options
144
+ error_log_data = json_response['logStats']['data'].find {|it| it['key'].to_s.upcase == 'ERROR' }
145
+ error_count = error_log_data["count"] rescue 0
146
+ fatal_log_data = json_response['logStats']['data'].find {|it| it['key'].to_s.upcase == 'FATAL' }
147
+ fatal_count = fatal_log_data["count"] rescue 0
148
+ # total error is actaully error + fatal
149
+ total_error_count = error_count + fatal_count
150
+ # if total_error_count.nil?
151
+ # print yellow + "n/a" + cyan + "\n"
152
+ # elsif total_error_count == 0
153
+ # print cyan + "0 Errors" + cyan + "\n"
154
+ # elsif total_error_count == 1
155
+ # print red + "1 Error" + cyan + "\n"
156
+ # else
157
+ # print red + "#{total_error_count} Errors" + cyan + "\n"
158
+ # end
159
+ if total_error_count == 0
160
+ print cyan + "(0 Errors)" + cyan + "\n"
161
+ #print cyan + "0-0-0-0-0-0-0-0 (0 Errors)" + cyan + "\n"
162
+ end
163
+ if error_count > 0
164
+ if error_log_data["values"]
165
+ log_plot = ""
166
+ plot_index = 0
167
+ error_log_data["values"].each do |k, v|
168
+ if v.to_i == 0
169
+ log_plot << cyan + v.to_s
170
+ else
171
+ log_plot << red + v.to_s
172
+ end
173
+ if plot_index != error_log_data["values"].size - 1
174
+ log_plot << cyan + "-"
175
+ end
176
+ plot_index +=1
177
+ end
178
+ print log_plot
179
+ print " "
180
+ if error_count == 0
181
+ print cyan + "(0 Errors)" + cyan
182
+ elsif error_count == 1
183
+ print red + "(1 Errors)" + cyan
184
+ else
185
+ print red + "(#{error_count} Errors)" + cyan
186
+ end
187
+ print reset + "\n"
188
+ end
189
+ end
190
+ if fatal_count > 0
191
+ if fatal_log_data["values"]
192
+ log_plot = ""
193
+ plot_index = 0
194
+ fatal_log_data["values"].each do |k, v|
195
+ if v.to_i == 0
196
+ log_plot << cyan + v.to_s
197
+ else
198
+ log_plot << red + v.to_s
199
+ end
200
+ if plot_index != fatal_log_data["values"].size - 1
201
+ log_plot << cyan + "-"
202
+ end
203
+ plot_index +=1
204
+ end
205
+ print log_plot
206
+ print " "
207
+ if fatal_count == 0
208
+ print cyan + "(0 FATAL)" + cyan
209
+ elsif fatal_count == 1
210
+ print red + "(1 FATAL)" + cyan
211
+ else
212
+ print red + "(#{fatal_count} FATAL)" + cyan
213
+ end
214
+ print reset + "\n"
215
+ end
216
+ end
217
+ end
218
+
219
+ print_h2 "Backups (7 Days)"
220
+ backup_status_column_definitions = {
221
+ # "Total" => lambda {|it|
222
+ # it['backups']['accountStats']['lastSevenDays']['completed'] rescue nil
223
+ # },
224
+ "Successful" => lambda {|it|
225
+ it['backups']['accountStats']['lastSevenDays']['successful'] rescue nil
226
+ },
227
+ "Failed" => lambda {|it|
228
+ n = it['backups']['accountStats']['lastSevenDays']['failed'] rescue nil
229
+ if n == 0
230
+ cyan + n.to_s + reset
231
+ else
232
+ red + n.to_s + reset
233
+ end
234
+ }
235
+ }
236
+ print as_description_list(json_response, backup_status_column_definitions, options)
237
+ #print as_pretty_table([json_response], backup_status_column_definitions, options)
238
+ # print reset,"\n"
239
+
240
+ favorite_instances = json_response["provisioning"]["favoriteInstances"] || [] rescue []
241
+ if favorite_instances.empty?
242
+ # print cyan, "No favorite instances.",reset,"\n"
47
243
  else
244
+ print_h2 "My Instances"
245
+ favorite_instances_columns = {
246
+ "ID" => lambda {|instance|
247
+ instance['id']
248
+ },
249
+ "Name" => lambda {|instance|
250
+ instance['name']
251
+ },
252
+ "Type" => lambda {|instance|
253
+ instance['instanceType']['name'] rescue nil
254
+ },
255
+ "IP/PORT" => lambda {|instance|
256
+ format_instance_connection_string(instance)
257
+ },
258
+ "Status" => lambda {|it| format_instance_status(it) }
259
+ }
260
+ #print as_description_list(json_response, status_column_definitions, options)
261
+ print as_pretty_table(favorite_instances, favorite_instances_columns, options)
262
+ # print reset,"\n"
263
+ end
48
264
 
49
- print_h1 "Dashboard"
50
- print cyan
51
- puts "Coming soon... see --json"
52
- print reset,"\n"
265
+ # RECENT ACTIVITY
266
+ activity = json_response["activity"] || json_response["recentActivity"] || []
267
+ print_h2 "Recent Activity", [], options
268
+ if activity.empty?
269
+ print cyan, "No activity found.",reset,"\n"
270
+ else
271
+ columns = [
272
+ # {"SEVERITY" => lambda {|record| format_activity_severity(record['severity']) } },
273
+ {"TYPE" => lambda {|record| record['activityType'] } },
274
+ {"NAME" => lambda {|record| record['name'] } },
275
+ {"RESOURCE" => lambda {|record| "#{record['objectType']} #{record['objectId']}" } },
276
+ {"MESSAGE" => lambda {|record| record['message'] || '' } },
277
+ {"USER" => lambda {|record| record['user'] ? record['user']['username'] : record['userName'] } },
278
+ #{"DATE" => lambda {|record| "#{format_duration_ago(record['ts'] || record['timestamp'])}" } },
279
+ {"DATE" => lambda {|record|
280
+ # show full time if searching for custom timerange, otherwise the default is to show relative time
281
+ if params['start'] || params['end'] || params['timeframe']
282
+ "#{format_local_dt(record['ts'] || record['timestamp'])}"
283
+ else
284
+ "#{format_duration_ago(record['ts'] || record['timestamp'])}"
285
+ end
286
+
287
+ } },
288
+ ]
289
+ print as_pretty_table(activity, columns, options)
290
+ # print_results_pagination(json_response)
291
+ # print reset,"\n"
53
292
 
54
293
  end
55
- rescue RestClient::Exception => e
56
- print_rest_exception(e, options)
57
- exit 1
294
+
58
295
  end
296
+ print reset,"\n"
297
+ return 0, nil
59
298
  end
60
299
 
300
+
61
301
  end