ndo 0.2.1 → 0.2.2
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/Gemfile +8 -3
- data/Gemfile.lock +27 -14
- data/README +33 -0
- data/bin/ndo +4 -3
- data/lib/ndo.rb +6 -2
- data/lib/ndo/host.rb +56 -49
- data/lib/ndo/{multi_command.rb → multi_host.rb} +12 -15
- data/lib/ndo/popen.rb +54 -0
- data/lib/ndo/result.rb +31 -0
- metadata +56 -97
- data/lib/ndo/results.rb +0 -25
data/Gemfile
CHANGED
@@ -1,10 +1,15 @@
|
|
1
1
|
source "http://rubygems.org"
|
2
2
|
|
3
|
-
gem 'open4', '
|
4
|
-
gem 'procrastinate', '~> 0.3
|
5
|
-
gem 'text-highlight', '~> 1.0
|
3
|
+
gem 'open4', '>= 0.9'
|
4
|
+
gem 'procrastinate', '~> 0.3'
|
5
|
+
gem 'text-highlight', '~> 1.0'
|
6
6
|
|
7
7
|
group :development do
|
8
8
|
gem 'rspec'
|
9
9
|
gem 'flexmock'
|
10
|
+
|
11
|
+
gem 'guard'
|
12
|
+
gem 'guard-rspec'
|
13
|
+
gem 'rb-fsevent'
|
14
|
+
gem 'growl_notify'
|
10
15
|
end
|
data/Gemfile.lock
CHANGED
@@ -1,28 +1,41 @@
|
|
1
1
|
GEM
|
2
2
|
remote: http://rubygems.org/
|
3
3
|
specs:
|
4
|
-
diff-lcs (1.1.
|
5
|
-
flexmock (0.
|
6
|
-
|
7
|
-
|
4
|
+
diff-lcs (1.1.3)
|
5
|
+
flexmock (0.9.0)
|
6
|
+
growl_notify (0.0.1)
|
7
|
+
rb-appscript
|
8
|
+
guard (0.6.3)
|
9
|
+
thor (~> 0.14.6)
|
10
|
+
guard-rspec (0.4.5)
|
11
|
+
guard (>= 0.4.0)
|
12
|
+
open4 (1.1.0)
|
13
|
+
procrastinate (0.3.1)
|
8
14
|
state_machine (~> 0.9.4)
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
rspec-
|
13
|
-
|
14
|
-
|
15
|
+
rb-appscript (0.6.1)
|
16
|
+
rb-fsevent (0.4.3.1)
|
17
|
+
rspec (2.6.0)
|
18
|
+
rspec-core (~> 2.6.0)
|
19
|
+
rspec-expectations (~> 2.6.0)
|
20
|
+
rspec-mocks (~> 2.6.0)
|
21
|
+
rspec-core (2.6.4)
|
22
|
+
rspec-expectations (2.6.0)
|
15
23
|
diff-lcs (~> 1.1.2)
|
16
|
-
rspec-mocks (2.
|
24
|
+
rspec-mocks (2.6.0)
|
17
25
|
state_machine (0.9.4)
|
18
26
|
text-highlight (1.0.2)
|
27
|
+
thor (0.14.6)
|
19
28
|
|
20
29
|
PLATFORMS
|
21
30
|
ruby
|
22
31
|
|
23
32
|
DEPENDENCIES
|
24
33
|
flexmock
|
25
|
-
|
26
|
-
|
34
|
+
growl_notify
|
35
|
+
guard
|
36
|
+
guard-rspec
|
37
|
+
open4 (>= 0.9)
|
38
|
+
procrastinate (~> 0.3)
|
39
|
+
rb-fsevent
|
27
40
|
rspec
|
28
|
-
text-highlight (~> 1.0
|
41
|
+
text-highlight (~> 1.0)
|
data/README
CHANGED
@@ -0,0 +1,33 @@
|
|
1
|
+
ndo does things N times.
|
2
|
+
|
3
|
+
SYNOPSIS
|
4
|
+
|
5
|
+
mc = Ndo::MultiCommand.new('uname -n', %w(hostA hostB hostC))
|
6
|
+
results = mc.run
|
7
|
+
|
8
|
+
results['hostA'] # => 'hostA'
|
9
|
+
results.each do |result|
|
10
|
+
result # => "hostA", "hostB", "hostC"
|
11
|
+
end
|
12
|
+
|
13
|
+
ON THE COMMAND LINE
|
14
|
+
|
15
|
+
1) Create a host set
|
16
|
+
|
17
|
+
A host set is a file in below ~/.ndo that contains a list of host names,
|
18
|
+
separated by newlines. Easily generated.
|
19
|
+
|
20
|
+
2) Run a command on a host set
|
21
|
+
|
22
|
+
$ ndo my_host_set ls
|
23
|
+
callisto Tue Sep 13 09:17:41 CEST 2011
|
24
|
+
cyllene Tue Sep 13 09:17:41 CEST 2011
|
25
|
+
helike Tue Sep 13 09:17:41 CEST 2011
|
26
|
+
himalia Tue Sep 13 09:17:41 CEST 2011
|
27
|
+
|
28
|
+
As you can see, it appears that time synch works on these machines.
|
29
|
+
|
30
|
+
STATUS
|
31
|
+
|
32
|
+
This is a very early version; It could handle errors better. That said, ndo is
|
33
|
+
a very useful tool that can replace vlad or capistrano for easy things.
|
data/bin/ndo
CHANGED
@@ -29,9 +29,10 @@ require 'text/highlight'
|
|
29
29
|
hl = Text::ANSIHighlighter.new
|
30
30
|
String.highlighter = hl
|
31
31
|
|
32
|
-
results = Ndo::
|
33
|
-
results.each do |host,
|
34
|
-
output.chomp
|
32
|
+
results = Ndo::MultiHost.new(hosts).run(command)
|
33
|
+
results.each do |host, result|
|
34
|
+
output = result.stdout.chomp
|
35
|
+
|
35
36
|
if output.index("\n")
|
36
37
|
# Multiline output
|
37
38
|
output.gsub!(/\n/, "\n ")
|
data/lib/ndo.rb
CHANGED
data/lib/ndo/host.rb
CHANGED
@@ -1,6 +1,9 @@
|
|
1
1
|
require 'stringio'
|
2
2
|
require 'open4'
|
3
3
|
|
4
|
+
# Runs a command via ssh on a host. This is initially stolen from vlad (the
|
5
|
+
# deployer), then rewritten and modified.
|
6
|
+
#
|
4
7
|
class Ndo::Host
|
5
8
|
attr_reader :name
|
6
9
|
def initialize(hostname)
|
@@ -20,61 +23,65 @@ class Ndo::Host
|
|
20
23
|
end
|
21
24
|
end
|
22
25
|
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
streams = [out, err]
|
31
|
-
out_stream = {
|
32
|
-
out => StringIO.new,
|
33
|
-
err => StringIO.new,
|
34
|
-
}
|
35
|
-
|
36
|
-
# Handle process termination ourselves
|
37
|
-
status = nil
|
38
|
-
Thread.start do
|
39
|
-
status = Process.waitpid2(pid).last
|
26
|
+
class Accumulator
|
27
|
+
attr_reader :buffer
|
28
|
+
|
29
|
+
def initialize(stream)
|
30
|
+
@stream = stream
|
31
|
+
@buffer = ''
|
32
|
+
@eof = false
|
40
33
|
end
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
selected.each do |stream|
|
49
|
-
if stream.eof? then
|
50
|
-
streams.delete stream if status # we've quit, so no more writing
|
51
|
-
next
|
52
|
-
end
|
53
|
-
|
54
|
-
data = stream.readpartial(1024)
|
55
|
-
out_stream[stream].write data
|
56
|
-
#
|
57
|
-
# if stream == err and data =~ sudo_prompt then
|
58
|
-
# inn.puts sudo_password
|
59
|
-
# data << "\n"
|
60
|
-
# $stderr.write "\n"
|
61
|
-
# end
|
62
|
-
|
63
|
-
result << data
|
64
|
-
end
|
34
|
+
|
35
|
+
def copy_if_ready(ready_list)
|
36
|
+
return unless ready_list.include?(@stream)
|
37
|
+
|
38
|
+
@buffer << @stream.read_nonblock(1024)
|
39
|
+
rescue EOFError
|
40
|
+
@eof = true
|
65
41
|
end
|
66
|
-
|
67
|
-
|
42
|
+
|
43
|
+
def eof?
|
44
|
+
@eof
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def run(command)
|
49
|
+
cmd = ['ssh', name, command].flatten
|
50
|
+
|
51
|
+
process = Ndo.popen(*cmd)
|
52
|
+
accums = [
|
53
|
+
Accumulator.new(process.stdout),
|
54
|
+
Accumulator.new(process.stderr)]
|
55
|
+
|
56
|
+
# Copy stdout, stderr to buffers
|
57
|
+
loop do
|
58
|
+
ios = [process.stdout, process.stderr]
|
59
|
+
ready,_,_ = IO.select(ios)
|
60
|
+
|
61
|
+
# Test for process closed
|
62
|
+
break if accums.any? { |acc| acc.eof? }
|
63
|
+
|
64
|
+
# Copy data
|
65
|
+
accums.each { |acc| acc.copy_if_ready(ready) }
|
66
|
+
end
|
67
|
+
|
68
|
+
# We're done reading: prepare return value
|
69
|
+
buffers = accums.map { |acc| acc.buffer }
|
70
|
+
|
71
|
+
process.wait
|
72
|
+
|
73
|
+
# Raise ExecutionFailure if the command failed
|
74
|
+
unless process.success?
|
75
|
+
status = process.status
|
68
76
|
raise ExecutionFailure.new(
|
69
77
|
"Command failed (#{status.inspect})",
|
70
|
-
*
|
78
|
+
*buffers
|
71
79
|
)
|
72
80
|
end
|
73
|
-
|
74
|
-
|
81
|
+
|
82
|
+
# Return [STDOUT, STDERR] buffers
|
83
|
+
buffers
|
75
84
|
ensure
|
76
|
-
|
77
|
-
out.close rescue nil
|
78
|
-
err.close rescue nil
|
85
|
+
process.close_all
|
79
86
|
end
|
80
87
|
end
|
@@ -2,39 +2,36 @@
|
|
2
2
|
require 'procrastinate'
|
3
3
|
require 'procrastinate/implicit'
|
4
4
|
|
5
|
-
require 'ndo/results'
|
6
|
-
require 'ndo/host'
|
7
|
-
|
8
5
|
# A class to execute a command on a list of hosts in parallel; allows access
|
9
6
|
# to results and is thus a) multi threaded and b) Ruby 1.9.2 only.
|
10
7
|
#
|
11
|
-
class Ndo::
|
8
|
+
class Ndo::MultiHost
|
12
9
|
include Procrastinate
|
13
10
|
|
14
|
-
attr_reader :command
|
15
11
|
attr_reader :hosts
|
16
12
|
|
17
|
-
def initialize(
|
18
|
-
@command = command
|
13
|
+
def initialize(hosts)
|
19
14
|
@hosts = hosts
|
20
15
|
end
|
21
16
|
|
22
17
|
# Runs the command on all hosts. Returns a result collection.
|
23
18
|
#
|
24
|
-
def run
|
19
|
+
def run(command)
|
25
20
|
proxy = Procrastinate.proxy(self)
|
26
21
|
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
22
|
+
hosts.inject(Hash.new) do |hash, host_name|
|
23
|
+
hash[host_name] = Ndo::Result.new(
|
24
|
+
host_name,
|
25
|
+
proxy.run_for_host(command, host_name))
|
26
|
+
hash
|
27
|
+
end
|
31
28
|
end
|
32
29
|
|
33
|
-
def run_for_host(host)
|
30
|
+
def run_for_host(command, host)
|
34
31
|
begin
|
35
|
-
Ndo::Host.new(host).run(
|
32
|
+
Ndo::Host.new(host).run(command)
|
36
33
|
rescue => b
|
37
|
-
|
34
|
+
b
|
38
35
|
end
|
39
36
|
end
|
40
37
|
end
|
data/lib/ndo/popen.rb
ADDED
@@ -0,0 +1,54 @@
|
|
1
|
+
|
2
|
+
module Ndo
|
3
|
+
Process = Struct.new(:pid, :stdin, :stdout, :stderr) do
|
4
|
+
def status
|
5
|
+
wait unless @status
|
6
|
+
@status
|
7
|
+
end
|
8
|
+
def wait
|
9
|
+
_, @status = ::Process.waitpid2(pid)
|
10
|
+
end
|
11
|
+
def success?
|
12
|
+
status.success?
|
13
|
+
end
|
14
|
+
def close_all
|
15
|
+
[stdin, stdout, stderr].
|
16
|
+
each(&:close)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
Pipe = Struct.new(:read, :write)
|
21
|
+
|
22
|
+
# NOTE: This was using the Open4 gem previously. That method turned out to
|
23
|
+
# not be portable across rubies and operating systems, that's why we try
|
24
|
+
# our luck with maintaining this popen4 here.
|
25
|
+
#
|
26
|
+
def popen(*cmd)
|
27
|
+
c_in, c_out, c_err = 3.times.map { Pipe.new(*IO.pipe) }
|
28
|
+
|
29
|
+
pid = fork do
|
30
|
+
c_in.write.close
|
31
|
+
STDIN.reopen(c_in.read)
|
32
|
+
c_in.read.close
|
33
|
+
|
34
|
+
c_out.read.close
|
35
|
+
STDOUT.reopen(c_out.write)
|
36
|
+
c_out.write.close
|
37
|
+
|
38
|
+
c_err.read.close
|
39
|
+
STDERR.reopen(c_err.write)
|
40
|
+
c_err.write.close
|
41
|
+
|
42
|
+
exec(*cmd)
|
43
|
+
fail "NOT REACHED: EXEC FAILED"
|
44
|
+
end
|
45
|
+
|
46
|
+
c_in.read.close
|
47
|
+
c_out.write.close
|
48
|
+
c_err.write.close
|
49
|
+
|
50
|
+
# c_in.sync = true
|
51
|
+
Process.new(pid, c_in.write, c_out.read, c_err.read)
|
52
|
+
end
|
53
|
+
module_function :popen
|
54
|
+
end
|
data/lib/ndo/result.rb
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
|
2
|
+
# Represents a command result for a single host.
|
3
|
+
#
|
4
|
+
class Ndo::Result
|
5
|
+
attr_reader :host_name
|
6
|
+
attr_reader :future
|
7
|
+
|
8
|
+
def initialize(host_name, future)
|
9
|
+
@host_name, @future = host_name, future
|
10
|
+
end
|
11
|
+
|
12
|
+
def value
|
13
|
+
future.value
|
14
|
+
end
|
15
|
+
|
16
|
+
def stdout
|
17
|
+
value.first
|
18
|
+
end
|
19
|
+
|
20
|
+
def stderr
|
21
|
+
value.last
|
22
|
+
end
|
23
|
+
|
24
|
+
def success?
|
25
|
+
value.kind_of?(Array)
|
26
|
+
end
|
27
|
+
|
28
|
+
def exception
|
29
|
+
value
|
30
|
+
end
|
31
|
+
end
|
metadata
CHANGED
@@ -1,143 +1,102 @@
|
|
1
|
-
--- !ruby/object:Gem::Specification
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
2
|
name: ndo
|
3
|
-
version: !ruby/object:Gem::Version
|
4
|
-
|
5
|
-
|
6
|
-
- 0
|
7
|
-
- 2
|
8
|
-
- 1
|
9
|
-
version: 0.2.1
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.2.2
|
5
|
+
prerelease:
|
10
6
|
platform: ruby
|
11
|
-
authors:
|
7
|
+
authors:
|
12
8
|
- Kaspar Schiess
|
13
9
|
autorequire:
|
14
10
|
bindir: bin
|
15
11
|
cert_chain: []
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
dependencies:
|
20
|
-
- !ruby/object:Gem::Dependency
|
21
|
-
name: open4
|
22
|
-
prerelease: false
|
23
|
-
requirement: &id001 !ruby/object:Gem::Requirement
|
24
|
-
none: false
|
25
|
-
requirements:
|
26
|
-
- - ~>
|
27
|
-
- !ruby/object:Gem::Version
|
28
|
-
segments:
|
29
|
-
- 1
|
30
|
-
- 0
|
31
|
-
- 1
|
32
|
-
version: 1.0.1
|
33
|
-
type: :runtime
|
34
|
-
version_requirements: *id001
|
35
|
-
- !ruby/object:Gem::Dependency
|
12
|
+
date: 2011-09-13 00:00:00.000000000Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
36
15
|
name: procrastinate
|
37
|
-
|
38
|
-
requirement: &id002 !ruby/object:Gem::Requirement
|
16
|
+
requirement: &70263477941020 !ruby/object:Gem::Requirement
|
39
17
|
none: false
|
40
|
-
requirements:
|
18
|
+
requirements:
|
41
19
|
- - ~>
|
42
|
-
- !ruby/object:Gem::Version
|
43
|
-
|
44
|
-
- 0
|
45
|
-
- 3
|
46
|
-
- 0
|
47
|
-
version: 0.3.0
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '0.3'
|
48
22
|
type: :runtime
|
49
|
-
version_requirements: *id002
|
50
|
-
- !ruby/object:Gem::Dependency
|
51
|
-
name: text-highlight
|
52
23
|
prerelease: false
|
53
|
-
|
24
|
+
version_requirements: *70263477941020
|
25
|
+
- !ruby/object:Gem::Dependency
|
26
|
+
name: text-highlight
|
27
|
+
requirement: &70263477940560 !ruby/object:Gem::Requirement
|
54
28
|
none: false
|
55
|
-
requirements:
|
29
|
+
requirements:
|
56
30
|
- - ~>
|
57
|
-
- !ruby/object:Gem::Version
|
58
|
-
|
59
|
-
- 1
|
60
|
-
- 0
|
61
|
-
- 2
|
62
|
-
version: 1.0.2
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: '1.0'
|
63
33
|
type: :runtime
|
64
|
-
version_requirements: *id003
|
65
|
-
- !ruby/object:Gem::Dependency
|
66
|
-
name: rspec
|
67
34
|
prerelease: false
|
68
|
-
|
35
|
+
version_requirements: *70263477940560
|
36
|
+
- !ruby/object:Gem::Dependency
|
37
|
+
name: rspec
|
38
|
+
requirement: &70263477940180 !ruby/object:Gem::Requirement
|
69
39
|
none: false
|
70
|
-
requirements:
|
71
|
-
- -
|
72
|
-
- !ruby/object:Gem::Version
|
73
|
-
|
74
|
-
- 0
|
75
|
-
version: "0"
|
40
|
+
requirements:
|
41
|
+
- - ! '>='
|
42
|
+
- !ruby/object:Gem::Version
|
43
|
+
version: '0'
|
76
44
|
type: :development
|
77
|
-
version_requirements: *id004
|
78
|
-
- !ruby/object:Gem::Dependency
|
79
|
-
name: flexmock
|
80
45
|
prerelease: false
|
81
|
-
|
46
|
+
version_requirements: *70263477940180
|
47
|
+
- !ruby/object:Gem::Dependency
|
48
|
+
name: flexmock
|
49
|
+
requirement: &70263477939760 !ruby/object:Gem::Requirement
|
82
50
|
none: false
|
83
|
-
requirements:
|
84
|
-
- -
|
85
|
-
- !ruby/object:Gem::Version
|
86
|
-
|
87
|
-
- 0
|
88
|
-
version: "0"
|
51
|
+
requirements:
|
52
|
+
- - ! '>='
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
89
55
|
type: :development
|
90
|
-
|
56
|
+
prerelease: false
|
57
|
+
version_requirements: *70263477939760
|
91
58
|
description:
|
92
59
|
email: kaspar.schiess@absurd.li
|
93
|
-
executables:
|
60
|
+
executables:
|
94
61
|
- ndo
|
95
62
|
extensions: []
|
96
|
-
|
97
|
-
extra_rdoc_files:
|
63
|
+
extra_rdoc_files:
|
98
64
|
- README
|
99
|
-
files:
|
65
|
+
files:
|
100
66
|
- Gemfile
|
101
67
|
- Gemfile.lock
|
102
68
|
- LICENSE
|
103
69
|
- README
|
104
70
|
- lib/ndo/host.rb
|
105
|
-
- lib/ndo/
|
106
|
-
- lib/ndo/
|
71
|
+
- lib/ndo/multi_host.rb
|
72
|
+
- lib/ndo/popen.rb
|
73
|
+
- lib/ndo/result.rb
|
107
74
|
- lib/ndo.rb
|
108
75
|
- bin/ndo
|
109
|
-
has_rdoc: true
|
110
76
|
homepage: http://blog.absurd.li
|
111
77
|
licenses: []
|
112
|
-
|
113
78
|
post_install_message:
|
114
|
-
rdoc_options:
|
79
|
+
rdoc_options:
|
115
80
|
- --main
|
116
81
|
- README
|
117
|
-
require_paths:
|
82
|
+
require_paths:
|
118
83
|
- lib
|
119
|
-
required_ruby_version: !ruby/object:Gem::Requirement
|
84
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
120
85
|
none: false
|
121
|
-
requirements:
|
122
|
-
- -
|
123
|
-
- !ruby/object:Gem::Version
|
124
|
-
|
125
|
-
|
126
|
-
version: "0"
|
127
|
-
required_rubygems_version: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - ! '>='
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '0'
|
90
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
128
91
|
none: false
|
129
|
-
requirements:
|
130
|
-
- -
|
131
|
-
- !ruby/object:Gem::Version
|
132
|
-
|
133
|
-
- 0
|
134
|
-
version: "0"
|
92
|
+
requirements:
|
93
|
+
- - ! '>='
|
94
|
+
- !ruby/object:Gem::Version
|
95
|
+
version: '0'
|
135
96
|
requirements: []
|
136
|
-
|
137
97
|
rubyforge_project:
|
138
|
-
rubygems_version: 1.
|
98
|
+
rubygems_version: 1.8.10
|
139
99
|
signing_key:
|
140
100
|
specification_version: 3
|
141
101
|
summary: Execute commands on multiple hosts at once.
|
142
102
|
test_files: []
|
143
|
-
|
data/lib/ndo/results.rb
DELETED
@@ -1,25 +0,0 @@
|
|
1
|
-
|
2
|
-
class Ndo::Results
|
3
|
-
def initialize
|
4
|
-
@map = Hash.new
|
5
|
-
end
|
6
|
-
|
7
|
-
def [](host)
|
8
|
-
@map[host].value
|
9
|
-
end
|
10
|
-
|
11
|
-
include Enumerable
|
12
|
-
def each
|
13
|
-
@map.each { |host, future|
|
14
|
-
begin
|
15
|
-
yield host, future.value
|
16
|
-
rescue Procrastinate::ChildDeath
|
17
|
-
|
18
|
-
end }
|
19
|
-
end
|
20
|
-
|
21
|
-
def store(host, future)
|
22
|
-
@map.store host, future
|
23
|
-
end
|
24
|
-
end
|
25
|
-
|