wavefront-cli 0.0.2

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