morpheus-cli 5.2.1 → 5.2.2

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.
@@ -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'})?")
@@ -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
@@ -8,7 +8,7 @@ class Morpheus::Cli::InvoicesCommand
8
8
 
9
9
  set_command_name :'invoices'
10
10
 
11
- register_subcommands :list, :get, :refresh,
11
+ register_subcommands :list, :get, :update, :refresh,
12
12
  :list_line_items, :get_line_item
13
13
 
14
14
  def connect(opts)
@@ -591,6 +591,68 @@ EOT
591
591
  end
592
592
  end
593
593
 
594
+ def update(args)
595
+ options = {}
596
+ params = {}
597
+ payload = {}
598
+ optparse = Morpheus::Cli::OptionParser.new do |opts|
599
+ opts.banner = subcommand_usage("[invoice] [options]")
600
+ opts.on('--tags LIST', String, "Tags in the format 'name:value, name:value'. This will add and remove tags.") do |val|
601
+ options[:tags] = val
602
+ end
603
+ opts.on('--add-tags TAGS', String, "Add Tags in the format 'name:value, name:value'. This will only add/update tags.") do |val|
604
+ options[:add_tags] = val
605
+ end
606
+ opts.on('--remove-tags TAGS', String, "Remove Tags in the format 'name, name:value'. This removes tags, the :value component is optional and must match if passed.") do |val|
607
+ options[:remove_tags] = val
608
+ end
609
+ build_standard_update_options(opts, options)
610
+ opts.footer = <<-EOT
611
+ Update an invoice.
612
+ [invoice] is required. This is the id of an invoice.
613
+ EOT
614
+ end
615
+ optparse.parse!(args)
616
+ verify_args!(args:args, optparse:optparse, count:1)
617
+ connect(options)
618
+ json_response = @invoices_interface.get(args[0])
619
+ invoice = json_response['invoice']
620
+
621
+ invoice_payload = parse_passed_options(options)
622
+ if options[:tags]
623
+ invoice_payload['tags'] = parse_metadata(options[:tags])
624
+ end
625
+ if options[:add_tags]
626
+ invoice_payload['addTags'] = parse_metadata(options[:add_tags])
627
+ end
628
+ if options[:remove_tags]
629
+ invoice_payload['removeTags'] = parse_metadata(options[:remove_tags])
630
+ end
631
+
632
+ payload = {}
633
+ if options[:payload]
634
+ payload = options[:payload]
635
+ payload.deep_merge!({'invoice' => invoice_payload})
636
+ else
637
+ payload.deep_merge!({'invoice' => invoice_payload})
638
+ if invoice_payload.empty?
639
+ raise_command_error "Specify at least one option to update.\n#{optparse}"
640
+ end
641
+ end
642
+ @invoices_interface.setopts(options)
643
+ if options[:dry_run]
644
+ print_dry_run @invoices_interface.dry.update(invoice['id'], payload)
645
+ return
646
+ end
647
+ json_response = @invoices_interface.update(invoice['id'], payload)
648
+ invoice = json_response['invoice']
649
+ render_response(json_response, options, 'invoice') do
650
+ print_green_success "Updated invoice #{invoice['id']}"
651
+ return _get(invoice["id"], options)
652
+ end
653
+ return 0, nil
654
+ end
655
+
594
656
  def refresh(args)
595
657
  options = {}
596
658
  params = {}