gofer 0.4.0 → 0.5.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/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
|
+
[](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=
|