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.
Files changed (68) hide show
  1. checksums.yaml +4 -4
  2. data/bin/morpheus +5 -96
  3. data/lib/morpheus/api/api_client.rb +23 -1
  4. data/lib/morpheus/api/checks_interface.rb +106 -0
  5. data/lib/morpheus/api/incidents_interface.rb +102 -0
  6. data/lib/morpheus/api/monitoring_apps_interface.rb +47 -0
  7. data/lib/morpheus/api/monitoring_contacts_interface.rb +47 -0
  8. data/lib/morpheus/api/monitoring_groups_interface.rb +47 -0
  9. data/lib/morpheus/api/monitoring_interface.rb +36 -0
  10. data/lib/morpheus/api/option_type_lists_interface.rb +47 -0
  11. data/lib/morpheus/api/option_types_interface.rb +47 -0
  12. data/lib/morpheus/api/roles_interface.rb +0 -1
  13. data/lib/morpheus/api/setup_interface.rb +19 -9
  14. data/lib/morpheus/cli.rb +20 -1
  15. data/lib/morpheus/cli/accounts.rb +8 -3
  16. data/lib/morpheus/cli/app_templates.rb +19 -11
  17. data/lib/morpheus/cli/apps.rb +52 -37
  18. data/lib/morpheus/cli/cli_command.rb +229 -53
  19. data/lib/morpheus/cli/cli_registry.rb +48 -40
  20. data/lib/morpheus/cli/clouds.rb +55 -26
  21. data/lib/morpheus/cli/command_error.rb +12 -0
  22. data/lib/morpheus/cli/credentials.rb +68 -26
  23. data/lib/morpheus/cli/curl_command.rb +98 -0
  24. data/lib/morpheus/cli/dashboard_command.rb +2 -7
  25. data/lib/morpheus/cli/deployments.rb +4 -4
  26. data/lib/morpheus/cli/deploys.rb +1 -2
  27. data/lib/morpheus/cli/dot_file.rb +5 -8
  28. data/lib/morpheus/cli/error_handler.rb +179 -15
  29. data/lib/morpheus/cli/groups.rb +21 -13
  30. data/lib/morpheus/cli/hosts.rb +220 -110
  31. data/lib/morpheus/cli/instance_types.rb +2 -2
  32. data/lib/morpheus/cli/instances.rb +257 -167
  33. data/lib/morpheus/cli/key_pairs.rb +15 -9
  34. data/lib/morpheus/cli/library.rb +673 -27
  35. data/lib/morpheus/cli/license.rb +2 -2
  36. data/lib/morpheus/cli/load_balancers.rb +4 -4
  37. data/lib/morpheus/cli/log_level_command.rb +6 -4
  38. data/lib/morpheus/cli/login.rb +17 -3
  39. data/lib/morpheus/cli/logout.rb +25 -11
  40. data/lib/morpheus/cli/man_command.rb +388 -0
  41. data/lib/morpheus/cli/mixins/accounts_helper.rb +1 -1
  42. data/lib/morpheus/cli/mixins/monitoring_helper.rb +434 -0
  43. data/lib/morpheus/cli/mixins/print_helper.rb +620 -112
  44. data/lib/morpheus/cli/mixins/provisioning_helper.rb +1 -1
  45. data/lib/morpheus/cli/monitoring_apps_command.rb +29 -0
  46. data/lib/morpheus/cli/monitoring_checks_command.rb +427 -0
  47. data/lib/morpheus/cli/monitoring_contacts_command.rb +373 -0
  48. data/lib/morpheus/cli/monitoring_groups_command.rb +29 -0
  49. data/lib/morpheus/cli/monitoring_incidents_command.rb +711 -0
  50. data/lib/morpheus/cli/option_types.rb +10 -1
  51. data/lib/morpheus/cli/recent_activity_command.rb +2 -5
  52. data/lib/morpheus/cli/remote.rb +874 -134
  53. data/lib/morpheus/cli/roles.rb +54 -27
  54. data/lib/morpheus/cli/security_group_rules.rb +2 -2
  55. data/lib/morpheus/cli/security_groups.rb +23 -19
  56. data/lib/morpheus/cli/set_prompt_command.rb +50 -0
  57. data/lib/morpheus/cli/shell.rb +222 -157
  58. data/lib/morpheus/cli/tasks.rb +19 -15
  59. data/lib/morpheus/cli/users.rb +27 -17
  60. data/lib/morpheus/cli/version.rb +1 -1
  61. data/lib/morpheus/cli/virtual_images.rb +28 -13
  62. data/lib/morpheus/cli/whoami.rb +131 -52
  63. data/lib/morpheus/cli/workflows.rb +24 -9
  64. data/lib/morpheus/formatters.rb +195 -3
  65. data/lib/morpheus/logging.rb +86 -0
  66. data/lib/morpheus/terminal.rb +371 -0
  67. data/scripts/generate_morpheus_commands_help.morpheus +60 -0
  68. 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 print_red_alert(msg)
12
- print "#{red}#{msg}#{reset}\n"
14
+ def self.terminal_width
15
+ @@terminal_width ||= 80
13
16
  end
14
17
 
15
- def print_yellow_warning(msg)
16
- print "#{yellow}#{msg}#{reset}\n"
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 print_green_success(msg)
20
- print "#{green}#{msg}#{reset}\n"
27
+ def current_terminal_width
28
+ return IO.console.winsize[1] rescue 0
21
29
  end
22
30
 
23
- def print_errors(response, options={})
24
- begin
25
- if options[:json]
26
- print red
27
- print JSON.pretty_generate(response)
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
- def print_rest_exception(e, options={})
52
- if e.response
53
- if options[:debug]
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
- def print_rest_request(req)
88
- # JD: IOError when accessing payload... we should probably just be printing at the time the request is made..
89
- #out = []
90
- #out << "#{req.method} #{req.url.inspect}"
91
- #out << req.payload.short_inspect if req.payload
92
- # payload = req.instance_variable_get("@payload")
93
- # out << payload if payload
94
- #out << req.processed_headers.to_a.sort.map { |(k, v)| [k.inspect, v.inspect].join("=>") }.join(", ")
95
- #print out.join(', ') + "\n"
96
- print "Request:"
97
- print "\n"
98
- print "#{req.method.to_s.upcase} #{req.url.inspect}"
99
- print "\n"
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 print_rest_response(res)
103
- # size = @raw_response ? File.size(@tf.path) : (res.body.nil? ? 0 : res.body.size)
104
- size = (res.body.nil? ? 0 : res.body.size)
105
- print "Response:"
106
- print "\n"
107
- display_size = Filesize.from("#{size} B").pretty rescue size
108
- print "HTTP #{res.net_http_res.code} - #{res.net_http_res.message} | #{(res['Content-type'] || '').gsub(/;.*$/, '')} #{display_size}"
109
- print "\n"
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
- print "\n"
74
+ out << "\n"
75
+ out << "#{color}---------------------#{reset}"
76
+ out << "\n\n"
77
+ out << reset
78
+ print out
116
79
  end
117
80
 
118
- def print_rest_exception_request_and_response(e)
119
- print_red_alert "Error Communicating with the Appliance. (#{e.response.code}) #{e}"
120
- response = e.response
121
- request = response.instance_variable_get("@request")
122
- print red
123
- print_rest_request(request)
124
- print "\n"
125
- print_rest_response(response)
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
- print "\n" ,cyan, bold, "DRY RUN\n","==================", "\n\n", reset
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
- if json_response && json_response["meta"]
165
- 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
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
- print cyan, "Memory:".ljust(10, ' ') + 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"
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
- print cyan, "Storage:".ljust(10, ' ') + 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"
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