14 """ |
14 """ |
15 |
15 |
16 import pvl.args |
16 import pvl.args |
17 import pvl.hosts |
17 import pvl.hosts |
18 from pvl.invoke import merge |
18 from pvl.invoke import merge |
|
19 from pvl.snmp import snmp, lldp |
19 |
20 |
20 import logging; log = logging.getLogger('pvl.hosts-snmp') |
21 import logging; log = logging.getLogger('pvl.hosts-lldp') |
21 import optparse |
22 import optparse |
22 |
|
23 from pysnmp.entity.rfc3413.oneliner import cmdgen as pysnmp |
|
24 import collections |
|
25 |
|
26 class SNMPError (Exception) : |
|
27 pass |
|
28 |
|
29 class SNMPEngineError (SNMPError) : |
|
30 """ |
|
31 Internal SNMP Engine error (?) |
|
32 """ |
|
33 |
|
34 class SNMPAgent (object) : |
|
35 """ |
|
36 GET SNMP shit from a remote host. |
|
37 """ |
|
38 |
|
39 SNMP_PORT = 161 |
|
40 snmp_cmdgen = pysnmp.CommandGenerator() |
|
41 |
|
42 @classmethod |
|
43 def apply (cls, options, host, community=None) : |
|
44 port = cls.SNMP_PORT |
|
45 |
|
46 if community is None : |
|
47 community = options.snmp_community |
|
48 |
|
49 |
|
50 if '@' in host : |
|
51 community, host = host.split('@', 1) |
|
52 |
|
53 if ':' in host : |
|
54 host, port = host.rsplit(':', 1) |
|
55 |
|
56 return cls( |
|
57 pysnmp.CommunityData(community), |
|
58 pysnmp.UdpTransportTarget((host, port)) |
|
59 ) |
|
60 |
|
61 def __init__ (self, security, transport) : |
|
62 self.security = security |
|
63 self.transport = transport |
|
64 |
|
65 def get (self, *request) : |
|
66 """ |
|
67 request = ( |
|
68 pysnmp.MibVariable('IF-MIB', 'ifInOctets', 1), |
|
69 ) |
|
70 """ |
|
71 |
|
72 opts = dict( |
|
73 lookupNames = True, |
|
74 lookupValues = True, |
|
75 ) |
|
76 |
|
77 try : |
|
78 error, error_status, error_index, response = self.snmp_cmdgen.getCmd(self.security, self.transport, *request, **opts) |
|
79 except pysnmp.error.PySnmpError as ex : |
|
80 raise SNMPEngineError(ex) |
|
81 |
|
82 if error : |
|
83 raise SNMPEngineError(error) |
|
84 |
|
85 if error_status : |
|
86 raise SNMPError(errorStatus.prettyPrint()) |
|
87 |
|
88 return response |
|
89 #for name, value in response : |
|
90 # yield name.getMibSymbol(), value.prettyPrint() |
|
91 |
|
92 def walk (self, *request) : |
|
93 """ |
|
94 request = ( |
|
95 pysnmp.MibVariable('IF-MIB', 'ifInOctets'), |
|
96 ) |
|
97 """ |
|
98 |
|
99 opts = dict( |
|
100 lookupNames = True, |
|
101 lookupValues = True, |
|
102 ) |
|
103 |
|
104 try : |
|
105 error, error_status, error_index, responses = self.snmp_cmdgen.nextCmd(self.security, self.transport, *request, **opts) |
|
106 except pysnmp.error.PySnmpError as ex : |
|
107 raise SNMPEngineError(ex) |
|
108 |
|
109 if error : |
|
110 raise SNMPEngineError(error) |
|
111 |
|
112 if error_status : |
|
113 raise SNMPError(errorStatus.prettyPrint()) |
|
114 |
|
115 return responses |
|
116 #for response in responses: |
|
117 # for name, value in response : |
|
118 # yield name.getMibSymbol(), value.prettyPrint() |
|
119 |
|
120 def table (self, *columns) : |
|
121 """ |
|
122 Given [oid] returns { idx: { oid: value } } |
|
123 """ |
|
124 |
|
125 data = collections.defaultdict(dict) |
|
126 |
|
127 for row in self.walk(*columns) : |
|
128 log.debug("%s", row) |
|
129 |
|
130 for column, (field, value) in zip(columns, row) : |
|
131 mib, sym, idx = field.getMibSymbol() |
|
132 |
|
133 log.debug("%s::%s[%s]: %s: %s", mib, sym, ', '.join(str(x) for x in idx), column, value) |
|
134 |
|
135 data[idx][column] = value |
|
136 |
|
137 return data.items() |
|
138 |
|
139 from memoized_property import memoized_property |
|
140 |
|
141 def macaddr (value) : |
|
142 """ |
|
143 Excepts a MAC address from an SNMP OctetString. |
|
144 """ |
|
145 |
|
146 return ':'.join('{octet:02x}'.format(octet=c) for c in value.asNumbers()) |
|
147 |
|
148 class LLDPAgent (SNMPAgent) : |
|
149 """ |
|
150 Query LLDP info from a remote agent. |
|
151 """ |
|
152 |
|
153 |
|
154 @classmethod |
|
155 def _chassis_id (cls, chassis_id, subtype) : |
|
156 log.debug("%s: %r", subtype, chassis_id) |
|
157 |
|
158 # XXX: reference names from LLDP-MIB.py |
|
159 if subtype == 4: |
|
160 return macaddr(chassis_id) |
|
161 else : |
|
162 return chassis_id |
|
163 |
|
164 @classmethod |
|
165 def _port_id (cls, port_id, subtype) : |
|
166 log.debug("%s: %r", subtype, port_id) |
|
167 |
|
168 # XXX: reference names from LLDP-MIB.py |
|
169 if subtype == 5: # interfaceName -> IF-MIB::ifName ? |
|
170 return str(port_id) |
|
171 elif subtype == 3 : # macAddress |
|
172 return macaddr(port_id) |
|
173 elif subtype == 7 : # local |
|
174 return str(port_id) # XXX: integer? |
|
175 else : |
|
176 log.warn("unknown subtype=%d: %r", subtype, port_id) |
|
177 |
|
178 return port_id |
|
179 |
|
180 LLDP_LOC_CHASSIS_ID = pysnmp.MibVariable('LLDP-MIB', 'lldpLocChassisId') |
|
181 LLDP_LOC_CHASSIS_ID_SUBTYPE = pysnmp.MibVariable('LLDP-MIB', 'lldpLocChassisIdSubtype') |
|
182 LLDP_LOC_SYS_NAME = pysnmp.MibVariable('LLDP-MIB', 'lldpLocSysName') |
|
183 |
|
184 @memoized_property |
|
185 def local (self) : |
|
186 """ |
|
187 Describe the local system. |
|
188 """ |
|
189 |
|
190 for idx, data in self.table( |
|
191 self.LLDP_LOC_CHASSIS_ID, |
|
192 self.LLDP_LOC_CHASSIS_ID_SUBTYPE, |
|
193 self.LLDP_LOC_SYS_NAME, |
|
194 ) : |
|
195 return { |
|
196 'chassis': self._chassis_id(data[self.LLDP_LOC_CHASSIS_ID], data[self.LLDP_LOC_CHASSIS_ID_SUBTYPE]), |
|
197 'sys_name': str(data[self.LLDP_LOC_SYS_NAME]), |
|
198 } |
|
199 |
|
200 LLDP_LOC_PORT_ID = pysnmp.MibVariable('LLDP-MIB', 'lldpLocPortId') |
|
201 LLDP_LOC_PORT_ID_SUBTYPE = pysnmp.MibVariable('LLDP-MIB', 'lldpLocPortIdSubtype') |
|
202 |
|
203 @memoized_property |
|
204 def ports (self) : |
|
205 """ |
|
206 Describe the local ports. |
|
207 """ |
|
208 |
|
209 ports = { } |
|
210 |
|
211 for idx, data in self.table( |
|
212 self.LLDP_LOC_PORT_ID, |
|
213 self.LLDP_LOC_PORT_ID_SUBTYPE, |
|
214 ) : |
|
215 port, = idx |
|
216 |
|
217 ports[int(port)] = { |
|
218 'port': self._port_id(data[self.LLDP_LOC_PORT_ID], data[self.LLDP_LOC_PORT_ID_SUBTYPE]), |
|
219 } |
|
220 |
|
221 return ports |
|
222 |
|
223 def port (self, port) : |
|
224 return self.ports[int(port)] |
|
225 |
|
226 LLDP_REM_CHASSIS_ID = pysnmp.MibVariable('LLDP-MIB', 'lldpRemChassisId') |
|
227 LLDP_REM_CHASSIS_ID_SUBTYPE = pysnmp.MibVariable('LLDP-MIB', 'lldpRemChassisIdSubtype') |
|
228 LLDP_REM_SYS_NAME = pysnmp.MibVariable('LLDP-MIB', 'lldpRemSysName') |
|
229 LLDP_REM_PORT_ID = pysnmp.MibVariable('LLDP-MIB', 'lldpRemPortId') |
|
230 LLDP_REM_PORT_ID_SUBTYPE = pysnmp.MibVariable('LLDP-MIB', 'lldpRemPortIdSubtype') |
|
231 |
|
232 def remotes (self) : |
|
233 """ |
|
234 Describe remote systems, indexed by local port. |
|
235 """ |
|
236 |
|
237 for idx, data in self.table( |
|
238 self.LLDP_REM_CHASSIS_ID, |
|
239 self.LLDP_REM_CHASSIS_ID_SUBTYPE, |
|
240 self.LLDP_REM_SYS_NAME, |
|
241 self.LLDP_REM_PORT_ID, |
|
242 self.LLDP_REM_PORT_ID_SUBTYPE, |
|
243 ) : |
|
244 time, port, idx = idx |
|
245 |
|
246 yield int(port), { |
|
247 'chassis': self._chassis_id(data[self.LLDP_REM_CHASSIS_ID], data[self.LLDP_REM_CHASSIS_ID_SUBTYPE]), |
|
248 'sys_name': str(data[self.LLDP_REM_SYS_NAME]), |
|
249 'port': self._port_id(data[self.LLDP_REM_PORT_ID], data[self.LLDP_REM_PORT_ID_SUBTYPE]), |
|
250 } |
|
251 |
23 |
252 def hosts_lldp (options, hosts) : |
24 def hosts_lldp (options, hosts) : |
253 """ |
25 """ |
254 Discover LLDP-supporting hosts. |
26 Discover LLDP-supporting hosts. |
255 |
27 |
256 Yields Host, LLDPAgent |
28 Yields Host, LLDPAgent |
257 """ |
29 """ |
258 |
30 |
259 for host in hosts : |
31 for host in hosts : |
260 snmp = host.extensions.get('snmp') |
32 host_snmp = host.extensions.get('snmp') |
261 |
33 |
262 log.debug("%s: %s", host, snmp) |
|
263 |
34 |
264 if not snmp : |
35 if not host_snmp : |
|
36 log.debug("%s: skip non-snmp host", host) |
265 continue |
37 continue |
266 |
38 |
267 lldp = LLDPAgent.apply(options, host.fqdn(), community=snmp.get('community')) |
39 elif host.down : |
|
40 log.debug("%s: skip down host", host) |
|
41 continue |
|
42 |
|
43 else : |
|
44 log.debug("%s: %s", host, host_snmp) |
|
45 |
|
46 agent = lldp.LLDPAgent.apply(options, host.fqdn(), community=host_snmp.get('community')) |
268 |
47 |
269 try : |
48 try : |
270 local = lldp.local |
49 local = agent.local |
271 except SNMPError as ex : |
50 except snmp.SNMPError as ex : |
272 log.warning("%s: %s", host, ex) |
51 log.warning("%s: %s", host, ex) |
273 continue |
52 continue |
|
53 |
|
54 log.info("%s: %s", host, local) |
274 |
55 |
275 if local['sys_name'] != host.host : |
56 if local['sys_name'] != host.host : |
276 log.warning("%s: SNMP sys_name mismatch: %s", host, local['sys_name']) |
57 log.warning("%s: SNMP sys_name mismatch: %s", host, local['sys_name']) |
277 |
58 |
278 yield host, lldp |
59 yield host, agent |
279 |
60 |
280 def apply_hosts_lldp (options, hosts) : |
61 def apply_hosts_lldp (options, hosts) : |
281 """ |
62 """ |
282 Query host LLDP info. |
63 Query host LLDP info. |
283 """ |
64 """ |
|
65 |
|
66 _hosts_lldp = list(hosts_lldp(options, hosts)) |
|
67 hosts_by_chassis = { } |
|
68 |
|
69 # first pass to discover hosts |
|
70 for host, agent in _hosts_lldp : |
|
71 chassis = agent.local['chassis'] |
|
72 log.info("%s: %s", host, chassis) |
284 |
73 |
285 for host, lldp in hosts_lldp(options, hosts) : |
74 hosts_by_chassis[chassis] = host |
286 log.info("%s: %s", host, lldp.local) |
75 |
287 |
76 # second pass to discver links |
288 for port, remote in lldp.remotes() : |
77 for host, agent in _hosts_lldp : |
289 port = lldp.port(port) |
78 for port, remote in agent.remotes() : |
|
79 port = agent.port(port) |
|
80 |
|
81 remote_chassis = remote['chassis'] |
|
82 remote_host = hosts_by_chassis.get(remote_chassis) |
290 |
83 |
291 log.info("%s: %s: %s", host, port, remote) |
84 log.info("%s: %s: %s (%s)", host, port, remote, remote_host) |
292 |
85 |
293 yield host, merge(lldp.local, port), remote |
86 yield host, merge(agent.local, port), remote, remote_host |
294 |
87 |
295 def main (argv) : |
88 def main (argv) : |
296 """ |
89 """ |
297 SNMP polling. |
90 SNMP polling. |
298 """ |
91 """ |
299 |
92 |
300 parser = optparse.OptionParser(main.__doc__) |
93 parser = optparse.OptionParser(main.__doc__) |
301 parser.add_option_group(pvl.args.parser(parser)) |
94 parser.add_option_group(pvl.args.parser(parser)) |
302 parser.add_option_group(pvl.hosts.optparser(parser)) |
95 parser.add_option_group(pvl.hosts.optparser(parser)) |
|
96 parser.add_option_group(pvl.snmp.snmp.options(parser)) |
303 |
97 |
304 parser.add_option('--snmp-community', |
|
305 help="SNMPv2 read community") |
|
306 |
98 |
307 options, args = parser.parse_args(argv[1:]) |
99 options, args = parser.parse_args(argv[1:]) |
308 pvl.args.apply(options) |
100 pvl.args.apply(options) |
309 |
101 |
310 # input |
102 # input |
311 hosts = pvl.hosts.apply(options, args) |
103 hosts = pvl.hosts.apply(options, args) |
312 |
104 |
313 # apply |
105 # apply |
314 for host, local, remote in apply_hosts_lldp(options, hosts) : |
106 for host, local, remote, remote_host in apply_hosts_lldp(options, hosts) : |
315 print "{host:30} {local:40} <- {remote:40}".format( |
107 if remote_host : |
316 host = host, |
108 print "{host:30} {local[port]:25} <-> {remote[port]:25} {remote_host:30}".format(host=host, local=local, remote=remote, remote_host=remote_host) |
317 local = "{local[chassis]:15}[{local[port]}]".format(local=local), |
109 else : |
318 remote = "{remote[chassis]:15}[{remote[port]}]".format(remote=remote), |
110 print "{host:30} {local[port]:25} <-- {remote[port]:25} # {remote[chassis]} ({remote[sys_name]})".format(host=host, local=local, remote=remote) |
319 ) |
|
320 |
111 |
321 if __name__ == '__main__': |
112 if __name__ == '__main__': |
322 pvl.args.main(main) |
113 pvl.args.main(main) |