netdisco 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: a0ce0d6214b2a7b6c2b0619f00c82a84656ad9c1b784336305f9bde8587fbfc8
4
+ data.tar.gz: f3d6b2d3ae31913f42f24ff065146a737da60cf38e36d2d6064ccb59e3e90aaf
5
+ SHA512:
6
+ metadata.gz: fd1ae67b1b81632bf30e4166c1eb45c890352bf10e56bd14c1e6983943c09b3fff5449dba27ea2f23e7296477b25236720f1ecb7d2070792aa2e0aaea4437717
7
+ data.tar.gz: 71c515cfdc3ff4d03167053df28d64364381416f38e73273d7565292deb34ad670df87dbfc8a15378f94fa942c0d6a7b5b819fdd52cc3ce8f92e4c4bc2591cf9
data/CHANGELOG.md ADDED
@@ -0,0 +1,5 @@
1
+ ## [Unreleased]
2
+
3
+ ## [0.1.0] - 2022-04-14
4
+
5
+ - Initial release
@@ -0,0 +1,84 @@
1
+ # Contributor Covenant Code of Conduct
2
+
3
+ ## Our Pledge
4
+
5
+ We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation.
6
+
7
+ We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community.
8
+
9
+ ## Our Standards
10
+
11
+ Examples of behavior that contributes to a positive environment for our community include:
12
+
13
+ * Demonstrating empathy and kindness toward other people
14
+ * Being respectful of differing opinions, viewpoints, and experiences
15
+ * Giving and gracefully accepting constructive feedback
16
+ * Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience
17
+ * Focusing on what is best not just for us as individuals, but for the overall community
18
+
19
+ Examples of unacceptable behavior include:
20
+
21
+ * The use of sexualized language or imagery, and sexual attention or
22
+ advances of any kind
23
+ * Trolling, insulting or derogatory comments, and personal or political attacks
24
+ * Public or private harassment
25
+ * Publishing others' private information, such as a physical or email
26
+ address, without their explicit permission
27
+ * Other conduct which could reasonably be considered inappropriate in a
28
+ professional setting
29
+
30
+ ## Enforcement Responsibilities
31
+
32
+ Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful.
33
+
34
+ Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate.
35
+
36
+ ## Scope
37
+
38
+ This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event.
39
+
40
+ ## Enforcement
41
+
42
+ Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at careline@foxmail.com. All complaints will be reviewed and investigated promptly and fairly.
43
+
44
+ All community leaders are obligated to respect the privacy and security of the reporter of any incident.
45
+
46
+ ## Enforcement Guidelines
47
+
48
+ Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct:
49
+
50
+ ### 1. Correction
51
+
52
+ **Community Impact**: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community.
53
+
54
+ **Consequence**: A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested.
55
+
56
+ ### 2. Warning
57
+
58
+ **Community Impact**: A violation through a single incident or series of actions.
59
+
60
+ **Consequence**: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban.
61
+
62
+ ### 3. Temporary Ban
63
+
64
+ **Community Impact**: A serious violation of community standards, including sustained inappropriate behavior.
65
+
66
+ **Consequence**: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban.
67
+
68
+ ### 4. Permanent Ban
69
+
70
+ **Community Impact**: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals.
71
+
72
+ **Consequence**: A permanent ban from any sort of public interaction within the community.
73
+
74
+ ## Attribution
75
+
76
+ This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.0,
77
+ available at https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
78
+
79
+ Community Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder](https://github.com/mozilla/diversity).
80
+
81
+ [homepage]: https://www.contributor-covenant.org
82
+
83
+ For answers to common questions about this code of conduct, see the FAQ at
84
+ https://www.contributor-covenant.org/faq. Translations are available at https://www.contributor-covenant.org/translations.
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2022 WENWU.YAN
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,117 @@
1
+ # netdisco
2
+
3
+ Given snmp community and single device, discover the network via discovering
4
+ LLDP/CDP neighbours, while producing list or dot file (for graphviz digraphs)
5
+
6
+ ## Install
7
+
8
+ % gem install netdisco
9
+
10
+ ## Use
11
+
12
+ % netdisco --graphiz router.example.com
13
+
14
+ ## Command line
15
+
16
+ ```shell
17
+ Usage: netdisco [options] hostname
18
+ -g, --graphviz dot output use 'dot -Tpng -o map.png map.dot'
19
+ -l, --list list nodes
20
+ -j, --json json output
21
+ -y, --yaml yaml output
22
+ -a, --hash hash/associative array output
23
+ -r, --resolve resolve addresses to names
24
+ -p, --purge remove peers not in configured CIDR
25
+ -c, --community SNMP community to use
26
+ -d, --debug turn debugging on
27
+ -h, --help Display this help message.
28
+
29
+ ```
30
+
31
+ * graphiz - graphis (dot) output
32
+ * list - list nodes found
33
+ * json - json output
34
+ * yaml - yaml output
35
+ * hash - ruby hash output
36
+ * resolve - resolve IP addresses
37
+ * purge - remove non-cidr matching peers from output
38
+ * community - sets snmp community
39
+ * debug - turn on debugging
40
+
41
+ ## Config
42
+
43
+ ```yaml
44
+ ---
45
+ use:
46
+ - LLDP
47
+ - CDP
48
+ poll:
49
+ - 192.0.2.0/24
50
+ snmp:
51
+ community: public
52
+ timeout: 1
53
+ retries: 2
54
+ bulkrows: 35
55
+ dot:
56
+ bothlinks: true
57
+ color:
58
+ - - cpe
59
+ - gold
60
+ - - -sw
61
+ - blue
62
+ - - -pe
63
+ - red
64
+ - - ' -p'
65
+ - yellow
66
+ dns:
67
+ afi:
68
+ log: STDERR
69
+ debug: false
70
+ namemap:
71
+ - - -re\d+
72
+ - ''
73
+ - - (.*(?<!as23456.net)$)
74
+ - \1.as23456.net
75
+ ```
76
+
77
+ * use - methods to use for crawling
78
+ * poll - cidrs to allow snmp for
79
+ * snmp community - snmp community to use
80
+ * snmp timout - snmp timout in seconds
81
+ * snmp retries - snmp retries count
82
+ * snmp bulkrows - snmp row count for bulkget
83
+ * dot bothlinks - show a-b and b-a link
84
+ * dot color - regexp to color, first hit used
85
+ * dns afi - ipv4/ipv6 or nil
86
+ * log - STDERR/STDOUT or file
87
+ * debug - debugging
88
+ * namemap - map (LLDP) name to FQDN (JunOS does not give domain)
89
+
90
+ ## Library use
91
+
92
+ require 'netdisco'
93
+ output = netdisco.new.discover('192.0.2.1').to_hash
94
+
95
+ ## Development
96
+
97
+ After checking out the repo, run `bin/setup` to install dependencies. You can also run `bin/console` for an interactive
98
+ prompt that will allow you to experiment.
99
+
100
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the
101
+ version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version,
102
+ push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
103
+
104
+ ## Contributing
105
+
106
+ Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/netdisco. This project is intended
107
+ to be a safe, welcoming space for collaboration, and contributors are expected to adhere to
108
+ the [code of conduct](https://github.com/ciscolive/netdisco-ruby/blob/main/CODE_OF_CONDUCT.md).
109
+
110
+ ## License
111
+
112
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
113
+
114
+ ## Code of Conduct
115
+
116
+ Everyone interacting in the Netdisco project's codebases, issue trackers, chat rooms and mailing lists is expected to
117
+ follow the [code of conduct](https://github.com/ciscolive/netdisco-ruby/blob/main/CODE_OF_CONDUCT.md).
data/Rakefile ADDED
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rubocop/rake_task"
5
+
6
+ RuboCop::RakeTask.new
7
+
8
+ task default: :rubocop
data/bin/netdisco ADDED
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ begin
5
+ require "netdisco/cli"
6
+ puts Netdisco::CLI.new.run
7
+ rescue StandardError => e
8
+ warn e.to_s
9
+ raise if Netdisco::CFG.debug
10
+ end
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../netdisco"
4
+ require "slop"
5
+
6
+ class Netdisco
7
+ class CLI
8
+ class MissingHost < NetdiscoError; end
9
+
10
+ class NoConfig < NetdiscoError; end
11
+
12
+ # 网络邻居发现入口函数
13
+ def run
14
+ output = Netdisco.new.discover @host
15
+
16
+ # 邻居关系发现后修正处理逻辑钩子函数
17
+ output.clean! if @opts[:purge]
18
+ output.resolve! if @opts[:resolve]
19
+ # 将计算结果转换格式打印
20
+ if @opts[:graphviz]
21
+ output.to_dot
22
+ elsif @opts[:list]
23
+ output.to_a
24
+ elsif @opts[:json]
25
+ output.to_json
26
+ elsif @opts[:yaml]
27
+ output.to_yaml
28
+ else
29
+ output.to_hash
30
+ end
31
+ end
32
+
33
+ # 类对象初始化入口函数
34
+ def initialize
35
+ raise NoConfig, "edit ~/.config/netdisco/config" if CONFIG.create
36
+ args, @opts = opt_parse
37
+ @host = DNS.getip args.shift
38
+ raise MissingHost, "no hostname given as argument" unless @host
39
+
40
+ CFG.snmp.community = @opts[:community] if @opts[:community]
41
+ CFG.debug = true if @opts[:debug]
42
+ CFG.ipname = true if @opts[:ipname]
43
+ end
44
+
45
+ # CLI 命令行交互解析
46
+ def opt_parse
47
+ opts = Slop.parse do |o|
48
+ o.on "-h", "--help", "show usage" do
49
+ puts o
50
+ exit
51
+ end
52
+ o.on "-g", "--graphviz", "dot output use 'dot -Tpng -o map.png map.dot'"
53
+ o.on "-l", "--list", "list nodes"
54
+ o.on "-j", "--json", "json output"
55
+ o.on "-y", "--yaml", "yaml output"
56
+ o.on "-a", "--hash", "hash/associative array output"
57
+ o.on "-r", "--resolve", "resolve addresses to names"
58
+ o.on "-p", "--purge", "remove peers not in configured CIDR"
59
+ o.string "-c=", "--community", "SNMP community to use"
60
+ o.bool "-d", "--debug", "turn debugging on"
61
+ o.bool "-i", "--ipname", "use rev(ip) name instead of discovered name"
62
+ o.on "-v", "--version", "print the version" do
63
+ puts Slop::VERSION
64
+ exit
65
+ end
66
+ end
67
+ [opts.arguments, opts]
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "strada"
4
+ require "logger"
5
+
6
+ class Netdisco
7
+ # 实例化配置对象
8
+ CONFIG = Strada.new name: "netdisco", load: false
9
+
10
+ # 设置默认参数
11
+ CONFIG.default.use = %w(LLDP CDP)
12
+ CONFIG.default.poll = %w[192.168.8.0/24]
13
+ CONFIG.default.snmp.community = "cisco"
14
+ CONFIG.default.snmp.timeout = 15
15
+ CONFIG.default.snmp.retries = 1
16
+ CONFIG.default.snmp.bulkrows = 30 # 1500B packet should fit about 50 :cdpCacheAddress rows
17
+ CONFIG.default.dot.bothlinks = false # keep both a-b and b-a links
18
+ CONFIG.default.dot.linklabels = true # label link with interface names
19
+ CONFIG.default.dot.color = [# regexp of host => color
20
+ %w[cpe gold],
21
+ %w[-sw blue],
22
+ %w[-pe red],
23
+ %w[-p yellow],
24
+ ]
25
+ CONFIG.default.dns.afi = nil # could be 'ipv4' or 'ipv6'
26
+ CONFIG.default.log = "STDERR"
27
+ CONFIG.default.debug = true
28
+ CONFIG.default.name_map = [# regexp match+sub of hostname (needed for LLDP)
29
+ ['-re\d+', ""],
30
+ ["^KILLME(.*(?<!mojo.local)$)", '\1.mojo.local'], # adds missing domain name
31
+ ]
32
+ # 加载配置并设置对应常量
33
+ CONFIG.load
34
+ CFG = CONFIG.cfg
35
+ # 设置项目日志参数
36
+ log = CFG.log
37
+ log = STDERR if log == "STDERR"
38
+ log = STDOUT if log == "STDOUT"
39
+ Log = Logger.new log
40
+ Log.level = Logger::INFO
41
+ Log.level = Logger::DEBUG if CFG.debug
42
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "resolv"
4
+
5
+ class Netdisco
6
+ class Resolve
7
+ # 对象实例化入口函数
8
+ def initialize
9
+ @cache_ip = {}
10
+ @cache_name = {}
11
+ end
12
+
13
+ # @param [String] name DNS name which we try to resolve to IP
14
+ # @return [String, nil] string if name resolves to IP, otherwise nil
15
+ # 优先加载缓存,不能存在则实时采集
16
+ def getip(name)
17
+ if @cache_ip.has_key? name
18
+ @cache_ip[name]
19
+ else
20
+ # 可能一个 name 同时存在 IPV4 和 IPV6 解析
21
+ begin
22
+ if CFG.dns.afi == "ipv4"
23
+ @cache_ip[name] = Resolv::DNS.new.getresource(name, Resolv::DNS::Resource::IN::A).address
24
+ elsif CFG.dns.afi == "ipv6"
25
+ @cache_ip[name] = Resolv::DNS.new.getresource(name, Resolv::DNS::Resource::IN::AAAA).address
26
+ else
27
+ @cache_ip[name] = Resolv.getaddress name
28
+ end
29
+ rescue => error
30
+ Log.debug "DNS resolution for '#{name}' raised error '#{error.class}' with message '#{error.message}'"
31
+ nil
32
+ end
33
+ end
34
+ end
35
+
36
+ # @param [String] ip DNS IP which we try to resolve to name
37
+ # @return [String] name if it resolves, ip otherwise
38
+ # 优先加载缓存,不能存在则实时采集
39
+ def getname(ip)
40
+ if @cache_name.has_key? ip
41
+ @cache_name[ip]
42
+ else
43
+ begin
44
+ @cache_name[ip] = Resolv.getname ip
45
+ rescue => error
46
+ Log.debug "DNS resolution for '#{ip}' raised error '#{error.class}' with message '#{error.message}'"
47
+ @cache_name[ip] = ip
48
+ end
49
+ end
50
+ end
51
+ end
52
+
53
+ DNS = Resolve.new
54
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Netdisco
4
+ class CDP < XDP
5
+ MIB = "1.3.6.1.4.1.9.9.23" # ciscoCdpMIB
6
+
7
+ OID = {
8
+ # http://tools.cisco.com/Support/SNMP/do/BrowseOID.do?local=en&translate=Translate&objectInput=1.3.6.1.4.1.9.9.23.1.2.1.1
9
+ # cdpInterfaceName: "1.3.6.1.2.1.31.1.1.1.18",
10
+ cdpInterfaceName: "1.3.6.1.4.1.9.9.23.1.1.1.1.6",
11
+ cdpCacheAddress: "1.3.6.1.4.1.9.9.23.1.2.1.1.4",
12
+ cdpCacheDeviceId: "1.3.6.1.4.1.9.9.23.1.2.1.1.6",
13
+ cdpCacheDevicePort: "1.3.6.1.4.1.9.9.23.1.2.1.1.7",
14
+ }
15
+ PEERS_BY = OID[:cdpCacheDeviceId]
16
+
17
+ private
18
+ def make_peers
19
+ peers = []
20
+ @mib.by_oid(PEERS_BY).each do |_, vb|
21
+ peer = Peer.new
22
+ peer_id = vb.oid_id(PEERS_BY)
23
+ peer.oid = get_oid_hash(peer_id)
24
+ peer.raw_ip = @mib[OID[:cdpCacheAddress], peer_id].as_ip
25
+ peer.raw_name = @mib[OID[:cdpCacheDeviceId], peer_id].value
26
+ peer.ip = get_ip(peer.raw_ip, peer.raw_name)
27
+ peer.dst = @mib[OID[:cdpCacheDevicePort], peer_id].value
28
+ peer.src = @mib[OID[:cdpInterfaceName], peer_id.first]
29
+ peer.src = peer.src&.value if peer.src
30
+ peer.raw_ip = @mib[OID[:cdpCacheAddress], peer_id].value
31
+ peers << peer
32
+ end
33
+ peers
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Netdisco
4
+ class LLDP < XDP
5
+ MIB = "1.0.8802.1.1.2" # lldpMIB
6
+ OID = {
7
+ # http://standards.ieee.org/getieee802/download/802.1AB-2009.pdf
8
+ # finding IP address for LLDP neighbour as of JunOS 13.3R1 and IOS 15.0(2)SG8 is not practical
9
+ # ifsubtype is ifindex but value 0 for JunOS neighbours
10
+ # ifsubtype is systemportnumber for IOS neighbours (what ever that is)
11
+ # luckily some IP address is in the OID key itself, while dodgy, better than nothing
12
+ # in JunOS it was some random RFC1918 address in VRF interface, not something I could poll
13
+ # .1.0.8802.1.1.2.1.4.2.1.3.0.134.10.1.4.10.0.0.4
14
+ # in IOS it was usable address
15
+ # .1.0.8802.1.1.2.1.4.2.1.3.0.257.1.1.4.62.243.146.245
16
+ # (1.4 is IPv4)
17
+ # as well LocPortId/RemPortId is hard, it is 'local' (snmpifindex really) in JunOS, but ifName in IOS
18
+ lldpLocPortId: "1.0.8802.1.1.2.1.3.7.1.3",
19
+ lldpRemChassisIdSubtype: "1.0.8802.1.1.2.1.4.1.1.4", # CSCO and JNPR use 4 (MAC address) rendering ChassisID useless
20
+ lldpRemChassisId: "1.0.8802.1.1.2.1.4.1.1.5",
21
+ lldpRemPortIdSubtype: "1.0.8802.1.1.2.1.4.1.1.6",
22
+ lldpRemPortId: "1.0.8802.1.1.2.1.4.1.1.7",
23
+ lldpRemSysName: "1.0.8802.1.1.2.1.4.1.1.9",
24
+ lldpRemManAddrIfSubtype: "1.0.8802.1.1.2.1.4.2.1.3",
25
+ }
26
+ PEERS_BY = OID[:lldpRemChassisId]
27
+ PortSubType = {
28
+ mac_address: 3,
29
+ }
30
+
31
+ private
32
+ def make_peers
33
+ peers = []
34
+ @mib.by_oid(PEERS_BY).each do |_, vb|
35
+ peer = Peer.new
36
+ peer_id = vb.oid_id(PEERS_BY)
37
+ peer.oid = get_oid_hash peer_id
38
+ ip = @mib.by_partial(OID[:lldpRemManAddrIfSubtype], peer_id)
39
+ peer.raw_ip = ip.oid[-4..-1].join(".") if ip # FIXME: IPv4 specific
40
+ peer.raw_ip ||= "192.0.2.255" # sometimes we can't fnd any IP (EX2200 talking Arista found)
41
+ peer.raw_name = @mib[OID[:lldpRemSysName], peer_id].value
42
+ peer.ip = get_ip(peer.raw_ip, peer.raw_name)
43
+ peer.dst = @mib[OID[:lldpRemPortId], peer_id].value
44
+ if @mib[OID[:lldpRemPortIdSubtype], peer_id].value.to_i == PortSubType[:mac_address]
45
+ peer.dst = peer.dst&.each_char&.map { |e| "%02x" % e.ord }&.join&.scan(/..../).join(".")
46
+ end
47
+ peer.src = @mib[OID[:lldpLocPortId], peer_id[1]].value rescue nil
48
+ peers << peer
49
+ end
50
+ peers
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../snmp"
4
+
5
+ class Netdisco
6
+ class XDP
7
+ # 类对象方法属性
8
+ attr_reader :mib
9
+
10
+ # 加载模块方法到类对象
11
+ include NameMap
12
+
13
+ # 对象实例化入口函数
14
+ def initialize(host)
15
+ @snmp = SNMP.new host
16
+ end
17
+
18
+ # 类属性方法
19
+ # @param [String] host host to query
20
+ # @return [Array(Netdisco::Peer)] neighbor information
21
+ def self.peers(host)
22
+ new(host).poll
23
+ end
24
+
25
+ # 轮询设备邻居关系
26
+ def poll
27
+ @mib = @snmp.hashwalk self.class::MIB
28
+ # require "pp"; PP.pp @mib
29
+ make_peers
30
+ rescue SNMP::NoResponse
31
+ []
32
+ end
33
+
34
+ private
35
+ def get_ip(ip, name)
36
+ DNS.getip(name_map(name)) || ip
37
+ end
38
+
39
+ def get_oid_hash(peer_id)
40
+ oid_hash = {}
41
+ self.class::OID.each do |name, oid|
42
+ oid_hash[name] = @mib[oid, peer_id]
43
+ end
44
+ oid_hash
45
+ end
46
+ end
47
+ end
48
+
49
+ require_relative "cdp"
50
+ require_relative "lldp"
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Netdisco
4
+ module NameMap
5
+ def name_map(origin_name)
6
+ name = origin_name.dup
7
+ CFG.name_map.each do |match, replace|
8
+ re = Regexp.new match
9
+ name = name.sub re, replace
10
+ end
11
+ name
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Netdisco
4
+ class Output
5
+ class Dot
6
+ INDENT = " " * 2
7
+ DEFAULT_COLOR = "black"
8
+ def self.output(output)
9
+ new(output).to_s
10
+ end
11
+
12
+ def to_s
13
+ str = "graph Netdisco {\n"
14
+ @output.to_a.each do |host|
15
+ host_label = label(host)
16
+ # 调整图形修饰符
17
+ str += INDENT + id(host) + "[label=\"#{host_label}\" color=\"#{color(host_label)}\"]\n"
18
+
19
+ if @peers.has_key? host
20
+ @peers[host].each do |peer|
21
+ peer_name = @resolve ? peer.name : peer.ip
22
+ next if (not CFG.dot.bothlinks) && @connections.include?([peer_name, host].sort)
23
+ @connections << [peer_name, host].sort
24
+ labels = ""
25
+ labels = "[headlabel=\"#{peer.dst}\" taillabel=\"#{peer.src}\"]" if CFG.dot.linklabel
26
+ str << INDENT + INDENT + id(host) + " -- " + id(peer_name) + labels + "\n"
27
+ end
28
+ end
29
+ end
30
+ str += "}\n"
31
+ str
32
+ end
33
+
34
+ private
35
+ def initialize(output)
36
+ @output = output
37
+ @connections = []
38
+ @peers = @output.peers
39
+ @resolve = @output.resolve
40
+ end
41
+
42
+ def id(host)
43
+ host = host.gsub(/[-.]/, "_")
44
+ "_" + host
45
+ end
46
+
47
+ def label(want_host)
48
+ label = nil
49
+ return want_host if CFG.ipname == true
50
+ @peers.each do |host, peers|
51
+ peers.each do |peer|
52
+ got_host = @resolve ? peer.name : peer.ip
53
+ if want_host == got_host
54
+ label = peer.raw_name
55
+ break
56
+ end
57
+ end
58
+ break if label
59
+ end
60
+ label or want_host
61
+ end
62
+
63
+ def color(host)
64
+ color = nil
65
+ CFG.dot.color.each do |re, clr|
66
+ if host.match re
67
+ color = clr
68
+ break
69
+ end
70
+ end
71
+ color or DEFAULT_COLOR
72
+ end
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,94 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Netdisco
4
+ class Output
5
+ # 类对象属性
6
+ attr_reader :peers, :resolve, :poll_map
7
+
8
+ # 对象初始化入口函数
9
+ # 将邻居发现清单以及本地的轮询列表清单比对后,格式化输出相关数据
10
+ def initialize(peers, poll_map)
11
+ @peers = peers
12
+ @poll_map = poll_map
13
+ @resolve = false
14
+ end
15
+
16
+ # 动态方法
17
+ def method_missing(name, *args)
18
+ raise NoMethodError, "invalid method #{name} for #{inspect}:#{self.class}" unless name.match?(/to_.*/)
19
+ output = File.basename name[3..-1]
20
+ require_relative "output/" + output
21
+ output = Netdisco::Output.const_get output.capitalize
22
+ output.send :output, self
23
+ end
24
+
25
+ # 自动更新设备主机名,并更新解析状态
26
+ # resolves ip addresses of peers and @peers keys
27
+ # @return [void]
28
+ def resolve!
29
+ @resolve = true
30
+ new_peers = {}
31
+ @peers.each do |host, peers|
32
+ peers.each { |peer| peer.name }
33
+ name = DNS.getname host
34
+ new_peers[name] = peers
35
+ end
36
+ @peers = new_peers
37
+ end
38
+
39
+ # 自动移除非探测名单范围内的设备
40
+ # remove peers not matching to configured CIDR
41
+ # @return [void]
42
+ def clean!
43
+ @peers.values.each do |peers|
44
+ peers.delete_if { |peer| not @poll_map.include? peer.ip }
45
+ end
46
+ end
47
+
48
+ # @return [Hash] of nodes found
49
+ def to_h
50
+ hash = {}
51
+ @peers.each do |host, peers|
52
+ array = []
53
+ peers.each do |peer|
54
+ array << peer.to_h
55
+ end
56
+ hash[host] = array
57
+ end
58
+ hash
59
+ end
60
+
61
+ # @return [Array] of nodes found
62
+ def to_a
63
+ nodes = []
64
+ @peers.each do |host, peers|
65
+ nodes << host
66
+ peers.each do |peer|
67
+ nodes << @resolve ? peer.name : peer.ip
68
+ end
69
+ end
70
+ # 将 nodes 展开为属组并去重排序
71
+ nodes.flatten.uniq
72
+ end
73
+
74
+ # @return [String] pretty print of hash
75
+ def to_hash
76
+ require "pp"
77
+ out = ""
78
+ PP.pp to_h, out
79
+ out
80
+ end
81
+
82
+ # @return [String] yaml of hosts and peers found
83
+ def to_yaml
84
+ require "yaml"
85
+ YAML.dump to_h
86
+ end
87
+
88
+ # @return [String] json of hosts and peers found
89
+ def to_json
90
+ require "json"
91
+ JSON.pretty_generate to_h
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Netdisco
4
+ class Peer
5
+ # 类对象方法属性
6
+ attr_accessor :ip, :raw_ip, :raw_name, :src, :dst, :oid
7
+
8
+ # 初始化函数
9
+ def initialize
10
+ @ip = nil # Best guess of system IP
11
+ @name = nil # Reverse of said IP
12
+ @raw_ip = nil # IP as seen in polling
13
+ @raw_name = nil # Name as seen in polling
14
+ @src = nil # SRC/local interface
15
+ @dst = nil # DST/remote interface
16
+ @oid = {} # Hash of oids collected
17
+ end
18
+
19
+ # 设置邻居名称
20
+ def name
21
+ @name ||= DNS.getname @ip
22
+ end
23
+
24
+ # 将邻居对象转换为 HASH
25
+ def to_h
26
+ {
27
+ "ip" => ip.to_s,
28
+ "name" => name.to_s,
29
+ "interface" => {
30
+ "source" => src.to_s,
31
+ "destination" => dst.to_s
32
+ },
33
+ "raw" => {
34
+ "ip" => raw_ip.to_s,
35
+ "name" => raw_name.to_s
36
+ }
37
+ }
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ipaddr"
4
+
5
+ class Netdisco
6
+ class PollMap
7
+ attr_reader :poll
8
+
9
+ # 类对象初始化函数入口
10
+ def initialize
11
+ @poll = CFG.poll.map { |cidr| IPAddr.new cidr }
12
+ end
13
+
14
+ # 判断是否包含某个地址
15
+ def include?(addr)
16
+ @poll.any? { |cidr| cidr.include? addr }
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,155 @@
1
+ require "snmp"
2
+
3
+ class Netdisco
4
+ class SNMP
5
+ class NetdiscoError < StandardError; end
6
+
7
+ class NoResponse < NetdiscoError; end
8
+
9
+ # 类对象初始化入口函数
10
+ def initialize(host, community = CFG.snmp.community, timeout = CFG.snmp.timeout, retries = CFG.snmp.retries)
11
+ @host = host
12
+ @snmp = ::SNMP::Manager.new(Host: @host, Community: community, Timeout: timeout, Retries: retries, MibModules: [])
13
+ end
14
+
15
+ # Closes the SNMP connection
16
+ # @return [void]
17
+ def close
18
+ @snmp.close
19
+ end
20
+
21
+ # 查询某个 oid 监控值
22
+ # Gets one oid, return value
23
+ # @param [String] oid to get
24
+ # @return [SNMP::VarBind]
25
+ def get(oid)
26
+ mget([oid]).first
27
+ end
28
+
29
+ # 批量查询多个 oids 监控值
30
+ # Get multiple oids, return array of values
31
+ # @param [Array(String)] oids to get
32
+ # @return [SNMP::VarBindList]
33
+ def mget(oids)
34
+ snmp :get, oids
35
+ end
36
+
37
+ # 遍历 root_oid 监控值
38
+ # Bulkwalk everything below root oid
39
+ # @param [String] root oid to start from
40
+ # @return [Array(SNMP::VarBind)]
41
+ def bulkwalk(root)
42
+ # 初始化变量
43
+ last, oid, results = false, root.dup, []
44
+ # 将点分10进制字串转换为数组对象
45
+ root = root.split(".").map { |chr| chr.to_i }
46
+ # 遍历 root_oid
47
+ until last
48
+ vbs = snmp(:get_bulk, 0, CFG.snmp.bulkrows, oid).varbind_list
49
+ vbs.each do |vb|
50
+ oid = vb.oid
51
+ # 解析到的 oid 不匹配 root 跳出循环
52
+ (last = true; break) unless oid[0..root.size - 1] == root
53
+ results << vb
54
+ end
55
+ end
56
+ results
57
+ end
58
+
59
+ # 将遍历的 root 监控值转换为 VBHash 样式
60
+ # bulkwalks oid and returns hash with oid as key
61
+ # @param [String] oid root oid to walk
62
+ # @yield [VBHash] hash containing oids found
63
+ # @return [Hash] resulting hash
64
+ def hashwalk(oid, &block)
65
+ hash = VBHash.new
66
+ bulkwalk(oid).each do |vb|
67
+ # value, key = block.call(vb)
68
+ # key ||= vb.oid
69
+ hash[vb.oid] = vb
70
+ end
71
+ hash
72
+ end
73
+
74
+ private
75
+ def snmp(method, *args)
76
+ @snmp.send method, *args
77
+ rescue ::SNMP::RequestTimeout, Errno::EACCES => error
78
+ msg = "host '#{@host}' raised '#{error.class}' with message '#{error.message}' for method '#{method}' with args '#{args}'"
79
+ Log.warn msg
80
+ raise NoResponse, msg
81
+ end
82
+
83
+ # Hash with some helper methods to easier work with VarBinds
84
+ class VBHash < Hash
85
+ alias :org_bracket :[]
86
+ undef :[]
87
+
88
+ # 精确查询
89
+ # @param [Array(Strin, Array)] oid root oid under which you want all oids below it
90
+ # @return [VBHash] oids which start with param oid
91
+ def by_oid(*oid)
92
+ oid = arg_to_oid(*oid)
93
+ hash = select do |key, value|
94
+ key[0..oid.size - 1] == oid
95
+ end
96
+ new_hash = VBHash.new
97
+ new_hash.merge hash
98
+ end
99
+
100
+ # 模糊匹配
101
+ # @param [Array(String, Array)] args partial match 3.4.6 would match to 1.2.3.4.6.7.8
102
+ # @return [SNMP::VarBind, nil] matching element
103
+ def by_partial(*args)
104
+ oid = arg_to_oid(*args)
105
+ # got = nil
106
+ keys.each do |key|
107
+ return self[key] if key.each_cons(oid.size).find { |e| e == oid }
108
+ # if key.each_cons(oid.size).find { |e| e == oid }
109
+ # got = self[key]
110
+ # break
111
+ # end
112
+ end
113
+ nil
114
+ end
115
+
116
+ # @param [Array[String, Array)] key which you want, multiple arguments compiled into single key
117
+ # @return [SNMP::VarBind] matching element
118
+ def [](*args)
119
+ org_bracket arg_to_oid(*args)
120
+ end
121
+
122
+ private
123
+ def arg_to_oid(*args)
124
+ # FIXME && TODO
125
+ key = []
126
+ args.each do |arg|
127
+ if Array === arg
128
+ key += arg.map { |e| e.to_i }
129
+ elsif Fixnum === arg
130
+ key << arg
131
+ else
132
+ key += arg.split(".").map { |e| e.to_i }
133
+ end
134
+ end
135
+ key
136
+ end
137
+ end
138
+ end
139
+ end
140
+
141
+ module SNMP
142
+ class VarBind
143
+ # @return [String] VarBind value as IP address
144
+ def as_ip
145
+ SNMP::IpAddress.new(value).to_s
146
+ end
147
+
148
+ # @param [String] root oid which is removed from self.oid
149
+ # @return [Array] oid remaining after specified root oid
150
+ def oid_id(root)
151
+ root = root.split(".").map { |e| e.to_i }
152
+ oid[root.size..-1]
153
+ end
154
+ end
155
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Netdisco
4
+ VERSION = "0.0.2"
5
+ end
data/lib/netdisco.rb ADDED
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ # 加载项目依赖
4
+ require_relative "netdisco/config"
5
+ require_relative "netdisco/poll_map"
6
+ require_relative "netdisco/name_map"
7
+ require_relative "netdisco/peer"
8
+ require_relative "netdisco/method/xdp"
9
+ require_relative "netdisco/dns"
10
+ require_relative "netdisco/output"
11
+
12
+ class Netdisco
13
+ class NetdiscoError < StandardError; end
14
+
15
+ class MethodNotFound < NetdiscoError; end
16
+
17
+ # 类对象方法属性
18
+ attr_reader :hosts
19
+
20
+ # 类对象初始化函数
21
+ def initialize
22
+ @methods = []
23
+ @hosts = {}
24
+ @poll = PollMap.new
25
+
26
+ # 动态加载邻居发现协议
27
+ CFG.use.each do |method|
28
+ method = File.basename method.to_s
29
+ file = "netdisco/method/#{method.downcase}"
30
+ require_relative file
31
+ @methods << Netdisco.const_get(method)
32
+ rescue NameError, LoadError
33
+ raise MethodNotFound, "unable to find method '#{method}'"
34
+ end
35
+ end
36
+
37
+ # @param [String] host host to start discover from
38
+ # @return [Netdisco::Output]
39
+ def discover(host)
40
+ recurse host
41
+ Output.new @hosts, @poll
42
+ end
43
+
44
+ # @param [String] host host to get list of peers from
45
+ # @return [Array] list of peers seen connected to host
46
+ def neighbors(host)
47
+ peers = []
48
+ # 不同协议可能发现相同的联结关系,处理完成后根据 ip 自动去重
49
+ @methods.each do |method|
50
+ peers << method.send(:peers, host)
51
+ end
52
+ peers.flatten.uniq { |peer| peer.ip }
53
+ end
54
+
55
+ # Given string of IP address, recurses through peers seen and populates @hosts hash
56
+ # @param [String] host host to start recurse from
57
+ # @return [void]
58
+ def recurse(host)
59
+ # 第一步获取设备的邻居关系,同时将邻居关系缓存下来
60
+ @hosts[host] = peers = neighbors(host)
61
+ # 第二步遍历邻居的邻居关系
62
+ require "pp"; PP.pp @hosts[host]
63
+ peers.each do |peer|
64
+ next if @hosts.has_key? peer.ip
65
+ next unless @poll.include? peer.ip
66
+ discover peer.ip
67
+ end
68
+ end
69
+ end
metadata ADDED
@@ -0,0 +1,109 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: netdisco
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.2
5
+ platform: ruby
6
+ authors:
7
+ - WENWU.YAN
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2022-04-16 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: snmp
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.3'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.3'
27
+ - !ruby/object:Gem::Dependency
28
+ name: slop
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '4.0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '4.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: strada
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: 0.0.2
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: 0.0.2
55
+ description: Netdisco is working for network snmp discovery
56
+ email:
57
+ - careline@foxmail.com
58
+ executables:
59
+ - netdisco
60
+ extensions: []
61
+ extra_rdoc_files: []
62
+ files:
63
+ - CHANGELOG.md
64
+ - CODE_OF_CONDUCT.md
65
+ - LICENSE.txt
66
+ - README.md
67
+ - Rakefile
68
+ - bin/netdisco
69
+ - lib/netdisco.rb
70
+ - lib/netdisco/cli.rb
71
+ - lib/netdisco/config.rb
72
+ - lib/netdisco/dns.rb
73
+ - lib/netdisco/method/cdp.rb
74
+ - lib/netdisco/method/lldp.rb
75
+ - lib/netdisco/method/xdp.rb
76
+ - lib/netdisco/name_map.rb
77
+ - lib/netdisco/output.rb
78
+ - lib/netdisco/output/dot.rb
79
+ - lib/netdisco/peer.rb
80
+ - lib/netdisco/poll_map.rb
81
+ - lib/netdisco/snmp.rb
82
+ - lib/netdisco/version.rb
83
+ homepage: https://github.com/ciscolive/netdisco-ruby
84
+ licenses:
85
+ - MIT
86
+ metadata:
87
+ homepage_uri: https://github.com/ciscolive/netdisco-ruby
88
+ source_code_uri: https://github.com/ciscolive/zabbix-rails
89
+ changelog_uri: https://github.com/ciscolive/zabbix-rails/blob/main/README.md
90
+ post_install_message:
91
+ rdoc_options: []
92
+ require_paths:
93
+ - lib
94
+ required_ruby_version: !ruby/object:Gem::Requirement
95
+ requirements:
96
+ - - ">="
97
+ - !ruby/object:Gem::Version
98
+ version: 2.6.0
99
+ required_rubygems_version: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ requirements: []
105
+ rubygems_version: 3.3.3
106
+ signing_key:
107
+ specification_version: 4
108
+ summary: Netdisco is working for network snmp discovery
109
+ test_files: []