pdo 0.0.1

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.
checksums.yaml ADDED
@@ -0,0 +1,15 @@
1
+ ---
2
+ !binary "U0hBMQ==":
3
+ metadata.gz: !binary |-
4
+ ZjJkNzY1MWMyNjZiODFiMjY2ZDliZDIyZGMyNTkxNjM1MmJjNTQwYg==
5
+ data.tar.gz: !binary |-
6
+ NzVmYWRhOTQwMzdmNGYwOGY5YWVlN2ZjMzIzM2Y3ZWQ5MThjMjRmMQ==
7
+ !binary "U0hBNTEy":
8
+ metadata.gz: !binary |-
9
+ YjNlM2QyMGEyY2M0Nzg3YzFlODhlNWQzMGNlYWE4NGRhZmI0NWQ1NmYyYWZi
10
+ NTk3MDRiNWZjOWY2ZDFkYjQ5ZGI5ZDExZDU2ZGQwNmUwYWM0NjU1NzBjMWJi
11
+ OWQzMzZjYmRjZTI2YzE3MDNkMGFmMjdkMjc0MDU5Mzk4YzYyMmM=
12
+ data.tar.gz: !binary |-
13
+ OGJjMmU2MTk1YTJjMzAzNzIwY2E3NDBjNjc0MjAxYWMwZDVlMTExYWNjZTA3
14
+ NTFkMjk4Zjc2ZTg3YWUzMTVkMDE5ODQ3ZDY3YzQ3ZjcyNzY4MWNkMDFiYmE2
15
+ ZDk0ZWM3YzMyOTJkYWIzYTViZjVmNWI5MzJkYzE4ZWExMGY0YzE=
data/.gitignore ADDED
@@ -0,0 +1,21 @@
1
+ *.gem
2
+ *.log
3
+ *.rbc
4
+ .*.swp
5
+ .bundle
6
+ .config
7
+ .rvmrc
8
+ .yardoc
9
+ Gemfile.lock
10
+ InstalledFiles
11
+ _yardoc
12
+ coverage
13
+ doc/
14
+ lib/bundler/man
15
+ pdo.yaml
16
+ pkg
17
+ rdoc
18
+ spec/reports
19
+ test/tmp
20
+ test/version_tmp
21
+ tmp
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in pdo.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2013 bqbn
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following 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 OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,50 @@
1
+ # Pdo
2
+
3
+ pdo is a wrapper for running commands on or against multiple hosts at
4
+ the same time.
5
+
6
+ ## Installation
7
+
8
+ Add this line to your application's Gemfile:
9
+
10
+ gem 'pdo'
11
+
12
+ And then execute:
13
+
14
+ $ bundle
15
+
16
+ Or install it yourself as:
17
+
18
+ $ gem install pdo
19
+
20
+ ## Usage
21
+
22
+ The following usage examples assume the host definition file in the
23
+ example/ directory is used.
24
+
25
+ * to list the hosts or groups
26
+ $ pdo -g /
27
+ $ pdo -g dc0/perf
28
+
29
+ * to enumerate hosts in groups
30
+ $ pdo -g lab0 --enum
31
+ $ pdo -g dc0/perf --enum
32
+
33
+ * to count # of hosts in groups
34
+ $ pdo -g dc0/func,dc0/perf --count
35
+ $ pdo -g lab0/empty --count
36
+
37
+ * to run 'ls -l' on some hosts
38
+ $ pdo -g env0 -c 'ls -l' -t 3 -y
39
+
40
+ * to rsync some file to some hosts
41
+ $ pdo -g lab0/dc1/util -c "rsync -avc example.txt _HOST_:/var/tmp/" -l -t 5
42
+
43
+
44
+ ## Contributing
45
+
46
+ 1. Fork it
47
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
48
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
49
+ 4. Push to the branch (`git push origin my-new-feature`)
50
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
data/bin/pdo ADDED
@@ -0,0 +1,98 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ lib = File.expand_path("#{File.dirname(__FILE__)}/../lib")
4
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include? lib
5
+
6
+ #require 'pdo/version'
7
+
8
+ require 'ostruct'
9
+ require 'pdo'
10
+ require 'pdo/logging'
11
+ require 'pdo/host'
12
+ require 'pdo/pdoopts'
13
+ require 'pdo/task'
14
+ require 'pp'
15
+
16
+ logger = Logger['main'] || Logger['pdo']
17
+
18
+ opts = OpenStruct.new
19
+ # setting default values
20
+ opts.confirm_before_execute = true
21
+ opts.hosts = []
22
+ opts.local = false
23
+ opts.step = [0, 0]
24
+ opts.thread_num = 1
25
+ opts.nohush = false
26
+
27
+ begin
28
+ # parse ARGV; defaults in opts is overwritten if needed
29
+ opts = PdoOpts.parse(ARGV, opts)
30
+ logger.debug { opts }
31
+ rescue => err
32
+ logger.error { "#{err.class}: #{err.message}" }
33
+ logger.debug { err.backtrace.join "\n" }
34
+ exit 1
35
+ end
36
+
37
+ include Host
38
+ Host.load_file opts.host_definition_file
39
+
40
+ excludes = []
41
+ opts.e_groups and opts.e_groups.each do |group|
42
+ excludes += get_hosts(group, opts.step)
43
+ end
44
+
45
+ if opts.groups then
46
+ # the following all depend on existing of opts.groups option
47
+ if opts.count then
48
+ opts.groups.each do |group|
49
+ hosts = get_hosts(group, opts.step) - excludes
50
+ printf "%s: %d\n", group, hosts.size
51
+ end
52
+ exit 0
53
+ elsif opts.enum then
54
+ hosts = []
55
+ opts.groups.each do |group|
56
+ hosts += get_hosts(group, opts.step)
57
+ end
58
+ puts (hosts - excludes).join(' ')
59
+ exit 0
60
+ elsif opts.cmd then
61
+ hosts = []
62
+ opts.groups.each do |group|
63
+ hosts += get_hosts(group, opts.step)
64
+ end
65
+ hosts -= excludes
66
+
67
+ tasks = Task.new
68
+ hosts.each do |host|
69
+ ssh = SSHCmd.new(opts.local, opts.sshopts)
70
+ tasks.add [host, ssh.form(host, opts.cmd)]
71
+ end
72
+
73
+ if opts.confirm_before_execute
74
+ tasks.print_all
75
+ loop do
76
+ printf "proceed? (y/N) : "
77
+ response = gets.rstrip
78
+ if response == 'y'
79
+ break
80
+ elsif response == 'N' or response == ''
81
+ exit 2 # user given up
82
+ end
83
+ end
84
+ end
85
+ pdo = PDO.new(tasks, opts.thread_num)
86
+ pdo.run
87
+ else
88
+ opts.groups.each do |key|
89
+ printf "%s:\n", key
90
+ get_sub_groups(key).each do |sub_group|
91
+ printf "#{' ' * key.length}%s\n", sub_group
92
+ end
93
+ end
94
+ exit
95
+ end
96
+ end # end of "if opts.groups then"
97
+
98
+ # vim: set et ts=2 sts=2 sw=2 si sta :
data/conf/log4r.yaml ADDED
@@ -0,0 +1,18 @@
1
+ ---
2
+ log4r_config:
3
+ loggers:
4
+ - name : pdo
5
+ level : INFO
6
+ outputters:
7
+ - stderr
8
+
9
+ # define all outputters (incl. formatters)
10
+ outputters:
11
+ - type : StderrOutputter
12
+ name : stderr
13
+ level : INFO
14
+ formatter:
15
+ type : PatternFormatter
16
+ pattern: "%d, %C: %l, %m"
17
+
18
+ # vim: set et ts=2 sts=2 sw=2 si sta:
@@ -0,0 +1,83 @@
1
+ #
2
+ # This is an example host definition file.
3
+ #
4
+ # The host definition file must be a YAML (http://www.yaml.org/) file.
5
+ #
6
+ # The below rules should be followed when defining your environments
7
+ # and hosts.
8
+ #
9
+ # * A host name should be in the format of [user@]<host>[:port].
10
+ # * A group name should be in the format of
11
+ # <group1/>[group2[/group3[...]]]
12
+ # Note the trailing slash ("/") for "group1". Without it, "group1"
13
+ # will be deemed as a host name.
14
+ # * A group names must be an absolute path. The leading slash ("/")
15
+ # can be omitted.
16
+ # * The leaf node must be either a group or host name.
17
+
18
+
19
+ # A simple environment with 3 hosts.
20
+ env0:
21
+ - h1
22
+ - h2
23
+ - h3
24
+
25
+ # A data center that contains 2 environments
26
+ dc0:
27
+ func: # functional test environment
28
+ - h1.func.dc0
29
+ - h2.func.dc0
30
+
31
+ perf: # performance test environment
32
+ - dc0/env2/type1
33
+ - dc0/env2/type2
34
+ - h3.perf.dc0
35
+ - h4.perf.dc0
36
+
37
+ env2: # dc0/env2
38
+ type1:
39
+ - h1.type1.env2.dc0
40
+ - h2.type2.env2.dc0
41
+ type2:
42
+ - h1.type2.dc0
43
+ - h2.type2.dc0
44
+
45
+ # A lab that contains 2 data centers and more
46
+ lab0:
47
+ empty: # empty group is OK.
48
+
49
+ # a functional grouping containing two data centers and an
50
+ # extra host
51
+ func:
52
+ - lab0/dc1
53
+ - lab0/dc2
54
+ - funchost1
55
+
56
+ # lab0/dc1
57
+ dc1:
58
+ env1:
59
+ - lab0/dc1/util/admin
60
+ - host1.dc1
61
+ - host2.dc1
62
+ env2:
63
+ - lab0/dc1/util/tool
64
+ - host3.dc1
65
+ - host4.dc1
66
+ util:
67
+ admin:
68
+ - root@admin.example.com
69
+ tool:
70
+ - tools@tool.example.com:443
71
+
72
+ # lab0/dc2
73
+ dc2:
74
+ - host1.dc2
75
+ - host2.dc2
76
+
77
+ # ALL of it
78
+ all:
79
+ - env0/ # trailing slash is needed to indicate group names.
80
+ - lab0/
81
+ - dc0/
82
+
83
+ # vim: set et ts=2 sts=2 sw=2 si sta :
data/lib/pdo/host.rb ADDED
@@ -0,0 +1,191 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'log4r'
4
+ include Log4r
5
+
6
+ require 'pp'
7
+ require 'yaml'
8
+
9
+ module Host
10
+
11
+ @@logger = Logger[self.class.to_s] || Logger['pdo']
12
+
13
+ @@recursive_level = 9
14
+ def self.recursive_level=(level)
15
+ @@recursive_level = level
16
+ end
17
+ def self.recursive_level
18
+ @@recursive_level
19
+ end
20
+
21
+ def self.load_file(def_file=nil)
22
+ if def_file then
23
+ files = [ def_file ]
24
+ else
25
+ files = [ "/etc/pdo/pdo.yaml", "#{ENV['HOME']}/.pdo/pdo.yaml", ]
26
+ end
27
+
28
+ hosts = {}
29
+ files.each do |f|
30
+ begin
31
+ hosts.update(YAML::load_file(f))
32
+ rescue => ex
33
+ @@logger.warn { "#{ex.class}: #{ex.message}" }
34
+ @@logger.debug { ex.backtrace.join "\n" }
35
+ end
36
+ end
37
+ return @@host_hash = hosts
38
+
39
+ end
40
+
41
+ def expand_group(group, level)
42
+ # given a group, recursively expand it into a list of hosts.
43
+ # if the group can't expand to a list of hosts, return that group.
44
+ # if there are exception expanding the group, return an empty list.
45
+
46
+ hosts = []
47
+
48
+ level += 1
49
+ if level > @@recursive_level then
50
+ @@logger.warn "circle detected in the host definition file."
51
+ return []
52
+ end
53
+
54
+ # get the hash value
55
+ keys = group.gsub(/^\//,'').split('/')
56
+ list_of_hosts = @@host_hash
57
+ keys.each do |k|
58
+ begin
59
+ if list_of_hosts.key? k then
60
+ list_of_hosts = list_of_hosts[k]
61
+ else
62
+ # if any part of the group name is not a key in the
63
+ # host hash, then return the group.
64
+ return [ group ]
65
+ end
66
+ rescue => ex
67
+ @@logger.warn {
68
+ "failed to get hash value for #{group.inspect}. "\
69
+ 'possibly an error in the yaml file.'
70
+ }
71
+ @@logger.debug { ex.backtrace.join "\n" }
72
+ return []
73
+ end
74
+ end
75
+
76
+ # return empty array if the group is empty.
77
+ return [] if list_of_hosts.nil?
78
+
79
+ if list_of_hosts.is_a? Hash then
80
+ list_of_hosts.keys.each do |key|
81
+ hosts += expand_group("#{group}/#{key}".squeeze('/'), level)
82
+ end
83
+ elsif list_of_hosts.is_a? Array then
84
+ list_of_hosts.each do |h|
85
+ if not h.is_a? String then
86
+ @@logger.warn {
87
+ "invalid host or group format: #{h.inspect}\n"\
88
+ 'possibly an error in the yaml file.'
89
+ }
90
+ next
91
+ end
92
+ if h.include? "/" then
93
+ hosts += expand_group(h, level)
94
+ else
95
+ hosts << h
96
+ end
97
+ end
98
+ else
99
+ @@logger.fatal "I don't know how to handle this. "\
100
+ 'possibly an error in the yaml file.'
101
+ exit 1
102
+ end
103
+ return hosts
104
+
105
+ end # expand_group
106
+ private :expand_group
107
+
108
+ def get_hosts(group, step)
109
+ hosts = []
110
+ hosts += expand_group(group, 0)
111
+ hosts.uniq!
112
+ hosts = stepping hosts, step
113
+ return hosts
114
+
115
+ end
116
+
117
+ def stepping(hosts, step)
118
+ unless hosts.is_a? Array or step.is_a? Array then
119
+ @@logger.warn "both hosts or step should be array."
120
+ return nil
121
+ end
122
+
123
+ start, stride = step[0].to_i, step[1].to_i
124
+ # if stride > 0, stepping forward
125
+ # if stride < 0, stepping backward
126
+ # if stride == 0, no stepping
127
+ # if start or stride makes no sense, no stepping either
128
+ index = case
129
+ when stride > 0 then
130
+ (start-1...hosts.size).step(stride).to_a
131
+ when stride < 0 then
132
+ (-start+1..0).step(-stride).to_a.map {|x| -x}
133
+ else []
134
+ end
135
+
136
+ unless index.empty? then
137
+ alist = []
138
+ index.each do |i|
139
+ alist << hosts[i]
140
+ end
141
+ hosts = alist
142
+ end
143
+
144
+ return hosts
145
+ end
146
+
147
+ def show_hosts
148
+ pp @@host_hash
149
+ end
150
+
151
+ def get_sub_groups(group)
152
+
153
+ keys = group.gsub(/^\//, '').split('/')
154
+ pointer = @@host_hash
155
+ keys.each do |k|
156
+ begin
157
+ if pointer.key? k then
158
+ pointer = pointer[k]
159
+ else
160
+ # if any part of the given group name is not a key in the
161
+ # host hash, then return an empty array.
162
+ return [ ]
163
+ end
164
+ rescue => ex
165
+ @@logger.warn {
166
+ "failed to get hash value for #{group.inspect}. "\
167
+ 'maybe a wrong key is specifid?'
168
+ }
169
+ @@logger.debug { ex.backtrace.join "\n" }
170
+ return []
171
+ end
172
+ end
173
+
174
+ # return empty array if the group is empty.
175
+ return [] if pointer.nil?
176
+
177
+ if pointer.is_a? Hash then
178
+ return pointer.keys
179
+ elsif pointer.is_a? Array then
180
+ return pointer
181
+ else
182
+ @@logger.fatal "I don't know how to handle this. "\
183
+ 'possibly an error in the yaml file.'
184
+ exit 1
185
+ end
186
+
187
+ end
188
+
189
+ end
190
+
191
+ # vim: set et ts=2 sts=2 sw=2 si sta :
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'log4r'
4
+ require 'log4r/yamlconfigurator'
5
+ require 'log4r/outputter/datefileoutputter'
6
+
7
+ conf_dir = File.expand_path("#{File.dirname(__FILE__)}/../../conf")
8
+
9
+ [conf_dir, '/etc/pdo', "#{ENV['HOME']}/.pdo"].each { |dir|
10
+ conf = "#{dir}/log4r.yaml"
11
+ Log4r::YamlConfigurator.load_yaml_file conf if File.exists? conf
12
+ }
13
+
14
+ # vim: set et ts=2 sts=2 sw=2 si sta :
@@ -0,0 +1,94 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'optparse'
4
+
5
+ class PdoOpts
6
+ def self.parse(args, opts)
7
+ op = OptionParser.new
8
+ op.set_summary_width 15
9
+
10
+ op.banner = "pdo [opts]"
11
+ op.separator ""
12
+ op.separator "Specific options:"
13
+
14
+ op.on('-c CMD', 'the command to be executed',
15
+ "if CMD equals to '-', then read command from stdin") do |cmd|
16
+ if cmd == '-' then
17
+ opts.cmd = $stdin.read
18
+ else
19
+ opts.cmd = cmd
20
+ end
21
+ end
22
+
23
+ op.on('--count', 'count the number of hosts') do |count|
24
+ opts.count = true
25
+ end
26
+
27
+ op.on('--enum', 'enumerate the hosts') do |enum|
28
+ opts.enum = true
29
+ end
30
+
31
+ op.on('-f <name>',
32
+ 'name of the alternative host definition file') do |fn|
33
+ opts.host_definition_file = fn
34
+ end
35
+
36
+ op.on('-g <name1,name2,...>', Array,
37
+ 'comma separated group or host names') do |groups|
38
+ opts.groups = groups
39
+ end
40
+
41
+ op.on('-l', 'run the command CMD locally') do
42
+ opts.local = true
43
+ end
44
+
45
+ op.on('--step <m,n>', Array,
46
+ 'stepping the hosts, m and n must be integers;',
47
+ 'if n > 0, stepping forward; if n < 0 stepping',
48
+ 'backward') do |step|
49
+ opts.step = [step[0].to_i, step[1].to_i]
50
+ end
51
+
52
+ op.on('--sshopts <key1=val1,key2=val2,...>', Array,
53
+ 'ssh options as described in "man ssh_config"') do |sshopts|
54
+ opts.sshopts = {} unless opts.sshopts
55
+ sshopts.each do |opt|
56
+ key, val = opt.split '='
57
+ opts.sshopts[:"#{key}"] = val if key and val
58
+ end
59
+ end
60
+
61
+ op.on('-t INTEGER', Integer, 'number of threads') do |thread_num|
62
+ if thread_num < 0 then
63
+ raise ArgumentError,
64
+ "thread number must be greater or equal to zero"
65
+ end
66
+ opts.thread_num = thread_num
67
+ end
68
+
69
+ op.on('-x <name1,name2,...>', Array,
70
+ 'comma separated group or host names, which should be',
71
+ 'excluded from the final list') do |e_groups|
72
+ opts.e_groups = e_groups
73
+ end
74
+
75
+ op.on('-y', 'do not confirm, execute immediately') do
76
+ opts.confirm_before_execute = false
77
+ end
78
+
79
+ op.separator ""
80
+ op.separator "Common options:"
81
+
82
+ op.on_tail('-h', '--help', 'this help') do
83
+ puts op.help
84
+ exit 0
85
+ end
86
+
87
+ op.parse!(args)
88
+ return opts
89
+
90
+ end # self.parse()
91
+
92
+ end
93
+
94
+ # vim: set et ts=2 sts=2 sw=2 si sta :
data/lib/pdo/task.rb ADDED
@@ -0,0 +1,103 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'log4r'
4
+ include Log4r
5
+
6
+ class SSHCmd
7
+ def initialize(local, sshopts)
8
+ @logger = Logger[self.class.to_s] || Logger['pdo']
9
+
10
+ @defaults = {
11
+ :user => 'root',
12
+ :port => '22',
13
+ :ssh => '/usr/bin/ssh',
14
+ :sshopts => {
15
+ :ConnectTimeout => '60',
16
+ :StrictHostKeyChecking => 'no',
17
+ },
18
+ }
19
+ @local = local
20
+ @sshopts = @defaults[:sshopts]
21
+ @sshopts = @defaults[:sshopts].update sshopts if sshopts
22
+ @logger.debug { @sshopts.inspect }
23
+ end
24
+
25
+ def form(host, cmd)
26
+ if host.match(/^(?:(\w+)@)?(\w+(?:\.\w+)*)(?::(\d+))?$/) then
27
+ user, host, port = $1, $2, $3
28
+ end
29
+
30
+ cmd = cmd.dup # without dup, all tasks refer to the same
31
+ cmd.strip!
32
+ cmd.gsub! '_USER_', user ? user : @defaults[:user]
33
+ cmd.gsub! '_HOST_', host
34
+ cmd.gsub! '_PORT_', port ? port : @defaults[:port]
35
+ # when opts.cmd == '-', the command is read from stdin.
36
+ # in this case multiple lines can be entered. here I'm doing some
37
+ # simple substitution for "\n".
38
+ cmd.gsub! "\n", '; '
39
+
40
+ # escape "`$ characters in cmd, unless they're already escaped.
41
+ if RUBY_VERSION.to_f < 1.9 then
42
+ # v1.8 dose not support negative look-behind assertion, thus
43
+ # doing it in 2 steps.
44
+ if cmd.match /["`$]/ then
45
+ if not cmd.match /['\\]["`$]/ then
46
+ cmd.gsub! /(["`$])/, '\\\\\1'
47
+ end
48
+ end
49
+ else
50
+ cmd.gsub! /(?<!['\\])(["`$])/, '\\\\\1'
51
+ end
52
+
53
+ if @local then
54
+ return cmd
55
+ else
56
+ ssh = @defaults[:ssh]
57
+ ssh = [ ssh, '-l', "#{user}"].join ' ' if user
58
+ ssh = [ ssh, '-p', "#{port}"].join ' ' if port
59
+ @sshopts.each do |k, v|
60
+ ssh = [ssh, '-o', "#{k}=#{v}"].join ' '
61
+ end
62
+ ssh = [ ssh, "#{host}", "\"#{cmd}\""].join ' '
63
+ return ssh
64
+ end
65
+ end
66
+ end
67
+
68
+ class Task
69
+
70
+ def initialize
71
+ @logger = Logger[self.class.to_s] || Logger['pdo']
72
+ @task_q = Queue.new
73
+ end
74
+
75
+ def add(task)
76
+ @task_q << task
77
+ end
78
+
79
+ def next
80
+ begin
81
+ @task_q.deq(non_block=true)
82
+ rescue ThreadError
83
+ nil
84
+ end
85
+ end
86
+
87
+ def size
88
+ @task_q.length
89
+ end
90
+
91
+ def print_all
92
+ tmp_q = Queue.new
93
+ while not @task_q.empty? do
94
+ task = @task_q.deq
95
+ printf "%s\n", task
96
+ tmp_q << task
97
+ end
98
+ @task_q = tmp_q
99
+ end
100
+
101
+ end
102
+
103
+ # vim: set et ts=2 sts=2 sw=2 si sta :
@@ -0,0 +1,3 @@
1
+ module Pdo
2
+ VERSION = "0.0.1"
3
+ end
data/lib/pdo.rb ADDED
@@ -0,0 +1,87 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ include Process
4
+
5
+ class PDO
6
+ def initialize(tasks, thread_num)
7
+ @logger = Logger[self.class.to_s] || Logger['pdo']
8
+ @tasks = tasks
9
+ @thread_num = thread_num
10
+ end
11
+
12
+ def run
13
+ # run first spawns a set of threads, tells them to execute the task
14
+ # queue. it then loop through the current Thread list (note
15
+ # Thread.list returns all running and sleeping threads), and outputs
16
+ # those new data.
17
+ # for the spawned threads, they execute one task, then wait for their
18
+ # data to be picked up by the main thread, then do the next task.
19
+
20
+ return 1 if @tasks.size == 0
21
+
22
+ n = @thread_num < @tasks.size ? @thread_num : @tasks.size
23
+ 1.upto(n) do
24
+ Thread.new do
25
+ while true do
26
+ @logger.info "#{Thread.current.object_id} started."
27
+ begin
28
+ execute(@tasks.next)
29
+ # presumably new data is ready, thus i stop.
30
+ # main thread will read :output and then wakes me up.
31
+ Thread.stop
32
+ rescue
33
+ @logger.info "No more task for #{Thread.current.object_id}."
34
+ break
35
+ end
36
+ end
37
+ end
38
+ end
39
+
40
+ loop do
41
+ # break because the only left thread must be the main thread.
42
+ break if Thread.list.size == 1
43
+
44
+ Thread.list.each do |t|
45
+ next if t == Thread.main
46
+ if t.key? :output and t.key? :new_data and t[:new_data] then
47
+ @logger.info "#{t.object_id} finished."
48
+ puts "=== #{t[:target]} ==="
49
+ t[:output].each {|x| puts x}
50
+ # puts t[:output].join('').gsub("\n", ' | ').chomp(' | ')
51
+ t[:new_data] = false
52
+ # wakes up the thread
53
+ t.run
54
+ end
55
+ end
56
+ end
57
+ end
58
+
59
+ def execute(task)
60
+ # execute forks a child process to run the task, then saves the
61
+ # output from the child process into a thread local variable, marks
62
+ # the :new_data tag.
63
+
64
+ raise "no task" if task.nil?
65
+
66
+ target, cmd = task
67
+
68
+ open("|-") do |child_io|
69
+ if child_io
70
+ # The waitpid statement below causes the program to hang when
71
+ # running some commands, such as wget.
72
+ # waitpid child_io.pid
73
+ Thread.current[:output] = child_io.readlines
74
+ Thread.current[:new_data] = true
75
+ Thread.current[:target] = target
76
+ else
77
+ STDIN.close
78
+ STDERR.reopen(STDOUT)
79
+ exec(*cmd)
80
+ end
81
+ end
82
+
83
+ end
84
+
85
+ end
86
+
87
+ # vim: set et ts=2 sts=2 sw=2 si sta :
data/pdo.gemspec ADDED
@@ -0,0 +1,25 @@
1
+ # -*- encoding: utf-8 -*-
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'pdo/version'
5
+
6
+ Gem::Specification.new do |gem|
7
+ gem.name = "pdo"
8
+ gem.licenses = "MIT"
9
+ gem.version = Pdo::VERSION
10
+ gem.authors = ["bqbn"]
11
+ gem.email = ["bqbn@openken.com"]
12
+ gem.description = 'pdo is a wrapper for running commands on or '\
13
+ 'against multiple hosts at the same time.'
14
+ gem.summary = 'pdo is a wrapper for running commands on or '\
15
+ 'against multiple hosts at the same time.'
16
+ gem.homepage = "https://github.com/bqbn/pdo"
17
+
18
+ gem.files = `git ls-files`.split($/)
19
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
20
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
21
+ gem.require_paths = ["lib"]
22
+
23
+ gem.add_dependency('log4r', '> 1.1.9')
24
+ end
25
+ # vim: set et ts=2 sts=2 sw=2 si sta :
metadata ADDED
@@ -0,0 +1,76 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: pdo
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - bqbn
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2013-03-10 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: log4r
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ! '>'
18
+ - !ruby/object:Gem::Version
19
+ version: 1.1.9
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ! '>'
25
+ - !ruby/object:Gem::Version
26
+ version: 1.1.9
27
+ description: pdo is a wrapper for running commands on or against multiple hosts at
28
+ the same time.
29
+ email:
30
+ - bqbn@openken.com
31
+ executables:
32
+ - pdo
33
+ extensions: []
34
+ extra_rdoc_files: []
35
+ files:
36
+ - .gitignore
37
+ - Gemfile
38
+ - LICENSE.txt
39
+ - README.md
40
+ - Rakefile
41
+ - bin/pdo
42
+ - conf/log4r.yaml
43
+ - examples/pdo.yaml.example
44
+ - lib/pdo.rb
45
+ - lib/pdo/host.rb
46
+ - lib/pdo/logging.rb
47
+ - lib/pdo/pdoopts.rb
48
+ - lib/pdo/task.rb
49
+ - lib/pdo/version.rb
50
+ - pdo.gemspec
51
+ homepage: https://github.com/bqbn/pdo
52
+ licenses:
53
+ - MIT
54
+ metadata: {}
55
+ post_install_message:
56
+ rdoc_options: []
57
+ require_paths:
58
+ - lib
59
+ required_ruby_version: !ruby/object:Gem::Requirement
60
+ requirements:
61
+ - - ! '>='
62
+ - !ruby/object:Gem::Version
63
+ version: '0'
64
+ required_rubygems_version: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ! '>='
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ requirements: []
70
+ rubyforge_project:
71
+ rubygems_version: 2.0.2
72
+ signing_key:
73
+ specification_version: 4
74
+ summary: pdo is a wrapper for running commands on or against multiple hosts at the
75
+ same time.
76
+ test_files: []