barking_iguana-compound 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +9 -0
- data/.rspec +2 -0
- data/Gemfile +4 -0
- data/README.md +179 -0
- data/Rakefile +6 -0
- data/barking_iguana-compound.gemspec +44 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/lib/barking_iguana/compound.rb +27 -0
- data/lib/barking_iguana/compound/ansible.rb +13 -0
- data/lib/barking_iguana/compound/ansible/inventory.rb +28 -0
- data/lib/barking_iguana/compound/ansible/inventory_parser.rb +165 -0
- data/lib/barking_iguana/compound/ansible/playbook.rb +96 -0
- data/lib/barking_iguana/compound/host.rb +21 -0
- data/lib/barking_iguana/compound/host_manager.rb +63 -0
- data/lib/barking_iguana/compound/server_spec.rb +66 -0
- data/lib/barking_iguana/compound/spec_helper.rb +6 -0
- data/lib/barking_iguana/compound/test.rb +84 -0
- data/lib/barking_iguana/compound/test_stage.rb +102 -0
- data/lib/barking_iguana/compound/test_suite.rb +75 -0
- data/lib/barking_iguana/compound/vagrant.rb +79 -0
- data/lib/barking_iguana/compound/version.rb +5 -0
- data/resources/Vagrantfile.erb +18 -0
- metadata +221 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: e3c49e4fa77451a0f78931175dfeedfa2736d3a8
|
4
|
+
data.tar.gz: dc148ad917a0182a1a8e94db603a5d12f53c5a85
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: f4a53fe0a6346889d69ccf9fa1933c4743ae4932afbde85799237ac837be6c10bb538cb58b14d202a2a53a12b9a4b2e1c79e2dcbe0a0b380312b93badc712eb3
|
7
|
+
data.tar.gz: 4890023cfae6d8c76e4fa97fa9cceb14f39930a37861b73f7a35eea57b0f96eaaf0d114a6ff684ff283b0083d9c5d11d9e572e933e8c3b12d31fd1c907c47f36
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/Gemfile
ADDED
data/README.md
ADDED
@@ -0,0 +1,179 @@
|
|
1
|
+
# BarkingIguana::Compound
|
2
|
+
|
3
|
+
Compound testing for Ansible atoms.
|
4
|
+
|
5
|
+
When you need to test several roles and/or playbooks together over their lifetime e.g. testing database failover and recovery.
|
6
|
+
|
7
|
+
## Installation
|
8
|
+
|
9
|
+
Add this line to your application's Gemfile:
|
10
|
+
|
11
|
+
```ruby
|
12
|
+
gem 'barking_iguana-compound'
|
13
|
+
```
|
14
|
+
|
15
|
+
And then execute:
|
16
|
+
|
17
|
+
$ bundle
|
18
|
+
|
19
|
+
Or install it yourself as:
|
20
|
+
|
21
|
+
$ gem install barking_iguana-compound
|
22
|
+
|
23
|
+
## Usage
|
24
|
+
|
25
|
+
Install it, as per above.
|
26
|
+
|
27
|
+
Normally you'll use Compond as part of a Rake task, so include it in your
|
28
|
+
`Rakefile`. Tell it to define Rake tasks for your compound tests - we assume
|
29
|
+
these live in `test/compound` - and where your playbooks live, your control
|
30
|
+
repository:
|
31
|
+
|
32
|
+
```ruby
|
33
|
+
require 'barking_iguana/compound'
|
34
|
+
ansible_control_repo = File.dirname __FILE__
|
35
|
+
BarkingIguana::Compound.new('test/compound', ansible_control_repo).define_rake_tasks
|
36
|
+
```
|
37
|
+
|
38
|
+
Now let's define a test managing hosts files. It's a trivial test, but it
|
39
|
+
suffices to demonstrate the capacilities of Compound.
|
40
|
+
|
41
|
+
### Writing a compound test
|
42
|
+
|
43
|
+
Create a directory for the test:
|
44
|
+
|
45
|
+
$ mkdir -p test/compound/hosts_file_management
|
46
|
+
|
47
|
+
Each test will have several _stages_, which represent parts of the expected
|
48
|
+
lifecycle of your servers. For example, you set up your servers using Ansible,
|
49
|
+
to you probably have a setup stage. You may be setting up a HA cluster, so
|
50
|
+
perhaps you have a stage where the master in the cluster fails.
|
51
|
+
|
52
|
+
Each stage involves several actions. We'll cover them in detail below, but in
|
53
|
+
summary:
|
54
|
+
|
55
|
+
The `setup` action runs first. For each stage this action decides which virtual
|
56
|
+
machines should be powered on or off during the rest of the stage.
|
57
|
+
|
58
|
+
Next, the `converge` action runs the playbook for that stage, if one is provided
|
59
|
+
by you, to allow things to happen. The playbook may apply roles or kill
|
60
|
+
processes, whatever you need to simulate what's happening to your servers at
|
61
|
+
that part of the lifecycle.
|
62
|
+
|
63
|
+
Finally, the `verify` stage runs some serverspec tests which you will define
|
64
|
+
against the results of the `converge` action. This allows you to check what
|
65
|
+
happens after the tasks in the playbook have been run e.g. you may verify that a
|
66
|
+
standby server has become the master.
|
67
|
+
|
68
|
+
Each stage lives in a directory inside the test. They're executed in
|
69
|
+
alphabetical order, and you are encouraged to prefix each stage with numbers to
|
70
|
+
make it very clear which one should execute in which order.
|
71
|
+
|
72
|
+
We'll cover each of those actions in greater detail now, for the setup stage of
|
73
|
+
our hosts file example.
|
74
|
+
|
75
|
+
#### Test Stages
|
76
|
+
|
77
|
+
Start by creating the directory for that stage:
|
78
|
+
|
79
|
+
$ mkdir -p test/compound/hosts_file_management/000-setup
|
80
|
+
|
81
|
+
##### The Setup Action
|
82
|
+
|
83
|
+
This action controls which virtual machines are available for you to work with,
|
84
|
+
by looking at an Ansible inventory file in the stage directory and starting all
|
85
|
+
the hosts it finds.
|
86
|
+
|
87
|
+
Let's create a simple inventory file with 2 hosts and 1 group.
|
88
|
+
|
89
|
+
The inventory file for each stage lives in the stage directory in a file called
|
90
|
+
`inventory`, so in our example so for that's `test/compound/hosts_file_management/000-setup/inventory`.
|
91
|
+
|
92
|
+
They're just normal Ansible inventory files, with only one restriction: the
|
93
|
+
`ansible_host` _must_ start with `10.8.`.
|
94
|
+
|
95
|
+
```
|
96
|
+
[linux]
|
97
|
+
host001 ansible_host=10.8.100.11
|
98
|
+
host002 ansible_host=10.8.100.12
|
99
|
+
```
|
100
|
+
|
101
|
+
When the setup action for a stage with this inventory is run, Compound will
|
102
|
+
launch two virtual machines for you to test with, `host001` and `host002` with
|
103
|
+
the respective IP addresses.
|
104
|
+
|
105
|
+
##### The Converge Action
|
106
|
+
|
107
|
+
Now that virtual machines are available to use, the converge action can run
|
108
|
+
the playbook for your stage against them.
|
109
|
+
|
110
|
+
Playbooks live in the stage directory, in a file called `playbook.yml`. I'm
|
111
|
+
inventive like that. For our example stage that's `test/compound/hosts_file_management/000-setup/playbook.yml`.
|
112
|
+
|
113
|
+
For our setup action we'll apply the `hosts` role to all hosts.
|
114
|
+
|
115
|
+
```
|
116
|
+
---
|
117
|
+
- hosts: all
|
118
|
+
roles:
|
119
|
+
- hosts
|
120
|
+
```
|
121
|
+
|
122
|
+
After a converge, Compound will attempt to verify whatever you ask it to.
|
123
|
+
Onwards, to the verify action.
|
124
|
+
|
125
|
+
##### The Verify Action
|
126
|
+
|
127
|
+
Now we run automated tests to assert that our virtual machines behave like we
|
128
|
+
think they should, after the stage playbook has been applied in the converge
|
129
|
+
action.
|
130
|
+
|
131
|
+
The verify action is based around serverspec tests, arranged around the name of
|
132
|
+
the host which they test.
|
133
|
+
|
134
|
+
For example, to test `host001` we'd create tests under a `test/compound/hosts_file_management/host001/`
|
135
|
+
directory. Each test file must be suffixed with `_spec.rb`. A simple example
|
136
|
+
would be checking that the other host of the pair is resolvable now that the
|
137
|
+
hosts role has been applied to the hosts:
|
138
|
+
|
139
|
+
```ruby
|
140
|
+
# test/compound/hosts_file_management/host001/resolving_spec.rb
|
141
|
+
describe host('host002') do
|
142
|
+
it "is resolvable" do
|
143
|
+
expect(subject).to be_resolvable
|
144
|
+
end
|
145
|
+
end
|
146
|
+
```
|
147
|
+
|
148
|
+
```ruby
|
149
|
+
# test/compound/hosts_file_management/host002/resolving_spec.rb
|
150
|
+
describe host('host001') do
|
151
|
+
it "is resolvable" do
|
152
|
+
expect(subject).to be_resolvable
|
153
|
+
end
|
154
|
+
end
|
155
|
+
```
|
156
|
+
|
157
|
+
Compound will take the appropriate actions to make sure your tests are run on
|
158
|
+
the correct hosts, you don't need to worry about SSH keys, passwords or ports.
|
159
|
+
|
160
|
+
### Running the tests
|
161
|
+
|
162
|
+
Since we've asked Compound to define rake tasks above, we can run those. The tasks generated are based on the directory names we use in the tests. The above test can be run like this:
|
163
|
+
|
164
|
+
$ bundle exec rake compound:hosts_file_management
|
165
|
+
|
166
|
+
You can see a list of all tests by asking Rake to list them:
|
167
|
+
|
168
|
+
$ bundle exec rake -T
|
169
|
+
|
170
|
+
## Development
|
171
|
+
|
172
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
173
|
+
|
174
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
175
|
+
|
176
|
+
## Contributing
|
177
|
+
|
178
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/barking_iguana-compound.
|
179
|
+
|
data/Rakefile
ADDED
@@ -0,0 +1,44 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'barking_iguana/compound/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "barking_iguana-compound"
|
8
|
+
spec.version = BarkingIguana::Compound::VERSION
|
9
|
+
spec.authors = ["Craig R Webster"]
|
10
|
+
spec.email = ["craig@barkingiguana.com"]
|
11
|
+
|
12
|
+
spec.summary = %q{Compound testing of Ansible playbooks}
|
13
|
+
spec.description = spec.summary
|
14
|
+
spec.homepage = "https://github.com/barkingiguana/compound"
|
15
|
+
|
16
|
+
# Prevent pushing this gem to RubyGems.org. To allow pushes either set the 'allowed_push_host'
|
17
|
+
# to allow pushing to a single host or delete this section to allow pushing to any host.
|
18
|
+
if spec.respond_to?(:metadata)
|
19
|
+
spec.metadata['allowed_push_host'] = "https://rubygems.org"
|
20
|
+
else
|
21
|
+
raise "RubyGems 2.0 or newer is required to protect against " \
|
22
|
+
"public gem pushes."
|
23
|
+
end
|
24
|
+
|
25
|
+
spec.files = `git ls-files -z`.split("\x0").reject do |f|
|
26
|
+
f.match(%r{^(test|spec|features)/})
|
27
|
+
end
|
28
|
+
spec.bindir = "exe"
|
29
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
30
|
+
spec.require_paths = ["lib"]
|
31
|
+
|
32
|
+
spec.add_development_dependency "bundler", "~> 1.13"
|
33
|
+
spec.add_development_dependency "rake", "~> 10.0"
|
34
|
+
spec.add_development_dependency "rspec", "~> 3.0"
|
35
|
+
|
36
|
+
spec.add_dependency 'barking_iguana-logging'
|
37
|
+
spec.add_dependency 'barking_iguana-benchmark'
|
38
|
+
spec.add_dependency 'mixlib-shellout'
|
39
|
+
spec.add_dependency 'rspec-wait'
|
40
|
+
spec.add_dependency 'ansible_spec'
|
41
|
+
spec.add_dependency 'colorize'
|
42
|
+
spec.add_dependency 'hostlist_expression'
|
43
|
+
spec.add_dependency 'oj'
|
44
|
+
end
|
data/bin/console
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "bundler/setup"
|
4
|
+
require "barking_iguana/compound"
|
5
|
+
|
6
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
7
|
+
# with your gem easier. You can also use a different console, if you like.
|
8
|
+
|
9
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
10
|
+
# require "pry"
|
11
|
+
# Pry.start
|
12
|
+
|
13
|
+
require "irb"
|
14
|
+
IRB.start
|
data/bin/setup
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
require 'barking_iguana/logging'
|
2
|
+
require 'barking_iguana/benchmark'
|
3
|
+
|
4
|
+
require 'erb'
|
5
|
+
require 'mixlib/shellout'
|
6
|
+
require 'hostlist_expression'
|
7
|
+
require 'oj'
|
8
|
+
require 'yaml'
|
9
|
+
|
10
|
+
require 'barking_iguana/compound/version'
|
11
|
+
require 'barking_iguana/compound/ansible'
|
12
|
+
require 'barking_iguana/compound/ansible/inventory'
|
13
|
+
require 'barking_iguana/compound/ansible/inventory_parser'
|
14
|
+
require 'barking_iguana/compound/ansible/playbook'
|
15
|
+
require 'barking_iguana/compound/test_stage'
|
16
|
+
require 'barking_iguana/compound/test'
|
17
|
+
require 'barking_iguana/compound/test_suite'
|
18
|
+
require 'barking_iguana/compound/host_manager'
|
19
|
+
require 'barking_iguana/compound/host'
|
20
|
+
require 'barking_iguana/compound/vagrant'
|
21
|
+
|
22
|
+
module BarkingIguana
|
23
|
+
module Compound
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
BarkingIguana::Logging.default_level = Logger.const_get(ENV['LOG_LEVEL'].upcase) if ENV['LOG_LEVEL']
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module BarkingIguana
|
2
|
+
module Compound
|
3
|
+
module Ansible
|
4
|
+
class Inventory
|
5
|
+
attr_accessor :path
|
6
|
+
private :path=
|
7
|
+
|
8
|
+
def initialize path
|
9
|
+
self.path = path
|
10
|
+
end
|
11
|
+
|
12
|
+
def hostgroups
|
13
|
+
InventoryParser.load_targets path
|
14
|
+
end
|
15
|
+
|
16
|
+
def hosts
|
17
|
+
h = hostgroups
|
18
|
+
h.reject! { |hg| hg[0].empty? }
|
19
|
+
hosts = h.values.flatten.uniq { |h| h['uri'] }
|
20
|
+
hosts.map do |data|
|
21
|
+
name = data['name'].gsub(/ .*/, '')
|
22
|
+
Host.new name: name, uri: data['uri']
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,165 @@
|
|
1
|
+
module BarkingIguana
|
2
|
+
module Compound
|
3
|
+
module Ansible
|
4
|
+
class InventoryParser
|
5
|
+
def self.get_variables(host, group_idx, hosts=nil)
|
6
|
+
vars = {}
|
7
|
+
p = self.get_properties
|
8
|
+
|
9
|
+
# roles default
|
10
|
+
p[group_idx]['roles'].each do |role|
|
11
|
+
vars = load_vars_file(vars ,"roles/#{role}/defaults/main.yml")
|
12
|
+
end
|
13
|
+
|
14
|
+
# all group
|
15
|
+
vars = load_vars_file(vars ,'group_vars/all', true)
|
16
|
+
|
17
|
+
# each group vars
|
18
|
+
if p[group_idx].has_key?('group')
|
19
|
+
vars = load_vars_file(vars ,"group_vars/#{p[group_idx]['group']}", true)
|
20
|
+
end
|
21
|
+
|
22
|
+
# each host vars
|
23
|
+
vars = load_vars_file(vars ,"host_vars/#{host}", true)
|
24
|
+
|
25
|
+
# site vars
|
26
|
+
if p[group_idx].has_key?('vars')
|
27
|
+
vars = merge_variables(vars, p[group_idx]['vars'])
|
28
|
+
end
|
29
|
+
|
30
|
+
# roles vars
|
31
|
+
p[group_idx]['roles'].each do |role|
|
32
|
+
vars = load_vars_file(vars ,"roles/#{role}/vars/main.yml")
|
33
|
+
end
|
34
|
+
|
35
|
+
# multiple host and children dependencies group vars
|
36
|
+
unless hosts.nil? || p[group_idx]["hosts_childrens"].nil?
|
37
|
+
hosts_childrens = p[group_idx]["hosts_childrens"]
|
38
|
+
next_find_target = hosts
|
39
|
+
while(!next_find_target.nil? && hosts_childrens.size > 0)
|
40
|
+
vars = load_vars_file(vars ,"group_vars/#{next_find_target}", true)
|
41
|
+
group_vars_file = find_group_vars_file(hosts_childrens,next_find_target)
|
42
|
+
next_find_target = group_vars_file
|
43
|
+
hosts_childrens.delete(group_vars_file)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
return vars
|
48
|
+
|
49
|
+
end
|
50
|
+
|
51
|
+
# param hash {"server"=>["192.168.0.103"], "databases"=>["192.168.0.104"], "pg:children"=>["server", "databases"]}
|
52
|
+
# param search ":children"
|
53
|
+
# param k "pg:children"
|
54
|
+
# return {"server"=>["192.168.0.103"], "databases"=>["192.168.0.104"], "pg"=>["192.168.0.103", "192.168.0.104"]}
|
55
|
+
def self.get_parent(hash,search,k)
|
56
|
+
k_parent = k.gsub(search,'')
|
57
|
+
arry = Array.new
|
58
|
+
hash["#{k}"].each{|group|
|
59
|
+
next if hash["#{group}"].nil?
|
60
|
+
arry = arry + hash["#{group}"]
|
61
|
+
}
|
62
|
+
h = Hash.new
|
63
|
+
h["#{k_parent}"] = arry
|
64
|
+
return h
|
65
|
+
end
|
66
|
+
|
67
|
+
def self.load_targets(file)
|
68
|
+
f = File.open(file).read
|
69
|
+
groups = Hash.new
|
70
|
+
group = ''
|
71
|
+
hosts = Hash.new
|
72
|
+
hosts.default = Hash.new
|
73
|
+
f.each_line{|line|
|
74
|
+
line = line.chomp
|
75
|
+
# skip
|
76
|
+
next if line.start_with?('#') #comment
|
77
|
+
next if line.empty? == true #null
|
78
|
+
|
79
|
+
# get group
|
80
|
+
if line.start_with?('[') && line.end_with?(']')
|
81
|
+
group = line.gsub('[','').gsub(']','')
|
82
|
+
groups["#{group}"] = Array.new
|
83
|
+
next
|
84
|
+
end
|
85
|
+
|
86
|
+
# get host
|
87
|
+
host_name = line.split[0]
|
88
|
+
if group.empty? == false
|
89
|
+
if groups.has_key?(line)
|
90
|
+
groups["#{group}"] << line
|
91
|
+
next
|
92
|
+
elsif host_name.include?("[") && host_name.include?("]")
|
93
|
+
# www[01:50].example.com
|
94
|
+
# db-[a:f].example.com
|
95
|
+
hostlist_expression(line,":").each{|h|
|
96
|
+
host = hosts[h.split[0]]
|
97
|
+
groups["#{group}"] << get_inventory_param(h).merge(host)
|
98
|
+
}
|
99
|
+
next
|
100
|
+
else
|
101
|
+
# 1つのみ、かつ:を含まない場合
|
102
|
+
# 192.168.0.1
|
103
|
+
# 192.168.0.1 ansible_ssh_host=127.0.0.1 ...
|
104
|
+
host = hosts[host_name]
|
105
|
+
groups["#{group}"] << get_inventory_param(line).merge(host)
|
106
|
+
next
|
107
|
+
end
|
108
|
+
else
|
109
|
+
if host_name.include?("[") && host_name.include?("]")
|
110
|
+
hostlist_expression(line, ":").each{|h|
|
111
|
+
hosts[h.split[0]] = get_inventory_param(h)
|
112
|
+
}
|
113
|
+
else
|
114
|
+
hosts[host_name] = get_inventory_param(line)
|
115
|
+
end
|
116
|
+
end
|
117
|
+
}
|
118
|
+
|
119
|
+
# parse children [group:children]
|
120
|
+
search = Regexp.new(":children".to_s)
|
121
|
+
groups.keys.each{|k|
|
122
|
+
unless (k =~ search).nil?
|
123
|
+
# get group parent & merge parent
|
124
|
+
groups.merge!(get_parent(groups,search,k))
|
125
|
+
# delete group children
|
126
|
+
if groups.has_key?("#{k}") && groups.has_key?("#{k.gsub(search,'')}")
|
127
|
+
groups.delete("#{k}")
|
128
|
+
end
|
129
|
+
end
|
130
|
+
}
|
131
|
+
return groups
|
132
|
+
end
|
133
|
+
|
134
|
+
# param ansible_ssh_port=22
|
135
|
+
# return: hash
|
136
|
+
def self.get_inventory_param(line)
|
137
|
+
host = Hash.new
|
138
|
+
# 初期値
|
139
|
+
host['name'] = line
|
140
|
+
host['port'] = 22
|
141
|
+
if line.include?(":") # 192.168.0.1:22
|
142
|
+
host['uri'] = line.split(":")[0]
|
143
|
+
host['port'] = line.split(":")[1].to_i
|
144
|
+
return host
|
145
|
+
end
|
146
|
+
# 192.168.0.1 ansible_ssh_port=22
|
147
|
+
line.split.each{|v|
|
148
|
+
unless v.include?("=")
|
149
|
+
host['uri'] = v
|
150
|
+
else
|
151
|
+
key,value = v.split("=")
|
152
|
+
host['port'] = value.to_i if key == "ansible_ssh_port" or key == "ansible_port"
|
153
|
+
host['private_key'] = value if key == "ansible_ssh_private_key_file"
|
154
|
+
host['user'] = value if key == "ansible_ssh_user" or key == "ansible_user"
|
155
|
+
host['uri'] = value if key == "ansible_ssh_host" or key == "ansible_host"
|
156
|
+
host['pass'] = value if key == "ansible_ssh_pass"
|
157
|
+
host['connection'] = value if key == "ansible_connection"
|
158
|
+
end
|
159
|
+
}
|
160
|
+
return host
|
161
|
+
end
|
162
|
+
end
|
163
|
+
end
|
164
|
+
end
|
165
|
+
end
|