slushy 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 +4 -0
- data/.rvmrc +1 -0
- data/CHANGELOG.md +2 -0
- data/Gemfile +3 -0
- data/README.md +27 -0
- data/Rakefile +7 -0
- data/lib/slushy/instance.rb +132 -0
- data/lib/slushy/version.rb +3 -0
- data/lib/slushy.rb +4 -0
- data/slushy.gemspec +23 -0
- data/spec/lib/instance_spec.rb +152 -0
- data/spec/spec_helper.rb +17 -0
- data/spec/support/output_streams.rb +12 -0
- metadata +134 -0
data/.gitignore
ADDED
data/.rvmrc
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
rvm use ruby-1.9.2@slushy --create
|
data/CHANGELOG.md
ADDED
data/Gemfile
ADDED
data/README.md
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
## Description
|
2
|
+
|
3
|
+
Giving Chef a hand in the provisional kitchen, [aussie style](http://www.mrl.nott.ac.uk/~mbf/paula/slushy.htm).
|
4
|
+
Assumes Fog's API for connecting to and creating instances.
|
5
|
+
|
6
|
+
## Usage
|
7
|
+
|
8
|
+
Provision and converge an instance:
|
9
|
+
|
10
|
+
```ruby
|
11
|
+
connection = Fog::Compute.new :provider => 'AWS', :aws_access_key => 'KEY',
|
12
|
+
:aws_secret_access_key => 'SECRET'
|
13
|
+
# Second arg is a hash passed to Fog::Compute::AWS::Servers.create
|
14
|
+
instance = Slushy::Instance.launch connection, :flavor_id => 'm1.large', :more => :keys
|
15
|
+
instance.bootstrap
|
16
|
+
# Point at directory containing Chef cookbooks
|
17
|
+
instance.converge Rails.root.join('provision')
|
18
|
+
```
|
19
|
+
|
20
|
+
## TODO
|
21
|
+
|
22
|
+
* Speed up slow Instance.launch tests caused by Fog's mocking
|
23
|
+
* Add SystemTimer for a working 1.8.7 timeout
|
24
|
+
* Support providers other than AWS
|
25
|
+
* Support OSes other ubuntu
|
26
|
+
* Don't hardcode path to chef, caused by ubuntu installing weirdness
|
27
|
+
* Fix Instance#wait_for_connectivity occasionally hanging
|
data/Rakefile
ADDED
@@ -0,0 +1,132 @@
|
|
1
|
+
require 'timeout'
|
2
|
+
|
3
|
+
class Slushy::Instance
|
4
|
+
class AptInstallError < StandardError; end
|
5
|
+
|
6
|
+
attr_reader :connection, :instance_id
|
7
|
+
|
8
|
+
def self.launch(connection, config)
|
9
|
+
server = connection.servers.create(config)
|
10
|
+
server.wait_for { ready? }
|
11
|
+
new(connection, server.id)
|
12
|
+
end
|
13
|
+
|
14
|
+
def initialize(connection, instance_id)
|
15
|
+
@connection = connection
|
16
|
+
@instance_id = instance_id
|
17
|
+
end
|
18
|
+
|
19
|
+
def server
|
20
|
+
@server ||= @connection.servers.get(instance_id)
|
21
|
+
end
|
22
|
+
|
23
|
+
def ssh(*args)
|
24
|
+
server.ssh(*args)
|
25
|
+
end
|
26
|
+
|
27
|
+
def scp(*args)
|
28
|
+
server.scp(*args)
|
29
|
+
end
|
30
|
+
|
31
|
+
def dns_name(*args)
|
32
|
+
server.dns_name(*args)
|
33
|
+
end
|
34
|
+
|
35
|
+
def snapshot(name, description)
|
36
|
+
response = connection.create_image(instance_id, name, description)
|
37
|
+
image_id = response.body["imageId"]
|
38
|
+
image = connection.images.get(image_id)
|
39
|
+
image.wait_for { state == "available" }
|
40
|
+
image_id
|
41
|
+
end
|
42
|
+
|
43
|
+
def terminate
|
44
|
+
server.destroy
|
45
|
+
server.wait_for { state == "terminated" }
|
46
|
+
end
|
47
|
+
|
48
|
+
def stop
|
49
|
+
server.stop
|
50
|
+
server.wait_for { state == "stopped" }
|
51
|
+
end
|
52
|
+
|
53
|
+
def wait_for_connectivity
|
54
|
+
puts "Waiting for ssh connectivity..."
|
55
|
+
retry_block(5, [Errno::ECONNREFUSED, Timeout::Error], "Connecting to Amazon refused") do
|
56
|
+
sleep 10
|
57
|
+
Timeout.timeout(60) { ssh('ls') }
|
58
|
+
end
|
59
|
+
puts "Server up and listening for SSH!"
|
60
|
+
end
|
61
|
+
|
62
|
+
def run_command(command)
|
63
|
+
jobs = ssh(command)
|
64
|
+
jobs_succeeded?(jobs)
|
65
|
+
end
|
66
|
+
|
67
|
+
def run_command!(command)
|
68
|
+
exit 1 unless run_command(command)
|
69
|
+
end
|
70
|
+
|
71
|
+
def run_apt_command!(command)
|
72
|
+
raise AptInstallError unless run_command(command)
|
73
|
+
end
|
74
|
+
|
75
|
+
def apt_installs
|
76
|
+
retry_block(5, [AptInstallError], "Command 'apt-get' failed") do
|
77
|
+
puts "Updating apt cache..."
|
78
|
+
run_apt_command!('sudo apt-get update')
|
79
|
+
puts "Installing ruby..."
|
80
|
+
run_apt_command!('sudo apt-get -y install ruby')
|
81
|
+
puts "Installing rubygems..."
|
82
|
+
run_apt_command!('sudo apt-get -y install rubygems1.8')
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
def bootstrap
|
87
|
+
wait_for_connectivity
|
88
|
+
apt_installs
|
89
|
+
puts "Installing chef..."
|
90
|
+
run_command!('sudo gem install chef --no-ri --no-rdoc --version 0.10.8')
|
91
|
+
end
|
92
|
+
|
93
|
+
def converge(cookbooks_path) # TODO: find the standard Chef term for this
|
94
|
+
puts "Copying chef resources from provision directory..."
|
95
|
+
cookbooks_path = "#{cookbooks_path}/" unless cookbooks_path.end_with?('/')
|
96
|
+
scp(cookbooks_path, '/tmp/chef-solo', :recursive => true)
|
97
|
+
puts "Converging server, this may take a while (10-20 minutes)"
|
98
|
+
run_command!('cd /tmp/chef-solo && sudo /var/lib/gems/1.8/bin/chef-solo -c solo.rb -j dna.json')
|
99
|
+
end
|
100
|
+
|
101
|
+
protected
|
102
|
+
|
103
|
+
def retry_block(times, errors, failure)
|
104
|
+
succeeded = false
|
105
|
+
attempts = 0
|
106
|
+
last_error = nil
|
107
|
+
until succeeded || attempts > times-1
|
108
|
+
begin
|
109
|
+
retval = yield
|
110
|
+
succeeded = true
|
111
|
+
rescue *errors => e
|
112
|
+
attempts +=1
|
113
|
+
puts "#{failure}. Attempting retry #{attempts}..."
|
114
|
+
last_error = e
|
115
|
+
end
|
116
|
+
end
|
117
|
+
exit 1 if !succeeded
|
118
|
+
retval
|
119
|
+
end
|
120
|
+
|
121
|
+
def jobs_succeeded?(jobs)
|
122
|
+
return true if jobs.all? { |job| job.status == 0 }
|
123
|
+
jobs.each do |job|
|
124
|
+
puts "----------------------"
|
125
|
+
puts "Command '#{job.command}'"
|
126
|
+
puts "STDOUT: #{job.stdout}"
|
127
|
+
puts "STDERR: #{job.stderr}"
|
128
|
+
puts "----------------------"
|
129
|
+
end
|
130
|
+
false
|
131
|
+
end
|
132
|
+
end
|
data/lib/slushy.rb
ADDED
data/slushy.gemspec
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
3
|
+
require "slushy/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = "slushy"
|
7
|
+
s.version = Slushy::VERSION
|
8
|
+
s.homepage = "http://github.com/relevance/slushy"
|
9
|
+
s.authors = ["Sam Umbach", "Gabriel Horner", "Alex Redington"]
|
10
|
+
s.email = ["sam@thinkrelevance.com"]
|
11
|
+
s.homepage = "http://github.com/relevance/slushy"
|
12
|
+
s.summary = %q{An Aussie kitchen hand helping out Chef}
|
13
|
+
s.description = "Giving Chef a hand in the provisional kitchen - Aussie style. Using Fog's API, creates an instance and converges chef recipes on it."
|
14
|
+
|
15
|
+
s.files = `git ls-files`.split("\n")
|
16
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
17
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
18
|
+
|
19
|
+
s.add_development_dependency 'fog', '1.3.1'
|
20
|
+
s.add_development_dependency 'rspec'
|
21
|
+
s.add_development_dependency 'rake', '~> 0.9.2.2'
|
22
|
+
s.add_development_dependency 'bundler', '~> 1.1'
|
23
|
+
end
|
@@ -0,0 +1,152 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'fog'
|
3
|
+
|
4
|
+
describe Slushy::Instance do
|
5
|
+
def mock_job(options={})
|
6
|
+
mock({:command => 'foo', :stdout => '', :stderr => '', :status => 0}.merge(options))
|
7
|
+
end
|
8
|
+
|
9
|
+
before(:all) { Fog.mock! }
|
10
|
+
|
11
|
+
let(:connection) { Fog::Compute.new(:provider => 'AWS', :aws_access_key_id => "foo", :aws_secret_access_key => "bar") }
|
12
|
+
let(:config) { {:flavor_id => 'm1.large', :image_id => 'ami-123456', :groups => ['default']} }
|
13
|
+
let(:server) { connection.servers.create(config) }
|
14
|
+
let(:instance_id) { server.id }
|
15
|
+
let(:instance) { Slushy::Instance.new(connection, instance_id) }
|
16
|
+
|
17
|
+
describe ".launch" do
|
18
|
+
it "launches a new instance" do
|
19
|
+
lambda { described_class.launch(connection, config) }.should change { connection.servers.size }.by(1)
|
20
|
+
end
|
21
|
+
|
22
|
+
it "returns the instance object" do
|
23
|
+
described_class.launch(connection, config).should be_a Slushy::Instance
|
24
|
+
end
|
25
|
+
|
26
|
+
describe "the new instance" do
|
27
|
+
subject { described_class.launch(connection, config).server }
|
28
|
+
|
29
|
+
its(:flavor_id) { should == 'm1.large' }
|
30
|
+
its(:image_id) { should == 'ami-123456' }
|
31
|
+
its(:groups) { should include('default') }
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
describe "#server" do
|
36
|
+
it "retrieves the server from the connection based on instance_id" do
|
37
|
+
servers = stub(:create => server)
|
38
|
+
connection.stub(:servers).and_return(servers)
|
39
|
+
servers.should_receive(:get).with(server.id).and_return(server)
|
40
|
+
instance.server.should == server
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
describe "#snapshot" do
|
45
|
+
let!(:image) { connection.images.new(Fog::AWS::Mock.image) }
|
46
|
+
let(:images) { stub(:get => image) }
|
47
|
+
let(:response) { stub(:body => {"imageId" => :some_ami_id}) }
|
48
|
+
|
49
|
+
before { connection.stub(:images).and_return(images) }
|
50
|
+
|
51
|
+
it "creates a new AMI from the given instance and returns the AMI id string" do
|
52
|
+
connection.should_receive(:create_image).with(instance_id, :some_name, :some_description).and_return(response)
|
53
|
+
instance.snapshot(:some_name, :some_description).should == :some_ami_id
|
54
|
+
end
|
55
|
+
|
56
|
+
it "does NOT return until image creation is complete" do
|
57
|
+
connection.stub(:create_image).and_return(response)
|
58
|
+
images.should_receive(:get).with(:some_ami_id).and_return(image)
|
59
|
+
image.should_receive(:wait_for)
|
60
|
+
instance.snapshot(:some_name, :some_description)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
describe "#terminate" do
|
65
|
+
it "terminates the given instance" do
|
66
|
+
instance.stub(:server).and_return(server)
|
67
|
+
server.should_receive(:destroy)
|
68
|
+
server.should_receive(:wait_for)
|
69
|
+
instance.terminate
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
describe '#wait_for_connectivity' do
|
74
|
+
it 'retries if the first attempt fails' do
|
75
|
+
instance.should_receive(:ssh).ordered.and_raise(Errno::ECONNREFUSED)
|
76
|
+
instance.should_receive(:ssh).ordered.and_return([mock_job])
|
77
|
+
instance.should_receive(:sleep).twice.with(10).and_return(10)
|
78
|
+
expect do
|
79
|
+
capture_stdout { instance.wait_for_connectivity }
|
80
|
+
end.to_not raise_error
|
81
|
+
end
|
82
|
+
|
83
|
+
it 'prints a message for each retry attempt' do
|
84
|
+
instance.should_receive(:ssh).ordered.exactly(3).times.and_raise(Errno::ECONNREFUSED)
|
85
|
+
instance.should_receive(:ssh).ordered.and_return([mock_job])
|
86
|
+
instance.stub(:sleep).and_return(10)
|
87
|
+
stdout = capture_stdout { instance.wait_for_connectivity }
|
88
|
+
stdout.should include 'Attempting retry 1...'
|
89
|
+
stdout.should include 'Attempting retry 2...'
|
90
|
+
stdout.should include 'Attempting retry 3...'
|
91
|
+
end
|
92
|
+
|
93
|
+
it 'retries up to five times, then aborts' do
|
94
|
+
instance.should_receive(:ssh).exactly(5).times.and_raise(Errno::ECONNREFUSED)
|
95
|
+
instance.stub(:sleep).and_return(10)
|
96
|
+
expect do
|
97
|
+
capture_stdout { instance.wait_for_connectivity }
|
98
|
+
end.to raise_error SystemExit
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
describe '#run_command!' do
|
103
|
+
it "fails fast if a command fails" do
|
104
|
+
job = mock_job(:status => 1, :stderr => 'FAIL WHALE')
|
105
|
+
instance.stub(:ssh).with("ls").and_return([job])
|
106
|
+
capture_stdout do
|
107
|
+
expect do
|
108
|
+
instance.run_command!("ls")
|
109
|
+
end.to raise_error SystemExit
|
110
|
+
end.should =~ /STDERR: FAIL WHALE/
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
describe '#apt_installs' do
|
115
|
+
it 'retries if the first attempt fails' do
|
116
|
+
instance.should_receive(:ssh).ordered.with('sudo apt-get update').and_return([mock_job])
|
117
|
+
instance.should_receive(:ssh).ordered.with('sudo apt-get -y install ruby').and_return([mock_job(:status => 1)])
|
118
|
+
instance.should_receive(:ssh).ordered.with('sudo apt-get update').and_return([mock_job])
|
119
|
+
instance.should_receive(:ssh).ordered.with('sudo apt-get -y install ruby').and_return([mock_job])
|
120
|
+
instance.should_receive(:ssh).ordered.with('sudo apt-get -y install rubygems1.8').and_return([mock_job])
|
121
|
+
expect do
|
122
|
+
capture_stdout { instance.apt_installs }
|
123
|
+
end.to_not raise_error
|
124
|
+
end
|
125
|
+
|
126
|
+
it 'retries up to five times, then aborts' do
|
127
|
+
instance.should_receive(:ssh).exactly(5).times.with('sudo apt-get update').and_return([mock_job(:status => 1)])
|
128
|
+
expect do
|
129
|
+
capture_stdout { instance.apt_installs }
|
130
|
+
end.to raise_error SystemExit
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
describe "#bootstrap" do
|
135
|
+
it "installs prerequisites on the given instance" do
|
136
|
+
instance.should_receive(:wait_for_connectivity).ordered
|
137
|
+
instance.should_receive(:run_command).ordered.with("sudo apt-get update").and_return(true)
|
138
|
+
instance.should_receive(:run_command).ordered.with("sudo apt-get -y install ruby").and_return(true)
|
139
|
+
instance.should_receive(:run_command).ordered.with("sudo apt-get -y install rubygems1.8").and_return(true)
|
140
|
+
instance.should_receive(:run_command).ordered.with("sudo gem install chef --no-ri --no-rdoc --version 0.10.8").and_return(true)
|
141
|
+
capture_stdout { instance.bootstrap }
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
describe "#converge" do
|
146
|
+
it "converges the given instance" do
|
147
|
+
instance.should_receive(:scp).ordered.with('some_path/', "/tmp/chef-solo", :recursive => true).and_return(true)
|
148
|
+
instance.should_receive(:run_command).ordered.with("cd /tmp/chef-solo && sudo /var/lib/gems/1.8/bin/chef-solo -c solo.rb -j dna.json").and_return(true)
|
149
|
+
capture_stdout { instance.converge('some_path') }
|
150
|
+
end
|
151
|
+
end
|
152
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
require 'rspec/autorun'
|
2
|
+
require 'slushy'
|
3
|
+
|
4
|
+
# Requires supporting ruby files with custom matchers and macros, etc,
|
5
|
+
# in spec/support/ and its subdirectories.
|
6
|
+
Dir[File.expand_path("support/*.rb", File.dirname(__FILE__))].each {|f| require f}
|
7
|
+
|
8
|
+
RSpec.configure do |config|
|
9
|
+
config.include OutputStreams
|
10
|
+
config.filter_run :focused => true
|
11
|
+
config.filter_run_excluding :disabled => true
|
12
|
+
config.run_all_when_everything_filtered = true
|
13
|
+
|
14
|
+
config.alias_example_to :fit, :focused => true
|
15
|
+
config.alias_example_to :xit, :disabled => true
|
16
|
+
config.alias_example_to :they
|
17
|
+
end
|
metadata
ADDED
@@ -0,0 +1,134 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: slushy
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Sam Umbach
|
9
|
+
- Gabriel Horner
|
10
|
+
- Alex Redington
|
11
|
+
autorequire:
|
12
|
+
bindir: bin
|
13
|
+
cert_chain: []
|
14
|
+
date: 2012-04-28 00:00:00.000000000 Z
|
15
|
+
dependencies:
|
16
|
+
- !ruby/object:Gem::Dependency
|
17
|
+
name: fog
|
18
|
+
requirement: !ruby/object:Gem::Requirement
|
19
|
+
none: false
|
20
|
+
requirements:
|
21
|
+
- - '='
|
22
|
+
- !ruby/object:Gem::Version
|
23
|
+
version: 1.3.1
|
24
|
+
type: :development
|
25
|
+
prerelease: false
|
26
|
+
version_requirements: !ruby/object:Gem::Requirement
|
27
|
+
none: false
|
28
|
+
requirements:
|
29
|
+
- - '='
|
30
|
+
- !ruby/object:Gem::Version
|
31
|
+
version: 1.3.1
|
32
|
+
- !ruby/object:Gem::Dependency
|
33
|
+
name: rspec
|
34
|
+
requirement: !ruby/object:Gem::Requirement
|
35
|
+
none: false
|
36
|
+
requirements:
|
37
|
+
- - ! '>='
|
38
|
+
- !ruby/object:Gem::Version
|
39
|
+
version: '0'
|
40
|
+
type: :development
|
41
|
+
prerelease: false
|
42
|
+
version_requirements: !ruby/object:Gem::Requirement
|
43
|
+
none: false
|
44
|
+
requirements:
|
45
|
+
- - ! '>='
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
- !ruby/object:Gem::Dependency
|
49
|
+
name: rake
|
50
|
+
requirement: !ruby/object:Gem::Requirement
|
51
|
+
none: false
|
52
|
+
requirements:
|
53
|
+
- - ~>
|
54
|
+
- !ruby/object:Gem::Version
|
55
|
+
version: 0.9.2.2
|
56
|
+
type: :development
|
57
|
+
prerelease: false
|
58
|
+
version_requirements: !ruby/object:Gem::Requirement
|
59
|
+
none: false
|
60
|
+
requirements:
|
61
|
+
- - ~>
|
62
|
+
- !ruby/object:Gem::Version
|
63
|
+
version: 0.9.2.2
|
64
|
+
- !ruby/object:Gem::Dependency
|
65
|
+
name: bundler
|
66
|
+
requirement: !ruby/object:Gem::Requirement
|
67
|
+
none: false
|
68
|
+
requirements:
|
69
|
+
- - ~>
|
70
|
+
- !ruby/object:Gem::Version
|
71
|
+
version: '1.1'
|
72
|
+
type: :development
|
73
|
+
prerelease: false
|
74
|
+
version_requirements: !ruby/object:Gem::Requirement
|
75
|
+
none: false
|
76
|
+
requirements:
|
77
|
+
- - ~>
|
78
|
+
- !ruby/object:Gem::Version
|
79
|
+
version: '1.1'
|
80
|
+
description: Giving Chef a hand in the provisional kitchen - Aussie style. Using Fog's
|
81
|
+
API, creates an instance and converges chef recipes on it.
|
82
|
+
email:
|
83
|
+
- sam@thinkrelevance.com
|
84
|
+
executables: []
|
85
|
+
extensions: []
|
86
|
+
extra_rdoc_files: []
|
87
|
+
files:
|
88
|
+
- .gitignore
|
89
|
+
- .rvmrc
|
90
|
+
- CHANGELOG.md
|
91
|
+
- Gemfile
|
92
|
+
- README.md
|
93
|
+
- Rakefile
|
94
|
+
- lib/slushy.rb
|
95
|
+
- lib/slushy/instance.rb
|
96
|
+
- lib/slushy/version.rb
|
97
|
+
- slushy.gemspec
|
98
|
+
- spec/lib/instance_spec.rb
|
99
|
+
- spec/spec_helper.rb
|
100
|
+
- spec/support/output_streams.rb
|
101
|
+
homepage: http://github.com/relevance/slushy
|
102
|
+
licenses: []
|
103
|
+
post_install_message:
|
104
|
+
rdoc_options: []
|
105
|
+
require_paths:
|
106
|
+
- lib
|
107
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
108
|
+
none: false
|
109
|
+
requirements:
|
110
|
+
- - ! '>='
|
111
|
+
- !ruby/object:Gem::Version
|
112
|
+
version: '0'
|
113
|
+
segments:
|
114
|
+
- 0
|
115
|
+
hash: 3888575469612740202
|
116
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
117
|
+
none: false
|
118
|
+
requirements:
|
119
|
+
- - ! '>='
|
120
|
+
- !ruby/object:Gem::Version
|
121
|
+
version: '0'
|
122
|
+
segments:
|
123
|
+
- 0
|
124
|
+
hash: 3888575469612740202
|
125
|
+
requirements: []
|
126
|
+
rubyforge_project:
|
127
|
+
rubygems_version: 1.8.21
|
128
|
+
signing_key:
|
129
|
+
specification_version: 3
|
130
|
+
summary: An Aussie kitchen hand helping out Chef
|
131
|
+
test_files:
|
132
|
+
- spec/lib/instance_spec.rb
|
133
|
+
- spec/spec_helper.rb
|
134
|
+
- spec/support/output_streams.rb
|