testbot_cloud 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/.autotest ADDED
@@ -0,0 +1,7 @@
1
+ require 'autotest/restart'
2
+
3
+ Autotest.add_hook :initialize do |at|
4
+ # Makes change detection much faster (but CPU usage higher)
5
+ at.sleep = 0.2
6
+ end
7
+
data/.gemtest ADDED
File without changes
data/.gitignore ADDED
@@ -0,0 +1,9 @@
1
+ *.gem
2
+ .bundle
3
+ Gemfile.lock
4
+ pkg/*
5
+ todo.txt
6
+ tmp
7
+ ec2
8
+ bb
9
+ bb_albert
data/.rvmrc ADDED
@@ -0,0 +1 @@
1
+ rvm ree-1.8.7-2011.03@testbot_cloud --create
data/CHANGELOG ADDED
File without changes
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in testbot_cloud.gemspec
4
+ gemspec
data/README.markdown ADDED
@@ -0,0 +1,86 @@
1
+ A tool for creating and managing [testbot](https://github.com/joakimk/testbot) clusters in the cloud.
2
+
3
+ TestbotCloud is based around the idea that you have a project folder for each cluster (which you can store in verison control). You then use the "testbot_cloud" command to start and stop the cluster.
4
+
5
+ The motivation behind this tool, besides making distributed testing simpler is to be able to run a cluster only when it's needed (by scheduling it with tools like cron).
6
+
7
+ Installing
8
+ ----
9
+
10
+ gem install testbot_cloud
11
+
12
+ Getting started
13
+ ----
14
+
15
+ Using AWS EC2:
16
+
17
+ * Get a AWS account at [http://aws.amazon.com/](http://aws.amazon.com/).
18
+ * Create a Key Pair.
19
+ * Allow SSH login to a security group. For example: SSH, tcp, 22, 22, 0.0.0.0/0.
20
+
21
+ Using Brightbox:
22
+
23
+ * Get a beta account at [http://beta.brightbox.com/beta](http://beta.brightbox.com/beta).
24
+ * Follow [http://docs.brightbox.com/guides/getting_started](http://docs.brightbox.com/guides/getting_started) to setup a SSH key.
25
+
26
+ Creating a cluster
27
+ ----
28
+
29
+ Create a project
30
+
31
+ testbot_cloud new demo
32
+
33
+ # create demo/config.yml
34
+ # create demo/.gitignore
35
+ # create demo/bootstrap/runner.sh
36
+
37
+ Start
38
+
39
+ cd demo
40
+ testbot_cloud start
41
+
42
+ # Starting 1 runners...
43
+ # i-dd2222dd is being created...
44
+ # i-dd2222dd is up, installing testbot...
45
+ # i-dd2222dd ready.
46
+
47
+ Shutdown
48
+
49
+ testbot_cloud stop
50
+
51
+ # Shutting down i-dd2222dd...
52
+
53
+ Debugging
54
+ ----
55
+
56
+ * Run **DEBUG=true testbot_cloud start** to show all SSH commands and output.
57
+
58
+ Gotchas
59
+ -----
60
+
61
+ * Don't create more than 10-15 or so runners at a time (some cloud providers don't allow more than that many connections at once). This might be fixed by batching the creation process in a later version of TestbotCloud.
62
+
63
+ * Don't create more than 20 runners in total on EC2 as it has a limit by default. See the [EC2 FAQ](http://aws.amazon.com/ec2/faqs) for more info.
64
+
65
+ Features
66
+ -----
67
+
68
+ * TestbotCloud is continuously tested for compability with Ruby 1.8.7, 1.9.2, JRuby 1.5.5 and Rubinius 1.1.1.
69
+ * TestbotCloud is designed to be as reliable as possible when starting and stopping so that you can schedule it with tools like cron and save money.
70
+
71
+ How to add support for additional cloud computing providers
72
+ -----
73
+
74
+ Basics:
75
+
76
+ * Look at lib/server/aws.rb and lib/server/brightbox.rb.
77
+ * Write you own and add it to lib/server/factory.rb.
78
+ * Add fog config suitable for the provider to your config.yml.
79
+
80
+ When contributing:
81
+
82
+ * Make sure you have the tests running on your machine (should be just running "bundle" and "rake").
83
+ * Write tests.
84
+ * Add a config example to the template at lib/templates/config.yml.
85
+ * Update this readme.
86
+
data/Rakefile ADDED
@@ -0,0 +1,16 @@
1
+ require 'bundler'
2
+ require 'cucumber'
3
+ require 'cucumber/rake/task'
4
+ require 'rspec/core/rake_task'
5
+
6
+ Bundler::GemHelper.install_tasks
7
+
8
+ Cucumber::Rake::Task.new(:features) do |t|
9
+ t.cucumber_opts = "features --format progress"
10
+ end
11
+
12
+ desc "Run specs"
13
+ RSpec::Core::RakeTask.new do |t|
14
+ end
15
+
16
+ task :default => [ :spec, :features ]
@@ -0,0 +1 @@
1
+ Autotest.add_discovery { "rspec2" }
data/bin/testbot_cloud ADDED
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env ruby
2
+ require File.expand_path(File.join(File.dirname(__FILE__), '../lib/testbot_cloud.rb'))
3
+
4
+ if ARGV.count == 0
5
+ puts "TestbotCloud #{TestbotCloud::VERSION}"
6
+ puts
7
+ end
8
+
9
+ TestbotCloud::Cli.start
10
+
@@ -0,0 +1,13 @@
1
+ Feature: Cluster management
2
+ In order to run my tests in the cloud
3
+ As a build monkey
4
+ I want to be able to start, stop and check status on the cloud instances
5
+
6
+ Scenario: Staring with 2 instances
7
+ Given I generate a project
8
+ And I start the testbot cluster
9
+ Then I should see "Starting 2 runners..."
10
+ And I should see " up, installing testbot..."
11
+ And I should see " ready."
12
+ And I should not see any errors
13
+
@@ -0,0 +1,14 @@
1
+ Feature: Project creation
2
+ In order to run my tests in the cloud
3
+ As a build monkey
4
+ I want to create a project that represents my testbot cluster
5
+
6
+ Background:
7
+ Given there is no project
8
+
9
+ Scenario: Creating a project
10
+ When I generate a project
11
+ Then there should be a project folder
12
+ And the project folder should contain a config file
13
+ And the project folder should contain bootstrap files
14
+
@@ -0,0 +1,35 @@
1
+ Given /^there is no project$/ do
2
+ system "rm -rf tmp/cluster"
3
+ end
4
+
5
+ When /^I generate a project$/ do
6
+ system("INTEGRATION_TEST=true bin/testbot_cloud new tmp/cluster 1> /dev/null") || raise
7
+ end
8
+
9
+ Then /^there should be a project folder$/ do
10
+ File.exists?("tmp/cluster") || raise
11
+ end
12
+
13
+ Then /^the project folder should contain a config file$/ do
14
+ File.exists?("tmp/cluster/config.yml") || raise
15
+ end
16
+
17
+ Then /^the project folder should contain bootstrap files$/ do
18
+ File.exists?("tmp/cluster/bootstrap/runner.sh") || raise
19
+ end
20
+
21
+ Given /^I start the testbot cluster$/ do
22
+ @results = `cd tmp/cluster; INTEGRATION_TEST=true ../../bin/testbot_cloud start 2>&1`
23
+ end
24
+
25
+ Then /^I should see "([^"]*)"$/ do |text|
26
+ @results.include?(text) || raise("Did not see '#{text}' in #{@results}")
27
+ end
28
+
29
+ Then /^I should not see any errors$/ do
30
+ if @results.include?("testbot_cloud/lib")
31
+ puts @results
32
+ raise
33
+ end
34
+ end
35
+
data/lib/cli.rb ADDED
@@ -0,0 +1,38 @@
1
+ require 'rubygems'
2
+ require 'thor'
3
+ require File.expand_path(File.join(File.dirname(__FILE__), 'cluster.rb'))
4
+ require File.expand_path(File.join(File.dirname(__FILE__), 'testbot_cloud/version.rb'))
5
+
6
+ module TestbotCloud
7
+ class Cli < Thor
8
+ include Thor::Actions
9
+
10
+ def self.source_root
11
+ File.dirname(__FILE__) + "/templates"
12
+ end
13
+
14
+ desc "new PROJECT_NAME", "Generate a testbot cloud project"
15
+ def new(name)
16
+ copy_file "config.yml", "#{name}/config.yml"
17
+ copy_file "gitignore", "#{name}/.gitignore"
18
+ copy_file "runner.sh", "#{name}/bootstrap/runner.sh"
19
+ end
20
+
21
+ desc "start", "Start a testbot cluster as configured in config.yml"
22
+ def start
23
+ Cluster.new.start
24
+ end
25
+
26
+ desc "stop", "Shutdown servers"
27
+ def stop
28
+ Cluster.new.stop
29
+ end
30
+
31
+ desc "version", "Show version"
32
+ def version
33
+ puts "TestbotCloud #{TestbotCloud::VERSION}"
34
+ end
35
+
36
+ end
37
+ end
38
+
data/lib/cluster.rb ADDED
@@ -0,0 +1,111 @@
1
+ require 'fog'
2
+ require 'yaml'
3
+ require 'active_support/core_ext/hash/keys'
4
+ require File.expand_path(File.join(File.dirname(__FILE__), 'server/factory.rb'))
5
+ require File.expand_path(File.join(File.dirname(__FILE__), 'servers.rb'))
6
+
7
+ module TestbotCloud
8
+ class Cluster
9
+
10
+ def initialize
11
+ Fog.mock! if ENV['INTEGRATION_TEST']
12
+ if project?
13
+ load_config
14
+
15
+ @compute = Fog::Compute.new(@provider_config)
16
+ end
17
+ end
18
+
19
+ def start
20
+ project? || return
21
+
22
+ puts "Starting #{@runner_count} runners..."
23
+ for_each_runner_in_a_thread do |mutex|
24
+ server = nil
25
+ with_retries("server creation") do
26
+ mutex.synchronize {
27
+ server = @compute.servers.create(@runner_config)
28
+ Servers.log_creation(server)
29
+
30
+ # Brightbox API is a bit unstable when creating multiple servers at the same time
31
+ if @provider_config[:provider] == "Brightbox"
32
+ # < Caius> joakimk2: yeah, there's a race condition with creating two servers within the
33
+ # same second currently. We're working on a fix. Workaround is to
34
+ # wait a second or two before creating the second.
35
+ sleep 3
36
+ end
37
+ }
38
+ end
39
+
40
+ puts "#{server.id} is being created..."
41
+ with_retries("#{server.id} ready check") do
42
+ server.wait_for { ready? }
43
+ end
44
+
45
+ puts "#{server.id} is up, installing testbot..."
46
+ with_retries("testbot installation") do
47
+ if Server::Factory.create(@compute, @opts, server).bootstrap!(mutex)
48
+ puts "#{server.id} ready."
49
+ else
50
+ puts "#{server.id} failed, shutting down."
51
+ server.destroy
52
+ Servers.log_destruction(server)
53
+ end
54
+ end
55
+ end
56
+ end
57
+
58
+ def stop
59
+ project? || return
60
+
61
+ @compute.servers.each do |server|
62
+ if Servers.known?(server) && server.ready?
63
+ puts "Shutting down #{server.id}..."
64
+ with_retries("server destruction") do
65
+ server.destroy
66
+ end
67
+ Servers.log_destruction(server)
68
+ end
69
+ end
70
+ end
71
+
72
+ private
73
+
74
+ def project?
75
+ File.exists?("config.yml")
76
+ end
77
+
78
+ def for_each_runner_in_a_thread
79
+ threads = []
80
+ mutex = Mutex.new
81
+ @runner_count.times do
82
+ threads << Thread.new do
83
+ yield mutex
84
+ end
85
+ end
86
+ threads.each { |thread| thread.join }
87
+ end
88
+
89
+ def load_config
90
+ config = YAML.load_file("config.yml")
91
+ @provider_config = config["provider"].symbolize_keys
92
+ @runner_config = config["runner"].symbolize_keys
93
+ @runner_count = config["runners"]
94
+ @opts = { :ssh_user => config["ssh_user"] }
95
+ end
96
+
97
+ def with_retries(job)
98
+ 5.times do
99
+ begin
100
+ yield
101
+ rescue Excon::Errors::SocketError => ex
102
+ puts "#{job} failed, retrying..."
103
+ sleep 3
104
+ else
105
+ break
106
+ end
107
+ end
108
+ end
109
+
110
+ end
111
+ end
data/lib/server/aws.rb ADDED
@@ -0,0 +1,19 @@
1
+ require File.expand_path(File.join(File.dirname(__FILE__), 'shared', 'bootstrap.rb'))
2
+
3
+ module TestbotCloud
4
+ module Server
5
+ class AWS
6
+ def initialize(compute, opts, server)
7
+ @server, @opts = server, opts
8
+ end
9
+
10
+ def bootstrap!(mutex)
11
+ # We use the AWS adapter in integration tests.
12
+ return true if ENV['INTEGRATION_TEST']
13
+
14
+ Bootstrap.new(@server.dns_name, @server.id, @opts.merge({ :ssh_opts => "-i testbot.pem" })).install
15
+ end
16
+ end
17
+ end
18
+ end
19
+
@@ -0,0 +1,38 @@
1
+ module TestbotCloud
2
+ module Server
3
+ class Brightbox
4
+ def initialize(compute, opts, server)
5
+ @compute, @opts, @server = compute, opts, server
6
+ end
7
+
8
+ def bootstrap!(mutex)
9
+ ip = nil
10
+ mutex.synchronize { ip = map_ip! }
11
+ Bootstrap.new(ip.public_ip, @server.id, @opts).install
12
+ end
13
+
14
+ private
15
+
16
+ def map_ip!
17
+ ip = get_ip
18
+ ip.map(@server.interfaces.first["id"])
19
+ ip
20
+ end
21
+
22
+ def get_ip
23
+ if available_ip = find_available_ip
24
+ available_ip
25
+ else
26
+ @compute.create_cloud_ip
27
+ find_available_ip
28
+ end
29
+ end
30
+
31
+ def find_available_ip
32
+ @compute.cloud_ips.find { |ip| ip.status == "unmapped" }
33
+ end
34
+ end
35
+ end
36
+ end
37
+
38
+
@@ -0,0 +1,21 @@
1
+ require 'fog/compute/models/slicehost/server'
2
+ require 'fog/compute/models/brightbox/server'
3
+ require 'fog/compute/models/aws/server'
4
+ require File.expand_path(File.join(File.dirname(__FILE__), 'brightbox.rb'))
5
+ require File.expand_path(File.join(File.dirname(__FILE__), 'aws.rb'))
6
+
7
+ module TestbotCloud
8
+ module Server
9
+ class Factory
10
+ def self.create(compute, opts, server)
11
+ if server.is_a?(Fog::Brightbox::Compute::Server)
12
+ Brightbox.new(compute, opts, server)
13
+ elsif server.is_a?(Fog::AWS::Compute::Server)
14
+ AWS.new(compute, opts, server)
15
+ else
16
+ raise "Unsupported server type: #{server}"
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,60 @@
1
+ module TestbotCloud
2
+ module Server
3
+ class Bootstrap
4
+ def initialize(host, id, opts)
5
+ @host, @id, @opts = host, id, opts
6
+ end
7
+
8
+ def install
9
+ wait_for_server && upload_bootstrap_files && run('cd bootstrap; sudo sh runner.sh')
10
+ end
11
+
12
+ private
13
+
14
+ def wait_for_server
15
+ fail_count = 0
16
+ 10.times do
17
+ if run('true')
18
+ return true
19
+ else
20
+ fail_count += 1
21
+
22
+ # Usally fails atleast once, so better to keep it quiet to begin with
23
+ if fail_count > 2
24
+ puts "#{@id} ssh connection failed, retrying..."
25
+ end
26
+
27
+ sleep 3
28
+ end
29
+ end
30
+
31
+ false
32
+ end
33
+
34
+ def upload_bootstrap_files
35
+ run_with_debug("scp #{ssh_opts("-r bootstrap")}:~ &> /dev/null")
36
+ end
37
+
38
+ def run(command)
39
+ run_with_debug("ssh #{ssh_opts} '#{command}' &> /dev/null")
40
+ end
41
+
42
+ def run_with_debug(cmd)
43
+ if ENV["DEBUG"]
44
+ puts "DEBUG - CMD: #{cmd}"
45
+ return_status = system(cmd.gsub(/&.+/, ''))
46
+ puts "DEBUG - SUCCESS: #{return_status}"
47
+ return_status
48
+ else
49
+ system(cmd)
50
+ end
51
+ end
52
+
53
+ def ssh_opts(custom_opts = nil)
54
+ [ "-o StrictHostKeyChecking=no", @opts[:ssh_opts],
55
+ custom_opts, "#{@opts[:ssh_user]}@#{@host}" ].compact.join(' ')
56
+ end
57
+ end
58
+ end
59
+ end
60
+
data/lib/servers.rb ADDED
@@ -0,0 +1,15 @@
1
+ module TestbotCloud
2
+ class Servers
3
+ def self.log_creation(server)
4
+ FileUtils.mkdir_p ".servers/#{server.id}"
5
+ end
6
+
7
+ def self.known?(server)
8
+ File.exists? ".servers/#{server.id}"
9
+ end
10
+
11
+ def self.log_destruction(server)
12
+ FileUtils.rm_rf ".servers/#{server.id}"
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,53 @@
1
+ # How many instances to run
2
+ runners: 2
3
+
4
+ # The name of the user provided by the cloud image
5
+ ssh_user: ubuntu
6
+
7
+ # Using EC2
8
+ provider:
9
+ provider: AWS
10
+ aws_access_key_id: AWS_ACCESS_ID
11
+ aws_secret_access_key: AWS_ACCESS_KEY
12
+
13
+ runner:
14
+ key_name: ec2_key
15
+ groups: default
16
+ flavor_id: c1.medium
17
+
18
+ ### Ubuntu 10.04 images ###
19
+
20
+ # 32 bit, ebs (use with t1.micro)
21
+ #image_id: ami-480df921
22
+
23
+ # 32 bit, instance
24
+ image_id: ami-480df921
25
+
26
+ # 64 bit, instance
27
+ #image_id: ami-da0cf8b3
28
+
29
+ # Has the cheapest instances
30
+ region: us-east-1
31
+
32
+ # Using Brightbox
33
+ #provider:
34
+ # provider: Brightbox
35
+ # brightbox_client_id: cli-XXXXX
36
+ # brightbox_secret: xxx
37
+ #
38
+ #runner:
39
+ # # Ubuntu 10.04, 32 bit
40
+ # image_id: img-hm6oj
41
+ #
42
+ # # 4 cores, 1 GB (256 MB / core)
43
+ # flavor_id: typ-iqisj
44
+ #
45
+ # # 2 cores, 512 MB (256 MB / core)
46
+ # # flavor_id: typ-4nssg
47
+ #
48
+ # # 4 cores, 2 GB (512 MB / core)
49
+ # # flavor_id: typ-urtky
50
+ #
51
+ # # 8 cores, 4 GB (512 MB / core)
52
+ # # flavor_id: typ-qdiwq
53
+
@@ -0,0 +1 @@
1
+ .servers
@@ -0,0 +1,25 @@
1
+ #!/bin/sh
2
+ export DEBIAN_FRONTEND=noninteractive
3
+
4
+ apt-get update &&
5
+ apt-get -y install curl wget ruby ruby-dev libopenssl-ruby &&
6
+ wget http://production.cf.rubygems.org/rubygems/rubygems-1.3.7.tgz &&
7
+ tar xvfz rubygems-1.3.7.tgz && cd rubygems-1.3.7 && ruby setup.rb && ln -s /usr/bin/gem1.8 /usr/bin/gem
8
+
9
+ su ubuntu -c 'GEM_HOME="~/.gem" gem install testbot --no-ri --no-rdoc'
10
+
11
+ # Ramdisk for fast rsync
12
+ echo 'none /tmp tmpfs defaults 0 0' >> /etc/fstab
13
+ mount -a
14
+
15
+ cd /home/ubuntu
16
+ mv bootstrap/ssh/* .ssh/
17
+ chown -R ubuntu .ssh
18
+ chmod 0600 .ssh/id_rsa
19
+
20
+ # TODO:
21
+ # Haven't gotten auto_update to run using this cloud setup
22
+ # script yet, it updates but does not restart the process.
23
+
24
+ su ubuntu -c 'export PATH="$HOME/.gem/bin:$PATH"; export GEM_HOME="$HOME/.gem"; testbot --runner --ssh_tunnel --auto_update --connect your_server_host'
25
+
@@ -0,0 +1,5 @@
1
+ require File.expand_path(File.join(File.dirname(__FILE__), 'cli.rb'))
2
+
3
+ module TestbotCloud
4
+ end
5
+
@@ -0,0 +1,3 @@
1
+ module TestbotCloud
2
+ VERSION = "0.1.0"
3
+ end
data/spec/cli_spec.rb ADDED
@@ -0,0 +1,56 @@
1
+ require File.expand_path(File.join(File.dirname(__FILE__), 'spec_helper.rb'))
2
+
3
+ describe TestbotCloud::Cli do
4
+
5
+ it "should include Thor::Actions" do
6
+ TestbotCloud::Cli.included_modules.should include(Thor::Actions)
7
+ end
8
+
9
+ it "should set source_root to lib/templates" do
10
+ TestbotCloud::Cli.source_root.should include("lib/templates")
11
+ end
12
+
13
+ describe "when calling new with a project name" do
14
+
15
+ it "should generate a project" do
16
+ cli = TestbotCloud::Cli.new
17
+ cli.should_receive(:copy_file).with("config.yml", "app_name/config.yml")
18
+ cli.should_receive(:copy_file).with("gitignore", "app_name/.gitignore")
19
+ cli.should_receive(:copy_file).with("runner.sh", "app_name/bootstrap/runner.sh")
20
+ cli.new("app_name")
21
+ end
22
+
23
+ end
24
+
25
+ describe "when calling start" do
26
+
27
+ it "should start a cluster" do
28
+ TestbotCloud::Cluster.stub!(:new).and_return(cluster = mock(Object))
29
+ cluster.should_receive(:start)
30
+ TestbotCloud::Cli.new.start
31
+ end
32
+
33
+ end
34
+
35
+ describe "when calling stop" do
36
+
37
+ it "should stop a cluster" do
38
+ TestbotCloud::Cluster.stub!(:new).and_return(cluster = mock(Object))
39
+ cluster.should_receive(:stop)
40
+ TestbotCloud::Cli.new.stop
41
+ end
42
+
43
+ end
44
+
45
+ describe "when calling version" do
46
+
47
+ it "should print the version" do
48
+ cli = TestbotCloud::Cli.new
49
+ cli.should_receive(:puts).with("TestbotCloud #{TestbotCloud::VERSION}")
50
+ cli.version
51
+ end
52
+
53
+ end
54
+
55
+ end
56
+
@@ -0,0 +1,223 @@
1
+ require File.expand_path(File.join(File.dirname(__FILE__), 'spec_helper.rb'))
2
+
3
+ describe TestbotCloud::Cluster do
4
+
5
+ describe "when not in a project" do
6
+
7
+ it "should do nothing if there is no config.yml file" do
8
+ File.should_receive(:exists?).any_number_of_times.with("config.yml").and_return(false)
9
+ cluster = TestbotCloud::Cluster.new
10
+ cluster.start
11
+ cluster.stop
12
+ end
13
+
14
+ end
15
+
16
+ describe "when calling start" do
17
+
18
+ before do
19
+ File.stub!(:exists?).with("config.yml").and_return(true)
20
+ YAML.should_receive(:load_file).with("config.yml").and_return({
21
+ "runners" => 2,
22
+ "ssh_user" => "ubuntu",
23
+ "provider" => {
24
+ "provider" => "AWS",
25
+ "aws_access_key_id" => "KEY_ID"
26
+ },
27
+ "runner" => {
28
+ "image_id" => "ami-xxxx"
29
+ }
30
+ });
31
+
32
+ FileUtils.stub!(:mkdir_p)
33
+ Fog::Compute.should_receive(:new).with({ :provider => "AWS",
34
+ :aws_access_key_id => "KEY_ID" }).
35
+ and_return(@compute = mock(Object))
36
+ @compute.stub!(:servers).and_return(mock(Object))
37
+ end
38
+
39
+ it "should create servers based on config.yml" do
40
+ @compute.servers.should_receive(:create).twice.with(:image_id => "ami-xxxx").
41
+ and_return(fog_server = mock(Object, :id => nil, :wait_for => nil))
42
+
43
+ TestbotCloud::Server::Factory.should_receive(:create).twice.with(@compute, { :ssh_user => 'ubuntu' }, fog_server).
44
+ and_return(server = mock(Object))
45
+ server.should_receive(:bootstrap!).twice.and_return(true)
46
+
47
+ cluster = TestbotCloud::Cluster.new
48
+ cluster.stub!(:puts)
49
+ cluster.start
50
+ end
51
+
52
+ it "should log the servers that are created" do
53
+ @compute.servers.stub!(:create).and_return(mock(Object, :id => "srv-boo", :wait_for => nil),
54
+ mock(Object, :id => "srv-doo", :wait_for => nil))
55
+
56
+ TestbotCloud::Server::Factory.should_receive(:create).twice.and_return(server = mock(Object))
57
+ server.stub!(:bootstrap!).and_return(true)
58
+
59
+ FileUtils.should_receive(:mkdir_p).with(".servers/srv-boo")
60
+ FileUtils.should_receive(:mkdir_p).with(".servers/srv-doo")
61
+
62
+ cluster = TestbotCloud::Cluster.new
63
+ cluster.stub!(:puts)
64
+ cluster.stub!(:sleep)
65
+ cluster.start
66
+ end
67
+
68
+ it "should destroy the server if bootstrap fails" do
69
+ @compute.servers.stub!(:create).and_return(fog_server0 = mock(Object, :id => "srv-boo", :wait_for => nil),
70
+ fog_server1 = mock(Object, :id => "srv-moo", :wait_for => nil))
71
+
72
+ TestbotCloud::Server::Factory.should_receive(:create).twice.and_return(server = mock(Object))
73
+ server.stub!(:bootstrap!).and_return(false)
74
+
75
+ FileUtils.should_receive(:rm_rf).with(".servers/srv-moo")
76
+ FileUtils.should_receive(:rm_rf).with(".servers/srv-boo")
77
+
78
+ fog_server0.should_receive(:destroy)
79
+ fog_server1.should_receive(:destroy)
80
+
81
+ cluster = TestbotCloud::Cluster.new
82
+ cluster.stub!(:puts)
83
+ cluster.start
84
+ end
85
+
86
+ it "should retry server create if it fails with a socket error" do
87
+ class OpenStructWithQuietId < OpenStruct; def id; end; end
88
+ class SocketErrorFogServerCollection
89
+ attr_reader :id
90
+
91
+ def create(opts)
92
+ unless @called_once
93
+ @called_once = true
94
+ raise Excon::Errors::SocketError.new(Exception.new)
95
+ else
96
+ return OpenStructWithQuietId.new
97
+ end
98
+ end
99
+ end
100
+
101
+ @compute.stub!(:servers).and_return(SocketErrorFogServerCollection.new)
102
+
103
+ TestbotCloud::Server::Factory.should_receive(:create).twice.
104
+ and_return(server = mock(Object))
105
+ server.stub!(:bootstrap!).and_return(true)
106
+
107
+ cluster = TestbotCloud::Cluster.new
108
+ cluster.stub!(:puts)
109
+ cluster.stub!(:sleep)
110
+ cluster.start
111
+ end
112
+
113
+ it "should retry wait for ready if it fails with a socket error" do
114
+ class SocketErrorFogServer
115
+ attr_reader :id
116
+
117
+ def wait_for
118
+ unless @called_once
119
+ @called_once = true
120
+ raise Excon::Errors::SocketError.new(Exception.new)
121
+ else
122
+ return true
123
+ end
124
+ end
125
+ end
126
+
127
+ @compute.servers.stub!(:create).and_return(fog_server = SocketErrorFogServer.new)
128
+
129
+ TestbotCloud::Server::Factory.should_receive(:create).twice.with(@compute,
130
+ { :ssh_user => "ubuntu" }, fog_server).
131
+ and_return(server = mock(Object))
132
+ server.stub!(:bootstrap!).and_return(true)
133
+
134
+ cluster = TestbotCloud::Cluster.new
135
+ cluster.stub!(:puts)
136
+ cluster.stub!(:sleep)
137
+ cluster.start
138
+ end
139
+
140
+ it "should retry bootstrap if it fails with a socket error" do
141
+ @compute.servers.stub!(:create).and_return(mock(Object, :id => nil, :wait_for => nil))
142
+
143
+ class SocketErrorServer
144
+ def bootstrap!(mutex)
145
+ unless @called_once
146
+ @called_once = true
147
+ raise Excon::Errors::SocketError.new(Exception.new)
148
+ else
149
+ return true
150
+ end
151
+ end
152
+ end
153
+
154
+ TestbotCloud::Server::Factory.stub!(:create).and_return(SocketErrorServer.new)
155
+
156
+ cluster = TestbotCloud::Cluster.new
157
+ cluster.stub!(:puts)
158
+ cluster.stub!(:sleep)
159
+ cluster.start
160
+ end
161
+
162
+ end
163
+
164
+ describe "when calling stop" do
165
+
166
+ before do
167
+ File.stub!(:exists?).with("config.yml").and_return(true)
168
+ YAML.should_receive(:load_file).with("config.yml").and_return({
169
+ "provider" => {
170
+ "provider" => "AWS",
171
+ "aws_access_key_id" => "KEY_ID"
172
+ },
173
+ "runner" => {},
174
+ "runners" => 0
175
+ });
176
+
177
+ Fog::Compute.should_receive(:new).with({ :provider => "AWS",
178
+ :aws_access_key_id => "KEY_ID" }).
179
+ and_return(@compute = mock(Object))
180
+ end
181
+
182
+ it "should stop servers that are listed in the log file and ready" do
183
+ @compute.stub!(:servers).and_return([ mock(Object, :ready? => false, :id => "srv-boo"),
184
+ server = mock(Object, :ready? => true, :id => "srv-moo"),
185
+ unrelated_server = mock(Object, :ready? => true, :id => "srv-ahh") ])
186
+
187
+ File.should_receive(:exists?).with(".servers/srv-boo").and_return(true)
188
+ File.should_receive(:exists?).with(".servers/srv-moo").and_return(true)
189
+ File.should_receive(:exists?).with(".servers/srv-ahh").and_return(false)
190
+ FileUtils.should_receive(:rm_rf).with(".servers/srv-moo")
191
+ server.should_receive(:destroy)
192
+
193
+ cluster = TestbotCloud::Cluster.new
194
+ cluster.stub!(:puts)
195
+ cluster.stop
196
+ end
197
+
198
+ it "should retry destroy if it fails with a socket error" do
199
+ class SocketErrorServer
200
+ def id; "srv-moo"; end
201
+ def ready?; true; end
202
+
203
+ def destroy
204
+ return if @called_once
205
+ @called_once = true
206
+ raise Excon::Errors::SocketError.new(Exception.new)
207
+ end
208
+ end
209
+
210
+ @compute.stub!(:servers).and_return([ server = SocketErrorServer.new ])
211
+
212
+ File.should_receive(:exists?).with(".servers/srv-moo").and_return(true)
213
+ FileUtils.should_receive(:rm_rf).with(".servers/srv-moo")
214
+
215
+ cluster = TestbotCloud::Cluster.new
216
+ cluster.stub!(:puts)
217
+ cluster.stub!(:sleep)
218
+ cluster.stop
219
+ end
220
+
221
+ end
222
+
223
+ end
@@ -0,0 +1,18 @@
1
+ require File.expand_path(File.join(File.dirname(__FILE__), '../spec_helper.rb'))
2
+
3
+ describe TestbotCloud::Server::AWS, "bootstrap!" do
4
+
5
+ it "should run bootstrap" do
6
+ fog_server = mock(:dns_name => "example.com", :id => "i-500")
7
+
8
+ TestbotCloud::Server::Bootstrap.should_receive(:new).with("example.com", "i-500",
9
+ :ssh_opts => "-i testbot.pem",
10
+ :ssh_user => "ubuntu").
11
+ and_return(bootstrap = mock)
12
+ bootstrap.should_receive(:install)
13
+ server = TestbotCloud::Server::AWS.new(nil, { :ssh_user => "ubuntu" }, fog_server)
14
+ server.bootstrap!(nil)
15
+ end
16
+
17
+ end
18
+
@@ -0,0 +1,39 @@
1
+ require File.expand_path(File.join(File.dirname(__FILE__), '../spec_helper.rb'))
2
+
3
+ describe TestbotCloud::Server::Brightbox, "bootstrap!" do
4
+
5
+ before :each do
6
+ TestbotCloud::Server::Bootstrap.stub!(:new).and_return(mock(:install => true))
7
+ end
8
+
9
+ it "should find unmapped ips and map to the server" do
10
+ compute = mock(Object, :cloud_ips =>
11
+ [ mock(Object, :status => "mapped"),
12
+ available_ip = mock(Object, :status => "unmapped", :public_ip => nil) ])
13
+ fog_server = mock(Object, :interfaces => [ { "id" => "int-xxyyy" } ], :id => nil)
14
+ server = TestbotCloud::Server::Brightbox.new(compute, {}, fog_server)
15
+
16
+ available_ip.should_receive(:map).with("int-xxyyy")
17
+ server.bootstrap!(Mutex.new)
18
+ end
19
+
20
+ it "should create a cloud ip and map when none are available" do
21
+ # Found no good way to assert the order of events other than creating a custom mock class.
22
+ class ComputeWithNoFreeIps
23
+ def initialize; @cloud_ips = []; end
24
+ attr_reader :cloud_ips
25
+
26
+ def create_cloud_ip
27
+ @cloud_ips << OpenStruct.new(:status => "unmapped")
28
+ @cloud_ips.last.should_receive(:map).with("int-xxyyy")
29
+ end
30
+ end
31
+
32
+ compute = ComputeWithNoFreeIps.new
33
+ fog_server = mock(Object, :interfaces => [ { "id" => "int-xxyyy" } ], :id => nil)
34
+ server = TestbotCloud::Server::Brightbox.new(compute, {}, fog_server)
35
+
36
+ server.bootstrap!(Mutex.new)
37
+ end
38
+
39
+ end
@@ -0,0 +1,22 @@
1
+ require File.expand_path(File.join(File.dirname(__FILE__), '../spec_helper.rb'))
2
+
3
+ describe TestbotCloud::Server::Factory do
4
+
5
+ it "should create a server wrapper for Brightbox" do
6
+ server = TestbotCloud::Server::Factory.create(nil, nil, Fog::Brightbox::Compute::Server.new)
7
+ server.should be_instance_of(TestbotCloud::Server::Brightbox)
8
+ end
9
+
10
+ it "should create a server wrapper for AWS" do
11
+ server = TestbotCloud::Server::Factory.create(nil, nil, Fog::AWS::Compute::Server.new)
12
+ server.should be_instance_of(TestbotCloud::Server::AWS)
13
+ end
14
+
15
+ it "should raise an error when there is no server wrapper" do
16
+ expect {
17
+ TestbotCloud::Server::Factory.create(nil, nil, Fog::Slicehost::Compute::Server.new)
18
+ }.to raise_error(/Unsupported server type: /)
19
+ end
20
+
21
+
22
+ end
@@ -0,0 +1,39 @@
1
+ require File.expand_path(File.join(File.dirname(__FILE__), '../../spec_helper.rb'))
2
+
3
+ describe TestbotCloud::Server::Bootstrap do
4
+
5
+ it "should be able to upload bootstrap files and run them" do
6
+ bootstrap = TestbotCloud::Server::Bootstrap.new("15.14.13.12", "srv-foo", { :ssh_user => "ubuntu" })
7
+
8
+ bootstrap.should_receive(:system).and_return(true)
9
+ bootstrap.should_receive(:system).with("scp -o StrictHostKeyChecking=no -r bootstrap ubuntu@15.14.13.12:~ &> /dev/null").and_return(true)
10
+ bootstrap.should_receive(:system).with("ssh -o StrictHostKeyChecking=no ubuntu@15.14.13.12 'cd bootstrap; sudo sh runner.sh' &> /dev/null").and_return(true)
11
+
12
+ bootstrap.stub!(:sleep)
13
+ bootstrap.install
14
+ end
15
+
16
+ it "should try to get a connection going before running bootstrap" do
17
+ bootstrap = TestbotCloud::Server::Bootstrap.new("15.14.13.12", "srv-foo", { :ssh_user => "ubuntu" })
18
+
19
+ bootstrap.should_receive(:system).and_return(false)
20
+ bootstrap.should_receive(:system).and_return(false)
21
+ bootstrap.should_receive(:system).and_return(true)
22
+ bootstrap.should_receive(:system).with("scp -o StrictHostKeyChecking=no -r bootstrap ubuntu@15.14.13.12:~ &> /dev/null").and_return(true)
23
+ bootstrap.should_receive(:system).with("ssh -o StrictHostKeyChecking=no ubuntu@15.14.13.12 'cd bootstrap; sudo sh runner.sh' &> /dev/null").and_return(true)
24
+
25
+ bootstrap.stub!(:sleep).any_number_of_times
26
+ bootstrap.install
27
+ end
28
+
29
+ it "should not try to bootstrap if the connection fails" do
30
+ bootstrap = TestbotCloud::Server::Bootstrap.new("15.14.13.12", "srv-foo", { :ssh_user => "ubuntu" })
31
+ bootstrap.stub!(:system).and_return(false)
32
+ bootstrap.should_not_receive(:system).with("scp -o StrictHostKeyChecking=no -r bootstrap ubuntu@15.14.13.12:~ &> /dev/null")
33
+
34
+ bootstrap.stub!(:sleep)
35
+ bootstrap.stub!(:puts)
36
+ bootstrap.install
37
+ end
38
+
39
+ end
@@ -0,0 +1 @@
1
+ require File.expand_path(File.join(File.dirname(__FILE__), '../lib/testbot_cloud.rb'))
@@ -0,0 +1,26 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "testbot_cloud/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "testbot_cloud"
7
+ s.version = TestbotCloud::VERSION
8
+ s.platform = Gem::Platform::RUBY
9
+ s.authors = ["Joakim Kolsjö"]
10
+ s.email = ["joakim.kolsjo@gmail.com"]
11
+ s.homepage = ""
12
+ s.summary = %q{Run your tests in the cloud}
13
+ s.description = %q{A tool for creating and managing testbot clusters in the cloud}
14
+
15
+ s.files = `git ls-files`.split("\n")
16
+ s.test_files = `git ls-files -- {spec}/*`.split("\n")
17
+ s.executables = `git ls-files -- bin/*`.split("\n").map { |f| File.basename(f) }
18
+ s.require_paths = ["lib"]
19
+
20
+ s.add_dependency "thor", ">0.14.5"
21
+ s.add_dependency "fog", ">0.7.1"
22
+ s.add_dependency "activesupport", ">3.0.0"
23
+ s.add_development_dependency "bundler"
24
+ s.add_development_dependency "rspec"
25
+ s.add_development_dependency "cucumber"
26
+ end
metadata ADDED
@@ -0,0 +1,189 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: testbot_cloud
3
+ version: !ruby/object:Gem::Version
4
+ hash: 27
5
+ prerelease:
6
+ segments:
7
+ - 0
8
+ - 1
9
+ - 0
10
+ version: 0.1.0
11
+ platform: ruby
12
+ authors:
13
+ - "Joakim Kolsj\xC3\xB6"
14
+ autorequire:
15
+ bindir: bin
16
+ cert_chain: []
17
+
18
+ date: 2011-04-17 00:00:00 +02:00
19
+ default_executable:
20
+ dependencies:
21
+ - !ruby/object:Gem::Dependency
22
+ name: thor
23
+ prerelease: false
24
+ requirement: &id001 !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ">"
28
+ - !ruby/object:Gem::Version
29
+ hash: 45
30
+ segments:
31
+ - 0
32
+ - 14
33
+ - 5
34
+ version: 0.14.5
35
+ type: :runtime
36
+ version_requirements: *id001
37
+ - !ruby/object:Gem::Dependency
38
+ name: fog
39
+ prerelease: false
40
+ requirement: &id002 !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ">"
44
+ - !ruby/object:Gem::Version
45
+ hash: 1
46
+ segments:
47
+ - 0
48
+ - 7
49
+ - 1
50
+ version: 0.7.1
51
+ type: :runtime
52
+ version_requirements: *id002
53
+ - !ruby/object:Gem::Dependency
54
+ name: activesupport
55
+ prerelease: false
56
+ requirement: &id003 !ruby/object:Gem::Requirement
57
+ none: false
58
+ requirements:
59
+ - - ">"
60
+ - !ruby/object:Gem::Version
61
+ hash: 7
62
+ segments:
63
+ - 3
64
+ - 0
65
+ - 0
66
+ version: 3.0.0
67
+ type: :runtime
68
+ version_requirements: *id003
69
+ - !ruby/object:Gem::Dependency
70
+ name: bundler
71
+ prerelease: false
72
+ requirement: &id004 !ruby/object:Gem::Requirement
73
+ none: false
74
+ requirements:
75
+ - - ">="
76
+ - !ruby/object:Gem::Version
77
+ hash: 3
78
+ segments:
79
+ - 0
80
+ version: "0"
81
+ type: :development
82
+ version_requirements: *id004
83
+ - !ruby/object:Gem::Dependency
84
+ name: rspec
85
+ prerelease: false
86
+ requirement: &id005 !ruby/object:Gem::Requirement
87
+ none: false
88
+ requirements:
89
+ - - ">="
90
+ - !ruby/object:Gem::Version
91
+ hash: 3
92
+ segments:
93
+ - 0
94
+ version: "0"
95
+ type: :development
96
+ version_requirements: *id005
97
+ - !ruby/object:Gem::Dependency
98
+ name: cucumber
99
+ prerelease: false
100
+ requirement: &id006 !ruby/object:Gem::Requirement
101
+ none: false
102
+ requirements:
103
+ - - ">="
104
+ - !ruby/object:Gem::Version
105
+ hash: 3
106
+ segments:
107
+ - 0
108
+ version: "0"
109
+ type: :development
110
+ version_requirements: *id006
111
+ description: A tool for creating and managing testbot clusters in the cloud
112
+ email:
113
+ - joakim.kolsjo@gmail.com
114
+ executables:
115
+ - testbot_cloud
116
+ extensions: []
117
+
118
+ extra_rdoc_files: []
119
+
120
+ files:
121
+ - .autotest
122
+ - .gemtest
123
+ - .gitignore
124
+ - .rvmrc
125
+ - CHANGELOG
126
+ - Gemfile
127
+ - README.markdown
128
+ - Rakefile
129
+ - autotest/discover.rb
130
+ - bin/testbot_cloud
131
+ - features/cluster_management.feature
132
+ - features/project_creation.feature
133
+ - features/step_definitions/shared_steps.rb
134
+ - lib/cli.rb
135
+ - lib/cluster.rb
136
+ - lib/server/aws.rb
137
+ - lib/server/brightbox.rb
138
+ - lib/server/factory.rb
139
+ - lib/server/shared/bootstrap.rb
140
+ - lib/servers.rb
141
+ - lib/templates/config.yml
142
+ - lib/templates/gitignore
143
+ - lib/templates/runner.sh
144
+ - lib/testbot_cloud.rb
145
+ - lib/testbot_cloud/version.rb
146
+ - spec/cli_spec.rb
147
+ - spec/cluster_spec.rb
148
+ - spec/server/aws_spec.rb
149
+ - spec/server/brightbox_spec.rb
150
+ - spec/server/factory_spec.rb
151
+ - spec/server/shared/bootstrap_spec.rb
152
+ - spec/spec_helper.rb
153
+ - testbot_cloud.gemspec
154
+ has_rdoc: true
155
+ homepage: ""
156
+ licenses: []
157
+
158
+ post_install_message:
159
+ rdoc_options: []
160
+
161
+ require_paths:
162
+ - lib
163
+ required_ruby_version: !ruby/object:Gem::Requirement
164
+ none: false
165
+ requirements:
166
+ - - ">="
167
+ - !ruby/object:Gem::Version
168
+ hash: 3
169
+ segments:
170
+ - 0
171
+ version: "0"
172
+ required_rubygems_version: !ruby/object:Gem::Requirement
173
+ none: false
174
+ requirements:
175
+ - - ">="
176
+ - !ruby/object:Gem::Version
177
+ hash: 3
178
+ segments:
179
+ - 0
180
+ version: "0"
181
+ requirements: []
182
+
183
+ rubyforge_project:
184
+ rubygems_version: 1.5.3
185
+ signing_key:
186
+ specification_version: 3
187
+ summary: Run your tests in the cloud
188
+ test_files: []
189
+