auger 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ lib/bundler/man
11
+ pkg
12
+ doc
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/Gemfile ADDED
@@ -0,0 +1,11 @@
1
+ source 'https://rubygems.org'
2
+ gem 'host_range'
3
+ gem 'rainbow'
4
+ gem 'cassandra-cql'
5
+ gem 'net-dns'
6
+ gem 'redis'
7
+ gem 'json'
8
+
9
+ group :development do
10
+ gem 'gemcutter'
11
+ end
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2012 Grant Heffernan
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,339 @@
1
+ # Auger
2
+
3
+ The Auger library implements a ruby DSL for describing tests to be run
4
+ against remote applications on multiple servers. The gem includes
5
+ 'aug', a multi-threaded command-line client.
6
+
7
+ The primary goal of Auger is test-driven operations: unit testing for
8
+ application admins. The library can also be used as a framework for
9
+ implmenting automated tests.
10
+
11
+ * these are the sorts of questions auger can answer:
12
+
13
+ * is port :80 on my application webservers open? does /index.html
14
+ contain a response tag that we know should be served from a given
15
+ backend data source?
16
+
17
+ * is redis running? is it configured as a master? a slave?
18
+
19
+ * is elasticsearch responding on all my hosts it should be? what's
20
+ the cluster state? do I have the number of data nodes responding
21
+ that we're supposed to have?
22
+
23
+ * clearly a lot of this information includes things you should be
24
+ graphing. What auger wants to do is give you a quick overview
25
+ of current status: green == good, red == ruh roh!
26
+
27
+ ## Plugins
28
+
29
+ Specific protocols are implemented using plugins, which are designed
30
+ to be easy to write wrappers on existing gems. Auger currently includes
31
+ the following plugins:
32
+
33
+ * http - http and https requests using `net/http`
34
+ * telnet - send arbitrary commands to a port using `net/telnet`
35
+ * socket - test whether a port is open
36
+ * cassandra - CQL requests using `cassandra-cql` gem
37
+
38
+ ## Installation
39
+
40
+ * gem install auger
41
+
42
+ ### If you want to run the latest source:
43
+
44
+ * `git clone git@github.com/blah/auger` TODO => fix github url
45
+ * `cd auger; bundle install && rake install`
46
+
47
+ ## Command-line client usage
48
+
49
+ * sample configs included in cfg/examples/ can be moved into cfg/ and
50
+ then run via `aug redis` etc.
51
+
52
+ * if you've installed as a gem, the examples will be located wherever your gems get installed
53
+
54
+ * one quick way to find them should be `cd $GEM_HOME/gems/auger-x.x.x/cfg/examples`
55
+
56
+ * alternatively, you can place your configs anywhere you'd like and
57
+ set the env_var: `AUGER_CFG=/path/to/your/configs/prod:/path/to/your/configs/stage`
58
+
59
+ * now call your tests via `aug name_of_my_config`, e.g. `aug redis`
60
+
61
+ * configs should take the format `name.rb`
62
+
63
+ * `aug -l` will print available tests
64
+ * `aug -h` will print usage details
65
+
66
+ ## Writing tests
67
+
68
+ Tests are written as ruby code describing a test configuration. Files
69
+ containing tests should be placed in the path described by the `AUGER_CFG`
70
+ environment variable.
71
+
72
+ ### Example 1 - testing a webserver response
73
+
74
+ ```ruby
75
+ project "Front-end Web Servers" do
76
+ server "web-fe-[01-02]"
77
+
78
+ http 8000 do
79
+ get '/' do
80
+ test 'status code is 200' do |response|
81
+ response.code == '200'
82
+ end
83
+ end
84
+ end
85
+
86
+ end
87
+ ```
88
+
89
+ The `project` command takes a project description, and a block containing multiple
90
+ tests to be run together for that project.
91
+
92
+ `server` lists hosts that should be tested. It may be called multiple times, and
93
+ also parses host range expressions using the HostRange gem.
94
+
95
+ `http` is an example of a connection, it takes an argument with the port to
96
+ connect, and a block containing multiple requests to make.
97
+
98
+ `get` is a request, in this case an HTTP GET to the provided url, and takes a block
99
+ with multiple tests to run on the response. Plugins can return any object from
100
+ a request, in the case of `http` the response is an HTTP::Reponse object.
101
+
102
+ `test` describes a test to run on the provided response; it takes a description,
103
+ and the response is passed to a block. The result of executing the block is
104
+ presented as the result of this test (in this case true or false).
105
+
106
+ Save the config to a file `fe_web` and run with the `aug` command:
107
+
108
+ $ aug ./fe_web
109
+ [web-fe-01]
110
+ status code is 200 ✓
111
+ [web-fe-02]
112
+ status code is 200 ✓
113
+
114
+ ### Example 2 - adding more tests
115
+
116
+ Let's extend our example to be more interesting.
117
+
118
+ ```ruby
119
+ project "Front-end Web Servers" do
120
+ server 'web-fe-[01-02]', :web
121
+ server 'www.mydomain.com', :vip, :port => 80
122
+
123
+ socket 8000 do
124
+ roles :web
125
+ open? do
126
+ test "port 8000 is open?"
127
+ end
128
+ end
129
+
130
+ http 8000 do
131
+ roles :web, :vip
132
+
133
+ get '/' do
134
+ test 'status code is 200' do |response|
135
+ response.code == '200'
136
+ end
137
+
138
+ test 'document title' do |response|
139
+ response.body.match /<title>([\w\s]+)<\/title>/
140
+ end
141
+ end
142
+
143
+ get '/image.png' do
144
+ header 'user-agent: Auger Test'
145
+
146
+ test 'image.png has correct content-type' do |respose|
147
+ response['Content-Type'] == 'image/png'
148
+ end
149
+ end
150
+ end
151
+
152
+ end
153
+ ```
154
+
155
+ Servers can have roles attached to them, in this case `:web` and
156
+ `:vip`. By default a connection will be run for all servers, but the
157
+ `roles` command allows connections to be limited to the given roles.
158
+
159
+ Servers can also have a hash of options, which will override
160
+ the matching connection options for just that server. In this case
161
+ we want to connect to port 80 on the vip rather than 8000.
162
+
163
+ The `header` command demonstrates setting options for a request,
164
+ in this case setting an http request header.
165
+
166
+ The `socket` command creates a connection to the given port, and
167
+ `open?` returns true if the port is open. We just apply this to
168
+ the real web servers and not the vip.
169
+
170
+ The document title test demonstrates how to extract and return a regex
171
+ match. Tests can return almost any object (including Exceptions), and
172
+ auger will try to display the result using the `.to_s` method. Ruby's
173
+ MatchData object, however, gets special treatment. If the MatchData
174
+ has captures (captured using parentheses in the regex) they will be
175
+ displayed, as in this case. If no captures, the MatchData will be
176
+ treated as a boolean. The `aug` cmdline client displays booleans with
177
+ a checkmark or an 'x'.
178
+
179
+ ### Example 3 - testing ElasticSearch
180
+
181
+ ```ruby
182
+ require 'json'
183
+
184
+ project "Elasticsearch" do
185
+ servers 'prod-es-[01-04]'
186
+
187
+ http 9200 do
188
+ get "/_cluster/health" do
189
+
190
+ # this runs after request returns, but before tests
191
+ # use it to munge response body from json string into a hash
192
+ before_tests do |r|
193
+ r.body = JSON.parse(r.body)
194
+ end
195
+
196
+ test "Status 200" do |r|
197
+ r.code == '200'
198
+ end
199
+
200
+ # Now we'll define an array called stats, which contains all the keys we
201
+ # want to retrieve values from in our /_cluster/health output. In this
202
+ # case, we'll just return the body of the response, as it's relatively
203
+ # small. You can of course parse this however you'd like for this or
204
+ # other cases.
205
+ stats = %w[
206
+ cluster_name
207
+ status
208
+ timed_out
209
+ number_of_nodes
210
+ number_of_data_nodes
211
+ active_primary_shards
212
+ active_shards
213
+ relocating_shards
214
+ initializing_shards
215
+ unassigned_shards
216
+ ]
217
+
218
+ stats.each do |stat|
219
+ test "#{stat}" do |r|
220
+ r.body[stat]
221
+ end
222
+ end
223
+
224
+ # I've discovered that a typical fail case with elasticsearch is
225
+ # that on occassion, nodes will come up and not join the cluster
226
+ # This is an easy way to see if the number of nodes that the host
227
+ # actually sees (actual_data_nodes) matches what we're
228
+ # expecting (expected_data_nodes).
229
+ # TODO: dynamically update expected_data_nodes based on defined hosts:
230
+ test "Expected vs Actual Nodes" do |r|
231
+ r.body['number_of_data_nodes'] == 8
232
+ end
233
+
234
+ end
235
+
236
+ end
237
+ ```
238
+
239
+ ## Writing plugins
240
+
241
+ Let's look at a simplified http plugin.
242
+
243
+ ```ruby
244
+ require "net/http"
245
+
246
+ module Auger
247
+
248
+ class Project
249
+ def http(port = 80, &block)
250
+ @connections << Http.load(port, &block)
251
+ end
252
+ end
253
+
254
+ class Http < Auger::Connection
255
+ def open(host, options)
256
+ http = Net::HTTP.new(host, options[:port])
257
+ http.start
258
+ http
259
+ end
260
+
261
+ def close(http)
262
+ http.finish
263
+ end
264
+
265
+ def get(url, &block)
266
+ @requests << Auger::HttpRequest.load(url, &block)
267
+ end
268
+ end
269
+
270
+ class HttpRequest < Auger::Request
271
+ def run(http)
272
+ get = Net::HTTP::Get.new(@arg)
273
+ http.request(get)
274
+ end
275
+ end
276
+
277
+ end
278
+ ```
279
+
280
+ First, we add the `http` method to the Project class. This simply causes
281
+ the 'http' command to add a connection of class Http to the project's
282
+ list of connections.
283
+
284
+ Next, we define the Http connection class by sub-classing `Auger::Connection`.
285
+ A connection class needs to define `open` and `close` methods, which will
286
+ create and destroy a connection object (in this case a Net::HTTP object).
287
+ `open` takes a hostname and the connection @options hash, and returns an
288
+ instance of the relevant request object.
289
+
290
+
291
+ ## Command Line Auto-completion for aug tool
292
+
293
+ BASH completion (with file completion and a rolling cache, if you're incredibly impatient like me):
294
+ ```bash
295
+ _augcomp()
296
+ {
297
+ count=100
298
+ augcache="/tmp/.aug_cache"
299
+ augcounter="/tmp/.aug_counter"
300
+
301
+ # if the cache or the counter don't exist, create
302
+ if [ ! -f "$augcache" ] || [ ! -f "$augcounter" ]
303
+ then
304
+ aug -l >$augcache && echo 0 >$augcounter
305
+ else
306
+ # if the counter reaches $count, re-generate the complete list
307
+ if [ $(cat "$augcounter") -eq "$count" ]
308
+ then
309
+ aug -l >$augcache && echo 0 >$augcounter
310
+ # if the counter hasn't reached $count, increment it
311
+ else
312
+ expr $(cat $augcounter) + 1 >$augcounter
313
+ fi
314
+ fi
315
+ augcfgs=$(cat "$augcache" | xargs)
316
+
317
+ word=${COMP_WORDS[COMP_CWORD]}
318
+
319
+ _compopt_o_filenames
320
+ COMPREPLY=($(compgen -f -W "$augcfgs" -- "${word}"))
321
+ }
322
+ complete -F _augcomp aug
323
+ ```
324
+
325
+ ZSH completion:
326
+
327
+ _augprojects () { _files; compadd $(aug -l) }
328
+ compdef _augprojects aug
329
+
330
+ ## Pull Requests
331
+
332
+ * yes please
333
+ * new plugins and genereal bug fixes, updates, etc are all welcome
334
+ * generally, we'd prefer you do the following to submit a pull:
335
+ * fork
336
+ * create a local topic branch
337
+ * make your changes and push
338
+ * submit your pull request
339
+
data/Rakefile ADDED
@@ -0,0 +1,104 @@
1
+ #!/usr/bin/env rake
2
+ require "bundler/gem_tasks"
3
+ require "rainbow"
4
+
5
+ ## begin version management
6
+ def valid? version
7
+ pattern = /^\d+\.\d+\.\d+(\-(dev|beta|rc\d+))?$/
8
+ raise "Tried to set invalid version: #{version}".color(:red) unless version =~ pattern
9
+ end
10
+
11
+ def correct_version version
12
+ ver, flag = version.split '-'
13
+ v = ver.split '.'
14
+ (0..2).each do |n|
15
+ v[n] = v[n].to_i
16
+ end
17
+ [v.join('.'), flag].compact.join '-'
18
+ end
19
+
20
+ def read_version
21
+ begin
22
+ File.read 'VERSION'
23
+ rescue
24
+ raise "VERSION file not found or unreadable.".color(:red)
25
+ end
26
+ end
27
+
28
+ def write_version version
29
+ valid? version
30
+ begin
31
+ File.open 'VERSION', 'w' do |file|
32
+ file.write correct_version(version)
33
+ end
34
+ rescue
35
+ raise "VERSION file not found or unwritable.".color(:red)
36
+ end
37
+ end
38
+
39
+ def reset current, which
40
+ version, flag = current.split '-'
41
+ v = version.split '.'
42
+ which.each do |part|
43
+ v[part] = 0
44
+ end
45
+ [v.join('.'), flag].compact.join '-'
46
+ end
47
+
48
+ def increment current, which
49
+ version, flag = current.split '-'
50
+ v = version.split '.'
51
+ v[which] = v[which].to_i + 1
52
+ [v.join('.'), flag].compact.join '-'
53
+ end
54
+
55
+ desc "Prints the current application version"
56
+ version = read_version
57
+ task :version do
58
+ puts <<HELP
59
+ Available commands are:
60
+ -----------------------
61
+ rake version:write[version] # set version explicitly
62
+ rake version:patch # increment the patch x.x.x+1
63
+ rake version:minor # increment minor and reset patch x.x+1.0
64
+ rake version:major # increment major and reset others x+1.0.0
65
+
66
+ HELP
67
+ puts "Current version is: #{version.color(:green)}"
68
+ puts "NOTE: version should always be in the format of x.x.x".color(:red)
69
+ end
70
+
71
+ namespace :version do
72
+
73
+ desc "Write version explicitly by specifying version number as a parameter"
74
+ task :write, [:version] do |task, args|
75
+ write_version args[:version].strip
76
+ puts "Version explicitly written: #{read_version.color(:green)}"
77
+ end
78
+
79
+ desc "Increments the patch version"
80
+ task :patch do
81
+ new_version = increment read_version, 2
82
+ write_version new_version
83
+ puts "Application patched: #{new_version.color(:green)}"
84
+ end
85
+
86
+ desc "Increments the minor version and resets the patch"
87
+ task :minor do
88
+ incremented = increment read_version, 1
89
+ new_version = reset incremented, [2]
90
+ write_version new_version
91
+ puts "New version released: #{new_version.color(:green)}"
92
+ end
93
+
94
+ desc "Increments the major version and resets both minor and patch"
95
+ task :major do
96
+ incremented = increment read_version, 0
97
+ new_version = reset incremented, [1, 2]
98
+ write_version new_version
99
+ puts "Major application version change: #{new_version.color(:green)}. Congratulations!"
100
+ end
101
+
102
+ end
103
+ ## end version management
104
+