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 +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: []
|