morpheus-cli 2.10.3 → 2.11.0
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.
- 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
|