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 ADDED
@@ -0,0 +1,10 @@
1
+ source "http://rubygems.org"
2
+
3
+ gem 'open4', '~> 1.0.1'
4
+ gem 'procrastinate', '~> 0.2.0'
5
+ gem 'text-highlight'
6
+
7
+ group :development do
8
+ gem 'rspec'
9
+ gem 'flexmock'
10
+ end
@@ -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
@@ -0,0 +1,4 @@
1
+
2
+ module Ndo
3
+ autoload :MultiCommand, 'ndo/multi_command'
4
+ end
@@ -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
@@ -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
+