corona 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.
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: []