Cleanup config.nodes_of
[check_mk.git] / checks / legacy_docker.include
blobe114f633e4b384d65829352413b666b4265a5bda
1 #!/usr/bin/python
2 # -*- encoding: utf-8; py-indent-offset: 4 -*-
3 # +------------------------------------------------------------------+
4 # | ____ _ _ __ __ _ __ |
5 # | / ___| |__ ___ ___| | __ | \/ | |/ / |
6 # | | | | '_ \ / _ \/ __| |/ / | |\/| | ' / |
7 # | | |___| | | | __/ (__| < | | | | . \ |
8 # | \____|_| |_|\___|\___|_|\_\___|_| |_|_|\_\ |
9 # | |
10 # | Copyright Mathias Kettner 2018 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.
26 import json
27 import re
28 import functools
31 class DeprecatedDict(dict):
32 pass
35 class DeprecatedList(list):
36 pass
39 def append_deprecation_warning(check_function):
40 '''A wrapper to WARN if legacy code is used
42 If the parse result is of one of the legacy Types the decorated
43 check function will yield an additional WARNING state.
45 These legacy parse results correspond to agents/plugins from version
46 1.5.0b1 to 1.5.0p12
47 '''
49 @functools.wraps(check_function)
50 def wrapper(item, params, parsed):
52 is_deprecated = isinstance(parsed, (DeprecatedDict, DeprecatedList))
53 catch_these = Exception if is_deprecated else ()
55 try:
56 results = check_function(item, params, parsed)
57 if isinstance(results, tuple):
58 yield results
59 else:
60 for result in results:
61 yield result
62 except catch_these:
63 yield 3, "Could not handle data"
64 finally:
65 if is_deprecated:
66 yield 1, ("Deprecated plugin/agent (see long output)(!)\n"
67 "You are using legacy code, which may lead to crashes and/or"
68 " incomplete information. Please upgrade the monitored host to"
69 " use the plugin 'mk_docker.py'.")
71 return wrapper
74 def _legacy_docker_get_bytes(string):
75 '''get number of bytes from string
77 e.g.
78 "123GB (42%)" -> 123000000000
79 "0 B" -> 0
80 "2B" -> 2
81 "23 kB" -> 23000
82 '''
83 # remove percent
84 string = string.split('(')[0].strip()
85 tmp = re.split('([a-zA-Z]+)', string)
86 value_string = tmp[0].strip()
87 unit_string = tmp[1].strip() if len(tmp) > 1 else 'B'
88 try:
89 factor = {
90 'TB': 10**12,
91 'GB': 10**9,
92 'MB': 10**6,
93 'KB': 10**3,
94 'kB': 10**3,
95 'B': 1,
96 '': 1,
97 }[unit_string]
98 return int(float(value_string) * factor)
99 except (ValueError, TypeError):
100 return None
103 def _legacy_docker_trunk_id(hash_string):
104 '''normalize to short ID
106 Some docker commands use shortened, some long IDs:
107 Convert long ones to short ones, e.g.
108 "sha256:8b15606a9e3e430cb7ba739fde2fbb3734a19f8a59a825ffa877f9be49059817"
110 "8b15606a9e3e"
112 long_id = hash_string.split(':', 1)[-1]
113 return long_id[:12]
116 def parse_legacy_docker_node_info(info): # pylint: disable=too-many-branches
117 '''parse output of "docker info"'''
118 parsed = DeprecatedDict()
119 if not info:
120 return parsed
122 # parse legacy json output (verisons 1.5.0 - 1.5.0p6)
123 joined = " ".join(info[0])
124 if joined.endswith("permission denied"):
125 return parsed
126 try:
127 # this may contain a certificate containing newlines.
128 return json.loads(joined.replace("\n", "\\n"))
129 except ValueError:
130 pass
132 prefix = ""
133 for row in info:
134 if not row:
135 continue
136 # remove '|', it was protecting leading whitespace
137 row0 = row[0][1:]
138 if not row0:
139 continue
140 # ignore misssing keys / pad lines that are not of "key: value" type
141 if len(row) == 1:
142 row.append('')
143 key = row0.strip()
144 value = ':'.join(row[1:]).strip()
145 # indented keys are prefixed by the last not indented key
146 if len(row0) - len(key) == 0:
147 parsed[key] = value
148 prefix = key
149 else:
150 parsed[prefix + key] = value
152 ## some modifications to match json output:
153 for key in ("Images", "Containers", "ContainersRunning", "ContainersStopped",
154 "ContainersPaused"):
155 try:
156 parsed[key] = int(parsed[key])
157 except (KeyError, ValueError):
158 pass
159 # reconstruct labels (they where not in "k: v" format)
160 parsed["Labels"] = []
161 for k in sorted(parsed.keys()): # pylint: disable=consider-iterating-dictionary
162 if k.startswith("Labels") and k != "Labels":
163 parsed["Labels"].append(k[6:] + parsed.pop(k))
164 # reconstruct swarm info:
165 if "Swarm" in parsed:
166 swarm = {"LocalNodeState": parsed["Swarm"]}
167 if "SwarmNodeID" in parsed:
168 swarm["NodeID"] = parsed.pop("SwarmNodeID")
169 if "SwarmManagers" in parsed:
170 swarm["RemoteManagers"] = parsed.pop("SwarmManagers")
171 parsed["Swarm"] = swarm
173 if "Server Version" in parsed:
174 parsed["ServerVersion"] = parsed.pop("Server Version")
175 if "Registry" in parsed:
176 parsed["IndexServerAddress"] = parsed.pop("Registry")
178 return parsed
181 def _legacy_docker_parse_table(rows, keys):
182 '''docker provides us with space separated tables with field containing spaces
184 e.g.:
186 TYPE TOTAL ACTIVE SIZE RECLAIMABLE
187 Images 7 6 2.076 GB 936.9 MB (45%)
188 Containers 22 0 2.298 GB 2.298 GB (100%)
189 Local Volumes 5 5 304 B 0 B (0%)
191 if not rows or not rows[0]:
192 return []
194 indices = []
195 for key in keys:
196 field = key.upper()
197 rex = regex(field + r'\ *')
198 match = rex.search(rows[0][0])
199 if match is not None:
200 start, end = match.start(), match.end()
201 if end - start == len(field):
202 end = None
203 indices.append((start, end))
204 else:
205 indices.append((0, 0))
207 table = []
208 for row in rows[1:]:
209 if not row:
210 continue
211 try:
212 line = {k: row[0][i:j].strip() for k, (i, j) in zip(keys, indices)}
213 except IndexError:
214 continue
215 table.append(line)
217 return table
220 def _legacy_map_keys(dictionary, map_keys):
221 for old, new in map_keys:
222 if old in dictionary:
223 dictionary[new] = dictionary.pop(old)
226 def parse_legacy_docker_system_df(info):
227 def int_or_zero(string):
228 return int(string.strip() or 0)
230 type_map = (
231 ('type', 'total', 'active', 'size', 'reclaimable'),
232 (str, int_or_zero, int_or_zero, _legacy_docker_get_bytes, _legacy_docker_get_bytes),
235 try: # parse legacy json output: from 1.5.0 - 1.5.0p6
236 table = [json.loads(",".join(row)) for row in info if row]
237 except ValueError:
238 table = _legacy_docker_parse_table(info, type_map[0])
240 parsed = DeprecatedDict()
241 for line in table:
242 sane_line = {k.lower(): v for k, v in line.items()}
243 _legacy_map_keys(sane_line, (('totalcount', 'total'),))
244 for key, type_ in zip(type_map[0], type_map[1]):
245 val = sane_line.get(key)
246 if val is not None:
247 sane_line[key] = type_(val)
248 _legacy_map_keys(sane_line, (('total', 'count'),))
249 parsed[sane_line.get("type").lower()] = sane_line
251 return parsed
254 def _get_json_list(info):
255 json_list = []
256 for row in info:
257 if not row:
258 continue
259 try:
260 json_list.append(json.loads(' '.join(row)))
261 except ValueError:
262 pass
263 # some buggy docker commands produce empty output
264 return [element for element in json_list if element]
267 def parse_legacy_docker_subsection_images(info):
269 table = _get_json_list(info)
271 map_keys = (("ID", "Id"), ("CreatedAt", "Created"))
273 parsed = DeprecatedDict()
274 for item in table:
275 _legacy_map_keys(item, map_keys)
277 val = item.get("VirtualSize")
278 if val is not None:
279 item["VirtualSize"] = _legacy_docker_get_bytes(val)
281 repotags = item.setdefault("RepoTags", [])
282 if not repotags and item.get("Repository"):
283 repotags.append('%s:%s' % (item["Repository"], item.get("Tag", "latest")))
285 parsed[item.get("Id")] = item
287 return parsed
290 def parse_legacy_docker_subsection_image_labels(info):
292 table = _get_json_list(info)
294 parsed = DeprecatedDict()
295 for long_id, data in table:
296 if data is not None:
297 parsed[_legacy_docker_trunk_id(long_id)] = data
298 return parsed
301 def parse_legacy_docker_subsection_image_inspect(info):
302 parsed = DeprecatedDict()
303 try:
304 table = json.loads(' '.join(' '.join(row) for row in info if row))
305 except ValueError:
306 return parsed
307 for image in table:
308 parsed[_legacy_docker_trunk_id(image["Id"])] = image
309 return parsed
312 def parse_legacy_docker_subsection_containers(info):
314 table = _get_json_list(info)
316 map_keys = (("ID", "Id"), ("CreatedAt", "Created"), ("Names", "Name"))
318 parsed = DeprecatedDict()
319 for item in table:
320 _legacy_map_keys(item, map_keys)
321 if "Status" in item:
322 item["State"] = {"Status": item["Status"]}
324 parsed[item.get("Id")] = item
326 return parsed
329 def parse_legacy_docker_messed_up_labels(string):
330 '''yield key value pairs
332 'string' is in the format "key1=value1,key2=value2,...", but there
333 may be unescaped commas in the values.
336 def toggle_key_value():
337 for chunk in string.split('='):
338 for item in chunk.rsplit(',', 1):
339 yield item
341 toggler = toggle_key_value()
342 return dict(zip(toggler, toggler))
345 def parse_legacy_docker_node_images(subsections):
346 images = parse_legacy_docker_subsection_images(subsections.get("images", []))
347 image_labels = parse_legacy_docker_subsection_image_labels(subsections.get("image_labels", []))
348 image_inspect = parse_legacy_docker_subsection_image_inspect(
349 subsections.get("image_inspect", []))
350 containers = parse_legacy_docker_subsection_containers(subsections.get("containers", []))
352 for image_id, pref_info in image_inspect.iteritems():
353 image = images.setdefault(image_id, {})
354 image["Id"] = image_id
355 labels = pref_info.get("Config", {}).get("Labels") or {}
356 image.setdefault("Labels", {}).update(labels)
357 image["Created"] = pref_info["Created"]
358 image["VirtualSize"] = pref_info["VirtualSize"]
360 repotags = pref_info.get("RepoTags")
361 if repotags:
362 image["RepoTags"] = repotags
364 repodigests = pref_info.get("RepoDigests") or []
365 if 'RepoDigest' in pref_info:
366 # Singular? I think this was a bug, and never existed.
367 # But better safe than sorry.
368 repodigests.append(pref_info['RepoDigest'])
369 image["RepoDigests"] = repodigests
371 images_lookup = {}
372 for image_id, image in images.iteritems():
373 image["amount_containers"] = 0
374 image.setdefault("Labels", {})
375 for reta in image.get("RepoTags", []):
376 images_lookup[reta] = image
377 images_lookup[_legacy_docker_trunk_id(image_id) + ':latest'] = image
379 for image_id, labels in image_labels.iteritems():
380 image = images.get(_legacy_docker_trunk_id(image_id))
381 if image is not None and labels is not None:
382 image["Labels"].update(labels)
384 for cont in containers.itervalues():
385 if 'Image' in cont:
386 image_repotag = cont["Image"]
387 if ':' not in image_repotag:
388 image_repotag += ':latest'
389 image = images_lookup.get(image_repotag)
390 if image is not None:
391 image["amount_containers"] += 1
393 labels = cont.get("Labels")
394 if isinstance(labels, (str, unicode)):
395 cont["Labels"] = parse_legacy_docker_messed_up_labels(labels)
397 return DeprecatedDict((("images", images), ("containers", containers)))
400 def parse_legacy_docker_network_inspect(info):
401 try:
402 raw = json.loads(''.join(row[0] for row in info if row))
403 except ValueError:
404 raw = []
405 return DeprecatedList(raw)
408 def parse_legacy_docker_container_node_name(info):
409 try:
410 return info[0][0]
411 except IndexError:
412 return None