remote_run 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,4 @@
1
+ *.gem
2
+ .bundle
3
+ Gemfile.lock
4
+ pkg/*
data/.rvmrc ADDED
@@ -0,0 +1,3 @@
1
+ export rvm_path="/Users/pivotal/.rvm"
2
+ rvm_gemset_create_on_use_flag=1
3
+ rvm_install_on_use_flag=1
data/Gemfile ADDED
@@ -0,0 +1,7 @@
1
+ source "http://rubygems.org"
2
+
3
+ gem "highline"
4
+ gem "rspec"
5
+ gem "rake"
6
+
7
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,19 @@
1
+ Copyright (c) 2011 Case Commons, LLC
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ of this software and associated documentation files (the "Software"), to deal
5
+ in the Software without restriction, including without limitation the rights
6
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ copies of the Software, and to permit persons to whom the Software is
8
+ furnished to do so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in
11
+ all copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
+ THE SOFTWARE.
data/Rakefile ADDED
@@ -0,0 +1,2 @@
1
+ require 'bundler'
2
+ Bundler::GemHelper.install_tasks
data/Readme.md ADDED
@@ -0,0 +1,79 @@
1
+ # Remote Run.
2
+
3
+ Our development team wanted a way to distribute our test suite to many machines
4
+ to reduce the total run time. Remote Run is intended to be a simple way to
5
+ run a list of shell scripts on a pool of hosts until all have completed.
6
+
7
+ When two Remote Runs are in progress, the runners will compete to lock
8
+ machines until all tasks are complete.
9
+
10
+
11
+ ## Example:
12
+
13
+ require 'rubygems'
14
+ require 'remote_run'
15
+
16
+ hosts = ["broadway", "wall"]
17
+ setup = "source ~/.profile; rvm use ree; bundle install;"
18
+ tasks = [
19
+ "#{setup} bundle exec rspec spec/models",
20
+ "#{setup} bundle exec rspec spec/controllers"
21
+ ]
22
+
23
+ # configure runner
24
+ runner = Runner.new do |config|
25
+ config.hosts = hosts
26
+ config.tasks = tasks
27
+ end
28
+
29
+ # kick off the run
30
+ runner.run
31
+
32
+
33
+ ## Configuration Options:
34
+
35
+ Required:
36
+ hosts - hostnames of remote machines.
37
+ tasks - a string that is a shell script to be run on one of the hosts.
38
+
39
+ Optional:
40
+ local_path - the local path to be rsync'd (default: working directory)
41
+ temp_path - the location where the working directory is cached on the local machine when starting a run (default: /tmp/remote)
42
+ remote_path - the location to rsync files to on the remote host. (default: /tmp/remote)
43
+ exclude - directories to exclude when rsyncing to remote host (default: [])
44
+ login_as - the user used to log into ssh (default: current user)
45
+
46
+
47
+ ## Accessible Attributes:
48
+
49
+ local_hostname - your computer's hostname
50
+ identifier - a unique identifier for your test run
51
+
52
+
53
+ ## What it Does:
54
+
55
+ * checks that all hosts can be logged into via ssh
56
+ * runs each task on a remote host in parallel
57
+ * finds an unlocked remote host
58
+ * locks a remote host (puts a file on the remote host)
59
+ * finds a task to run
60
+ * forks a separate process and gives it the selected task
61
+ - rsyncs your current directory to the locked remote host
62
+ - via ssh, runs a shell command of your choice on the locked remote host
63
+ - unlocks the machine (removes the file from the remote host)
64
+ - returns the status code of the shell script
65
+ * finds the next machine to be locked
66
+ * waits for all forks to return
67
+ * displays success message if all status codes from the forks are zero (0)
68
+ * displays failure message if any status codes from the forks are non-zero
69
+
70
+
71
+ Dependencies:
72
+ ----------------------------------------------------------------------
73
+ * HighLine
74
+
75
+
76
+ License:
77
+ ----------------------------------------------------------------------
78
+
79
+ MIT
@@ -0,0 +1,22 @@
1
+ #!/usr/bin/env ruby
2
+ require 'rubygems'
3
+ require 'bundler/setup'
4
+ require 'remote_run'
5
+
6
+ Runner.log("Scanning 10.64.20.* for hosts...", :yellow)
7
+ #hosts = `nmap -p 22 10.64.20.1/24 --open -sV | grep -B3 "Debian" | grep 10.64.20 | cut -f 5 -d " "`.lines.map(&:strip)
8
+ hosts = %w{ 10.64.20.1 10.64.20.12 10.64.20.15 10.64.20.18 10.64.20.21 10.64.20.26 10.64.20.30 10.64.20.38 10.64.20.43 10.64.20.47 10.64.20.49 10.64.20.50 10.64.20.51 10.64.20.53 10.64.20.56 10.64.20.58 10.64.20.59 10.64.20.61 10.64.20.62 10.64.20.64 10.64.20.66 10.64.20.69 10.64.20.74 10.64.20.76 10.64.20.79 10.64.20.80 10.64.20.93 10.64.20.101 10.64.20.110 10.64.20.111 10.64.20.122 10.64.20.149 10.64.20.164 10.64.20.168 10.64.20.198 10.64.20.209 10.64.20.214 10.64.20.221 10.64.20.244 10.64.20.245 10.64.20.246 10.64.20.247 10.64.20.248 10.64.20.249 10.64.20.250 10.64.20.251 10.64.20.252 10.64.20.253 10.64.20.254 }
9
+ Runner.log("With all tasks passing...")
10
+ tasks = []
11
+ 50.times do |n|
12
+ tasks << "sleep 2; date;"
13
+ end
14
+ tasks << "cat /foo/bar"
15
+
16
+ Runner.run do |config|
17
+ config.tasks = tasks
18
+ config.login_as = "pivotalcb"
19
+ config.hosts = hosts
20
+ end
21
+
22
+ Runner.log("Expected one failure to test that it still works...")
@@ -0,0 +1,130 @@
1
+ class Host
2
+ FAIL = 1
3
+ PASS = 0
4
+ SSH_CONFIG = " -o ControlMaster=auto -o ControlPath=~/.ssh/master-%l-%r@%h:%p -o NumberOfPasswordPrompts=0 -o StrictHostKeyChecking=no -4 "
5
+ attr_reader :hostname
6
+ attr_reader :lock_file
7
+
8
+ def initialize(hostname)
9
+ @hostname = hostname
10
+ @lock_file = LockFile.new(@hostname, $runner.local_hostname, $runner.identifier)
11
+ end
12
+
13
+ def lock
14
+ unless locked?
15
+ @lock_file.get && locked_by_me?
16
+ end
17
+ end
18
+
19
+ def unlock
20
+ @lock_file.release
21
+ end
22
+
23
+ def run(task)
24
+ Runner.log("Running '#{task}' on #{@hostname}", :white)
25
+ command = %Q{ssh #{SSH_CONFIG} #{ssh_host_and_user} 'cd #{$runner.remote_path}; #{task}' 2>&1}
26
+ system(command)
27
+ $?.exitstatus
28
+ end
29
+
30
+ def copy_codebase
31
+ Runner.log("Copying from #{$runner.temp_path} to #{@hostname}:#{$runner.remote_path}", :yellow)
32
+ system("ssh #{SSH_CONFIG} #{ssh_host_and_user} 'mkdir -p #{$runner.remote_path}'")
33
+ excludes = $runner.exclude.map { |dir| "--exclude '#{dir}'"}
34
+ if system(%{rsync --delete --delete-excluded #{excludes.join(" ")} --rsh='ssh #{SSH_CONFIG}' --timeout=60 -a #{$runner.temp_path}/ #{ssh_host_and_user}:#{$runner.remote_path}/})
35
+ Runner.log("Finished copying to #{@hostname}", :green)
36
+ return true
37
+ else
38
+ Runner.log("rsync failed on #{@hostname}.", :red)
39
+ return false
40
+ end
41
+ end
42
+
43
+ def is_up?
44
+ result = `ssh #{SSH_CONFIG} -o ConnectTimeout=2 #{ssh_host_and_user} "echo 'success'" 2>/dev/null`.strip
45
+ if result == "success"
46
+ Runner.log("#{@hostname} is up", :green)
47
+ return true
48
+ else
49
+ Runner.log("#{@hostname} is down: #{result}", :red)
50
+ return false
51
+ end
52
+ end
53
+
54
+ def start_ssh_master_connection
55
+ system("ssh #{SSH_CONFIG} #{ssh_host_and_user} -M &> /dev/null")
56
+ end
57
+
58
+ private
59
+
60
+ def ssh_host_and_user
61
+ "#{$runner.login_as}@#{@hostname}"
62
+ end
63
+
64
+ def locked?
65
+ @lock_file.locked?
66
+ end
67
+
68
+ def locked_by_me?
69
+ @lock_file.locked_by_me?
70
+ end
71
+
72
+ class LockFile
73
+ FILE = "/tmp/remote-run-lock"
74
+
75
+ def initialize(remote_hostname, local_hostname, unique_run_marker)
76
+ @filename = FILE
77
+ @locker = "#{local_hostname}-#{unique_run_marker}"
78
+ @remote_file = RemoteFile.new(remote_hostname)
79
+ end
80
+
81
+ def release
82
+ if locked_by_me?
83
+ @remote_file.delete(@filename)
84
+ end
85
+ end
86
+
87
+ def locked?
88
+ @remote_file.exist?(@filename)
89
+ end
90
+
91
+ def locked_by_me?
92
+ @remote_file.exist?(@filename) && @remote_file.read(@filename).strip == @locker
93
+ end
94
+
95
+ def get
96
+ @remote_file.write(@filename, @locker)
97
+ end
98
+
99
+ class RemoteFile
100
+ def initialize(hostname)
101
+ @hostname = hostname
102
+ end
103
+
104
+ def exist?(file_path)
105
+ run_and_test("test -f #{file_path}")
106
+ end
107
+
108
+ def read(file_path)
109
+ run("test -e #{file_path} && cat #{file_path}")
110
+ end
111
+
112
+ def write(file_path, text)
113
+ run_and_test("test -e #{file_path} || echo #{text} > #{file_path}")
114
+ end
115
+
116
+ def delete(file_path)
117
+ run_and_test("rm -f #{file_path}")
118
+ end
119
+
120
+ def run(command)
121
+ `ssh #{Host::SSH_CONFIG} #{$runner.login_as}@#{@hostname} '#{command};'`.strip
122
+ end
123
+
124
+ def run_and_test(command)
125
+ system("ssh #{Host::SSH_CONFIG} #{$runner.login_as}@#{@hostname} '#{command}' 2>/dev/null")
126
+ end
127
+ end
128
+ end
129
+ end
130
+
@@ -0,0 +1,233 @@
1
+ class Runner
2
+ attr_accessor :remote_path, :local_path, :login_as, :exclude, :temp_path
3
+ attr_reader :local_hostname, :identifier
4
+ @@start_time = Time.now
5
+ @@stty_config = `stty -g`
6
+
7
+ def initialize
8
+ @task_manager = TaskManager.new
9
+ @host_manager = HostManager.new
10
+
11
+ # config options
12
+ @local_path = Dir.getwd
13
+ @login_as = `whoami`.strip
14
+ @remote_path = "/tmp/remote"
15
+ @exclude = []
16
+ @temp_path = "/tmp/remote"
17
+
18
+ # used in the runner
19
+ @identifier = `echo $RANDOM`.strip
20
+ @local_hostname = `hostname`.strip
21
+ @results = []
22
+ @children = []
23
+ @failed = []
24
+ @last_timestamp = Time.now.strftime("%S")[0]
25
+
26
+ $runner = self
27
+ yield self
28
+ end
29
+
30
+ def self.run(&block)
31
+ @@start_time = Time.now
32
+ runner = new(&block)
33
+ runner.run
34
+ end
35
+
36
+ def self.run_time
37
+ minutes = ((Time.now - @@start_time) / 60).to_i
38
+ seconds = ((Time.now - @@start_time) % 60).to_i
39
+ "#{minutes}:#{"%02d" % seconds}"
40
+ end
41
+
42
+ def self.log(message, color = :yellow)
43
+ highline = HighLine.new
44
+ system("stty #{@@stty_config} 2>/dev/null")
45
+ highline.say(highline.color("[Remote :: #{$runner.identifier} :: #{run_time}] #{message}", color))
46
+ end
47
+
48
+ def hosts
49
+ @host_manager.all
50
+ end
51
+
52
+ def hosts=(hostnames)
53
+ hostnames.each do |hostname|
54
+ @host_manager.add(hostname)
55
+ end
56
+ end
57
+
58
+ def tasks=(shell_commands)
59
+ shell_commands.each do |shell_command|
60
+ @task_manager.add(shell_command)
61
+ end
62
+ end
63
+
64
+ def run
65
+ @host_manager.unlock_on_exit
66
+ @host_manager.start_ssh_master_connections
67
+ sync_working_copy_to_temp_location
68
+ hosts = []
69
+
70
+ Runner.log("Starting tasks... #{Time.now}")
71
+
72
+ @starting_number_of_tasks = @task_manager.count
73
+ while @task_manager.has_more_tasks?
74
+ hosts = @host_manager.hosts.dup if hosts.empty?
75
+
76
+ display_log
77
+ check_for_finished
78
+
79
+ if host = hosts.sample
80
+ hosts.delete(host)
81
+ if host.lock
82
+ task = @task_manager.find_task
83
+ @children << fork do
84
+ begin
85
+ this_host = host.dup
86
+ unless this_host.copy_codebase
87
+ @task_manager.add(task)
88
+ status = 0
89
+ end
90
+ status = this_host.run(task)
91
+ host.unlock
92
+ Runner.log("#{host.hostname} failed.", :red) if status != 0
93
+ rescue Errno::EPIPE
94
+ Runner.log("broken pipe on #{host.hostname}...")
95
+ ensure
96
+ Process.exit!(status)
97
+ end
98
+ end
99
+ end
100
+ end
101
+ end
102
+
103
+ Runner.log("All tasks started... #{Time.now}")
104
+
105
+ while @children.length > 0
106
+ display_log
107
+ check_for_finished
108
+ end
109
+
110
+ failed_tasks = @results.select { |result| result != 0 }
111
+ status_code = if failed_tasks.length == 0
112
+ Runner.log("Task passed.", :green)
113
+ Host::PASS
114
+ else
115
+ Runner.log("#{failed_tasks.length} task(s) failed.", :red)
116
+ Host::FAIL
117
+ end
118
+
119
+ Runner.log("Total Time: #{self.class.run_time} minutes.")
120
+ status_code
121
+ end
122
+
123
+ def check_for_finished
124
+ @children.each do |child_pid|
125
+ if Process.waitpid(child_pid, Process::WNOHANG)
126
+ if $?.exitstatus != 0
127
+ @failed << child_pid
128
+ end
129
+
130
+ @results << $?.exitstatus
131
+ @children.delete(child_pid)
132
+ end
133
+ end
134
+ sleep(0.5)
135
+ end
136
+
137
+ private
138
+
139
+ def sync_working_copy_to_temp_location
140
+ Runner.log("Creating temporary copy of #{@local_path} in #{@temp_path}...")
141
+ excludes = exclude.map { |dir| "--exclude '#{dir}'"}
142
+ system("rsync --delete --delete-excluded #{excludes.join(" ")} -aq #{@local_path}/ #{@temp_path}/")
143
+ Runner.log("Done.")
144
+ end
145
+
146
+ def display_log
147
+ now = Time.now.strftime("%S")[0]
148
+ unless now == @last_timestamp
149
+ display_status("Waiting on #{@task_manager.count} of #{@starting_number_of_tasks} tasks to start.") if @task_manager.count > 0
150
+ display_status("Waiting on #{@children.length} of #{@starting_number_of_tasks - @task_manager.count} started tasks to finish. #{@failed.size} failed.") if @children.length > 0
151
+ $stdout.print("\n\n")
152
+ $stdout.flush
153
+ @last_timestamp = now
154
+ end
155
+ end
156
+
157
+ def display_status(message)
158
+ Runner.log(message, :yellow)
159
+ end
160
+
161
+ class HostManager
162
+ def initialize(&block)
163
+ @hosts = []
164
+ end
165
+
166
+ def all
167
+ @hosts
168
+ end
169
+
170
+ def add(hostname)
171
+ host = Host.new(hostname)
172
+ Thread.new do
173
+ if host.is_up?
174
+ @hosts << host
175
+ end
176
+ end
177
+ end
178
+
179
+ def hosts
180
+ while @hosts.empty?
181
+ Runner.log("Waiting for hosts...")
182
+ sleep(0.5)
183
+ end
184
+
185
+ @hosts
186
+ end
187
+
188
+ def unlock_on_exit
189
+ at_exit do
190
+ all.each do |host|
191
+ begin
192
+ host.unlock
193
+ rescue Errno::EPIPE
194
+ end
195
+ end
196
+ end
197
+ end
198
+
199
+ def start_ssh_master_connections
200
+ all.each do |host|
201
+ fork do
202
+ host.start_ssh_master_connection
203
+ end
204
+ end
205
+ end
206
+ end
207
+
208
+ class TaskManager
209
+ def initialize
210
+ @tasks = []
211
+ end
212
+
213
+ def add(script)
214
+ @tasks.push(script)
215
+ end
216
+
217
+ def find_task
218
+ @tasks.shift
219
+ end
220
+
221
+ def all
222
+ @tasks
223
+ end
224
+
225
+ def count
226
+ @tasks.length
227
+ end
228
+
229
+ def has_more_tasks?
230
+ @tasks.size > 0
231
+ end
232
+ end
233
+ end
@@ -0,0 +1,3 @@
1
+ module RemoteRun
2
+ VERSION = "0.1.0"
3
+ end
data/lib/remote_run.rb ADDED
@@ -0,0 +1,3 @@
1
+ require File.join(File.dirname(__FILE__), "remote_run", "host")
2
+ require File.join(File.dirname(__FILE__), "remote_run", "runner")
3
+ require 'highline'
@@ -0,0 +1,23 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "remote_run/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "remote_run"
7
+ s.version = RemoteRun::VERSION
8
+ s.platform = Gem::Platform::RUBY
9
+ s.authors = ["Case Commons, LLC"]
10
+ s.email = ["casecommons-dev@googlegroups.com"]
11
+ s.homepage = "https://github.com/Casecommons/remote_run"
12
+ s.summary = %q{Run N shell scripts on a pool of remote hosts}
13
+ s.description = %q{Can be used as a parallel unit test runner}
14
+
15
+ s.rubyforge_project = "remote_run"
16
+
17
+ s.files = `git ls-files`.split("\n")
18
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
19
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
20
+ s.require_paths = ["lib"]
21
+ s.add_runtime_dependency("highline")
22
+ s.add_development_dependency('rspec')
23
+ end
@@ -0,0 +1,108 @@
1
+ require 'spec_helper'
2
+
3
+ describe Host do
4
+ context "when locking" do
5
+ let(:host) { Host.new("localhost") }
6
+
7
+ it "can be locked" do
8
+ host.lock.should be_true
9
+ end
10
+
11
+ it "cannot be locked twice" do
12
+ host.lock.should be_true
13
+ host.lock.should be_false
14
+ end
15
+
16
+ it "tells the lock file to get a lock" do
17
+ host.lock
18
+ `ssh localhost 'cat /tmp/remote-run-lock'`.strip.should == "#{$runner.local_hostname}-#{$runner.identifier}"
19
+ end
20
+ end
21
+
22
+ context "when locked by someone else" do
23
+ before { lock_file.get }
24
+ let(:host) { Host.new("localhost") }
25
+ let(:lock_file) {
26
+ lock_file = Host::LockFile.new("localhost", "myfakelocalhost", "999")
27
+ }
28
+
29
+ it "cannot be unlocked by me" do
30
+ host.unlock.should be_false
31
+ end
32
+ end
33
+
34
+ context "when locked by me" do
35
+ before { host.lock }
36
+ let(:host) { Host.new("localhost") }
37
+
38
+ it "cannot be locked" do
39
+ host.lock.should be_false
40
+ end
41
+
42
+ it "can be unlocked" do
43
+ host.unlock.should be_true
44
+ end
45
+
46
+ it "removes a file on the remote filesystem to unlock" do
47
+ `ssh localhost 'test -e /tmp/remote-run-lock'; echo $?;`.strip.should == "0"
48
+ host.unlock
49
+ `ssh localhost 'test -e /tmp/remote-run-lock'; echo $?;`.strip.should == "1"
50
+ end
51
+ end
52
+
53
+ context "when checking to see if a host is up" do
54
+ context "when using an authorized host" do
55
+ let(:host) { Host.new("localhost") }
56
+
57
+ it "returns true" do
58
+ host.is_up?.should be_true
59
+ end
60
+ end
61
+
62
+ context "when using an unauthorized host" do
63
+ let(:host) { Host.new("foozmcbarry") }
64
+
65
+ it "returns false" do
66
+ host.is_up?.should be_false
67
+ end
68
+ end
69
+ end
70
+
71
+ context "when running a task" do
72
+ before do
73
+ `ssh localhost 'rm -rf /tmp/testing-remote-run'`
74
+ host.lock
75
+ end
76
+
77
+ let(:host) { Host.new("localhost") }
78
+
79
+ context "when executing a shell command with a zero status code" do
80
+ it "returns zero" do
81
+ host.run("date > /dev/null").should == 0
82
+ end
83
+ end
84
+
85
+ context "when executing a shell command with a non-zero status code" do
86
+ it "returns non-zero status code" do
87
+ host.run("cat /foo/bar 2>/dev/null").should_not == 0
88
+ end
89
+ end
90
+ end
91
+
92
+ describe "#copy_codebase" do
93
+ before do
94
+ `ssh localhost 'rm -rf /tmp/testing-remote-run'`
95
+ host.lock
96
+ end
97
+
98
+ let(:host) { Host.new("localhost") }
99
+
100
+ it "copies the codebase to a remote directory" do
101
+ $runner.remote_path = "/tmp/testing-remote-run"
102
+ `ssh localhost 'test -e /tmp/testing-remote-run'; echo $?`.strip.should_not == "0"
103
+ host.copy_codebase
104
+ `ssh localhost 'test -e /tmp/testing-remote-run'; echo $?`.strip.should == "0"
105
+ end
106
+ end
107
+ end
108
+
@@ -0,0 +1,15 @@
1
+ require 'rubygems'
2
+ require 'bundler/setup'
3
+ require 'rspec'
4
+ require Dir.pwd + '/lib/remote_run'
5
+
6
+ Runner.new do |config|
7
+ config.tasks = []
8
+ config.hosts = []
9
+ end
10
+
11
+ RSpec.configure do |config|
12
+ config.after(:each) do
13
+ system("rm -f #{Host::LockFile::FILE}")
14
+ end
15
+ end
metadata ADDED
@@ -0,0 +1,89 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: remote_run
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Case Commons, LLC
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2011-08-11 00:00:00.000000000Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: highline
16
+ requirement: &2151825780 !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '0'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: *2151825780
25
+ - !ruby/object:Gem::Dependency
26
+ name: rspec
27
+ requirement: &2151824680 !ruby/object:Gem::Requirement
28
+ none: false
29
+ requirements:
30
+ - - ! '>='
31
+ - !ruby/object:Gem::Version
32
+ version: '0'
33
+ type: :development
34
+ prerelease: false
35
+ version_requirements: *2151824680
36
+ description: Can be used as a parallel unit test runner
37
+ email:
38
+ - casecommons-dev@googlegroups.com
39
+ executables: []
40
+ extensions: []
41
+ extra_rdoc_files: []
42
+ files:
43
+ - .gitignore
44
+ - .rvmrc
45
+ - Gemfile
46
+ - LICENSE
47
+ - Rakefile
48
+ - Readme.md
49
+ - examples/demo-remote-run
50
+ - lib/remote_run.rb
51
+ - lib/remote_run/host.rb
52
+ - lib/remote_run/runner.rb
53
+ - lib/remote_run/version.rb
54
+ - remote_run.gemspec
55
+ - spec/remote_run/host_spec.rb
56
+ - spec/spec_helper.rb
57
+ homepage: https://github.com/Casecommons/remote_run
58
+ licenses: []
59
+ post_install_message:
60
+ rdoc_options: []
61
+ require_paths:
62
+ - lib
63
+ required_ruby_version: !ruby/object:Gem::Requirement
64
+ none: false
65
+ requirements:
66
+ - - ! '>='
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ segments:
70
+ - 0
71
+ hash: -4543623679604844651
72
+ required_rubygems_version: !ruby/object:Gem::Requirement
73
+ none: false
74
+ requirements:
75
+ - - ! '>='
76
+ - !ruby/object:Gem::Version
77
+ version: '0'
78
+ segments:
79
+ - 0
80
+ hash: -4543623679604844651
81
+ requirements: []
82
+ rubyforge_project: remote_run
83
+ rubygems_version: 1.8.6
84
+ signing_key:
85
+ specification_version: 3
86
+ summary: Run N shell scripts on a pool of remote hosts
87
+ test_files:
88
+ - spec/remote_run/host_spec.rb
89
+ - spec/spec_helper.rb