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.
Files changed (51) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +2 -0
  3. data/Gemfile +3 -0
  4. data/README.md +133 -0
  5. data/Rakefile +46 -0
  6. data/TODO.md +20 -0
  7. data/bin/oxidized +13 -0
  8. data/extra/rest_client.rb +25 -0
  9. data/extra/syslog.rb +110 -0
  10. data/lib/oxidized.rb +6 -0
  11. data/lib/oxidized/cli.rb +42 -0
  12. data/lib/oxidized/config.rb +55 -0
  13. data/lib/oxidized/config/vars.rb +10 -0
  14. data/lib/oxidized/core.rb +41 -0
  15. data/lib/oxidized/input/cli.rb +48 -0
  16. data/lib/oxidized/input/input.rb +19 -0
  17. data/lib/oxidized/input/ssh.rb +111 -0
  18. data/lib/oxidized/input/telnet.rb +145 -0
  19. data/lib/oxidized/job.rb +14 -0
  20. data/lib/oxidized/jobs.rb +24 -0
  21. data/lib/oxidized/log.rb +21 -0
  22. data/lib/oxidized/manager.rb +57 -0
  23. data/lib/oxidized/model/acos.rb +69 -0
  24. data/lib/oxidized/model/aireos.rb +55 -0
  25. data/lib/oxidized/model/aos.rb +38 -0
  26. data/lib/oxidized/model/aos7.rb +58 -0
  27. data/lib/oxidized/model/aosw.rb +43 -0
  28. data/lib/oxidized/model/eos.rb +32 -0
  29. data/lib/oxidized/model/fortios.rb +44 -0
  30. data/lib/oxidized/model/ios.rb +63 -0
  31. data/lib/oxidized/model/iosxr.rb +47 -0
  32. data/lib/oxidized/model/ironware.rb +33 -0
  33. data/lib/oxidized/model/junos.rb +56 -0
  34. data/lib/oxidized/model/model.rb +152 -0
  35. data/lib/oxidized/model/powerconnect.rb +38 -0
  36. data/lib/oxidized/model/procurve.rb +45 -0
  37. data/lib/oxidized/model/timos.rb +45 -0
  38. data/lib/oxidized/node.rb +169 -0
  39. data/lib/oxidized/node/stats.rb +33 -0
  40. data/lib/oxidized/nodes.rb +143 -0
  41. data/lib/oxidized/output/file.rb +42 -0
  42. data/lib/oxidized/output/git.rb +78 -0
  43. data/lib/oxidized/output/output.rb +5 -0
  44. data/lib/oxidized/source/csv.rb +42 -0
  45. data/lib/oxidized/source/source.rb +11 -0
  46. data/lib/oxidized/source/sql.rb +61 -0
  47. data/lib/oxidized/string.rb +13 -0
  48. data/lib/oxidized/worker.rb +54 -0
  49. data/oxidized.gemspec +20 -0
  50. data/spec/nodes_spec.rb +46 -0
  51. 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
@@ -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
@@ -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