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 +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
|