cloud_runner 0.0.1

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/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --color
2
+ --order random
3
+ --format progress
data/.rvmrc ADDED
@@ -0,0 +1 @@
1
+ rvm use --create ruby-1.9.3-p327@cloud_runner
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in cloud_runner.gemspec
4
+ gemspec
data/Gemfile.lock ADDED
@@ -0,0 +1,44 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ cloud_runner (0.0.1)
5
+ digital_ocean
6
+ net-scp
7
+ net-ssh
8
+
9
+ GEM
10
+ remote: http://rubygems.org/
11
+ specs:
12
+ diff-lcs (1.2.1)
13
+ digital_ocean (1.0.0)
14
+ faraday (~> 0.8.4)
15
+ faraday_middleware
16
+ json
17
+ rash
18
+ faraday (0.8.6)
19
+ multipart-post (~> 1.1)
20
+ faraday_middleware (0.9.0)
21
+ faraday (>= 0.7.4, < 0.9)
22
+ hashie (2.0.2)
23
+ json (1.7.7)
24
+ multipart-post (1.2.0)
25
+ net-scp (1.1.0)
26
+ net-ssh (>= 2.6.5)
27
+ net-ssh (2.6.6)
28
+ rash (0.4.0)
29
+ hashie (~> 2.0.0)
30
+ rspec (2.13.0)
31
+ rspec-core (~> 2.13.0)
32
+ rspec-expectations (~> 2.13.0)
33
+ rspec-mocks (~> 2.13.0)
34
+ rspec-core (2.13.1)
35
+ rspec-expectations (2.13.0)
36
+ diff-lcs (>= 1.1.3, < 2.0)
37
+ rspec-mocks (2.13.0)
38
+
39
+ PLATFORMS
40
+ ruby
41
+
42
+ DEPENDENCIES
43
+ cloud_runner!
44
+ rspec
data/README.md ADDED
@@ -0,0 +1,33 @@
1
+ # Cloud Runner
2
+
3
+ Cloud Runner creates a new VM somewhere in the cloud and
4
+ runs a single script against it.
5
+
6
+ Exit code correctly propagates.
7
+
8
+
9
+ ## Installation
10
+
11
+ gem install cloud_runner
12
+
13
+
14
+ ## DigitalOcean Cloud
15
+
16
+ Uses `https://www.digitalocean.com/api` for creating new
17
+ droplets (VM) and associating ssh keys.
18
+
19
+ Run script against new droplet:
20
+
21
+ cr-new \
22
+ --client-id CID \
23
+ --app-key APP_KEY \
24
+ --script something.sh
25
+
26
+ Run script against existing droplet:
27
+
28
+ cr-over \
29
+ --client-id CID \
30
+ --app-key APP_KEY \
31
+ --droplet-id DROPLET_ID \
32
+ --ssh-key SSH_KEY \
33
+ --script something.sh
data/Rakefile ADDED
@@ -0,0 +1,2 @@
1
+ require "bundler"
2
+ Bundler::GemHelper.install_tasks
data/bin/cr-new ADDED
@@ -0,0 +1,48 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "optparse"
4
+ require "bundler"
5
+ Bundler.setup
6
+
7
+ $:.unshift(File.expand_path("../lib", __FILE__))
8
+ require "cloud_runner/digital_ocean/cli/new"
9
+
10
+ options = {
11
+ :client_id => ENV["CR_CLIENT_ID"],
12
+ :api_key => ENV["CR_API_KEY"],
13
+ :host_image => "ubuntu-10-04",
14
+ }
15
+
16
+ OptionParser.new do |p|
17
+ p.on("-c", "--client-id CLIENT_ID", String,
18
+ "DigitalOcean Client id / ENV['CR_CLIENT_ID']") do |v|
19
+ options[:client_id] = v
20
+ end
21
+
22
+ p.on("-a", "--api-key API_KEY", String,
23
+ "DigitalOcean API key / ENV['CR_API_KEY']") do |v|
24
+ options[:api_key] = v
25
+ end
26
+
27
+ p.on("-s", "--script SCRIPT", String,
28
+ "Path to script to run on specified droplet") do |v|
29
+ options[:script] = v
30
+ end
31
+
32
+ p.on("-h", "--host-image [HOST_IMAGE]", String,
33
+ "Host image to use for building droplet") do |v|
34
+ options[:host_image] = v
35
+ end
36
+
37
+ p.on("--keep-droplet",
38
+ "Keep droplet after script finishes") do |v|
39
+ options[:keep_droplet] = true
40
+ end
41
+
42
+ p.on("-h", "--help", "Display this screen") do
43
+ puts(p)
44
+ exit
45
+ end
46
+ end.parse!
47
+
48
+ exit CloudRunner::DigitalOcean::Cli::New.new(options).run_script($stdout, $stderr)
data/bin/cr-over ADDED
@@ -0,0 +1,47 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "optparse"
4
+ require "bundler"
5
+ Bundler.setup
6
+
7
+ $:.unshift(File.expand_path("../lib", __FILE__))
8
+ require "cloud_runner/digital_ocean/cli/over"
9
+
10
+ options = {
11
+ :client_id => ENV["CR_CLIENT_ID"],
12
+ :api_key => ENV["CR_API_KEY"],
13
+ }
14
+
15
+ OptionParser.new do |p|
16
+ p.on("-c", "--client-id CLIENT_ID", String,
17
+ "DigitalOcean Client id / ENV['CR_CLIENT_ID']") do |v|
18
+ options[:client_id] = v
19
+ end
20
+
21
+ p.on("-a", "--api-key API_KEY", String,
22
+ "DigitalOcean API key / ENV['CR_API_KEY']") do |v|
23
+ options[:api_key] = v
24
+ end
25
+
26
+ p.on("-d", "--droplet-id DROPLET_ID", String,
27
+ "Droplet to find") do |v|
28
+ options[:droplet_id] = v
29
+ end
30
+
31
+ p.on("-k", "--ssh-key SSH_KEY", String,
32
+ "Path to SSH key for specified droplet") do |v|
33
+ options[:ssh_key] = v
34
+ end
35
+
36
+ p.on("-s", "--script SCRIPT", String,
37
+ "Path to script to run on specified droplet") do |v|
38
+ options[:script] = v
39
+ end
40
+
41
+ p.on("-h", "--help", "Display this screen") do
42
+ puts(p)
43
+ exit
44
+ end
45
+ end.parse!
46
+
47
+ exit CloudRunner::DigitalOcean::Cli::Over.new(options).run_script($stdout, $stderr)
@@ -0,0 +1,27 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "cloud_runner/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "cloud_runner"
7
+ s.version = CloudRunner::VERSION
8
+ s.platform = Gem::Platform::RUBY
9
+ s.authors = ["Dmitriy Kalinin", "Alex Suraci"]
10
+ s.email = ["cppforlife@gmail.com"]
11
+ s.homepage = "http://github.com/cppforlife/cloud_runner"
12
+ s.summary = %q{Quickly spin up new VM in the cloud to run a script.}
13
+ s.description = %q{Currently only supports DigitalOcean.}
14
+
15
+ s.rubyforge_project = "cloud_runner"
16
+
17
+ s.add_dependency "digital_ocean"
18
+ s.add_dependency "net-ssh"
19
+ s.add_dependency "net-scp"
20
+
21
+ s.add_development_dependency "rspec"
22
+
23
+ s.files = `git ls-files`.split("\n")
24
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
25
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
26
+ s.require_paths = ["lib"]
27
+ end
@@ -0,0 +1,44 @@
1
+ require "digital_ocean"
2
+ require "cloud_runner/digital_ocean/base"
3
+
4
+ module CloudRunner::DigitalOcean
5
+ class Api
6
+ def initialize(client_id, api_key)
7
+ @client_id = client_id
8
+ @api_key = api_key
9
+ end
10
+
11
+ def self.find(object_type, field)
12
+ define_method("all_#{object_type}s") do
13
+ method = "#{object_type}s"
14
+ api.send(method).list.send(method)
15
+ end
16
+
17
+ define_method("find_#{object_type}_by_#{field}") do |value|
18
+ send("all_#{object_type}s").detect do |object|
19
+ object.send(field) == value
20
+ end
21
+ end
22
+ end
23
+
24
+ find :region, :name
25
+ find :size, :name
26
+ find :image, :name
27
+
28
+ def self.delegate(method_name)
29
+ define_method(method_name) { api.send(method_name) }
30
+ end
31
+
32
+ delegate :ssh_keys
33
+ delegate :droplets
34
+
35
+ private
36
+
37
+ def api
38
+ @api ||= ::DigitalOcean::API.new(
39
+ :client_id => @client_id,
40
+ :api_key => @api_key,
41
+ )
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,3 @@
1
+ module CloudRunner
2
+ module DigitalOcean; end
3
+ end
@@ -0,0 +1,79 @@
1
+ require "cloud_runner/ssh_key"
2
+ require "cloud_runner/one_line_logger"
3
+ require "cloud_runner/digital_ocean/base"
4
+ require "cloud_runner/digital_ocean/api"
5
+ require "cloud_runner/digital_ocean/run"
6
+
7
+ module CloudRunner::DigitalOcean
8
+ module Cli
9
+ class Base
10
+ attr_reader :options
11
+
12
+ def initialize(opts={})
13
+ @options = opts.clone.freeze
14
+
15
+ raise "Client id must be specified" \
16
+ unless @client_id = options[:client_id]
17
+
18
+ raise "Api key must be specified" \
19
+ unless @api_key = options[:api_key]
20
+
21
+ raise "Script must be specified" \
22
+ unless @script_path = options[:script]
23
+
24
+ @script_path = File.realpath(@script_path)
25
+ end
26
+
27
+ def run_script(out, err)
28
+ @out, @err = out, err
29
+
30
+ set_up
31
+ execute_script
32
+ ensure
33
+ clean_up
34
+ end
35
+
36
+ protected
37
+
38
+ def set_up
39
+ raise NotImplementedError
40
+ end
41
+
42
+ def execute_script
43
+ step("Executing script '#{@script_path}'") do
44
+ run.run_script(@script_path, @out, @err, {
45
+ :ssh_logger => OneLineLogger.new(@err),
46
+ })
47
+ end
48
+ end
49
+
50
+ def clean_up
51
+ raise NotImplementedError
52
+ end
53
+
54
+ def run
55
+ @ci_run ||= Run.new(api)
56
+ end
57
+
58
+ def ssh_key
59
+ raise NotImplementedError
60
+ end
61
+
62
+ def step(description, &action)
63
+ @out.puts("-----> #{description}...")
64
+
65
+ action.call.tap do |result|
66
+ @out.puts(result)
67
+ end if action
68
+ end
69
+
70
+ private
71
+
72
+ def api
73
+ @api ||= Api.new(@client_id, @api_key)
74
+ end
75
+ end
76
+
77
+ #~
78
+ end
79
+ end
@@ -0,0 +1,49 @@
1
+ require "cloud_runner/ssh_key"
2
+ require "cloud_runner/digital_ocean/cli/base"
3
+
4
+ module CloudRunner::DigitalOcean::Cli
5
+ class New < Base
6
+ SHORT_HOST_IMAGE_NAMES = {
7
+ "ubuntu-10-04" => "Ubuntu 10.04 x64 Server",
8
+ "ubuntu-12-04" => "Ubuntu 12.04 x64 Server",
9
+ "centos-6-3" => "CentOS 6.3 x64",
10
+ }.freeze
11
+
12
+ def initialize(opts={})
13
+ super
14
+
15
+ raise "Host image must be specified" \
16
+ unless @host_image = options[:host_image]
17
+
18
+ raise "Host image is not available" \
19
+ unless @image_name = SHORT_HOST_IMAGE_NAMES[@host_image]
20
+ end
21
+
22
+ protected
23
+
24
+ def set_up
25
+ step("Creating ssh key '#{ssh_key.private_path}'") do
26
+ run.create_ssh_key(ssh_key)
27
+ end
28
+
29
+ step("Creating droplet '#{run.name}'") do
30
+ run.create_droplet(:image_name => @image_name)
31
+ end
32
+ end
33
+
34
+ # Since we created ssh key/droplet
35
+ # assume that's it is safe to delete.
36
+ def clean_up
37
+ if options[:keep_droplet]
38
+ step("Skipping deletion of droplet and ssh key")
39
+ else
40
+ step("Deleting droplet") { run.delete_droplet }
41
+ step("Deleting ssh key") { run.delete_ssh_key }
42
+ end
43
+ end
44
+
45
+ def ssh_key
46
+ @ssh_key ||= CloudRunner::SshKey.new
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,39 @@
1
+ require "cloud_runner/ssh_key"
2
+ require "cloud_runner/digital_ocean/cli/base"
3
+
4
+ module CloudRunner::DigitalOcean::Cli
5
+ class Over < Base
6
+ def initialize(opts={})
7
+ super
8
+
9
+ raise "Droplet id must be specified" \
10
+ unless @droplet_id = options[:droplet_id]
11
+
12
+ raise "Ssh key must be specified" \
13
+ unless @ssh_key_path = options[:ssh_key]
14
+
15
+ @ssh_key_path = File.realpath(@ssh_key_path)
16
+ end
17
+
18
+ protected
19
+
20
+ def set_up
21
+ step("Using ssh key '#{ssh_key.private_path}'") do
22
+ run.use_ssh_key(ssh_key)
23
+ end
24
+
25
+ step("Finding droplet '#{@droplet_id}'") do
26
+ run.find_dropley_by_id(@droplet_id)
27
+ end
28
+ end
29
+
30
+ # Nothing to clean up since we did not
31
+ # create a new ssh/droplet.
32
+ def clean_up
33
+ end
34
+
35
+ def ssh_key
36
+ @ssh_key ||= CloudRunner::SshKey.new("rsa", @ssh_key_path)
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,105 @@
1
+ require "securerandom"
2
+ require "cloud_runner/ssh"
3
+ require "cloud_runner/digital_ocean/base"
4
+
5
+ module CloudRunner::DigitalOcean
6
+ class Run
7
+ DROPLET_DEFAULT_NAMES = {
8
+ :region => "New York 1",
9
+ :image => "Ubuntu 12.04 x64 Server",
10
+ :size => "512MB",
11
+ }.freeze
12
+
13
+ def initialize(api)
14
+ @api = api
15
+ end
16
+
17
+ def name
18
+ @name ||= "run-#{SecureRandom.hex}"
19
+ end
20
+
21
+ def create_ssh_key(ssh_key)
22
+ use_ssh_key(ssh_key)
23
+
24
+ @public_ssh_key ||= @api.ssh_keys.add(
25
+ :name => "#{name}-ssh-key",
26
+ :ssh_pub_key => ssh_key.public,
27
+ ).ssh_key
28
+ end
29
+
30
+ def use_ssh_key(ssh_key)
31
+ raise "Ssh key must specified" unless @ssh_key = ssh_key
32
+ end
33
+
34
+ def delete_ssh_key
35
+ @api.ssh_keys.delete(@public_ssh_key.id) if @public_ssh_key
36
+ end
37
+
38
+ def create_droplet(opts={})
39
+ raise "Ssh key must be created" unless @public_ssh_key
40
+
41
+ attrs = extract_droplet_attrs(opts)
42
+
43
+ @droplet ||= @api.droplets.create(
44
+ :name => "#{name}-droplet",
45
+ :region_id => attrs[:region].id,
46
+ :image_id => attrs[:image].id,
47
+ :size_id => attrs[:size].id,
48
+ :ssh_key_ids => "#{@public_ssh_key.id}",
49
+ ).droplet
50
+
51
+ wait_for_droplet_to_be_alive
52
+ end
53
+
54
+ def find_droplet_by_id(droplet_id)
55
+ raise "Droplet id must be specified" unless droplet_id
56
+
57
+ @droplet ||= @api.droplets.show(droplet_id).droplet
58
+
59
+ wait_for_droplet_to_be_alive
60
+ end
61
+
62
+ def delete_droplet
63
+ @api.droplets.delete(@droplet.id) if @droplet
64
+ end
65
+
66
+ def run_script(local_path, out, err, opts={})
67
+ raise "Droplet must be created" unless @droplet
68
+ raise "Local path must be specified" unless local_path
69
+
70
+ ssh = CloudRunner::Ssh.new(@droplet.ip_address, "root", @ssh_key)
71
+ ssh.run_script(local_path, out, err, opts)
72
+ end
73
+
74
+ private
75
+
76
+ def extract_droplet_attrs(opts)
77
+ {}.tap do |attrs|
78
+ DROPLET_DEFAULT_NAMES.each do |field, name|
79
+ find_method_name = "find_#{field}_by_name"
80
+ find_name = opts[:"#{field}_name"] || name
81
+
82
+ record = @api.send(find_method_name, find_name)
83
+ raise "#{field} was not found" \
84
+ unless attrs[field] = record
85
+ end
86
+ end
87
+ end
88
+
89
+ def wait_for_droplet_to_be_alive
90
+ wait_for_droplet { |d| d.ip_address && d.status == "active" }
91
+ end
92
+
93
+ def wait_for_droplet(&blk)
94
+ raise "Droplet must be created" unless @droplet
95
+
96
+ Timeout.timeout(180) do
97
+ while !blk.call(@droplet) && sleep(5)
98
+ @droplet = @api.droplets.show(@droplet.id).droplet
99
+ end
100
+ end
101
+
102
+ @droplet
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,18 @@
1
+ require "logger"
2
+
3
+ class OneLineLogger < Logger
4
+ def initialize(*args)
5
+ super
6
+ customize_formatter
7
+ end
8
+
9
+ def customize_formatter
10
+ last_line_len = 0
11
+ self.formatter = proc do |severity, datetime, progname, msg|
12
+ line = msg.split("\n").last
13
+ "\r#{" " * last_line_len}\r#{line}".tap do
14
+ last_line_len = line.size
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,90 @@
1
+ require "securerandom"
2
+ require "stringio"
3
+ require "net/ssh"
4
+ require "net/scp"
5
+
6
+ module CloudRunner
7
+ class Ssh
8
+ DEFAULT_OPTIONS = {
9
+ :auth_methods => ["publickey"],
10
+ :paranoid => false,
11
+ }.freeze
12
+
13
+ def initialize(host, user, ssh_key)
14
+ raise "Host must be specified" unless @host = host
15
+ raise "User must be specified" unless @user = user
16
+ raise "Key must be specified" unless @ssh_key = ssh_key
17
+ end
18
+
19
+ def run_script(local_path, out, err, opts={})
20
+ ssh_opts = DEFAULT_OPTIONS.clone.merge(
21
+ :keys => [@ssh_key.private_path],
22
+ :host_key => "ssh-#{@ssh_key.type}",
23
+ :logger => opts[:ssh_logger] || StringIO.new,
24
+ :verbose => :debug,
25
+ )
26
+
27
+ # Assume the worst
28
+ @exit_code = 1
29
+
30
+ Net::SSH.start(@host, @user, ssh_opts) do |ssh|
31
+ ssh.scp.upload!(local_path, remote_path)
32
+ ssh.exec!("chmod +x #{remote_path}")
33
+ full_exec(ssh, remote_path, out, err)
34
+ end
35
+
36
+ @exit_code
37
+ end
38
+
39
+ private
40
+
41
+ def remote_path
42
+ @remote_path ||= "/tmp/run-#{SecureRandom.hex}.sh"
43
+ end
44
+
45
+ def full_exec(ssh, command, out, err)
46
+ channel_exec(ssh, command) do |ch|
47
+ stream_stdout(ch, out)
48
+ stream_stderr(ch, err)
49
+ handle_exit(ch)
50
+ handle_signal(ch)
51
+ end
52
+ end
53
+
54
+ def channel_exec(ssh, command, &blk)
55
+ ssh.open_channel do |channel|
56
+ channel.exec(command) do |ch, success|
57
+ return @exit_code = 1 unless success
58
+ blk.call(ch)
59
+ end
60
+ end
61
+ ssh.loop
62
+ end
63
+
64
+ def stream_stdout(channel, stream)
65
+ channel.on_data do |ch, data|
66
+ stream.print(data)
67
+ end
68
+ end
69
+
70
+ def stream_stderr(channel, stream)
71
+ channel.on_extended_data do |ch, type, data|
72
+ stream.print(data)
73
+ end
74
+ end
75
+
76
+ def handle_exit(channel)
77
+ channel.on_request("exit-status") do |ch, data|
78
+ @exit_code = data.read_long
79
+ end
80
+ end
81
+
82
+ def handle_signal(channel)
83
+ channel.on_request("exit-signal") do |ch, data|
84
+ raise "Received signal: #{data.read_long}"
85
+ end
86
+ end
87
+
88
+ #~
89
+ end
90
+ end
@@ -0,0 +1,35 @@
1
+ require "securerandom"
2
+
3
+ module CloudRunner
4
+ class SshKey
5
+ attr_reader :type
6
+
7
+ def initialize(type="rsa", private_path=nil)
8
+ raise "Type must be specified" unless @type = type
9
+ @private_path = private_path
10
+ end
11
+
12
+ def public
13
+ File.read("#{private_path}.pub")
14
+ end
15
+
16
+ def private
17
+ File.read(private_path)
18
+ end
19
+
20
+ def private_path
21
+ unless @private_path
22
+ @private_path = "/tmp/ssh-key-#{SecureRandom.hex}"
23
+ generate
24
+ end
25
+ @private_path
26
+ end
27
+
28
+ private
29
+
30
+ def generate
31
+ `ssh-keygen -t '#{@type}' -f '#{@private_path}' -N ''`
32
+ raise "Failed to generate ssh key" unless $? == 0
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,3 @@
1
+ module CloudRunner
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,3 @@
1
+ module CloudRunner
2
+ # Your code goes here...
3
+ end
@@ -0,0 +1,33 @@
1
+ require "spec_helper"
2
+ require "stringio"
3
+ require "cloud_runner/digital_ocean/cli/new"
4
+
5
+ describe "cr-new" do
6
+ let(:client_id) { ENV["SPEC_CR_CLIENT_ID"] || raise }
7
+ let(:api_key) { ENV["SPEC_CR_API_KEY"] || raise }
8
+
9
+ def self.it_runs_script(script, expected_exit_code)
10
+ describe "for '#{script}' script" do
11
+ let(:out) { StringIO.new }
12
+ let(:err) { StringIO.new }
13
+
14
+ it "runs script and returns exit code" do
15
+ CloudRunner::DigitalOcean::Cli::New.new(
16
+ :client_id => client_id,
17
+ :api_key => api_key,
18
+ :script => "./spec/fixtures/#{script}.sh",
19
+ :host_image => "ubuntu-10-04",
20
+ ).run_script(
21
+ Multiplexer.new(out, $stdout),
22
+ Multiplexer.new(err, $stderr),
23
+ ).should == expected_exit_code
24
+
25
+ out.string.should include("Echo to stdout\n")
26
+ err.string.should include("Echo to stderr\n")
27
+ end
28
+ end
29
+ end
30
+
31
+ it_runs_script "success", 0
32
+ it_runs_script "fail", 128
33
+ end
@@ -0,0 +1,6 @@
1
+ #!/bin/bash
2
+
3
+ echo "Echo to stdout" > /dev/stdout
4
+ echo "Echo to stderr" > /dev/stderr
5
+
6
+ exit 128
@@ -0,0 +1,6 @@
1
+ #!/bin/bash
2
+
3
+ echo "Echo to stdout" > /dev/stdout
4
+ echo "Echo to stderr" > /dev/stderr
5
+
6
+ exit 0
@@ -0,0 +1,6 @@
1
+ require "bundler"
2
+ Bundler.setup
3
+
4
+ $:.unshift(File.expand_path("../lib", __FILE__))
5
+
6
+ Dir[File.expand_path("../support/**/*.rb", __FILE__)].each { |f| require(f) }
@@ -0,0 +1,9 @@
1
+ class Multiplexer < BasicObject
2
+ def initialize(*objs)
3
+ @objs = objs
4
+ end
5
+
6
+ def method_missing(*args, &blk)
7
+ @objs.map { |o| o.send(*args, &blk) }.first
8
+ end
9
+ end
metadata ADDED
@@ -0,0 +1,143 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: cloud_runner
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Dmitriy Kalinin
9
+ - Alex Suraci
10
+ autorequire:
11
+ bindir: bin
12
+ cert_chain: []
13
+ date: 2013-03-19 00:00:00.000000000 Z
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: digital_ocean
17
+ requirement: !ruby/object:Gem::Requirement
18
+ none: false
19
+ requirements:
20
+ - - ! '>='
21
+ - !ruby/object:Gem::Version
22
+ version: '0'
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ none: false
27
+ requirements:
28
+ - - ! '>='
29
+ - !ruby/object:Gem::Version
30
+ version: '0'
31
+ - !ruby/object:Gem::Dependency
32
+ name: net-ssh
33
+ requirement: !ruby/object:Gem::Requirement
34
+ none: false
35
+ requirements:
36
+ - - ! '>='
37
+ - !ruby/object:Gem::Version
38
+ version: '0'
39
+ type: :runtime
40
+ prerelease: false
41
+ version_requirements: !ruby/object:Gem::Requirement
42
+ none: false
43
+ requirements:
44
+ - - ! '>='
45
+ - !ruby/object:Gem::Version
46
+ version: '0'
47
+ - !ruby/object:Gem::Dependency
48
+ name: net-scp
49
+ requirement: !ruby/object:Gem::Requirement
50
+ none: false
51
+ requirements:
52
+ - - ! '>='
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ type: :runtime
56
+ prerelease: false
57
+ version_requirements: !ruby/object:Gem::Requirement
58
+ none: false
59
+ requirements:
60
+ - - ! '>='
61
+ - !ruby/object:Gem::Version
62
+ version: '0'
63
+ - !ruby/object:Gem::Dependency
64
+ name: rspec
65
+ requirement: !ruby/object:Gem::Requirement
66
+ none: false
67
+ requirements:
68
+ - - ! '>='
69
+ - !ruby/object:Gem::Version
70
+ version: '0'
71
+ type: :development
72
+ prerelease: false
73
+ version_requirements: !ruby/object:Gem::Requirement
74
+ none: false
75
+ requirements:
76
+ - - ! '>='
77
+ - !ruby/object:Gem::Version
78
+ version: '0'
79
+ description: Currently only supports DigitalOcean.
80
+ email:
81
+ - cppforlife@gmail.com
82
+ executables:
83
+ - cr-new
84
+ - cr-over
85
+ extensions: []
86
+ extra_rdoc_files: []
87
+ files:
88
+ - .gitignore
89
+ - .rspec
90
+ - .rvmrc
91
+ - Gemfile
92
+ - Gemfile.lock
93
+ - README.md
94
+ - Rakefile
95
+ - bin/cr-new
96
+ - bin/cr-over
97
+ - cloud_runner.gemspec
98
+ - lib/cloud_runner.rb
99
+ - lib/cloud_runner/digital_ocean/api.rb
100
+ - lib/cloud_runner/digital_ocean/base.rb
101
+ - lib/cloud_runner/digital_ocean/cli/base.rb
102
+ - lib/cloud_runner/digital_ocean/cli/new.rb
103
+ - lib/cloud_runner/digital_ocean/cli/over.rb
104
+ - lib/cloud_runner/digital_ocean/run.rb
105
+ - lib/cloud_runner/one_line_logger.rb
106
+ - lib/cloud_runner/ssh.rb
107
+ - lib/cloud_runner/ssh_key.rb
108
+ - lib/cloud_runner/version.rb
109
+ - spec/cr_new_spec.rb
110
+ - spec/fixtures/fail.sh
111
+ - spec/fixtures/success.sh
112
+ - spec/spec_helper.rb
113
+ - spec/support/multiplexer.rb
114
+ homepage: http://github.com/cppforlife/cloud_runner
115
+ licenses: []
116
+ post_install_message:
117
+ rdoc_options: []
118
+ require_paths:
119
+ - lib
120
+ required_ruby_version: !ruby/object:Gem::Requirement
121
+ none: false
122
+ requirements:
123
+ - - ! '>='
124
+ - !ruby/object:Gem::Version
125
+ version: '0'
126
+ required_rubygems_version: !ruby/object:Gem::Requirement
127
+ none: false
128
+ requirements:
129
+ - - ! '>='
130
+ - !ruby/object:Gem::Version
131
+ version: '0'
132
+ requirements: []
133
+ rubyforge_project: cloud_runner
134
+ rubygems_version: 1.8.24
135
+ signing_key:
136
+ specification_version: 3
137
+ summary: Quickly spin up new VM in the cloud to run a script.
138
+ test_files:
139
+ - spec/cr_new_spec.rb
140
+ - spec/fixtures/fail.sh
141
+ - spec/fixtures/success.sh
142
+ - spec/spec_helper.rb
143
+ - spec/support/multiplexer.rb