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 +4 -0
- data/.rspec +3 -0
- data/.rvmrc +1 -0
- data/Gemfile +4 -0
- data/Gemfile.lock +44 -0
- data/README.md +33 -0
- data/Rakefile +2 -0
- data/bin/cr-new +48 -0
- data/bin/cr-over +47 -0
- data/cloud_runner.gemspec +27 -0
- data/lib/cloud_runner/digital_ocean/api.rb +44 -0
- data/lib/cloud_runner/digital_ocean/base.rb +3 -0
- data/lib/cloud_runner/digital_ocean/cli/base.rb +79 -0
- data/lib/cloud_runner/digital_ocean/cli/new.rb +49 -0
- data/lib/cloud_runner/digital_ocean/cli/over.rb +39 -0
- data/lib/cloud_runner/digital_ocean/run.rb +105 -0
- data/lib/cloud_runner/one_line_logger.rb +18 -0
- data/lib/cloud_runner/ssh.rb +90 -0
- data/lib/cloud_runner/ssh_key.rb +35 -0
- data/lib/cloud_runner/version.rb +3 -0
- data/lib/cloud_runner.rb +3 -0
- data/spec/cr_new_spec.rb +33 -0
- data/spec/fixtures/fail.sh +6 -0
- data/spec/fixtures/success.sh +6 -0
- data/spec/spec_helper.rb +6 -0
- data/spec/support/multiplexer.rb +9 -0
- metadata +143 -0
data/.gitignore
ADDED
data/.rspec
ADDED
data/.rvmrc
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
rvm use --create ruby-1.9.3-p327@cloud_runner
|
data/Gemfile
ADDED
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
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,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
|
data/lib/cloud_runner.rb
ADDED
data/spec/cr_new_spec.rb
ADDED
@@ -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
|
data/spec/spec_helper.rb
ADDED
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
|