wavefront-cli 0.0.2

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 (82) hide show
  1. checksums.yaml +7 -0
  2. data/.codeclimate.yml +20 -0
  3. data/.gitignore +4 -0
  4. data/.travis.yml +16 -0
  5. data/Gemfile +2 -0
  6. data/Gemfile.lock +65 -0
  7. data/README.md +221 -0
  8. data/Rakefile +18 -0
  9. data/bin/wavefront +14 -0
  10. data/lib/wavefront-cli/alert.rb +60 -0
  11. data/lib/wavefront-cli/base.rb +320 -0
  12. data/lib/wavefront-cli/cloudintegration.rb +12 -0
  13. data/lib/wavefront-cli/commands/alert.rb +38 -0
  14. data/lib/wavefront-cli/commands/base.rb +105 -0
  15. data/lib/wavefront-cli/commands/dashboard.rb +29 -0
  16. data/lib/wavefront-cli/commands/event.rb +44 -0
  17. data/lib/wavefront-cli/commands/integration.rb +33 -0
  18. data/lib/wavefront-cli/commands/link.rb +34 -0
  19. data/lib/wavefront-cli/commands/message.rb +23 -0
  20. data/lib/wavefront-cli/commands/metric.rb +20 -0
  21. data/lib/wavefront-cli/commands/proxy.rb +25 -0
  22. data/lib/wavefront-cli/commands/query.rb +32 -0
  23. data/lib/wavefront-cli/commands/savedsearch.rb +32 -0
  24. data/lib/wavefront-cli/commands/source.rb +27 -0
  25. data/lib/wavefront-cli/commands/user.rb +24 -0
  26. data/lib/wavefront-cli/commands/webhook.rb +25 -0
  27. data/lib/wavefront-cli/commands/window.rb +33 -0
  28. data/lib/wavefront-cli/commands/write.rb +35 -0
  29. data/lib/wavefront-cli/constants.rb +17 -0
  30. data/lib/wavefront-cli/controller.rb +134 -0
  31. data/lib/wavefront-cli/dashboard.rb +27 -0
  32. data/lib/wavefront-cli/display/alert.rb +44 -0
  33. data/lib/wavefront-cli/display/base.rb +304 -0
  34. data/lib/wavefront-cli/display/cloudintegration.rb +18 -0
  35. data/lib/wavefront-cli/display/dashboard.rb +21 -0
  36. data/lib/wavefront-cli/display/event.rb +19 -0
  37. data/lib/wavefront-cli/display/externallink.rb +13 -0
  38. data/lib/wavefront-cli/display/maintenancewindow.rb +19 -0
  39. data/lib/wavefront-cli/display/message.rb +8 -0
  40. data/lib/wavefront-cli/display/metric.rb +22 -0
  41. data/lib/wavefront-cli/display/proxy.rb +13 -0
  42. data/lib/wavefront-cli/display/query.rb +69 -0
  43. data/lib/wavefront-cli/display/savedsearch.rb +17 -0
  44. data/lib/wavefront-cli/display/source.rb +26 -0
  45. data/lib/wavefront-cli/display/user.rb +16 -0
  46. data/lib/wavefront-cli/display/webhook.rb +24 -0
  47. data/lib/wavefront-cli/display/write.rb +19 -0
  48. data/lib/wavefront-cli/event.rb +162 -0
  49. data/lib/wavefront-cli/exception.rb +5 -0
  50. data/lib/wavefront-cli/externallink.rb +16 -0
  51. data/lib/wavefront-cli/maintenancewindow.rb +16 -0
  52. data/lib/wavefront-cli/message.rb +19 -0
  53. data/lib/wavefront-cli/metric.rb +24 -0
  54. data/lib/wavefront-cli/opt_handler.rb +62 -0
  55. data/lib/wavefront-cli/proxy.rb +22 -0
  56. data/lib/wavefront-cli/query.rb +74 -0
  57. data/lib/wavefront-cli/savedsearch.rb +24 -0
  58. data/lib/wavefront-cli/source.rb +20 -0
  59. data/lib/wavefront-cli/user.rb +25 -0
  60. data/lib/wavefront-cli/version.rb +1 -0
  61. data/lib/wavefront-cli/webhook.rb +8 -0
  62. data/lib/wavefront-cli/write.rb +244 -0
  63. data/spec/spec_helper.rb +197 -0
  64. data/spec/wavefront-cli/alert_spec.rb +44 -0
  65. data/spec/wavefront-cli/base_spec.rb +47 -0
  66. data/spec/wavefront-cli/cli_help_spec.rb +47 -0
  67. data/spec/wavefront-cli/cloudintegration_spec.rb +24 -0
  68. data/spec/wavefront-cli/dashboard_spec.rb +37 -0
  69. data/spec/wavefront-cli/event_spec.rb +19 -0
  70. data/spec/wavefront-cli/externallink_spec.rb +18 -0
  71. data/spec/wavefront-cli/maintanancewindow_spec.rb +19 -0
  72. data/spec/wavefront-cli/message_spec.rb +28 -0
  73. data/spec/wavefront-cli/metric_spec.rb +22 -0
  74. data/spec/wavefront-cli/proxy_spec.rb +26 -0
  75. data/spec/wavefront-cli/query_spec.rb +63 -0
  76. data/spec/wavefront-cli/resources/conf.yaml +10 -0
  77. data/spec/wavefront-cli/savedsearch_spec.rb +18 -0
  78. data/spec/wavefront-cli/source_spec.rb +18 -0
  79. data/spec/wavefront-cli/user_spec.rb +31 -0
  80. data/spec/wavefront-cli/webhook_spec.rb +17 -0
  81. data/wavefront-cli.gemspec +36 -0
  82. metadata +279 -0
@@ -0,0 +1,5 @@
1
+ module WavefrontCli
2
+ class Exception
3
+ class UnhandledCommand < ::Exception; end
4
+ end
5
+ end
@@ -0,0 +1,16 @@
1
+ require_relative './base'
2
+
3
+ module WavefrontCli
4
+ #
5
+ # CLI coverage for the v2 'externallink' API.
6
+ #
7
+ class ExternalLink < WavefrontCli::Base
8
+ def validator_method
9
+ :wf_link_id?
10
+ end
11
+
12
+ def validator_exception
13
+ Wavefront::Exception::InvalidExternalLinkId
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,16 @@
1
+ require_relative './base'
2
+
3
+ module WavefrontCli
4
+ #
5
+ # CLI coverage for the v2 'maintenancewindow' API.
6
+ #
7
+ class MaintenanceWindow < WavefrontCli::Base
8
+ def validator_method
9
+ :wf_maintenance_window_id?
10
+ end
11
+
12
+ def validator_exception
13
+ Wavefront::Exception::InvalidMaintenanceWindowId
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,19 @@
1
+ require_relative './base'
2
+
3
+ module WavefrontCli
4
+ #
5
+ # CLI coverage for the v2 'message' API.
6
+ #
7
+ class Message < WavefrontCli::Base
8
+ #
9
+ # There's an extra flag to "list" that no other commands have.
10
+ #
11
+ def do_list
12
+ wf.list(options[:offset] || 0, options[:limit] || 100, !options[:all])
13
+ end
14
+
15
+ def do_mark
16
+ wf.read(options[:'<id>'])
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,24 @@
1
+ require_relative './base'
2
+
3
+ module WavefrontCli
4
+ #
5
+ # CLI coverage for the v2 'metric' API.
6
+ #
7
+ class Metric < WavefrontCli::Base
8
+ #
9
+ # There's an extra describe flag that other classes don't have.
10
+ #
11
+ def do_describe
12
+ wf.detail(options[:'<metric>'], options[:glob] || [], options[:offset])
13
+ end
14
+
15
+ def extra_validation
16
+ return unless options[:'<metric>']
17
+ begin
18
+ wf_metric_name?(options[:'<metric>'])
19
+ rescue Wavefront::Exception::InvalidMetricName
20
+ abort "'#{options[:'<metric>']}' is not a valid metric."
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,62 @@
1
+ require 'inifile'
2
+ require 'pathname'
3
+ require_relative './constants.rb'
4
+
5
+ module WavefrontCli
6
+ #
7
+ # Options to commands can come from three sources, with the
8
+ # following order of precedence: program defaults, a configuration
9
+ # file, and command-line options. Docopt is not well suited to
10
+ # this, as it will "fill in" any missing options with defaults,
11
+ # producing a single hash which must be merged with values from
12
+ # the config file. Assuming we give the command-line higher
13
+ # precedence, a default value, not supplied by the user, will
14
+ # override a value in the config file. The other way round, and
15
+ # you can't override anything in the config file from the
16
+ # command-line. I think this behaviour is far from unique to
17
+ # Docopt.
18
+ #
19
+ # So, we have a hash of defaults, and we do the merging ourselves,
20
+ # in this class. We trick Docopt into not using the defaults by
21
+ # avoiding the magic string 'default: ' in our options stanzas.
22
+ #
23
+ class OptHandler
24
+ include WavefrontCli::Constants
25
+
26
+ attr_reader :opts, :cli_opts, :conf_file
27
+
28
+ def initialize(conf_file, cli_opts = {})
29
+ @conf_file = if cli_opts.key?(:config) && cli_opts[:config]
30
+ Pathname.new(cli_opts[:config])
31
+ else
32
+ conf_file
33
+ end
34
+
35
+ @cli_opts = cli_opts.reject { |_k, v| v.nil? }
36
+
37
+ @opts = DEFAULT_OPTS.merge(load_profile).merge(@cli_opts)
38
+ end
39
+
40
+ def load_profile
41
+ #
42
+ # Load in configuration options from the (optionally) given
43
+ # section of an ini-style configuration file. If the file's
44
+ # not there, we don't consider that an error. Returns a hash
45
+ # of options which matches what Docopt gives us.
46
+ #
47
+ unless conf_file.exist?
48
+ puts "config file '#{conf_file}' not found. Taking options " \
49
+ 'from command-line.'
50
+ return {}
51
+ end
52
+
53
+ pf = cli_opts.fetch(:profile, 'default')
54
+
55
+ puts "reading '#{pf}' profile from '#{conf_file}'" if cli_opts[:debug]
56
+
57
+ IniFile.load(conf_file)[pf].each_with_object({}) do |(k, v), memo|
58
+ memo[k.to_sym] = v
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,22 @@
1
+ require_relative './base'
2
+
3
+ module WavefrontCli
4
+ #
5
+ # CLI coverage for the v2 'proxy' API.
6
+ #
7
+ class Proxy < WavefrontCli::Base
8
+ def do_rename
9
+ wf_string?(options[:'<name>'])
10
+ wf.rename(options[:'<id>'], options[:'<name>'])
11
+ end
12
+
13
+ def extra_validation
14
+ return unless options[:'<name>']
15
+ begin
16
+ wf_string?(options[:'<name>'])
17
+ rescue Wavefront::Exception::InvalidString
18
+ abort "'#{options[:'<name>']}' is not a valid proxy name."
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,74 @@
1
+ require 'wavefront-sdk/mixins'
2
+ require_relative './base'
3
+
4
+ module WavefrontCli
5
+ #
6
+ # CLI coverage for the v2 'query' API.
7
+ #
8
+ class Query < WavefrontCli::Base
9
+ include Wavefront::Mixins
10
+
11
+ def do_default
12
+ opts = {
13
+ autoEvents: options[:events],
14
+ i: options[:inclusive],
15
+ summarization: options[:summarize] || 'mean',
16
+ listMode: true,
17
+ strict: true,
18
+ includeObsoleteMetrics: options[:obsolete],
19
+ sorted: true
20
+ }
21
+
22
+ if options[:start]
23
+ options[:start] = parse_time(options[:start], true)
24
+ else
25
+ options[:start] = (Time.now - 600).to_i
26
+ end
27
+
28
+ if options[:end]
29
+ options[:end] = parse_time(options[:end], true)
30
+ t_end = options[:end]
31
+ else
32
+ t_end = Time.now.to_i
33
+ end
34
+
35
+ options[:granularity] ||= default_granularity((t_end -
36
+ options[:start]).to_i)
37
+
38
+ opts[:n] = options[:name] if options[:name]
39
+ opts[:p] = options[:points] if options[:points]
40
+
41
+ wf.query(options[:'<query>'], options[:granularity],
42
+ options[:start], options[:end] || nil, opts)
43
+ end
44
+
45
+
46
+ # Work out a sensible granularity based on the time window
47
+ #
48
+ def default_granularity(window)
49
+ if window < 300
50
+ :s
51
+ elsif window < 10800
52
+ :m
53
+ elsif window < 259200
54
+ :h
55
+ else
56
+ :d
57
+ end
58
+ end
59
+
60
+ def extra_validation
61
+ return unless options[:granularity]
62
+ begin
63
+ wf_granularity?(options[:granularity])
64
+ rescue Wavefront::Exception::InvalidGranularity
65
+ abort "'#{options[:granularity]}' is not a valid granularity."
66
+ end
67
+ end
68
+
69
+ def do_raw
70
+ wf.raw(options[:'<metric>'], options[:host], options[:start],
71
+ options[:end])
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,24 @@
1
+ require_relative './base'
2
+
3
+ module WavefrontCli
4
+ #
5
+ # CLI coverage for the v2 'savedsearch' API.
6
+ #
7
+ class SavedSearch < WavefrontCli::Base
8
+ def do_list
9
+ wf.list(options[:offset] || 0, options[:limit] || 100)
10
+ end
11
+
12
+ def do_describe
13
+ wf.describe(options[:'<id>'])
14
+ end
15
+
16
+ def do_delete
17
+ wf.delete(options[:'<id>'])
18
+ end
19
+
20
+ def validator_exception
21
+ Wavefront::Exception::InvalidSavedSearchId
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,20 @@
1
+ require_relative './base'
2
+
3
+ module WavefrontCli
4
+ #
5
+ # CLI coverage for the v2 'source' API.
6
+ #
7
+ class Source < WavefrontCli::Base
8
+ def do_clear
9
+ wf.delete(options[:'<id>'])
10
+ end
11
+
12
+ def do_description_set
13
+ wf.update(options[:'<id>'], description: options[:'<description>'])
14
+ end
15
+
16
+ def do_description_clear
17
+ wf.update(options[:'<id>'], { description: ''}, false)
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,25 @@
1
+ require_relative './base'
2
+
3
+ module WavefrontCli
4
+ #
5
+ # CLI coverage for the v2 'user' API.
6
+ #
7
+ class User < WavefrontCli::Base
8
+ def do_list
9
+ wf.list
10
+ end
11
+
12
+ def do_grant
13
+ wf.grant(options[:'<id>'], options[:'<privilege>'])
14
+ end
15
+
16
+ def do_revoke
17
+ wf.revoke(options[:'<id>'], options[:'<privilege>'])
18
+ end
19
+
20
+ def import_to_create(raw)
21
+ raw['emailAddress'] = raw['identifier']
22
+ raw.delete_if { |k, _v| k == 'customer' || k == 'identifier' }
23
+ end
24
+ end
25
+ end
@@ -0,0 +1 @@
1
+ WF_CLI_VERSION = '0.0.2'.freeze
@@ -0,0 +1,8 @@
1
+ require_relative './base'
2
+
3
+ module WavefrontCli
4
+ #
5
+ # CLI coverage for the v2 'webhook' API.
6
+ #
7
+ class Webhook < WavefrontCli::Base; end
8
+ end
@@ -0,0 +1,244 @@
1
+ require 'wavefront-sdk/mixins'
2
+ require_relative './base'
3
+
4
+ module WavefrontCli
5
+ #
6
+ # Send points to a proxy.
7
+ #
8
+ class Write < Base
9
+ attr_reader :fmt
10
+ include Wavefront::Mixins
11
+
12
+ def mk_creds
13
+ { proxy: options[:proxy], port: options[:port] || 2878 }
14
+ end
15
+
16
+ def do_point
17
+ p = { path: options[:'<metric>'],
18
+ value: options[:'<value>'].to_f,
19
+ tags: tags_to_hash(options[:tag]) }
20
+
21
+ p[:source] = options[:host] if options[:host]
22
+ p[:ts] = parse_time(options[:time]) if options[:time]
23
+
24
+ begin
25
+ wf.write(p)
26
+ rescue Wavefront::Exception::InvalidEndpoint
27
+ abort 'could not speak to proxy ' \
28
+ "'#{options[:proxy]}:#{options[:port]}'."
29
+ end
30
+ end
31
+
32
+ def do_file
33
+ valid_format?(options[:infileformat])
34
+ setup_fmt(options[:infileformat] || 'tmv')
35
+ process_input(options[:'<file>'])
36
+ end
37
+
38
+ # Read the input, from a file or from STDIN, and turn each line
39
+ # into Wavefront points.
40
+ #
41
+ def process_input(file)
42
+ if file == '-'
43
+ read_stdin
44
+ else
45
+ data = load_data(Pathname.new(file)).split("\n").map do |l|
46
+ process_line(l)
47
+ end
48
+
49
+ wf.write(data)
50
+ end
51
+ end
52
+
53
+ # Read from standard in and stream points through an open
54
+ # socket. If the user hits ctrl-c, close the socket and exit
55
+ # politely.
56
+ #
57
+ def read_stdin
58
+ wf.open
59
+ STDIN.each_line { |l| wf.write(process_line(l.strip), false) }
60
+ wf.close
61
+ rescue SystemExit, Interrupt
62
+ puts 'ctrl-c. Exiting.'
63
+ wf.close
64
+ exit 0
65
+ end
66
+
67
+ # Find and return the value in a chunked line of input
68
+ #
69
+ # param chunks [Array] a chunked line of input from #process_line
70
+ # return [Float] the value
71
+ # raise TypeError if field does not exist
72
+ # raise Wavefront::Exception::InvalidValue if it's not a value
73
+ #
74
+ def extract_value(chunks)
75
+ v = chunks[fmt.index('v')]
76
+ v.to_f
77
+ end
78
+
79
+ # Find and return the source in a chunked line of input.
80
+ #
81
+ # param chunks [Array] a chunked line of input from #process_line
82
+ # return [Float] the timestamp, if it is there, or the current
83
+ # UTC time if it is not.
84
+ # raise TypeError if field does not exist
85
+ #
86
+ def extract_ts(chunks)
87
+ ts = chunks[fmt.index('t')]
88
+ return parse_time(ts) if valid_timestamp?(ts)
89
+ rescue TypeError
90
+ Time.now.utc.to_i
91
+ end
92
+
93
+ def extract_tags(chunks)
94
+ tags_to_hash(chunks.last.split(/\s(?=(?:[^"]|"[^"]*")*$)/))
95
+ end
96
+
97
+ # Find and return the metric path in a chunked line of input.
98
+ # The path can be in the data, or passed as an option, or both.
99
+ # If the latter, then we assume the option is a prefix, and
100
+ # concatenate the value in the data.
101
+ #
102
+ # param chunks [Array] a chunked line of input from #process_line
103
+ # return [String] the metric path
104
+ # raise TypeError if field does not exist
105
+ #
106
+ def extract_path(chunks)
107
+ m = chunks[fmt.index('m')]
108
+ return options[:metric] ? [options[:metric], m].join('.') : m
109
+ rescue TypeError
110
+ return options[:metric] if options[:metric]
111
+ raise
112
+ end
113
+
114
+ # Find and return the source in a chunked line of input.
115
+ #
116
+ # param chunks [Array] a chunked line of input from #process_line
117
+ # return [String] the source, if it is there, or if not, the
118
+ # value passed through by -H, or the local hostname.
119
+ #
120
+ def extract_source(chunks)
121
+ return chunks[fmt.index('s')]
122
+ rescue TypeError
123
+ options[:source] || Socket.gethostname
124
+ end
125
+
126
+ # Process a line of input, as described by the format string
127
+ # held in @fmt. Produces a hash suitable for the SDK to send on.
128
+ #
129
+ # We let the user define most of the fields, but anything beyond
130
+ # what they define is always assumed to be point tags. This is
131
+ # because you can have arbitrarily many of those for each point.
132
+ #
133
+ def process_line(l)
134
+ return true if l.empty?
135
+ chunks = l.split(/\s+/, fmt.length)
136
+ raise 'wrong number of fields' unless enough_fields?(l)
137
+
138
+ begin
139
+ point = { path: extract_path(chunks),
140
+ value: extract_value(chunks) }
141
+ point[:ts] = extract_ts(chunks) if fmt.include?('t')
142
+ point[:source] = extract_source(chunks) if fmt.include?('s')
143
+ point[:tags] = line_tags(chunks)
144
+ rescue TypeError
145
+ raise "could not process #{l}"
146
+ end
147
+
148
+ point
149
+ end
150
+
151
+ # We can get tags from the file, from the -T option, or both.
152
+ # Merge them, making the -T win if there is a collision.
153
+ #
154
+ def line_tags(chunks)
155
+ file_tags = fmt.last == 'T' ? extract_tags(chunks) : {}
156
+ opt_tags = tags_to_hash(options[:tag])
157
+ file_tags.merge(opt_tags)
158
+ end
159
+
160
+ # Takes an array of key=value tags (as produced by docopt) and
161
+ # turns it into a hash of key: value tags. Anything not of the
162
+ # form key=val is dropped. If key or value are quoted, we
163
+ # remove the quotes.
164
+ #
165
+ # @param tags [Array]
166
+ # return Hash
167
+ #
168
+ def tags_to_hash(tags)
169
+ return nil unless tags
170
+
171
+ [tags].flatten.each_with_object({}) do |t, ret|
172
+ k, v = t.split('=', 2)
173
+ k.gsub!(/^["']|["']$/, '')
174
+ ret[k] = v.to_s.gsub(/^["']|["']$/, '') if v
175
+ end
176
+ end
177
+
178
+ # The format string must contain a 'v'. It must not contain
179
+ # anything other than 'm', 't', 'T', 's', or 'v', and the 'T',
180
+ # if there, must be at the end. No letter must appear more than
181
+ # once.
182
+ #
183
+ # @param fmt [String] format of input file
184
+ #
185
+ def valid_format?(fmt)
186
+ if fmt.include?('v') && fmt.match(/^[mstv]+T?$/) &&
187
+ fmt == fmt.split('').uniq.join
188
+ return true
189
+ end
190
+
191
+ raise 'Invalid format string.'
192
+ end
193
+
194
+ # Make sure we have the right number of columns, according to
195
+ # the format string. We want to take every precaution we can to
196
+ # stop users accidentally polluting their metric namespace with
197
+ # junk.
198
+ #
199
+ # If the format string says we are expecting point tags, we
200
+ # may have more columns than the length of the format string.
201
+ #
202
+ def enough_fields?(l)
203
+ ncols = l.split.length
204
+
205
+ if fmt.include?('T')
206
+ return false unless ncols >= fmt.length
207
+ else
208
+ return false unless ncols == fmt.length
209
+ end
210
+
211
+ true
212
+ end
213
+
214
+ # Although the SDK does value checking, we'll add another layer
215
+ # of input checing here. See if the time looks valid. We'll
216
+ # assume anything before 2000/01/01 or after a year from now is
217
+ # wrong. Arbitrary, but there has to be a cut-off somewhere.
218
+ #
219
+ def valid_timestamp?(ts)
220
+ (ts.is_a?(Integer) || ts.match(/^\d+$/)) &&
221
+ ts.to_i > 946684800 && ts.to_i < (Time.now.to_i + 31557600)
222
+ end
223
+
224
+ def validate_opts
225
+ unless options[:metric] || options[:format].include?('m')
226
+ abort "Supply a metric path in the file or with '-m'."
227
+ end
228
+
229
+ raise 'Please supply a proxy address.' unless options[:proxy]
230
+ end
231
+
232
+ private
233
+
234
+ def setup_fmt(fmt)
235
+ @fmt = fmt.split('')
236
+ end
237
+
238
+ def load_data(file)
239
+ IO.read(file)
240
+ rescue
241
+ raise "Cannot open file '#{file}'." unless file.exist?
242
+ end
243
+ end
244
+ end