ndo 0.2.1 → 0.2.2

Sign up to get free protection for your applications and to get access to all the features.
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
-