Refactoring: Changed all check parameters starting with an 'o' to the new rulespec...
[check_mk.git] / checks / legacy_docker.include
blob87b30fd416f2988ce9ca27b53e5dcec51ecf9451
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
30 class DeprecatedDict(dict):
31 pass
34 class DeprecatedList(list):
35 pass
38 def append_deprecation_warning(check_function):
39 '''A wrapper to WARN if legacy code is used
41 If the parse result is of one of the legacy Types the decorated
42 check function will yield an additional WARNING state.
44 These legacy parse results correspond to agents/plugins from version
45 1.5.0b1 to 1.5.0p12
46 '''
48 @functools.wraps(check_function)
49 def wrapper(item, params, parsed):
51 is_deprecated = isinstance(parsed, (DeprecatedDict, DeprecatedList))
52 catch_these = Exception if is_deprecated else ()
54 try:
55 results = check_function(item, params, parsed)
56 if isinstance(results, tuple):
57 yield results
58 else:
59 for result in results:
60 yield result
61 except catch_these:
62 yield 3, "Could not handle data"
63 finally:
64 if is_deprecated:
65 yield 1, ("Deprecated plugin/agent (see long output)(!)\n"
66 "You are using legacy code, which may lead to crashes and/or"
67 " incomplete information. Please upgrade the monitored host to"
68 " use the plugin 'mk_docker.py'.")
70 return wrapper
73 def _legacy_docker_get_bytes(string):
74 '''get number of bytes from string
76 e.g.
77 "123GB (42%)" -> 123000000000
78 "0 B" -> 0
79 "2B" -> 2
80 "23 kB" -> 23000
81 '''
82 # remove percent
83 string = string.split('(')[0].strip()
84 tmp = re.split('([a-zA-Z]+)', string)
85 value_string = tmp[0].strip()
86 unit_string = tmp[1].strip() if len(tmp) > 1 else 'B'
87 try:
88 factor = {
89 'TB': 10**12,
90 'GB': 10**9,
91 'MB': 10**6,
92 'KB': 10**3,
93 'kB': 10**3,
94 'B': 1,
95 '': 1,
96 }[unit_string]
97 return int(float(value_string) * factor)
98 except (ValueError, TypeError):
99 return None
102 def _legacy_docker_trunk_id(hash_string):
103 '''normalize to short ID
105 Some docker commands use shortened, some long IDs:
106 Convert long ones to short ones, e.g.
107 "sha256:8b15606a9e3e430cb7ba739fde2fbb3734a19f8a59a825ffa877f9be49059817"
109 "8b15606a9e3e"
111 long_id = hash_string.split(':', 1)[-1]
112 return long_id[:12]
115 def _get_repo_tag(string):
116 if ":" in string:
117 return tuple(string.rsplit(":", 1))
118 return string, "latest"
121 def parse_legacy_docker_node_info(info):
122 '''parse output of "docker info"'''
123 parsed = DeprecatedDict()
124 if not info:
125 return parsed
127 # parse legacy json output (verisons 1.5.0 - 1.5.0p6)
128 joined = " ".join(info[0])
129 if joined.endswith("permission denied"):
130 return parsed
131 try:
132 # this may contain a certificate containing newlines.
133 return json.loads(joined.replace("\n", "\\n"))
134 except ValueError:
135 pass
137 prefix = ""
138 for row in info:
139 if not row:
140 continue
141 # remove '|', it was protecting leading whitespace
142 row0 = row[0][1:]
143 if not row0:
144 continue
145 # ignore misssing keys / pad lines that are not of "key: value" type
146 if len(row) == 1:
147 row.append('')
148 key = row0.strip()
149 value = ':'.join(row[1:]).strip()
150 # indented keys are prefixed by the last not indented key
151 if len(row0) - len(key) == 0:
152 parsed[key] = value
153 prefix = key
154 else:
155 parsed[prefix + key] = value
157 ## some modifications to match json output:
158 for key in ("Images", "Containers", "ContainersRunning", "ContainersStopped",
159 "ContainersPaused"):
160 try:
161 parsed[key] = int(parsed[key])
162 except (KeyError, ValueError):
163 pass
164 # reconstruct labels (they where not in "k: v" format)
165 parsed["Labels"] = []
166 for k in sorted(parsed.keys()): # pylint: disable=consider-iterating-dictionary
167 if k.startswith("Labels") and k != "Labels":
168 parsed["Labels"].append(k[6:] + parsed.pop(k))
169 # reconstruct swarm info:
170 if "Swarm" in parsed:
171 swarm = {"LocalNodeState": parsed["Swarm"]}
172 if "SwarmNodeID" in parsed:
173 swarm["NodeID"] = parsed.pop("SwarmNodeID")
174 if "SwarmManagers" in parsed:
175 swarm["RemoteManagers"] = parsed.pop("SwarmManagers")
176 parsed["Swarm"] = swarm
178 if "Server Version" in parsed:
179 parsed["ServerVersion"] = parsed.pop("Server Version")
180 if "Registry" in parsed:
181 parsed["IndexServerAddress"] = parsed.pop("Registry")
183 return parsed
186 def _legacy_docker_parse_table(rows, fields, keys):
187 '''docker provides us with space separated tables with field containing spaces
189 e.g.:
191 TYPE TOTAL ACTIVE SIZE RECLAIMABLE
192 Images 7 6 2.076 GB 936.9 MB (45%)
193 Containers 22 0 2.298 GB 2.298 GB (100%)
194 Local Volumes 5 5 304 B 0 B (0%)
196 if not rows or not rows[0]:
197 return []
199 indices = []
200 for field in fields:
201 rex = regex(field + r'\ *')
202 m = rex.search(rows[0][0])
203 if m is not None:
204 start, end = m.start(), m.end()
205 if end - start == len(field):
206 end = None
207 indices.append((start, end))
208 else:
209 indices.append((0, 0))
211 table = []
212 for row in rows[1:]:
213 if not row:
214 continue
215 try:
216 line = {k: row[0][i:j].strip() for k, (i, j) in zip(keys, indices)}
217 except IndexError:
218 continue
219 table.append(line)
221 return table
224 def parse_legacy_docker_system_df(info):
225 def int_or_zero(s):
226 return int(s.strip() or 0)
228 field_map = (
229 ('TYPE', 'TOTAL', 'ACTIVE', 'SIZE', 'RECLAIMABLE'),
230 ('Type', 'TotalCount', 'Active', 'Size', 'Reclaimable'),
231 (str, int_or_zero, int_or_zero, _legacy_docker_get_bytes, _legacy_docker_get_bytes),
234 try: # parse legacy json output: from 1.5.0 - 1.5.0p6
235 table = [json.loads(",".join(row)) for row in info if row]
236 except ValueError:
237 table = _legacy_docker_parse_table(info, field_map[0], field_map[1])
239 parsed = DeprecatedDict()
240 for line in table:
241 for key, type_ in zip(field_map[1], field_map[2]):
242 val = line.get(key)
243 if val is not None:
244 line[key] = type_(val)
245 parsed[line.get("Type").lower()] = line
247 return parsed
250 def _get_json_list(info):
251 json_list = []
252 for row in info:
253 if not row:
254 continue
255 try:
256 json_list.append(json.loads(' '.join(row)))
257 except ValueError:
258 pass
259 # some buggy docker commands produce empty output
260 return [element for element in json_list if element]
263 def parse_legacy_docker_subsection_images(info):
265 table = _get_json_list(info)
267 parsed = DeprecatedDict()
268 for item in table:
269 val = item.get("VirtualSize")
270 if val is not None:
271 item["VirtualSize"] = _legacy_docker_get_bytes(val)
272 parsed[item.get("ID")] = item
274 return parsed
277 def parse_legacy_docker_subsection_image_labels(info):
279 table = _get_json_list(info)
281 parsed = DeprecatedDict()
282 for long_id, data in table:
283 if data is not None:
284 parsed[_legacy_docker_trunk_id(long_id)] = data
285 return parsed
288 def parse_legacy_docker_subsection_image_inspect(info):
289 parsed = DeprecatedDict()
290 try:
291 table = json.loads(' '.join(' '.join(row) for row in info if row))
292 except ValueError:
293 return parsed
294 for image in table:
295 parsed[_legacy_docker_trunk_id(image["Id"])] = image
296 return parsed
299 def parse_legacy_docker_subsection_containers(info):
301 table = _get_json_list(info)
303 parsed = DeprecatedDict()
304 for item in table:
305 image_name = item.get("Image", "")
306 item["Repository"], item["Tag"] = _get_repo_tag(image_name)
307 parsed[item.get("ID")] = item
309 return parsed
312 def _split_subsections(info):
313 subname = ''
314 subsections = {}
315 for row in info:
316 if not row:
317 continue
318 if row[0].startswith('[[[') and row[0].endswith(']]]'):
319 subname = row[0].strip('[]')
320 continue
321 subsections.setdefault(subname, []).append(row)
322 return subsections
325 def parse_legacy_docker_messed_up_labels(string):
326 '''yield key value pairs
328 'string' is in the format "key1=value1,key2=value2,...", but there
329 may be unescaped commas in the values.
332 def toggle_key_value():
333 for chunk in string.split('='):
334 for item in chunk.rsplit(',', 1):
335 yield item
337 toggler = toggle_key_value()
338 return dict(zip(toggler, toggler))
341 def parse_legacy_docker_node_images(info):
342 subsections = _split_subsections(info)
343 images = parse_legacy_docker_subsection_images(subsections.get("images", []))
344 image_labels = parse_legacy_docker_subsection_image_labels(subsections.get("image_labels", []))
345 image_inspect = parse_legacy_docker_subsection_image_inspect(
346 subsections.get("image_inspect", []))
347 containers = parse_legacy_docker_subsection_containers(subsections.get("containers", []))
349 for image_id, pref_info in image_inspect.iteritems():
350 image = images.setdefault(image_id, {})
351 image["ID"] = image_id
352 labels = pref_info.get("Config", {}).get("Labels")
353 if labels is not None:
354 image.setdefault("__labels__", {}).update(labels)
355 image["CreatedAt"] = pref_info["Created"]
356 image["VirtualSize"] = pref_info["VirtualSize"]
357 repotags = pref_info.get("RepoTags")
358 if repotags:
359 image["Repository"], image["Tag"] = _get_repo_tag(repotags[-1])
360 else:
361 repo = pref_info.get("RepoDigest", "").split('@', 1)[0]
362 image["Repository"], image["Tag"] = _get_repo_tag(repo)
364 for image in images.itervalues():
365 image["amount_containers"] = 0
366 image.setdefault("__labels__", {})
368 for image_id, labels in image_labels.iteritems():
369 image = images.get(_legacy_docker_trunk_id(image_id))
370 if image is not None and labels is not None:
371 image["__labels__"].update(labels)
373 mapping = {(i['Repository'], i['Tag']): i['ID'] for i in images.itervalues()}
375 for cont in containers.itervalues():
376 image_id = mapping.get((cont["Repository"], cont["Tag"]))
377 image = images.get(image_id)
378 if image is not None:
379 image["amount_containers"] += 1
381 labels = cont.get("Labels")
382 if isinstance(labels, (str, unicode)):
383 cont["Labels"] = parse_legacy_docker_messed_up_labels(labels)
385 return DeprecatedDict((("images", images), ("containers", containers)))
388 def parse_legacy_docker_network_inspect(info):
389 try:
390 raw = json.loads(''.join(row[0] for row in info if row))
391 except ValueError:
392 raw = []
393 return DeprecatedList(raw)