gofer 0.4.0 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -1,5 +1,5 @@
1
1
  source "http://rubygems.org"
2
2
 
3
- gem 'rspec'
3
+ gem 'rspec', '~>2.13.0'
4
4
  # Specify your gem's dependencies in gofer.gemspec
5
5
  gemspec
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 => ['key.pem'])
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
 
@@ -1,9 +1,10 @@
1
1
  require 'gofer/ssh_wrapper'
2
2
  require 'gofer/response'
3
+ require 'gofer/host_error'
3
4
  require 'gofer/host'
4
5
  require 'gofer/cluster'
5
6
  require 'gofer/version'
6
7
 
7
- # See Gofer::Host
8
+ # See Gofer::Host or Gofer::Cluster
8
9
  module Gofer
9
10
  end
@@ -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
- %w{run exist? read directory? ls upload read write}.each do |host_method|
30
- define_method host_method do |*args|
31
- threaded(host_method, *args)
32
- end
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
@@ -1,33 +1,37 @@
1
1
  require 'tempfile'
2
2
 
3
3
  module Gofer
4
- class HostError < Exception # :nodoc:
5
- attr_reader :host, :response
6
- def initialize host, response, message
7
- @host = host
8
- @response = response
9
- super "#{host.hostname}: #{message}"
10
- end
11
- end
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 Host connection
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
- # Raise an error if +command+ exits with a non-zero status.
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
- # Return a Gofer::Response object.
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
- # Run +commands+ one by one in order.
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
- # Return the contents of the file at +path+.
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
- # Return +true+ if +path+ is a directory.
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
- # Return a list of files in the directory at +path+.
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
- # Upload the file or directory at +from+ to +to+.
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
- # Download the file or directory at +from+ to +to+
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
- # Write +data+ to a file at +to+
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
@@ -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
- def initialize (_stdout, _stderr, _output, _exit_status)
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
@@ -1,3 +1,3 @@
1
1
  module Gofer # :nodoc:
2
- VERSION = "0.4.0"
2
+ VERSION = "0.5.0"
3
3
  end
@@ -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(HOSTNAME, USERNAME, :keys => [IDENTITY_FILE], :quiet => true)
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 :all do
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(HOSTNAME, USERNAME, IDENTITY_FILE).run("echo hello", :quiet => true).should == "hello\n"
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(HOSTNAME, USERNAME, :identity_file => IDENTITY_FILE).run("echo hello", :quiet => true).should == "hello\n"
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 == HOSTNAME
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
@@ -1,2 +1,8 @@
1
1
  require "rspec"
2
2
  require "gofer"
3
+
4
+ require File.expand_path("../support/integration_helpers", __FILE__)
5
+
6
+ RSpec.configure do |config|
7
+ config.include IntegrationHelpers
8
+ end
@@ -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
@@ -0,0 +1,11 @@
1
+ #!/bin/sh
2
+
3
+ set -e
4
+ set -x
5
+
6
+ VERSIONS="1.9.3-p392 1.8.7-p370 2.0.0-p0 jruby-1.7.3"
7
+
8
+ for version in $VERSIONS; do
9
+ RBENV_VERSION=$version bundle install --quiet
10
+ RBENV_VERSION=$version bundle exec rspec spec
11
+ done
metadata CHANGED
@@ -1,57 +1,59 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: gofer
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.0
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-02 00:00:00.000000000 Z
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
- requirement: !ruby/object:Gem::Requirement
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
- type: :runtime
21
- prerelease: false
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
- requirement: !ruby/object:Gem::Requirement
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
- type: :runtime
35
- prerelease: false
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
- description: ! '
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/integration_spec.rb
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
- metadata: {}
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: '0'
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: '0'
91
+ version: !binary |-
92
+ MA==
93
+ none: false
85
94
  requirements: []
86
- rubyforge_project:
87
- rubygems_version: 2.0.3
88
- signing_key:
89
- specification_version: 4
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/integration_spec.rb
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=