chef-workflow-testlib 0.1.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 +19 -0
- data/Gemfile +8 -0
- data/LICENSE.txt +22 -0
- data/README.md +316 -0
- data/Rakefile +9 -0
- data/chef-workflow-testlib.gemspec +26 -0
- data/lib/chef-workflow/helper.rb +12 -0
- data/lib/chef-workflow/helpers/chef.rb +15 -0
- data/lib/chef-workflow/helpers/minitest.rb +39 -0
- data/lib/chef-workflow/helpers/provision.rb +36 -0
- data/lib/chef-workflow/helpers/ssh.rb +99 -0
- data/lib/chef-workflow/runner/provisioned.rb +23 -0
- data/lib/chef-workflow/test-case/ec2.rb +41 -0
- data/lib/chef-workflow/test-case/provisioned.rb +88 -0
- data/lib/chef-workflow/test-case/vagrant.rb +44 -0
- data/lib/chef-workflow-testlib/version.rb +7 -0
- data/lib/chef-workflow-testlib.rb +1 -0
- metadata +143 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2012 Erik Hollensbe
|
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,316 @@
|
|
1
|
+
# Chef Workflow - Test Libraries and Tooling
|
2
|
+
|
3
|
+
This gem provides a set of libraries to drive
|
4
|
+
[minitest](https://github.com/seattlerb/minitest) integration for a number of things:
|
5
|
+
|
6
|
+
1. spawning whole networks of machines
|
7
|
+
2. testing their converge successes
|
8
|
+
3. testing interoperability between machines
|
9
|
+
4. testing advanced functionality in chef, like search.
|
10
|
+
|
11
|
+
**It is not intended for testing individual cookbooks.** The default
|
12
|
+
provisioning systems expect roles, for example, but it's a different tool for a
|
13
|
+
different problem. If you want to verify your open-source cookbooks against a
|
14
|
+
variety of environments, I strongly suggest you look at
|
15
|
+
[test-kitchen](https://github.com/opscode/test-kitchen), which is intended to
|
16
|
+
solve this problem.
|
17
|
+
|
18
|
+
## Installation
|
19
|
+
|
20
|
+
Add this line to your application's Gemfile:
|
21
|
+
|
22
|
+
gem 'chef-workflow-testlib'
|
23
|
+
|
24
|
+
And then execute:
|
25
|
+
|
26
|
+
$ bundle
|
27
|
+
|
28
|
+
Or install it yourself as:
|
29
|
+
|
30
|
+
$ gem install chef-workflow-testlib
|
31
|
+
|
32
|
+
## Quick Start
|
33
|
+
|
34
|
+
The easiest way to get started is to integrate
|
35
|
+
[chef-workflow-tasklib](https://github.com/chef-workflow/chef-workflow-tasklib)
|
36
|
+
into your environment. This has a number of benefits, such as working within
|
37
|
+
the same system to control machine provisioning, building a chef server,
|
38
|
+
uploading your repository to a chef server, several options for driving tests,
|
39
|
+
and so on. Once you've set that up, use the instructions over there to build a
|
40
|
+
test chef server.
|
41
|
+
|
42
|
+
So, after you've done that go ahead and create a `test` directory at the root
|
43
|
+
of your repository. Inside that, create a `test_mytest.rb` that provisions a
|
44
|
+
few machines and performs a search on them. This actually doesn't have much
|
45
|
+
real-world application, but it'll get you familiar with the workflow.
|
46
|
+
|
47
|
+
You will need some working roles to perform this test. We'll name these roles
|
48
|
+
`my_role` and `my_other_role` in the example, but you'll need to replace them
|
49
|
+
with roles you actually use.
|
50
|
+
|
51
|
+
This test will use `vagrant` to provision your machines, bootstrap them with
|
52
|
+
knife, then run your tests. After it's done, it will tear those machines down.
|
53
|
+
If the nodes fail to come up for any reason, your test should yield an error
|
54
|
+
and deprovision the machines.
|
55
|
+
|
56
|
+
```ruby
|
57
|
+
require 'chef-workflow/helper'
|
58
|
+
class MyTest < MiniTest::Unit::VagrantTestCase
|
59
|
+
def self.before_suite
|
60
|
+
provision('my_role')
|
61
|
+
provision('my_other_role')
|
62
|
+
wait_for('my_role')
|
63
|
+
wait_for('my_other_role')
|
64
|
+
end
|
65
|
+
|
66
|
+
def self.after_suite
|
67
|
+
deprovision('my_role')
|
68
|
+
deprovision('my_other_role')
|
69
|
+
end
|
70
|
+
|
71
|
+
def test_searching_one
|
72
|
+
assert_search_count(:node, 'roles:my_role', 1)
|
73
|
+
end
|
74
|
+
|
75
|
+
def test_searching_two
|
76
|
+
assert_search_count(:node, 'roles:my_other_role', 1)
|
77
|
+
end
|
78
|
+
end
|
79
|
+
```
|
80
|
+
|
81
|
+
Then run your test suite with `bundle exec rake test:build`, which will perform
|
82
|
+
an upload of your repository to the test chef server, and run your tests.
|
83
|
+
|
84
|
+
We're quite aware this takes a while to run, and does very little. There are a
|
85
|
+
couple of ways to deal with this, but involve understanding the system a little
|
86
|
+
more; something we're about to cover.
|
87
|
+
|
88
|
+
## Dealing with Slow
|
89
|
+
|
90
|
+
Ok, so we got our test suite off the ground, and boy do those tests take a
|
91
|
+
while to run. I have two bits of good news for you:
|
92
|
+
|
93
|
+
You aren't locked into using Vagrant, and while Vagrant has limitations which
|
94
|
+
make it impossible to parallelize, machine provisions don't have to be serial.
|
95
|
+
EC2 support allows you to provision many machines in parallel, which can
|
96
|
+
improve run time. The provisioning system is also flexible enough to support
|
97
|
+
other systems too, so if you'd like to see your favorite system get some love,
|
98
|
+
the best way to do so is to file an issue or pull request.
|
99
|
+
|
100
|
+
The second is that in most scenarios, you have a few servers that you don't
|
101
|
+
really care about for this test, but need to be available for the machine to
|
102
|
+
function or converge properly, like a DNS or syslog server. All provisioning is
|
103
|
+
tracked and the state information is actually written to disk in your
|
104
|
+
`.chef-workflow` directory. What that means is that if you don't need to
|
105
|
+
rebuild your machines, you don't actually have to until you're ready to.
|
106
|
+
|
107
|
+
### State Management with MiniTest Subclasses
|
108
|
+
|
109
|
+
Let's start off easy. Take our example above, and remove the deprovisioning
|
110
|
+
lines in the `after_suite` call:
|
111
|
+
|
112
|
+
```ruby
|
113
|
+
require 'chef-workflow/helper'
|
114
|
+
class MyTest < MiniTest::Unit::VagrantTestCase
|
115
|
+
def self.before_suite
|
116
|
+
provision('my_role')
|
117
|
+
provision('my_other_role')
|
118
|
+
wait_for('my_role')
|
119
|
+
wait_for('my_other_role')
|
120
|
+
end
|
121
|
+
|
122
|
+
def self.after_suite
|
123
|
+
# omg, where did they go?
|
124
|
+
end
|
125
|
+
|
126
|
+
def test_searching_one
|
127
|
+
assert_search_count(:node, 'roles:my_role', 1)
|
128
|
+
end
|
129
|
+
|
130
|
+
def test_searching_two
|
131
|
+
assert_search_count(:node, 'roles:my_other_role', 1)
|
132
|
+
end
|
133
|
+
end
|
134
|
+
```
|
135
|
+
|
136
|
+
And run your tests again. Go ahead, run them twice. The second time should be
|
137
|
+
pretty quick, in the order of a second or so. This is because your machines
|
138
|
+
weren't provisioned the second time; the system determined they were already
|
139
|
+
built and didn't have to provision them again.
|
140
|
+
|
141
|
+
So how do we clean them up? `bundle exec rake chef:clean:machines`, or add our
|
142
|
+
deprovisioning lines back in and run the suite again. The state for a provision
|
143
|
+
lasts until the machine is deprovisioned, whether or not that's the same test
|
144
|
+
run. The rake task just reaps everything except for your chef server.
|
145
|
+
|
146
|
+
Now that we've seen the obvious performance increase, what is this good for?
|
147
|
+
|
148
|
+
### Pre-baking essential infra with MiniTest subclasses
|
149
|
+
|
150
|
+
MiniTest (and all SUnit alikes) leverage subclassing pretty heavily when done
|
151
|
+
right to manage a common set of test dependencies. For example, we have a
|
152
|
+
subclass called 'WithInfra' in our test suite that looks like this:
|
153
|
+
|
154
|
+
```ruby
|
155
|
+
class MiniTest::Unit::VagrantTestCase::WithInfra < MiniTest::Unit::VagrantTestCase
|
156
|
+
include SSHHelper
|
157
|
+
|
158
|
+
def self.before_suite
|
159
|
+
super
|
160
|
+
|
161
|
+
provision('bind_master')
|
162
|
+
provision('syslog_server')
|
163
|
+
|
164
|
+
wait_for('bind_master', 'syslog_server')
|
165
|
+
|
166
|
+
ssh_role_command('bind_master', 'chef-client')
|
167
|
+
ssh_role_command('syslog_server', 'chef-client')
|
168
|
+
end
|
169
|
+
end
|
170
|
+
```
|
171
|
+
|
172
|
+
`SSHHelper` is a small mixin that provides `ssh_role_command` and other bits of
|
173
|
+
ssh-related functionality. In this case, our syslog depends on working DNS and
|
174
|
+
our BIND depends on working syslog; while they have cookbooks that are smart
|
175
|
+
enough to walk past those dependencies in the first chef run, once they're both
|
176
|
+
up converging them again will resolve each other and configure them
|
177
|
+
appropriately.
|
178
|
+
|
179
|
+
The good news is that we don't have to (but can) tear these servers down during
|
180
|
+
a run or even between runs if they're not causing problems, and you can always
|
181
|
+
check from a fresh provision by running the `chef:clean:machines` task before
|
182
|
+
`test`, or simply `test:rebuild`, which does a full deprovision, repository
|
183
|
+
upload, and test run. This saves you time (and money, in the EC2 case) and
|
184
|
+
still gives you a way to do a "full" run without a lot of extra work.
|
185
|
+
|
186
|
+
An actual test suite that uses this looks like this:
|
187
|
+
|
188
|
+
```ruby
|
189
|
+
class TestBIND < MiniTest::Unit::VagrantTestCase::WithInfra
|
190
|
+
def self.before_suite
|
191
|
+
provision('bind_slave', 1, %w[bind_master syslog_server])
|
192
|
+
wait_for('bind_slave')
|
193
|
+
end
|
194
|
+
|
195
|
+
def test_something
|
196
|
+
# maybe a test that nsupdates the master and ensures it made it to slave
|
197
|
+
# could go here
|
198
|
+
end
|
199
|
+
|
200
|
+
def self.after_suite
|
201
|
+
deprovision('bind_slave')
|
202
|
+
end
|
203
|
+
end
|
204
|
+
```
|
205
|
+
|
206
|
+
This test when run will create, test, and destroy the bind slave, but leave the
|
207
|
+
master and the syslog server alone -- something that we will undoubtedly need
|
208
|
+
for the next test case.
|
209
|
+
|
210
|
+
### Dependency-based Provisioning
|
211
|
+
|
212
|
+
All provisions have a dependency list. Until they are satisfied, the stated
|
213
|
+
provision will not happen. This is mostly noticable in threaded provisioning
|
214
|
+
mode where multiple provisions can occur at once.
|
215
|
+
|
216
|
+
To wait for a specific machine to provision before continuing the test process,
|
217
|
+
use the `wait_for` method.
|
218
|
+
|
219
|
+
We can see this in our example above:
|
220
|
+
|
221
|
+
```ruby
|
222
|
+
def self.before_suite
|
223
|
+
provision('bind_slave', 1, %w[bind_master syslog_server])
|
224
|
+
wait_for('bind_slave')
|
225
|
+
end
|
226
|
+
```
|
227
|
+
|
228
|
+
In this instance, a `bind_slave` provision is scheduled, which depends on the
|
229
|
+
`bind_master` and `syslog_server` provisions. The `wait_for` statement is used
|
230
|
+
to control the test flow, so that it does not continue before `bind_slave`
|
231
|
+
provisions, which is what we want to actually test in this test case. Properly
|
232
|
+
used, this can enhance provisioning times by letting the scheduler provision
|
233
|
+
things as soon as they are capable of being provisioned, where you just care
|
234
|
+
about the servers you wish to test at any given point, instead of having to
|
235
|
+
manage this problem yourself.
|
236
|
+
|
237
|
+
You can't declare provisions that are dependent on provisions that don't exist
|
238
|
+
-- this will raise an exception. So, don't worry about fat-fingering it. :)
|
239
|
+
|
240
|
+
Here's a more advanced example that abuses the performance characteristics of the
|
241
|
+
scheduler. In this instance, we intend to test our monitoring system, which
|
242
|
+
will assert the "alive" status of many servers. We provision numerous ones in
|
243
|
+
the setup, and only wait for what we care about in each unit test.
|
244
|
+
|
245
|
+
```ruby
|
246
|
+
class TestNagios < MiniTest::Unit::VagrantTestCase
|
247
|
+
def self.before_suite
|
248
|
+
provision('syslog_server')
|
249
|
+
provision('bind_master', 1, %w[syslog_server])
|
250
|
+
# we just care about bind for all these
|
251
|
+
provision('bind_slave', 1, %w[bind_master])
|
252
|
+
provision('web_server', 1, %w[bind_master])
|
253
|
+
provision('db_server', 1, %w[bind_master])
|
254
|
+
provision('nagios_server', 1, %w[bind_master])
|
255
|
+
# we need the nagios server available for all tests, so wait for that
|
256
|
+
wait_for('nagios_server')
|
257
|
+
end
|
258
|
+
|
259
|
+
def test_dns_monitoring
|
260
|
+
wait_for('bind_master', 'bind_slave')
|
261
|
+
# ensure monitoring works
|
262
|
+
end
|
263
|
+
|
264
|
+
def test_web_monitoring
|
265
|
+
wait_for('web_server')
|
266
|
+
# ensure monitoring works
|
267
|
+
end
|
268
|
+
|
269
|
+
def test_db_monitoring
|
270
|
+
wait_for('db_server')
|
271
|
+
# ensure monitoring works
|
272
|
+
end
|
273
|
+
|
274
|
+
def self.after_suite
|
275
|
+
%w[
|
276
|
+
bind_master
|
277
|
+
bind_slave
|
278
|
+
syslog_server
|
279
|
+
web_server
|
280
|
+
db_server
|
281
|
+
nagios_server
|
282
|
+
].each { |x| deprovision(x) }
|
283
|
+
end
|
284
|
+
end
|
285
|
+
```
|
286
|
+
|
287
|
+
The flow of this test is as such:
|
288
|
+
|
289
|
+
As soon as `bind_master` completes, `bind_slave`, `web_server`, `db_server`,
|
290
|
+
and `nagios_server` will begin provisioning. `setup` will wait until
|
291
|
+
`nagios_server` completes, and normal testing will begin. Each test waits for
|
292
|
+
its testable unit to finish provisioning before running the actual tests
|
293
|
+
against those units. With rare exception, after a test or two, the `wait_for`
|
294
|
+
commands will succeed immediately, largely because they have been provsioning
|
295
|
+
in the background while the other tests have been running.
|
296
|
+
|
297
|
+
If you're curious how all this works under the hood, see the
|
298
|
+
[chef-workflow](https://github.com/chef-workflow/chef-workflow) documentation on
|
299
|
+
the subject.
|
300
|
+
|
301
|
+
## Contributing
|
302
|
+
|
303
|
+
1. Fork it
|
304
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
305
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
306
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
307
|
+
5. Create new Pull Request
|
308
|
+
6. Adjustment of the license terms or the author credits will result in a
|
309
|
+
rejected pull request. It ain't cool, yo.
|
310
|
+
|
311
|
+
## Authors
|
312
|
+
|
313
|
+
This work was partially sponsored by [HotelTonight](http://hoteltonight.com),
|
314
|
+
and you should check them out. They use this system for testing their
|
315
|
+
infrastructure. The programming was done primarily by [Erik
|
316
|
+
Hollensbe](https://github.com/erikh).
|
data/Rakefile
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'chef-workflow-testlib/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |gem|
|
7
|
+
gem.name = "chef-workflow-testlib"
|
8
|
+
gem.version = Chef::Workflow::Testlib::VERSION
|
9
|
+
gem.authors = ["Erik Hollensbe"]
|
10
|
+
gem.email = ["erik+github@hollensbe.org"]
|
11
|
+
gem.description = %q{Test helpers and assertions for chef-workflow}
|
12
|
+
gem.summary = %q{Test helpers and assertions for chef-workflow}
|
13
|
+
gem.homepage = "https://github.com/chef-workflow/chef-workflow-testlib"
|
14
|
+
|
15
|
+
gem.files = `git ls-files`.split($/)
|
16
|
+
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
17
|
+
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
18
|
+
gem.require_paths = ["lib"]
|
19
|
+
|
20
|
+
gem.add_dependency 'chef-workflow', '~> 0.1.0'
|
21
|
+
gem.add_dependency 'minitest', '~> 4.3.0'
|
22
|
+
gem.add_dependency 'net-ssh', '~> 2.6.0'
|
23
|
+
|
24
|
+
gem.add_development_dependency 'rdoc'
|
25
|
+
gem.add_development_dependency 'rake'
|
26
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
require 'chef-workflow'
|
2
|
+
require 'chef-workflow/helpers/minitest'
|
3
|
+
require 'chef-workflow/test-case/ec2'
|
4
|
+
require 'chef-workflow/test-case/vagrant'
|
5
|
+
require 'minitest/unit'
|
6
|
+
|
7
|
+
class MiniTest::Unit::TestCase
|
8
|
+
include MiniTest::Assertions::RemoteChef
|
9
|
+
end
|
10
|
+
|
11
|
+
require 'chef-workflow/runner/provisioned'
|
12
|
+
require 'minitest/autorun'
|
@@ -0,0 +1,15 @@
|
|
1
|
+
require 'chef/search/query'
|
2
|
+
require 'chef/config'
|
3
|
+
|
4
|
+
#
|
5
|
+
# Small helper library, intended to be mixed into others that provides short
|
6
|
+
# helpers for doing complicated things with the Chef API.
|
7
|
+
#
|
8
|
+
module ChefHelper
|
9
|
+
#
|
10
|
+
# Perform a search and return the names of the nodes that match the search.
|
11
|
+
#
|
12
|
+
def perform_search(type, query)
|
13
|
+
Chef::Search::Query.new.search(type, query).first.map(&:name)
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
require 'chef-workflow/helpers/chef'
|
2
|
+
require 'minitest/unit'
|
3
|
+
|
4
|
+
#
|
5
|
+
# Small assertion library for minitest to assist with remote chef tests.
|
6
|
+
#
|
7
|
+
module MiniTest::Assertions::RemoteChef
|
8
|
+
include ChefHelper
|
9
|
+
|
10
|
+
#
|
11
|
+
# Assert that a search included the node names.
|
12
|
+
#
|
13
|
+
def assert_search(type, query, node_names)
|
14
|
+
assert_equal(node_names.sort, perform_search(type, query).sort)
|
15
|
+
end
|
16
|
+
|
17
|
+
#
|
18
|
+
# Refute that a search included the node names.
|
19
|
+
#
|
20
|
+
def refute_search(type, query, node_names)
|
21
|
+
refute_equal(node_names.sort, perform_search(type, query).sort)
|
22
|
+
end
|
23
|
+
|
24
|
+
#
|
25
|
+
# Assert the search included `count` elements. Does not verify what that
|
26
|
+
# count is of.
|
27
|
+
#
|
28
|
+
def assert_search_count(type, query, count)
|
29
|
+
assert_equal(count, perform_search(type, query).count)
|
30
|
+
end
|
31
|
+
|
32
|
+
#
|
33
|
+
# Refute the search included `count` elements. Does not verify what that
|
34
|
+
# count is of.
|
35
|
+
#
|
36
|
+
def refute_search_count(type, query, count)
|
37
|
+
refute_equal(count, perform_search(type, query).count)
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
require 'chef-workflow/support/scheduler'
|
2
|
+
|
3
|
+
$SCHEDULER ||= Scheduler.new
|
4
|
+
|
5
|
+
#
|
6
|
+
# Helper for provisioning. Intended to be instanced and assigned to the
|
7
|
+
# provision_helper attribute of a ProvisionedTestCase.
|
8
|
+
#
|
9
|
+
# All methods except `provision`, which is shorthand, are passed directly to
|
10
|
+
# the scheduler.
|
11
|
+
#
|
12
|
+
class ProvisionHelper
|
13
|
+
def schedule_provision(*args)
|
14
|
+
$SCHEDULER.schedule_provision(*args)
|
15
|
+
end
|
16
|
+
|
17
|
+
def deprovision(group_name)
|
18
|
+
$SCHEDULER.deprovision_group(group_name)
|
19
|
+
end
|
20
|
+
|
21
|
+
def wait_for(*args)
|
22
|
+
$SCHEDULER.wait_for(*args)
|
23
|
+
end
|
24
|
+
|
25
|
+
def serial=(arg)
|
26
|
+
$SCHEDULER.serial = arg
|
27
|
+
end
|
28
|
+
|
29
|
+
def run
|
30
|
+
$SCHEDULER.run
|
31
|
+
end
|
32
|
+
|
33
|
+
def provision(group_name, number_of_servers, dependencies)
|
34
|
+
raise "Please override this method"
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,99 @@
|
|
1
|
+
require 'chef-workflow/support/ip'
|
2
|
+
require 'chef-workflow/support/knife'
|
3
|
+
require 'chef-workflow/support/debug'
|
4
|
+
require 'net/ssh'
|
5
|
+
|
6
|
+
#
|
7
|
+
# Helper for performing SSH on groups of servers. Intended to be mixed into
|
8
|
+
# test case classes.
|
9
|
+
#
|
10
|
+
module SSHHelper
|
11
|
+
include KnifePluginSupport
|
12
|
+
include DebugSupport
|
13
|
+
|
14
|
+
#
|
15
|
+
# run a command against a group of servers. These commands are run in
|
16
|
+
# parallel, but the command itself does not complete until all the threads
|
17
|
+
# have finished running.
|
18
|
+
#
|
19
|
+
def ssh_role_command(role, command)
|
20
|
+
t = []
|
21
|
+
IPSupport.singleton.get_role_ips(role).each do |ip|
|
22
|
+
t.push(
|
23
|
+
Thread.new do
|
24
|
+
ssh_command(ip, command)
|
25
|
+
end
|
26
|
+
)
|
27
|
+
end
|
28
|
+
t.each(&:join)
|
29
|
+
end
|
30
|
+
|
31
|
+
#
|
32
|
+
# takes a block which it uses inside of the open_channel block that Net::SSH
|
33
|
+
# uses. Intended to provide a consistent way of setting up Net::SSH Makes
|
34
|
+
# heavy use of KnifeSupport to determine how to drive the command.
|
35
|
+
#
|
36
|
+
def configure_ssh_command(ip, command)
|
37
|
+
command = "#{KnifeSupport.singleton.use_sudo ? 'sudo ': ''}#{command}"
|
38
|
+
|
39
|
+
options = { }
|
40
|
+
|
41
|
+
options[:password] = KnifeSupport.singleton.ssh_password if KnifeSupport.singleton.ssh_password
|
42
|
+
options[:keys] = [KnifeSupport.singleton.ssh_identity_file] if KnifeSupport.singleton.ssh_identity_file
|
43
|
+
|
44
|
+
Net::SSH.start(ip, KnifeSupport.singleton.ssh_user, options) do |ssh|
|
45
|
+
ssh.open_channel do |ch|
|
46
|
+
ch.on_open_failed do |ch, code, desc|
|
47
|
+
raise "Connection Error to #{ip}: #{desc}"
|
48
|
+
end
|
49
|
+
|
50
|
+
ch.exec(command) do |ch, success|
|
51
|
+
yield ch, success
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
ssh.loop
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
#
|
60
|
+
# Run a command against a single IP. Returns the exit status.
|
61
|
+
#
|
62
|
+
#
|
63
|
+
def ssh_command(ip, command)
|
64
|
+
configure_ssh_command(ip, command) do |ch, success|
|
65
|
+
return 1 unless success
|
66
|
+
|
67
|
+
if_debug(2) do
|
68
|
+
ch.on_data do |ch, data|
|
69
|
+
$stderr.puts data
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
ch.on_request("exit-status") do |ch, data|
|
74
|
+
return data.read_long
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
#
|
80
|
+
# run a command, and instead of capturing the exit status, return the data
|
81
|
+
# captured during the command run.
|
82
|
+
#
|
83
|
+
def ssh_capture(ip, command)
|
84
|
+
retval = ""
|
85
|
+
configure_ssh_command(ip, command) do |ch, success|
|
86
|
+
return "" unless success
|
87
|
+
|
88
|
+
ch.on_data do |ch, data|
|
89
|
+
retval << data
|
90
|
+
end
|
91
|
+
|
92
|
+
ch.on_request("exit-status") do |ch, data|
|
93
|
+
return retval
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
return retval
|
98
|
+
end
|
99
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
require 'minitest/unit'
|
2
|
+
|
3
|
+
class MiniTest::Unit::TestCase
|
4
|
+
def self.before_suite; end
|
5
|
+
def self.after_suite; end
|
6
|
+
v = $VERBOSE
|
7
|
+
$VERBOSE = nil
|
8
|
+
const_set(:SUPPORTS_INFO_SIGNAL, nil)
|
9
|
+
$VERBOSE = v
|
10
|
+
end
|
11
|
+
|
12
|
+
class MiniTest::Unit::ProvisionedRunner < MiniTest::Unit
|
13
|
+
def _run_suite(suite, type)
|
14
|
+
begin
|
15
|
+
suite.before_suite unless suite.test_methods.empty?
|
16
|
+
super(suite, type)
|
17
|
+
ensure
|
18
|
+
suite.after_suite unless suite.test_methods.empty?
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
MiniTest::Unit.runner = MiniTest::Unit::ProvisionedRunner.new
|
@@ -0,0 +1,41 @@
|
|
1
|
+
require 'chef-workflow/support/vm/ec2'
|
2
|
+
require 'chef-workflow/support/vm/knife'
|
3
|
+
require 'tempfile'
|
4
|
+
require 'chef-workflow/test-case/provisioned'
|
5
|
+
require 'chef-workflow/helpers/provision'
|
6
|
+
require 'chef-workflow/helpers/ssh'
|
7
|
+
|
8
|
+
#
|
9
|
+
# Subclass of ProvisionHelper, centered around EC2. Pulls some
|
10
|
+
# configuration from KnifeSupport and then drives VM::EC2Provisioner and
|
11
|
+
# VM::KnifeProvisioner.
|
12
|
+
#
|
13
|
+
class EC2ProvisionHelper < ProvisionHelper
|
14
|
+
def provision(group_name, number_of_servers=1, dependencies=[])
|
15
|
+
kp = VM::KnifeProvisioner.new
|
16
|
+
kp.username = KnifeSupport.singleton.ssh_user
|
17
|
+
kp.password = KnifeSupport.singleton.ssh_password
|
18
|
+
kp.use_sudo = KnifeSupport.singleton.use_sudo
|
19
|
+
kp.ssh_key = KnifeSupport.singleton.ssh_identity_file
|
20
|
+
kp.environment = KnifeSupport.singleton.test_environment
|
21
|
+
|
22
|
+
schedule_provision(
|
23
|
+
group_name,
|
24
|
+
[
|
25
|
+
VM::EC2Provisioner.new(group_name, number_of_servers),
|
26
|
+
kp
|
27
|
+
],
|
28
|
+
dependencies
|
29
|
+
)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
#
|
34
|
+
# ProvisionedTestCase that uses EC2ProvisionHelper
|
35
|
+
#
|
36
|
+
class MiniTest::Unit::EC2TestCase < MiniTest::Unit::ProvisionedTestCase
|
37
|
+
include SSHHelper
|
38
|
+
extend SSHHelper
|
39
|
+
|
40
|
+
self.provision_helper = EC2ProvisionHelper.new
|
41
|
+
end
|
@@ -0,0 +1,88 @@
|
|
1
|
+
require 'chef-workflow/support/knife'
|
2
|
+
require 'chef-workflow/runner/provisioned'
|
3
|
+
|
4
|
+
#
|
5
|
+
# Basic provisioned test case. Generally not intended for direct use but
|
6
|
+
# provides the scaffolding for subclasses.
|
7
|
+
#
|
8
|
+
# Set the class attribute `provision_helper` to configure your provision
|
9
|
+
# helper, which will be used for many methods this class provides.
|
10
|
+
#
|
11
|
+
class MiniTest::Unit::ProvisionedTestCase < MiniTest::Unit::TestCase
|
12
|
+
module ProvisionHelper
|
13
|
+
def inherited(klass)
|
14
|
+
unless klass.provision_helper
|
15
|
+
klass.provision_helper = self.provision_helper
|
16
|
+
end
|
17
|
+
|
18
|
+
MiniTest::Unit::TestCase.inherited(klass)
|
19
|
+
end
|
20
|
+
|
21
|
+
#
|
22
|
+
# Retrieves the provision helper.
|
23
|
+
#
|
24
|
+
def provision_helper
|
25
|
+
@provision_helper || (self.class.provision_helper rescue nil)
|
26
|
+
end
|
27
|
+
|
28
|
+
#
|
29
|
+
# Sets the provision helper.
|
30
|
+
#
|
31
|
+
def provision_helper=(arg)
|
32
|
+
@provision_helper = arg
|
33
|
+
end
|
34
|
+
|
35
|
+
#
|
36
|
+
# wait for a provision. takes a list of server group names. delegates to the
|
37
|
+
# provision helper.
|
38
|
+
#
|
39
|
+
def wait_for(*deps)
|
40
|
+
provision_helper.wait_for(*deps)
|
41
|
+
end
|
42
|
+
|
43
|
+
#
|
44
|
+
# Provision a server group. Takes a name, number of servers, and a list of
|
45
|
+
# dependencies (server group names). Delegates to the provision helper.
|
46
|
+
#
|
47
|
+
def provision(role, number_of_servers=1, addl_dependencies=[])
|
48
|
+
provision_helper.provision(role, number_of_servers, addl_dependencies)
|
49
|
+
provision_helper.run
|
50
|
+
end
|
51
|
+
|
52
|
+
#
|
53
|
+
# De-Provision a server group. Takes a name. Delegates to the provision
|
54
|
+
# helper.
|
55
|
+
#
|
56
|
+
def deprovision(role)
|
57
|
+
provision_helper.deprovision(role)
|
58
|
+
end
|
59
|
+
|
60
|
+
#
|
61
|
+
# Obtains the IP addresses for a given role as an array.
|
62
|
+
#
|
63
|
+
def get_role_ips(role)
|
64
|
+
IPSupport.singleton.get_role_ips(role)
|
65
|
+
end
|
66
|
+
|
67
|
+
#
|
68
|
+
# Easy way to reference KnifeSupport for getting configuration data.
|
69
|
+
#
|
70
|
+
def knife_config
|
71
|
+
KnifeSupport.singleton
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
include ProvisionHelper
|
76
|
+
extend ProvisionHelper
|
77
|
+
|
78
|
+
#
|
79
|
+
# Hook before the suite starts. Be sure in your subclasses to call this with
|
80
|
+
# `super`. Provisions machines configured as dependencies and starts the
|
81
|
+
# scheduler.
|
82
|
+
#
|
83
|
+
def self.before_suite
|
84
|
+
super
|
85
|
+
|
86
|
+
Chef::Config.from_file(KnifeSupport.singleton.knife_config_path)
|
87
|
+
end
|
88
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
require 'chef-workflow/support/vagrant'
|
2
|
+
require 'chef-workflow/support/vm/vagrant'
|
3
|
+
require 'chef-workflow/support/vm/knife'
|
4
|
+
require 'tempfile'
|
5
|
+
require 'chef-workflow/test-case/provisioned'
|
6
|
+
require 'chef-workflow/helpers/provision'
|
7
|
+
require 'chef-workflow/helpers/ssh'
|
8
|
+
|
9
|
+
#
|
10
|
+
# Subclass of ProvisionHelper, centered around Vagrant. Pulls some
|
11
|
+
# configuration from KnifeSupport and then drives VM::VagrantProvisioner and
|
12
|
+
# VM::KnifeProvisioner.
|
13
|
+
#
|
14
|
+
class VagrantProvisionHelper < ProvisionHelper
|
15
|
+
def provision(group_name, number_of_servers=1, dependencies=[])
|
16
|
+
self.serial = true
|
17
|
+
|
18
|
+
kp = VM::KnifeProvisioner.new
|
19
|
+
kp.username = KnifeSupport.singleton.ssh_user
|
20
|
+
kp.password = KnifeSupport.singleton.ssh_password
|
21
|
+
kp.use_sudo = KnifeSupport.singleton.use_sudo
|
22
|
+
kp.ssh_key = KnifeSupport.singleton.ssh_identity_file
|
23
|
+
kp.environment = KnifeSupport.singleton.test_environment
|
24
|
+
|
25
|
+
schedule_provision(
|
26
|
+
group_name,
|
27
|
+
[
|
28
|
+
VM::VagrantProvisioner.new(group_name, number_of_servers),
|
29
|
+
kp
|
30
|
+
],
|
31
|
+
dependencies
|
32
|
+
)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
#
|
37
|
+
# ProvisionedTestCase that uses VagrantProvisionHelper.
|
38
|
+
#
|
39
|
+
class MiniTest::Unit::VagrantTestCase < MiniTest::Unit::ProvisionedTestCase
|
40
|
+
include SSHHelper
|
41
|
+
extend SSHHelper
|
42
|
+
|
43
|
+
self.provision_helper = VagrantProvisionHelper.new
|
44
|
+
end
|
@@ -0,0 +1 @@
|
|
1
|
+
require "chef-workflow-testlib/version"
|
metadata
ADDED
@@ -0,0 +1,143 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: chef-workflow-testlib
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Erik Hollensbe
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2012-12-21 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: chef-workflow
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ~>
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: 0.1.0
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ~>
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: 0.1.0
|
30
|
+
- !ruby/object:Gem::Dependency
|
31
|
+
name: minitest
|
32
|
+
requirement: !ruby/object:Gem::Requirement
|
33
|
+
none: false
|
34
|
+
requirements:
|
35
|
+
- - ~>
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: 4.3.0
|
38
|
+
type: :runtime
|
39
|
+
prerelease: false
|
40
|
+
version_requirements: !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ~>
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: 4.3.0
|
46
|
+
- !ruby/object:Gem::Dependency
|
47
|
+
name: net-ssh
|
48
|
+
requirement: !ruby/object:Gem::Requirement
|
49
|
+
none: false
|
50
|
+
requirements:
|
51
|
+
- - ~>
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: 2.6.0
|
54
|
+
type: :runtime
|
55
|
+
prerelease: false
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
57
|
+
none: false
|
58
|
+
requirements:
|
59
|
+
- - ~>
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: 2.6.0
|
62
|
+
- !ruby/object:Gem::Dependency
|
63
|
+
name: rdoc
|
64
|
+
requirement: !ruby/object:Gem::Requirement
|
65
|
+
none: false
|
66
|
+
requirements:
|
67
|
+
- - ! '>='
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
version: '0'
|
70
|
+
type: :development
|
71
|
+
prerelease: false
|
72
|
+
version_requirements: !ruby/object:Gem::Requirement
|
73
|
+
none: false
|
74
|
+
requirements:
|
75
|
+
- - ! '>='
|
76
|
+
- !ruby/object:Gem::Version
|
77
|
+
version: '0'
|
78
|
+
- !ruby/object:Gem::Dependency
|
79
|
+
name: rake
|
80
|
+
requirement: !ruby/object:Gem::Requirement
|
81
|
+
none: false
|
82
|
+
requirements:
|
83
|
+
- - ! '>='
|
84
|
+
- !ruby/object:Gem::Version
|
85
|
+
version: '0'
|
86
|
+
type: :development
|
87
|
+
prerelease: false
|
88
|
+
version_requirements: !ruby/object:Gem::Requirement
|
89
|
+
none: false
|
90
|
+
requirements:
|
91
|
+
- - ! '>='
|
92
|
+
- !ruby/object:Gem::Version
|
93
|
+
version: '0'
|
94
|
+
description: Test helpers and assertions for chef-workflow
|
95
|
+
email:
|
96
|
+
- erik+github@hollensbe.org
|
97
|
+
executables: []
|
98
|
+
extensions: []
|
99
|
+
extra_rdoc_files: []
|
100
|
+
files:
|
101
|
+
- .gitignore
|
102
|
+
- Gemfile
|
103
|
+
- LICENSE.txt
|
104
|
+
- README.md
|
105
|
+
- Rakefile
|
106
|
+
- chef-workflow-testlib.gemspec
|
107
|
+
- lib/chef-workflow-testlib.rb
|
108
|
+
- lib/chef-workflow-testlib/version.rb
|
109
|
+
- lib/chef-workflow/helper.rb
|
110
|
+
- lib/chef-workflow/helpers/chef.rb
|
111
|
+
- lib/chef-workflow/helpers/minitest.rb
|
112
|
+
- lib/chef-workflow/helpers/provision.rb
|
113
|
+
- lib/chef-workflow/helpers/ssh.rb
|
114
|
+
- lib/chef-workflow/runner/provisioned.rb
|
115
|
+
- lib/chef-workflow/test-case/ec2.rb
|
116
|
+
- lib/chef-workflow/test-case/provisioned.rb
|
117
|
+
- lib/chef-workflow/test-case/vagrant.rb
|
118
|
+
homepage: https://github.com/chef-workflow/chef-workflow-testlib
|
119
|
+
licenses: []
|
120
|
+
post_install_message:
|
121
|
+
rdoc_options: []
|
122
|
+
require_paths:
|
123
|
+
- lib
|
124
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
125
|
+
none: false
|
126
|
+
requirements:
|
127
|
+
- - ! '>='
|
128
|
+
- !ruby/object:Gem::Version
|
129
|
+
version: '0'
|
130
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
131
|
+
none: false
|
132
|
+
requirements:
|
133
|
+
- - ! '>='
|
134
|
+
- !ruby/object:Gem::Version
|
135
|
+
version: '0'
|
136
|
+
requirements: []
|
137
|
+
rubyforge_project:
|
138
|
+
rubygems_version: 1.8.24
|
139
|
+
signing_key:
|
140
|
+
specification_version: 3
|
141
|
+
summary: Test helpers and assertions for chef-workflow
|
142
|
+
test_files: []
|
143
|
+
has_rdoc:
|