pdo 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +15 -0
- data/.gitignore +21 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +50 -0
- data/Rakefile +1 -0
- data/bin/pdo +98 -0
- data/conf/log4r.yaml +18 -0
- data/examples/pdo.yaml.example +83 -0
- data/lib/pdo/host.rb +191 -0
- data/lib/pdo/logging.rb +14 -0
- data/lib/pdo/pdoopts.rb +94 -0
- data/lib/pdo/task.rb +103 -0
- data/lib/pdo/version.rb +3 -0
- data/lib/pdo.rb +87 -0
- data/pdo.gemspec +25 -0
- metadata +76 -0
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
data/Gemfile
ADDED
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 :
|
data/lib/pdo/logging.rb
ADDED
@@ -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 :
|
data/lib/pdo/pdoopts.rb
ADDED
@@ -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 :
|
data/lib/pdo/version.rb
ADDED
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: []
|