corona 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ --colour
data/Gemfile ADDED
@@ -0,0 +1,14 @@
1
+ source 'https://rubygems.org'
2
+ ruby '2.0.0'
3
+
4
+ gem 'sequel'
5
+ gem 'sqlite3'
6
+ gem 'snmp'
7
+
8
+ group :development do
9
+ gem 'pry'
10
+ end
11
+
12
+ group :test do
13
+ gem 'rspec'
14
+ end
data/Gemfile.lock ADDED
@@ -0,0 +1,32 @@
1
+ GEM
2
+ remote: https://rubygems.org/
3
+ specs:
4
+ coderay (1.0.9)
5
+ diff-lcs (1.2.3)
6
+ method_source (0.8.1)
7
+ pry (0.9.12)
8
+ coderay (~> 1.0.5)
9
+ method_source (~> 0.8)
10
+ slop (~> 3.4)
11
+ rspec (2.13.0)
12
+ rspec-core (~> 2.13.0)
13
+ rspec-expectations (~> 2.13.0)
14
+ rspec-mocks (~> 2.13.0)
15
+ rspec-core (2.13.1)
16
+ rspec-expectations (2.13.0)
17
+ diff-lcs (>= 1.1.3, < 2.0)
18
+ rspec-mocks (2.13.1)
19
+ sequel (3.46.0)
20
+ slop (3.4.4)
21
+ snmp (1.1.1)
22
+ sqlite3 (1.3.7)
23
+
24
+ PLATFORMS
25
+ ruby
26
+
27
+ DEPENDENCIES
28
+ pry
29
+ rspec
30
+ sequel
31
+ snmp
32
+ sqlite3
data/README.md ADDED
@@ -0,0 +1,17 @@
1
+ # About
2
+ Corona sends SNMP queries to defined CIDR ranges and populates SQL database based on nodes found. Some particular problems it tries to deal with:
3
+ * Only discover one node once
4
+ * To that effect it has priority list of idDescr lo0.0, loopback0, vlan2 etc. Higher priority will always replace lower priority interface (say you have MGMT in loop0 but giga0/2.42 has valid MGMT address towards L2 metro)
5
+ * Tries to handle gracefully renumbering, renaming, etc
6
+
7
+ # Install
8
+ 1. gem install corona
9
+ 2. corona
10
+ 3. ^C (break it)
11
+ 4. edit ~/.config/corona/config
12
+ 5. put corona in crontab as _corona|mail -E -s 'new nodes found' foo@example.com_
13
+
14
+ # Config
15
+ * You need to configure SNMP community
16
+ * You need to define CIDR to poll and CIDRs to ignore (subset of those you poll)
17
+ * CIDR in example config is list, but can be replaced with 'string' which points to file, where CIDRs are listed
data/Rakefile ADDED
@@ -0,0 +1,34 @@
1
+ begin
2
+ require 'bundler'
3
+ require 'rspec/core/rake_task'
4
+ Bundler.setup
5
+ rescue LoadError
6
+ warn 'missing dependencies'
7
+ exit 42
8
+ end
9
+
10
+ gemspec = eval(File.read(Dir['*.gemspec'].first))
11
+
12
+ desc 'Validate the gemspec'
13
+ task :gemspec do
14
+ gemspec.validate
15
+ end
16
+
17
+ RSpec::Core::RakeTask.new(:spec)
18
+
19
+ desc "Build gem locally"
20
+ task :build => [:spec, :gemspec] do
21
+ system "gem build #{gemspec.name}.gemspec"
22
+ FileUtils.mkdir_p "gems"
23
+ FileUtils.mv "#{gemspec.name}-#{gemspec.version}.gem", "gems"
24
+ end
25
+
26
+ desc "Install gem locally"
27
+ task :install => :build do
28
+ system "sudo sh -c \'umask 022; gem install gems/#{gemspec.name}-#{gemspec.version}\'"
29
+ end
30
+
31
+ desc "Clean automatically generated files"
32
+ task :clean do
33
+ FileUtils.rm_rf "gems"
34
+ end
data/TODO.md ADDED
@@ -0,0 +1,6 @@
1
+ # CLI options
2
+ * option to purge old device
3
+ * instead of _sqlite corona.db "delete from device where last_seen < datetime('now', '-7days')"
4
+
5
+ # Core#process
6
+ * maybe it can be made cleaner/smarter
data/bin/corona ADDED
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'corona'
4
+
5
+ Corona.new
data/corona.gemspec ADDED
@@ -0,0 +1,18 @@
1
+ Gem::Specification.new do |s|
2
+ s.name = 'corona'
3
+ s.version = '0.0.1'
4
+ s.platform = Gem::Platform::RUBY
5
+ s.authors = [ 'Saku Ytti' ]
6
+ s.email = %w( saku@ytti.fi )
7
+ s.homepage = 'http://github.com/ytti/corona'
8
+ s.summary = 'device discovery via snmp polls'
9
+ s.description = 'Threaded SNMP poll based network discovery. Devices are stored in SQL'
10
+ s.rubyforge_project = s.name
11
+ s.files = `git ls-files`.split("\n")
12
+ s.executables = %w( corona )
13
+ s.require_path = 'lib'
14
+
15
+ s.add_dependency 'sequel'
16
+ s.add_dependency 'sqlite3'
17
+ s.add_dependency 'snmp'
18
+ end
@@ -0,0 +1,13 @@
1
+ module Corona
2
+ require 'fileutils'
3
+ FileUtils.mkdir_p Config::Root
4
+ CFG.community = 'public'
5
+ CFG.db = File.join Config::Root, 'corona.db'
6
+ CFG.poll = %w( 10.10.10.0/24 10.10.20.0/24 )
7
+ CFG.ignore = %w( 10.10.10.42/32 10.10.20.42/32 )
8
+ CFG.mgmt = %w( lo0.0 loopback0 vlan2 )
9
+ CFG.threads = 50
10
+ CFG.log = File.join Config::Root, 'log'
11
+ CFG.debug = false
12
+ CFG.save
13
+ end
@@ -0,0 +1,26 @@
1
+ module Corona
2
+ require 'ostruct'
3
+ require 'yaml'
4
+ class Config < OpenStruct
5
+ Root = File.join ENV['HOME'], '.config', 'corona'
6
+ Crash = File.join Root, 'crash'
7
+ def initialize file=File.join(Config::Root, 'config')
8
+ super()
9
+ @file = file.to_s
10
+ end
11
+ def load
12
+ if File.exists? @file
13
+ marshal_load YAML.load_file @file
14
+ else
15
+ require 'corona/config/bootstrap'
16
+ end
17
+ end
18
+ def save
19
+ File.write @file, YAML.dump(marshal_dump)
20
+ end
21
+ end
22
+ CFG = Config.new
23
+ CFG.load
24
+ Log.file = CFG.log if CFG.log
25
+ Log.level = Logger::INFO unless CFG.debug
26
+ end
@@ -0,0 +1,179 @@
1
+ require 'corona/log'
2
+ require 'corona/config/core'
3
+ require 'corona/snmp'
4
+ require 'corona/db'
5
+ require 'ipaddr'
6
+ require 'resolv'
7
+
8
+ module Corona
9
+ class << self
10
+ def new
11
+ Core.new
12
+ end
13
+ end
14
+
15
+ class Core
16
+
17
+ private
18
+
19
+ def initialize
20
+ poll, ignore = resolve_networks
21
+ @mutex = Mutex.new
22
+ @db = DB.new
23
+ threads = []
24
+ Thread.abort_on_exception = true
25
+ poll.each do |net|
26
+ net.to_range.each do |ip|
27
+ next if ignore.any? { |ignore| ignore.include? ip }
28
+ while threads.size >= CFG.threads
29
+ threads.delete_if { |thread| not thread.alive? }
30
+ sleep 0.01
31
+ end
32
+ threads << Thread.new { poll ip }
33
+ end
34
+ end
35
+ threads.each { |thread| thread.join }
36
+ end
37
+
38
+ def poll ip
39
+ snmp = SNMP.new ip.to_s
40
+ oids = snmp.dbget
41
+ if oids
42
+ if index = snmp.ip2index(ip.to_s)
43
+ if int = snmp.ifdescr(index)
44
+ @mutex.synchronize { process :oids=>oids, :int=>int.downcase, :ip=>ip }
45
+ else
46
+ Log.warn "no ifDescr for #{index} at #{ip}"
47
+ end
48
+ else
49
+ Log.warn "no ifIndex for #{ip}"
50
+ end
51
+ end
52
+ end
53
+
54
+ def process opt
55
+ opt = normalize_opt opt
56
+ record = mkrecord opt
57
+ old_by_ip, old_by_sysname = @db.old record[:id], record[:oid_sysName]
58
+
59
+ # unique box having non-unique sysname
60
+ # old_by_sysname = false if record[:oid_sysDescr].match 'Application Control Engine'
61
+
62
+ if not old_by_sysname and not old_by_ip
63
+ # all new device
64
+ puts "ptr [%s] sysName [%s] ip [%s]" % [record[:ptr], record[:oid_sysName], record[:ip]]
65
+ Log.info "#{record[:ip]} added"
66
+ @db.add record
67
+
68
+ elsif not old_by_sysname and old_by_ip
69
+ # IP seen, name not, device got renamed?
70
+ Log.info "#{record[:ip]} got renamed"
71
+ @db.update record, [:ip, old_by_ip[:ip]]
72
+
73
+ elsif old_by_sysname and not old_by_ip
74
+ # name exists, but IP is new, figure out if we wan to use old or new IP
75
+ decide_old_new record, old_by_sysname
76
+
77
+ elsif old_by_sysname and old_by_ip
78
+ both_seen record, old_by_sysname, old_by_ip
79
+ end
80
+ end
81
+
82
+ def both_seen record, old_by_sysname, old_by_ip
83
+ if old_by_sysname == old_by_ip
84
+ # no changes, updating same record
85
+ Log.debug "#{record[:ip]} refreshed, no channges"
86
+ @db.update record, [:oid_sysName, old_by_sysname[:oid_sysName]]
87
+ else
88
+ # same name seen and same IP seen, but records were not same (device got renumbered to existing node + existing node got delete?)
89
+ Log.warn "#{record[:ip]}, unique entries for IP and sysName in DB, updating by IP"
90
+ @db.update record, [:ip, old_by_ip[:ip]]
91
+ end
92
+ end
93
+
94
+ def decide_old_new record, old_by_sysname
95
+ new_int_pref = (CFG.mgmt.index(record[:oid_ifDescr]) or 100)
96
+ old_int_pref = (CFG.mgmt.index(old_by_sysname[:oid_ifDescr]) or 99)
97
+
98
+ if new_int_pref < old_int_pref
99
+ # new int is more preferable than old
100
+ Log.info "#{record[:ip]} is replacing inferior #{old_by_sysname[:ip]}"
101
+ @db.update record, [:oid_sysName, old_by_sysname[:oid_sysName]]
102
+
103
+ elsif new_int_pref == 100 and old_int_pref == 99
104
+ # neither old or new interface is known good MGMT interface
105
+ if SNMP.new(old_by_sysname[:ip]).sysdescr
106
+ # if old IP works, don't update
107
+ Log.debug "#{record[:ip]} not updating, previously seen as #{old_by_sysname[:ip]}"
108
+ else
109
+ Log.info "#{record[:ip]} updating, old #{old_by_sysname[:ip]} is dead"
110
+ @db.update record, [:oid_sysName, old_by_sysname[:oid_sysName]]
111
+ end
112
+
113
+ elsif new_int_pref >= old_int_pref
114
+ # nothing to do, we have better entry
115
+ Log.debug "#{record[:ip]} already seen as superior via #{old_by_sysname[:ip]}"
116
+
117
+ else
118
+ Log.error "not updating, new: #{record[:ip]}, old: #{old_by_sysname[:ip]}"
119
+ end
120
+ end
121
+
122
+ def mkrecord opt
123
+ {
124
+ :id => opt[:ip].to_i,
125
+ :ip => opt[:ip].to_s,
126
+ :ptr => ip2name(opt[:ip].to_s),
127
+ :model => model(opt),
128
+ :oid_ifDescr => opt[:int],
129
+ :oid_sysName => opt[:oids][:sysName],
130
+ :oid_sysLocation => opt[:oids][:sysLocation],
131
+ :oid_sysDescr => opt[:oids][:sysDescr],
132
+ }
133
+ end
134
+
135
+ def normalize_opt opt
136
+ opt[:oids][:sysName].sub! /-re[1-9]\./, '-re0.'
137
+ opt
138
+ end
139
+
140
+ def ip2name ip
141
+ Resolv.getname ip rescue ip
142
+ end
143
+
144
+ def model opt
145
+ case opt[:oids][:sysDescr]
146
+ when /Cisco Catalyst operating System/
147
+ 'catos'
148
+ when /cisco/i, /Application Control Engine/i
149
+ 'ios'
150
+ when /JUNOS/
151
+ 'junos'
152
+ when /^NetScreen/, /^SSG-\d+/
153
+ 'screenos'
154
+ when /IronWare/
155
+ 'ironware'
156
+ when /^Summit/
157
+ 'xos'
158
+ when /^\d+[A-Z]\sEthernet Switch$/
159
+ 'powerconnect'
160
+ end
161
+ end
162
+
163
+ def resolve_networks
164
+ [CFG.poll, CFG.ignore].map do |nets|
165
+ if nets.respond_to? :each
166
+ nets.map { |net| IPAddr.new net }
167
+ else
168
+ out = []
169
+ File.read(nets).each_line do |net|
170
+ net = net.match /^([\d.\/]+)/
171
+ out.push IPAddr.new net[1] if net
172
+ end
173
+ out
174
+ end
175
+ end
176
+ end
177
+
178
+ end
179
+ end
data/lib/corona/db.rb ADDED
@@ -0,0 +1,50 @@
1
+ module Corona
2
+ class DB
3
+ require 'sequel'
4
+ require 'sqlite3'
5
+ def initialize
6
+ @db = Sequel.sqlite(CFG.db, :max_connections => 1, :pool_timeout => 60)
7
+
8
+ @db.create_table :device do
9
+ primary_key :id
10
+ String :ip
11
+ String :ptr
12
+ String :model
13
+ String :oid_ifDescr
14
+ Boolean :active
15
+ Time :first_seen
16
+ Time :last_seen
17
+ String :oid_sysName
18
+ String :oid_sysLocation
19
+ String :oid_sysDescr
20
+ end unless @db.table_exists? :device
21
+ end
22
+
23
+ # http://sequel.rubyforge.org/rdoc/files/doc/cheat_sheet_rdoc.html
24
+ #
25
+
26
+ def add record
27
+ record[:first_seen] = record[:last_seen] = Time.now.utc
28
+ record[:active] = true
29
+ #Log.debug "adding: #{record}"
30
+ @db[:device].insert record
31
+ end
32
+
33
+ def update record, where
34
+ record[:last_seen] = Time.now.utc
35
+ record[:active] = true
36
+ #Log.debug "updating (where: #{where}): #{record}"
37
+ @db[:device].where('? == ?', where.first, where.last).update record
38
+ end
39
+
40
+ def [] primary_key
41
+ @db[:device].where('id == ?', primary_key).first
42
+ end
43
+
44
+ def old primary_key, oid_sysName
45
+ ip = self[primary_key]
46
+ sysName = @db[:device].where('oid_sysName == ?', oid_sysName).first
47
+ [ip, sysName]
48
+ end
49
+ end
50
+ end
data/lib/corona/log.rb ADDED
@@ -0,0 +1,13 @@
1
+ module Corona
2
+ require 'logger'
3
+ class Logger < Logger
4
+ def initialize target=STDOUT
5
+ super target
6
+ self.level = Logger::DEBUG
7
+ end
8
+ def file= target
9
+ @logdev = LogDevice.new target
10
+ end
11
+ end
12
+ Log = Logger.new
13
+ end
@@ -0,0 +1,82 @@
1
+ module Corona
2
+ class SNMP
3
+ DB_OID = {
4
+ :sysDescr => '1.3.6.1.2.1.1.1.0',
5
+ :sysLocation => '1.3.6.1.2.1.1.6.0',
6
+ :sysName => '1.3.6.1.2.1.1.5.0',
7
+ }
8
+ OID = {
9
+ :ipCidrRouteIfIndex => '1.3.6.1.2.1.4.24.4.1.5', # addr.255.255.255.255.0.0.0.0.0
10
+ :ipAdEntIfIndex => '1.3.6.1.2.1.4.20.1.2', # addr
11
+ :ipAddressIfIndex => '1.3.6.1.2.1.4.34.1.3', # 1,2 (uni,any) . 4,16 (size) . addr
12
+ :ifDescr => '1.3.6.1.2.1.2.2.1.2',
13
+ }
14
+ UNICAST = 1
15
+ IPV4 = 4
16
+
17
+ BULK_MAX = 30
18
+ require 'snmp'
19
+ def initialize host, community=CFG.community
20
+ @snmp = ::SNMP::Manager.new :Host => host, :Community => community,
21
+ :Timeout => 1, :Retries => 3, :MibModules => false
22
+ end
23
+ def get *oid
24
+ oid = [oid].flatten.join('.')
25
+ begin
26
+ @snmp.get(oid).each_varbind { |vb| return vb }
27
+ rescue ::SNMP::RequestTimeout
28
+ return false
29
+ end
30
+ end
31
+ def mget oids=DB_OID
32
+ result = {}
33
+ begin
34
+ @snmp.get(oids.map{|_,oid|oid}).each_varbind do |vb|
35
+ oids.each do |name,oid|
36
+ if vb.name.to_str == oid
37
+ result[name] = vb.value
38
+ next
39
+ end
40
+ end
41
+ end
42
+ rescue ::SNMP::RequestTimeout
43
+ return false
44
+ end
45
+ result
46
+ end
47
+ alias dbget mget
48
+ def bulkwalk root
49
+ last, oid, vbs = false, root, []
50
+ while not last
51
+ r = @snmp.get_bulk 0, BULK_MAX, oid
52
+ r.varbind_list.each do |vb|
53
+ oid = vb.name.to_str
54
+ (last = true; break) if not oid.match /^#{Regexp.quote root}/
55
+ vbs.push vb
56
+ end
57
+ end
58
+ vbs
59
+ end
60
+
61
+ def sysdescr
62
+ get DB_OID[:sysDescr]
63
+ end
64
+
65
+ def ip2index ip
66
+ oids = mget :route => [OID[:ipCidrRouteIfIndex], ip, '255.255.255.255.0.0.0.0.0'].join('.'),
67
+ :new => [OID[:ipAddressIfIndex], UNICAST, IPV4, ip].join('.'),
68
+ :old => [OID[:ipAdEntIfIndex], ip].join('.')
69
+ return false unless oids
70
+ index = oids[:route]
71
+ index = oids[:new] if not index.class == ::SNMP::Integer or index.to_s == '0'
72
+ index = oids[:old] if not index.class == ::SNMP::Integer or index.to_s == '0'
73
+ return false unless index.class == ::SNMP::Integer
74
+ index.to_s
75
+ end
76
+ def ifdescr index
77
+ descr = get OID[:ifDescr], index
78
+ return false unless descr and descr.value.class == ::SNMP::OctetString
79
+ descr.value.to_s
80
+ end
81
+ end
82
+ end
data/lib/corona.rb ADDED
@@ -0,0 +1,3 @@
1
+ module Corona
2
+ require 'corona/core'
3
+ end
metadata ADDED
@@ -0,0 +1,112 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: corona
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Saku Ytti
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2013-05-01 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: sequel
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '0'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - '>='
28
+ - !ruby/object:Gem::Version
29
+ version: '0'
30
+ - !ruby/object:Gem::Dependency
31
+ name: sqlite3
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - '>='
36
+ - !ruby/object:Gem::Version
37
+ version: '0'
38
+ type: :runtime
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - '>='
44
+ - !ruby/object:Gem::Version
45
+ version: '0'
46
+ - !ruby/object:Gem::Dependency
47
+ name: snmp
48
+ requirement: !ruby/object:Gem::Requirement
49
+ none: false
50
+ requirements:
51
+ - - '>='
52
+ - !ruby/object:Gem::Version
53
+ version: '0'
54
+ type: :runtime
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ none: false
58
+ requirements:
59
+ - - '>='
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ description: Threaded SNMP poll based network discovery. Devices are stored in SQL
63
+ email:
64
+ - saku@ytti.fi
65
+ executables:
66
+ - corona
67
+ extensions: []
68
+ extra_rdoc_files: []
69
+ files:
70
+ - .rspec
71
+ - Gemfile
72
+ - Gemfile.lock
73
+ - README.md
74
+ - Rakefile
75
+ - TODO.md
76
+ - bin/corona
77
+ - corona.gemspec
78
+ - lib/corona.rb
79
+ - lib/corona/config/bootstrap.rb
80
+ - lib/corona/config/core.rb
81
+ - lib/corona/core.rb
82
+ - lib/corona/db.rb
83
+ - lib/corona/log.rb
84
+ - lib/corona/snmp.rb
85
+ homepage: http://github.com/ytti/corona
86
+ licenses: []
87
+ post_install_message:
88
+ rdoc_options: []
89
+ require_paths:
90
+ - lib
91
+ required_ruby_version: !ruby/object:Gem::Requirement
92
+ none: false
93
+ requirements:
94
+ - - '>='
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ segments:
98
+ - 0
99
+ hash: 1454207244225852473
100
+ required_rubygems_version: !ruby/object:Gem::Requirement
101
+ none: false
102
+ requirements:
103
+ - - '>='
104
+ - !ruby/object:Gem::Version
105
+ version: '0'
106
+ requirements: []
107
+ rubyforge_project: corona
108
+ rubygems_version: 1.8.25
109
+ signing_key:
110
+ specification_version: 3
111
+ summary: device discovery via snmp polls
112
+ test_files: []