ndo 0.1.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/Gemfile +10 -0
- data/Gemfile.lock +28 -0
- data/LICENSE +23 -0
- data/README +0 -0
- data/bin/ndo +43 -0
- data/lib/ndo.rb +4 -0
- data/lib/ndo/host.rb +80 -0
- data/lib/ndo/multi_command.rb +37 -0
- data/lib/ndo/results.rb +20 -0
- metadata +141 -0
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
GEM
|
2
|
+
remote: http://rubygems.org/
|
3
|
+
specs:
|
4
|
+
diff-lcs (1.1.2)
|
5
|
+
flexmock (0.8.11)
|
6
|
+
open4 (1.0.1)
|
7
|
+
procrastinate (0.2.0)
|
8
|
+
state_machine (~> 0.9.4)
|
9
|
+
rspec (2.3.0)
|
10
|
+
rspec-core (~> 2.3.0)
|
11
|
+
rspec-expectations (~> 2.3.0)
|
12
|
+
rspec-mocks (~> 2.3.0)
|
13
|
+
rspec-core (2.3.1)
|
14
|
+
rspec-expectations (2.3.0)
|
15
|
+
diff-lcs (~> 1.1.2)
|
16
|
+
rspec-mocks (2.3.0)
|
17
|
+
state_machine (0.9.4)
|
18
|
+
text-highlight (1.0.2)
|
19
|
+
|
20
|
+
PLATFORMS
|
21
|
+
ruby
|
22
|
+
|
23
|
+
DEPENDENCIES
|
24
|
+
flexmock
|
25
|
+
open4 (~> 1.0.1)
|
26
|
+
procrastinate (~> 0.2.0)
|
27
|
+
rspec
|
28
|
+
text-highlight
|
data/LICENSE
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
|
2
|
+
Copyright (c) 2010 Kaspar Schiess
|
3
|
+
|
4
|
+
Permission is hereby granted, free of charge, to any person
|
5
|
+
obtaining a copy of this software and associated documentation
|
6
|
+
files (the "Software"), to deal in the Software without
|
7
|
+
restriction, including without limitation the rights to use,
|
8
|
+
copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the
|
10
|
+
Software is furnished to do so, subject to the following
|
11
|
+
conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
18
|
+
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
20
|
+
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
21
|
+
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
22
|
+
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
23
|
+
OTHER DEALINGS IN THE SOFTWARE.
|
data/README
ADDED
File without changes
|
data/bin/ndo
ADDED
@@ -0,0 +1,43 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'optparse'
|
4
|
+
|
5
|
+
optparse = OptionParser.new do |opts|
|
6
|
+
opts.banner = "Usage: ndo [options] HOST_SET COMMAND"
|
7
|
+
end
|
8
|
+
optparse.parse!
|
9
|
+
|
10
|
+
unless ARGV.size >= 2
|
11
|
+
puts optparse
|
12
|
+
puts
|
13
|
+
puts "You need to specify at least a host set and a command."
|
14
|
+
exit 1
|
15
|
+
end
|
16
|
+
|
17
|
+
host_set, *cmd_parts = ARGV
|
18
|
+
command = cmd_parts.join(' ')
|
19
|
+
|
20
|
+
|
21
|
+
hosts = File.read(
|
22
|
+
File.join(ENV['HOME'], '.ndo', host_set)).
|
23
|
+
lines.map { |l| l.chomp.strip }
|
24
|
+
|
25
|
+
$:.unshift File.dirname(__FILE__) + "/../lib"
|
26
|
+
require 'ndo'
|
27
|
+
require 'text/highlight'
|
28
|
+
|
29
|
+
hl = Text::ANSIHighlighter.new
|
30
|
+
String.highlighter = hl
|
31
|
+
|
32
|
+
results = Ndo::MultiCommand.new(command, hosts).run
|
33
|
+
results.each do |host, output|
|
34
|
+
output.chomp!
|
35
|
+
if output.index("\n")
|
36
|
+
# Multiline output
|
37
|
+
output.gsub!(/\n/, "\n ")
|
38
|
+
output = " "+output
|
39
|
+
printf "%s {\n%s}\n\n", host.bold, output
|
40
|
+
else
|
41
|
+
printf "%-20s %s\n", host.bold, output
|
42
|
+
end
|
43
|
+
end
|
data/lib/ndo.rb
ADDED
data/lib/ndo/host.rb
ADDED
@@ -0,0 +1,80 @@
|
|
1
|
+
require 'stringio'
|
2
|
+
require 'open4'
|
3
|
+
|
4
|
+
class Ndo::Host
|
5
|
+
attr_reader :name
|
6
|
+
def initialize(hostname)
|
7
|
+
@name = hostname
|
8
|
+
end
|
9
|
+
|
10
|
+
class ExecutionFailure < StandardError
|
11
|
+
def initialize(message=nil, stdout=nil, stderr=nil)
|
12
|
+
super(message)
|
13
|
+
@stdout, @stderr = stdout, stderr
|
14
|
+
end
|
15
|
+
|
16
|
+
attr_reader :stdout, :stderr
|
17
|
+
|
18
|
+
def to_s
|
19
|
+
super + "\nstdout: #{stdout}\nstderr: #{stderr}"
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def run command
|
24
|
+
cmd = ['ssh', name, command].flatten
|
25
|
+
result = []
|
26
|
+
|
27
|
+
pid, inn, out, err = Open4.popen4(*cmd)
|
28
|
+
|
29
|
+
inn.sync = true
|
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
|
40
|
+
end
|
41
|
+
|
42
|
+
until streams.empty? do
|
43
|
+
# don't busy loop
|
44
|
+
selected, = select streams, nil, nil, 0.1
|
45
|
+
|
46
|
+
next if selected.nil? or selected.empty?
|
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
|
65
|
+
end
|
66
|
+
|
67
|
+
unless status.success? then
|
68
|
+
raise ExecutionFailure.new(
|
69
|
+
"Command failed (#{status.inspect})",
|
70
|
+
*streams.map { |strm| out_stream[strm].string }
|
71
|
+
)
|
72
|
+
end
|
73
|
+
|
74
|
+
out_stream.map { |io, copy| copy.string }
|
75
|
+
ensure
|
76
|
+
inn.close rescue nil
|
77
|
+
out.close rescue nil
|
78
|
+
err.close rescue nil
|
79
|
+
end
|
80
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
|
2
|
+
require 'ndo/results'
|
3
|
+
require 'ndo/host'
|
4
|
+
require 'procrastinate'
|
5
|
+
|
6
|
+
# A class to execute a command on a list of hosts in parallel; allows access
|
7
|
+
# to results and is thus a) multi threaded and b) Ruby 1.9.2 only.
|
8
|
+
#
|
9
|
+
class Ndo::MultiCommand
|
10
|
+
include Procrastinate
|
11
|
+
|
12
|
+
attr_reader :command
|
13
|
+
attr_reader :hosts
|
14
|
+
|
15
|
+
def initialize(command, hosts)
|
16
|
+
@command = command
|
17
|
+
@hosts = hosts
|
18
|
+
end
|
19
|
+
|
20
|
+
# Runs the command on all hosts. Returns a result collection.
|
21
|
+
#
|
22
|
+
def run
|
23
|
+
scheduler = Scheduler.start(SpawnStrategy::Throttled.new(5))
|
24
|
+
proxy = scheduler.create_proxy(self)
|
25
|
+
|
26
|
+
Ndo::Results.new.tap { |results|
|
27
|
+
hosts.each { |host|
|
28
|
+
results.store host, proxy.run_for_host(host)
|
29
|
+
}}
|
30
|
+
ensure
|
31
|
+
scheduler.shutdown
|
32
|
+
end
|
33
|
+
|
34
|
+
def run_for_host(host)
|
35
|
+
Ndo::Host.new(host).run(@command).first
|
36
|
+
end
|
37
|
+
end
|
data/lib/ndo/results.rb
ADDED
@@ -0,0 +1,20 @@
|
|
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| yield host, future.value }
|
14
|
+
end
|
15
|
+
|
16
|
+
def store(host, future)
|
17
|
+
@map.store host, future
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
metadata
ADDED
@@ -0,0 +1,141 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: ndo
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
prerelease: false
|
5
|
+
segments:
|
6
|
+
- 0
|
7
|
+
- 1
|
8
|
+
- 0
|
9
|
+
version: 0.1.0
|
10
|
+
platform: ruby
|
11
|
+
authors:
|
12
|
+
- Kaspar Schiess
|
13
|
+
autorequire:
|
14
|
+
bindir: bin
|
15
|
+
cert_chain: []
|
16
|
+
|
17
|
+
date: 2010-12-22 00:00:00 +01:00
|
18
|
+
default_executable:
|
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
|
36
|
+
name: procrastinate
|
37
|
+
prerelease: false
|
38
|
+
requirement: &id002 !ruby/object:Gem::Requirement
|
39
|
+
none: false
|
40
|
+
requirements:
|
41
|
+
- - ~>
|
42
|
+
- !ruby/object:Gem::Version
|
43
|
+
segments:
|
44
|
+
- 0
|
45
|
+
- 2
|
46
|
+
- 0
|
47
|
+
version: 0.2.0
|
48
|
+
type: :runtime
|
49
|
+
version_requirements: *id002
|
50
|
+
- !ruby/object:Gem::Dependency
|
51
|
+
name: text-highlight
|
52
|
+
prerelease: false
|
53
|
+
requirement: &id003 !ruby/object:Gem::Requirement
|
54
|
+
none: false
|
55
|
+
requirements:
|
56
|
+
- - ">="
|
57
|
+
- !ruby/object:Gem::Version
|
58
|
+
segments:
|
59
|
+
- 0
|
60
|
+
version: "0"
|
61
|
+
type: :runtime
|
62
|
+
version_requirements: *id003
|
63
|
+
- !ruby/object:Gem::Dependency
|
64
|
+
name: rspec
|
65
|
+
prerelease: false
|
66
|
+
requirement: &id004 !ruby/object:Gem::Requirement
|
67
|
+
none: false
|
68
|
+
requirements:
|
69
|
+
- - ">="
|
70
|
+
- !ruby/object:Gem::Version
|
71
|
+
segments:
|
72
|
+
- 0
|
73
|
+
version: "0"
|
74
|
+
type: :development
|
75
|
+
version_requirements: *id004
|
76
|
+
- !ruby/object:Gem::Dependency
|
77
|
+
name: flexmock
|
78
|
+
prerelease: false
|
79
|
+
requirement: &id005 !ruby/object:Gem::Requirement
|
80
|
+
none: false
|
81
|
+
requirements:
|
82
|
+
- - ">="
|
83
|
+
- !ruby/object:Gem::Version
|
84
|
+
segments:
|
85
|
+
- 0
|
86
|
+
version: "0"
|
87
|
+
type: :development
|
88
|
+
version_requirements: *id005
|
89
|
+
description:
|
90
|
+
email: kaspar.schiess@absurd.li
|
91
|
+
executables:
|
92
|
+
- ndo
|
93
|
+
extensions: []
|
94
|
+
|
95
|
+
extra_rdoc_files:
|
96
|
+
- README
|
97
|
+
files:
|
98
|
+
- Gemfile
|
99
|
+
- Gemfile.lock
|
100
|
+
- LICENSE
|
101
|
+
- README
|
102
|
+
- lib/ndo/host.rb
|
103
|
+
- lib/ndo/multi_command.rb
|
104
|
+
- lib/ndo/results.rb
|
105
|
+
- lib/ndo.rb
|
106
|
+
- bin/ndo
|
107
|
+
has_rdoc: true
|
108
|
+
homepage: http://blog.absurd.li
|
109
|
+
licenses: []
|
110
|
+
|
111
|
+
post_install_message:
|
112
|
+
rdoc_options:
|
113
|
+
- --main
|
114
|
+
- README
|
115
|
+
require_paths:
|
116
|
+
- lib
|
117
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
118
|
+
none: false
|
119
|
+
requirements:
|
120
|
+
- - ">="
|
121
|
+
- !ruby/object:Gem::Version
|
122
|
+
segments:
|
123
|
+
- 0
|
124
|
+
version: "0"
|
125
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
126
|
+
none: false
|
127
|
+
requirements:
|
128
|
+
- - ">="
|
129
|
+
- !ruby/object:Gem::Version
|
130
|
+
segments:
|
131
|
+
- 0
|
132
|
+
version: "0"
|
133
|
+
requirements: []
|
134
|
+
|
135
|
+
rubyforge_project:
|
136
|
+
rubygems_version: 1.3.7
|
137
|
+
signing_key:
|
138
|
+
specification_version: 3
|
139
|
+
summary: Execute commands on multiple hosts at once.
|
140
|
+
test_files: []
|
141
|
+
|