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 +17 -0
- data/Gemfile +11 -0
- data/LICENSE +22 -0
- data/README.md +339 -0
- data/Rakefile +104 -0
- data/VERSION +1 -0
- data/auger.gemspec +26 -0
- data/bin/aug +112 -0
- data/cfg/examples/elasticsearch.rb +75 -0
- data/cfg/examples/redis.rb +31 -0
- data/cfg/examples/riak.rb +42 -0
- data/cfg/examples/webserver.rb +56 -0
- data/lib/auger.rb +12 -0
- data/lib/auger/config.rb +22 -0
- data/lib/auger/connection.rb +39 -0
- data/lib/auger/project.rb +64 -0
- data/lib/auger/request.rb +40 -0
- data/lib/auger/result.rb +27 -0
- data/lib/auger/server.rb +13 -0
- data/lib/auger/test.rb +25 -0
- data/lib/auger/version.rb +3 -0
- data/lib/plugins/cql.rb +31 -0
- data/lib/plugins/dns.rb +44 -0
- data/lib/plugins/http.rb +68 -0
- data/lib/plugins/redis.rb +73 -0
- data/lib/plugins/socket.rb +30 -0
- data/lib/plugins/telnet.rb +37 -0
- metadata +171 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
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
|
+
|