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 ADDED
@@ -0,0 +1,4 @@
1
+ *.gem
2
+ .bundle
3
+ Gemfile.lock
4
+ pkg/*
data/.rvmrc ADDED
@@ -0,0 +1 @@
1
+ rvm use ruby-1.9.2@slushy --create
data/CHANGELOG.md ADDED
@@ -0,0 +1,2 @@
1
+ ## 0.1.0
2
+ * Initial release!
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source :rubygems
2
+
3
+ gemspec
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,7 @@
1
+ require 'bundler/gem_tasks'
2
+ require 'rspec/core/rake_task'
3
+
4
+ RSpec::Core::RakeTask.new
5
+ task :default => :spec
6
+
7
+ Dir["lib/tasks/*.rake"].each { |ext| load(ext) }
@@ -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
@@ -0,0 +1,3 @@
1
+ module Slushy
2
+ VERSION = "0.1.0"
3
+ end
data/lib/slushy.rb ADDED
@@ -0,0 +1,4 @@
1
+ module Slushy; end
2
+
3
+ require 'slushy/version'
4
+ require 'slushy/instance'
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
@@ -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
@@ -0,0 +1,12 @@
1
+ module OutputStreams
2
+ def capture_stdout(&block)
3
+ original_stdout = $stdout
4
+ $stdout = fake = StringIO.new
5
+ begin
6
+ yield
7
+ ensure
8
+ $stdout = original_stdout
9
+ end
10
+ fake.string
11
+ end
12
+ 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