auger 1.2.0

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.
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
+