wavefront-client 3.4.0 → 3.5.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,15 +1,7 @@
1
1
  ---
2
- !binary "U0hBMQ==":
3
- metadata.gz: !binary |-
4
- ZDFjYTkzZTY4OGIzM2JlYTJiYmZmMGU0NjQzYjVmYTY1Y2MyZTBlOA==
5
- data.tar.gz: !binary |-
6
- MjBhZjI0MzA5NjdhNDhlZmU4OTBkN2E1MjVmNmYxZDA2MWNhN2YwMA==
2
+ SHA1:
3
+ metadata.gz: eb93397040acca02ee9271182b09bf0d1e34a57e
4
+ data.tar.gz: 324e62b59979579b09b750398d53257142cab2a0
7
5
  SHA512:
8
- metadata.gz: !binary |-
9
- Nzc4MWJhMzNiYjAzZmYzYTYyYWRlNTQ4OTBjNzI2ZDg0YmVkMTdhYjNlYjNj
10
- MjM2MjEzMmQzNzFiYjI3YjQ1Y2FjZTlhYTFlODlhODg4Yjk5NjFhNTI4NzU1
11
- YjBhMWU3ODA4M2EzZTZiNWYyZTMwNWRhMTY4NDM4MzE2OGE1YzY=
12
- data.tar.gz: !binary |-
13
- MjQ3MDIzNzRhZTYwZGMwM2FlMjM3N2FjN2UzODg3OGQxNWYxMGY3ZDM3ZDc5
14
- OGQ0NGRjYjY1NmUxZDYxODJkYjg5NmU3MzQ3NTRkNzY5NzYxY2YyNDdmMzRj
15
- NDlmZGM3YWM5ZmU2YTMxOGFiMzk3MzRkM2E2MmE5MzA4MzZkYjU=
6
+ metadata.gz: b6093581d6aa9403fe3de7cbf683bc2698460093ba623651a3b2e75d243d5ea8745cdd13b8c87d73ee073cd07704f46eaee33f95417d65a1fc13a5f55eea023a
7
+ data.tar.gz: 03fad5a006ac5a881ef03db7dd859a54afa3eb57a25933c2c0ce3a18eb4ff862930ab8a032f6aa50dd93cecdb1647b699fc70dc4618c3f4948bc641d84f08124
data/.gitignore CHANGED
@@ -3,3 +3,5 @@ Gemfile.lock
3
3
  pkg
4
4
  rdoc
5
5
  spec/reports
6
+ .yardoc/
7
+ doc
data/.travis.yml CHANGED
@@ -3,9 +3,9 @@ cache: bundler
3
3
  rvm:
4
4
  - 1.9.3
5
5
  - 2.0.0
6
- - 2.1.0
7
- - 2.2.2
8
- - 2.3.0
6
+ - 2.1.10
7
+ - 2.2.5
8
+ - 2.3.1
9
9
  deploy:
10
10
  provider: rubygems
11
11
  api_key:
data/README-cli.md CHANGED
@@ -357,6 +357,103 @@ second. Plot the parabola in Wavefront.
357
357
  $ parabola.rb | wavefront write file -F tv -m cli.demo.parabola -
358
358
  ```
359
359
 
360
+ ## `sources` Mode: Tagging and Describing
361
+
362
+ This command is used to add tags and descriptions to Wavefront
363
+ sources. Note that source tags are not the same as point tags.
364
+
365
+ ```
366
+ Usage:
367
+ wavefront source list [-c file] [-P profile] [-E endpoint] [-t token]
368
+ [-f format] [-T tag ...] [-at] [-s source] [-l limit] <pattern>
369
+ wavefront source show [-c file] [-P profile] [-E endpoint] [-t token]
370
+ [-f format] <host> ...
371
+ wavefront source describe [-c file] [-P profile] [-E endpoint] [-t token]
372
+ [-H host ... ] <description>
373
+ wavefront source undescribe [-c file] [-P profile] [-E endpoint] [-t token]
374
+ [<host>] ...
375
+ wavefront source tag add [-c file] [-P profile] [-E endpoint] [-t token]
376
+ [-H host ... ] <tag> ...
377
+ wavefront source tag delete [-c file] [-P profile] [-E endpoint] [-t token]
378
+ [-H host ... ] <tag> ...
379
+ wavefront source untag [-c file] [-P profile] [-E endpoint] [-t token]
380
+ [<host>] ...
381
+ wavefront source --help
382
+
383
+ Global options:
384
+ -c, --config=FILE path to configuration file [default: /home/rob/.wavefront]
385
+ -P, --profile=NAME profile in configuration file [default: default]
386
+ -D, --debug enable debug mode
387
+ -h, --help show this message
388
+
389
+ Options:
390
+ -a, --all including hidden sources in 'human' output
391
+ -t, --tags show tag counts in 'human' output
392
+ -T, --tagged=STRING only list sources with this tag in 'human' output
393
+ -s, --start=STRING start the list after the named source
394
+ -l, --limit=NUMBER only list NUMBER sources
395
+ -H, --host=STRING source to manipulate
396
+ -f, --format=STRING output format (ruby, json, human)
397
+ [default: human]
398
+ ```
399
+
400
+ Tags and descriptions can be applied to multiple sources by repeated
401
+ `-H` options. If no source name is supplied, `wavefront` will use
402
+ the name of the local machine, as supplied by Ruby's
403
+ `Socket.gethostname` method.
404
+
405
+ The `<pattern>` argument in to the `source list` works as a
406
+ substring match. So `pie` will match `pie`, `pier`, `timepieces`,
407
+ etc. Regular expressions will not work.
408
+
409
+ ### Examples
410
+
411
+ List, in human-readable format, all active (non-hidden) sources whose name
412
+ contains `cassandra`, which are tagged with `prod` and `eu-west-1`.
413
+
414
+ ```
415
+ $ wavefront source list -T prod -T eu-west-1 -f human cassandra
416
+ ```
417
+
418
+ Tag this host with `dev` and the kernel version:
419
+
420
+ ```
421
+ $ wavefront tag add dev $(uname -r)
422
+ ```
423
+
424
+ Remove all the tags from `i-123456` and `i-abcdef`
425
+
426
+ ```
427
+ $ wavefront source untag i-123456 i-abcdef
428
+ ```
429
+
430
+ Get the description and tags for the host `build-001`, in JSON format.
431
+
432
+ ```
433
+ $ wavefront source show -f json build-001 | json
434
+ {
435
+ "hostname": "build-001",
436
+ "userTags": [
437
+ "JPC",
438
+ "SmartOS",
439
+ "dev"
440
+ ],
441
+ "description": "build server"
442
+ }
443
+ ```
444
+
445
+ Get a human-readable summary of all the source tags in Wavefront. This works by giving a source name pattern that won't match anything.
446
+
447
+ ```
448
+ $ wavefront source list -t '^$'
449
+ HOSTNAME DESCRIPTION TAGS
450
+
451
+ TAG COUNT
452
+ hidden 339
453
+ physical 10
454
+ zone 363
455
+ ```
456
+
360
457
  ## Notes on Options
361
458
 
362
459
  ### Times
@@ -406,3 +503,6 @@ endpoint = metrics.wavefront.com
406
503
  The key for each key-value pair can match any long option show in the
407
504
  command `help`, so you can set, for instance, a default output
408
505
  format, as shown above.
506
+
507
+ If an option is defined by a command-line switch, and in the
508
+ configuration file, the config file will win.
data/README.md CHANGED
@@ -4,6 +4,20 @@ Wavefront [![Build Status](https://travis-ci.org/wavefrontHQ/ruby-client.svg?bra
4
4
  This is a ruby gem for speaking to the [Wavefront][1] monitoring and graphing system.
5
5
 
6
6
  ## Usage
7
+
8
+ To build API documentation with [YARD](https://github.com/lsegal/yard)
9
+ run
10
+
11
+ ```
12
+ $ rake yard
13
+ ...
14
+ $ cd doc
15
+ $ yard server
16
+ ```
17
+
18
+ and documentation will be available at
19
+ [http://localhost:8808](http://localhost:8808).
20
+
7
21
  Within your own ruby code:
8
22
 
9
23
  ### Writer
@@ -164,13 +178,13 @@ response.highcharts[0]['data'].first # [1436849460000, 517160277.3333333]
164
178
  ```
165
179
 
166
180
  ### Command-line client
167
- A command line client is included too. Please see [README-cli.md]
168
- for details.
181
+ A command line client is included too. Please see
182
+ [README-cli.md](README-cli.md) for details.
169
183
 
170
184
  ## Building and installing
171
185
 
172
186
  ```bash
173
- gem build ./wavefront-client.gemspec && gem install ./wavefront*.gem --no-rdoc --no-ri
187
+ rake build
174
188
  ```
175
189
 
176
190
  or
data/Rakefile CHANGED
@@ -1,4 +1,4 @@
1
- =begin
1
+ =begin
2
2
  Copyright 2015 Wavefront Inc.
3
3
  Licensed under the Apache License, Version 2.0 (the "License");
4
4
  you may not use this file except in compliance with the License.
@@ -16,6 +16,7 @@ See the License for the specific language governing permissions and
16
16
 
17
17
  require "bundler/gem_tasks"
18
18
  require "rspec/core/rake_task"
19
+ require 'yard'
19
20
 
20
21
  RSpec::Core::RakeTask.new(:spec)
21
22
 
@@ -26,3 +27,7 @@ task :install do
26
27
  sh 'gem install wavefront-client-*.gem --no-rdoc --no-ri'
27
28
  sh 'rm wavefront-client-*.gem'
28
29
  end
30
+
31
+ YARD::Rake::YardocTask.new do |t|
32
+ t.files = ['lib/**/*.rb']
33
+ end
data/bin/wavefront CHANGED
@@ -39,7 +39,6 @@ DEF_CF = if ENV['HOME']
39
39
  Pathname.new('/etc/wavefront/client.conf')
40
40
  end
41
41
 
42
-
43
42
  # The global_opts are available in every command.
44
43
  #
45
44
  global_opts = %(
@@ -146,6 +145,34 @@ Files are whitespace separated, and fields can be defined with the -F
146
145
  option. Use 't' for timestamp; 'm' for metric name; 'v' for value
147
146
  and 'T' for tags. Put 'T' last.
148
147
  ),
148
+ source: %(
149
+ Usage:
150
+ #{ME} source list [-c file] [-P profile] [-E endpoint] [-t token]
151
+ [-f format] [-T tag ...] [-at] [-s source] [-l limit] <pattern>
152
+ #{ME} source show [-c file] [-P profile] [-E endpoint] [-t token]
153
+ [-f format] <host> ...
154
+ #{ME} source describe [-c file] [-P profile] [-E endpoint] [-t token]
155
+ [-H host ... ] <description>
156
+ #{ME} source undescribe [-c file] [-P profile] [-E endpoint] [-t token]
157
+ [<host>] ...
158
+ #{ME} source tag add [-c file] [-P profile] [-E endpoint] [-t token]
159
+ [-H host ... ] <tag> ...
160
+ #{ME} source tag delete [-c file] [-P profile] [-E endpoint] [-t token]
161
+ [-H host ... ] <tag> ...
162
+ #{ME} source untag [-c file] [-P profile] [-E endpoint] [-t token]
163
+ [<host>] ...
164
+ #{ME} source --help
165
+ #{global_opts}
166
+ Options:
167
+ -a, --all including hidden sources in 'human' output
168
+ -t, --tags show tag counts in 'human' output
169
+ -T, --tagged=STRING only list sources with this tag in 'human' output
170
+ -s, --start=STRING start the list after the named source
171
+ -l, --limit=NUMBER only list NUMBER sources
172
+ -H, --host=STRING source to manipulate
173
+ -f, --format=STRING output format (#{ALERT_FORMATS.join(', ')})
174
+ [default: #{DEFAULT_ALERT_FORMAT}]
175
+ ),
149
176
  default: %(
150
177
  Wavefront CLI
151
178
 
@@ -158,6 +185,7 @@ Commands:
158
185
  ts view timeseries data
159
186
  alerts view alerts
160
187
  event open and close events
188
+ source view and manage source tags and descriptions
161
189
  write send data points to a Wavefront proxy
162
190
 
163
191
  Use '#{ME} <command> --help' for further information.)
@@ -182,8 +210,8 @@ rescue Docopt::Exit => e
182
210
  end
183
211
  end
184
212
 
185
- # Load the config file. Values in there take priority. Probably
186
- # should be the other way round.
213
+ # Load the config file. Values in there take priority, otherwise
214
+ # default (unset) options will win.
187
215
  #
188
216
  opts.merge!(Wavefront::Cli.new(opts, nil).load_profile || {})
189
217
 
@@ -197,6 +225,9 @@ when :event
197
225
  when :alerts
198
226
  require 'wavefront/cli/alerts'
199
227
  cli = Wavefront::Cli::Alerts.new(opts, [opts[:'<state>']])
228
+ when :source
229
+ require 'wavefront/cli/sources'
230
+ cli = Wavefront::Cli::Sources.new(opts, [opts[:'<state>']])
200
231
  when :write
201
232
  if opts[:file]
202
233
  require 'wavefront/cli/batch_write'
@@ -1,4 +1,4 @@
1
- =begin
1
+ =begin
2
2
  Copyright 2015 Wavefront Inc.
3
3
  Licensed under the Apache License, Version 2.0 (the "License");
4
4
  you may not use this file except in compliance with the License.
@@ -15,8 +15,8 @@ See the License for the specific language governing permissions and
15
15
  =end
16
16
 
17
17
  require "wavefront/client/version"
18
- require "wavefront/exception"
19
18
  require "wavefront/constants"
19
+ require 'wavefront/mixins'
20
20
  require 'rest_client'
21
21
  require 'uri'
22
22
  require 'logger'
@@ -24,6 +24,8 @@ require 'logger'
24
24
  module Wavefront
25
25
  class Alerting
26
26
  include Wavefront::Constants
27
+ include Wavefront::Validators
28
+ include Wavefront::Mixins
27
29
  DEFAULT_PATH = '/api/alert/'
28
30
 
29
31
  attr_reader :token
@@ -55,35 +57,39 @@ module Wavefront
55
57
 
56
58
  private
57
59
 
58
- def get_alerts(path, options={})
59
- options[:host] ||= DEFAULT_HOST
60
- options[:path] ||= DEFAULT_PATH
60
+ def list_of_tags(t)
61
+ t.is_a?(Array) ? t : [t]
62
+ end
63
+
64
+ def mk_qs(options)
65
+ query = "t=#{token}"
66
+
67
+ query += '&' + list_of_tags(options[:shared_tags]).map do |t|
68
+ "customerTag=#{t}"
69
+ end.join('&') if options[:shared_tags]
61
70
 
62
- query = "t=#{@token}"
71
+ query += '&' + list_of_tags(options[:private_tags]).map do |t|
72
+ "userTag=#{t}"
73
+ end.join('&') if options[:private_tags]
63
74
 
64
- if options[:shared_tags]
65
- tags = options[:shared_tags].class == Array ? options[:shared_tags] : [ options[:shared_tags] ] # Force an array, even if string given
66
- query += "&#{tags.map{|t| "customerTag=#{t}"}.join('&')}"
67
- end
75
+ query
76
+ end
68
77
 
69
- if options[:private_tags]
70
- tags = options[:private_tags].class == Array ? options[:private_tags] : [ options[:private_tags] ] # Force an array, even if string given
71
- query += "&#{tags.map{|t| "userTag=#{t}"}.join('&')}"
72
- end
78
+ def get_alerts(path, options={})
79
+ options[:host] ||= DEFAULT_HOST
80
+ options[:path] ||= DEFAULT_PATH
73
81
 
74
82
  uri = URI::HTTPS.build(
75
- host: options[:host],
76
- path: File.join(options[:path], path),
77
- query: query
83
+ host: options[:host],
84
+ path: uri_concat(options[:path], path),
85
+ query: mk_qs(options),
78
86
  )
87
+
79
88
  RestClient.get(uri.to_s)
80
89
  end
81
90
 
82
91
  def debug(enabled)
83
- if enabled
84
- RestClient.log = 'stdout'
85
- end
92
+ RestClient.log = 'stdout' if enabled
86
93
  end
87
-
88
94
  end
89
95
  end
@@ -1,6 +1,7 @@
1
1
  require 'wavefront/client/version'
2
2
  require 'wavefront/exception'
3
3
  require 'wavefront/constants'
4
+ require 'wavefront/validators'
4
5
  require 'uri'
5
6
  require 'socket'
6
7
 
@@ -27,6 +28,7 @@ module Wavefront
27
28
  class BatchWriter
28
29
  attr_reader :sock, :opts, :summary
29
30
  include Wavefront::Constants
31
+ include Wavefront::Validators
30
32
 
31
33
  def initialize(options = {})
32
34
  #
@@ -117,7 +119,7 @@ module Wavefront
117
119
 
118
120
  send_point(hash_to_wf(p))
119
121
  end
120
- return summary[:rejected] == 0 ? true : false
122
+ summary[:rejected] == 0 ? true : false
121
123
  end
122
124
 
123
125
  def valid_point?(point)
@@ -134,41 +136,6 @@ module Wavefront
134
136
  true
135
137
  end
136
138
 
137
- def valid_path?(path)
138
- fail Wavefront::Exception::InvalidMetricName unless \
139
- path.is_a?(String) && path.match(/^[a-z0-9\-_\.]+$/) &&
140
- path.length < 1024
141
- true
142
- end
143
-
144
- def valid_value?(value)
145
- fail Wavefront::Exception::InvalidMetricValue unless value.is_a?(Numeric)
146
- true
147
- end
148
-
149
- def valid_ts?(ts)
150
- unless ts.is_a?(Time) || ts.is_a?(Date)
151
- fail Wavefront::Exception::InvalidTimestamp
152
- end
153
- true
154
- end
155
-
156
- def valid_source?(path)
157
- unless path.is_a?(String) && path.match(/^[a-z0-9\-_\.]+$/) &&
158
- path.length < 1024
159
- fail Wavefront::Exception::InvalidSource
160
- end
161
- true
162
- end
163
-
164
- def valid_tags?(tags)
165
- tags.each do |k, v|
166
- fail Wavefront::Exception::InvalidTag unless (k.length +
167
- v.length < 254) && k.match(/^[a-z0-9\-_\.]+$/)
168
- end
169
- true
170
- end
171
-
172
139
  def hash_to_wf(p)
173
140
  #
174
141
  # Convert the hash received by the write() method to a string
@@ -65,10 +65,9 @@ class Wavefront::Cli::Events < Wavefront::Cli
65
65
  end
66
66
 
67
67
  begin
68
- response = wf_event.delete({
69
- startTime: options[:'<timestamp>'],
70
- name: options[:'<event>'],
71
- })
68
+ wf_event.delete(startTime: options[:'<timestamp>'],
69
+ name: options[:'<event>']
70
+ )
72
71
  rescue RestClient::Unauthorized
73
72
  raise 'Cannot connect to Wavefront API.'
74
73
  rescue RestClient::ResourceNotFound
@@ -78,7 +77,7 @@ class Wavefront::Cli::Events < Wavefront::Cli
78
77
  raise 'Cannot delete event.'
79
78
  end
80
79
 
81
- puts "Deleted event."
80
+ puts 'Deleted event.'
82
81
  end
83
82
 
84
83
  def prep_time(t)
@@ -0,0 +1,193 @@
1
+ require 'wavefront/cli'
2
+ require 'wavefront/metadata'
3
+ require 'json'
4
+ require 'pp'
5
+
6
+ #
7
+ # Turn CLI input, from the 'sources' command, into metadata API calls
8
+ #
9
+ class Wavefront::Cli::Sources < Wavefront::Cli
10
+ attr_accessor :wf, :out_format, :show_hidden, :show_tags
11
+
12
+ def setup_wf
13
+ @wf = Wavefront::Metadata.new(options[:token])
14
+ end
15
+
16
+ def run
17
+ setup_wf
18
+ @out_format = options[:format]
19
+ @show_hidden = options[:all]
20
+ @show_tags = options[:tags]
21
+
22
+ begin
23
+ if options[:list]
24
+ list_source_handler(options[:'<pattern>'], options[:start],
25
+ options[:limit])
26
+ elsif options[:show]
27
+ show_source_handler(options[:'<host>'])
28
+ elsif options[:tag] && options[:add]
29
+ add_tag_handler(options[:host], options[:'<tag>'])
30
+ elsif options[:tag] && options[:delete]
31
+ delete_tag_handler(options[:host], options[:'<tag>'])
32
+ elsif options[:describe]
33
+ describe_handler(options[:host], options[:'<description>'])
34
+ elsif options[:undescribe]
35
+ describe_handler(options[:'<host>'], '')
36
+ elsif options[:untag]
37
+ untag_handler(options[:'<host>'])
38
+ else
39
+ fail 'undefined sources error'
40
+ end
41
+ rescue Wavefront::Exception::InvalidSource
42
+ abort 'ERROR: invalid source name.'
43
+ end
44
+ end
45
+
46
+ def list_source_handler(pattern, start = false, limit = false)
47
+ limit ||= 100
48
+
49
+ q = {
50
+ desc: false,
51
+ limit: limit,
52
+ pattern: pattern
53
+ }
54
+
55
+ q[:lastEntityId] = start if start
56
+
57
+ display_data(JSON.parse(wf.show_sources(q)), 'list_source')
58
+ end
59
+
60
+ def describe_handler(hosts, desc)
61
+ hosts = [Socket.gethostname] if hosts.empty?
62
+
63
+ hosts.each do |h|
64
+ if desc.empty?
65
+ puts "clearing description of '#{h}'"
66
+ else
67
+ puts "setting '#{h}' description to '#{desc}'"
68
+ end
69
+
70
+ begin
71
+ wf.set_description(h, desc)
72
+ rescue Wavefront::Exception::InvalidString
73
+ puts 'ERROR: description contains invalid characters.'
74
+ end
75
+ end
76
+ end
77
+
78
+ def untag_handler(hosts)
79
+ hosts ||= [Socket.gethostname]
80
+
81
+ hosts.each do |h|
82
+ puts "Removing all tags from '#{h}'"
83
+ wf.delete_tags(h)
84
+ end
85
+ end
86
+
87
+ def add_tag_handler(hosts, tags)
88
+ hosts ||= [Socket.gethostname]
89
+
90
+ hosts.each do |h|
91
+ tags.each do |t|
92
+ puts "Tagging '#{h}' with '#{t}'"
93
+ begin
94
+ wf.set_tag(h, t)
95
+ rescue Wavefront::Exception::InvalidString
96
+ puts 'ERROR: tag contains invalid characters.'
97
+ end
98
+ end
99
+ end
100
+ end
101
+
102
+ def delete_tag_handler(hosts, tags)
103
+ hosts ||= [Socket.gethostname]
104
+
105
+ hosts.each do |h|
106
+ tags.each do |t|
107
+ puts "Removing tag '#{t}' from '#{h}'"
108
+ wf.delete_tag(h, t)
109
+ end
110
+ end
111
+ end
112
+
113
+ def show_source_handler(sources)
114
+ sources.each do |s|
115
+ begin
116
+ result = JSON.parse(wf.show_source(s))
117
+ rescue RestClient::ResourceNotFound
118
+ puts "Source '#{s}' not found."
119
+ next
120
+ end
121
+
122
+ display_data(result, 'show_source')
123
+ end
124
+ end
125
+
126
+ def display_data(result, method)
127
+ if out_format == 'human'
128
+ puts public_send('humanize_' + method, result)
129
+ elsif out_format == 'json'
130
+ puts result.to_json
131
+ else
132
+ pp result
133
+ end
134
+ end
135
+
136
+ def humanize_list_source(result)
137
+ hdr = format('%-25s %-30s %s', 'HOSTNAME', 'DESCRIPTION', 'TAGS')
138
+
139
+ ret = result['sources'].each_with_object([hdr]) do |s, aggr|
140
+ if s.include?('userTags') && s['userTags'].include?('hidden') &&
141
+ !show_hidden
142
+ next
143
+ end
144
+
145
+ if options[:tagged]
146
+ skip = false
147
+ options[:tagged].each do |t|
148
+ unless s['userTags'].include?(t)
149
+ skip = true
150
+ break
151
+ end
152
+ end
153
+ next if skip
154
+ end
155
+
156
+ if s['description']
157
+ desc = s['description']
158
+ desc = desc[0..27] + '...' if desc.length > 30
159
+ else
160
+ desc = ''
161
+ end
162
+
163
+ tags = s['userTags'] ? s['userTags'].join(', ') : ''
164
+
165
+ aggr.<< format('%-25s %-30s %s', s['hostname'], desc, tags)
166
+ end
167
+
168
+ if show_tags
169
+ ret.<< ['', format('%-25s%s', 'TAG', 'COUNT')]
170
+
171
+ result['counts'].each do |tag, count|
172
+ ret.<< format('%-25s%s', tag, count)
173
+ end
174
+ end
175
+
176
+ ret.join("\n")
177
+ end
178
+
179
+ def humanize_show_source(data)
180
+ ret = [data['hostname']]
181
+
182
+ if data['description']
183
+ ret.<< format(' %-15s%s', 'description', data['description'])
184
+ end
185
+
186
+ if data['userTags']
187
+ ret.<< format(' %-15s%s', 'tags', data['userTags'].shift)
188
+ data['userTags'].each { |t| ret.<< format(' %-15s%s', '', t) }
189
+ end
190
+
191
+ ret.join("\n")
192
+ end
193
+ end