oxidized 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/CHANGELOG.md +2 -0
- data/Gemfile +3 -0
- data/README.md +133 -0
- data/Rakefile +46 -0
- data/TODO.md +20 -0
- data/bin/oxidized +13 -0
- data/extra/rest_client.rb +25 -0
- data/extra/syslog.rb +110 -0
- data/lib/oxidized.rb +6 -0
- data/lib/oxidized/cli.rb +42 -0
- data/lib/oxidized/config.rb +55 -0
- data/lib/oxidized/config/vars.rb +10 -0
- data/lib/oxidized/core.rb +41 -0
- data/lib/oxidized/input/cli.rb +48 -0
- data/lib/oxidized/input/input.rb +19 -0
- data/lib/oxidized/input/ssh.rb +111 -0
- data/lib/oxidized/input/telnet.rb +145 -0
- data/lib/oxidized/job.rb +14 -0
- data/lib/oxidized/jobs.rb +24 -0
- data/lib/oxidized/log.rb +21 -0
- data/lib/oxidized/manager.rb +57 -0
- data/lib/oxidized/model/acos.rb +69 -0
- data/lib/oxidized/model/aireos.rb +55 -0
- data/lib/oxidized/model/aos.rb +38 -0
- data/lib/oxidized/model/aos7.rb +58 -0
- data/lib/oxidized/model/aosw.rb +43 -0
- data/lib/oxidized/model/eos.rb +32 -0
- data/lib/oxidized/model/fortios.rb +44 -0
- data/lib/oxidized/model/ios.rb +63 -0
- data/lib/oxidized/model/iosxr.rb +47 -0
- data/lib/oxidized/model/ironware.rb +33 -0
- data/lib/oxidized/model/junos.rb +56 -0
- data/lib/oxidized/model/model.rb +152 -0
- data/lib/oxidized/model/powerconnect.rb +38 -0
- data/lib/oxidized/model/procurve.rb +45 -0
- data/lib/oxidized/model/timos.rb +45 -0
- data/lib/oxidized/node.rb +169 -0
- data/lib/oxidized/node/stats.rb +33 -0
- data/lib/oxidized/nodes.rb +143 -0
- data/lib/oxidized/output/file.rb +42 -0
- data/lib/oxidized/output/git.rb +78 -0
- data/lib/oxidized/output/output.rb +5 -0
- data/lib/oxidized/source/csv.rb +42 -0
- data/lib/oxidized/source/source.rb +11 -0
- data/lib/oxidized/source/sql.rb +61 -0
- data/lib/oxidized/string.rb +13 -0
- data/lib/oxidized/worker.rb +54 -0
- data/oxidized.gemspec +20 -0
- data/spec/nodes_spec.rb +46 -0
- metadata +136 -0
@@ -0,0 +1,10 @@
|
|
1
|
+
module Oxidized::Config::Vars
|
2
|
+
# convenience method for accessing node, group or global level user variables
|
3
|
+
# nil values will be ignored
|
4
|
+
def vars name
|
5
|
+
r = @node.vars[name] unless @node.vars.nil?
|
6
|
+
r ||= Oxidized::CFG.groups[@node.group].vars[name.to_s] if Oxidized::CFG.groups.has_key?(@node.group)
|
7
|
+
r ||= Oxidized::CFG.vars[name.to_s] if Oxidized::CFG.vars.has_key?(name)
|
8
|
+
r
|
9
|
+
end
|
10
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
module Oxidized
|
2
|
+
require 'oxidized/log'
|
3
|
+
require 'oxidized/string'
|
4
|
+
require 'oxidized/config'
|
5
|
+
require 'oxidized/config/vars'
|
6
|
+
require 'oxidized/worker'
|
7
|
+
require 'oxidized/nodes'
|
8
|
+
require 'oxidized/manager'
|
9
|
+
class << self
|
10
|
+
def new *args
|
11
|
+
Core.new args
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
class Core
|
16
|
+
def initialize args
|
17
|
+
Oxidized.mgr = Manager.new
|
18
|
+
nodes = Nodes.new
|
19
|
+
@worker = Worker.new nodes
|
20
|
+
if CFG.rest?
|
21
|
+
begin
|
22
|
+
require 'oxidized/web'
|
23
|
+
rescue LoadError
|
24
|
+
raise OxidizedError, 'oxidized-web not found: sudo gem install oxidized-web - or disable web support by setting "rest: false" in your configuration'
|
25
|
+
end
|
26
|
+
@rest = API::Web.new nodes, CFG.rest
|
27
|
+
@rest.run
|
28
|
+
end
|
29
|
+
run
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
def run
|
35
|
+
while true
|
36
|
+
@worker.work
|
37
|
+
sleep Config::Sleep
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
module Oxidized
|
2
|
+
class Input
|
3
|
+
module CLI
|
4
|
+
|
5
|
+
def initialize
|
6
|
+
@post_login = []
|
7
|
+
@pre_logout = []
|
8
|
+
@username, @password, @exec = nil
|
9
|
+
end
|
10
|
+
|
11
|
+
def get
|
12
|
+
connect_cli
|
13
|
+
d = @node.model.get
|
14
|
+
disconnect
|
15
|
+
d
|
16
|
+
end
|
17
|
+
|
18
|
+
def connect_cli
|
19
|
+
@post_login.each { |command, block| block ? block.call : (cmd command) }
|
20
|
+
end
|
21
|
+
|
22
|
+
def disconnect_cli
|
23
|
+
@pre_logout.each { |command, block| block ? block.call : (cmd command, nil) }
|
24
|
+
end
|
25
|
+
|
26
|
+
def post_login _post_login=nil, &block
|
27
|
+
unless @exec
|
28
|
+
@post_login << [_post_login, block]
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def pre_logout _pre_logout=nil, &block
|
33
|
+
unless @exec
|
34
|
+
@pre_logout << [_pre_logout, block]
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def username re=/^(Username|login)/
|
39
|
+
@username or @username = re
|
40
|
+
end
|
41
|
+
|
42
|
+
def password re=/^Password/
|
43
|
+
@password or @password = re
|
44
|
+
end
|
45
|
+
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module Oxidized
|
2
|
+
class Input
|
3
|
+
include Oxidized::Config::Vars
|
4
|
+
|
5
|
+
RescueFail = {
|
6
|
+
:debug => [
|
7
|
+
Errno::ECONNREFUSED,
|
8
|
+
],
|
9
|
+
:warn => [
|
10
|
+
IOError,
|
11
|
+
Timeout::Error,
|
12
|
+
Errno::ECONNRESET,
|
13
|
+
Errno::EHOSTUNREACH,
|
14
|
+
Errno::ENETUNREACH,
|
15
|
+
Errno::EPIPE,
|
16
|
+
],
|
17
|
+
}
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,111 @@
|
|
1
|
+
module Oxidized
|
2
|
+
require 'net/ssh'
|
3
|
+
require 'timeout'
|
4
|
+
require 'oxidized/input/cli'
|
5
|
+
class SSH < Input
|
6
|
+
RescueFail = {
|
7
|
+
:debug => [
|
8
|
+
Net::SSH::Disconnect,
|
9
|
+
],
|
10
|
+
:warn => [
|
11
|
+
RuntimeError,
|
12
|
+
Net::SSH::AuthenticationFailed,
|
13
|
+
],
|
14
|
+
}
|
15
|
+
include Input::CLI
|
16
|
+
class NoShell < OxidizedError; end
|
17
|
+
|
18
|
+
def connect node
|
19
|
+
@node = node
|
20
|
+
@output = ''
|
21
|
+
@node.model.cfg['ssh'].each { |cb| instance_exec(&cb) }
|
22
|
+
secure = CFG.input.ssh.secure
|
23
|
+
@ssh = Net::SSH.start @node.ip, @node.auth[:username],
|
24
|
+
:password => @node.auth[:password], :timeout => CFG.timeout,
|
25
|
+
:paranoid => secure
|
26
|
+
unless @exec
|
27
|
+
shell_open @ssh
|
28
|
+
@username ? shell_login : expect(@node.prompt)
|
29
|
+
end
|
30
|
+
connected?
|
31
|
+
end
|
32
|
+
|
33
|
+
def connected?
|
34
|
+
@ssh and not @ssh.closed?
|
35
|
+
end
|
36
|
+
|
37
|
+
def cmd cmd, expect=@node.prompt
|
38
|
+
Log.debug "SSH: #{cmd} @ #{@node.name}"
|
39
|
+
if @exec
|
40
|
+
@ssh.exec! cmd
|
41
|
+
else
|
42
|
+
cmd_shell(cmd, expect).gsub(/\r\n/, "\n")
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def send data
|
47
|
+
@ses.send_data data
|
48
|
+
end
|
49
|
+
|
50
|
+
def output
|
51
|
+
@output
|
52
|
+
end
|
53
|
+
|
54
|
+
private
|
55
|
+
|
56
|
+
def disconnect
|
57
|
+
disconnect_cli
|
58
|
+
# if disconnect does not disconnect us, give up after timeout
|
59
|
+
Timeout::timeout(CFG.timeout) { @ssh.loop }
|
60
|
+
rescue Errno::ECONNRESET, Net::SSH::Disconnect, IOError
|
61
|
+
ensure
|
62
|
+
@ssh.close if not @ssh.closed?
|
63
|
+
end
|
64
|
+
|
65
|
+
def shell_open ssh
|
66
|
+
@ses = ssh.open_channel do |ch|
|
67
|
+
ch.on_data do |_ch, data|
|
68
|
+
@output << data
|
69
|
+
@output = @node.model.expects @output
|
70
|
+
end
|
71
|
+
ch.request_pty do |_ch, success_pty|
|
72
|
+
raise NoShell, "Can't get PTY" unless success_pty
|
73
|
+
ch.send_channel_request 'shell' do |_ch, success_shell|
|
74
|
+
raise NoShell, "Can't get shell" unless success_shell
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
# Cisco WCS has extremely dubious SSH implementation, SSH auth is always
|
81
|
+
# success, it always opens shell and then run auth in shell. I guess
|
82
|
+
# they'll never support exec() :)
|
83
|
+
def shell_login
|
84
|
+
expect username
|
85
|
+
cmd @node.auth[:username], password
|
86
|
+
cmd @node.auth[:password]
|
87
|
+
end
|
88
|
+
|
89
|
+
def exec state=nil
|
90
|
+
state == nil ? @exec : (@exec=state)
|
91
|
+
end
|
92
|
+
|
93
|
+
def cmd_shell(cmd, expect_re)
|
94
|
+
@output = ''
|
95
|
+
@ses.send_data cmd + "\n"
|
96
|
+
@ses.process
|
97
|
+
expect expect_re if expect_re
|
98
|
+
@output
|
99
|
+
end
|
100
|
+
|
101
|
+
def expect regexp
|
102
|
+
Timeout::timeout(CFG.timeout) do
|
103
|
+
@ssh.loop(0.1) do
|
104
|
+
sleep 0.1
|
105
|
+
not @output.match regexp
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
end
|
111
|
+
end
|
@@ -0,0 +1,145 @@
|
|
1
|
+
module Oxidized
|
2
|
+
require 'net/telnet'
|
3
|
+
require 'oxidized/input/cli'
|
4
|
+
class Telnet < Input
|
5
|
+
RescueFail = {}
|
6
|
+
include Input::CLI
|
7
|
+
attr_reader :telnet
|
8
|
+
|
9
|
+
def connect node
|
10
|
+
@node = node
|
11
|
+
@timeout = CFG.timeout
|
12
|
+
@node.model.cfg['telnet'].each { |cb| instance_exec(&cb) }
|
13
|
+
@telnet = Net::Telnet.new 'Host' => @node.ip, 'Timeout' => @timeout,
|
14
|
+
'Model' => @node.model
|
15
|
+
expect username
|
16
|
+
@telnet.puts @node.auth[:username]
|
17
|
+
expect password
|
18
|
+
@telnet.puts @node.auth[:password]
|
19
|
+
expect @node.prompt
|
20
|
+
end
|
21
|
+
|
22
|
+
def connected?
|
23
|
+
@telnet and not @telnet.sock.closed?
|
24
|
+
end
|
25
|
+
|
26
|
+
def cmd cmd, expect=@node.prompt
|
27
|
+
Log.debug "Telnet: #{cmd} @#{@node.name}"
|
28
|
+
args = { 'String' => cmd }
|
29
|
+
args.merge!({ 'Match' => expect, 'Timeout' => @timeout }) if expect
|
30
|
+
@telnet.cmd args
|
31
|
+
end
|
32
|
+
|
33
|
+
def send data
|
34
|
+
@telnet.write data
|
35
|
+
end
|
36
|
+
|
37
|
+
def output
|
38
|
+
@telnet.output
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
|
43
|
+
def expect re
|
44
|
+
@telnet.waitfor 'Match' => re, 'Timeout' => @timeout
|
45
|
+
end
|
46
|
+
|
47
|
+
def disconnect
|
48
|
+
begin
|
49
|
+
disconnect_cli
|
50
|
+
@telnet.close
|
51
|
+
rescue Errno::ECONNRESET
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
|
59
|
+
class Net::Telnet
|
60
|
+
## FIXME: we just need 'line = model.expects line' to handle pager
|
61
|
+
## how to do this, without redefining the whole damn thing
|
62
|
+
## FIXME: we also need output (not sure I'm going to support this)
|
63
|
+
attr_reader :output
|
64
|
+
def waitfor(options) # :yield: recvdata
|
65
|
+
time_out = @options["Timeout"]
|
66
|
+
waittime = @options["Waittime"]
|
67
|
+
fail_eof = @options["FailEOF"]
|
68
|
+
model = @options["Model"]
|
69
|
+
|
70
|
+
if options.kind_of?(Hash)
|
71
|
+
prompt = if options.has_key?("Match")
|
72
|
+
options["Match"]
|
73
|
+
elsif options.has_key?("Prompt")
|
74
|
+
options["Prompt"]
|
75
|
+
elsif options.has_key?("String")
|
76
|
+
Regexp.new( Regexp.quote(options["String"]) )
|
77
|
+
end
|
78
|
+
time_out = options["Timeout"] if options.has_key?("Timeout")
|
79
|
+
waittime = options["Waittime"] if options.has_key?("Waittime")
|
80
|
+
fail_eof = options["FailEOF"] if options.has_key?("FailEOF")
|
81
|
+
else
|
82
|
+
prompt = options
|
83
|
+
end
|
84
|
+
|
85
|
+
if time_out == false
|
86
|
+
time_out = nil
|
87
|
+
end
|
88
|
+
|
89
|
+
line = ''
|
90
|
+
buf = ''
|
91
|
+
rest = ''
|
92
|
+
until(prompt === line and not IO::select([@sock], nil, nil, waittime))
|
93
|
+
unless IO::select([@sock], nil, nil, time_out)
|
94
|
+
raise TimeoutError, "timed out while waiting for more data"
|
95
|
+
end
|
96
|
+
begin
|
97
|
+
c = @sock.readpartial(1024 * 1024)
|
98
|
+
@output = c
|
99
|
+
@dumplog.log_dump('<', c) if @options.has_key?("Dump_log")
|
100
|
+
if @options["Telnetmode"]
|
101
|
+
c = rest + c
|
102
|
+
if Integer(c.rindex(/#{IAC}#{SE}/no) || 0) <
|
103
|
+
Integer(c.rindex(/#{IAC}#{SB}/no) || 0)
|
104
|
+
buf = preprocess(c[0 ... c.rindex(/#{IAC}#{SB}/no)])
|
105
|
+
rest = c[c.rindex(/#{IAC}#{SB}/no) .. -1]
|
106
|
+
elsif pt = c.rindex(/#{IAC}[^#{IAC}#{AO}#{AYT}#{DM}#{IP}#{NOP}]?\z/no) ||
|
107
|
+
c.rindex(/\r\z/no)
|
108
|
+
buf = preprocess(c[0 ... pt])
|
109
|
+
rest = c[pt .. -1]
|
110
|
+
else
|
111
|
+
buf = preprocess(c)
|
112
|
+
rest = ''
|
113
|
+
end
|
114
|
+
else
|
115
|
+
# Not Telnetmode.
|
116
|
+
#
|
117
|
+
# We cannot use preprocess() on this data, because that
|
118
|
+
# method makes some Telnetmode-specific assumptions.
|
119
|
+
buf = rest + c
|
120
|
+
rest = ''
|
121
|
+
unless @options["Binmode"]
|
122
|
+
if pt = buf.rindex(/\r\z/no)
|
123
|
+
buf = buf[0 ... pt]
|
124
|
+
rest = buf[pt .. -1]
|
125
|
+
end
|
126
|
+
buf.gsub!(/#{EOL}/no, "\n")
|
127
|
+
end
|
128
|
+
end
|
129
|
+
@log.print(buf) if @options.has_key?("Output_log")
|
130
|
+
line += buf
|
131
|
+
line = model.expects line
|
132
|
+
line = yield line if block_given?
|
133
|
+
yield buf if block_given?
|
134
|
+
rescue EOFError # End of file reached
|
135
|
+
raise if fail_eof
|
136
|
+
if line == ''
|
137
|
+
line = nil
|
138
|
+
yield nil if block_given?
|
139
|
+
end
|
140
|
+
break
|
141
|
+
end
|
142
|
+
end
|
143
|
+
line
|
144
|
+
end
|
145
|
+
end
|
data/lib/oxidized/job.rb
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
module Oxidized
|
2
|
+
class Job < Thread
|
3
|
+
attr_reader :start, :end, :status, :time, :node, :config
|
4
|
+
def initialize node
|
5
|
+
@node = node
|
6
|
+
@start = Time.now.utc
|
7
|
+
super do |node|
|
8
|
+
@status, @config = node.run
|
9
|
+
@end = Time.now.utc
|
10
|
+
@time = @end - @start
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module Oxidized
|
2
|
+
class Jobs < Array
|
3
|
+
attr_accessor :interval, :duration, :max, :want
|
4
|
+
def initialize max, interval, nodes
|
5
|
+
@max = max
|
6
|
+
#@interval = interval * 60
|
7
|
+
@interval = interval
|
8
|
+
@nodes = nodes
|
9
|
+
@duration = 4
|
10
|
+
new_count
|
11
|
+
super()
|
12
|
+
end
|
13
|
+
def duration last
|
14
|
+
@duration = (@duration + last) / 2
|
15
|
+
new_count
|
16
|
+
end
|
17
|
+
def new_count
|
18
|
+
@want = ((@nodes.size * @duration) / @interval).to_i
|
19
|
+
@want = 1 if @want < 1
|
20
|
+
@want = @nodes.size if @want > @nodes.size
|
21
|
+
@want = @max if @want > @max
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
data/lib/oxidized/log.rb
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
module Oxidized
|
2
|
+
|
3
|
+
begin
|
4
|
+
require 'syslog/logger'
|
5
|
+
Log = Syslog::Logger.new 'oxidized'
|
6
|
+
Log.define_singleton_method(:file=){|arg|}
|
7
|
+
rescue LoadError
|
8
|
+
# 1.9.3 has no love for syslog
|
9
|
+
require 'logger'
|
10
|
+
class Logger < Logger
|
11
|
+
def initialize target=STDOUT
|
12
|
+
super target
|
13
|
+
end
|
14
|
+
def file= target
|
15
|
+
@logdev = LogDevice.new target
|
16
|
+
end
|
17
|
+
end
|
18
|
+
Log = Logger.new
|
19
|
+
end
|
20
|
+
|
21
|
+
end
|