morpheus-cli 2.10.3 → 2.11.0

Sign up to get free protection for your applications and to get access to all the features.
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