oxidized 0.1.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 +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
|