morpheus-cli 2.10.3 → 2.11.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/bin/morpheus +5 -96
- data/lib/morpheus/api/api_client.rb +23 -1
- data/lib/morpheus/api/checks_interface.rb +106 -0
- data/lib/morpheus/api/incidents_interface.rb +102 -0
- data/lib/morpheus/api/monitoring_apps_interface.rb +47 -0
- data/lib/morpheus/api/monitoring_contacts_interface.rb +47 -0
- data/lib/morpheus/api/monitoring_groups_interface.rb +47 -0
- data/lib/morpheus/api/monitoring_interface.rb +36 -0
- data/lib/morpheus/api/option_type_lists_interface.rb +47 -0
- data/lib/morpheus/api/option_types_interface.rb +47 -0
- data/lib/morpheus/api/roles_interface.rb +0 -1
- data/lib/morpheus/api/setup_interface.rb +19 -9
- data/lib/morpheus/cli.rb +20 -1
- data/lib/morpheus/cli/accounts.rb +8 -3
- data/lib/morpheus/cli/app_templates.rb +19 -11
- data/lib/morpheus/cli/apps.rb +52 -37
- data/lib/morpheus/cli/cli_command.rb +229 -53
- data/lib/morpheus/cli/cli_registry.rb +48 -40
- data/lib/morpheus/cli/clouds.rb +55 -26
- data/lib/morpheus/cli/command_error.rb +12 -0
- data/lib/morpheus/cli/credentials.rb +68 -26
- data/lib/morpheus/cli/curl_command.rb +98 -0
- data/lib/morpheus/cli/dashboard_command.rb +2 -7
- data/lib/morpheus/cli/deployments.rb +4 -4
- data/lib/morpheus/cli/deploys.rb +1 -2
- data/lib/morpheus/cli/dot_file.rb +5 -8
- data/lib/morpheus/cli/error_handler.rb +179 -15
- data/lib/morpheus/cli/groups.rb +21 -13
- data/lib/morpheus/cli/hosts.rb +220 -110
- data/lib/morpheus/cli/instance_types.rb +2 -2
- data/lib/morpheus/cli/instances.rb +257 -167
- data/lib/morpheus/cli/key_pairs.rb +15 -9
- data/lib/morpheus/cli/library.rb +673 -27
- data/lib/morpheus/cli/license.rb +2 -2
- data/lib/morpheus/cli/load_balancers.rb +4 -4
- data/lib/morpheus/cli/log_level_command.rb +6 -4
- data/lib/morpheus/cli/login.rb +17 -3
- data/lib/morpheus/cli/logout.rb +25 -11
- data/lib/morpheus/cli/man_command.rb +388 -0
- data/lib/morpheus/cli/mixins/accounts_helper.rb +1 -1
- data/lib/morpheus/cli/mixins/monitoring_helper.rb +434 -0
- data/lib/morpheus/cli/mixins/print_helper.rb +620 -112
- data/lib/morpheus/cli/mixins/provisioning_helper.rb +1 -1
- data/lib/morpheus/cli/monitoring_apps_command.rb +29 -0
- data/lib/morpheus/cli/monitoring_checks_command.rb +427 -0
- data/lib/morpheus/cli/monitoring_contacts_command.rb +373 -0
- data/lib/morpheus/cli/monitoring_groups_command.rb +29 -0
- data/lib/morpheus/cli/monitoring_incidents_command.rb +711 -0
- data/lib/morpheus/cli/option_types.rb +10 -1
- data/lib/morpheus/cli/recent_activity_command.rb +2 -5
- data/lib/morpheus/cli/remote.rb +874 -134
- data/lib/morpheus/cli/roles.rb +54 -27
- data/lib/morpheus/cli/security_group_rules.rb +2 -2
- data/lib/morpheus/cli/security_groups.rb +23 -19
- data/lib/morpheus/cli/set_prompt_command.rb +50 -0
- data/lib/morpheus/cli/shell.rb +222 -157
- data/lib/morpheus/cli/tasks.rb +19 -15
- data/lib/morpheus/cli/users.rb +27 -17
- data/lib/morpheus/cli/version.rb +1 -1
- data/lib/morpheus/cli/virtual_images.rb +28 -13
- data/lib/morpheus/cli/whoami.rb +131 -52
- data/lib/morpheus/cli/workflows.rb +24 -9
- data/lib/morpheus/formatters.rb +195 -3
- data/lib/morpheus/logging.rb +86 -0
- data/lib/morpheus/terminal.rb +371 -0
- data/scripts/generate_morpheus_commands_help.morpheus +60 -0
- metadata +21 -2
@@ -243,7 +243,7 @@ module Morpheus::Cli::AccountsHelper
|
|
243
243
|
def print_users_table(users, opts={})
|
244
244
|
table_color = opts[:color] || cyan
|
245
245
|
rows = users.collect do |user|
|
246
|
-
{id: user['id'], username: user['username'], first: user['firstName'], last: user['lastName'], email: user['email'], role: format_user_role_names(user), account: user['account'] ? user['account']['name'] : nil}
|
246
|
+
{id: user['id'], username: user['username'], name: user['displayName'], first: user['firstName'], last: user['lastName'], email: user['email'], role: format_user_role_names(user), account: user['account'] ? user['account']['name'] : nil}
|
247
247
|
end
|
248
248
|
print table_color
|
249
249
|
tp rows, :id, :account, :first, :last, :username, :email, :role
|
@@ -0,0 +1,434 @@
|
|
1
|
+
require 'morpheus/cli/mixins/print_helper'
|
2
|
+
require 'morpheus/cli/option_types'
|
3
|
+
require 'morpheus/rest_client'
|
4
|
+
# Mixin for Morpheus::Cli command classes
|
5
|
+
# Provides common methods for the monitoring domain, incidents, checks, 'n such
|
6
|
+
module Morpheus::Cli::MonitoringHelper
|
7
|
+
|
8
|
+
def self.included(klass)
|
9
|
+
klass.send :include, Morpheus::Cli::PrintHelper
|
10
|
+
end
|
11
|
+
|
12
|
+
def monitoring_interface
|
13
|
+
# @api_client.monitoring
|
14
|
+
raise "#{self.class} has not defined @monitoring_interface" if @monitoring_interface.nil?
|
15
|
+
@monitoring_interface
|
16
|
+
end
|
17
|
+
|
18
|
+
def find_check_by_name_or_id(val)
|
19
|
+
if val.to_s =~ /\A\d{1,}\Z/
|
20
|
+
return find_check_by_id(val)
|
21
|
+
else
|
22
|
+
return find_check_by_name(val)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def find_check_by_id(id)
|
27
|
+
begin
|
28
|
+
json_response = monitoring_interface.checks.get(id.to_i)
|
29
|
+
return json_response['check']
|
30
|
+
rescue RestClient::Exception => e
|
31
|
+
if e.response && e.response.code == 404
|
32
|
+
print_red_alert "Check not found by id #{id}"
|
33
|
+
exit 1
|
34
|
+
else
|
35
|
+
raise e
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def find_check_by_name(name)
|
41
|
+
json_results = monitoring_interface.checks.list({name: name})
|
42
|
+
if json_results['checks'].empty?
|
43
|
+
print_red_alert "Check not found by name #{name}"
|
44
|
+
exit 1
|
45
|
+
end
|
46
|
+
check = json_results['checks'][0]
|
47
|
+
return check
|
48
|
+
end
|
49
|
+
|
50
|
+
# def find_incident_by_name_or_id(val)
|
51
|
+
# if val.to_s =~ /\A\d{1,}\Z/
|
52
|
+
# return find_incident_by_id(val)
|
53
|
+
# else
|
54
|
+
# return find_incident_by_name(val)
|
55
|
+
# end
|
56
|
+
# end
|
57
|
+
|
58
|
+
def find_incident_by_id(id)
|
59
|
+
begin
|
60
|
+
json_response = monitoring_interface.incidents.get(id.to_i)
|
61
|
+
return json_response['incident']
|
62
|
+
rescue RestClient::Exception => e
|
63
|
+
if e.response && e.response.code == 404
|
64
|
+
print_red_alert "Incident not found by id #{id}"
|
65
|
+
exit 1
|
66
|
+
else
|
67
|
+
raise e
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
# def find_incident_by_name(name)
|
73
|
+
# json_results = monitoring_interface.incidents.get({name: name})
|
74
|
+
# if json_results['incidents'].empty?
|
75
|
+
# print_red_alert "Incident not found by name #{name}"
|
76
|
+
# exit 1
|
77
|
+
# end
|
78
|
+
# incident = json_results['incidents'][0]
|
79
|
+
# return incident
|
80
|
+
# end
|
81
|
+
|
82
|
+
|
83
|
+
def get_available_check_types(refresh=false)
|
84
|
+
if !@available_check_types || refresh
|
85
|
+
# @available_check_types = [{name: 'A Fake Check Type', code: 'achecktype'}]
|
86
|
+
# todo: use options api instead probably...
|
87
|
+
@available_check_types = check_types_interface.list_check_types['checkTypes']
|
88
|
+
end
|
89
|
+
return @available_check_types
|
90
|
+
end
|
91
|
+
|
92
|
+
def check_type_for_name_or_id(val)
|
93
|
+
if val.to_s =~ /\A\d{1,}\Z/
|
94
|
+
return check_type_for_id(val)
|
95
|
+
else
|
96
|
+
return check_type_for_name(val)
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
def check_type_for_id(id)
|
101
|
+
return get_available_check_types().find { |z| z['id'].to_i == id.to_i}
|
102
|
+
end
|
103
|
+
|
104
|
+
def check_type_for_name(name)
|
105
|
+
return get_available_check_types().find { |z| z['name'].downcase == name.downcase || z['code'].downcase == name.downcase}
|
106
|
+
end
|
107
|
+
|
108
|
+
def format_severity(severity, return_color=cyan)
|
109
|
+
out = ""
|
110
|
+
status_string = severity
|
111
|
+
if status_string == 'critical'
|
112
|
+
out << "#{red}#{status_string.capitalize}#{return_color}"
|
113
|
+
elsif status_string == 'warning'
|
114
|
+
out << "#{yellow}#{status_string.capitalize}#{return_color}"
|
115
|
+
elsif status_string == 'info'
|
116
|
+
out << "#{cyan}#{status_string.capitalize}#{return_color}"
|
117
|
+
else
|
118
|
+
out << "#{cyan}#{status_string}#{return_color}"
|
119
|
+
end
|
120
|
+
out
|
121
|
+
end
|
122
|
+
|
123
|
+
def format_monitoring_issue_attachment_type(issue)
|
124
|
+
if issue["app"]
|
125
|
+
"App"
|
126
|
+
elsif issue["check"]
|
127
|
+
"Check"
|
128
|
+
elsif issue["checkGroup"]
|
129
|
+
"Group"
|
130
|
+
else
|
131
|
+
"Severity Change"
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
def format_monitoring_incident_status(incident)
|
136
|
+
status_string = incident['status']
|
137
|
+
if status_string == 'closed'
|
138
|
+
"closed ✓"
|
139
|
+
else
|
140
|
+
status_string
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
def format_monitoring_issue_status(issue)
|
145
|
+
format_monitoring_incident_status(issue)
|
146
|
+
end
|
147
|
+
|
148
|
+
|
149
|
+
|
150
|
+
# Incidents
|
151
|
+
|
152
|
+
def print_incidents_table(incidents, opts={})
|
153
|
+
columns = [
|
154
|
+
{"ID" => lambda {|incident| incident['id'] } },
|
155
|
+
{"SEVERITY" => lambda {|incident| format_severity(incident['severity']) } },
|
156
|
+
{"NAME" => lambda {|incident| incident['name'] || 'No Subject' } },
|
157
|
+
{"TIME" => lambda {|incident| format_local_dt(incident['startDate']) } },
|
158
|
+
{"STATUS" => lambda {|incident| format_monitoring_incident_status(incident) } },
|
159
|
+
{"DURATION" => lambda {|incident| format_duration(incident['startDate'], incident['endDate']) } }
|
160
|
+
]
|
161
|
+
if opts[:include_fields]
|
162
|
+
columns = opts[:include_fields]
|
163
|
+
end
|
164
|
+
print as_pretty_table(incidents, columns, opts)
|
165
|
+
end
|
166
|
+
|
167
|
+
def print_incident_history_table(history_items, opts={})
|
168
|
+
columns = [
|
169
|
+
# {"ID" => lambda {|issue| issue['id'] } },
|
170
|
+
{"SEVERITY" => lambda {|issue| format_severity(issue['severity']) } },
|
171
|
+
{"AVAILABLE" => lambda {|issue| format_boolean issue['available'] } },
|
172
|
+
{"TYPE" => lambda {|issue| issue["attachmentType"] } },
|
173
|
+
{"NAME" => lambda {|issue| issue['name'] } },
|
174
|
+
{"DATE CREATED" => lambda {|issue| format_local_dt(issue['startDate']) } }
|
175
|
+
]
|
176
|
+
if opts[:include_fields]
|
177
|
+
columns = opts[:include_fields]
|
178
|
+
end
|
179
|
+
print as_pretty_table(history_items, columns, opts)
|
180
|
+
end
|
181
|
+
|
182
|
+
def print_incident_notifications_table(notifications, opts={})
|
183
|
+
columns = [
|
184
|
+
{"NAME" => lambda {|notification| notification['recipient'] ? notification['recipient']['name'] : '' } },
|
185
|
+
{"DELIVERY TYPE" => lambda {|notification| notification['addressTypes'].to_s } },
|
186
|
+
{"NOTIFIED ON" => lambda {|notification| format_local_dt(notification['dateCreated']) } },
|
187
|
+
# {"AVAILABLE" => lambda {|notification| format_boolean notification['available'] } },
|
188
|
+
# {"TYPE" => lambda {|notification| notification["attachmentType"] } },
|
189
|
+
# {"NAME" => lambda {|notification| notification['name'] } },
|
190
|
+
{"DATE CREATED" => lambda {|notification|
|
191
|
+
date_str = format_local_dt(notification['startDate']).to_s
|
192
|
+
if notification['pendingUtil']
|
193
|
+
"(pending) #{date_str}"
|
194
|
+
else
|
195
|
+
date_str
|
196
|
+
end
|
197
|
+
} }
|
198
|
+
]
|
199
|
+
#event['pendingUntil']
|
200
|
+
if opts[:include_fields]
|
201
|
+
columns = opts[:include_fields]
|
202
|
+
end
|
203
|
+
print as_pretty_table(notifications, columns, opts)
|
204
|
+
end
|
205
|
+
|
206
|
+
|
207
|
+
# Checks
|
208
|
+
|
209
|
+
def format_monitoring_check_status(check, return_color=cyan)
|
210
|
+
#<morph:statusIcon unknown="${!check.lastRunDate}" muted="${!check.createIncident}" failure="${check.lastCheckStatus == 'error'}" health="${check.health}" class="pull-left"/>
|
211
|
+
out = ""
|
212
|
+
muted = !check['createIncident']
|
213
|
+
status_string = check['lastCheckStatus'].to_s
|
214
|
+
failure = check['lastCheckStatus'] == 'error'
|
215
|
+
health = check['health'] # todo: examine at this too?
|
216
|
+
if failure
|
217
|
+
out << "#{red}#{status_string.capitalize}#{return_color}"
|
218
|
+
else
|
219
|
+
out << "#{cyan}#{status_string.capitalize}#{return_color}"
|
220
|
+
end
|
221
|
+
if muted
|
222
|
+
out << "(muted)"
|
223
|
+
end
|
224
|
+
out
|
225
|
+
end
|
226
|
+
|
227
|
+
def format_monitoring_check_last_metric(check)
|
228
|
+
#<td class="last-metric-col">${check.lastMetric} ${check.lastMetric ? checkTypes.find{ type -> type.id == check.checkTypeId}?.metricName : ''}</td>
|
229
|
+
out = ""
|
230
|
+
out << "#{check['lastMetric']} "
|
231
|
+
# todo:
|
232
|
+
out.strip
|
233
|
+
end
|
234
|
+
|
235
|
+
def format_monitoring_check_type(check)
|
236
|
+
#<td class="check-type-col"><div class="check-type-icon ${morph.checkTypeCode(id:check.checkTypeId, checkTypes:checkTypes)}"></div></td>
|
237
|
+
# return get_object_value(check, 'checkType.name') # this works too
|
238
|
+
out = ""
|
239
|
+
if check && check['checkType'] && check['checkType']['name']
|
240
|
+
out << check['checkType']['name']
|
241
|
+
elsif check['checkTypeId']
|
242
|
+
out << check['checkTypeId'].to_s
|
243
|
+
elsif !check.empty?
|
244
|
+
out << check.to_s
|
245
|
+
end
|
246
|
+
out.strip! + "WEEEEEE"
|
247
|
+
end
|
248
|
+
|
249
|
+
def print_checks_table(incidents, opts={})
|
250
|
+
columns = [
|
251
|
+
{"ID" => lambda {|check| check['id'] } },
|
252
|
+
{"STATUS" => lambda {|check| format_monitoring_check_status(check) } },
|
253
|
+
{"NAME" => lambda {|check| check['name'] } },
|
254
|
+
{"TIME" => lambda {|check| format_local_dt(check['lastRunDate']) } },
|
255
|
+
{"AVAILABILITY" => {display_method: lambda {|check| check['availability'] ? "#{check['availability'].to_f.round(3).to_s}%" : "N/A"} }, justify: "center" },
|
256
|
+
{"RESPONSE TIME" => {display_method: lambda {|check| check['lastTimer'] ? "#{check['lastTimer']}ms" : "N/A" } }, justify: "center" },
|
257
|
+
{"LAST METRIC" => {display_method: lambda {|check| check['lastMetric'] ? "#{check['lastMetric']}" : "N/A" } }, justify: "center" },
|
258
|
+
{"TYPE" => 'checkType.name'},
|
259
|
+
|
260
|
+
]
|
261
|
+
if opts[:include_fields]
|
262
|
+
columns = opts[:include_fields]
|
263
|
+
end
|
264
|
+
print as_pretty_table(incidents, columns, opts)
|
265
|
+
end
|
266
|
+
|
267
|
+
def print_check_history_table(history_items, opts={})
|
268
|
+
columns = [
|
269
|
+
# {"ID" => lambda {|issue| issue['id'] } },
|
270
|
+
{"SEVERITY" => lambda {|issue| format_severity(issue['severity']) } },
|
271
|
+
{"AVAILABLE" => lambda {|issue| format_boolean issue['available'] } },
|
272
|
+
{"TYPE" => lambda {|issue| issue["attachmentType"] } },
|
273
|
+
{"NAME" => lambda {|issue| issue['name'] } },
|
274
|
+
{"DATE CREATED" => lambda {|issue| format_local_dt(issue['startDate']) } }
|
275
|
+
]
|
276
|
+
if opts[:include_fields]
|
277
|
+
columns = opts[:include_fields]
|
278
|
+
end
|
279
|
+
print as_pretty_table(history_items, columns, opts)
|
280
|
+
end
|
281
|
+
|
282
|
+
def print_check_notifications_table(notifications, opts={})
|
283
|
+
columns = [
|
284
|
+
{"NAME" => lambda {|notification| notification['recipient'] ? notification['recipient']['name'] : '' } },
|
285
|
+
{"DELIVERY TYPE" => lambda {|notification| notification['addressTypes'].to_s } },
|
286
|
+
{"NOTIFIED ON" => lambda {|notification| format_local_dt(notification['dateCreated']) } },
|
287
|
+
# {"AVAILABLE" => lambda {|notification| format_boolean notification['available'] } },
|
288
|
+
# {"TYPE" => lambda {|notification| notification["attachmentType"] } },
|
289
|
+
# {"NAME" => lambda {|notification| notification['name'] } },
|
290
|
+
{"DATE CREATED" => lambda {|notification|
|
291
|
+
date_str = format_local_dt(notification['startDate']).to_s
|
292
|
+
if notification['pendingUtil']
|
293
|
+
"(pending) #{date_str}"
|
294
|
+
else
|
295
|
+
date_str
|
296
|
+
end
|
297
|
+
} }
|
298
|
+
]
|
299
|
+
#event['pendingUntil']
|
300
|
+
if opts[:include_fields]
|
301
|
+
columns = opts[:include_fields]
|
302
|
+
end
|
303
|
+
print as_pretty_table(notifications, columns, opts)
|
304
|
+
end
|
305
|
+
|
306
|
+
# Monitoring Contacts
|
307
|
+
|
308
|
+
def find_contact_by_name_or_id(val)
|
309
|
+
if val.to_s =~ /\A\d{1,}\Z/
|
310
|
+
return find_contact_by_id(val)
|
311
|
+
else
|
312
|
+
return find_contact_by_name(val)
|
313
|
+
end
|
314
|
+
end
|
315
|
+
|
316
|
+
def find_contact_by_id(id)
|
317
|
+
begin
|
318
|
+
json_response = monitoring_interface.contacts.get(id.to_i)
|
319
|
+
return json_response['contact']
|
320
|
+
rescue RestClient::Exception => e
|
321
|
+
if e.response && e.response.code == 404
|
322
|
+
print_red_alert "Contact not found by id #{id}"
|
323
|
+
exit 1 # return nil
|
324
|
+
else
|
325
|
+
raise e
|
326
|
+
end
|
327
|
+
end
|
328
|
+
end
|
329
|
+
|
330
|
+
def find_contact_by_name(name)
|
331
|
+
json_results = monitoring_interface.contacts.list({name: name})
|
332
|
+
contacts = json_results["contacts"]
|
333
|
+
if contacts.empty?
|
334
|
+
print_red_alert "Contact not found by name #{name}"
|
335
|
+
exit 1 # return nil
|
336
|
+
elsif contacts.size > 1
|
337
|
+
print_red_alert "#{contacts.size} Contacts found by name #{name}"
|
338
|
+
print "\n"
|
339
|
+
puts as_pretty_table(contacts, [{"ID" => "id" }, {"NAME" => "name"}], {color: red})
|
340
|
+
print_red_alert "Try passing ID instead"
|
341
|
+
print reset,"\n"
|
342
|
+
exit 1 # return nil
|
343
|
+
else
|
344
|
+
return contacts[0]
|
345
|
+
end
|
346
|
+
end
|
347
|
+
|
348
|
+
|
349
|
+
# Monitoring Check Groups
|
350
|
+
|
351
|
+
def find_check_group_by_name_or_id(val)
|
352
|
+
if val.to_s =~ /\A\d{1,}\Z/
|
353
|
+
return find_check_group_by_id(val)
|
354
|
+
else
|
355
|
+
return find_check_group_by_name(val)
|
356
|
+
end
|
357
|
+
end
|
358
|
+
|
359
|
+
def find_check_group_by_id(id)
|
360
|
+
begin
|
361
|
+
json_response = monitoring_interface.groups.get(id.to_i)
|
362
|
+
return json_response['checkGroup']
|
363
|
+
rescue RestClient::Exception => e
|
364
|
+
if e.response && e.response.code == 404
|
365
|
+
print_red_alert "Check Group not found by id #{id}"
|
366
|
+
exit 1 # return nil
|
367
|
+
else
|
368
|
+
raise e
|
369
|
+
end
|
370
|
+
end
|
371
|
+
end
|
372
|
+
|
373
|
+
def find_check_group_by_name(name)
|
374
|
+
json_results = monitoring_interface.groups.list({name: name})
|
375
|
+
groups = json_results["groups"]
|
376
|
+
if groups.empty?
|
377
|
+
print_red_alert "Check Group not found by name #{name}"
|
378
|
+
exit 1 # return nil
|
379
|
+
elsif groups.size > 1
|
380
|
+
print_red_alert "#{groups.size} Check Groups found by name #{name}"
|
381
|
+
print "\n"
|
382
|
+
puts as_pretty_table(groups, [{"ID" => "id" }, {"NAME" => "name"}], {color: red})
|
383
|
+
print_red_alert "Try passing ID instead"
|
384
|
+
print reset,"\n"
|
385
|
+
exit 1 # return nil
|
386
|
+
else
|
387
|
+
return groups[0]
|
388
|
+
end
|
389
|
+
end
|
390
|
+
|
391
|
+
# Monitoring apps
|
392
|
+
|
393
|
+
def find_app_by_name_or_id(val)
|
394
|
+
if val.to_s =~ /\A\d{1,}\Z/
|
395
|
+
return find_app_by_id(val)
|
396
|
+
else
|
397
|
+
return find_app_by_name(val)
|
398
|
+
end
|
399
|
+
end
|
400
|
+
|
401
|
+
def find_app_by_id(id)
|
402
|
+
begin
|
403
|
+
json_response = monitoring_interface.apps.get(id.to_i)
|
404
|
+
return json_response['app']
|
405
|
+
rescue RestClient::Exception => e
|
406
|
+
if e.response && e.response.code == 404
|
407
|
+
print_red_alert "Monitor App not found by id #{id}"
|
408
|
+
exit 1 # return nil
|
409
|
+
else
|
410
|
+
raise e
|
411
|
+
end
|
412
|
+
end
|
413
|
+
end
|
414
|
+
|
415
|
+
def find_app_by_name(name)
|
416
|
+
json_results = monitoring_interface.apps.list({name: name})
|
417
|
+
apps = json_results["apps"]
|
418
|
+
if apps.empty?
|
419
|
+
print_red_alert "Monitor App not found by name #{name}"
|
420
|
+
exit 1 # return nil
|
421
|
+
elsif apps.size > 1
|
422
|
+
print_red_alert "#{apps.size} apps found by name #{name}"
|
423
|
+
print "\n"
|
424
|
+
puts as_pretty_table(apps, [{"ID" => "id" }, {"NAME" => "name"}], {color: red})
|
425
|
+
print_red_alert "Try passing ID instead"
|
426
|
+
print reset,"\n"
|
427
|
+
exit 1 # return nil
|
428
|
+
else
|
429
|
+
return apps[0]
|
430
|
+
end
|
431
|
+
end
|
432
|
+
|
433
|
+
|
434
|
+
end
|
@@ -1,6 +1,9 @@
|
|
1
1
|
require 'uri'
|
2
2
|
require 'term/ansicolor'
|
3
3
|
require 'json'
|
4
|
+
require 'yaml'
|
5
|
+
require 'ostruct'
|
6
|
+
require 'io/console'
|
4
7
|
|
5
8
|
module Morpheus::Cli::PrintHelper
|
6
9
|
|
@@ -8,122 +11,81 @@ module Morpheus::Cli::PrintHelper
|
|
8
11
|
klass.send :include, Term::ANSIColor
|
9
12
|
end
|
10
13
|
|
11
|
-
def
|
12
|
-
|
14
|
+
def self.terminal_width
|
15
|
+
@@terminal_width ||= 80
|
13
16
|
end
|
14
17
|
|
15
|
-
def
|
16
|
-
|
18
|
+
def self.terminal_width=(v)
|
19
|
+
if v.nil? || v.to_i == 0
|
20
|
+
@@terminal_width = nil
|
21
|
+
else
|
22
|
+
@@terminal_width = v.to_i
|
23
|
+
end
|
24
|
+
@@terminal_width
|
17
25
|
end
|
18
26
|
|
19
|
-
def
|
20
|
-
|
27
|
+
def current_terminal_width
|
28
|
+
return IO.console.winsize[1] rescue 0
|
21
29
|
end
|
22
30
|
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
print reset, "\n"
|
29
|
-
else
|
30
|
-
if !response['success']
|
31
|
-
print red,bold
|
32
|
-
if response['msg']
|
33
|
-
puts response['msg']
|
34
|
-
end
|
35
|
-
if response['errors']
|
36
|
-
response['errors'].each do |key, value|
|
37
|
-
print "* #{key}: #{value}\n"
|
38
|
-
end
|
39
|
-
end
|
40
|
-
print reset
|
41
|
-
else
|
42
|
-
# this should not really happen
|
43
|
-
print cyan,bold, "\nSuccess!"
|
44
|
-
end
|
45
|
-
end
|
46
|
-
ensure
|
47
|
-
print reset
|
48
|
-
end
|
31
|
+
# puts red message to stderr
|
32
|
+
def print_red_alert(msg)
|
33
|
+
#$stderr.print "#{red}#{msg}#{reset}\n"
|
34
|
+
print "#{red}#{msg}#{reset}\n"
|
35
|
+
#puts_error "#{red}#{msg}#{reset}"
|
49
36
|
end
|
50
37
|
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
begin
|
55
|
-
print_rest_exception_request_and_response(e)
|
56
|
-
ensure
|
57
|
-
print reset
|
58
|
-
end
|
59
|
-
return
|
60
|
-
end
|
61
|
-
if e.response.code == 400
|
62
|
-
response = JSON.parse(e.response.to_s)
|
63
|
-
print_errors(response, options)
|
64
|
-
else
|
65
|
-
print_red_alert "Error Communicating with the Appliance. #{e}"
|
66
|
-
if options[:json] || options[:debug]
|
67
|
-
begin
|
68
|
-
response = JSON.parse(e.response.to_s)
|
69
|
-
print red
|
70
|
-
print JSON.pretty_generate(response)
|
71
|
-
print reset, "\n"
|
72
|
-
rescue TypeError, JSON::ParserError => ex
|
73
|
-
print_red_alert "Failed to parse JSON response: #{ex}"
|
74
|
-
print red
|
75
|
-
print response.to_s
|
76
|
-
print reset, "\n"
|
77
|
-
ensure
|
78
|
-
print reset
|
79
|
-
end
|
80
|
-
end
|
81
|
-
end
|
82
|
-
else
|
83
|
-
print_red_alert "Error Communicating with the Appliance. #{e}"
|
84
|
-
end
|
38
|
+
# puts green message to stdout
|
39
|
+
def print_green_success(msg)
|
40
|
+
print "#{green}#{msg}#{reset}\n"
|
85
41
|
end
|
86
42
|
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
#
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
43
|
+
# print_h1 prints a header title and optional subtitles
|
44
|
+
# Output:
|
45
|
+
#
|
46
|
+
# title - subtitle1, subtitle2
|
47
|
+
# ==================
|
48
|
+
#
|
49
|
+
def print_h1(title, subtitles=[], color=cyan)
|
50
|
+
#print "\n" ,color, bold, title, (subtitles.empty? ? "" : " - #{subtitles.join(', ')}"), "\n", "==================", reset, "\n\n"
|
51
|
+
subtitles = subtitles.flatten
|
52
|
+
out = ""
|
53
|
+
out << "\n"
|
54
|
+
out << "#{color}#{bold}#{title}#{reset}"
|
55
|
+
if !subtitles.empty?
|
56
|
+
out << "#{color} - #{subtitles.join(', ')}#{reset}"
|
57
|
+
end
|
58
|
+
out << "\n"
|
59
|
+
out << "#{color}#{bold}==================#{reset}"
|
60
|
+
out << "\n\n"
|
61
|
+
out << reset
|
62
|
+
print out
|
100
63
|
end
|
101
64
|
|
102
|
-
def
|
103
|
-
#
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
begin
|
111
|
-
print JSON.pretty_generate(JSON.parse(res.body))
|
112
|
-
rescue
|
113
|
-
print res.body.to_s
|
65
|
+
def print_h2(title, subtitles=[], color=cyan)
|
66
|
+
#print "\n" ,color, bold, title, (subtitles.empty? ? "" : " - #{subtitles.join(', ')}"), "\n", "---------------------", reset, "\n\n"
|
67
|
+
subtitles = subtitles.flatten
|
68
|
+
out = ""
|
69
|
+
out << "\n"
|
70
|
+
out << "#{color}#{bold}#{title}#{reset}"
|
71
|
+
if !subtitles.empty?
|
72
|
+
out << "#{color} - #{subtitles.join(', ')}#{reset}"
|
114
73
|
end
|
115
|
-
|
74
|
+
out << "\n"
|
75
|
+
out << "#{color}---------------------#{reset}"
|
76
|
+
out << "\n\n"
|
77
|
+
out << reset
|
78
|
+
print out
|
116
79
|
end
|
117
80
|
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
print reset
|
81
|
+
# @deprecated, use ErrorHandler.print_rest_exception()
|
82
|
+
def print_rest_exception(e, options={})
|
83
|
+
# ugh, time to clean this stuff up
|
84
|
+
if respond_to?(:my_terminal)
|
85
|
+
Morpheus::Cli::ErrorHandler.new(my_terminal.stderr).print_rest_exception(e, options)
|
86
|
+
else
|
87
|
+
Morpheus::Cli::ErrorHandler.new.print_rest_exception(e, options)
|
88
|
+
end
|
127
89
|
end
|
128
90
|
|
129
91
|
def print_dry_run(opts)
|
@@ -137,7 +99,7 @@ module Morpheus::Cli::PrintHelper
|
|
137
99
|
end
|
138
100
|
request_string = "#{http_method.to_s.upcase} #{url}".strip
|
139
101
|
payload = opts[:payload]
|
140
|
-
|
102
|
+
print_h1 "DRY RUN"
|
141
103
|
print cyan
|
142
104
|
print "Request: ", "\n"
|
143
105
|
print reset
|
@@ -160,10 +122,61 @@ module Morpheus::Cli::PrintHelper
|
|
160
122
|
print reset
|
161
123
|
end
|
162
124
|
|
163
|
-
def print_results_pagination(json_response)
|
164
|
-
|
165
|
-
|
125
|
+
def print_results_pagination(json_response, options={})
|
126
|
+
# print cyan,"\nViewing #{json_response['meta']['offset'].to_i + 1}-#{json_response['meta']['offset'].to_i + json_response['meta']['size'].to_i} of #{json_response['meta']['total']}\n", reset
|
127
|
+
print format_results_pagination(json_response, options)
|
128
|
+
end
|
129
|
+
|
130
|
+
def format_results_pagination(json_response, options={})
|
131
|
+
# no output for strange, empty data
|
132
|
+
if json_response.nil? || json_response.empty?
|
133
|
+
return ""
|
134
|
+
end
|
135
|
+
|
136
|
+
# options = OpenStruct.new(options) # laff, let's do this instead
|
137
|
+
color = options.key?(:color) ? options[:color] : cyan
|
138
|
+
label = options[:label]
|
139
|
+
n_label = options[:n_label]
|
140
|
+
# label = n_label if !label && n_label
|
141
|
+
message = options[:message] || "Viewing %{start_index}-%{end_index} of %{total} %{label}"
|
142
|
+
blank_message = options[:blank_message] || nil # "No %{label} found"
|
143
|
+
|
144
|
+
# support lazy passing of common json_response {"meta": {"size": {25}, "total": 56} }
|
145
|
+
# otherwise use the root values given
|
146
|
+
meta = OpenStruct.new(json_response)
|
147
|
+
if meta.meta
|
148
|
+
meta = OpenStruct.new(meta.meta)
|
149
|
+
end
|
150
|
+
offset, size, total = meta.offset.to_i, meta.size.to_i, meta.total.to_i
|
151
|
+
#objects = meta.objects || options[:objects_key] ? json_response[options[:objects_key]] : nil
|
152
|
+
#objects ||= meta.instances || meta.servers || meta.users || meta.roles
|
153
|
+
#size = objects.size if objects && size == 0
|
154
|
+
if total == 0
|
155
|
+
total = size
|
156
|
+
end
|
157
|
+
if total != 1
|
158
|
+
label = n_label || label
|
166
159
|
end
|
160
|
+
out_str = ""
|
161
|
+
string_key_values = {start_index: offset + 1, end_index: offset + size, total: total, size: size, offset: offset, label: label}
|
162
|
+
if size > 0
|
163
|
+
if message
|
164
|
+
out_str << message % string_key_values
|
165
|
+
end
|
166
|
+
else
|
167
|
+
if blank_message
|
168
|
+
out_str << blank_message % string_key_values
|
169
|
+
else
|
170
|
+
#out << "No records"
|
171
|
+
end
|
172
|
+
end
|
173
|
+
out = ""
|
174
|
+
out << "\n"
|
175
|
+
out << color if color
|
176
|
+
out << out_str.strip
|
177
|
+
out << reset if color
|
178
|
+
out << "\n"
|
179
|
+
out
|
167
180
|
end
|
168
181
|
|
169
182
|
def required_blue_prompt
|
@@ -184,7 +197,7 @@ module Morpheus::Cli::PrintHelper
|
|
184
197
|
else
|
185
198
|
percent = ((used_value.to_f / max_value.to_f) * 100)
|
186
199
|
end
|
187
|
-
percent_label = (used_value.nil? || max_value.to_f == 0.0) ? "n/a" : "#{percent.round(2)}%".rjust(6, ' ')
|
200
|
+
percent_label = ((used_value.nil? || max_value.to_f == 0.0) ? "n/a" : "#{percent.round(2)}%").rjust(6, ' ')
|
188
201
|
bar_display = ""
|
189
202
|
if percent > 100
|
190
203
|
max_bars.times { bars << "|" }
|
@@ -235,17 +248,25 @@ module Morpheus::Cli::PrintHelper
|
|
235
248
|
end
|
236
249
|
|
237
250
|
def print_stats_usage(stats, opts={})
|
251
|
+
label_width = opts[:label_width] || 10
|
252
|
+
out = ""
|
253
|
+
if stats.nil? || stats.empty?
|
254
|
+
out << cyan + "No data." + "\n" + reset
|
255
|
+
print out
|
256
|
+
return
|
257
|
+
end
|
238
258
|
opts[:include] ||= [:memory, :storage, :cpu]
|
259
|
+
if opts[:include].include?(:cpu)
|
260
|
+
cpu_usage = (stats['usedCpu'] || stats['cpuUsage'])
|
261
|
+
out << cyan + "CPU".rjust(label_width, ' ') + ": " + generate_usage_bar(cpu_usage.to_f, 100) + "\n"
|
262
|
+
end
|
239
263
|
if opts[:include].include?(:memory)
|
240
|
-
|
264
|
+
out << cyan + "Memory".rjust(label_width, ' ') + ": " + generate_usage_bar(stats['usedMemory'], stats['maxMemory']) + cyan + Filesize.from("#{stats['usedMemory']} B").pretty.strip.rjust(15, ' ') + " / " + Filesize.from("#{stats['maxMemory']} B").pretty.strip.ljust(15, ' ') + "\n"
|
241
265
|
end
|
242
266
|
if opts[:include].include?(:storage)
|
243
|
-
|
244
|
-
end
|
245
|
-
if opts[:include].include?(:cpu)
|
246
|
-
cpu_usage = (stats['usedCpu'] || stats['cpuUsage'])
|
247
|
-
print cyan, "CPU:".ljust(10, ' ') + generate_usage_bar(cpu_usage.to_f, 100) + "\n"
|
267
|
+
out << cyan + "Storage".rjust(label_width, ' ') + ": " + generate_usage_bar(stats['usedStorage'], stats['maxStorage']) + cyan + Filesize.from("#{stats['usedStorage']} B").pretty.strip.rjust(15, ' ') + " / " + Filesize.from("#{stats['maxStorage']} B").pretty.strip.ljust(15, ' ') + "\n"
|
248
268
|
end
|
269
|
+
print out
|
249
270
|
end
|
250
271
|
|
251
272
|
def print_available_options(option_types)
|
@@ -253,4 +274,491 @@ module Morpheus::Cli::PrintHelper
|
|
253
274
|
puts "Available Options:\n#{option_lines}\n\n"
|
254
275
|
end
|
255
276
|
|
277
|
+
def format_dt_dd(label, value, label_width=10, justify="right", do_wrap=true)
|
278
|
+
# JD: uncomment next line to do away with justified labels
|
279
|
+
# label_width, justify = 0, "none"
|
280
|
+
out = ""
|
281
|
+
value = value.to_s
|
282
|
+
if do_wrap && value && Morpheus::Cli::PrintHelper.terminal_width
|
283
|
+
value_width = Morpheus::Cli::PrintHelper.terminal_width - label_width
|
284
|
+
if value_width > 0 && value.to_s.size > value_width
|
285
|
+
wrap_indent = label_width + 1 # plus 1 needs to go away
|
286
|
+
value = wrap(value, value_width, wrap_indent)
|
287
|
+
end
|
288
|
+
end
|
289
|
+
if justify == "right"
|
290
|
+
out << "#{label}:".rjust(label_width, ' ') + " #{value}"
|
291
|
+
elsif justify == "left"
|
292
|
+
out << "#{label}:".ljust(label_width, ' ') + " #{value}"
|
293
|
+
else
|
294
|
+
# default is none
|
295
|
+
out << "#{label}:" + " #{value}"
|
296
|
+
end
|
297
|
+
out
|
298
|
+
end
|
299
|
+
|
300
|
+
# truncate_string truncates a string and appends the suffix "..."
|
301
|
+
# @param value [String] the string to pad
|
302
|
+
# @param width [Integer] the length to truncate to
|
303
|
+
# @param pad_char [String] the character to pad with. Default is ' '
|
304
|
+
def truncate_string(value, width, suffix="...")
|
305
|
+
value = value.to_s
|
306
|
+
# JD: hack alerty.. this sux, but it's a best effort to preserve values containing ascii coloring codes
|
307
|
+
# it stops working when there are words separated by ascii codes, eg. two diff colors
|
308
|
+
# plus this is probably pretty slow...
|
309
|
+
uncolored_value = Term::ANSIColor.coloring? ? Term::ANSIColor.uncolored(value.to_s) : value.to_s
|
310
|
+
if uncolored_value != value
|
311
|
+
trimmed_value = nil
|
312
|
+
if uncolored_value.size > width
|
313
|
+
if suffix
|
314
|
+
trimmed_value = uncolored_value[0..width-(suffix.size+1)] + suffix
|
315
|
+
else
|
316
|
+
trimmed_value = uncolored_value[0..width-1]
|
317
|
+
end
|
318
|
+
return value.gsub(uncolored_value, trimmed_value)
|
319
|
+
else
|
320
|
+
return value
|
321
|
+
end
|
322
|
+
else
|
323
|
+
if value.size > width
|
324
|
+
if suffix
|
325
|
+
return value[0..width-(suffix.size+1)] + suffix
|
326
|
+
else
|
327
|
+
return value[0..width-1]
|
328
|
+
end
|
329
|
+
else
|
330
|
+
return value
|
331
|
+
end
|
332
|
+
end
|
333
|
+
end
|
334
|
+
|
335
|
+
# justified returns a left, center, or right aligned string.
|
336
|
+
# @param value [String] the string to pad
|
337
|
+
# @param width [Integer] the length to truncate to
|
338
|
+
# @param pad_char [String] the character to pad with. Default is ' '
|
339
|
+
# @return [String]
|
340
|
+
def justify_string(value, width, justify="left", pad_char=" ")
|
341
|
+
# JD: hack alert! this sux, but it's a best effort to preserve values containing ascii coloring codes
|
342
|
+
value = value.to_s
|
343
|
+
uncolored_value = Term::ANSIColor.coloring? ? Term::ANSIColor.uncolored(value.to_s) : value.to_s
|
344
|
+
if value.size != uncolored_value.size
|
345
|
+
width = width + (value.size - uncolored_value.size)
|
346
|
+
end
|
347
|
+
if justify == "right"
|
348
|
+
return "#{value}".rjust(width, pad_char)
|
349
|
+
elsif justify == "center"
|
350
|
+
return "#{value}".center(width, pad_char)
|
351
|
+
else
|
352
|
+
return "#{value}".ljust(width, pad_char)
|
353
|
+
end
|
354
|
+
end
|
355
|
+
|
356
|
+
def format_table_cell(value, width, justify="left", pad_char=" ", suffix="...")
|
357
|
+
#puts "format_table_cell(#{value}, #{width}, #{justify}, #{pad_char.inspect})"
|
358
|
+
cell = value.to_s
|
359
|
+
cell = truncate_string(cell, width, suffix)
|
360
|
+
cell = justify_string(cell, width, justify, pad_char)
|
361
|
+
cell
|
362
|
+
end
|
363
|
+
|
364
|
+
# as_pretty_table generates a table with aligned columns and truncated values.
|
365
|
+
# This can be used in place of TablePrint.tp()
|
366
|
+
# @param data [Array] A list of objects to extract the data from.
|
367
|
+
# @param columns - [Array of Objects] list of column definitions, A column definition can be a String, Symbol, or Hash
|
368
|
+
# @return [String]
|
369
|
+
# Usage: puts as_pretty_table(my_objects, [:id, :name])
|
370
|
+
# puts as_pretty_table(my_objects, ["id", "name", {"plan" => "plan.name" }], {color: white})
|
371
|
+
#
|
372
|
+
def as_pretty_table(data, columns, options={})
|
373
|
+
data = [data].flatten
|
374
|
+
columns = build_column_definitions(columns)
|
375
|
+
|
376
|
+
table_color = options[:color] || cyan
|
377
|
+
cell_delim = options[:delim] || " | "
|
378
|
+
|
379
|
+
header_row = []
|
380
|
+
|
381
|
+
columns.each do |column_def|
|
382
|
+
header_row << column_def.label
|
383
|
+
end
|
384
|
+
|
385
|
+
# generate rows matrix data for the specified columns
|
386
|
+
rows = []
|
387
|
+
data.each do |row_data|
|
388
|
+
row = []
|
389
|
+
columns.each do |column_def|
|
390
|
+
# r << column_def.display_method.respond_to?(:call) ? column_def.display_method.call(row_data) : get_object_value(row_data, column_def.display_method)
|
391
|
+
value = column_def.display_method.call(row_data)
|
392
|
+
row << value
|
393
|
+
end
|
394
|
+
rows << row
|
395
|
+
end
|
396
|
+
|
397
|
+
# all rows (pre-formatted)
|
398
|
+
data_matrix = [header_row] + rows
|
399
|
+
|
400
|
+
# determine column meta info i.e. width
|
401
|
+
columns.each_with_index do |column_def, column_index|
|
402
|
+
# column_def.meta = {
|
403
|
+
# max_value_size: (header_row + rows).max {|row| row[column_index] ? row[column_index].to_s.size : 0 }.size
|
404
|
+
# }
|
405
|
+
if column_def.fixed_width
|
406
|
+
column_def.width = column_def.fixed_width.to_i
|
407
|
+
else
|
408
|
+
max_value_size = 0
|
409
|
+
data_matrix.each do |row|
|
410
|
+
v = row[column_index].to_s
|
411
|
+
v_size = Term::ANSIColor.coloring? ? Term::ANSIColor.uncolored(v).size : v.size
|
412
|
+
if v_size > max_value_size
|
413
|
+
max_value_size = v_size
|
414
|
+
end
|
415
|
+
end
|
416
|
+
|
417
|
+
max_width = (column_def.max_width.to_i > 0) ? column_def.max_width.to_i : nil
|
418
|
+
min_width = (column_def.min_width.to_i > 0) ? column_def.min_width.to_i : nil
|
419
|
+
if min_width && max_value_size < min_width
|
420
|
+
column_def.width = min_width
|
421
|
+
elsif max_width && max_value_size > max_width
|
422
|
+
column_def.width = max_width
|
423
|
+
else
|
424
|
+
# expand / contract to size of the value by default
|
425
|
+
column_def.width = max_value_size
|
426
|
+
end
|
427
|
+
#puts "DEBUG: #{column_index} column_def.width: #{column_def.width}"
|
428
|
+
end
|
429
|
+
end
|
430
|
+
|
431
|
+
# format header row
|
432
|
+
header_cells = []
|
433
|
+
columns.each_with_index do |column_def, column_index|
|
434
|
+
value = header_row[column_index] # column_def.label
|
435
|
+
header_cells << format_table_cell(value, column_def.width, column_def.justify)
|
436
|
+
end
|
437
|
+
|
438
|
+
# format header spacer row
|
439
|
+
h_line = header_cells.collect {|cell| ("-" * cell.size) }.join(cell_delim.gsub(" ", "-"))
|
440
|
+
|
441
|
+
# format data rows
|
442
|
+
formatted_rows = []
|
443
|
+
rows.each_with_index do |row, row_index|
|
444
|
+
formatted_row = []
|
445
|
+
row.each_with_index do |value, column_index|
|
446
|
+
column_def = columns[column_index]
|
447
|
+
formatted_row << format_table_cell(value, column_def.width, column_def.justify)
|
448
|
+
end
|
449
|
+
formatted_rows << formatted_row
|
450
|
+
end
|
451
|
+
|
452
|
+
|
453
|
+
|
454
|
+
table_str = ""
|
455
|
+
table_str << header_cells.join(cell_delim) + "\n"
|
456
|
+
table_str << h_line + "\n"
|
457
|
+
formatted_rows.each do |row|
|
458
|
+
table_str << row.join(cell_delim) + "\n"
|
459
|
+
end
|
460
|
+
|
461
|
+
out = ""
|
462
|
+
out << table_color if table_color
|
463
|
+
out << table_str
|
464
|
+
out << reset if table_color
|
465
|
+
out
|
466
|
+
end
|
467
|
+
|
468
|
+
|
469
|
+
# as_description_list() prints a a two column table containing
|
470
|
+
# the name and value of a list of descriptions
|
471
|
+
# @param columns - [Hash or Array or Hashes] list of column definitions, A column defintion can be a String, Symbol, Hash or Proc
|
472
|
+
# @param obj [Object] an object to extract the data from, it is treated like a Hash.
|
473
|
+
# @param opts [Map] rendering options for label :justify, :wrap
|
474
|
+
# Usage:
|
475
|
+
# print_description_list([:id, :name, :status], my_instance, {})
|
476
|
+
#
|
477
|
+
def as_description_list(obj, columns, opts={})
|
478
|
+
|
479
|
+
columns = build_column_definitions(columns)
|
480
|
+
|
481
|
+
#label_width = opts[:label_width] || 10
|
482
|
+
max_label_width = 0
|
483
|
+
justify = opts.key?(:justify) ? opts[:justify] : "right"
|
484
|
+
do_wrap = opts.key?(:wrap) ? !!opts[:wrap] : true
|
485
|
+
|
486
|
+
rows = []
|
487
|
+
|
488
|
+
columns.flatten.each do |column_def|
|
489
|
+
label = column_def.label
|
490
|
+
# value = get_object_value(obj, column_def.display_method)
|
491
|
+
value = column_def.display_method.call(obj)
|
492
|
+
if label.size > max_label_width
|
493
|
+
max_label_width = label.size
|
494
|
+
end
|
495
|
+
rows << {label: label, value: value}
|
496
|
+
end
|
497
|
+
label_width = max_label_width + 1 # for a leading space ' ' ..ew
|
498
|
+
value_width = nil
|
499
|
+
if Morpheus::Cli::PrintHelper.terminal_width
|
500
|
+
value_width = Morpheus::Cli::PrintHelper.terminal_width - label_width
|
501
|
+
end
|
502
|
+
|
503
|
+
out = ""
|
504
|
+
rows.each do |row|
|
505
|
+
value = row[:value].to_s
|
506
|
+
if do_wrap
|
507
|
+
if value_width && value_width < value.size
|
508
|
+
wrap_indent = label_width + 1
|
509
|
+
value = wrap(value, value_width, wrap_indent)
|
510
|
+
end
|
511
|
+
end
|
512
|
+
out << format_dt_dd(row[:label], value, label_width, justify) + "\n"
|
513
|
+
end
|
514
|
+
return out
|
515
|
+
end
|
516
|
+
|
517
|
+
# print_description_list() is an alias for `print generate_description_list()`
|
518
|
+
def print_description_list(columns, obj, opts={})
|
519
|
+
# raise "oh no.. replace with as_description_list()"
|
520
|
+
print as_description_list(obj, columns, opts)
|
521
|
+
end
|
522
|
+
|
523
|
+
# build_column_definitions constructs an Array of column definitions (OpenStruct)
|
524
|
+
# Each column is defined by a label (String), and a display_method (Proc)
|
525
|
+
#
|
526
|
+
# @columns [Array] list of definitions. A column definition can be a String, Symbol, Proc or Hash
|
527
|
+
# @return [Array of OpenStruct] list of column definitions (OpenStruct) like:
|
528
|
+
# [{label: "ID", display_method: 'id'}, {label: "Name", display_method: Proc}]
|
529
|
+
# Usage:
|
530
|
+
# build_column_definitions(:id, :name)
|
531
|
+
# build_column_definitions({"Object Id" => 'id'}, :name)
|
532
|
+
# build_column_definitions({"ID" => 'id'}, "name", "plan.name", {status: lambda {|data| data['status'].upcase } })
|
533
|
+
#
|
534
|
+
def build_column_definitions(*columns)
|
535
|
+
# allow passing a single hash instead of an array of hashes
|
536
|
+
if columns.size == 1 && columns[0].is_a?(Hash)
|
537
|
+
columns = columns[0].collect {|k,v| {(k) => v} }
|
538
|
+
else
|
539
|
+
columns = columns.flatten.compact
|
540
|
+
end
|
541
|
+
results = []
|
542
|
+
columns.each do |col|
|
543
|
+
# determine label
|
544
|
+
if col.is_a?(String)
|
545
|
+
k = col
|
546
|
+
v = col
|
547
|
+
build_column_definitions([{(k) => v}]).each do |r|
|
548
|
+
results << r if r
|
549
|
+
end
|
550
|
+
elsif col.is_a?(Symbol)
|
551
|
+
k = col.to_s.upcase #.capitalize
|
552
|
+
v = col.to_s
|
553
|
+
build_column_definitions([{(k) => v}]).each do |r|
|
554
|
+
results << r if r
|
555
|
+
end
|
556
|
+
elsif col.is_a?(Hash)
|
557
|
+
column_def = OpenStruct.new
|
558
|
+
k, v = col.keys[0], col.values[0]
|
559
|
+
if k.is_a?(String)
|
560
|
+
column_def.label = k
|
561
|
+
elsif k.is_a?(Symbol)
|
562
|
+
column_def.label = k
|
563
|
+
else
|
564
|
+
column_def.label = k.to_s
|
565
|
+
# raise "invalid column definition label (#{k.class}) #{k.inspect}. Should be a String or Symbol."
|
566
|
+
end
|
567
|
+
|
568
|
+
# determine display_method
|
569
|
+
if v.is_a?(String)
|
570
|
+
column_def.display_method = lambda {|data| get_object_value(data, v) }
|
571
|
+
elsif v.is_a?(Symbol)
|
572
|
+
column_def.display_method = lambda {|data| get_object_value(data, v) }
|
573
|
+
elsif v.is_a?(Proc)
|
574
|
+
column_def.display_method = v
|
575
|
+
elsif v.is_a?(Hash) || v.is_a?(OStruct)
|
576
|
+
if v[:display_name] || v[:label]
|
577
|
+
column_def.label = v[:display_name] || v[:label]
|
578
|
+
end
|
579
|
+
if v[:display_method]
|
580
|
+
if v[:display_method].is_a?(Proc)
|
581
|
+
column_def.display_method = v[:display_method]
|
582
|
+
else
|
583
|
+
# assume v[:display_method] is a String, Symbol
|
584
|
+
column_def.display_method = lambda {|data| get_object_value(data, v[:display_method]) }
|
585
|
+
end
|
586
|
+
else
|
587
|
+
# the default behavior is to use the key (undoctored) to find the data
|
588
|
+
# column_def.display_method = k
|
589
|
+
column_def.display_method = lambda {|data| get_object_value(data, k) }
|
590
|
+
end
|
591
|
+
|
592
|
+
# other column rendering options
|
593
|
+
column_def.justify = v[:justify]
|
594
|
+
if v[:max_width]
|
595
|
+
column_def.max_width = v[:max_width]
|
596
|
+
end
|
597
|
+
if v[:min_width]
|
598
|
+
column_def.min_width = v[:min_width]
|
599
|
+
end
|
600
|
+
# tp uses width to behave like max_width
|
601
|
+
if v[:width]
|
602
|
+
column_def.width = v[:width]
|
603
|
+
column_def.max_width = v[:width]
|
604
|
+
end
|
605
|
+
column_def.wrap = v[:wrap].nil? ? true : v[:wrap] # only utlized in as_description_list() right now
|
606
|
+
|
607
|
+
else
|
608
|
+
raise "invalid column definition value (#{v.class}) #{v.inspect}. Should be a String, Symbol, Proc or Hash"
|
609
|
+
end
|
610
|
+
|
611
|
+
# only upcase label for symbols, this is silly anyway,
|
612
|
+
# just pass the exact label (key) that you want printed..
|
613
|
+
if column_def.label.is_a?(Symbol)
|
614
|
+
column_def.label = column_def.label.to_s.upcase
|
615
|
+
end
|
616
|
+
|
617
|
+
results << column_def
|
618
|
+
|
619
|
+
else
|
620
|
+
raise "invalid column definition (#{column_def.class}) #{column_def.inspect}. Should be a String, Symbol or Hash"
|
621
|
+
end
|
622
|
+
|
623
|
+
end
|
624
|
+
|
625
|
+
return results
|
626
|
+
end
|
627
|
+
|
628
|
+
def wrap(s, width, indent=0)
|
629
|
+
out = s
|
630
|
+
if s.size > width
|
631
|
+
if indent > 0
|
632
|
+
out = s.gsub(/(.{1,#{width}})(\s+|\Z)/, "#{' ' * indent}\\1\n").strip
|
633
|
+
else
|
634
|
+
out = s.gsub(/(.{1,#{width}})(\s+|\Z)/, "\\1\n")
|
635
|
+
end
|
636
|
+
else
|
637
|
+
return s
|
638
|
+
end
|
639
|
+
end
|
640
|
+
|
641
|
+
def format_boolean(v)
|
642
|
+
!!v ? 'Yes' : 'No'
|
643
|
+
end
|
644
|
+
|
645
|
+
def quote_csv_value(v)
|
646
|
+
'"' + v.to_s.gsub('"', '""') + '"'
|
647
|
+
end
|
648
|
+
|
649
|
+
def as_csv(data, columns, opts={})
|
650
|
+
out = ""
|
651
|
+
delim = opts[:csv_delim] || opts[:delim] || ","
|
652
|
+
newline = opts[:csv_newline] || opts[:newline] || "\n"
|
653
|
+
include_header = opts[:csv_no_header] ? false : true
|
654
|
+
do_quotes = opts[:csv_quotes] || opts[:quotes]
|
655
|
+
# allow passing a single hash instead of an array of hashes
|
656
|
+
# todo: stop doing this, always pass an array!
|
657
|
+
if columns.is_a?(Hash)
|
658
|
+
columns = columns.collect {|k,v| {(k) => v} }
|
659
|
+
end
|
660
|
+
columns = columns.flatten.compact
|
661
|
+
data_array = [data].flatten.compact
|
662
|
+
if include_header
|
663
|
+
headers = columns.collect {|column_def| column_def.is_a?(Hash) ? column_def.keys[0].to_s : column_def.to_s }
|
664
|
+
if do_quotes
|
665
|
+
headers = headers.collect {|it| quote_csv_value(it) }
|
666
|
+
end
|
667
|
+
out << headers.join(delim)
|
668
|
+
out << newline
|
669
|
+
end
|
670
|
+
lines = []
|
671
|
+
data_array.each do |obj|
|
672
|
+
if obj
|
673
|
+
cells = []
|
674
|
+
columns.each do |column_def|
|
675
|
+
value = get_object_value(obj, column_def)
|
676
|
+
if do_quotes
|
677
|
+
cells << quote_csv_value(value)
|
678
|
+
else
|
679
|
+
cells << value.to_s
|
680
|
+
end
|
681
|
+
end
|
682
|
+
end
|
683
|
+
line = cells.join(delim)
|
684
|
+
lines << line
|
685
|
+
end
|
686
|
+
out << lines.join(newline)
|
687
|
+
#out << delim
|
688
|
+
out
|
689
|
+
end
|
690
|
+
|
691
|
+
def records_as_csv(records, opts={}, default_columns=nil)
|
692
|
+
out = ""
|
693
|
+
if !records
|
694
|
+
#raise "records_as_csv expects records as an Array of objects to render"
|
695
|
+
return out
|
696
|
+
end
|
697
|
+
cols = []
|
698
|
+
all_fields = records.first ? records.first.keys : []
|
699
|
+
if opts[:include_fields]
|
700
|
+
if opts[:include_fields] == 'all' || opts[:include_fields].include?('all')
|
701
|
+
cols = all_fields
|
702
|
+
else
|
703
|
+
cols = opts[:include_fields]
|
704
|
+
end
|
705
|
+
elsif default_columns
|
706
|
+
cols = default_columns
|
707
|
+
else
|
708
|
+
cols = all_fields
|
709
|
+
end
|
710
|
+
out << as_csv(records, cols, opts)
|
711
|
+
out
|
712
|
+
end
|
713
|
+
|
714
|
+
def as_json(data, options={})
|
715
|
+
out = ""
|
716
|
+
if !data
|
717
|
+
return "null" # "No data"
|
718
|
+
end
|
719
|
+
|
720
|
+
# include_fields = options[:include_fields]
|
721
|
+
# if include_fields
|
722
|
+
# json_fields_for = options[:json_fields_for] || options[:fields_for] || options[:root_field]
|
723
|
+
# if json_fields_for && data[json_fields_for]
|
724
|
+
# data[json_fields_for] = filtered_data(data[json_fields_for], include_fields)
|
725
|
+
# else
|
726
|
+
# data = filtered_data(data, include_fields)
|
727
|
+
# end
|
728
|
+
# end
|
729
|
+
do_pretty = options.key?(:pretty_json) ? options[:pretty_json] : true
|
730
|
+
if do_pretty
|
731
|
+
out << JSON.pretty_generate(data)
|
732
|
+
else
|
733
|
+
out << JSON.fast_generate(data)
|
734
|
+
end
|
735
|
+
#out << "\n"
|
736
|
+
out
|
737
|
+
end
|
738
|
+
|
739
|
+
def anded_list(items)
|
740
|
+
items = items ? items.clone : []
|
741
|
+
last_item = items.pop
|
742
|
+
if items.empty?
|
743
|
+
return "#{last_item}"
|
744
|
+
else
|
745
|
+
return items.join(", ") + " and #{last_item}"
|
746
|
+
end
|
747
|
+
end
|
748
|
+
|
749
|
+
def as_yaml(data, options={})
|
750
|
+
out = ""
|
751
|
+
if !data
|
752
|
+
return "null" # "No data"
|
753
|
+
end
|
754
|
+
begin
|
755
|
+
out << data.to_yaml
|
756
|
+
rescue => err
|
757
|
+
puts "failed to render YAML from data: #{data.inspect}"
|
758
|
+
puts err.message
|
759
|
+
end
|
760
|
+
#out << "\n"
|
761
|
+
out
|
762
|
+
end
|
763
|
+
|
256
764
|
end
|