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.
@@ -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=