gofer 0.4.0 → 0.5.0
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG.md +7 -0
- data/Gemfile +1 -1
- data/README.md +7 -9
- data/lib/gofer.rb +2 -1
- data/lib/gofer/cluster.rb +54 -4
- data/lib/gofer/host.rb +36 -48
- data/lib/gofer/host_error.rb +17 -0
- data/lib/gofer/response.rb +13 -2
- data/lib/gofer/version.rb +1 -1
- data/spec/gofer/cluster_spec.rb +89 -0
- data/spec/gofer/{integration_spec.rb → host_spec.rb} +8 -89
- data/spec/spec_helper.rb +6 -0
- data/spec/support/integration_helpers.rb +57 -0
- data/test.sh +11 -0
- metadata +43 -32
- checksums.yaml +0 -15
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,12 @@
|
|
1
1
|
# Revision History
|
2
2
|
|
3
|
+
### v0.5.0
|
4
|
+
|
5
|
+
* Deprecate legacy arguments in Gofer::Host.new
|
6
|
+
* Remove superfluous `run_multiple`
|
7
|
+
* Better RDoc & tests for Gofer::Cluster
|
8
|
+
* `test.sh` to test on multiple rubies
|
9
|
+
|
3
10
|
### v0.4.0
|
4
11
|
|
5
12
|
* Rework `Gofer::Cluster` to be a direct proxy, rather than requiring a block
|
data/Gemfile
CHANGED
data/README.md
CHANGED
@@ -1,5 +1,7 @@
|
|
1
1
|
# Gofer!
|
2
2
|
|
3
|
+
[![Code Climate](https://codeclimate.com/github/mipearson/gofer.png)](https://codeclimate.com/github/mipearson/gofer)
|
4
|
+
|
3
5
|
**Gofer** is a set of wrappers around the Net::SSH suite of tools to enable consistent access to remote systems.
|
4
6
|
|
5
7
|
**Gofer** has been written to support the needs of system automation scripts. As such, **gofer** will:
|
@@ -9,13 +11,16 @@
|
|
9
11
|
* allow you to access captured STDOUT and STDERR individually or as a combined string
|
10
12
|
* override the above: return non-zero exit status instead of raising an error, suppress output
|
11
13
|
* persist the SSH connection so that multiple commands don't incur connection penalties
|
14
|
+
* allow multiple simultaneous command execution on a cluster of hosts via `Gofer::Cluster`
|
15
|
+
|
16
|
+
Full documentation for latest gem release is at [RDoc](http://rdoc.info/gems/gofer/frames)
|
12
17
|
|
13
18
|
## Examples
|
14
19
|
|
15
20
|
### Instantiation
|
16
21
|
|
17
22
|
``` ruby
|
18
|
-
h = Gofer::Host.new('my.host.com', 'ubuntu', :keys => ['
|
23
|
+
h = Gofer::Host.new('my.host.com', 'ubuntu', :keys => ['~/.ssh/id_rsa'])
|
19
24
|
```
|
20
25
|
|
21
26
|
### Run a command
|
@@ -76,13 +81,6 @@ h.run "echo noisier 1>&2", :quiet_stderr => true # don't print stderr
|
|
76
81
|
h.quiet = true # never print stdout
|
77
82
|
```
|
78
83
|
|
79
|
-
### Run multiple commands
|
80
|
-
|
81
|
-
``` ruby
|
82
|
-
response = h.run_multiple(['echo hello', 'echo goodbye'], :quiet => true)
|
83
|
-
puts response.stdout # will print "hello\ngoodbye\n"
|
84
|
-
```
|
85
|
-
|
86
84
|
### Run the same commands on multiple hosts
|
87
85
|
|
88
86
|
``` ruby
|
@@ -110,6 +108,7 @@ puts results.values.join(", ") # will print "my.host.com, other.host.com"
|
|
110
108
|
|
111
109
|
* Ensure that your user can ssh as itself to localhost using the key in `~/.ssh/id_rsa`.
|
112
110
|
* Run `rspec spec` or `bundle install && rake spec`
|
111
|
+
* rbenv users can run `test.sh` and ensure their code works on Ruby versions we support
|
113
112
|
|
114
113
|
## Contributing
|
115
114
|
|
@@ -121,7 +120,6 @@ Contributions should be via pull request. Please add tests and a note in the
|
|
121
120
|
* ls, exists?, directory? should use sftp if available rather than shell commands
|
122
121
|
* Deal with timeouts/disconnects on persistent connections
|
123
122
|
* Release 1.0 & use Semver
|
124
|
-
* Ensure RDodc is complete & up to date, link to rdoc.info from README
|
125
123
|
* Add unit tests, bring in Travis.ci
|
126
124
|
* Local system usage (eg `Gofer::Localhost.new.run "hostname"`)
|
127
125
|
|
data/lib/gofer.rb
CHANGED
data/lib/gofer/cluster.rb
CHANGED
@@ -1,11 +1,29 @@
|
|
1
1
|
require 'thread'
|
2
2
|
|
3
3
|
module Gofer
|
4
|
+
# A collection of Gofer::Host instances that can run commands simultaneously
|
5
|
+
#
|
6
|
+
# Gofer::Cluster supports most of the methods of Gofer::Host. Commands
|
7
|
+
# will be run simultaneously, with up to +max_concurrency+ commands running
|
8
|
+
# at the same time. If +max_concurrency+ is unset all hosts in the cluster
|
9
|
+
# will receive commands at the same time.
|
10
|
+
#
|
11
|
+
# Results from commands run are returned in a Hash, keyed by host.
|
4
12
|
class Cluster
|
5
13
|
|
14
|
+
# Hosts in this cluster
|
6
15
|
attr_reader :hosts
|
16
|
+
|
17
|
+
# Maximum number of commands to run simultaneously
|
7
18
|
attr_accessor :max_concurrency
|
8
19
|
|
20
|
+
# Create a new cluster of Gofer::Host connections.
|
21
|
+
#
|
22
|
+
# +parties+:: Gofer::Host or other Gofer::Cluster instances
|
23
|
+
#
|
24
|
+
# Options:
|
25
|
+
#
|
26
|
+
# +max_concurrency+:: Maximum number of commands to run simultaneously
|
9
27
|
def initialize(parties=[], opts={})
|
10
28
|
@hosts = []
|
11
29
|
@max_concurrency = opts.delete(:max_concurrency)
|
@@ -13,10 +31,13 @@ module Gofer
|
|
13
31
|
parties.each { |i| self << i }
|
14
32
|
end
|
15
33
|
|
34
|
+
# Currency effective concurrency, either +max_concurrency+ or the number of
|
35
|
+
# Gofer::Host instances we contain.
|
16
36
|
def concurrency
|
17
37
|
max_concurrency.nil? ? hosts.length : [max_concurrency, hosts.length].min
|
18
38
|
end
|
19
39
|
|
40
|
+
# Add a Gofer::Host or the hosts belonging to a Gofer::Cluster to this instance.
|
20
41
|
def <<(other)
|
21
42
|
case other
|
22
43
|
when Cluster
|
@@ -26,10 +47,39 @@ module Gofer
|
|
26
47
|
end
|
27
48
|
end
|
28
49
|
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
50
|
+
# Run a command on this Gofer::Cluster. See Gofer::Host#run
|
51
|
+
def run *args
|
52
|
+
threaded(:run, *args)
|
53
|
+
end
|
54
|
+
|
55
|
+
# Check if a path exists on each host in the cluster. See Gofer::Host#exist?
|
56
|
+
def exist? *args
|
57
|
+
threaded(:exist?, *args)
|
58
|
+
end
|
59
|
+
|
60
|
+
# Check if a path is a directory on each host in the cluster. See Gofer::Host#directory?
|
61
|
+
def directory? *args
|
62
|
+
threaded(:directory?, *args)
|
63
|
+
end
|
64
|
+
|
65
|
+
# List a directory on each host in the cluster. See Gofer::Host#ls
|
66
|
+
def ls *args
|
67
|
+
threaded(:ls, *args)
|
68
|
+
end
|
69
|
+
|
70
|
+
# Upload to each host in the cluster. See Gofer::Host#ls
|
71
|
+
def upload *args
|
72
|
+
threaded(:upload, *args)
|
73
|
+
end
|
74
|
+
|
75
|
+
# Read a file on each host in the cluster. See Gofer::Host#read
|
76
|
+
def read *args
|
77
|
+
threaded(:read, *args)
|
78
|
+
end
|
79
|
+
|
80
|
+
# Write a file to each host in the cluster. See Gofer::Host#write
|
81
|
+
def write *args
|
82
|
+
threaded(:write, *args)
|
33
83
|
end
|
34
84
|
|
35
85
|
private
|
data/lib/gofer/host.rb
CHANGED
@@ -1,33 +1,37 @@
|
|
1
1
|
require 'tempfile'
|
2
2
|
|
3
3
|
module Gofer
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
4
|
+
# A persistent, authenticated SSH connection to a single host.
|
5
|
+
#
|
6
|
+
# Connections are persistent, but not encapsulated within a shell.
|
7
|
+
# This means that while it won't need to reconnect & re-authenticate for
|
8
|
+
# each operation, don't assume that environment variables will be
|
9
|
+
# persisted between commands like they will in a shell-based SSH session.
|
10
|
+
#
|
11
|
+
# +/etc/ssh/config+ and <tt>~/.ssh/config</tt> are not recognized by Net::SSH, and thus
|
12
|
+
# not recognized by Gofer::Host.
|
12
13
|
|
13
14
|
class Host
|
14
15
|
|
15
16
|
attr_reader :hostname
|
16
17
|
attr_accessor :quiet, :output_prefix
|
17
18
|
|
18
|
-
# Create a new
|
19
|
+
# Create a new connection to a host
|
20
|
+
#
|
21
|
+
# Passed options not included in the below are passed directly to
|
22
|
+
# <tt>Net::SSH.start</tt>. See http://net-ssh.github.com/ssh/v2/api/index.html
|
23
|
+
# for valid arguments.
|
19
24
|
#
|
20
25
|
# Options:
|
21
26
|
#
|
22
27
|
# +quiet+:: Don't print stdout output from +run+ commands
|
23
|
-
# +output_prefix+:: Prefix each line of stdout to differentiate multiple host output
|
24
|
-
# All other+opts+ is passed through directly to Net::SSH.start
|
25
|
-
# See http://net-ssh.github.com/ssh/v2/api/index.html for valid arguments.
|
28
|
+
# +output_prefix+:: Prefix each line of stdout and stderr to differentiate multiple host output
|
26
29
|
def initialize _hostname, username, opts={}
|
27
30
|
@hostname = _hostname
|
28
31
|
|
29
32
|
# support legacy positional argument use
|
30
33
|
if opts.is_a? String
|
34
|
+
warn "Gofer::Host.new identify file positional argument will be removed in 1.0, use :keys instead"
|
31
35
|
opts = { :keys => [opts]}
|
32
36
|
end
|
33
37
|
|
@@ -36,6 +40,7 @@ module Gofer
|
|
36
40
|
|
37
41
|
# support legacy identity_file argument
|
38
42
|
if opts[:identity_file]
|
43
|
+
warn "Gofer::Host.new option :identify_file will be removed in 1.0, use :keys instead"
|
39
44
|
opts[:keys] = [opts.delete(:identity_file)]
|
40
45
|
end
|
41
46
|
|
@@ -44,11 +49,13 @@ module Gofer
|
|
44
49
|
|
45
50
|
# Run +command+.
|
46
51
|
#
|
47
|
-
#
|
52
|
+
# Will raise an error if +command+ exits with a non-zero status, unless
|
53
|
+
# +capture_exit_status+ is true.
|
48
54
|
#
|
49
55
|
# Print +stdout+ and +stderr+ as they're received.
|
50
56
|
#
|
51
|
-
#
|
57
|
+
# Returns an intance of Gofer::Response, containing captured +stdout+,
|
58
|
+
# +stderr+, and an exit status if +capture_exit_status+ is true.
|
52
59
|
#
|
53
60
|
# Options:
|
54
61
|
#
|
@@ -65,49 +72,22 @@ module Gofer
|
|
65
72
|
response
|
66
73
|
end
|
67
74
|
|
68
|
-
#
|
69
|
-
#
|
70
|
-
# Raise an error if a command in +commands+ exits with a non-zero status.
|
71
|
-
#
|
72
|
-
# Print +stdout+ and +stderr+ as they're received.
|
73
|
-
#
|
74
|
-
# Return a Gofer::Response object.
|
75
|
-
#
|
76
|
-
# Options:
|
77
|
-
#
|
78
|
-
# +quiet+:: Don't print +stdout+, can also be set with +quiet=+ on the instance
|
79
|
-
# +quiet_stderr+:: Don't print +stderr+
|
80
|
-
#
|
81
|
-
# The behaviour of passing +capture_exit_status+ here is undefined.
|
82
|
-
def run_multiple commands, opts={}
|
83
|
-
return if commands.empty?
|
84
|
-
|
85
|
-
responses = commands.map do |command|
|
86
|
-
run command, opts
|
87
|
-
end
|
88
|
-
|
89
|
-
first_response = responses.shift
|
90
|
-
responses.reduce(first_response) do |cursor, response|
|
91
|
-
Response.new(cursor.stdout + response.stdout, cursor.stderr + response.stderr, cursor.output + response.output, 0)
|
92
|
-
end
|
93
|
-
end
|
94
|
-
|
95
|
-
# Return +true+ if +path+ exits.
|
75
|
+
# Returns +true+ if +path+ exists, +false+ otherwise.
|
96
76
|
def exist? path
|
97
77
|
@ssh.run("sh -c '[ -e #{path} ]'").exit_status == 0
|
98
78
|
end
|
99
79
|
|
100
|
-
#
|
80
|
+
# Returnss the contents of the file at +path+.
|
101
81
|
def read path
|
102
82
|
@ssh.read_file path
|
103
83
|
end
|
104
84
|
|
105
|
-
#
|
85
|
+
# Returns +true+ if +path+ is a directory, +false+ otherwise.
|
106
86
|
def directory? path
|
107
87
|
@ssh.run("sh -c '[ -d #{path} ]'").exit_status == 0
|
108
88
|
end
|
109
89
|
|
110
|
-
#
|
90
|
+
# Returns a list of the files in the directory at +path+.
|
111
91
|
def ls path
|
112
92
|
response = @ssh.run "ls -1 #{path}", :quiet => true
|
113
93
|
if response.exit_status == 0
|
@@ -117,17 +97,25 @@ module Gofer
|
|
117
97
|
end
|
118
98
|
end
|
119
99
|
|
120
|
-
#
|
100
|
+
# Uploads the file or directory at +from+ to +to+.
|
101
|
+
#
|
102
|
+
# Options:
|
103
|
+
#
|
104
|
+
# +recursive+: Perform a recursive upload, similar to +scp -r+. +true+ by default if +from+ is a directory.
|
121
105
|
def upload from, to, opts = {}
|
122
106
|
@ssh.upload from, to, {:recursive => File.directory?(from)}.merge(opts)
|
123
107
|
end
|
124
108
|
|
125
|
-
#
|
109
|
+
# Downloads the file or directory at +from+ to +to+
|
110
|
+
#
|
111
|
+
# Options:
|
112
|
+
#
|
113
|
+
# +recursive+: Perform a recursive download, similar to +scp -r+. +true+ by default if +from+ is a directory.
|
126
114
|
def download from, to, opts = {}
|
127
115
|
@ssh.download from, to, {:recursive => directory?(from)}.merge(opts)
|
128
116
|
end
|
129
117
|
|
130
|
-
#
|
118
|
+
# Writes +data+ to a file at +to+
|
131
119
|
def write data, to
|
132
120
|
Tempfile.open "gofer_write" do |file|
|
133
121
|
file.write data
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module Gofer
|
2
|
+
# An error encountered performing a Gofer command
|
3
|
+
class HostError < Exception
|
4
|
+
|
5
|
+
# Instance of Gofer::Host that raised the error
|
6
|
+
attr_reader :host
|
7
|
+
|
8
|
+
# Instance of Gofer::Response encapsulating the error output
|
9
|
+
attr_reader :response
|
10
|
+
|
11
|
+
def initialize host, response, message
|
12
|
+
@host = host
|
13
|
+
@response = response
|
14
|
+
super "#{host.hostname}: #{message}"
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
data/lib/gofer/response.rb
CHANGED
@@ -2,9 +2,20 @@ module Gofer
|
|
2
2
|
|
3
3
|
# Response container for the various outputs from Gofer::Host#run
|
4
4
|
class Response < String
|
5
|
-
attr_reader :stdout, :stderr, :output, :exit_status
|
6
5
|
|
7
|
-
|
6
|
+
# Captured STDOUT output
|
7
|
+
attr_reader :stdout
|
8
|
+
|
9
|
+
# Captured STDERR output
|
10
|
+
attr_reader :stderr
|
11
|
+
|
12
|
+
# Combined STDOUT / STDERR output (also value of this as a String)
|
13
|
+
attr_reader :output
|
14
|
+
|
15
|
+
# Exit status of command, only available if :capture_exit_status is used
|
16
|
+
attr_reader :exit_status
|
17
|
+
|
18
|
+
def initialize (_stdout, _stderr, _output, _exit_status) # :nodoc:
|
8
19
|
super _stdout
|
9
20
|
@stdout = _stdout
|
10
21
|
@stderr = _stderr
|
data/lib/gofer/version.rb
CHANGED
@@ -0,0 +1,89 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Gofer::Cluster do
|
4
|
+
|
5
|
+
before :all do
|
6
|
+
@cluster = Gofer::Cluster.new
|
7
|
+
# Cheat and use the same host repeatedly
|
8
|
+
@host1 = Gofer::Host.new(test_hostname, test_username, :keys => [test_identity_file], :quiet => true)
|
9
|
+
@host2 = Gofer::Host.new(test_hostname, test_username, :keys => [test_identity_file], :quiet => true)
|
10
|
+
@cluster << @host1
|
11
|
+
@cluster << @host2
|
12
|
+
make_tmpdir
|
13
|
+
end
|
14
|
+
|
15
|
+
after(:all) { clean_tmpdir }
|
16
|
+
|
17
|
+
it "should run commands in parallel" do
|
18
|
+
results = @cluster.run("ruby -e 'puts Time.now.to_f; sleep 0.1; puts Time.now.to_f'")
|
19
|
+
|
20
|
+
res1 = results[@host1].stdout.lines.to_a
|
21
|
+
res2 = results[@host2].stdout.lines.to_a
|
22
|
+
|
23
|
+
expect(res1[1].to_f).to be > res2[0].to_f
|
24
|
+
end
|
25
|
+
|
26
|
+
it "should respect max_concurrency" do
|
27
|
+
@cluster.max_concurrency = 1
|
28
|
+
results = @cluster.run("ruby -e 'puts Time.now.to_f; sleep 0.1; puts Time.now.to_f'")
|
29
|
+
|
30
|
+
res1 = results[@host1].stdout.lines.to_a
|
31
|
+
res2 = results[@host2].stdout.lines.to_a
|
32
|
+
|
33
|
+
expect(res2[0].to_f).to be >= res1[1].to_f
|
34
|
+
end
|
35
|
+
|
36
|
+
# TODO: Make this a custom matcher?
|
37
|
+
def results_should_eq expected, &block
|
38
|
+
results = block.call
|
39
|
+
results[@host1].should eq expected
|
40
|
+
results[@host2].should eq expected
|
41
|
+
end
|
42
|
+
|
43
|
+
describe :exist? do
|
44
|
+
it "should return true if a directory exists" do
|
45
|
+
results_should_eq(true) { @cluster.exist?(@tmpdir) }
|
46
|
+
results_should_eq(false) { @cluster.exist?(@tmpdir + '/blargh') }
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
describe :directory? do
|
51
|
+
it "should return true if a path is a directory" do
|
52
|
+
results_should_eq(true) { @cluster.directory?(@tmpdir)}
|
53
|
+
raw_ssh "touch #{@tmpdir}/a_file"
|
54
|
+
results_should_eq(false) { @cluster.directory?("#{@tmpdir}/a_file")}
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
describe :read do
|
59
|
+
it "should read in the contents of a file" do
|
60
|
+
raw_ssh "echo hello > #{@tmpdir}/hello.txt"
|
61
|
+
results_should_eq("hello\n") { @cluster.read(@tmpdir + '/hello.txt')}
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
describe :ls do
|
66
|
+
it "should list the contents of a directory" do
|
67
|
+
raw_ssh "mkdir #{@tmpdir}/lstmp && touch #{@tmpdir}/lstmp/f"
|
68
|
+
results_should_eq(['f']) { @cluster.ls(@tmpdir + '/lstmp') }
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
describe :upload do
|
73
|
+
it "should upload a file to the remote server" do
|
74
|
+
pending "testing problematic as we're connecting to the same host twice"
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
describe :write do
|
79
|
+
it "should write a file to the remote server" do
|
80
|
+
pending "testing problematic as we're connecting to the same host twice"
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
describe :download do
|
85
|
+
it "should deliberately not be implemented as destination files would be overwritten" do
|
86
|
+
expect { @cluster.download("whut") }.to raise_error(NoMethodError)
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
@@ -1,68 +1,30 @@
|
|
1
1
|
require 'spec_helper'
|
2
2
|
require 'tempfile'
|
3
3
|
|
4
|
-
describe Gofer do
|
5
|
-
|
6
|
-
HOSTNAME = ENV['TEST_HOST'] || 'localhost'
|
7
|
-
USERNAME = ENV['TEST_USER'] || ENV['USER']
|
8
|
-
IDENTITY_FILE = ENV['TEST_IDENTITY_FILE'] || '~/.ssh/id_rsa'
|
9
|
-
|
10
|
-
def raw_ssh command
|
11
|
-
out = `ssh -o PasswordAuthentication=no -ni #{IDENTITY_FILE} #{USERNAME}@#{HOSTNAME} #{command}`
|
12
|
-
raise "Command #{command} failed" unless $? == 0
|
13
|
-
out
|
14
|
-
end
|
15
|
-
|
16
|
-
def in_tmpdir path
|
17
|
-
File.join(@tmpdir, path)
|
18
|
-
end
|
19
|
-
|
20
|
-
def with_local_tmpdir template
|
21
|
-
f = Tempfile.new template
|
22
|
-
path = f.path
|
23
|
-
f.unlink
|
24
|
-
FileUtils.mkdir path
|
25
|
-
begin
|
26
|
-
yield path
|
27
|
-
ensure
|
28
|
-
FileUtils.rm_rf path unless ENV['KEEPTMPDIR']
|
29
|
-
end
|
30
|
-
end
|
31
|
-
|
32
|
-
def with_captured_output
|
33
|
-
@stdout = ''
|
34
|
-
@stderr = ''
|
35
|
-
@combined = ''
|
36
|
-
$stdout.stub!( :write ) { |*args| @stdout.<<( *args ); @combined.<<( *args )}
|
37
|
-
$stderr.stub!( :write ) { |*args| @stderr.<<( *args ); @combined.<<( *args )}
|
38
|
-
end
|
4
|
+
describe Gofer::Host do
|
39
5
|
|
40
6
|
before :all do
|
41
|
-
@host = Gofer::Host.new(
|
7
|
+
@host = Gofer::Host.new(test_hostname, test_username, :keys => [test_identity_file], :quiet => true)
|
42
8
|
@tmpdir = raw_ssh("mktemp -d /tmp/gofertest.XXXXX").chomp
|
9
|
+
make_tmpdir
|
43
10
|
end
|
44
11
|
|
45
|
-
after
|
46
|
-
if ENV['KEEPTMPDIR']
|
47
|
-
puts "TMPDIR is #{@tmpdir}"
|
48
|
-
else
|
49
|
-
raw_ssh "rm -rf #{@tmpdir}" if @tmpdir && @tmpdir =~ %r{gofertest}
|
50
|
-
end
|
51
|
-
end
|
12
|
+
after(:all) { clean_tmpdir }
|
52
13
|
|
53
14
|
describe :new do
|
15
|
+
before(:each) { Gofer::Host.any_instance.stub(:warn => nil) }
|
54
16
|
it "should support the legacy positional argument" do
|
55
|
-
Gofer::Host.new(
|
17
|
+
Gofer::Host.new(test_hostname, test_username, test_identity_file).run("echo hello", :quiet => true).should == "hello\n"
|
56
18
|
end
|
57
19
|
|
58
20
|
it "should support the legacy identity_file key" do
|
59
|
-
Gofer::Host.new(
|
21
|
+
Gofer::Host.new(test_hostname, test_username, :identity_file => test_identity_file).run("echo hello", :quiet => true).should == "hello\n"
|
60
22
|
end
|
61
23
|
end
|
62
24
|
|
63
25
|
describe :hostname do
|
64
26
|
it "should be the hostname of the host we're connecting to" do
|
65
|
-
@host.hostname.should ==
|
27
|
+
@host.hostname.should == test_hostname
|
66
28
|
end
|
67
29
|
end
|
68
30
|
|
@@ -139,19 +101,6 @@ describe Gofer do
|
|
139
101
|
end
|
140
102
|
end
|
141
103
|
|
142
|
-
describe :run_multiple do
|
143
|
-
describe "with stdout and stderr responses" do
|
144
|
-
before :all do
|
145
|
-
@response = @host.run_multiple ["echo stdout", "echo stderr 1>&2"], :quiet_stderr => true
|
146
|
-
end
|
147
|
-
it_should_behave_like "an output capturer"
|
148
|
-
end
|
149
|
-
|
150
|
-
it "should error if a command returns a non-zero response" do
|
151
|
-
lambda {@host.run_multiple ["echo", "false"]}.should raise_error /failed with exit status/
|
152
|
-
end
|
153
|
-
end
|
154
|
-
|
155
104
|
describe :exist? do
|
156
105
|
it "should return true if a path or file exists" do
|
157
106
|
raw_ssh "touch #{in_tmpdir 'exists'}"
|
@@ -239,34 +188,4 @@ describe Gofer do
|
|
239
188
|
end
|
240
189
|
end
|
241
190
|
end
|
242
|
-
|
243
|
-
describe :cluster do
|
244
|
-
before do
|
245
|
-
@cluster = Gofer::Cluster.new
|
246
|
-
# Cheat and use the same host repeatedly
|
247
|
-
@host1 = Gofer::Host.new(HOSTNAME, USERNAME, :keys => [IDENTITY_FILE], :quiet => true)
|
248
|
-
@host2 = Gofer::Host.new(HOSTNAME, USERNAME, :keys => [IDENTITY_FILE], :quiet => true)
|
249
|
-
@cluster << @host1
|
250
|
-
@cluster << @host2
|
251
|
-
end
|
252
|
-
|
253
|
-
it "should run commands in parallel" do
|
254
|
-
results = @cluster.run("ruby -e 'puts Time.now.to_f; sleep 0.1; puts Time.now.to_f'")
|
255
|
-
|
256
|
-
res1 = results[@host1].stdout.lines.to_a
|
257
|
-
res2 = results[@host2].stdout.lines.to_a
|
258
|
-
|
259
|
-
expect(res1[1].to_f).to be > res2[0].to_f
|
260
|
-
end
|
261
|
-
|
262
|
-
it "should respect max_concurrency" do
|
263
|
-
@cluster.max_concurrency = 1
|
264
|
-
results = @cluster.run("ruby -e 'puts Time.now.to_f; sleep 0.1; puts Time.now.to_f'")
|
265
|
-
|
266
|
-
res1 = results[@host1].stdout.lines.to_a
|
267
|
-
res2 = results[@host2].stdout.lines.to_a
|
268
|
-
|
269
|
-
expect(res2[0].to_f).to be >= res1[1].to_f
|
270
|
-
end
|
271
|
-
end
|
272
191
|
end
|
data/spec/spec_helper.rb
CHANGED
@@ -0,0 +1,57 @@
|
|
1
|
+
module IntegrationHelpers
|
2
|
+
|
3
|
+
def test_hostname
|
4
|
+
ENV['TEST_HOST'] || 'localhost'
|
5
|
+
end
|
6
|
+
|
7
|
+
def test_username
|
8
|
+
ENV['TEST_USER'] || ENV['USER']
|
9
|
+
end
|
10
|
+
|
11
|
+
def test_identity_file
|
12
|
+
ENV['TEST_IDENTITY_FILE'] || '~/.ssh/id_rsa'
|
13
|
+
end
|
14
|
+
|
15
|
+
def raw_ssh command
|
16
|
+
out = `ssh -o PasswordAuthentication=no -ni #{test_identity_file} #{test_username}@#{test_hostname} #{command}`
|
17
|
+
raise "Command #{command} failed" unless $? == 0
|
18
|
+
out
|
19
|
+
end
|
20
|
+
|
21
|
+
def make_tmpdir
|
22
|
+
@tmpdir = raw_ssh("mktemp -d /tmp/gofertest.XXXXX").chomp
|
23
|
+
end
|
24
|
+
|
25
|
+
def clean_tmpdir
|
26
|
+
if ENV['KEEPTMPDIR']
|
27
|
+
puts "TMPDIR is #{@tmpdir}"
|
28
|
+
else
|
29
|
+
raw_ssh "rm -rf #{@tmpdir}" if @tmpdir && @tmpdir =~ %r{gofertest}
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def in_tmpdir path
|
34
|
+
File.join(@tmpdir, path)
|
35
|
+
end
|
36
|
+
|
37
|
+
def with_local_tmpdir template
|
38
|
+
f = Tempfile.new template
|
39
|
+
path = f.path
|
40
|
+
f.close
|
41
|
+
f.unlink
|
42
|
+
FileUtils.mkdir path
|
43
|
+
begin
|
44
|
+
yield path
|
45
|
+
ensure
|
46
|
+
FileUtils.rm_rf path unless ENV['KEEPTMPDIR']
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def with_captured_output
|
51
|
+
@stdout = ''
|
52
|
+
@stderr = ''
|
53
|
+
@combined = ''
|
54
|
+
$stdout.stub!( :write ) { |*args| @stdout.<<( *args ); @combined.<<( *args )}
|
55
|
+
$stderr.stub!( :write ) { |*args| @stderr.<<( *args ); @combined.<<( *args )}
|
56
|
+
end
|
57
|
+
end
|
data/test.sh
ADDED
metadata
CHANGED
@@ -1,57 +1,59 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: gofer
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
|
4
|
+
prerelease:
|
5
|
+
version: 0.5.0
|
5
6
|
platform: ruby
|
6
7
|
authors:
|
7
8
|
- Michael Pearson
|
8
|
-
autorequire:
|
9
|
+
autorequire:
|
9
10
|
bindir: bin
|
10
11
|
cert_chain: []
|
11
|
-
date: 2013-06-
|
12
|
+
date: 2013-06-07 00:00:00.000000000 Z
|
12
13
|
dependencies:
|
13
14
|
- !ruby/object:Gem::Dependency
|
14
15
|
name: net-ssh
|
15
|
-
|
16
|
+
version_requirements: !ruby/object:Gem::Requirement
|
16
17
|
requirements:
|
17
|
-
- -
|
18
|
+
- - ">="
|
18
19
|
- !ruby/object:Gem::Version
|
19
20
|
version: 2.0.23
|
20
|
-
|
21
|
-
|
22
|
-
version_requirements: !ruby/object:Gem::Requirement
|
21
|
+
none: false
|
22
|
+
requirement: !ruby/object:Gem::Requirement
|
23
23
|
requirements:
|
24
|
-
- -
|
24
|
+
- - ">="
|
25
25
|
- !ruby/object:Gem::Version
|
26
26
|
version: 2.0.23
|
27
|
+
none: false
|
28
|
+
prerelease: false
|
29
|
+
type: :runtime
|
27
30
|
- !ruby/object:Gem::Dependency
|
28
31
|
name: net-scp
|
29
|
-
|
32
|
+
version_requirements: !ruby/object:Gem::Requirement
|
30
33
|
requirements:
|
31
|
-
- -
|
34
|
+
- - ">="
|
32
35
|
- !ruby/object:Gem::Version
|
33
36
|
version: 1.0.4
|
34
|
-
|
35
|
-
|
36
|
-
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
none: false
|
38
|
+
requirement: !ruby/object:Gem::Requirement
|
37
39
|
requirements:
|
38
|
-
- -
|
40
|
+
- - ">="
|
39
41
|
- !ruby/object:Gem::Version
|
40
42
|
version: 1.0.4
|
41
|
-
|
43
|
+
none: false
|
44
|
+
prerelease: false
|
45
|
+
type: :runtime
|
46
|
+
description: |2
|
42
47
|
|
43
48
|
Gofer provides a flexible and reliable model for performing tasks on remote
|
44
|
-
|
45
49
|
server using Net::SSH
|
46
|
-
|
47
|
-
'
|
48
50
|
email:
|
49
51
|
- mipearson@gmail.com
|
50
52
|
executables: []
|
51
53
|
extensions: []
|
52
54
|
extra_rdoc_files: []
|
53
55
|
files:
|
54
|
-
- .gitignore
|
56
|
+
- ".gitignore"
|
55
57
|
- CHANGELOG.md
|
56
58
|
- Gemfile
|
57
59
|
- README.md
|
@@ -60,34 +62,43 @@ files:
|
|
60
62
|
- lib/gofer.rb
|
61
63
|
- lib/gofer/cluster.rb
|
62
64
|
- lib/gofer/host.rb
|
65
|
+
- lib/gofer/host_error.rb
|
63
66
|
- lib/gofer/response.rb
|
64
67
|
- lib/gofer/ssh_wrapper.rb
|
65
68
|
- lib/gofer/version.rb
|
66
|
-
- spec/gofer/
|
69
|
+
- spec/gofer/cluster_spec.rb
|
70
|
+
- spec/gofer/host_spec.rb
|
67
71
|
- spec/spec_helper.rb
|
72
|
+
- spec/support/integration_helpers.rb
|
73
|
+
- test.sh
|
68
74
|
homepage: https://github.com/mipearson/gofer
|
69
75
|
licenses: []
|
70
|
-
|
71
|
-
post_install_message:
|
76
|
+
post_install_message:
|
72
77
|
rdoc_options: []
|
73
78
|
require_paths:
|
74
79
|
- lib
|
75
80
|
required_ruby_version: !ruby/object:Gem::Requirement
|
76
81
|
requirements:
|
77
|
-
- -
|
82
|
+
- - ">="
|
78
83
|
- !ruby/object:Gem::Version
|
79
|
-
version:
|
84
|
+
version: !binary |-
|
85
|
+
MA==
|
86
|
+
none: false
|
80
87
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
81
88
|
requirements:
|
82
|
-
- -
|
89
|
+
- - ">="
|
83
90
|
- !ruby/object:Gem::Version
|
84
|
-
version:
|
91
|
+
version: !binary |-
|
92
|
+
MA==
|
93
|
+
none: false
|
85
94
|
requirements: []
|
86
|
-
rubyforge_project:
|
87
|
-
rubygems_version:
|
88
|
-
signing_key:
|
89
|
-
specification_version:
|
95
|
+
rubyforge_project:
|
96
|
+
rubygems_version: 1.8.24
|
97
|
+
signing_key:
|
98
|
+
specification_version: 3
|
90
99
|
summary: run commands on remote servers using SSH
|
91
100
|
test_files:
|
92
|
-
- spec/gofer/
|
101
|
+
- spec/gofer/cluster_spec.rb
|
102
|
+
- spec/gofer/host_spec.rb
|
93
103
|
- spec/spec_helper.rb
|
104
|
+
- spec/support/integration_helpers.rb
|
checksums.yaml
DELETED
@@ -1,15 +0,0 @@
|
|
1
|
-
---
|
2
|
-
!binary "U0hBMQ==":
|
3
|
-
metadata.gz: !binary |-
|
4
|
-
NjZkNzBmMjZlMWI5ODk4MDE0ZDNmNTFlYzAxNjU4NTFlNzgyMDQ4NQ==
|
5
|
-
data.tar.gz: !binary |-
|
6
|
-
ZjMwYjNmMTAwMWQ2NDJlOWUzZTE1MTdmODBkNjg2ZWVlMTQ2OTZiOA==
|
7
|
-
!binary "U0hBNTEy":
|
8
|
-
metadata.gz: !binary |-
|
9
|
-
N2MwNTRkYzQzZDM4MGIyN2Q0ZTJmNmY3MDY2NWFhZTgwNTg2ZmFhOWU1OGRk
|
10
|
-
NWE3ZGRjZWJkMjM2OGFkOTA0Mjk4NTIyMDA4YTcwYjlhYWM0NmE1OWU2Mzdm
|
11
|
-
YzNlZjU1MzYxY2ExYWJkOGY5NTVjYmUxZGMzOGVlNzIzMzMwNzQ=
|
12
|
-
data.tar.gz: !binary |-
|
13
|
-
ZTYyNDAxYTU2MTdjNTQ1YWRjMjNmODFkYWI4MTA4YTVkYmQxZDlmMWIxMTE0
|
14
|
-
NTU2NmQ1MTJiOThkMTBkMjNhNGZhYzBjNTIwY2U2M2UzMGI0ZmU1MjVjYTAw
|
15
|
-
MDA1NmUxYWZlNzcxY2U5M2M2MTZmMzFmNGQzNjM1Zjc4OWI1YWE=
|