gofer 0.2.6 → 0.3.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.
- checksums.yaml +15 -0
- data/.gitignore +1 -0
- data/README.md +79 -43
- data/lib/gofer.rb +2 -1
- data/lib/gofer/cluster.rb +73 -0
- data/lib/gofer/response.rb +3 -3
- data/lib/gofer/ssh_wrapper.rb +6 -6
- data/lib/gofer/version.rb +1 -1
- data/spec/gofer/integration_spec.rb +40 -0
- metadata +50 -71
checksums.yaml
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
---
|
2
|
+
!binary "U0hBMQ==":
|
3
|
+
metadata.gz: !binary |-
|
4
|
+
MWQwYzJjNDA5ZGZiOGQ3MDRkYWFhYmRhNGQ4MmZlNmM5OGU5MmY0OQ==
|
5
|
+
data.tar.gz: !binary |-
|
6
|
+
MTcwYTIwZDJjZWZlOGZhYWI5OTZjOWQ4NzUzM2NmYTBhNTU0YjhiMA==
|
7
|
+
!binary "U0hBNTEy":
|
8
|
+
metadata.gz: !binary |-
|
9
|
+
NmM0OTY3MjQ4ZDZjYjkxZDNlY2M0OTdlMzMyNmFkODA4N2Q5OTkzMzUyNGQ2
|
10
|
+
OWNiYjNkOTNjN2NlMDg2MDNiYTUwODEyMjUyYTFlNDg3OGNiNDE1YjI4NTNh
|
11
|
+
ZTJkMDlmMzcyMmMzZDI0NGEyZGQ3ZTc2Yzg3NzA4MTFkYjIyOGM=
|
12
|
+
data.tar.gz: !binary |-
|
13
|
+
ZDE5ODhmYmM5NzEwZjg4MjA5YzRmYTE5ZTcxMzgzN2NmYjRkYjExMTBmMWQ1
|
14
|
+
NzgwYjc4OTBhYjM2YTBhZjk3ODRhYzExMzFiMzU0NDYwYTUwNGEwYzcwNDcy
|
15
|
+
NGNmODY4NDY4MzY0YWIxNGI2YTNhMzFiZTFiYzE5N2Y1OTBhNDI=
|
data/.gitignore
CHANGED
data/README.md
CHANGED
@@ -14,82 +14,118 @@
|
|
14
14
|
|
15
15
|
### Instantiation
|
16
16
|
|
17
|
-
|
17
|
+
``` ruby
|
18
|
+
h = Gofer::Host.new('my.host.com', 'ubuntu', :keys => ['key.pem'])
|
19
|
+
```
|
18
20
|
|
19
21
|
### Run a command
|
20
22
|
|
21
|
-
|
23
|
+
``` ruby
|
24
|
+
h.run "sudo stop mysqld"
|
25
|
+
```
|
22
26
|
|
23
27
|
### Copy some files
|
24
28
|
|
25
|
-
|
26
|
-
|
29
|
+
``` ruby
|
30
|
+
h.upload 'file', 'remote_file'
|
31
|
+
h.download 'remote_dir', 'dir'
|
32
|
+
```
|
27
33
|
|
28
34
|
### Interact with the filesystem
|
29
35
|
|
30
|
-
|
31
|
-
|
32
|
-
|
36
|
+
``` ruby
|
37
|
+
if h.exist?('remote_directory')
|
38
|
+
h.run "rm -rf 'remote_directory'"
|
39
|
+
end
|
40
|
+
|
41
|
+
h.write("a string buffer", 'a_remote_file')
|
42
|
+
puts h.read('a_remote_file')
|
43
|
+
puts h.ls('a_remote_dir').join(", ")
|
44
|
+
```
|
33
45
|
|
34
|
-
h.write("a string buffer", 'a_remote_file')
|
35
|
-
puts h.read('a_remote_file')
|
36
|
-
puts h.ls('a_remote_dir').join(", ")
|
37
|
-
|
38
46
|
### Respond to command errors
|
39
47
|
|
40
|
-
|
41
|
-
|
42
|
-
|
48
|
+
``` ruby
|
49
|
+
h.run "false" # this will raise an error
|
50
|
+
response = h.run "false", :capture_exit_status => true # this won't ...
|
51
|
+
puts response.exit_status # and will make the exit status available
|
52
|
+
```
|
43
53
|
|
44
54
|
### Capture output
|
45
55
|
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
56
|
+
``` ruby
|
57
|
+
response = h.run "echo hello; echo goodbye 1>&2\n"
|
58
|
+
puts response # will print "hello\n"
|
59
|
+
puts response.stdout # will also print "hello\n"
|
60
|
+
puts response.stderr # will print "goodbye\n"
|
61
|
+
puts response.output # will print "hello\ngoodbye\n"
|
62
|
+
```
|
51
63
|
|
52
64
|
### Suppress output
|
53
65
|
|
54
|
-
|
55
|
-
|
56
|
-
|
66
|
+
``` ruby
|
67
|
+
h.run "echo noisy", :quiet => true # don't print stdout
|
68
|
+
h.run "echo noisier 1>&2", :quiet_stderr => true # don't print stderr
|
69
|
+
h.quiet = true # never print stdout
|
70
|
+
```
|
57
71
|
|
58
72
|
### Run multiple commands
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
73
|
+
|
74
|
+
``` ruby
|
75
|
+
response = h.run_multiple(['echo hello', 'echo goodbye'], :quiet => true)
|
76
|
+
puts response.stdout # will print "hello\ngoodbye\n"
|
77
|
+
```
|
78
|
+
|
79
|
+
### Run the same commands on multiple hosts
|
80
|
+
|
81
|
+
``` ruby
|
82
|
+
cluster = Gopher::Cluster.new
|
83
|
+
cluster << Gofer::Host.new('my.host.com', 'ubuntu', :keys => ['key.pem'])
|
84
|
+
cluster << Gofer::Host.new('other.host.com', 'ubuntu', :keys => ['key.pem'])
|
85
|
+
|
86
|
+
cluster.run do |c|
|
87
|
+
c.run("hostname") # This will run on both hosts at once
|
88
|
+
end
|
89
|
+
|
90
|
+
cluster.run(:max_concurrency => 1) do |c|
|
91
|
+
c.run("sudo /etc/init.d/apache2 restart") # This will run on one machine at a time
|
92
|
+
end
|
93
|
+
```
|
94
|
+
|
63
95
|
## Planned Features
|
64
96
|
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
97
|
+
``` ruby
|
98
|
+
# constant connection (no reconnect for each action)
|
99
|
+
Gofer::Host.new(...).open do |h|
|
100
|
+
h.run( ... )
|
101
|
+
end
|
102
|
+
|
103
|
+
# overriding defaults
|
104
|
+
h.set :quiet => true
|
105
|
+
h.set :capture_exit_status => false
|
106
|
+
|
107
|
+
# Local system usage, too:
|
108
|
+
Gofer::Localhost.new.run "hostname" # > my.macbook.com
|
109
|
+
```
|
76
110
|
|
77
111
|
## Testing
|
78
|
-
|
112
|
+
|
79
113
|
* Ensure that your user can ssh as itself to localhost using the key in `~/.ssh/id_rsa`.
|
80
114
|
* Run `rspec spec` or `bundle install && rake spec`
|
81
115
|
|
82
116
|
## TODO
|
83
|
-
|
84
|
-
* ls, exists?, directory? should use sftp if available rather than shell commands
|
85
|
-
* wrap STDOUT with host prefix for easy identification of system output
|
86
|
-
* Deal with timeouts/disconnects on persistent connections
|
117
|
+
|
118
|
+
* ls, exists?, directory? should use sftp if available rather than shell commands
|
119
|
+
* wrap STDOUT with host prefix for easy identification of system output
|
120
|
+
* Deal with timeouts/disconnects on persistent connections
|
121
|
+
* CHANGELOG.md
|
122
|
+
* Release 1.0 & use Semver
|
87
123
|
|
88
124
|
## License
|
89
125
|
|
90
126
|
(The MIT License)
|
91
127
|
|
92
|
-
Copyright (c) 2011 Michael Pearson
|
128
|
+
Copyright (c) 2011-13 Michael Pearson
|
93
129
|
|
94
130
|
Permission is hereby granted, free of charge, to any person obtaining
|
95
131
|
a copy of this software and associated documentation files (the
|
data/lib/gofer.rb
CHANGED
@@ -0,0 +1,73 @@
|
|
1
|
+
require 'thread'
|
2
|
+
|
3
|
+
module Gofer
|
4
|
+
class Cluster
|
5
|
+
attr_reader :hosts
|
6
|
+
attr_accessor :max_concurrency
|
7
|
+
def initialize(parties=[])
|
8
|
+
@hosts = []
|
9
|
+
@max_concurrency = nil
|
10
|
+
|
11
|
+
parties.each do |i|
|
12
|
+
self << i
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def <<(other)
|
17
|
+
case other
|
18
|
+
when Cluster
|
19
|
+
other.hosts.each do |host|
|
20
|
+
@hosts << host
|
21
|
+
end
|
22
|
+
when Host
|
23
|
+
@hosts << other
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def run(opts={}, &block)
|
28
|
+
concurrency = opts[:max_concurrency] || max_concurrency || hosts.length
|
29
|
+
block.call(ClusterCommandRunner.new(hosts, concurrency))
|
30
|
+
end
|
31
|
+
|
32
|
+
class ClusterCommandRunner
|
33
|
+
def initialize(hosts, concurrency)
|
34
|
+
@concurrency = concurrency
|
35
|
+
@hosts = hosts
|
36
|
+
end
|
37
|
+
|
38
|
+
# Spawn +concurrency+ worker threads, each of which pops work off the
|
39
|
+
# +_in+ queue, and writes values to the +_out+ queue for syncronisation.
|
40
|
+
def run(cmd, opts={})
|
41
|
+
_in = run_queue
|
42
|
+
length = run_queue.length
|
43
|
+
_out = Queue.new
|
44
|
+
results = {}
|
45
|
+
(0...@concurrency).map do
|
46
|
+
Thread.new do
|
47
|
+
loop do
|
48
|
+
host = _in.pop(false) rescue Thread.exit
|
49
|
+
|
50
|
+
results[host] = host.run(cmd, opts)
|
51
|
+
_out << true
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
length.times do
|
57
|
+
_out.pop
|
58
|
+
end
|
59
|
+
|
60
|
+
return results
|
61
|
+
end
|
62
|
+
|
63
|
+
def run_queue
|
64
|
+
Queue.new.tap do |q|
|
65
|
+
@hosts.each do |h|
|
66
|
+
q << h
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
end
|
73
|
+
end
|
data/lib/gofer/response.rb
CHANGED
@@ -1,9 +1,9 @@
|
|
1
1
|
module Gofer
|
2
2
|
|
3
|
-
# Response container for the various outputs from Gofer::Host#run
|
3
|
+
# Response container for the various outputs from Gofer::Host#run
|
4
4
|
class Response < String
|
5
5
|
attr_reader :stdout, :stderr, :output, :exit_status
|
6
|
-
|
6
|
+
|
7
7
|
def initialize (_stdout, _stderr, _output, _exit_status)
|
8
8
|
super _stdout
|
9
9
|
@stdout = _stdout
|
@@ -12,4 +12,4 @@ module Gofer
|
|
12
12
|
@exit_status = _exit_status
|
13
13
|
end
|
14
14
|
end
|
15
|
-
end
|
15
|
+
end
|
data/lib/gofer/ssh_wrapper.rb
CHANGED
@@ -13,7 +13,7 @@ module Gofer
|
|
13
13
|
def run command, opts={}
|
14
14
|
ssh_execute(ssh, command, opts)
|
15
15
|
end
|
16
|
-
|
16
|
+
|
17
17
|
def read_file path
|
18
18
|
scp.download! path
|
19
19
|
end
|
@@ -27,12 +27,12 @@ module Gofer
|
|
27
27
|
end
|
28
28
|
|
29
29
|
private
|
30
|
-
|
30
|
+
|
31
31
|
def ssh
|
32
32
|
@ssh ||= Net::SSH.start(*@net_ssh_args)
|
33
33
|
end
|
34
|
-
|
35
|
-
def scp
|
34
|
+
|
35
|
+
def scp
|
36
36
|
@scp ||= Net::SCP.new(ssh)
|
37
37
|
end
|
38
38
|
|
@@ -40,7 +40,7 @@ module Gofer
|
|
40
40
|
stdout, stderr, output = '', '', ''
|
41
41
|
exit_code = 0
|
42
42
|
ssh.open_channel do |channel|
|
43
|
-
|
43
|
+
|
44
44
|
channel.exec(command) do |ch, success|
|
45
45
|
unless success
|
46
46
|
raise "Couldn't execute command #{command} (ssh channel failure)"
|
@@ -63,7 +63,7 @@ module Gofer
|
|
63
63
|
exit_code = data.read_long
|
64
64
|
channel.close # Necessary or backgrounded processes will 'hang' the channel
|
65
65
|
end
|
66
|
-
|
66
|
+
|
67
67
|
end
|
68
68
|
end
|
69
69
|
|
data/lib/gofer/version.rb
CHANGED
@@ -202,4 +202,44 @@ describe Gofer do
|
|
202
202
|
end
|
203
203
|
end
|
204
204
|
end
|
205
|
+
|
206
|
+
describe :cluster do
|
207
|
+
it "should run commands in parallel" do
|
208
|
+
cluster = Gofer::Cluster.new
|
209
|
+
# Cheat and use the same host repeatedly
|
210
|
+
host1 = Gofer::Host.new(HOSTNAME, USERNAME, :keys => [IDENTITY_FILE], :quiet => true)
|
211
|
+
host2 = Gofer::Host.new(HOSTNAME, USERNAME, :keys => [IDENTITY_FILE], :quiet => true)
|
212
|
+
cluster << host1
|
213
|
+
cluster << host2
|
214
|
+
|
215
|
+
results = cluster.run do |c|
|
216
|
+
c.run "date '+%s%N'; sleep 1; date '+%s%N'"
|
217
|
+
end
|
218
|
+
|
219
|
+
res1 = results[host1].stdout.lines.to_a
|
220
|
+
res2 = results[host2].stdout.lines.to_a
|
221
|
+
|
222
|
+
expect(res1[1].to_f).to be > res2[0].to_f
|
223
|
+
|
224
|
+
end
|
225
|
+
|
226
|
+
it "should respect max_concurrency" do
|
227
|
+
cluster = Gofer::Cluster.new
|
228
|
+
# Cheat and use the same host repeatedly
|
229
|
+
host1 = Gofer::Host.new(HOSTNAME, USERNAME, :keys => [IDENTITY_FILE], :quiet => true)
|
230
|
+
host2 = Gofer::Host.new(HOSTNAME, USERNAME, :keys => [IDENTITY_FILE], :quiet => true)
|
231
|
+
cluster << host1
|
232
|
+
cluster << host2
|
233
|
+
|
234
|
+
results = cluster.run(:max_concurrency => 1) do |c|
|
235
|
+
c.run "date '+%s%N'; sleep 1; date '+%s%N'"
|
236
|
+
end
|
237
|
+
|
238
|
+
res1 = results[host1].stdout.lines.to_a
|
239
|
+
res2 = results[host2].stdout.lines.to_a
|
240
|
+
|
241
|
+
expect(res2[0].to_f).to be >= res1[1].to_f
|
242
|
+
|
243
|
+
end
|
244
|
+
end
|
205
245
|
end
|
metadata
CHANGED
@@ -1,68 +1,56 @@
|
|
1
|
-
--- !ruby/object:Gem::Specification
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
2
|
name: gofer
|
3
|
-
version: !ruby/object:Gem::Version
|
4
|
-
|
5
|
-
prerelease:
|
6
|
-
segments:
|
7
|
-
- 0
|
8
|
-
- 2
|
9
|
-
- 6
|
10
|
-
version: 0.2.6
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.3.0
|
11
5
|
platform: ruby
|
12
|
-
authors:
|
6
|
+
authors:
|
13
7
|
- Michael Pearson
|
14
8
|
autorequire:
|
15
9
|
bindir: bin
|
16
10
|
cert_chain: []
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
- !ruby/object:Gem::Dependency
|
11
|
+
date: 2013-05-23 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
21
14
|
name: net-ssh
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
- - ">="
|
27
|
-
- !ruby/object:Gem::Version
|
28
|
-
hash: 33
|
29
|
-
segments:
|
30
|
-
- 2
|
31
|
-
- 0
|
32
|
-
- 23
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ! '>='
|
18
|
+
- !ruby/object:Gem::Version
|
33
19
|
version: 2.0.23
|
34
20
|
type: :runtime
|
35
|
-
version_requirements: *id001
|
36
|
-
- !ruby/object:Gem::Dependency
|
37
|
-
name: net-scp
|
38
21
|
prerelease: false
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ! '>='
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: 2.0.23
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: net-scp
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ! '>='
|
32
|
+
- !ruby/object:Gem::Version
|
49
33
|
version: 1.0.4
|
50
34
|
type: :runtime
|
51
|
-
|
52
|
-
|
53
|
-
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ! '>='
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: 1.0.4
|
41
|
+
description: ! '
|
42
|
+
|
54
43
|
Gofer provides a flexible and reliable model for performing tasks on remote
|
44
|
+
|
55
45
|
server using Net::SSH
|
56
46
|
|
57
|
-
|
47
|
+
'
|
48
|
+
email:
|
58
49
|
- mipearson@gmail.com
|
59
50
|
executables: []
|
60
|
-
|
61
51
|
extensions: []
|
62
|
-
|
63
52
|
extra_rdoc_files: []
|
64
|
-
|
65
|
-
files:
|
53
|
+
files:
|
66
54
|
- .gitignore
|
67
55
|
- Gemfile
|
68
56
|
- HISTORY.md
|
@@ -70,6 +58,7 @@ files:
|
|
70
58
|
- Rakefile
|
71
59
|
- gofer.gemspec
|
72
60
|
- lib/gofer.rb
|
61
|
+
- lib/gofer/cluster.rb
|
73
62
|
- lib/gofer/host.rb
|
74
63
|
- lib/gofer/response.rb
|
75
64
|
- lib/gofer/ssh_wrapper.rb
|
@@ -78,37 +67,27 @@ files:
|
|
78
67
|
- spec/spec_helper.rb
|
79
68
|
homepage: https://github.com/mipearson/gofer
|
80
69
|
licenses: []
|
81
|
-
|
70
|
+
metadata: {}
|
82
71
|
post_install_message:
|
83
72
|
rdoc_options: []
|
84
|
-
|
85
|
-
require_paths:
|
73
|
+
require_paths:
|
86
74
|
- lib
|
87
|
-
required_ruby_version: !ruby/object:Gem::Requirement
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
none: false
|
98
|
-
requirements:
|
99
|
-
- - ">="
|
100
|
-
- !ruby/object:Gem::Version
|
101
|
-
hash: 3
|
102
|
-
segments:
|
103
|
-
- 0
|
104
|
-
version: "0"
|
75
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
76
|
+
requirements:
|
77
|
+
- - ! '>='
|
78
|
+
- !ruby/object:Gem::Version
|
79
|
+
version: '0'
|
80
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
81
|
+
requirements:
|
82
|
+
- - ! '>='
|
83
|
+
- !ruby/object:Gem::Version
|
84
|
+
version: '0'
|
105
85
|
requirements: []
|
106
|
-
|
107
86
|
rubyforge_project:
|
108
|
-
rubygems_version:
|
87
|
+
rubygems_version: 2.0.3
|
109
88
|
signing_key:
|
110
|
-
specification_version:
|
89
|
+
specification_version: 4
|
111
90
|
summary: run commands on remote servers using SSH
|
112
|
-
test_files:
|
91
|
+
test_files:
|
113
92
|
- spec/gofer/integration_spec.rb
|
114
93
|
- spec/spec_helper.rb
|