remote_run 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +4 -0
- data/.rvmrc +3 -0
- data/Gemfile +7 -0
- data/LICENSE +19 -0
- data/Rakefile +2 -0
- data/Readme.md +79 -0
- data/examples/demo-remote-run +22 -0
- data/lib/remote_run/host.rb +130 -0
- data/lib/remote_run/runner.rb +233 -0
- data/lib/remote_run/version.rb +3 -0
- data/lib/remote_run.rb +3 -0
- data/remote_run.gemspec +23 -0
- data/spec/remote_run/host_spec.rb +108 -0
- data/spec/spec_helper.rb +15 -0
- metadata +89 -0
data/.gitignore
ADDED
data/.rvmrc
ADDED
data/Gemfile
ADDED
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
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
|
data/lib/remote_run.rb
ADDED
data/remote_run.gemspec
ADDED
@@ -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
|
+
|
data/spec/spec_helper.rb
ADDED
@@ -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
|