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 CHANGED
@@ -1,10 +1,15 @@
1
1
  source "http://rubygems.org"
2
2
 
3
- gem 'open4', '~> 1.0.1'
4
- gem 'procrastinate', '~> 0.3.0'
5
- gem 'text-highlight', '~> 1.0.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.2)
5
- flexmock (0.8.11)
6
- open4 (1.0.1)
7
- procrastinate (0.3.0)
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
- 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
+ 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.3.0)
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
- open4 (~> 1.0.1)
26
- procrastinate (~> 0.3.0)
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.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::MultiCommand.new(command, hosts).run
33
- results.each do |host, output|
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
@@ -1,4 +1,8 @@
1
1
 
2
2
  module Ndo
3
- autoload :MultiCommand, 'ndo/multi_command'
4
- end
3
+ end
4
+
5
+ require 'ndo/host'
6
+ require 'ndo/popen'
7
+ require 'ndo/result'
8
+ require 'ndo/multi_host'
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
- 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
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
- 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
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
- unless status.success? then
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
- *streams.map { |strm| out_stream[strm].string }
78
+ *buffers
71
79
  )
72
80
  end
73
-
74
- out_stream.map { |io, copy| copy.string }
81
+
82
+ # Return [STDOUT, STDERR] buffers
83
+ buffers
75
84
  ensure
76
- inn.close rescue nil
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::MultiCommand
8
+ class Ndo::MultiHost
12
9
  include Procrastinate
13
10
 
14
- attr_reader :command
15
11
  attr_reader :hosts
16
12
 
17
- def initialize(command, hosts)
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
- Ndo::Results.new.tap { |results|
28
- hosts.each { |host|
29
- results.store host, proxy.run_for_host(host)
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(@command).first
32
+ Ndo::Host.new(host).run(command)
36
33
  rescue => b
37
- "Failure: #{b}"
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
- prerelease: false
5
- segments:
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
- date: 2010-12-28 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
12
+ date: 2011-09-13 00:00:00.000000000Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
36
15
  name: procrastinate
37
- prerelease: false
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
- segments:
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
- requirement: &id003 !ruby/object:Gem::Requirement
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
- segments:
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
- requirement: &id004 !ruby/object:Gem::Requirement
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
- segments:
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
- requirement: &id005 !ruby/object:Gem::Requirement
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
- segments:
87
- - 0
88
- version: "0"
51
+ requirements:
52
+ - - ! '>='
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
89
55
  type: :development
90
- version_requirements: *id005
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/multi_command.rb
106
- - lib/ndo/results.rb
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
- segments:
125
- - 0
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
- segments:
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.3.7
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
-