Licenses: Updated the list of licenses and added a PDF containing all license texts
[check_mk.git] / cmk_base / classic_snmp.py
blob80ea42a76336abbffe81ae910b1a6702a09ba466
1 #!/usr/bin/env python
2 # -*- encoding: utf-8; py-indent-offset: 4 -*-
3 # +------------------------------------------------------------------+
4 # | ____ _ _ __ __ _ __ |
5 # | / ___| |__ ___ ___| | __ | \/ | |/ / |
6 # | | | | '_ \ / _ \/ __| |/ / | |\/| | ' / |
7 # | | |___| | | | __/ (__| < | | | | . \ |
8 # | \____|_| |_|\___|\___|_|\_\___|_| |_|_|\_\ |
9 # | |
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.
27 import os
28 import subprocess
29 import signal
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 # ||____/|_| \_|_| |_|_| |_|_| |_|\__\___|_| |_| \__,_|\___\___| |
45 # | |
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))
63 snmp_process = None
64 exitstatus = None
65 rowinfo = []
66 try:
67 snmp_process = subprocess.Popen(
68 command,
69 close_fds=True,
70 stdin=open(os.devnull),
71 stdout=subprocess.PIPE,
72 stderr=subprocess.PIPE)
74 rowinfo = _get_rowinfo_from_snmp_process(snmp_process, hex_plain)
76 except MKTimeout:
77 # On timeout exception try to stop the process to prevent child process "leakage"
78 if snmp_process:
79 os.kill(snmp_process.pid, signal.SIGTERM)
80 snmp_process.wait()
81 raise
83 finally:
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..
87 if snmp_process:
88 exitstatus = snmp_process.wait()
89 error = snmp_process.stderr.read()
90 snmp_process.stdout.close()
91 snmp_process.stderr.close()
93 if exitstatus:
94 console.verbose(tty.red + tty.bold + "ERROR: " + tty.normal +
95 "SNMP error: %s\n" % error.strip())
96 raise MKSNMPError(
97 "SNMP Error on %s: %s (Exit-Code: %d)" % (ipaddress, error.strip(), exitstatus))
98 return rowinfo
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.
109 rowinfo = []
110 while True:
111 try:
112 line = line_iter.next().strip()
113 except StopIteration:
114 break
116 parts = line.split('=', 1)
117 if len(parts) < 2:
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'):
125 continue
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
132 if value[-1] == '"':
133 break
134 rowinfo.append((oid, strip_snmp_value(value, hex_plain)))
135 return rowinfo
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"
143 else:
144 oid_prefix = oid
145 commandtype = "get"
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),
155 oid_prefix ]
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()
162 if exitstatus:
163 console.verbose(tty.red + tty.bold + "ERROR: " + tty.normal + "SNMP error\n")
164 console.verbose(snmp_process.stderr.read() + "\n")
165 return None
167 line = snmp_process.stdout.readline().strip()
168 if not line:
169 console.verbose("Error in response to snmpget.\n")
170 return None
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'):
177 value = None
179 # In case of .*, check if prefix is the one we are looking for
180 if commandtype == "getnext" and not item.startswith(oid_prefix + "."):
181 value = None
183 # Strip quotes
184 if value and value.startswith('"') and value.endswith('"'):
185 value = value[1:-1]
186 return value
189 def _snmp_port_spec(host_config):
190 if host_config.port == 161:
191 return ""
192 return ":%d" % host_config.port
195 def _snmp_proto_spec(host_config):
196 if host_config.is_ipv6_primary:
197 return "udp6:"
199 return ""
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):
219 options = []
221 if what == 'get':
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)
229 else:
230 command = ['snmpwalk']
232 if not snmp_utils.is_snmpv3_host(host_config):
233 # Handle V1 and V2C
234 if host_config.is_bulkwalk_host:
235 options.append('-v2c')
236 else:
237 if what == 'walk':
238 command = ['snmpwalk']
239 if host_config.is_snmpv2or3_without_bulkwalk_host:
240 options.append('-v2c')
241 else:
242 options.append('-v1')
244 options += ["-c", host_config.credentials]
246 else:
247 # Handle V3
248 if len(host_config.credentials) == 6:
249 options += [
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:
255 options += [
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]]
261 else:
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 # | |____/|_| \_|_| |_|_| |_| |_|\___|_| .__/ \___|_| |___/ |
289 # | |_| |
290 # +----------------------------------------------------------------------+
291 # | Internal helpers for processing SNMP things |
292 # '----------------------------------------------------------------------'
295 def strip_snmp_value(value, hex_plain=False):
296 v = value.strip()
297 if v.startswith('"'):
298 v = v[1:-1]
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('\\\\', '\\')
306 return v
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.
313 if value[-1] != ' ':
314 return False
315 hexdigits = "0123456789abcdefABCDEF"
316 n = 0
317 for x in value:
318 if n % 3 == 2:
319 if x != ' ':
320 return False
321 else:
322 if x not in hexdigits:
323 return False
324 n += 1
325 return True
328 def _convert_from_hex(value):
329 hexparts = value.split()
330 r = ""
331 for hx in hexparts:
332 r += chr(int(hx, 16))
333 return r