slushy 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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