2 # -*- encoding: utf-8; py-indent-offset: 4 -*-
3 # +------------------------------------------------------------------+
4 # | ____ _ _ __ __ _ __ |
5 # | / ___| |__ ___ ___| | __ | \/ | |/ / |
6 # | | | | '_ \ / _ \/ __| |/ / | |\/| | ' / |
7 # | | |___| | | | __/ (__| < | | | | . \ |
8 # | \____|_| |_|\___|\___|_|\_\___|_| |_|_|\_\ |
10 # | Copyright Mathias Kettner 2014 mk@mathias-kettner.de |
11 # +------------------------------------------------------------------+
13 # This file is part of Check_MK.
14 # The official homepage is at http://mathias-kettner.de/check_mk.
16 # check_mk is free software; you can redistribute it and/or modify it
17 # under the terms of the GNU General Public License as published by
18 # the Free Software Foundation in version 2. check_mk is distributed
19 # in the hope that it will be useful, but WITHOUT ANY WARRANTY; with-
20 # out even the implied warranty of MERCHANTABILITY or FITNESS FOR A
21 # PARTICULAR PURPOSE. See the GNU General Public License for more de-
22 # tails. You should have received a copy of the GNU General Public
23 # License along with GNU Make; see the file COPYING. If not, write
24 # to the Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
25 # Boston, MA 02110-1301 USA.
31 import cmk
.utils
.tty
as tty
32 from cmk
.utils
.exceptions
import MKGeneralException
, MKTimeout
34 import cmk_base
.console
as console
35 import cmk_base
.snmp_utils
as snmp_utils
36 from cmk_base
.exceptions
import MKSNMPError
39 # .--SNMP interface------------------------------------------------------.
40 # | ____ _ _ __ __ ____ _ _ __ |
41 # |/ ___|| \ | | \/ | _ \ (_)_ __ | |_ ___ _ __ / _| __ _ ___ ___ |
42 # |\___ \| \| | |\/| | |_) | | | '_ \| __/ _ \ '__| |_ / _` |/ __/ _ \ |
43 # | ___) | |\ | | | | __/ | | | | | || __/ | | _| (_| | (_| __/ |
44 # ||____/|_| \_|_| |_|_| |_|_| |_|\__\___|_| |_| \__,_|\___\___| |
46 # +----------------------------------------------------------------------+
47 # | Implements the neccessary function for Check_MK |
48 # '----------------------------------------------------------------------'
51 def walk(host_config
, oid
, hex_plain
=False, context_name
=None):
52 protospec
= _snmp_proto_spec(host_config
)
54 ipaddress
= host_config
.ipaddress
55 if protospec
== "udp6:":
56 ipaddress
= "[" + ipaddress
+ "]"
57 portspec
= _snmp_port_spec(host_config
)
58 command
= _snmp_walk_command(host_config
, context_name
)
59 command
+= ["-OQ", "-OU", "-On", "-Ot", "%s%s%s" % (protospec
, ipaddress
, portspec
), oid
]
61 console
.vverbose("Running '%s'\n" % subprocess
.list2cmdline(command
))
67 snmp_process
= subprocess
.Popen(
70 stdin
=open(os
.devnull
),
71 stdout
=subprocess
.PIPE
,
72 stderr
=subprocess
.PIPE
)
74 rowinfo
= _get_rowinfo_from_snmp_process(snmp_process
, hex_plain
)
77 # On timeout exception try to stop the process to prevent child process "leakage"
79 os
.kill(snmp_process
.pid
, signal
.SIGTERM
)
84 # The stdout and stderr pipe are not closed correctly on a MKTimeout
85 # Normally these pipes getting closed after p.communicate finishes
86 # Closing them a second time in a OK scenario won't hurt neither..
88 exitstatus
= snmp_process
.wait()
89 error
= snmp_process
.stderr
.read()
90 snmp_process
.stdout
.close()
91 snmp_process
.stderr
.close()
94 console
.verbose(tty
.red
+ tty
.bold
+ "ERROR: " + tty
.normal
+
95 "SNMP error: %s\n" % error
.strip())
97 "SNMP Error on %s: %s (Exit-Code: %d)" % (ipaddress
, error
.strip(), exitstatus
))
101 def _get_rowinfo_from_snmp_process(snmp_process
, hex_plain
):
102 line_iter
= snmp_process
.stdout
.xreadlines()
103 # Ugly(1): in some cases snmpwalk inserts line feed within one
104 # dataset. This happens for example on hexdump outputs longer
105 # than a few bytes. Those dumps are enclosed in double quotes.
106 # So if the value begins with a double quote, but the line
107 # does not end with a double quote, we take the next line(s) as
108 # a continuation line.
112 line
= line_iter
.next().strip()
113 except StopIteration:
116 parts
= line
.split('=', 1)
118 continue # broken line, must contain =
119 oid
= parts
[0].strip()
120 value
= parts
[1].strip()
121 # Filter out silly error messages from snmpwalk >:-P
122 if value
.startswith('No more variables') or value
.startswith('End of MIB') \
123 or value
.startswith('No Such Object available') \
124 or value
.startswith('No Such Instance currently exists'):
127 if value
== '"' or (len(value
) > 1 and value
[0] == '"' and
128 (value
[-1] != '"')): # to be continued
129 while True: # scan for end of this dataset
130 nextline
= line_iter
.next().strip()
131 value
+= " " + nextline
134 rowinfo
.append((oid
, strip_snmp_value(value
, hex_plain
)))
138 class ClassicSNMPBackend(snmp_utils
.ABCSNMPBackend
):
139 def get(self
, host_config
, oid
, context_name
=None):
140 if oid
.endswith(".*"):
141 oid_prefix
= oid
[:-2]
142 commandtype
= "getnext"
147 protospec
= _snmp_proto_spec(host_config
)
148 ipaddress
= host_config
.ipaddress
149 if protospec
== "udp6:":
150 ipaddress
= "[" + ipaddress
+ "]"
151 portspec
= _snmp_port_spec(host_config
)
152 command
= _snmp_base_command(commandtype
, host_config
, context_name
) + \
153 [ "-On", "-OQ", "-Oe", "-Ot",
154 "%s%s%s" % (protospec
, ipaddress
, portspec
),
157 console
.vverbose("Running '%s'\n" % subprocess
.list2cmdline(command
))
159 snmp_process
= subprocess
.Popen(
160 command
, close_fds
=True, stdout
=subprocess
.PIPE
, stderr
=subprocess
.PIPE
)
161 exitstatus
= snmp_process
.wait()
163 console
.verbose(tty
.red
+ tty
.bold
+ "ERROR: " + tty
.normal
+ "SNMP error\n")
164 console
.verbose(snmp_process
.stderr
.read() + "\n")
167 line
= snmp_process
.stdout
.readline().strip()
169 console
.verbose("Error in response to snmpget.\n")
172 item
, value
= line
.split("=", 1)
173 value
= value
.strip()
174 console
.vverbose("SNMP answer: ==> [%s]\n" % value
)
175 if value
.startswith('No more variables') or value
.startswith('End of MIB') \
176 or value
.startswith('No Such Object available') or value
.startswith('No Such Instance currently exists'):
179 # In case of .*, check if prefix is the one we are looking for
180 if commandtype
== "getnext" and not item
.startswith(oid_prefix
+ "."):
184 if value
and value
.startswith('"') and value
.endswith('"'):
189 def _snmp_port_spec(host_config
):
190 if host_config
.port
== 161:
192 return ":%d" % host_config
.port
195 def _snmp_proto_spec(host_config
):
196 if host_config
.is_ipv6_primary
:
202 # Returns command lines for snmpwalk and snmpget including
203 # options for authentication. This handles communities and
204 # authentication for SNMP V3. Also bulkwalk hosts
205 def _snmp_walk_command(host_config
, context_name
):
206 return _snmp_base_command('walk', host_config
, context_name
) + ["-Cc"]
209 # if the credentials are a string, we use that as community,
210 # if it is a four-tuple, we use it as V3 auth parameters:
211 # (1) security level (-l)
212 # (2) auth protocol (-a, e.g. 'md5')
213 # (3) security name (-u)
214 # (4) auth password (-A)
215 # And if it is a six-tuple, it has the following additional arguments:
216 # (5) privacy protocol (DES|AES) (-x)
217 # (6) privacy protocol pass phrase (-X)
218 def _snmp_base_command(what
, host_config
, context_name
):
222 command
= ['snmpget']
223 elif what
== 'getnext':
224 command
= ['snmpgetnext', '-Cf']
225 elif host_config
.is_bulkwalk_host
:
226 command
= ['snmpbulkwalk']
228 options
.append("-Cr%d" % host_config
.bulk_walk_size_of
)
230 command
= ['snmpwalk']
232 if not snmp_utils
.is_snmpv3_host(host_config
):
234 if host_config
.is_bulkwalk_host
:
235 options
.append('-v2c')
238 command
= ['snmpwalk']
239 if host_config
.is_snmpv2or3_without_bulkwalk_host
:
240 options
.append('-v2c')
242 options
.append('-v1')
244 options
+= ["-c", host_config
.credentials
]
248 if len(host_config
.credentials
) == 6:
250 "-v3", "-l", host_config
.credentials
[0], "-a", host_config
.credentials
[1], "-u",
251 host_config
.credentials
[2], "-A", host_config
.credentials
[3], "-x",
252 host_config
.credentials
[4], "-X", host_config
.credentials
[5]
254 elif len(host_config
.credentials
) == 4:
256 "-v3", "-l", host_config
.credentials
[0], "-a", host_config
.credentials
[1], "-u",
257 host_config
.credentials
[2], "-A", host_config
.credentials
[3]
259 elif len(host_config
.credentials
) == 2:
260 options
+= ["-v3", "-l", host_config
.credentials
[0], "-u", host_config
.credentials
[1]]
262 raise MKGeneralException("Invalid SNMP credentials '%r' for host %s: must be "
263 "string, 2-tuple, 4-tuple or 6-tuple" %
264 (host_config
.credentials
, host_config
.hostname
))
266 # Do not load *any* MIB files. This save lot's of CPU.
267 options
+= ["-m", "", "-M", ""]
269 # Configuration of timing and retries
270 settings
= host_config
.timing
271 if "timeout" in settings
:
272 options
+= ["-t", "%0.2f" % settings
["timeout"]]
273 if "retries" in settings
:
274 options
+= ["-r", "%d" % settings
["retries"]]
276 if context_name
is not None:
277 options
+= ["-n", context_name
]
279 return command
+ options
283 # .--SNMP helpers--------------------------------------------------------.
284 # | ____ _ _ __ __ ____ _ _ |
285 # | / ___|| \ | | \/ | _ \ | |__ ___| |_ __ ___ _ __ ___ |
286 # | \___ \| \| | |\/| | |_) | | '_ \ / _ \ | '_ \ / _ \ '__/ __| |
287 # | ___) | |\ | | | | __/ | | | | __/ | |_) | __/ | \__ \ |
288 # | |____/|_| \_|_| |_|_| |_| |_|\___|_| .__/ \___|_| |___/ |
290 # +----------------------------------------------------------------------+
291 # | Internal helpers for processing SNMP things |
292 # '----------------------------------------------------------------------'
295 def strip_snmp_value(value
, hex_plain
=False):
297 if v
.startswith('"'):
299 if len(v
) > 2 and _is_hex_string(v
):
300 return value
if hex_plain
else _convert_from_hex(v
)
301 # Fix for non hex encoded string which have been somehow encoded by the
302 # netsnmp command line tools. An example:
303 # Checking windows systems via SNMP with hr_fs: disk names like c:\
304 # are reported as c:\\, fix this to single \
305 return v
.strip().replace('\\\\', '\\')
309 def _is_hex_string(value
):
310 # as far as I remember, snmpwalk puts a trailing space within
311 # the quotes in case of hex strings. So we require that space
312 # to be present in order make sure, we really deal with a hex string.
315 hexdigits
= "0123456789abcdefABCDEF"
322 if x
not in hexdigits
:
328 def _convert_from_hex(value
):
329 hexparts
= value
.split()
332 r
+= chr(int(hx
, 16))