remote_run 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/.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