testbot_cloud 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/.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
+